Compare commits

..

2 Commits

Author SHA1 Message Date
Jonas H
11b31169b1 dialog WIP paths consolidation and rendering 2026-03-28 10:34:19 +01:00
Jonas H
4c3ebca96e editor update 2026-03-28 10:31:05 +01:00
72 changed files with 2709 additions and 492 deletions

View File

@@ -21,6 +21,7 @@ exr = "1.72"
kurbo = "0.11" kurbo = "0.11"
nalgebra = { version = "0.34.1", features = ["convert-glam030"] } nalgebra = { version = "0.34.1", features = ["convert-glam030"] }
serde_json = "1.0" serde_json = "1.0"
bladeink = "1.2"
wesl = "0.2" wesl = "0.2"
[build-dependencies] [build-dependencies]

View File

@@ -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

View File

@@ -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":{}}

View File

@@ -43,7 +43,10 @@
1, 1,
2, 2,
3, 3,
4 4,
5,
6,
7
] ]
} }
], ],
@@ -58,11 +61,44 @@
] ]
}, },
{ {
"children":[ "mesh":1,
5, "name":"Lighthouse_base",
6 "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", "name":"TerrainPlane",
"scale":[ "scale":[
1.000100016593933, 1.000100016593933,
@@ -100,56 +136,37 @@
0.9110424518585205 0.9110424518585205
], ],
"translation":[ "translation":[
-392.0350036621094, -344.301025390625,
238.72787475585938, 223.67401123046875,
244.30006408691406 232.61265563964844
]
},
{
"name":"TestCharSpawn",
"translation":[
-381.1509704589844,
106.53739166259766,
107.46959686279297
] ]
}, },
{ {
"extensions":{ "extensions":{
"EXT_mesh_gpu_instancing":{ "EXT_mesh_gpu_instancing":{
"attributes":{ "attributes":{
"TRANSLATION":17, "TRANSLATION":19,
"ROTATION":18, "ROTATION":20,
"SCALE":19 "SCALE":21
} }
} }
}, },
"mesh":1, "mesh":3,
"name":"TerrainPlane.0" "name":"TerrainPlane.0"
},
{
"extensions":{
"EXT_mesh_gpu_instancing":{
"attributes":{
"TRANSLATION":17,
"ROTATION":18,
"SCALE":19
}
}
},
"mesh":2,
"name":"TerrainPlane.1"
} }
], ],
"materials":[ "materials":[
{ {
"doubleSided":true, "doubleSided":true,
"name":"terrain" "name":"terrain"
},
{
"doubleSided":true,
"name":"snow",
"pbrMetallicRoughness":{
"baseColorFactor":[
0.800000011920929,
0.800000011920929,
0.800000011920929,
1
],
"metallicFactor":0,
"roughnessFactor":0.5
}
} }
], ],
"meshes":[ "meshes":[
@@ -167,7 +184,7 @@
] ]
}, },
{ {
"name":"Cylinder", "name":"Cube",
"primitives":[ "primitives":[
{ {
"attributes":{ "attributes":{
@@ -175,8 +192,20 @@
"NORMAL":5, "NORMAL":5,
"TEXCOORD_0":6 "TEXCOORD_0":6
}, },
"indices":3, "indices":7
"material":0 }
]
},
{
"name":"Cylinder.001",
"primitives":[
{
"attributes":{
"POSITION":8,
"NORMAL":9,
"TEXCOORD_0":10
},
"indices":11
} }
] ]
}, },
@@ -185,12 +214,11 @@
"primitives":[ "primitives":[
{ {
"attributes":{ "attributes":{
"POSITION":7, "POSITION":12,
"NORMAL":8, "NORMAL":13,
"TEXCOORD_0":9 "TEXCOORD_0":14
}, },
"indices":3, "indices":3
"material":1
} }
] ]
}, },
@@ -199,21 +227,12 @@
"primitives":[ "primitives":[
{ {
"attributes":{ "attributes":{
"POSITION":10, "POSITION":15,
"NORMAL":11, "NORMAL":16,
"TEXCOORD_0":12 "TEXCOORD_0":17
}, },
"indices":13, "indices":18,
"material":0 "material":0
},
{
"attributes":{
"POSITION":14,
"NORMAL":15,
"TEXCOORD_0":16
},
"indices":13,
"material":1
} }
] ]
} }
@@ -256,33 +275,73 @@
{ {
"bufferView":4, "bufferView":4,
"componentType":5126, "componentType":5126,
"count":1280, "count":24,
"max":[ "max":[
5.561562538146973, 1,
16.066009521484375, 1,
5.561562538146973 1
], ],
"min":[ "min":[
-5.561562538146973, -1,
0, -1,
-5.561562538146973 -1
], ],
"type":"VEC3" "type":"VEC3"
}, },
{ {
"bufferView":5, "bufferView":5,
"componentType":5126, "componentType":5126,
"count":1280, "count":24,
"type":"VEC3" "type":"VEC3"
}, },
{ {
"bufferView":6, "bufferView":6,
"componentType":5126, "componentType":5126,
"count":1280, "count":24,
"type":"VEC2" "type":"VEC2"
}, },
{ {
"bufferView":7, "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, "componentType":5126,
"count":1280, "count":1280,
"max":[ "max":[
@@ -298,19 +357,19 @@
"type":"VEC3" "type":"VEC3"
}, },
{ {
"bufferView":8, "bufferView":13,
"componentType":5126, "componentType":5126,
"count":1280, "count":1280,
"type":"VEC3" "type":"VEC3"
}, },
{ {
"bufferView":9, "bufferView":14,
"componentType":5126, "componentType":5126,
"count":1280, "count":1280,
"type":"VEC2" "type":"VEC2"
}, },
{ {
"bufferView":10, "bufferView":15,
"componentType":5126, "componentType":5126,
"count":10404, "count":10404,
"max":[ "max":[
@@ -326,65 +385,37 @@
"type":"VEC3" "type":"VEC3"
}, },
{ {
"bufferView":11, "bufferView":16,
"componentType":5126, "componentType":5126,
"count":10404, "count":10404,
"type":"VEC3" "type":"VEC3"
}, },
{ {
"bufferView":12, "bufferView":17,
"componentType":5126, "componentType":5126,
"count":10404, "count":10404,
"type":"VEC2" "type":"VEC2"
}, },
{ {
"bufferView":13, "bufferView":18,
"componentType":5123, "componentType":5123,
"count":61206, "count":61206,
"type":"SCALAR" "type":"SCALAR"
}, },
{ {
"bufferView":14, "bufferView":19,
"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,
"componentType":5126, "componentType":5126,
"count":2380, "count":2380,
"type":"VEC3" "type":"VEC3"
}, },
{ {
"bufferView":18, "bufferView":20,
"componentType":5126, "componentType":5126,
"count":2380, "count":2380,
"type":"VEC4" "type":"VEC4"
}, },
{ {
"bufferView":19, "bufferView":21,
"componentType":5126, "componentType":5126,
"count":2380, "count":2380,
"type":"VEC3" "type":"VEC3"
@@ -417,101 +448,113 @@
}, },
{ {
"buffer":0, "buffer":0,
"byteLength":15360, "byteLength":288,
"byteOffset":45160, "byteOffset":45160,
"target":34962 "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, "buffer":0,
"byteLength":15360, "byteLength":15360,
"byteOffset":60520, "byteOffset":70808,
"target":34962
},
{
"buffer":0,
"byteLength":15360,
"byteOffset":86168,
"target":34962 "target":34962
}, },
{ {
"buffer":0, "buffer":0,
"byteLength":10240, "byteLength":10240,
"byteOffset":75880, "byteOffset":101528,
"target":34962
},
{
"buffer":0,
"byteLength":15360,
"byteOffset":86120,
"target":34962
},
{
"buffer":0,
"byteLength":15360,
"byteOffset":101480,
"target":34962
},
{
"buffer":0,
"byteLength":10240,
"byteOffset":116840,
"target":34962 "target":34962
}, },
{ {
"buffer":0, "buffer":0,
"byteLength":124848, "byteLength":124848,
"byteOffset":127080, "byteOffset":111768,
"target":34962 "target":34962
}, },
{ {
"buffer":0, "buffer":0,
"byteLength":124848, "byteLength":124848,
"byteOffset":251928, "byteOffset":236616,
"target":34962 "target":34962
}, },
{ {
"buffer":0, "buffer":0,
"byteLength":83232, "byteLength":83232,
"byteOffset":376776, "byteOffset":361464,
"target":34962 "target":34962
}, },
{ {
"buffer":0, "buffer":0,
"byteLength":122412, "byteLength":122412,
"byteOffset":460008, "byteOffset":444696,
"target":34963 "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, "buffer":0,
"byteLength":28560, "byteLength":28560,
"byteOffset":915348 "byteOffset":567108
}, },
{ {
"buffer":0, "buffer":0,
"byteLength":38080, "byteLength":38080,
"byteOffset":943908 "byteOffset":595668
}, },
{ {
"buffer":0, "buffer":0,
"byteLength":28560, "byteLength":28560,
"byteOffset":981988 "byteOffset":633748
} }
], ],
"buffers":[ "buffers":[
{ {
"byteLength":1010548, "byteLength":662308,
"uri":"terrain.bin" "uri":"terrain.bin"
} }
] ]

BIN
assets/meshes/test_char.bin Normal file

Binary file not shown.

View File

@@ -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"
}
]
}

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 425 B

