diff --git a/Cargo.toml b/Cargo.toml index f594016..4dd5353 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ exr = "1.72" kurbo = "0.11" nalgebra = { version = "0.34.1", features = ["convert-glam030"] } serde_json = "1.0" +bladeink = "1.2" wesl = "0.2" [build-dependencies] diff --git a/assets/dialogs/test_char.ink b/assets/dialogs/test_char.ink new file mode 100644 index 0000000..49625e4 --- /dev/null +++ b/assets/dialogs/test_char.ink @@ -0,0 +1,48 @@ +-> encounter + +=== encounter === +So. You came after all. # timer: 3.0 # parry: J ++ [hit] -> outcome_hit ++ [evaded] -> outcome_evaded ++ [wrong_parry] -> outcome_wrong ++ [parried_i] -> outcome_wrong ++ [parried_j] -> outcome_correct ++ [parried_l] -> outcome_wrong + +=== outcome_correct === +Exactly right. # timer: 2.5 +-> END + +=== outcome_hit === +You hesitate. Interesting. # timer: 2.5 # parry: I ++ [hit] -> end_beaten ++ [evaded] -> end_fled ++ [wrong_parry] -> end_beaten ++ [parried_i] -> end_strong ++ [parried_j] -> end_beaten ++ [parried_l] -> end_beaten + +=== outcome_evaded === +Running already? # timer: 2.5 +-> END + +=== outcome_wrong === +That wasn't the answer I expected. # timer: 2.5 # parry: L ++ [hit] -> end_beaten ++ [evaded] -> end_fled ++ [wrong_parry] -> end_beaten ++ [parried_i] -> end_beaten ++ [parried_j] -> end_beaten ++ [parried_l] -> end_strong + +=== end_beaten === +I see. You are not ready. # timer: 2.5 +-> END + +=== end_fled === +So be it. # timer: 2.0 +-> END + +=== end_strong === +Good. Perhaps you are worthy after all. # timer: 3.0 +-> END diff --git a/assets/dialogs/test_char.ink.json b/assets/dialogs/test_char.ink.json new file mode 100644 index 0000000..ceb80f8 --- /dev/null +++ b/assets/dialogs/test_char.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[[[{"->":"encounter"},{"#n":"g-0"}],null],"done",{"encounter":[["^So. You came after all. ","#","^timer: 3.0 ","/#","#","^parry: J","/#","\n","ev","str","^hit","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^evaded","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^wrong_parry","/str","/ev",{"*":".^.c-2","flg":4},"ev","str","^parried_i","/str","/ev",{"*":".^.c-3","flg":4},"ev","str","^parried_j","/str","/ev",{"*":".^.c-4","flg":4},"ev","str","^parried_l","/str","/ev",{"*":".^.c-5","flg":4},{"c-0":["^ ",{"->":"outcome_hit"},"\n",null],"c-1":["^ ",{"->":"outcome_evaded"},"\n",null],"c-2":["^ ",{"->":"outcome_wrong"},"\n",null],"c-3":["^ ",{"->":"outcome_wrong"},"\n",null],"c-4":["^ ",{"->":"outcome_correct"},"\n",null],"c-5":["^ ",{"->":"outcome_wrong"},"\n",null]}],null],"outcome_correct":["^Exactly right. ","#","^timer: 2.5","/#","\n","end",null],"outcome_hit":[["^You hesitate. Interesting. ","#","^timer: 2.5 ","/#","#","^parry: I","/#","\n","ev","str","^hit","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^evaded","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^wrong_parry","/str","/ev",{"*":".^.c-2","flg":4},"ev","str","^parried_i","/str","/ev",{"*":".^.c-3","flg":4},"ev","str","^parried_j","/str","/ev",{"*":".^.c-4","flg":4},"ev","str","^parried_l","/str","/ev",{"*":".^.c-5","flg":4},{"c-0":["^ ",{"->":"end_beaten"},"\n",null],"c-1":["^ ",{"->":"end_fled"},"\n",null],"c-2":["^ ",{"->":"end_beaten"},"\n",null],"c-3":["^ ",{"->":"end_strong"},"\n",null],"c-4":["^ ",{"->":"end_beaten"},"\n",null],"c-5":["^ ",{"->":"end_beaten"},"\n",null]}],null],"outcome_evaded":["^Running already? ","#","^timer: 2.5","/#","\n","end",null],"outcome_wrong":[["^That wasn't the answer I expected. ","#","^timer: 2.5 ","/#","#","^parry: L","/#","\n","ev","str","^hit","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^evaded","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^wrong_parry","/str","/ev",{"*":".^.c-2","flg":4},"ev","str","^parried_i","/str","/ev",{"*":".^.c-3","flg":4},"ev","str","^parried_j","/str","/ev",{"*":".^.c-4","flg":4},"ev","str","^parried_l","/str","/ev",{"*":".^.c-5","flg":4},{"c-0":["^ ",{"->":"end_beaten"},"\n",null],"c-1":["^ ",{"->":"end_fled"},"\n",null],"c-2":["^ ",{"->":"end_beaten"},"\n",null],"c-3":["^ ",{"->":"end_beaten"},"\n",null],"c-4":["^ ",{"->":"end_beaten"},"\n",null],"c-5":["^ ",{"->":"end_strong"},"\n",null]}],null],"end_beaten":["^I see. You are not ready. ","#","^timer: 2.5","/#","\n","end",null],"end_fled":["^So be it. ","#","^timer: 2.0","/#","\n","end",null],"end_strong":["^Good. Perhaps you are worthy after all. ","#","^timer: 3.0","/#","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/meshes/burrs.gltf b/assets/meshes/burrs.gltf similarity index 100% rename from meshes/burrs.gltf rename to assets/meshes/burrs.gltf diff --git a/meshes/player_mesh.glb b/assets/meshes/player_mesh.glb similarity index 100% rename from meshes/player_mesh.glb rename to assets/meshes/player_mesh.glb diff --git a/meshes/terrain.bin b/assets/meshes/terrain.bin similarity index 63% rename from meshes/terrain.bin rename to assets/meshes/terrain.bin index b6fba5a..7816679 100644 Binary files a/meshes/terrain.bin and b/assets/meshes/terrain.bin differ diff --git a/meshes/terrain.gltf b/assets/meshes/terrain.gltf similarity index 71% rename from meshes/terrain.gltf rename to assets/meshes/terrain.gltf index 1eb0ba2..edaeb6c 100644 --- a/meshes/terrain.gltf +++ b/assets/meshes/terrain.gltf @@ -43,7 +43,10 @@ 1, 2, 3, - 4 + 4, + 5, + 6, + 7 ] } ], @@ -58,11 +61,44 @@ ] }, { - "children":[ - 5, - 6 + "mesh":1, + "name":"Lighthouse_base", + "rotation":[ + 0, + -0.31448131799697876, + 0, + 0.9492637515068054 ], - "mesh":3, + "scale":[ + 25.161100387573242, + 8.222701072692871, + 8.222701072692871 + ], + "translation":[ + -367.9805908203125, + 113.61730194091797, + 212.62832641601562 + ] + }, + { + "mesh":2, + "name":"Lighthouse", + "scale":[ + 2.1515703201293945, + 24.724905014038086, + 2.1515703201293945 + ], + "translation":[ + -343.6542053222656, + 152.96629333496094, + 231.81927490234375 + ] + }, + { + "children":[ + 8 + ], + "mesh":4, "name":"TerrainPlane", "scale":[ 1.000100016593933, @@ -100,56 +136,37 @@ 0.9110424518585205 ], "translation":[ - -392.0350036621094, - 238.72787475585938, - 244.30006408691406 + -344.301025390625, + 223.67401123046875, + 232.61265563964844 + ] + }, + { + "name":"TestCharSpawn", + "translation":[ + -381.1509704589844, + 106.53739166259766, + 107.46959686279297 ] }, { "extensions":{ "EXT_mesh_gpu_instancing":{ "attributes":{ - "TRANSLATION":17, - "ROTATION":18, - "SCALE":19 + "TRANSLATION":19, + "ROTATION":20, + "SCALE":21 } } }, - "mesh":1, + "mesh":3, "name":"TerrainPlane.0" - }, - { - "extensions":{ - "EXT_mesh_gpu_instancing":{ - "attributes":{ - "TRANSLATION":17, - "ROTATION":18, - "SCALE":19 - } - } - }, - "mesh":2, - "name":"TerrainPlane.1" } ], "materials":[ { "doubleSided":true, "name":"terrain" - }, - { - "doubleSided":true, - "name":"snow", - "pbrMetallicRoughness":{ - "baseColorFactor":[ - 0.800000011920929, - 0.800000011920929, - 0.800000011920929, - 1 - ], - "metallicFactor":0, - "roughnessFactor":0.5 - } } ], "meshes":[ @@ -167,7 +184,7 @@ ] }, { - "name":"Cylinder", + "name":"Cube", "primitives":[ { "attributes":{ @@ -175,8 +192,20 @@ "NORMAL":5, "TEXCOORD_0":6 }, - "indices":3, - "material":0 + "indices":7 + } + ] + }, + { + "name":"Cylinder.001", + "primitives":[ + { + "attributes":{ + "POSITION":8, + "NORMAL":9, + "TEXCOORD_0":10 + }, + "indices":11 } ] }, @@ -185,12 +214,11 @@ "primitives":[ { "attributes":{ - "POSITION":7, - "NORMAL":8, - "TEXCOORD_0":9 + "POSITION":12, + "NORMAL":13, + "TEXCOORD_0":14 }, - "indices":3, - "material":1 + "indices":3 } ] }, @@ -199,21 +227,12 @@ "primitives":[ { "attributes":{ - "POSITION":10, - "NORMAL":11, - "TEXCOORD_0":12 + "POSITION":15, + "NORMAL":16, + "TEXCOORD_0":17 }, - "indices":13, + "indices":18, "material":0 - }, - { - "attributes":{ - "POSITION":14, - "NORMAL":15, - "TEXCOORD_0":16 - }, - "indices":13, - "material":1 } ] } @@ -256,33 +275,73 @@ { "bufferView":4, "componentType":5126, - "count":1280, + "count":24, "max":[ - 5.561562538146973, - 16.066009521484375, - 5.561562538146973 + 1, + 1, + 1 ], "min":[ - -5.561562538146973, - 0, - -5.561562538146973 + -1, + -1, + -1 ], "type":"VEC3" }, { "bufferView":5, "componentType":5126, - "count":1280, + "count":24, "type":"VEC3" }, { "bufferView":6, "componentType":5126, - "count":1280, + "count":24, "type":"VEC2" }, { "bufferView":7, + "componentType":5123, + "count":36, + "type":"SCALAR" + }, + { + "bufferView":8, + "componentType":5126, + "count":704, + "max":[ + 6.253715991973877, + 2.2209415435791016, + 6.253798961639404 + ], + "min":[ + -6.253823757171631, + -1.9492745399475098, + -6.25374174118042 + ], + "type":"VEC3" + }, + { + "bufferView":9, + "componentType":5126, + "count":704, + "type":"VEC3" + }, + { + "bufferView":10, + "componentType":5126, + "count":704, + "type":"VEC2" + }, + { + "bufferView":11, + "componentType":5123, + "count":1140, + "type":"SCALAR" + }, + { + "bufferView":12, "componentType":5126, "count":1280, "max":[ @@ -298,19 +357,19 @@ "type":"VEC3" }, { - "bufferView":8, + "bufferView":13, "componentType":5126, "count":1280, "type":"VEC3" }, { - "bufferView":9, + "bufferView":14, "componentType":5126, "count":1280, "type":"VEC2" }, { - "bufferView":10, + "bufferView":15, "componentType":5126, "count":10404, "max":[ @@ -326,65 +385,37 @@ "type":"VEC3" }, { - "bufferView":11, + "bufferView":16, "componentType":5126, "count":10404, "type":"VEC3" }, { - "bufferView":12, + "bufferView":17, "componentType":5126, "count":10404, "type":"VEC2" }, { - "bufferView":13, + "bufferView":18, "componentType":5123, "count":61206, "type":"SCALAR" }, { - "bufferView":14, - "componentType":5126, - "count":10404, - "max":[ - 500, - 110.7568588256836, - 500 - ], - "min":[ - -500, - -0.6476199626922607, - -500 - ], - "type":"VEC3" - }, - { - "bufferView":15, - "componentType":5126, - "count":10404, - "type":"VEC3" - }, - { - "bufferView":16, - "componentType":5126, - "count":10404, - "type":"VEC2" - }, - { - "bufferView":17, + "bufferView":19, "componentType":5126, "count":2380, "type":"VEC3" }, { - "bufferView":18, + "bufferView":20, "componentType":5126, "count":2380, "type":"VEC4" }, { - "bufferView":19, + "bufferView":21, "componentType":5126, "count":2380, "type":"VEC3" @@ -417,101 +448,113 @@ }, { "buffer":0, - "byteLength":15360, + "byteLength":288, "byteOffset":45160, "target":34962 }, + { + "buffer":0, + "byteLength":288, + "byteOffset":45448, + "target":34962 + }, + { + "buffer":0, + "byteLength":192, + "byteOffset":45736, + "target":34962 + }, + { + "buffer":0, + "byteLength":72, + "byteOffset":45928, + "target":34963 + }, + { + "buffer":0, + "byteLength":8448, + "byteOffset":46000, + "target":34962 + }, + { + "buffer":0, + "byteLength":8448, + "byteOffset":54448, + "target":34962 + }, + { + "buffer":0, + "byteLength":5632, + "byteOffset":62896, + "target":34962 + }, + { + "buffer":0, + "byteLength":2280, + "byteOffset":68528, + "target":34963 + }, { "buffer":0, "byteLength":15360, - "byteOffset":60520, + "byteOffset":70808, + "target":34962 + }, + { + "buffer":0, + "byteLength":15360, + "byteOffset":86168, "target":34962 }, { "buffer":0, "byteLength":10240, - "byteOffset":75880, - "target":34962 - }, - { - "buffer":0, - "byteLength":15360, - "byteOffset":86120, - "target":34962 - }, - { - "buffer":0, - "byteLength":15360, - "byteOffset":101480, - "target":34962 - }, - { - "buffer":0, - "byteLength":10240, - "byteOffset":116840, + "byteOffset":101528, "target":34962 }, { "buffer":0, "byteLength":124848, - "byteOffset":127080, + "byteOffset":111768, "target":34962 }, { "buffer":0, "byteLength":124848, - "byteOffset":251928, + "byteOffset":236616, "target":34962 }, { "buffer":0, "byteLength":83232, - "byteOffset":376776, + "byteOffset":361464, "target":34962 }, { "buffer":0, "byteLength":122412, - "byteOffset":460008, + "byteOffset":444696, "target":34963 }, - { - "buffer":0, - "byteLength":124848, - "byteOffset":582420, - "target":34962 - }, - { - "buffer":0, - "byteLength":124848, - "byteOffset":707268, - "target":34962 - }, - { - "buffer":0, - "byteLength":83232, - "byteOffset":832116, - "target":34962 - }, { "buffer":0, "byteLength":28560, - "byteOffset":915348 + "byteOffset":567108 }, { "buffer":0, "byteLength":38080, - "byteOffset":943908 + "byteOffset":595668 }, { "buffer":0, "byteLength":28560, - "byteOffset":981988 + "byteOffset":633748 } ], "buffers":[ { - "byteLength":1010548, + "byteLength":662308, "uri":"terrain.bin" } ] diff --git a/assets/meshes/test_char.bin b/assets/meshes/test_char.bin new file mode 100644 index 0000000..5711527 Binary files /dev/null and b/assets/meshes/test_char.bin differ diff --git a/assets/meshes/test_char.gltf b/assets/meshes/test_char.gltf new file mode 100644 index 0000000..02b9131 --- /dev/null +++ b/assets/meshes/test_char.gltf @@ -0,0 +1,195 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v5.0.21", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0, + 1 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"Cylinder", + "scale":[ + 1, + 2.6253442764282227, + 1 + ], + "translation":[ + 0, + 3.3726491928100586, + 0 + ] + }, + { + "mesh":1, + "name":"Sphere", + "translation":[ + 0, + 7.17788553237915, + 0 + ] + } + ], + "meshes":[ + { + "name":"Cylinder", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3 + } + ] + }, + { + "name":"Sphere", + "primitives":[ + { + "attributes":{ + "POSITION":4, + "NORMAL":5, + "TEXCOORD_0":6 + }, + "indices":7 + } + ] + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":192, + "max":[ + 1.9433555603027344, + 1, + 1.9433555603027344 + ], + "min":[ + -1.9433555603027344, + -1, + -1.9433555603027344 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":192, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":192, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":372, + "type":"SCALAR" + }, + { + "bufferView":4, + "componentType":5126, + "count":480, + "max":[ + 1, + 1, + 1 + ], + "min":[ + -0.9999999403953552, + -1, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":5, + "componentType":5126, + "count":480, + "type":"VEC3" + }, + { + "bufferView":6, + "componentType":5126, + "count":480, + "type":"VEC2" + }, + { + "bufferView":7, + "componentType":5123, + "count":672, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":2304, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":2304, + "byteOffset":2304, + "target":34962 + }, + { + "buffer":0, + "byteLength":1536, + "byteOffset":4608, + "target":34962 + }, + { + "buffer":0, + "byteLength":744, + "byteOffset":6144, + "target":34963 + }, + { + "buffer":0, + "byteLength":5760, + "byteOffset":6888, + "target":34962 + }, + { + "buffer":0, + "byteLength":5760, + "byteOffset":12648, + "target":34962 + }, + { + "buffer":0, + "byteLength":3840, + "byteOffset":18408, + "target":34962 + }, + { + "buffer":0, + "byteLength":1344, + "byteOffset":22248, + "target":34963 + } + ], + "buffers":[ + { + "byteLength":23592, + "uri":"test_char.bin" + } + ] +} diff --git a/textures/blue_noise.png b/assets/textures/blue_noise.png similarity index 100% rename from textures/blue_noise.png rename to assets/textures/blue_noise.png diff --git a/textures/dither/octave_0.png b/assets/textures/dither/octave_0.png similarity index 100% rename from textures/dither/octave_0.png rename to assets/textures/dither/octave_0.png diff --git a/textures/dither/octave_1.png b/assets/textures/dither/octave_1.png similarity index 100% rename from textures/dither/octave_1.png rename to assets/textures/dither/octave_1.png diff --git a/textures/dither/octave_2.png b/assets/textures/dither/octave_2.png similarity index 100% rename from textures/dither/octave_2.png rename to assets/textures/dither/octave_2.png diff --git a/textures/dither/octave_3.png b/assets/textures/dither/octave_3.png similarity index 100% rename from textures/dither/octave_3.png rename to assets/textures/dither/octave_3.png diff --git a/textures/height_map_x0_y0.exr b/assets/textures/height_map_x0_y0.exr similarity index 100% rename from textures/height_map_x0_y0.exr rename to assets/textures/height_map_x0_y0.exr diff --git a/textures/path_direction_debug.png b/assets/textures/path_direction_debug.png similarity index 100% rename from textures/path_direction_debug.png rename to assets/textures/path_direction_debug.png diff --git a/textures/path_distance_debug.png b/assets/textures/path_distance_debug.png similarity index 100% rename from textures/path_distance_debug.png rename to assets/textures/path_distance_debug.png diff --git a/textures/path_hotspot_debug.exr b/assets/textures/path_hotspot_debug.exr similarity index 100% rename from textures/path_hotspot_debug.exr rename to assets/textures/path_hotspot_debug.exr diff --git a/textures/path_hotspot_heatmap.png b/assets/textures/path_hotspot_heatmap.png similarity index 100% rename from textures/path_hotspot_heatmap.png rename to assets/textures/path_hotspot_heatmap.png diff --git a/textures/path_segment_debug.png b/assets/textures/path_segment_debug.png similarity index 100% rename from textures/path_segment_debug.png rename to assets/textures/path_segment_debug.png diff --git a/textures/scripts/README.md b/assets/textures/scripts/README.md similarity index 100% rename from textures/scripts/README.md rename to assets/textures/scripts/README.md diff --git a/textures/scripts/generate_blue_noise.py b/assets/textures/scripts/generate_blue_noise.py similarity index 100% rename from textures/scripts/generate_blue_noise.py rename to assets/textures/scripts/generate_blue_noise.py diff --git a/textures/snow_depth.exr b/assets/textures/snow_depth.exr similarity index 100% rename from textures/snow_depth.exr rename to assets/textures/snow_depth.exr diff --git a/textures/terrain.exr b/assets/textures/terrain.exr similarity index 100% rename from textures/terrain.exr rename to assets/textures/terrain.exr diff --git a/textures/terrain_flowmap.exr b/assets/textures/terrain_flowmap.exr similarity index 100% rename from textures/terrain_flowmap.exr rename to assets/textures/terrain_flowmap.exr diff --git a/textures/terrain_flowmap.png b/assets/textures/terrain_flowmap.png similarity index 100% rename from textures/terrain_flowmap.png rename to assets/textures/terrain_flowmap.png diff --git a/textures/terrain_heightmap.exr b/assets/textures/terrain_heightmap.exr similarity index 100% rename from textures/terrain_heightmap.exr rename to assets/textures/terrain_heightmap.exr diff --git a/textures/terrain_normals.png b/assets/textures/terrain_normals.png similarity index 100% rename from textures/terrain_normals.png rename to assets/textures/terrain_normals.png diff --git a/blender/terrain.blend b/blender/terrain.blend index b2b495d..9cd7ca9 100644 Binary files a/blender/terrain.blend and b/blender/terrain.blend differ diff --git a/blender/terrain.blend1 b/blender/terrain.blend1 index d9e4968..694d3d1 100644 Binary files a/blender/terrain.blend1 and b/blender/terrain.blend1 differ diff --git a/blender/test_char.blend b/blender/test_char.blend new file mode 100644 index 0000000..5fdc06c Binary files /dev/null and b/blender/test_char.blend differ diff --git a/blender/test_char.blend1 b/blender/test_char.blend1 new file mode 100644 index 0000000..781d06c Binary files /dev/null and b/blender/test_char.blend1 differ diff --git a/build.rs b/build.rs index 98da93e..35e7b83 100644 --- a/build.rs +++ b/build.rs @@ -1,6 +1,88 @@ +use std::path::Path; +use std::process::Command; + fn main() { + compile_ink_stories(); + let wesl = wesl::Wesl::new("src/shaders"); wesl.build_artifact(&"package::main".parse().unwrap(), "main"); wesl.build_artifact(&"package::shadow".parse().unwrap(), "shadow"); } + +fn compile_ink_stories() +{ + let dialogs_dir = Path::new("assets/dialogs"); + + if !dialogs_dir.exists() + { + return; + } + + let inklecate = Path::new("tools/inklecate.dll"); + + if !inklecate.exists() + { + eprintln!("cargo:warning=tools/inklecate.dll not found — ink stories will not be compiled"); + return; + } + + let entries = match std::fs::read_dir(dialogs_dir) + { + Ok(e) => e, + Err(_) => return, + }; + + for entry in entries.flatten() + { + let path = entry.path(); + + if path.extension().and_then(|e| e.to_str()) != Some("ink") + { + continue; + } + + let ink_path = path.to_str().unwrap(); + let json_path = format!("{}.json", ink_path); + + println!("cargo:rerun-if-changed={ink_path}"); + + let ink_modified = std::fs::metadata(ink_path).and_then(|m| m.modified()).ok(); + let json_modified = std::fs::metadata(&json_path) + .and_then(|m| m.modified()) + .ok(); + + let needs_compile = match (ink_modified, json_modified) + { + (Some(ink_time), Some(json_time)) => ink_time > json_time, + _ => true, + }; + + if !needs_compile + { + continue; + } + + println!("cargo:warning=Compiling {ink_path}"); + + let status = Command::new("dotnet") + .args([inklecate.to_str().unwrap(), "-o", &json_path, ink_path]) + .status(); + + match status + { + Ok(s) if s.success() => + { + println!("cargo:warning=Compiled {ink_path} → {json_path}"); + } + Ok(s) => + { + println!("cargo:warning=inklecate exited with {s} for {ink_path}"); + } + Err(e) => + { + println!("cargo:warning=Failed to run dotnet inklecate: {e}"); + } + } + } +} diff --git a/src/bundles/mod.rs b/src/bundles/mod.rs index 8bd64c6..e22c92e 100644 --- a/src/bundles/mod.rs +++ b/src/bundles/mod.rs @@ -2,6 +2,7 @@ pub mod camera; pub mod player; pub mod spotlight; pub mod terrain; +pub mod test_char; use crate::entity::EntityHandle; use crate::world::World; diff --git a/src/bundles/player.rs b/src/bundles/player.rs index 94307fd..9bcd8db 100644 --- a/src/bundles/player.rs +++ b/src/bundles/player.rs @@ -7,17 +7,19 @@ use rapier3d::prelude::{ColliderBuilder, RigidBodyBuilder}; use crate::bundles::Bundle; use crate::components::lights::spot::SpotlightComponent; +use crate::components::player_states::{ + FallingState, IdleState, JumpingState, LeapingState, RollingState, WalkingState, +}; use crate::components::{ InputComponent, JumpComponent, MeshComponent, MovementComponent, PhysicsComponent, }; use crate::entity::EntityHandle; use crate::loaders::mesh::Mesh; +use crate::paths; use crate::physics::PhysicsManager; use crate::render::Pipeline; use crate::state::StateMachine; -use crate::systems::player_states::{ - PlayerFallingState, PlayerIdleState, PlayerJumpingState, PlayerWalkingState, -}; +use crate::systems::player_states::{LEAP_DURATION, ROLL_DURATION}; use crate::world::{Transform, World}; pub struct PlayerBundle @@ -47,28 +49,20 @@ impl Bundle for PlayerBundle let rigidbody_handle = PhysicsManager::add_rigidbody(rigidbody); let collider_handle = PhysicsManager::add_collider(collider, Some(rigidbody_handle)); - let mesh = Mesh::load_mesh("meshes/player_mesh.glb") + let mesh = Mesh::load_mesh(&paths::meshes::player()) .map_err(|e| format!("missing player mesh: {}", e))?; - let falling_state = PlayerFallingState { entity }; - let idle_state = PlayerIdleState { entity }; - let walking_state = PlayerWalkingState { - entity, - enter_time_stamp: 0.0, - }; - let jumping_state = PlayerJumpingState { - entity, - enter_time_stamp: 0.0, - }; - - let mut state_machine = StateMachine::new(Box::new(falling_state)); - state_machine.add_state(walking_state); - state_machine.add_state(idle_state); - state_machine.add_state(jumping_state); + let mut state_machine = StateMachine::new::(); + state_machine.register_state(|w: &mut World| &mut w.falling_states); + state_machine.register_state(|w: &mut World| &mut w.idle_states); + state_machine.register_state(|w: &mut World| &mut w.walking_states); + state_machine.register_state(|w: &mut World| &mut w.jumping_states); + state_machine.register_state(|w: &mut World| &mut w.leaping_states); + state_machine.register_state(|w: &mut World| &mut w.rolling_states); let entity_id = entity; - state_machine.add_transition::(move |world| { + state_machine.add_transition::(move |world| { let is_grounded = world .movements .with(entity_id, |m| m.movement_context.is_floored) @@ -80,7 +74,7 @@ impl Bundle for PlayerBundle is_grounded && !has_input }); - state_machine.add_transition::(move |world| { + state_machine.add_transition::(move |world| { let is_grounded = world .movements .with(entity_id, |m| m.movement_context.is_floored) @@ -92,7 +86,7 @@ impl Bundle for PlayerBundle is_grounded && has_input }); - state_machine.add_transition::(move |world| { + state_machine.add_transition::(move |world| { let is_grounded = world .movements .with(entity_id, |m| m.movement_context.is_floored) @@ -104,7 +98,7 @@ impl Bundle for PlayerBundle is_grounded && has_input }); - state_machine.add_transition::(move |world| { + state_machine.add_transition::(move |world| { let is_grounded = world .movements .with(entity_id, |m| m.movement_context.is_floored) @@ -116,23 +110,21 @@ impl Bundle for PlayerBundle is_grounded && !has_input }); - state_machine.add_transition::(move |world| { - let is_grounded = world + state_machine.add_transition::(move |world| { + !world .movements .with(entity_id, |m| m.movement_context.is_floored) - .unwrap_or(false); - !is_grounded + .unwrap_or(false) }); - state_machine.add_transition::(move |world| { - let is_grounded = world + state_machine.add_transition::(move |world| { + !world .movements .with(entity_id, |m| m.movement_context.is_floored) - .unwrap_or(false); - !is_grounded + .unwrap_or(false) }); - state_machine.add_transition::(move |world| { + state_machine.add_transition::(move |world| { let is_grounded = world .movements .with(entity_id, |m| m.movement_context.is_floored) @@ -144,7 +136,7 @@ impl Bundle for PlayerBundle is_grounded && jump_pressed }); - state_machine.add_transition::(move |world| { + state_machine.add_transition::(move |world| { let is_grounded = world .movements .with(entity_id, |m| m.movement_context.is_floored) @@ -156,15 +148,76 @@ impl Bundle for PlayerBundle is_grounded && jump_pressed }); - state_machine.add_transition::(move |world| { + state_machine.add_transition::(move |world| { + let time = world + .jumping_states + .with(entity_id, |s| s.time_in_state) + .unwrap_or(0.0); world .jumps - .with(entity_id, |jump| { - jump.jump_context.duration >= jump.jump_duration - }) + .with(entity_id, |j| time >= j.jump_duration) .unwrap_or(true) }); + state_machine.add_transition::(move |world| { + world + .inputs + .with(entity_id, |i| i.roll_just_pressed) + .unwrap_or(false) + }); + + state_machine.add_transition::(move |world| { + world + .inputs + .with(entity_id, |i| i.roll_just_pressed) + .unwrap_or(false) + }); + + state_machine.add_transition::(move |world| { + world + .leaping_states + .with(entity_id, |s| s.time_in_state >= LEAP_DURATION) + .unwrap_or(false) + }); + + state_machine.add_transition::(move |world| { + let leap_done = world + .leaping_states + .with(entity_id, |s| s.time_in_state >= LEAP_DURATION) + .unwrap_or(false); + let not_grounded = !world + .movements + .with(entity_id, |m| m.movement_context.is_floored) + .unwrap_or(true); + leap_done && not_grounded + }); + + state_machine.add_transition::(move |world| { + let done = world + .rolling_states + .with(entity_id, |s| s.time_in_state >= ROLL_DURATION) + .unwrap_or(false); + let grounded = world + .movements + .with(entity_id, |m| m.movement_context.is_floored) + .unwrap_or(false); + done && grounded + }); + + state_machine.add_transition::(move |world| { + let done = world + .rolling_states + .with(entity_id, |s| s.time_in_state >= ROLL_DURATION) + .unwrap_or(false); + let grounded = world + .movements + .with(entity_id, |m| m.movement_context.is_floored) + .unwrap_or(false); + done && !grounded + }); + + world.falling_states.insert(entity, FallingState::default()); + world.transforms.insert(entity, spawn_transform); world.movements.insert(entity, MovementComponent::new()); world.jumps.insert(entity, JumpComponent::default()); diff --git a/src/bundles/terrain.rs b/src/bundles/terrain.rs index d2afe0f..c68de92 100644 --- a/src/bundles/terrain.rs +++ b/src/bundles/terrain.rs @@ -8,6 +8,7 @@ use crate::components::{MeshComponent, PhysicsComponent, TreeInstancesComponent} use crate::entity::EntityHandle; use crate::loaders::mesh::{InstanceData, InstanceRaw, Mesh}; use crate::loaders::terrain::load_heightfield_from_exr; +use crate::paths; use crate::physics::PhysicsManager; use crate::render; use crate::world::{Transform, World}; @@ -33,8 +34,8 @@ impl TerrainConfig pub fn default() -> Self { Self { - gltf_path: "meshes/terrain.gltf".to_string(), - heightmap_path: "textures/terrain_heightmap.exr".to_string(), + gltf_path: paths::meshes::terrain(), + heightmap_path: paths::textures::terrain_heightmap(), size: Vec2::new(1000.0, 1000.0), } } diff --git a/src/bundles/test_char.rs b/src/bundles/test_char.rs new file mode 100644 index 0000000..813a912 --- /dev/null +++ b/src/bundles/test_char.rs @@ -0,0 +1,119 @@ +use std::rc::Rc; + +use glam::Vec3; +use rapier3d::control::{CharacterAutostep, KinematicCharacterController}; +use rapier3d::prelude::{ColliderBuilder, RigidBodyBuilder}; + +use crate::bundles::Bundle; +use crate::components::dialog::DialogSourceComponent; +use crate::components::player_states::{FallingState, IdleState, WalkingState}; +use crate::components::trigger::{TriggerComponent, TriggerFilter, TriggerShape, TriggerState}; +use crate::components::{MeshComponent, MovementComponent, PhysicsComponent}; +use crate::entity::EntityHandle; +use crate::loaders::mesh::Mesh; +use crate::paths; +use crate::physics::PhysicsManager; +use crate::render::Pipeline; +use crate::state::StateMachine; +use crate::world::{Transform, World}; + +pub struct TestCharBundle +{ + pub position: Vec3, +} + +impl Bundle for TestCharBundle +{ + fn spawn(self, world: &mut World) -> Result + { + let entity = world.spawn(); + + let spawn_transform = Transform::from_position(self.position); + + let rigidbody = RigidBodyBuilder::kinematic_position_based() + .translation(spawn_transform.position.into()) + .build(); + let collider = ColliderBuilder::capsule_y(0.5, 0.5).build(); + let _controller = KinematicCharacterController { + slide: true, + autostep: Some(CharacterAutostep::default()), + max_slope_climb_angle: 45.0, + ..Default::default() + }; + + let rigidbody_handle = PhysicsManager::add_rigidbody(rigidbody); + let collider_handle = PhysicsManager::add_collider(collider, Some(rigidbody_handle)); + + let mesh = Mesh::load_mesh(&paths::meshes::test_char()) + .map_err(|e| format!("missing test_char mesh: {}", e))?; + + let mut state_machine = StateMachine::new::(); + state_machine.register_state(|w: &mut World| &mut w.falling_states); + state_machine.register_state(|w: &mut World| &mut w.idle_states); + state_machine.register_state(|w: &mut World| &mut w.walking_states); + + let entity_id = entity; + + state_machine.add_transition::(move |world| { + world + .movements + .with(entity_id, |m| m.movement_context.is_floored) + .unwrap_or(false) + }); + + state_machine.add_transition::(move |world| { + !world + .movements + .with(entity_id, |m| m.movement_context.is_floored) + .unwrap_or(false) + }); + + state_machine.add_transition::(move |world| { + !world + .movements + .with(entity_id, |m| m.movement_context.is_floored) + .unwrap_or(false) + }); + + world.falling_states.insert(entity, FallingState::default()); + + world.transforms.insert(entity, spawn_transform); + world.movements.insert(entity, MovementComponent::new()); + world.physics.insert( + entity, + PhysicsComponent { + rigidbody: rigidbody_handle, + collider: Some(collider_handle), + }, + ); + world.meshes.insert( + entity, + MeshComponent { + mesh: Rc::new(mesh), + pipeline: Pipeline::Standard, + instance_buffer: None, + num_instances: 1, + tile_scale: 4.0, + enable_dissolve: false, + enable_snow_light: false, + }, + ); + world.state_machines.insert(entity, state_machine); + world.names.insert(entity, "TestChar".to_string()); + world.triggers.insert( + entity, + TriggerComponent { + shape: TriggerShape::Sphere { radius: 20.0 }, + filter: TriggerFilter::Player, + state: TriggerState::Idle, + }, + ); + + let ink_json = std::fs::read_to_string(&paths::dialogs::test_char()).unwrap_or_default(); + world + .dialog_sources + .insert(entity, DialogSourceComponent { ink_json }); + + Ok(entity) + } +} diff --git a/src/components/dialog.rs b/src/components/dialog.rs new file mode 100644 index 0000000..bfdfbcc --- /dev/null +++ b/src/components/dialog.rs @@ -0,0 +1,92 @@ +use bladeink::story::Story; + +use crate::entity::EntityHandle; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ParryButton +{ + I, + J, + L, +} + +impl ParryButton +{ + pub fn from_tag(tag: &str) -> Option + { + match tag.trim().to_lowercase().as_str() + { + "i" => Some(ParryButton::I), + "j" => Some(ParryButton::J), + "l" => Some(ParryButton::L), + _ => None, + } + } +} + +pub enum DialogPhase +{ + Displaying + { + timer: f32 + }, + ProjectileInFlight + { + projectile_entity: EntityHandle + }, +} + +pub struct DialogBubbleComponent +{ + pub story: Story, + pub character_entity: EntityHandle, + pub current_text: String, + pub phase: DialogPhase, + pub correct_parry: Option, + pub display_time: f32, +} + +pub struct DialogSourceComponent +{ + pub ink_json: String, +} + +pub struct DialogProjectileComponent +{ + pub bubble_entity: EntityHandle, + pub correct_parry: ParryButton, + pub parry_window_open: bool, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum DialogOutcome +{ + Hit, + Evaded, + WrongParry, + ParriedI, + ParriedJ, + ParriedL, +} + +impl DialogOutcome +{ + pub fn to_choice_tag(&self) -> &'static str + { + match self + { + DialogOutcome::Hit => "hit", + DialogOutcome::Evaded => "evaded", + DialogOutcome::WrongParry => "wrong_parry", + DialogOutcome::ParriedI => "parried_i", + DialogOutcome::ParriedJ => "parried_j", + DialogOutcome::ParriedL => "parried_l", + } + } +} + +pub struct DialogOutcomeEvent +{ + pub bubble_entity: EntityHandle, + pub outcome: DialogOutcome, +} diff --git a/src/components/input.rs b/src/components/input.rs index e481aee..07dbc9a 100644 --- a/src/components/input.rs +++ b/src/components/input.rs @@ -6,4 +6,8 @@ pub struct InputComponent pub move_direction: Vec3, pub jump_pressed: bool, pub jump_just_pressed: bool, + pub roll_just_pressed: bool, + pub parry_i_just_pressed: bool, + pub parry_j_just_pressed: bool, + pub parry_l_just_pressed: bool, } diff --git a/src/components/mod.rs b/src/components/mod.rs index 6469300..095e215 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,4 +1,5 @@ pub mod camera; +pub mod dialog; pub mod dissolve; pub mod follow; pub mod input; @@ -8,11 +9,16 @@ pub mod mesh; pub mod movement; pub mod noclip; pub mod physics; +pub mod player_states; pub mod rotate; pub mod tree_instances; pub mod trigger; pub use camera::CameraComponent; +pub use dialog::{ + DialogBubbleComponent, DialogOutcome, DialogOutcomeEvent, DialogPhase, + DialogProjectileComponent, DialogSourceComponent, ParryButton, +}; pub use dissolve::DissolveComponent; pub use follow::FollowComponent; pub use input::InputComponent; @@ -22,4 +28,6 @@ pub use movement::MovementComponent; pub use physics::PhysicsComponent; pub use rotate::RotateComponent; pub use tree_instances::TreeInstancesComponent; -pub use trigger::{TriggerComponent, TriggerEvent, TriggerEventKind, TriggerFilter, TriggerShape, TriggerState}; +pub use trigger::{ + TriggerComponent, TriggerEvent, TriggerEventKind, TriggerFilter, TriggerShape, TriggerState, +}; diff --git a/src/components/player_states.rs b/src/components/player_states.rs new file mode 100644 index 0000000..b341ebe --- /dev/null +++ b/src/components/player_states.rs @@ -0,0 +1,59 @@ +use glam::Vec3; + +#[derive(Default)] +pub struct IdleState +{ + pub time_in_state: f32, +} + +#[derive(Default)] +pub struct WalkingState +{ + pub time_in_state: f32, +} + +#[derive(Default)] +pub struct JumpingState +{ + pub time_in_state: f32, +} + +#[derive(Default)] +pub struct FallingState +{ + pub time_in_state: f32, +} + +pub struct LeapingState +{ + pub time_in_state: f32, + pub leap_direction: Vec3, +} + +impl Default for LeapingState +{ + fn default() -> Self + { + Self { + time_in_state: 0.0, + leap_direction: Vec3::Z, + } + } +} + +pub struct RollingState +{ + pub time_in_state: f32, + pub roll_direction: Vec3, +} + +impl Default for RollingState +{ + fn default() -> Self + { + Self { + time_in_state: 0.0, + roll_direction: Vec3::Z, + } + } +} diff --git a/src/components/trigger.rs b/src/components/trigger.rs index c9571c6..0d496ad 100644 --- a/src/components/trigger.rs +++ b/src/components/trigger.rs @@ -2,7 +2,10 @@ use crate::entity::EntityHandle; pub enum TriggerShape { - Sphere { radius: f32 }, + Sphere + { + radius: f32 + }, } pub enum TriggerFilter @@ -23,12 +26,14 @@ pub struct TriggerComponent pub state: TriggerState, } +#[derive(Clone, Copy, PartialEq, Eq)] pub enum TriggerEventKind { Entered, Exited, } +#[derive(Clone, Copy)] pub struct TriggerEvent { pub trigger_entity: EntityHandle, diff --git a/src/loaders/scene.rs b/src/loaders/scene.rs index d0e917b..3881e89 100644 --- a/src/loaders/scene.rs +++ b/src/loaders/scene.rs @@ -13,6 +13,7 @@ pub struct Space pub mesh_data: Vec<(Mesh, Vec)>, pub spotlights: Vec, pub player_spawn: Vec3, + pub test_char_spawn: Vec3, } impl Space @@ -27,18 +28,20 @@ impl Space let spotlights = lights.into_spotlights(); - let player_spawn = Self::get_player_spawn(gltf_path)?; + let player_spawn = Self::get_spawn(gltf_path, "PlayerSpawn")?; + let test_char_spawn = Self::get_spawn(gltf_path, "TestCharSpawn")?; Ok(Space { mesh_data, spotlights, player_spawn, + test_char_spawn, }) } - fn get_player_spawn(gltf_path: &str) -> Result + fn get_spawn(gltf_path: &str, name: &str) -> Result { - let empty = Empties::get_empty_by_name(gltf_path, "PlayerSpawn")?; + let empty = Empties::get_empty_by_name(gltf_path, name)?; if let Some(empty_node) = empty { @@ -48,7 +51,7 @@ impl Space } else { - println!("Warning: PlayerSpawn empty not found, using default position"); + println!("Warning: {} empty not found, using default position", name); Ok(Vec3::new(0.0, 5.0, 0.0)) } } diff --git a/src/main.rs b/src/main.rs index bbdca0b..3fa1b2b 100755 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod debug; mod editor; mod entity; mod loaders; +mod paths; mod physics; mod picking; mod postprocess; @@ -33,18 +34,21 @@ use crate::bundles::camera::CameraBundle; use crate::bundles::player::PlayerBundle; use crate::bundles::spotlight::spawn_spotlights; use crate::bundles::terrain::{TerrainBundle, TerrainConfig}; +use crate::bundles::test_char::TestCharBundle; use crate::bundles::Bundle; use crate::loaders::scene::Space; use crate::physics::PhysicsManager; use crate::snow::{SnowConfig, SnowLayer}; + +use crate::systems::camera::stop_camera_following; use crate::systems::{ - camera_follow_system, camera_input_system, camera_view_matrix, physics_sync_system, + camera_follow_system, camera_input_system, camera_view_matrix, dialog_bubble_render_system, + dialog_camera_system, dialog_projectile_system, dialog_system, physics_sync_system, player_input_system, render_system, rotate_system, snow_system, spotlight_sync_system, start_camera_following, state_machine_physics_system, state_machine_system, tree_dissolve_update_system, tree_instance_buffer_update_system, tree_occlusion_system, trigger_system, }; -use crate::systems::camera::stop_camera_following; use crate::utility::time::Time; fn main() -> Result<(), Box> @@ -68,7 +72,7 @@ fn main() -> Result<(), Box> }); editor.init_platform(&window); - let space = Space::load_space("meshes/terrain.gltf")?; + let space = Space::load_space(&crate::paths::meshes::terrain())?; let terrain_config = TerrainConfig::default(); let player_spawn = space.player_spawn; @@ -87,6 +91,13 @@ fn main() -> Result<(), Box> } .spawn(&mut world) .unwrap(); + + let _test_char_entity = TestCharBundle { + position: space.test_char_spawn, + } + .spawn(&mut world) + .unwrap(); + let _terrain_entity = TerrainBundle::spawn(&mut world, space.mesh_data, &terrain_config)?; spawn_spotlights(&mut world, space.spotlights); @@ -160,7 +171,7 @@ fn main() -> Result<(), Box> } Event::KeyDown { - keycode: Some(Keycode::I), + keycode: Some(Keycode::Tab), repeat: false, .. } => @@ -262,6 +273,11 @@ fn main() -> Result<(), Box> println!("Debug mode: {:?}", debug_mode); } + if input_state.f2_just_pressed + { + editor.show_player_state = !editor.show_player_state; + } + camera_input_system(&mut world, &input_state); if editor.active @@ -270,8 +286,20 @@ fn main() -> Result<(), Box> } else { - camera_follow_system(&mut world); + let dialog_active = !world.bubble_tags.all().is_empty(); + if dialog_active + { + dialog_camera_system(&mut world, delta); + } + else + { + camera_follow_system(&mut world); + } player_input_system(&mut world, &input_state); + if editor.show_player_state + { + editor.build_hud(&world); + } } let physics_start = Instant::now(); @@ -286,6 +314,8 @@ fn main() -> Result<(), Box> physics_sync_system(&mut world); trigger_system(&mut world); + dialog_system(&mut world, FIXED_TIMESTEP); + dialog_projectile_system(&mut world, &input_state); physics_accumulator -= FIXED_TIMESTEP; } @@ -322,6 +352,10 @@ fn main() -> Result<(), Box> if let Some(view) = camera_view_matrix(&world) { let projection = camera_component.projection_matrix(); + let view_proj = projection * view; + + let billboard_calls = + dialog_bubble_render_system(&world, camera_transform.position, view_proj); stats.draw_call_count = draw_calls.len(); stats.fps = 1.0 / delta; @@ -333,12 +367,13 @@ fn main() -> Result<(), Box> camera_transform.position, player_pos, &draw_calls, + &billboard_calls, time, delta, debug_mode, ); - if editor.active + if editor.active || editor.show_player_state { let screen_view = frame .texture diff --git a/src/paths.rs b/src/paths.rs new file mode 100644 index 0000000..cb5ee2a --- /dev/null +++ b/src/paths.rs @@ -0,0 +1,93 @@ +pub const TEXTURES_DIR: &str = "assets/textures"; +pub const MESHES_DIR: &str = "assets/meshes"; +pub const SHADERS_DIR: &str = "src/shaders"; +pub const ASSETS_DIR: &str = "assets"; + +pub mod textures +{ + use crate::paths::TEXTURES_DIR; + + pub fn snow_depth() -> String + { + format!("{}/snow_depth.exr", TEXTURES_DIR) + } + + pub fn terrain_heightmap() -> String + { + format!("{}/terrain_heightmap.exr", TEXTURES_DIR) + } + + pub fn terrain_flowmap() -> String + { + format!("{}/terrain_flowmap.exr", TEXTURES_DIR) + } + + pub fn blue_noise() -> String + { + format!("{}/blue_noise.png", TEXTURES_DIR) + } + + pub fn dither_octave(octave: u32) -> String + { + format!("{}/dither/octave_{}.png", TEXTURES_DIR, octave) + } +} + +pub mod meshes +{ + use crate::paths::MESHES_DIR; + + pub fn terrain() -> String + { + format!("{}/terrain.gltf", MESHES_DIR) + } + + pub fn test_char() -> String + { + format!("{}/test_char.gltf", MESHES_DIR) + } + + pub fn player() -> String + { + format!("{}/player_mesh.glb", MESHES_DIR) + } +} + +pub mod dialogs +{ + use crate::paths::ASSETS_DIR; + + pub fn test_char() -> String + { + format!("{}/dialogs/test_char.ink.json", ASSETS_DIR) + } +} + +pub mod shaders +{ + use crate::paths::SHADERS_DIR; + + pub fn blit() -> String + { + format!("{}/blit.wgsl", SHADERS_DIR) + } + + pub fn bubble() -> String + { + format!("{}/bubble.wgsl", SHADERS_DIR) + } + + pub fn debug_overlay() -> String + { + format!("{}/debug_overlay.wgsl", SHADERS_DIR) + } + + pub fn snow_deform() -> String + { + format!("{}/snow_deform.wgsl", SHADERS_DIR) + } + + pub const SHADOW_PACKAGE: &str = "package::shadow"; + pub const MAIN_PACKAGE: &str = "package::main"; + pub const SNOW_LIGHT_ACCUMULATION_PACKAGE: &str = "package::snow_light_accumulation"; +} diff --git a/src/postprocess.rs b/src/postprocess.rs index d94cbbc..6a4263a 100644 --- a/src/postprocess.rs +++ b/src/postprocess.rs @@ -1,3 +1,4 @@ +use crate::paths; use bytemuck::{Pod, Zeroable}; #[repr(C)] @@ -147,7 +148,7 @@ pub fn create_blit_pipeline( ) -> wgpu::RenderPipeline { let shader_source = - std::fs::read_to_string("src/shaders/blit.wgsl").expect("Failed to read blit shader"); + std::fs::read_to_string(&paths::shaders::blit()).expect("Failed to read blit shader"); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("Blit Shader"), diff --git a/src/render/billboard.rs b/src/render/billboard.rs new file mode 100644 index 0000000..38d1986 --- /dev/null +++ b/src/render/billboard.rs @@ -0,0 +1,276 @@ +use crate::paths; +use bytemuck::{Pod, Zeroable}; +use wgpu::util::DeviceExt; + +const MAX_BUBBLES: usize = 8; +const UNIFORM_STRIDE: usize = 256; + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct BillboardVertex +{ + pub position: [f32; 3], + pub uv: [f32; 2], +} + +impl BillboardVertex +{ + pub fn desc() -> wgpu::VertexBufferLayout<'static> + { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x3, + }, + wgpu::VertexAttribute { + offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, + shader_location: 1, + format: wgpu::VertexFormat::Float32x2, + }, + ], + } + } +} + +/// Uniform block layout must match `bubble.wgsl` exactly (including implicit WGSL padding). +/// +/// WGSL layout (offsets in bytes): +/// view_proj : mat4x4 → offset 0, size 64 +/// size : vec2 → offset 64, size 8 +/// body_frac : f32 → offset 72, size 4 +/// corner_r : f32 → offset 76, size 4 +/// border_w : f32 → offset 80, size 4 +/// [12 bytes WGSL padding to align vec4] +/// fill_color : vec4 → offset 96, size 16 +/// border_color : vec4 → offset 112, size 16 +/// total WGSL struct: 128 bytes +/// +/// Rust struct is padded to 256 bytes for the dynamic-offset alignment requirement. +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct BubbleUniforms +{ + pub view_proj: [[f32; 4]; 4], + pub size: [f32; 2], + pub body_frac: f32, + pub corner_r: f32, + pub border_w: f32, + pub _pad1: [f32; 3], + pub fill_color: [f32; 4], + pub border_color: [f32; 4], + pub _pad2: [f32; 32], +} + +const _: () = assert!(std::mem::size_of::() == UNIFORM_STRIDE); + +pub struct BillboardDrawCall +{ + /// Four corners in world space: top-left, top-right, bottom-right, bottom-left. + pub vertices: [BillboardVertex; 4], + pub uniforms: BubbleUniforms, +} + +pub struct BillboardPipeline +{ + pipeline: wgpu::RenderPipeline, + vertex_buffer: wgpu::Buffer, + index_buffer: wgpu::Buffer, + uniform_buffer: wgpu::Buffer, + bind_group: wgpu::BindGroup, +} + +impl BillboardPipeline +{ + pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self + { + let shader_source = + std::fs::read_to_string(&paths::shaders::bubble()).expect("Failed to read bubble.wgsl"); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Bubble Shader"), + source: wgpu::ShaderSource::Wgsl(shader_source.into()), + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Billboard Bind Group Layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: std::num::NonZeroU64::new( + std::mem::size_of::() as u64, + ), + }, + count: None, + }], + }); + + let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Billboard Uniform Buffer"), + size: (MAX_BUBBLES * UNIFORM_STRIDE) as wgpu::BufferAddress, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Billboard Bind Group"), + layout: &bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &uniform_buffer, + offset: 0, + size: std::num::NonZeroU64::new(std::mem::size_of::() as u64), + }), + }], + }); + + let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Billboard Vertex Buffer"), + size: (MAX_BUBBLES * 4 * std::mem::size_of::()) as wgpu::BufferAddress, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let indices: [u16; 6] = [0, 1, 2, 2, 3, 0]; + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Billboard Index Buffer"), + contents: bytemuck::cast_slice(&indices), + usage: wgpu::BufferUsages::INDEX, + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Billboard Pipeline Layout"), + bind_group_layouts: &[&bind_group_layout], + immediate_size: 0, + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Billboard Pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[BillboardVertex::desc()], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: false, + depth_compare: wgpu::CompareFunction::LessEqual, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview_mask: None, + cache: None, + }); + + Self { + pipeline, + vertex_buffer, + index_buffer, + uniform_buffer, + bind_group, + } + } + + pub fn render( + &self, + encoder: &mut wgpu::CommandEncoder, + queue: &wgpu::Queue, + color_view: &wgpu::TextureView, + depth_view: &wgpu::TextureView, + calls: &[BillboardDrawCall], + ) + { + if calls.is_empty() + { + return; + } + + let n = calls.len().min(MAX_BUBBLES); + + let mut all_vertices: Vec = Vec::with_capacity(n * 4); + for call in calls.iter().take(n) + { + all_vertices.extend_from_slice(&call.vertices); + } + queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&all_vertices)); + + for (i, call) in calls.iter().take(n).enumerate() + { + queue.write_buffer( + &self.uniform_buffer, + (i * UNIFORM_STRIDE) as u64, + bytemuck::cast_slice(&[call.uniforms]), + ); + } + + { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Billboard Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: color_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + multiview_mask: None, + }); + + pass.set_pipeline(&self.pipeline); + pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16); + + for i in 0..n + { + let dynamic_offset = (i * UNIFORM_STRIDE) as u32; + pass.set_bind_group(0, &self.bind_group, &[dynamic_offset]); + pass.draw_indexed(0..6, (i * 4) as i32, 0..1); + } + } + } +} diff --git a/src/render/debug_overlay.rs b/src/render/debug_overlay.rs index 4f8adf6..9e3993e 100644 --- a/src/render/debug_overlay.rs +++ b/src/render/debug_overlay.rs @@ -1,3 +1,4 @@ +use crate::paths; use crate::postprocess::ScreenVertex; pub struct DebugOverlay @@ -12,7 +13,7 @@ impl DebugOverlay { pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { - let shader_source = std::fs::read_to_string("src/shaders/debug_overlay.wgsl") + let shader_source = std::fs::read_to_string(&paths::shaders::debug_overlay()) .expect("Failed to read debug_overlay.wgsl"); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { diff --git a/src/render/mod.rs b/src/render/mod.rs index 635c9fc..b3943e9 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1,14 +1,17 @@ +pub mod billboard; mod bind_group; mod debug_overlay; mod pipeline; mod shadow; mod types; +pub use billboard::{BillboardDrawCall, BillboardPipeline}; pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS}; use crate::entity::EntityHandle; use crate::debug::DebugMode; +use crate::paths; use crate::postprocess::{create_blit_pipeline, create_fullscreen_quad, LowResFramebuffer}; use crate::texture::{DitherTextures, FlowmapTexture}; use pipeline::{ @@ -34,6 +37,7 @@ pub struct Renderer wireframe_pipeline: Option, debug_lines_pipeline: Option, debug_overlay: Option, + billboard_pipeline: BillboardPipeline, wireframe_supported: bool, uniform_buffer: wgpu::Buffer, @@ -162,7 +166,7 @@ impl Renderer }; let flowmap_texture = - match FlowmapTexture::load(&device, &queue, "textures/terrain_flowmap.exr") + match FlowmapTexture::load(&device, &queue, &paths::textures::terrain_flowmap()) { Ok(texture) => { @@ -179,7 +183,7 @@ impl Renderer } }; - let blue_noise_data = image::open("textures/blue_noise.png") + let blue_noise_data = image::open(&paths::textures::blue_noise()) .expect("Failed to load blue noise texture") .to_luma8(); let blue_noise_size = blue_noise_data.dimensions(); @@ -490,6 +494,8 @@ impl Renderer &bind_group_layout, )); + let billboard_pipeline = BillboardPipeline::new(&device, config.format); + let debug_overlay = Some(debug_overlay::DebugOverlay::new(&device, config.format)); let shadow_bind_group_layout = @@ -518,6 +524,7 @@ impl Renderer wireframe_pipeline, debug_lines_pipeline, debug_overlay, + billboard_pipeline, wireframe_supported, uniform_buffer, bind_group_layout, @@ -561,6 +568,7 @@ impl Renderer camera_position: glam::Vec3, player_position: glam::Vec3, draw_calls: &[DrawCall], + billboard_calls: &[BillboardDrawCall], time: f32, delta_time: f32, debug_mode: DebugMode, @@ -939,6 +947,14 @@ impl Renderer } } + self.billboard_pipeline.render( + &mut encoder, + &self.queue, + &self.framebuffer.view, + &self.framebuffer.depth_view, + billboard_calls, + ); + self.queue.submit(std::iter::once(encoder.finish())); let frame = match self.surface.get_current_texture() @@ -1049,7 +1065,7 @@ impl Renderer match crate::texture::HeightmapTexture::load( &self.device, &self.queue, - "textures/terrain_heightmap.exr", + &paths::textures::terrain_heightmap(), ) { Ok(heightmap) => @@ -1151,6 +1167,7 @@ pub fn render( camera_position: glam::Vec3, player_position: glam::Vec3, draw_calls: &[DrawCall], + billboard_calls: &[BillboardDrawCall], time: f32, delta_time: f32, debug_mode: DebugMode, @@ -1165,6 +1182,7 @@ pub fn render( camera_position, player_position, draw_calls, + billboard_calls, time, delta_time, debug_mode, diff --git a/src/render/pipeline.rs b/src/render/pipeline.rs index 42ec4f4..19e9c55 100644 --- a/src/render/pipeline.rs +++ b/src/render/pipeline.rs @@ -1,3 +1,4 @@ +use crate::paths; use wesl::Wesl; pub fn create_shadow_pipeline( @@ -5,9 +6,9 @@ pub fn create_shadow_pipeline( bind_group_layout: &wgpu::BindGroupLayout, ) -> wgpu::RenderPipeline { - let compiler = Wesl::new("src/shaders"); + let compiler = Wesl::new(&paths::SHADERS_DIR); let shader_source = compiler - .compile(&"package::shadow".parse().unwrap()) + .compile(&paths::shaders::SHADOW_PACKAGE.parse().unwrap()) .inspect_err(|e| eprintln!("WESL error: {e}")) .unwrap() .to_string(); @@ -70,9 +71,9 @@ pub fn create_main_pipeline( label: &str, ) -> wgpu::RenderPipeline { - let compiler = Wesl::new("src/shaders"); + let compiler = Wesl::new(&paths::SHADERS_DIR); let shader_source = compiler - .compile(&"package::main".parse().unwrap()) + .compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap()) .inspect_err(|e| eprintln!("WESL error: {e}")) .unwrap() .to_string(); @@ -142,9 +143,9 @@ pub fn create_wireframe_pipeline( bind_group_layout: &wgpu::BindGroupLayout, ) -> wgpu::RenderPipeline { - let compiler = Wesl::new("src/shaders"); + let compiler = Wesl::new(&paths::SHADERS_DIR); let shader_source = compiler - .compile(&"package::main".parse().unwrap()) + .compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap()) .inspect_err(|e| eprintln!("WESL error: {e}")) .unwrap() .to_string(); @@ -214,9 +215,9 @@ pub fn create_debug_lines_pipeline( bind_group_layout: &wgpu::BindGroupLayout, ) -> wgpu::RenderPipeline { - let compiler = Wesl::new("src/shaders"); + let compiler = Wesl::new(&paths::SHADERS_DIR); let shader_source = compiler - .compile(&"package::main".parse().unwrap()) + .compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap()) .inspect_err(|e| eprintln!("WESL error: {e}")) .unwrap() .to_string(); @@ -286,9 +287,9 @@ pub fn create_snow_clipmap_pipeline( main_bind_group_layout: &wgpu::BindGroupLayout, ) -> wgpu::RenderPipeline { - let compiler = Wesl::new("src/shaders"); + let compiler = Wesl::new(&paths::SHADERS_DIR); let shader_source = compiler - .compile(&"package::main".parse().unwrap()) + .compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap()) .inspect_err(|e| eprintln!("WESL error: {e}")) .unwrap() .to_string(); diff --git a/src/shaders/bubble.wgsl b/src/shaders/bubble.wgsl new file mode 100644 index 0000000..4b69a05 --- /dev/null +++ b/src/shaders/bubble.wgsl @@ -0,0 +1,118 @@ +struct Uniforms +{ + view_proj: mat4x4, + size: vec2, + body_frac: f32, + corner_r: f32, + border_w: f32, + fill_color: vec4, + border_color: vec4, +} + +@group(0) @binding(0) +var u: Uniforms; + +struct VertexIn +{ + @location(0) position: vec3, + @location(1) uv: vec2, +} + +struct VertexOut +{ + @builtin(position) clip_pos: vec4, + @location(0) uv: vec2, +} + +@vertex +fn vs_main(in: VertexIn) -> VertexOut +{ + var out: VertexOut; + out.clip_pos = u.view_proj * vec4(in.position, 1.0); + out.uv = in.uv; + return out; +} + +fn sdf_rounded_box(p: vec2, b: vec2, r: f32) -> f32 +{ + let q = abs(p) - b + r; + return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - r; +} + +fn sdf_triangle(p: vec2, a: vec2, b: vec2, c: vec2) -> f32 +{ + let e0 = b - a; + let e1 = c - b; + let e2 = a - c; + let v0 = p - a; + let v1 = p - b; + let v2 = p - c; + let pq0 = v0 - e0 * clamp(dot(v0, e0) / dot(e0, e0), 0.0, 1.0); + let pq1 = v1 - e1 * clamp(dot(v1, e1) / dot(e1, e1), 0.0, 1.0); + let pq2 = v2 - e2 * clamp(dot(v2, e2) / dot(e2, e2), 0.0, 1.0); + let s = sign(e0.x * e2.y - e0.y * e2.x); + let d = min( + min( + vec2(dot(pq0, pq0), s * (v0.x * e0.y - v0.y * e0.x)), + vec2(dot(pq1, pq1), s * (v1.x * e1.y - v1.y * e1.x)), + ), + vec2(dot(pq2, pq2), s * (v2.x * e2.y - v2.y * e2.x)), + ); + return -sqrt(d.x) * sign(d.y); +} + +var bayer: array = array( + 0.0, 8.0, 2.0, 10.0, + 12.0, 4.0, 14.0, 6.0, + 3.0, 11.0, 1.0, 9.0, + 15.0, 7.0, 13.0, 5.0, +); + +@fragment +fn fs_main(in: VertexOut) -> @location(0) vec4 +{ + let body_half_w = u.size.x * 0.5; + let body_half_h = u.size.y * u.body_frac * 0.5; + + // World-unit coords centred at body centre (y+ = up in billboard local space). + let p = vec2( + (in.uv.x - 0.5) * u.size.x, + (u.body_frac * 0.5 - in.uv.y) * u.size.y, + ); + + // Body rounded rect. + let body_half = vec2(body_half_w, body_half_h); + let corner_r = u.corner_r * min(body_half_w, body_half_h); + let d_body = sdf_rounded_box(p, body_half, corner_r); + + // Tail triangle pointing downward (toward character). + let tail_height = u.size.y * (1.0 - u.body_frac); + let tail_base_half = body_half_w * 0.15; + let ta = vec2(-tail_base_half, -body_half_h); + let tb = vec2( tail_base_half, -body_half_h); + let tc = vec2(0.0, -(body_half_h + tail_height)); + let d_tail = sdf_triangle(p, ta, tb, tc); + + let d_shape = min(d_body, d_tail); + + // Alpha with a tiny anti-alias band, then Bayer threshold. + let aa = 0.01 * min(body_half_w, body_half_h); + let alpha = 1.0 - smoothstep(-aa, aa, d_shape); + let fx = u32(in.clip_pos.x) % 4u; + let fy = u32(in.clip_pos.y) % 4u; + let thresh = bayer[fy * 4u + fx] / 16.0; + if alpha <= thresh + { + discard; + } + + // Border ring on body only. + let border_w = u.border_w * min(body_half_w, body_half_h); + let inner_r = max(corner_r - border_w, 0.0); + let d_inner = sdf_rounded_box(p, body_half - border_w, inner_r); + if d_body <= 0.0 && d_inner > 0.0 + { + return u.border_color; + } + return u.fill_color; +} diff --git a/src/snow.rs b/src/snow.rs index d26b20d..dcbf7db 100644 --- a/src/snow.rs +++ b/src/snow.rs @@ -6,6 +6,7 @@ use wgpu::util::DeviceExt; use crate::{ loaders::mesh::{InstanceRaw, Mesh, Vertex}, + paths, render::{self, DrawCall, Pipeline}, texture::HeightmapTexture, }; @@ -23,8 +24,8 @@ impl SnowConfig pub fn default() -> Self { Self { - depth_map_path: "textures/snow_depth.exr".to_string(), - heightmap_path: "textures/terrain_heightmap.exr".to_string(), + depth_map_path: paths::textures::snow_depth(), + heightmap_path: paths::textures::terrain_heightmap(), terrain_size: Vec2::new(1000.0, 1000.0), resolution: (1000, 1000), } @@ -300,7 +301,7 @@ impl SnowLayer ) -> (wgpu::ComputePipeline, wgpu::BindGroup, wgpu::Buffer) { render::with_device(|device| { - let shader_source = std::fs::read_to_string("src/shaders/snow_deform.wgsl") + let shader_source = std::fs::read_to_string(&paths::shaders::snow_deform()) .expect("Failed to load snow deform shader"); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { diff --git a/src/snow_light.rs b/src/snow_light.rs index 33799de..bf256a9 100644 --- a/src/snow_light.rs +++ b/src/snow_light.rs @@ -1,3 +1,4 @@ +use crate::paths; use bytemuck::{Pod, Zeroable}; use glam::Vec2; use wgpu::util::DeviceExt; @@ -170,9 +171,13 @@ impl SnowLightAccumulation ], }); - let compiler = wesl::Wesl::new("src/shaders"); + let compiler = wesl::Wesl::new(&paths::SHADERS_DIR); let shader_source = compiler - .compile(&"package::snow_light_accumulation".parse().unwrap()) + .compile( + &paths::shaders::SNOW_LIGHT_ACCUMULATION_PACKAGE + .parse() + .unwrap(), + ) .inspect_err(|e| eprintln!("WESL error: {e}")) .unwrap() .to_string(); diff --git a/src/state.rs b/src/state.rs index bfb947c..2db2ac7 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,28 +1,30 @@ -use std::any::{Any, TypeId}; +use std::any::TypeId; use std::collections::HashMap; -use crate::world::World; +use crate::entity::EntityHandle; +use crate::world::{Storage, World}; -pub trait StateAgent {} - -pub trait State: Any +pub trait PlayerState { - fn get_state_name(&self) -> &'static str; - fn on_state_enter(&mut self, world: &mut World) {} - fn on_state_exit(&mut self, world: &mut World) {} - fn on_state_update(&mut self, world: &mut World, delta: f32) {} - fn on_state_physics_update(&mut self, world: &mut World, delta: f32) {} + fn tick_time(&mut self, _delta: f32) {} + fn on_enter(&mut self, _world: &mut World, _entity: EntityHandle) {} + fn on_exit(&mut self, _world: &mut World, _entity: EntityHandle) {} + fn on_update(&mut self, _world: &mut World, _entity: EntityHandle, _delta: f32) {} + fn on_physics_update(&mut self, _world: &mut World, _entity: EntityHandle, _delta: f32) {} } -impl dyn State +struct StateOps { - fn dyn_type_id(&self) -> std::any::TypeId - { - Any::type_id(self) - } + name: &'static str, + remove: Box, + insert_default: Box, + on_enter: Box, + on_exit: Box, + tick: Box, + physics_tick: Box, } -pub struct StateTransition +struct StateTransition { to_state_id: TypeId, condition: Box bool>, @@ -30,42 +32,93 @@ pub struct StateTransition pub struct StateMachine { + state_ops: HashMap, state_transitions: HashMap>, current_state_id: TypeId, - states: HashMap>, - pub time_in_state: f32, +} + +fn short_type_name() -> &'static str +{ + let full = std::any::type_name::(); + full.rsplit("::").next().unwrap_or(full) } impl StateMachine { - pub fn new(enter_state: Box) -> Self + pub fn new() -> Self { - let state_id = enter_state.dyn_type_id(); - let mut states = HashMap::new(); - states.insert(state_id, enter_state); - Self { + state_ops: HashMap::new(), state_transitions: HashMap::new(), - current_state_id: state_id, - states, - time_in_state: 0.0, + current_state_id: TypeId::of::(), } } - pub fn update(&mut self, world: &mut World, delta: f32) + pub fn register_state( + &mut self, + storage: fn(&mut World) -> &mut Storage, + ) + { + let ops = StateOps { + name: short_type_name::(), + remove: Box::new(move |world, entity| { + storage(world).components.remove(&entity); + }), + insert_default: Box::new(move |world, entity| { + storage(world).insert(entity, S::default()); + }), + on_enter: Box::new(move |world, entity| { + if let Some(mut state) = storage(world).components.remove(&entity) + { + state.on_enter(world, entity); + storage(world).insert(entity, state); + } + }), + on_exit: Box::new(move |world, entity| { + if let Some(mut state) = storage(world).components.remove(&entity) + { + state.on_exit(world, entity); + storage(world).insert(entity, state); + } + }), + tick: Box::new(move |world, entity, delta| { + if let Some(mut state) = storage(world).components.remove(&entity) + { + state.tick_time(delta); + state.on_update(world, entity, delta); + storage(world).insert(entity, state); + } + }), + physics_tick: Box::new(move |world, entity, delta| { + if let Some(mut state) = storage(world).components.remove(&entity) + { + state.on_physics_update(world, entity, delta); + storage(world).insert(entity, state); + } + }), + }; + self.state_ops.insert(TypeId::of::(), ops); + } + + pub fn update(&mut self, world: &mut World, entity: EntityHandle, delta: f32) { if let Some(next_state_id) = self.get_transition_state_id(world) { - self.time_in_state = 0.0; - self.transition_to(world, next_state_id); + self.apply_transition(world, entity, next_state_id); } - if let Some(current_state) = self.states.get_mut(&self.current_state_id) + if let Some(ops) = self.state_ops.get(&self.current_state_id) { - current_state.on_state_update(world, delta); + (ops.tick)(world, entity, delta); } + } - self.time_in_state += delta; + pub fn physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32) + { + if let Some(ops) = self.state_ops.get(&self.current_state_id) + { + (ops.physics_tick)(world, entity, delta); + } } fn get_transition_state_id(&self, world: &World) -> Option @@ -83,47 +136,30 @@ impl StateMachine None } - fn transition_to(&mut self, world: &mut World, new_state_id: TypeId) + fn apply_transition(&mut self, world: &mut World, entity: EntityHandle, next_id: TypeId) { - if let Some(current_state) = self.states.get_mut(&self.current_state_id) + if let Some(ops) = self.state_ops.get(&self.current_state_id) { - current_state.on_state_exit(world); + (ops.on_exit)(world, entity); + (ops.remove)(world, entity); } - self.current_state_id = new_state_id; + self.current_state_id = next_id; - if let Some(new_state) = self.states.get_mut(&self.current_state_id) + if let Some(ops) = self.state_ops.get(&next_id) { - new_state.on_state_enter(world); + (ops.insert_default)(world, entity); + (ops.on_enter)(world, entity); } } - pub fn get_current_state(&self) -> Option<&dyn State> - { - self.states.get(&self.current_state_id).map(|b| b.as_ref()) - } - - pub fn get_current_state_mut(&mut self) -> Option<&mut dyn State> - { - self.states - .get_mut(&self.current_state_id) - .map(|b| b.as_mut()) - } - - pub fn add_state(&mut self, state: T) - { - let state_id = TypeId::of::(); - self.states.insert(state_id, Box::new(state)); - } - - pub fn add_transition( + pub fn add_transition( &mut self, condition: impl Fn(&World) -> bool + 'static, ) { let from_id = TypeId::of::(); let to_id = TypeId::of::(); - let transitions = self.state_transitions.entry(from_id).or_default(); transitions.push(StateTransition { to_state_id: to_id, @@ -131,11 +167,11 @@ impl StateMachine }); } - pub fn get_available_transitions_count(&self) -> usize + pub fn get_current_state_name(&self) -> &'static str { - self.state_transitions + self.state_ops .get(&self.current_state_id) - .map(|transitions| transitions.len()) - .unwrap_or(0) + .map(|ops| ops.name) + .unwrap_or("Unknown") } } diff --git a/src/systems/dialog.rs b/src/systems/dialog.rs new file mode 100644 index 0000000..a058941 --- /dev/null +++ b/src/systems/dialog.rs @@ -0,0 +1,346 @@ +use bladeink::story::Story; +use glam::Vec3; + +use crate::components::dialog::{ + DialogBubbleComponent, DialogPhase, DialogProjectileComponent, ParryButton, +}; +use crate::components::trigger::TriggerEventKind; +use crate::entity::EntityHandle; +use crate::world::{Transform, World}; + +const DEFAULT_DISPLAY_TIME: f32 = 3.0; +const PARRY_TAG_PREFIX: &str = "parry:"; +const TIMER_TAG_PREFIX: &str = "timer:"; + +pub fn dialog_system(world: &mut World, delta: f32) +{ + process_trigger_events(world); + tick_displaying_bubbles(world, delta); + process_outcomes(world); +} + +fn process_trigger_events(world: &mut World) +{ + let events: Vec<_> = world.trigger_events.iter().cloned().collect(); + + for event in events + { + let has_source = world.dialog_sources.get(event.trigger_entity).is_some(); + if !has_source + { + continue; + } + + match event.kind + { + TriggerEventKind::Entered => + { + let already_active = world.bubble_tags.all().iter().any(|&b| { + world + .dialog_bubbles + .with(b, |db| db.character_entity == event.trigger_entity) + .unwrap_or(false) + }); + + if already_active + { + continue; + } + + spawn_bubble(world, event.trigger_entity); + } + + TriggerEventKind::Exited => + { + despawn_bubbles_for_character(world, event.trigger_entity); + } + } + } +} + +fn spawn_bubble(world: &mut World, character_entity: EntityHandle) +{ + let ink_json = match world + .dialog_sources + .with(character_entity, |s| s.ink_json.clone()) + { + Some(json) => json, + None => return, + }; + + let mut story = match Story::new(&ink_json) + { + Ok(s) => s, + Err(e) => + { + eprintln!("Failed to load ink story: {e}"); + return; + } + }; + + let (text, parry, display_time) = advance_story(&mut story); + + let character_pos = world + .transforms + .with(character_entity, |t| t.position) + .unwrap_or(Vec3::ZERO); + + let bubble_entity = world.spawn(); + world.transforms.insert( + bubble_entity, + Transform::from_position(character_pos + Vec3::new(0.0, 2.5, 0.0)), + ); + world + .names + .insert(bubble_entity, "DialogBubble".to_string()); + world.bubble_tags.insert(bubble_entity, ()); + world.dialog_bubbles.insert( + bubble_entity, + DialogBubbleComponent { + story, + character_entity, + current_text: text, + phase: DialogPhase::Displaying { + timer: display_time, + }, + correct_parry: parry, + display_time, + }, + ); + + println!("Dialog bubble spawned for character {character_entity}"); +} + +fn despawn_bubbles_for_character(world: &mut World, character_entity: EntityHandle) +{ + let bubbles: Vec = world.bubble_tags.all(); + + for bubble_entity in bubbles + { + let matches = world + .dialog_bubbles + .with(bubble_entity, |b| b.character_entity == character_entity) + .unwrap_or(false); + + if !matches + { + continue; + } + + if let Some(bubble) = world.dialog_bubbles.get(bubble_entity) + { + if let DialogPhase::ProjectileInFlight { projectile_entity } = bubble.phase + { + world.despawn(projectile_entity); + } + } + + world.despawn(bubble_entity); + println!("Dialog bubble despawned for character {character_entity}"); + } +} + +fn tick_displaying_bubbles(world: &mut World, delta: f32) +{ + let bubbles: Vec = world.bubble_tags.all(); + + for bubble_entity in bubbles + { + let character_entity = match world.dialog_bubbles.with(bubble_entity, |b| { + if matches!(b.phase, DialogPhase::Displaying { .. }) + { + Some(b.character_entity) + } + else + { + None + } + }) + { + Some(Some(e)) => e, + _ => continue, + }; + + let expired = world + .dialog_bubbles + .with_mut(bubble_entity, |b| { + if let DialogPhase::Displaying { ref mut timer } = b.phase + { + *timer -= delta; + *timer <= 0.0 + } + else + { + false + } + }) + .unwrap_or(false); + + if expired + { + let correct_parry = match world + .dialog_bubbles + .with(bubble_entity, |b| b.correct_parry) + { + Some(Some(p)) => p, + _ => + { + world.despawn(bubble_entity); + continue; + } + }; + + let character_pos = world + .transforms + .with(character_entity, |t| t.position) + .unwrap_or(Vec3::ZERO); + + let projectile_entity = world.spawn(); + world.transforms.insert( + projectile_entity, + Transform::from_position(character_pos + Vec3::new(0.0, 1.5, 0.0)), + ); + world + .names + .insert(projectile_entity, "DialogProjectile".to_string()); + world.projectile_tags.insert(projectile_entity, ()); + world.dialog_projectiles.insert( + projectile_entity, + DialogProjectileComponent { + bubble_entity, + correct_parry, + parry_window_open: false, + }, + ); + + world.dialog_bubbles.with_mut(bubble_entity, |b| { + b.phase = DialogPhase::ProjectileInFlight { projectile_entity }; + }); + + println!( + "Dialog projectile spawned, correct parry: {:?}", + correct_parry + ); + } + } +} + +fn process_outcomes(world: &mut World) +{ + let outcomes: Vec<_> = world.dialog_outcomes.drain(..).collect(); + + for event in outcomes + { + let bubble_entity = event.bubble_entity; + + if world.dialog_bubbles.get(bubble_entity).is_none() + { + continue; + } + + let choice_tag = event.outcome.to_choice_tag(); + + let next = world.dialog_bubbles.with_mut(bubble_entity, |b| { + let choices = b.story.get_current_choices(); + let idx = choices + .iter() + .position(|c| c.tags.iter().any(|t| t.trim() == choice_tag)); + + if let Some(idx) = idx + { + let _ = b.story.choose_choice_index(idx); + } + else + { + println!("No choice found for outcome tag '{choice_tag}', using first available"); + if !choices.is_empty() + { + let _ = b.story.choose_choice_index(0); + } + } + + if b.story.can_continue() + { + let (text, parry, display_time) = advance_story(&mut b.story); + Some((text, parry, display_time)) + } + else + { + None + } + }); + + match next + { + Some(Some((text, parry, display_time))) => + { + world.dialog_bubbles.with_mut(bubble_entity, |b| { + b.current_text = text; + b.correct_parry = parry; + b.display_time = display_time; + b.phase = DialogPhase::Displaying { + timer: display_time, + }; + }); + println!( + "Dialog advanced: '{}'", + world + .dialog_bubbles + .with(bubble_entity, |b| b.current_text.clone()) + .unwrap_or_default() + ); + } + + Some(None) => + { + world.despawn(bubble_entity); + println!("Dialog story finished"); + } + + None => + {} + } + } +} + +fn advance_story(story: &mut Story) -> (String, Option, f32) +{ + let mut full_text = String::new(); + let mut parry: Option = None; + let mut display_time = DEFAULT_DISPLAY_TIME; + + while story.can_continue() + { + match story.cont() + { + Ok(line) => full_text.push_str(&line), + Err(e) => + { + eprintln!("Story error: {e}"); + break; + } + } + + if let Ok(tags) = story.get_current_tags() + { + for tag in tags + { + let tag = tag.trim().to_string(); + let tag = tag.as_str(); + if let Some(val) = tag.strip_prefix(PARRY_TAG_PREFIX) + { + parry = ParryButton::from_tag(val.trim()); + } + else if let Some(val) = tag.strip_prefix(TIMER_TAG_PREFIX) + { + if let Ok(t) = val.trim().parse::() + { + display_time = t; + } + } + } + } + } + + (full_text.trim().to_string(), parry, display_time) +} diff --git a/src/systems/dialog_camera.rs b/src/systems/dialog_camera.rs new file mode 100644 index 0000000..009cbc8 --- /dev/null +++ b/src/systems/dialog_camera.rs @@ -0,0 +1,74 @@ +use glam::Vec3; + +use crate::world::World; + +const CAMERA_LAG: f32 = 4.0; +const VERTICAL_BIAS: f32 = 0.4; +const MIN_DISTANCE: f32 = 8.0; +const MAX_DISTANCE: f32 = 24.0; + +pub fn dialog_camera_system(world: &mut World, delta: f32) +{ + let Some((camera_entity, _)) = world.active_camera() + else + { + return; + }; + + let player_pos = world.player_position(); + + let character_positions: Vec = world + .bubble_tags + .all() + .iter() + .filter_map(|&bubble| { + let char_entity = world.dialog_bubbles.with(bubble, |b| b.character_entity)?; + world.transforms.with(char_entity, |t| t.position) + }) + .collect(); + + if character_positions.is_empty() + { + return; + } + + let all_positions: Vec = std::iter::once(player_pos) + .chain(character_positions.iter().copied()) + .collect(); + + let centroid = + all_positions.iter().copied().fold(Vec3::ZERO, |a, b| a + b) / all_positions.len() as f32; + + let max_spread = all_positions + .iter() + .map(|p| (*p - centroid).length()) + .fold(0.0f32, f32::max); + + let camera_distance = (max_spread * 1.8 + MIN_DISTANCE).min(MAX_DISTANCE); + + let to_player = (player_pos - centroid).normalize_or(Vec3::Z); + let camera_back_dir = Vec3::new(to_player.x, 0.0, to_player.z).normalize_or(Vec3::Z); + + let target_camera_pos = + centroid + camera_back_dir * camera_distance + Vec3::Y * camera_distance * VERTICAL_BIAS; + + let current_camera_pos = world + .transforms + .with(camera_entity, |t| t.position) + .unwrap_or(target_camera_pos); + + let smoothed = current_camera_pos.lerp(target_camera_pos, (CAMERA_LAG * delta).min(1.0)); + + world.transforms.with_mut(camera_entity, |t| { + t.position = smoothed; + }); + + let look_target = centroid + Vec3::Y * 1.0; + + if let Some(camera) = world.cameras.get_mut(camera_entity) + { + let look_dir = (look_target - smoothed).normalize_or(-Vec3::Z); + camera.yaw = look_dir.z.atan2(look_dir.x); + camera.pitch = look_dir.y.asin(); + } +} diff --git a/src/systems/dialog_projectile.rs b/src/systems/dialog_projectile.rs new file mode 100644 index 0000000..eb87080 --- /dev/null +++ b/src/systems/dialog_projectile.rs @@ -0,0 +1,182 @@ +use glam::Vec3; + +use crate::components::dialog::{DialogOutcome, DialogOutcomeEvent, ParryButton}; +use crate::entity::EntityHandle; + +use crate::utility::input::InputState; +use crate::world::World; + +const PROJECTILE_SPEED: f32 = 6.0; +const PARRY_WINDOW_RADIUS: f32 = 3.5; +const HIT_RADIUS: f32 = 1.2; + +pub fn dialog_projectile_system(world: &mut World, input_state: &InputState) +{ + let player_entity = world.player_tags.all().into_iter().next(); + let Some(player_entity) = player_entity + else + { + return; + }; + + let player_pos = world + .transforms + .with(player_entity, |t| t.position) + .unwrap_or(Vec3::ZERO); + + let player_is_evading = is_player_evading(world, player_entity); + + let projectiles: Vec = world.projectile_tags.all(); + let mut outcomes: Vec = Vec::new(); + let mut to_despawn: Vec = Vec::new(); + + for proj_entity in projectiles + { + let proj_pos = match world.transforms.with(proj_entity, |t| t.position) + { + Some(p) => p, + None => continue, + }; + + let to_player = player_pos - proj_pos; + let distance = to_player.length(); + + let window_open = world + .dialog_projectiles + .with(proj_entity, |p| p.parry_window_open) + .unwrap_or(false); + + if window_open + { + if let Some(outcome) = resolve_parry(world, proj_entity, input_state, player_is_evading) + { + let bubble_entity = world + .dialog_projectiles + .with(proj_entity, |p| p.bubble_entity) + .unwrap(); + outcomes.push(DialogOutcomeEvent { + bubble_entity, + outcome, + }); + to_despawn.push(proj_entity); + continue; + } + } + + if distance < HIT_RADIUS + { + let bubble_entity = world + .dialog_projectiles + .with(proj_entity, |p| p.bubble_entity) + .unwrap(); + let outcome = if player_is_evading + { + DialogOutcome::Evaded + } + else + { + DialogOutcome::Hit + }; + outcomes.push(DialogOutcomeEvent { + bubble_entity, + outcome, + }); + to_despawn.push(proj_entity); + continue; + } + + if distance < PARRY_WINDOW_RADIUS + { + world.dialog_projectiles.with_mut(proj_entity, |p| { + p.parry_window_open = true; + }); + } + + let direction = if distance > 0.001 + { + to_player / distance + } + else + { + Vec3::ZERO + }; + + world.transforms.with_mut(proj_entity, |t| { + t.position += direction * PROJECTILE_SPEED * (1.0 / 60.0); + }); + } + + for entity in to_despawn + { + world.despawn(entity); + } + + world.dialog_outcomes.extend(outcomes); +} + +fn resolve_parry( + world: &World, + proj_entity: EntityHandle, + input_state: &InputState, + player_is_evading: bool, +) -> Option +{ + if player_is_evading + { + return Some(DialogOutcome::Evaded); + } + + let correct_parry = world + .dialog_projectiles + .with(proj_entity, |p| p.correct_parry)?; + + if input_state.i_just_pressed + { + return Some( + if correct_parry == ParryButton::I + { + DialogOutcome::ParriedI + } + else + { + DialogOutcome::WrongParry + }, + ); + } + + if input_state.j_just_pressed + { + return Some( + if correct_parry == ParryButton::J + { + DialogOutcome::ParriedJ + } + else + { + DialogOutcome::WrongParry + }, + ); + } + + if input_state.l_just_pressed + { + return Some( + if correct_parry == ParryButton::L + { + DialogOutcome::ParriedL + } + else + { + DialogOutcome::WrongParry + }, + ); + } + + None +} + +fn is_player_evading(world: &World, player_entity: EntityHandle) -> bool +{ + world.leaping_states.get(player_entity).is_some() + || world.rolling_states.get(player_entity).is_some() +} diff --git a/src/systems/dialog_render.rs b/src/systems/dialog_render.rs new file mode 100644 index 0000000..d0eee82 --- /dev/null +++ b/src/systems/dialog_render.rs @@ -0,0 +1,109 @@ +use glam::{Mat4, Vec3}; + +use crate::render::billboard::{BillboardDrawCall, BillboardVertex, BubbleUniforms}; +use crate::world::World; + +const BUBBLE_WIDTH: f32 = 2.2; +const BUBBLE_HEIGHT: f32 = 1.1; +const BODY_FRAC: f32 = 0.78; +const CORNER_R: f32 = 0.18; +const BORDER_W: f32 = 0.06; +const HEIGHT_OFFSET: f32 = 8.2; + +const FILL_COLOR: [f32; 4] = [0.05, 0.05, 0.05, 1.0]; +const BORDER_COLOR: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; + +pub fn dialog_bubble_render_system( + world: &World, + camera_pos: Vec3, + view_proj: Mat4, +) -> Vec +{ + let mut calls = Vec::new(); + + for bubble_entity in world.bubble_tags.all() + { + let character_entity = match world + .dialog_bubbles + .with(bubble_entity, |b| b.character_entity) + { + Some(e) => e, + None => continue, + }; + + let character_pos = match world.transforms.with(character_entity, |t| t.position) + { + Some(p) => p, + None => continue, + }; + + let body_half_h = BUBBLE_HEIGHT * BODY_FRAC * 0.5; + let tail_height = BUBBLE_HEIGHT * (1.0 - BODY_FRAC); + let anchor = character_pos + Vec3::Y * (HEIGHT_OFFSET + body_half_h); + + let to_camera = camera_pos - anchor; + let forward = if to_camera.length_squared() > 1e-6 + { + to_camera.normalize() + } + else + { + Vec3::Z + }; + + let up_ref = Vec3::Y; + let right = if forward.abs().dot(up_ref) > 0.99 + { + Vec3::X + } + else + { + up_ref.cross(forward).normalize() + }; + let up = forward.cross(right).normalize(); + + let half_w = BUBBLE_WIDTH * 0.5; + let total_down = body_half_h + tail_height; + + // Corners: tl → tr → br → bl (CCW in clip space when billboard faces camera). + let tl = anchor - right * half_w + up * body_half_h; + let tr = anchor + right * half_w + up * body_half_h; + let br = anchor + right * half_w - up * total_down; + let bl = anchor - right * half_w - up * total_down; + + let vertices = [ + BillboardVertex { + position: tl.to_array(), + uv: [0.0, 0.0], + }, + BillboardVertex { + position: tr.to_array(), + uv: [1.0, 0.0], + }, + BillboardVertex { + position: br.to_array(), + uv: [1.0, 1.0], + }, + BillboardVertex { + position: bl.to_array(), + uv: [0.0, 1.0], + }, + ]; + + let uniforms = BubbleUniforms { + view_proj: view_proj.to_cols_array_2d(), + size: [BUBBLE_WIDTH, BUBBLE_HEIGHT], + body_frac: BODY_FRAC, + corner_r: CORNER_R, + border_w: BORDER_W, + _pad1: [0.0; 3], + fill_color: FILL_COLOR, + border_color: BORDER_COLOR, + _pad2: [0.0; 32], + }; + + calls.push(BillboardDrawCall { vertices, uniforms }); + } + + calls +} diff --git a/src/systems/input.rs b/src/systems/input.rs index 4caa50f..0c5c885 100644 --- a/src/systems/input.rs +++ b/src/systems/input.rs @@ -50,6 +50,10 @@ pub fn player_input_system(world: &mut World, input_state: &InputState) input_component.move_direction = move_direction; input_component.jump_pressed = input_state.space; input_component.jump_just_pressed = input_state.space_just_pressed; + input_component.roll_just_pressed = input_state.roll_just_pressed; + input_component.parry_i_just_pressed = input_state.i_just_pressed; + input_component.parry_j_just_pressed = input_state.j_just_pressed; + input_component.parry_l_just_pressed = input_state.l_just_pressed; }); } } diff --git a/src/systems/mod.rs b/src/systems/mod.rs index bcf4afe..3c55d93 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -1,4 +1,8 @@ pub mod camera; +pub mod dialog; +pub mod dialog_camera; +pub mod dialog_projectile; +pub mod dialog_render; pub mod follow; pub mod input; pub mod physics_sync; @@ -15,6 +19,10 @@ pub use camera::{ camera_follow_system, camera_input_system, camera_noclip_system, camera_view_matrix, start_camera_following, }; +pub use dialog::dialog_system; +pub use dialog_camera::dialog_camera_system; +pub use dialog_projectile::dialog_projectile_system; +pub use dialog_render::dialog_bubble_render_system; pub use input::player_input_system; pub use physics_sync::physics_sync_system; pub use render::render_system; diff --git a/src/systems/player_states.rs b/src/systems/player_states.rs index 1b31d21..75b371e 100644 --- a/src/systems/player_states.rs +++ b/src/systems/player_states.rs @@ -2,121 +2,34 @@ use glam::Vec3; use kurbo::ParamCurve; use rapier3d::math::Vector; +use crate::components::player_states::{ + FallingState, IdleState, JumpingState, LeapingState, RollingState, WalkingState, +}; use crate::entity::EntityHandle; use crate::physics::PhysicsManager; -use crate::state::State; -use crate::utility::time::Time; +use crate::state::PlayerState; use crate::world::World; -pub struct PlayerFallingState -{ - pub entity: EntityHandle, -} +pub const LEAP_DURATION: f32 = 0.18; +pub const ROLL_DURATION: f32 = 0.42; +const LEAP_SPEED: f32 = 18.0; +const ROLL_SPEED: f32 = 14.0; -impl State for PlayerFallingState +impl PlayerState for IdleState { - fn get_state_name(&self) -> &'static str + fn tick_time(&mut self, delta: f32) { - "PlayerFallingState" + self.time_in_state += delta; } - fn on_state_enter(&mut self, _world: &mut World) + fn on_enter(&mut self, world: &mut World, entity: EntityHandle) { - println!("entered falling"); - } - - fn on_state_exit(&mut self, _world: &mut World) {} - - fn on_state_update(&mut self, _world: &mut World, _delta: f32) {} - - fn on_state_physics_update(&mut self, world: &mut World, delta: f32) - { - const GRAVITY: f32 = -9.81 * 5.0; - const GROUND_CHECK_DISTANCE: f32 = 0.6; - - let (current_pos, velocity) = world - .physics - .with(self.entity, |physics| { - PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { - let mut vel = *rigidbody.linvel(); - vel.y += GRAVITY * delta; - (*rigidbody.translation(), vel) - }) - }) - .flatten() - .unwrap(); - - let next_pos = current_pos + velocity; - let terrain_height = PhysicsManager::get_terrain_height_at(next_pos.x, next_pos.z); - - let is_grounded = if let Some(height) = terrain_height - { - let target_y = height + 1.0; - let distance_to_ground = current_pos.y - target_y; - - if distance_to_ground < GROUND_CHECK_DISTANCE && velocity.y <= 0.01 - { - world.physics.with(self.entity, |physics| { - PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { - let next_pos = Vector::new(current_pos.x, target_y, current_pos.z); - rigidbody.set_next_kinematic_translation(next_pos); - rigidbody.set_linvel(Vector::new(velocity.x, 0.0, velocity.z), true); - }); - }); - true - } - else - { - world.physics.with(self.entity, |physics| { - PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { - let next_pos = current_pos + velocity * delta; - rigidbody.set_next_kinematic_translation(next_pos); - rigidbody.set_linvel(velocity, true); - }); - }); - false - } - } - else - { - world.physics.with(self.entity, |physics| { - PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { - let next_pos = current_pos + velocity * delta; - rigidbody.set_next_kinematic_translation(next_pos); - rigidbody.set_linvel(velocity, true); - }); - }); - false - }; - - world.movements.with_mut(self.entity, |movement| { - movement.movement_context.is_floored = is_grounded; - }); - } -} - -pub struct PlayerIdleState -{ - pub entity: EntityHandle, -} - -impl State for PlayerIdleState -{ - fn get_state_name(&self) -> &'static str - { - "PlayerIdleState" - } - - fn on_state_enter(&mut self, world: &mut World) - { - println!("entered idle"); - - world.physics.with(self.entity, |physics| { + world.physics.with(entity, |physics| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { let current_velocity = *rigidbody.linvel(); let idle_damping = world .movements - .with(self.entity, |m| m.idle_damping) + .with(entity, |m| m.idle_damping) .unwrap_or(0.1); let horizontal_velocity = Vec3::new(current_velocity.x, 0.0, current_velocity.z); @@ -134,17 +47,13 @@ impl State for PlayerIdleState }); } - fn on_state_exit(&mut self, _world: &mut World) {} - - fn on_state_update(&mut self, _world: &mut World, _delta: f32) {} - - fn on_state_physics_update(&mut self, world: &mut World, _delta: f32) + fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, _delta: f32) { const GROUND_CHECK_DISTANCE: f32 = 0.6; let current_translation = world .physics - .with(self.entity, |physics| { + .with(entity, |physics| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { *rigidbody.translation() }) @@ -162,7 +71,7 @@ impl State for PlayerIdleState if distance_to_ground.abs() < GROUND_CHECK_DISTANCE { - world.physics.with(self.entity, |physics| { + world.physics.with(entity, |physics| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { let next_translation = Vector::new(current_translation.x, target_y, current_translation.z); @@ -170,7 +79,7 @@ impl State for PlayerIdleState }); }); - world.movements.with_mut(self.entity, |movement| { + world.movements.with_mut(entity, |movement| { movement.movement_context.is_floored = true; }); } @@ -178,51 +87,32 @@ impl State for PlayerIdleState } } -pub struct PlayerWalkingState +impl PlayerState for WalkingState { - pub entity: EntityHandle, - pub enter_time_stamp: f32, -} - -impl State for PlayerWalkingState -{ - fn get_state_name(&self) -> &'static str + fn tick_time(&mut self, delta: f32) { - "PlayerWalkingState" + self.time_in_state += delta; } - fn on_state_enter(&mut self, _world: &mut World) - { - self.enter_time_stamp = Time::get_time_elapsed(); - println!("entered walking"); - } - - fn on_state_exit(&mut self, _world: &mut World) {} - - fn on_state_update(&mut self, _world: &mut World, _delta: f32) {} - - fn on_state_physics_update(&mut self, world: &mut World, delta: f32) + fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32) { let (movement_input, walking_config) = world .movements - .with(self.entity, |movement| { + .with(entity, |movement| { let input = world .inputs - .with(self.entity, |input| input.move_direction) + .with(entity, |input| input.move_direction) .unwrap_or(Vec3::ZERO); (input, movement.clone()) }) .unwrap(); - let current_time = Time::get_time_elapsed(); - let elapsed_time = current_time - self.enter_time_stamp; - - let t = (elapsed_time / walking_config.walking_acceleration_duration).clamp(0.0, 1.0); + let t = (self.time_in_state / walking_config.walking_acceleration_duration).clamp(0.0, 1.0); let acceleration_amount = walking_config.walking_acceleration_curve.eval(t as f64).y as f32; let current_translation = world .physics - .with(self.entity, |physics| { + .with(entity, |physics| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { *rigidbody.translation() }) @@ -271,7 +161,7 @@ impl State for PlayerWalkingState } }; - world.physics.with(self.entity, |physics| { + world.physics.with(entity, |physics| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { let current_velocity = *rigidbody.linvel(); @@ -300,13 +190,13 @@ impl State for PlayerWalkingState }); }); - world.movements.with_mut(self.entity, |movement| { + world.movements.with_mut(entity, |movement| { movement.movement_context.is_floored = terrain_height.is_some(); }); if movement_input.length_squared() > 0.1 { - world.transforms.with_mut(self.entity, |transform| { + world.transforms.with_mut(entity, |transform| { let target_rotation = f32::atan2(movement_input.x, movement_input.z); transform.rotation.y = target_rotation; }); @@ -314,64 +204,44 @@ impl State for PlayerWalkingState } } -pub struct PlayerJumpingState +impl PlayerState for JumpingState { - pub entity: EntityHandle, - pub enter_time_stamp: f32, -} - -impl State for PlayerJumpingState -{ - fn get_state_name(&self) -> &'static str + fn tick_time(&mut self, delta: f32) { - "PlayerJumpingState" + self.time_in_state += delta; } - fn on_state_enter(&mut self, world: &mut World) + fn on_enter(&mut self, world: &mut World, entity: EntityHandle) { - self.enter_time_stamp = Time::get_time_elapsed(); + self.time_in_state = 0.0; - let current_position = world.transforms.get(self.entity).unwrap().position; + let current_position = world.transforms.get(entity).unwrap().position; - world.jumps.with_mut(self.entity, |jump| { + world.jumps.with_mut(entity, |jump| { jump.jump_context.in_progress = true; - jump.jump_context.execution_time = self.enter_time_stamp; jump.jump_context.origin_height = current_position.y; jump.jump_context.duration = 0.0; jump.jump_context.normal = Vec3::Y; }); - - println!("entered jumping"); } - fn on_state_exit(&mut self, world: &mut World) + fn on_exit(&mut self, world: &mut World, entity: EntityHandle) { - world.jumps.with_mut(self.entity, |jump| { + world.jumps.with_mut(entity, |jump| { jump.jump_context.in_progress = false; jump.jump_context.duration = 0.0; }); - - println!("exited jumping"); } - fn on_state_update(&mut self, _world: &mut World, _delta: f32) {} - - fn on_state_physics_update(&mut self, world: &mut World, delta: f32) + fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32) { - let current_time = Time::get_time_elapsed(); - - world.jumps.with_mut(self.entity, |jump| { - jump.jump_context.duration = - current_time - jump.jump_context.execution_time; + world.jumps.with_mut(entity, |jump| { + jump.jump_context.duration = self.time_in_state; }); - let jump = world - .jumps - .with(self.entity, |jump| jump.clone()) - .unwrap(); + let jump = world.jumps.with(entity, |jump| jump.clone()).unwrap(); - let elapsed_time = jump.jump_context.duration; - let normalized_time = (elapsed_time / jump.jump_duration).min(1.0); + let normalized_time = (self.time_in_state / jump.jump_duration).min(1.0); let height_progress = jump.jump_curve.eval(normalized_time as f64).y as f32; let origin_height = jump.jump_context.origin_height; @@ -379,7 +249,7 @@ impl State for PlayerJumpingState let current_translation = world .physics - .with(self.entity, |physics| { + .with(entity, |physics| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { *rigidbody.translation() }) @@ -387,11 +257,10 @@ impl State for PlayerJumpingState .flatten() .unwrap(); - let current_y = current_translation.y; - let height_diff = target_height - current_y; + let height_diff = target_height - current_translation.y; let required_velocity = height_diff / delta; - world.physics.with(self.entity, |physics| { + world.physics.with(entity, |physics| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { let current_velocity = *rigidbody.linvel(); let next_translation = Vector::new( @@ -408,8 +277,220 @@ impl State for PlayerJumpingState }); }); - world.movements.with_mut(self.entity, |movement| { + world.movements.with_mut(entity, |movement| { movement.movement_context.is_floored = false; }); } } + +impl PlayerState for FallingState +{ + fn tick_time(&mut self, delta: f32) + { + self.time_in_state += delta; + } + + fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32) + { + const GRAVITY: f32 = -9.81 * 5.0; + const GROUND_CHECK_DISTANCE: f32 = 0.6; + + let (current_pos, velocity) = world + .physics + .with(entity, |physics| { + PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { + let mut vel = *rigidbody.linvel(); + vel.y += GRAVITY * delta; + (*rigidbody.translation(), vel) + }) + }) + .flatten() + .unwrap(); + + let next_pos = current_pos + velocity; + let terrain_height = PhysicsManager::get_terrain_height_at(next_pos.x, next_pos.z); + + let is_grounded = if let Some(height) = terrain_height + { + let target_y = height + 1.0; + let distance_to_ground = current_pos.y - target_y; + + if distance_to_ground < GROUND_CHECK_DISTANCE && velocity.y <= 0.01 + { + world.physics.with(entity, |physics| { + PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { + let next_pos = Vector::new(current_pos.x, target_y, current_pos.z); + rigidbody.set_next_kinematic_translation(next_pos); + rigidbody.set_linvel(Vector::new(velocity.x, 0.0, velocity.z), true); + }); + }); + true + } + else + { + world.physics.with(entity, |physics| { + PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { + let next_pos = current_pos + velocity * delta; + rigidbody.set_next_kinematic_translation(next_pos); + rigidbody.set_linvel(velocity, true); + }); + }); + false + } + } + else + { + world.physics.with(entity, |physics| { + PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { + let next_pos = current_pos + velocity * delta; + rigidbody.set_next_kinematic_translation(next_pos); + rigidbody.set_linvel(velocity, true); + }); + }); + false + }; + + world.movements.with_mut(entity, |movement| { + movement.movement_context.is_floored = is_grounded; + }); + } +} + +impl PlayerState for LeapingState +{ + fn tick_time(&mut self, delta: f32) + { + self.time_in_state += delta; + } + + fn on_enter(&mut self, world: &mut World, entity: EntityHandle) + { + self.time_in_state = 0.0; + + let facing = world + .transforms + .with(entity, |t| { + let yaw = t.rotation.y; + Vec3::new(yaw.sin(), 0.0, yaw.cos()) + }) + .unwrap_or(Vec3::Z); + + let move_dir = world + .inputs + .with(entity, |i| i.move_direction) + .unwrap_or(Vec3::ZERO); + + self.leap_direction = if move_dir.length_squared() > 0.01 + { + move_dir + } + else + { + facing + }; + } + + fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32) + { + let current_translation = world + .physics + .with(entity, |physics| { + PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| *rb.translation()) + }) + .flatten() + .unwrap(); + + let terrain_height = + PhysicsManager::get_terrain_height_at(current_translation.x, current_translation.z); + let target_y = terrain_height + .map(|h| h + 1.0) + .unwrap_or(current_translation.y); + + let velocity = self.leap_direction * LEAP_SPEED; + + world.physics.with(entity, |physics| { + PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| { + let next = Vector::new( + current_translation.x + velocity.x * delta, + target_y, + current_translation.z + velocity.z * delta, + ); + rb.set_linvel(Vector::new(velocity.x, 0.0, velocity.z), true); + rb.set_next_kinematic_translation(next); + }); + }); + + world.movements.with_mut(entity, |m| { + m.movement_context.is_floored = terrain_height.is_some(); + }); + + world.transforms.with_mut(entity, |t| { + t.rotation.y = f32::atan2(self.leap_direction.x, self.leap_direction.z); + }); + } +} + +impl PlayerState for RollingState +{ + fn tick_time(&mut self, delta: f32) + { + self.time_in_state += delta; + } + + fn on_enter(&mut self, world: &mut World, entity: EntityHandle) + { + self.time_in_state = 0.0; + + self.roll_direction = world + .inputs + .with(entity, |i| i.move_direction) + .filter(|d| d.length_squared() > 0.01) + .unwrap_or_else(|| { + world + .transforms + .with(entity, |t| { + let yaw = t.rotation.y; + Vec3::new(yaw.sin(), 0.0, yaw.cos()) + }) + .unwrap_or(Vec3::Z) + }); + } + + fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32) + { + let t = (self.time_in_state / ROLL_DURATION).clamp(0.0, 1.0); + let speed = ROLL_SPEED * (1.0 - t * 0.6); + + let current_translation = world + .physics + .with(entity, |physics| { + PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| *rb.translation()) + }) + .flatten() + .unwrap(); + + let terrain_height = + PhysicsManager::get_terrain_height_at(current_translation.x, current_translation.z); + let target_y = terrain_height + .map(|h| h + 1.0) + .unwrap_or(current_translation.y); + + let velocity = self.roll_direction * speed; + + world.physics.with(entity, |physics| { + PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| { + let next = Vector::new( + current_translation.x + velocity.x * delta, + target_y, + current_translation.z + velocity.z * delta, + ); + rb.set_linvel(Vector::new(velocity.x, 0.0, velocity.z), true); + rb.set_next_kinematic_translation(next); + }); + }); + + world.movements.with_mut(entity, |m| { + m.movement_context.is_floored = terrain_height.is_some(); + }); + } +} diff --git a/src/systems/state_machine.rs b/src/systems/state_machine.rs index d52c250..6d92a65 100644 --- a/src/systems/state_machine.rs +++ b/src/systems/state_machine.rs @@ -2,13 +2,11 @@ use crate::world::World; pub fn state_machine_system(world: &mut World, delta: f32) { - let entities: Vec<_> = world.state_machines.all(); - - for entity in entities + for entity in world.state_machines.all() { if let Some(mut state_machine) = world.state_machines.components.remove(&entity) { - state_machine.update(world, delta); + state_machine.update(world, entity, delta); world .state_machines .components @@ -19,16 +17,11 @@ pub fn state_machine_system(world: &mut World, delta: f32) pub fn state_machine_physics_system(world: &mut World, delta: f32) { - let entities: Vec<_> = world.state_machines.all(); - - for entity in entities + for entity in world.state_machines.all() { if let Some(mut state_machine) = world.state_machines.components.remove(&entity) { - if let Some(current_state) = state_machine.get_current_state_mut() - { - current_state.on_state_physics_update(world, delta); - } + state_machine.physics_update(world, entity, delta); world .state_machines .components diff --git a/src/systems/trigger.rs b/src/systems/trigger.rs index 42f45d8..019498f 100644 --- a/src/systems/trigger.rs +++ b/src/systems/trigger.rs @@ -1,6 +1,8 @@ use glam::Vec3; -use crate::components::trigger::{TriggerEvent, TriggerEventKind, TriggerFilter, TriggerShape, TriggerState}; +use crate::components::trigger::{ + TriggerEvent, TriggerEventKind, TriggerFilter, TriggerShape, TriggerState, +}; use crate::entity::EntityHandle; use crate::world::World; @@ -35,10 +37,12 @@ pub fn trigger_system(world: &mut World) let overlapping = match world.triggers.get(trigger_entity) { - Some(trigger) => activator_positions.iter().any(|(_, pos)| match &trigger.shape - { - TriggerShape::Sphere { radius } => (trigger_pos - *pos).length() < *radius, - }), + Some(trigger) => activator_positions + .iter() + .any(|(_, pos)| match &trigger.shape + { + TriggerShape::Sphere { radius } => (trigger_pos - *pos).length() < *radius, + }), None => continue, }; diff --git a/src/texture.rs b/src/texture.rs index de6df2f..094829e 100644 --- a/src/texture.rs +++ b/src/texture.rs @@ -1,3 +1,4 @@ +use crate::paths; use anyhow::Result; use exr::prelude::{ReadChannels, ReadLayers}; @@ -27,10 +28,10 @@ impl DitherTextures pub fn load_octaves(device: &wgpu::Device, queue: &wgpu::Queue) -> Result { let octave_paths = [ - "textures/dither/octave_0.png", - "textures/dither/octave_1.png", - "textures/dither/octave_2.png", - "textures/dither/octave_3.png", + paths::textures::dither_octave(0), + paths::textures::dither_octave(1), + paths::textures::dither_octave(2), + paths::textures::dither_octave(3), ]; let mut images = Vec::new(); @@ -38,7 +39,7 @@ impl DitherTextures for path in &octave_paths { - let img = image::open(path)?.to_luma8(); + let img = image::open(&path)?.to_luma8(); let (width, height) = img.dimensions(); if texture_size == 0 diff --git a/src/utility/input.rs b/src/utility/input.rs index b2161ec..791f689 100755 --- a/src/utility/input.rs +++ b/src/utility/input.rs @@ -9,9 +9,15 @@ pub struct InputState pub d: bool, pub space: bool, pub shift: bool, + pub lctrl: bool, pub space_just_pressed: bool, pub debug_cycle_just_pressed: bool, + pub i_just_pressed: bool, + pub j_just_pressed: bool, + pub l_just_pressed: bool, + pub roll_just_pressed: bool, + pub f2_just_pressed: bool, pub mouse_delta: (f32, f32), pub mouse_captured: bool, @@ -29,8 +35,14 @@ impl InputState d: false, space: false, shift: false, + lctrl: false, space_just_pressed: false, debug_cycle_just_pressed: false, + i_just_pressed: false, + j_just_pressed: false, + l_just_pressed: false, + roll_just_pressed: false, + f2_just_pressed: false, mouse_delta: (0.0, 0.0), mouse_captured: true, quit_requested: false, @@ -101,7 +113,19 @@ impl InputState self.space = true; } Keycode::LShift | Keycode::RShift => self.shift = true, + Keycode::LCtrl => + { + if !self.lctrl + { + self.roll_just_pressed = true; + } + self.lctrl = true; + } + Keycode::I => self.i_just_pressed = true, + Keycode::J => self.j_just_pressed = true, + Keycode::L => self.l_just_pressed = true, Keycode::F1 => self.debug_cycle_just_pressed = true, + Keycode::F2 => self.f2_just_pressed = true, _ => {} } @@ -117,6 +141,7 @@ impl InputState Keycode::D => self.d = false, Keycode::Space => self.space = false, Keycode::LShift | Keycode::RShift => self.shift = false, + Keycode::LCtrl => self.lctrl = false, _ => {} } @@ -134,6 +159,11 @@ impl InputState { self.space_just_pressed = false; self.debug_cycle_just_pressed = false; + self.i_just_pressed = false; + self.j_just_pressed = false; + self.l_just_pressed = false; + self.roll_just_pressed = false; + self.f2_just_pressed = false; self.mouse_delta = (0.0, 0.0); } diff --git a/src/world.rs b/src/world.rs index 9bc9722..3c324df 100644 --- a/src/world.rs +++ b/src/world.rs @@ -1,8 +1,14 @@ use std::collections::HashMap; +use crate::components::dialog::{ + DialogBubbleComponent, DialogOutcomeEvent, DialogProjectileComponent, DialogSourceComponent, +}; use crate::components::dissolve::DissolveComponent; use crate::components::follow::FollowComponent; use crate::components::lights::spot::SpotlightComponent; +use crate::components::player_states::{ + FallingState, IdleState, JumpingState, LeapingState, RollingState, WalkingState, +}; use crate::components::tree_instances::TreeInstancesComponent; use crate::components::trigger::{TriggerComponent, TriggerEvent}; use crate::components::{ @@ -79,6 +85,12 @@ pub struct World pub inputs: Storage, pub player_tags: Storage<()>, pub state_machines: Storage, + pub idle_states: Storage, + pub walking_states: Storage, + pub jumping_states: Storage, + pub falling_states: Storage, + pub leaping_states: Storage, + pub rolling_states: Storage, pub cameras: Storage, pub spotlights: Storage, pub tree_tags: Storage<()>, @@ -89,6 +101,12 @@ pub struct World pub names: Storage, pub triggers: Storage, pub trigger_events: Vec, + pub dialog_sources: Storage, + pub dialog_bubbles: Storage, + pub dialog_projectiles: Storage, + pub bubble_tags: Storage<()>, + pub projectile_tags: Storage<()>, + pub dialog_outcomes: Vec, } impl World @@ -105,6 +123,12 @@ impl World inputs: Storage::new(), player_tags: Storage::new(), state_machines: Storage::new(), + idle_states: Storage::new(), + walking_states: Storage::new(), + jumping_states: Storage::new(), + falling_states: Storage::new(), + leaping_states: Storage::new(), + rolling_states: Storage::new(), cameras: Storage::new(), spotlights: Storage::new(), tree_tags: Storage::new(), @@ -115,6 +139,12 @@ impl World names: Storage::new(), triggers: Storage::new(), trigger_events: Vec::new(), + dialog_sources: Storage::new(), + dialog_bubbles: Storage::new(), + dialog_projectiles: Storage::new(), + bubble_tags: Storage::new(), + projectile_tags: Storage::new(), + dialog_outcomes: Vec::new(), } } @@ -133,6 +163,12 @@ impl World self.inputs.remove(entity); self.player_tags.remove(entity); self.state_machines.remove(entity); + self.idle_states.remove(entity); + self.walking_states.remove(entity); + self.jumping_states.remove(entity); + self.falling_states.remove(entity); + self.leaping_states.remove(entity); + self.rolling_states.remove(entity); self.cameras.remove(entity); self.spotlights.remove(entity); self.tree_tags.remove(entity); @@ -142,6 +178,11 @@ impl World self.tree_instances.remove(entity); self.names.remove(entity); self.triggers.remove(entity); + self.dialog_sources.remove(entity); + self.dialog_bubbles.remove(entity); + self.dialog_projectiles.remove(entity); + self.bubble_tags.remove(entity); + self.projectile_tags.remove(entity); self.entities.despawn(entity); } diff --git a/tools/ink-engine-runtime.dll b/tools/ink-engine-runtime.dll new file mode 100644 index 0000000..4f396a4 Binary files /dev/null and b/tools/ink-engine-runtime.dll differ diff --git a/tools/ink_compiler.dll b/tools/ink_compiler.dll new file mode 100644 index 0000000..0fd0603 Binary files /dev/null and b/tools/ink_compiler.dll differ diff --git a/tools/inklecate b/tools/inklecate new file mode 100755 index 0000000..1fc9bbc Binary files /dev/null and b/tools/inklecate differ diff --git a/tools/inklecate.dll b/tools/inklecate.dll new file mode 100644 index 0000000..2e56b0b Binary files /dev/null and b/tools/inklecate.dll differ