dialog WIP paths consolidation and rendering
@@ -21,6 +21,7 @@ exr = "1.72"
|
||||
kurbo = "0.11"
|
||||
nalgebra = { version = "0.34.1", features = ["convert-glam030"] }
|
||||
serde_json = "1.0"
|
||||
bladeink = "1.2"
|
||||
wesl = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
48
assets/dialogs/test_char.ink
Normal 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
|
||||
1
assets/dialogs/test_char.ink.json
Normal 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":{}}
|
||||
@@ -43,7 +43,10 @@
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -58,11 +61,44 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"children":[
|
||||
5,
|
||||
6
|
||||
"mesh":1,
|
||||
"name":"Lighthouse_base",
|
||||
"rotation":[
|
||||
0,
|
||||
-0.31448131799697876,
|
||||
0,
|
||||
0.9492637515068054
|
||||
],
|
||||
"mesh":3,
|
||||
"scale":[
|
||||
25.161100387573242,
|
||||
8.222701072692871,
|
||||
8.222701072692871
|
||||
],
|
||||
"translation":[
|
||||
-367.9805908203125,
|
||||
113.61730194091797,
|
||||
212.62832641601562
|
||||
]
|
||||
},
|
||||
{
|
||||
"mesh":2,
|
||||
"name":"Lighthouse",
|
||||
"scale":[
|
||||
2.1515703201293945,
|
||||
24.724905014038086,
|
||||
2.1515703201293945
|
||||
],
|
||||
"translation":[
|
||||
-343.6542053222656,
|
||||
152.96629333496094,
|
||||
231.81927490234375
|
||||
]
|
||||
},
|
||||
{
|
||||
"children":[
|
||||
8
|
||||
],
|
||||
"mesh":4,
|
||||
"name":"TerrainPlane",
|
||||
"scale":[
|
||||
1.000100016593933,
|
||||
@@ -100,56 +136,37 @@
|
||||
0.9110424518585205
|
||||
],
|
||||
"translation":[
|
||||
-392.0350036621094,
|
||||
238.72787475585938,
|
||||
244.30006408691406
|
||||
-344.301025390625,
|
||||
223.67401123046875,
|
||||
232.61265563964844
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"TestCharSpawn",
|
||||
"translation":[
|
||||
-381.1509704589844,
|
||||
106.53739166259766,
|
||||
107.46959686279297
|
||||
]
|
||||
},
|
||||
{
|
||||
"extensions":{
|
||||
"EXT_mesh_gpu_instancing":{
|
||||
"attributes":{
|
||||
"TRANSLATION":17,
|
||||
"ROTATION":18,
|
||||
"SCALE":19
|
||||
"TRANSLATION":19,
|
||||
"ROTATION":20,
|
||||
"SCALE":21
|
||||
}
|
||||
}
|
||||
},
|
||||
"mesh":1,
|
||||
"mesh":3,
|
||||
"name":"TerrainPlane.0"
|
||||
},
|
||||
{
|
||||
"extensions":{
|
||||
"EXT_mesh_gpu_instancing":{
|
||||
"attributes":{
|
||||
"TRANSLATION":17,
|
||||
"ROTATION":18,
|
||||
"SCALE":19
|
||||
}
|
||||
}
|
||||
},
|
||||
"mesh":2,
|
||||
"name":"TerrainPlane.1"
|
||||
}
|
||||
],
|
||||
"materials":[
|
||||
{
|
||||
"doubleSided":true,
|
||||
"name":"terrain"
|
||||
},
|
||||
{
|
||||
"doubleSided":true,
|
||||
"name":"snow",
|
||||
"pbrMetallicRoughness":{
|
||||
"baseColorFactor":[
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
1
|
||||
],
|
||||
"metallicFactor":0,
|
||||
"roughnessFactor":0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"meshes":[
|
||||
@@ -167,7 +184,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"Cylinder",
|
||||
"name":"Cube",
|
||||
"primitives":[
|
||||
{
|
||||
"attributes":{
|
||||
@@ -175,8 +192,20 @@
|
||||
"NORMAL":5,
|
||||
"TEXCOORD_0":6
|
||||
},
|
||||
"indices":3,
|
||||
"material":0
|
||||
"indices":7
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"Cylinder.001",
|
||||
"primitives":[
|
||||
{
|
||||
"attributes":{
|
||||
"POSITION":8,
|
||||
"NORMAL":9,
|
||||
"TEXCOORD_0":10
|
||||
},
|
||||
"indices":11
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -185,12 +214,11 @@
|
||||
"primitives":[
|
||||
{
|
||||
"attributes":{
|
||||
"POSITION":7,
|
||||
"NORMAL":8,
|
||||
"TEXCOORD_0":9
|
||||
"POSITION":12,
|
||||
"NORMAL":13,
|
||||
"TEXCOORD_0":14
|
||||
},
|
||||
"indices":3,
|
||||
"material":1
|
||||
"indices":3
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -199,21 +227,12 @@
|
||||
"primitives":[
|
||||
{
|
||||
"attributes":{
|
||||
"POSITION":10,
|
||||
"NORMAL":11,
|
||||
"TEXCOORD_0":12
|
||||
"POSITION":15,
|
||||
"NORMAL":16,
|
||||
"TEXCOORD_0":17
|
||||
},
|
||||
"indices":13,
|
||||
"indices":18,
|
||||
"material":0
|
||||
},
|
||||
{
|
||||
"attributes":{
|
||||
"POSITION":14,
|
||||
"NORMAL":15,
|
||||
"TEXCOORD_0":16
|
||||
},
|
||||
"indices":13,
|
||||
"material":1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -256,33 +275,73 @@
|
||||
{
|
||||
"bufferView":4,
|
||||
"componentType":5126,
|
||||
"count":1280,
|
||||
"count":24,
|
||||
"max":[
|
||||
5.561562538146973,
|
||||
16.066009521484375,
|
||||
5.561562538146973
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"min":[
|
||||
-5.561562538146973,
|
||||
0,
|
||||
-5.561562538146973
|
||||
-1,
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"type":"VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView":5,
|
||||
"componentType":5126,
|
||||
"count":1280,
|
||||
"count":24,
|
||||
"type":"VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView":6,
|
||||
"componentType":5126,
|
||||
"count":1280,
|
||||
"count":24,
|
||||
"type":"VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView":7,
|
||||
"componentType":5123,
|
||||
"count":36,
|
||||
"type":"SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView":8,
|
||||
"componentType":5126,
|
||||
"count":704,
|
||||
"max":[
|
||||
6.253715991973877,
|
||||
2.2209415435791016,
|
||||
6.253798961639404
|
||||
],
|
||||
"min":[
|
||||
-6.253823757171631,
|
||||
-1.9492745399475098,
|
||||
-6.25374174118042
|
||||
],
|
||||
"type":"VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView":9,
|
||||
"componentType":5126,
|
||||
"count":704,
|
||||
"type":"VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView":10,
|
||||
"componentType":5126,
|
||||
"count":704,
|
||||
"type":"VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView":11,
|
||||
"componentType":5123,
|
||||
"count":1140,
|
||||
"type":"SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView":12,
|
||||
"componentType":5126,
|
||||
"count":1280,
|
||||
"max":[
|
||||
@@ -298,19 +357,19 @@
|
||||
"type":"VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView":8,
|
||||
"bufferView":13,
|
||||
"componentType":5126,
|
||||
"count":1280,
|
||||
"type":"VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView":9,
|
||||
"bufferView":14,
|
||||
"componentType":5126,
|
||||
"count":1280,
|
||||
"type":"VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView":10,
|
||||
"bufferView":15,
|
||||
"componentType":5126,
|
||||
"count":10404,
|
||||
"max":[
|
||||
@@ -326,65 +385,37 @@
|
||||
"type":"VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView":11,
|
||||
"bufferView":16,
|
||||
"componentType":5126,
|
||||
"count":10404,
|
||||
"type":"VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView":12,
|
||||
"bufferView":17,
|
||||
"componentType":5126,
|
||||
"count":10404,
|
||||
"type":"VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView":13,
|
||||
"bufferView":18,
|
||||
"componentType":5123,
|
||||
"count":61206,
|
||||
"type":"SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView":14,
|
||||
"componentType":5126,
|
||||
"count":10404,
|
||||
"max":[
|
||||
500,
|
||||
110.7568588256836,
|
||||
500
|
||||
],
|
||||
"min":[
|
||||
-500,
|
||||
-0.6476199626922607,
|
||||
-500
|
||||
],
|
||||
"type":"VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView":15,
|
||||
"componentType":5126,
|
||||
"count":10404,
|
||||
"type":"VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView":16,
|
||||
"componentType":5126,
|
||||
"count":10404,
|
||||
"type":"VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView":17,
|
||||
"bufferView":19,
|
||||
"componentType":5126,
|
||||
"count":2380,
|
||||
"type":"VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView":18,
|
||||
"bufferView":20,
|
||||
"componentType":5126,
|
||||
"count":2380,
|
||||
"type":"VEC4"
|
||||
},
|
||||
{
|
||||
"bufferView":19,
|
||||
"bufferView":21,
|
||||
"componentType":5126,
|
||||
"count":2380,
|
||||
"type":"VEC3"
|
||||
@@ -417,101 +448,113 @@
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":15360,
|
||||
"byteLength":288,
|
||||
"byteOffset":45160,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":288,
|
||||
"byteOffset":45448,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":192,
|
||||
"byteOffset":45736,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":72,
|
||||
"byteOffset":45928,
|
||||
"target":34963
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":8448,
|
||||
"byteOffset":46000,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":8448,
|
||||
"byteOffset":54448,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":5632,
|
||||
"byteOffset":62896,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":2280,
|
||||
"byteOffset":68528,
|
||||
"target":34963
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":15360,
|
||||
"byteOffset":60520,
|
||||
"byteOffset":70808,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":15360,
|
||||
"byteOffset":86168,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":10240,
|
||||
"byteOffset":75880,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":15360,
|
||||
"byteOffset":86120,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":15360,
|
||||
"byteOffset":101480,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":10240,
|
||||
"byteOffset":116840,
|
||||
"byteOffset":101528,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":124848,
|
||||
"byteOffset":127080,
|
||||
"byteOffset":111768,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":124848,
|
||||
"byteOffset":251928,
|
||||
"byteOffset":236616,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":83232,
|
||||
"byteOffset":376776,
|
||||
"byteOffset":361464,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":122412,
|
||||
"byteOffset":460008,
|
||||
"byteOffset":444696,
|
||||
"target":34963
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":124848,
|
||||
"byteOffset":582420,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":124848,
|
||||
"byteOffset":707268,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":83232,
|
||||
"byteOffset":832116,
|
||||
"target":34962
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":28560,
|
||||
"byteOffset":915348
|
||||
"byteOffset":567108
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":38080,
|
||||
"byteOffset":943908
|
||||
"byteOffset":595668
|
||||
},
|
||||
{
|
||||
"buffer":0,
|
||||
"byteLength":28560,
|
||||
"byteOffset":981988
|
||||
"byteOffset":633748
|
||||
}
|
||||
],
|
||||
"buffers":[
|
||||
{
|
||||
"byteLength":1010548,
|
||||
"byteLength":662308,
|
||||
"uri":"terrain.bin"
|
||||
}
|
||||
]
|
||||
BIN
assets/meshes/test_char.bin
Normal file
195
assets/meshes/test_char.gltf
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 425 B After Width: | Height: | Size: 425 B |
|
Before Width: | Height: | Size: 751 B After Width: | Height: | Size: 751 B |
|
Before Width: | Height: | Size: 824 B After Width: | Height: | Size: 824 B |
|
Before Width: | Height: | Size: 425 B After Width: | Height: | Size: 425 B |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 392 KiB After Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 910 KiB After Width: | Height: | Size: 910 KiB |
BIN
blender/test_char.blend
Normal file
BIN
blender/test_char.blend1
Normal file
82
build.rs
@@ -1,6 +1,88 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
fn main()
|
||||
{
|
||||
compile_ink_stories();
|
||||
|
||||
let wesl = wesl::Wesl::new("src/shaders");
|
||||
wesl.build_artifact(&"package::main".parse().unwrap(), "main");
|
||||
wesl.build_artifact(&"package::shadow".parse().unwrap(), "shadow");
|
||||
}
|
||||
|
||||
fn compile_ink_stories()
|
||||
{
|
||||
let dialogs_dir = Path::new("assets/dialogs");
|
||||
|
||||
if !dialogs_dir.exists()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let inklecate = Path::new("tools/inklecate.dll");
|
||||
|
||||
if !inklecate.exists()
|
||||
{
|
||||
eprintln!("cargo:warning=tools/inklecate.dll not found — ink stories will not be compiled");
|
||||
return;
|
||||
}
|
||||
|
||||
let entries = match std::fs::read_dir(dialogs_dir)
|
||||
{
|
||||
Ok(e) => e,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
for entry in entries.flatten()
|
||||
{
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("ink")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let ink_path = path.to_str().unwrap();
|
||||
let json_path = format!("{}.json", ink_path);
|
||||
|
||||
println!("cargo:rerun-if-changed={ink_path}");
|
||||
|
||||
let ink_modified = std::fs::metadata(ink_path).and_then(|m| m.modified()).ok();
|
||||
let json_modified = std::fs::metadata(&json_path)
|
||||
.and_then(|m| m.modified())
|
||||
.ok();
|
||||
|
||||
let needs_compile = match (ink_modified, json_modified)
|
||||
{
|
||||
(Some(ink_time), Some(json_time)) => ink_time > json_time,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if !needs_compile
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
println!("cargo:warning=Compiling {ink_path}");
|
||||
|
||||
let status = Command::new("dotnet")
|
||||
.args([inklecate.to_str().unwrap(), "-o", &json_path, ink_path])
|
||||
.status();
|
||||
|
||||
match status
|
||||
{
|
||||
Ok(s) if s.success() =>
|
||||
{
|
||||
println!("cargo:warning=Compiled {ink_path} → {json_path}");
|
||||
}
|
||||
Ok(s) =>
|
||||
{
|
||||
println!("cargo:warning=inklecate exited with {s} for {ink_path}");
|
||||
}
|
||||
Err(e) =>
|
||||
{
|
||||
println!("cargo:warning=Failed to run dotnet inklecate: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod camera;
|
||||
pub mod player;
|
||||
pub mod spotlight;
|
||||
pub mod terrain;
|
||||
pub mod test_char;
|
||||
|
||||
use crate::entity::EntityHandle;
|
||||
use crate::world::World;
|
||||
|
||||
@@ -7,17 +7,19 @@ use rapier3d::prelude::{ColliderBuilder, RigidBodyBuilder};
|
||||
|
||||
use crate::bundles::Bundle;
|
||||
use crate::components::lights::spot::SpotlightComponent;
|
||||
use crate::components::player_states::{
|
||||
FallingState, IdleState, JumpingState, LeapingState, RollingState, WalkingState,
|
||||
};
|
||||
use crate::components::{
|
||||
InputComponent, JumpComponent, MeshComponent, MovementComponent, PhysicsComponent,
|
||||
};
|
||||
use crate::entity::EntityHandle;
|
||||
use crate::loaders::mesh::Mesh;
|
||||
use crate::paths;
|
||||
use crate::physics::PhysicsManager;
|
||||
use crate::render::Pipeline;
|
||||
use crate::state::StateMachine;
|
||||
use crate::systems::player_states::{
|
||||
PlayerFallingState, PlayerIdleState, PlayerJumpingState, PlayerWalkingState,
|
||||
};
|
||||
use crate::systems::player_states::{LEAP_DURATION, ROLL_DURATION};
|
||||
use crate::world::{Transform, World};
|
||||
|
||||
pub struct PlayerBundle
|
||||
@@ -47,28 +49,20 @@ impl Bundle for PlayerBundle
|
||||
let rigidbody_handle = PhysicsManager::add_rigidbody(rigidbody);
|
||||
let collider_handle = PhysicsManager::add_collider(collider, Some(rigidbody_handle));
|
||||
|
||||
let mesh = Mesh::load_mesh("meshes/player_mesh.glb")
|
||||
let mesh = Mesh::load_mesh(&paths::meshes::player())
|
||||
.map_err(|e| format!("missing player mesh: {}", e))?;
|
||||
|
||||
let falling_state = PlayerFallingState { entity };
|
||||
let idle_state = PlayerIdleState { entity };
|
||||
let walking_state = PlayerWalkingState {
|
||||
entity,
|
||||
enter_time_stamp: 0.0,
|
||||
};
|
||||
let jumping_state = PlayerJumpingState {
|
||||
entity,
|
||||
enter_time_stamp: 0.0,
|
||||
};
|
||||
|
||||
let mut state_machine = StateMachine::new(Box::new(falling_state));
|
||||
state_machine.add_state(walking_state);
|
||||
state_machine.add_state(idle_state);
|
||||
state_machine.add_state(jumping_state);
|
||||
let mut state_machine = StateMachine::new::<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);
|
||||
state_machine.register_state(|w: &mut World| &mut w.jumping_states);
|
||||
state_machine.register_state(|w: &mut World| &mut w.leaping_states);
|
||||
state_machine.register_state(|w: &mut World| &mut w.rolling_states);
|
||||
|
||||
let entity_id = entity;
|
||||
|
||||
state_machine.add_transition::<PlayerFallingState, PlayerIdleState>(move |world| {
|
||||
state_machine.add_transition::<FallingState, IdleState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_context.is_floored)
|
||||
@@ -80,7 +74,7 @@ impl Bundle for PlayerBundle
|
||||
is_grounded && !has_input
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerFallingState, PlayerWalkingState>(move |world| {
|
||||
state_machine.add_transition::<FallingState, WalkingState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_context.is_floored)
|
||||
@@ -92,7 +86,7 @@ impl Bundle for PlayerBundle
|
||||
is_grounded && has_input
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerIdleState, PlayerWalkingState>(move |world| {
|
||||
state_machine.add_transition::<IdleState, WalkingState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_context.is_floored)
|
||||
@@ -104,7 +98,7 @@ impl Bundle for PlayerBundle
|
||||
is_grounded && has_input
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerWalkingState, PlayerIdleState>(move |world| {
|
||||
state_machine.add_transition::<WalkingState, IdleState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_context.is_floored)
|
||||
@@ -116,23 +110,21 @@ impl Bundle for PlayerBundle
|
||||
is_grounded && !has_input
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerIdleState, PlayerFallingState>(move |world| {
|
||||
let is_grounded = world
|
||||
state_machine.add_transition::<IdleState, FallingState>(move |world| {
|
||||
!world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_context.is_floored)
|
||||
.unwrap_or(false);
|
||||
!is_grounded
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerWalkingState, PlayerFallingState>(move |world| {
|
||||
let is_grounded = world
|
||||
state_machine.add_transition::<WalkingState, FallingState>(move |world| {
|
||||
!world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_context.is_floored)
|
||||
.unwrap_or(false);
|
||||
!is_grounded
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerIdleState, PlayerJumpingState>(move |world| {
|
||||
state_machine.add_transition::<IdleState, JumpingState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_context.is_floored)
|
||||
@@ -144,7 +136,7 @@ impl Bundle for PlayerBundle
|
||||
is_grounded && jump_pressed
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerWalkingState, PlayerJumpingState>(move |world| {
|
||||
state_machine.add_transition::<WalkingState, JumpingState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_context.is_floored)
|
||||
@@ -156,15 +148,76 @@ impl Bundle for PlayerBundle
|
||||
is_grounded && jump_pressed
|
||||
});
|
||||
|
||||
state_machine.add_transition::<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
|
||||
.jumps
|
||||
.with(entity_id, |jump| {
|
||||
jump.jump_context.duration >= jump.jump_duration
|
||||
})
|
||||
.with(entity_id, |j| time >= j.jump_duration)
|
||||
.unwrap_or(true)
|
||||
});
|
||||
|
||||
state_machine.add_transition::<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.movements.insert(entity, MovementComponent::new());
|
||||
world.jumps.insert(entity, JumpComponent::default());
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::components::{MeshComponent, PhysicsComponent, TreeInstancesComponent}
|
||||
use crate::entity::EntityHandle;
|
||||
use crate::loaders::mesh::{InstanceData, InstanceRaw, Mesh};
|
||||
use crate::loaders::terrain::load_heightfield_from_exr;
|
||||
use crate::paths;
|
||||
use crate::physics::PhysicsManager;
|
||||
use crate::render;
|
||||
use crate::world::{Transform, World};
|
||||
@@ -33,8 +34,8 @@ impl TerrainConfig
|
||||
pub fn default() -> Self
|
||||
{
|
||||
Self {
|
||||
gltf_path: "meshes/terrain.gltf".to_string(),
|
||||
heightmap_path: "textures/terrain_heightmap.exr".to_string(),
|
||||
gltf_path: paths::meshes::terrain(),
|
||||
heightmap_path: paths::textures::terrain_heightmap(),
|
||||
size: Vec2::new(1000.0, 1000.0),
|
||||
}
|
||||
}
|
||||
|
||||
119
src/bundles/test_char.rs
Normal 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
@@ -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,
|
||||
}
|
||||
@@ -6,4 +6,8 @@ pub struct InputComponent
|
||||
pub move_direction: Vec3,
|
||||
pub jump_pressed: bool,
|
||||
pub jump_just_pressed: bool,
|
||||
pub roll_just_pressed: bool,
|
||||
pub parry_i_just_pressed: bool,
|
||||
pub parry_j_just_pressed: bool,
|
||||
pub parry_l_just_pressed: bool,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod camera;
|
||||
pub mod dialog;
|
||||
pub mod dissolve;
|
||||
pub mod follow;
|
||||
pub mod input;
|
||||
@@ -8,11 +9,16 @@ pub mod mesh;
|
||||
pub mod movement;
|
||||
pub mod noclip;
|
||||
pub mod physics;
|
||||
pub mod player_states;
|
||||
pub mod rotate;
|
||||
pub mod tree_instances;
|
||||
pub mod trigger;
|
||||
|
||||
pub use camera::CameraComponent;
|
||||
pub use dialog::{
|
||||
DialogBubbleComponent, DialogOutcome, DialogOutcomeEvent, DialogPhase,
|
||||
DialogProjectileComponent, DialogSourceComponent, ParryButton,
|
||||
};
|
||||
pub use dissolve::DissolveComponent;
|
||||
pub use follow::FollowComponent;
|
||||
pub use input::InputComponent;
|
||||
@@ -22,4 +28,6 @@ pub use movement::MovementComponent;
|
||||
pub use physics::PhysicsComponent;
|
||||
pub use rotate::RotateComponent;
|
||||
pub use tree_instances::TreeInstancesComponent;
|
||||
pub use trigger::{TriggerComponent, TriggerEvent, TriggerEventKind, TriggerFilter, TriggerShape, TriggerState};
|
||||
pub use trigger::{
|
||||
TriggerComponent, TriggerEvent, TriggerEventKind, TriggerFilter, TriggerShape, TriggerState,
|
||||
};
|
||||
|
||||
59
src/components/player_states.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,10 @@ use crate::entity::EntityHandle;
|
||||
|
||||
pub enum TriggerShape
|
||||
{
|
||||
Sphere { radius: f32 },
|
||||
Sphere
|
||||
{
|
||||
radius: f32
|
||||
},
|
||||
}
|
||||
|
||||
pub enum TriggerFilter
|
||||
@@ -23,12 +26,14 @@ pub struct TriggerComponent
|
||||
pub state: TriggerState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TriggerEventKind
|
||||
{
|
||||
Entered,
|
||||
Exited,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct TriggerEvent
|
||||
{
|
||||
pub trigger_entity: EntityHandle,
|
||||
|
||||
@@ -13,6 +13,7 @@ pub struct Space
|
||||
pub mesh_data: Vec<(Mesh, Vec<InstanceData>)>,
|
||||
pub spotlights: Vec<LightData>,
|
||||
pub player_spawn: Vec3,
|
||||
pub test_char_spawn: Vec3,
|
||||
}
|
||||
|
||||
impl Space
|
||||
@@ -27,18 +28,20 @@ impl Space
|
||||
|
||||
let spotlights = lights.into_spotlights();
|
||||
|
||||
let player_spawn = Self::get_player_spawn(gltf_path)?;
|
||||
let player_spawn = Self::get_spawn(gltf_path, "PlayerSpawn")?;
|
||||
let test_char_spawn = Self::get_spawn(gltf_path, "TestCharSpawn")?;
|
||||
|
||||
Ok(Space {
|
||||
mesh_data,
|
||||
spotlights,
|
||||
player_spawn,
|
||||
test_char_spawn,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_player_spawn(gltf_path: &str) -> Result<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
|
||||
{
|
||||
@@ -48,7 +51,7 @@ impl Space
|
||||
}
|
||||
else
|
||||
{
|
||||
println!("Warning: PlayerSpawn empty not found, using default position");
|
||||
println!("Warning: {} empty not found, using default position", name);
|
||||
Ok(Vec3::new(0.0, 5.0, 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
47
src/main.rs
@@ -4,6 +4,7 @@ mod debug;
|
||||
mod editor;
|
||||
mod entity;
|
||||
mod loaders;
|
||||
mod paths;
|
||||
mod physics;
|
||||
mod picking;
|
||||
mod postprocess;
|
||||
@@ -33,18 +34,21 @@ use crate::bundles::camera::CameraBundle;
|
||||
use crate::bundles::player::PlayerBundle;
|
||||
use crate::bundles::spotlight::spawn_spotlights;
|
||||
use crate::bundles::terrain::{TerrainBundle, TerrainConfig};
|
||||
use crate::bundles::test_char::TestCharBundle;
|
||||
use crate::bundles::Bundle;
|
||||
use crate::loaders::scene::Space;
|
||||
use crate::physics::PhysicsManager;
|
||||
use crate::snow::{SnowConfig, SnowLayer};
|
||||
|
||||
use crate::systems::camera::stop_camera_following;
|
||||
use crate::systems::{
|
||||
camera_follow_system, camera_input_system, camera_view_matrix, physics_sync_system,
|
||||
camera_follow_system, camera_input_system, camera_view_matrix, dialog_bubble_render_system,
|
||||
dialog_camera_system, dialog_projectile_system, dialog_system, physics_sync_system,
|
||||
player_input_system, render_system, rotate_system, snow_system, spotlight_sync_system,
|
||||
start_camera_following, state_machine_physics_system, state_machine_system,
|
||||
tree_dissolve_update_system, tree_instance_buffer_update_system, tree_occlusion_system,
|
||||
trigger_system,
|
||||
};
|
||||
use crate::systems::camera::stop_camera_following;
|
||||
use crate::utility::time::Time;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
@@ -68,7 +72,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
});
|
||||
editor.init_platform(&window);
|
||||
|
||||
let space = Space::load_space("meshes/terrain.gltf")?;
|
||||
let space = Space::load_space(&crate::paths::meshes::terrain())?;
|
||||
let terrain_config = TerrainConfig::default();
|
||||
|
||||
let player_spawn = space.player_spawn;
|
||||
@@ -87,6 +91,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
}
|
||||
.spawn(&mut world)
|
||||
.unwrap();
|
||||
|
||||
let _test_char_entity = TestCharBundle {
|
||||
position: space.test_char_spawn,
|
||||
}
|
||||
.spawn(&mut world)
|
||||
.unwrap();
|
||||
|
||||
let _terrain_entity = TerrainBundle::spawn(&mut world, space.mesh_data, &terrain_config)?;
|
||||
spawn_spotlights(&mut world, space.spotlights);
|
||||
|
||||
@@ -160,7 +171,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
}
|
||||
|
||||
Event::KeyDown {
|
||||
keycode: Some(Keycode::I),
|
||||
keycode: Some(Keycode::Tab),
|
||||
repeat: false,
|
||||
..
|
||||
} =>
|
||||
@@ -262,6 +273,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
println!("Debug mode: {:?}", debug_mode);
|
||||
}
|
||||
|
||||
if input_state.f2_just_pressed
|
||||
{
|
||||
editor.show_player_state = !editor.show_player_state;
|
||||
}
|
||||
|
||||
camera_input_system(&mut world, &input_state);
|
||||
|
||||
if editor.active
|
||||
@@ -270,8 +286,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
}
|
||||
else
|
||||
{
|
||||
camera_follow_system(&mut world);
|
||||
let dialog_active = !world.bubble_tags.all().is_empty();
|
||||
if dialog_active
|
||||
{
|
||||
dialog_camera_system(&mut world, delta);
|
||||
}
|
||||
else
|
||||
{
|
||||
camera_follow_system(&mut world);
|
||||
}
|
||||
player_input_system(&mut world, &input_state);
|
||||
if editor.show_player_state
|
||||
{
|
||||
editor.build_hud(&world);
|
||||
}
|
||||
}
|
||||
|
||||
let physics_start = Instant::now();
|
||||
@@ -286,6 +314,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
|
||||
physics_sync_system(&mut world);
|
||||
trigger_system(&mut world);
|
||||
dialog_system(&mut world, FIXED_TIMESTEP);
|
||||
dialog_projectile_system(&mut world, &input_state);
|
||||
|
||||
physics_accumulator -= FIXED_TIMESTEP;
|
||||
}
|
||||
@@ -322,6 +352,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
if let Some(view) = camera_view_matrix(&world)
|
||||
{
|
||||
let projection = camera_component.projection_matrix();
|
||||
let view_proj = projection * view;
|
||||
|
||||
let billboard_calls =
|
||||
dialog_bubble_render_system(&world, camera_transform.position, view_proj);
|
||||
|
||||
stats.draw_call_count = draw_calls.len();
|
||||
stats.fps = 1.0 / delta;
|
||||
@@ -333,12 +367,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
camera_transform.position,
|
||||
player_pos,
|
||||
&draw_calls,
|
||||
&billboard_calls,
|
||||
time,
|
||||
delta,
|
||||
debug_mode,
|
||||
);
|
||||
|
||||
if editor.active
|
||||
if editor.active || editor.show_player_state
|
||||
{
|
||||
let screen_view = frame
|
||||
.texture
|
||||
|
||||
93
src/paths.rs
Normal 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";
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::paths;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
#[repr(C)]
|
||||
@@ -147,7 +148,7 @@ pub fn create_blit_pipeline(
|
||||
) -> wgpu::RenderPipeline
|
||||
{
|
||||
let shader_source =
|
||||
std::fs::read_to_string("src/shaders/blit.wgsl").expect("Failed to read blit shader");
|
||||
std::fs::read_to_string(&paths::shaders::blit()).expect("Failed to read blit shader");
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Blit Shader"),
|
||||
|
||||
276
src/render/billboard.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::paths;
|
||||
use crate::postprocess::ScreenVertex;
|
||||
|
||||
pub struct DebugOverlay
|
||||
@@ -12,7 +13,7 @@ impl DebugOverlay
|
||||
{
|
||||
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self
|
||||
{
|
||||
let shader_source = std::fs::read_to_string("src/shaders/debug_overlay.wgsl")
|
||||
let shader_source = std::fs::read_to_string(&paths::shaders::debug_overlay())
|
||||
.expect("Failed to read debug_overlay.wgsl");
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
pub mod billboard;
|
||||
mod bind_group;
|
||||
mod debug_overlay;
|
||||
mod pipeline;
|
||||
mod shadow;
|
||||
mod types;
|
||||
|
||||
pub use billboard::{BillboardDrawCall, BillboardPipeline};
|
||||
pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS};
|
||||
|
||||
use crate::entity::EntityHandle;
|
||||
|
||||
use crate::debug::DebugMode;
|
||||
use crate::paths;
|
||||
use crate::postprocess::{create_blit_pipeline, create_fullscreen_quad, LowResFramebuffer};
|
||||
use crate::texture::{DitherTextures, FlowmapTexture};
|
||||
use pipeline::{
|
||||
@@ -34,6 +37,7 @@ pub struct Renderer
|
||||
wireframe_pipeline: Option<wgpu::RenderPipeline>,
|
||||
debug_lines_pipeline: Option<wgpu::RenderPipeline>,
|
||||
debug_overlay: Option<debug_overlay::DebugOverlay>,
|
||||
billboard_pipeline: BillboardPipeline,
|
||||
wireframe_supported: bool,
|
||||
|
||||
uniform_buffer: wgpu::Buffer,
|
||||
@@ -162,7 +166,7 @@ impl Renderer
|
||||
};
|
||||
|
||||
let flowmap_texture =
|
||||
match FlowmapTexture::load(&device, &queue, "textures/terrain_flowmap.exr")
|
||||
match FlowmapTexture::load(&device, &queue, &paths::textures::terrain_flowmap())
|
||||
{
|
||||
Ok(texture) =>
|
||||
{
|
||||
@@ -179,7 +183,7 @@ impl Renderer
|
||||
}
|
||||
};
|
||||
|
||||
let blue_noise_data = image::open("textures/blue_noise.png")
|
||||
let blue_noise_data = image::open(&paths::textures::blue_noise())
|
||||
.expect("Failed to load blue noise texture")
|
||||
.to_luma8();
|
||||
let blue_noise_size = blue_noise_data.dimensions();
|
||||
@@ -490,6 +494,8 @@ impl Renderer
|
||||
&bind_group_layout,
|
||||
));
|
||||
|
||||
let billboard_pipeline = BillboardPipeline::new(&device, config.format);
|
||||
|
||||
let debug_overlay = Some(debug_overlay::DebugOverlay::new(&device, config.format));
|
||||
|
||||
let shadow_bind_group_layout =
|
||||
@@ -518,6 +524,7 @@ impl Renderer
|
||||
wireframe_pipeline,
|
||||
debug_lines_pipeline,
|
||||
debug_overlay,
|
||||
billboard_pipeline,
|
||||
wireframe_supported,
|
||||
uniform_buffer,
|
||||
bind_group_layout,
|
||||
@@ -561,6 +568,7 @@ impl Renderer
|
||||
camera_position: glam::Vec3,
|
||||
player_position: glam::Vec3,
|
||||
draw_calls: &[DrawCall],
|
||||
billboard_calls: &[BillboardDrawCall],
|
||||
time: f32,
|
||||
delta_time: f32,
|
||||
debug_mode: DebugMode,
|
||||
@@ -939,6 +947,14 @@ impl Renderer
|
||||
}
|
||||
}
|
||||
|
||||
self.billboard_pipeline.render(
|
||||
&mut encoder,
|
||||
&self.queue,
|
||||
&self.framebuffer.view,
|
||||
&self.framebuffer.depth_view,
|
||||
billboard_calls,
|
||||
);
|
||||
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
|
||||
let frame = match self.surface.get_current_texture()
|
||||
@@ -1049,7 +1065,7 @@ impl Renderer
|
||||
match crate::texture::HeightmapTexture::load(
|
||||
&self.device,
|
||||
&self.queue,
|
||||
"textures/terrain_heightmap.exr",
|
||||
&paths::textures::terrain_heightmap(),
|
||||
)
|
||||
{
|
||||
Ok(heightmap) =>
|
||||
@@ -1151,6 +1167,7 @@ pub fn render(
|
||||
camera_position: glam::Vec3,
|
||||
player_position: glam::Vec3,
|
||||
draw_calls: &[DrawCall],
|
||||
billboard_calls: &[BillboardDrawCall],
|
||||
time: f32,
|
||||
delta_time: f32,
|
||||
debug_mode: DebugMode,
|
||||
@@ -1165,6 +1182,7 @@ pub fn render(
|
||||
camera_position,
|
||||
player_position,
|
||||
draw_calls,
|
||||
billboard_calls,
|
||||
time,
|
||||
delta_time,
|
||||
debug_mode,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::paths;
|
||||
use wesl::Wesl;
|
||||
|
||||
pub fn create_shadow_pipeline(
|
||||
@@ -5,9 +6,9 @@ pub fn create_shadow_pipeline(
|
||||
bind_group_layout: &wgpu::BindGroupLayout,
|
||||
) -> wgpu::RenderPipeline
|
||||
{
|
||||
let compiler = Wesl::new("src/shaders");
|
||||
let compiler = Wesl::new(&paths::SHADERS_DIR);
|
||||
let shader_source = compiler
|
||||
.compile(&"package::shadow".parse().unwrap())
|
||||
.compile(&paths::shaders::SHADOW_PACKAGE.parse().unwrap())
|
||||
.inspect_err(|e| eprintln!("WESL error: {e}"))
|
||||
.unwrap()
|
||||
.to_string();
|
||||
@@ -70,9 +71,9 @@ pub fn create_main_pipeline(
|
||||
label: &str,
|
||||
) -> wgpu::RenderPipeline
|
||||
{
|
||||
let compiler = Wesl::new("src/shaders");
|
||||
let compiler = Wesl::new(&paths::SHADERS_DIR);
|
||||
let shader_source = compiler
|
||||
.compile(&"package::main".parse().unwrap())
|
||||
.compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
|
||||
.inspect_err(|e| eprintln!("WESL error: {e}"))
|
||||
.unwrap()
|
||||
.to_string();
|
||||
@@ -142,9 +143,9 @@ pub fn create_wireframe_pipeline(
|
||||
bind_group_layout: &wgpu::BindGroupLayout,
|
||||
) -> wgpu::RenderPipeline
|
||||
{
|
||||
let compiler = Wesl::new("src/shaders");
|
||||
let compiler = Wesl::new(&paths::SHADERS_DIR);
|
||||
let shader_source = compiler
|
||||
.compile(&"package::main".parse().unwrap())
|
||||
.compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
|
||||
.inspect_err(|e| eprintln!("WESL error: {e}"))
|
||||
.unwrap()
|
||||
.to_string();
|
||||
@@ -214,9 +215,9 @@ pub fn create_debug_lines_pipeline(
|
||||
bind_group_layout: &wgpu::BindGroupLayout,
|
||||
) -> wgpu::RenderPipeline
|
||||
{
|
||||
let compiler = Wesl::new("src/shaders");
|
||||
let compiler = Wesl::new(&paths::SHADERS_DIR);
|
||||
let shader_source = compiler
|
||||
.compile(&"package::main".parse().unwrap())
|
||||
.compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
|
||||
.inspect_err(|e| eprintln!("WESL error: {e}"))
|
||||
.unwrap()
|
||||
.to_string();
|
||||
@@ -286,9 +287,9 @@ pub fn create_snow_clipmap_pipeline(
|
||||
main_bind_group_layout: &wgpu::BindGroupLayout,
|
||||
) -> wgpu::RenderPipeline
|
||||
{
|
||||
let compiler = Wesl::new("src/shaders");
|
||||
let compiler = Wesl::new(&paths::SHADERS_DIR);
|
||||
let shader_source = compiler
|
||||
.compile(&"package::main".parse().unwrap())
|
||||
.compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
|
||||
.inspect_err(|e| eprintln!("WESL error: {e}"))
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
118
src/shaders/bubble.wgsl
Normal 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;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use wgpu::util::DeviceExt;
|
||||
|
||||
use crate::{
|
||||
loaders::mesh::{InstanceRaw, Mesh, Vertex},
|
||||
paths,
|
||||
render::{self, DrawCall, Pipeline},
|
||||
texture::HeightmapTexture,
|
||||
};
|
||||
@@ -23,8 +24,8 @@ impl SnowConfig
|
||||
pub fn default() -> Self
|
||||
{
|
||||
Self {
|
||||
depth_map_path: "textures/snow_depth.exr".to_string(),
|
||||
heightmap_path: "textures/terrain_heightmap.exr".to_string(),
|
||||
depth_map_path: paths::textures::snow_depth(),
|
||||
heightmap_path: paths::textures::terrain_heightmap(),
|
||||
terrain_size: Vec2::new(1000.0, 1000.0),
|
||||
resolution: (1000, 1000),
|
||||
}
|
||||
@@ -300,7 +301,7 @@ impl SnowLayer
|
||||
) -> (wgpu::ComputePipeline, wgpu::BindGroup, wgpu::Buffer)
|
||||
{
|
||||
render::with_device(|device| {
|
||||
let shader_source = std::fs::read_to_string("src/shaders/snow_deform.wgsl")
|
||||
let shader_source = std::fs::read_to_string(&paths::shaders::snow_deform())
|
||||
.expect("Failed to load snow deform shader");
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::paths;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use glam::Vec2;
|
||||
use wgpu::util::DeviceExt;
|
||||
@@ -170,9 +171,13 @@ impl SnowLightAccumulation
|
||||
],
|
||||
});
|
||||
|
||||
let compiler = wesl::Wesl::new("src/shaders");
|
||||
let compiler = wesl::Wesl::new(&paths::SHADERS_DIR);
|
||||
let shader_source = compiler
|
||||
.compile(&"package::snow_light_accumulation".parse().unwrap())
|
||||
.compile(
|
||||
&paths::shaders::SNOW_LIGHT_ACCUMULATION_PACKAGE
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.inspect_err(|e| eprintln!("WESL error: {e}"))
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
160
src/state.rs
@@ -1,28 +1,30 @@
|
||||
use std::any::{Any, TypeId};
|
||||
use std::any::TypeId;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::world::World;
|
||||
use crate::entity::EntityHandle;
|
||||
use crate::world::{Storage, World};
|
||||
|
||||
pub trait StateAgent {}
|
||||
|
||||
pub trait State: Any
|
||||
pub trait PlayerState
|
||||
{
|
||||
fn get_state_name(&self) -> &'static str;
|
||||
fn on_state_enter(&mut self, world: &mut World) {}
|
||||
fn on_state_exit(&mut self, world: &mut World) {}
|
||||
fn on_state_update(&mut self, world: &mut World, delta: f32) {}
|
||||
fn on_state_physics_update(&mut self, world: &mut World, delta: f32) {}
|
||||
fn tick_time(&mut self, _delta: f32) {}
|
||||
fn on_enter(&mut self, _world: &mut World, _entity: EntityHandle) {}
|
||||
fn on_exit(&mut self, _world: &mut World, _entity: EntityHandle) {}
|
||||
fn on_update(&mut self, _world: &mut World, _entity: EntityHandle, _delta: f32) {}
|
||||
fn on_physics_update(&mut self, _world: &mut World, _entity: EntityHandle, _delta: f32) {}
|
||||
}
|
||||
|
||||
impl dyn State
|
||||
struct StateOps
|
||||
{
|
||||
fn dyn_type_id(&self) -> std::any::TypeId
|
||||
{
|
||||
Any::type_id(self)
|
||||
}
|
||||
name: &'static str,
|
||||
remove: Box<dyn Fn(&mut World, EntityHandle)>,
|
||||
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,
|
||||
condition: Box<dyn Fn(&World) -> bool>,
|
||||
@@ -30,42 +32,93 @@ pub struct StateTransition
|
||||
|
||||
pub struct StateMachine
|
||||
{
|
||||
state_ops: HashMap<TypeId, StateOps>,
|
||||
state_transitions: HashMap<TypeId, Vec<StateTransition>>,
|
||||
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
|
||||
{
|
||||
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 {
|
||||
state_ops: HashMap::new(),
|
||||
state_transitions: HashMap::new(),
|
||||
current_state_id: state_id,
|
||||
states,
|
||||
time_in_state: 0.0,
|
||||
current_state_id: TypeId::of::<S>(),
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
self.time_in_state = 0.0;
|
||||
self.transition_to(world, next_state_id);
|
||||
self.apply_transition(world, entity, next_state_id);
|
||||
}
|
||||
|
||||
if let Some(current_state) = self.states.get_mut(&self.current_state_id)
|
||||
if let Some(ops) = self.state_ops.get(&self.current_state_id)
|
||||
{
|
||||
current_state.on_state_update(world, delta);
|
||||
(ops.tick)(world, entity, delta);
|
||||
}
|
||||
}
|
||||
|
||||
self.time_in_state += delta;
|
||||
pub fn physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32)
|
||||
{
|
||||
if let Some(ops) = self.state_ops.get(&self.current_state_id)
|
||||
{
|
||||
(ops.physics_tick)(world, entity, delta);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_transition_state_id(&self, world: &World) -> Option<TypeId>
|
||||
@@ -83,47 +136,30 @@ impl StateMachine
|
||||
None
|
||||
}
|
||||
|
||||
fn transition_to(&mut self, world: &mut World, new_state_id: TypeId)
|
||||
fn apply_transition(&mut self, world: &mut World, entity: EntityHandle, next_id: TypeId)
|
||||
{
|
||||
if let Some(current_state) = self.states.get_mut(&self.current_state_id)
|
||||
if let Some(ops) = self.state_ops.get(&self.current_state_id)
|
||||
{
|
||||
current_state.on_state_exit(world);
|
||||
(ops.on_exit)(world, entity);
|
||||
(ops.remove)(world, entity);
|
||||
}
|
||||
|
||||
self.current_state_id = new_state_id;
|
||||
self.current_state_id = next_id;
|
||||
|
||||
if let Some(new_state) = self.states.get_mut(&self.current_state_id)
|
||||
if let Some(ops) = self.state_ops.get(&next_id)
|
||||
{
|
||||
new_state.on_state_enter(world);
|
||||
(ops.insert_default)(world, entity);
|
||||
(ops.on_enter)(world, entity);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_current_state(&self) -> Option<&dyn State>
|
||||
{
|
||||
self.states.get(&self.current_state_id).map(|b| b.as_ref())
|
||||
}
|
||||
|
||||
pub fn get_current_state_mut(&mut self) -> Option<&mut dyn State>
|
||||
{
|
||||
self.states
|
||||
.get_mut(&self.current_state_id)
|
||||
.map(|b| b.as_mut())
|
||||
}
|
||||
|
||||
pub fn add_state<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>(
|
||||
pub fn add_transition<TFrom: 'static, TTo: 'static>(
|
||||
&mut self,
|
||||
condition: impl Fn(&World) -> bool + 'static,
|
||||
)
|
||||
{
|
||||
let from_id = TypeId::of::<TFrom>();
|
||||
let to_id = TypeId::of::<TTo>();
|
||||
|
||||
let transitions = self.state_transitions.entry(from_id).or_default();
|
||||
transitions.push(StateTransition {
|
||||
to_state_id: to_id,
|
||||
@@ -131,11 +167,11 @@ impl StateMachine
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_available_transitions_count(&self) -> usize
|
||||
pub fn get_current_state_name(&self) -> &'static str
|
||||
{
|
||||
self.state_transitions
|
||||
self.state_ops
|
||||
.get(&self.current_state_id)
|
||||
.map(|transitions| transitions.len())
|
||||
.unwrap_or(0)
|
||||
.map(|ops| ops.name)
|
||||
.unwrap_or("Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
346
src/systems/dialog.rs
Normal 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)
|
||||
}
|
||||
74
src/systems/dialog_camera.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
182
src/systems/dialog_projectile.rs
Normal 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()
|
||||
}
|
||||
109
src/systems/dialog_render.rs
Normal 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
|
||||
}
|
||||
@@ -50,6 +50,10 @@ pub fn player_input_system(world: &mut World, input_state: &InputState)
|
||||
input_component.move_direction = move_direction;
|
||||
input_component.jump_pressed = input_state.space;
|
||||
input_component.jump_just_pressed = input_state.space_just_pressed;
|
||||
input_component.roll_just_pressed = input_state.roll_just_pressed;
|
||||
input_component.parry_i_just_pressed = input_state.i_just_pressed;
|
||||
input_component.parry_j_just_pressed = input_state.j_just_pressed;
|
||||
input_component.parry_l_just_pressed = input_state.l_just_pressed;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
pub mod camera;
|
||||
pub mod dialog;
|
||||
pub mod dialog_camera;
|
||||
pub mod dialog_projectile;
|
||||
pub mod dialog_render;
|
||||
pub mod follow;
|
||||
pub mod input;
|
||||
pub mod physics_sync;
|
||||
@@ -15,6 +19,10 @@ pub use camera::{
|
||||
camera_follow_system, camera_input_system, camera_noclip_system, camera_view_matrix,
|
||||
start_camera_following,
|
||||
};
|
||||
pub use dialog::dialog_system;
|
||||
pub use dialog_camera::dialog_camera_system;
|
||||
pub use dialog_projectile::dialog_projectile_system;
|
||||
pub use dialog_render::dialog_bubble_render_system;
|
||||
pub use input::player_input_system;
|
||||
pub use physics_sync::physics_sync_system;
|
||||
pub use render::render_system;
|
||||
|
||||
@@ -2,121 +2,34 @@ use glam::Vec3;
|
||||
use kurbo::ParamCurve;
|
||||
use rapier3d::math::Vector;
|
||||
|
||||
use crate::components::player_states::{
|
||||
FallingState, IdleState, JumpingState, LeapingState, RollingState, WalkingState,
|
||||
};
|
||||
use crate::entity::EntityHandle;
|
||||
use crate::physics::PhysicsManager;
|
||||
use crate::state::State;
|
||||
use crate::utility::time::Time;
|
||||
use crate::state::PlayerState;
|
||||
use crate::world::World;
|
||||
|
||||
pub struct PlayerFallingState
|
||||
{
|
||||
pub entity: EntityHandle,
|
||||
}
|
||||
pub const LEAP_DURATION: f32 = 0.18;
|
||||
pub const ROLL_DURATION: f32 = 0.42;
|
||||
const LEAP_SPEED: f32 = 18.0;
|
||||
const ROLL_SPEED: f32 = 14.0;
|
||||
|
||||
impl State for PlayerFallingState
|
||||
impl PlayerState for IdleState
|
||||
{
|
||||
fn get_state_name(&self) -> &'static str
|
||||
fn tick_time(&mut self, delta: f32)
|
||||
{
|
||||
"PlayerFallingState"
|
||||
self.time_in_state += delta;
|
||||
}
|
||||
|
||||
fn on_state_enter(&mut self, _world: &mut World)
|
||||
fn on_enter(&mut self, world: &mut World, entity: EntityHandle)
|
||||
{
|
||||
println!("entered falling");
|
||||
}
|
||||
|
||||
fn on_state_exit(&mut self, _world: &mut World) {}
|
||||
|
||||
fn on_state_update(&mut self, _world: &mut World, _delta: f32) {}
|
||||
|
||||
fn on_state_physics_update(&mut self, world: &mut World, delta: f32)
|
||||
{
|
||||
const GRAVITY: f32 = -9.81 * 5.0;
|
||||
const GROUND_CHECK_DISTANCE: f32 = 0.6;
|
||||
|
||||
let (current_pos, velocity) = world
|
||||
.physics
|
||||
.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let mut vel = *rigidbody.linvel();
|
||||
vel.y += GRAVITY * delta;
|
||||
(*rigidbody.translation(), vel)
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.unwrap();
|
||||
|
||||
let next_pos = current_pos + velocity;
|
||||
let terrain_height = PhysicsManager::get_terrain_height_at(next_pos.x, next_pos.z);
|
||||
|
||||
let is_grounded = if let Some(height) = terrain_height
|
||||
{
|
||||
let target_y = height + 1.0;
|
||||
let distance_to_ground = current_pos.y - target_y;
|
||||
|
||||
if distance_to_ground < GROUND_CHECK_DISTANCE && velocity.y <= 0.01
|
||||
{
|
||||
world.physics.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let next_pos = Vector::new(current_pos.x, target_y, current_pos.z);
|
||||
rigidbody.set_next_kinematic_translation(next_pos);
|
||||
rigidbody.set_linvel(Vector::new(velocity.x, 0.0, velocity.z), true);
|
||||
});
|
||||
});
|
||||
true
|
||||
}
|
||||
else
|
||||
{
|
||||
world.physics.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let next_pos = current_pos + velocity * delta;
|
||||
rigidbody.set_next_kinematic_translation(next_pos);
|
||||
rigidbody.set_linvel(velocity, true);
|
||||
});
|
||||
});
|
||||
false
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
world.physics.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let next_pos = current_pos + velocity * delta;
|
||||
rigidbody.set_next_kinematic_translation(next_pos);
|
||||
rigidbody.set_linvel(velocity, true);
|
||||
});
|
||||
});
|
||||
false
|
||||
};
|
||||
|
||||
world.movements.with_mut(self.entity, |movement| {
|
||||
movement.movement_context.is_floored = is_grounded;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PlayerIdleState
|
||||
{
|
||||
pub entity: EntityHandle,
|
||||
}
|
||||
|
||||
impl State for PlayerIdleState
|
||||
{
|
||||
fn get_state_name(&self) -> &'static str
|
||||
{
|
||||
"PlayerIdleState"
|
||||
}
|
||||
|
||||
fn on_state_enter(&mut self, world: &mut World)
|
||||
{
|
||||
println!("entered idle");
|
||||
|
||||
world.physics.with(self.entity, |physics| {
|
||||
world.physics.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let current_velocity = *rigidbody.linvel();
|
||||
let idle_damping = world
|
||||
.movements
|
||||
.with(self.entity, |m| m.idle_damping)
|
||||
.with(entity, |m| m.idle_damping)
|
||||
.unwrap_or(0.1);
|
||||
|
||||
let horizontal_velocity = Vec3::new(current_velocity.x, 0.0, current_velocity.z);
|
||||
@@ -134,17 +47,13 @@ impl State for PlayerIdleState
|
||||
});
|
||||
}
|
||||
|
||||
fn on_state_exit(&mut self, _world: &mut World) {}
|
||||
|
||||
fn on_state_update(&mut self, _world: &mut World, _delta: f32) {}
|
||||
|
||||
fn on_state_physics_update(&mut self, world: &mut World, _delta: f32)
|
||||
fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, _delta: f32)
|
||||
{
|
||||
const GROUND_CHECK_DISTANCE: f32 = 0.6;
|
||||
|
||||
let current_translation = world
|
||||
.physics
|
||||
.with(self.entity, |physics| {
|
||||
.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
*rigidbody.translation()
|
||||
})
|
||||
@@ -162,7 +71,7 @@ impl State for PlayerIdleState
|
||||
|
||||
if distance_to_ground.abs() < GROUND_CHECK_DISTANCE
|
||||
{
|
||||
world.physics.with(self.entity, |physics| {
|
||||
world.physics.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let next_translation =
|
||||
Vector::new(current_translation.x, target_y, current_translation.z);
|
||||
@@ -170,7 +79,7 @@ impl State for PlayerIdleState
|
||||
});
|
||||
});
|
||||
|
||||
world.movements.with_mut(self.entity, |movement| {
|
||||
world.movements.with_mut(entity, |movement| {
|
||||
movement.movement_context.is_floored = true;
|
||||
});
|
||||
}
|
||||
@@ -178,51 +87,32 @@ impl State for PlayerIdleState
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PlayerWalkingState
|
||||
impl PlayerState for WalkingState
|
||||
{
|
||||
pub entity: EntityHandle,
|
||||
pub enter_time_stamp: f32,
|
||||
}
|
||||
|
||||
impl State for PlayerWalkingState
|
||||
{
|
||||
fn get_state_name(&self) -> &'static str
|
||||
fn tick_time(&mut self, delta: f32)
|
||||
{
|
||||
"PlayerWalkingState"
|
||||
self.time_in_state += delta;
|
||||
}
|
||||
|
||||
fn on_state_enter(&mut self, _world: &mut World)
|
||||
{
|
||||
self.enter_time_stamp = Time::get_time_elapsed();
|
||||
println!("entered walking");
|
||||
}
|
||||
|
||||
fn on_state_exit(&mut self, _world: &mut World) {}
|
||||
|
||||
fn on_state_update(&mut self, _world: &mut World, _delta: f32) {}
|
||||
|
||||
fn on_state_physics_update(&mut self, world: &mut World, delta: f32)
|
||||
fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32)
|
||||
{
|
||||
let (movement_input, walking_config) = world
|
||||
.movements
|
||||
.with(self.entity, |movement| {
|
||||
.with(entity, |movement| {
|
||||
let input = world
|
||||
.inputs
|
||||
.with(self.entity, |input| input.move_direction)
|
||||
.with(entity, |input| input.move_direction)
|
||||
.unwrap_or(Vec3::ZERO);
|
||||
(input, movement.clone())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let current_time = Time::get_time_elapsed();
|
||||
let elapsed_time = current_time - self.enter_time_stamp;
|
||||
|
||||
let t = (elapsed_time / walking_config.walking_acceleration_duration).clamp(0.0, 1.0);
|
||||
let t = (self.time_in_state / walking_config.walking_acceleration_duration).clamp(0.0, 1.0);
|
||||
let acceleration_amount = walking_config.walking_acceleration_curve.eval(t as f64).y as f32;
|
||||
|
||||
let current_translation = world
|
||||
.physics
|
||||
.with(self.entity, |physics| {
|
||||
.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
*rigidbody.translation()
|
||||
})
|
||||
@@ -271,7 +161,7 @@ impl State for PlayerWalkingState
|
||||
}
|
||||
};
|
||||
|
||||
world.physics.with(self.entity, |physics| {
|
||||
world.physics.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let current_velocity = *rigidbody.linvel();
|
||||
|
||||
@@ -300,13 +190,13 @@ impl State for PlayerWalkingState
|
||||
});
|
||||
});
|
||||
|
||||
world.movements.with_mut(self.entity, |movement| {
|
||||
world.movements.with_mut(entity, |movement| {
|
||||
movement.movement_context.is_floored = terrain_height.is_some();
|
||||
});
|
||||
|
||||
if movement_input.length_squared() > 0.1
|
||||
{
|
||||
world.transforms.with_mut(self.entity, |transform| {
|
||||
world.transforms.with_mut(entity, |transform| {
|
||||
let target_rotation = f32::atan2(movement_input.x, movement_input.z);
|
||||
transform.rotation.y = target_rotation;
|
||||
});
|
||||
@@ -314,64 +204,44 @@ impl State for PlayerWalkingState
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PlayerJumpingState
|
||||
impl PlayerState for JumpingState
|
||||
{
|
||||
pub entity: EntityHandle,
|
||||
pub enter_time_stamp: f32,
|
||||
}
|
||||
|
||||
impl State for PlayerJumpingState
|
||||
{
|
||||
fn get_state_name(&self) -> &'static str
|
||||
fn tick_time(&mut self, delta: f32)
|
||||
{
|
||||
"PlayerJumpingState"
|
||||
self.time_in_state += delta;
|
||||
}
|
||||
|
||||
fn on_state_enter(&mut self, world: &mut World)
|
||||
fn on_enter(&mut self, world: &mut World, entity: EntityHandle)
|
||||
{
|
||||
self.enter_time_stamp = Time::get_time_elapsed();
|
||||
self.time_in_state = 0.0;
|
||||
|
||||
let current_position = world.transforms.get(self.entity).unwrap().position;
|
||||
let current_position = world.transforms.get(entity).unwrap().position;
|
||||
|
||||
world.jumps.with_mut(self.entity, |jump| {
|
||||
world.jumps.with_mut(entity, |jump| {
|
||||
jump.jump_context.in_progress = true;
|
||||
jump.jump_context.execution_time = self.enter_time_stamp;
|
||||
jump.jump_context.origin_height = current_position.y;
|
||||
jump.jump_context.duration = 0.0;
|
||||
jump.jump_context.normal = Vec3::Y;
|
||||
});
|
||||
|
||||
println!("entered jumping");
|
||||
}
|
||||
|
||||
fn on_state_exit(&mut self, world: &mut World)
|
||||
fn on_exit(&mut self, world: &mut World, entity: EntityHandle)
|
||||
{
|
||||
world.jumps.with_mut(self.entity, |jump| {
|
||||
world.jumps.with_mut(entity, |jump| {
|
||||
jump.jump_context.in_progress = false;
|
||||
jump.jump_context.duration = 0.0;
|
||||
});
|
||||
|
||||
println!("exited jumping");
|
||||
}
|
||||
|
||||
fn on_state_update(&mut self, _world: &mut World, _delta: f32) {}
|
||||
|
||||
fn on_state_physics_update(&mut self, world: &mut World, delta: f32)
|
||||
fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32)
|
||||
{
|
||||
let current_time = Time::get_time_elapsed();
|
||||
|
||||
world.jumps.with_mut(self.entity, |jump| {
|
||||
jump.jump_context.duration =
|
||||
current_time - jump.jump_context.execution_time;
|
||||
world.jumps.with_mut(entity, |jump| {
|
||||
jump.jump_context.duration = self.time_in_state;
|
||||
});
|
||||
|
||||
let jump = world
|
||||
.jumps
|
||||
.with(self.entity, |jump| jump.clone())
|
||||
.unwrap();
|
||||
let jump = world.jumps.with(entity, |jump| jump.clone()).unwrap();
|
||||
|
||||
let elapsed_time = jump.jump_context.duration;
|
||||
let normalized_time = (elapsed_time / jump.jump_duration).min(1.0);
|
||||
let normalized_time = (self.time_in_state / jump.jump_duration).min(1.0);
|
||||
let height_progress = jump.jump_curve.eval(normalized_time as f64).y as f32;
|
||||
|
||||
let origin_height = jump.jump_context.origin_height;
|
||||
@@ -379,7 +249,7 @@ impl State for PlayerJumpingState
|
||||
|
||||
let current_translation = world
|
||||
.physics
|
||||
.with(self.entity, |physics| {
|
||||
.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
*rigidbody.translation()
|
||||
})
|
||||
@@ -387,11 +257,10 @@ impl State for PlayerJumpingState
|
||||
.flatten()
|
||||
.unwrap();
|
||||
|
||||
let current_y = current_translation.y;
|
||||
let height_diff = target_height - current_y;
|
||||
let height_diff = target_height - current_translation.y;
|
||||
let required_velocity = height_diff / delta;
|
||||
|
||||
world.physics.with(self.entity, |physics| {
|
||||
world.physics.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let current_velocity = *rigidbody.linvel();
|
||||
let next_translation = Vector::new(
|
||||
@@ -408,8 +277,220 @@ impl State for PlayerJumpingState
|
||||
});
|
||||
});
|
||||
|
||||
world.movements.with_mut(self.entity, |movement| {
|
||||
world.movements.with_mut(entity, |movement| {
|
||||
movement.movement_context.is_floored = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayerState for FallingState
|
||||
{
|
||||
fn tick_time(&mut self, delta: f32)
|
||||
{
|
||||
self.time_in_state += delta;
|
||||
}
|
||||
|
||||
fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32)
|
||||
{
|
||||
const GRAVITY: f32 = -9.81 * 5.0;
|
||||
const GROUND_CHECK_DISTANCE: f32 = 0.6;
|
||||
|
||||
let (current_pos, velocity) = world
|
||||
.physics
|
||||
.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let mut vel = *rigidbody.linvel();
|
||||
vel.y += GRAVITY * delta;
|
||||
(*rigidbody.translation(), vel)
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.unwrap();
|
||||
|
||||
let next_pos = current_pos + velocity;
|
||||
let terrain_height = PhysicsManager::get_terrain_height_at(next_pos.x, next_pos.z);
|
||||
|
||||
let is_grounded = if let Some(height) = terrain_height
|
||||
{
|
||||
let target_y = height + 1.0;
|
||||
let distance_to_ground = current_pos.y - target_y;
|
||||
|
||||
if distance_to_ground < GROUND_CHECK_DISTANCE && velocity.y <= 0.01
|
||||
{
|
||||
world.physics.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let next_pos = Vector::new(current_pos.x, target_y, current_pos.z);
|
||||
rigidbody.set_next_kinematic_translation(next_pos);
|
||||
rigidbody.set_linvel(Vector::new(velocity.x, 0.0, velocity.z), true);
|
||||
});
|
||||
});
|
||||
true
|
||||
}
|
||||
else
|
||||
{
|
||||
world.physics.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let next_pos = current_pos + velocity * delta;
|
||||
rigidbody.set_next_kinematic_translation(next_pos);
|
||||
rigidbody.set_linvel(velocity, true);
|
||||
});
|
||||
});
|
||||
false
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
world.physics.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let next_pos = current_pos + velocity * delta;
|
||||
rigidbody.set_next_kinematic_translation(next_pos);
|
||||
rigidbody.set_linvel(velocity, true);
|
||||
});
|
||||
});
|
||||
false
|
||||
};
|
||||
|
||||
world.movements.with_mut(entity, |movement| {
|
||||
movement.movement_context.is_floored = is_grounded;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayerState for LeapingState
|
||||
{
|
||||
fn tick_time(&mut self, delta: f32)
|
||||
{
|
||||
self.time_in_state += delta;
|
||||
}
|
||||
|
||||
fn on_enter(&mut self, world: &mut World, entity: EntityHandle)
|
||||
{
|
||||
self.time_in_state = 0.0;
|
||||
|
||||
let facing = world
|
||||
.transforms
|
||||
.with(entity, |t| {
|
||||
let yaw = t.rotation.y;
|
||||
Vec3::new(yaw.sin(), 0.0, yaw.cos())
|
||||
})
|
||||
.unwrap_or(Vec3::Z);
|
||||
|
||||
let move_dir = world
|
||||
.inputs
|
||||
.with(entity, |i| i.move_direction)
|
||||
.unwrap_or(Vec3::ZERO);
|
||||
|
||||
self.leap_direction = if move_dir.length_squared() > 0.01
|
||||
{
|
||||
move_dir
|
||||
}
|
||||
else
|
||||
{
|
||||
facing
|
||||
};
|
||||
}
|
||||
|
||||
fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32)
|
||||
{
|
||||
let current_translation = world
|
||||
.physics
|
||||
.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| *rb.translation())
|
||||
})
|
||||
.flatten()
|
||||
.unwrap();
|
||||
|
||||
let terrain_height =
|
||||
PhysicsManager::get_terrain_height_at(current_translation.x, current_translation.z);
|
||||
let target_y = terrain_height
|
||||
.map(|h| h + 1.0)
|
||||
.unwrap_or(current_translation.y);
|
||||
|
||||
let velocity = self.leap_direction * LEAP_SPEED;
|
||||
|
||||
world.physics.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| {
|
||||
let next = Vector::new(
|
||||
current_translation.x + velocity.x * delta,
|
||||
target_y,
|
||||
current_translation.z + velocity.z * delta,
|
||||
);
|
||||
rb.set_linvel(Vector::new(velocity.x, 0.0, velocity.z), true);
|
||||
rb.set_next_kinematic_translation(next);
|
||||
});
|
||||
});
|
||||
|
||||
world.movements.with_mut(entity, |m| {
|
||||
m.movement_context.is_floored = terrain_height.is_some();
|
||||
});
|
||||
|
||||
world.transforms.with_mut(entity, |t| {
|
||||
t.rotation.y = f32::atan2(self.leap_direction.x, self.leap_direction.z);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayerState for RollingState
|
||||
{
|
||||
fn tick_time(&mut self, delta: f32)
|
||||
{
|
||||
self.time_in_state += delta;
|
||||
}
|
||||
|
||||
fn on_enter(&mut self, world: &mut World, entity: EntityHandle)
|
||||
{
|
||||
self.time_in_state = 0.0;
|
||||
|
||||
self.roll_direction = world
|
||||
.inputs
|
||||
.with(entity, |i| i.move_direction)
|
||||
.filter(|d| d.length_squared() > 0.01)
|
||||
.unwrap_or_else(|| {
|
||||
world
|
||||
.transforms
|
||||
.with(entity, |t| {
|
||||
let yaw = t.rotation.y;
|
||||
Vec3::new(yaw.sin(), 0.0, yaw.cos())
|
||||
})
|
||||
.unwrap_or(Vec3::Z)
|
||||
});
|
||||
}
|
||||
|
||||
fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32)
|
||||
{
|
||||
let t = (self.time_in_state / ROLL_DURATION).clamp(0.0, 1.0);
|
||||
let speed = ROLL_SPEED * (1.0 - t * 0.6);
|
||||
|
||||
let current_translation = world
|
||||
.physics
|
||||
.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| *rb.translation())
|
||||
})
|
||||
.flatten()
|
||||
.unwrap();
|
||||
|
||||
let terrain_height =
|
||||
PhysicsManager::get_terrain_height_at(current_translation.x, current_translation.z);
|
||||
let target_y = terrain_height
|
||||
.map(|h| h + 1.0)
|
||||
.unwrap_or(current_translation.y);
|
||||
|
||||
let velocity = self.roll_direction * speed;
|
||||
|
||||
world.physics.with(entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| {
|
||||
let next = Vector::new(
|
||||
current_translation.x + velocity.x * delta,
|
||||
target_y,
|
||||
current_translation.z + velocity.z * delta,
|
||||
);
|
||||
rb.set_linvel(Vector::new(velocity.x, 0.0, velocity.z), true);
|
||||
rb.set_next_kinematic_translation(next);
|
||||
});
|
||||
});
|
||||
|
||||
world.movements.with_mut(entity, |m| {
|
||||
m.movement_context.is_floored = terrain_height.is_some();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@ use crate::world::World;
|
||||
|
||||
pub fn state_machine_system(world: &mut World, delta: f32)
|
||||
{
|
||||
let entities: Vec<_> = world.state_machines.all();
|
||||
|
||||
for entity in entities
|
||||
for entity in world.state_machines.all()
|
||||
{
|
||||
if let Some(mut state_machine) = world.state_machines.components.remove(&entity)
|
||||
{
|
||||
state_machine.update(world, delta);
|
||||
state_machine.update(world, entity, delta);
|
||||
world
|
||||
.state_machines
|
||||
.components
|
||||
@@ -19,16 +17,11 @@ pub fn state_machine_system(world: &mut World, delta: f32)
|
||||
|
||||
pub fn state_machine_physics_system(world: &mut World, delta: f32)
|
||||
{
|
||||
let entities: Vec<_> = world.state_machines.all();
|
||||
|
||||
for entity in entities
|
||||
for entity in world.state_machines.all()
|
||||
{
|
||||
if let Some(mut state_machine) = world.state_machines.components.remove(&entity)
|
||||
{
|
||||
if let Some(current_state) = state_machine.get_current_state_mut()
|
||||
{
|
||||
current_state.on_state_physics_update(world, delta);
|
||||
}
|
||||
state_machine.physics_update(world, entity, delta);
|
||||
world
|
||||
.state_machines
|
||||
.components
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::components::trigger::{TriggerEvent, TriggerEventKind, TriggerFilter, TriggerShape, TriggerState};
|
||||
use crate::components::trigger::{
|
||||
TriggerEvent, TriggerEventKind, TriggerFilter, TriggerShape, TriggerState,
|
||||
};
|
||||
use crate::entity::EntityHandle;
|
||||
use crate::world::World;
|
||||
|
||||
@@ -35,10 +37,12 @@ pub fn trigger_system(world: &mut World)
|
||||
|
||||
let overlapping = match world.triggers.get(trigger_entity)
|
||||
{
|
||||
Some(trigger) => activator_positions.iter().any(|(_, pos)| match &trigger.shape
|
||||
{
|
||||
TriggerShape::Sphere { radius } => (trigger_pos - *pos).length() < *radius,
|
||||
}),
|
||||
Some(trigger) => activator_positions
|
||||
.iter()
|
||||
.any(|(_, pos)| match &trigger.shape
|
||||
{
|
||||
TriggerShape::Sphere { radius } => (trigger_pos - *pos).length() < *radius,
|
||||
}),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::paths;
|
||||
use anyhow::Result;
|
||||
use exr::prelude::{ReadChannels, ReadLayers};
|
||||
|
||||
@@ -27,10 +28,10 @@ impl DitherTextures
|
||||
pub fn load_octaves(device: &wgpu::Device, queue: &wgpu::Queue) -> Result<Self>
|
||||
{
|
||||
let octave_paths = [
|
||||
"textures/dither/octave_0.png",
|
||||
"textures/dither/octave_1.png",
|
||||
"textures/dither/octave_2.png",
|
||||
"textures/dither/octave_3.png",
|
||||
paths::textures::dither_octave(0),
|
||||
paths::textures::dither_octave(1),
|
||||
paths::textures::dither_octave(2),
|
||||
paths::textures::dither_octave(3),
|
||||
];
|
||||
|
||||
let mut images = Vec::new();
|
||||
@@ -38,7 +39,7 @@ impl DitherTextures
|
||||
|
||||
for path in &octave_paths
|
||||
{
|
||||
let img = image::open(path)?.to_luma8();
|
||||
let img = image::open(&path)?.to_luma8();
|
||||
let (width, height) = img.dimensions();
|
||||
|
||||
if texture_size == 0
|
||||
|
||||
@@ -9,9 +9,15 @@ pub struct InputState
|
||||
pub d: bool,
|
||||
pub space: bool,
|
||||
pub shift: bool,
|
||||
pub lctrl: bool,
|
||||
|
||||
pub space_just_pressed: bool,
|
||||
pub debug_cycle_just_pressed: bool,
|
||||
pub i_just_pressed: bool,
|
||||
pub j_just_pressed: bool,
|
||||
pub l_just_pressed: bool,
|
||||
pub roll_just_pressed: bool,
|
||||
pub f2_just_pressed: bool,
|
||||
|
||||
pub mouse_delta: (f32, f32),
|
||||
pub mouse_captured: bool,
|
||||
@@ -29,8 +35,14 @@ impl InputState
|
||||
d: false,
|
||||
space: false,
|
||||
shift: false,
|
||||
lctrl: false,
|
||||
space_just_pressed: false,
|
||||
debug_cycle_just_pressed: false,
|
||||
i_just_pressed: false,
|
||||
j_just_pressed: false,
|
||||
l_just_pressed: false,
|
||||
roll_just_pressed: false,
|
||||
f2_just_pressed: false,
|
||||
mouse_delta: (0.0, 0.0),
|
||||
mouse_captured: true,
|
||||
quit_requested: false,
|
||||
@@ -101,7 +113,19 @@ impl InputState
|
||||
self.space = true;
|
||||
}
|
||||
Keycode::LShift | Keycode::RShift => self.shift = true,
|
||||
Keycode::LCtrl =>
|
||||
{
|
||||
if !self.lctrl
|
||||
{
|
||||
self.roll_just_pressed = true;
|
||||
}
|
||||
self.lctrl = true;
|
||||
}
|
||||
Keycode::I => self.i_just_pressed = true,
|
||||
Keycode::J => self.j_just_pressed = true,
|
||||
Keycode::L => self.l_just_pressed = true,
|
||||
Keycode::F1 => self.debug_cycle_just_pressed = true,
|
||||
Keycode::F2 => self.f2_just_pressed = true,
|
||||
_ =>
|
||||
{}
|
||||
}
|
||||
@@ -117,6 +141,7 @@ impl InputState
|
||||
Keycode::D => self.d = false,
|
||||
Keycode::Space => self.space = false,
|
||||
Keycode::LShift | Keycode::RShift => self.shift = false,
|
||||
Keycode::LCtrl => self.lctrl = false,
|
||||
_ =>
|
||||
{}
|
||||
}
|
||||
@@ -134,6 +159,11 @@ impl InputState
|
||||
{
|
||||
self.space_just_pressed = false;
|
||||
self.debug_cycle_just_pressed = false;
|
||||
self.i_just_pressed = false;
|
||||
self.j_just_pressed = false;
|
||||
self.l_just_pressed = false;
|
||||
self.roll_just_pressed = false;
|
||||
self.f2_just_pressed = false;
|
||||
self.mouse_delta = (0.0, 0.0);
|
||||
}
|
||||
|
||||
|
||||
41
src/world.rs
@@ -1,8 +1,14 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::components::dialog::{
|
||||
DialogBubbleComponent, DialogOutcomeEvent, DialogProjectileComponent, DialogSourceComponent,
|
||||
};
|
||||
use crate::components::dissolve::DissolveComponent;
|
||||
use crate::components::follow::FollowComponent;
|
||||
use crate::components::lights::spot::SpotlightComponent;
|
||||
use crate::components::player_states::{
|
||||
FallingState, IdleState, JumpingState, LeapingState, RollingState, WalkingState,
|
||||
};
|
||||
use crate::components::tree_instances::TreeInstancesComponent;
|
||||
use crate::components::trigger::{TriggerComponent, TriggerEvent};
|
||||
use crate::components::{
|
||||
@@ -79,6 +85,12 @@ pub struct World
|
||||
pub inputs: Storage<InputComponent>,
|
||||
pub player_tags: Storage<()>,
|
||||
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 spotlights: Storage<SpotlightComponent>,
|
||||
pub tree_tags: Storage<()>,
|
||||
@@ -89,6 +101,12 @@ pub struct World
|
||||
pub names: Storage<String>,
|
||||
pub triggers: Storage<TriggerComponent>,
|
||||
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
|
||||
@@ -105,6 +123,12 @@ impl World
|
||||
inputs: Storage::new(),
|
||||
player_tags: Storage::new(),
|
||||
state_machines: Storage::new(),
|
||||
idle_states: Storage::new(),
|
||||
walking_states: Storage::new(),
|
||||
jumping_states: Storage::new(),
|
||||
falling_states: Storage::new(),
|
||||
leaping_states: Storage::new(),
|
||||
rolling_states: Storage::new(),
|
||||
cameras: Storage::new(),
|
||||
spotlights: Storage::new(),
|
||||
tree_tags: Storage::new(),
|
||||
@@ -115,6 +139,12 @@ impl World
|
||||
names: Storage::new(),
|
||||
triggers: Storage::new(),
|
||||
trigger_events: Vec::new(),
|
||||
dialog_sources: Storage::new(),
|
||||
dialog_bubbles: Storage::new(),
|
||||
dialog_projectiles: Storage::new(),
|
||||
bubble_tags: Storage::new(),
|
||||
projectile_tags: Storage::new(),
|
||||
dialog_outcomes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +163,12 @@ impl World
|
||||
self.inputs.remove(entity);
|
||||
self.player_tags.remove(entity);
|
||||
self.state_machines.remove(entity);
|
||||
self.idle_states.remove(entity);
|
||||
self.walking_states.remove(entity);
|
||||
self.jumping_states.remove(entity);
|
||||
self.falling_states.remove(entity);
|
||||
self.leaping_states.remove(entity);
|
||||
self.rolling_states.remove(entity);
|
||||
self.cameras.remove(entity);
|
||||
self.spotlights.remove(entity);
|
||||
self.tree_tags.remove(entity);
|
||||
@@ -142,6 +178,11 @@ impl World
|
||||
self.tree_instances.remove(entity);
|
||||
self.names.remove(entity);
|
||||
self.triggers.remove(entity);
|
||||
self.dialog_sources.remove(entity);
|
||||
self.dialog_bubbles.remove(entity);
|
||||
self.dialog_projectiles.remove(entity);
|
||||
self.bubble_tags.remove(entity);
|
||||
self.projectile_tags.remove(entity);
|
||||
self.entities.despawn(entity);
|
||||
}
|
||||
|
||||
|
||||