After

Width:  |  Height:  |  Size: 425 B

View File

Before

Width:  |  Height:  |  Size: 751 B

After

Width:  |  Height:  |  Size: 751 B

View File

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 824 B

View File

Before

Width:  |  Height:  |  Size: 425 B

After

Width:  |  Height:  |  Size: 425 B

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 392 KiB

After

Width:  |  Height:  |  Size: 392 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 184 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

Before

Width:  |  Height:  |  Size: 910 KiB

After

Width:  |  Height:  |  Size: 910 KiB

Binary file not shown.

Binary file not shown.

BIN
blender/test_char.blend Normal file

Binary file not shown.

BIN
blender/test_char.blend1 Normal file

Binary file not shown.

View File

@@ -1,6 +1,88 @@
use std::path::Path;
use std::process::Command;
fn main() fn main()
{ {
compile_ink_stories();
let wesl = wesl::Wesl::new("src/shaders"); let wesl = wesl::Wesl::new("src/shaders");
wesl.build_artifact(&"package::main".parse().unwrap(), "main"); wesl.build_artifact(&"package::main".parse().unwrap(), "main");
wesl.build_artifact(&"package::shadow".parse().unwrap(), "shadow"); 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}");
}
}
}
}

View File

@@ -2,6 +2,7 @@ pub mod camera;
pub mod player; pub mod player;
pub mod spotlight; pub mod spotlight;
pub mod terrain; pub mod terrain;
pub mod test_char;
use crate::entity::EntityHandle; use crate::entity::EntityHandle;
use crate::world::World; use crate::world::World;

View File

@@ -7,17 +7,19 @@ use rapier3d::prelude::{ColliderBuilder, RigidBodyBuilder};
use crate::bundles::Bundle; use crate::bundles::Bundle;
use crate::components::lights::spot::SpotlightComponent; use crate::components::lights::spot::SpotlightComponent;
use crate::components::player_states::{
FallingState, IdleState, JumpingState, LeapingState, RollingState, WalkingState,
};
use crate::components::{ use crate::components::{
InputComponent, JumpComponent, MeshComponent, MovementComponent, PhysicsComponent, InputComponent, JumpComponent, MeshComponent, MovementComponent, PhysicsComponent,
}; };
use crate::entity::EntityHandle; use crate::entity::EntityHandle;
use crate::loaders::mesh::Mesh; use crate::loaders::mesh::Mesh;
use crate::paths;
use crate::physics::PhysicsManager; use crate::physics::PhysicsManager;
use crate::render::Pipeline; use crate::render::Pipeline;
use crate::state::StateMachine; use crate::state::StateMachine;
use crate::systems::player_states::{ use crate::systems::player_states::{LEAP_DURATION, ROLL_DURATION};
PlayerFallingState, PlayerIdleState, PlayerJumpingState, PlayerWalkingState,
};
use crate::world::{Transform, World}; use crate::world::{Transform, World};
pub struct PlayerBundle pub struct PlayerBundle
@@ -47,28 +49,20 @@ impl Bundle for PlayerBundle
let rigidbody_handle = PhysicsManager::add_rigidbody(rigidbody); let rigidbody_handle = PhysicsManager::add_rigidbody(rigidbody);
let collider_handle = PhysicsManager::add_collider(collider, Some(rigidbody_handle)); 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))?; .map_err(|e| format!("missing player mesh: {}", e))?;
let falling_state = PlayerFallingState { entity }; let mut state_machine = StateMachine::new::<FallingState>();
let idle_state = PlayerIdleState { entity }; state_machine.register_state(|w: &mut World| &mut w.falling_states);
let walking_state = PlayerWalkingState { state_machine.register_state(|w: &mut World| &mut w.idle_states);
entity, state_machine.register_state(|w: &mut World| &mut w.walking_states);
enter_time_stamp: 0.0, state_machine.register_state(|w: &mut World| &mut w.jumping_states);
}; state_machine.register_state(|w: &mut World| &mut w.leaping_states);
let jumping_state = PlayerJumpingState { state_machine.register_state(|w: &mut World| &mut w.rolling_states);
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 entity_id = entity; let entity_id = entity;
state_machine.add_transition::<PlayerFallingState, PlayerIdleState>(move |world| { state_machine.add_transition::<FallingState, IdleState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
@@ -80,7 +74,7 @@ impl Bundle for PlayerBundle
is_grounded && !has_input is_grounded && !has_input
}); });
state_machine.add_transition::<PlayerFallingState, PlayerWalkingState>(move |world| { state_machine.add_transition::<FallingState, WalkingState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
@@ -92,7 +86,7 @@ impl Bundle for PlayerBundle
is_grounded && has_input is_grounded && has_input
}); });
state_machine.add_transition::<PlayerIdleState, PlayerWalkingState>(move |world| { state_machine.add_transition::<IdleState, WalkingState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
@@ -104,7 +98,7 @@ impl Bundle for PlayerBundle
is_grounded && has_input is_grounded && has_input
}); });
state_machine.add_transition::<PlayerWalkingState, PlayerIdleState>(move |world| { state_machine.add_transition::<WalkingState, IdleState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
@@ -116,23 +110,21 @@ impl Bundle for PlayerBundle
is_grounded && !has_input is_grounded && !has_input
}); });
state_machine.add_transition::<PlayerIdleState, PlayerFallingState>(move |world| { state_machine.add_transition::<IdleState, FallingState>(move |world| {
let is_grounded = world !world
.movements .movements
.with(entity_id, |m| m.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
.unwrap_or(false); .unwrap_or(false)
!is_grounded
}); });
state_machine.add_transition::<PlayerWalkingState, PlayerFallingState>(move |world| { state_machine.add_transition::<WalkingState, FallingState>(move |world| {
let is_grounded = world !world
.movements .movements
.with(entity_id, |m| m.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
.unwrap_or(false); .unwrap_or(false)
!is_grounded
}); });
state_machine.add_transition::<PlayerIdleState, PlayerJumpingState>(move |world| { state_machine.add_transition::<IdleState, JumpingState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
@@ -144,7 +136,7 @@ impl Bundle for PlayerBundle
is_grounded && jump_pressed is_grounded && jump_pressed
}); });
state_machine.add_transition::<PlayerWalkingState, PlayerJumpingState>(move |world| { state_machine.add_transition::<WalkingState, JumpingState>(move |world| {
let is_grounded = world let is_grounded = world
.movements .movements
.with(entity_id, |m| m.movement_context.is_floored) .with(entity_id, |m| m.movement_context.is_floored)
@@ -156,15 +148,76 @@ impl Bundle for PlayerBundle
is_grounded && jump_pressed is_grounded && jump_pressed
}); });
state_machine.add_transition::<PlayerJumpingState, PlayerFallingState>(move |world| { state_machine.add_transition::<JumpingState, FallingState>(move |world| {
let time = world
.jumping_states
.with(entity_id, |s| s.time_in_state)
.unwrap_or(0.0);
world world
.jumps .jumps
.with(entity_id, |jump| { .with(entity_id, |j| time >= j.jump_duration)
jump.jump_context.duration >= jump.jump_duration
})
.unwrap_or(true) .unwrap_or(true)
}); });
state_machine.add_transition::<IdleState, LeapingState>(move |world| {
world
.inputs
.with(entity_id, |i| i.roll_just_pressed)
.unwrap_or(false)
});
state_machine.add_transition::<WalkingState, LeapingState>(move |world| {
world
.inputs
.with(entity_id, |i| i.roll_just_pressed)
.unwrap_or(false)
});
state_machine.add_transition::<LeapingState, RollingState>(move |world| {
world
.leaping_states
.with(entity_id, |s| s.time_in_state >= LEAP_DURATION)
.unwrap_or(false)
});
state_machine.add_transition::<LeapingState, FallingState>(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::<RollingState, IdleState>(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::<RollingState, FallingState>(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.transforms.insert(entity, spawn_transform);
world.movements.insert(entity, MovementComponent::new()); world.movements.insert(entity, MovementComponent::new());
world.jumps.insert(entity, JumpComponent::default()); world.jumps.insert(entity, JumpComponent::default());

View File

@@ -8,6 +8,7 @@ use crate::components::{MeshComponent, PhysicsComponent, TreeInstancesComponent}
use crate::entity::EntityHandle; use crate::entity::EntityHandle;
use crate::loaders::mesh::{InstanceData, InstanceRaw, Mesh}; use crate::loaders::mesh::{InstanceData, InstanceRaw, Mesh};
use crate::loaders::terrain::load_heightfield_from_exr; use crate::loaders::terrain::load_heightfield_from_exr;
use crate::paths;
use crate::physics::PhysicsManager; use crate::physics::PhysicsManager;
use crate::render; use crate::render;
use crate::world::{Transform, World}; use crate::world::{Transform, World};
@@ -33,8 +34,8 @@ impl TerrainConfig
pub fn default() -> Self pub fn default() -> Self
{ {
Self { Self {
gltf_path: "meshes/terrain.gltf".to_string(), gltf_path: paths::meshes::terrain(),
heightmap_path: "textures/terrain_heightmap.exr".to_string(), heightmap_path: paths::textures::terrain_heightmap(),
size: Vec2::new(1000.0, 1000.0), size: Vec2::new(1000.0, 1000.0),
} }
} }

119
src/bundles/test_char.rs Normal file
View File

@@ -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<EntityHandle, String>
{
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::<FallingState>();
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::<FallingState, IdleState>(move |world| {
world
.movements
.with(entity_id, |m| m.movement_context.is_floored)
.unwrap_or(false)
});
state_machine.add_transition::<IdleState, FallingState>(move |world| {
!world
.movements
.with(entity_id, |m| m.movement_context.is_floored)
.unwrap_or(false)
});
state_machine.add_transition::<WalkingState, FallingState>(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)
}
}

92
src/components/dialog.rs Normal file
View File

@@ -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<Self>
{
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<ParryButton>,
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,
}

View File

@@ -6,4 +6,8 @@ pub struct InputComponent
pub move_direction: Vec3, pub move_direction: Vec3,
pub jump_pressed: bool, pub jump_pressed: bool,
pub jump_just_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,
} }

