diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fadccdab1..628f68ae7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added dragging and right-click to use functionality to inventory, armor & hotbar slots - Added capes, lanterns, tabards, rings, helmets & necklaces as equippable armor - 6 new music tracks +- Added basic world and civilisation simulation +- Added overhauled towns +- Added fields, crops and scarecrows +- Added paths +- Added bridges +- Added procedural house generation +- Added lampposts +- Added NPCs that spawn in towns +- Added simple dungeons +- Added sub-voxel noise effect +- Added waypoints next to dungeons +- Made players spawn in towns +- Added non-uniform block heights ### Changed diff --git a/Cargo.lock b/Cargo.lock index 98cf25c4af..4ebcf41d72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2589,17 +2589,23 @@ dependencies = [ [[package]] name = "minifb" -version = "0.14.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799c20458eb0dd69f48cea5014afe736b363818c86d7ca61dbb56e1c0585014c" +checksum = "b18d2987dac6afdd7f6d81101a3b422b7da3e6799d7f11863ad006d8ccd562b2" dependencies = [ "cast", "cc", "orbclient", "raw-window-handle", + "tempfile", "time", + "wayland-client 0.25.0", + "wayland-cursor", + "wayland-protocols 0.25.0", "winapi 0.3.8", "x11-dl", + "xkb", + "xkbcommon-sys", ] [[package]] @@ -2728,6 +2734,19 @@ dependencies = [ "void", ] +[[package]] +name = "nix" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "void", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -3947,6 +3966,12 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + [[package]] name = "scoped_threadpool" version = "0.1.9" @@ -4406,6 +4431,20 @@ dependencies = [ "remove_dir_all", ] +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if", + "libc", + "rand 0.7.3", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.8", +] + [[package]] name = "term" version = "0.5.2" @@ -5217,6 +5256,22 @@ dependencies = [ "wayland-sys 0.23.6", ] +[[package]] +name = "wayland-client" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a42cb608953ec8e132c7f53fde722cca9bfbf8b2071d685dbbb8df2b567fee8b" +dependencies = [ + "bitflags", + "downcast-rs", + "libc", + "nix 0.17.0", + "scoped-tls", + "wayland-commons 0.25.0", + "wayland-scanner 0.25.0", + "wayland-sys 0.25.0", +] + [[package]] name = "wayland-commons" version = "0.21.13" @@ -5237,6 +5292,28 @@ dependencies = [ "wayland-sys 0.23.6", ] +[[package]] +name = "wayland-commons" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8caa2f106138cf71358c6a9e84468e4406069cec93cbd6dbfce92225fc175932" +dependencies = [ + "nix 0.17.0", + "once_cell", + "smallvec 1.2.0", + "wayland-sys 0.25.0", +] + +[[package]] +name = "wayland-cursor" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d774f69a6a4a9eac6d1a29cea45a4750ee7f997520421b2068f099a11b4cbba" +dependencies = [ + "wayland-client 0.25.0", + "wayland-sys 0.25.0", +] + [[package]] name = "wayland-protocols" version = "0.21.13" @@ -5262,6 +5339,18 @@ dependencies = [ "wayland-scanner 0.23.6", ] +[[package]] +name = "wayland-protocols" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f784a990d5fa6d846fa93eb8d3bb744ff1e6ec60c7f785b0a0ee2f1a1f20bee9" +dependencies = [ + "bitflags", + "wayland-client 0.25.0", + "wayland-commons 0.25.0", + "wayland-scanner 0.25.0", +] + [[package]] name = "wayland-scanner" version = "0.21.13" @@ -5284,6 +5373,17 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "wayland-scanner" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f45ddc08a8078f3efa96b5f413268cc9c53b30712891de081fbc1d5846fbc736" +dependencies = [ + "proc-macro2 1.0.9", + "quote 1.0.3", + "xml-rs", +] + [[package]] name = "wayland-sys" version = "0.21.13" @@ -5304,6 +5404,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "wayland-sys" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80f9fc64f9045ad5ff491886a9460437655353e8be73c1b3f29f569342553319" +dependencies = [ + "dlib", +] + [[package]] name = "web-sys" version = "0.3.36" @@ -5467,6 +5576,27 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" +[[package]] +name = "xkb" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aec02bc5de902aa579f3d2f2c522edaf40fa42963cbaffe645b058ddcc68fdb2" +dependencies = [ + "bitflags", + "libc", + "xkbcommon-sys", +] + +[[package]] +name = "xkbcommon-sys" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa434980dca02ebf28795d71e570dbb78316d095a228707efd6117bf8246d78b" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "xml-rs" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index f4a8194459..98990c57d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,10 @@ members = [ # default profile for devs, fast to compile, okay enough to run, no debug information [profile.dev] opt-level = 2 -overflow-checks = true +overflow-checks = true debug-assertions = true panic = "abort" -debug = false +debug = false codegen-units = 8 lto = false incremental = true diff --git a/assets/common/items/coconut.ron b/assets/common/items/coconut.ron new file mode 100644 index 0000000000..219af26991 --- /dev/null +++ b/assets/common/items/coconut.ron @@ -0,0 +1,13 @@ +Item( + name: "Coconut", + description: "Reliable source of water and fat. + +Restores 30 health.", + kind: Consumable( + kind: Coconut, + effect: Health(( + amount: 30, + cause: Item, + )), + ), +) diff --git a/assets/voxygen/background/bg_10.png b/assets/voxygen/background/bg_10.png new file mode 100644 index 0000000000..8d27d84d83 --- /dev/null +++ b/assets/voxygen/background/bg_10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d34cd8aa7655efae1ab92b86dfa6916560eeed22d60e7ce45de6ff4d77e4340 +size 1175394 diff --git a/assets/voxygen/background/bg_11.png b/assets/voxygen/background/bg_11.png new file mode 100644 index 0000000000..3aa49f8c2d --- /dev/null +++ b/assets/voxygen/background/bg_11.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cc0e614d52708cba30d71f658f0c290395371d4333f2f6a866c207a8f471878 +size 1301002 diff --git a/assets/voxygen/element/icons/item_coconut.png b/assets/voxygen/element/icons/item_coconut.png new file mode 100644 index 0000000000..69f5f9046e --- /dev/null +++ b/assets/voxygen/element/icons/item_coconut.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0307232e80cbbc31cf5ec9d03483cbd0851334caffe09d3a4d7bcb71a77e5097 +size 344 diff --git a/assets/voxygen/item_image_manifest.ron b/assets/voxygen/item_image_manifest.ron index 58dde6e4f9..a2c33550b6 100644 --- a/assets/voxygen/item_image_manifest.ron +++ b/assets/voxygen/item_image_manifest.ron @@ -272,9 +272,13 @@ (0.0, 0.0, 0.0), (-90.0, 180.0, 0.0), 1.0, ), // Consumables - Consumable(Apple): VoxTrans( + Consumable(Apple): + VoxTrans( "element.icons.item_apple", (0.0, 0.0, 0.0), (-90.0, 90.0, 0.0), 1.0, + ), + Consumable(Coconut): Png( + "element.icons.item_coconut", ), Consumable(PotionMinor): VoxTrans( "voxel.object.potion_red", diff --git a/assets/voxygen/shaders/figure-frag.glsl b/assets/voxygen/shaders/figure-frag.glsl index df2439e47d..ee5d6d5d47 100644 --- a/assets/voxygen/shaders/figure-frag.glsl +++ b/assets/voxygen/shaders/figure-frag.glsl @@ -4,6 +4,7 @@ in vec3 f_pos; in vec3 f_col; +in float f_ao; flat in vec3 f_norm; layout (std140) @@ -39,6 +40,12 @@ void main() { vec3 point_light = light_at(f_pos, f_norm); light += point_light; diffuse_light += point_light; + + float ao = pow(f_ao, 0.5) * 0.85 + 0.15; + + ambient_light *= ao; + diffuse_light *= ao; + vec3 surf_color = illuminate(srgb_to_linear(model_col.rgb * f_col), light, diffuse_light, ambient_light); float fog_level = fog(f_pos.xyz, focus_pos.xyz, medium.x); @@ -48,7 +55,7 @@ void main() { if ((flags & 1) == 1 && int(cam_mode) == 1) { float distance = distance(vec3(cam_pos), vec3(model_mat * vec4(vec3(0), 1))) - 2; - + float opacity = clamp(distance / distance_divider, 0, 1); if(threshold_matrix[int(gl_FragCoord.x) % 4][int(gl_FragCoord.y) % 4] > opacity) { diff --git a/assets/voxygen/shaders/figure-vert.glsl b/assets/voxygen/shaders/figure-vert.glsl index c42e5d68bc..b9fa68bf15 100644 --- a/assets/voxygen/shaders/figure-vert.glsl +++ b/assets/voxygen/shaders/figure-vert.glsl @@ -5,6 +5,7 @@ in vec3 v_pos; in vec3 v_norm; in vec3 v_col; +in float v_ao; in uint v_bone_idx; layout (std140) @@ -27,6 +28,7 @@ uniform u_bones { out vec3 f_pos; out vec3 f_col; +out float f_ao; flat out vec3 f_norm; void main() { @@ -39,6 +41,8 @@ void main() { f_col = v_col; + f_ao = v_ao; + // Calculate normal here rather than for each pixel in the fragment shader f_norm = normalize(( combined_mat * diff --git a/assets/voxygen/shaders/fluid-frag/cheap.glsl b/assets/voxygen/shaders/fluid-frag/cheap.glsl index f3c92ab429..e56796d1d5 100644 --- a/assets/voxygen/shaders/fluid-frag/cheap.glsl +++ b/assets/voxygen/shaders/fluid-frag/cheap.glsl @@ -48,9 +48,9 @@ void main() { vec4 clouds; vec3 fog_color = get_sky_color(normalize(f_pos - cam_pos.xyz), time_of_day.x, cam_pos.xyz, f_pos, 0.25, true, clouds); - float passthrough = pow(dot(faceforward(f_norm, f_norm, cam_to_frag), -cam_to_frag), 0.5); + float passthrough = dot(faceforward(f_norm, f_norm, cam_to_frag), -cam_to_frag); - vec4 color = mix(vec4(surf_color, 1.0), vec4(surf_color, 1.0 / (1.0 + diffuse_light * 0.25)), passthrough); + vec4 color = mix(vec4(surf_color, 1.0), vec4(surf_color, 1.0 / (1.0 + diffuse_light)), passthrough); - tgt_color = mix(mix(color, vec4(fog_color, 0.0), fog_level), vec4(clouds.rgb, 0.0), clouds.a); + tgt_color = mix(color, vec4(fog_color, 0.0), 0.0); } diff --git a/assets/voxygen/shaders/fluid-frag/shiny.glsl b/assets/voxygen/shaders/fluid-frag/shiny.glsl index 8d06ee468c..f6692cbeb9 100644 --- a/assets/voxygen/shaders/fluid-frag/shiny.glsl +++ b/assets/voxygen/shaders/fluid-frag/shiny.glsl @@ -28,30 +28,33 @@ vec3 warp_normal(vec3 norm, vec3 pos, float time) { } float wave_height(vec3 pos) { + float timer = tick.x * 0.75; + + pos *= 0.5; vec3 big_warp = ( - texture(t_waves, fract(pos.xy * 0.03 + tick.x * 0.01)).xyz * 0.5 + - texture(t_waves, fract(pos.yx * 0.03 - tick.x * 0.01)).xyz * 0.5 + + texture(t_waves, fract(pos.xy * 0.03 + timer * 0.01)).xyz * 0.5 + + texture(t_waves, fract(pos.yx * 0.03 - timer * 0.01)).xyz * 0.5 + vec3(0) ); vec3 warp = ( - texture(t_noise, fract(pos.yx * 0.1 + tick.x * 0.02)).xyz * 0.3 + - texture(t_noise, fract(pos.yx * 0.1 - tick.x * 0.02)).xyz * 0.3 + + texture(t_noise, fract(pos.yx * 0.1 + timer * 0.02)).xyz * 0.3 + + texture(t_noise, fract(pos.yx * 0.1 - timer * 0.02)).xyz * 0.3 + vec3(0) ); float height = ( - (texture(t_noise, pos.xy * 0.03 + big_warp.xy + tick.x * 0.05).y - 0.5) * 1.0 + - (texture(t_noise, pos.yx * 0.03 + big_warp.yx - tick.x * 0.05).y - 0.5) * 1.0 + - (texture(t_waves, pos.xy * 0.1 + warp.xy + tick.x * 0.1).x - 0.5) * 0.5 + - (texture(t_waves, pos.yx * 0.1 + warp.yx - tick.x * 0.1).x - 0.5) * 0.5 + - (texture(t_noise, pos.yx * 0.3 + warp.xy * 0.5 + tick.x * 0.1).x - 0.5) * 0.2 + - (texture(t_noise, pos.yx * 0.3 + warp.yx * 0.5 - tick.x * 0.1).x - 0.5) * 0.2 + - (texture(t_noise, pos.yx * 1.0 + warp.yx * 0.0 - tick.x * 0.1).x - 0.5) * 0.05 + + (texture(t_noise, pos.xy * 0.03 + big_warp.xy + timer * 0.05).y - 0.5) * 1.0 + + (texture(t_noise, pos.yx * 0.03 + big_warp.yx - timer * 0.05).y - 0.5) * 1.0 + + (texture(t_waves, pos.xy * 0.1 + warp.xy + timer * 0.1).x - 0.5) * 0.5 + + (texture(t_waves, pos.yx * 0.1 + warp.yx - timer * 0.1).x - 0.5) * 0.5 + + (texture(t_noise, pos.yx * 0.3 + warp.xy * 0.5 + timer * 0.1).x - 0.5) * 0.2 + + (texture(t_noise, pos.yx * 0.3 + warp.yx * 0.5 - timer * 0.1).x - 0.5) * 0.2 + + (texture(t_noise, pos.yx * 1.0 + warp.yx * 0.0 - timer * 0.1).x - 0.5) * 0.05 + 0.0 ); - return pow(abs(height), 0.5) * sign(height) * 5.5; + return pow(abs(height), 0.5) * sign(height) * 10.5; } void main() { @@ -89,9 +92,9 @@ void main() { 0.1 / slope ); - nmap = mix(vec3(0, 0, 1), normalize(nmap), min(1.0 / pow(frag_dist, 0.75), 1)); + nmap = mix(f_norm, normalize(nmap), min(1.0 / pow(frag_dist, 0.75), 1)); - vec3 norm = f_norm * nmap.z + b_norm * nmap.x + c_norm * nmap.y; + vec3 norm = vec3(0, 0, 1) * nmap.z + b_norm * nmap.x + c_norm * nmap.y; vec3 light, diffuse_light, ambient_light; get_sun_diffuse(norm, time_of_day.x, light, diffuse_light, ambient_light, 0.0); @@ -101,7 +104,6 @@ void main() { vec3 point_light = light_at(f_pos, norm); light += point_light; diffuse_light += point_light; - vec3 surf_color = srgb_to_linear(vec3(0.2, 0.5, 1.0)) * light * diffuse_light * ambient_light; float fog_level = fog(f_pos.xyz, focus_pos.xyz, medium.x); vec4 clouds; @@ -114,11 +116,11 @@ void main() { vec4 _clouds; vec3 reflect_color = get_sky_color(reflect_ray_dir, time_of_day.x, f_pos, vec3(-100000), 0.25, false, _clouds) * f_light; // Tint - reflect_color = mix(reflect_color, surf_color, 0.6); + reflect_color = reflect_color * 0.5 * (diffuse_light + ambient_light); // 0 = 100% reflection, 1 = translucent water - float passthrough = pow(dot(faceforward(f_norm, f_norm, cam_to_frag), -cam_to_frag), 0.5); + float passthrough = dot(faceforward(f_norm, f_norm, cam_to_frag), -cam_to_frag); - vec4 color = mix(vec4(reflect_color * 2.0, 1.0), vec4(surf_color, 1.0 / (1.0 + diffuse_light * 0.25)), passthrough); + vec4 color = mix(vec4(reflect_color, 1.0), vec4(vec3(0), 1.0 / (1.0 + diffuse_light * 0.25)), passthrough); tgt_color = mix(mix(color, vec4(fog_color, 0.0), fog_level), vec4(clouds.rgb, 0.0), clouds.a); } diff --git a/assets/voxygen/shaders/fluid-vert.glsl b/assets/voxygen/shaders/fluid-vert.glsl index 412b46bd80..52907b1ef6 100644 --- a/assets/voxygen/shaders/fluid-vert.glsl +++ b/assets/voxygen/shaders/fluid-vert.glsl @@ -18,17 +18,16 @@ flat out vec3 f_norm; out vec3 f_col; out float f_light; +const float EXTRA_NEG_Z = 65536.0; + void main() { - f_pos = vec3( - float((v_pos_norm >> 0) & 0x00FFu), - float((v_pos_norm >> 8) & 0x00FFu), - float((v_pos_norm >> 16) & 0x1FFFu) - ) + model_offs; - f_pos.z *= min(1.0001 - 0.02 / pow(tick.x - load_time, 10.0), 1.0); + f_pos = vec3((uvec3(v_pos_norm) >> uvec3(0, 6, 12)) & uvec3(0x3Fu, 0x3Fu, 0x1FFFFu)) - vec3(0, 0, EXTRA_NEG_Z) + model_offs; + f_pos.z -= 250.0 * (1.0 - min(1.0001 - 0.02 / pow(tick.x - load_time, 10.0), 1.0)); f_pos.z -= 25.0 * pow(distance(focus_pos.xy, f_pos.xy) / view_distance.x, 20.0); // Small waves - f_pos.z -= 0.05 + 0.05 * (sin(tick.x * 2.0 + f_pos.x * 2.0 + f_pos.y * 2.0) + 1.0) * 0.5; + f_pos.xy += 0.01; // Avoid z-fighting + f_pos.z -= 0.1 + 0.1 * (sin(tick.x * 2.0 + f_pos.x * 2.0 + f_pos.y * 2.0) + 1.0) * 0.5; f_col = vec3( float((v_col_light >> 8) & 0xFFu), diff --git a/assets/voxygen/shaders/include/cloud/regular.glsl b/assets/voxygen/shaders/include/cloud/regular.glsl index 285a5d3646..a40c700e01 100644 --- a/assets/voxygen/shaders/include/cloud/regular.glsl +++ b/assets/voxygen/shaders/include/cloud/regular.glsl @@ -1,9 +1,9 @@ uniform sampler2D t_noise; const float CLOUD_AVG_HEIGHT = 1025.0; -const float CLOUD_HEIGHT_MIN = CLOUD_AVG_HEIGHT - 50.0; -const float CLOUD_HEIGHT_MAX = CLOUD_AVG_HEIGHT + 50.0; -const float CLOUD_THRESHOLD = 0.25; +const float CLOUD_HEIGHT_MIN = CLOUD_AVG_HEIGHT - 60.0; +const float CLOUD_HEIGHT_MAX = CLOUD_AVG_HEIGHT + 60.0; +const float CLOUD_THRESHOLD = 0.27; const float CLOUD_SCALE = 5.0; const float CLOUD_DENSITY = 100.0; @@ -15,27 +15,24 @@ vec2 cloud_at(vec3 pos) { vec2 scaled_pos = pos.xy / CLOUD_SCALE; float tick_offs = 0.0 - + texture(t_noise, scaled_pos * 0.0005 - time_of_day.x * 0.00002).x * 0.5 - + texture(t_noise, scaled_pos * 0.000015).x * 5.0; + + texture(t_noise, scaled_pos * 0.0005 - time_of_day.x * 0.00001).x * 0.5 + + texture(t_noise, scaled_pos * 0.0015).x * 0.15; float value = ( 0.0 + texture(t_noise, scaled_pos * 0.0003 + tick_offs).x + texture(t_noise, scaled_pos * 0.0015 - tick_offs * 2.0).x * 0.5 - //+ texture(t_noise, scaled_pos * 0.0025 - time_of_day.x * 0.0002).x * 0.25 - //+ texture(t_noise, scaled_pos * 0.008 + time_of_day.x * 0.0004).x * 0.15 - //+ texture(t_noise, scaled_pos * 0.02 + tick_offs + time_of_day.x * 0.0004).x * 0.2 ) / 3.0; value += (0.0 - + texture(t_noise, scaled_pos * 0.008 + time_of_day.x * 0.0004).x * 0.25 - + texture(t_noise, scaled_pos * 0.02 + tick_offs + time_of_day.x * 0.0004).x * 0.15 + + texture(t_noise, scaled_pos * 0.008 + time_of_day.x * 0.0002).x * 0.25 + + texture(t_noise, scaled_pos * 0.02 + tick_offs + time_of_day.x * 0.0002).x * 0.15 ) * value; - float density = max((value - CLOUD_THRESHOLD) - abs(pos.z - CLOUD_AVG_HEIGHT) / 400.0, 0.0) * CLOUD_DENSITY; + float density = max((value - CLOUD_THRESHOLD) - abs(pos.z - CLOUD_AVG_HEIGHT) / 200.0, 0.0) * CLOUD_DENSITY; const float SHADE_GRADIENT = 1.5 / (CLOUD_AVG_HEIGHT - CLOUD_HEIGHT_MIN); - float shade = ((pos.z - CLOUD_AVG_HEIGHT) / (CLOUD_HEIGHT_MAX - CLOUD_HEIGHT_MIN)) * 2.5 + 0.7; + float shade = (pos.z - CLOUD_AVG_HEIGHT) / (CLOUD_HEIGHT_MAX - CLOUD_HEIGHT_MIN) * 5.0 + 0.3; return vec2(shade, density / (1.0 + vsum(abs(pos - cam_pos.xyz)) / 5000)); } @@ -67,7 +64,7 @@ vec4 get_cloud_color(vec3 dir, vec3 origin, float time_of_day, float max_dist, f cloud_shade = mix(cloud_shade, sample.x, passthrough * integral); dist += INCR * delta; - if (passthrough < 0.05 || (passthrough > 0.8 && dist > (maxd + mind) * 0.7)) { + if (passthrough < 0.1) { break; } } diff --git a/assets/voxygen/shaders/include/light.glsl b/assets/voxygen/shaders/include/light.glsl index 1496dd2d42..ee0920ccc6 100644 --- a/assets/voxygen/shaders/include/light.glsl +++ b/assets/voxygen/shaders/include/light.glsl @@ -25,7 +25,9 @@ vec3 illuminate(vec3 color, vec3 light, vec3 diffuse, vec3 ambience) { } float attenuation_strength(vec3 rpos) { - return 1.0 / pow(rpos.x * rpos.x + rpos.y * rpos.y + rpos.z * rpos.z, 0.6); + // This is not how light attenuation works at all, but it produces visually pleasing and mechanically useful properties + float d2 = rpos.x * rpos.x + rpos.y * rpos.y + rpos.z * rpos.z; + return max(2.0 / pow(d2 + 10, 0.35) - pow(d2 / 50000.0, 0.8), 0.0); } vec3 light_at(vec3 wpos, vec3 wnorm) { @@ -43,7 +45,7 @@ vec3 light_at(vec3 wpos, vec3 wnorm) { // Pre-calculate difference between light and fragment vec3 difference = light_pos - wpos; - float strength = pow(attenuation_strength(difference), 0.6); + float strength = attenuation_strength(difference); // Multiply the vec3 only once vec3 color = srgb_to_linear(L.light_col.rgb) * (strength * L.light_col.a); diff --git a/assets/voxygen/shaders/include/random.glsl b/assets/voxygen/shaders/include/random.glsl index b1f0e18a21..240d17e50a 100644 --- a/assets/voxygen/shaders/include/random.glsl +++ b/assets/voxygen/shaders/include/random.glsl @@ -1,7 +1,7 @@ float hash(vec4 p) { - p = fract( p*0.3183099+.1); + p = fract(p * 0.3183099 + 0.1); p *= 17.0; - return (fract(p.x*p.y*p.z*p.w*(p.x+p.y+p.z+p.w)) - 0.5) * 2.0; + return (fract(p.x * p.y * p.z * p.w * (p.x + p.y + p.z + p.w)) - 0.5) * 2.0; } float snoise(in vec4 x) { @@ -33,7 +33,7 @@ float snoise(in vec4 x) { } vec3 rand_perm_3(vec3 pos) { - return sin(pos * vec3(1473.7 * pos.z + 472.3, 8891.1 * pos.x + 723.1, 3813.3 * pos.y + 982.5)); + return abs(sin(pos * vec3(1473.7 * pos.z + 472.3, 8891.1 * pos.x + 723.1, 3813.3 * pos.y + 982.5))); } vec4 rand_perm_4(vec4 pos) { diff --git a/assets/voxygen/shaders/include/sky.glsl b/assets/voxygen/shaders/include/sky.glsl index 7ac0dbc6c9..6fd726230b 100644 --- a/assets/voxygen/shaders/include/sky.glsl +++ b/assets/voxygen/shaders/include/sky.glsl @@ -4,8 +4,8 @@ const float PI = 3.141592; -const vec3 SKY_DAY_TOP = vec3(0.1, 0.2, 0.9); -const vec3 SKY_DAY_MID = vec3(0.02, 0.08, 0.8); +const vec3 SKY_DAY_TOP = vec3(0.1, 0.5, 0.9); +const vec3 SKY_DAY_MID = vec3(0.02, 0.28, 0.8); const vec3 SKY_DAY_BOT = vec3(0.1, 0.2, 0.3); const vec3 DAY_LIGHT = vec3(1.2, 1.0, 1.0); const vec3 SUN_HALO_DAY = vec3(0.35, 0.35, 0.0); @@ -187,7 +187,7 @@ vec3 get_sky_color(vec3 dir, float time_of_day, vec3 origin, vec3 f_pos, float q // Clouds clouds = get_cloud_color(dir, origin, time_of_day, f_dist, quality); - clouds.rgb *= get_sun_brightness(sun_dir) * (sun_halo * 1.5 + get_sun_color(sun_dir)) + get_moon_brightness(moon_dir) * (moon_halo * 80.0 + get_moon_color(moon_dir)); + clouds.rgb *= get_sun_brightness(sun_dir) * (sun_halo * 1.5 + get_sun_color(sun_dir)) + get_moon_brightness(moon_dir) * (moon_halo * 80.0 + get_moon_color(moon_dir) + 0.25); if (f_dist > 5000.0) { sky_color += sun_light + moon_light; diff --git a/assets/voxygen/shaders/postprocess-frag.glsl b/assets/voxygen/shaders/postprocess-frag.glsl index cd0c87a772..1a3c12cbbe 100644 --- a/assets/voxygen/shaders/postprocess-frag.glsl +++ b/assets/voxygen/shaders/postprocess-frag.glsl @@ -36,7 +36,6 @@ void main() { uv = clamp(uv + vec2(sin(uv.y * 16.0 + tick.x), sin(uv.x * 24.0 + tick.x)) * 0.005, 0, 1); } - vec4 aa_color = aa_apply(src_color, uv * screen_res.xy, screen_res.xy); //vec4 hsva_color = vec4(rgb2hsv(fxaa_color.rgb), fxaa_color.a); @@ -44,7 +43,7 @@ void main() { //hsva_color.z *= 0.85; //hsva_color.z = 1.0 - 1.0 / (1.0 * hsva_color.z + 1.0); //vec4 final_color = vec4(hsv2rgb(hsva_color.rgb), hsva_color.a); - + vec4 final_color = pow(aa_color, gamma); if (medium.x == 1u) { diff --git a/assets/voxygen/shaders/sprite-frag.glsl b/assets/voxygen/shaders/sprite-frag.glsl index 302bd0faeb..4fd0a975f8 100644 --- a/assets/voxygen/shaders/sprite-frag.glsl +++ b/assets/voxygen/shaders/sprite-frag.glsl @@ -5,6 +5,7 @@ in vec3 f_pos; flat in vec3 f_norm; in vec3 f_col; +in float f_ao; in float f_light; out vec4 tgt_color; @@ -24,6 +25,9 @@ void main() { vec3 point_light = light_at(f_pos, f_norm); light += point_light; diffuse_light += point_light; + float ao = pow(f_ao, 0.5) * 0.85 + 0.15; + ambient_light *= ao; + diffuse_light *= ao; vec3 surf_color = illuminate(f_col, light, diffuse_light, ambient_light); float fog_level = fog(f_pos.xyz, focus_pos.xyz, medium.x); diff --git a/assets/voxygen/shaders/sprite-vert.glsl b/assets/voxygen/shaders/sprite-vert.glsl index 39dbdb003b..45b3d60dea 100644 --- a/assets/voxygen/shaders/sprite-vert.glsl +++ b/assets/voxygen/shaders/sprite-vert.glsl @@ -6,6 +6,7 @@ in vec3 v_pos; in vec3 v_norm; in vec3 v_col; +in float v_ao; in vec4 inst_mat0; in vec4 inst_mat1; in vec4 inst_mat2; @@ -16,6 +17,7 @@ in float inst_wind_sway; out vec3 f_pos; flat out vec3 f_norm; out vec3 f_col; +out float f_ao; out float f_light; const float SCALE = 1.0 / 11.0; @@ -41,6 +43,7 @@ void main() { f_norm = (inst_mat * vec4(v_norm, 0)).xyz; f_col = srgb_to_linear(v_col) * srgb_to_linear(inst_col); + f_ao = v_ao; // Select glowing if (select_pos.w > 0 && select_pos.xyz == floor(sprite_pos)) { diff --git a/assets/voxygen/shaders/terrain-frag.glsl b/assets/voxygen/shaders/terrain-frag.glsl index 069cc01f11..a1d526bd95 100644 --- a/assets/voxygen/shaders/terrain-frag.glsl +++ b/assets/voxygen/shaders/terrain-frag.glsl @@ -1,11 +1,14 @@ #version 330 core #include +#include in vec3 f_pos; +in vec3 f_chunk_pos; flat in uint f_pos_norm; in vec3 f_col; in float f_light; +in float f_ao; layout (std140) uniform u_locals { @@ -29,15 +32,21 @@ void main() { // Use an array to avoid conditional branching vec3 f_norm = normals[(f_pos_norm >> 29) & 0x7u]; + float ao = pow(f_ao, 0.5) * 0.9 + 0.1; + vec3 light, diffuse_light, ambient_light; get_sun_diffuse(f_norm, time_of_day.x, light, diffuse_light, ambient_light, 1.0); float point_shadow = shadow_at(f_pos, f_norm); - diffuse_light *= f_light * point_shadow; - ambient_light *= f_light * point_shadow; + diffuse_light *= point_shadow; + ambient_light *= point_shadow; vec3 point_light = light_at(f_pos, f_norm); light += point_light; - diffuse_light += point_light; - vec3 surf_color = illuminate(srgb_to_linear(f_col), light, diffuse_light, ambient_light); + ambient_light *= f_light * ao; + diffuse_light *= f_light * ao; + diffuse_light += point_light * ao; + + vec3 col = f_col + hash(vec4(floor(f_chunk_pos * 3.0 + 0.5), 0)) * 0.02; // Small-scale noise + vec3 surf_color = illuminate(srgb_to_linear(col), light, diffuse_light, ambient_light); float fog_level = fog(f_pos.xyz, focus_pos.xyz, medium.x); vec4 clouds; diff --git a/assets/voxygen/shaders/terrain-vert.glsl b/assets/voxygen/shaders/terrain-vert.glsl index 52b21fad54..0b4664b24d 100644 --- a/assets/voxygen/shaders/terrain-vert.glsl +++ b/assets/voxygen/shaders/terrain-vert.glsl @@ -13,19 +13,25 @@ uniform u_locals { }; out vec3 f_pos; +out vec3 f_chunk_pos; flat out uint f_pos_norm; out vec3 f_col; out float f_light; +out float f_ao; + +const int EXTRA_NEG_Z = 65536; void main() { - f_pos = vec3((uvec3(v_pos_norm) >> uvec3(0, 8, 16)) & uvec3(0xFFu, 0xFFu, 0x1FFFu)) + model_offs; + f_chunk_pos = vec3(ivec3((uvec3(v_pos_norm) >> uvec3(0, 6, 12)) & uvec3(0x3Fu, 0x3Fu, 0x1FFFFu)) - ivec3(0, 0, EXTRA_NEG_Z)); + f_pos = f_chunk_pos + model_offs; - f_pos.z *= min(1.0001 - 0.02 / pow(tick.x - load_time, 10.0), 1.0); + f_pos.z -= 250.0 * (1.0 - min(1.0001 - 0.02 / pow(tick.x - load_time, 10.0), 1.0)); f_pos.z -= 25.0 * pow(distance(focus_pos.xy, f_pos.xy) / view_distance.x, 20.0); f_col = vec3((uvec3(v_col_light) >> uvec3(8, 16, 24)) & uvec3(0xFFu)) / 255.0; - f_light = float(v_col_light & 0xFFu) / 255.0; + f_light = float(v_col_light & 0x3Fu) / 64.0; + f_ao = float((v_col_light >> 6u) & 3u) / 4.0; f_pos_norm = v_pos_norm; diff --git a/assets/voxygen/voxel/biped_large_lateral_manifest.ron b/assets/voxygen/voxel/biped_large_lateral_manifest.ron index 764872c8ef..31855a5fbd 100644 --- a/assets/voxygen/voxel/biped_large_lateral_manifest.ron +++ b/assets/voxygen/voxel/biped_large_lateral_manifest.ron @@ -9,27 +9,27 @@ lateral: ("npc.giant.male.shoulder_r"), ), hand_l: ( - offset: (-3.0, -2.5, -14.0), + offset: (-2.5, -2.5, -14.0), lateral: ("npc.giant.male.hand_l"), ), hand_r: ( - offset: (-3.0, -2.5, -14.0), + offset: (-2.5, -2.5, -14.0), lateral: ("npc.giant.male.hand_r"), ), leg_l: ( - offset: (-3.0, -3.5, -7.0), + offset: (-6.0, -3.5, -7.0), lateral: ("npc.giant.male.leg_l"), ), leg_r: ( - offset: (-3.0, -3.5, -7.0), + offset: (0.0, -3.5, -7.0), lateral: ("npc.giant.male.leg_r"), ), foot_l: ( - offset: (-3.0, -5.5, -10.5), + offset: (-3.0, -5.0, -2.5), lateral: ("npc.giant.male.foot_l"), ), foot_r: ( - offset: (-3.0, -5.5, -10.5), + offset: (-3.0, -5.0, -2.5), lateral: ("npc.giant.male.foot_r"), ) ), @@ -43,27 +43,27 @@ lateral: ("npc.giant.female.shoulder_r"), ), hand_l: ( - offset: (-3.0, -2.5, -14.0), + offset: (-2.5, -2.5, -14.0), lateral: ("npc.giant.female.hand_l"), ), hand_r: ( - offset: (-3.0, -2.5, -14.0), + offset: (-2.5, -2.5, -14.0), lateral: ("npc.giant.female.hand_r"), ), leg_l: ( - offset: (-3.0, -3.5, -7.0), + offset: (-6.0, -3.5, -7.0), lateral: ("npc.giant.female.leg_l"), ), leg_r: ( - offset: (-3.0, -3.5, -7.0), + offset: (0.0, -3.5, -7.0), lateral: ("npc.giant.female.leg_r"), ), foot_l: ( - offset: (-3.0, -5.5, -10.5), + offset: (-3.0, -5.0, -2.5), lateral: ("npc.giant.female.foot_l"), ), foot_r: ( - offset: (-3.0, -5.5, -10.5), + offset: (-3.0, -5.0, -2.5), lateral: ("npc.giant.female.foot_r"), ) ), diff --git a/assets/voxygen/voxel/humanoid_main_weapon_manifest.ron b/assets/voxygen/voxel/humanoid_main_weapon_manifest.ron index 5a689b103d..04e8a06384 100644 --- a/assets/voxygen/voxel/humanoid_main_weapon_manifest.ron +++ b/assets/voxygen/voxel/humanoid_main_weapon_manifest.ron @@ -55,4 +55,8 @@ vox_spec: ("weapon.debug_wand", (-1.5, -9.5, -4.0)), color: None ), + Empty: ( + vox_spec: ("armor.empty", (-3.0, -3.5, 1.0)), + color: None + ), }) diff --git a/assets/voxygen/voxel/npc/giant/female/foot_l.vox b/assets/voxygen/voxel/npc/giant/female/foot_l.vox index a6e7f95b14..6a6f33e1e8 100644 --- a/assets/voxygen/voxel/npc/giant/female/foot_l.vox +++ b/assets/voxygen/voxel/npc/giant/female/foot_l.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0381fb00349446b2b7bb9b3569037bc745699bfaf2ade019efde70e233a5c4c3 -size 1964 +oid sha256:28d297cc21308270571f67ffd2d89d9f2019e24577fea3f17bbe5f18d72f6dca +size 1920 diff --git a/assets/voxygen/voxel/npc/giant/female/foot_r.vox b/assets/voxygen/voxel/npc/giant/female/foot_r.vox index a6e7f95b14..08efe9ec67 100644 --- a/assets/voxygen/voxel/npc/giant/female/foot_r.vox +++ b/assets/voxygen/voxel/npc/giant/female/foot_r.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0381fb00349446b2b7bb9b3569037bc745699bfaf2ade019efde70e233a5c4c3 -size 1964 +oid sha256:f399f33fd5f0f3abdb6b6466ee28fed54e323fe4dee2f2c5accc4fac375fa00f +size 1920 diff --git a/assets/voxygen/voxel/npc/giant/female/hand_l.vox b/assets/voxygen/voxel/npc/giant/female/hand_l.vox index 25decb62bc..3209a770c9 100644 --- a/assets/voxygen/voxel/npc/giant/female/hand_l.vox +++ b/assets/voxygen/voxel/npc/giant/female/hand_l.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:944eddaefc49572e18222028419cc2ecf492bc90a665c43ab7ecb1d8c8bd8044 -size 1392 +oid sha256:e4d18bfbe4f6fd079d295238bae40a491c9b2c13c7a6d538b4c3adf94b5ce88c +size 1576 diff --git a/assets/voxygen/voxel/npc/giant/female/hand_r.vox b/assets/voxygen/voxel/npc/giant/female/hand_r.vox index 958ee44410..7447d81403 100644 --- a/assets/voxygen/voxel/npc/giant/female/hand_r.vox +++ b/assets/voxygen/voxel/npc/giant/female/hand_r.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7f35050d1534a1add47893c9651ec220aac67e6190747a4b3c947744c9a9a36 -size 1392 +oid sha256:5492321b3af03ffc78f162a0f40e7fa1e1fb6f7285327ca47af043b3c43d63ac +size 1576 diff --git a/assets/voxygen/voxel/npc/giant/female/head.vox b/assets/voxygen/voxel/npc/giant/female/head.vox index c8788e4449..0ab6f106de 100644 --- a/assets/voxygen/voxel/npc/giant/female/head.vox +++ b/assets/voxygen/voxel/npc/giant/female/head.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbeea43aec6edc007c8d271271837664edbd46400200bf5f0bbfe2332f5c6675 -size 5756 +oid sha256:8ccc48349b2beb4a9c5a416e460bfe31ec0246c7aab0ad65b3c176eb27380c1d +size 5860 diff --git a/assets/voxygen/voxel/npc/giant/female/leg_l.vox b/assets/voxygen/voxel/npc/giant/female/leg_l.vox index 8172ab59a3..250ae798fb 100644 --- a/assets/voxygen/voxel/npc/giant/female/leg_l.vox +++ b/assets/voxygen/voxel/npc/giant/female/leg_l.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3565904f0822154d0474ea6498a02a9ee3604fa919f6417af0fdaf49014ed25 -size 1576 +oid sha256:dfb9da63ede02f3d4cac323d7152d7764b3144e517cfc46fe1ceb1f9accf0378 +size 1704 diff --git a/assets/voxygen/voxel/npc/giant/female/leg_r.vox b/assets/voxygen/voxel/npc/giant/female/leg_r.vox index 35057ff177..c2ac8a3699 100644 --- a/assets/voxygen/voxel/npc/giant/female/leg_r.vox +++ b/assets/voxygen/voxel/npc/giant/female/leg_r.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2f75264495aaf6ad3568758bd6fdf53a2f9cf05d2d1a7dcacc0735253ed6564 -size 1576 +oid sha256:817b57a4b69c99e8c34c908a33d8436867a1563e25d2b632b6519b15c3b72512 +size 1704 diff --git a/assets/voxygen/voxel/npc/giant/female/torso_lower.vox b/assets/voxygen/voxel/npc/giant/female/torso_lower.vox index 14e262784c..bfbc82b50b 100644 --- a/assets/voxygen/voxel/npc/giant/female/torso_lower.vox +++ b/assets/voxygen/voxel/npc/giant/female/torso_lower.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e9c3d59e74bb7c1c3fee2bd0a63b78e2e29ddd34322a7260bf575d743c0434e +oid sha256:6b4b9201910750c3d04924faaaaf34e6bb7283d33f6fb6208a9696d767f01ed7 size 2172 diff --git a/assets/voxygen/voxel/npc/giant/female/torso_upper.vox b/assets/voxygen/voxel/npc/giant/female/torso_upper.vox index 56b1bffafe..62c7c557a9 100644 --- a/assets/voxygen/voxel/npc/giant/female/torso_upper.vox +++ b/assets/voxygen/voxel/npc/giant/female/torso_upper.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62ef18d8a1d599b5de13a6d383af92562aa982dde6fca3356400138614d22657 -size 4932 +oid sha256:2f92b25d03056e0097910f0898e8f2cbaedf062aaeab60d5ba68a524976ec5ec +size 4980 diff --git a/assets/voxygen/voxel/npc/giant/male/foot_l.vox b/assets/voxygen/voxel/npc/giant/male/foot_l.vox index a6e7f95b14..6a6f33e1e8 100644 --- a/assets/voxygen/voxel/npc/giant/male/foot_l.vox +++ b/assets/voxygen/voxel/npc/giant/male/foot_l.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0381fb00349446b2b7bb9b3569037bc745699bfaf2ade019efde70e233a5c4c3 -size 1964 +oid sha256:28d297cc21308270571f67ffd2d89d9f2019e24577fea3f17bbe5f18d72f6dca +size 1920 diff --git a/assets/voxygen/voxel/npc/giant/male/foot_r.vox b/assets/voxygen/voxel/npc/giant/male/foot_r.vox index a6e7f95b14..08efe9ec67 100644 --- a/assets/voxygen/voxel/npc/giant/male/foot_r.vox +++ b/assets/voxygen/voxel/npc/giant/male/foot_r.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0381fb00349446b2b7bb9b3569037bc745699bfaf2ade019efde70e233a5c4c3 -size 1964 +oid sha256:f399f33fd5f0f3abdb6b6466ee28fed54e323fe4dee2f2c5accc4fac375fa00f +size 1920 diff --git a/assets/voxygen/voxel/npc/giant/male/hand_l.vox b/assets/voxygen/voxel/npc/giant/male/hand_l.vox index 25decb62bc..3209a770c9 100644 --- a/assets/voxygen/voxel/npc/giant/male/hand_l.vox +++ b/assets/voxygen/voxel/npc/giant/male/hand_l.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:944eddaefc49572e18222028419cc2ecf492bc90a665c43ab7ecb1d8c8bd8044 -size 1392 +oid sha256:e4d18bfbe4f6fd079d295238bae40a491c9b2c13c7a6d538b4c3adf94b5ce88c +size 1576 diff --git a/assets/voxygen/voxel/npc/giant/male/hand_r.vox b/assets/voxygen/voxel/npc/giant/male/hand_r.vox index 958ee44410..7447d81403 100644 --- a/assets/voxygen/voxel/npc/giant/male/hand_r.vox +++ b/assets/voxygen/voxel/npc/giant/male/hand_r.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7f35050d1534a1add47893c9651ec220aac67e6190747a4b3c947744c9a9a36 -size 1392 +oid sha256:5492321b3af03ffc78f162a0f40e7fa1e1fb6f7285327ca47af043b3c43d63ac +size 1576 diff --git a/assets/voxygen/voxel/npc/giant/male/head.vox b/assets/voxygen/voxel/npc/giant/male/head.vox index c8788e4449..0ab6f106de 100644 --- a/assets/voxygen/voxel/npc/giant/male/head.vox +++ b/assets/voxygen/voxel/npc/giant/male/head.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbeea43aec6edc007c8d271271837664edbd46400200bf5f0bbfe2332f5c6675 -size 5756 +oid sha256:8ccc48349b2beb4a9c5a416e460bfe31ec0246c7aab0ad65b3c176eb27380c1d +size 5860 diff --git a/assets/voxygen/voxel/npc/giant/male/leg_l.vox b/assets/voxygen/voxel/npc/giant/male/leg_l.vox index 8172ab59a3..250ae798fb 100644 --- a/assets/voxygen/voxel/npc/giant/male/leg_l.vox +++ b/assets/voxygen/voxel/npc/giant/male/leg_l.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3565904f0822154d0474ea6498a02a9ee3604fa919f6417af0fdaf49014ed25 -size 1576 +oid sha256:dfb9da63ede02f3d4cac323d7152d7764b3144e517cfc46fe1ceb1f9accf0378 +size 1704 diff --git a/assets/voxygen/voxel/npc/giant/male/leg_r.vox b/assets/voxygen/voxel/npc/giant/male/leg_r.vox index 35057ff177..c2ac8a3699 100644 --- a/assets/voxygen/voxel/npc/giant/male/leg_r.vox +++ b/assets/voxygen/voxel/npc/giant/male/leg_r.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2f75264495aaf6ad3568758bd6fdf53a2f9cf05d2d1a7dcacc0735253ed6564 -size 1576 +oid sha256:817b57a4b69c99e8c34c908a33d8436867a1563e25d2b632b6519b15c3b72512 +size 1704 diff --git a/assets/voxygen/voxel/npc/giant/male/torso_lower.vox b/assets/voxygen/voxel/npc/giant/male/torso_lower.vox index 14e262784c..bfbc82b50b 100644 --- a/assets/voxygen/voxel/npc/giant/male/torso_lower.vox +++ b/assets/voxygen/voxel/npc/giant/male/torso_lower.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e9c3d59e74bb7c1c3fee2bd0a63b78e2e29ddd34322a7260bf575d743c0434e +oid sha256:6b4b9201910750c3d04924faaaaf34e6bb7283d33f6fb6208a9696d767f01ed7 size 2172 diff --git a/assets/voxygen/voxel/npc/giant/male/torso_upper.vox b/assets/voxygen/voxel/npc/giant/male/torso_upper.vox index 56b1bffafe..62c7c557a9 100644 --- a/assets/voxygen/voxel/npc/giant/male/torso_upper.vox +++ b/assets/voxygen/voxel/npc/giant/male/torso_upper.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62ef18d8a1d599b5de13a6d383af92562aa982dde6fca3356400138614d22657 -size 4932 +oid sha256:2f92b25d03056e0097910f0898e8f2cbaedf062aaeab60d5ba68a524976ec5ec +size 4980 diff --git a/assets/voxygen/voxel/sprite/cabbage/cabbage-0.vox b/assets/voxygen/voxel/sprite/cabbage/cabbage-0.vox new file mode 100644 index 0000000000..44e668cd82 --- /dev/null +++ b/assets/voxygen/voxel/sprite/cabbage/cabbage-0.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d791e42efa4d87cffa51e09bfe308576390ae1ce392e2c2ec971ce41a0ee35b5 +size 2064 diff --git a/assets/voxygen/voxel/sprite/cabbage/cabbage-1.vox b/assets/voxygen/voxel/sprite/cabbage/cabbage-1.vox new file mode 100644 index 0000000000..34e4d5cb5e --- /dev/null +++ b/assets/voxygen/voxel/sprite/cabbage/cabbage-1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eaa8cf37b170c93a1c6ecf5a71376ed041d5e1a58faedcb96ee3f128aa5fa2d4 +size 2064 diff --git a/assets/voxygen/voxel/sprite/cabbage/cabbage-2.vox b/assets/voxygen/voxel/sprite/cabbage/cabbage-2.vox new file mode 100644 index 0000000000..da5cdbed44 --- /dev/null +++ b/assets/voxygen/voxel/sprite/cabbage/cabbage-2.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9bc7359384c83b0e71f67df185e0c86594375335cf24845469f3f873985c9147 +size 2064 diff --git a/assets/voxygen/voxel/sprite/carrot/0.vox b/assets/voxygen/voxel/sprite/carrot/0.vox new file mode 100644 index 0000000000..0a31db6a43 --- /dev/null +++ b/assets/voxygen/voxel/sprite/carrot/0.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdccd8d4e34a2bc43ef75d406b01806b757a2602ee300d58c82ea2818ebb45b8 +size 1716 diff --git a/assets/voxygen/voxel/sprite/carrot/1.vox b/assets/voxygen/voxel/sprite/carrot/1.vox new file mode 100644 index 0000000000..a34498bf76 --- /dev/null +++ b/assets/voxygen/voxel/sprite/carrot/1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbe059eaf30ea72eb10981c8f6f52b9604c2fbf161240e298240c580d5c6cf81 +size 1560 diff --git a/assets/voxygen/voxel/sprite/carrot/2.vox b/assets/voxygen/voxel/sprite/carrot/2.vox new file mode 100644 index 0000000000..fa50b10e15 --- /dev/null +++ b/assets/voxygen/voxel/sprite/carrot/2.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b29b616c5557510e857735d694e05af85668f0e8b44b9096ad0a1d1a1d7d03f1 +size 1664 diff --git a/assets/voxygen/voxel/sprite/carrot/3.vox b/assets/voxygen/voxel/sprite/carrot/3.vox new file mode 100644 index 0000000000..91f533832a --- /dev/null +++ b/assets/voxygen/voxel/sprite/carrot/3.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14e7624d5fc4d068f3886a00a389e6fd25f9b2c027bb3b51a9d577f404dc51b0 +size 1656 diff --git a/assets/voxygen/voxel/sprite/carrot/4.vox b/assets/voxygen/voxel/sprite/carrot/4.vox new file mode 100644 index 0000000000..f7de3722ac --- /dev/null +++ b/assets/voxygen/voxel/sprite/carrot/4.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0d9442ed362367c675d33552b5fbe0157c69ce900860ce5667414ad05de19b8 +size 1672 diff --git a/assets/voxygen/voxel/sprite/carrot/5.vox b/assets/voxygen/voxel/sprite/carrot/5.vox new file mode 100644 index 0000000000..ceb03ecdbc --- /dev/null +++ b/assets/voxygen/voxel/sprite/carrot/5.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f97c8894e1c607dd9d71967e33fa5221be743e21aef0fae5587c0011de290035 +size 1632 diff --git a/assets/voxygen/voxel/sprite/corn/corn-0.vox b/assets/voxygen/voxel/sprite/corn/corn-0.vox new file mode 100644 index 0000000000..e1eba1c9cd --- /dev/null +++ b/assets/voxygen/voxel/sprite/corn/corn-0.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8d6525d00da9357c5a732b32dfcced3761888a72709186498d22614d1c90c8c +size 2252 diff --git a/assets/voxygen/voxel/sprite/corn/corn-1.vox b/assets/voxygen/voxel/sprite/corn/corn-1.vox new file mode 100644 index 0000000000..2368757a4a --- /dev/null +++ b/assets/voxygen/voxel/sprite/corn/corn-1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a933f0ba129c777c5fd1bae7db55afe1caf7680a7b91467073aee2f7d134fc2 +size 2420 diff --git a/assets/voxygen/voxel/sprite/corn/corn-2.vox b/assets/voxygen/voxel/sprite/corn/corn-2.vox new file mode 100644 index 0000000000..9296f2dfc2 --- /dev/null +++ b/assets/voxygen/voxel/sprite/corn/corn-2.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b41032bc335fcae7f526873b0e54bd0bfb3ce182811af03b747c92d38485173 +size 2204 diff --git a/assets/voxygen/voxel/sprite/corn/corn-3.vox b/assets/voxygen/voxel/sprite/corn/corn-3.vox new file mode 100644 index 0000000000..6315ee2469 --- /dev/null +++ b/assets/voxygen/voxel/sprite/corn/corn-3.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4cecfceca8280aa54b30382e3913d041d3d30658d9f38edd94b84365cd72a8bd +size 1832 diff --git a/assets/voxygen/voxel/sprite/corn/corn-4.vox b/assets/voxygen/voxel/sprite/corn/corn-4.vox new file mode 100644 index 0000000000..7723d17f82 --- /dev/null +++ b/assets/voxygen/voxel/sprite/corn/corn-4.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eaee492532b274e1f2e405c64fcf6a01f30e5a799681b4ef0d3ac9bdc37a06be +size 2096 diff --git a/assets/voxygen/voxel/sprite/corn/corn-5.vox b/assets/voxygen/voxel/sprite/corn/corn-5.vox new file mode 100644 index 0000000000..e7c78406bd --- /dev/null +++ b/assets/voxygen/voxel/sprite/corn/corn-5.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffefe83d5d1894158672664287b88593a593790a7dc56a861297bfea095971e0 +size 2252 diff --git a/assets/voxygen/voxel/sprite/door/door-0.vox b/assets/voxygen/voxel/sprite/door/door-0.vox new file mode 100644 index 0000000000..87960f0ef8 --- /dev/null +++ b/assets/voxygen/voxel/sprite/door/door-0.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4f201e1f3968ed005eb8080cf3ff181f3ec34f42ddba17dc09e30adb180c36c +size 6156 diff --git a/assets/voxygen/voxel/sprite/ember/1.vox b/assets/voxygen/voxel/sprite/ember/1.vox new file mode 100644 index 0000000000..f7b6f438ee --- /dev/null +++ b/assets/voxygen/voxel/sprite/ember/1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fb63355560eb8f6d4661f8e3eb41ea9a5b3ede8628a84c3d36d5c1063e7154c +size 3124 diff --git a/assets/voxygen/voxel/sprite/flax/flax-0.vox b/assets/voxygen/voxel/sprite/flax/flax-0.vox new file mode 100644 index 0000000000..b98bde1434 --- /dev/null +++ b/assets/voxygen/voxel/sprite/flax/flax-0.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb094f06bb530d44dab7581539de04bb011208f4883e40999be6445538cb6e5a +size 1664 diff --git a/assets/voxygen/voxel/sprite/flax/flax-1.vox b/assets/voxygen/voxel/sprite/flax/flax-1.vox new file mode 100644 index 0000000000..d0c80d4766 --- /dev/null +++ b/assets/voxygen/voxel/sprite/flax/flax-1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d7ee17a1f40778a071929be44f7d63e449fcb7dc35ac3fa5aa20bf38e9252c3 +size 1768 diff --git a/assets/voxygen/voxel/sprite/flax/flax-2.vox b/assets/voxygen/voxel/sprite/flax/flax-2.vox new file mode 100644 index 0000000000..af5b27d1a7 --- /dev/null +++ b/assets/voxygen/voxel/sprite/flax/flax-2.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:632b581575bae82cf5b9a106bd11a98fbed8c7b85b85fc652a689a890a2d985b +size 1800 diff --git a/assets/voxygen/voxel/sprite/flax/flax-3.vox b/assets/voxygen/voxel/sprite/flax/flax-3.vox new file mode 100644 index 0000000000..9db44fb2cd --- /dev/null +++ b/assets/voxygen/voxel/sprite/flax/flax-3.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a979f7d9b0d8047a6720ab17bf35b005c14190ccc4a35435d56e77a3e5ebb8f +size 1472 diff --git a/assets/voxygen/voxel/sprite/flax/flax-4.vox b/assets/voxygen/voxel/sprite/flax/flax-4.vox new file mode 100644 index 0000000000..93e28fb98a --- /dev/null +++ b/assets/voxygen/voxel/sprite/flax/flax-4.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f33665ea85935b94de2db220c2921ad7525b681ac5513daa5030e2b3b96930e +size 1632 diff --git a/assets/voxygen/voxel/sprite/flax/flax-5.vox b/assets/voxygen/voxel/sprite/flax/flax-5.vox new file mode 100644 index 0000000000..9396ef40d1 --- /dev/null +++ b/assets/voxygen/voxel/sprite/flax/flax-5.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ceab01a1f453292838c38d4c1cd26c0ec94842edf01461cd6fe4f080d788688 +size 1380 diff --git a/assets/voxygen/voxel/sprite/fruit/coconut.vox b/assets/voxygen/voxel/sprite/fruit/coconut.vox new file mode 100644 index 0000000000..56545168fa --- /dev/null +++ b/assets/voxygen/voxel/sprite/fruit/coconut.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24821d19153f380c18b8e255edcd1d9a5473485742e8b4fd6d9eaa9921d96c5c +size 3100 diff --git a/assets/voxygen/voxel/sprite/misc/scarecrow.vox b/assets/voxygen/voxel/sprite/misc/scarecrow.vox new file mode 100644 index 0000000000..85d65d4841 --- /dev/null +++ b/assets/voxygen/voxel/sprite/misc/scarecrow.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ce75f40786d154186e6da42ed99117c19e53c02ec8a61adc9216de4935739cf +size 4468 diff --git a/assets/voxygen/voxel/sprite/misc/street_lamp.vox b/assets/voxygen/voxel/sprite/misc/street_lamp.vox new file mode 100644 index 0000000000..6f738e8bb5 --- /dev/null +++ b/assets/voxygen/voxel/sprite/misc/street_lamp.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5909211edd73fe743f81ae280aaf2e6022a3e5d0430a7c79509479401a01dac0 +size 4776 diff --git a/assets/voxygen/voxel/sprite/pumpkin/1.vox b/assets/voxygen/voxel/sprite/pumpkin/1.vox index 731d773e04..e04c596c6d 100644 --- a/assets/voxygen/voxel/sprite/pumpkin/1.vox +++ b/assets/voxygen/voxel/sprite/pumpkin/1.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09aedb0c998e662d2d9cb8a38d31094316804442b6aa68cb5533c043c07b87b8 +oid sha256:f0580c2e316710f88f1bb7a20462e64ad44aa6a47b3b58069d1d6b1015bdec06 size 2568 diff --git a/assets/voxygen/voxel/sprite/pumpkin/2.vox b/assets/voxygen/voxel/sprite/pumpkin/2.vox index a26eae8383..2cb8808644 100644 --- a/assets/voxygen/voxel/sprite/pumpkin/2.vox +++ b/assets/voxygen/voxel/sprite/pumpkin/2.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdf17c9e4feac356bf4941df983bfdb1c25bbe151d124dc24e177b793553bc99 +oid sha256:2616a9c1e18ec29006d0fce787957cb9c19aab3a25b2b360f0dd490515ad57d3 size 2612 diff --git a/assets/voxygen/voxel/sprite/pumpkin/3.vox b/assets/voxygen/voxel/sprite/pumpkin/3.vox index bbbace60ec..ce49f8bbed 100644 --- a/assets/voxygen/voxel/sprite/pumpkin/3.vox +++ b/assets/voxygen/voxel/sprite/pumpkin/3.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c42591c770fe910efb091b607e8f9c86f39ac687d70766251ee2a7f0e3da303e +oid sha256:f338dd3b94bb36a7f36e5272e847e83bfa9317daf3f7b8535e73ea39842ef1b6 size 2580 diff --git a/assets/voxygen/voxel/sprite/pumpkin/4.vox b/assets/voxygen/voxel/sprite/pumpkin/4.vox index 8aea98cb56..ebe4c384a3 100644 --- a/assets/voxygen/voxel/sprite/pumpkin/4.vox +++ b/assets/voxygen/voxel/sprite/pumpkin/4.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:735a9f6b32f207bf1ae0e1551c8789f08116991d97743e93620406b1d2850d2d +oid sha256:6e65e3d6c3bda693b7ef21a86aac13f7a4fa87a09a1b731e58b13369e75751d7 size 2592 diff --git a/assets/voxygen/voxel/sprite/pumpkin/5.vox b/assets/voxygen/voxel/sprite/pumpkin/5.vox index 2bdf3cef8c..1ebaedd451 100644 --- a/assets/voxygen/voxel/sprite/pumpkin/5.vox +++ b/assets/voxygen/voxel/sprite/pumpkin/5.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:260a5e45c90cbc153ecf1846a3461980710c648f682f62cf416d25c4c078db04 +oid sha256:d96e0c99bd97e8a16d74f6419d6c5ea4740b5fbf82f7d61835f68dfb75debc87 size 2616 diff --git a/assets/voxygen/voxel/sprite/pumpkin/6.vox b/assets/voxygen/voxel/sprite/pumpkin/6.vox new file mode 100644 index 0000000000..6e2e3bf480 --- /dev/null +++ b/assets/voxygen/voxel/sprite/pumpkin/6.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0abc7fd43fbb5cfbe6ff4a341a94d40f09693164a0d35c480f297ff1bae5eae +size 4668 diff --git a/assets/voxygen/voxel/sprite/pumpkin/7.vox b/assets/voxygen/voxel/sprite/pumpkin/7.vox new file mode 100644 index 0000000000..3842695cce --- /dev/null +++ b/assets/voxygen/voxel/sprite/pumpkin/7.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f965826e3c5d890d32cdc1de331ae60e6d35e672cc6f44a4dd407268f9e66ab +size 4740 diff --git a/assets/voxygen/voxel/sprite/radish/0.vox b/assets/voxygen/voxel/sprite/radish/0.vox new file mode 100644 index 0000000000..2dbf98d2d8 --- /dev/null +++ b/assets/voxygen/voxel/sprite/radish/0.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acf2166f24ef4931837e10ca8b8ff71b1963480db475f98227cf13053b08d8e6 +size 1640 diff --git a/assets/voxygen/voxel/sprite/radish/1.vox b/assets/voxygen/voxel/sprite/radish/1.vox new file mode 100644 index 0000000000..745b8f6df4 --- /dev/null +++ b/assets/voxygen/voxel/sprite/radish/1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25f1d72e72380947c51610d83c693a0fcbd985ff0f65dd11065d95d01f0b66e1 +size 1604 diff --git a/assets/voxygen/voxel/sprite/radish/2.vox b/assets/voxygen/voxel/sprite/radish/2.vox new file mode 100644 index 0000000000..4560be2fdb --- /dev/null +++ b/assets/voxygen/voxel/sprite/radish/2.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b872912c813034d2d398ec2aa2d9f9a410a8f28766b2070a6307d6bef495d8f5 +size 1452 diff --git a/assets/voxygen/voxel/sprite/radish/3.vox b/assets/voxygen/voxel/sprite/radish/3.vox new file mode 100644 index 0000000000..01957a6b9c --- /dev/null +++ b/assets/voxygen/voxel/sprite/radish/3.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e6d4fbed96dc161c5fbd3de4cb8e62e0b6b79b8430f0c01b2d6bd48ba77b213 +size 1488 diff --git a/assets/voxygen/voxel/sprite/radish/4.vox b/assets/voxygen/voxel/sprite/radish/4.vox new file mode 100644 index 0000000000..d5d4efb5aa --- /dev/null +++ b/assets/voxygen/voxel/sprite/radish/4.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21d9b7c04d63493858ebdc0162a00633927c71f79fce5df0aa06fffbd5f7a477 +size 1520 diff --git a/assets/voxygen/voxel/sprite/tomato/0.vox b/assets/voxygen/voxel/sprite/tomato/0.vox new file mode 100644 index 0000000000..c018365cbb --- /dev/null +++ b/assets/voxygen/voxel/sprite/tomato/0.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0d0296db4368f79f0231f5cfd5d2b89cf06c4820196bf5a5a3b2a4f2430bb23 +size 2520 diff --git a/assets/voxygen/voxel/sprite/tomato/1.vox b/assets/voxygen/voxel/sprite/tomato/1.vox new file mode 100644 index 0000000000..f44563c1d3 --- /dev/null +++ b/assets/voxygen/voxel/sprite/tomato/1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0b588c45d77f3712195f992f7e979724aa196ea0648a07c002998d2f1b8435b +size 2616 diff --git a/assets/voxygen/voxel/sprite/tomato/2.vox b/assets/voxygen/voxel/sprite/tomato/2.vox new file mode 100644 index 0000000000..99127bfa46 --- /dev/null +++ b/assets/voxygen/voxel/sprite/tomato/2.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1c035ba21740573df46c3fa0a49aaf00b82758279f716c32ea5763df0a3a8b3 +size 2500 diff --git a/assets/voxygen/voxel/sprite/tomato/3.vox b/assets/voxygen/voxel/sprite/tomato/3.vox new file mode 100644 index 0000000000..149bce29f6 --- /dev/null +++ b/assets/voxygen/voxel/sprite/tomato/3.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:019abd752ceba6bb30cefebbb55a9c4742f114cb7c155f93642253cb71ee2532 +size 2444 diff --git a/assets/voxygen/voxel/sprite/tomato/4.vox b/assets/voxygen/voxel/sprite/tomato/4.vox new file mode 100644 index 0000000000..f909913d61 --- /dev/null +++ b/assets/voxygen/voxel/sprite/tomato/4.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4fc2ca75eb934c5684e8911b2b4fb4ee3f22b9ae2cdfec780f5f2e4025ceaa3 +size 2456 diff --git a/assets/voxygen/voxel/sprite/turnip/turnip-0.vox b/assets/voxygen/voxel/sprite/turnip/turnip-0.vox new file mode 100644 index 0000000000..fc55170674 --- /dev/null +++ b/assets/voxygen/voxel/sprite/turnip/turnip-0.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08fb3876bbc499ab64f62776fcfbdab9709ed5ac90c5c94d283a5c6defe7a0fd +size 1360 diff --git a/assets/voxygen/voxel/sprite/turnip/turnip-1.vox b/assets/voxygen/voxel/sprite/turnip/turnip-1.vox new file mode 100644 index 0000000000..9ab7b388d6 --- /dev/null +++ b/assets/voxygen/voxel/sprite/turnip/turnip-1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34d59017041f4f340b57d67b0929482bf170d996404f15dbb323f65e72c6b3f6 +size 1528 diff --git a/assets/voxygen/voxel/sprite/turnip/turnip-2.vox b/assets/voxygen/voxel/sprite/turnip/turnip-2.vox new file mode 100644 index 0000000000..d3b58dbed2 --- /dev/null +++ b/assets/voxygen/voxel/sprite/turnip/turnip-2.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c86e20979271f63338dc4ac7afecb79f9461297d460731f145f6b35d149eefa +size 1328 diff --git a/assets/voxygen/voxel/sprite/turnip/turnip-3.vox b/assets/voxygen/voxel/sprite/turnip/turnip-3.vox new file mode 100644 index 0000000000..e34eb2b716 --- /dev/null +++ b/assets/voxygen/voxel/sprite/turnip/turnip-3.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea31d0b8739c93d07566f6c01f782aacd7873ff41407f2bd9880776dca7bcb48 +size 1480 diff --git a/assets/voxygen/voxel/sprite/turnip/turnip-4.vox b/assets/voxygen/voxel/sprite/turnip/turnip-4.vox new file mode 100644 index 0000000000..a7b8e8ae2d --- /dev/null +++ b/assets/voxygen/voxel/sprite/turnip/turnip-4.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1844a7c3c35b1e0b3ad6914eee0193b2f7aca0a1ff745b408b9ea89809bc9c09 +size 1336 diff --git a/assets/voxygen/voxel/sprite/turnip/turnip-5.vox b/assets/voxygen/voxel/sprite/turnip/turnip-5.vox new file mode 100644 index 0000000000..bc49b4c6e3 --- /dev/null +++ b/assets/voxygen/voxel/sprite/turnip/turnip-5.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b18479edcd9b10aa4ba7c7bdc1f1456ea09236aee2f499a5f86cf7adb72214d8 +size 1312 diff --git a/assets/voxygen/voxel/sprite/wheat_green/wheat-0.vox b/assets/voxygen/voxel/sprite/wheat_green/wheat-0.vox new file mode 100644 index 0000000000..1e2cd0f8f8 --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_green/wheat-0.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ea27a3e7a8be87331cbad2f32ed4ae437243faa780772d491597a033de6eddf +size 1424 diff --git a/assets/voxygen/voxel/sprite/wheat_green/wheat-1.vox b/assets/voxygen/voxel/sprite/wheat_green/wheat-1.vox new file mode 100644 index 0000000000..bdd9e2a198 --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_green/wheat-1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a80334b4be59c5a64a5246dff836ea448770e98a6f2e3a3a60a13d2174507f84 +size 1464 diff --git a/assets/voxygen/voxel/sprite/wheat_green/wheat-2.vox b/assets/voxygen/voxel/sprite/wheat_green/wheat-2.vox new file mode 100644 index 0000000000..5bd2acf9e7 --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_green/wheat-2.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de10ae4506e55d1b3b2ee8b5f2cc3eb0262778ef3570c5d3af40aef8e6a9cc78 +size 1364 diff --git a/assets/voxygen/voxel/sprite/wheat_green/wheat-3.vox b/assets/voxygen/voxel/sprite/wheat_green/wheat-3.vox new file mode 100644 index 0000000000..0cd7197aae --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_green/wheat-3.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6e2ad13a9b22484327aff8084f43d93867c5ef009f0e20334f1f9ed605fdb2a +size 1452 diff --git a/assets/voxygen/voxel/sprite/wheat_green/wheat-4.vox b/assets/voxygen/voxel/sprite/wheat_green/wheat-4.vox new file mode 100644 index 0000000000..5128876e60 --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_green/wheat-4.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eab723b6b0d81fba634beb363d1363ba155f3964876f1c71b43ad70d00efdf32 +size 1296 diff --git a/assets/voxygen/voxel/sprite/wheat_green/wheat-5.vox b/assets/voxygen/voxel/sprite/wheat_green/wheat-5.vox new file mode 100644 index 0000000000..80ad6a3f22 --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_green/wheat-5.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b53d94bffbdf2581a9c399d4052baffead2156b79bc9c2018de6c714bdcdc287 +size 1576 diff --git a/assets/voxygen/voxel/sprite/wheat_green/wheat-6.vox b/assets/voxygen/voxel/sprite/wheat_green/wheat-6.vox new file mode 100644 index 0000000000..1a794d41a2 --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_green/wheat-6.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:990b221356fdcac1be68830dcf81552b3878f428eef83f21dee7352c693e70a4 +size 1432 diff --git a/assets/voxygen/voxel/sprite/wheat_green/wheat-7.vox b/assets/voxygen/voxel/sprite/wheat_green/wheat-7.vox new file mode 100644 index 0000000000..602e2ebf04 --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_green/wheat-7.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fde089733a0f63fe192aa156703630d17ac859a46ad69f88dd0873dbb3edc82 +size 1504 diff --git a/assets/voxygen/voxel/sprite/wheat_green/wheat-8.vox b/assets/voxygen/voxel/sprite/wheat_green/wheat-8.vox new file mode 100644 index 0000000000..82310499a8 --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_green/wheat-8.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f4f96a6df2c70159c929f9b8354502855f7b4ca120da0a1d66c663a2863f558 +size 1548 diff --git a/assets/voxygen/voxel/sprite/wheat_green/wheat-9.vox b/assets/voxygen/voxel/sprite/wheat_green/wheat-9.vox new file mode 100644 index 0000000000..149bb832eb --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_green/wheat-9.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:754ca2705d1a0ea93096475ac976bd48b0f03fb5b8327e13a5c767e7204b188a +size 1580 diff --git a/assets/voxygen/voxel/sprite/wheat_yellow/wheat-0.vox b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-0.vox new file mode 100644 index 0000000000..f30de9cb1f --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-0.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdd5bf80f996bae9f28f48d2a5fb618336b3de11ee4a8ee681083e30c02b2d8a +size 1424 diff --git a/assets/voxygen/voxel/sprite/wheat_yellow/wheat-1.vox b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-1.vox new file mode 100644 index 0000000000..3ff4fa2c6d --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83eb117be030060821ccf7a03c53135c5508cd4adec7f3164b912323c2580958 +size 1464 diff --git a/assets/voxygen/voxel/sprite/wheat_yellow/wheat-2.vox b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-2.vox new file mode 100644 index 0000000000..bd1b67472b --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-2.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:731230c97d1b80ec0d5457f8f8d121084da2f516888f3ea3f45c37e0474415fd +size 1364 diff --git a/assets/voxygen/voxel/sprite/wheat_yellow/wheat-3.vox b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-3.vox new file mode 100644 index 0000000000..28772db191 --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-3.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ee83d5b33e7c8f8f51272462ae874dd1c62e600a095130046d49f62bbfe2a04 +size 1452 diff --git a/assets/voxygen/voxel/sprite/wheat_yellow/wheat-4.vox b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-4.vox new file mode 100644 index 0000000000..0c1755a4b6 --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-4.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbbada83a34bf25c12965ba758c1c88250cf2712c75149342fcebca4550d019f +size 1296 diff --git a/assets/voxygen/voxel/sprite/wheat_yellow/wheat-5.vox b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-5.vox new file mode 100644 index 0000000000..42f2aa6ebc --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-5.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35c9a77bef70904013325fe92692e60e10b13efc0a710ccf59c23ef4185879d9 +size 1576 diff --git a/assets/voxygen/voxel/sprite/wheat_yellow/wheat-6.vox b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-6.vox new file mode 100644 index 0000000000..ee7fef75f9 --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-6.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fc5ea30ec387fd54aebad5cca3207c310d23a7ce4cb2c6fc2e6290b5e520b40 +size 1432 diff --git a/assets/voxygen/voxel/sprite/wheat_yellow/wheat-7.vox b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-7.vox new file mode 100644 index 0000000000..ef3f36738f --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-7.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:127a88529c25346b51fab378af6d2d19ea5d427b3dd8cfbd6270b675eeb578c6 +size 1504 diff --git a/assets/voxygen/voxel/sprite/wheat_yellow/wheat-8.vox b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-8.vox new file mode 100644 index 0000000000..fffe9c7010 --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-8.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1e58d31d75947c2e18e5b207e7df7cf266949b95e678ef2d93491f3c81ea508 +size 1548 diff --git a/assets/voxygen/voxel/sprite/wheat_yellow/wheat-9.vox b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-9.vox new file mode 100644 index 0000000000..213c625a46 --- /dev/null +++ b/assets/voxygen/voxel/sprite/wheat_yellow/wheat-9.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9784152567b21d75d6266338d13fc05179debb01a6da2490f2b99c38935a9e1f +size 1580 diff --git a/assets/voxygen/voxel/sprite/window/window-0.vox b/assets/voxygen/voxel/sprite/window/window-0.vox new file mode 100644 index 0000000000..1bd7d003d9 --- /dev/null +++ b/assets/voxygen/voxel/sprite/window/window-0.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe857eae2b685cd9bf7abdf68e6d5c0e108886f61e6a4f0004eee30ff441e500 +size 1560 diff --git a/assets/voxygen/voxel/sprite/window/window-1.vox b/assets/voxygen/voxel/sprite/window/window-1.vox new file mode 100644 index 0000000000..24e3d6877f --- /dev/null +++ b/assets/voxygen/voxel/sprite/window/window-1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4beef6bee7cfdef4f41336775cdaac73376a8edc40a0e22bfb3e8d620fb9f49 +size 1544 diff --git a/assets/voxygen/voxel/sprite/window/window-2.vox b/assets/voxygen/voxel/sprite/window/window-2.vox new file mode 100644 index 0000000000..07bfd7f345 --- /dev/null +++ b/assets/voxygen/voxel/sprite/window/window-2.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:430ebb35fc140cb1203a3e0b4be4f52205db628cc9071f05b35a27f16e4f9a88 +size 1528 diff --git a/assets/voxygen/voxel/sprite/window/window-3.vox b/assets/voxygen/voxel/sprite/window/window-3.vox new file mode 100644 index 0000000000..f0d8901ae4 --- /dev/null +++ b/assets/voxygen/voxel/sprite/window/window-3.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ae2e9c20bb7a32202f3c72abc3793725be8059f5b877e161762c28cd6b2cdbb +size 1540 diff --git a/assets/world/map/veloren_0_5_0_0.bin b/assets/world/map/veloren_0_5_0_0.bin index f121098936..3c52bd8161 100644 --- a/assets/world/map/veloren_0_5_0_0.bin +++ b/assets/world/map/veloren_0_5_0_0.bin @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fd18a0b50764564cfc7ffc6c2551100ca564c951fe531f508469e029d02482c +oid sha256:267884532ae482c58f8cc2308c36824b4f4216b124b0310104364c7619e20130 size 16777236 diff --git a/assets/world/tree/desert_palm/1.vox b/assets/world/tree/desert_palm/1.vox index bec36a7ddf..3fa08ff0ff 100644 --- a/assets/world/tree/desert_palm/1.vox +++ b/assets/world/tree/desert_palm/1.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abe347ac442a2afc287869f9d2256c5af2b6507f4d1fa370f39769a95119713c -size 2568 +oid sha256:6cfa51cd9bedf24d727aeffb809b0c673dc8cb15aeb0a7b6a23e05aac2121436 +size 2580 diff --git a/assets/world/tree/desert_palm/10.vox b/assets/world/tree/desert_palm/10.vox index c0daf1cff2..4f5a6d283d 100644 --- a/assets/world/tree/desert_palm/10.vox +++ b/assets/world/tree/desert_palm/10.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06a39754b0d316639d487275ea85534eb6a325a4d70b4e987450c1c702f12e01 +oid sha256:51c8a5c03190aaf0752b0c2dcc1f2d3aa4744afadd3b45774b157498ac0ad717 size 3356 diff --git a/assets/world/tree/desert_palm/2.vox b/assets/world/tree/desert_palm/2.vox index 7c582c5ae1..644b57f599 100644 --- a/assets/world/tree/desert_palm/2.vox +++ b/assets/world/tree/desert_palm/2.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:807b9c55da8106d93d538d2e55b9cb3056cd01c35d98eb6cead69a9814e71225 -size 2536 +oid sha256:3980edde33843925e017e7986c3722ce03202119f95b177f3dbde45dd47084ef +size 2544 diff --git a/assets/world/tree/desert_palm/3.vox b/assets/world/tree/desert_palm/3.vox index c8862eaee3..681c7602ef 100644 --- a/assets/world/tree/desert_palm/3.vox +++ b/assets/world/tree/desert_palm/3.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8869e83bc5fd1263d512153ea1e30296ffcce3d87f319edd488ee4d1c9fb4a7 -size 2728 +oid sha256:65973116a2c4e2166b06c93c40bbdf2cb0cd055186e997eb13a9d4665a360407 +size 2740 diff --git a/assets/world/tree/desert_palm/4.vox b/assets/world/tree/desert_palm/4.vox index 83ee9bcdbb..486f8ce1ae 100644 --- a/assets/world/tree/desert_palm/4.vox +++ b/assets/world/tree/desert_palm/4.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9d7b5343efd602dfbb5f2fa61ad2882a67a1554a977f14d68b916229e284409 -size 2664 +oid sha256:c739af1da0a6502dc333e8f26e6aa3c5b43dd1bf9a739fa1183a6925b06c3d95 +size 2676 diff --git a/assets/world/tree/desert_palm/5.vox b/assets/world/tree/desert_palm/5.vox index 3ca1dc1d68..82f66b0390 100644 --- a/assets/world/tree/desert_palm/5.vox +++ b/assets/world/tree/desert_palm/5.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d37842b815dcb28c0c2004d150ef2f0cbb466a1fc64934beb469e6f8312502c8 -size 2668 +oid sha256:839d59a6899359d75ae40ede6eab197e4438753afb36b59dac03217a1867cdf1 +size 2680 diff --git a/assets/world/tree/desert_palm/6.vox b/assets/world/tree/desert_palm/6.vox index 93e02f1ba3..2978b19852 100644 --- a/assets/world/tree/desert_palm/6.vox +++ b/assets/world/tree/desert_palm/6.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06876ab785668631c73147d4cea201e40579746055cab5f505f0d3017e21fe88 -size 2768 +oid sha256:fa64cff8589f64f1857975b3acba8fa16c24b1d19d6482f9ff08fdd95d8705e7 +size 2780 diff --git a/assets/world/tree/desert_palm/7.vox b/assets/world/tree/desert_palm/7.vox index 4445dbf3b3..f3095c9076 100644 --- a/assets/world/tree/desert_palm/7.vox +++ b/assets/world/tree/desert_palm/7.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7ae32444793fe113333e6969597eb8864b5212b0326eb0506c68cbde224d810 -size 3040 +oid sha256:2bf99aeaf0936a67fea13d715095ed53506859723e2543838de9d561c8b9e2cb +size 3048 diff --git a/assets/world/tree/desert_palm/8.vox b/assets/world/tree/desert_palm/8.vox index 579d53bf10..fbd63a59d0 100644 --- a/assets/world/tree/desert_palm/8.vox +++ b/assets/world/tree/desert_palm/8.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c33f5c43adf96b9a405f50b29bdb226b2775b3cca69692f3629afcc558f54cab -size 2704 +oid sha256:2581ec5257928a5d5fd64e533bef1ba7267438ce6fae5849a39bccdd547b7ba3 +size 2716 diff --git a/assets/world/tree/desert_palm/9.vox b/assets/world/tree/desert_palm/9.vox index 58315225c9..6af18dd413 100644 --- a/assets/world/tree/desert_palm/9.vox +++ b/assets/world/tree/desert_palm/9.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3005d011c0a2cb0117adafffbb884c97ee8b6491e104cbcb97410be325acc096 -size 3152 +oid sha256:b787e74402728a73be2963533ae5f2e1438fdb90345b0e3d0e6f0452ae314b60 +size 3164 diff --git a/common/src/astar.rs b/common/src/astar.rs index 4c153c9de9..3ad4912733 100644 --- a/common/src/astar.rs +++ b/common/src/astar.rs @@ -34,6 +34,15 @@ pub enum PathResult { Pending, } +impl PathResult { + pub fn into_path(self) -> Option> { + match self { + PathResult::Path(path) => Some(path), + _ => None, + } + } +} + #[derive(Clone, Debug)] pub struct Astar { iter: usize, @@ -43,7 +52,8 @@ pub struct Astar { cheapest_scores: HashMap, final_scores: HashMap, visited: HashSet, - lowest_cost: Option, + cheapest_node: Option, + cheapest_cost: Option, } impl Astar { @@ -60,7 +70,8 @@ impl Astar { cheapest_scores: std::iter::once((start.clone(), 0.0)).collect(), final_scores: std::iter::once((start.clone(), heuristic(&start))).collect(), visited: std::iter::once(start).collect(), - lowest_cost: None, + cheapest_node: None, + cheapest_cost: None, } } @@ -77,11 +88,12 @@ impl Astar { { let iter_limit = self.max_iters.min(self.iter + iters); while self.iter < iter_limit { - if let Some(PathEntry { node, .. }) = self.potential_nodes.pop() { + if let Some(PathEntry { node, cost }) = self.potential_nodes.pop() { + self.cheapest_cost = Some(cost); if satisfied(&node) { return PathResult::Path(self.reconstruct_path_to(node)); } else { - self.lowest_cost = Some(node.clone()); + self.cheapest_node = Some(node.clone()); for neighbor in neighbors(&node) { let node_cheapest = self.cheapest_scores.get(&node).unwrap_or(&f32::MAX); let neighbor_cheapest = @@ -105,7 +117,7 @@ impl Astar { } } else { return PathResult::None( - self.lowest_cost + self.cheapest_node .clone() .map(|lc| self.reconstruct_path_to(lc)) .unwrap_or_default(), @@ -117,7 +129,7 @@ impl Astar { if self.iter >= self.max_iters { PathResult::Exhausted( - self.lowest_cost + self.cheapest_node .clone() .map(|lc| self.reconstruct_path_to(lc)) .unwrap_or_default(), @@ -127,6 +139,8 @@ impl Astar { } } + pub fn get_cheapest_cost(&self) -> Option { self.cheapest_cost } + fn reconstruct_path_to(&mut self, end: S) -> Path { let mut path = vec![end.clone()]; let mut cnode = &end; diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 80965cf2cc..be33b4bb49 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -25,6 +25,7 @@ impl Alignment { // Never attacks pub fn passive_towards(self, other: Alignment) -> bool { match (self, other) { + (Alignment::Enemy, Alignment::Enemy) => true, (Alignment::Owned(a), Alignment::Owned(b)) if a == b => true, _ => false, } @@ -61,6 +62,7 @@ pub enum Activity { chaser: Chaser, time: f64, been_close: bool, + powerup: f32, }, } diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index 42dcdd6a01..3e818a0393 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -16,6 +16,7 @@ use std::{fs::File, io::BufReader}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Consumable { + Coconut, Apple, Cheese, Potion, @@ -126,6 +127,7 @@ impl Item { Some(assets::load_expect_cloned("common.items.grasses.medium")) }, BlockKind::ShortGrass => Some(assets::load_expect_cloned("common.items.grasses.short")), + BlockKind::Coconut => Some(assets::load_expect_cloned("common.items.coconut")), BlockKind::Chest => Some(assets::load_expect_cloned( [ "common.items.apple", diff --git a/common/src/generation.rs b/common/src/generation.rs index c88ddc9c5c..22cf420021 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -1,14 +1,95 @@ +use crate::{ + comp::{self, humanoid, Alignment, Body, Item}, + npc::{self, NPC_NAMES}, +}; use vek::*; -pub enum EntityKind { - Enemy, - Boss, - Waypoint, +pub enum EntityTemplate { + Traveller, } pub struct EntityInfo { pub pos: Vec3, - pub kind: EntityKind, + pub is_waypoint: bool, // Edge case, overrides everything else + pub is_giant: bool, + pub alignment: Alignment, + pub body: Body, + pub name: Option, + pub main_tool: Option, +} + +impl EntityInfo { + pub fn at(pos: Vec3) -> Self { + Self { + pos, + is_waypoint: false, + is_giant: false, + alignment: Alignment::Wild, + body: Body::Humanoid(humanoid::Body::random()), + name: None, + main_tool: Some(Item::empty()), + } + } + + pub fn do_if(mut self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self { + if cond { + self = f(self); + } + self + } + + pub fn into_waypoint(mut self) -> Self { + self.is_waypoint = true; + self + } + + pub fn into_giant(mut self) -> Self { + self.is_giant = true; + self + } + + pub fn with_alignment(mut self, alignment: Alignment) -> Self { + self.alignment = alignment; + self + } + + pub fn with_body(mut self, body: Body) -> Self { + self.body = body; + self + } + + pub fn with_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } + + pub fn with_main_tool(mut self, main_tool: Item) -> Self { + self.main_tool = Some(main_tool); + self + } + + pub fn with_automatic_name(mut self) -> Self { + self.name = match &self.body { + Body::Humanoid(body) => Some(get_npc_name(&NPC_NAMES.humanoid, body.race)), + Body::QuadrupedMedium(body) => { + Some(get_npc_name(&NPC_NAMES.quadruped_medium, body.species)) + }, + Body::BirdMedium(body) => Some(get_npc_name(&NPC_NAMES.bird_medium, body.species)), + Body::Critter(body) => Some(get_npc_name(&NPC_NAMES.critter, body.species)), + Body::QuadrupedSmall(body) => { + Some(get_npc_name(&NPC_NAMES.quadruped_small, body.species)) + }, + _ => None, + } + .map(|s| { + if self.is_giant { + format!("Giant {}", s) + } else { + s.to_string() + } + }); + self + } } #[derive(Default)] @@ -17,8 +98,16 @@ pub struct ChunkSupplement { } impl ChunkSupplement { - pub fn with_entity(mut self, entity: EntityInfo) -> Self { - self.entities.push(entity); - self - } + pub fn add_entity(&mut self, entity: EntityInfo) { self.entities.push(entity); } +} + +pub fn get_npc_name< + 'a, + Species, + SpeciesData: for<'b> core::ops::Index<&'b Species, Output = npc::SpeciesNames>, +>( + body_data: &'a comp::BodyData, + species: Species, +) -> &'a str { + &body_data.species[&species].generic } diff --git a/common/src/lib.rs b/common/src/lib.rs index df969e5af4..899578a760 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -25,8 +25,10 @@ pub mod npc; pub mod path; pub mod ray; pub mod region; +pub mod spiral; pub mod state; pub mod states; +pub mod store; pub mod sync; pub mod sys; pub mod terrain; diff --git a/common/src/path.rs b/common/src/path.rs index 85e8f174af..a3c649fc57 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -33,9 +33,13 @@ impl FromIterator for Path { impl Path { pub fn len(&self) -> usize { self.nodes.len() } + pub fn iter(&self) -> impl Iterator { self.nodes.iter() } + pub fn start(&self) -> Option<&T> { self.nodes.first() } pub fn end(&self) -> Option<&T> { self.nodes.last() } + + pub fn nodes(&self) -> &[T] { &self.nodes } } // Route: A path that can be progressed along @@ -57,7 +61,12 @@ impl Route { pub fn is_finished(&self) -> bool { self.next().is_none() } - pub fn traverse(&mut self, vol: &V, pos: Vec3) -> Option> + pub fn traverse( + &mut self, + vol: &V, + pos: Vec3, + traversal_tolerance: f32, + ) -> Option> where V: BaseVol + ReadVol, { @@ -66,7 +75,9 @@ impl Route { None } else { let next_tgt = next.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0); - if ((pos - next_tgt) * Vec3::new(1.0, 1.0, 0.3)).magnitude_squared() < 1.0f32.powf(2.0) + if ((pos - (next_tgt + Vec3::unit_z() * 0.5)) * Vec3::new(1.0, 1.0, 0.3)) + .magnitude_squared() + < (traversal_tolerance * 2.0).powf(2.0) { self.next_idx += 1; } @@ -91,13 +102,14 @@ impl Chaser { pos: Vec3, tgt: Vec3, min_dist: f32, + traversal_tolerance: f32, ) -> Option> where V: BaseVol + ReadVol, { let pos_to_tgt = pos.distance(tgt); - if ((pos - tgt) * Vec3::new(1.0, 1.0, 0.3)).magnitude_squared() < min_dist.powf(2.0) { + if ((pos - tgt) * Vec3::new(1.0, 1.0, 0.15)).magnitude_squared() < min_dist.powf(2.0) { return None; } @@ -111,7 +123,7 @@ impl Chaser { self.route = Route::default(); } - self.route.traverse(vol, pos) + self.route.traverse(vol, pos, traversal_tolerance) } } else { None @@ -145,7 +157,7 @@ where { let is_walkable = |pos: &Vec3| { vol.get(*pos - Vec3::new(0, 0, 1)) - .map(|b| b.is_solid()) + .map(|b| b.is_solid() && b.get_height() == 1.0) .unwrap_or(false) && vol .get(*pos + Vec3::new(0, 0, 0)) diff --git a/common/src/spiral.rs b/common/src/spiral.rs new file mode 100644 index 0000000000..f0044763ca --- /dev/null +++ b/common/src/spiral.rs @@ -0,0 +1,37 @@ +use vek::*; + +/// An iterator of coordinates that create a rectangular spiral out from the +/// origin +#[derive(Clone)] +pub struct Spiral2d { + layer: i32, + i: i32, +} + +impl Spiral2d { + pub fn new() -> Self { Self { layer: 0, i: 0 } } +} + +impl Iterator for Spiral2d { + type Item = Vec2; + + fn next(&mut self) -> Option { + let layer_size = (self.layer * 8 + 4 * self.layer.min(1) - 4).max(1); + if self.i >= layer_size { + self.layer += 1; + self.i = 0; + } + let layer_size = (self.layer * 8 + 4 * self.layer.min(1) - 4).max(1); + + let pos = Vec2::new( + -self.layer + (self.i - (layer_size / 4) * 0).max(0).min(self.layer * 2) + - (self.i - (layer_size / 4) * 2).max(0).min(self.layer * 2), + -self.layer + (self.i - (layer_size / 4) * 1).max(0).min(self.layer * 2) + - (self.i - (layer_size / 4) * 3).max(0).min(self.layer * 2), + ); + + self.i += 1; + + Some(pos) + } +} diff --git a/common/src/store.rs b/common/src/store.rs new file mode 100644 index 0000000000..983d6a156b --- /dev/null +++ b/common/src/store.rs @@ -0,0 +1,74 @@ +use std::{ + cmp::{Eq, PartialEq}, + fmt, hash, + marker::PhantomData, + ops::{Index, IndexMut}, +}; + +pub struct Id(usize, PhantomData); + +impl Id { + pub fn id(&self) -> usize { self.0 } +} + +impl Copy for Id {} +impl Clone for Id { + fn clone(&self) -> Self { Self(self.0, PhantomData) } +} +impl Eq for Id {} +impl PartialEq for Id { + fn eq(&self, other: &Self) -> bool { self.0 == other.0 } +} +impl fmt::Debug for Id { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Id<{}>({})", std::any::type_name::(), self.0) + } +} +impl hash::Hash for Id { + fn hash(&self, h: &mut H) { self.0.hash(h); } +} + +pub struct Store { + items: Vec, +} + +impl Default for Store { + fn default() -> Self { Self { items: Vec::new() } } +} + +impl Store { + pub fn get(&self, id: Id) -> &T { self.items.get(id.0).unwrap() } + + pub fn get_mut(&mut self, id: Id) -> &mut T { self.items.get_mut(id.0).unwrap() } + + pub fn ids(&self) -> impl Iterator> { + (0..self.items.len()).map(|i| Id(i, PhantomData)) + } + + pub fn iter(&self) -> impl Iterator { self.items.iter() } + + pub fn iter_mut(&mut self) -> impl Iterator { self.items.iter_mut() } + + pub fn iter_ids(&self) -> impl Iterator, &T)> { + self.items + .iter() + .enumerate() + .map(|(i, item)| (Id(i, PhantomData), item)) + } + + pub fn insert(&mut self, item: T) -> Id { + let id = Id(self.items.len(), PhantomData); + self.items.push(item); + id + } +} + +impl Index> for Store { + type Output = T; + + fn index(&self, id: Id) -> &Self::Output { self.get(id) } +} + +impl IndexMut> for Store { + fn index_mut(&mut self, id: Id) -> &mut Self::Output { self.get_mut(id) } +} diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index b11dcf22bb..61ce280579 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -1,7 +1,13 @@ use crate::{ - comp::{self, agent::Activity, Agent, Alignment, Controller, MountState, Ori, Pos, Stats}, + comp::{ + self, + agent::Activity, + item::{tool::ToolKind, ItemKind}, + Agent, Alignment, CharacterState, ControlAction, Controller, Loadout, MountState, Ori, Pos, + Scale, Stats, + }, path::Chaser, - state::Time, + state::{DeltaTime, Time}, sync::UidAllocator, terrain::TerrainGrid, util::Dir, @@ -20,10 +26,14 @@ impl<'a> System<'a> for Sys { type SystemData = ( Read<'a, UidAllocator>, Read<'a, Time>, + Read<'a, DeltaTime>, Entities<'a>, ReadStorage<'a, Pos>, ReadStorage<'a, Ori>, + ReadStorage<'a, Scale>, ReadStorage<'a, Stats>, + ReadStorage<'a, Loadout>, + ReadStorage<'a, CharacterState>, ReadExpect<'a, TerrainGrid>, ReadStorage<'a, Alignment>, WriteStorage<'a, Agent>, @@ -36,10 +46,14 @@ impl<'a> System<'a> for Sys { ( uid_allocator, time, + dt, entities, positions, orientations, + scales, stats, + loadouts, + character_states, terrain, alignments, mut agents, @@ -47,11 +61,23 @@ impl<'a> System<'a> for Sys { mount_states, ): Self::SystemData, ) { - for (entity, pos, ori, alignment, agent, controller, mount_state) in ( + for ( + entity, + pos, + ori, + alignment, + loadout, + character_state, + agent, + controller, + mount_state, + ) in ( &entities, &positions, &orientations, alignments.maybe(), + &loadouts, + &character_states, &mut agents, &mut controllers, mount_states.maybe(), @@ -82,10 +108,19 @@ impl<'a> System<'a> for Sys { const AVG_FOLLOW_DIST: f32 = 6.0; const MAX_FOLLOW_DIST: f32 = 12.0; const MAX_CHASE_DIST: f32 = 24.0; - const SEARCH_DIST: f32 = 30.0; - const SIGHT_DIST: f32 = 64.0; + const LISTEN_DIST: f32 = 16.0; + const SEARCH_DIST: f32 = 48.0; + const SIGHT_DIST: f32 = 128.0; const MIN_ATTACK_DIST: f32 = 3.25; + let scale = scales.get(entity).map(|s| s.0).unwrap_or(1.0); + + // This controls how picky NPCs are about their pathfinding. Giants are larger + // and so can afford to be less precise when trying to move around + // the world (especially since they would otherwise get stuck on + // obstacles that smaller entities would not). + let traversal_tolerance = scale; + let mut do_idle = false; let mut choose_target = false; @@ -96,19 +131,23 @@ impl<'a> System<'a> for Sys { thread_rng().gen::() - 0.5, thread_rng().gen::() - 0.5, ) * 0.1 - - *bearing * 0.01 + - *bearing * 0.003 - if let Some(patrol_origin) = agent.patrol_origin { Vec2::::from(pos.0 - patrol_origin) * 0.0002 } else { Vec2::zero() }; + // Stop if we're too close to a wall *bearing *= 0.1 + if terrain .ray( pos.0 + Vec3::unit_z(), pos.0 - + Vec3::from(*bearing).normalized() * 1.5 + + Vec3::from(*bearing) + .try_normalized() + .unwrap_or(Vec3::unit_y()) + * 5.0 + Vec3::unit_z(), ) .until(|block| block.is_solid()) @@ -122,9 +161,18 @@ impl<'a> System<'a> for Sys { 0.0 }; - if bearing.magnitude_squared() > 0.25f32.powf(2.0) { - inputs.move_dir = - bearing.try_normalized().unwrap_or(Vec2::zero()) * 0.65; + if bearing.magnitude_squared() > 0.5f32.powf(2.0) { + inputs.move_dir = *bearing * 0.65; + } + + // Put away weapon + if thread_rng().gen::() < 0.005 { + controller.actions.push(ControlAction::Unwield); + } + + // Sit + if thread_rng().gen::() < 0.0035 { + controller.actions.push(ControlAction::Sit); } // Sometimes try searching for new targets @@ -139,9 +187,13 @@ impl<'a> System<'a> for Sys { let dist_sqrd = pos.0.distance_squared(tgt_pos.0); // Follow, or return to idle if dist_sqrd > AVG_FOLLOW_DIST.powf(2.0) { - if let Some(bearing) = - chaser.chase(&*terrain, pos.0, tgt_pos.0, AVG_FOLLOW_DIST) - { + if let Some(bearing) = chaser.chase( + &*terrain, + pos.0, + tgt_pos.0, + AVG_FOLLOW_DIST, + traversal_tolerance, + ) { inputs.move_dir = Vec2::from(bearing) .try_normalized() .unwrap_or(Vec2::zero()); @@ -158,8 +210,27 @@ impl<'a> System<'a> for Sys { target, chaser, been_close, + powerup, .. } => { + enum Tactic { + Melee, + RangedPowerup, + Staff, + } + + let tactic = match loadout.active_item.as_ref().and_then(|ic| { + if let ItemKind::Tool(tool) = &ic.item.kind { + Some(&tool.kind) + } else { + None + } + }) { + Some(ToolKind::Bow(_)) => Tactic::RangedPowerup, + Some(ToolKind::Staff(_)) => Tactic::Staff, + _ => Tactic::Melee, + }; + if let (Some(tgt_pos), Some(tgt_stats), tgt_alignment) = ( positions.get(*target), stats.get(*target), @@ -183,32 +254,68 @@ impl<'a> System<'a> for Sys { } let dist_sqrd = pos.0.distance_squared(tgt_pos.0); - if dist_sqrd < MIN_ATTACK_DIST.powf(2.0) { + if dist_sqrd < (MIN_ATTACK_DIST * scale).powf(2.0) { // Close-range attack inputs.move_dir = Vec2::from(tgt_pos.0 - pos.0) .try_normalized() .unwrap_or(Vec2::unit_y()) * 0.7; - inputs.primary.set_state(true); + + match tactic { + Tactic::Melee | Tactic::Staff => inputs.primary.set_state(true), + Tactic::RangedPowerup => inputs.roll.set_state(true), + } } else if dist_sqrd < MAX_CHASE_DIST.powf(2.0) - || (dist_sqrd < SIGHT_DIST.powf(2.0) && !*been_close) + || (dist_sqrd < SIGHT_DIST.powf(2.0) + && (!*been_close || !matches!(tactic, Tactic::Melee))) { + let can_see_tgt = terrain + .ray(pos.0 + Vec3::unit_z(), tgt_pos.0 + Vec3::unit_z()) + .until(|block| !block.is_air()) + .cast() + .0 + .powf(2.0) + >= dist_sqrd; + + if can_see_tgt { + if let Tactic::RangedPowerup = tactic { + if *powerup > 2.0 { + inputs.primary.set_state(false); + *powerup = 0.0; + } else { + inputs.primary.set_state(true); + *powerup += dt.0; + } + } else if let Tactic::Staff = tactic { + if !character_state.is_wield() { + inputs.primary.set_state(true); + } + + inputs.secondary.set_state(true); + } + } + if dist_sqrd < MAX_CHASE_DIST.powf(2.0) { *been_close = true; } // Long-range chase - if let Some(bearing) = - chaser.chase(&*terrain, pos.0, tgt_pos.0, 1.25) - { + if let Some(bearing) = chaser.chase( + &*terrain, + pos.0, + tgt_pos.0, + 1.25, + traversal_tolerance, + ) { inputs.move_dir = Vec2::from(bearing) .try_normalized() .unwrap_or(Vec2::zero()); inputs.jump.set_state(bearing.z > 1.0); } - if dist_sqrd < (MAX_CHASE_DIST * 0.65).powf(2.0) - && thread_rng().gen::() < 0.01 + if dist_sqrd < 16.0f32.powf(2.0) + && matches!(tactic, Tactic::Melee) + && thread_rng().gen::() < 0.02 { inputs.roll.set_state(true); } @@ -234,13 +341,23 @@ impl<'a> System<'a> for Sys { let closest_entity = (&entities, &positions, &stats, alignments.maybe()) .join() .filter(|(e, e_pos, e_stats, e_alignment)| { - e_pos.0.distance_squared(pos.0) < SEARCH_DIST.powf(2.0) + ((e_pos.0.distance_squared(pos.0) < SEARCH_DIST.powf(2.0) && + // Within our view + (e_pos.0 - pos.0).try_normalized().map(|v| v.dot(*inputs.look_dir) > 0.15).unwrap_or(true)) + // Within listen distance + || e_pos.0.distance_squared(pos.0) < LISTEN_DIST.powf(2.0)) && *e != entity && !e_stats.is_dead && alignment .and_then(|a| e_alignment.map(|b| a.hostile_towards(*b))) .unwrap_or(false) }) + // Can we even see them? + .filter(|(_, e_pos, _, _)| terrain + .ray(pos.0 + Vec3::unit_z(), e_pos.0 + Vec3::unit_z()) + .until(|block| !block.is_air()) + .cast() + .0 >= e_pos.0.distance(pos.0)) .min_by_key(|(_, e_pos, _, _)| (e_pos.0.distance_squared(pos.0) * 100.0) as i32) .map(|(e, _, _, _)| e); @@ -250,6 +367,7 @@ impl<'a> System<'a> for Sys { chaser: Chaser::default(), time: time.0, been_close: false, + powerup: 0.0, }; } } @@ -273,6 +391,7 @@ impl<'a> System<'a> for Sys { chaser: Chaser::default(), time: time.0, been_close: false, + powerup: 0.0, }; } } @@ -303,6 +422,7 @@ impl<'a> System<'a> for Sys { chaser: Chaser::default(), time: time.0, been_close: false, + powerup: 0.0, }; } } diff --git a/common/src/sys/phys.rs b/common/src/sys/phys.rs index 2546af51a9..45cf64efdc 100644 --- a/common/src/sys/phys.rs +++ b/common/src/sys/phys.rs @@ -3,7 +3,7 @@ use crate::{ event::{EventBus, ServerEvent}, state::DeltaTime, sync::Uid, - terrain::{Block, TerrainGrid}, + terrain::{Block, BlockKind, TerrainGrid}, vol::ReadVol, }; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage}; @@ -110,7 +110,9 @@ impl<'a> System<'a> for Sys { // Neighbouring blocks iterator let near_iter = (-hdist..hdist + 1) .map(move |i| { - (-hdist..hdist + 1).map(move |j| (0..vdist + 1).map(move |k| (i, j, k))) + (-hdist..hdist + 1).map(move |j| { + (1 - BlockKind::MAX_HEIGHT.ceil() as i32..vdist + 1).map(move |k| (i, j, k)) + }) }) .flatten() .flatten(); @@ -150,18 +152,19 @@ impl<'a> System<'a> for Sys { // Function for determining whether the player at a specific position collides // with the ground - let collision_with = |pos: Vec3, hit: fn(&Block) -> bool, near_iter| { + let collision_with = |pos: Vec3, hit: &dyn Fn(&Block) -> bool, near_iter| { for (i, j, k) in near_iter { let block_pos = pos.map(|e| e.floor() as i32) + Vec3::new(i, j, k); - if terrain.get(block_pos).map(hit).unwrap_or(false) { + if let Some(block) = terrain.get(block_pos).ok().copied().filter(hit) { let player_aabb = Aabb { min: pos + Vec3::new(-player_rad, -player_rad, 0.0), max: pos + Vec3::new(player_rad, player_rad, player_height), }; let block_aabb = Aabb { min: block_pos.map(|e| e as f32), - max: block_pos.map(|e| e as f32) + 1.0, + max: block_pos.map(|e| e as f32) + + Vec3::new(1.0, 1.0, block.get_height()), }; if player_aabb.collides_with_aabb(block_aabb) { @@ -189,7 +192,7 @@ impl<'a> System<'a> for Sys { const MAX_ATTEMPTS: usize = 16; // While the player is colliding with the terrain... - while collision_with(pos.0, |vox| vox.is_solid(), near_iter.clone()) + while collision_with(pos.0, &|block| block.is_solid(), near_iter.clone()) && attempts < MAX_ATTEMPTS { // Calculate the player's AABB @@ -200,31 +203,34 @@ impl<'a> System<'a> for Sys { // Determine the block that we are colliding with most (based on minimum // collision axis) - let (_block_pos, block_aabb) = near_iter + let (_block_pos, block_aabb, block_height) = near_iter .clone() // Calculate the block's position in world space .map(|(i, j, k)| pos.0.map(|e| e.floor() as i32) + Vec3::new(i, j, k)) - // Calculate the AABB of the block - .map(|block_pos| { - ( - block_pos, - Aabb { - min: block_pos.map(|e| e as f32), - max: block_pos.map(|e| e as f32) + 1.0, - }, - ) - }) // Make sure the block is actually solid - .filter(|(block_pos, _)| { - terrain - .get(*block_pos) - .map(|vox| vox.is_solid()) - .unwrap_or(false) + .filter_map(|block_pos| { + if let Some(block) = terrain + .get(block_pos) + .ok() + .filter(|block| block.is_solid()) + { + // Calculate block AABB + Some(( + block_pos, + Aabb { + min: block_pos.map(|e| e as f32), + max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.get_height()), + }, + block.get_height(), + )) + } else { + None + } }) // Determine whether the block's AABB collides with the player's AABB - .filter(|(_, block_aabb)| block_aabb.collides_with_aabb(player_aabb)) + .filter(|(_, block_aabb, _)| block_aabb.collides_with_aabb(player_aabb)) // Find the maximum of the minimum collision axes (this bit is weird, trust me that it works) - .min_by_key(|(_, block_aabb)| { + .min_by_key(|(_, block_aabb, _)| { ((block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5) .map(|e| e.abs()) .sum() @@ -257,7 +263,7 @@ impl<'a> System<'a> for Sys { // When the resolution direction is non-vertical, we must be colliding with a // wall If the space above is free... - if !collision_with(Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()), |vox| vox.is_solid(), near_iter.clone()) + if !collision_with(Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()), &|block| block.is_solid(), near_iter.clone()) // ...and we're being pushed out horizontally... && resolve_dir.z == 0.0 // ...and the vertical resolution direction is sufficiently great... @@ -265,17 +271,17 @@ impl<'a> System<'a> for Sys { // ...and we're falling/standing OR there is a block *directly* beneath our current origin (note: not hitbox)... && (vel.0.z <= 0.0 || terrain .get((pos.0 - Vec3::unit_z() * 0.1).map(|e| e.floor() as i32)) - .map(|vox| vox.is_solid()) + .map(|block| block.is_solid()) .unwrap_or(false)) // ...and there is a collision with a block beneath our current hitbox... && collision_with( old_pos + resolve_dir - Vec3::unit_z() * 1.05, - |vox| vox.is_solid(), + &|block| block.is_solid(), near_iter.clone(), ) { // ...block-hop! - pos.0.z = (pos.0.z + 0.1).ceil(); + pos.0.z = (pos.0.z + 0.1).floor() + block_height; vel.0.z = 0.0; on_ground = true; break; @@ -304,20 +310,26 @@ impl<'a> System<'a> for Sys { // If the space below us is free, then "snap" to the ground } else if collision_with( pos.0 - Vec3::unit_z() * 1.05, - |vox| vox.is_solid(), + &|block| block.is_solid(), near_iter.clone(), ) && vel.0.z < 0.0 && vel.0.z > -1.5 && was_on_ground - && !terrain - .get( - Vec3::new(pos.0.x, pos.0.y, (pos.0.z - 0.05).floor()) - .map(|e| e.floor() as i32), - ) - .map(|vox| vox.is_solid()) - .unwrap_or(false) + && !collision_with( + pos.0 - Vec3::unit_z() * 0.05, + &|block| { + block.is_solid() && block.get_height() >= (pos.0.z - 0.05).rem_euclid(1.0) + }, + near_iter.clone(), + ) { - pos.0.z = (pos.0.z - 0.05).floor(); + let snap_height = terrain + .get(Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.05).map(|e| e.floor() as i32)) + .ok() + .filter(|block| block.is_solid()) + .map(|block| block.get_height()) + .unwrap_or(0.0); + pos.0.z = (pos.0.z - 0.05).floor() + snap_height; physics_state.on_ground = true; } @@ -329,7 +341,11 @@ impl<'a> System<'a> for Sys { ]; if let (wall_dir, true) = dirs.iter().fold((Vec3::zero(), false), |(a, hit), dir| { - if collision_with(pos.0 + *dir * 0.01, |vox| vox.is_solid(), near_iter.clone()) { + if collision_with( + pos.0 + *dir * 0.01, + &|block| block.is_solid(), + near_iter.clone(), + ) { (a + dir, true) } else { (a, hit) @@ -341,7 +357,8 @@ impl<'a> System<'a> for Sys { } // Figure out if we're in water - physics_state.in_fluid = collision_with(pos.0, |vox| vox.is_fluid(), near_iter.clone()); + physics_state.in_fluid = + collision_with(pos.0, &|block| block.is_fluid(), near_iter.clone()); let _ = physics_states.insert(entity, physics_state); } diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 0c5c97155a..056b8715eb 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -40,9 +40,29 @@ pub enum BlockKind { Fern, DeadBush, Blueberry, + Ember, + Corn, + WheatYellow, + WheatGreen, + Cabbage, + Flax, + Carrot, + Tomato, + Radish, + Coconut, + Turnip, + Window1, + Window2, + Window3, + Window4, + Scarecrow, + StreetLamp, + Door, } impl BlockKind { + pub const MAX_HEIGHT: f32 = 3.0; + pub fn is_tangible(&self) -> bool { match self { BlockKind::Air => false, @@ -81,6 +101,25 @@ impl BlockKind { BlockKind::Fern => true, BlockKind::DeadBush => true, BlockKind::Blueberry => true, + BlockKind::Ember => true, + BlockKind::Corn => true, + BlockKind::WheatYellow => true, + BlockKind::WheatGreen => true, + BlockKind::Cabbage => false, + BlockKind::Pumpkin => false, + BlockKind::Flax => true, + BlockKind::Carrot => true, + BlockKind::Tomato => false, + BlockKind::Radish => true, + BlockKind::Turnip => true, + BlockKind::Coconut => true, + BlockKind::Window1 => true, + BlockKind::Window2 => true, + BlockKind::Window3 => true, + BlockKind::Window4 => true, + BlockKind::Scarecrow => true, + BlockKind::StreetLamp => true, + BlockKind::Door => false, _ => false, } } @@ -118,12 +157,31 @@ impl BlockKind { BlockKind::Velorite => false, BlockKind::VeloriteFrag => false, BlockKind::Chest => false, + BlockKind::Pumpkin => false, BlockKind::Welwitch => false, BlockKind::LingonBerry => false, BlockKind::LeafyPlant => false, BlockKind::Fern => false, BlockKind::DeadBush => false, BlockKind::Blueberry => false, + BlockKind::Ember => false, + BlockKind::Corn => false, + BlockKind::WheatYellow => false, + BlockKind::WheatGreen => false, + BlockKind::Cabbage => false, + BlockKind::Flax => false, + BlockKind::Carrot => false, + BlockKind::Tomato => false, + BlockKind::Radish => false, + BlockKind::Turnip => false, + BlockKind::Coconut => false, + BlockKind::Window1 => false, + BlockKind::Window2 => false, + BlockKind::Window3 => false, + BlockKind::Window4 => false, + BlockKind::Scarecrow => false, + BlockKind::StreetLamp => false, + BlockKind::Door => false, _ => true, } } @@ -159,10 +217,44 @@ impl BlockKind { BlockKind::Fern => false, BlockKind::DeadBush => false, BlockKind::Blueberry => false, + BlockKind::Ember => false, + BlockKind::Corn => false, + BlockKind::WheatYellow => false, + BlockKind::WheatGreen => false, + BlockKind::Cabbage => true, + BlockKind::Flax => false, + BlockKind::Carrot => true, + BlockKind::Tomato => true, + BlockKind::Radish => true, + BlockKind::Turnip => true, + BlockKind::Coconut => true, + BlockKind::Scarecrow => true, + BlockKind::StreetLamp => true, + BlockKind::Door => false, _ => true, } } + // TODO: Integrate this into `is_solid` by returning an `Option` + pub fn get_height(&self) -> f32 { + // Beware: the height *must* be <= `MAX_HEIGHT` or the collision system will not + // properly detect it! + match self { + BlockKind::Tomato => 1.65, + BlockKind::LargeCactus => 2.5, + BlockKind::Scarecrow => 3.0, + BlockKind::Turnip => 0.36, + BlockKind::Pumpkin => 0.81, + BlockKind::Cabbage => 0.45, + BlockKind::Chest => 1.09, + BlockKind::StreetLamp => 3.0, + BlockKind::Carrot => 0.18, + BlockKind::Radish => 0.18, + BlockKind::Door => 3.0, + _ => 1.0, + } + } + pub fn is_collectible(&self) -> bool { match self { BlockKind::BlueFlower => false, @@ -180,7 +272,7 @@ impl BlockKind { BlockKind::Velorite => true, BlockKind::VeloriteFrag => true, BlockKind::Chest => true, - BlockKind::Pumpkin => true, + BlockKind::Coconut => true, _ => false, } } @@ -209,6 +301,17 @@ impl Block { } } + pub fn get_ori(&self) -> Option { + match self.kind { + BlockKind::Window1 + | BlockKind::Window2 + | BlockKind::Window3 + | BlockKind::Window4 + | BlockKind::Door => Some(self.color[0] & 0b111), + _ => None, + } + } + pub fn kind(&self) -> BlockKind { self.kind } } diff --git a/common/src/terrain/structure.rs b/common/src/terrain/structure.rs index 06637ecdfe..8bdf71586e 100644 --- a/common/src/terrain/structure.rs +++ b/common/src/terrain/structure.rs @@ -20,6 +20,7 @@ pub enum StructureBlock { Water, GreenSludge, Fruit, + Coconut, Chest, Hollow, Liana, @@ -114,6 +115,7 @@ impl Asset for Structure { 7 => StructureBlock::Fruit, 9 => StructureBlock::Liana, 10 => StructureBlock::Chest, + 11 => StructureBlock::Coconut, 13 => StructureBlock::PalmLeavesOuter, 14 => StructureBlock::PalmLeavesInner, 15 => StructureBlock::Hollow, diff --git a/server/src/cmd.rs b/server/src/cmd.rs index b9a107d4d8..7bc96dde8f 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -216,7 +216,7 @@ lazy_static! { "waypoint", "{}", "/waypoint : Set your waypoint to your current position", - false, + true, handle_waypoint, ), ChatCommand::new( diff --git a/server/src/lib.rs b/server/src/lib.rs index 825583c5d9..ecb35c6594 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -49,6 +49,7 @@ use uvth::{ThreadPool, ThreadPoolBuilder}; use vek::*; #[cfg(feature = "worldgen")] use world::{ + civ::SiteKind, sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP, WORLD_SIZE}, World, }; @@ -126,11 +127,23 @@ impl Server { // complaining) // spawn in the chunk, that is in the middle of the world - let spawn_chunk: Vec2 = WORLD_SIZE.map(|e| e as i32) / 2; + let center_chunk: Vec2 = WORLD_SIZE.map(|e| e as i32) / 2; + + // Find a town to spawn in that's close to the centre of the world + let spawn_chunk = world + .civs() + .sites() + .filter(|site| matches!(site.kind, SiteKind::Settlement)) + .map(|site| site.center) + .min_by_key(|site_pos| site_pos.distance_squared(center_chunk)) + .unwrap_or(center_chunk); + // calculate the absolute position of the chunk in the world // (we could add TerrainChunkSize::RECT_SIZE / 2 here, to spawn in the midde of // the chunk) - let spawn_location = spawn_chunk * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); + let spawn_location = spawn_chunk.map2(TerrainChunkSize::RECT_SIZE, |e, sz| { + e as i32 * sz as i32 + sz as i32 / 2 + }); // get a z cache for the collumn in which we want to spawn let mut block_sampler = world.sample_blocks(); diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 352ed91ecc..dab026bf29 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -4,13 +4,13 @@ use common::{ assets, comp::{self, item, CharacterAbility, Item, ItemConfig, Player, Pos}, event::{EventBus, ServerEvent}, - generation::EntityKind, + generation::get_npc_name, msg::ServerMsg, - npc::{self, NPC_NAMES}, + npc::NPC_NAMES, state::TerrainChanges, terrain::TerrainGrid, }; -use rand::{seq::SliceRandom, Rng}; +use rand::Rng; use specs::{Join, Read, ReadStorage, System, Write, WriteExpect, WriteStorage}; use std::{sync::Arc, time::Duration}; use vek::*; @@ -101,295 +101,217 @@ impl<'a> System<'a> for Sys { // Handle chunk supplement for entity in supplement.entities { - if let EntityKind::Waypoint = entity.kind { + if entity.is_waypoint { server_emitter.emit(ServerEvent::CreateWaypoint(entity.pos)); - } else { - fn get_npc_name< - 'a, - Species, - SpeciesData: for<'b> core::ops::Index<&'b Species, Output = npc::SpeciesNames>, - >( - body_data: &'a comp::BodyData, - species: Species, - ) -> &'a str { - &body_data.species[&species].generic - } + continue; + } - const SPAWN_NPCS: &'static [fn() -> ( - String, - comp::Body, - Option, - comp::Alignment, - )] = &[ - (|| { - let body = comp::humanoid::Body::random(); - ( - format!( - "{} Traveler", - get_npc_name(&NPC_NAMES.humanoid, body.race) - ), - comp::Body::Humanoid(body), - Some(assets::load_expect_cloned( - "common.items.weapons.starter_axe", - )), - comp::Alignment::Npc, - ) - }) as _, - (|| { - let body = comp::humanoid::Body::random(); - ( - format!("{} Bandit", get_npc_name(&NPC_NAMES.humanoid, body.race)), - comp::Body::Humanoid(body), - Some(assets::load_expect_cloned( - "common.items.weapons.short_sword_0", - )), - comp::Alignment::Enemy, - ) - }) as _, - (|| { - let body = comp::quadruped_medium::Body::random(); - ( - get_npc_name(&NPC_NAMES.quadruped_medium, body.species).into(), - comp::Body::QuadrupedMedium(body), - None, - comp::Alignment::Enemy, - ) - }) as _, - (|| { - let body = comp::bird_medium::Body::random(); - ( - get_npc_name(&NPC_NAMES.bird_medium, body.species).into(), - comp::Body::BirdMedium(body), - None, - comp::Alignment::Wild, - ) - }) as _, - (|| { - let body = comp::critter::Body::random(); - ( - get_npc_name(&NPC_NAMES.critter, body.species).into(), - comp::Body::Critter(body), - None, - comp::Alignment::Wild, - ) - }) as _, - (|| { - let body = comp::quadruped_small::Body::random(); - ( - get_npc_name(&NPC_NAMES.quadruped_small, body.species).into(), - comp::Body::QuadrupedSmall(body), - None, - comp::Alignment::Wild, - ) - }), - ]; - let (name, mut body, main, mut alignment) = SPAWN_NPCS - .choose(&mut rand::thread_rng()) - .expect("SPAWN_NPCS is nonempty")( - ); - let mut stats = comp::Stats::new(name, body); + let mut body = entity.body; + let name = entity.name.unwrap_or("Unnamed".to_string()); + let alignment = entity.alignment; + let main_tool = entity.main_tool; - let active_item = - if let Some(item::ItemKind::Tool(tool)) = main.as_ref().map(|i| &i.kind) { - let mut abilities = tool.get_abilities(); - let mut ability_drain = abilities.drain(..); + let mut stats = comp::Stats::new(name, body); - main.map(|item| comp::ItemConfig { - item, - ability1: ability_drain.next(), - ability2: ability_drain.next(), - ability3: ability_drain.next(), - block_ability: None, - dodge_ability: Some(comp::CharacterAbility::Roll), - }) - } else { - Some(ItemConfig { - // We need the empty item so npcs can attack - item: Item::empty(), - ability1: Some(CharacterAbility::BasicMelee { - energy_cost: 0, - buildup_duration: Duration::from_millis(0), - recover_duration: Duration::from_millis(400), - base_healthchange: -4, - range: 3.5, - max_angle: 60.0, - }), - ability2: None, - ability3: None, - block_ability: None, - dodge_ability: None, - }) - }; + let active_item = + if let Some(item::ItemKind::Tool(tool)) = main_tool.as_ref().map(|i| &i.kind) { + let mut abilities = tool.get_abilities(); + let mut ability_drain = abilities.drain(..); - let mut loadout = match alignment { - comp::Alignment::Npc => comp::Loadout { - active_item, - second_item: None, - shoulder: Some(assets::load_expect_cloned( - "common.items.armor.shoulder.leather_0", - )), - chest: Some(assets::load_expect_cloned( - "common.items.armor.chest.leather_0", - )), - belt: Some(assets::load_expect_cloned( - "common.items.armor.belt.plate_0", - )), - hand: Some(assets::load_expect_cloned( - "common.items.armor.hand.plate_0", - )), - pants: Some(assets::load_expect_cloned( - "common.items.armor.pants.plate_green_0", - )), - foot: Some(assets::load_expect_cloned( - "common.items.armor.foot.leather_0", - )), - back: None, - ring: None, - neck: None, - lantern: None, - head: None, - tabard: None, - }, - comp::Alignment::Enemy => comp::Loadout { - active_item, - second_item: None, - shoulder: Some(assets::load_expect_cloned( - "common.items.armor.shoulder.leather_0", - )), - chest: Some(assets::load_expect_cloned( - "common.items.armor.chest.plate_green_0", - )), - belt: Some(assets::load_expect_cloned( - "common.items.armor.belt.plate_0", - )), - hand: Some(assets::load_expect_cloned( - "common.items.armor.hand.plate_0", - )), - pants: Some(assets::load_expect_cloned( - "common.items.armor.pants.plate_green_0", - )), - foot: Some(assets::load_expect_cloned( - "common.items.armor.foot.plate_0", - )), - back: None, - ring: None, - neck: None, - lantern: None, - head: None, - tabard: None, - }, - _ => comp::Loadout { - active_item, - second_item: None, - shoulder: None, - chest: None, - belt: None, - hand: None, - pants: None, - foot: None, - back: None, - ring: None, - neck: None, - lantern: None, - head: None, - tabard: None, - }, + main_tool.map(|item| comp::ItemConfig { + item, + ability1: ability_drain.next(), + ability2: ability_drain.next(), + ability3: ability_drain.next(), + block_ability: None, + dodge_ability: Some(comp::CharacterAbility::Roll), + }) + } else { + Some(ItemConfig { + // We need the empty item so npcs can attack + item: Item::empty(), + ability1: Some(CharacterAbility::BasicMelee { + energy_cost: 0, + buildup_duration: Duration::from_millis(0), + recover_duration: Duration::from_millis(400), + base_healthchange: -4, + range: 3.5, + max_angle: 60.0, + }), + ability2: None, + ability3: None, + block_ability: None, + dodge_ability: None, + }) }; - let mut scale = 1.0; + let mut loadout = match alignment { + comp::Alignment::Npc => comp::Loadout { + active_item, + second_item: None, + shoulder: Some(assets::load_expect_cloned( + "common.items.armor.shoulder.leather_0", + )), + chest: Some(assets::load_expect_cloned( + "common.items.armor.chest.leather_0", + )), + belt: Some(assets::load_expect_cloned( + "common.items.armor.belt.plate_0", + )), + hand: Some(assets::load_expect_cloned( + "common.items.armor.hand.plate_0", + )), + pants: Some(assets::load_expect_cloned( + "common.items.armor.pants.plate_green_0", + )), + foot: Some(assets::load_expect_cloned( + "common.items.armor.foot.leather_0", + )), + back: None, + ring: None, + neck: None, + lantern: None, + head: None, + tabard: None, + }, + comp::Alignment::Enemy => comp::Loadout { + active_item, + second_item: None, + shoulder: Some(assets::load_expect_cloned( + "common.items.armor.shoulder.leather_0", + )), + chest: Some(assets::load_expect_cloned( + "common.items.armor.chest.plate_green_0", + )), + belt: Some(assets::load_expect_cloned( + "common.items.armor.belt.plate_0", + )), + hand: Some(assets::load_expect_cloned( + "common.items.armor.hand.plate_0", + )), + pants: Some(assets::load_expect_cloned( + "common.items.armor.pants.plate_green_0", + )), + foot: Some(assets::load_expect_cloned( + "common.items.armor.foot.plate_0", + )), + back: None, + ring: None, + neck: None, + lantern: None, + head: None, + tabard: None, + }, + _ => comp::Loadout { + active_item, + second_item: None, + shoulder: None, + chest: None, + belt: None, + hand: None, + pants: None, + foot: None, + back: None, + ring: None, + neck: None, + lantern: None, + head: None, + tabard: None, + }, + }; - // TODO: Remove this and implement scaling or level depending on stuff like - // species instead - stats.level.set_level(rand::thread_rng().gen_range(1, 9)); + let mut scale = 1.0; - // Replace stuff if it's a boss - if let EntityKind::Boss = entity.kind { - if rand::random::() < 0.65 { - let body_new = comp::humanoid::Body::random(); - body = comp::Body::Humanoid(body_new); - alignment = comp::Alignment::Npc; - stats = comp::Stats::new( - format!( - "Fearless Giant {}", - get_npc_name(&NPC_NAMES.humanoid, body_new.race) - ), - body, - ); - } - loadout = comp::Loadout { - active_item: Some(comp::ItemConfig { - item: assets::load_expect_cloned( - "common.items.weapons.zweihander_sword_0", - ), - ability1: Some(CharacterAbility::BasicMelee { - energy_cost: 0, - buildup_duration: Duration::from_millis(800), - recover_duration: Duration::from_millis(200), - base_healthchange: -13, - range: 3.5, - max_angle: 60.0, - }), - ability2: None, - ability3: None, - block_ability: None, - dodge_ability: None, + // TODO: Remove this and implement scaling or level depending on stuff like + // species instead + stats.level.set_level(rand::thread_rng().gen_range(1, 9)); + + // Replace stuff if it's a boss + if entity.is_giant { + if rand::random::() < 0.65 { + let body_new = comp::humanoid::Body::random(); + body = comp::Body::Humanoid(body_new); + stats = comp::Stats::new( + format!( + "Fearless Giant {}", + get_npc_name(&NPC_NAMES.humanoid, body_new.race) + ), + body, + ); + } + loadout = comp::Loadout { + active_item: Some(comp::ItemConfig { + item: assets::load_expect_cloned( + "common.items.weapons.zweihander_sword_0", + ), + ability1: Some(CharacterAbility::BasicMelee { + energy_cost: 0, + buildup_duration: Duration::from_millis(800), + recover_duration: Duration::from_millis(200), + base_healthchange: -13, + range: 3.5, + max_angle: 60.0, }), - second_item: None, - shoulder: Some(assets::load_expect_cloned( - "common.items.armor.shoulder.plate_0", - )), - chest: Some(assets::load_expect_cloned( - "common.items.armor.chest.plate_green_0", - )), - belt: Some(assets::load_expect_cloned( - "common.items.armor.belt.plate_0", - )), - hand: Some(assets::load_expect_cloned( - "common.items.armor.hand.plate_0", - )), - pants: Some(assets::load_expect_cloned( - "common.items.armor.pants.plate_green_0", - )), - foot: Some(assets::load_expect_cloned( - "common.items.armor.foot.plate_0", - )), - back: None, - ring: None, - neck: None, - lantern: None, - head: None, - tabard: None, - }; + ability2: None, + ability3: None, + block_ability: None, + dodge_ability: None, + }), + second_item: None, + shoulder: Some(assets::load_expect_cloned( + "common.items.armor.shoulder.plate_0", + )), + chest: Some(assets::load_expect_cloned( + "common.items.armor.chest.plate_green_0", + )), + belt: Some(assets::load_expect_cloned( + "common.items.armor.belt.plate_0", + )), + hand: Some(assets::load_expect_cloned( + "common.items.armor.hand.plate_0", + )), + pants: Some(assets::load_expect_cloned( + "common.items.armor.pants.plate_green_0", + )), + foot: Some(assets::load_expect_cloned( + "common.items.armor.foot.plate_0", + )), + back: None, + ring: None, + neck: None, + lantern: None, + head: None, + tabard: None, + }; - stats.level.set_level(rand::thread_rng().gen_range(30, 35)); - scale = 2.0 + rand::random::(); - } - - stats.update_max_hp(); - - stats - .health - .set_to(stats.health.maximum(), comp::HealthSource::Revive); - - // TODO: This code sets an appropriate base_damage for the enemy. This doesn't - // work because the damage is now saved in an ability - /* - if let Some(item::ItemKind::Tool(item::ToolData { base_damage, .. })) = - &mut loadout.active_item.map(|i| i.item.kind) - { - *base_damage = stats.level.level() as u32 * 3; - } - */ - server_emitter.emit(ServerEvent::CreateNpc { - pos: Pos(entity.pos), - stats, - loadout, - body, - alignment, - agent: comp::Agent::default().with_patrol_origin(entity.pos), - scale: comp::Scale(scale), - }) + stats.level.set_level(rand::thread_rng().gen_range(30, 35)); + scale = 2.0 + rand::random::(); } + + stats.update_max_hp(); + + stats + .health + .set_to(stats.health.maximum(), comp::HealthSource::Revive); + + // TODO: This code sets an appropriate base_damage for the enemy. This doesn't + // work because the damage is now saved in an ability + /* + if let Some(item::ItemKind::Tool(item::ToolData { base_damage, .. })) = + &mut loadout.active_item.map(|i| i.item.kind) + { + *base_damage = stats.level.level() as u32 * 3; + } + */ + server_emitter.emit(ServerEvent::CreateNpc { + pos: Pos(entity.pos), + stats, + loadout, + body, + alignment, + agent: comp::Agent::default().with_patrol_origin(entity.pos), + scale: comp::Scale(scale), + }) } } diff --git a/voxygen/examples/character_renderer.rs b/voxygen/examples/character_renderer.rs index 05483f2680..289227bcb1 100644 --- a/voxygen/examples/character_renderer.rs +++ b/voxygen/examples/character_renderer.rs @@ -56,7 +56,7 @@ fn main() { }; scene.camera_mut().set_focus_pos(Vec3::unit_z() * 0.8); scene.camera_mut().set_distance(1.5); - scene.camera_mut().update(0.0); + scene.camera_mut().update(0.0, 1.0 / 60.0); scene.maintain(&mut renderer, scene_data); // Render diff --git a/voxygen/src/anim/biped_large/idle.rs b/voxygen/src/anim/biped_large/idle.rs index 6a463c5bd3..79f778c92b 100644 --- a/voxygen/src/anim/biped_large/idle.rs +++ b/voxygen/src/anim/biped_large/idle.rs @@ -36,10 +36,10 @@ impl Animation for IdleAnimation { next.head.offset = Vec3::new( 0.0, skeleton_attr.head.0, - skeleton_attr.head.1 + torso * 0.6, - ) / 8.0; + skeleton_attr.head.1 + torso * 0.2, + ) * 1.02; next.head.ori = Quaternion::rotation_z(look.x * 0.6) * Quaternion::rotation_x(look.y * 0.6); - next.head.scale = Vec3::one() / 8.0; + next.head.scale = Vec3::one() * 1.02; next.upper_torso.offset = Vec3::new( 0.0, @@ -52,7 +52,7 @@ impl Animation for IdleAnimation { next.lower_torso.offset = Vec3::new( 0.0, skeleton_attr.lower_torso.0, - skeleton_attr.lower_torso.1 + torso * 0.35, + skeleton_attr.lower_torso.1 + torso * 0.15, ); next.lower_torso.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0); next.lower_torso.scale = Vec3::one() * 1.02; @@ -79,7 +79,7 @@ impl Animation for IdleAnimation { skeleton_attr.hand.2 + torso * 0.6, ); next.hand_l.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0); - next.hand_l.scale = Vec3::one(); + next.hand_l.scale = Vec3::one() * 1.02; next.hand_r.offset = Vec3::new( skeleton_attr.hand.0, @@ -87,23 +87,23 @@ impl Animation for IdleAnimation { skeleton_attr.hand.2 + torso * 0.6, ); next.hand_r.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0); - next.hand_r.scale = Vec3::one(); + next.hand_r.scale = Vec3::one() * 1.02; next.leg_l.offset = Vec3::new( -skeleton_attr.leg.0, skeleton_attr.leg.1, skeleton_attr.leg.2 + torso * 0.2, - ) / 8.0; + ) * 1.02; next.leg_l.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0); - next.leg_l.scale = Vec3::one() / 8.0; + next.leg_l.scale = Vec3::one() * 1.02; next.leg_r.offset = Vec3::new( skeleton_attr.leg.0, skeleton_attr.leg.1, skeleton_attr.leg.2 + torso * 0.2, - ) / 8.0; + ) * 1.02; next.leg_r.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0); - next.leg_r.scale = Vec3::one() / 8.0; + next.leg_r.scale = Vec3::one() * 1.02; next.foot_l.offset = Vec3::new( -skeleton_attr.foot.0, diff --git a/voxygen/src/anim/biped_large/mod.rs b/voxygen/src/anim/biped_large/mod.rs index 85611e944e..ba73161fb7 100644 --- a/voxygen/src/anim/biped_large/mod.rs +++ b/voxygen/src/anim/biped_large/mod.rs @@ -56,23 +56,19 @@ impl Skeleton for BipedLargeSkeleton { let torso_mat = self.torso.compute_base_matrix(); [ - FigureBoneData::new(torso_mat * self.head.compute_base_matrix()), + FigureBoneData::new(torso_mat * upper_torso_mat * self.head.compute_base_matrix()), FigureBoneData::new(torso_mat * upper_torso_mat), FigureBoneData::new( torso_mat * upper_torso_mat * self.lower_torso.compute_base_matrix(), ), FigureBoneData::new(torso_mat * upper_torso_mat * shoulder_l_mat), FigureBoneData::new(torso_mat * upper_torso_mat * shoulder_r_mat), - FigureBoneData::new( - torso_mat * upper_torso_mat * shoulder_l_mat * self.hand_l.compute_base_matrix(), - ), - FigureBoneData::new( - torso_mat * upper_torso_mat * shoulder_r_mat * self.hand_r.compute_base_matrix(), - ), - FigureBoneData::new(torso_mat * leg_l_mat), - FigureBoneData::new(torso_mat * leg_r_mat), - FigureBoneData::new(torso_mat * self.foot_l.compute_base_matrix()), - FigureBoneData::new(torso_mat * self.foot_r.compute_base_matrix()), + FigureBoneData::new(torso_mat * upper_torso_mat * self.hand_l.compute_base_matrix()), + FigureBoneData::new(torso_mat * upper_torso_mat * self.hand_r.compute_base_matrix()), + FigureBoneData::new(torso_mat * upper_torso_mat * leg_l_mat), + FigureBoneData::new(torso_mat * upper_torso_mat * leg_r_mat), + FigureBoneData::new(self.foot_l.compute_base_matrix()), + FigureBoneData::new(self.foot_r.compute_base_matrix()), FigureBoneData::default(), FigureBoneData::default(), FigureBoneData::default(), @@ -137,10 +133,10 @@ impl<'a> From<&'a comp::biped_large::Body> for SkeletonAttr { use comp::biped_large::Species::*; Self { head: match (body.species, body.body_type) { - (Giant, _) => (0.0, 28.5), + (Giant, _) => (0.0, 10.0), }, upper_torso: match (body.species, body.body_type) { - (Giant, _) => (0.0, 18.5), + (Giant, _) => (0.0, 20.0), }, lower_torso: match (body.species, body.body_type) { (Giant, _) => (1.0, -9.5), @@ -149,13 +145,13 @@ impl<'a> From<&'a comp::biped_large::Body> for SkeletonAttr { (Giant, _) => (6.0, 0.5, 2.5), }, hand: match (body.species, body.body_type) { - (Giant, _) => (3.5, -1.0, 3.0), + (Giant, _) => (10.5, -1.0, 3.5), }, leg: match (body.species, body.body_type) { - (Giant, _) => (3.5, 0.0, 14.0), + (Giant, _) => (0.0, 0.0, -6.0), }, foot: match (body.species, body.body_type) { - (Giant, _) => (4.0, 0.5, 11.0), + (Giant, _) => (4.0, 0.5, 2.5), }, } } diff --git a/voxygen/src/anim/biped_large/run.rs b/voxygen/src/anim/biped_large/run.rs index 2165834fb4..e41f67a41e 100644 --- a/voxygen/src/anim/biped_large/run.rs +++ b/voxygen/src/anim/biped_large/run.rs @@ -17,29 +17,36 @@ impl Animation for RunAnimation { ) -> Self::Skeleton { let mut next = (*skeleton).clone(); - let lab = 14.0; + let lab = 10.0; - let legl = (anim_time as f32 * lab as f32).sin(); - let legr = (anim_time as f32 * lab as f32 + PI).sin(); let belt = (anim_time as f32 * lab as f32 + 1.5 * PI).sin(); - let foothoril = (anim_time as f32 * lab as f32).sin(); - let foothorir = (anim_time as f32 * lab as f32 + PI).sin(); + let foothoril = (anim_time as f32 * lab as f32 + PI * 1.4).sin(); + let foothorir = (anim_time as f32 * lab as f32 + PI * 0.4).sin(); - let footvertl = (anim_time as f32 * lab as f32 + PI * 1.4).sin().max(0.0); - let footvertr = (anim_time as f32 * lab as f32 + PI * 0.4).sin().max(0.0); + let footvertl = (anim_time as f32 * lab as f32).sin().max(0.1); + let footvertr = (anim_time as f32 * lab as f32 + PI).sin().max(0.1); - next.head.offset = - Vec3::new(0.0, skeleton_attr.head.0, skeleton_attr.head.1 + belt * 1.0) / 8.0; - next.head.ori = Quaternion::rotation_z(belt * 0.1) * Quaternion::rotation_x(0.3); - next.head.scale = Vec3::one() / 8.0; + let footrotl = (((5.0) + / (1.0 + (4.0) * ((anim_time as f32 * lab as f32 + PI * 1.4).sin()).powf(2.0 as f32))) + .sqrt()) + * ((anim_time as f32 * lab as f32 + PI * 1.4).sin()); + + let footrotr = (((5.0) + / (1.0 + (4.0) * ((anim_time as f32 * lab as f32 + PI * 0.4).sin()).powf(2.0 as f32))) + .sqrt()) + * ((anim_time as f32 * lab as f32 + PI * 0.4).sin()); + + next.head.offset = Vec3::new(0.0, skeleton_attr.head.0, skeleton_attr.head.1) * 1.02; + next.head.ori = Quaternion::rotation_z(belt * -0.3) * Quaternion::rotation_x(0.3); + next.head.scale = Vec3::one() * 1.02; next.upper_torso.offset = Vec3::new( 0.0, skeleton_attr.upper_torso.0, skeleton_attr.upper_torso.1 + belt * 1.0, ) / 8.0; - next.upper_torso.ori = Quaternion::rotation_z(belt * 0.3) * Quaternion::rotation_x(0.0); + next.upper_torso.ori = Quaternion::rotation_z(belt * 0.40) * Quaternion::rotation_x(0.0); next.upper_torso.scale = Vec3::one() / 8.0; next.lower_torso.offset = Vec3::new( @@ -47,7 +54,7 @@ impl Animation for RunAnimation { skeleton_attr.lower_torso.0, skeleton_attr.lower_torso.1, ); - next.lower_torso.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0); + next.lower_torso.ori = Quaternion::rotation_z(belt * -0.55) * Quaternion::rotation_x(0.0); next.lower_torso.scale = Vec3::one() * 1.02; next.shoulder_l.offset = Vec3::new( @@ -55,7 +62,8 @@ impl Animation for RunAnimation { skeleton_attr.shoulder.1, skeleton_attr.shoulder.2, ); - next.shoulder_l.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(legr * 0.06); + next.shoulder_l.ori = + Quaternion::rotation_z(0.0) * Quaternion::rotation_x(footrotl * -0.15); next.shoulder_l.scale = Vec3::one(); next.shoulder_r.offset = Vec3::new( @@ -63,7 +71,8 @@ impl Animation for RunAnimation { skeleton_attr.shoulder.1, skeleton_attr.shoulder.2, ); - next.shoulder_r.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(legl * 0.1); + next.shoulder_r.ori = + Quaternion::rotation_z(0.0) * Quaternion::rotation_x(footrotr * -0.15); next.shoulder_r.scale = Vec3::one(); next.hand_l.offset = Vec3::new( @@ -71,51 +80,56 @@ impl Animation for RunAnimation { skeleton_attr.hand.1, skeleton_attr.hand.2, ); - next.hand_l.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.3 + legr * 0.5); - next.hand_l.scale = Vec3::one(); + next.hand_l.ori = + Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.3 + footrotl * -0.8); + next.hand_l.scale = Vec3::one() * 1.02; next.hand_r.offset = Vec3::new( skeleton_attr.hand.0, skeleton_attr.hand.1, skeleton_attr.hand.2, ); - next.hand_r.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.3 - legr * 0.5); - next.hand_r.scale = Vec3::one(); + next.hand_r.ori = + Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.3 + footrotr * -0.8); + next.hand_r.scale = Vec3::one() * 1.02; next.leg_l.offset = Vec3::new( -skeleton_attr.leg.0, skeleton_attr.leg.1, - skeleton_attr.leg.2 + belt * 0.4, - ) / 8.0; - next.leg_l.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(-legl * 0.3); - next.leg_l.scale = Vec3::one() / 8.0; + skeleton_attr.leg.2, + ) * 1.02; + next.leg_l.ori = + Quaternion::rotation_z(belt * -0.8) * Quaternion::rotation_x(foothoril * 0.2); + next.leg_l.scale = Vec3::one() * 1.02; next.leg_r.offset = Vec3::new( skeleton_attr.leg.0, skeleton_attr.leg.1, - skeleton_attr.leg.2 + belt * 0.4, - ) / 8.0; - next.leg_r.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(-legr * 0.3); - next.leg_r.scale = Vec3::one() / 8.0; + skeleton_attr.leg.2, + ) * 1.02; + + next.leg_r.ori = + Quaternion::rotation_z(belt * -0.8) * Quaternion::rotation_x(foothorir * 0.2); + next.leg_r.scale = Vec3::one() * 1.02; next.foot_l.offset = Vec3::new( -skeleton_attr.foot.0, - skeleton_attr.foot.1 - foothoril * 4.5 - 0.5, - skeleton_attr.foot.2 + footvertl * 6.0, + skeleton_attr.foot.1 + foothoril * 8.0 + 3.0, + skeleton_attr.foot.2 + footvertl * 4.0, ) / 8.0; - next.foot_l.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0 - legl * 0.5); + next.foot_l.ori = Quaternion::rotation_x(footrotl * 0.5); next.foot_l.scale = Vec3::one() / 8.0 * 0.98; next.foot_r.offset = Vec3::new( skeleton_attr.foot.0, - skeleton_attr.foot.1 - foothorir * 4.5 - 0.5, - skeleton_attr.foot.2 + footvertr * 6.0, + skeleton_attr.foot.1 + foothorir * 8.0 + 3.0, + skeleton_attr.foot.2 + footvertr * 4.0, ) / 8.0; - next.foot_r.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(0.0 - legr * 0.5); + next.foot_r.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(footrotr * 0.5); next.foot_r.scale = Vec3::one() / 8.0 * 0.98; - next.torso.offset = Vec3::new(0.0, 0.0, 0.0); - next.torso.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(-0.3); + next.torso.offset = Vec3::new(0.0, 0.0, belt * 0.15); + next.torso.ori = Quaternion::rotation_z(0.0) * Quaternion::rotation_x(-0.2); next.torso.scale = Vec3::one(); next } diff --git a/voxygen/src/anim/character/run.rs b/voxygen/src/anim/character/run.rs index 7c475ba3bb..64d41f5e67 100644 --- a/voxygen/src/anim/character/run.rs +++ b/voxygen/src/anim/character/run.rs @@ -21,35 +21,44 @@ impl Animation for RunAnimation { let speed = Vec2::::from(velocity).magnitude(); *rate = 1.0; + let walkintensity = if speed > 5.0 { 1.0 } else { 0.7 }; + let walk = if speed > 5.0 { 1.0 } else { 0.5 }; + let lower = if speed > 5.0 { 0.0 } else { 2.0 }; + let snapfoot = if speed > 5.0 { 1.1 } else { 2.0 }; let lab = 1.0; let long = (((5.0) - / (1.5 + 3.5 * ((anim_time as f32 * lab as f32 * 8.0).sin()).powf(2.0 as f32))) + / (1.5 + 3.5 * ((anim_time as f32 * lab as f32 * 8.0 * walk).sin()).powf(2.0 as f32))) .sqrt()) - * ((anim_time as f32 * lab as f32 * 8.0).sin()); + * ((anim_time as f32 * lab as f32 * 8.0 * walk).sin()); let short = (((5.0) - / (1.5 + 3.5 * ((anim_time as f32 * lab as f32 * 16.0).sin()).powf(2.0 as f32))) + / (1.5 + + 3.5 * ((anim_time as f32 * lab as f32 * 16.0 * walk).sin()).powf(2.0 as f32))) .sqrt()) - * ((anim_time as f32 * lab as f32 * 16.0).sin()); + * ((anim_time as f32 * lab as f32 * 16.0 * walk).sin()); let noisea = (anim_time as f32 * 11.0 + PI / 6.0).sin(); let noiseb = (anim_time as f32 * 19.0 + PI / 4.0).sin(); let shorte = (((5.0) - / (4.0 + 1.0 * ((anim_time as f32 * lab as f32 * 16.0).sin()).powf(2.0 as f32))) + / (4.0 + + 1.0 * ((anim_time as f32 * lab as f32 * 16.0 * walk).sin()).powf(2.0 as f32))) .sqrt()) - * ((anim_time as f32 * lab as f32 * 16.0).sin()); + * ((anim_time as f32 * lab as f32 * 16.0 * walk).sin()); let shortalt = (((5.0) / (1.5 + 3.5 - * ((anim_time as f32 * lab as f32 * 16.0 + PI / 2.0).sin()).powf(2.0 as f32))) + * ((anim_time as f32 * lab as f32 * 16.0 * walk + PI / 2.0).sin()) + .powf(2.0 as f32))) .sqrt()) - * ((anim_time as f32 * lab as f32 * 16.0 + PI / 2.0).sin()); + * ((anim_time as f32 * lab as f32 * 16.0 * walk + PI / 2.0).sin()); let foot = (((5.0) - / (1.1 + 3.9 * ((anim_time as f32 * lab as f32 * 16.0).sin()).powf(2.0 as f32))) + / (snapfoot + + (5.0 - snapfoot) + * ((anim_time as f32 * lab as f32 * 16.0 * walk).sin()).powf(2.0 as f32))) .sqrt()) - * ((anim_time as f32 * lab as f32 * 16.0).sin()); + * ((anim_time as f32 * lab as f32 * 16.0 * walk).sin()); let wave_stop = (anim_time as f32 * 26.0).min(PI / 2.0 / 2.0).sin(); @@ -89,8 +98,8 @@ impl Animation for RunAnimation { * Quaternion::rotation_x(head_look.y + 0.35); next.head.scale = Vec3::one() * skeleton_attr.head_scale; - next.chest.offset = Vec3::new(0.0, 0.0, 10.5 + short * 1.1); - next.chest.ori = Quaternion::rotation_z(short * 0.3); + next.chest.offset = Vec3::new(0.0, 0.0, 10.5 + short * 1.1 - lower); + next.chest.ori = Quaternion::rotation_z(short * 0.3 * walkintensity); next.chest.scale = Vec3::one(); next.belt.offset = Vec3::new(0.0, 0.0, -2.0); @@ -106,37 +115,37 @@ impl Animation for RunAnimation { next.shorts.scale = Vec3::one(); next.l_hand.offset = Vec3::new( - -6.0 + wave_stop * -1.0, - -0.25 + short * 3.0, - 5.0 + short * -1.5, + -6.0 + wave_stop * -1.0 * walkintensity, + -0.25 + short * 3.0 * walkintensity, + 5.0 + short * -1.5 * walkintensity, ); - next.l_hand.ori = - Quaternion::rotation_x(0.8 + short * 1.2) * Quaternion::rotation_y(wave_stop * 0.1); + next.l_hand.ori = Quaternion::rotation_x(0.8 + short * 1.2 * walk) + * Quaternion::rotation_y(wave_stop * 0.1); next.l_hand.scale = Vec3::one(); next.r_hand.offset = Vec3::new( - 6.0 + wave_stop * 1.0, - -0.25 + short * -3.0, - 5.0 + short * 1.5, + 6.0 + wave_stop * 1.0 * walkintensity, + -0.25 + short * -3.0 * walkintensity, + 5.0 + short * 1.5 * walkintensity, ); - next.r_hand.ori = - Quaternion::rotation_x(0.8 + short * -1.2) * Quaternion::rotation_y(wave_stop * -0.1); + next.r_hand.ori = Quaternion::rotation_x(0.8 + short * -1.2 * walk) + * Quaternion::rotation_y(wave_stop * -0.1); next.r_hand.scale = Vec3::one(); next.l_foot.offset = Vec3::new(-3.4, foot * 1.0, 9.5); - next.l_foot.ori = Quaternion::rotation_x(foot * -1.2); + next.l_foot.ori = Quaternion::rotation_x(foot * -1.2 * walkintensity); next.l_foot.scale = Vec3::one(); next.r_foot.offset = Vec3::new(3.4, foot * -1.0, 9.5); - next.r_foot.ori = Quaternion::rotation_x(foot * 1.2); + next.r_foot.ori = Quaternion::rotation_x(foot * 1.2 * walkintensity); next.r_foot.scale = Vec3::one(); next.l_shoulder.offset = Vec3::new(-5.0, -1.0, 4.7); - next.l_shoulder.ori = Quaternion::rotation_x(short * 0.15); + next.l_shoulder.ori = Quaternion::rotation_x(short * 0.15 * walkintensity); next.l_shoulder.scale = Vec3::one() * 1.1; next.r_shoulder.offset = Vec3::new(5.0, -1.0, 4.7); - next.r_shoulder.ori = Quaternion::rotation_x(short * -0.15); + next.r_shoulder.ori = Quaternion::rotation_x(short * -0.15 * walkintensity); next.r_shoulder.scale = Vec3::one() * 1.1; next.glider.offset = Vec3::new(0.0, 0.0, 10.0); diff --git a/voxygen/src/anim/mod.rs b/voxygen/src/anim/mod.rs index 7d6e5b2086..de57de2c81 100644 --- a/voxygen/src/anim/mod.rs +++ b/voxygen/src/anim/mod.rs @@ -81,7 +81,7 @@ impl SkeletonAttr { (Human, Female) => 0.75, (Elf, Male) => 0.85, (Elf, Female) => 0.8, - (Dwarf, Male) => 0.7, + (Dwarf, Male) => 0.1, (Dwarf, Female) => 0.65, (Undead, Male) => 0.8, (Undead, Female) => 0.75, diff --git a/voxygen/src/menu/main/ui.rs b/voxygen/src/menu/main/ui.rs index e841d304bf..10a9bc2117 100644 --- a/voxygen/src/menu/main/ui.rs +++ b/voxygen/src/menu/main/ui.rs @@ -177,6 +177,8 @@ impl MainMenuUi { "voxygen.background.bg_7", "voxygen.background.bg_8", "voxygen.background.bg_9", + "voxygen.background.bg_10", + "voxygen.background.bg_11", ]; let mut rng = thread_rng(); diff --git a/voxygen/src/mesh/segment.rs b/voxygen/src/mesh/segment.rs index f01513e6df..962a6ed93e 100644 --- a/voxygen/src/mesh/segment.rs +++ b/voxygen/src/mesh/segment.rs @@ -30,28 +30,25 @@ impl Meshable for Segment { faces_to_make(self, pos, true, |vox| vox.is_empty()), offs + pos.map(|e| e as f32), &[[[Rgba::from_opaque(col); 3]; 3]; 3], - |origin, norm, col, ao, light| { + |origin, norm, col, light, ao| { FigureVertex::new( origin, norm, - linear_to_srgb(srgb_to_linear(col) * light.min(ao)), + linear_to_srgb(srgb_to_linear(col) * light), + ao, 0, ) }, &{ - let mut ls = [[[0.0; 3]; 3]; 3]; + let mut ls = [[[None; 3]; 3]; 3]; for x in 0..3 { for y in 0..3 { for z in 0..3 { - ls[z][y][x] = if self + ls[z][y][x] = self .get(pos + Vec3::new(x as i32, y as i32, z as i32) - 1) .map(|v| v.is_empty()) .unwrap_or(true) - { - 1.0 - } else { - 0.0 - }; + .then_some(1.0); } } } @@ -83,14 +80,29 @@ impl Meshable for Segment { faces_to_make(self, pos, true, |vox| vox.is_empty()), offs + pos.map(|e| e as f32), &[[[Rgba::from_opaque(col); 3]; 3]; 3], - |origin, norm, col, ao, light| { + |origin, norm, col, light, ao| { SpriteVertex::new( origin, norm, - linear_to_srgb(srgb_to_linear(col) * ao * light), + linear_to_srgb(srgb_to_linear(col) * light), + ao, ) }, - &[[[1.0; 3]; 3]; 3], + &{ + let mut ls = [[[None; 3]; 3]; 3]; + for x in 0..3 { + for y in 0..3 { + for z in 0..3 { + ls[z][y][x] = self + .get(pos + Vec3::new(x as i32, y as i32, z as i32) - 1) + .map(|v| v.is_empty()) + .unwrap_or(true) + .then_some(1.0); + } + } + } + ls + }, ); } } diff --git a/voxygen/src/mesh/terrain.rs b/voxygen/src/mesh/terrain.rs index 362fb81408..fa8e94ea01 100644 --- a/voxygen/src/mesh/terrain.rs +++ b/voxygen/src/mesh/terrain.rs @@ -28,7 +28,7 @@ impl Blendable for BlockKind { fn calc_light + ReadVol + Debug>( bounds: Aabb, vol: &VolGrid2d, -) -> impl Fn(Vec3) -> f32 { +) -> impl FnMut(Vec3) -> f32 + '_ { const UNKNOWN: u8 = 255; const OPAQUE: u8 = 254; const SUNLIGHT: u8 = 24; @@ -209,10 +209,7 @@ impl + ReadVol + Debug> Meshable (Mesh, Mesh) { - let mut opaque_mesh = Mesh::new(); - let mut fluid_mesh = Mesh::new(); - - let light = calc_light(range, self); + let mut light = calc_light(range, self); let mut lowest_opaque = range.size().d; let mut highest_opaque = 0; @@ -290,17 +287,47 @@ impl + ReadVol + Debug> Meshable + ReadVol + Debug> Meshable + ReadVol + Debug> Meshable + ReadVol + Debug> Meshable + ReadVol + Debug> Meshable + ReadVol + Debug> Meshable + ReadVol + Debug> Meshable| { + // m.verts().chunks_exact(3).rev().for_each(|vs| { + // opaque_mesh.push(vs[0]); + // opaque_mesh.push(vs[1]); + // opaque_mesh.push(vs[2]); + // }); + // opaque_mesh + // }); + (opaque_mesh, fluid_mesh) } } diff --git a/voxygen/src/mesh/vol.rs b/voxygen/src/mesh/vol.rs index 76979bf2e8..d0ff5af21a 100644 --- a/voxygen/src/mesh/vol.rs +++ b/voxygen/src/mesh/vol.rs @@ -12,19 +12,13 @@ use crate::render::{ fn get_ao_quad( shift: Vec3, dirs: &[Vec3], - darknesses: &[[[f32; 3]; 3]; 3], + darknesses: &[[[Option; 3]; 3]; 3], ) -> Vec4<(f32, f32)> { dirs.windows(2) .map(|offs| { let vox_opaque = |pos: Vec3| { let pos = (pos + 1).map(|e| e as usize); - unsafe { - darknesses - .get_unchecked(pos.z) - .get_unchecked(pos.y) - .get_unchecked(pos.x) - <= &0.0 - } + darknesses[pos.z][pos.y][pos.x].is_none() }; let (s1, s2) = ( @@ -41,17 +35,19 @@ fn get_ao_quad( ); let mut darkness = 0.0; + let mut total = 0.0f32; for x in 0..2 { for y in 0..2 { let dark_pos = shift + offs[0] * x + offs[1] * y + 1; - darkness += unsafe { - darknesses - .get_unchecked(dark_pos.z as usize) - .get_unchecked(dark_pos.y as usize) - .get_unchecked(dark_pos.x as usize) - } / 4.0; + if let Some(dark) = + darknesses[dark_pos.z as usize][dark_pos.y as usize][dark_pos.x as usize] + { + darkness += dark; + total += 1.0; + } } } + let darkness = darkness / total.max(1.0); ( darkness, @@ -60,7 +56,7 @@ fn get_ao_quad( } else { let corner = vox_opaque(shift + offs[0] + offs[1]); // Map both 1 and 2 neighbors to 0.5 occlusion. - if s1 || s2 || corner { 0.5 } else { 1.0 } + if s1 || s2 || corner { 0.4 } else { 1.0 } }, ) }) @@ -114,9 +110,7 @@ fn create_quad, Vec3, Rgb, f32, f32) -> P let ao_map = ao; - if ao[0].min(ao[2]).min(darkness[0]).min(darkness[2]) - < ao[1].min(ao[3]).min(darkness[1]).min(darkness[3]) - { + if ao[0].min(ao[2]) < ao[1].min(ao[3]) { Quad::new( vcons(origin + unit_y, norm, cols[3], darkness[3], ao_map[3]), vcons(origin, norm, cols[0], darkness[0], ao_map[0]), @@ -151,7 +145,7 @@ pub fn push_vox_verts( offs: Vec3, cols: &[[[Rgba; 3]; 3]; 3], vcons: impl Fn(Vec3, Vec3, Rgb, f32, f32) -> P::Vertex, - darknesses: &[[[f32; 3]; 3]; 3], + darknesses: &[[[Option; 3]; 3]; 3], ) { let (x, y, z) = (Vec3::unit_x(), Vec3::unit_y(), Vec3::unit_z()); diff --git a/voxygen/src/render/mesh.rs b/voxygen/src/render/mesh.rs index 1d3a45431b..d0c48275c6 100644 --- a/voxygen/src/render/mesh.rs +++ b/voxygen/src/render/mesh.rs @@ -1,11 +1,21 @@ use super::Pipeline; /// A `Vec`-based mesh structure used to store mesh data on the CPU. -#[derive(Clone)] pub struct Mesh { verts: Vec, } +impl Clone for Mesh

+where + P::Vertex: Clone, +{ + fn clone(&self) -> Self { + Self { + verts: self.verts.clone(), + } + } +} + impl Mesh

{ /// Create a new `Mesh`. pub fn new() -> Self { Self { verts: vec![] } } @@ -55,6 +65,15 @@ impl Mesh

{ self.verts.push(f(vert.clone())); } } + + pub fn iter(&self) -> std::slice::Iter { self.verts.iter() } +} + +impl IntoIterator for Mesh

{ + type IntoIter = std::vec::IntoIter; + type Item = P::Vertex; + + fn into_iter(self) -> Self::IntoIter { self.verts.into_iter() } } /// Represents a triangle stored on the CPU. diff --git a/voxygen/src/render/pipelines/figure.rs b/voxygen/src/render/pipelines/figure.rs index 94a252e68c..2fa38cbc1d 100644 --- a/voxygen/src/render/pipelines/figure.rs +++ b/voxygen/src/render/pipelines/figure.rs @@ -14,6 +14,7 @@ gfx_defines! { pos: [f32; 3] = "v_pos", norm: [f32; 3] = "v_norm", col: [f32; 3] = "v_col", + ao: f32 = "v_ao", bone_idx: u8 = "v_bone_idx", } @@ -44,11 +45,12 @@ gfx_defines! { } impl Vertex { - pub fn new(pos: Vec3, norm: Vec3, col: Rgb, bone_idx: u8) -> Self { + pub fn new(pos: Vec3, norm: Vec3, col: Rgb, ao: f32, bone_idx: u8) -> Self { Self { pos: pos.into_array(), col: col.into_array(), norm: norm.into_array(), + ao, bone_idx, } } diff --git a/voxygen/src/render/pipelines/fluid.rs b/voxygen/src/render/pipelines/fluid.rs index dfae0af2df..2e67ae9600 100644 --- a/voxygen/src/render/pipelines/fluid.rs +++ b/voxygen/src/render/pipelines/fluid.rs @@ -40,14 +40,16 @@ impl Vertex { .enumerate() .find(|(_i, e)| **e != 0.0) .unwrap_or((0, &1.0)); - let norm_bits = (norm_axis << 1) | if *norm_dir > 0.0 { 1 } else { 0 }; + let norm_bits = ((norm_axis << 1) | if *norm_dir > 0.0 { 1 } else { 0 }) as u32; + + const EXTRA_NEG_Z: f32 = 65536.0; Self { pos_norm: 0 - | ((pos.x as u32) & 0x00FF) << 0 - | ((pos.y as u32) & 0x00FF) << 8 - | ((pos.z.max(0.0).min((1 << 13) as f32) as u32) & 0x1FFF) << 16 - | ((norm_bits as u32) & 0x7) << 29, + | ((pos.x as u32) & 0x003F) << 0 + | ((pos.y as u32) & 0x003F) << 6 + | (((pos.z + EXTRA_NEG_Z).max(0.0).min((1 << 17) as f32) as u32) & 0x1FFFF) << 12 + | (norm_bits & 0x7) << 29, col_light: 0 | ((col.r.mul(200.0) as u32) & 0xFF) << 8 | ((col.g.mul(200.0) as u32) & 0xFF) << 16 diff --git a/voxygen/src/render/pipelines/sprite.rs b/voxygen/src/render/pipelines/sprite.rs index b39ca90a17..74091e3bd5 100644 --- a/voxygen/src/render/pipelines/sprite.rs +++ b/voxygen/src/render/pipelines/sprite.rs @@ -14,6 +14,7 @@ gfx_defines! { pos: [f32; 3] = "v_pos", norm: [f32; 3] = "v_norm", col: [f32; 3] = "v_col", + ao: f32 = "v_ao", } vertex Instance { @@ -41,11 +42,12 @@ gfx_defines! { } impl Vertex { - pub fn new(pos: Vec3, norm: Vec3, col: Rgb) -> Self { + pub fn new(pos: Vec3, norm: Vec3, col: Rgb, ao: f32) -> Self { Self { pos: pos.into_array(), col: col.into_array(), norm: norm.into_array(), + ao, } } } diff --git a/voxygen/src/render/pipelines/terrain.rs b/voxygen/src/render/pipelines/terrain.rs index 8fe5658a6b..9d6f884ff4 100644 --- a/voxygen/src/render/pipelines/terrain.rs +++ b/voxygen/src/render/pipelines/terrain.rs @@ -37,18 +37,21 @@ gfx_defines! { } impl Vertex { - pub fn new(norm_bits: u32, light: u32, pos: Vec3, col: Rgb) -> Self { + pub fn new(norm_bits: u32, light: u32, ao: u32, pos: Vec3, col: Rgb) -> Self { + const EXTRA_NEG_Z: f32 = 65536.0; + Self { pos_norm: 0 - | ((pos.x as u32) & 0x00FF) << 0 - | ((pos.y as u32) & 0x00FF) << 8 - | ((pos.z.max(0.0).min((1 << 13) as f32) as u32) & 0x1FFF) << 16 + | ((pos.x as u32) & 0x003F) << 0 + | ((pos.y as u32) & 0x003F) << 6 + | (((pos + EXTRA_NEG_Z).z.max(0.0).min((1 << 17) as f32) as u32) & 0xFFFFF) << 12 | (norm_bits & 0x7) << 29, col_light: 0 | ((col.r.mul(255.0) as u32) & 0xFF) << 8 | ((col.g.mul(255.0) as u32) & 0xFF) << 16 | ((col.b.mul(255.0) as u32) & 0xFF) << 24 - | (light & 0xFF) << 0, + | (ao >> 6) << 6 + | ((light >> 2) & 0x3F) << 0, } } } diff --git a/voxygen/src/scene/camera.rs b/voxygen/src/scene/camera.rs index 3847b0293e..020263f035 100644 --- a/voxygen/src/scene/camera.rs +++ b/voxygen/src/scene/camera.rs @@ -8,6 +8,7 @@ const FAR_PLANE: f32 = 100000.0; const FIRST_PERSON_INTERP_TIME: f32 = 0.1; const THIRD_PERSON_INTERP_TIME: f32 = 0.1; +const LERP_ORI_RATE: f32 = 15.0; pub const MIN_ZOOM: f32 = 0.1; // Possible TODO: Add more modes @@ -31,6 +32,7 @@ pub struct Dependents { pub struct Camera { tgt_focus: Vec3, focus: Vec3, + tgt_ori: Vec3, ori: Vec3, tgt_dist: f32, dist: f32, @@ -49,6 +51,7 @@ impl Camera { Self { tgt_focus: Vec3::unit_z() * 10.0, focus: Vec3::unit_z() * 10.0, + tgt_ori: Vec3::zero(), ori: Vec3::zero(), tgt_dist: 10.0, dist: 10.0, @@ -121,21 +124,33 @@ impl Camera { /// accordingly. pub fn rotate_by(&mut self, delta: Vec3) { // Wrap camera yaw - self.ori.x = (self.ori.x + delta.x) % (2.0 * PI); + self.tgt_ori.x = (self.tgt_ori.x + delta.x).rem_euclid(2.0 * PI); // Clamp camera pitch to the vertical limits - self.ori.y = (self.ori.y + delta.y).min(PI / 2.0).max(-PI / 2.0); + self.tgt_ori.y = (self.tgt_ori.y + delta.y) + .min(PI / 2.0 - 0.0001) + .max(-PI / 2.0 + 0.0001); // Wrap camera roll - self.ori.z = (self.ori.z + delta.z) % (2.0 * PI); + self.tgt_ori.z = (self.tgt_ori.z + delta.z).rem_euclid(2.0 * PI); } /// Set the orientation of the camera about its focus. - pub fn set_orientation(&mut self, orientation: Vec3) { + pub fn set_orientation(&mut self, ori: Vec3) { // Wrap camera yaw - self.ori.x = orientation.x % (2.0 * PI); + self.tgt_ori.x = ori.x.rem_euclid(2.0 * PI); // Clamp camera pitch to the vertical limits - self.ori.y = orientation.y.min(PI / 2.0).max(-PI / 2.0); + self.tgt_ori.y = ori.y.min(PI / 2.0 - 0.0001).max(-PI / 2.0 + 0.0001); // Wrap camera roll - self.ori.z = orientation.z % (2.0 * PI); + self.tgt_ori.z = ori.z.rem_euclid(2.0 * PI); + } + + /// Set the orientation of the camera about its focus without lerping. + pub fn set_ori_instant(&mut self, ori: Vec3) { + // Wrap camera yaw + self.ori.x = ori.x.rem_euclid(2.0 * PI); + // Clamp camera pitch to the vertical limits + self.ori.y = ori.y.min(PI / 2.0 - 0.0001).max(-PI / 2.0 + 0.0001); + // Wrap camera roll + self.ori.z = ori.z.rem_euclid(2.0 * PI); } /// Zoom the camera by the given delta, limiting the input accordingly. @@ -175,7 +190,7 @@ impl Camera { /// Set the distance of the camera from the target (i.e., zoom). pub fn set_distance(&mut self, dist: f32) { self.tgt_dist = dist; } - pub fn update(&mut self, time: f64) { + pub fn update(&mut self, time: f64, dt: f32) { // This is horribly frame time dependent, but so is most of the game let delta = self.last_time.replace(time).map_or(0.0, |t| time - t); if (self.dist - self.tgt_dist).abs() > 0.01 { @@ -193,6 +208,20 @@ impl Camera { (delta as f32) / self.interp_time(), ); } + + let lerp_angle = |a: f32, b: f32, rate: f32| { + let offs = [-2.0 * PI, 0.0, 2.0 * PI] + .iter() + .min_by_key(|offs: &&f32| ((a - (b + *offs)).abs() * 1000.0) as i32) + .unwrap(); + Lerp::lerp(a, b + *offs, rate) + }; + + self.set_ori_instant(Vec3::new( + lerp_angle(self.ori.x, self.tgt_ori.x, LERP_ORI_RATE * dt), + Lerp::lerp(self.ori.y, self.tgt_ori.y, LERP_ORI_RATE * dt), + lerp_angle(self.ori.z, self.tgt_ori.z, LERP_ORI_RATE * dt), + )); } pub fn interp_time(&self) -> f32 { diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index d8a2fbe300..8edf8e4635 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -236,11 +236,14 @@ impl Scene { }; self.camera.set_focus_pos( - player_pos + Vec3::unit_z() * (up + dist * 0.15 - tilt.min(0.0) * dist * 0.75), + player_pos + Vec3::unit_z() * (up + dist * 0.15 - tilt.min(0.0) * dist * 0.4), ); // Tick camera for interpolation. - self.camera.update(scene_data.state.get_time()); + self.camera.update( + scene_data.state.get_time(), + scene_data.state.get_delta_time(), + ); // Compute camera matrices. self.camera.compute_dependents(&*scene_data.state.terrain()); diff --git a/voxygen/src/scene/simple.rs b/voxygen/src/scene/simple.rs index e663fb841f..7a17754231 100644 --- a/voxygen/src/scene/simple.rs +++ b/voxygen/src/scene/simple.rs @@ -146,7 +146,7 @@ impl Scene { } pub fn maintain(&mut self, renderer: &mut Renderer, scene_data: SceneData) { - self.camera.update(scene_data.time); + self.camera.update(scene_data.time, 1.0 / 60.0); self.camera.compute_dependents(&VoidVol); let camera::Dependents { diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index be5f7c40c3..b6bd3f4450 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -10,6 +10,7 @@ use super::SceneData; use common::{ assets, figure::Segment, + spiral::Spiral2d, terrain::{Block, BlockKind, TerrainChunk}, vol::{BaseVol, ReadVol, RectRasterableVol, SampleVol, Vox}, volumes::vol_grid_2d::{VolGrid2d, VolGrid2dError}, @@ -58,6 +59,22 @@ struct SpriteConfig { fn sprite_config_for(kind: BlockKind) -> Option { match kind { + BlockKind::Window1 => Some(SpriteConfig { + variations: 1, + wind_sway: 0.0, + }), + BlockKind::Window2 => Some(SpriteConfig { + variations: 1, + wind_sway: 0.0, + }), + BlockKind::Window3 => Some(SpriteConfig { + variations: 1, + wind_sway: 0.0, + }), + BlockKind::Window4 => Some(SpriteConfig { + variations: 1, + wind_sway: 0.0, + }), BlockKind::LargeCactus => Some(SpriteConfig { variations: 2, wind_sway: 0.0, @@ -150,7 +167,7 @@ fn sprite_config_for(kind: BlockKind) -> Option { wind_sway: 0.1, }), BlockKind::Pumpkin => Some(SpriteConfig { - variations: 5, + variations: 7, wind_sway: 0.0, }), BlockKind::LingonBerry => Some(SpriteConfig { @@ -169,7 +186,62 @@ fn sprite_config_for(kind: BlockKind) -> Option { variations: 4, wind_sway: 0.1, }), - + BlockKind::Ember => Some(SpriteConfig { + variations: 1, + wind_sway: 0.8, + }), + BlockKind::Corn => Some(SpriteConfig { + variations: 6, + wind_sway: 0.4, + }), + BlockKind::WheatYellow => Some(SpriteConfig { + variations: 10, + wind_sway: 0.4, + }), + BlockKind::WheatGreen => Some(SpriteConfig { + variations: 10, + wind_sway: 0.4, + }), + BlockKind::Cabbage => Some(SpriteConfig { + variations: 3, + wind_sway: 0.0, + }), + BlockKind::Flax => Some(SpriteConfig { + variations: 6, + wind_sway: 0.4, + }), + BlockKind::Carrot => Some(SpriteConfig { + variations: 6, + wind_sway: 0.1, + }), + BlockKind::Tomato => Some(SpriteConfig { + variations: 5, + wind_sway: 0.0, + }), + BlockKind::Radish => Some(SpriteConfig { + variations: 5, + wind_sway: 0.1, + }), + BlockKind::Turnip => Some(SpriteConfig { + variations: 6, + wind_sway: 0.1, + }), + BlockKind::Coconut => Some(SpriteConfig { + variations: 1, + wind_sway: 0.0, + }), + BlockKind::Scarecrow => Some(SpriteConfig { + variations: 1, + wind_sway: 0.0, + }), + BlockKind::StreetLamp => Some(SpriteConfig { + variations: 1, + wind_sway: 0.0, + }), + BlockKind::Door => Some(SpriteConfig { + variations: 1, + wind_sway: 0.0, + }), _ => None, } } @@ -198,16 +270,17 @@ fn mesh_worker + RectRasterableVol + ReadVol + Debug>( let wpos = Vec3::from(pos * V::RECT_SIZE.map(|e: u32| e as i32)) + Vec3::new(x, y, z); - let kind = volume.get(wpos).unwrap_or(&Block::empty()).kind(); + let block = volume.get(wpos).ok().copied().unwrap_or(Block::empty()); - if let Some(cfg) = sprite_config_for(kind) { + if let Some(cfg) = sprite_config_for(block.kind()) { let seed = wpos.x as u64 * 3 + wpos.y as u64 * 7 + wpos.x as u64 * wpos.y as u64; // Awful PRNG + let ori = block.get_ori().unwrap_or((seed % 4) as u8 * 2); let instance = SpriteInstance::new( Mat4::identity() - .rotated_z(f32::consts::PI * 0.5 * (seed % 4) as f32) + .rotated_z(f32::consts::PI * 0.25 * ori as f32) .translated_3d( wpos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0), ), @@ -216,7 +289,7 @@ fn mesh_worker + RectRasterableVol + ReadVol + Debug>( ); instances - .entry((kind, seed as usize % cfg.variations)) + .entry((block.kind(), seed as usize % cfg.variations)) .or_insert_with(|| Vec::new()) .push(instance); } @@ -271,6 +344,35 @@ impl Terrain { mesh_recv: recv, mesh_todo: HashMap::default(), sprite_models: vec![ + // Windows + ( + (BlockKind::Window1, 0), + make_model( + "voxygen.voxel.sprite.window.window-0", + Vec3::new(-5.5, -5.5, 0.0), + ), + ), + ( + (BlockKind::Window2, 0), + make_model( + "voxygen.voxel.sprite.window.window-1", + Vec3::new(-5.5, -5.5, 0.0), + ), + ), + ( + (BlockKind::Window3, 0), + make_model( + "voxygen.voxel.sprite.window.window-2", + Vec3::new(-5.5, -5.5, 0.0), + ), + ), + ( + (BlockKind::Window4, 0), + make_model( + "voxygen.voxel.sprite.window.window-3", + Vec3::new(-5.5, -5.5, 0.0), + ), + ), // Cacti ( (BlockKind::LargeCactus, 0), @@ -830,6 +932,20 @@ impl Terrain { Vec3::new(-6.0, -6.0, -0.0), ), ), + ( + (BlockKind::Pumpkin, 5), + make_model( + "voxygen.voxel.sprite.pumpkin.6", + Vec3::new(-7.0, -6.5, -0.0), + ), + ), + ( + (BlockKind::Pumpkin, 6), + make_model( + "voxygen.voxel.sprite.pumpkin.7", + Vec3::new(-7.0, -9.5, -0.0), + ), + ), //Lingonberries ( (BlockKind::LingonBerry, 0), @@ -1065,6 +1181,435 @@ impl Terrain { Vec3::new(-6.0, -6.0, -0.0), ), ), + // Ember + ( + (BlockKind::Ember, 0), + make_model("voxygen.voxel.sprite.ember.1", Vec3::new(-7.0, -7.0, -2.9)), + ), + // Corn + ( + (BlockKind::Corn, 0), + make_model( + "voxygen.voxel.sprite.corn.corn-0", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::Corn, 1), + make_model( + "voxygen.voxel.sprite.corn.corn-1", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::Corn, 2), + make_model( + "voxygen.voxel.sprite.corn.corn-2", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::Corn, 3), + make_model( + "voxygen.voxel.sprite.corn.corn-3", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::Corn, 4), + make_model( + "voxygen.voxel.sprite.corn.corn-4", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::Corn, 5), + make_model( + "voxygen.voxel.sprite.corn.corn-5", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + // Yellow Wheat + ( + (BlockKind::WheatYellow, 0), + make_model( + "voxygen.voxel.sprite.wheat_yellow.wheat-0", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatYellow, 1), + make_model( + "voxygen.voxel.sprite.wheat_yellow.wheat-1", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatYellow, 2), + make_model( + "voxygen.voxel.sprite.wheat_yellow.wheat-2", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatYellow, 3), + make_model( + "voxygen.voxel.sprite.wheat_yellow.wheat-3", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatYellow, 4), + make_model( + "voxygen.voxel.sprite.wheat_yellow.wheat-4", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatYellow, 5), + make_model( + "voxygen.voxel.sprite.wheat_yellow.wheat-5", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatYellow, 6), + make_model( + "voxygen.voxel.sprite.wheat_yellow.wheat-6", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatYellow, 7), + make_model( + "voxygen.voxel.sprite.wheat_yellow.wheat-7", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatYellow, 8), + make_model( + "voxygen.voxel.sprite.wheat_yellow.wheat-8", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatYellow, 9), + make_model( + "voxygen.voxel.sprite.wheat_yellow.wheat-9", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + // Green Wheat + ( + (BlockKind::WheatGreen, 0), + make_model( + "voxygen.voxel.sprite.wheat_green.wheat-0", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatGreen, 1), + make_model( + "voxygen.voxel.sprite.wheat_green.wheat-1", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatGreen, 2), + make_model( + "voxygen.voxel.sprite.wheat_green.wheat-2", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatGreen, 3), + make_model( + "voxygen.voxel.sprite.wheat_green.wheat-3", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatGreen, 4), + make_model( + "voxygen.voxel.sprite.wheat_green.wheat-4", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatGreen, 5), + make_model( + "voxygen.voxel.sprite.wheat_green.wheat-5", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatGreen, 6), + make_model( + "voxygen.voxel.sprite.wheat_green.wheat-6", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatGreen, 7), + make_model( + "voxygen.voxel.sprite.wheat_green.wheat-7", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatGreen, 8), + make_model( + "voxygen.voxel.sprite.wheat_green.wheat-8", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::WheatGreen, 9), + make_model( + "voxygen.voxel.sprite.wheat_green.wheat-9", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + // Cabbage + ( + (BlockKind::Cabbage, 0), + make_model( + "voxygen.voxel.sprite.cabbage.cabbage-0", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::Cabbage, 1), + make_model( + "voxygen.voxel.sprite.cabbage.cabbage-1", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::Cabbage, 2), + make_model( + "voxygen.voxel.sprite.cabbage.cabbage-2", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + // Flax + ( + (BlockKind::Flax, 0), + make_model( + "voxygen.voxel.sprite.flax.flax-0", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::Flax, 1), + make_model( + "voxygen.voxel.sprite.flax.flax-1", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::Flax, 2), + make_model( + "voxygen.voxel.sprite.flax.flax-2", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::Flax, 3), + make_model( + "voxygen.voxel.sprite.flax.flax-3", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::Flax, 4), + make_model( + "voxygen.voxel.sprite.flax.flax-4", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + ( + (BlockKind::Flax, 5), + make_model( + "voxygen.voxel.sprite.flax.flax-5", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + // Carrot + ( + (BlockKind::Carrot, 0), + make_model( + "voxygen.voxel.sprite.carrot.0", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Carrot, 1), + make_model( + "voxygen.voxel.sprite.carrot.1", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Carrot, 2), + make_model( + "voxygen.voxel.sprite.carrot.2", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Carrot, 3), + make_model( + "voxygen.voxel.sprite.carrot.3", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Carrot, 4), + make_model( + "voxygen.voxel.sprite.carrot.4", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Carrot, 5), + make_model( + "voxygen.voxel.sprite.carrot.5", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Tomato, 0), + make_model("voxygen.voxel.sprite.tomato.0", Vec3::new(-5.5, -5.5, 0.0)), + ), + ( + (BlockKind::Tomato, 1), + make_model("voxygen.voxel.sprite.tomato.1", Vec3::new(-5.5, -5.5, 0.0)), + ), + ( + (BlockKind::Tomato, 2), + make_model("voxygen.voxel.sprite.tomato.2", Vec3::new(-5.5, -5.5, 0.0)), + ), + ( + (BlockKind::Tomato, 3), + make_model("voxygen.voxel.sprite.tomato.3", Vec3::new(-5.5, -5.5, 0.0)), + ), + ( + (BlockKind::Tomato, 4), + make_model("voxygen.voxel.sprite.tomato.4", Vec3::new(-5.5, -5.5, 0.0)), + ), + // Radish + ( + (BlockKind::Radish, 0), + make_model( + "voxygen.voxel.sprite.radish.0", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Radish, 1), + make_model( + "voxygen.voxel.sprite.radish.1", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Radish, 2), + make_model( + "voxygen.voxel.sprite.radish.2", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Radish, 3), + make_model( + "voxygen.voxel.sprite.radish.3", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Radish, 4), + make_model( + "voxygen.voxel.sprite.radish.4", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + // Turnip + ( + (BlockKind::Turnip, 0), + make_model( + "voxygen.voxel.sprite.turnip.turnip-0", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Turnip, 1), + make_model( + "voxygen.voxel.sprite.turnip.turnip-1", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Turnip, 2), + make_model( + "voxygen.voxel.sprite.turnip.turnip-2", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Turnip, 3), + make_model( + "voxygen.voxel.sprite.turnip.turnip-3", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Turnip, 4), + make_model( + "voxygen.voxel.sprite.turnip.turnip-4", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + ( + (BlockKind::Turnip, 5), + make_model( + "voxygen.voxel.sprite.turnip.turnip-5", + Vec3::new(-5.5, -5.5, -0.25), + ), + ), + // Coconut + ( + (BlockKind::Coconut, 0), + make_model( + "voxygen.voxel.sprite.fruit.coconut", + Vec3::new(-6.0, -6.0, 0.0), + ), + ), + // Scarecrow + ( + (BlockKind::Scarecrow, 0), + make_model( + "voxygen.voxel.sprite.misc.scarecrow", + Vec3::new(-9.5, -3.0, -0.25), + ), + ), + // Street Light + ( + (BlockKind::StreetLamp, 0), + make_model( + "voxygen.voxel.sprite.misc.street_lamp", + Vec3::new(-4.5, -4.5, 0.0), + ), + ), + // Door + ( + (BlockKind::Door, 0), + make_model( + "voxygen.voxel.sprite.door.door-0", + Vec3::new(-5.5, -5.5, 0.0), + ), + ), ] .into_iter() .collect(), @@ -1403,9 +1948,11 @@ impl Terrain { if *n >= chunks.len() { None } else { - *n += 1; let pos = focus_chunk + rpos; - Some(chunks.get(&pos).map(|c| (pos, c))) + Some(chunks.get(&pos).map(|c| { + *n += 1; + (pos, c) + })) } }) .filter_map(|x| x); @@ -1442,9 +1989,11 @@ impl Terrain { if *n >= chunks.len() { None } else { - *n += 1; let pos = focus_chunk + rpos; - Some(chunks.get(&pos).map(|c| (pos, c))) + Some(chunks.get(&pos).map(|c| { + *n += 1; + (pos, c) + })) } }) .filter_map(|x| x); @@ -1482,42 +2031,11 @@ impl Terrain { .as_ref() .map(|model| (model, &chunk.locals)) }) + .collect::>() + .into_iter() + .rev() // Render back-to-front .for_each(|(model, locals)| { renderer.render_fluid_chunk(model, globals, locals, lights, shadows, &self.waves) }); } } - -#[derive(Clone)] -struct Spiral2d { - layer: i32, - i: i32, -} - -impl Spiral2d { - pub fn new() -> Self { Self { layer: 0, i: 0 } } -} - -impl Iterator for Spiral2d { - type Item = Vec2; - - fn next(&mut self) -> Option { - let layer_size = (self.layer * 8 + 4 * self.layer.min(1) - 4).max(1); - if self.i >= layer_size { - self.layer += 1; - self.i = 0; - } - let layer_size = (self.layer * 8 + 4 * self.layer.min(1) - 4).max(1); - - let pos = Vec2::new( - -self.layer + (self.i - (layer_size / 4) * 0).max(0).min(self.layer * 2) - - (self.i - (layer_size / 4) * 2).max(0).min(self.layer * 2), - -self.layer + (self.i - (layer_size / 4) * 1).max(0).min(self.layer * 2) - - (self.i - (layer_size / 4) * 3).max(0).min(self.layer * 2), - ); - - self.i += 1; - - Some(pos) - } -} diff --git a/world/Cargo.toml b/world/Cargo.toml index 0ff1838510..6a28140cc0 100644 --- a/world/Cargo.toml +++ b/world/Cargo.toml @@ -29,4 +29,4 @@ ron = "0.5.1" [dev-dependencies] pretty_env_logger = "0.3.0" -minifb = "0.14.0" +minifb = "0.16.0" diff --git a/world/examples/city.rs b/world/examples/city.rs deleted file mode 100644 index e98b379220..0000000000 --- a/world/examples/city.rs +++ /dev/null @@ -1,35 +0,0 @@ -use rand::thread_rng; - -use vek::*; -use veloren_world::sim::Settlement; - -const W: usize = 640; -const H: usize = 480; - -fn main() { - let mut win = - minifb::Window::new("City Viewer", W, H, minifb::WindowOptions::default()).unwrap(); - - let settlement = Settlement::generate(&mut thread_rng()); - - while win.is_open() { - let mut buf = vec![0; W * H]; - - for i in 0..W { - for j in 0..H { - let pos = Vec2::new(i as f32, j as f32) * 0.002; - - let seed = settlement.get_at(pos).map(|b| b.seed).unwrap_or(0); - - buf[j * W + i] = u32::from_le_bytes([ - (seed >> 0) as u8, - (seed >> 8) as u8, - (seed >> 16) as u8, - (seed >> 24) as u8, - ]); - } - } - - win.update_with_buffer(&buf).unwrap(); - } -} diff --git a/world/examples/settlement_viewer.rs b/world/examples/settlement_viewer.rs new file mode 100644 index 0000000000..ccfb4e321d --- /dev/null +++ b/world/examples/settlement_viewer.rs @@ -0,0 +1,57 @@ +use rand::thread_rng; +use vek::*; +use veloren_world::site::Settlement; + +const W: usize = 640; +const H: usize = 480; + +fn main() { + let mut win = + minifb::Window::new("Settlement Viewer", W, H, minifb::WindowOptions::default()).unwrap(); + + let settlement = Settlement::generate(Vec2::zero(), None, &mut thread_rng()); + + let mut focus = Vec2::::zero(); + let mut zoom = 1.0; + + while win.is_open() { + let mut buf = vec![0; W * H]; + + let win_to_pos = + |wp: Vec2| (wp.map(|e| e as f32) - Vec2::new(W as f32, H as f32) * 0.5) * zoom; + + for i in 0..W { + for j in 0..H { + let pos = focus + win_to_pos(Vec2::new(i, j)) * zoom; + + let color = settlement + .get_color(pos.map(|e| e.floor() as i32)) + .unwrap_or(Rgb::new(35, 50, 20)); + + buf[j * W + i] = u32::from_le_bytes([color.b, color.g, color.r, 255]); + } + } + + let spd = 20.0; + if win.is_key_down(minifb::Key::W) { + focus.y -= spd * zoom; + } + if win.is_key_down(minifb::Key::A) { + focus.x -= spd * zoom; + } + if win.is_key_down(minifb::Key::S) { + focus.y += spd * zoom; + } + if win.is_key_down(minifb::Key::D) { + focus.x += spd * zoom; + } + if win.is_key_down(minifb::Key::Q) { + zoom *= 1.05; + } + if win.is_key_down(minifb::Key::E) { + zoom /= 1.05; + } + + win.update_with_buffer_size(&buf, W, H).unwrap(); + } +} diff --git a/world/examples/turb.rs b/world/examples/turb.rs index 302306527e..aa4444c36f 100644 --- a/world/examples/turb.rs +++ b/world/examples/turb.rs @@ -34,7 +34,7 @@ fn main() { } } - win.update_with_buffer(&buf).unwrap(); + win.update_with_buffer(&buf, W, H).unwrap(); _time += 1.0 / 60.0; } diff --git a/world/examples/view.rs b/world/examples/view.rs index eab8ef1398..7fa4bd7c38 100644 --- a/world/examples/view.rs +++ b/world/examples/view.rs @@ -27,21 +27,21 @@ fn main() { for j in 0..H { let pos = focus + Vec2::new(i as i32, j as i32) * scale; - let (alt, location) = sampler + let (alt, place) = sampler .get(pos) .map(|sample| { ( sample.alt.sub(64.0).add(gain).mul(0.7).max(0.0).min(255.0) as u8, - sample.location, + sample.chunk.place, ) }) .unwrap_or((0, None)); - let loc_color = location - .map(|l| (l.loc_idx as u8 * 17, l.loc_idx as u8 * 13)) + let place_color = place + .map(|p| ((p.id() % 256) as u8 * 17, (p.id() % 256) as u8 * 13)) .unwrap_or((0, 0)); - buf[j * W + i] = u32::from_le_bytes([loc_color.0, loc_color.1, alt, alt]); + buf[j * W + i] = u32::from_le_bytes([place_color.0, place_color.1, alt, alt]); } } @@ -71,6 +71,6 @@ fn main() { scale -= 6; } - win.update_with_buffer(&buf).unwrap(); + win.update_with_buffer(&buf, W, H).unwrap(); } } diff --git a/world/examples/water.rs b/world/examples/water.rs index 4f6696b152..e9207bdbf5 100644 --- a/world/examples/water.rs +++ b/world/examples/water.rs @@ -18,13 +18,13 @@ fn main() { let map_file = // "map_1575990726223.bin"; // "map_1575987666972.bin"; - "map_1576046079066.bin"; - let mut _map_file = PathBuf::from("./maps"); - _map_file.push(map_file); + "map_1585335358316.bin"; + let mut map_path = PathBuf::from("./maps"); + map_path.push(map_file); let world = World::generate(5284, WorldOpts { seed_elements: false, - // world_file: sim::FileOpts::Load(_map_file), + //world_file: sim::FileOpts::Load(map_path), world_file: sim::FileOpts::Save, ..WorldOpts::default() }); @@ -150,12 +150,25 @@ fn main() { } if win.get_mouse_down(minifb::MouseButton::Left) { if let Some((mx, my)) = win.get_mouse_pos(minifb::MouseMode::Clamp) { - let pos = (Vec2::::from(focus) + (Vec2::new(mx as f64, my as f64) * scale)) + let chunk_pos = (Vec2::::from(focus) + + (Vec2::new(mx as f64, my as f64) * scale)) .map(|e| e as i32); + let block_pos = chunk_pos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e * f as i32); println!( - "Chunk position: {:?}", - pos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e * f as i32) + "Block: ({}, {}), Chunk: ({}, {})", + block_pos.x, block_pos.y, chunk_pos.x, chunk_pos.y ); + if let Some(chunk) = sampler.get(chunk_pos) { + //println!("Chunk info: {:#?}", chunk); + if let Some(id) = &chunk.place { + let place = world.civs().place(*id); + println!("Place {} info: {:#?}", id.id(), place); + + if let Some(site) = world.civs().sites().find(|site| site.place == *id) { + println!("Site: {}", site); + } + } + } } } let is_camera = win.is_key_down(minifb::Key::C); @@ -239,6 +252,6 @@ fn main() { } } - win.update_with_buffer(&buf).unwrap(); + win.update_with_buffer(&buf, W, H).unwrap(); } } diff --git a/world/src/block/mod.rs b/world/src/block/mod.rs index 8f60479481..8aa8020af6 100644 --- a/world/src/block/mod.rs +++ b/world/src/block/mod.rs @@ -2,7 +2,6 @@ mod natural; use crate::{ column::{ColumnGen, ColumnSample}, - generator::{Generator, TownGen}, util::{RandomField, Sampler, SmallCache}, CONFIG, }; @@ -51,11 +50,7 @@ impl<'a> BlockGen<'a> { cache, Vec2::from(*cliff_pos), ) { - Some(cliff_sample) - if cliff_sample.is_cliffs - && cliff_sample.spawn_rate > 0.5 - && cliff_sample.spawn_rules.cliffs => - { + Some(cliff_sample) if cliff_sample.is_cliffs && cliff_sample.spawn_rate > 0.5 => { let cliff_pos3d = Vec3::from(*cliff_pos); // Conservative range of height: [15.70, 49.33] @@ -71,16 +66,24 @@ impl<'a> BlockGen<'a> { // Conservative range of radius: [8, 47] let radius = RandomField::new(seed + 2).get(cliff_pos3d) % 48 + 8; - max_height.max( - if cliff_pos.map(|e| e as f32).distance_squared(wpos) - < (radius as f32 + tolerance).powf(2.0) - { - cliff_sample.alt + height * (1.0 - cliff_sample.chaos) + cliff_hill - } else { - 0.0 - }, - ) - } + if cliff_sample + .water_dist + .map(|d| d > radius as f32) + .unwrap_or(true) + { + max_height.max( + if cliff_pos.map(|e| e as f32).distance_squared(wpos) + < (radius as f32 + tolerance).powf(2.0) + { + cliff_sample.alt + height * (1.0 - cliff_sample.chaos) + cliff_hill + } else { + 0.0 + }, + ) + } else { + max_height + } + }, _ => max_height, }, ) @@ -169,7 +172,6 @@ impl<'a> BlockGen<'a> { close_cliffs, temp, humidity, - chunk, stone_col, .. } = sample; @@ -178,7 +180,7 @@ impl<'a> BlockGen<'a> { let wposf = wpos.map(|e| e as f64); - let (block, height) = if !only_structures { + let (block, _height) = if !only_structures { let (_definitely_underground, height, on_cliff, basement_height, water_height) = if (wposf.z as f32) < alt - 64.0 * chaos { // Shortcut warping @@ -397,14 +399,6 @@ impl<'a> BlockGen<'a> { (None, sample.alt) }; - // Structures (like towns) - let block = chunk - .structures - .town - .as_ref() - .and_then(|town| TownGen.get((town, wpos, sample, height))) - .or(block); - let block = structures .iter() .find_map(|st| { @@ -419,7 +413,7 @@ impl<'a> BlockGen<'a> { pub struct ZCache<'a> { wpos: Vec2, - sample: ColumnSample<'a>, + pub sample: ColumnSample<'a>, structures: [Option<(StructureInfo, ColumnSample<'a>)>; 9], } @@ -471,19 +465,6 @@ impl<'a> ZCache<'a> { let min = min + structure_min; let max = (ground_max + structure_max).max(self.sample.water_level + 2.0); - // Structures - let (min, max) = self - .sample - .chunk - .structures - .town - .as_ref() - .map(|town| { - let (town_min, town_max) = TownGen.get_z_limits(town, self.wpos, &self.sample); - (town_min.min(min), town_max.max(max)) - }) - .unwrap_or((min, max)); - let structures_only_min_z = ground_max.max(self.sample.water_level + 2.0); (min, structures_only_min_z, max) @@ -630,12 +611,17 @@ pub fn block_from_structure( StructureBlock::Fruit => Some(if field.get(pos + structure_pos) % 3 > 0 { Block::empty() } else { - Block::new(BlockKind::Apple, Rgb::new(194, 30, 37)) + Block::new(BlockKind::Apple, Rgb::new(1, 1, 1)) + }), + StructureBlock::Coconut => Some(if field.get(pos + structure_pos) % 3 > 0 { + Block::empty() + } else { + Block::new(BlockKind::Coconut, Rgb::new(1, 1, 1)) }), StructureBlock::Chest => Some(if structure_seed % 10 < 7 { Block::empty() } else { - Block::new(BlockKind::Chest, Rgb::new(0, 0, 0)) + Block::new(BlockKind::Chest, Rgb::new(1, 1, 1)) }), StructureBlock::Liana => Some(Block::new( BlockKind::Liana, diff --git a/world/src/block/natural.rs b/world/src/block/natural.rs index 0c2814dada..232708dad6 100644 --- a/world/src/block/natural.rs +++ b/world/src/block/natural.rs @@ -28,7 +28,8 @@ pub fn structure_gen<'a>( if (st_sample.tree_density as f64) < random_seed || st_sample.alt < st_sample.water_level || st_sample.spawn_rate < 0.5 - || !st_sample.spawn_rules.trees + || st_sample.water_dist.map(|d| d < 8.0).unwrap_or(false) + || st_sample.path.map(|(d, _)| d < 12.0).unwrap_or(false) { return None; } diff --git a/world/src/civ/econ.rs b/world/src/civ/econ.rs new file mode 100644 index 0000000000..5245627305 --- /dev/null +++ b/world/src/civ/econ.rs @@ -0,0 +1,81 @@ +#![allow(dead_code)] + +use super::GenCtx; +use rand::prelude::*; + +pub struct SellOrder { + pub quantity: f32, + pub price: f32, + + // The money returned to the seller + pub q_sold: f32, +} + +pub struct BuyOrder { + quantity: f32, + max_price: f32, +} + +#[derive(Clone, Debug)] +pub struct Belief { + pub price: f32, + pub confidence: f32, +} + +impl Belief { + pub fn choose_price(&self, ctx: &mut GenCtx) -> f32 { + self.price + ctx.rng.gen_range(-1.0, 1.0) * self.confidence + } + + pub fn update_buyer(&mut self, _years: f32, new_price: f32) { + if (self.price - new_price).abs() < self.confidence { + self.confidence *= 0.8; + } else { + self.price += (new_price - self.price) * 0.5; // TODO: Make this vary with `years` + self.confidence = (self.price - new_price).abs(); + } + } + + pub fn update_seller(&mut self, proportion: f32) { + self.price *= 1.0 + (proportion - 0.5) * 0.25; + self.confidence /= 1.0 + (proportion - 0.5) * 0.25; + } +} + +pub fn buy_units<'a>( + _ctx: &mut GenCtx, + sellers: impl Iterator, + max_quantity: f32, + max_price: f32, + max_spend: f32, +) -> (f32, f32) { + let mut sell_orders = sellers.filter(|so| so.quantity > 0.0).collect::>(); + // Sort sell orders by price, cheapest first + sell_orders.sort_by(|a, b| { + a.price + .partial_cmp(&b.price) + .unwrap_or_else(|| panic!("{} and {}", a.price, b.price)) + }); + + let mut quantity = 0.0; + let mut spent = 0.0; + + for order in sell_orders { + if quantity >= max_quantity || // We've purchased enough + spent >= max_spend || // We've spent enough + order.price > max_price + // Price is too high + { + break; + } else { + let q = (max_quantity - quantity) + .min(order.quantity - order.q_sold) + .min((max_spend - spent) / order.price); + order.q_sold += q; + quantity += q; + spent += q * order.price; + } + } + + (quantity, spent) +} diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs new file mode 100644 index 0000000000..a8a9d83846 --- /dev/null +++ b/world/src/civ/mod.rs @@ -0,0 +1,986 @@ +#![allow(dead_code)] + +mod econ; + +use crate::{ + sim::WorldSim, + site::{Dungeon, Settlement, Site as WorldSite}, + util::{attempt, seed_expan, CARDINALS, NEIGHBORS}, +}; +use common::{ + astar::Astar, + path::Path, + spiral::Spiral2d, + store::{Id, Store}, + terrain::TerrainChunkSize, + vol::RectVolSize, +}; +use hashbrown::{HashMap, HashSet}; +use rand::prelude::*; +use rand_chacha::ChaChaRng; +use std::{fmt, hash::Hash, ops::Range}; +use vek::*; + +const INITIAL_CIV_COUNT: usize = 32; + +#[derive(Default)] +pub struct Civs { + civs: Store, + places: Store, + + tracks: Store, + track_map: HashMap, HashMap, Id>>, + + sites: Store, +} + +pub struct GenCtx<'a, R: Rng> { + sim: &'a mut WorldSim, + rng: R, +} + +impl<'a, R: Rng> GenCtx<'a, R> { + pub fn reseed(&mut self) -> GenCtx<'_, impl Rng> { + GenCtx { + sim: self.sim, + rng: ChaChaRng::from_seed(self.rng.gen()), + } + } +} + +impl Civs { + pub fn generate(seed: u32, sim: &mut WorldSim) -> Self { + let mut this = Self::default(); + let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed)); + let mut ctx = GenCtx { sim, rng }; + + for _ in 0..INITIAL_CIV_COUNT { + log::info!("Creating civilisation..."); + if this.birth_civ(&mut ctx.reseed()).is_none() { + log::warn!("Failed to find starting site for civilisation."); + } + } + + for _ in 0..INITIAL_CIV_COUNT * 2 { + attempt(5, || { + let loc = find_site_loc(&mut ctx, None)?; + this.establish_site(&mut ctx.reseed(), loc, |place| Site { + kind: SiteKind::Dungeon, + center: loc, + place, + + population: 0.0, + + stocks: Stocks::from_default(100.0), + surplus: Stocks::from_default(0.0), + values: Stocks::from_default(None), + + labors: MapVec::from_default(0.01), + yields: MapVec::from_default(1.0), + productivity: MapVec::from_default(1.0), + + last_exports: Stocks::from_default(0.0), + export_targets: Stocks::from_default(0.0), + trade_states: Stocks::default(), + coin: 1000.0, + }) + }); + } + + // Tick + const SIM_YEARS: usize = 1000; + for _ in 0..SIM_YEARS { + this.tick(&mut ctx, 1.0); + } + + // Flatten ground around sites + for site in this.sites.iter() { + if let SiteKind::Settlement = &site.kind { + } else { + continue; + } + + let radius = 48i32; + let wpos = site.center * Vec2::from(TerrainChunkSize::RECT_SIZE).map(|e: u32| e as i32); + + // Flatten ground + let flatten_radius = 10.0; + if let Some(center_alt) = ctx.sim.get_alt_approx(wpos) { + for offs in Spiral2d::new().take(radius.pow(2) as usize) { + let center_alt = center_alt + + if offs.magnitude_squared() <= 6i32.pow(2) { + 16.0 + } else { + 0.0 + }; // Raise the town centre up a little + let pos = site.center + offs; + let factor = (1.0 + - (site.center - pos).map(|e| e as f32).magnitude() / flatten_radius) + * 1.15; + ctx.sim + .get_mut(pos) + // Don't disrupt chunks that are near water + .filter(|chunk| !chunk.river.near_water()) + .map(|chunk| { + let diff = Lerp::lerp_precise(chunk.alt, center_alt, factor) - chunk.alt; + chunk.alt += diff; + chunk.basement += diff; + chunk.rockiness = 0.0; + chunk.warp_factor = 0.0; + }); + } + } + } + + // Place sites in world + for site in this.sites.iter() { + let wpos = site + .center + .map2(Vec2::from(TerrainChunkSize::RECT_SIZE), |e, sz: u32| { + e * sz as i32 + sz as i32 / 2 + }); + + let world_site = match &site.kind { + SiteKind::Settlement => { + WorldSite::from(Settlement::generate(wpos, Some(ctx.sim), &mut ctx.rng)) + }, + SiteKind::Dungeon => { + WorldSite::from(Dungeon::generate(wpos, Some(ctx.sim), &mut ctx.rng)) + }, + }; + + let radius_chunks = + (world_site.radius() / TerrainChunkSize::RECT_SIZE.x as f32).ceil() as usize; + for pos in Spiral2d::new() + .map(|offs| site.center + offs) + .take((radius_chunks * 2).pow(2)) + { + ctx.sim + .get_mut(pos) + .map(|chunk| chunk.sites.push(world_site.clone())); + } + log::info!("Placed site at {:?}", site.center); + } + + //this.display_info(); + + this + } + + pub fn place(&self, id: Id) -> &Place { self.places.get(id) } + + pub fn sites(&self) -> impl Iterator + '_ { self.sites.iter() } + + #[allow(dead_code)] + fn display_info(&self) { + for (id, civ) in self.civs.iter_ids() { + println!("# Civilisation {:?}", id); + println!("Name: {}", ""); + println!("Homeland: {:#?}", self.places.get(civ.homeland)); + } + + for (id, site) in self.sites.iter_ids() { + println!("# Site {:?}", id); + println!("{:#?}", site); + } + } + + /// Return the direct track between two places + fn track_between(&self, a: Id, b: Id) -> Option> { + self.track_map + .get(&a) + .and_then(|dests| dests.get(&b)) + .or_else(|| self.track_map.get(&b).and_then(|dests| dests.get(&a))) + .copied() + } + + /// Return an iterator over a site's neighbors + fn neighbors(&self, site: Id) -> impl Iterator> + '_ { + let to = self + .track_map + .get(&site) + .map(|dests| dests.keys()) + .into_iter() + .flatten(); + let fro = self + .track_map + .iter() + .filter(move |(_, dests)| dests.contains_key(&site)) + .map(|(p, _)| p); + to.chain(fro).filter(move |p| **p != site).copied() + } + + /// Find the cheapest route between two places + fn route_between(&self, a: Id, b: Id) -> Option<(Path>, f32)> { + let heuristic = move |p: &Id| { + (self + .sites + .get(*p) + .center + .distance_squared(self.sites.get(b).center) as f32) + .sqrt() + }; + let neighbors = |p: &Id| self.neighbors(*p); + let transition = + |a: &Id, b: &Id| self.tracks.get(self.track_between(*a, *b).unwrap()).cost; + let satisfied = |p: &Id| *p == b; + let mut astar = Astar::new(100, a, heuristic); + astar + .poll(100, heuristic, neighbors, transition, satisfied) + .into_path() + .and_then(|path| astar.get_cheapest_cost().map(|cost| (path, cost))) + } + + fn birth_civ(&mut self, ctx: &mut GenCtx) -> Option> { + let site = attempt(5, || { + let loc = find_site_loc(ctx, None)?; + self.establish_site(ctx, loc, |place| Site { + kind: SiteKind::Settlement, + center: loc, + place, + + population: 24.0, + + stocks: Stocks::from_default(100.0), + surplus: Stocks::from_default(0.0), + values: Stocks::from_default(None), + + labors: MapVec::from_default(0.01), + yields: MapVec::from_default(1.0), + productivity: MapVec::from_default(1.0), + + last_exports: Stocks::from_default(0.0), + export_targets: Stocks::from_default(0.0), + trade_states: Stocks::default(), + coin: 1000.0, + }) + })?; + + let civ = self.civs.insert(Civ { + capital: site, + homeland: self.sites.get(site).place, + }); + + Some(civ) + } + + fn establish_place( + &mut self, + ctx: &mut GenCtx, + loc: Vec2, + area: Range, + ) -> Option> { + let mut dead = HashSet::new(); + let mut alive = HashSet::new(); + alive.insert(loc); + + // Fill the surrounding area + while let Some(cloc) = alive.iter().choose(&mut ctx.rng).copied() { + for dir in CARDINALS.iter() { + if site_in_dir(&ctx.sim, cloc, *dir) { + let rloc = cloc + *dir; + if !dead.contains(&rloc) + && ctx + .sim + .get(rloc) + .map(|c| c.place.is_none()) + .unwrap_or(false) + { + alive.insert(rloc); + } + } + } + alive.remove(&cloc); + dead.insert(cloc); + + if dead.len() + alive.len() >= area.end { + break; + } + } + // Make sure the place is large enough + if dead.len() + alive.len() <= area.start { + return None; + } + + let place = self.places.insert(Place { + center: loc, + nat_res: NaturalResources::default(), + }); + + // Write place to map + for cell in dead.union(&alive) { + if let Some(chunk) = ctx.sim.get_mut(*cell) { + chunk.place = Some(place); + self.places.get_mut(place).nat_res.include_chunk(ctx, *cell); + } + } + + Some(place) + } + + fn establish_site( + &mut self, + ctx: &mut GenCtx, + loc: Vec2, + site_fn: impl FnOnce(Id) -> Site, + ) -> Option> { + const SITE_AREA: Range = 64..256; + + let place = match ctx.sim.get(loc).and_then(|site| site.place) { + Some(place) => place, + None => self.establish_place(ctx, loc, SITE_AREA)?, + }; + + let site = self.sites.insert(site_fn(place)); + + // Find neighbors + const MAX_NEIGHBOR_DISTANCE: f32 = 250.0; + let mut nearby = self + .sites + .iter_ids() + .map(|(id, p)| (id, (p.center.distance_squared(loc) as f32).sqrt())) + .filter(|(_, dist)| *dist < MAX_NEIGHBOR_DISTANCE) + .collect::>(); + nearby.sort_by_key(|(_, dist)| *dist as i32); + + if let SiteKind::Settlement = self.sites[site].kind { + for (nearby, _) in nearby.into_iter().take(5) { + // Find a novel path + if let Some((path, cost)) = find_path(ctx, loc, self.sites.get(nearby).center) { + // Find a path using existing paths + if self + .route_between(site, nearby) + // If the novel path isn't efficient compared to existing routes, don't use it + .filter(|(_, route_cost)| *route_cost < cost * 3.0) + .is_none() + { + // Write the track to the world as a path + for locs in path.nodes().windows(3) { + let to_prev_idx = NEIGHBORS + .iter() + .enumerate() + .find(|(_, dir)| **dir == locs[0] - locs[1]) + .expect("Track locations must be neighbors") + .0; + let to_next_idx = NEIGHBORS + .iter() + .enumerate() + .find(|(_, dir)| **dir == locs[2] - locs[1]) + .expect("Track locations must be neighbors") + .0; + + ctx.sim.get_mut(locs[0]).unwrap().path.neighbors |= + 1 << ((to_prev_idx as u8 + 4) % 8); + ctx.sim.get_mut(locs[2]).unwrap().path.neighbors |= + 1 << ((to_next_idx as u8 + 4) % 8); + let mut chunk = ctx.sim.get_mut(locs[1]).unwrap(); + chunk.path.neighbors |= + (1 << (to_prev_idx as u8)) | (1 << (to_next_idx as u8)); + chunk.path.offset = Vec2::new( + ctx.rng.gen_range(-16.0, 16.0), + ctx.rng.gen_range(-16.0, 16.0), + ); + } + + // Take note of the track + let track = self.tracks.insert(Track { cost, path }); + self.track_map + .entry(site) + .or_default() + .insert(nearby, track); + } + } + } + } + + Some(site) + } + + fn tick(&mut self, _ctx: &mut GenCtx, years: f32) { + for site in self.sites.iter_mut() { + site.simulate(years, &self.places.get(site.place).nat_res); + } + + // Trade stocks + // let mut stocks = TRADE_STOCKS; + // stocks.shuffle(ctx.rng); // Give each stock a chance to be traded + // first for stock in stocks.iter().copied() { + // let mut sell_orders = self.sites + // .iter_ids() + // .map(|(id, site)| (id, { + // econ::SellOrder { + // quantity: + // site.export_targets[stock].max(0.0).min(site.stocks[stock]), + // price: + // site.trade_states[stock].sell_belief.choose_price(ctx) * 1.25, // + // Trade cost q_sold: 0.0, + // } + // })) + // .filter(|(_, order)| order.quantity > 0.0) + // .collect::>(); + + // let mut sites = self.sites + // .ids() + // .collect::>(); + // sites.shuffle(ctx.rng); // Give all sites a chance to buy first + // for site in sites { + // let (max_spend, max_price, max_import) = { + // let site = self.sites.get(site); + // let budget = site.coin * 0.5; + // let total_value = site.values.iter().map(|(_, v)| + // (*v).unwrap_or(0.0)).sum::(); ( + // 100000.0,//(site.values[stock].unwrap_or(0.1) / + // total_value * budget).min(budget), + // site.trade_states[stock].buy_belief.price, + // -site.export_targets[stock].min(0.0), ) + // }; + // let (quantity, spent) = econ::buy_units(ctx, sell_orders + // .iter_mut() + // .filter(|(id, _)| site != *id && self.track_between(site, + // *id).is_some()) .map(|(_, order)| order), + // max_import, + // 1000000.0, // Max price TODO + // max_spend, + // ); + // let mut site = self.sites.get_mut(site); + // site.coin -= spent; + // if quantity > 0.0 { + // site.stocks[stock] += quantity; + // site.last_exports[stock] = -quantity; + // site.trade_states[stock].buy_belief.update_buyer(years, + // spent / quantity); println!("Belief: {:?}", + // site.trade_states[stock].buy_belief); } + // } + + // for (site, order) in sell_orders { + // let mut site = self.sites.get_mut(site); + // site.coin += order.q_sold * order.price; + // if order.q_sold > 0.0 { + // site.stocks[stock] -= order.q_sold; + // site.last_exports[stock] = order.q_sold; + // + // site.trade_states[stock].sell_belief.update_seller(order.q_sold / + // order.quantity); } + // } + // } + } +} + +/// Attempt to find a path between two locations +fn find_path( + ctx: &mut GenCtx, + a: Vec2, + b: Vec2, +) -> Option<(Path>, f32)> { + let sim = &ctx.sim; + let heuristic = move |l: &Vec2| (l.distance_squared(b) as f32).sqrt(); + let neighbors = |l: &Vec2| { + let l = *l; + NEIGHBORS + .iter() + .filter(move |dir| walk_in_dir(sim, l, **dir).is_some()) + .map(move |dir| l + *dir) + }; + let transition = + |a: &Vec2, b: &Vec2| 1.0 + walk_in_dir(sim, *a, *b - *a).unwrap_or(10000.0); + let satisfied = |l: &Vec2| *l == b; + let mut astar = Astar::new(20000, a, heuristic); + astar + .poll(20000, heuristic, neighbors, transition, satisfied) + .into_path() + .and_then(|path| astar.get_cheapest_cost().map(|cost| (path, cost))) +} + +/// Return Some if travel between a location and a chunk next to it is permitted +/// If permitted, the approximate relative const of traversal is given +// (TODO: by whom?) +fn walk_in_dir(sim: &WorldSim, a: Vec2, dir: Vec2) -> Option { + if loc_suitable_for_walking(sim, a) && loc_suitable_for_walking(sim, a + dir) { + let a_chunk = sim.get(a)?; + let b_chunk = sim.get(a + dir)?; + + let hill_cost = ((b_chunk.alt - a_chunk.alt).abs() / 2.5).powf(2.0); + let water_cost = if b_chunk.river.near_water() { + 50.0 + } else { + 0.0 + }; + let wild_cost = if b_chunk.path.is_path() { + 0.0 // Traversing existing paths has no additional cost! + } else { + 2.0 + }; + Some(1.0 + hill_cost + water_cost + wild_cost) + } else { + None + } +} + +/// Return true if a position is suitable for walking on +fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2) -> bool { + if let Some(chunk) = sim.get(loc) { + !chunk.river.is_ocean() && !chunk.river.is_lake() + } else { + false + } +} + +/// Return true if a site could be constructed between a location and a chunk +/// next to it is permitted (TODO: by whom?) +fn site_in_dir(sim: &WorldSim, a: Vec2, dir: Vec2) -> bool { + loc_suitable_for_site(sim, a) && loc_suitable_for_site(sim, a + dir) +} + +/// Return true if a position is suitable for site construction (TODO: +/// criteria?) +fn loc_suitable_for_site(sim: &WorldSim, loc: Vec2) -> bool { + if let Some(chunk) = sim.get(loc) { + !chunk.river.is_ocean() + && !chunk.river.is_lake() + && sim + .get_gradient_approx(loc) + .map(|grad| grad < 1.0) + .unwrap_or(false) + } else { + false + } +} + +/// Attempt to search for a location that's suitable for site construction +fn find_site_loc(ctx: &mut GenCtx, near: Option<(Vec2, f32)>) -> Option> { + const MAX_ATTEMPTS: usize = 100; + let mut loc = None; + for _ in 0..MAX_ATTEMPTS { + let test_loc = loc.unwrap_or_else(|| match near { + Some((origin, dist)) => { + origin + + (Vec2::new(ctx.rng.gen_range(-1.0, 1.0), ctx.rng.gen_range(-1.0, 1.0)) + .try_normalized() + .unwrap_or(Vec2::zero()) + * ctx.rng.gen::() + * dist) + .map(|e| e as i32) + }, + None => Vec2::new( + ctx.rng.gen_range(0, ctx.sim.get_size().x as i32), + ctx.rng.gen_range(0, ctx.sim.get_size().y as i32), + ), + }); + + if loc_suitable_for_site(&ctx.sim, test_loc) { + return Some(test_loc); + } + + loc = ctx.sim.get(test_loc).and_then(|c| { + Some( + c.downhill? + .map2(Vec2::from(TerrainChunkSize::RECT_SIZE), |e, sz: u32| { + e / (sz as i32) + }), + ) + }); + } + None +} + +#[derive(Debug)] +pub struct Civ { + capital: Id, + homeland: Id, +} + +#[derive(Debug)] +pub struct Place { + center: Vec2, + nat_res: NaturalResources, +} + +// Productive capacity per year +#[derive(Default, Debug)] +pub struct NaturalResources { + wood: f32, + rock: f32, + river: f32, + farmland: f32, +} + +impl NaturalResources { + fn include_chunk(&mut self, ctx: &mut GenCtx, loc: Vec2) { + let chunk = if let Some(chunk) = ctx.sim.get(loc) { + chunk + } else { + return; + }; + + self.wood += chunk.tree_density; + self.rock += chunk.rockiness; + self.river += if chunk.river.is_river() { 5.0 } else { 0.0 }; + self.farmland += if chunk.humidity > 0.35 + && chunk.temp > -0.3 + && chunk.temp < 0.75 + && chunk.chaos < 0.5 + && ctx + .sim + .get_gradient_approx(loc) + .map(|grad| grad < 0.7) + .unwrap_or(false) + { + 1.0 + } else { + 0.0 + }; + } +} + +pub struct Track { + /// Cost of using this track relative to other paths. This cost is an + /// arbitrary unit and doesn't make sense unless compared to other track + /// costs. + cost: f32, + path: Path>, +} + +#[derive(Debug)] +pub struct Site { + pub kind: SiteKind, + pub center: Vec2, + pub place: Id, + + population: f32, + + // Total amount of each stock + stocks: Stocks, + // Surplus stock compared to demand orders + surplus: Stocks, + // For some goods, such a goods without any supply, it doesn't make sense to talk about value + values: Stocks>, + + // Proportion of individuals dedicated to an industry + labors: MapVec, + // Per worker, per year, of their output good + yields: MapVec, + productivity: MapVec, + + last_exports: Stocks, + export_targets: Stocks, + trade_states: Stocks, + coin: f32, +} + +impl fmt::Display for Site { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.kind { + SiteKind::Settlement => writeln!(f, "Settlement")?, + SiteKind::Dungeon => writeln!(f, "Dungeon")?, + } + writeln!(f, "- population: {}", self.population.floor() as u32)?; + writeln!(f, "- coin: {}", self.coin.floor() as u32)?; + writeln!(f, "Stocks")?; + for (stock, q) in self.stocks.iter() { + writeln!(f, "- {}: {}", stock, q.floor())?; + } + writeln!(f, "Values")?; + for stock in TRADE_STOCKS.iter() { + writeln!( + f, + "- {}: {}", + stock, + self.values[*stock] + .map(|x| x.to_string()) + .unwrap_or_else(|| "N/A".to_string()) + )?; + } + writeln!(f, "Laborers")?; + for (labor, n) in self.labors.iter() { + writeln!(f, "- {}: {}", labor, (*n * self.population).floor() as u32)?; + } + writeln!(f, "Export targets")?; + for (stock, n) in self.export_targets.iter() { + writeln!(f, "- {}: {}", stock, n)?; + } + + Ok(()) + } +} + +#[derive(Debug)] +pub enum SiteKind { + Settlement, + Dungeon, +} + +impl Site { + pub fn simulate(&mut self, years: f32, nat_res: &NaturalResources) { + // Insert natural resources into the economy + if self.stocks[FISH] < nat_res.river { + self.stocks[FISH] = nat_res.river; + } + if self.stocks[WHEAT] < nat_res.farmland { + self.stocks[WHEAT] = nat_res.farmland; + } + if self.stocks[LOGS] < nat_res.wood { + self.stocks[LOGS] = nat_res.wood; + } + if self.stocks[GAME] < nat_res.wood { + self.stocks[GAME] = nat_res.wood; + } + if self.stocks[ROCK] < nat_res.rock { + self.stocks[ROCK] = nat_res.rock; + } + + let orders = vec![ + (None, vec![(FOOD, 0.5)]), + (Some(COOK), vec![(FLOUR, 16.0), (MEAT, 4.0), (WOOD, 3.0)]), + (Some(LUMBERJACK), vec![(LOGS, 4.5)]), + (Some(MINER), vec![(ROCK, 7.5)]), + (Some(FISHER), vec![(FISH, 4.0)]), + (Some(HUNTER), vec![(GAME, 4.0)]), + (Some(FARMER), vec![(WHEAT, 4.0)]), + ] + .into_iter() + .collect::>>(); + + // Per labourer, per year + let production = Stocks::from_list(&[ + (FARMER, (FLOUR, 2.0)), + (LUMBERJACK, (WOOD, 1.5)), + (MINER, (STONE, 0.6)), + (FISHER, (MEAT, 3.0)), + (HUNTER, (MEAT, 0.25)), + (COOK, (FOOD, 20.0)), + ]); + + let mut demand = Stocks::from_default(0.0); + for (labor, orders) in &orders { + let scale = if let Some(labor) = labor { + self.labors[*labor] + } else { + 1.0 + } * self.population; + for (stock, amount) in orders { + demand[*stock] += *amount * scale; + } + } + + let mut supply = Stocks::from_default(0.0); + for (labor, (output_stock, _)) in production.iter() { + supply[*output_stock] += self.yields[labor] * self.labors[labor] * self.population; + } + + let last_exports = &self.last_exports; + let stocks = &self.stocks; + self.surplus = demand + .clone() + .map(|stock, _| supply[stock] + stocks[stock] - demand[stock] - last_exports[stock]); + + // Update values according to the surplus of each stock + let values = &mut self.values; + self.surplus.iter().for_each(|(stock, surplus)| { + let val = 3.5f32.powf(1.0 - *surplus / demand[stock]); + values[stock] = if val > 0.001 && val < 1000.0 { + Some(val) + } else { + None + }; + }); + + // Update export targets based on relative values + let value_avg = values + .iter() + .map(|(_, v)| (*v).unwrap_or(0.0)) + .sum::() + .max(0.01) + / values.iter().filter(|(_, v)| v.is_some()).count() as f32; + let export_targets = &mut self.export_targets; + let last_exports = &self.last_exports; + self.values.iter().for_each(|(stock, value)| { + let rvalue = (*value).map(|v| v - value_avg).unwrap_or(0.0); + //let factor = if export_targets[stock] > 0.0 { 1.0 / rvalue } else { rvalue }; + export_targets[stock] = last_exports[stock] - rvalue * 0.1; // + (trade_states[stock].sell_belief.price - trade_states[stock].buy_belief.price) * 0.025; + }); + + let population = self.population; + + // Redistribute workforce according to relative good values + let labor_ratios = production.clone().map(|labor, (output_stock, _)| { + self.productivity[labor] * demand[output_stock] / supply[output_stock].max(0.001) + }); + let labor_ratio_sum = labor_ratios.iter().map(|(_, r)| *r).sum::().max(0.01); + production.iter().for_each(|(labor, _)| { + let smooth = 0.8; + self.labors[labor] = smooth * self.labors[labor] + + (1.0 - smooth) + * (labor_ratios[labor].max(labor_ratio_sum / 1000.0) / labor_ratio_sum); + }); + + // Production + let stocks_before = self.stocks.clone(); + for (labor, orders) in orders.iter() { + let scale = if let Some(labor) = labor { + self.labors[*labor] + } else { + 1.0 + } * population; + + // For each order, we try to find the minimum satisfaction rate - this limits + // how much we can produce! For example, if we need 0.25 fish and + // 0.75 oats to make 1 unit of food, but only 0.5 units of oats are + // available then we only need to consume 2/3rds + // of other ingredients and leave the rest in stock + // In effect, this is the productivity + let productivity = orders + .iter() + .map(|(stock, amount)| { + // What quantity is this order requesting? + let _quantity = *amount * scale; + // What proportion of this order is the economy able to satisfy? + let satisfaction = (stocks_before[*stock] / demand[*stock]).min(1.0); + satisfaction + }) + .min_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap_or_else(|| { + panic!("Industry {:?} requires at least one input order", labor) + }); + + for (stock, amount) in orders { + // What quantity is this order requesting? + let quantity = *amount * scale; + // What amount gets actually used in production? + let used = quantity * productivity; + + // Deplete stocks accordingly + self.stocks[*stock] = (self.stocks[*stock] - used).max(0.0); + } + + // Industries produce things + if let Some(labor) = labor { + let (stock, rate) = production[*labor]; + let workers = self.labors[*labor] * population; + let final_rate = rate; + let yield_per_worker = productivity * final_rate; + self.yields[*labor] = yield_per_worker; + self.productivity[*labor] = productivity; + self.stocks[stock] += yield_per_worker * workers.powf(1.1); + } + } + + // Denature stocks + self.stocks.iter_mut().for_each(|(_, v)| *v *= 0.9); + + // Births/deaths + const NATURAL_BIRTH_RATE: f32 = 0.15; + const DEATH_RATE: f32 = 0.05; + let birth_rate = if self.surplus[FOOD] > 0.0 { + NATURAL_BIRTH_RATE + } else { + 0.0 + }; + self.population += years * self.population * (birth_rate - DEATH_RATE); + } +} + +type Occupation = &'static str; +const FARMER: Occupation = "farmer"; +const LUMBERJACK: Occupation = "lumberjack"; +const MINER: Occupation = "miner"; +const FISHER: Occupation = "fisher"; +const HUNTER: Occupation = "hunter"; +const COOK: Occupation = "cook"; + +type Stock = &'static str; +const WHEAT: Stock = "wheat"; +const FLOUR: Stock = "flour"; +const MEAT: Stock = "meat"; +const FISH: Stock = "fish"; +const GAME: Stock = "game"; +const FOOD: Stock = "food"; +const LOGS: Stock = "logs"; +const WOOD: Stock = "wood"; +const ROCK: Stock = "rock"; +const STONE: Stock = "stone"; +const TRADE_STOCKS: [Stock; 5] = [FLOUR, MEAT, FOOD, WOOD, STONE]; + +#[derive(Debug, Clone)] +struct TradeState { + buy_belief: econ::Belief, + sell_belief: econ::Belief, +} + +impl Default for TradeState { + fn default() -> Self { + Self { + buy_belief: econ::Belief { + price: 1.0, + confidence: 0.25, + }, + sell_belief: econ::Belief { + price: 1.0, + confidence: 0.25, + }, + } + } +} + +pub type Stocks = MapVec; + +#[derive(Default, Clone, Debug)] +pub struct MapVec { + entries: HashMap, + default: T, +} + +impl MapVec { + pub fn from_list<'a>(i: impl IntoIterator) -> Self + where + K: 'a, + T: 'a, + { + Self { + entries: i.into_iter().cloned().collect(), + default: T::default(), + } + } + + pub fn from_default(default: T) -> Self { + Self { + entries: HashMap::default(), + default, + } + } + + pub fn get_mut(&mut self, entry: K) -> &mut T { + let default = &self.default; + self.entries.entry(entry).or_insert_with(|| default.clone()) + } + + pub fn get(&self, entry: K) -> &T { self.entries.get(&entry).unwrap_or(&self.default) } + + pub fn map(self, mut f: impl FnMut(K, T) -> U) -> MapVec { + MapVec { + entries: self + .entries + .into_iter() + .map(|(s, v)| (s.clone(), f(s, v))) + .collect(), + default: U::default(), + } + } + + pub fn iter(&self) -> impl Iterator + '_ { + self.entries.iter().map(|(s, v)| (*s, v)) + } + + pub fn iter_mut(&mut self) -> impl Iterator + '_ { + self.entries.iter_mut().map(|(s, v)| (*s, v)) + } +} + +impl std::ops::Index for MapVec { + type Output = T; + + fn index(&self, entry: K) -> &Self::Output { self.get(entry) } +} + +impl std::ops::IndexMut for MapVec { + fn index_mut(&mut self, entry: K) -> &mut Self::Output { self.get_mut(entry) } +} diff --git a/world/src/column/mod.rs b/world/src/column/mod.rs index c7a8f1babf..563ca2e507 100644 --- a/world/src/column/mod.rs +++ b/world/src/column/mod.rs @@ -1,11 +1,7 @@ use crate::{ all::ForestKind, block::StructureMeta, - generator::{Generator, SpawnRules, TownGen}, - sim::{ - local_cells, uniform_idx_as_vec2, vec2_as_uniform_idx, LocationInfo, RiverKind, SimChunk, - WorldSim, - }, + sim::{local_cells, uniform_idx_as_vec2, vec2_as_uniform_idx, RiverKind, SimChunk, WorldSim}, util::{RandomPerm, Sampler, UnitChooser}, CONFIG, }; @@ -145,7 +141,7 @@ fn river_spline_coeffs( /// curve"... hopefully this works out okay and gives us what we want (a /// river that extends outwards tangent to a quadratic curve, with width /// configured by distance along the line). -fn quadratic_nearest_point( +pub fn quadratic_nearest_point( spline: &Vec3>, point: Vec2, ) -> Option<(f64, Vec2, f64)> { @@ -221,11 +217,11 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { let sim = &self.sim; - let turb = Vec2::new( + let _turb = Vec2::new( sim.gen_ctx.turb_x_nz.get((wposf.div(48.0)).into_array()) as f32, sim.gen_ctx.turb_y_nz.get((wposf.div(48.0)).into_array()) as f32, ) * 12.0; - let wposf_turb = wposf + turb.map(|e| e as f64); + let wposf_turb = wposf; // + turb.map(|e| e as f64); let chaos = sim.get_interpolated(wpos, |chunk| chunk.chaos)?; let temp = sim.get_interpolated(wpos, |chunk| chunk.temp)?; @@ -234,6 +230,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { let tree_density = sim.get_interpolated(wpos, |chunk| chunk.tree_density)?; let spawn_rate = sim.get_interpolated(wpos, |chunk| chunk.spawn_rate)?; let alt = sim.get_interpolated_monotone(wpos, |chunk| chunk.alt)?; + let chunk_warp_factor = sim.get_interpolated_monotone(wpos, |chunk| chunk.warp_factor)?; let sim_chunk = sim.get(chunk_pos)?; let neighbor_coef = TerrainChunkSize::RECT_SIZE.map(|e| e as f64); let my_chunk_idx = vec2_as_uniform_idx(chunk_pos); @@ -593,223 +590,280 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { let near_cliffs = sim_chunk.near_cliffs; let river_gouge = 0.5; - let (in_water, alt_, water_level, warp_factor) = if let Some(( - max_border_river_pos, - river_chunk, - max_border_river, - max_border_river_dist, - )) = max_river + let (_in_water, water_dist, alt_, water_level, riverless_alt, warp_factor) = if let Some( + (max_border_river_pos, river_chunk, max_border_river, max_border_river_dist), + ) = + max_river { // This is flowing into a lake, or a lake, or is at least a non-ocean tile. // // If we are <= water_alt, we are in the lake; otherwise, we are flowing into // it. - let (in_water, new_alt, new_water_alt, warp_factor) = max_border_river - .river_kind - .and_then(|river_kind| { - if let RiverKind::River { cross_section } = river_kind { - if max_border_river_dist.map(|(_, dist, _, _)| dist) != Some(Vec2::zero()) { - return None; - } - let (_, _, river_width, (river_t, (river_pos, _), downhill_river_chunk)) = - max_border_river_dist.unwrap(); - let river_alt = Lerp::lerp( - river_chunk.alt.max(river_chunk.water_alt), - downhill_river_chunk.alt.max(downhill_river_chunk.water_alt), - river_t as f32, - ); - let new_alt = river_alt - river_gouge; - let river_dist = wposf.distance(river_pos); - let river_height_factor = river_dist / (river_width * 0.5); + let (in_water, water_dist, new_alt, new_water_alt, riverless_alt, warp_factor) = + max_border_river + .river_kind + .and_then(|river_kind| { + if let RiverKind::River { cross_section } = river_kind { + if max_border_river_dist.map(|(_, dist, _, _)| dist) + != Some(Vec2::zero()) + { + return None; + } + let ( + _, + _, + river_width, + (river_t, (river_pos, _), downhill_river_chunk), + ) = max_border_river_dist.unwrap(); + let river_alt = Lerp::lerp( + river_chunk.alt.max(river_chunk.water_alt), + downhill_river_chunk.alt.max(downhill_river_chunk.water_alt), + river_t as f32, + ); + let new_alt = river_alt - river_gouge; + let river_dist = wposf.distance(river_pos); + let river_height_factor = river_dist / (river_width * 0.5); - Some(( - true, - Lerp::lerp( + let valley_alt = Lerp::lerp( new_alt - cross_section.y.max(1.0), new_alt - 1.0, (river_height_factor * river_height_factor) as f32, - ), - new_alt, - 0.0, - )) - } else { - None - } - }) - .unwrap_or_else(|| { - max_border_river - .river_kind - .and_then(|river_kind| { - match river_kind { - RiverKind::Ocean => { - let ( - _, - dist, - river_width, - (river_t, (river_pos, _), downhill_river_chunk), - ) = if let Some(dist) = max_border_river_dist { - dist - } else { - log::error!( - "Ocean: {:?} Here: {:?}, Ocean: {:?}", - max_border_river, - chunk_pos, - max_border_river_pos - ); - panic!( - "Oceans should definitely have a downhill! ...Right?" - ); - }; - let lake_water_alt = Lerp::lerp( - river_chunk.alt.max(river_chunk.water_alt), - downhill_river_chunk - .alt - .max(downhill_river_chunk.water_alt), - river_t as f32, - ); + ); - if dist == Vec2::zero() { - let river_dist = wposf.distance(river_pos); - let _river_height_factor = river_dist / (river_width * 0.5); - return Some(( - true, - alt_for_river.min(lake_water_alt - 1.0 - river_gouge), - lake_water_alt - river_gouge, - 0.0, - )); - } - - Some(( - river_scale_factor <= 1.0, - alt_for_river, - downhill_water_alt, - river_scale_factor as f32, - )) - }, - RiverKind::Lake { .. } => { - let lake_dist = (max_border_river_pos.map(|e| e as f64) - * neighbor_coef) - .distance(wposf); - let downhill_river_chunk = max_border_river_pos; - let lake_id_dist = downhill_river_chunk - chunk_pos; - let in_bounds = lake_id_dist.x >= -1 - && lake_id_dist.y >= -1 - && lake_id_dist.x <= 1 - && lake_id_dist.y <= 1; - let in_bounds = - in_bounds && (lake_id_dist.x >= 0 && lake_id_dist.y >= 0); - let (_, dist, _, (river_t, _, downhill_river_chunk)) = - if let Some(dist) = max_border_river_dist { + Some(( + true, + Some((river_dist - river_width * 0.5) as f32), + valley_alt, + new_alt, + river_alt, + 0.0, + )) + } else { + None + } + }) + .unwrap_or_else(|| { + max_border_river + .river_kind + .and_then(|river_kind| { + match river_kind { + RiverKind::Ocean => { + let ( + _, + dist, + river_width, + (river_t, (river_pos, _), downhill_river_chunk), + ) = if let Some(dist) = max_border_river_dist { dist } else { - if lake_dist - <= TerrainChunkSize::RECT_SIZE.x as f64 * 1.0 - || in_bounds - { - let gouge_factor = 0.0; - return Some(( - in_bounds - || downhill_water_alt + log::error!( + "Ocean: {:?} Here: {:?}, Ocean: {:?}", + max_border_river, + chunk_pos, + max_border_river_pos + ); + panic!( + "Oceans should definitely have a downhill! \ + ...Right?" + ); + }; + let lake_water_alt = Lerp::lerp( + river_chunk.alt.max(river_chunk.water_alt), + downhill_river_chunk + .alt + .max(downhill_river_chunk.water_alt), + river_t as f32, + ); + + if dist == Vec2::zero() { + let river_dist = wposf.distance(river_pos); + let _river_height_factor = + river_dist / (river_width * 0.5); + return Some(( + true, + Some((river_dist - river_width * 0.5) as f32), + alt_for_river + .min(lake_water_alt - 1.0 - river_gouge), + lake_water_alt - river_gouge, + alt_for_river.max(lake_water_alt), + 0.0, + )); + } + + Some(( + river_scale_factor <= 1.0, + Some( + (wposf.distance(river_pos) - river_width * 0.5) + as f32, + ), + alt_for_river, + downhill_water_alt, + alt_for_river, + river_scale_factor as f32, + )) + }, + RiverKind::Lake { .. } => { + let lake_dist = (max_border_river_pos.map(|e| e as f64) + * neighbor_coef) + .distance(wposf); + let downhill_river_chunk = max_border_river_pos; + let lake_id_dist = downhill_river_chunk - chunk_pos; + let in_bounds = lake_id_dist.x >= -1 + && lake_id_dist.y >= -1 + && lake_id_dist.x <= 1 + && lake_id_dist.y <= 1; + let in_bounds = in_bounds + && (lake_id_dist.x >= 0 && lake_id_dist.y >= 0); + let (_, dist, _, (river_t, _, downhill_river_chunk)) = + if let Some(dist) = max_border_river_dist { + dist + } else { + if lake_dist + <= TerrainChunkSize::RECT_SIZE.x as f64 * 1.0 + || in_bounds + { + let gouge_factor = 0.0; + return Some(( + in_bounds + || downhill_water_alt + .max(river_chunk.water_alt) + > alt_for_river, + Some(lake_dist as f32), + alt_for_river, + (downhill_water_alt .max(river_chunk.water_alt) - > alt_for_river, - alt_for_river, - (downhill_water_alt.max(river_chunk.water_alt) - - river_gouge), - river_scale_factor as f32 - * (1.0 - gouge_factor), + - river_gouge), + alt_for_river, + river_scale_factor as f32 + * (1.0 - gouge_factor), + )); + } else { + return Some(( + false, + Some(lake_dist as f32), + alt_for_river, + downhill_water_alt, + alt_for_river, + river_scale_factor as f32, + )); + } + }; + + let lake_dist = dist.y; + let lake_water_alt = Lerp::lerp( + river_chunk.alt.max(river_chunk.water_alt), + downhill_river_chunk + .alt + .max(downhill_river_chunk.water_alt), + river_t as f32, + ); + if dist == Vec2::zero() { + return Some(( + true, + Some(lake_dist as f32), + alt_for_river + .min(lake_water_alt - 1.0 - river_gouge), + lake_water_alt - river_gouge, + alt_for_river.max(lake_water_alt), + 0.0, + )); + } + if lake_dist <= TerrainChunkSize::RECT_SIZE.x as f64 * 1.0 + || in_bounds + { + let gouge_factor = if in_bounds && lake_dist <= 1.0 { + 1.0 + } else { + 0.0 + }; + let in_bounds_ = lake_dist + <= TerrainChunkSize::RECT_SIZE.x as f64 * 0.5; + if gouge_factor == 1.0 { + return Some(( + true, + Some(lake_dist as f32), + alt.min(lake_water_alt - 1.0 - river_gouge), + downhill_water_alt.max(lake_water_alt) + - river_gouge, + alt.max(lake_water_alt), + 0.0, )); } else { return Some(( - false, + true, + None, alt_for_river, - downhill_water_alt, - river_scale_factor as f32, + if in_bounds_ { + downhill_water_alt.max(lake_water_alt) + } else { + downhill_water_alt + } - river_gouge, + alt_for_river, + river_scale_factor as f32 + * (1.0 - gouge_factor), )); } - }; - - let lake_dist = dist.y; - let lake_water_alt = Lerp::lerp( - river_chunk.alt.max(river_chunk.water_alt), - downhill_river_chunk - .alt - .max(downhill_river_chunk.water_alt), - river_t as f32, - ); - if dist == Vec2::zero() { - return Some(( - true, - alt_for_river.min(lake_water_alt - 1.0 - river_gouge), - lake_water_alt - river_gouge, - 0.0, - )); - } - if lake_dist <= TerrainChunkSize::RECT_SIZE.x as f64 * 1.0 - || in_bounds - { - let gouge_factor = if in_bounds && lake_dist <= 1.0 { - 1.0 - } else { - 0.0 - }; - let in_bounds_ = - lake_dist <= TerrainChunkSize::RECT_SIZE.x as f64 * 0.5; - if gouge_factor == 1.0 { - return Some(( - true, - alt.min(lake_water_alt - 1.0 - river_gouge), - downhill_water_alt.max(lake_water_alt) - - river_gouge, - 0.0, - )); - } else { - return Some(( - true, - alt_for_river, - if in_bounds_ { - downhill_water_alt.max(lake_water_alt) - - river_gouge - } else { - downhill_water_alt - river_gouge - }, - river_scale_factor as f32 * (1.0 - gouge_factor), - )); } - } - Some(( - river_scale_factor <= 1.0, - alt_for_river, - downhill_water_alt, - river_scale_factor as f32, - )) - }, - RiverKind::River { .. } => { - // FIXME: Make water altitude accurate. - Some(( - river_scale_factor <= 1.0, - alt_for_river, - downhill_water_alt, - river_scale_factor as f32, - )) - }, - } - }) - .unwrap_or(( - false, - alt_for_river, - downhill_water_alt, - river_scale_factor as f32, - )) - }); - (in_water, new_alt, new_water_alt, warp_factor) + Some(( + river_scale_factor <= 1.0, + Some(lake_dist as f32), + alt_for_river, + downhill_water_alt, + alt_for_river, + river_scale_factor as f32, + )) + }, + RiverKind::River { .. } => { + let (_, _, river_width, (_, (river_pos, _), _)) = + max_border_river_dist.unwrap(); + let river_dist = wposf.distance(river_pos); + + // FIXME: Make water altitude accurate. + Some(( + river_scale_factor <= 1.0, + Some((river_dist - river_width * 0.5) as f32), + alt_for_river, + downhill_water_alt, + alt_for_river, + river_scale_factor as f32, + )) + }, + } + }) + .unwrap_or(( + false, + None, + alt_for_river, + downhill_water_alt, + alt_for_river, + river_scale_factor as f32, + )) + }); + ( + in_water, + water_dist, + new_alt, + new_water_alt, + riverless_alt, + warp_factor, + ) } else { - (false, alt_for_river, downhill_water_alt, 1.0) + ( + false, + None, + alt_for_river, + downhill_water_alt, + alt_for_river, + 1.0, + ) }; + let warp_factor = warp_factor * chunk_warp_factor; // NOTE: To disable warp, uncomment this line. // let warp_factor = 0.0; let riverless_alt_delta = Lerp::lerp(0.0, riverless_alt_delta, warp_factor); let alt = alt_ + riverless_alt_delta; + let riverless_alt = riverless_alt + riverless_alt_delta; let basement = alt + sim.get_interpolated_monotone(wpos, |chunk| chunk.basement.sub(chunk.alt))?; @@ -1061,8 +1115,11 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { 5.0 }; + let path = sim.get_nearest_path(wpos); + Some(ColumnSample { alt, + riverless_alt, basement, chaos, water_level, @@ -1076,7 +1133,17 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { ), sub_surface_color, // No growing directly on bedrock. - tree_density: Lerp::lerp(0.0, tree_density, alt.sub(2.0).sub(basement).mul(0.5)), + // And, no growing on sites that don't want them TODO: More precise than this when we + // apply trees as a post-processing layer + tree_density: if sim_chunk + .sites + .iter() + .all(|site| site.spawn_rules(wpos).trees) + { + Lerp::lerp(0.0, tree_density, alt.sub(2.0).sub(basement).mul(0.5)) + } else { + 0.0 + }, forest_kind: sim_chunk.forest_kind, close_structures: self.gen_close_structures(wpos), cave_xy, @@ -1091,20 +1158,11 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { temp, humidity, spawn_rate, - location: sim_chunk.location.as_ref(), stone_col, + water_dist, + path, chunk: sim_chunk, - spawn_rules: sim_chunk - .structures - .town - .as_ref() - .map(|town| TownGen.spawn_rules(town, wpos)) - .unwrap_or(SpawnRules::default()) - .and(SpawnRules { - cliffs: !in_water, - trees: true, - }), }) } } @@ -1112,6 +1170,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { #[derive(Clone)] pub struct ColumnSample<'a> { pub alt: f32, + pub riverless_alt: f32, pub basement: f32, pub chaos: f32, pub water_level: f32, @@ -1133,11 +1192,11 @@ pub struct ColumnSample<'a> { pub temp: f32, pub humidity: f32, pub spawn_rate: f32, - pub location: Option<&'a LocationInfo>, pub stone_col: Rgb, + pub water_dist: Option, + pub path: Option<(f32, Vec2)>, pub chunk: &'a SimChunk, - pub spawn_rules: SpawnRules, } #[derive(Copy, Clone)] diff --git a/world/src/generator/mod.rs b/world/src/generator/mod.rs deleted file mode 100644 index 46c9d41640..0000000000 --- a/world/src/generator/mod.rs +++ /dev/null @@ -1,39 +0,0 @@ -mod town; - -// Reexports -pub use self::town::{TownGen, TownState}; - -use crate::{column::ColumnSample, util::Sampler}; -use common::terrain::Block; -use vek::*; - -#[derive(Copy, Clone, Debug)] -pub struct SpawnRules { - pub trees: bool, - pub cliffs: bool, -} - -impl Default for SpawnRules { - fn default() -> Self { - Self { - trees: true, - cliffs: true, - } - } -} - -impl SpawnRules { - pub fn and(self, other: Self) -> Self { - Self { - trees: self.trees && other.trees, - cliffs: self.cliffs && other.cliffs, - } - } -} - -pub trait Generator<'a, T: 'a>: - Sampler<'a, Index = (&'a T, Vec3, &'a ColumnSample<'a>, f32), Sample = Option> -{ - fn get_z_limits(&self, state: &'a T, wpos: Vec2, sample: &ColumnSample) -> (f32, f32); - fn spawn_rules(&self, town: &'a TownState, wpos: Vec2) -> SpawnRules; -} diff --git a/world/src/generator/town/mod.rs b/world/src/generator/town/mod.rs deleted file mode 100644 index b04798dba5..0000000000 --- a/world/src/generator/town/mod.rs +++ /dev/null @@ -1,710 +0,0 @@ -mod util; -mod vol; - -use super::{Generator, SpawnRules}; -use crate::{ - block::{block_from_structure, BlockGen}, - column::ColumnSample, - util::Sampler, - CONFIG, -}; -use common::{ - assets, - terrain::{Block, BlockKind, Structure}, - vol::{ReadVol, Vox, WriteVol}, -}; -use hashbrown::HashSet; -use lazy_static::lazy_static; -use rand::prelude::*; -use std::{ops::Add, sync::Arc}; -use vek::*; - -use self::vol::{CellKind, ColumnKind, Module, TownCell, TownColumn, TownVol}; - -const CELL_SIZE: i32 = 9; -const CELL_HEIGHT: i32 = 9; - -pub struct TownGen; - -impl<'a> Sampler<'a> for TownGen { - type Index = (&'a TownState, Vec3, &'a ColumnSample<'a>, f32); - type Sample = Option; - - fn get(&self, (town, wpos, sample, height): Self::Index) -> Self::Sample { - let cell_pos = (wpos - town.center) - .map2(Vec3::new(CELL_SIZE, CELL_SIZE, CELL_HEIGHT), |e, sz| { - e.div_euclid(sz) - }) - .add(Vec3::from(town.vol.size() / 2)); - let inner_pos = (wpos - town.center) - .map2(Vec3::new(CELL_SIZE, CELL_SIZE, CELL_HEIGHT), |e, sz| { - e.rem_euclid(sz) - }); - - let cell = town.vol.get(cell_pos).ok()?; - - match (modules_from_kind(&cell.kind), &cell.module) { - (Some(module_list), Some(module)) => { - let transform = [ - (Vec2::new(0, 0), Vec2::unit_x(), Vec2::unit_y()), - (Vec2::new(0, 1), -Vec2::unit_y(), Vec2::unit_x()), - (Vec2::new(1, 1), -Vec2::unit_x(), -Vec2::unit_y()), - (Vec2::new(1, 0), Vec2::unit_y(), -Vec2::unit_x()), - ]; - - module_list[module.vol_idx] - .0 - .get( - Vec3::from( - transform[module.dir].0 * (CELL_SIZE - 1) - + transform[module.dir].1 * inner_pos.x - + transform[module.dir].2 * inner_pos.y, - ) + Vec3::unit_z() * inner_pos.z, - ) - .ok() - .and_then(|sb| { - block_from_structure(*sb, BlockKind::Normal, wpos, wpos.into(), 0, sample) - }) - }, - _ => match cell.kind { - CellKind::Empty => None, - CellKind::Park => None, - CellKind::Rock => Some(Block::new(BlockKind::Normal, Rgb::broadcast(100))), - CellKind::Wall => Some(Block::new(BlockKind::Normal, Rgb::broadcast(175))), - CellKind::Well => Some(Block::new(BlockKind::Normal, Rgb::broadcast(0))), - CellKind::Road => { - if (wpos.z as f32) < height - 1.0 { - Some(Block::new( - BlockKind::Normal, - Lerp::lerp( - Rgb::new(150.0, 140.0, 50.0), - Rgb::new(100.0, 95.0, 30.0), - sample.marble_small, - ) - .map(|e| e as u8), - )) - } else { - Some(Block::empty()) - } - }, - CellKind::House(idx) => Some(Block::new(BlockKind::Normal, town.houses[idx].color)), - }, - } - } -} - -impl<'a> Generator<'a, TownState> for TownGen { - fn get_z_limits( - &self, - _town: &'a TownState, - _wpos: Vec2, - sample: &ColumnSample, - ) -> (f32, f32) { - (sample.alt - 32.0, sample.alt + 75.0) - } - - fn spawn_rules(&self, town: &'a TownState, wpos: Vec2) -> SpawnRules { - SpawnRules { - trees: wpos.distance_squared(town.center.into()) > (town.radius + 32).pow(2), - cliffs: false, - } - } -} - -struct House { - color: Rgb, -} - -pub struct TownState { - center: Vec3, - radius: i32, - vol: TownVol, - houses: Vec, -} - -impl TownState { - pub fn generate(center: Vec2, gen: &mut BlockGen, rng: &mut impl Rng) -> Option { - let radius = rng.gen_range(18, 20) * 9; - let size = Vec2::broadcast(radius * 2 / 9 - 2); - - let center_col = BlockGen::sample_column(&gen.column_gen, &mut gen.column_cache, center); - - if center_col.map(|sample| sample.chaos).unwrap_or(0.0) > 0.35 - || center_col.map(|sample| sample.alt).unwrap_or(0.0) < CONFIG.sea_level + 10.0 - { - return None; - } - - let alt = center_col.map(|sample| sample.alt).unwrap_or(0.0) as i32; - - let mut vol = TownVol::generate_from( - size, - |pos| { - let wpos = center + (pos - size / 2) * CELL_SIZE + CELL_SIZE / 2; - let rel_alt = BlockGen::sample_column(&gen.column_gen, &mut gen.column_cache, wpos) - .map(|sample| sample.alt) - .unwrap_or(0.0) as i32 - + CELL_HEIGHT / 2 - - alt; - - let col = TownColumn { - ground: rel_alt.div_euclid(CELL_HEIGHT), - kind: None, - }; - - (col.ground, col) - }, - |(col, pos)| { - if pos.z >= col.ground { - TownCell::empty() - } else { - TownCell::from(CellKind::Rock) - } - }, - ); - - // Generation passes - vol.setup(rng); - vol.gen_roads(rng, 30); - vol.gen_parks(rng, 3); - vol.emplace_columns(); - let houses = vol.gen_houses(rng, 50); - vol.gen_wells(rng, 5); - vol.gen_walls(rng); - vol.resolve_modules(rng); - vol.cull_unused(); - - Some(Self { - center: Vec3::new(center.x, center.y, alt), - radius, - vol, - houses, - }) - } - - pub fn center(&self) -> Vec3 { self.center } - - pub fn radius(&self) -> i32 { self.radius } -} - -impl TownVol { - fn floodfill( - &self, - limit: Option, - mut opens: HashSet>, - mut f: impl FnMut(Vec2, &TownColumn) -> bool, - ) -> HashSet> { - let mut closed = HashSet::new(); - - while opens.len() > 0 { - let mut new_opens = HashSet::new(); - - 'search: for open in opens.iter() { - for i in -1..2 { - for j in -1..2 { - let pos = *open + Vec2::new(i, j); - - if let Some(col) = self.col(pos) { - if !closed.contains(&pos) && !opens.contains(&pos) && f(pos, col) { - match limit { - Some(limit) - if limit - <= new_opens.len() + closed.len() + opens.len() => - { - break 'search; - } - _ => { - new_opens.insert(pos); - }, - } - } - } - } - } - } - - closed = closed.union(&opens).copied().collect(); - opens = new_opens; - } - - closed - } - - fn setup(&mut self, rng: &mut impl Rng) { - // Place a single road tile at first - let root_road = self - .size() - .map(|sz| (sz / 8) * 2 + rng.gen_range(0, sz / 4) * 2); - self.set_col_kind(root_road, Some(ColumnKind::Road)); - } - - fn gen_roads(&mut self, rng: &mut impl Rng, n: usize) { - const ATTEMPTS: usize = 5; - - let mut junctions = HashSet::new(); - junctions.insert(self.choose_column(rng, |_, col| col.is_road()).unwrap()); - - for _road in 0..n { - for _ in 0..ATTEMPTS { - let start = *junctions.iter().choose(rng).unwrap(); - //let start = self.choose_column(rng, |pos, col| pos.map(|e| e % 2 == - // 0).reduce_and() && col.is_road()).unwrap(); - let dir = util::gen_dir(rng); - - // If the direction we want to paint a path in is obstructed, abandon this - // attempt - if self - .col(start + dir) - .map(|col| !col.is_empty()) - .unwrap_or(true) - { - continue; - } - - // How long should this road be? - let len = rng.gen_range(1, 10) * 2 + 1; - - // Paint the road until we hit an obstacle - let success = (1..len) - .map(|i| start + dir * i) - .try_for_each(|pos| { - if self.col(pos).map(|col| col.is_empty()).unwrap_or(false) { - self.set_col_kind(pos, Some(ColumnKind::Road)); - Ok(()) - } else { - junctions.insert(pos); - Err(()) - } - }) - .is_ok(); - - if success { - junctions.insert(start + dir * (len - 1)); - } - - break; - } - } - } - - fn gen_parks(&mut self, rng: &mut impl Rng, n: usize) { - const ATTEMPTS: usize = 5; - - for _ in 0..n { - for _ in 0..ATTEMPTS { - let start = self - .choose_column(rng, |pos, col| { - col.is_empty() - && (0..4).any(|i| { - self.col(pos + util::dir(i)) - .map(|col| col.is_road()) - .unwrap_or(false) - }) - }) - .unwrap(); - - let park = self.floodfill(Some(16), [start].iter().copied().collect(), |_, col| { - col.is_empty() - }); - - if park.len() < 4 { - continue; - } - - for cell in park { - self.set_col_kind(cell, Some(ColumnKind::Internal)); - let col = self.col(cell).unwrap(); - let ground = col.ground; - let _ = self.set(Vec3::new(cell.x, cell.y, ground), CellKind::Park.into()); - } - - break; - } - } - } - - fn gen_walls(&mut self, _rng: &mut impl Rng) { - let mut outer = HashSet::new(); - for i in 0..self.size().x { - outer.insert(Vec2::new(i, 0)); - outer.insert(Vec2::new(i, self.size().y - 1)); - } - for j in 0..self.size().y { - outer.insert(Vec2::new(0, j)); - outer.insert(Vec2::new(self.size().x - 1, j)); - } - - let outer = self.floodfill(None, outer, |_, col| col.is_empty()); - - let mut walls = HashSet::new(); - let _inner = self.floodfill( - None, - [self.size() / 2].iter().copied().collect(), - |pos, _| { - if outer.contains(&pos) { - walls.insert(pos); - false - } else { - true - } - }, - ); - - while let Some(wall) = walls - .iter() - .filter(|pos| { - let lateral_count = (0..4) - .filter(|i| walls.contains(&(**pos + util::dir(*i)))) - .count(); - let max_quadrant_count = (0..4) - .map(|i| { - let units = util::unit(i); - (0..2) - .map(|i| (0..2).map(move |j| (i, j))) - .flatten() - .filter(|(i, j)| walls.contains(&(**pos + units.0 * *i + units.1 * *j))) - .count() - }) - .max() - .unwrap(); - - lateral_count < 2 || (lateral_count == 2 && max_quadrant_count == 4) - }) - .next() - { - let wall = *wall; - walls.remove(&wall); - } - - for wall in walls.iter() { - let col = self.col(*wall).unwrap(); - let ground = col.ground; - for z in -1..3 { - let _ = self.set(Vec3::new(wall.x, wall.y, ground + z), CellKind::Wall.into()); - } - } - } - - fn emplace_columns(&mut self) { - for i in 0..self.size().x { - for j in 0..self.size().y { - let col = self.col(Vec2::new(i, j)).unwrap(); - let ground = col.ground; - - match col.kind { - None => {}, - Some(ColumnKind::Internal) => {}, - //Some(ColumnKind::External) => {} - Some(ColumnKind::Road) => { - for z in -1..2 { - let _ = self.set(Vec3::new(i, j, ground + z), CellKind::Road.into()); - } - }, - } - } - } - } - - fn gen_wells(&mut self, rng: &mut impl Rng, n: usize) { - for _ in 0..n { - if let Some(cell) = self.choose_cell(rng, |_, cell| { - if let CellKind::Park = cell.kind { - true - } else { - false - } - }) { - let _ = self.set(cell, CellKind::Well.into()); - } - } - } - - fn gen_houses(&mut self, rng: &mut impl Rng, n: usize) -> Vec { - const ATTEMPTS: usize = 10; - - let mut houses = Vec::new(); - for _ in 0..n { - for _ in 0..ATTEMPTS { - let entrance = { - let start_col = self.choose_column(rng, |_, col| col.is_road()).unwrap(); - let start = Vec3::new( - start_col.x, - start_col.y, - self.col(start_col).unwrap().ground, - ); - let dir = Vec3::from(util::gen_dir(rng)); - - if self - .get(start + dir) - .map(|col| !col.is_empty()) - .unwrap_or(true) - || self - .get(start + dir - Vec3::unit_z()) - .map(|col| !col.is_foundation()) - .unwrap_or(true) - { - continue; - } else { - start + dir - } - }; - - let mut cells = HashSet::new(); - - let mut energy = 2300; - while energy > 0 { - energy -= 1; - - let parent = *cells.iter().choose(rng).unwrap_or(&entrance); - let dir = util::UNITS_3D - .choose_weighted(rng, |pos| 1 + pos.z.max(0)) - .unwrap(); - - if self - .get(parent + dir) - .map(|cell| cell.is_empty()) - .unwrap_or(false) - && self - .get(parent + dir - Vec3::unit_z()) - .map(|cell| { - cell.is_foundation() - || cells.contains(&(parent + dir - Vec3::unit_z())) - }) - .unwrap_or(false) - && parent.z + dir.z <= entrance.z + 2 - // Maximum house height - { - cells.insert(parent + dir); - energy -= 10; - } - } - - // Remove cells that are too isolated - loop { - let cells_copy = cells.clone(); - - let mut any_removed = false; - cells.retain(|pos| { - let neighbour_count = (0..6) - .filter(|i| { - let neighbour = pos + util::dir_3d(*i); - cells_copy.contains(&neighbour) - }) - .count(); - - if neighbour_count < 3 { - any_removed = true; - false - } else { - true - } - }); - - if !any_removed { - break; - } - } - - // Get rid of houses that are too small - if cells.len() < 6 { - continue; - } - - for cell in cells { - let _ = self.set(cell, CellKind::House(houses.len()).into()); - self.set_col_kind(Vec2::from(cell), Some(ColumnKind::Internal)); - } - - houses.push(House { - color: Rgb::new(rng.gen(), rng.gen(), rng.gen()), - }); - } - } - - houses - } - - fn cull_unused(&mut self) { - for x in 0..self.size().x { - for y in 0..self.size().y { - for z in self.col_range(Vec2::new(x, y)).unwrap().rev() { - let pos = Vec3::new(x, y, z); - - // Remove foundations that don't have anything on top of them - if self.get(pos).unwrap().is_foundation() - && self - .get(pos + Vec3::unit_z()) - .map(TownCell::is_space) - .unwrap_or(true) - { - let _ = self.set(pos, TownCell::empty()); - } - } - } - } - } - - fn resolve_modules(&mut self, rng: &mut impl Rng) { - fn classify(cell: &TownCell, this_cell: &TownCell) -> ModuleKind { - match (&cell.kind, &this_cell.kind) { - (CellKind::House(a), CellKind::House(b)) if a == b => ModuleKind::This, - (CellKind::Wall, CellKind::Wall) => ModuleKind::This, - _ => ModuleKind::That, - } - } - - for x in 0..self.size().x { - for y in 0..self.size().y { - for z in self.col_range(Vec2::new(x, y)).unwrap() { - let pos = Vec3::new(x, y, z); - let this_cell = if let Ok(this_cell) = self.get(pos) { - this_cell - } else { - continue; - }; - - let mut signature = [ModuleKind::That; 6]; - for i in 0..6 { - signature[i] = self - .get(pos + util::dir_3d(i)) - .map(|cell| classify(cell, this_cell)) - .unwrap_or(ModuleKind::That); - } - - let module_list = if let Some(modules) = modules_from_kind(&this_cell.kind) { - modules - } else { - continue; - }; - - let module = module_list - .iter() - .enumerate() - .filter_map(|(i, module)| { - let perms = [[0, 1, 2, 3], [3, 0, 1, 2], [2, 3, 0, 1], [1, 2, 3, 0]]; - - let mut rotated_signature = [ModuleKind::That; 6]; - for (dir, perm) in perms.iter().enumerate() { - rotated_signature[perm[0]] = signature[0]; - rotated_signature[perm[1]] = signature[1]; - rotated_signature[perm[2]] = signature[2]; - rotated_signature[perm[3]] = signature[3]; - rotated_signature[4] = signature[4]; - rotated_signature[5] = signature[5]; - - if &module.1[0..6] == &rotated_signature[0..6] { - return Some(Module { vol_idx: i, dir }); - } - } - - None - }) - .choose(rng); - - if let Some(module) = module { - let kind = this_cell.kind.clone(); - let _ = self.set(pos, TownCell { - kind, - module: Some(module), - }); - } - } - } - } - } -} - -#[derive(Copy, Clone, PartialEq)] -pub enum ModuleKind { - This, - That, -} - -fn module(name: &str, sig: [ModuleKind; 6]) -> (Arc, [ModuleKind; 6]) { - ( - assets::load(&format!("world.module.{}", name)).unwrap(), - sig, - ) -} - -fn modules_from_kind(kind: &CellKind) -> Option<&'static [(Arc, [ModuleKind; 6])]> { - match kind { - CellKind::House(_) => Some(&HOUSE_MODULES), - CellKind::Wall => Some(&WALL_MODULES), - CellKind::Well => Some(&WELL_MODULES), - _ => None, - } -} - -lazy_static! { - pub static ref HOUSE_MODULES: Vec<(Arc, [ModuleKind; 6])> = { - use ModuleKind::*; - vec![ - module("human.floor_ground", [This, This, This, This, This, That]), - module("human.stair_ground", [This, This, This, This, This, That]), - module("human.corner_ground", [This, This, That, That, This, That]), - module("human.window_corner_ground", [ - This, This, That, That, This, That, - ]), - module("human.wall_ground", [This, This, This, That, This, That]), - module("human.door_ground", [This, This, This, That, This, That]), - module("human.window_ground", [This, This, This, That, This, That]), - module("human.floor_roof", [This, This, This, This, That, This]), - module("human.corner_roof", [This, This, That, That, That, This]), - module("human.chimney_roof", [This, This, That, That, That, This]), - module("human.wall_roof", [This, This, This, That, That, This]), - module("human.floor_upstairs", [This, This, This, This, This, This]), - module("human.balcony_upstairs", [ - This, This, This, This, This, This, - ]), - module("human.corner_upstairs", [ - This, This, That, That, This, This, - ]), - module("human.window_corner_upstairs", [ - This, This, That, That, This, This, - ]), - module("human.wall_upstairs", [This, This, This, That, This, This]), - module("human.window_upstairs", [ - This, This, This, That, This, This, - ]), - ] - }; - pub static ref WALL_MODULES: Vec<(Arc, [ModuleKind; 6])> = { - use ModuleKind::*; - vec![ - module("wall.edge_ground", [This, That, This, That, This, That]), - module("wall.edge_mid", [This, That, This, That, This, This]), - module("wall.edge_top", [This, That, This, That, That, This]), - module("wall.corner_ground", [This, This, That, That, This, That]), - module("wall.corner_mid", [This, This, That, That, This, This]), - module("wall.corner_top", [This, This, That, That, That, This]), - module("wall.end_top", [That, This, That, That, That, This]), - module("wall.single_top", [That, That, That, That, That, This]), - ] - }; - pub static ref WELL_MODULES: Vec<(Arc, [ModuleKind; 6])> = { - use ModuleKind::*; - vec![module("misc.well", [That; 6])] - }; -} - -/* -// TODO -struct ModuleModel { - near: u64, - mask: u64, - vol: Arc, -} - -#[derive(Copy, Clone)] -pub enum NearKind { - This, - That, -} - -impl ModuleModel { - pub fn generate_list(_details: &[(&str, &[([i32; 3], NearKind)])]) -> Vec { - unimplemented!() - } -} -*/ diff --git a/world/src/generator/town/util.rs b/world/src/generator/town/util.rs deleted file mode 100644 index 46fde3926f..0000000000 --- a/world/src/generator/town/util.rs +++ /dev/null @@ -1,36 +0,0 @@ -use rand::prelude::*; -use vek::*; - -pub const UNITS: [Vec2; 4] = [ - Vec2 { x: 1, y: 0 }, - Vec2 { x: 0, y: 1 }, - Vec2 { x: -1, y: 0 }, - Vec2 { x: 0, y: -1 }, -]; - -pub fn dir(i: usize) -> Vec2 { UNITS[i % 4] } - -pub fn unit(i: usize) -> (Vec2, Vec2) { (UNITS[i % 4], UNITS[(i + 1) % 4]) } - -// unused -//pub fn gen_unit(rng: &mut impl Rng) -> (Vec2, Vec2) { -// unit(rng.gen_range(0, 4)) -//} - -pub fn gen_dir(rng: &mut impl Rng) -> Vec2 { UNITS[rng.gen_range(0, 4)] } - -pub const UNITS_3D: [Vec3; 6] = [ - Vec3 { x: 1, y: 0, z: 0 }, - Vec3 { x: 0, y: 1, z: 0 }, - Vec3 { x: -1, y: 0, z: 0 }, - Vec3 { x: 0, y: -1, z: 0 }, - Vec3 { x: 0, y: 0, z: 1 }, - Vec3 { x: 0, y: 0, z: -1 }, -]; - -pub fn dir_3d(i: usize) -> Vec3 { UNITS_3D[i % 6] } - -// unused -//pub fn gen_dir_3d(rng: &mut impl Rng) -> Vec3 { -// UNITS_3D[rng.gen_range(0, 6)] -//} diff --git a/world/src/generator/town/vol.rs b/world/src/generator/town/vol.rs deleted file mode 100644 index 1d6d041ec5..0000000000 --- a/world/src/generator/town/vol.rs +++ /dev/null @@ -1,215 +0,0 @@ -use crate::util::Grid; -use common::vol::{BaseVol, ReadVol, Vox, WriteVol}; -use rand::prelude::*; -use std::ops::Range; -use vek::*; - -#[derive(Clone)] -pub enum ColumnKind { - Road, - //Wall, - Internal, - //External, // Outside the boundary wall -} - -#[derive(Clone, Default)] -pub struct TownColumn { - pub ground: i32, - pub kind: Option, -} - -impl TownColumn { - pub fn is_empty(&self) -> bool { self.kind.is_none() } - - pub fn is_road(&self) -> bool { - self.kind - .as_ref() - .map(|kind| match kind { - ColumnKind::Road => true, - _ => false, - }) - .unwrap_or(false) - } -} - -#[derive(Clone, PartialEq)] -pub struct Module { - pub vol_idx: usize, - pub dir: usize, -} - -#[derive(Clone, PartialEq)] -pub enum CellKind { - Empty, - Park, - Rock, - Road, - Wall, - House(usize), - Well, -} - -#[derive(Clone, PartialEq)] -pub struct TownCell { - pub kind: CellKind, - pub module: Option, -} - -impl TownCell { - pub fn is_space(&self) -> bool { - match self.kind { - CellKind::Empty => true, - CellKind::Park => true, - CellKind::Road => true, - _ => false, - } - } - - pub fn is_foundation(&self) -> bool { - match self.kind { - CellKind::Rock => true, - _ => false, - } - } -} - -impl Vox for TownCell { - fn empty() -> Self { - Self { - kind: CellKind::Empty, - module: None, - } - } - - fn is_empty(&self) -> bool { - match self.kind { - CellKind::Empty => true, - _ => false, - } - } -} - -impl From for TownCell { - fn from(kind: CellKind) -> Self { Self { kind, module: None } } -} - -#[derive(Debug)] -pub enum TownError { - OutOfBounds, -} - -const HEIGHT: usize = 24; -const UNDERGROUND_DEPTH: i32 = 5; - -type GridItem = (i32, TownColumn, Vec); - -pub struct TownVol { - grid: Grid, -} - -impl TownVol { - pub fn generate_from( - size: Vec2, - mut f: impl FnMut(Vec2) -> (i32, TownColumn), - mut g: impl FnMut((&TownColumn, Vec3)) -> TownCell, - ) -> Self { - let mut this = Self { - grid: Grid::new( - (0, TownColumn::default(), vec![TownCell::empty(); HEIGHT]), - size, - ), - }; - - for (pos, (base, col, cells)) in this.grid.iter_mut() { - let column = f(pos); - *base = column.0; - *col = column.1; - for z in 0..HEIGHT { - cells[z] = g(( - col, - Vec3::new(pos.x, pos.y, *base - UNDERGROUND_DEPTH + z as i32), - )); - } - } - - this - } - - pub fn size(&self) -> Vec2 { self.grid.size() } - - pub fn set_col_kind(&mut self, pos: Vec2, kind: Option) { - self.grid.get_mut(pos).map(|col| col.1.kind = kind); - } - - pub fn col(&self, pos: Vec2) -> Option<&TownColumn> { - self.grid.get(pos).map(|col| &col.1) - } - - pub fn col_range(&self, pos: Vec2) -> Option> { - self.grid.get(pos).map(|col| { - let lower = col.0 - UNDERGROUND_DEPTH; - lower..lower + HEIGHT as i32 - }) - } - - pub fn choose_column( - &self, - rng: &mut impl Rng, - mut f: impl FnMut(Vec2, &TownColumn) -> bool, - ) -> Option> { - self.grid - .iter() - .filter(|(pos, col)| f(*pos, &col.1)) - .choose(rng) - .map(|(pos, _)| pos) - } - - pub fn choose_cell( - &self, - rng: &mut impl Rng, - mut f: impl FnMut(Vec3, &TownCell) -> bool, - ) -> Option> { - self.grid - .iter() - .map(|(pos, (base, _, cells))| { - cells.iter().enumerate().map(move |(i, cell)| { - ( - Vec3::new(pos.x, pos.y, *base - UNDERGROUND_DEPTH + i as i32), - cell, - ) - }) - }) - .flatten() - .filter(|(pos, cell)| f(*pos, *cell)) - .choose(rng) - .map(|(pos, _)| pos) - } -} - -impl BaseVol for TownVol { - type Error = TownError; - type Vox = TownCell; -} - -impl ReadVol for TownVol { - fn get(&self, pos: Vec3) -> Result<&Self::Vox, Self::Error> { - match self.grid.get(Vec2::from(pos)) { - Some((base, _, cells)) => cells - .get((pos.z + UNDERGROUND_DEPTH - *base) as usize) - .ok_or(TownError::OutOfBounds), - None => Err(TownError::OutOfBounds), - } - } -} - -impl WriteVol for TownVol { - fn set(&mut self, pos: Vec3, vox: Self::Vox) -> Result<(), Self::Error> { - match self.grid.get_mut(Vec2::from(pos)) { - Some((base, _, cells)) => cells - .get_mut((pos.z + UNDERGROUND_DEPTH - *base) as usize) - .map(|cell| *cell = vox) - .ok_or(TownError::OutOfBounds), - None => Err(TownError::OutOfBounds), - } - } -} diff --git a/world/src/layer/mod.rs b/world/src/layer/mod.rs new file mode 100644 index 0000000000..6b3ebfbb50 --- /dev/null +++ b/world/src/layer/mod.rs @@ -0,0 +1,95 @@ +use crate::{ + column::ColumnSample, + util::{RandomField, Sampler}, +}; +use common::{ + terrain::{Block, BlockKind}, + vol::{BaseVol, ReadVol, RectSizedVol, Vox, WriteVol}, +}; +use std::f32; +use vek::*; + +pub fn apply_paths_to<'a>( + wpos2d: Vec2, + mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, + vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), +) { + for y in 0..vol.size_xy().y as i32 { + for x in 0..vol.size_xy().x as i32 { + let offs = Vec2::new(x, y); + + let wpos2d = wpos2d + offs; + + // Sample terrain + let col_sample = if let Some(col_sample) = get_column(offs) { + col_sample + } else { + continue; + }; + let surface_z = col_sample.riverless_alt.floor() as i32; + + let noisy_color = |col: Rgb, factor: u32| { + let nz = RandomField::new(0).get(Vec3::new(wpos2d.x, wpos2d.y, surface_z)); + col.map(|e| { + (e as u32 + nz % (factor * 2)) + .saturating_sub(factor) + .min(255) as u8 + }) + }; + + if let Some((path_dist, path_nearest)) = col_sample.path.filter(|(dist, _)| *dist < 5.0) + { + let inset = 0; + + // Try to use the column at the centre of the path for sampling to make them + // flatter + let col_pos = (offs - wpos2d).map(|e| e as f32) + path_nearest; + let col00 = get_column(col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 0)); + let col10 = get_column(col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 0)); + let col01 = get_column(col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 1)); + let col11 = get_column(col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 1)); + let col_attr = |col: &ColumnSample| { + Vec3::new(col.riverless_alt, col.alt, col.water_dist.unwrap_or(1000.0)) + }; + let [riverless_alt, alt, water_dist] = match (col00, col10, col01, col11) { + (Some(col00), Some(col10), Some(col01), Some(col11)) => Lerp::lerp( + Lerp::lerp(col_attr(col00), col_attr(col10), path_nearest.x.fract()), + Lerp::lerp(col_attr(col01), col_attr(col11), path_nearest.x.fract()), + path_nearest.y.fract(), + ), + _ => col_attr(col_sample), + } + .into_array(); + let (bridge_offset, depth) = ( + ((water_dist.max(0.0) * 0.2).min(f32::consts::PI).cos() + 1.0) * 5.0, + ((1.0 - ((water_dist + 2.0) * 0.3).min(0.0).cos().abs()) + * (riverless_alt + 5.0 - alt).max(0.0) + * 1.75 + + 3.0) as i32, + ); + let surface_z = (riverless_alt + bridge_offset).floor() as i32; + + for z in inset - depth..inset { + let _ = vol.set( + Vec3::new(offs.x, offs.y, surface_z + z), + if bridge_offset >= 2.0 && path_dist >= 3.0 || z < inset - 1 { + Block::new(BlockKind::Normal, noisy_color(Rgb::new(80, 80, 100), 8)) + } else { + let path_color = col_sample + .sub_surface_color + .map(|e| (e * 255.0 * 0.7) as u8); + Block::new(BlockKind::Normal, noisy_color(path_color, 8)) + }, + ); + } + let head_space = (8 - (path_dist * 0.25).powf(6.0).round() as i32).max(1); + for z in inset..inset + head_space { + let pos = Vec3::new(offs.x, offs.y, surface_z + z); + if vol.get(pos).unwrap().kind() != BlockKind::Water { + let _ = vol.set(pos, Block::empty()); + } + } + } + } + } +} diff --git a/world/src/lib.rs b/world/src/lib.rs index acb9b94ca4..6f47c7a311 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -4,10 +4,12 @@ mod all; mod block; +pub mod civ; mod column; pub mod config; -pub mod generator; +pub mod layer; pub mod sim; +pub mod site; pub mod util; // Reexports @@ -16,10 +18,11 @@ pub use crate::config::CONFIG; use crate::{ block::BlockGen, column::{ColumnGen, ColumnSample}, - util::Sampler, + util::{Grid, Sampler}, }; use common::{ - generation::{ChunkSupplement, EntityInfo, EntityKind}, + comp::{self, bird_medium, critter, quadruped_medium, quadruped_small}, + generation::{ChunkSupplement, EntityInfo}, terrain::{Block, BlockKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize}, vol::{ReadVol, RectVolSize, Vox, WriteVol}, }; @@ -34,17 +37,20 @@ pub enum Error { pub struct World { sim: sim::WorldSim, + civs: civ::Civs, } impl World { pub fn generate(seed: u32, opts: sim::WorldOpts) -> Self { - Self { - sim: sim::WorldSim::generate(seed, opts), - } + let mut sim = sim::WorldSim::generate(seed, opts); + let civs = civ::Civs::generate(seed, &mut sim); + Self { sim, civs } } pub fn sim(&self) -> &sim::WorldSim { &self.sim } + pub fn civs(&self) -> &civ::Civs { &self.civs } + pub fn tick(&self, _dt: Duration) { // TODO } @@ -63,8 +69,24 @@ impl World { // TODO: misleading name mut should_continue: impl FnMut() -> bool, ) -> Result<(TerrainChunk, ChunkSupplement), ()> { + let mut sampler = self.sample_blocks(); + + let chunk_wpos2d = Vec2::from(chunk_pos) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); + let grid_border = 4; + let zcache_grid = Grid::populate_from( + TerrainChunkSize::RECT_SIZE.map(|e| e as i32) + grid_border * 2, + |offs| sampler.get_z_cache(chunk_wpos2d - grid_border + offs), + ); + let air = Block::empty(); - let stone = Block::new(BlockKind::Dense, Rgb::new(200, 220, 255)); + let stone = Block::new( + BlockKind::Dense, + zcache_grid + .get(grid_border + TerrainChunkSize::RECT_SIZE.map(|e| e as i32) / 2) + .and_then(|zcache| zcache.as_ref()) + .map(|zcache| zcache.sample.stone_col) + .unwrap_or(Rgb::new(125, 120, 130)), + ); let water = Block::new(BlockKind::Water, Rgb::new(60, 90, 190)); let _chunk_size2d = TerrainChunkSize::RECT_SIZE; @@ -93,9 +115,6 @@ impl World { }; let meta = TerrainChunkMeta::new(sim_chunk.get_name(&self.sim), sim_chunk.get_biome()); - let mut sampler = self.sample_blocks(); - - let chunk_block_pos = Vec3::from(chunk_pos) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); let mut chunk = TerrainChunk::new(base_z, stone, air, meta); for y in 0..TerrainChunkSize::RECT_SIZE.y as i32 { @@ -103,12 +122,12 @@ impl World { if should_continue() { return Err(()); }; - let wpos2d = Vec2::new(x, y) - + Vec2::from(chunk_pos) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); - let z_cache = match sampler.get_z_cache(wpos2d) { - Some(z_cache) => z_cache, - None => continue, + let offs = Vec2::new(x, y); + + let z_cache = match zcache_grid.get(grid_border + offs) { + Some(Some(z_cache)) => z_cache, + _ => continue, }; let (min_z, only_structures_min_z, max_z) = z_cache.get_z_limits(&mut sampler); @@ -119,7 +138,7 @@ impl World { (min_z as i32..max_z as i32).for_each(|z| { let lpos = Vec3::new(x, y, z); - let wpos = chunk_block_pos + lpos; + let wpos = Vec3::from(chunk_wpos2d) + lpos; let only_structures = lpos.z >= only_structures_min_z as i32; if let Some(block) = @@ -131,45 +150,73 @@ impl World { } } + let sample_get = |offs| { + zcache_grid + .get(grid_border + offs) + .map(Option::as_ref) + .flatten() + .map(|zc| &zc.sample) + }; + + let mut rng = rand::thread_rng(); + + // Apply site generation + sim_chunk + .sites + .iter() + .for_each(|site| site.apply_to(chunk_wpos2d, sample_get, &mut chunk)); + + // Apply paths + layer::apply_paths_to(chunk_wpos2d, sample_get, &mut chunk); + let gen_entity_pos = || { let lpos2d = TerrainChunkSize::RECT_SIZE - .map(|sz| rand::thread_rng().gen::().rem_euclid(sz)); - let mut lpos = Vec3::new(lpos2d.x as i32, lpos2d.y as i32, 0); + .map(|sz| rand::thread_rng().gen::().rem_euclid(sz) as i32); + let mut lpos = Vec3::new( + lpos2d.x, + lpos2d.y, + sample_get(lpos2d).map(|s| s.alt as i32 - 32).unwrap_or(0), + ); while chunk.get(lpos).map(|vox| !vox.is_empty()).unwrap_or(false) { lpos.z += 1; } - (chunk_block_pos + lpos).map(|e| e as f32) + 0.5 + (Vec3::from(chunk_wpos2d) + lpos).map(|e: i32| e as f32) + 0.5 }; const SPAWN_RATE: f32 = 0.1; - const BOSS_RATE: f32 = 0.03; let mut supplement = ChunkSupplement { - entities: if rand::thread_rng().gen::() < SPAWN_RATE + entities: if rng.gen::() < SPAWN_RATE && sim_chunk.chaos < 0.5 && !sim_chunk.is_underwater() { - vec![EntityInfo { - pos: gen_entity_pos(), - kind: if rand::thread_rng().gen::() < BOSS_RATE { - EntityKind::Boss - } else { - EntityKind::Enemy - }, - }] + let entity = EntityInfo::at(gen_entity_pos()) + .with_alignment(comp::Alignment::Wild) + .do_if(rng.gen_range(0, 8) == 0, |e| e.into_giant()) + .with_body(match rng.gen_range(0, 4) { + 0 => comp::Body::QuadrupedMedium(quadruped_medium::Body::random()), + 1 => comp::Body::BirdMedium(bird_medium::Body::random()), + 2 => comp::Body::Critter(critter::Body::random()), + _ => comp::Body::QuadrupedSmall(quadruped_small::Body::random()), + }) + .with_automatic_name(); + + vec![entity] } else { Vec::new() }, }; if sim_chunk.contains_waypoint { - supplement = supplement.with_entity(EntityInfo { - pos: gen_entity_pos(), - kind: EntityKind::Waypoint, - }); + supplement.add_entity(EntityInfo::at(gen_entity_pos()).into_waypoint()); } + // Apply site supplementary information + sim_chunk.sites.iter().for_each(|site| { + site.apply_supplement(&mut rng, chunk_wpos2d, sample_get, &mut supplement) + }); + Ok((chunk, supplement)) } } diff --git a/world/src/sim/erosion.rs b/world/src/sim/erosion.rs index d359e04baf..27d4f05014 100644 --- a/world/src/sim/erosion.rs +++ b/world/src/sim/erosion.rs @@ -110,6 +110,14 @@ pub enum RiverKind { } impl RiverKind { + pub fn is_ocean(&self) -> bool { + if let RiverKind::Ocean = *self { + true + } else { + false + } + } + pub fn is_river(&self) -> bool { if let RiverKind::River { .. } = *self { true @@ -187,6 +195,13 @@ pub struct RiverData { } impl RiverData { + pub fn is_ocean(&self) -> bool { + self.river_kind + .as_ref() + .map(RiverKind::is_ocean) + .unwrap_or(false) + } + pub fn is_river(&self) -> bool { self.river_kind .as_ref() @@ -200,6 +215,10 @@ impl RiverData { .map(RiverKind::is_lake) .unwrap_or(false) } + + pub fn near_river(&self) -> bool { self.is_river() || self.neighbor_rivers.len() > 0 } + + pub fn near_water(&self) -> bool { self.near_river() || self.is_lake() || self.is_ocean() } } /// Draw rivers and assign them heights, widths, and velocities. Take some diff --git a/world/src/sim/location.rs b/world/src/sim/location.rs index c337357afe..2eeb1c7a3d 100644 --- a/world/src/sim/location.rs +++ b/world/src/sim/location.rs @@ -1,4 +1,3 @@ -use super::Settlement; use hashbrown::HashSet; use rand::{seq::SliceRandom, Rng}; use vek::*; @@ -9,7 +8,6 @@ pub struct Location { pub(crate) center: Vec2, pub(crate) kingdom: Option, pub(crate) neighbours: HashSet, - pub(crate) settlement: Settlement, } impl Location { @@ -19,7 +17,6 @@ impl Location { center, kingdom: None, neighbours: HashSet::default(), - settlement: Settlement::generate(rng), } } diff --git a/world/src/sim/map.rs b/world/src/sim/map.rs index 0707abd0d6..00849496e3 100644 --- a/world/src/sim/map.rs +++ b/world/src/sim/map.rs @@ -147,29 +147,39 @@ impl MapConfig { let pos = (focus_rect + Vec2::new(i as f64, j as f64) * scale).map(|e: f64| e as i32); - let (alt, basement, water_alt, humidity, temperature, downhill, river_kind) = - sampler - .get(pos) - .map(|sample| { - ( - sample.alt, - sample.basement, - sample.water_alt, - sample.humidity, - sample.temp, - sample.downhill, - sample.river.river_kind, - ) - }) - .unwrap_or(( - CONFIG.sea_level, - CONFIG.sea_level, - CONFIG.sea_level, - 0.0, - 0.0, - None, - None, - )); + let ( + alt, + basement, + water_alt, + humidity, + temperature, + downhill, + river_kind, + is_path, + ) = sampler + .get(pos) + .map(|sample| { + ( + sample.alt, + sample.basement, + sample.water_alt, + sample.humidity, + sample.temp, + sample.downhill, + sample.river.river_kind, + sample.path.is_path(), + ) + }) + .unwrap_or(( + CONFIG.sea_level, + CONFIG.sea_level, + CONFIG.sea_level, + 0.0, + 0.0, + None, + None, + false, + )); let humidity = humidity.min(1.0).max(0.0); let temperature = temperature.min(1.0).max(-1.0) * 0.5 + 0.5; let pos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32); @@ -295,6 +305,12 @@ impl MapConfig { ), }; + let rgba = if is_path { + (0x37, 0x29, 0x23, 0xFF) + } else { + rgba + }; + write_pixel(Vec2::new(i, j), rgba); }); diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index feb3643eb5..131eabac01 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -2,7 +2,7 @@ mod diffusion; mod erosion; mod location; mod map; -mod settlement; +mod path; mod util; // Reexports @@ -15,7 +15,7 @@ pub use self::{ }, location::Location, map::{MapConfig, MapDebug}, - settlement::Settlement, + path::PathData, util::{ cdf_irwin_hall, downhill, get_oceans, local_cells, map_edge_factor, neighbors, uniform_idx_as_vec2, uniform_noise, uphill, vec2_as_uniform_idx, InverseCdf, ScaleBias, @@ -25,18 +25,17 @@ pub use self::{ use crate::{ all::ForestKind, - block::BlockGen, - column::ColumnGen, - generator::TownState, - util::{seed_expan, FastNoise, RandomField, Sampler, StructureGen2d}, + civ::Place, + site::Site, + util::{seed_expan, FastNoise, RandomField, StructureGen2d, LOCALITY, NEIGHBORS}, CONFIG, }; use common::{ assets, + store::Id, terrain::{BiomeKind, TerrainChunkSize}, vol::RectVolSize, }; -use hashbrown::HashMap; use noise::{ BasicMulti, Billow, Fbm, HybridMulti, MultiFractal, NoiseFn, RangeFunction, RidgedMulti, Seedable, SuperSimplex, Worley, @@ -52,7 +51,6 @@ use std::{ io::{BufReader, BufWriter}, ops::{Add, Div, Mul, Neg, Sub}, path::PathBuf, - sync::Arc, }; use vek::*; @@ -1305,6 +1303,8 @@ impl WorldSim { this } + pub fn get_size(&self) -> Vec2 { WORLD_SIZE.map(|e| e as u32) } + /// Draw a map of the world based on chunk information. Returns a buffer of /// u32s. pub fn get_map(&self) -> Vec { @@ -1385,6 +1385,7 @@ impl WorldSim { }); // Place the locations onto the world + /* let gen = StructureGen2d::new(self.seed, cell_size as u32, cell_size as u32 / 2); self.chunks @@ -1424,74 +1425,9 @@ impl WorldSim { .cloned() .unwrap_or(None) .map(|loc_idx| LocationInfo { loc_idx, near }); - - let town_size = 200; - let in_town = chunk - .location - .as_ref() - .map(|l| { - locations[l.loc_idx] - .center - .map(|e| e as i64) - .distance_squared(block_pos.map(|e| e as i64)) - < town_size * town_size - }) - .unwrap_or(false); - - if in_town { - chunk.spawn_rate = 0.0; - } } }); - - // Stage 2 - towns! - let chunk_idx_center = |e: Vec2| { - e.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| { - e * sz as i32 + sz as i32 / 2 - }) - }; - let maybe_towns = self - .gen_ctx - .town_gen - .par_iter( - chunk_idx_center(Vec2::zero()), - chunk_idx_center(WORLD_SIZE.map(|e| e as i32)), - ) - .map_init( - || Box::new(BlockGen::new(ColumnGen::new(self))), - |mut block_gen, (pos, seed)| { - let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed)); - // println!("Town: {:?}", town); - TownState::generate(pos, &mut block_gen, &mut rng).map(|t| (pos, Arc::new(t))) - }, - ) - .filter_map(|x| x) - .collect::>(); - - let gen_ctx = &self.gen_ctx; - self.chunks - .par_iter_mut() - .enumerate() - .for_each(|(ij, chunk)| { - let chunk_pos = uniform_idx_as_vec2(ij); - let wpos = chunk_idx_center(chunk_pos); - - let near_towns = gen_ctx.town_gen.get(wpos); - let town = near_towns - .iter() - .min_by_key(|(pos, _seed)| wpos.distance_squared(*pos)); - - let maybe_town = town - .and_then(|(pos, _seed)| maybe_towns.get(pos)) - // Only care if we're close to the town - .filter(|town| { - Vec2::from(town.center()).distance_squared(wpos) - < town.radius().add(64).pow(2) - }) - .cloned(); - - chunk.structures.town = maybe_town; - }); + */ // Create waypoints const WAYPOINT_EVERY: usize = 16; @@ -1552,10 +1488,28 @@ impl WorldSim { } } + pub fn get_gradient_approx(&self, chunk_pos: Vec2) -> Option { + let a = self.get(chunk_pos)?; + if let Some(downhill) = a.downhill { + let b = self.get( + downhill.map2(Vec2::from(TerrainChunkSize::RECT_SIZE), |e, sz: u32| { + e / (sz as i32) + }), + )?; + Some((a.alt - b.alt).abs() / TerrainChunkSize::RECT_SIZE.x as f32) + } else { + Some(0.0) + } + } + + pub fn get_alt_approx(&self, wpos: Vec2) -> Option { + self.get_interpolated(wpos, |chunk| chunk.alt) + } + pub fn get_wpos(&self, wpos: Vec2) -> Option<&SimChunk> { self.get( wpos.map2(Vec2::from(TerrainChunkSize::RECT_SIZE), |e, sz: u32| { - e / sz as i32 + e.div_euclid(sz as i32) }), ) } @@ -1751,8 +1705,78 @@ impl WorldSim { Some(z0 + z1 + z2 + z3) } + + pub fn get_nearest_path(&self, wpos: Vec2) -> Option<(f32, Vec2)> { + let chunk_pos = wpos.map2(Vec2::from(TerrainChunkSize::RECT_SIZE), |e, sz: u32| { + e.div_euclid(sz as i32) + }); + let get_chunk_centre = |chunk_pos: Vec2| { + chunk_pos.map2(Vec2::from(TerrainChunkSize::RECT_SIZE), |e, sz: u32| { + e * sz as i32 + sz as i32 / 2 + }) + }; + + LOCALITY + .iter() + .filter_map(|ctrl| { + let chunk = self.get(chunk_pos + *ctrl)?; + let ctrl_pos = + get_chunk_centre(chunk_pos + *ctrl).map(|e| e as f32) + chunk.path.offset; + + let chunk_connections = chunk.path.neighbors.count_ones(); + if chunk_connections == 0 { + return None; + } + + let (start_pos, _start_idx) = if chunk_connections != 2 { + (ctrl_pos, None) + } else { + let (start_idx, start_rpos) = NEIGHBORS + .iter() + .copied() + .enumerate() + .find(|(i, _)| chunk.path.neighbors & (1 << *i as u8) != 0) + .unwrap(); + let start_pos_chunk = chunk_pos + *ctrl + start_rpos; + ( + get_chunk_centre(start_pos_chunk).map(|e| e as f32) + + self.get(start_pos_chunk)?.path.offset, + Some(start_idx), + ) + }; + + Some( + NEIGHBORS + .iter() + .enumerate() + .filter(move |(i, _)| chunk.path.neighbors & (1 << *i as u8) != 0) + .filter_map(move |(_, end_rpos)| { + let end_pos_chunk = chunk_pos + *ctrl + end_rpos; + let end_pos = get_chunk_centre(end_pos_chunk).map(|e| e as f32) + + self.get(end_pos_chunk)?.path.offset; + + let bez = QuadraticBezier2 { + start: (start_pos + ctrl_pos) / 2.0, + ctrl: ctrl_pos, + end: (end_pos + ctrl_pos) / 2.0, + }; + let nearest_interval = bez + .binary_search_point_by_steps(wpos.map(|e| e as f32), 16, 0.001) + .0 + .clamped(0.0, 1.0); + let pos = bez.evaluate(nearest_interval); + let dist_sqrd = pos.distance_squared(wpos.map(|e| e as f32)); + Some((dist_sqrd, pos)) + }), + ) + }) + .flatten() + .min_by_key(|(dist_sqrd, _)| (dist_sqrd * 1024.0) as i32) + .map(|(dist, pos)| (dist.sqrt(), pos)) + } } +#[derive(Debug)] pub struct SimChunk { pub chaos: f32, pub alt: f32, @@ -1768,10 +1792,12 @@ pub struct SimChunk { pub tree_density: f32, pub forest_kind: ForestKind, pub spawn_rate: f32, - pub location: Option, pub river: RiverData, + pub warp_factor: f32, - pub structures: Structures, + pub sites: Vec, + pub place: Option>, + pub path: PathData, pub contains_waypoint: bool, } @@ -1783,17 +1809,6 @@ pub struct RegionInfo { pub seed: u32, } -#[derive(Clone)] -pub struct LocationInfo { - pub loc_idx: usize, - pub near: Vec, -} - -#[derive(Clone)] -pub struct Structures { - pub town: Option>, -} - impl SimChunk { fn generate(posi: usize, gen_ctx: &GenCtx, gen_cdf: &GenCdf) -> Self { let pos = uniform_idx_as_vec2(posi); @@ -1858,7 +1873,9 @@ impl SimChunk { ) }; - let cliff = gen_ctx.cliff_nz.get((wposf.div(2048.0)).into_array()) as f32 + chaos * 0.2; + //let cliff = gen_ctx.cliff_nz.get((wposf.div(2048.0)).into_array()) as f32 + + // chaos * 0.2; + let cliff = 0.0; // Disable cliffs // Logistic regression. Make sure x ∈ (0, 1). let logit = |x: f64| x.ln() - x.neg().ln_1p(); @@ -2013,23 +2030,33 @@ impl SimChunk { } }, spawn_rate: 1.0, - location: None, river, - structures: Structures { town: None }, + warp_factor: 1.0, + + sites: Vec::new(), + place: None, + path: PathData::default(), contains_waypoint: false, } } - pub fn is_underwater(&self) -> bool { self.river.river_kind.is_some() } + pub fn is_underwater(&self) -> bool { + self.water_alt > self.alt || self.river.river_kind.is_some() + } pub fn get_base_z(&self) -> f32 { self.alt - self.chaos * 50.0 - 16.0 } - pub fn get_name(&self, world: &WorldSim) -> Option { + pub fn get_name(&self, _world: &WorldSim) -> Option { + // TODO + None + + /* if let Some(loc) = &self.location { Some(world.locations[loc.loc_idx].name().to_string()) } else { None } + */ } pub fn get_biome(&self) -> BiomeKind { diff --git a/world/src/sim/path.rs b/world/src/sim/path.rs new file mode 100644 index 0000000000..525069a1d8 --- /dev/null +++ b/world/src/sim/path.rs @@ -0,0 +1,21 @@ +use vek::*; + +#[derive(Debug)] +pub struct PathData { + pub offset: Vec2, /* Offset from centre of chunk: must not be more than half chunk + * width in any direction */ + pub neighbors: u8, // One bit for each neighbor +} + +impl PathData { + pub fn is_path(&self) -> bool { self.neighbors != 0 } +} + +impl Default for PathData { + fn default() -> Self { + Self { + offset: Vec2::zero(), + neighbors: 0, + } + } +} diff --git a/world/src/sim/settlement.rs b/world/src/sim/settlement.rs deleted file mode 100644 index 951350bcfb..0000000000 --- a/world/src/sim/settlement.rs +++ /dev/null @@ -1,86 +0,0 @@ -use rand::Rng; -use vek::*; - -#[derive(Clone, Debug)] -pub struct Settlement { - lot: Lot, -} - -impl Settlement { - pub fn generate(rng: &mut impl Rng) -> Self { - Self { - lot: Lot::generate(0, 32.0, 1.0, rng), - } - } - - pub fn get_at(&self, pos: Vec2) -> Option<&Building> { self.lot.get_at(pos) } -} - -#[derive(Clone, Debug)] -pub struct Building { - pub seed: u32, -} - -#[derive(Clone, Debug)] -enum Lot { - None, - One(Building), - Many { split_x: bool, lots: Vec }, -} - -impl Lot { - pub fn generate(deep: usize, depth: f32, aspect: f32, rng: &mut impl Rng) -> Self { - let depth = if deep < 3 { 8.0 } else { depth }; - - if (depth < 1.0 || deep > 6) && !(deep < 3 || deep % 2 == 1) { - if rng.gen::() < 0.5 { - Lot::One(Building { seed: rng.gen() }) - } else { - Lot::None - } - } else { - Lot::Many { - split_x: aspect > 1.0, - lots: { - let pow2 = 1 + rng.gen::() % 1; - let n = 1 << pow2; - - let new_aspect = if aspect > 1.0 { - aspect / n as f32 - } else { - aspect * n as f32 - }; - - let vari = (rng.gen::() - 0.35) * 2.8; - let new_depth = depth * 0.5 * (1.0 + vari); - - (0..n) - .map(|_| Lot::generate(deep + 1, new_depth, new_aspect, rng)) - .collect() - }, - } - } - } - - pub fn get_at(&self, pos: Vec2) -> Option<&Building> { - match self { - Lot::None => None, - Lot::One(building) => { - if pos.map(|e| e > 0.1 && e < 0.9).reduce_and() { - Some(building) - } else { - None - } - }, - Lot::Many { split_x, lots } => { - let split_dim = if *split_x { pos.x } else { pos.y }; - let idx = (split_dim * lots.len() as f32).floor() as usize; - lots[idx.min(lots.len() - 1)].get_at(if *split_x { - Vec2::new((pos.x * lots.len() as f32).fract(), pos.y) - } else { - Vec2::new(pos.x, (pos.y * lots.len() as f32).fract()) - }) - }, - } - } -} diff --git a/world/src/site/dungeon/mod.rs b/world/src/site/dungeon/mod.rs new file mode 100644 index 0000000000..881b5dea4d --- /dev/null +++ b/world/src/site/dungeon/mod.rs @@ -0,0 +1,489 @@ +use super::SpawnRules; +use crate::{ + column::ColumnSample, + sim::WorldSim, + site::BlockMask, + util::{attempt, Grid, RandomField, Sampler, CARDINALS, DIRS}, +}; +use common::{ + assets, + astar::Astar, + comp, + generation::{ChunkSupplement, EntityInfo}, + store::{Id, Store}, + terrain::{Block, BlockKind, TerrainChunkSize}, + vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol}, +}; +use rand::prelude::*; +use std::f32; +use vek::*; + +impl WorldSim { + #[allow(dead_code)] + fn can_host_dungeon(&self, pos: Vec2) -> bool { + self.get(pos) + .map(|chunk| !chunk.near_cliffs && !chunk.river.is_river() && !chunk.river.is_lake()) + .unwrap_or(false) + && self + .get_gradient_approx(pos) + .map(|grad| grad > 0.25 && grad < 1.5) + .unwrap_or(false) + } +} + +pub struct Dungeon { + origin: Vec2, + alt: i32, + #[allow(dead_code)] + noise: RandomField, + floors: Vec, +} + +pub struct GenCtx<'a, R: Rng> { + sim: Option<&'a WorldSim>, + rng: &'a mut R, +} + +impl Dungeon { + pub fn generate(wpos: Vec2, sim: Option<&WorldSim>, rng: &mut impl Rng) -> Self { + let mut ctx = GenCtx { sim, rng }; + let this = Self { + origin: wpos, + alt: ctx + .sim + .and_then(|sim| sim.get_alt_approx(wpos)) + .unwrap_or(0.0) as i32 + + 6, + noise: RandomField::new(ctx.rng.gen()), + floors: (0..6) + .scan(Vec2::zero(), |stair_tile, level| { + let (floor, st) = Floor::generate(&mut ctx, *stair_tile, level); + *stair_tile = st; + Some(floor) + }) + .collect(), + }; + + this + } + + pub fn radius(&self) -> f32 { 1200.0 } + + pub fn spawn_rules(&self, _wpos: Vec2) -> SpawnRules { + SpawnRules { + ..SpawnRules::default() + } + } + + pub fn apply_to<'a>( + &'a self, + wpos2d: Vec2, + _get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, + vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), + ) { + for y in 0..vol.size_xy().y as i32 { + for x in 0..vol.size_xy().x as i32 { + let offs = Vec2::new(x, y); + + let wpos2d = wpos2d + offs; + let rpos = wpos2d - self.origin; + + let mut z = self.alt; + for floor in &self.floors { + z -= floor.total_depth(); + + let mut sampler = floor.col_sampler(rpos, z); + + for rz in 0..floor.total_depth() { + if let Some(block) = sampler(rz).finish() { + let _ = vol.set(Vec3::new(offs.x, offs.y, z + rz), block); + } + } + } + } + } + } + + pub fn apply_supplement<'a>( + &'a self, + rng: &mut impl Rng, + wpos2d: Vec2, + _get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, + supplement: &mut ChunkSupplement, + ) { + let rpos = wpos2d - self.origin; + let area = Aabr { + min: rpos, + max: rpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + }; + + if area.contains_point(Vec2::zero()) { + let offs = Vec2::new(rng.gen_range(-1.0, 1.0), rng.gen_range(-1.0, 1.0)) + .try_normalized() + .unwrap_or(Vec2::unit_y()) + * 16.0; + supplement.add_entity( + EntityInfo::at( + Vec3::new(self.origin.x, self.origin.y, self.alt + 4).map(|e| e as f32) + + Vec3::from(offs), + ) + .into_waypoint(), + ); + } + + let mut z = self.alt; + for floor in &self.floors { + z -= floor.total_depth(); + let origin = Vec3::new(self.origin.x, self.origin.y, z); + floor.apply_supplement(rng, area, origin, supplement); + } + } +} + +const TILE_SIZE: i32 = 13; + +#[derive(Clone)] +pub enum Tile { + UpStair, + DownStair, + Room(Id), + Tunnel, + Solid, +} + +impl Tile { + fn is_passable(&self) -> bool { + match self { + Tile::UpStair => true, + Tile::DownStair => true, + Tile::Room(_) => true, + Tile::Tunnel => true, + _ => false, + } + } +} + +pub struct Room { + seed: u32, + loot_density: f32, + enemy_density: f32, + area: Rect, +} + +pub struct Floor { + tile_offset: Vec2, + tiles: Grid, + rooms: Store, + solid_depth: i32, + hollow_depth: i32, + #[allow(dead_code)] + stair_tile: Vec2, +} + +const FLOOR_SIZE: Vec2 = Vec2::new(18, 18); + +impl Floor { + pub fn generate( + ctx: &mut GenCtx, + stair_tile: Vec2, + level: i32, + ) -> (Self, Vec2) { + let new_stair_tile = std::iter::from_fn(|| { + Some(FLOOR_SIZE.map(|sz| ctx.rng.gen_range(-sz / 2 + 2, sz / 2 - 1))) + }) + .filter(|pos| *pos != stair_tile) + .take(8) + .max_by_key(|pos| (*pos - stair_tile).map(|e| e.abs()).sum()) + .unwrap(); + + let tile_offset = -FLOOR_SIZE / 2; + let mut this = Floor { + tile_offset, + tiles: Grid::new(FLOOR_SIZE, Tile::Solid), + rooms: Store::default(), + solid_depth: if level == 0 { 80 } else { 13 * 2 }, + hollow_depth: 13, + stair_tile: new_stair_tile - tile_offset, + }; + + // Create rooms for entrance and exit + this.create_room(Room { + seed: ctx.rng.gen(), + loot_density: 0.0, + enemy_density: 0.0, + area: Rect::from((stair_tile - tile_offset - 1, Extent2::broadcast(3))), + }); + this.tiles.set(stair_tile - tile_offset, Tile::UpStair); + this.create_room(Room { + seed: ctx.rng.gen(), + loot_density: 0.0, + enemy_density: 0.0, + area: Rect::from((new_stair_tile - tile_offset - 1, Extent2::broadcast(3))), + }); + this.tiles + .set(new_stair_tile - tile_offset, Tile::DownStair); + + this.create_rooms(ctx, level, 7); + // Create routes between all rooms + let room_areas = this.rooms.iter().map(|r| r.area).collect::>(); + for a in room_areas.iter() { + for b in room_areas.iter() { + this.create_route(ctx, a.center(), b.center()); + } + } + + (this, new_stair_tile) + } + + fn create_room(&mut self, room: Room) -> Id { + let area = room.area; + let id = self.rooms.insert(room); + for x in 0..area.extent().w { + for y in 0..area.extent().h { + self.tiles + .set(area.position() + Vec2::new(x, y), Tile::Room(id)); + } + } + id + } + + fn create_rooms(&mut self, ctx: &mut GenCtx, level: i32, n: usize) { + let dim_limits = (3, 6); + + for _ in 0..n { + let area = match attempt(64, || { + let sz = Vec2::::zero().map(|_| ctx.rng.gen_range(dim_limits.0, dim_limits.1)); + let pos = FLOOR_SIZE.map2(sz, |floor_sz, room_sz| { + ctx.rng.gen_range(0, floor_sz + 1 - room_sz) + }); + let area = Rect::from((pos, Extent2::from(sz))); + let area_border = Rect::from((pos - 1, Extent2::from(sz) + 2)); // The room, but with some personal space + + // Ensure no overlap + if self + .rooms + .iter() + .any(|r| r.area.collides_with_rect(area_border)) + { + return None; + } + + Some(area) + }) { + Some(area) => area, + None => return, + }; + + self.create_room(Room { + seed: ctx.rng.gen(), + loot_density: 0.000025 + level as f32 * 0.00015, + enemy_density: 0.001 + level as f32 * 0.00004, + area, + }); + } + } + + fn create_route(&mut self, _ctx: &mut GenCtx, a: Vec2, b: Vec2) { + let heuristic = move |l: &Vec2| (l - b).map(|e| e.abs()).reduce_max() as f32; + let neighbors = |l: &Vec2| { + let l = *l; + CARDINALS + .iter() + .map(move |dir| l + dir) + .filter(|pos| self.tiles.get(*pos).is_some()) + }; + let transition = |_a: &Vec2, b: &Vec2| match self.tiles.get(*b) { + Some(Tile::Room(_)) | Some(Tile::Tunnel) => 1.0, + Some(Tile::Solid) => 25.0, + Some(Tile::UpStair) | Some(Tile::DownStair) => 0.0, + _ => 100000.0, + }; + let satisfied = |l: &Vec2| *l == b; + let mut astar = Astar::new(20000, a, heuristic); + let path = astar + .poll( + FLOOR_SIZE.product() as usize + 1, + heuristic, + neighbors, + transition, + satisfied, + ) + .into_path() + .expect("No route between locations - this shouldn't be able to happen"); + + for pos in path.iter() { + if let Some(tile @ Tile::Solid) = self.tiles.get_mut(*pos) { + *tile = Tile::Tunnel; + } + } + } + + pub fn apply_supplement( + &self, + rng: &mut impl Rng, + area: Aabr, + origin: Vec3, + supplement: &mut ChunkSupplement, + ) { + let align = |e: i32| { + e.div_euclid(TILE_SIZE) + + if e.rem_euclid(TILE_SIZE) > TILE_SIZE / 2 { + 1 + } else { + 0 + } + }; + let aligned_area = Aabr { + min: area.min.map(align) + self.tile_offset, + max: area.max.map(align) + self.tile_offset, + }; + + for x in aligned_area.min.x..aligned_area.max.x { + for y in aligned_area.min.y..aligned_area.max.y { + let tile_pos = Vec2::new(x, y); + if let Some(Tile::Room(room)) = self.tiles.get(tile_pos) { + let room = &self.rooms[*room]; + + for x in 0..TILE_SIZE { + for y in 0..TILE_SIZE { + let pos = tile_pos * TILE_SIZE + Vec2::new(x, y); + + let nth_block = + pos.x + TILE_SIZE + (pos.y + TILE_SIZE) * TILE_SIZE * FLOOR_SIZE.x; + if nth_block.rem_euclid(room.enemy_density.recip() as i32) == 0 { + // Bad + let entity = EntityInfo::at( + (origin + + Vec3::from(self.tile_offset + tile_pos) * TILE_SIZE + + TILE_SIZE / 2) + .map(|e| e as f32) + // Randomly displace them a little + + Vec3::::iota() + .map(|e| (RandomField::new(room.seed.wrapping_add(10 + e)).get(Vec3::from(tile_pos)) % 32) as i32 - 16) + .map(|e| e as f32 / 16.0), + ) + .do_if(RandomField::new(room.seed.wrapping_add(1)).chance(Vec3::from(tile_pos), 0.2), |e| e.into_giant()) + .with_alignment(comp::Alignment::Enemy) + .with_body(comp::Body::Humanoid(comp::humanoid::Body::random())) + .with_automatic_name() + .with_main_tool(assets::load_expect_cloned(match rng.gen_range(0, 6) { + 0 => "common.items.weapons.starter_axe", + 1 => "common.items.weapons.starter_sword", + 2 => "common.items.weapons.short_sword_0", + 3 => "common.items.weapons.hammer_1", + 4 => "common.items.weapons.starter_staff", + _ => "common.items.weapons.starter_bow", + })); + + supplement.add_entity(entity); + } + } + } + } + } + } + } + + pub fn total_depth(&self) -> i32 { self.solid_depth + self.hollow_depth } + + pub fn nearest_wall(&self, rpos: Vec2) -> Option> { + let tile_pos = rpos.map(|e| e.div_euclid(TILE_SIZE)); + + DIRS.iter() + .map(|dir| tile_pos + *dir) + .filter(|other_tile_pos| { + self.tiles + .get(*other_tile_pos) + .filter(|tile| tile.is_passable()) + .is_none() + }) + .map(|other_tile_pos| { + rpos.clamped( + other_tile_pos * TILE_SIZE, + (other_tile_pos + 1) * TILE_SIZE - 1, + ) + }) + .min_by_key(|nearest| rpos.distance_squared(*nearest)) + } + + pub fn col_sampler(&self, pos: Vec2, floor_z: i32) -> impl FnMut(i32) -> BlockMask + '_ { + let rpos = pos - self.tile_offset * TILE_SIZE; + let tile_pos = rpos.map(|e| e.div_euclid(TILE_SIZE)); + let tile_center = tile_pos * TILE_SIZE + TILE_SIZE / 2; + let rtile_pos = rpos - tile_center; + + let empty = BlockMask::new(Block::empty(), 1); + + let make_staircase = move |pos: Vec3, radius: f32, inner_radius: f32, stretch: f32| { + let stone = BlockMask::new(Block::new(BlockKind::Normal, Rgb::new(150, 150, 175)), 5); + + if (pos.xy().magnitude_squared() as f32) < inner_radius.powf(2.0) { + stone + } else if (pos.xy().magnitude_squared() as f32) < radius.powf(2.0) { + if ((pos.x as f32).atan2(pos.y as f32) / (f32::consts::PI * 2.0) * stretch + + (floor_z + pos.z) as f32) + .rem_euclid(stretch) + < 1.5 + { + stone + } else { + empty + } + } else { + BlockMask::nothing() + } + }; + + let wall_thickness = 3.0; + let dist_to_wall = self + .nearest_wall(rpos) + .map(|nearest| (nearest.distance_squared(rpos) as f32).sqrt()) + .unwrap_or(TILE_SIZE as f32); + let tunnel_dist = + 1.0 - (dist_to_wall - wall_thickness).max(0.0) / (TILE_SIZE as f32 - wall_thickness); + + move |z| match self.tiles.get(tile_pos) { + Some(Tile::Solid) => BlockMask::nothing(), + Some(Tile::Tunnel) => { + if dist_to_wall >= wall_thickness && (z as f32) < 8.0 - 8.0 * tunnel_dist.powf(4.0) + { + empty + } else { + BlockMask::nothing() + } + }, + Some(Tile::Room(_)) | Some(Tile::DownStair) + if dist_to_wall < wall_thickness + || z as f32 >= self.hollow_depth as f32 - 13.0 * tunnel_dist.powf(4.0) => + { + BlockMask::nothing() + }, + Some(Tile::Room(room)) => { + let room = &self.rooms[*room]; + if z == 0 && RandomField::new(room.seed).chance(Vec3::from(pos), room.loot_density) + { + BlockMask::new(Block::new(BlockKind::Chest, Rgb::white()), 1) + } else { + empty + } + }, + Some(Tile::DownStair) => { + make_staircase(Vec3::new(rtile_pos.x, rtile_pos.y, z), 0.0, 0.5, 9.0) + .resolve_with(empty) + }, + Some(Tile::UpStair) => { + let mut block = make_staircase( + Vec3::new(rtile_pos.x, rtile_pos.y, z), + TILE_SIZE as f32 / 2.0, + 0.5, + 9.0, + ); + if z < self.hollow_depth { + block = block.resolve_with(empty); + } + block + }, + None => BlockMask::nothing(), + } + } +} diff --git a/world/src/site/mod.rs b/world/src/site/mod.rs new file mode 100644 index 0000000000..12cd3d996b --- /dev/null +++ b/world/src/site/mod.rs @@ -0,0 +1,127 @@ +mod dungeon; +mod settlement; + +// Reexports +pub use self::{dungeon::Dungeon, settlement::Settlement}; + +use crate::column::ColumnSample; +use common::{ + generation::ChunkSupplement, + terrain::Block, + vol::{BaseVol, ReadVol, RectSizedVol, Vox, WriteVol}, +}; +use rand::Rng; +use std::{fmt, sync::Arc}; +use vek::*; + +#[derive(Copy, Clone)] +pub struct BlockMask { + block: Block, + priority: i32, +} + +impl BlockMask { + pub fn new(block: Block, priority: i32) -> Self { Self { block, priority } } + + pub fn nothing() -> Self { + Self { + block: Block::empty(), + priority: 0, + } + } + + pub fn with_priority(mut self, priority: i32) -> Self { + self.priority = priority; + self + } + + pub fn resolve_with(self, other: Self) -> Self { + if self.priority >= other.priority { + self + } else { + other + } + } + + pub fn finish(self) -> Option { + if self.priority > 0 { + Some(self.block) + } else { + None + } + } +} + +pub struct SpawnRules { + pub trees: bool, +} + +impl Default for SpawnRules { + fn default() -> Self { Self { trees: true } } +} + +#[derive(Clone)] +pub enum Site { + Settlement(Arc), + Dungeon(Arc), +} + +impl Site { + pub fn radius(&self) -> f32 { + match self { + Site::Settlement(settlement) => settlement.radius(), + Site::Dungeon(dungeon) => dungeon.radius(), + } + } + + pub fn spawn_rules(&self, wpos: Vec2) -> SpawnRules { + match self { + Site::Settlement(s) => s.spawn_rules(wpos), + Site::Dungeon(d) => d.spawn_rules(wpos), + } + } + + pub fn apply_to<'a>( + &'a self, + wpos2d: Vec2, + get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, + vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), + ) { + match self { + Site::Settlement(settlement) => settlement.apply_to(wpos2d, get_column, vol), + Site::Dungeon(dungeon) => dungeon.apply_to(wpos2d, get_column, vol), + } + } + + pub fn apply_supplement<'a>( + &'a self, + rng: &mut impl Rng, + wpos2d: Vec2, + get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, + supplement: &mut ChunkSupplement, + ) { + match self { + Site::Settlement(settlement) => { + settlement.apply_supplement(rng, wpos2d, get_column, supplement) + }, + Site::Dungeon(dungeon) => dungeon.apply_supplement(rng, wpos2d, get_column, supplement), + } + } +} + +impl From for Site { + fn from(settlement: Settlement) -> Self { Site::Settlement(Arc::new(settlement)) } +} + +impl From for Site { + fn from(dungeon: Dungeon) -> Self { Site::Dungeon(Arc::new(dungeon)) } +} + +impl fmt::Debug for Site { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Site::Settlement(_) => write!(f, "Settlement"), + Site::Dungeon(_) => write!(f, "Dungeon"), + } + } +} diff --git a/world/src/site/settlement/building/archetype/house.rs b/world/src/site/settlement/building/archetype/house.rs new file mode 100644 index 0000000000..cc499d61e2 --- /dev/null +++ b/world/src/site/settlement/building/archetype/house.rs @@ -0,0 +1,454 @@ +#![allow(dead_code)] + +use super::{super::skeleton::*, Archetype}; +use crate::{ + site::BlockMask, + util::{RandomField, Sampler}, +}; +use common::{ + terrain::{Block, BlockKind}, + vol::Vox, +}; +use rand::prelude::*; +use vek::*; + +const COLOR_THEMES: [Rgb; 11] = [ + Rgb::new(0x1D, 0x4D, 0x45), + Rgb::new(0xB3, 0x7D, 0x60), + Rgb::new(0xAC, 0x5D, 0x26), + Rgb::new(0x32, 0x46, 0x6B), + Rgb::new(0x2B, 0x19, 0x0F), + Rgb::new(0x93, 0x78, 0x51), + Rgb::new(0x92, 0x57, 0x24), + Rgb::new(0x4A, 0x4E, 0x4E), + Rgb::new(0x2F, 0x32, 0x47), + Rgb::new(0x8F, 0x35, 0x43), + Rgb::new(0x6D, 0x1E, 0x3A), +]; + +pub struct House { + roof_color: Rgb, + noise: RandomField, + roof_ribbing: bool, + roof_ribbing_diagonal: bool, +} + +enum Pillar { + None, + Chimney(i32), + Tower(i32), +} + +enum RoofStyle { + Hip, + Gable, + Rounded, +} + +enum StoreyFill { + None, + Upper, + All, +} + +impl StoreyFill { + fn has_lower(&self) -> bool { + if let StoreyFill::All = self { + true + } else { + false + } + } + + fn has_upper(&self) -> bool { + if let StoreyFill::None = self { + false + } else { + true + } + } +} + +pub struct Attr { + central_supports: bool, + storey_fill: StoreyFill, + roof_style: RoofStyle, + mansard: i32, + pillar: Pillar, +} + +impl Attr { + fn generate(rng: &mut R, locus: i32) -> Self { + Self { + central_supports: rng.gen(), + storey_fill: match rng.gen_range(0, 2) { + //0 => StoreyFill::None, + 0 => StoreyFill::Upper, + _ => StoreyFill::All, + }, + roof_style: match rng.gen_range(0, 3) { + 0 => RoofStyle::Hip, + 1 => RoofStyle::Gable, + _ => RoofStyle::Rounded, + }, + mansard: rng.gen_range(-7, 4).max(0), + pillar: match rng.gen_range(0, 4) { + 0 => Pillar::Chimney(9 + locus + rng.gen_range(0, 4)), + _ => Pillar::None, + }, + } + } +} + +impl Archetype for House { + type Attr = Attr; + + fn generate(rng: &mut R) -> (Self, Skeleton) { + let len = rng.gen_range(-8, 24).clamped(0, 20); + let locus = 6 + rng.gen_range(0, 5); + let branches_per_side = 1 + len as usize / 20; + let skel = Skeleton { + offset: -rng.gen_range(0, len + 7).clamped(0, len), + ori: if rng.gen() { Ori::East } else { Ori::North }, + root: Branch { + len, + attr: Attr { + storey_fill: StoreyFill::All, + mansard: 0, + pillar: match rng.gen_range(0, 3) { + 0 => Pillar::Chimney(9 + locus + rng.gen_range(0, 4)), + 1 => Pillar::Tower(15 + locus + rng.gen_range(0, 4)), + _ => Pillar::None, + }, + ..Attr::generate(rng, locus) + }, + locus, + border: 4, + children: [1, -1] + .iter() + .map(|flip| (0..branches_per_side).map(move |i| (i, *flip))) + .flatten() + .filter_map(|(i, flip)| { + if rng.gen() { + Some(( + i as i32 * len / (branches_per_side - 1).max(1) as i32, + Branch { + len: rng.gen_range(8, 16) * flip, + attr: Attr::generate(rng, locus), + locus: (6 + rng.gen_range(0, 3)).min(locus), + border: 4, + children: Vec::new(), + }, + )) + } else { + None + } + }) + .collect(), + }, + }; + + let this = Self { + roof_color: COLOR_THEMES + .choose(rng) + .unwrap() + .map(|e| e.saturating_add(rng.gen_range(0, 20)) - 10), + noise: RandomField::new(rng.gen()), + roof_ribbing: rng.gen(), + roof_ribbing_diagonal: rng.gen(), + }; + + (this, skel) + } + + fn draw( + &self, + dist: i32, + bound_offset: Vec2, + center_offset: Vec2, + z: i32, + ori: Ori, + branch: &Branch, + ) -> BlockMask { + let profile = Vec2::new(bound_offset.x, z); + + let make_meta = |ori| { + Rgb::new( + match ori { + Ori::East => 0, + Ori::North => 2, + }, + 0, + 0, + ) + }; + + let make_block = |r, g, b| { + let nz = self + .noise + .get(Vec3::new(center_offset.x, center_offset.y, z * 8)); + BlockMask::new( + Block::new( + BlockKind::Normal, + Rgb::new(r, g, b) + .map(|e: u8| e.saturating_add((nz & 0x0F) as u8).saturating_sub(8)), + ), + 2, + ) + }; + + let facade_layer = 3; + let structural_layer = facade_layer + 1; + let foundation_layer = structural_layer + 1; + let floor_layer = foundation_layer + 1; + + let foundation = make_block(100, 100, 100).with_priority(foundation_layer); + let log = make_block(60, 45, 30); + let floor = make_block(100, 75, 50); + let wall = make_block(200, 180, 150).with_priority(facade_layer); + let roof = make_block(self.roof_color.r, self.roof_color.g, self.roof_color.b) + .with_priority(facade_layer); + let empty = BlockMask::nothing(); + let internal = BlockMask::new(Block::empty(), structural_layer); + let end_window = BlockMask::new( + Block::new(BlockKind::Window1, make_meta(ori.flip())), + structural_layer, + ); + let fire = BlockMask::new(Block::new(BlockKind::Ember, Rgb::white()), foundation_layer); + + let ceil_height = 6; + let lower_width = branch.locus - 1; + let upper_width = branch.locus; + let width = if profile.y >= ceil_height { + upper_width + } else { + lower_width + }; + let foundation_height = 0 - (dist - width - 1).max(0); + let roof_top = 8 + width; + + if let Pillar::Chimney(chimney_top) = branch.attr.pillar { + // Chimney shaft + if center_offset.map(|e| e.abs()).reduce_max() == 0 + && profile.y >= foundation_height + 1 + { + return if profile.y == foundation_height + 1 { + fire + } else { + internal.with_priority(foundation_layer) + }; + } + + // Chimney + if center_offset.map(|e| e.abs()).reduce_max() <= 1 && profile.y < chimney_top { + // Fireplace + if center_offset.product() == 0 + && profile.y > foundation_height + 1 + && profile.y <= foundation_height + 3 + { + return internal; + } else { + return foundation; + } + } + } + + if profile.y <= foundation_height && dist < width + 3 { + // Foundations + if branch.attr.storey_fill.has_lower() { + if dist == width - 1 { + // Floor lining + return log.with_priority(floor_layer); + } else if dist < width - 1 && profile.y == foundation_height { + // Floor + return floor.with_priority(floor_layer); + } + } + + if dist < width && profile.y < foundation_height && profile.y >= foundation_height - 3 { + // Basement + return internal; + } else { + return foundation.with_priority(1); + } + } + + // Roofs and walls + let do_roof_wall = + |profile: Vec2, width, dist, bound_offset: Vec2, roof_top, mansard| { + // Roof + + let (roof_profile, roof_dist) = match &branch.attr.roof_style { + RoofStyle::Hip => (Vec2::new(dist, profile.y), dist), + RoofStyle::Gable => (profile, dist), + RoofStyle::Rounded => { + let circular_dist = (bound_offset.map(|e| e.pow(4) as f32).sum().powf(0.25) + - 0.5) + .ceil() as i32; + (Vec2::new(circular_dist, profile.y), circular_dist) + }, + }; + + let roof_level = roof_top - roof_profile.x.max(mansard); + + if profile.y > roof_level { + return None; + } + + // Roof + if profile.y == roof_level && roof_dist <= width + 2 { + let is_ribbing = ((profile.y - ceil_height) % 3 == 0 && self.roof_ribbing) + || (bound_offset.x == bound_offset.y && self.roof_ribbing_diagonal); + if (roof_profile.x == 0 && mansard == 0) || roof_dist == width + 2 || is_ribbing + { + // Eaves + return Some(log); + } else { + return Some(roof); + } + } + + // Wall + + if dist == width && profile.y < roof_level { + // Doors + if center_offset.x > 0 + && center_offset.y > 0 + && bound_offset.x > 0 + && bound_offset.x < width + && profile.y < ceil_height + && branch.attr.storey_fill.has_lower() + { + return Some( + if (bound_offset.x == (width - 1) / 2 + || bound_offset.x == (width - 1) / 2 + 1) + && profile.y <= foundation_height + 3 + { + if profile.y == foundation_height + 1 { + BlockMask::new( + Block::new( + BlockKind::Door, + if bound_offset.x == (width - 1) / 2 { + make_meta(ori.flip()) + } else { + make_meta(ori.flip()) + Rgb::new(4, 0, 0) + }, + ), + structural_layer, + ) + } else { + empty.with_priority(structural_layer) + } + } else { + wall + }, + ); + } + + if bound_offset.x == bound_offset.y || profile.y == ceil_height { + // Support beams + return Some(log); + } else if !branch.attr.storey_fill.has_lower() && profile.y < ceil_height { + return Some(empty); + } else if !branch.attr.storey_fill.has_upper() { + return Some(empty); + } else { + let (frame_bounds, frame_borders) = if profile.y >= ceil_height { + ( + Aabr { + min: Vec2::new(-1, ceil_height + 2), + max: Vec2::new(1, ceil_height + 5), + }, + Vec2::new(1, 1), + ) + } else { + ( + Aabr { + min: Vec2::new(2, foundation_height + 2), + max: Vec2::new(width - 2, ceil_height - 2), + }, + Vec2::new(1, 0), + ) + }; + let window_bounds = Aabr { + min: (frame_bounds.min + frame_borders) + .map2(frame_bounds.center(), |a, b| a.min(b)), + max: (frame_bounds.max - frame_borders) + .map2(frame_bounds.center(), |a, b| a.max(b)), + }; + + // Window + if (frame_bounds.size() + 1).reduce_min() > 2 { + // Window frame is large enough for a window + let surface_pos = Vec2::new(bound_offset.x, profile.y); + if window_bounds.contains_point(surface_pos) { + return Some(end_window); + } else if frame_bounds.contains_point(surface_pos) { + return Some(log.with_priority(structural_layer)); + }; + } + + // Wall + return Some(if branch.attr.central_supports && profile.x == 0 { + // Support beams + log.with_priority(structural_layer) + } else { + wall + }); + } + } + + if dist < width { + // Internals + if profile.y == ceil_height { + if profile.x == 0 { + // Rafters + return Some(log); + } else if branch.attr.storey_fill.has_upper() { + // Ceiling + return Some(floor); + } + } else if (!branch.attr.storey_fill.has_lower() && profile.y < ceil_height) + || (!branch.attr.storey_fill.has_upper() && profile.y >= ceil_height) + { + return Some(empty); + } else { + return Some(internal); + } + } + + None + }; + + let mut cblock = empty; + + if let Some(block) = do_roof_wall( + profile, + width, + dist, + bound_offset, + roof_top, + branch.attr.mansard, + ) { + cblock = cblock.resolve_with(block); + } + + if let Pillar::Tower(tower_top) = branch.attr.pillar { + let profile = Vec2::new(center_offset.x.abs(), profile.y); + let dist = center_offset.map(|e| e.abs()).reduce_max(); + + if let Some(block) = do_roof_wall( + profile, + 4, + dist, + center_offset.map(|e| e.abs()), + tower_top, + branch.attr.mansard, + ) { + cblock = cblock.resolve_with(block); + } + } + + cblock + } +} diff --git a/world/src/site/settlement/building/archetype/keep.rs b/world/src/site/settlement/building/archetype/keep.rs new file mode 100644 index 0000000000..6354413547 --- /dev/null +++ b/world/src/site/settlement/building/archetype/keep.rs @@ -0,0 +1,81 @@ +use super::{super::skeleton::*, Archetype}; +use crate::site::BlockMask; +use common::{ + terrain::{Block, BlockKind}, + vol::Vox, +}; +use rand::prelude::*; +use vek::*; + +pub struct Keep; + +impl Archetype for Keep { + type Attr = (); + + fn generate(rng: &mut R) -> (Self, Skeleton) { + let len = rng.gen_range(-8, 12).max(0); + let skel = Skeleton { + offset: -rng.gen_range(0, len + 7).clamped(0, len), + ori: if rng.gen() { Ori::East } else { Ori::North }, + root: Branch { + len, + attr: Self::Attr::default(), + locus: 5 + rng.gen_range(0, 5), + border: 3, + children: (0..rng.gen_range(0, 4)) + .map(|_| { + ( + rng.gen_range(-5, len + 5).clamped(0, len.max(1) - 1), + Branch { + len: rng.gen_range(5, 12) * if rng.gen() { 1 } else { -1 }, + attr: Self::Attr::default(), + locus: 5 + rng.gen_range(0, 3), + border: 3, + children: Vec::new(), + }, + ) + }) + .collect(), + }, + }; + + (Self, skel) + } + + fn draw( + &self, + dist: i32, + bound_offset: Vec2, + _center_offset: Vec2, + z: i32, + _ori: Ori, + branch: &Branch, + ) -> BlockMask { + let profile = Vec2::new(bound_offset.x, z); + + let make_block = + |r, g, b| BlockMask::new(Block::new(BlockKind::Normal, Rgb::new(r, g, b)), 2); + + let foundation = make_block(100, 100, 100); + let wall = make_block(75, 100, 125); + let roof = make_block(150, 120, 50); + let empty = BlockMask::new(Block::empty(), 2); + + let width = branch.locus; + let rampart_width = 5 + branch.locus; + let ceil_height = 16; + + if profile.y <= 1 - (dist - width - 1).max(0) && dist < width + 3 { + // Foundations + foundation + } else if profile.y == ceil_height && dist < rampart_width { + roof + } else if dist == rampart_width && profile.y >= ceil_height && profile.y < ceil_height + 4 { + wall + } else if dist == width && profile.y <= ceil_height { + wall + } else { + empty + } + } +} diff --git a/world/src/site/settlement/building/archetype/mod.rs b/world/src/site/settlement/building/archetype/mod.rs new file mode 100644 index 0000000000..89ca5dca24 --- /dev/null +++ b/world/src/site/settlement/building/archetype/mod.rs @@ -0,0 +1,24 @@ +pub mod house; +pub mod keep; + +use super::skeleton::*; +use crate::site::BlockMask; +use rand::prelude::*; +use vek::*; + +pub trait Archetype { + type Attr; + + fn generate(rng: &mut R) -> (Self, Skeleton) + where + Self: Sized; + fn draw( + &self, + dist: i32, + bound_offset: Vec2, + center_offset: Vec2, + z: i32, + ori: Ori, + branch: &Branch, + ) -> BlockMask; +} diff --git a/world/src/site/settlement/building/mod.rs b/world/src/site/settlement/building/mod.rs new file mode 100644 index 0000000000..7687bd4bf9 --- /dev/null +++ b/world/src/site/settlement/building/mod.rs @@ -0,0 +1,61 @@ +mod archetype; +mod skeleton; + +// Reexports +pub use self::archetype::Archetype; + +use self::skeleton::*; +use common::terrain::Block; +use rand::prelude::*; +use vek::*; + +pub type HouseBuilding = Building; + +pub struct Building { + skel: Skeleton, + archetype: A, + origin: Vec3, +} + +impl Building { + pub fn generate(rng: &mut impl Rng, origin: Vec3) -> Self + where + A: Sized, + { + let (archetype, skel) = A::generate(rng); + Self { + skel, + archetype, + origin, + } + } + + pub fn bounds_2d(&self) -> Aabr { + let b = self.skel.bounds(); + Aabr { + min: Vec2::from(self.origin) + b.min, + max: Vec2::from(self.origin) + b.max, + } + } + + pub fn bounds(&self) -> Aabb { + let aabr = self.bounds_2d(); + Aabb { + min: Vec3::from(aabr.min) + Vec3::unit_z() * (self.origin.z - 8), + max: Vec3::from(aabr.max) + Vec3::unit_z() * (self.origin.z + 32), + } + } + + pub fn sample(&self, pos: Vec3) -> Option { + let rpos = pos - self.origin; + self.skel + .sample_closest( + rpos.into(), + |dist, bound_offset, center_offset, ori, branch| { + self.archetype + .draw(dist, bound_offset, center_offset, rpos.z, ori, branch) + }, + ) + .finish() + } +} diff --git a/world/src/site/settlement/building/skeleton.rs b/world/src/site/settlement/building/skeleton.rs new file mode 100644 index 0000000000..4387a9e590 --- /dev/null +++ b/world/src/site/settlement/building/skeleton.rs @@ -0,0 +1,126 @@ +use crate::site::BlockMask; +use vek::*; + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Ori { + East, + North, +} + +impl Ori { + pub fn flip(self) -> Self { + match self { + Ori::East => Ori::North, + Ori::North => Ori::East, + } + } + + pub fn dir(self) -> Vec2 { + match self { + Ori::East => Vec2::unit_x(), + Ori::North => Vec2::unit_y(), + } + } +} + +pub struct Branch { + pub len: i32, + pub attr: T, + pub locus: i32, + pub border: i32, + pub children: Vec<(i32, Branch)>, +} + +impl Branch { + fn for_each<'a>( + &'a self, + node: Vec2, + ori: Ori, + is_child: bool, + parent_locus: i32, + f: &mut impl FnMut(Vec2, Ori, &'a Branch, bool, i32), + ) { + f(node, ori, self, is_child, parent_locus); + for (offset, child) in &self.children { + child.for_each(node + ori.dir() * *offset, ori.flip(), true, self.locus, f); + } + } +} + +pub struct Skeleton { + pub offset: i32, + pub ori: Ori, + pub root: Branch, +} + +impl Skeleton { + pub fn for_each<'a>(&'a self, mut f: impl FnMut(Vec2, Ori, &'a Branch, bool, i32)) { + self.root + .for_each(self.ori.dir() * self.offset, self.ori, false, 0, &mut f); + } + + pub fn bounds(&self) -> Aabr { + let mut bounds = Aabr::new_empty(self.ori.dir() * self.offset); + self.for_each(|node, ori, branch, _, _| { + let node2 = node + ori.dir() * branch.len; + + let a = node.map2(node2, |a, b| a.min(b)) - (branch.locus + branch.border); + let b = node.map2(node2, |a, b| a.max(b)) + (branch.locus + branch.border); + bounds.expand_to_contain_point(a); + bounds.expand_to_contain_point(b); + }); + bounds + } + + pub fn sample_closest( + &self, + pos: Vec2, + mut f: impl FnMut(i32, Vec2, Vec2, Ori, &Branch) -> BlockMask, + ) -> BlockMask { + let mut min = None::<(_, BlockMask)>; + self.for_each(|node, ori, branch, is_child, parent_locus| { + let node2 = node + ori.dir() * branch.len; + let node = node + + if is_child { + ori.dir() + * branch.len.signum() + * (branch.locus - parent_locus).clamped(0, branch.len.abs()) + } else { + Vec2::zero() + }; + let bounds = Aabr::new_empty(node).expanded_to_contain_point(node2); + let bound_offset = if ori == Ori::East { + Vec2::new( + node.y - pos.y, + pos.x - pos.x.clamped(bounds.min.x, bounds.max.x), + ) + } else { + Vec2::new( + node.x - pos.x, + pos.y - pos.y.clamped(bounds.min.y, bounds.max.y), + ) + } + .map(|e| e.abs()); + let center_offset = if ori == Ori::East { + Vec2::new(pos.y - bounds.center().y, pos.x - bounds.center().x) + } else { + Vec2::new(pos.x - bounds.center().x, pos.y - bounds.center().y) + }; + let dist = bound_offset.reduce_max(); + let dist_locus = dist - branch.locus; + if !is_child + || match ori { + Ori::East => (pos.x - node.x) * branch.len.signum() >= 0, + Ori::North => (pos.y - node.y) * branch.len.signum() >= 0, + } + || true + { + let new_bm = f(dist, bound_offset, center_offset, ori, branch); + min = min + .map(|(_, bm)| (dist_locus, bm.resolve_with(new_bm))) + .or(Some((dist_locus, new_bm))); + } + }); + min.map(|(_, bm)| bm).unwrap_or(BlockMask::nothing()) + } +} diff --git a/world/src/site/settlement/mod.rs b/world/src/site/settlement/mod.rs new file mode 100644 index 0000000000..89484dadbb --- /dev/null +++ b/world/src/site/settlement/mod.rs @@ -0,0 +1,1175 @@ +mod building; + +use self::building::HouseBuilding; +use super::SpawnRules; +use crate::{ + column::ColumnSample, + sim::WorldSim, + util::{RandomField, Sampler, StructureGen2d}, +}; +use common::{ + assets, + astar::Astar, + comp::{self, bird_medium, humanoid, quadruped_small}, + generation::{ChunkSupplement, EntityInfo}, + path::Path, + spiral::Spiral2d, + store::{Id, Store}, + terrain::{Block, BlockKind, TerrainChunkSize}, + vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol}, +}; +use hashbrown::{HashMap, HashSet}; +use rand::prelude::*; +use std::{collections::VecDeque, f32}; +use vek::*; + +#[allow(dead_code)] +pub fn gradient(line: [Vec2; 2]) -> f32 { + let r = (line[0].y - line[1].y) / (line[0].x - line[1].x); + if r.is_nan() { 100000.0 } else { r } +} + +#[allow(dead_code)] +pub fn intersect(a: [Vec2; 2], b: [Vec2; 2]) -> Option> { + let ma = gradient(a); + let mb = gradient(b); + + let ca = a[0].y - ma * a[0].x; + let cb = b[0].y - mb * b[0].x; + + if (ma - mb).abs() < 0.0001 || (ca - cb).abs() < 0.0001 { + None + } else { + let x = (cb - ca) / (ma - mb); + let y = ma * x + ca; + + Some(Vec2::new(x, y)) + } +} + +#[allow(dead_code)] +pub fn center_of(p: [Vec2; 3]) -> Vec2 { + let ma = -1.0 / gradient([p[0], p[1]]); + let mb = -1.0 / gradient([p[1], p[2]]); + + let pa = (p[0] + p[1]) * 0.5; + let pb = (p[1] + p[2]) * 0.5; + + let ca = pa.y - ma * pa.x; + let cb = pb.y - mb * pb.x; + + let x = (cb - ca) / (ma - mb); + let y = ma * x + ca; + + Vec2::new(x, y) +} + +impl WorldSim { + fn can_host_settlement(&self, pos: Vec2) -> bool { + self.get(pos) + .map(|chunk| !chunk.near_cliffs && !chunk.river.is_river() && !chunk.river.is_lake()) + .unwrap_or(false) + && self + .get_gradient_approx(pos) + .map(|grad| grad < 0.75) + .unwrap_or(false) + } +} + +const AREA_SIZE: u32 = 32; + +fn to_tile(e: i32) -> i32 { ((e as f32).div_euclid(AREA_SIZE as f32)).floor() as i32 } + +pub enum StructureKind { + House(HouseBuilding), +} + +pub struct Structure { + kind: StructureKind, +} + +impl Structure { + pub fn bounds_2d(&self) -> Aabr { + match &self.kind { + StructureKind::House(house) => house.bounds_2d(), + } + } +} + +pub struct Settlement { + seed: u32, + origin: Vec2, + land: Land, + farms: Store, + structures: Vec, + town: Option, + noise: RandomField, +} + +pub struct Town { + base_tile: Vec2, +} + +pub struct Farm { + #[allow(dead_code)] + base_tile: Vec2, +} + +pub struct GenCtx<'a, R: Rng> { + sim: Option<&'a WorldSim>, + rng: &'a mut R, +} + +impl Settlement { + pub fn generate(wpos: Vec2, sim: Option<&WorldSim>, rng: &mut impl Rng) -> Self { + let mut ctx = GenCtx { sim, rng }; + let mut this = Self { + seed: ctx.rng.gen(), + origin: wpos, + land: Land::new(ctx.rng), + farms: Store::default(), + structures: Vec::new(), + town: None, + noise: RandomField::new(ctx.rng.gen()), + }; + + if let Some(sim) = ctx.sim { + this.designate_from_world(sim, ctx.rng); + } + + //this.place_river(rng); + + this.place_farms(&mut ctx); + this.place_town(&mut ctx); + //this.place_paths(ctx.rng); + this.place_buildings(&mut ctx); + + this + } + + /// Designate hazardous terrain based on world data + pub fn designate_from_world(&mut self, sim: &WorldSim, rng: &mut impl Rng) { + let tile_radius = self.radius() as i32 / AREA_SIZE as i32; + let hazard = self.land.hazard; + Spiral2d::new() + .take_while(|tile| tile.map(|e| e.abs()).reduce_max() < tile_radius) + .for_each(|tile| { + let wpos = self.origin + tile * AREA_SIZE as i32; + + if (0..4) + .map(|x| (0..4).map(move |y| Vec2::new(x, y))) + .flatten() + .any(|offs| { + let wpos = wpos + offs * AREA_SIZE as i32 / 2; + let cpos = wpos.map(|e| e.div_euclid(TerrainChunkSize::RECT_SIZE.x as i32)); + !sim.can_host_settlement(cpos) + }) + || rng.gen_range(0, 16) == 0 + // Randomly consider some tiles inaccessible + { + self.land.set(tile, hazard); + } + }) + } + + /// Testing only + pub fn place_river(&mut self, rng: &mut impl Rng) { + let river_dir = Vec2::new(rng.gen::() - 0.5, rng.gen::() - 0.5).normalized(); + let radius = 500.0 + rng.gen::().powf(2.0) * 1000.0; + let river = self.land.new_plot(Plot::Water); + let river_offs = Vec2::new(rng.gen_range(-3, 4), rng.gen_range(-3, 4)); + + for x in (0..100).map(|e| e as f32 / 100.0) { + let theta0 = x as f32 * f32::consts::PI * 2.0; + let theta1 = (x + 0.01) as f32 * f32::consts::PI * 2.0; + + let pos0 = (river_dir * radius + Vec2::new(theta0.sin(), theta0.cos()) * radius) + .map(|e| e.floor() as i32) + .map(to_tile) + + river_offs; + let pos1 = (river_dir * radius + Vec2::new(theta1.sin(), theta1.cos()) * radius) + .map(|e| e.floor() as i32) + .map(to_tile) + + river_offs; + + if pos0.magnitude_squared() > 15i32.pow(2) { + continue; + } + + if let Some(path) = self.land.find_path(pos0, pos1, |_, _| 1.0) { + for pos in path.iter().copied() { + self.land.set(pos, river); + } + } + } + } + + pub fn place_paths(&mut self, rng: &mut impl Rng) { + const PATH_COUNT: usize = 6; + + let mut dir = Vec2::zero(); + for _ in 0..PATH_COUNT { + dir = (Vec2::new(rng.gen::() - 0.5, rng.gen::() - 0.5) * 2.0 - dir) + .try_normalized() + .unwrap_or(Vec2::zero()); + let origin = dir.map(|e| (e * 100.0) as i32); + let origin = self + .land + .find_tile_near(origin, |plot| match plot { + Some(&Plot::Field { .. }) => true, + _ => false, + }) + .unwrap(); + + if let Some(path) = self.town.as_ref().and_then(|town| { + self.land + .find_path(origin, town.base_tile, |from, to| match (from, to) { + (_, Some(b)) if self.land.plot(b.plot) == &Plot::Dirt => 0.0, + (_, Some(b)) if self.land.plot(b.plot) == &Plot::Water => 20.0, + (_, Some(b)) if self.land.plot(b.plot) == &Plot::Hazard => 50.0, + (Some(a), Some(b)) if a.contains(WayKind::Wall) => { + if b.contains(WayKind::Wall) { + 1000.0 + } else { + 10.0 + } + }, + (Some(_), Some(_)) => 1.0, + _ => 1000.0, + }) + }) { + let path = path.iter().copied().collect::>(); + self.land.write_path(&path, WayKind::Path, |_| true, false); + } + } + } + + pub fn place_town(&mut self, ctx: &mut GenCtx) { + const PLOT_COUNT: usize = 3; + + let mut origin = Vec2::new(ctx.rng.gen_range(-2, 3), ctx.rng.gen_range(-2, 3)); + + for i in 0..PLOT_COUNT { + if let Some(base_tile) = self.land.find_tile_near(origin, |plot| match plot { + Some(Plot::Field { .. }) => true, + Some(Plot::Dirt) => true, + _ => false, + }) { + self.land + .plot_at_mut(base_tile) + .map(|plot| *plot = Plot::Town); + + if i == 0 { + self.town = Some(Town { base_tile }); + origin = base_tile; + } + } + } + + // Boundary wall + /* + let spokes = CARDINALS + .iter() + .filter_map(|dir| { + self.land.find_tile_dir(origin, *dir, |plot| match plot { + Some(Plot::Water) => false, + Some(Plot::Town) => false, + _ => true, + }) + }) + .collect::>(); + let mut wall_path = Vec::new(); + for i in 0..spokes.len() { + self.land + .find_path(spokes[i], spokes[(i + 1) % spokes.len()], |_, to| match to + .map(|to| self.land.plot(to.plot)) + { + Some(Plot::Hazard) => 200.0, + Some(Plot::Water) => 40.0, + Some(Plot::Town) => 10000.0, + _ => 10.0, + }) + .map(|path| wall_path.extend(path.iter().copied())); + } + let grass = self.land.new_plot(Plot::Grass); + let buildable = |plot: &Plot| match plot { + Plot::Water => false, + _ => true, + }; + for pos in wall_path.iter() { + if self.land.tile_at(*pos).is_none() { + self.land.set(*pos, grass); + } + if self.land.plot_at(*pos).copied().filter(buildable).is_some() { + self.land + .tile_at_mut(*pos) + .map(|tile| tile.tower = Some(Tower::Wall)); + } + } + if wall_path.len() > 0 { + wall_path.push(wall_path[0]); + } + self.land + .write_path(&wall_path, WayKind::Wall, buildable, true); + */ + } + + pub fn place_buildings(&mut self, ctx: &mut GenCtx) { + let town_center = if let Some(town) = self.town.as_ref() { + town.base_tile + } else { + return; + }; + + for tile in Spiral2d::new() + .map(|offs| town_center + offs) + .take(16usize.pow(2)) + { + // This is a stupid way to decide how to place buildings + for _ in 0..ctx.rng.gen_range(2, 5) { + for _ in 0..25 { + let house_pos = tile.map(|e| e * AREA_SIZE as i32 + AREA_SIZE as i32 / 2) + + Vec2::::zero().map(|_| { + ctx.rng + .gen_range(-(AREA_SIZE as i32) / 2, AREA_SIZE as i32 / 2) + }); + + let tile_pos = house_pos.map(|e| e.div_euclid(AREA_SIZE as i32)); + if !matches!(self.land.plot_at(tile_pos), Some(Plot::Town)) + || self + .land + .tile_at(tile_pos) + .map(|t| t.contains(WayKind::Path)) + .unwrap_or(true) + || ctx + .sim + .and_then(|sim| sim.get_nearest_path(self.origin + house_pos)) + .map(|(dist, _)| dist < 28.0) + .unwrap_or(false) + { + continue; + } + + let structure = Structure { + kind: StructureKind::House(HouseBuilding::generate( + ctx.rng, + Vec3::new( + house_pos.x, + house_pos.y, + ctx.sim + .and_then(|sim| sim.get_alt_approx(self.origin + house_pos)) + .unwrap_or(0.0) + .ceil() as i32, + ), + )), + }; + + let bounds = structure.bounds_2d(); + + // Check for collision with other structures + if self + .structures + .iter() + .any(|s| s.bounds_2d().collides_with_aabr(bounds)) + { + continue; + } + + self.structures.push(structure); + break; + } + } + } + } + + pub fn place_farms(&mut self, ctx: &mut GenCtx) { + const FARM_COUNT: usize = 6; + const FIELDS_PER_FARM: usize = 5; + + for _ in 0..FARM_COUNT { + if let Some(base_tile) = self + .land + .find_tile_near(Vec2::zero(), |plot| plot.is_none()) + { + // Farm + //let farmhouse = self.land.new_plot(Plot::Dirt); + //self.land.set(base_tile, farmhouse); + + // Farmhouses + // for _ in 0..ctx.rng.gen_range(1, 3) { + // let house_pos = base_tile.map(|e| e * AREA_SIZE as i32 + AREA_SIZE as i32 + // / 2) + Vec2::new(ctx.rng.gen_range(-16, 16), + // ctx.rng.gen_range(-16, 16)); + + // self.structures.push(Structure { + // kind: StructureKind::House(HouseBuilding::generate(ctx.rng, + // Vec3::new( house_pos.x, + // house_pos.y, + // ctx.sim + // .and_then(|sim| sim.get_alt_approx(self.origin + house_pos)) + // .unwrap_or(0.0) + // .ceil() as i32, + // ))), + // }); + // } + + // Fields + let farmland = self.farms.insert(Farm { base_tile }); + for _ in 0..FIELDS_PER_FARM { + self.place_field(farmland, base_tile, ctx.rng); + } + } + } + } + + pub fn place_field( + &mut self, + farm: Id, + origin: Vec2, + rng: &mut impl Rng, + ) -> Option> { + const MAX_FIELD_SIZE: usize = 24; + + if let Some(center) = self.land.find_tile_near(origin, |plot| plot.is_none()) { + let field = self.land.new_plot(Plot::Field { + farm, + seed: rng.gen(), + crop: match rng.gen_range(0, 8) { + 0 => Crop::Corn, + 1 => Crop::Wheat, + 2 => Crop::Cabbage, + 3 => Crop::Pumpkin, + 4 => Crop::Flax, + 5 => Crop::Carrot, + 6 => Crop::Tomato, + 7 => Crop::Radish, + _ => Crop::Sunflower, + }, + }); + let tiles = + self.land + .grow_from(center, rng.gen_range(5, MAX_FIELD_SIZE), rng, |plot| { + plot.is_none() + }); + for pos in tiles.into_iter() { + self.land.set(pos, field); + } + Some(field) + } else { + None + } + } + + pub fn radius(&self) -> f32 { 1200.0 } + + pub fn spawn_rules(&self, wpos: Vec2) -> SpawnRules { + SpawnRules { + trees: self + .land + .get_at_block(wpos - self.origin) + .plot + .map(|p| if let Plot::Hazard = p { true } else { false }) + .unwrap_or(true), + ..SpawnRules::default() + } + } + + pub fn apply_to<'a>( + &'a self, + wpos2d: Vec2, + mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, + vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), + ) { + for y in 0..vol.size_xy().y as i32 { + for x in 0..vol.size_xy().x as i32 { + let offs = Vec2::new(x, y); + + let wpos2d = wpos2d + offs; + let rpos = wpos2d - self.origin; + + // Sample terrain + let col_sample = if let Some(col_sample) = get_column(offs) { + col_sample + } else { + continue; + }; + let surface_z = col_sample.riverless_alt.floor() as i32; + + // Sample settlement + let sample = self.land.get_at_block(rpos); + + let noisy_color = |col: Rgb, factor: u32| { + let nz = self.noise.get(Vec3::new(wpos2d.x, wpos2d.y, surface_z)); + col.map(|e| { + (e as u32 + nz % (factor * 2)) + .saturating_sub(factor) + .min(255) as u8 + }) + }; + + // Paths + if let Some((WayKind::Path, dist, nearest)) = sample.way { + let inset = -1; + + // Try to use the column at the centre of the path for sampling to make them + // flatter + let col = get_column(offs + (nearest.floor().map(|e| e as i32) - rpos)) + .unwrap_or(col_sample); + let (bridge_offset, depth) = if let Some(water_dist) = col.water_dist { + ( + ((water_dist.max(0.0) * 0.2).min(f32::consts::PI).cos() + 1.0) * 5.0, + ((1.0 - ((water_dist + 2.0) * 0.3).min(0.0).cos().abs()) + * (col.riverless_alt + 5.0 - col.alt).max(0.0) + * 1.75 + + 3.0) as i32, + ) + } else { + (0.0, 3) + }; + let surface_z = (col.riverless_alt + bridge_offset).floor() as i32; + + for z in inset - depth..inset { + let _ = vol.set( + Vec3::new(offs.x, offs.y, surface_z + z), + if bridge_offset >= 2.0 && dist >= 3.0 || z < inset - 1 { + Block::new(BlockKind::Normal, noisy_color(Rgb::new(80, 80, 100), 8)) + } else { + Block::new(BlockKind::Normal, noisy_color(Rgb::new(80, 50, 30), 8)) + }, + ); + } + let head_space = (8 - (dist * 0.25).powf(6.0).round() as i32).max(1); + for z in inset..inset + head_space { + let pos = Vec3::new(offs.x, offs.y, surface_z + z); + if vol.get(pos).unwrap().kind() != BlockKind::Water { + let _ = vol.set(pos, Block::empty()); + } + } + // Ground colour + } else { + let mut surface_block = None; + + let roll = + |seed, n| self.noise.get(Vec3::new(wpos2d.x, wpos2d.y, seed * 5)) % n; + + let color = match sample.plot { + Some(Plot::Dirt) => Some(Rgb::new(90, 70, 50)), + Some(Plot::Grass) => Some(Rgb::new(100, 200, 0)), + Some(Plot::Water) => Some(Rgb::new(100, 150, 250)), + Some(Plot::Town) => { + if let Some((_, path_nearest)) = col_sample.path { + let path_dir = (path_nearest - wpos2d.map(|e| e as f32)) + .rotated_z(f32::consts::PI / 2.0) + .normalized(); + let is_lamp = if path_dir.x.abs() > path_dir.y.abs() { + wpos2d.x as f32 % 20.0 / path_dir.dot(Vec2::unit_y()).abs() + <= 1.0 + } else { + wpos2d.y as f32 % 20.0 / path_dir.dot(Vec2::unit_x()).abs() + <= 1.0 + }; + if (col_sample.path.map(|(dist, _)| dist > 6.0 && dist < 7.0).unwrap_or(false) && is_lamp) //roll(0, 50) == 0) + || roll(0, 2000) == 0 + { + surface_block = + Some(Block::new(BlockKind::StreetLamp, Rgb::white())); + } + } + + Some(Rgb::new(100, 90, 75).map2(Rgb::iota(), |e: u8, i: i32| { + e.saturating_add( + (self.noise.get(Vec3::new(wpos2d.x, wpos2d.y, i * 5)) % 1) + as u8, + ) + .saturating_sub(8) + })) + }, + Some(Plot::Field { seed, crop, .. }) => { + let furrow_dirs = [ + Vec2::new(1, 0), + Vec2::new(0, 1), + Vec2::new(1, 1), + Vec2::new(-1, 1), + ]; + let furrow_dir = furrow_dirs[*seed as usize % furrow_dirs.len()]; + let in_furrow = (wpos2d * furrow_dir).sum().rem_euclid(5) < 2; + + let dirt = Rgb::new(80, 55, 35).map(|e| { + e + (self.noise.get(Vec3::broadcast((seed % 4096 + 0) as i32)) % 32) + as u8 + }); + let mound = + Rgb::new(70, 80, 30).map(|e| e + roll(0, 8) as u8).map(|e| { + e + (self.noise.get(Vec3::broadcast((seed % 4096 + 1) as i32)) + % 32) as u8 + }); + + if in_furrow { + if roll(0, 5) == 0 { + surface_block = match crop { + Crop::Corn => Some(BlockKind::Corn), + Crop::Wheat if roll(1, 2) == 0 => { + Some(BlockKind::WheatYellow) + }, + Crop::Wheat => Some(BlockKind::WheatGreen), + Crop::Cabbage if roll(2, 2) == 0 => { + Some(BlockKind::Cabbage) + }, + Crop::Pumpkin if roll(3, 2) == 0 => { + Some(BlockKind::Pumpkin) + }, + Crop::Flax if roll(4, 2) == 0 => Some(BlockKind::Flax), + Crop::Carrot if roll(5, 2) == 0 => Some(BlockKind::Carrot), + Crop::Tomato if roll(6, 2) == 0 => Some(BlockKind::Tomato), + Crop::Radish if roll(7, 2) == 0 => Some(BlockKind::Radish), + Crop::Turnip if roll(8, 2) == 0 => Some(BlockKind::Turnip), + Crop::Sunflower => Some(BlockKind::Sunflower), + _ => None, + } + .or_else(|| { + if roll(9, 400) == 0 { + Some(BlockKind::Scarecrow) + } else { + None + } + }) + .map(|kind| Block::new(kind, Rgb::white())); + } + } else { + if roll(0, 20) == 0 { + surface_block = + Some(Block::new(BlockKind::ShortGrass, Rgb::white())); + } else if roll(1, 30) == 0 { + surface_block = + Some(Block::new(BlockKind::MediumGrass, Rgb::white())); + } + } + + Some(if in_furrow { dirt } else { mound }) + }, + _ => None, + }; + + if let Some(color) = color { + if col_sample.water_dist.map(|dist| dist > 2.0).unwrap_or(true) { + for z in -8..3 { + let pos = Vec3::new(offs.x, offs.y, surface_z + z); + + if let (0, Some(block)) = (z, surface_block) { + let _ = vol.set(pos, block); + } else if z >= 0 { + if vol.get(pos).unwrap().kind() != BlockKind::Water { + let _ = vol.set(pos, Block::empty()); + } + } else { + let _ = vol.set( + pos, + Block::new(BlockKind::Normal, noisy_color(color, 4)), + ); + } + } + } + } + } + + // Walls + if let Some((WayKind::Wall, dist, _)) = sample.way { + let color = Lerp::lerp( + Rgb::new(130i32, 100, 0), + Rgb::new(90, 70, 50), + (RandomField::new(0).get(wpos2d.into()) % 256) as f32 / 256.0, + ) + .map(|e| (e % 256) as u8); + + let z_offset = if let Some(water_dist) = col_sample.water_dist { + // Water gate + ((water_dist.max(0.0) * 0.45).min(f32::consts::PI).cos() + 1.0) * 4.0 + } else { + 0.0 + } as i32; + + for z in z_offset..12 { + if dist / WayKind::Wall.width() < ((1.0 - z as f32 / 12.0) * 2.0).min(1.0) { + let _ = vol.set( + Vec3::new(offs.x, offs.y, surface_z + z), + Block::new(BlockKind::Normal, color), + ); + } + } + } + + // Towers + if let Some((Tower::Wall, _pos)) = sample.tower { + for z in -2..16 { + let _ = vol.set( + Vec3::new(offs.x, offs.y, surface_z + z), + Block::new(BlockKind::Normal, Rgb::new(50, 50, 50)), + ); + } + } + } + } + + // Apply structures + for structure in &self.structures { + let bounds = structure.bounds_2d(); + + // Skip this structure if it's not near this chunk + if !bounds.collides_with_aabr(Aabr { + min: wpos2d - self.origin, + max: wpos2d - self.origin + vol.size_xy().map(|e| e as i32 + 1), + }) { + continue; + } + + match &structure.kind { + StructureKind::House(b) => { + let bounds = b.bounds(); + + for x in bounds.min.x..bounds.max.x + 1 { + for y in bounds.min.y..bounds.max.y + 1 { + let col = if let Some(col) = + get_column(self.origin + Vec2::new(x, y) - wpos2d) + { + col + } else { + continue; + }; + + for z in bounds.min.z.min(col.alt.floor() as i32 - 1)..bounds.max.z + 1 + { + let rpos = Vec3::new(x, y, z); + let wpos = Vec3::from(self.origin) + rpos; + let coffs = wpos - Vec3::from(wpos2d); + + if let Some(block) = b.sample(rpos) { + let _ = vol.set(coffs, block); + } + } + } + } + }, + } + } + } + + pub fn apply_supplement<'a>( + &'a self, + rng: &mut impl Rng, + wpos2d: Vec2, + mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, + supplement: &mut ChunkSupplement, + ) { + for y in 0..TerrainChunkSize::RECT_SIZE.y as i32 { + for x in 0..TerrainChunkSize::RECT_SIZE.x as i32 { + let offs = Vec2::new(x, y); + + let wpos2d = wpos2d + offs; + let rpos = wpos2d - self.origin; + + // Sample terrain + let col_sample = if let Some(col_sample) = get_column(offs) { + col_sample + } else { + continue; + }; + + let sample = self.land.get_at_block(rpos); + + let entity_wpos = Vec3::new(wpos2d.x as f32, wpos2d.y as f32, col_sample.alt + 3.0); + + if matches!(sample.plot, Some(Plot::Town)) + && RandomField::new(self.seed).chance(Vec3::from(wpos2d), 1.0 / (50.0 * 50.0)) + { + let entity = EntityInfo::at(entity_wpos) + .with_alignment(comp::Alignment::Npc) + .with_body(match rng.gen_range(0, 4) { + 0 => { + let species = match rng.gen_range(0, 3) { + 0 => quadruped_small::Species::Pig, + 1 => quadruped_small::Species::Sheep, + _ => quadruped_small::Species::Cat, + }; + + comp::Body::QuadrupedSmall(quadruped_small::Body::random_with( + rng, &species, + )) + }, + 1 => { + let species = match rng.gen_range(0, 4) { + 0 => bird_medium::Species::Duck, + 1 => bird_medium::Species::Chicken, + 2 => bird_medium::Species::Goose, + _ => bird_medium::Species::Peacock, + }; + + comp::Body::BirdMedium(bird_medium::Body::random_with( + rng, &species, + )) + }, + _ => comp::Body::Humanoid(humanoid::Body::random()), + }) + .do_if(rng.gen(), |entity| { + entity.with_main_tool(assets::load_expect_cloned( + match rng.gen_range(0, 6) { + 0 => "common.items.weapons.starter_axe", + 1 => "common.items.weapons.starter_sword", + 2 => "common.items.weapons.short_sword_0", + 3 => "common.items.weapons.hammer_1", + 4 => "common.items.weapons.starter_staff", + _ => "common.items.weapons.starter_bow", + }, + )) + }) + .with_automatic_name(); + + supplement.add_entity(entity); + } + } + } + } + + pub fn get_color(&self, pos: Vec2) -> Option> { + let sample = self.land.get_at_block(pos); + + // match sample.tower { + // Some((Tower::Wall, _)) => return Some(Rgb::new(50, 50, 50)), + // _ => {}, + // } + + // match sample.way { + // Some((WayKind::Path, _, _)) => return Some(Rgb::new(90, 70, 50)), + // Some((WayKind::Hedge, _, _)) => return Some(Rgb::new(0, 150, 0)), + // Some((WayKind::Wall, _, _)) => return Some(Rgb::new(60, 60, 60)), + // _ => {}, + // } + + match sample.plot { + Some(Plot::Dirt) => return Some(Rgb::new(90, 70, 50)), + Some(Plot::Grass) => return Some(Rgb::new(100, 200, 0)), + Some(Plot::Water) => return Some(Rgb::new(100, 150, 250)), + Some(Plot::Town) => { + return Some(Rgb::new(150, 110, 60).map2(Rgb::iota(), |e: u8, i: i32| { + e.saturating_add((self.noise.get(Vec3::new(pos.x, pos.y, i * 5)) % 16) as u8) + .saturating_sub(8) + })); + }, + Some(Plot::Field { seed, .. }) => { + let furrow_dirs = [ + Vec2::new(1, 0), + Vec2::new(0, 1), + Vec2::new(1, 1), + Vec2::new(-1, 1), + ]; + let furrow_dir = furrow_dirs[*seed as usize % furrow_dirs.len()]; + let furrow = (pos * furrow_dir).sum().rem_euclid(6) < 3; + return Some(Rgb::new( + if furrow { + 100 + } else { + 32 + seed.to_le_bytes()[0] % 64 + }, + 64 + seed.to_le_bytes()[1] % 128, + 16 + seed.to_le_bytes()[2] % 32, + )); + }, + _ => {}, + } + + None + } +} + +#[derive(Copy, Clone, PartialEq)] +pub enum Crop { + Corn, + Wheat, + Cabbage, + Pumpkin, + Flax, + Carrot, + Tomato, + Radish, + Turnip, + Sunflower, +} + +#[derive(Copy, Clone, PartialEq)] +pub enum Plot { + Hazard, + Dirt, + Grass, + Water, + Town, + Field { + farm: Id, + seed: u32, + crop: Crop, + }, +} + +const CARDINALS: [Vec2; 4] = [ + Vec2::new(0, 1), + Vec2::new(1, 0), + Vec2::new(0, -1), + Vec2::new(-1, 0), +]; + +#[derive(Copy, Clone, PartialEq)] +pub enum WayKind { + Path, + #[allow(dead_code)] + Wall, +} + +impl WayKind { + pub fn width(&self) -> f32 { + match self { + WayKind::Path => 4.0, + WayKind::Wall => 3.0, + } + } +} + +#[derive(Copy, Clone, PartialEq)] +pub enum Tower { + #[allow(dead_code)] + Wall, +} + +impl Tower { + pub fn radius(&self) -> f32 { + match self { + Tower::Wall => 6.0, + } + } +} + +pub struct Tile { + plot: Id, + ways: [Option; 4], + tower: Option, +} + +impl Tile { + pub fn contains(&self, kind: WayKind) -> bool { self.ways.iter().any(|way| way == &Some(kind)) } +} + +#[derive(Default)] +pub struct Sample<'a> { + plot: Option<&'a Plot>, + way: Option<(&'a WayKind, f32, Vec2)>, + tower: Option<(&'a Tower, Vec2)>, +} + +pub struct Land { + tiles: HashMap, Tile>, + plots: Store, + sampler_warp: StructureGen2d, + hazard: Id, +} + +impl Land { + pub fn new(rng: &mut impl Rng) -> Self { + let mut plots = Store::default(); + let hazard = plots.insert(Plot::Hazard); + Self { + tiles: HashMap::new(), + plots, + sampler_warp: StructureGen2d::new(rng.gen(), AREA_SIZE, AREA_SIZE * 2 / 5), + hazard, + } + } + + pub fn get_at_block(&self, pos: Vec2) -> Sample { + let mut sample = Sample::default(); + + let neighbors = self.sampler_warp.get(pos); + let closest = neighbors + .iter() + .min_by_key(|(center, _)| center.distance_squared(pos)) + .unwrap() + .0; + + let center_tile = self.tile_at(neighbors[4].0.map(to_tile)); + + if let Some(tower) = center_tile.and_then(|tile| tile.tower.as_ref()) { + if (neighbors[4].0.distance_squared(pos) as f32) < tower.radius().powf(2.0) { + sample.tower = Some((tower, neighbors[4].0)); + } + } + + for (i, _) in CARDINALS.iter().enumerate() { + let map = [1, 5, 7, 3]; + let line = LineSegment2 { + start: neighbors[4].0.map(|e| e as f32), + end: neighbors[map[i]].0.map(|e| e as f32), + }; + if let Some(way) = center_tile.and_then(|tile| tile.ways[i].as_ref()) { + let proj_point = line.projected_point(pos.map(|e| e as f32)); + let dist = proj_point.distance(pos.map(|e| e as f32)); + if dist < way.width() { + sample.way = sample + .way + .filter(|(_, d, _)| *d < dist) + .or(Some((way, dist, proj_point))); + } + } + } + + sample.plot = self.plot_at(closest.map(to_tile)); + + sample + } + + pub fn tile_at(&self, pos: Vec2) -> Option<&Tile> { self.tiles.get(&pos) } + + #[allow(dead_code)] + pub fn tile_at_mut(&mut self, pos: Vec2) -> Option<&mut Tile> { self.tiles.get_mut(&pos) } + + pub fn plot(&self, id: Id) -> &Plot { self.plots.get(id) } + + pub fn plot_at(&self, pos: Vec2) -> Option<&Plot> { + self.tiles.get(&pos).map(|tile| self.plots.get(tile.plot)) + } + + pub fn plot_at_mut(&mut self, pos: Vec2) -> Option<&mut Plot> { + self.tiles + .get(&pos) + .map(|tile| tile.plot) + .map(move |plot| self.plots.get_mut(plot)) + } + + pub fn set(&mut self, pos: Vec2, plot: Id) { + self.tiles.insert(pos, Tile { + plot, + ways: [None; 4], + tower: None, + }); + } + + fn find_tile_near( + &self, + origin: Vec2, + mut match_fn: impl FnMut(Option<&Plot>) -> bool, + ) -> Option> { + Spiral2d::new() + .map(|pos| origin + pos) + .find(|pos| match_fn(self.plot_at(*pos))) + } + + #[allow(dead_code)] + fn find_tile_dir( + &self, + origin: Vec2, + dir: Vec2, + mut match_fn: impl FnMut(Option<&Plot>) -> bool, + ) -> Option> { + (0..) + .map(|i| origin + dir * i) + .find(|pos| match_fn(self.plot_at(*pos))) + } + + fn find_path( + &self, + origin: Vec2, + dest: Vec2, + mut path_cost_fn: impl FnMut(Option<&Tile>, Option<&Tile>) -> f32, + ) -> Option>> { + let heuristic = |pos: &Vec2| (pos - dest).map(|e| e as f32).magnitude(); + let neighbors = |pos: &Vec2| { + let pos = *pos; + CARDINALS.iter().map(move |dir| pos + *dir) + }; + let transition = + |from: &Vec2, to: &Vec2| path_cost_fn(self.tile_at(*from), self.tile_at(*to)); + let satisfied = |pos: &Vec2| *pos == dest; + + Astar::new(250, origin, heuristic) + .poll(250, heuristic, neighbors, transition, satisfied) + .into_path() + } + + fn grow_from( + &self, + start: Vec2, + max_size: usize, + _rng: &mut impl Rng, + mut match_fn: impl FnMut(Option<&Plot>) -> bool, + ) -> HashSet> { + let mut open = VecDeque::new(); + open.push_back(start); + let mut closed = HashSet::new(); + + while open.len() + closed.len() < max_size { + let next_pos = if let Some(next_pos) = open.pop_front() { + closed.insert(next_pos); + next_pos + } else { + break; + }; + + let dirs = [ + Vec2::new(1, 0), + Vec2::new(-1, 0), + Vec2::new(0, 1), + Vec2::new(0, -1), + ]; + + for dir in dirs.iter() { + let neighbor = next_pos + dir; + if !closed.contains(&neighbor) && match_fn(self.plot_at(neighbor)) { + open.push_back(neighbor); + } + } + } + + closed.into_iter().chain(open.into_iter()).collect() + } + + fn write_path( + &mut self, + tiles: &[Vec2], + kind: WayKind, + mut permit_fn: impl FnMut(&Plot) -> bool, + overwrite: bool, + ) { + for tiles in tiles.windows(2) { + let dir = tiles[1] - tiles[0]; + let idx = if dir.y > 0 { + 1 + } else if dir.x > 0 { + 2 + } else if dir.y < 0 { + 3 + } else if dir.x < 0 { + 0 + } else { + continue; + }; + if self.tile_at(tiles[0]).is_none() { + self.set(tiles[0], self.hazard); + } + let plots = &self.plots; + + self.tiles + .get_mut(&tiles[1]) + .filter(|tile| permit_fn(plots.get(tile.plot))) + .map(|tile| { + if overwrite || tile.ways[(idx + 2) % 4].is_none() { + tile.ways[(idx + 2) % 4] = Some(kind); + } + }); + self.tiles + .get_mut(&tiles[0]) + .filter(|tile| permit_fn(plots.get(tile.plot))) + .map(|tile| { + if overwrite || tile.ways[idx].is_none() { + tile.ways[idx] = Some(kind); + } + }); + } + } + + pub fn new_plot(&mut self, plot: Plot) -> Id { self.plots.insert(plot) } +} diff --git a/world/src/util/grid.rs b/world/src/util/grid.rs index b6a421ceb0..7c57cc2050 100644 --- a/world/src/util/grid.rs +++ b/world/src/util/grid.rs @@ -5,8 +5,22 @@ pub struct Grid { size: Vec2, } -impl Grid { - pub fn new(default_cell: T, size: Vec2) -> Self { +impl Grid { + pub fn populate_from(size: Vec2, mut f: impl FnMut(Vec2) -> T) -> Self { + Self { + cells: (0..size.y) + .map(|y| (0..size.x).map(move |x| Vec2::new(x, y))) + .flatten() + .map(&mut f) + .collect(), + size, + } + } + + pub fn new(size: Vec2, default_cell: T) -> Self + where + T: Clone, + { Self { cells: vec![default_cell; size.product() as usize], size, diff --git a/world/src/util/mod.rs b/world/src/util/mod.rs index 489e3469b2..ce4dbd3971 100644 --- a/world/src/util/mod.rs +++ b/world/src/util/mod.rs @@ -19,3 +19,58 @@ pub use self::{ structure::StructureGen2d, unit_chooser::UnitChooser, }; + +use vek::*; + +pub fn attempt(max_iters: usize, mut f: impl FnMut() -> Option) -> Option { + (0..max_iters).find_map(|_| f()) +} + +pub const CARDINALS: [Vec2; 4] = [ + Vec2::new(0, 1), + Vec2::new(1, 0), + Vec2::new(0, -1), + Vec2::new(-1, 0), +]; + +pub const DIRS: [Vec2; 8] = [ + Vec2::new(1, 0), + Vec2::new(1, 1), + Vec2::new(0, 1), + Vec2::new(-1, 1), + Vec2::new(-1, 0), + Vec2::new(-1, -1), + Vec2::new(0, -1), + Vec2::new(1, -1), +]; + +pub const NEIGHBORS: [Vec2; 8] = [ + Vec2::new(1, 0), + Vec2::new(1, 1), + Vec2::new(0, 1), + Vec2::new(-1, 1), + Vec2::new(-1, 0), + Vec2::new(-1, -1), + Vec2::new(0, -1), + Vec2::new(1, -1), +]; + +pub const LOCALITY: [Vec2; 9] = [ + Vec2::new(0, 0), + Vec2::new(0, 1), + Vec2::new(1, 0), + Vec2::new(0, -1), + Vec2::new(-1, 0), + Vec2::new(1, 1), + Vec2::new(1, -1), + Vec2::new(-1, 1), + Vec2::new(-1, -1), +]; + +pub const CARDINAL_LOCALITY: [Vec2; 5] = [ + Vec2::new(0, 0), + Vec2::new(0, 1), + Vec2::new(1, 0), + Vec2::new(0, -1), + Vec2::new(-1, 0), +]; diff --git a/world/src/util/random.rs b/world/src/util/random.rs index 3a36c16fa9..f9d1afdcc8 100644 --- a/world/src/util/random.rs +++ b/world/src/util/random.rs @@ -8,6 +8,10 @@ pub struct RandomField { impl RandomField { pub const fn new(seed: u32) -> Self { Self { seed } } + + pub fn chance(&self, pos: Vec3, chance: f32) -> bool { + (self.get(pos) % (1 << 10)) as f32 / ((1 << 10) as f32) < chance + } } impl Sampler<'static> for RandomField { diff --git a/world/src/util/structure.rs b/world/src/util/structure.rs index 4868159d0b..48429f3e65 100644 --- a/world/src/util/structure.rs +++ b/world/src/util/structure.rs @@ -54,10 +54,14 @@ impl StructureGen2d { let pos = Vec3::from(center); ( center - + Vec2::new( - (x_field.get(pos) % spread_mul) as i32 - spread, - (y_field.get(pos) % spread_mul) as i32 - spread, - ), + + if spread_mul > 0 { + Vec2::new( + (x_field.get(pos) % spread_mul) as i32 - spread, + (y_field.get(pos) % spread_mul) as i32 - spread, + ) + } else { + Vec2::zero() + }, seed_field.get(pos), ) }