Merge branch 'james/biome-specific-music' into 'master'

Biome/site specific music and ambient sfx

See merge request veloren/veloren!1488
This commit is contained in:
Joshua Barretto 2020-11-18 22:53:33 +00:00
commit 08c12b99f8
83 changed files with 2193 additions and 675 deletions

View File

@ -38,6 +38,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Upscaling support
- Added "Persist Combo from Combo Melee State" when rolling mid-combo
- You can no longer spam hammer and bow special when stamina is 0
- Biome and site specific music system
- Ambient SFX emitted from terrain blocks
- Campfire SFX
- Wind SFX system
### Changed

300
Cargo.lock generated
View File

@ -56,10 +56,22 @@ dependencies = [
]
[[package]]
name = "alsa-sys"
version = "0.1.2"
name = "alsa"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0edcbbf9ef68f15ae1b620f722180b82a98b6f0628d30baa6b8d2a5abc87d58"
checksum = "44581add1add74ade32aca327b550342359ec00191672c23c1caa3d492b85930"
dependencies = [
"alsa-sys",
"bitflags",
"libc",
"nix 0.15.0",
]
[[package]]
name = "alsa-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5a0559bcd3f7a482690d98be41c08a43e92f669b179433e95ddf5e8b8fd36a3"
dependencies = [
"libc",
"pkg-config",
@ -196,6 +208,12 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
[[package]]
name = "ascii"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e"
[[package]]
name = "ascii"
version = "1.0.0"
@ -275,7 +293,7 @@ dependencies = [
"hex",
"rust-argon2",
"serde_json",
"ureq",
"ureq 1.4.1",
"uuid",
]
@ -311,6 +329,21 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b20b618342cf9891c292c4f5ac2cde7287cc5c87e87e9c769d617793607dec1"
[[package]]
name = "base64"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
dependencies = [
"byteorder",
]
[[package]]
name = "base64"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
[[package]]
name = "base64"
version = "0.12.3"
@ -435,6 +468,12 @@ dependencies = [
"iovec",
]
[[package]]
name = "bytes"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
[[package]]
name = "calloop"
version = "0.6.4"
@ -469,6 +508,12 @@ dependencies = [
"jobserver",
]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.4.0"
@ -643,6 +688,30 @@ dependencies = [
"objc",
]
[[package]]
name = "combine"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680"
dependencies = [
"ascii 0.9.3",
"byteorder",
"either",
"memchr",
"unreachable",
]
[[package]]
name = "combine"
version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2809f67365382d65fd2b6d9c22577231b954ed27400efeafbe687bda75abcc0b"
dependencies = [
"bytes 0.5.6",
"memchr",
"pin-project-lite",
]
[[package]]
name = "conrod_core"
version = "0.63.0"
@ -885,18 +954,26 @@ dependencies = [
[[package]]
name = "cpal"
version = "0.11.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b55d55d69f403f62a95bd3c04b431e0aedf5120c70f15d07a8edd234443dd59"
checksum = "4919d30839e3924b45b84319997a554db1a56918bc5b2a08a6c29886e65e2dca"
dependencies = [
"alsa-sys",
"alsa",
"core-foundation-sys 0.6.2",
"coreaudio-rs",
"jni 0.17.0",
"js-sys",
"lazy_static",
"libc",
"num-traits 0.2.12",
"mach 0.3.2",
"ndk",
"ndk-glue",
"nix 0.15.0",
"oboe",
"parking_lot 0.9.0",
"stdweb 0.1.3",
"thiserror",
"web-sys",
"winapi 0.3.9",
]
@ -1490,6 +1567,17 @@ dependencies = [
"syn 1.0.42",
]
[[package]]
name = "fetch_unroll"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5c55005e95bbe15f5f72a73b6597d0dc82ddc97ffe2ca097a99dcd591fefbca"
dependencies = [
"libflate",
"tar",
"ureq 0.11.4",
]
[[package]]
name = "filetime"
version = "0.2.12"
@ -1980,7 +2068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462"
dependencies = [
"byteorder",
"bytes",
"bytes 0.4.12",
"fnv",
"futures 0.1.29",
"http",
@ -2067,7 +2155,7 @@ version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0"
dependencies = [
"bytes",
"bytes 0.4.12",
"fnv",
"itoa",
]
@ -2078,7 +2166,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d"
dependencies = [
"bytes",
"bytes 0.4.12",
"futures 0.1.29",
"http",
"tokio-buf",
@ -2091,7 +2179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9625f605ddfaf894bf78a544a7b8e31f562dc843654723a49892d9c7e75ac708"
dependencies = [
"async-std",
"bytes",
"bytes 0.4.12",
"futures 0.3.5",
"http",
"pin-project-lite",
@ -2121,7 +2209,7 @@ version = "0.12.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dbe6ed1438e1f8ad955a4701e9a944938e9519f6888d12d8558b645e247d5f6"
dependencies = [
"bytes",
"bytes 0.4.12",
"futures 0.1.29",
"futures-cpupool",
"h2",
@ -2305,7 +2393,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f21dcc74995dd4cd090b147e79789f8d65959cbfb5f0b118002db869ea3bd0a0"
dependencies = [
"core-foundation-sys 0.6.2",
"mach",
"mach 0.2.3",
]
[[package]]
@ -2332,6 +2420,34 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
[[package]]
name = "jni"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1981310da491a4f0f815238097d0d43d8072732b5ae5f8bd0d8eadf5bf245402"
dependencies = [
"cesu8",
"combine 3.8.1",
"error-chain",
"jni-sys",
"log",
"walkdir 2.3.1",
]
[[package]]
name = "jni"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36bcc950632e48b86da402c5c077590583da5ac0d480103611d5374e7c967a3c"
dependencies = [
"cesu8",
"combine 4.3.2",
"error-chain",
"jni-sys",
"log",
"walkdir 2.3.1",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
@ -2416,6 +2532,18 @@ version = "0.2.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235"
[[package]]
name = "libflate"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9135df43b1f5d0e333385cb6e7897ecd1a43d7d11b91ac003f4d2c2d2401fdd"
dependencies = [
"adler32",
"crc32fast",
"rle-decode-fast",
"take_mut",
]
[[package]]
name = "libgit2-sys"
version = "0.12.13+1.0.1"
@ -2567,6 +2695,15 @@ dependencies = [
"libc",
]
[[package]]
name = "mach"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@ -3125,6 +3262,29 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5"
[[package]]
name = "oboe"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6a13c9fe73346ff3cee5530b8dd9da1ce3e13cb663be210af3938a2a1dd3eab"
dependencies = [
"jni 0.14.0",
"ndk",
"ndk-glue",
"num-derive",
"num-traits 0.2.12",
"oboe-sys",
]
[[package]]
name = "oboe-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ff7a51600eabe34e189eec5c995a62f151d8d97e5fbca39e87ca738bb99b82"
dependencies = [
"fetch_unroll",
]
[[package]]
name = "ogg"
version = "0.7.0"
@ -3842,14 +4002,19 @@ dependencies = [
]
[[package]]
name = "rodio"
version = "0.11.0"
name = "rle-decode-fast"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73bbf260262fd5501b7a17d6827e0d25c1127e921eb177150a060faf6e217a70"
checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac"
[[package]]
name = "rodio"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9683532495146e98878d4948fa1a1953f584cd923f2a5f5c26b7a8701b56943"
dependencies = [
"cpal",
"hound",
"lazy_static",
"lewton",
]
@ -3859,7 +4024,7 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a58080b7bb83b2ea28c3b7a9a994fd5e310330b7c8ca5258d99b98128ecfe4"
dependencies = [
"base64",
"base64 0.12.3",
"bitflags",
"serde",
]
@ -3882,7 +4047,7 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19"
dependencies = [
"base64",
"base64 0.12.3",
"blake2b_simd",
"constant_time_eq",
"crossbeam-utils 0.7.2",
@ -3909,13 +4074,26 @@ dependencies = [
"semver",
]
[[package]]
name = "rustls"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e"
dependencies = [
"base64 0.10.1",
"log",
"ring",
"sct",
"webpki",
]
[[package]]
name = "rustls"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81"
dependencies = [
"base64",
"base64 0.12.3",
"log",
"ring",
"sct",
@ -4424,7 +4602,7 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d"
dependencies = [
"bytes",
"bytes 0.4.12",
]
[[package]]
@ -4509,6 +4687,24 @@ dependencies = [
"unicode-xid 0.2.1",
]
[[package]]
name = "take_mut"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
[[package]]
name = "tar"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "489997b7557e9a43e192c527face4feacc78bfbe6eed67fd55c4c9e381cba290"
dependencies = [
"filetime",
"libc",
"redox_syscall",
"xattr",
]
[[package]]
name = "textwrap"
version = "0.11.0"
@ -4585,7 +4781,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15ce4fc3c4cdea1a4399bb1819a539195fb69db4bbe0bde5b7c7f18fed412e02"
dependencies = [
"ascii",
"ascii 1.0.0",
"chrono",
"chunked_transfer",
"log",
@ -4614,7 +4810,7 @@ version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6"
dependencies = [
"bytes",
"bytes 0.4.12",
"futures 0.1.29",
"mio 0.6.22",
"num_cpus",
@ -4632,7 +4828,7 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46"
dependencies = [
"bytes",
"bytes 0.4.12",
"either",
"futures 0.1.29",
]
@ -4663,7 +4859,7 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674"
dependencies = [
"bytes",
"bytes 0.4.12",
"futures 0.1.29",
"log",
]
@ -4703,7 +4899,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72"
dependencies = [
"bytes",
"bytes 0.4.12",
"futures 0.1.29",
"iovec",
"mio 0.6.22",
@ -4975,26 +5171,52 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "unreachable"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
dependencies = [
"void",
]
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "801125e6d1ba6864cf3a5a92cfb2f0b0a3ee73e40602a0cd206ad2f3c040aa96"
dependencies = [
"base64 0.11.0",
"chunked_transfer",
"cookie",
"lazy_static",
"qstring",
"rustls 0.16.0",
"url 2.1.1",
"webpki",
"webpki-roots 0.18.0",
]
[[package]]
name = "ureq"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fb6c9aba13a511bcbb7770864c0e9b8392acda0454a71104498a2bb112d701"
dependencies = [
"base64",
"base64 0.12.3",
"chunked_transfer",
"lazy_static",
"qstring",
"rustls",
"rustls 0.18.1",
"url 2.1.1",
"webpki",
"webpki-roots",
"webpki-roots 0.20.0",
]
[[package]]
@ -5648,6 +5870,15 @@ dependencies = [
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91cd5736df7f12a964a5067a12c62fa38e1bd8080aff1f80bc29be7c80d19ab4"
dependencies = [
"webpki",
]
[[package]]
name = "webpki-roots"
version = "0.20.0"
@ -5804,6 +6035,15 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "xattr"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c"
dependencies = [
"libc",
]
[[package]]
name = "xcb"
version = "0.9.0"

View File

@ -0,0 +1,9 @@
(
tracks: [
(
path: "voxygen.audio.ambient.wind",
length: 14.5,
tag: Wind,
),
]
)

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/audio/ambient/wind.ogg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,8 +1,69 @@
(
{
//
// Ambient
//
Campfire: (
files: [
"voxygen.audio.sfx.ambient.fire",
],
threshold: 0.5,
),
Embers: (
files: [
"voxygen.audio.sfx.ambient.embers",
],
threshold: 0.5,
),
Birdcall: (
files: [
"voxygen.audio.sfx.ambient.birdcall_1",
"voxygen.audio.sfx.ambient.birdcall_2",
],
threshold: 10.0,
),
Owl: (
files: [
"voxygen.audio.sfx.ambient.owl_1",
],
threshold: 14.0,
),
Cricket: (
files: [
"voxygen.audio.sfx.ambient.crickets_1",
],
threshold: 3.0,
),
Frog: (
files: [
"voxygen.audio.sfx.ambient.frog_croak_1",
],
threshold: 4.0,
),
Bees: (
files: [
"voxygen.audio.sfx.ambient.bees_1",
],
threshold: 15.0,
),
RunningWater: (
files: [
"voxygen.audio.sfx.ambient.running_water_1",
],
threshold: 7.0,
),
//
// Character States
//
Swim: (
files: [
"voxygen.audio.sfx.footsteps.water_splash_1",
"voxygen.audio.sfx.footsteps.water_splash_2",
"voxygen.audio.sfx.footsteps.water_splash_3",
"voxygen.audio.sfx.footsteps.water_splash_4",
],
threshold: 0.25,
),
Run: (
files: [
"voxygen.audio.sfx.footsteps.stepgrass_1",
@ -14,32 +75,59 @@
],
threshold: 0.25,
),
ExperienceGained: (
QuadRun: (
files: [
// "voxygen.audio.sfx.character.experience_gained_1",
// "voxygen.audio.sfx.character.experience_gained_2",
// "voxygen.audio.sfx.character.experience_gained_3",
"voxygen.audio.sfx.footsteps.stepgrass_1",
"voxygen.audio.sfx.footsteps.stepgrass_2",
"voxygen.audio.sfx.footsteps.stepgrass_3",
"voxygen.audio.sfx.footsteps.stepgrass_4",
"voxygen.audio.sfx.footsteps.stepgrass_5",
"voxygen.audio.sfx.footsteps.stepgrass_6",
],
threshold: 0.5,
threshold: 0.12,
),
SnowRun: (
files: [
"voxygen.audio.sfx.footsteps.snow_step_1",
"voxygen.audio.sfx.footsteps.snow_step_2",
"voxygen.audio.sfx.footsteps.snow_step_3",
],
threshold: 0.25,
),
QuadSnowRun: (
files: [
"voxygen.audio.sfx.footsteps.snow_step_1",
"voxygen.audio.sfx.footsteps.snow_step_2",
"voxygen.audio.sfx.footsteps.snow_step_3",
],
threshold: 0.12,
),
//ExperienceGained: (
// files: [
// "voxygen.audio.sfx.character.experience_gained_1",
// "voxygen.audio.sfx.character.experience_gained_2",
// "voxygen.audio.sfx.character.experience_gained_3",
// ],
// threshold: 0.5,
//),
LevelUp:(
files: [
"voxygen.audio.sfx.character.level_up_sound_-_shorter_wind_up",
],
threshold: 0.5,
),
Jump: (
files: [
// Event not implemented?
],
threshold: 0.25,
),
Fall: (
files: [
// Event not implemented?
],
threshold: 0.25,
),
//Jump: (
// files: [
// // Event not implemented?
// ],
// threshold: 0.25,
//),
//Fall: (
// files: [
// // Event not implemented?
// ],
// threshold: 0.25,
//),
Roll: (
files: [
"voxygen.audio.sfx.character.dive_roll_1",
@ -47,30 +135,30 @@
],
threshold: 0.25,
),
Climb: (
files: [
// TODO: sync with animation
//"voxygen.audio.sfx.footsteps.stepgrass_1",
//"voxygen.audio.sfx.footsteps.stepgrass_2",
//"voxygen.audio.sfx.footsteps.stepgrass_3",
//"voxygen.audio.sfx.footsteps.stepgrass_4",
//"voxygen.audio.sfx.footsteps.stepgrass_5",
//"voxygen.audio.sfx.footsteps.stepgrass_6",
],
threshold: 0.25,
),
//Climb: (
// files: [
// TODO: sync with animation
// "voxygen.audio.sfx.footsteps.stepgrass_1",
// "voxygen.audio.sfx.footsteps.stepgrass_2",
// "voxygen.audio.sfx.footsteps.stepgrass_3",
// "voxygen.audio.sfx.footsteps.stepgrass_4",
// "voxygen.audio.sfx.footsteps.stepgrass_5",
// "voxygen.audio.sfx.footsteps.stepgrass_6",
// ],
// threshold: 0.25,
//),
GliderOpen: (
files: [
"voxygen.audio.sfx.glider_open",
],
threshold: 0.5,
),
Glide: (
files: [
// Event Missing or not implemented?
],
threshold: 0.5,
),
//Glide: (
// files: [
// // Event Missing or not implemented?
// ],
// threshold: 0.5,
//),
GliderClose: (
files: [
"voxygen.audio.sfx.glider_close",
@ -145,7 +233,7 @@
],
threshold: 0.5,
),
Attack(BasicMelee, Hammer): (
Attack(ComboMelee(Swing, 1), Hammer): (
files: [
"voxygen.audio.sfx.abilities.swing",
],
@ -185,7 +273,13 @@
],
threshold: 0.5,
),
Attack(BasicMelee, Axe): (
Attack(ComboMelee(Swing, 1), Axe): (
files: [
"voxygen.audio.sfx.abilities.swing",
],
threshold: 0.7,
),
Attack(ComboMelee(Swing, 2), Axe): (
files: [
"voxygen.audio.sfx.abilities.swing",
],
@ -211,32 +305,32 @@
),
//
// Fire Rod / Regeneration Staff
// Fire Staff
//
Wield(Staff): (
files: [
"voxygen.audio.sfx.weapon.staff_out",
"voxygen.audio.sfx.weapon.sword_out",
],
threshold: 0.5,
),
Unwield(Staff): (
files: [
"voxygen.audio.sfx.weapon.staff_in",
"voxygen.audio.sfx.weapon.sword_in",
],
threshold: 0.5,
),
Attack(BasicMelee, Staff): (
Attack(BasicBeam, Staff): (
files: [
"voxygen.audio.sfx.abilities.swing",
"voxygen.audio.sfx.abilities.flame_thrower",
],
threshold: 0.8,
),
Attack(BasicRanged, Staff): (
files: [
// "voxygen.audio.sfx.abilities.staff_channeling",
],
threshold: 0.8,
threshold: 0.2,
),
//Attack(BasicRanged, Staff): (
// files: [
// "voxygen.audio.sfx.abilities.staff_channeling",
// ],
// threshold: 0.8,
//),
Inventory(CollectedTool(Staff)): (
files: [
"voxygen.audio.sfx.inventory.pickup_staff",
@ -259,12 +353,12 @@
],
threshold: 0.5,
),
Attack(BasicRanged, Bow): (
files: [
// channeling sound.
],
threshold: 0.8,
),
//Attack(BasicRanged, Bow): (
// files: [
// // channeling sound.
// ],
// threshold: 0.8,
//),
Inventory(CollectedTool(Bow)): (
files: [
"voxygen.audio.sfx.inventory.add_item",
@ -272,6 +366,22 @@
threshold: 0.3,
),
//
// Sceptre
//
Wield(Sceptre): (
files: [
"voxygen.audio.sfx.weapon.sword_out",
],
threshold: 0.5,
),
Unwield(Sceptre): (
files: [
"voxygen.audio.sfx.weapon.sword_in",
],
threshold: 0.5,
),
//
// Dagger
//
@ -343,6 +453,12 @@
],
threshold: 0.3,
),
Inventory(CollectedItem("ShinyGem")): (
files: [
"voxygen.audio.sfx.weapon.staff_out",
],
threshold: 0.3,
),
Inventory(CollectFailed): (
files: [
"voxygen.audio.sfx.inventory.add_failed",
@ -461,17 +577,17 @@
],
threshold: 0.3,
),
Explosion: (
files: [
// in code
],
threshold: 0.2,
),
ProjectileShot: (
files: [
// in code
],
threshold: 0.5,
),
//Explosion: (
// files: [
// // in code
// ],
// threshold: 0.2,
//),
//ProjectileShot: (
// files: [
// // in code
// ],
// threshold: 0.5,
//),
}
)

BIN
assets/voxygen/audio/sfx/abilities/flame_thrower.wav (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/audio/sfx/ambient/bees_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/ambient/birdcall_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/ambient/birdcall_2.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/ambient/crickets_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/ambient/embers.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/ambient/fire.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/ambient/frog_croak_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/ambient/owl_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/ambient/running_water_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/audio/sfx/footsteps/wood_step_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/footsteps/wood_step_2.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/footsteps/wood_step_3.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/footsteps/wood_step_4.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,119 +1,194 @@
// TODO: Re-add tunes that are not fitting general outside day/night situations
// TODO: Add an ambient-soundtrack that runs independently from the musical soundtrack
(
tracks: [
(
title: "Forest Day", // Ambience Track
path: "voxygen.audio.ambient.forest_day",
length: 629.0,
timing: Some(Day),
artist: "https://www.youtube.com/watch?v=FwVTkB-BIvM",
),
(
title: "A Solemn Quest",
path: "voxygen.audio.soundtrack.a_solemn_quest",
length: 206.0,
timing: Some(Night),
artist: "Eden",
),
(
title: "Into The Dark Forest",
path: "voxygen.audio.soundtrack.into_the_dark_forest",
length: 184.0,
timing: Some(Night),
artist: "Aeronic",
),
(
title: "Field Grazing",
path: "voxygen.audio.soundtrack.field_grazing",
length: 154.0,
timing: Some(Day),
artist: "Aeronic",
),
(
title: "Wandering Voices",
path: "voxygen.audio.soundtrack.wandering_voices",
length: 137.0,
timing: Some(Night),
artist: "Aeronic",
),
/*(
title: "Snowtop Volume", //Snow Region
path: "voxygen.audio.soundtrack.snowtop_volume",
length: 89.0,
timing: Some(Day),
artist: "Aeronic",
),*/
(
title: "Mineral Deposits",
path: "voxygen.audio.soundtrack.mineral_deposits",
length: 148.0,
timing: Some(Day),
artist: "Aeronic",
),
(
title: "Moonbeams",
path: "voxygen.audio.soundtrack.moonbeams",
length: 158.0,
timing: Some(Night),
artist: "Aeronic",
),
(
title: "Serene Meadows",
path: "voxygen.audio.soundtrack.serene_meadows",
length: 173.0,
timing: Some(Night),
artist: "Aeronic",
),
/*(
title: "Rest Assured", // Town/Shop
path: "voxygen.audio.soundtrack.rest_assured",
length: 185.0,
timing: Some(Day),
artist: "badbbad",
),*/
(
title: "Just The Beginning",
path: "voxygen.audio.soundtrack.just_the_beginning",
length: 188.0,
timing: Some(Day),
artist: "badbbad",
),
(
title: "Campfire Stories",
path: "voxygen.audio.soundtrack.campfire_stories",
length: 100.0,
timing: Some(Night),
artist: "badbbad",
),
(
title: "Limits",
path: "voxygen.audio.soundtrack.limits",
length: 203.0,
timing: Some(Night),
artist: "badbbad",
),
/*( // Dungeon
title: "Down The Rabbit Hole",
path: "voxygen.audio.soundtrack.down_the_rabbit_hole",
length: 244.0,
timing: Some(Night),
artist: "badbbad",
),*/
(
title: "Between The Fairies",
path: "voxygen.audio.soundtrack.between_the_fairies",
length: 175.0,
timing: Some(Night),
artist: "badbbad",
),
(
title: "Forest Morning", // Ambience Track
path: "voxygen.audio.ambient.forest_morning",
length: 600.0,
timing: Some(Day),
artist: "https://www.youtube.com/watch?v=eq4nfIdK6C4",
),
]
)
// TODO: Re-add tunes that are not fitting general outside day/night situations
// TODO: Add an ambient-soundtrack that runs independently from the musical soundtrack
// Times: Some(Day), Some(Night), None
// List of biomes currently: Grassland, Forest, Desert, Snowland, Lake, Mountain, Ocean
// Also Jungle and Swamp but these are not defined currently as the worldgen around
// them is changing and not stable
// Sites: Cave, Dungeon
(
tracks: [
(
title: "Oceania",
path: "voxygen.audio.soundtrack.oceania",
length: 135.0,
timing: None,
biomes: [
(Lake, 2),
(Ocean, 3),
],
site: Some(Void),
artist: "Eden",
),
(
title: "A Solemn Quest",
path: "voxygen.audio.soundtrack.a_solemn_quest",
length: 206.0,
timing: Some(Night),
biomes: [
(Desert, 1),
(Grassland, 1),
(Snowland, 1),
(Mountain, 1),
(Lake, 1),
],
site: Some(Void),
artist: "Eden",
),
(
title: "Into The Dark Forest",
path: "voxygen.audio.soundtrack.into_the_dark_forest",
length: 184.0,
timing: Some(Night),
biomes: [
(Forest, 1),
],
site: Some(Void),
artist: "Aeronic",
),
(
title: "Field Grazing",
path: "voxygen.audio.soundtrack.field_grazing",
length: 154.0,
timing: Some(Day),
biomes: [
(Grassland, 1),
],
site: Some(Void),
artist: "Aeronic",
),
(
title: "Wandering Voices",
path: "voxygen.audio.soundtrack.wandering_voices",
length: 137.0,
timing: Some(Night),
biomes: [],
site: Some(Void),
artist: "Aeronic",
),
(
title: "Snowtop Volume",
path: "voxygen.audio.soundtrack.snowtop_volume",
length: 89.0,
timing: None,
biomes: [
(Snowland, 1),
(Mountain, 1),
],
site: Some(Void),
artist: "Aeronic",
),
(
title: "Mineral Deposits",
path: "voxygen.audio.soundtrack.mineral_deposits",
length: 148.0,
timing: None,
biomes: [],
site: Some(Cave),
artist: "Aeronic",
),
( //Repeat for other site
title: "Mineral Deposits",
path: "voxygen.audio.soundtrack.mineral_deposits",
length: 148.0,
timing: None,
biomes: [],
site: Some(Dungeon),
artist: "Aeronic",
),
(
title: "Moonbeams",
path: "voxygen.audio.soundtrack.moonbeams",
length: 158.0,
timing: Some(Night),
biomes: [],
site: Some(Void),
artist: "Aeronic",
),
(
title: "Serene Meadows",
path: "voxygen.audio.soundtrack.serene_meadows",
length: 173.0,
timing: Some(Night),
biomes: [
(Grassland, 1),
],
site: Some(Void),
artist: "Aeronic",
),
//(
// title: "Rest Assured", // Town/Shop
// path: "voxygen.audio.soundtrack.rest_assured",
// length: 185.0,
// timing: Some(Day),
// biomes: [],
// site: Some(Void),
// artist: "badbbad",
//),
(
title: "Just The Beginning",
path: "voxygen.audio.soundtrack.just_the_beginning",
length: 188.0,
timing: Some(Day),
biomes: [
(Grassland, 1),
(Snowland, 1),
(Mountain, 1),
],
site: Some(Void),
artist: "badbbad",
),
(
title: "Campfire Stories",
path: "voxygen.audio.soundtrack.campfire_stories",
length: 100.0,
timing: Some(Night),
biomes: [],
site: Some(Void),
artist: "badbbad",
),
(
title: "Limits",
path: "voxygen.audio.soundtrack.limits",
length: 203.0,
timing: None,
biomes: [
(Desert, 1),
(Lake, 1)
],
site: Some(Void),
artist: "badbbad",
),
(
title: "Down The Rabbit Hole",
path: "voxygen.audio.soundtrack.down_the_rabbit_hole",
length: 244.0,
timing: None,
biomes: [],
site: Some(Cave),
artist: "badbbad",
),
( //Repeat for other site
title: "Down The Rabbit Hole",
path: "voxygen.audio.soundtrack.down_the_rabbit_hole",
length: 244.0,
timing: None,
biomes: [],
site: Some(Dungeon),
artist: "badbbad",
),
(
title: "Between The Fairies",
path: "voxygen.audio.soundtrack.between_the_fairies",
length: 175.0,
timing: Some(Night),
biomes: [
(Forest, 1),
(Lake, 1),
(Snowland, 1),
],
site: Some(Void),
artist: "badbbad",
)
]
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/audio/soundtrack/limits.ogg (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/audio/soundtrack/oceania.ogg (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -34,7 +34,7 @@ use common::{
recipe::RecipeBook,
state::State,
sync::{Uid, UidAllocator, WorldSyncExt},
terrain::{block::Block, neighbors, TerrainChunk, TerrainChunkSize},
terrain::{block::Block, neighbors, BiomeKind, SitesKind, TerrainChunk, TerrainChunkSize},
vol::RectVolSize,
};
use comp::BuffKind;
@ -46,6 +46,7 @@ use image::DynamicImage;
use network::{Network, Participant, Pid, ProtocolAddr, Stream};
use num::traits::FloatConst;
use rayon::prelude::*;
use specs::Component;
use std::{
collections::VecDeque,
net::SocketAddr,
@ -857,6 +858,40 @@ impl Client {
self.state.terrain().get_key_arc(chunk_pos).cloned()
}
pub fn current<C: Component>(&self) -> Option<C>
where
C: Clone,
{
Some(self.state.read_storage::<C>().get(self.entity).cloned()?)
}
pub fn current_biome(&self) -> BiomeKind {
match self.current_chunk() {
Some(chunk) => chunk.meta().biome(),
_ => BiomeKind::Void,
}
}
pub fn current_site(&self) -> SitesKind {
let mut player_alt = 0.0;
if let Some(position) = self.current::<comp::Pos>() {
player_alt = position.0.z;
}
let mut contains_cave = false;
let mut terrain_alt = 0.0;
if let Some(chunk) = self.current_chunk() {
terrain_alt = chunk.meta().alt();
contains_cave = chunk.meta().contains_cave();
}
if player_alt < (terrain_alt - 15.0) && contains_cave {
SitesKind::Cave
} else if player_alt < (terrain_alt - 15.0) {
SitesKind::Dungeon
} else {
SitesKind::Void
}
}
pub fn inventories(&self) -> ReadStorage<comp::Inventory> { self.state.read_storage() }
pub fn loadouts(&self) -> ReadStorage<comp::Loadout> { self.state.read_storage() }

View File

@ -22,6 +22,13 @@ pub enum Outcome {
body: comp::Body,
vel: Vec3<f32>,
},
LevelUp {
pos: Vec3<f32>,
},
Beam {
pos: Vec3<f32>,
heal: bool,
},
}
impl Outcome {
@ -29,6 +36,8 @@ impl Outcome {
match self {
Outcome::Explosion { pos, .. } => Some(*pos),
Outcome::ProjectileShot { pos, .. } => Some(*pos),
Outcome::LevelUp { pos } => Some(*pos),
Outcome::Beam { pos, .. } => Some(*pos),
}
}
}

View File

@ -56,7 +56,7 @@ impl<'a> System<'a> for Sys {
let start_time = std::time::Instant::now();
span!(_guard, "run", "melee::Sys::run");
let mut server_emitter = server_bus.emitter();
let mut _local_emitter = local_bus.emitter();
let _local_emitter = local_bus.emitter();
// Attacks
for (entity, uid, pos, ori, scale_maybe, attack) in (
&entities,

View File

@ -1,13 +1,15 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub enum BiomeKind {
Void,
Lake,
Grassland,
Ocean,
Mountain,
Snowlands,
Snowland,
Desert,
Swamp,
Jungle,
Forest,
}

View File

@ -42,7 +42,6 @@ make_case_elim!(
Wood = 0x40,
Leaves = 0x41,
// 0x42 <= x < 0x50 is reserved for future tree parts
// Covers all other cases (we sometimes have bizarrely coloured misc blocks, and also we
// often want to experiment with new kinds of block without allocating them a
// dedicated block kind.

View File

@ -2,6 +2,7 @@ pub mod biome;
pub mod block;
pub mod chonk;
pub mod map;
pub mod site;
pub mod sprite;
pub mod structure;
@ -10,6 +11,7 @@ pub use self::{
biome::BiomeKind,
block::{Block, BlockKind},
map::MapSizeLg,
site::SitesKind,
sprite::SpriteKind,
structure::Structure,
};
@ -50,21 +52,53 @@ impl RectVolSize for TerrainChunkSize {
pub struct TerrainChunkMeta {
name: Option<String>,
biome: BiomeKind,
alt: f32,
tree_density: f32,
contains_cave: bool,
contains_river: bool,
}
impl TerrainChunkMeta {
pub fn new(name: Option<String>, biome: BiomeKind) -> Self { Self { name, biome } }
pub fn new(
name: Option<String>,
biome: BiomeKind,
alt: f32,
tree_density: f32,
contains_cave: bool,
contains_river: bool,
) -> Self {
Self {
name,
biome,
alt,
tree_density,
contains_cave,
contains_river,
}
}
pub fn void() -> Self {
Self {
name: None,
biome: BiomeKind::Void,
alt: 0.0,
tree_density: 0.0,
contains_cave: false,
contains_river: false,
}
}
pub fn name(&self) -> &str { self.name.as_deref().unwrap_or("Wilderness") }
pub fn biome(&self) -> BiomeKind { self.biome }
pub fn alt(&self) -> f32 { self.alt }
pub fn tree_density(&self) -> f32 { self.tree_density }
pub fn contains_cave(&self) -> bool { self.contains_cave }
pub fn contains_river(&self) -> bool { self.contains_river }
}
// Terrain type aliases

View File

@ -0,0 +1,10 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub enum SitesKind {
Dungeon,
Cave,
Settlement,
Castle,
Void,
}

View File

@ -145,6 +145,11 @@ pub fn handle_shockwave(
pub fn handle_beam(server: &mut Server, properties: beam::Properties, pos: Pos, ori: Ori) {
let state = server.state_mut();
let ecs = state.ecs();
ecs.write_resource::<Vec<Outcome>>().push(Outcome::Beam {
pos: pos.0,
heal: properties.lifesteal_eff > 0.0,
});
state.create_beam(properties, pos, ori).build();
}

View File

@ -686,14 +686,24 @@ pub fn handle_explosion(
}
pub fn handle_level_up(server: &mut Server, entity: EcsEntity, new_level: u32) {
let ecs = &server.state.ecs();
let uids = server.state.ecs().read_storage::<Uid>();
let uid = uids
.get(entity)
.expect("Failed to fetch uid component for entity.");
let pos = server
.state
.ecs()
.read_storage::<Pos>()
.get(entity)
.expect("Failed to fetch position component for the entity.")
.0;
server.state.notify_players(ServerGeneral::PlayerListUpdate(
PlayerListUpdate::LevelChange(*uid, new_level),
));
ecs.write_resource::<Vec<Outcome>>()
.push(Outcome::LevelUp { pos });
}
pub fn handle_buff(server: &mut Server, entity: EcsEntity, buff_change: buff::BuffChange) {

View File

@ -58,7 +58,7 @@ server = {package = "veloren-server", path = "../server", optional = true}
backtrace = "0.3.40"
bincode = "1.2"
chrono = "0.4.9"
cpal = "0.11"
cpal = "0.13"
copy_dir = "0.1.2"
crossbeam = "=0.7.2"
deunicode = "1.0"
@ -75,7 +75,7 @@ native-dialog = { version = "0.4.2", default-features = false, optional = true }
num = "0.2"
ordered-float = { version = "2.0.0", default-features = false }
rand = "0.7"
rodio = {version = "0.11", default-features = false, features = ["wav", "vorbis"]}
rodio = {version = "0.13", default-features = false, features = ["wav", "vorbis"]}
ron = {version = "0.6", default-features = false}
serde = {version = "1.0", features = [ "rc", "derive" ]}
treeculler = "0.1.0"

View File

@ -0,0 +1,145 @@
//! Handles ambient non-positional sounds
use crate::{
audio::{channel::AmbientChannelTag, AudioFrontend},
scene::Camera,
};
use client::Client;
use common::{assets, state::State, vol::ReadVol};
use serde::Deserialize;
use std::time::Instant;
use tracing::warn;
use vek::*;
#[derive(Debug, Default, Deserialize)]
struct AmbientCollection {
tracks: Vec<AmbientItem>,
}
/// Configuration for a single music track in the soundtrack
#[derive(Debug, Deserialize)]
pub struct AmbientItem {
path: String,
/// Length of the track in seconds
length: f32,
/// Specifies which ambient channel to play on
tag: AmbientChannelTag,
}
pub struct AmbientMgr {
soundtrack: AmbientCollection,
began_playing: Instant,
next_track_change: f32,
volume: f32,
tree_multiplier: f32,
}
impl Default for AmbientMgr {
fn default() -> Self {
Self {
soundtrack: Self::load_soundtrack_items(),
began_playing: Instant::now(),
next_track_change: 0.0,
volume: 0.0,
tree_multiplier: 0.0,
}
}
}
impl AmbientMgr {
/// Checks whether the previous track has completed. If so, sends a
/// request to play the next (random) track
pub fn maintain(
&mut self,
audio: &mut AudioFrontend,
state: &State,
client: &Client,
camera: &Camera,
) {
if audio.sfx_enabled() && !self.soundtrack.tracks.is_empty() {
let focus_off = camera.get_focus_pos().map(f32::trunc);
let cam_pos = camera.dependents().cam_pos + focus_off;
let (terrain_alt, tree_density) = if let Some(chunk) = client.current_chunk() {
(chunk.meta().alt(), chunk.meta().tree_density())
} else {
(0.0, 0.0)
};
// The following code is specifically for wind, as it is the only
// non-positional ambient sound in the game. Others can be added
// as seen fit.
// Wind volume increases with altitude
let alt_multiplier = (cam_pos.z / 1200.0).abs();
// Tree density factors into wind volume. The more trees,
// the lower wind volume. The trees make more of an impact
// the closer the camera is to the ground.
self.tree_multiplier =
((1.0 - tree_density) + ((cam_pos.z - terrain_alt) / 150.0).powf(2.0)).min(1.0);
let mut volume_multiplier = alt_multiplier * self.tree_multiplier;
// Checks if the camera is underwater to stop ambient sounds
if state
.terrain()
.get((cam_pos).map(|e| e.floor() as i32))
.map(|b| b.is_liquid())
.unwrap_or(false)
{
volume_multiplier *= 0.1;
}
if cam_pos.z < terrain_alt - 10.0 {
volume_multiplier = 0.0;
}
let target_volume = volume_multiplier.clamped(0.0, 1.0);
// Transitions the ambient sounds (more) smoothly
self.volume = audio.get_ambient_volume();
audio.set_ambient_volume(Lerp::lerp(self.volume, target_volume, 0.01));
if self.began_playing.elapsed().as_secs_f32() > self.next_track_change {
// Right now there is only wind non-positional sfx so it is always
// selected. Modify this variable assignment when adding other non-
// positional sfx
let track = &self
.soundtrack
.tracks
.iter()
.find(|track| track.tag == AmbientChannelTag::Wind);
if let Some(track) = track {
self.began_playing = Instant::now();
self.next_track_change = track.length;
audio.play_ambient(AmbientChannelTag::Wind, &track.path, volume_multiplier);
}
}
}
}
fn load_soundtrack_items() -> AmbientCollection {
match assets::load_file("voxygen.audio.ambient", &["ron"]) {
Ok(file) => match ron::de::from_reader(file) {
Ok(config) => config,
Err(error) => {
warn!(
"Error parsing music config file, music will not be available: {}",
format!("{:#?}", error)
);
AmbientCollection::default()
},
},
Err(error) => {
warn!(
"Error reading music config file, music will not be available: {}",
format!("{:#?}", error)
);
AmbientCollection::default()
},
}
}
}

View File

@ -20,7 +20,9 @@ use crate::audio::{
fader::{FadeDirection, Fader},
Listener,
};
use rodio::{Device, Sample, Sink, Source, SpatialSink};
use rodio::{OutputStreamHandle, Sample, Sink, Source, SpatialSink};
use serde::Deserialize;
use tracing::warn;
use vek::*;
#[derive(PartialEq, Clone, Copy)]
@ -53,18 +55,30 @@ pub struct MusicChannel {
}
impl MusicChannel {
pub fn new(device: &Device) -> Self {
Self {
sink: Sink::new(device),
tag: MusicChannelTag::TitleMusic,
state: ChannelState::Stopped,
fader: Fader::default(),
pub fn new(stream: &OutputStreamHandle) -> Self {
let new_sink = Sink::try_new(stream);
match new_sink {
Ok(sink) => Self {
sink,
tag: MusicChannelTag::TitleMusic,
state: ChannelState::Stopped,
fader: Fader::default(),
},
Err(_) => {
warn!("Failed to create a rodio sink. May not play sounds.");
Self {
sink: Sink::new_idle().0,
tag: MusicChannelTag::TitleMusic,
state: ChannelState::Stopped,
fader: Fader::default(),
}
},
}
}
// Play a music track item on this channel. If the channel has an existing track
// playing, the new sounds will be appended and played once they complete.
// Otherwise it will begin playing immediately.
/// Play a music track item on this channel. If the channel has an existing
/// track playing, the new sounds will be appended and played once they
/// complete. Otherwise it will begin playing immediately.
pub fn play<S>(&mut self, source: S, tag: MusicChannelTag)
where
S: Source + Send + 'static,
@ -82,6 +96,12 @@ impl MusicChannel {
};
}
/// Stop whatever is playing on a given music channel
pub fn stop(&mut self, tag: MusicChannelTag) {
self.tag = tag;
self.sink.stop();
}
/// Set the volume of the current channel. If the channel is currently
/// fading, the volume of the fader is updated to this value.
pub fn set_volume(&mut self, volume: f32) {
@ -134,6 +154,51 @@ impl MusicChannel {
}
}
/// AmbientChannelTags are used for non-positional sfx. Currently the only use
/// is for wind.
#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
pub enum AmbientChannelTag {
Wind,
}
/// A AmbientChannel uses a non-positional audio sink designed to play sounds
/// which are always heard at the camera's position.
pub struct AmbientChannel {
tag: AmbientChannelTag,
sink: Sink,
}
impl AmbientChannel {
pub fn new(stream: &OutputStreamHandle, tag: AmbientChannelTag) -> Self {
let new_sink = Sink::try_new(stream);
match new_sink {
Ok(sink) => Self { sink, tag },
Err(_) => {
warn!("Failed to create rodio sink. May not play wind sounds.");
Self {
sink: Sink::new_idle().0,
tag,
}
},
}
}
pub fn play<S>(&mut self, source: S)
where
S: Source + Send + 'static,
S::Item: Sample,
S::Item: Send,
<S as std::iter::Iterator>::Item: std::fmt::Debug,
{
self.sink.append(source);
}
pub fn set_volume(&mut self, volume: f32) { self.sink.set_volume(volume); }
pub fn get_volume(&mut self) -> f32 { self.sink.volume() }
pub fn get_tag(&self) -> AmbientChannelTag { self.tag }
}
/// An SfxChannel uses a positional audio sink, and is designed for short-lived
/// audio which can be spatially controlled, but does not need control over
/// playback or fading/transitions
@ -145,9 +210,10 @@ pub struct SfxChannel {
}
impl SfxChannel {
pub fn new(device: &Device) -> Self {
pub fn new(stream: &OutputStreamHandle) -> Self {
Self {
sink: SpatialSink::new(device, [0.0; 3], [1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]),
sink: SpatialSink::try_new(stream, [0.0; 3], [1.0, 0.0, 0.0], [-1.0, 0.0, 0.0])
.unwrap(),
pos: Vec3::zero(),
}
}
@ -162,6 +228,17 @@ impl SfxChannel {
self.sink.append(source);
}
/// Same as SfxChannel::play but with the source passed through
/// a low pass filter at 300 Hz
pub fn play_with_low_pass_filter<S>(&mut self, source: S)
where
S: Sized + Send + 'static,
S: Source<Item = f32>,
{
let source = source.low_pass(300);
self.sink.append(source);
}
pub fn set_volume(&mut self, volume: f32) { self.sink.set_volume(volume); }
pub fn is_done(&self) -> bool { self.sink.empty() }

View File

@ -1,20 +1,21 @@
//! Handles audio device detection and playback of sound effects and music
pub mod ambient;
pub mod channel;
pub mod fader;
pub mod music;
pub mod sfx;
pub mod soundcache;
use channel::{MusicChannel, MusicChannelTag, SfxChannel};
use channel::{AmbientChannel, AmbientChannelTag, MusicChannel, MusicChannelTag, SfxChannel};
use fader::Fader;
use sfx::{SfxEvent, SfxTriggerItem};
use soundcache::SoundCache;
use std::time::Duration;
use tracing::warn;
use tracing::debug;
use common::assets;
use cpal::traits::DeviceTrait;
use rodio::{source::Source, Decoder, Device};
use rodio::{source::Source, Decoder, OutputStream, OutputStreamHandle, StreamError};
use vek::*;
#[derive(Default, Clone)]
@ -31,40 +32,56 @@ pub struct Listener {
/// Voxygen's [`GlobalState`](../struct.GlobalState.html#structfield.audio) to
/// provide access to devices and playback control in-game
pub struct AudioFrontend {
pub device: String,
pub device_list: Vec<String>,
audio_device: Option<Device>,
// The following is for the disabled device switcher
//pub device: String,
//pub device_list: Vec<String>,
//pub audio_device: Option<Device>,
pub stream: Option<rodio::OutputStream>,
audio_stream: Option<rodio::OutputStreamHandle>,
sound_cache: SoundCache,
music_channels: Vec<MusicChannel>,
ambient_channels: Vec<AmbientChannel>,
sfx_channels: Vec<SfxChannel>,
sfx_volume: f32,
music_volume: f32,
listener: Listener,
}
impl AudioFrontend {
/// Construct with given device
pub fn new(device: String, max_sfx_channels: usize) -> Self {
let audio_device = get_device_raw(&device);
pub fn new(/* dev: String, */ max_sfx_channels: usize) -> Self {
// Commented out until audio device switcher works
//let audio_device = get_device_raw(&dev);
//let device = match get_default_device() {
// Some(d) => d,
// None => "".to_string(),
//};
let (stream, audio_stream) = match get_default_stream() {
Ok(s) => (Some(s.0), Some(s.1)),
Err(_) => (None, None),
};
let mut sfx_channels = Vec::with_capacity(max_sfx_channels);
if let Some(audio_device) = &audio_device {
sfx_channels.resize_with(max_sfx_channels, || SfxChannel::new(&audio_device));
}
if let Some(audio_stream) = &audio_stream {
sfx_channels.resize_with(max_sfx_channels, || SfxChannel::new(audio_stream));
};
Self {
device,
device_list: list_devices(),
audio_device,
// The following is for the disabled device switcher
//device,
//device_list: list_devices(),
//audio_device,
stream,
audio_stream,
sound_cache: SoundCache::default(),
music_channels: Vec::new(),
sfx_channels,
ambient_channels: Vec::new(),
sfx_volume: 1.0,
music_volume: 1.0,
listener: Listener::default(),
}
}
@ -72,12 +89,16 @@ impl AudioFrontend {
/// Construct in `no-audio` mode for debugging
pub fn no_audio() -> Self {
Self {
device: "none".to_string(),
device_list: Vec::new(),
audio_device: None,
// The following is for the disabled device switcher
//device: "".to_string(),
//device_list: Vec::new(),
//audio_device: None,
stream: None,
audio_stream: None,
sound_cache: SoundCache::default(),
music_channels: Vec::new(),
sfx_channels: Vec::new(),
ambient_channels: Vec::new(),
sfx_volume: 1.0,
music_volume: 1.0,
listener: Listener::default(),
@ -93,8 +114,9 @@ impl AudioFrontend {
}
}
/// Retrive an empty sfx channel from the list
fn get_sfx_channel(&mut self) -> Option<&mut SfxChannel> {
if self.audio_device.is_some() {
if self.audio_stream.is_some() {
if let Some(channel) = self.sfx_channels.iter_mut().find(|c| c.is_done()) {
channel.set_volume(self.sfx_volume);
@ -114,9 +136,9 @@ impl AudioFrontend {
&mut self,
next_channel_tag: MusicChannelTag,
) -> Option<&mut MusicChannel> {
if let Some(audio_device) = &self.audio_device {
if let Some(audio_stream) = &self.audio_stream {
if self.music_channels.is_empty() {
let mut next_music_channel = MusicChannel::new(&audio_device);
let mut next_music_channel = MusicChannel::new(audio_stream);
next_music_channel.set_volume(self.music_volume);
self.music_channels.push(next_music_channel);
@ -128,7 +150,7 @@ impl AudioFrontend {
existing_channel
.set_fader(Fader::fade_out(Duration::from_secs(2), self.music_volume));
let mut next_music_channel = MusicChannel::new(&audio_device);
let mut next_music_channel = MusicChannel::new(&audio_stream);
next_music_channel
.set_fader(Fader::fade_in(Duration::from_secs(12), self.music_volume));
@ -141,9 +163,74 @@ impl AudioFrontend {
self.music_channels.last_mut()
}
/// Function to play sfx from external places. Useful for UI and
/// inventory events
pub fn emit_sfx_item(&mut self, trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>) {
if let Some((event, item)) = trigger_item {
let sfx_file = match item.files.len() {
0 => {
debug!("Sfx event {:?} is missing audio file.", event);
"voxygen.audio.sfx.placeholder"
},
1 => item
.files
.last()
.expect("Failed to determine sound file for this trigger item."),
_ => {
// If more than one file is listed, choose one at random
let rand_step = rand::random::<usize>() % item.files.len();
&item.files[rand_step]
},
};
self.play_sfx(sfx_file, self.listener.pos, None);
} else {
debug!("Missing sfx trigger config for external sfx event.",);
}
}
/// Play an sfx file given the position, SfxEvent, and whether it is
/// underwater or not
pub fn emit_sfx(
&mut self,
trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
position: Vec3<f32>,
volume: Option<f32>,
underwater: bool,
) {
if let Some((event, item)) = trigger_item {
let sfx_file = match item.files.len() {
0 => {
debug!("Sfx event {:?} is missing audio file.", event);
"voxygen.audio.sfx.placeholder"
},
1 => item
.files
.last()
.expect("Failed to determine sound file for this trigger item."),
_ => {
// If more than one file is listed, choose one at random
let rand_step = rand::random::<usize>() % item.files.len();
&item.files[rand_step]
},
};
if underwater {
self.play_underwater_sfx(sfx_file, position, volume);
} else {
self.play_sfx(sfx_file, position, volume);
}
} else {
debug!(
"Missing sfx trigger config for sfx event at position: {:?}",
position
);
}
}
/// Play (once) an sfx file by file path at the give position and volume
pub fn play_sfx(&mut self, sound: &str, pos: Vec3<f32>, vol: Option<f32>) {
if self.audio_device.is_some() {
if self.audio_stream.is_some() {
let sound = self
.sound_cache
.load_sound(sound)
@ -158,15 +245,119 @@ impl AudioFrontend {
}
}
fn play_music(&mut self, sound: &str, channel_tag: MusicChannelTag) {
if let Some(channel) = self.get_music_channel(channel_tag) {
let file = assets::load_file(&sound, &["ogg"]).expect("Failed to load sound");
let sound = Decoder::new(file).expect("Failed to decode sound");
/// Play (once) an sfx file by file path at the give position and volume
/// but with the sound passed through a low pass filter to simulate
/// being underwater
pub fn play_underwater_sfx(&mut self, sound: &str, pos: Vec3<f32>, vol: Option<f32>) {
if self.audio_stream.is_some() {
let sound = self
.sound_cache
.load_sound(sound)
.amplify(vol.unwrap_or(1.0));
channel.play(sound, channel_tag);
let listener = self.listener.clone();
if let Some(channel) = self.get_sfx_channel() {
channel.set_pos(pos);
channel.update(&listener);
channel.play_with_low_pass_filter(sound.convert_samples());
}
}
}
fn play_ambient(
&mut self,
channel_tag: AmbientChannelTag,
sound: &str,
volume_multiplier: f32,
) {
if self.audio_stream.is_some() {
if let Some(channel) = self.get_ambient_channel(channel_tag, volume_multiplier) {
let file = assets::load_file(&sound, &["ogg"]).expect("Failed to load sound");
let sound = Decoder::new(file).expect("Failed to decode sound");
channel.play(sound);
}
}
}
fn get_ambient_channel(
&mut self,
channel_tag: AmbientChannelTag,
volume_multiplier: f32,
) -> Option<&mut AmbientChannel> {
if let Some(audio_stream) = &self.audio_stream {
if self.ambient_channels.is_empty() {
let mut ambient_channel = AmbientChannel::new(audio_stream, channel_tag);
ambient_channel.set_volume(self.sfx_volume * volume_multiplier);
self.ambient_channels.push(ambient_channel);
} else {
for channel in self.ambient_channels.iter_mut() {
if channel.get_tag() == channel_tag {
channel.set_volume(self.sfx_volume * volume_multiplier);
return Some(channel);
}
}
}
}
None
}
fn set_ambient_volume(&mut self, volume_multiplier: f32) {
if self.audio_stream.is_some() {
if let Some(channel) = self.ambient_channels.iter_mut().last() {
channel.set_volume(self.sfx_volume * volume_multiplier);
}
}
}
fn get_ambient_volume(&mut self) -> f32 {
if self.audio_stream.is_some() {
if let Some(channel) = self.ambient_channels.iter_mut().last() {
channel.get_volume() / self.sfx_volume
} else {
0.0
}
} else {
0.0
}
}
fn play_music(&mut self, sound: &str, channel_tag: MusicChannelTag) {
if self.music_enabled() {
if let Some(channel) = self.get_music_channel(channel_tag) {
let file = assets::load_file(&sound, &["ogg"]).expect("Failed to load sound");
let sound = Decoder::new(file).expect("Failed to decode sound");
channel.play(sound, channel_tag);
}
}
}
/* These functions are saved for if we want music playback control at some
* point. They are not used currently but may be useful for later work.
*
fn fade_out_music(&mut self, channel_tag: MusicChannelTag) {
let music_volume = self.music_volume;
if let Some(channel) = self.get_music_channel(channel_tag) {
channel.set_fader(Fader::fade_out(Duration::from_secs(5), music_volume));
}
}
fn fade_in_music(&mut self, channel_tag: MusicChannelTag) {
let music_volume = self.music_volume;
if let Some(channel) = self.get_music_channel(channel_tag) {
channel.set_fader(Fader::fade_in(Duration::from_secs(5), music_volume));
}
}
fn stop_music(&mut self, channel_tag: MusicChannelTag) {
if let Some(channel) = self.get_music_channel(channel_tag) {
channel.stop(channel_tag);
}
}
*/
pub fn set_listener_pos(&mut self, pos: Vec3<f32>, ori: Vec3<f32>) {
self.listener.pos = pos;
self.listener.ori = ori.normalized();
@ -193,12 +384,6 @@ impl AudioFrontend {
}
}
pub fn play_exploration_music(&mut self, item: &str) {
if self.music_enabled() {
self.play_music(item, MusicChannelTag::Exploration)
}
}
pub fn get_sfx_volume(&self) -> f32 { self.sfx_volume }
pub fn get_music_volume(&self) -> f32 { self.music_volume }
@ -223,47 +408,58 @@ impl AudioFrontend {
}
}
// TODO: figure out how badly this will break things when it is called
pub fn set_device(&mut self, name: String) {
self.device = name.clone();
self.audio_device = get_device_raw(&name);
}
// The following is for the disabled device switcher
//// TODO: figure out how badly this will break things when it is called
//pub fn set_device(&mut self, name: String) {
// self.device = name.clone();
// self.audio_device = get_device_raw(&name);
//}
}
/// Returns the default audio device.
/// Does not return rodio Device struct in case our audio backend changes.
pub fn get_default_device() -> Option<String> {
match rodio::default_output_device() {
Some(x) => Some(x.name().ok()?),
None => None,
}
// The following is for the disabled device switcher
///// Returns the default audio device.
///// Does not return rodio Device struct in case our audio backend changes.
//pub fn get_default_device() -> Option<String> {
// match cpal::default_host().default_output_device() {
// Some(x) => Some(x.name().ok()?),
// None => None,
// }
//}
/// Returns the default stream
pub fn get_default_stream() -> Result<(OutputStream, OutputStreamHandle), StreamError> {
rodio::OutputStream::try_default()
}
/// Returns a vec of the audio devices available.
/// Does not return rodio Device struct in case our audio backend changes.
pub fn list_devices() -> Vec<String> {
list_devices_raw()
.iter()
.map(|x| x.name().unwrap())
.collect()
}
/// Returns vec of devices
fn list_devices_raw() -> Vec<Device> {
match rodio::output_devices() {
Ok(devices) => {
// Filter out any devices that the name isn't available for
devices.filter(|d| d.name().is_ok()).collect()
},
Err(_) => {
warn!("Failed to enumerate audio output devices, audio will not be available");
Vec::new()
},
}
}
fn get_device_raw(device: &str) -> Option<Device> {
list_devices_raw()
.into_iter()
.find(|d| d.name().unwrap() == device)
}
// The following is for the disabled device switcher
///// Returns a stream on the specified device
//pub fn get_stream(
// device: &rodio::Device,
//) -> Result<(OutputStream, OutputStreamHandle), StreamError> {
// rodio::OutputStream::try_from_device(device)
//}
//
//fn list_devices_raw() -> Vec<cpal::Device> {
// match cpal::default_host().devices() {
// Ok(devices) => devices.filter(|d| d.name().is_ok()).collect(),
// Err(_) => {
// warn!("Failed to enumerate audio output devices, audio will not be
// available"); Vec::new()
// },
// }
//}
//
///// Returns a vec of the audio devices available.
///// Does not return rodio Device struct in case our audio backend changes.
//fn list_devices() -> Vec<String> {
// list_devices_raw()
// .iter()
// .map(|x| x.name().unwrap())
// .collect()
//}
//
//fn get_device_raw(device: &str) -> Option<Device> {
// list_devices_raw()
// .into_iter()
// .find(|d| d.name().unwrap() == device)
//}

View File

@ -26,6 +26,11 @@
//! path: "voxygen.audio.soundtrack.sleepy",
//! length: 400.0,
//! timing: Some(Night),
//! biomes: [
//! (Forest, 1),
//! (Grassland, 2),
//! ],
//! site: None,
//! artist: "Elvis",
//! ),
//! ```
@ -37,30 +42,44 @@
//! tracks
//! - If you are not the author of the track, ensure that the song's licensing
//! permits usage of the track for non-commercial use
use crate::audio::AudioFrontend;
use common::{assets, state::State};
use rand::{seq::IteratorRandom, thread_rng};
use crate::audio::{AudioFrontend, MusicChannelTag};
use client::Client;
use common::{
assets,
state::State,
terrain::{BiomeKind, SitesKind},
};
use rand::{prelude::SliceRandom, thread_rng, Rng};
use serde::Deserialize;
use std::time::Instant;
use tracing::warn;
// TODO These should eventually not be constants if we have seasons
const DAY_START_SECONDS: u32 = 28800; // 8:00
const DAY_END_SECONDS: u32 = 70200; // 19:30
/// Collection of all the tracks
#[derive(Debug, Default, Deserialize)]
struct SoundtrackCollection {
/// List of tracks
tracks: Vec<SoundtrackItem>,
}
/// Configuration for a single music track in the soundtrack
#[derive(Debug, Deserialize)]
pub struct SoundtrackItem {
/// Song title
title: String,
/// File path to asset
path: String,
/// Length of the track in seconds
length: f64,
length: f32,
/// Whether this track should play during day or night
timing: Option<DayPeriod>,
/// What biomes this track should play in with chance of play
biomes: Vec<(BiomeKind, u8)>,
/// Whether this track should play in a specific site
site: Option<SitesKind>,
}
/// Allows control over when a track should play based on in-game time of day
@ -72,19 +91,30 @@ enum DayPeriod {
Night,
}
/// Determines whether the sound is stopped, playing, or fading
#[derive(Debug, Deserialize, PartialEq)]
enum PlayState {
Playing,
Stopped,
FadingOut,
FadingIn,
}
/// Provides methods to control music playback
pub struct MusicMgr {
/// Collection of all the tracks
soundtrack: SoundtrackCollection,
/// Instant at which the current track began playing
began_playing: Instant,
next_track_change: f64,
/// Time until the next track should be played
next_track_change: f32,
/// The title of the last track played. Used to prevent a track
/// being played twice in a row
last_track: String,
}
impl MusicMgr {
#[allow(clippy::new_without_default)] // TODO: Pending review in #587
pub fn new() -> Self {
impl Default for MusicMgr {
fn default() -> Self {
Self {
soundtrack: Self::load_soundtrack_items(),
began_playing: Instant::now(),
@ -92,26 +122,43 @@ impl MusicMgr {
last_track: String::from("None"),
}
}
}
impl MusicMgr {
/// Checks whether the previous track has completed. If so, sends a
/// request to play the next (random) track
pub fn maintain(&mut self, audio: &mut AudioFrontend, state: &State) {
pub fn maintain(&mut self, audio: &mut AudioFrontend, state: &State, client: &Client) {
//if let Some(current_chunk) = client.current_chunk() {
//println!("biome: {:?}", current_chunk.meta().biome());
//println!("chaos: {}", current_chunk.meta().chaos());
//println!("alt: {}", current_chunk.meta().alt());
//println!("tree_density: {}",
// current_chunk.meta().tree_density());
//if let Some(position) = client.current::<comp::Pos>() {
// player_alt = position.0.z;
//}
if audio.music_enabled()
&& !self.soundtrack.tracks.is_empty()
&& self.began_playing.elapsed().as_secs_f64() > self.next_track_change
&& self.began_playing.elapsed().as_secs_f32() > self.next_track_change
{
self.play_random_track(audio, state);
self.play_random_track(audio, state, client);
}
}
fn play_random_track(&mut self, audio: &mut AudioFrontend, state: &State) {
const SILENCE_BETWEEN_TRACKS_SECONDS: f64 = 45.0;
fn play_random_track(&mut self, audio: &mut AudioFrontend, state: &State, client: &Client) {
let mut rng = thread_rng();
// Adds a bit of randomness between plays
let silence_between_tracks_seconds: f32 = rng.gen_range(45.0, 120.0);
let game_time = (state.get_time_of_day() as u64 % 86400) as u32;
let current_period_of_day = Self::get_current_day_period(game_time);
let mut rng = thread_rng();
let current_biome = client.current_biome();
let current_site = client.current_site();
let maybe_track = self
// Filters out tracks not matching the timing, site, and biome
let maybe_tracks = self
.soundtrack
.tracks
.iter()
@ -121,15 +168,52 @@ impl MusicMgr {
Some(period_of_day) => period_of_day == &current_period_of_day,
None => true,
}
&& match &track.site {
Some(site) => site == &current_site,
None => true,
}
})
.choose(&mut rng);
.filter(|track| {
let mut result = false;
if !track.biomes.is_empty() {
for biome in track.biomes.iter() {
if biome.0 == current_biome {
result = true;
}
}
} else {
result = true;
}
result
})
.collect::<Vec<&SoundtrackItem>>();
if let Some(track) = maybe_track {
// Randomly selects a track from the remaining tracks weighted based
// on the biome
let new_maybe_track = maybe_tracks.choose_weighted(&mut rng, |track| {
let mut chance = 0;
if !track.biomes.is_empty() {
for biome in track.biomes.iter() {
if biome.0 == current_biome {
chance = biome.1;
}
}
} else {
// If no biome is listed, the song is still added to the
// rotation to allow for site specific songs to play
// in any biome
chance = 1;
}
chance
});
if let Ok(track) = new_maybe_track {
//println!("Now playing {:?}", track.title);
self.last_track = String::from(&track.title);
self.began_playing = Instant::now();
self.next_track_change = track.length + SILENCE_BETWEEN_TRACKS_SECONDS;
self.next_track_change = track.length + silence_between_tracks_seconds;
audio.play_exploration_music(&track.path);
audio.play_music(&track.path, MusicChannelTag::Exploration);
}
}

View File

@ -0,0 +1,240 @@
/// EventMapper::Block watches the sound emitting blocks within
/// chunk range of the player and emits ambient sfx
use crate::{
audio::sfx::{SfxEvent, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR},
scene::{terrain::BlocksOfInterest, Camera, Terrain},
AudioFrontend,
};
use super::EventMapper;
use client::Client;
use common::{
comp::Pos,
spiral::Spiral2d,
state::State,
terrain::TerrainChunk,
vol::{ReadVol, RectRasterableVol},
};
use hashbrown::HashMap;
use rand::{thread_rng, Rng};
use std::time::Instant;
use vek::*;
#[derive(Clone, PartialEq)]
struct PreviousBlockState {
event: SfxEvent,
time: Instant,
}
impl Default for PreviousBlockState {
fn default() -> Self {
Self {
event: SfxEvent::Idle,
time: Instant::now(),
}
}
}
pub struct BlockEventMapper {
history: HashMap<Vec3<i32>, PreviousBlockState>,
}
impl EventMapper for BlockEventMapper {
fn maintain(
&mut self,
audio: &mut AudioFrontend,
state: &State,
player_entity: specs::Entity,
camera: &Camera,
triggers: &SfxTriggers,
terrain: &Terrain<TerrainChunk>,
client: &Client,
) {
let focus_off = camera.get_focus_pos().map(f32::trunc);
let cam_pos = camera.dependents().cam_pos + focus_off;
// Get the player position and chunk
let player_pos = state
.read_component_copied::<Pos>(player_entity)
.unwrap_or_default();
let player_chunk = player_pos.0.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
(e.floor() as i32).div_euclid(sz as i32)
});
// For determining if underground
let terrain_alt = match client.current_chunk() {
Some(chunk) => chunk.meta().alt(),
None => 0.0,
};
struct BlockSounds<'a> {
// The function to select the blocks of interest that we should emit from
blocks: fn(&'a BlocksOfInterest) -> &'a [Vec3<i32>],
// The range, in chunks, that the particles should be generated in from the player
range: usize,
// The sound of the generated particle
sfx: SfxEvent,
// The volume of the sfx
volume: f32,
// Condition that must be true to play
cond: fn(&State) -> bool,
}
let sounds: &[BlockSounds] = &[
BlockSounds {
blocks: |boi| &boi.leaves,
range: 1,
sfx: SfxEvent::Birdcall,
volume: 1.0,
cond: |st| st.get_day_period().is_light(),
},
BlockSounds {
blocks: |boi| &boi.leaves,
range: 1,
sfx: SfxEvent::Owl,
volume: 1.0,
cond: |st| st.get_day_period().is_dark(),
},
BlockSounds {
blocks: |boi| &boi.river,
range: 1,
sfx: SfxEvent::RunningWater,
volume: 1.0,
cond: |_| true,
},
//BlockSounds {
// blocks: |boi| &boi.embers,
// range: 1,
// sfx: SfxEvent::Embers,
// volume: 0.15,
// //volume: 0.05,
// cond: |_| true,
// //cond: |st| st.get_day_period().is_dark(),
//},
BlockSounds {
blocks: |boi| &boi.reeds,
range: 1,
sfx: SfxEvent::Frog,
volume: 0.8,
cond: |st| st.get_day_period().is_dark(),
},
//BlockSounds {
// blocks: |boi| &boi.flowers,
// range: 4,
// sfx: SfxEvent::LevelUp,
// volume: 1.0,
// cond: |st| st.get_day_period().is_dark(),
//},
BlockSounds {
blocks: |boi| &boi.grass,
range: 1,
sfx: SfxEvent::Cricket,
volume: 0.5,
cond: |st| st.get_day_period().is_dark(),
},
BlockSounds {
blocks: |boi| &boi.beehives,
range: 1,
sfx: SfxEvent::Bees,
volume: 1.0,
cond: |st| st.get_day_period().is_light(),
},
];
// Iterate through each kind of block of interest
for sounds in sounds.iter() {
// If the timing condition is false, continue
// or if the player is far enough underground, continue
// TODO Address bird hack properly. See TODO on line 171
if !(sounds.cond)(state)
|| player_pos.0.z < (terrain_alt - 30.0)
|| ((sounds.sfx == SfxEvent::Birdcall || sounds.sfx == SfxEvent::Owl)
&& thread_rng().gen_bool(0.995))
{
continue;
}
// For chunks surrounding the player position
for offset in Spiral2d::new().take((sounds.range * 2 + 1).pow(2)) {
let chunk_pos = player_chunk + offset;
// Get all the blocks of interest in this chunk
terrain.get(chunk_pos).map(|chunk_data| {
// Get the positions of the blocks of type sounds
let blocks = (sounds.blocks)(&chunk_data.blocks_of_interest);
let absolute_pos: Vec3<i32> =
Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
// Iterate through each individual block
for block in blocks {
// TODO Address this hack properly, potentially by making a new
// block of interest type which picks fewer leaf blocks
// Hack to reduce the number of bird sounds (too many leaf blocks)
if (sounds.sfx == SfxEvent::Birdcall || sounds.sfx == SfxEvent::Owl)
&& thread_rng().gen_bool(0.999)
{
continue;
}
let block_pos: Vec3<i32> = absolute_pos + block;
let internal_state = self.history.entry(block_pos).or_default();
let block_pos = block_pos.map(|x| x as f32);
if Self::should_emit(internal_state, triggers.get_key_value(&sounds.sfx)) {
// If the camera is within SFX distance
if (block_pos.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR {
let underwater = state
.terrain()
.get(cam_pos.map(|e| e.floor() as i32))
.map(|b| b.is_liquid())
.unwrap_or(false);
let sfx_trigger_item = triggers.get_key_value(&sounds.sfx);
audio.emit_sfx(
sfx_trigger_item,
block_pos,
Some(sounds.volume),
underwater,
);
}
internal_state.time = Instant::now();
internal_state.event = sounds.sfx.clone();
}
}
});
}
}
}
}
impl BlockEventMapper {
pub fn new() -> Self {
Self {
history: HashMap::new(),
}
}
/// Ensures that:
/// 1. An sfx.ron entry exists for an SFX event
/// 2. The sfx has not been played since it's timeout threshold has elapsed,
/// which prevents firing every tick
/// Note that with so many blocks to choose from and different blocks being
/// selected each time, this is not perfect, but does reduce the number of
/// plays from blocks that have already emitted sfx and are stored in the
/// BlockEventMapper history.
fn should_emit(
previous_state: &PreviousBlockState,
sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
) -> bool {
if let Some((event, item)) = sfx_trigger_item {
if &previous_state.event == event {
previous_state.time.elapsed().as_secs_f32() >= item.threshold
} else {
true
}
} else {
false
}
}
}

View File

@ -0,0 +1,128 @@
/// EventMapper::Campfire maps sfx to campfires
use crate::{
audio::sfx::{SfxEvent, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR},
scene::{Camera, Terrain},
AudioFrontend,
};
use super::EventMapper;
use client::Client;
use common::{
comp::{object, Body, Pos},
state::State,
terrain::TerrainChunk,
vol::ReadVol,
};
use hashbrown::HashMap;
use specs::{Entity as EcsEntity, Join, WorldExt};
use std::time::{Duration, Instant};
#[derive(Clone)]
struct PreviousEntityState {
event: SfxEvent,
time: Instant,
}
impl Default for PreviousEntityState {
fn default() -> Self {
Self {
event: SfxEvent::Idle,
time: Instant::now(),
}
}
}
pub struct CampfireEventMapper {
event_history: HashMap<EcsEntity, PreviousEntityState>,
}
impl EventMapper for CampfireEventMapper {
fn maintain(
&mut self,
audio: &mut AudioFrontend,
state: &State,
player_entity: specs::Entity,
camera: &Camera,
triggers: &SfxTriggers,
_terrain: &Terrain<TerrainChunk>,
_client: &Client,
) {
let ecs = state.ecs();
let focus_off = camera.get_focus_pos().map(f32::trunc);
let cam_pos = camera.dependents().cam_pos + focus_off;
for (entity, body, pos) in (
&ecs.entities(),
&ecs.read_storage::<Body>(),
&ecs.read_storage::<Pos>(),
)
.join()
.filter(|(_, _, e_pos)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR)
{
if let Body::Object(object::Body::CampfireLit) = body {
let internal_state = self.event_history.entry(entity).or_default();
let mapped_event = SfxEvent::Campfire;
// Check for SFX config entry for this movement
if Self::should_emit(internal_state, triggers.get_key_value(&mapped_event)) {
let underwater = state
.terrain()
.get(cam_pos.map(|e| e.floor() as i32))
.map(|b| b.is_liquid())
.unwrap_or(false);
let sfx_trigger_item = triggers.get_key_value(&mapped_event);
audio.emit_sfx(sfx_trigger_item, pos.0, None, underwater);
internal_state.time = Instant::now();
}
// update state to determine the next event. We only record the time (above) if
// it was dispatched
internal_state.event = mapped_event;
}
}
self.cleanup(player_entity);
}
}
impl CampfireEventMapper {
pub fn new() -> Self {
Self {
event_history: HashMap::new(),
}
}
/// As the player explores the world, we track the last event of the nearby
/// entities to determine the correct SFX item to play next based on
/// their activity. `cleanup` will remove entities from event tracking if
/// they have not triggered an event for > n seconds. This prevents
/// stale records from bloating the Map size.
fn cleanup(&mut self, player: EcsEntity) {
const TRACKING_TIMEOUT: u64 = 10;
let now = Instant::now();
self.event_history.retain(|entity, event| {
now.duration_since(event.time) < Duration::from_secs(TRACKING_TIMEOUT)
|| entity.id() == player.id()
});
}
/// Ensures that:
/// 1. An sfx.ron entry exists for an SFX event
/// 2. The sfx has not been played since it's timeout threshold has elapsed,
/// which prevents firing every tick
fn should_emit(
previous_state: &PreviousEntityState,
sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
) -> bool {
if let Some((event, item)) = sfx_trigger_item {
if &previous_state.event == event {
previous_state.time.elapsed().as_secs_f32() >= item.threshold
} else {
true
}
} else {
false
}
}
}

View File

@ -1,16 +1,19 @@
/// EventMapper::Combat watches the combat states of surrounding entities' and
/// emits sfx related to weapons and attacks/abilities
use crate::{
audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR},
scene::Camera,
audio::sfx::{SfxEvent, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR},
scene::{Camera, Terrain},
AudioFrontend,
};
use super::EventMapper;
use client::Client;
use common::{
comp::{item::ItemKind, CharacterAbilityType, CharacterState, Loadout, Pos},
event::EventBus,
state::State,
terrain::TerrainChunk,
vol::ReadVol,
};
use hashbrown::HashMap;
use specs::{Entity as EcsEntity, Join, WorldExt};
@ -40,16 +43,16 @@ pub struct CombatEventMapper {
impl EventMapper for CombatEventMapper {
fn maintain(
&mut self,
audio: &mut AudioFrontend,
state: &State,
player_entity: specs::Entity,
camera: &Camera,
triggers: &SfxTriggers,
_terrain: &Terrain<TerrainChunk>,
_client: &Client,
) {
let ecs = state.ecs();
let sfx_event_bus = ecs.read_resource::<EventBus<SfxEventItem>>();
let mut sfx_emitter = sfx_event_bus.emitter();
let focus_off = camera.get_focus_pos().map(f32::trunc);
let cam_pos = camera.dependents().cam_pos + focus_off;
@ -63,21 +66,27 @@ impl EventMapper for CombatEventMapper {
.filter(|(_, e_pos, ..)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR)
{
if let Some(character) = character {
let state = self.event_history.entry(entity).or_default();
let sfx_state = self.event_history.entry(entity).or_default();
let mapped_event = Self::map_event(character, state, loadout);
let mapped_event = Self::map_event(character, sfx_state, loadout);
// Check for SFX config entry for this movement
if Self::should_emit(state, triggers.get_key_value(&mapped_event)) {
sfx_emitter.emit(SfxEventItem::new(mapped_event.clone(), Some(pos.0), None));
if Self::should_emit(sfx_state, triggers.get_key_value(&mapped_event)) {
let underwater = state
.terrain()
.get(cam_pos.map(|e| e.floor() as i32))
.map(|b| b.is_liquid())
.unwrap_or(false);
state.time = Instant::now();
let sfx_trigger_item = triggers.get_key_value(&mapped_event);
audio.emit_sfx(sfx_trigger_item, pos.0, None, underwater);
sfx_state.time = Instant::now();
}
// update state to determine the next event. We only record the time (above) if
// it was dispatched
state.event = mapped_event;
state.weapon_drawn = Self::weapon_drawn(character);
sfx_state.event = mapped_event;
sfx_state.weapon_drawn = Self::weapon_drawn(character);
}
}
@ -117,7 +126,7 @@ impl CombatEventMapper {
) -> bool {
if let Some((event, item)) = sfx_trigger_item {
if &previous_state.event == event {
previous_state.time.elapsed().as_secs_f64() >= item.threshold
previous_state.time.elapsed().as_secs_f32() >= item.threshold
} else {
true
}

View File

@ -1,23 +1,32 @@
mod block;
mod campfire;
mod combat;
mod movement;
mod progression;
use common::state::State;
use client::Client;
use common::{state::State, terrain::TerrainChunk};
use block::BlockEventMapper;
use campfire::CampfireEventMapper;
use combat::CombatEventMapper;
use movement::MovementEventMapper;
use progression::ProgressionEventMapper;
use super::SfxTriggers;
use crate::scene::Camera;
use crate::{
scene::{Camera, Terrain},
AudioFrontend,
};
trait EventMapper {
fn maintain(
&mut self,
audio: &mut AudioFrontend,
state: &State,
player_entity: specs::Entity,
camera: &Camera,
triggers: &SfxTriggers,
terrain: &Terrain<TerrainChunk>,
client: &Client,
);
}
@ -31,20 +40,32 @@ impl SfxEventMapper {
mappers: vec![
Box::new(CombatEventMapper::new()),
Box::new(MovementEventMapper::new()),
Box::new(ProgressionEventMapper::new()),
Box::new(BlockEventMapper::new()),
Box::new(CampfireEventMapper::new()),
],
}
}
pub fn maintain(
&mut self,
audio: &mut AudioFrontend,
state: &State,
player_entity: specs::Entity,
camera: &Camera,
triggers: &SfxTriggers,
terrain: &Terrain<TerrainChunk>,
client: &Client,
) {
for mapper in &mut self.mappers {
mapper.maintain(state, player_entity, camera, triggers);
mapper.maintain(
audio,
state,
player_entity,
camera,
triggers,
terrain,
client,
);
}
}
}

View File

@ -3,13 +3,16 @@
/// proportionate to the extity's size
use super::EventMapper;
use crate::{
audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR},
scene::Camera,
audio::sfx::{SfxEvent, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR},
scene::{Camera, Terrain},
AudioFrontend,
};
use client::Client;
use common::{
comp::{Body, CharacterState, PhysicsState, Pos, Vel},
event::EventBus,
state::State,
terrain::{BlockKind, TerrainChunk},
vol::ReadVol,
};
use hashbrown::HashMap;
use specs::{Entity as EcsEntity, Join, WorldExt};
@ -21,6 +24,7 @@ struct PreviousEntityState {
event: SfxEvent,
time: Instant,
on_ground: bool,
in_water: bool,
}
impl Default for PreviousEntityState {
@ -29,6 +33,7 @@ impl Default for PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
on_ground: true,
in_water: false,
}
}
}
@ -40,16 +45,16 @@ pub struct MovementEventMapper {
impl EventMapper for MovementEventMapper {
fn maintain(
&mut self,
audio: &mut AudioFrontend,
state: &State,
player_entity: specs::Entity,
camera: &Camera,
triggers: &SfxTriggers,
_terrain: &Terrain<TerrainChunk>,
_client: &Client,
) {
let ecs = state.ecs();
let sfx_event_bus = ecs.read_resource::<EventBus<SfxEventItem>>();
let mut sfx_emitter = sfx_event_bus.emitter();
let focus_off = camera.get_focus_pos().map(f32::trunc);
let cam_pos = camera.dependents().cam_pos + focus_off;
@ -65,34 +70,59 @@ impl EventMapper for MovementEventMapper {
.filter(|(_, e_pos, ..)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR)
{
if let Some(character) = character {
let state = self.event_history.entry(entity).or_default();
let internal_state = self.event_history.entry(entity).or_default();
// Get the underfoot block
let block_position = Vec3::new(pos.0.x, pos.0.y, pos.0.z - 1.0).map(|x| x as i32);
let underfoot_block_kind = match state.get_block(block_position) {
Some(block) => block.kind(),
None => BlockKind::Air,
};
let mapped_event = match body {
Body::Humanoid(_) => Self::map_movement_event(character, physics, state, vel.0),
Body::QuadrupedMedium(_)
| Body::QuadrupedSmall(_)
| Body::QuadrupedLow(_)
| Body::BirdMedium(_)
| Body::BirdSmall(_)
| Body::BipedLarge(_) => Self::map_non_humanoid_movement_event(physics, vel.0),
Body::Humanoid(_) => Self::map_movement_event(
character,
physics,
internal_state,
vel.0,
underfoot_block_kind,
),
Body::QuadrupedMedium(_) | Body::QuadrupedSmall(_) | Body::QuadrupedLow(_) => {
Self::map_quadruped_movement_event(physics, vel.0, underfoot_block_kind)
},
Body::BirdMedium(_) | Body::BirdSmall(_) | Body::BipedLarge(_) => {
Self::map_non_humanoid_movement_event(physics, vel.0, underfoot_block_kind)
},
_ => SfxEvent::Idle, // Ignore fish, etc...
};
// Check for SFX config entry for this movement
if Self::should_emit(state, triggers.get_key_value(&mapped_event)) {
sfx_emitter.emit(SfxEventItem::new(
mapped_event.clone(),
Some(pos.0),
Some(Self::get_volume_for_body_type(body)),
));
if Self::should_emit(internal_state, triggers.get_key_value(&mapped_event)) {
let underwater = state
.terrain()
.get(cam_pos.map(|e| e.floor() as i32))
.map(|b| b.is_liquid())
.unwrap_or(false);
state.time = Instant::now();
let sfx_trigger_item = triggers.get_key_value(&mapped_event);
audio.emit_sfx(
sfx_trigger_item,
pos.0,
Some(Self::get_volume_for_body_type(body)),
underwater,
);
internal_state.time = Instant::now();
}
// update state to determine the next event. We only record the time (above) if
// it was dispatched
state.event = mapped_event;
state.on_ground = physics.on_ground;
internal_state.event = mapped_event;
internal_state.on_ground = physics.on_ground;
if physics.in_liquid.is_some() {
internal_state.in_water = true;
} else {
internal_state.in_water = false;
}
}
}
@ -133,7 +163,7 @@ impl MovementEventMapper {
) -> bool {
if let Some((event, item)) = sfx_trigger_item {
if &previous_state.event == event {
previous_state.time.elapsed().as_secs_f64() >= item.threshold
previous_state.time.elapsed().as_secs_f32() >= item.threshold
} else {
true
}
@ -153,9 +183,14 @@ impl MovementEventMapper {
physics_state: &PhysicsState,
previous_state: &PreviousEntityState,
vel: Vec3<f32>,
underfoot_block_kind: BlockKind,
) -> SfxEvent {
// Match run / roll state
if physics_state.on_ground && vel.magnitude() > 0.1
// Match run / roll / swim state
if physics_state.in_liquid.is_some() && vel.magnitude() > 0.1
|| !previous_state.in_water && physics_state.in_liquid.is_some()
{
return SfxEvent::Swim;
} else if physics_state.on_ground && vel.magnitude() > 0.1
|| !previous_state.on_ground && physics_state.on_ground
{
return if matches!(character_state, CharacterState::Roll(_)) {
@ -163,7 +198,10 @@ impl MovementEventMapper {
} else if matches!(character_state, CharacterState::Sneak) {
SfxEvent::Sneak
} else {
SfxEvent::Run
match underfoot_block_kind {
BlockKind::Snow => SfxEvent::SnowRun,
_ => SfxEvent::Run,
}
};
}
@ -183,9 +221,36 @@ impl MovementEventMapper {
}
/// Maps a limited set of movements for other non-humanoid entities
fn map_non_humanoid_movement_event(physics_state: &PhysicsState, vel: Vec3<f32>) -> SfxEvent {
if physics_state.on_ground && vel.magnitude() > 0.1 {
SfxEvent::Run
fn map_non_humanoid_movement_event(
physics_state: &PhysicsState,
vel: Vec3<f32>,
underfoot_block_kind: BlockKind,
) -> SfxEvent {
if physics_state.in_liquid.is_some() && vel.magnitude() > 0.1 {
SfxEvent::Swim
} else if physics_state.on_ground && vel.magnitude() > 0.1 {
match underfoot_block_kind {
BlockKind::Snow => SfxEvent::SnowRun,
_ => SfxEvent::Run,
}
} else {
SfxEvent::Idle
}
}
/// Maps a limited set of movements for quadruped entities
fn map_quadruped_movement_event(
physics_state: &PhysicsState,
vel: Vec3<f32>,
underfoot_block_kind: BlockKind,
) -> SfxEvent {
if physics_state.in_liquid.is_some() && vel.magnitude() > 0.1 {
SfxEvent::Swim
} else if physics_state.on_ground && vel.magnitude() > 0.1 {
match underfoot_block_kind {
BlockKind::Snow => SfxEvent::QuadSnowRun,
_ => SfxEvent::QuadRun,
}
} else {
SfxEvent::Idle
}

View File

@ -5,6 +5,7 @@ use common::{
bird_small, humanoid, quadruped_medium, quadruped_small, Body, CharacterState, PhysicsState,
},
states,
terrain::BlockKind,
};
use std::time::{Duration, Instant};
@ -28,6 +29,7 @@ fn config_but_played_since_threshold_no_emit() {
event: SfxEvent::Run,
time: Instant::now(),
on_ground: true,
in_water: false,
};
let result =
@ -47,6 +49,7 @@ fn config_and_not_played_since_threshold_emits() {
event: SfxEvent::Idle,
time: Instant::now().checked_add(Duration::from_secs(1)).unwrap(),
on_ground: true,
in_water: false,
};
let result =
@ -68,6 +71,7 @@ fn same_previous_event_elapsed_emits() {
.checked_sub(Duration::from_millis(500))
.unwrap(),
on_ground: true,
in_water: false,
};
let result =
@ -88,8 +92,10 @@ fn maps_idle() {
event: SfxEvent::Idle,
time: Instant::now(),
on_ground: true,
in_water: false,
},
Vec3::zero(),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Idle);
@ -107,8 +113,10 @@ fn maps_run_with_sufficient_velocity() {
event: SfxEvent::Idle,
time: Instant::now(),
on_ground: true,
in_water: false,
},
Vec3::new(0.5, 0.8, 0.0),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Run);
@ -126,8 +134,10 @@ fn does_not_map_run_with_insufficient_velocity() {
event: SfxEvent::Idle,
time: Instant::now(),
on_ground: true,
in_water: false,
},
Vec3::new(0.02, 0.0001, 0.0),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Idle);
@ -142,8 +152,10 @@ fn does_not_map_run_with_sufficient_velocity_but_not_on_ground() {
event: SfxEvent::Idle,
time: Instant::now(),
on_ground: false,
in_water: false,
},
Vec3::new(0.5, 0.8, 0.0),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Idle);
@ -173,8 +185,10 @@ fn maps_roll() {
event: SfxEvent::Run,
time: Instant::now(),
on_ground: true,
in_water: false,
},
Vec3::new(0.5, 0.5, 0.0),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Roll);
@ -192,8 +206,10 @@ fn maps_land_on_ground_to_run() {
event: SfxEvent::Idle,
time: Instant::now(),
on_ground: false,
in_water: false,
},
Vec3::zero(),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Run);
@ -208,8 +224,10 @@ fn maps_glider_open() {
event: SfxEvent::Jump,
time: Instant::now(),
on_ground: false,
in_water: false,
},
Vec3::zero(),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::GliderOpen);
@ -224,8 +242,10 @@ fn maps_glide() {
event: SfxEvent::Glide,
time: Instant::now(),
on_ground: false,
in_water: false,
},
Vec3::zero(),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Glide);
@ -240,8 +260,10 @@ fn maps_glider_close_when_closing_mid_flight() {
event: SfxEvent::Glide,
time: Instant::now(),
on_ground: false,
in_water: false,
},
Vec3::zero(),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::GliderClose);
@ -260,8 +282,10 @@ fn maps_glider_close_when_landing() {
event: SfxEvent::Glide,
time: Instant::now(),
on_ground: false,
in_water: false,
},
Vec3::zero(),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::GliderClose);
@ -275,6 +299,7 @@ fn maps_quadrupeds_running() {
..Default::default()
},
Vec3::new(0.5, 0.8, 0.0),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Run);

View File

@ -1,82 +0,0 @@
/// EventMapper::Progress watches the player entity's stats
/// and triggers sfx for gaining experience and levelling up
use super::EventMapper;
use crate::{
audio::sfx::{SfxEvent, SfxEventItem, SfxTriggers},
scene::Camera,
};
use common::{comp::Stats, event::EventBus, state::State};
use specs::WorldExt;
#[derive(Clone, PartialEq)]
struct ProgressionState {
level: u32,
exp: u32,
}
impl Default for ProgressionState {
fn default() -> Self { Self { level: 1, exp: 0 } }
}
pub struct ProgressionEventMapper {
state: ProgressionState,
}
impl EventMapper for ProgressionEventMapper {
#[allow(clippy::op_ref)] // TODO: Pending review in #587
fn maintain(
&mut self,
state: &State,
player_entity: specs::Entity,
_camera: &Camera,
triggers: &SfxTriggers,
) {
let ecs = state.ecs();
let next_state = ecs.read_storage::<Stats>().get(player_entity).map_or(
ProgressionState::default(),
|stats| ProgressionState {
level: stats.level.level(),
exp: stats.exp.current(),
},
);
if &self.state != &next_state {
if let Some(mapped_event) = self.map_event(&next_state) {
let sfx_trigger_item = triggers.get_trigger(&mapped_event);
if sfx_trigger_item.is_some() {
ecs.read_resource::<EventBus<SfxEventItem>>()
.emit_now(SfxEventItem::at_player_position(mapped_event));
}
}
self.state = next_state;
}
}
}
impl ProgressionEventMapper {
pub fn new() -> Self {
Self {
state: ProgressionState::default(),
}
}
#[allow(clippy::let_and_return)] // TODO: Pending review in #587
fn map_event(&mut self, next_state: &ProgressionState) -> Option<SfxEvent> {
let sfx_event = if next_state.level > self.state.level {
Some(SfxEvent::LevelUp)
} else if next_state.exp > self.state.exp {
Some(SfxEvent::ExperienceGained)
} else {
None
};
sfx_event
}
}
#[cfg(test)] mod tests;

View File

@ -1,43 +0,0 @@
use super::*;
use crate::audio::sfx::SfxEvent;
#[test]
fn no_change_returns_none() {
let mut mapper = ProgressionEventMapper::new();
let next_client_state = ProgressionState::default();
assert_eq!(mapper.map_event(&next_client_state), None);
}
#[test]
fn change_level_returns_levelup() {
let mut mapper = ProgressionEventMapper::new();
let next_client_state = ProgressionState { level: 2, exp: 0 };
assert_eq!(
mapper.map_event(&next_client_state),
Some(SfxEvent::LevelUp)
);
}
#[test]
fn change_exp_returns_expup() {
let mut mapper = ProgressionEventMapper::new();
let next_client_state = ProgressionState { level: 1, exp: 100 };
assert_eq!(
mapper.map_event(&next_client_state),
Some(SfxEvent::ExperienceGained)
);
}
#[test]
fn level_up_and_gained_exp_prioritises_levelup() {
let mut mapper = ProgressionEventMapper::new();
let next_client_state = ProgressionState { level: 2, exp: 100 };
assert_eq!(
mapper.map_event(&next_client_state),
Some(SfxEvent::LevelUp)
);
}

View File

@ -82,24 +82,27 @@
mod event_mapper;
use crate::{audio::AudioFrontend, scene::Camera};
use crate::{
audio::AudioFrontend,
scene::{Camera, Terrain},
};
use client::Client;
use common::{
assets,
comp::{
item::{ItemKind, ToolKind},
object, Body, CharacterAbilityType, InventoryUpdateEvent,
},
event::EventBus,
outcome::Outcome,
state::State,
terrain::TerrainChunk,
};
use event_mapper::SfxEventMapper;
use hashbrown::HashMap;
use rand::prelude::*;
use serde::Deserialize;
use specs::WorldExt;
use tracing::{debug, warn};
use tracing::warn;
use vek::*;
/// We watch the states of nearby entities in order to emit SFX at their
@ -110,8 +113,11 @@ use vek::*;
const SFX_DIST_LIMIT_SQR: f32 = 20000.0;
pub struct SfxEventItem {
/// The SFX event that triggers this sound
pub sfx: SfxEvent,
/// The position at which the sound should play
pub pos: Option<Vec3<f32>>,
/// The volume to play the sound at
pub vol: Option<f32>,
}
@ -131,8 +137,20 @@ impl SfxEventItem {
#[derive(Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
pub enum SfxEvent {
Campfire,
Embers,
Birdcall,
Owl,
Cricket,
Frog,
Bees,
RunningWater,
Idle,
Swim,
Run,
QuadRun,
SnowRun,
QuadSnowRun,
Roll,
Sneak,
Climb,
@ -155,6 +173,7 @@ pub enum SfxEvent {
pub enum SfxInventoryEvent {
Collected,
CollectedTool(ToolKind),
CollectedItem(String),
CollectFailed,
Consumed(String),
Debug,
@ -163,16 +182,23 @@ pub enum SfxInventoryEvent {
Swapped,
}
// TODO Move to a separate event mapper?
impl From<&InventoryUpdateEvent> for SfxEvent {
fn from(value: &InventoryUpdateEvent) -> Self {
match value {
InventoryUpdateEvent::Collected(item) => {
// Handle sound effects for types of collected items, falling back to the
// default Collected event
// Handle sound effects for types of collected items, falling
// back to the default Collected event
match &item.kind() {
ItemKind::Tool(tool) => {
SfxEvent::Inventory(SfxInventoryEvent::CollectedTool(tool.kind.clone()))
},
ItemKind::Ingredient { kind } => match &kind[..] {
"ShinyGem" => {
SfxEvent::Inventory(SfxInventoryEvent::CollectedItem(kind.clone()))
},
_ => SfxEvent::Inventory(SfxInventoryEvent::Collected),
},
_ => SfxEvent::Inventory(SfxInventoryEvent::Collected),
}
},
@ -193,8 +219,10 @@ impl From<&InventoryUpdateEvent> for SfxEvent {
#[derive(Deserialize)]
pub struct SfxTriggerItem {
/// A list of SFX filepaths for this event
pub files: Vec<String>,
pub threshold: f64,
/// The time to wait before repeating this SfxEvent
pub threshold: f32,
}
#[derive(Deserialize, Default)]
@ -209,70 +237,51 @@ impl SfxTriggers {
}
pub struct SfxMgr {
triggers: SfxTriggers,
pub triggers: SfxTriggers,
event_mapper: SfxEventMapper,
}
impl SfxMgr {
#[allow(clippy::new_without_default)] // TODO: Pending review in #587
pub fn new() -> Self {
impl Default for SfxMgr {
fn default() -> Self {
Self {
triggers: Self::load_sfx_items(),
event_mapper: SfxEventMapper::new(),
}
}
}
impl SfxMgr {
pub fn maintain(
&mut self,
audio: &mut AudioFrontend,
state: &State,
player_entity: specs::Entity,
camera: &Camera,
terrain: &Terrain<TerrainChunk>,
client: &Client,
) {
// Checks if the SFX volume is set to zero or audio is disabled
// This prevents us from running all the following code unnecessarily
if !audio.sfx_enabled() {
return;
}
let ecs = state.ecs();
let focus_off = camera.get_focus_pos().map(f32::trunc);
let cam_pos = camera.dependents().cam_pos + focus_off;
// Sets the listener position to the camera position facing the
// same direction as the camera
audio.set_listener_pos(cam_pos, camera.dependents().cam_dir);
// TODO: replace; deprecated in favor of outcomes
self.event_mapper
.maintain(state, player_entity, camera, &self.triggers);
// TODO: replace; deprecated in favor of outcomes
let events = ecs.read_resource::<EventBus<SfxEventItem>>().recv_all();
for event in events {
let position = match event.pos {
Some(pos) => pos,
_ => cam_pos,
};
if let Some(item) = self.triggers.get_trigger(&event.sfx) {
let sfx_file = match item.files.len() {
0 => {
debug!("Sfx event {:?} is missing audio file.", event.sfx);
"voxygen.audio.sfx.placeholder"
},
1 => item
.files
.last()
.expect("Failed to determine sound file for this trigger item."),
_ => {
let rand_step = rand::random::<usize>() % item.files.len();
&item.files[rand_step]
},
};
audio.play_sfx(sfx_file, position, event.vol);
} else {
debug!("Missing sfx trigger config for sfx event. {:?}", event.sfx);
}
}
self.event_mapper.maintain(
audio,
state,
player_entity,
camera,
&self.triggers,
terrain,
client,
);
}
pub fn handle_outcome(&mut self, outcome: &Outcome, audio: &mut AudioFrontend) {
@ -304,7 +313,11 @@ impl SfxMgr {
audio.play_sfx(file_ref, *pos, None);
},
Body::Object(object::Body::BoltFire | object::Body::BoltFireBig) => {
Body::Object(
object::Body::BoltFire
| object::Body::BoltFireBig
| object::Body::BoltNature,
) => {
let file_ref = vec![
"voxygen.audio.sfx.abilities.fire_shot_1",
"voxygen.audio.sfx.abilities.fire_shot_2",
@ -317,6 +330,19 @@ impl SfxMgr {
},
}
},
Outcome::LevelUp { pos } => {
let file_ref = "voxygen.audio.sfx.character.level_up_sound_-_shorter_wind_up";
audio.play_sfx(file_ref, *pos, None);
},
Outcome::Beam { pos, heal } => {
if *heal {
let file_ref = "voxygen.audio.sfx.abilities.staff_channeling";
audio.play_sfx(file_ref, *pos, None);
} else {
let file_ref = "voxygen.audio.sfx.abilities.flame_thrower";
audio.play_sfx(file_ref, *pos, None);
}
},
}
}

View File

@ -2,7 +2,7 @@ pub mod comp;
pub mod sys;
use crate::audio::sfx::SfxEventItem;
use common::event::EventBus;
use common::{event::EventBus, outcome::Outcome};
use specs::{Entity, World, WorldExt};
#[derive(Copy, Clone, Debug)]
@ -27,6 +27,7 @@ pub fn init(world: &mut World) {
world.register::<comp::HpFloaterList>();
world.register::<comp::Interpolated>();
world.insert(MyExpFloaterList::default());
world.insert(Vec::<Outcome>::new());
// Voxygen event buses
world.insert(EventBus::<SfxEventItem>::default());

View File

@ -319,7 +319,7 @@ pub enum Event {
AdjustFigureLoDRenderDistance(u32),
AdjustMusicVolume(f32),
AdjustSfxVolume(f32),
ChangeAudioDevice(String),
//ChangeAudioDevice(String),
ChangeMaxFPS(u32),
ChangeFOV(u16),
ChangeGamma(f32),
@ -2131,9 +2131,9 @@ impl Hud {
settings_window::Event::MaximumFPS(max_fps) => {
events.push(Event::ChangeMaxFPS(max_fps));
},
settings_window::Event::ChangeAudioDevice(name) => {
events.push(Event::ChangeAudioDevice(name));
},
//settings_window::Event::ChangeAudioDevice(name) => {
// events.push(Event::ChangeAudioDevice(name));
//},
settings_window::Event::CrosshairType(crosshair_type) => {
events.push(Event::CrosshairType(crosshair_type));
},

View File

@ -291,7 +291,7 @@ pub enum Event {
ChangeRenderMode(Box<RenderMode>),
AdjustMusicVolume(f32),
AdjustSfxVolume(f32),
ChangeAudioDevice(String),
//ChangeAudioDevice(String),
MaximumFPS(u32),
CrosshairTransp(f32),
CrosshairType(CrosshairType),
@ -2693,30 +2693,32 @@ impl<'a> Widget for SettingsWindow<'a> {
events.push(Event::AdjustSfxVolume(new_val));
}
// Audio Device Selector --------------------------------------------
let device = &self.global_state.audio.device;
let device_list = &self.global_state.audio.device_list;
Text::new(&self.localized_strings.get("hud.settings.audio_device"))
.down_from(state.ids.sfx_volume_slider, 10.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.audio_device_text, ui);
// Audio Device Selector
// --------------------------------------------
// let device = &self.global_state.audio.device;
//let device_list = &self.global_state.audio.device_list;
//Text::new(&self.localized_strings.get("hud.settings.audio_device"
// )) .down_from(state.ids.sfx_volume_slider, 10.0)
// .font_size(self.fonts.cyri.scale(14))
// .font_id(self.fonts.cyri.conrod_id)
// .color(TEXT_COLOR)
// .set(state.ids.audio_device_text, ui);
// Get which device is currently selected
let selected = device_list.iter().position(|x| x.contains(device));
//// Get which device is currently selected
//let selected = device_list.iter().position(|x|
// x.contains(device));
if let Some(clicked) = DropDownList::new(&device_list, selected)
.w_h(400.0, 22.0)
.color(MENU_BG)
.label_color(TEXT_COLOR)
.label_font_id(self.fonts.opensans.conrod_id)
.down_from(state.ids.audio_device_text, 10.0)
.set(state.ids.audio_device_list, ui)
{
let new_val = device_list[clicked].clone();
events.push(Event::ChangeAudioDevice(new_val));
}
//if let Some(clicked) = DropDownList::new(&device_list, selected)
// .w_h(400.0, 22.0)
// .color(MENU_BG)
// .label_color(TEXT_COLOR)
// .label_font_id(self.fonts.opensans.conrod_id)
// .down_from(state.ids.audio_device_text, 10.0)
// .set(state.ids.audio_device_list, ui)
//{
// let new_val = device_list[clicked].clone();
// events.push(Event::ChangeAudioDevice(new_val));
//}
}
// 5) Languages Tab -----------------------------------

View File

@ -4,12 +4,12 @@
#![recursion_limit = "2048"]
use veloren_voxygen::{
audio::{self, AudioFrontend},
audio::AudioFrontend,
i18n::{self, i18n_asset_key, Localization},
logging,
profile::Profile,
run,
settings::{AudioOutput, Settings},
settings::Settings,
window::Window,
GlobalState,
};
@ -142,13 +142,14 @@ fn main() {
anim::init();
// Setup audio
let mut audio = match settings.audio.output {
AudioOutput::Off => None,
AudioOutput::Automatic => audio::get_default_device(),
AudioOutput::Device(ref dev) => Some(dev.clone()),
}
.map(|dev| AudioFrontend::new(dev, settings.audio.max_sfx_channels))
.unwrap_or_else(AudioFrontend::no_audio);
//let mut audio = match settings.audio.output {
// AudioOutput::Off => None,
// AudioOutput::Automatic => audio::get_default_device(),
// AudioOutput::Device(ref dev) => Some(dev.clone()),
//}
//.map(|dev| AudioFrontend::new(dev, settings.audio.max_sfx_channels))
//.unwrap_or_else(AudioFrontend::no_audio);
let mut audio = AudioFrontend::new(settings.audio.max_sfx_channels);
audio.set_music_volume(settings.audio.music_volume);
audio.set_sfx_volume(settings.audio.sfx_volume);

View File

@ -14,7 +14,7 @@ pub use self::{
terrain::Terrain,
};
use crate::{
audio::{music::MusicMgr, sfx::SfxMgr, AudioFrontend},
audio::{ambient::AmbientMgr, music::MusicMgr, sfx::SfxMgr, AudioFrontend},
render::{
create_clouds_mesh, create_pp_mesh, create_skybox_mesh, CloudsLocals, CloudsPipeline,
Consts, GlobalModel, Globals, Light, LodData, Model, PostProcessLocals,
@ -101,8 +101,9 @@ pub struct Scene {
particle_mgr: ParticleMgr,
figure_mgr: FigureMgr,
sfx_mgr: SfxMgr,
pub sfx_mgr: SfxMgr,
music_mgr: MusicMgr,
ambient_mgr: AmbientMgr,
}
pub struct SceneData<'a> {
@ -306,8 +307,9 @@ impl Scene {
light_data: Vec::new(),
particle_mgr: ParticleMgr::new(renderer),
figure_mgr: FigureMgr::new(renderer),
sfx_mgr: SfxMgr::new(),
music_mgr: MusicMgr::new(),
sfx_mgr: SfxMgr::default(),
music_mgr: MusicMgr::default(),
ambient_mgr: AmbientMgr::default(),
}
}
@ -431,6 +433,7 @@ impl Scene {
fadeout: |timeout| timeout * 2.0,
}),
Outcome::ProjectileShot { .. } => {},
_ => {},
}
}
@ -441,6 +444,7 @@ impl Scene {
renderer: &mut Renderer,
audio: &mut AudioFrontend,
scene_data: &SceneData,
client: &Client,
) {
span!(_guard, "maintain", "Scene::maintain");
// Get player position.
@ -993,8 +997,12 @@ impl Scene {
scene_data.state,
scene_data.player_entity,
&self.camera,
&self.terrain,
client,
);
self.music_mgr.maintain(audio, scene_data.state);
self.music_mgr.maintain(audio, scene_data.state, client);
self.ambient_mgr
.maintain(audio, scene_data.state, client, &self.camera);
}
/// Render the scene using the provided `Renderer`.

View File

@ -124,6 +124,7 @@ impl ParticleMgr {
}
},
Outcome::ProjectileShot { .. } => {},
_ => {},
}
}

View File

@ -9,6 +9,7 @@ use vek::*;
pub struct BlocksOfInterest {
pub leaves: Vec<Vec3<i32>>,
pub grass: Vec<Vec3<i32>>,
pub river: Vec<Vec3<i32>>,
pub embers: Vec<Vec3<i32>>,
pub beehives: Vec<Vec3<i32>>,
pub reeds: Vec<Vec3<i32>>,
@ -23,6 +24,7 @@ impl BlocksOfInterest {
span!(_guard, "from_chunk", "BlocksOfInterest::from_chunk");
let mut leaves = Vec::new();
let mut grass = Vec::new();
let mut river = Vec::new();
let mut embers = Vec::new();
let mut beehives = Vec::new();
let mut reeds = Vec::new();
@ -50,6 +52,11 @@ impl BlocksOfInterest {
grass.push(pos)
}
},
BlockKind::Water => {
if chunk.meta().contains_river() && thread_rng().gen_range(0, 16) == 0 {
river.push(pos)
}
},
_ => match block.get_sprite() {
Some(SpriteKind::Ember) => embers.push(pos),
Some(SpriteKind::Beehive) => beehives.push(pos),
@ -71,6 +78,7 @@ impl BlocksOfInterest {
Self {
leaves,
grass,
river,
embers,
beehives,
reeds,

View File

@ -1,5 +1,5 @@
use crate::{
audio::sfx::{SfxEvent, SfxEventItem},
audio::sfx::SfxEvent,
ecs::MyEntity,
hud::{DebugInfo, Event as HudEvent, Hud, HudInfo, PressBehavior},
i18n::{i18n_asset_key, Localization},
@ -7,7 +7,7 @@ use crate::{
menu::char_selection::CharSelectionState,
render::Renderer,
scene::{camera, CameraMode, Scene, SceneData},
settings::{AudioOutput, ControlSettings, Settings},
settings::{ControlSettings, Settings},
window::{AnalogGameInput, Event, GameInput},
Direction, Error, GlobalState, PlayState, PlayStateResult,
};
@ -17,7 +17,6 @@ use common::{
comp,
comp::{ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel},
consts::{MAX_MOUNT_RANGE, MAX_PICKUP_RANGE},
event::EventBus,
msg::PresenceKind,
outcome::Outcome,
span,
@ -121,12 +120,12 @@ impl SessionState {
self.hud.new_message(m);
},
client::Event::InventoryUpdated(inv_event) => {
let sfx_event = SfxEvent::from(&inv_event);
client
.state()
.ecs()
.read_resource::<EventBus<SfxEventItem>>()
.emit_now(SfxEventItem::at_player_position(sfx_event));
let sfx_trigger_item = self
.scene
.sfx_mgr
.triggers
.get_key_value(&SfxEvent::from(&inv_event));
global_state.audio.emit_sfx_item(sfx_trigger_item);
match inv_event {
InventoryUpdateEvent::CollectFailed => {
@ -901,12 +900,12 @@ impl PlayState for SessionState {
global_state.settings.audio.sfx_volume = sfx_volume;
global_state.settings.save_to_file_warn();
},
HudEvent::ChangeAudioDevice(name) => {
global_state.audio.set_device(name.clone());
//HudEvent::ChangeAudioDevice(name) => {
// global_state.audio.set_device(name.clone());
global_state.settings.audio.output = AudioOutput::Device(name);
global_state.settings.save_to_file_warn();
},
// global_state.settings.audio.output = AudioOutput::Device(name);
// global_state.settings.save_to_file_warn();
//},
HudEvent::ChangeMaxFPS(fps) => {
global_state.settings.graphics.max_fps = fps;
global_state.settings.save_to_file_warn();
@ -1077,6 +1076,7 @@ impl PlayState for SessionState {
global_state.window.renderer_mut(),
&mut global_state.audio,
&scene_data,
&client,
);
// Process outcomes from client

View File

@ -654,7 +654,6 @@ pub enum AudioOutput {
// If this option is disabled, functions in the rodio
// library MUST NOT be called.
Off,
Device(String),
#[serde(other)]
Automatic,
}

View File

@ -69,6 +69,7 @@ impl<'a> BlockGen<'a> {
// temp,
// humidity,
stone_col,
snow_cover,
..
} = sample;
@ -128,7 +129,10 @@ impl<'a> BlockGen<'a> {
let col = Lerp::lerp(sub_surface_color, surface_color, grass_factor);
// Surface
Some(Block::new(
if grass_factor > 0.7 {
if snow_cover {
//if temp < CONFIG.snow_temp + 0.031 {
BlockKind::Snow
} else if grass_factor > 0.7 {
BlockKind::Grass
} else {
BlockKind::Earth

View File

@ -155,7 +155,14 @@ impl World {
},
};
let meta = TerrainChunkMeta::new(sim_chunk.get_name(&self.sim), sim_chunk.get_biome());
let meta = TerrainChunkMeta::new(
sim_chunk.get_name(&self.sim),
sim_chunk.get_biome(),
sim_chunk.alt,
sim_chunk.tree_density,
sim_chunk.cave.1.alt != 0.0,
sim_chunk.river.is_river(),
);
let mut chunk = TerrainChunk::new(base_z, stone, air, meta);

View File

@ -2303,14 +2303,20 @@ impl SimChunk {
pub fn get_biome(&self) -> BiomeKind {
if self.alt < CONFIG.sea_level {
BiomeKind::Ocean
} else if self.chaos > 0.6 {
BiomeKind::Mountain
} else if self.temp > CONFIG.desert_temp {
BiomeKind::Desert
} else if (self.temp - 0.5) < 0.005 && self.humidity < 0.1 {
BiomeKind::Lake
} else if self.temp < CONFIG.snow_temp {
BiomeKind::Snowlands
} else if self.tree_density > 0.65 {
BiomeKind::Snowland
} else if self.alt > 450.0 && self.chaos > 0.3 && self.tree_density < 0.6 {
BiomeKind::Mountain
} else if self.temp > CONFIG.desert_temp && self.humidity < 0.6 {
BiomeKind::Desert
//} else if self.tree_density > 0.65 && self.humidity > 0.7 && self.temp > 0.8 {
// BiomeKind::Jungle
} else if self.tree_density > 0.5 {
BiomeKind::Forest
//} else if self.humidity > 0.8 {
// BiomeKind::Swamp
} else {
BiomeKind::Grassland
}