View File

@@ -1,4 +1,5 @@
pub mod camera; pub mod camera;
pub mod dialog;
pub mod dissolve; pub mod dissolve;
pub mod follow; pub mod follow;
pub mod input; pub mod input;
@@ -8,11 +9,16 @@ pub mod mesh;
pub mod movement; pub mod movement;
pub mod noclip; pub mod noclip;
pub mod physics; pub mod physics;
pub mod player_states;
pub mod rotate; pub mod rotate;
pub mod tree_instances; pub mod tree_instances;
pub mod trigger; pub mod trigger;
pub use camera::CameraComponent; pub use camera::CameraComponent;
pub use dialog::{
DialogBubbleComponent, DialogOutcome, DialogOutcomeEvent, DialogPhase,
DialogProjectileComponent, DialogSourceComponent, ParryButton,
};
pub use dissolve::DissolveComponent; pub use dissolve::DissolveComponent;
pub use follow::FollowComponent; pub use follow::FollowComponent;
pub use input::InputComponent; pub use input::InputComponent;
@@ -22,4 +28,6 @@ pub use movement::MovementComponent;
pub use physics::PhysicsComponent; pub use physics::PhysicsComponent;
pub use rotate::RotateComponent; pub use rotate::RotateComponent;
pub use tree_instances::TreeInstancesComponent; pub use tree_instances::TreeInstancesComponent;
pub use trigger::{TriggerComponent, TriggerEvent, TriggerEventKind, TriggerFilter, TriggerShape, TriggerState}; pub use trigger::{
TriggerComponent, TriggerEvent, TriggerEventKind, TriggerFilter, TriggerShape, TriggerState,
};

View File

@@ -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,
}
}
}

View File

@@ -2,7 +2,10 @@ use crate::entity::EntityHandle;
pub enum TriggerShape pub enum TriggerShape
{ {
Sphere { radius: f32 }, Sphere
{
radius: f32
},
} }
pub enum TriggerFilter pub enum TriggerFilter
@@ -23,12 +26,14 @@ pub struct TriggerComponent
pub state: TriggerState, pub state: TriggerState,
} }
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum TriggerEventKind pub enum TriggerEventKind
{ {
Entered, Entered,
Exited, Exited,
} }
#[derive(Clone, Copy)]
pub struct TriggerEvent pub struct TriggerEvent
{ {
pub trigger_entity: EntityHandle, pub trigger_entity: EntityHandle,

View File

@@ -1,4 +1,4 @@
use dear_imgui_rs::{Condition, Context}; use dear_imgui_rs::{Condition, Context, WindowFlags};
use dear_imgui_wgpu::{WgpuInitInfo, WgpuRenderer}; use dear_imgui_wgpu::{WgpuInitInfo, WgpuRenderer};
use glam::EulerRot; use glam::EulerRot;
use sdl3_sys::events::SDL_Event; use sdl3_sys::events::SDL_Event;
@@ -61,16 +61,53 @@ impl Inspector
self.imgui.io().want_capture_mouse() self.imgui.io().want_capture_mouse()
} }
pub fn build_minimal_ui(&mut self, world: &World)
{
let ui = self.imgui.frame();
let state_name = world
.player_tags
.all()
.first()
.and_then(|e| world.state_machines.get(*e))
.map(|sm| sm.get_current_state_name())
.unwrap_or("");
ui.window("Player State")
.position([10.0, 10.0], Condition::FirstUseEver)
.flags(WindowFlags::ALWAYS_AUTO_RESIZE)
.build(|| {
ui.text(format!("State: {}", state_name));
});
}
pub fn build_ui( pub fn build_ui(
&mut self, &mut self,
stats: &FrameStats, stats: &FrameStats,
world: &World, world: &World,
selected_entity: Option<EntityHandle>, selected_entity: Option<EntityHandle>,
show_player_state: bool,
) )
{ {
let ui = self.imgui.frame(); let ui = self.imgui.frame();
if show_player_state
{
let state_name = world
.player_tags
.all()
.first()
.and_then(|e| world.state_machines.get(*e))
.map(|sm| sm.get_current_state_name())
.unwrap_or("");
ui.window("Player State")
.position([10.0, 10.0], Condition::FirstUseEver)
.flags(WindowFlags::ALWAYS_AUTO_RESIZE)
.build(|| {
ui.text(format!("State: {}", state_name));
});
}
ui.window("Inspector") ui.window("Inspector")
.position([10.0, 10.0], Condition::FirstUseEver) .position([10.0, 40.0], Condition::FirstUseEver)
.build(|| { .build(|| {
ui.text(format!("FPS: {:.1}", stats.fps)); ui.text(format!("FPS: {:.1}", stats.fps));
ui.text(format!("Frame: {:.1} ms", stats.frame_ms)); ui.text(format!("Frame: {:.1} ms", stats.frame_ms));
@@ -128,10 +165,7 @@ impl Inspector
} }
if let Some(m) = world.movements.get(entity) if let Some(m) = world.movements.get(entity)
{ {
ui.text(format!( ui.text(format!(" Movement (max_speed {:.1})", m.max_walking_speed));
" Movement (max_speed {:.1})",
m.max_walking_speed
));
} }
if world.jumps.get(entity).is_some() if world.jumps.get(entity).is_some()
{ {

View File

@@ -15,6 +15,7 @@ pub struct EditorState
pub active: bool, pub active: bool,
pub right_mouse_held: bool, pub right_mouse_held: bool,
pub selected_entity: Option<EntityHandle>, pub selected_entity: Option<EntityHandle>,
pub show_player_state: bool,
inspector: Inspector, inspector: Inspector,
} }
@@ -30,6 +31,7 @@ impl EditorState
active: false, active: false,
right_mouse_held: false, right_mouse_held: false,
selected_entity: None, selected_entity: None,
show_player_state: false,
inspector: Inspector::new(device, queue, surface_format), inspector: Inspector::new(device, queue, surface_format),
} }
} }
@@ -60,6 +62,11 @@ impl EditorState
self.inspector.wants_mouse() self.inspector.wants_mouse()
} }
pub fn build_hud(&mut self, world: &World)
{
self.inspector.build_minimal_ui(world);
}
pub fn render(&mut self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) pub fn render(&mut self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView)
{ {
self.inspector.render(encoder, view); self.inspector.render(encoder, view);
@@ -79,5 +86,8 @@ pub fn editor_loop(
camera_noclip_system(world, input_state, delta); camera_noclip_system(world, input_state, delta);
} }
let selected = editor.selected_entity; let selected = editor.selected_entity;
editor.inspector.build_ui(stats, world, selected); let show_player_state = editor.show_player_state;
editor
.inspector
.build_ui(stats, world, selected, show_player_state);
} }

View File

@@ -13,6 +13,7 @@ pub struct Space
pub mesh_data: Vec<(Mesh, Vec<InstanceData>)>, pub mesh_data: Vec<(Mesh, Vec<InstanceData>)>,
pub spotlights: Vec<LightData>, pub spotlights: Vec<LightData>,
pub player_spawn: Vec3, pub player_spawn: Vec3,
pub test_char_spawn: Vec3,
} }
impl Space impl Space
@@ -27,18 +28,20 @@ impl Space
let spotlights = lights.into_spotlights(); 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 { Ok(Space {
mesh_data, mesh_data,
spotlights, spotlights,
player_spawn, player_spawn,
test_char_spawn,
}) })
} }
fn get_player_spawn(gltf_path: &str) -> Result<Vec3> fn get_spawn(gltf_path: &str, name: &str) -> Result<Vec3>
{ {
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 if let Some(empty_node) = empty
{ {
@@ -48,7 +51,7 @@ impl Space
} }
else 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)) Ok(Vec3::new(0.0, 5.0, 0.0))
} }
} }

View File

@@ -4,6 +4,7 @@ mod debug;
mod editor; mod editor;
mod entity; mod entity;
mod loaders; mod loaders;
mod paths;
mod physics; mod physics;
mod picking; mod picking;
mod postprocess; mod postprocess;
@@ -33,18 +34,21 @@ use crate::bundles::camera::CameraBundle;
use crate::bundles::player::PlayerBundle; use crate::bundles::player::PlayerBundle;
use crate::bundles::spotlight::spawn_spotlights; use crate::bundles::spotlight::spawn_spotlights;
use crate::bundles::terrain::{TerrainBundle, TerrainConfig}; use crate::bundles::terrain::{TerrainBundle, TerrainConfig};
use crate::bundles::test_char::TestCharBundle;
use crate::bundles::Bundle; use crate::bundles::Bundle;
use crate::loaders::scene::Space; use crate::loaders::scene::Space;
use crate::physics::PhysicsManager; use crate::physics::PhysicsManager;
use crate::snow::{SnowConfig, SnowLayer}; use crate::snow::{SnowConfig, SnowLayer};
use crate::systems::camera::stop_camera_following;
use crate::systems::{ 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, player_input_system, render_system, rotate_system, snow_system, spotlight_sync_system,
start_camera_following, state_machine_physics_system, state_machine_system, start_camera_following, state_machine_physics_system, state_machine_system,
tree_dissolve_update_system, tree_instance_buffer_update_system, tree_occlusion_system, tree_dissolve_update_system, tree_instance_buffer_update_system, tree_occlusion_system,
trigger_system, trigger_system,
}; };
use crate::systems::camera::stop_camera_following;
use crate::utility::time::Time; use crate::utility::time::Time;
fn main() -> Result<(), Box<dyn std::error::Error>> fn main() -> Result<(), Box<dyn std::error::Error>>
@@ -68,7 +72,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
}); });
editor.init_platform(&window); 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 terrain_config = TerrainConfig::default();
let player_spawn = space.player_spawn; let player_spawn = space.player_spawn;
@@ -87,6 +91,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
} }
.spawn(&mut world) .spawn(&mut world)
.unwrap(); .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)?; let _terrain_entity = TerrainBundle::spawn(&mut world, space.mesh_data, &terrain_config)?;
spawn_spotlights(&mut world, space.spotlights); spawn_spotlights(&mut world, space.spotlights);
@@ -160,7 +171,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
} }
Event::KeyDown { Event::KeyDown {
keycode: Some(Keycode::I), keycode: Some(Keycode::Tab),
repeat: false, repeat: false,
.. ..
} => } =>
@@ -262,6 +273,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
println!("Debug mode: {:?}", debug_mode); 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); camera_input_system(&mut world, &input_state);
if editor.active if editor.active
@@ -270,8 +286,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
} }
else 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); player_input_system(&mut world, &input_state);
if editor.show_player_state
{
editor.build_hud(&world);
}
} }
let physics_start = Instant::now(); let physics_start = Instant::now();
@@ -286,6 +314,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
physics_sync_system(&mut world); physics_sync_system(&mut world);
trigger_system(&mut world); trigger_system(&mut world);
dialog_system(&mut world, FIXED_TIMESTEP);
dialog_projectile_system(&mut world, &input_state);
physics_accumulator -= FIXED_TIMESTEP; physics_accumulator -= FIXED_TIMESTEP;
} }
@@ -322,6 +352,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
if let Some(view) = camera_view_matrix(&world) if let Some(view) = camera_view_matrix(&world)
{ {
let projection = camera_component.projection_matrix(); 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.draw_call_count = draw_calls.len();
stats.fps = 1.0 / delta; stats.fps = 1.0 / delta;
@@ -333,12 +367,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
camera_transform.position, camera_transform.position,
player_pos, player_pos,
&draw_calls, &draw_calls,
&billboard_calls,
time, time,
delta, delta,
debug_mode, debug_mode,
); );
if editor.active if editor.active || editor.show_player_state
{ {
let screen_view = frame let screen_view = frame
.texture .texture

93
src/paths.rs Normal file
View File

@@ -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";
}

View File

@@ -1,3 +1,4 @@
use crate::paths;
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
#[repr(C)] #[repr(C)]
@@ -147,7 +148,7 @@ pub fn create_blit_pipeline(
) -> wgpu::RenderPipeline ) -> wgpu::RenderPipeline
{ {
let shader_source = 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 { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Blit Shader"), label: Some("Blit Shader"),

276
src/render/billboard.rs Normal file
View File

@@ -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::<BillboardVertex>() 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<f32> → offset 0, size 64
/// size : vec2<f32> → 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<f32> → offset 96, size 16
/// border_color : vec4<f32> → 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::<BubbleUniforms>() == 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::<BubbleUniforms>() 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::<BubbleUniforms>() as u64),
}),
}],
});
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Billboard Vertex Buffer"),
size: (MAX_BUBBLES * 4 * std::mem::size_of::<BillboardVertex>()) 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<BillboardVertex> = 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);
}
}
}
}

View File

@@ -1,3 +1,4 @@
use crate::paths;
use crate::postprocess::ScreenVertex; use crate::postprocess::ScreenVertex;
pub struct DebugOverlay pub struct DebugOverlay
@@ -12,7 +13,7 @@ impl DebugOverlay
{ {
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self 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"); .expect("Failed to read debug_overlay.wgsl");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {

View File

@@ -1,14 +1,17 @@
pub mod billboard;
mod bind_group; mod bind_group;
mod debug_overlay; mod debug_overlay;
mod pipeline; mod pipeline;
mod shadow; mod shadow;
mod types; mod types;
pub use billboard::{BillboardDrawCall, BillboardPipeline};
pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS}; pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS};
use crate::entity::EntityHandle; use crate::entity::EntityHandle;
use crate::debug::DebugMode; use crate::debug::DebugMode;
use crate::paths;
use crate::postprocess::{create_blit_pipeline, create_fullscreen_quad, LowResFramebuffer}; use crate::postprocess::{create_blit_pipeline, create_fullscreen_quad, LowResFramebuffer};
use crate::texture::{DitherTextures, FlowmapTexture}; use crate::texture::{DitherTextures, FlowmapTexture};
use pipeline::{ use pipeline::{
@@ -34,6 +37,7 @@ pub struct Renderer
wireframe_pipeline: Option<wgpu::RenderPipeline>, wireframe_pipeline: Option<wgpu::RenderPipeline>,
debug_lines_pipeline: Option<wgpu::RenderPipeline>, debug_lines_pipeline: Option<wgpu::RenderPipeline>,
debug_overlay: Option<debug_overlay::DebugOverlay>, debug_overlay: Option<debug_overlay::DebugOverlay>,
billboard_pipeline: BillboardPipeline,
wireframe_supported: bool, wireframe_supported: bool,
uniform_buffer: wgpu::Buffer, uniform_buffer: wgpu::Buffer,
@@ -162,7 +166,7 @@ impl Renderer
}; };
let flowmap_texture = let flowmap_texture =
match FlowmapTexture::load(&device, &queue, "textures/terrain_flowmap.exr") match FlowmapTexture::load(&device, &queue, &paths::textures::terrain_flowmap())
{ {
Ok(texture) => 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") .expect("Failed to load blue noise texture")
.to_luma8(); .to_luma8();
let blue_noise_size = blue_noise_data.dimensions(); let blue_noise_size = blue_noise_data.dimensions();
@@ -490,6 +494,8 @@ impl Renderer
&bind_group_layout, &bind_group_layout,
)); ));
let billboard_pipeline = BillboardPipeline::new(&device, config.format);
let debug_overlay = Some(debug_overlay::DebugOverlay::new(&device, config.format)); let debug_overlay = Some(debug_overlay::DebugOverlay::new(&device, config.format));
let shadow_bind_group_layout = let shadow_bind_group_layout =
@@ -518,6 +524,7 @@ impl Renderer
wireframe_pipeline, wireframe_pipeline,
debug_lines_pipeline, debug_lines_pipeline,
debug_overlay, debug_overlay,
billboard_pipeline,
wireframe_supported, wireframe_supported,
uniform_buffer, uniform_buffer,
bind_group_layout, bind_group_layout,
@@ -561,6 +568,7 @@ impl Renderer
camera_position: glam::Vec3, camera_position: glam::Vec3,
player_position: glam::Vec3, player_position: glam::Vec3,
draw_calls: &[DrawCall], draw_calls: &[DrawCall],
billboard_calls: &[BillboardDrawCall],
time: f32, time: f32,
delta_time: f32, delta_time: f32,
debug_mode: DebugMode, 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())); self.queue.submit(std::iter::once(encoder.finish()));
let frame = match self.surface.get_current_texture() let frame = match self.surface.get_current_texture()
@@ -1049,7 +1065,7 @@ impl Renderer
match crate::texture::HeightmapTexture::load( match crate::texture::HeightmapTexture::load(
&self.device, &self.device,
&self.queue, &self.queue,
"textures/terrain_heightmap.exr", &paths::textures::terrain_heightmap(),
) )
{ {
Ok(heightmap) => Ok(heightmap) =>
@@ -1151,6 +1167,7 @@ pub fn render(
camera_position: glam::Vec3, camera_position: glam::Vec3,
player_position: glam::Vec3, player_position: glam::Vec3,
draw_calls: &[DrawCall], draw_calls: &[DrawCall],
billboard_calls: &[BillboardDrawCall],
time: f32, time: f32,
delta_time: f32, delta_time: f32,
debug_mode: DebugMode, debug_mode: DebugMode,
@@ -1165,6 +1182,7 @@ pub fn render(
camera_position, camera_position,
player_position, player_position,
draw_calls, draw_calls,
billboard_calls,
time, time,
delta_time, delta_time,
debug_mode, debug_mode,

View File

@@ -1,3 +1,4 @@
use crate::paths;
use wesl::Wesl; use wesl::Wesl;
pub fn create_shadow_pipeline( pub fn create_shadow_pipeline(
@@ -5,9 +6,9 @@ pub fn create_shadow_pipeline(
bind_group_layout: &wgpu::BindGroupLayout, bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline ) -> wgpu::RenderPipeline
{ {
let compiler = Wesl::new("src/shaders"); let compiler = Wesl::new(&paths::SHADERS_DIR);
let shader_source = compiler let shader_source = compiler
.compile(&"package::shadow".parse().unwrap()) .compile(&paths::shaders::SHADOW_PACKAGE.parse().unwrap())
.inspect_err(|e| eprintln!("WESL error: {e}")) .inspect_err(|e| eprintln!("WESL error: {e}"))
.unwrap() .unwrap()
.to_string(); .to_string();
@@ -70,9 +71,9 @@ pub fn create_main_pipeline(
label: &str, label: &str,
) -> wgpu::RenderPipeline ) -> wgpu::RenderPipeline
{ {
let compiler = Wesl::new("src/shaders"); let compiler = Wesl::new(&paths::SHADERS_DIR);
let shader_source = compiler let shader_source = compiler
.compile(&"package::main".parse().unwrap()) .compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
.inspect_err(|e| eprintln!("WESL error: {e}")) .inspect_err(|e| eprintln!("WESL error: {e}"))
.unwrap() .unwrap()
.to_string(); .to_string();
@@ -142,9 +143,9 @@ pub fn create_wireframe_pipeline(
bind_group_layout: &wgpu::BindGroupLayout, bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline ) -> wgpu::RenderPipeline
{ {
let compiler = Wesl::new("src/shaders"); let compiler = Wesl::new(&paths::SHADERS_DIR);
let shader_source = compiler let shader_source = compiler
.compile(&"package::main".parse().unwrap()) .compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
.inspect_err(|e| eprintln!("WESL error: {e}")) .inspect_err(|e| eprintln!("WESL error: {e}"))
.unwrap() .unwrap()
.to_string(); .to_string();
@@ -214,9 +215,9 @@ pub fn create_debug_lines_pipeline(
bind_group_layout: &wgpu::BindGroupLayout, bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline ) -> wgpu::RenderPipeline
{ {
let compiler = Wesl::new("src/shaders"); let compiler = Wesl::new(&paths::SHADERS_DIR);
let shader_source = compiler let shader_source = compiler
.compile(&"package::main".parse().unwrap()) .compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
.inspect_err(|e| eprintln!("WESL error: {e}")) .inspect_err(|e| eprintln!("WESL error: {e}"))
.unwrap() .unwrap()
.to_string(); .to_string();
@@ -286,9 +287,9 @@ pub fn create_snow_clipmap_pipeline(
main_bind_group_layout: &wgpu::BindGroupLayout, main_bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline ) -> wgpu::RenderPipeline
{ {
let compiler = Wesl::new("src/shaders"); let compiler = Wesl::new(&paths::SHADERS_DIR);
let shader_source = compiler let shader_source = compiler
.compile(&"package::main".parse().unwrap()) .compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
.inspect_err(|e| eprintln!("WESL error: {e}")) .inspect_err(|e| eprintln!("WESL error: {e}"))
.unwrap() .unwrap()
.to_string(); .to_string();

118
src/shaders/bubble.wgsl Normal file
View File

@@ -0,0 +1,118 @@
struct Uniforms
{
view_proj: mat4x4<f32>,
size: vec2<f32>,
body_frac: f32,
corner_r: f32,
border_w: f32,
fill_color: vec4<f32>,
border_color: vec4<f32>,
}
@group(0) @binding(0)
var<uniform> u: Uniforms;
struct VertexIn
{
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>,
}
struct VertexOut
{
@builtin(position) clip_pos: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex
fn vs_main(in: VertexIn) -> VertexOut
{
var out: VertexOut;
out.clip_pos = u.view_proj * vec4<f32>(in.position, 1.0);
out.uv = in.uv;
return out;
}
fn sdf_rounded_box(p: vec2<f32>, b: vec2<f32>, r: f32) -> f32
{
let q = abs(p) - b + r;
return length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - r;
}
fn sdf_triangle(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>, c: vec2<f32>) -> 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<f32>(dot(pq0, pq0), s * (v0.x * e0.y - v0.y * e0.x)),
vec2<f32>(dot(pq1, pq1), s * (v1.x * e1.y - v1.y * e1.x)),
),
vec2<f32>(dot(pq2, pq2), s * (v2.x * e2.y - v2.y * e2.x)),
);
return -sqrt(d.x) * sign(d.y);
}
var<private> bayer: array<f32, 16> = array<f32, 16>(
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<f32>
{
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<f32>(
(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<f32>(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<f32>(-tail_base_half, -body_half_h);
let tb = vec2<f32>( tail_base_half, -body_half_h);
let tc = vec2<f32>(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;
}

View File

@@ -6,6 +6,7 @@ use wgpu::util::DeviceExt;
use crate::{ use crate::{
loaders::mesh::{InstanceRaw, Mesh, Vertex}, loaders::mesh::{InstanceRaw, Mesh, Vertex},
paths,
render::{self, DrawCall, Pipeline}, render::{self, DrawCall, Pipeline},
texture::HeightmapTexture, texture::HeightmapTexture,
}; };
@@ -23,8 +24,8 @@ impl SnowConfig
pub fn default() -> Self pub fn default() -> Self
{ {
Self { Self {
depth_map_path: "textures/snow_depth.exr".to_string(), depth_map_path: paths::textures::snow_depth(),
heightmap_path: "textures/terrain_heightmap.exr".to_string(), heightmap_path: paths::textures::terrain_heightmap(),
terrain_size: Vec2::new(1000.0, 1000.0), terrain_size: Vec2::new(1000.0, 1000.0),
resolution: (1000, 1000), resolution: (1000, 1000),
} }
@@ -300,7 +301,7 @@ impl SnowLayer
) -> (wgpu::ComputePipeline, wgpu::BindGroup, wgpu::Buffer) ) -> (wgpu::ComputePipeline, wgpu::BindGroup, wgpu::Buffer)
{ {
render::with_device(|device| { 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"); .expect("Failed to load snow deform shader");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {

View File

@@ -1,3 +1,4 @@
use crate::paths;
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
use glam::Vec2; use glam::Vec2;
use wgpu::util::DeviceExt; 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 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}")) .inspect_err(|e| eprintln!("WESL error: {e}"))
.unwrap() .unwrap()
.to_string(); .to_string();

View File

@@ -1,28 +1,30 @@
use std::any::{Any, TypeId}; use std::any::TypeId;
use std::collections::HashMap; use std::collections::HashMap;
use crate::world::World; use crate::entity::EntityHandle;
use crate::world::{Storage, World};
pub trait StateAgent {} pub trait PlayerState
pub trait State: Any
{ {
fn get_state_name(&self) -> &'static str; fn tick_time(&mut self, _delta: f32) {}
fn on_state_enter(&mut self, world: &mut World) {} fn on_enter(&mut self, _world: &mut World, _entity: EntityHandle) {}
fn on_state_exit(&mut self, world: &mut World) {} fn on_exit(&mut self, _world: &mut World, _entity: EntityHandle) {}
fn on_state_update(&mut self, world: &mut World, delta: f32) {} fn on_update(&mut self, _world: &mut World, _entity: EntityHandle, _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) {}
} }
impl dyn State struct StateOps
{ {
fn dyn_type_id(&self) -> std::any::TypeId name: &'static str,
{ remove: Box<dyn Fn(&mut World, EntityHandle)>,
Any::type_id(self) insert_default: Box<dyn Fn(&mut World, EntityHandle)>,
} on_enter: Box<dyn Fn(&mut World, EntityHandle)>,
on_exit: Box<dyn Fn(&mut World, EntityHandle)>,
tick: Box<dyn Fn(&mut World, EntityHandle, f32)>,
physics_tick: Box<dyn Fn(&mut World, EntityHandle, f32)>,
} }
pub struct StateTransition struct StateTransition
{ {
to_state_id: TypeId, to_state_id: TypeId,
condition: Box<dyn Fn(&World) -> bool>, condition: Box<dyn Fn(&World) -> bool>,
@@ -30,42 +32,93 @@ pub struct StateTransition
pub struct StateMachine pub struct StateMachine
{ {
state_ops: HashMap<TypeId, StateOps>,
state_transitions: HashMap<TypeId, Vec<StateTransition>>, state_transitions: HashMap<TypeId, Vec<StateTransition>>,
current_state_id: TypeId, current_state_id: TypeId,
states: HashMap<TypeId, Box<dyn State>>, }
pub time_in_state: f32,
fn short_type_name<T: 'static>() -> &'static str
{
let full = std::any::type_name::<T>();
full.rsplit("::").next().unwrap_or(full)
} }
impl StateMachine impl StateMachine
{ {
pub fn new(enter_state: Box<dyn State>) -> Self pub fn new<S: 'static>() -> Self
{ {
let state_id = enter_state.dyn_type_id();
let mut states = HashMap::new();
states.insert(state_id, enter_state);
Self { Self {
state_ops: HashMap::new(),
state_transitions: HashMap::new(), state_transitions: HashMap::new(),
current_state_id: state_id, current_state_id: TypeId::of::<S>(),
states,
time_in_state: 0.0,
} }
} }
pub fn update(&mut self, world: &mut World, delta: f32) pub fn register_state<S: PlayerState + Default + 'static>(
&mut self,
storage: fn(&mut World) -> &mut Storage<S>,
)
{
let ops = StateOps {
name: short_type_name::<S>(),
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::<S>(), 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) if let Some(next_state_id) = self.get_transition_state_id(world)
{ {
self.time_in_state = 0.0; self.apply_transition(world, entity, next_state_id);
self.transition_to(world, 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<TypeId> fn get_transition_state_id(&self, world: &World) -> Option<TypeId>
@@ -83,47 +136,30 @@ impl StateMachine
None 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> pub fn add_transition<TFrom: 'static, TTo: 'static>(
{
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<T: State + 'static>(&mut self, state: T)
{
let state_id = TypeId::of::<T>();
self.states.insert(state_id, Box::new(state));
}
pub fn add_transition<TFrom: State + 'static, TTo: State + 'static>(
&mut self, &mut self,
condition: impl Fn(&World) -> bool + 'static, condition: impl Fn(&World) -> bool + 'static,
) )
{ {
let from_id = TypeId::of::<TFrom>(); let from_id = TypeId::of::<TFrom>();
let to_id = TypeId::of::<TTo>(); let to_id = TypeId::of::<TTo>();
let transitions = self.state_transitions.entry(from_id).or_default(); let transitions = self.state_transitions.entry(from_id).or_default();
transitions.push(StateTransition { transitions.push(StateTransition {
to_state_id: to_id, 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) .get(&self.current_state_id)
.map(|transitions| transitions.len()) .map(|ops| ops.name)
.unwrap_or(0) .unwrap_or("Unknown")
} }
} }

346
src/systems/dialog.rs Normal file
View File

@@ -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<EntityHandle> = 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<EntityHandle> = 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<ParryButton>, f32)
{
let mut full_text = String::new();
let mut parry: Option<ParryButton> = 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::<f32>()
{
display_time = t;
}
}
}
}
}
(full_text.trim().to_string(), parry, display_time)
}

View File

@@ -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<Vec3> = 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<Vec3> = 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();
}
}

View File

@@ -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<EntityHandle> = world.projectile_tags.all();
let mut outcomes: Vec<DialogOutcomeEvent> = Vec::new();
let mut to_despawn: Vec<EntityHandle> = 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<DialogOutcome>
{
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()
}

View File

@@ -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<BillboardDrawCall>
{
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
}

View File

@@ -50,6 +50,10 @@ pub fn player_input_system(world: &mut World, input_state: &InputState)
input_component.move_direction = move_direction; input_component.move_direction = move_direction;
input_component.jump_pressed = input_state.space; input_component.jump_pressed = input_state.space;
input_component.jump_just_pressed = input_state.space_just_pressed; 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;
}); });
} }
} }

View File

@@ -1,4 +1,8 @@
pub mod camera; pub mod camera;
pub mod dialog;
pub mod dialog_camera;
pub mod dialog_projectile;
pub mod dialog_render;
pub mod follow; pub mod follow;
pub mod input; pub mod input;
pub mod physics_sync; pub mod physics_sync;
@@ -15,6 +19,10 @@ pub use camera::{
camera_follow_system, camera_input_system, camera_noclip_system, camera_view_matrix, camera_follow_system, camera_input_system, camera_noclip_system, camera_view_matrix,
start_camera_following, 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 input::player_input_system;
pub use physics_sync::physics_sync_system; pub use physics_sync::physics_sync_system;
pub use render::render_system; pub use render::render_system;

View File

@@ -2,121 +2,34 @@ use glam::Vec3;
use kurbo::ParamCurve; use kurbo::ParamCurve;
use rapier3d::math::Vector; use rapier3d::math::Vector;
use crate::components::player_states::{
FallingState, IdleState, JumpingState, LeapingState, RollingState, WalkingState,
};
use crate::entity::EntityHandle; use crate::entity::EntityHandle;
use crate::physics::PhysicsManager; use crate::physics::PhysicsManager;
use crate::state::State; use crate::state::PlayerState;
use crate::utility::time::Time;
use crate::world::World; use crate::world::World;
pub struct PlayerFallingState pub const LEAP_DURATION: f32 = 0.18;
{ pub const ROLL_DURATION: f32 = 0.42;
pub entity: EntityHandle, 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"); world.physics.with(entity, |physics| {
}
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| {
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
let current_velocity = *rigidbody.linvel(); let current_velocity = *rigidbody.linvel();
let idle_damping = world let idle_damping = world
.movements .movements
.with(self.entity, |m| m.idle_damping) .with(entity, |m| m.idle_damping)
.unwrap_or(0.1); .unwrap_or(0.1);
let horizontal_velocity = Vec3::new(current_velocity.x, 0.0, current_velocity.z); 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_physics_update(&mut self, world: &mut World, entity: EntityHandle, _delta: f32)
fn on_state_update(&mut self, _world: &mut World, _delta: f32) {}
fn on_state_physics_update(&mut self, world: &mut World, _delta: f32)
{ {
const GROUND_CHECK_DISTANCE: f32 = 0.6; const GROUND_CHECK_DISTANCE: f32 = 0.6;
let current_translation = world let current_translation = world
.physics .physics
.with(self.entity, |physics| { .with(entity, |physics| {
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
*rigidbody.translation() *rigidbody.translation()
}) })
@@ -162,7 +71,7 @@ impl State for PlayerIdleState
if distance_to_ground.abs() < GROUND_CHECK_DISTANCE 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| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
let next_translation = let next_translation =
Vector::new(current_translation.x, target_y, current_translation.z); 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; movement.movement_context.is_floored = true;
}); });
} }
@@ -178,51 +87,32 @@ impl State for PlayerIdleState
} }
} }
pub struct PlayerWalkingState impl PlayerState for WalkingState
{ {
pub entity: EntityHandle, fn tick_time(&mut self, delta: f32)
pub enter_time_stamp: f32,
}
impl State for PlayerWalkingState
{
fn get_state_name(&self) -> &'static str
{ {
"PlayerWalkingState" self.time_in_state += delta;
} }
fn on_state_enter(&mut self, _world: &mut World) fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32)
{
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)
{ {
let (movement_input, walking_config) = world let (movement_input, walking_config) = world
.movements .movements
.with(self.entity, |movement| { .with(entity, |movement| {
let input = world let input = world
.inputs .inputs
.with(self.entity, |input| input.move_direction) .with(entity, |input| input.move_direction)
.unwrap_or(Vec3::ZERO); .unwrap_or(Vec3::ZERO);
(input, movement.clone()) (input, movement.clone())
}) })
.unwrap(); .unwrap();
let current_time = Time::get_time_elapsed(); let t = (self.time_in_state / walking_config.walking_acceleration_duration).clamp(0.0, 1.0);
let elapsed_time = current_time - self.enter_time_stamp;
let t = (elapsed_time / 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 acceleration_amount = walking_config.walking_acceleration_curve.eval(t as f64).y as f32;
let current_translation = world let current_translation = world
.physics .physics
.with(self.entity, |physics| { .with(entity, |physics| {
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
*rigidbody.translation() *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| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
let current_velocity = *rigidbody.linvel(); 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(); movement.movement_context.is_floored = terrain_height.is_some();
}); });
if movement_input.length_squared() > 0.1 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); let target_rotation = f32::atan2(movement_input.x, movement_input.z);
transform.rotation.y = target_rotation; transform.rotation.y = target_rotation;
}); });
@@ -314,64 +204,44 @@ impl State for PlayerWalkingState
} }
} }
pub struct PlayerJumpingState impl PlayerState for JumpingState
{ {
pub entity: EntityHandle, fn tick_time(&mut self, delta: f32)
pub enter_time_stamp: f32,
}
impl State for PlayerJumpingState
{
fn get_state_name(&self) -> &'static str
{ {
"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.in_progress = true;
jump.jump_context.execution_time = self.enter_time_stamp;
jump.jump_context.origin_height = current_position.y; jump.jump_context.origin_height = current_position.y;
jump.jump_context.duration = 0.0; jump.jump_context.duration = 0.0;
jump.jump_context.normal = Vec3::Y; 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.in_progress = false;
jump.jump_context.duration = 0.0; jump.jump_context.duration = 0.0;
}); });
println!("exited jumping");
} }
fn on_state_update(&mut self, _world: &mut World, _delta: f32) {} fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32)
fn on_state_physics_update(&mut self, world: &mut World, delta: f32)
{ {
let current_time = Time::get_time_elapsed(); world.jumps.with_mut(entity, |jump| {
jump.jump_context.duration = self.time_in_state;
world.jumps.with_mut(self.entity, |jump| {
jump.jump_context.duration =
current_time - jump.jump_context.execution_time;
}); });
let jump = world let jump = world.jumps.with(entity, |jump| jump.clone()).unwrap();
.jumps
.with(self.entity, |jump| jump.clone())
.unwrap();
let elapsed_time = jump.jump_context.duration; let normalized_time = (self.time_in_state / jump.jump_duration).min(1.0);
let normalized_time = (elapsed_time / jump.jump_duration).min(1.0);
let height_progress = jump.jump_curve.eval(normalized_time as f64).y as f32; let height_progress = jump.jump_curve.eval(normalized_time as f64).y as f32;
let origin_height = jump.jump_context.origin_height; let origin_height = jump.jump_context.origin_height;
@@ -379,7 +249,7 @@ impl State for PlayerJumpingState
let current_translation = world let current_translation = world
.physics .physics
.with(self.entity, |physics| { .with(entity, |physics| {
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
*rigidbody.translation() *rigidbody.translation()
}) })
@@ -387,11 +257,10 @@ impl State for PlayerJumpingState
.flatten() .flatten()
.unwrap(); .unwrap();
let current_y = current_translation.y; let height_diff = target_height - current_translation.y;
let height_diff = target_height - current_y;
let required_velocity = height_diff / delta; let required_velocity = height_diff / delta;
world.physics.with(self.entity, |physics| { world.physics.with(entity, |physics| {
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
let current_velocity = *rigidbody.linvel(); let current_velocity = *rigidbody.linvel();
let next_translation = Vector::new( 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; 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();
});
}
}

View File

@@ -2,13 +2,11 @@ use crate::world::World;
pub fn state_machine_system(world: &mut World, delta: f32) pub fn state_machine_system(world: &mut World, delta: f32)
{ {
let entities: Vec<_> = world.state_machines.all(); for entity in world.state_machines.all()
for entity in entities
{ {
if let Some(mut state_machine) = world.state_machines.components.remove(&entity) if let Some(mut state_machine) = world.state_machines.components.remove(&entity)
{ {
state_machine.update(world, delta); state_machine.update(world, entity, delta);
world world
.state_machines .state_machines
.components .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) pub fn state_machine_physics_system(world: &mut World, delta: f32)
{ {
let entities: Vec<_> = world.state_machines.all(); for entity in world.state_machines.all()
for entity in entities
{ {
if let Some(mut state_machine) = world.state_machines.components.remove(&entity) if let Some(mut state_machine) = world.state_machines.components.remove(&entity)
{ {
if let Some(current_state) = state_machine.get_current_state_mut() state_machine.physics_update(world, entity, delta);
{
current_state.on_state_physics_update(world, delta);
}
world world
.state_machines .state_machines
.components .components

View File

@@ -1,6 +1,8 @@
use glam::Vec3; 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::entity::EntityHandle;
use crate::world::World; use crate::world::World;
@@ -35,10 +37,12 @@ pub fn trigger_system(world: &mut World)
let overlapping = match world.triggers.get(trigger_entity) let overlapping = match world.triggers.get(trigger_entity)
{ {
Some(trigger) => activator_positions.iter().any(|(_, pos)| match &trigger.shape Some(trigger) => activator_positions
{ .iter()
TriggerShape::Sphere { radius } => (trigger_pos - *pos).length() < *radius, .any(|(_, pos)| match &trigger.shape
}), {
TriggerShape::Sphere { radius } => (trigger_pos - *pos).length() < *radius,
}),
None => continue, None => continue,
}; };

View File

@@ -1,3 +1,4 @@
use crate::paths;
use anyhow::Result; use anyhow::Result;
use exr::prelude::{ReadChannels, ReadLayers}; use exr::prelude::{ReadChannels, ReadLayers};
@@ -27,10 +28,10 @@ impl DitherTextures
pub fn load_octaves(device: &wgpu::Device, queue: &wgpu::Queue) -> Result<Self> pub fn load_octaves(device: &wgpu::Device, queue: &wgpu::Queue) -> Result<Self>
{ {
let octave_paths = [ let octave_paths = [
"textures/dither/octave_0.png", paths::textures::dither_octave(0),
"textures/dither/octave_1.png", paths::textures::dither_octave(1),
"textures/dither/octave_2.png", paths::textures::dither_octave(2),
"textures/dither/octave_3.png", paths::textures::dither_octave(3),
]; ];
let mut images = Vec::new(); let mut images = Vec::new();
@@ -38,7 +39,7 @@ impl DitherTextures
for path in &octave_paths for path in &octave_paths
{ {
let img = image::open(path)?.to_luma8(); let img = image::open(&path)?.to_luma8();
let (width, height) = img.dimensions(); let (width, height) = img.dimensions();
if texture_size == 0 if texture_size == 0

View File

@@ -9,9 +9,15 @@ pub struct InputState
pub d: bool, pub d: bool,
pub space: bool, pub space: bool,
pub shift: bool, pub shift: bool,
pub lctrl: bool,
pub space_just_pressed: bool, pub space_just_pressed: bool,
pub debug_cycle_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_delta: (f32, f32),
pub mouse_captured: bool, pub mouse_captured: bool,
@@ -29,8 +35,14 @@ impl InputState
d: false, d: false,
space: false, space: false,
shift: false, shift: false,
lctrl: false,
space_just_pressed: false, space_just_pressed: false,
debug_cycle_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_delta: (0.0, 0.0),
mouse_captured: true, mouse_captured: true,
quit_requested: false, quit_requested: false,
@@ -101,7 +113,19 @@ impl InputState
self.space = true; self.space = true;
} }
Keycode::LShift | Keycode::RShift => self.shift = 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::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::D => self.d = false,
Keycode::Space => self.space = false, Keycode::Space => self.space = false,
Keycode::LShift | Keycode::RShift => self.shift = 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.space_just_pressed = false;
self.debug_cycle_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); self.mouse_delta = (0.0, 0.0);
} }

View File

@@ -1,8 +1,14 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::components::dialog::{
DialogBubbleComponent, DialogOutcomeEvent, DialogProjectileComponent, DialogSourceComponent,
};
use crate::components::dissolve::DissolveComponent; use crate::components::dissolve::DissolveComponent;
use crate::components::follow::FollowComponent; use crate::components::follow::FollowComponent;
use crate::components::lights::spot::SpotlightComponent; 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::tree_instances::TreeInstancesComponent;
use crate::components::trigger::{TriggerComponent, TriggerEvent}; use crate::components::trigger::{TriggerComponent, TriggerEvent};
use crate::components::{ use crate::components::{
@@ -79,6 +85,12 @@ pub struct World
pub inputs: Storage<InputComponent>, pub inputs: Storage<InputComponent>,
pub player_tags: Storage<()>, pub player_tags: Storage<()>,
pub state_machines: Storage<StateMachine>, pub state_machines: Storage<StateMachine>,
pub idle_states: Storage<IdleState>,
pub walking_states: Storage<WalkingState>,
pub jumping_states: Storage<JumpingState>,
pub falling_states: Storage<FallingState>,
pub leaping_states: Storage<LeapingState>,
pub rolling_states: Storage<RollingState>,
pub cameras: Storage<CameraComponent>, pub cameras: Storage<CameraComponent>,
pub spotlights: Storage<SpotlightComponent>, pub spotlights: Storage<SpotlightComponent>,
pub tree_tags: Storage<()>, pub tree_tags: Storage<()>,
@@ -89,6 +101,12 @@ pub struct World
pub names: Storage<String>, pub names: Storage<String>,
pub triggers: Storage<TriggerComponent>, pub triggers: Storage<TriggerComponent>,
pub trigger_events: Vec<TriggerEvent>, pub trigger_events: Vec<TriggerEvent>,
pub dialog_sources: Storage<DialogSourceComponent>,
pub dialog_bubbles: Storage<DialogBubbleComponent>,
pub dialog_projectiles: Storage<DialogProjectileComponent>,
pub bubble_tags: Storage<()>,
pub projectile_tags: Storage<()>,
pub dialog_outcomes: Vec<DialogOutcomeEvent>,
} }
impl World impl World
@@ -105,6 +123,12 @@ impl World
inputs: Storage::new(), inputs: Storage::new(),
player_tags: Storage::new(), player_tags: Storage::new(),
state_machines: 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(), cameras: Storage::new(),
spotlights: Storage::new(), spotlights: Storage::new(),
tree_tags: Storage::new(), tree_tags: Storage::new(),
@@ -115,6 +139,12 @@ impl World
names: Storage::new(), names: Storage::new(),
triggers: Storage::new(), triggers: Storage::new(),
trigger_events: Vec::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.inputs.remove(entity);
self.player_tags.remove(entity); self.player_tags.remove(entity);
self.state_machines.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.cameras.remove(entity);
self.spotlights.remove(entity); self.spotlights.remove(entity);
self.tree_tags.remove(entity); self.tree_tags.remove(entity);
@@ -142,6 +178,11 @@ impl World
self.tree_instances.remove(entity); self.tree_instances.remove(entity);
self.names.remove(entity); self.names.remove(entity);
self.triggers.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); self.entities.despawn(entity);
} }

Binary file not shown.

BIN
tools/ink_compiler.dll Normal file

Binary file not shown.

BIN
tools/inklecate Executable file

Binary file not shown.

BIN
tools/inklecate.dll Normal file

Binary file not shown.