Compare commits
61
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3948607b1e | ||
|
|
6c180372fe | ||
|
|
fed04b66b8 | ||
|
|
a4ca718070 | ||
|
|
8dbd3e1dd2 | ||
|
|
7a9a2b9832 | ||
|
|
e8757c33e3 | ||
|
|
b498a12458 | ||
|
|
c9f098693c | ||
|
|
7aeffac9fc | ||
|
|
37e7e95ace | ||
|
|
e3f56c4d12 | ||
|
|
3675ec634f | ||
|
|
d8273a225c | ||
|
|
c1c8174b27 | ||
|
|
5697f5c064 | ||
|
|
3ce16cf7f4 | ||
|
|
2abd5b0c3c | ||
|
|
4b29bf7a30 | ||
|
|
6f1bbe4f2f | ||
|
|
f64364f37d | ||
|
|
d16e0bbfbf | ||
|
|
70ae41bd29 | ||
|
|
abf6863044 | ||
|
|
4279bd9675 | ||
|
|
5910110b95 | ||
|
|
44594f7011 | ||
|
|
dbb5301ade | ||
|
|
74a4290be4 | ||
|
|
3ce892f846 | ||
|
|
68cebce04a | ||
|
|
1646e964b5 | ||
|
|
2ee97ad4ce | ||
|
|
109ddc9e1e | ||
|
|
804f8aa1fa | ||
|
|
49b97ba508 | ||
|
|
6c11d8165d | ||
|
|
93554b39ba | ||
|
|
6af0e9cf28 | ||
|
|
083deb8357 | ||
|
|
ffb375faeb | ||
|
|
b8cbd8ff6a | ||
|
|
c2bd761383 | ||
|
|
878e3438c8 | ||
|
|
3da65370e0 | ||
|
|
83e646cba9 | ||
|
|
0566166af4 | ||
|
|
ed0b8c3911 | ||
|
|
03d753a83f | ||
|
|
cf51ed1988 | ||
|
|
70f46e89d3 | ||
|
|
efd760fb2e | ||
|
|
f303d24f4a | ||
|
|
9688e88a7c | ||
|
|
c8a6325615 | ||
|
|
798db14bb5 | ||
|
|
178d78ef72 | ||
|
|
8139a0ea54 | ||
|
|
327c2bec70 | ||
|
|
fc0195d9f7 | ||
|
|
db42bca44b |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"moves": {
|
||||
"fire": {
|
||||
// Target flags
|
||||
"enemies": true,
|
||||
"allies": true,
|
||||
"multiple": false,
|
||||
"self": true,
|
||||
"default": "enemy",
|
||||
"target": "SINGLE_ENEMY"
|
||||
},
|
||||
|
||||
"slash": {
|
||||
"enemies": true,
|
||||
"allies": true,
|
||||
"self": false,
|
||||
"default": "enemy",
|
||||
"target": "SINGLE_ENEMY"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"type": "MODAL",
|
||||
"title": "Fire",
|
||||
"message": "A burst of flame scorches the target."
|
||||
},
|
||||
{
|
||||
"type": "WAIT",
|
||||
"seconds": 2
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "BATTLE_FIGHTER_DAMAGE",
|
||||
"fighter": "target",
|
||||
"amount": 12
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"type": "MODAL",
|
||||
"title": "Slash",
|
||||
"message": "A quick blade strike cuts across the target."
|
||||
},
|
||||
{
|
||||
"type": "WAIT",
|
||||
"seconds": 2
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "BATTLE_FIGHTER_DAMAGE",
|
||||
"fighter": "target",
|
||||
"amount": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"type": "BATTLE_START",
|
||||
"enemies": [
|
||||
{
|
||||
"attack": 8, "defense": 4, "magic": 0, "speed": 5, "luck": 2,
|
||||
"healthMax": 30, "mpMax": 0,
|
||||
"abilities": ["fire"]
|
||||
},
|
||||
{
|
||||
"attack": 6, "defense": 3, "magic": 0, "speed": 7, "luck": 2,
|
||||
"healthMax": 20, "mpMax": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -62,7 +62,7 @@
|
||||
},
|
||||
{
|
||||
"type": "CUTSCENE",
|
||||
"name": "main_menu"
|
||||
"name": "cutscenes/main_menu.jsonc"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
// Placeholder for the new-game intro flow - just shows a modal for now.
|
||||
"items": [
|
||||
{
|
||||
"type": "MODAL",
|
||||
"title": "New Game",
|
||||
"message": "This is a placeholder for the new game intro.",
|
||||
"blocking": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"items": {
|
||||
"potion": { "type": "MEDICINE" },
|
||||
"potato": { "type": "FOOD" },
|
||||
"apple": { "type": "FOOD" }
|
||||
}
|
||||
}
|
||||
+20
-1
@@ -73,6 +73,12 @@ msgstr "Select Save"
|
||||
msgid "ui.select_save.empty"
|
||||
msgstr "Empty Slot"
|
||||
|
||||
msgid "ui.select_save.corrupt"
|
||||
msgstr "Corrupted Save"
|
||||
|
||||
msgid "ui.select_save.corrupt_warning"
|
||||
msgstr "This save file is corrupted and could not be loaded. Please contact support if this issue persists."
|
||||
|
||||
msgid "ui.select_save.slot_format"
|
||||
msgstr "%s Lv.%d %s"
|
||||
|
||||
@@ -260,6 +266,10 @@ msgstr "loading"
|
||||
msgid "ui.autosave.saving"
|
||||
msgstr "SAVING"
|
||||
|
||||
|
||||
#
|
||||
# Items
|
||||
#
|
||||
msgid "item.potion.name"
|
||||
msgstr "Potion"
|
||||
|
||||
@@ -267,4 +277,13 @@ msgid "item.potato.name"
|
||||
msgstr "Potato"
|
||||
|
||||
msgid "item.apple.name"
|
||||
msgstr "Apple"
|
||||
msgstr "Apple"
|
||||
|
||||
#
|
||||
# Moves
|
||||
#
|
||||
msgid "move.fire.name"
|
||||
msgstr "Fire"
|
||||
|
||||
msgid "move.slash.name"
|
||||
msgstr "Slash"
|
||||
@@ -0,0 +1 @@
|
||||
{"meshes":[{"file":"house_4_4.dmf","pos":[2,1,0]},{"file":"house_6_3.dmf","pos":[1,9,0]},{"file":"house_2_2.dmf","pos":[12,6,0]}],"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]]}
|
||||
@@ -0,0 +1 @@
|
||||
{"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[6,0],[2,0],[2,0],[2,0],[2,0],[2,0],[2,0],[7,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[8,0],[4,0],[4,0],[4,0],[12,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[8,0],[4,0],[4,0],[9,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]],"meshes":[{"file":"house_5_3.dmf","pos":[1,1,0]}],"entities":[{"type":"global","globalId":3,"pos":[8,8,1]},{"type":"global","globalId":4,"pos":[12,2,0]}],"areas":[{"min":[11,3,0],"max":[16,9,10],"callbackId":1,"notify":3,"trigger":6}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"meshes":[{"file":"house_8_4.dmf","pos":[1,1,0]},{"file":"house_3_3.dmf","pos":[11,2,0]},{"file":"house_2_2.dmf","pos":[3,11,0]}],"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,null,null,[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],null,null,null,null,null,null,null,null,[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],null,null,null,null,null,null,null,null,[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]]}
|
||||
@@ -0,0 +1 @@
|
||||
{"meshes":[{"file":"house_3_2.dmf","pos":[1,1,0]},{"file":"house_2_3.dmf","pos":[7,2,0]},{"file":"house_4_2.dmf","pos":[1,8,0]},{"file":"house_1_1.dmf","pos":[11,10,0]},{"file":"house_5_2.dmf","pos":[13,6,0]}],"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]]}
|
||||
@@ -0,0 +1 @@
|
||||
{"meshes":[{"file":"house_5_5.dmf","pos":[3,2,0]},{"file":"house_2_3.dmf","pos":[10,3,0]},{"file":"house_3_1.dmf","pos":[1,12,0]}],"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]]}
|
||||
@@ -0,0 +1 @@
|
||||
{"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,3],[1,3],[1,3],[1,3],[1,3],[1,3],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,3],[1,3],[1,3],[1,3],[1,3],[1,3],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,3],[1,3],[1,3],[1,3],[1,3],[1,3],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[4,1],[4,1],[4,1],[4,1],[4,1],[4,1],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]],"meshes":[]}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/chunks/chunk_-1_0_0_0.dmf",
|
||||
"color": [
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
255
|
||||
],
|
||||
"texture": "tiles.png"
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/chunks/chunk_0_0_0_0.dmf",
|
||||
"color": [
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
255
|
||||
],
|
||||
"texture": "tiles.png"
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/chunks/chunk_0_1_0_0.dmf",
|
||||
"color": [
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
255
|
||||
],
|
||||
"texture": "tiles.png"
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/chunks/chunk_1_0_0_0.dmf",
|
||||
"color": [
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
255
|
||||
],
|
||||
"texture": "tiles.png"
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/chunks/chunk_1_1_0_0.dmf",
|
||||
"color": [
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
255
|
||||
],
|
||||
"texture": "tiles.png"
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/chunks/chunk_2_0_0_0.dmf",
|
||||
"color": [
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
255
|
||||
],
|
||||
"texture": "tiles.png"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"members": {
|
||||
"hero": {
|
||||
"attack": 10,
|
||||
"defense": 5,
|
||||
"magic": 0,
|
||||
"speed": 10,
|
||||
"luck": 0,
|
||||
"healthMax": 30,
|
||||
"mpMax": 10
|
||||
},
|
||||
"companion": {
|
||||
"attack": 8,
|
||||
"defense": 6,
|
||||
"magic": 4,
|
||||
"speed": 8,
|
||||
"luck": 1,
|
||||
"healthMax": 25,
|
||||
"mpMax": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"flags": {
|
||||
"test": {
|
||||
"initial": true
|
||||
},
|
||||
"test2": {
|
||||
"initial": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
)
|
||||
|
||||
set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures.")
|
||||
set(DUSK_TRACK_MEMORY ON CACHE BOOL "Track memoryAllocate() call sites to help find memory leaks.")
|
||||
|
||||
# Define platform-specific macros.
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
|
||||
@@ -60,6 +60,19 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||
DUSK_DISPLAY_OVERSCAN=6
|
||||
)
|
||||
|
||||
# logDebug()/logError() additionally write every call to a file on the
|
||||
# Memory Stick (ms0:/PSP/GAME/Dusk/*.log) - a real, measured source of
|
||||
# per-call hitching (fopen+write+fclose every log line), not just a
|
||||
# theoretical cost. Default off so a normal build doesn't pay it; opt in
|
||||
# with -DDUSK_PSP_LOG_FILE=ON when the log file itself is actually needed
|
||||
# (e.g. no way to see stdout on real hardware).
|
||||
set(DUSK_PSP_LOG_FILE OFF CACHE BOOL "Also write PSP debug/error logs to ms0:/PSP/GAME/Dusk/*.log (slow - Memory Stick I/O per call).")
|
||||
if(DUSK_PSP_LOG_FILE)
|
||||
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||
DUSK_PSP_LOG_FILE
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||
DUSK_ASSERTIONS_FAKED
|
||||
|
||||
@@ -345,12 +345,14 @@
|
||||
return promise;
|
||||
}
|
||||
|
||||
const MAP_NAME = "overworld";
|
||||
|
||||
function chunkPaths(x, y, z) {
|
||||
return {
|
||||
raw: `assetsraw/chunks/chunk_${x}_${y}_${z}.json`,
|
||||
dcf: `assets/chunks/${x}_${y}_${z}.dcf`,
|
||||
terrainDmf: `assets/meshes/chunks/chunk_${x}_${y}_${z}_0.dmf`,
|
||||
terrainModel: `assets/models/chunks/chunk_${x}_${y}_${z}_0.json`,
|
||||
raw: `assetsraw/maps/${MAP_NAME}/chunks/chunk_${x}_${y}_${z}.json`,
|
||||
dcf: `assets/maps/${MAP_NAME}/chunks/${x}_${y}_${z}.dcf`,
|
||||
terrainDmf: `assets/maps/${MAP_NAME}/meshes/chunks/chunk_${x}_${y}_${z}_0.dmf`,
|
||||
terrainModel: `assets/maps/${MAP_NAME}/models/chunks/chunk_${x}_${y}_${z}_0.json`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -767,7 +769,7 @@
|
||||
async function buildChunkBrowseList() {
|
||||
const list = document.getElementById("chunkBrowseList");
|
||||
list.innerHTML = "";
|
||||
const res = await apiLs("assetsraw/chunks");
|
||||
const res = await apiLs(`assetsraw/maps/${MAP_NAME}/chunks`);
|
||||
for(const entry of res.entries || []) {
|
||||
const m = entry.name.match(/^chunk_(-?\d+)_(-?\d+)_(-?\d+)\.json$/);
|
||||
if(!m) continue;
|
||||
|
||||
@@ -209,7 +209,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_json({"error": "invalid chunk coordinates"}, 400)
|
||||
return
|
||||
|
||||
json_path = PROJECT_ROOT / "assetsraw" / "chunks" / f"chunk_{x}_{y}_{z}.json"
|
||||
json_path = (
|
||||
PROJECT_ROOT / "assetsraw" / "maps" / "overworld" / "chunks"
|
||||
/ f"chunk_{x}_{y}_{z}.json"
|
||||
)
|
||||
if not json_path.is_file():
|
||||
self.send_json({"error": "chunk json not found"}, 404)
|
||||
return
|
||||
|
||||
@@ -50,6 +50,12 @@ if(DUSK_BACKTRACE)
|
||||
)
|
||||
endif()
|
||||
|
||||
if(DUSK_TRACK_MEMORY)
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_TRACK_MEMORY
|
||||
)
|
||||
endif()
|
||||
|
||||
# Includes
|
||||
target_include_directories(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
@@ -74,6 +80,7 @@ add_subdirectory(engine)
|
||||
add_subdirectory(error)
|
||||
add_subdirectory(input)
|
||||
add_subdirectory(locale)
|
||||
add_subdirectory(perf)
|
||||
add_subdirectory(rpg)
|
||||
add_subdirectory(scene)
|
||||
add_subdirectory(system)
|
||||
|
||||
+4
-14
@@ -25,11 +25,9 @@ errorret_t assetInit(void) {
|
||||
threadMutexInit(&ASSET.loading[i].mutex);
|
||||
}
|
||||
|
||||
// assetInitPlatform must either define both ASSET.zip/ASSET.zipStored or
|
||||
// throw an error.
|
||||
// assetInitPlatform must either define ASSET.zip or throw an error.
|
||||
errorChain(assetInitPlatform());
|
||||
assertNotNull(ASSET.zip, "Asset zip null without error.");
|
||||
assertNotNull(ASSET.zipStored, "Asset stored zip null without error.");
|
||||
threadInit(&ASSET.loadThread, assetUpdateAsync);
|
||||
threadStart(&ASSET.loadThread);
|
||||
|
||||
@@ -40,9 +38,7 @@ bool_t assetFileExists(const char_t *filename) {
|
||||
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
|
||||
|
||||
threadMutexLock(&ASSET.zipLock);
|
||||
bool_t found =
|
||||
zip_name_locate(ASSET.zip, filename, 0) >= 0 ||
|
||||
zip_name_locate(ASSET.zipStored, filename, 0) >= 0;
|
||||
bool_t found = zip_name_locate(ASSET.zip, filename, 0) >= 0;
|
||||
threadMutexUnlock(&ASSET.zipLock);
|
||||
return found;
|
||||
}
|
||||
@@ -445,19 +441,13 @@ errorret_t assetDispose(void) {
|
||||
|
||||
errorChain(assetReapUnused());
|
||||
|
||||
// Cleanup zip files.
|
||||
// Cleanup zip file.
|
||||
if(ASSET.zip != NULL) {
|
||||
if(zip_close(ASSET.zip) != 0) {
|
||||
errorThrow("Failed to close compressed asset zip archive.");
|
||||
errorThrow("Failed to close asset zip archive.");
|
||||
}
|
||||
ASSET.zip = NULL;
|
||||
}
|
||||
if(ASSET.zipStored != NULL) {
|
||||
if(zip_close(ASSET.zipStored) != 0) {
|
||||
errorThrow("Failed to close stored asset zip archive.");
|
||||
}
|
||||
ASSET.zipStored = NULL;
|
||||
}
|
||||
|
||||
errorChain(assetDisposePlatform());
|
||||
threadMutexDispose(&ASSET.zipLock);
|
||||
|
||||
@@ -29,16 +29,12 @@
|
||||
#define ASSET_BASE_DIRECTORY_MAX 256
|
||||
|
||||
typedef struct asset_s {
|
||||
// Compressed (DEFLATE) archive - expected to hold the bulk of a game's
|
||||
// binary assets. Looked up first by assetFileInit().
|
||||
// The dusk.dsk archive - a single uncompressed (ZIP_STORED) zip, so
|
||||
// every entry gets the reliable repeated seeking/re-opening that
|
||||
// libzip only supports reliably for uncompressed entries (locale
|
||||
// strings, MP3 loop restarts, etc.).
|
||||
zip_t *zip;
|
||||
|
||||
// Stored (uncompressed) archive - expected to hold small files that need
|
||||
// reliable repeated seeking/re-opening (e.g. locale strings), which
|
||||
// libzip only supports reliably for uncompressed entries. Looked up by
|
||||
// assetFileInit() only if the name isn't found in `zip`.
|
||||
zip_t *zipStored;
|
||||
|
||||
// Directory dusk.dsk was actually found/opened in (on PSP, the directory
|
||||
// EBOOT.PBP lives in, since dusk.dsk is embedded inside it rather than a
|
||||
// loose file) - populated by each platform's own assetInitXxx() once it
|
||||
@@ -52,7 +48,7 @@ typedef struct asset_s {
|
||||
|
||||
assetplatform_t platform;
|
||||
|
||||
// Guards every libzip call against ASSET.zip/ASSET.zipStored (zip_fopen,
|
||||
// Guards every libzip call against ASSET.zip (zip_fopen,
|
||||
// zip_fread, zip_fclose, zip_fseek, zip_stat, zip_name_locate - see
|
||||
// assetfile.c/assetFileExists). libzip is documented as not thread-safe,
|
||||
// and this asset system genuinely calls it from multiple real threads at
|
||||
|
||||
+39
-163
@@ -30,9 +30,9 @@ errorret_t assetDskParseHeader(
|
||||
// Every field is a little-endian uint32_t regardless of host - convert
|
||||
// to host order (a no-op on little-endian hosts, a real byteswap on
|
||||
// Dolphin's big-endian PowerPC).
|
||||
uint32_t fields[7];
|
||||
uint32_t fields[3];
|
||||
memoryCopy(fields, bytes + ASSET_DSK_MAGIC_SIZE, sizeof(fields));
|
||||
for(uint8_t i = 0; i < 7; i++) {
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
fields[i] = endianLittleToHost32(fields[i]);
|
||||
}
|
||||
|
||||
@@ -41,24 +41,17 @@ errorret_t assetDskParseHeader(
|
||||
errorThrow("dusk.dsk has an unsupported version: %u", version);
|
||||
}
|
||||
|
||||
outHeader->compressedOffset = fields[1];
|
||||
outHeader->compressedSize = fields[2];
|
||||
outHeader->compressedChecksum = fields[3];
|
||||
outHeader->storedOffset = fields[4];
|
||||
outHeader->storedSize = fields[5];
|
||||
outHeader->storedChecksum = fields[6];
|
||||
outHeader->size = fields[1];
|
||||
outHeader->checksum = fields[2];
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetDskOpenFromPath(
|
||||
const char_t *path,
|
||||
zip_t **outCompressed,
|
||||
zip_t **outStored
|
||||
zip_t **outZip
|
||||
) {
|
||||
errorChain(assetDskOpenFromPathRange(
|
||||
path, 0, SIZE_MAX, outCompressed, outStored
|
||||
));
|
||||
errorChain(assetDskOpenFromPathRange(path, 0, SIZE_MAX, outZip));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -66,15 +59,12 @@ errorret_t assetDskOpenFromPathRange(
|
||||
const char_t *path,
|
||||
const size_t baseOffset,
|
||||
const size_t baseSize,
|
||||
zip_t **outCompressed,
|
||||
zip_t **outStored
|
||||
zip_t **outZip
|
||||
) {
|
||||
assertNotNull(path, "Path cannot be NULL.");
|
||||
assertNotNull(outCompressed, "Out compressed cannot be NULL.");
|
||||
assertNotNull(outStored, "Out stored cannot be NULL.");
|
||||
assertNotNull(outZip, "Out zip cannot be NULL.");
|
||||
|
||||
*outCompressed = NULL;
|
||||
*outStored = NULL;
|
||||
*outZip = NULL;
|
||||
|
||||
FILE *headerFile = fopen(path, "rb");
|
||||
if(headerFile == NULL) {
|
||||
@@ -96,104 +86,30 @@ errorret_t assetDskOpenFromPathRange(
|
||||
assetdskheader_t header;
|
||||
errorChain(assetDskParseHeader(headerBytes, sizeof(headerBytes), &header));
|
||||
|
||||
if(
|
||||
(size_t) header.compressedOffset + header.compressedSize > baseSize ||
|
||||
(size_t) header.storedOffset + header.storedSize > baseSize
|
||||
) {
|
||||
errorThrow("dusk.dsk header describes ranges beyond its containing file.");
|
||||
if((size_t) ASSET_DSK_HEADER_SIZE + header.size > baseSize) {
|
||||
errorThrow("dusk.dsk header describes a range beyond its containing file.");
|
||||
}
|
||||
|
||||
zip_error_t zipError;
|
||||
zip_error_init(&zipError);
|
||||
|
||||
zip_source_t *compressedSource = zip_source_file_create(
|
||||
zip_source_t *source = zip_source_file_create(
|
||||
path,
|
||||
(zip_uint64_t) (baseOffset + header.compressedOffset),
|
||||
(zip_int64_t) header.compressedSize,
|
||||
(zip_uint64_t) (baseOffset + ASSET_DSK_HEADER_SIZE),
|
||||
(zip_int64_t) header.size,
|
||||
&zipError
|
||||
);
|
||||
if(compressedSource == NULL) {
|
||||
if(source == NULL) {
|
||||
errorThrow(
|
||||
"Failed to create compressed dusk.dsk source: %s",
|
||||
zip_error_strerror(&zipError)
|
||||
"Failed to create dusk.dsk source: %s", zip_error_strerror(&zipError)
|
||||
);
|
||||
}
|
||||
|
||||
*outCompressed =
|
||||
zip_open_from_source(compressedSource, ZIP_RDONLY, &zipError);
|
||||
if(*outCompressed == NULL) {
|
||||
zip_source_free(compressedSource);
|
||||
*outZip = zip_open_from_source(source, ZIP_RDONLY, &zipError);
|
||||
if(*outZip == NULL) {
|
||||
zip_source_free(source);
|
||||
errorThrow(
|
||||
"Failed to open compressed dusk.dsk archive: %s",
|
||||
zip_error_strerror(&zipError)
|
||||
);
|
||||
}
|
||||
|
||||
zip_source_t *storedSource = zip_source_file_create(
|
||||
path,
|
||||
(zip_uint64_t) (baseOffset + header.storedOffset),
|
||||
(zip_int64_t) header.storedSize,
|
||||
&zipError
|
||||
);
|
||||
if(storedSource == NULL) {
|
||||
zip_close(*outCompressed);
|
||||
*outCompressed = NULL;
|
||||
errorThrow(
|
||||
"Failed to create stored dusk.dsk source: %s",
|
||||
zip_error_strerror(&zipError)
|
||||
);
|
||||
}
|
||||
|
||||
*outStored = zip_open_from_source(storedSource, ZIP_RDONLY, &zipError);
|
||||
if(*outStored == NULL) {
|
||||
zip_source_free(storedSource);
|
||||
zip_close(*outCompressed);
|
||||
*outCompressed = NULL;
|
||||
errorThrow(
|
||||
"Failed to open stored dusk.dsk archive: %s",
|
||||
zip_error_strerror(&zipError)
|
||||
);
|
||||
}
|
||||
|
||||
// The stored archive is small by convention, so verifying its checksum
|
||||
// here (one extra small read) is cheap; the compressed archive isn't
|
||||
// checked since it's meant to be read lazily/on-demand from here on.
|
||||
uint8_t *storedBytes = (uint8_t *) memoryAllocate(header.storedSize);
|
||||
FILE *storedFile = fopen(path, "rb");
|
||||
if(storedFile == NULL) {
|
||||
memoryFree(storedBytes);
|
||||
zip_close(*outStored);
|
||||
zip_close(*outCompressed);
|
||||
*outStored = NULL;
|
||||
*outCompressed = NULL;
|
||||
errorThrow(
|
||||
"Failed to re-open dusk.dsk to verify stored checksum: %s", path
|
||||
);
|
||||
}
|
||||
fseek(storedFile, (long) (baseOffset + header.storedOffset), SEEK_SET);
|
||||
size_t storedRead = fread(storedBytes, 1, header.storedSize, storedFile);
|
||||
fclose(storedFile);
|
||||
if(storedRead != header.storedSize) {
|
||||
memoryFree(storedBytes);
|
||||
zip_close(*outStored);
|
||||
zip_close(*outCompressed);
|
||||
*outStored = NULL;
|
||||
*outCompressed = NULL;
|
||||
errorThrow(
|
||||
"Failed to read dusk.dsk stored archive to verify checksum: %s", path
|
||||
);
|
||||
}
|
||||
|
||||
uint32_t checksum =
|
||||
(uint32_t) crc32(0L, storedBytes, (uInt) header.storedSize);
|
||||
memoryFree(storedBytes);
|
||||
if(checksum != header.storedChecksum) {
|
||||
zip_close(*outStored);
|
||||
zip_close(*outCompressed);
|
||||
*outStored = NULL;
|
||||
*outCompressed = NULL;
|
||||
errorThrow(
|
||||
"dusk.dsk stored archive failed checksum verification: %s", path
|
||||
"Failed to open dusk.dsk archive: %s", zip_error_strerror(&zipError)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -203,85 +119,45 @@ errorret_t assetDskOpenFromPathRange(
|
||||
errorret_t assetDskOpenFromBuffer(
|
||||
uint8_t *buffer,
|
||||
const size_t bufferSize,
|
||||
zip_t **outCompressed,
|
||||
zip_t **outStored
|
||||
zip_t **outZip
|
||||
) {
|
||||
assertNotNull(buffer, "Buffer cannot be NULL.");
|
||||
assertNotNull(outCompressed, "Out compressed cannot be NULL.");
|
||||
assertNotNull(outStored, "Out stored cannot be NULL.");
|
||||
assertNotNull(outZip, "Out zip cannot be NULL.");
|
||||
|
||||
*outCompressed = NULL;
|
||||
*outStored = NULL;
|
||||
*outZip = NULL;
|
||||
|
||||
assetdskheader_t header;
|
||||
errorChain(assetDskParseHeader(buffer, bufferSize, &header));
|
||||
|
||||
if(
|
||||
(size_t) header.compressedOffset + header.compressedSize > bufferSize ||
|
||||
(size_t) header.storedOffset + header.storedSize > bufferSize
|
||||
) {
|
||||
errorThrow("dusk.dsk header describes ranges beyond the buffer.");
|
||||
if((size_t) ASSET_DSK_HEADER_SIZE + header.size > bufferSize) {
|
||||
errorThrow("dusk.dsk header describes a range beyond the buffer.");
|
||||
}
|
||||
|
||||
uint32_t compressedChecksum = (uint32_t) crc32(
|
||||
0L, buffer + header.compressedOffset, (uInt) header.compressedSize
|
||||
);
|
||||
if(compressedChecksum != header.compressedChecksum) {
|
||||
errorThrow("dusk.dsk compressed archive failed checksum verification.");
|
||||
}
|
||||
|
||||
uint32_t storedChecksum = (uint32_t) crc32(
|
||||
0L, buffer + header.storedOffset, (uInt) header.storedSize
|
||||
);
|
||||
if(storedChecksum != header.storedChecksum) {
|
||||
errorThrow("dusk.dsk stored archive failed checksum verification.");
|
||||
uint8_t *data = buffer + ASSET_DSK_HEADER_SIZE;
|
||||
uint32_t checksum = (uint32_t) crc32(0L, data, (uInt) header.size);
|
||||
if(checksum != header.checksum) {
|
||||
errorThrow("dusk.dsk archive failed checksum verification.");
|
||||
}
|
||||
|
||||
zip_error_t zipError;
|
||||
zip_error_init(&zipError);
|
||||
|
||||
// freep=0 for both - they're non-owning windows into the same caller-owned
|
||||
// buffer, not independent allocations libzip should free.
|
||||
zip_source_t *compressedSource = zip_source_buffer_create(
|
||||
buffer + header.compressedOffset, header.compressedSize, 0, &zipError
|
||||
// freep=0 - this is a non-owning window into the same caller-owned
|
||||
// buffer, not an independent allocation libzip should free.
|
||||
zip_source_t *source = zip_source_buffer_create(
|
||||
data, header.size, 0, &zipError
|
||||
);
|
||||
if(compressedSource == NULL) {
|
||||
if(source == NULL) {
|
||||
errorThrow(
|
||||
"Failed to create compressed dusk.dsk source: %s",
|
||||
zip_error_strerror(&zipError)
|
||||
"Failed to create dusk.dsk source: %s", zip_error_strerror(&zipError)
|
||||
);
|
||||
}
|
||||
|
||||
*outCompressed =
|
||||
zip_open_from_source(compressedSource, ZIP_RDONLY, &zipError);
|
||||
if(*outCompressed == NULL) {
|
||||
zip_source_free(compressedSource);
|
||||
*outZip = zip_open_from_source(source, ZIP_RDONLY, &zipError);
|
||||
if(*outZip == NULL) {
|
||||
zip_source_free(source);
|
||||
errorThrow(
|
||||
"Failed to open compressed dusk.dsk archive: %s",
|
||||
zip_error_strerror(&zipError)
|
||||
);
|
||||
}
|
||||
|
||||
zip_source_t *storedSource = zip_source_buffer_create(
|
||||
buffer + header.storedOffset, header.storedSize, 0, &zipError
|
||||
);
|
||||
if(storedSource == NULL) {
|
||||
zip_close(*outCompressed);
|
||||
*outCompressed = NULL;
|
||||
errorThrow(
|
||||
"Failed to create stored dusk.dsk source: %s",
|
||||
zip_error_strerror(&zipError)
|
||||
);
|
||||
}
|
||||
|
||||
*outStored = zip_open_from_source(storedSource, ZIP_RDONLY, &zipError);
|
||||
if(*outStored == NULL) {
|
||||
zip_source_free(storedSource);
|
||||
zip_close(*outCompressed);
|
||||
*outCompressed = NULL;
|
||||
errorThrow(
|
||||
"Failed to open stored dusk.dsk archive: %s",
|
||||
zip_error_strerror(&zipError)
|
||||
"Failed to open dusk.dsk archive: %s", zip_error_strerror(&zipError)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+40
-48
@@ -12,31 +12,30 @@
|
||||
// "DSK2" - distinct from a plain zip's "PK\x03\x04" so a stray plain zip
|
||||
// never gets misread as a valid dusk.dsk.
|
||||
#define ASSET_DSK_MAGIC_SIZE 4
|
||||
#define ASSET_DSK_MAGIC "DSK2"
|
||||
#define ASSET_DSK_VERSION 1
|
||||
#define ASSET_DSK_MAGIC "DSK"
|
||||
#define ASSET_DSK_VERSION 2
|
||||
|
||||
// magic(4) + version(4) + compressedOffset(4) + compressedSize(4) +
|
||||
// compressedChecksum(4) + storedOffset(4) + storedSize(4) +
|
||||
// storedChecksum(4), all little-endian regardless of host - see
|
||||
// assetDskParseHeader.
|
||||
#define ASSET_DSK_HEADER_SIZE 32
|
||||
// Magic + version + size + checksum
|
||||
#define ASSET_DSK_HEADER_SIZE ( \
|
||||
ASSET_DSK_MAGIC_SIZE + \
|
||||
sizeof(uint32_t) + \
|
||||
sizeof(uint32_t) + \
|
||||
sizeof(uint32_t) \
|
||||
)
|
||||
|
||||
/**
|
||||
* Parsed dusk.dsk (DSK2 format) header. dusk.dsk is two independent, back
|
||||
* to back zip archives (see tools/asset/pack) rather than a single plain
|
||||
* zip: a "compressed" one (DEFLATE, expected to hold the bulk of a game's
|
||||
* binary assets, opened lazily/on-demand) and a "stored" one (uncompressed,
|
||||
* expected to hold small files - locale strings, config - that need
|
||||
* reliable repeated seeking/re-opening, which libzip only supports for
|
||||
* uncompressed entries).
|
||||
* Parsed dusk.dsk (DSK2 format) header. dusk.dsk is a single uncompressed
|
||||
* (ZIP_STORED) zip archive (see tools/asset/pack) rather than DEFLATEd -
|
||||
* the bulk of what's packed (audio, images) is already compressed at the
|
||||
* file level, so DEFLATE-ing the whole archive on top buys negligible
|
||||
* space for real CPU cost, and every entry gets the reliable repeated
|
||||
* seeking/re-opening that libzip only supports for uncompressed entries
|
||||
* (locale line-by-line reads, MP3 loop restarts, etc.) instead of just a
|
||||
* hand-picked subset of files.
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t compressedOffset;
|
||||
uint32_t compressedSize;
|
||||
uint32_t compressedChecksum;
|
||||
uint32_t storedOffset;
|
||||
uint32_t storedSize;
|
||||
uint32_t storedChecksum;
|
||||
uint32_t size;
|
||||
uint32_t checksum;
|
||||
} assetdskheader_t;
|
||||
|
||||
/**
|
||||
@@ -56,27 +55,24 @@ errorret_t assetDskParseHeader(
|
||||
);
|
||||
|
||||
/**
|
||||
* Opens both archives of a dusk.dsk file given a filesystem path, using
|
||||
* lazy/windowed file-backed zip sources - no more memory used than the
|
||||
* Opens the dusk.dsk archive given a filesystem path, using a lazy/
|
||||
* windowed file-backed zip source - no more memory used than the
|
||||
* existing per-platform small read buffers, matching the memory
|
||||
* characteristics of a plain zip_open() on the whole file. Verifies the
|
||||
* (small, by convention) stored archive's checksum; the compressed
|
||||
* archive's checksum is intentionally not verified here since doing so
|
||||
* would require reading the bulk of the game's assets just to compute it.
|
||||
* characteristics of a plain zip_open() on the whole file. The checksum
|
||||
* is intentionally not verified here since doing so would require
|
||||
* reading the whole (potentially large) archive just to compute it.
|
||||
*
|
||||
* See assetDskOpenFromPathRange() for the case where the DSK2 blob isn't
|
||||
* the whole file (e.g. embedded inside another container).
|
||||
*
|
||||
* @param path Filesystem path to the dusk.dsk file.
|
||||
* @param outCompressed Set to the opened compressed archive on success.
|
||||
* @param outStored Set to the opened stored archive on success.
|
||||
* @param outZip Set to the opened archive on success.
|
||||
* @return OK on success, error if the file is missing, too short, has a
|
||||
* bad header, or either archive fails to open/verify.
|
||||
* bad header, or the archive fails to open.
|
||||
*/
|
||||
errorret_t assetDskOpenFromPath(
|
||||
const char_t *path,
|
||||
zip_t **outCompressed,
|
||||
zip_t **outStored
|
||||
zip_t **outZip
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -90,39 +86,35 @@ errorret_t assetDskOpenFromPath(
|
||||
* @param path Filesystem path to the container file.
|
||||
* @param baseOffset Byte offset within `path` where the DSK2 blob starts.
|
||||
* @param baseSize Number of bytes available at `baseOffset`.
|
||||
* @param outCompressed Set to the opened compressed archive on success.
|
||||
* @param outStored Set to the opened stored archive on success.
|
||||
* @param outZip Set to the opened archive on success.
|
||||
* @return OK on success, error if the file is missing, too short, has a
|
||||
* bad header, or either archive fails to open/verify.
|
||||
* bad header, or the archive fails to open.
|
||||
*/
|
||||
errorret_t assetDskOpenFromPathRange(
|
||||
const char_t *path,
|
||||
const size_t baseOffset,
|
||||
const size_t baseSize,
|
||||
zip_t **outCompressed,
|
||||
zip_t **outStored
|
||||
zip_t **outZip
|
||||
);
|
||||
|
||||
/**
|
||||
* Opens both archives of a dusk.dsk file already fully resident in memory
|
||||
* (e.g. a PSAR embedded in an EBOOT.PBP, or an ISO-embedded file already
|
||||
* read via DVD_ReadAbs - platforms that already buffer the whole file for
|
||||
* reasons unrelated to this format). Neither archive takes ownership of
|
||||
* `buffer` (both are opened as non-owning sub-ranges of it) - the caller
|
||||
* Opens the dusk.dsk archive already fully resident in memory (e.g. a
|
||||
* PSAR embedded in an EBOOT.PBP, or an ISO-embedded file already read via
|
||||
* DVD_ReadAbs - platforms that already buffer the whole file for reasons
|
||||
* unrelated to this format). The returned archive doesn't take ownership
|
||||
* of `buffer` (it's opened as a non-owning sub-range of it) - the caller
|
||||
* remains responsible for freeing it, and must keep it alive for as long
|
||||
* as either archive stays open. Verifies both archives' checksums, since
|
||||
* the bytes are already resident.
|
||||
* as the archive stays open. Verifies the checksum, since the bytes are
|
||||
* already resident.
|
||||
*
|
||||
* @param buffer The whole dusk.dsk file's bytes.
|
||||
* @param bufferSize Number of bytes at `buffer`.
|
||||
* @param outCompressed Set to the opened compressed archive on success.
|
||||
* @param outStored Set to the opened stored archive on success.
|
||||
* @param outZip Set to the opened archive on success.
|
||||
* @return OK on success, error if too short, has a bad header/checksum, or
|
||||
* either archive fails to open.
|
||||
* the archive fails to open.
|
||||
*/
|
||||
errorret_t assetDskOpenFromBuffer(
|
||||
uint8_t *buffer,
|
||||
const size_t bufferSize,
|
||||
zip_t **outCompressed,
|
||||
zip_t **outStored
|
||||
zip_t **outZip
|
||||
);
|
||||
|
||||
@@ -24,20 +24,11 @@ errorret_t assetFileInit(
|
||||
file->params = params;
|
||||
file->output = output;
|
||||
|
||||
// Stat the file, trying the compressed archive first and falling back to
|
||||
// the stored one - remember which matched so assetFileOpen opens it from
|
||||
// the right archive.
|
||||
zip_stat_init(&file->stat);
|
||||
threadMutexLock(&ASSET.zipLock);
|
||||
if(zip_stat(ASSET.zip, filename, 0, &file->stat) == 0) {
|
||||
file->sourceZip = ASSET.zip;
|
||||
} else if(zip_stat(ASSET.zipStored, filename, 0, &file->stat) == 0) {
|
||||
file->sourceZip = ASSET.zipStored;
|
||||
} else {
|
||||
file->sourceZip = NULL;
|
||||
}
|
||||
int statResult = zip_stat(ASSET.zip, filename, 0, &file->stat);
|
||||
threadMutexUnlock(&ASSET.zipLock);
|
||||
if(file->sourceZip == NULL) {
|
||||
if(statResult != 0) {
|
||||
errorThrow("Failed to stat asset file: %s", filename);
|
||||
}
|
||||
|
||||
@@ -85,11 +76,10 @@ errorret_t assetFileRewind(assetfile_t *file) {
|
||||
errorret_t assetFileOpen(assetfile_t *file) {
|
||||
assertNotNull(file, "Asset file cannot be NULL.");
|
||||
assertNotNull(file->filename, "Asset file filename cannot be NULL.");
|
||||
assertNotNull(file->sourceZip, "Asset file must be inited before opening.");
|
||||
assertNull(file->zipFile, "Asset file already open.");
|
||||
|
||||
threadMutexLock(&ASSET.zipLock);
|
||||
file->zipFile = zip_fopen(file->sourceZip, file->filename, 0);
|
||||
file->zipFile = zip_fopen(ASSET.zip, file->filename, 0);
|
||||
threadMutexUnlock(&ASSET.zipLock);
|
||||
if(file->zipFile == NULL) {
|
||||
errorThrow("Failed to open asset file: %s", file->filename);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "error/error.h"
|
||||
#include <zip.h>
|
||||
|
||||
#define ASSET_FILE_NAME_MAX 48
|
||||
#define ASSET_FILE_NAME_MAX 128
|
||||
|
||||
// Max bytes requested per zip_fread() call in assetFileRead(). Some
|
||||
// zip_fread() implementations (seen on PSP) reject a single call asking
|
||||
@@ -32,11 +32,6 @@ typedef struct assetfile_s {
|
||||
zip_int64_t position;
|
||||
zip_int64_t lastRead;
|
||||
zip_file_t *zipFile;
|
||||
|
||||
// The archive this file was found in (ASSET.zip or ASSET.zipStored),
|
||||
// set by assetFileInit and used by assetFileOpen so lookups fall back
|
||||
// correctly between the two dusk.dsk archives.
|
||||
zip_t *sourceZip;
|
||||
} assetfile_t;
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,5 @@ add_subdirectory(locale)
|
||||
add_subdirectory(json)
|
||||
add_subdirectory(chunk)
|
||||
add_subdirectory(dmf)
|
||||
add_subdirectory(cutscene)
|
||||
add_subdirectory(wav)
|
||||
add_subdirectory(mp3)
|
||||
@@ -52,12 +52,6 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
||||
.dispose = assetChunkDispose
|
||||
},
|
||||
|
||||
[ASSET_LOADER_TYPE_CUTSCENE] = {
|
||||
.loadSync = assetCutsceneLoaderSync,
|
||||
.loadAsync = assetCutsceneLoaderAsync,
|
||||
.dispose = assetCutsceneDispose
|
||||
},
|
||||
|
||||
[ASSET_LOADER_TYPE_WAV] = {
|
||||
.loadSync = assetWavLoaderSync,
|
||||
.loadAsync = assetWavLoaderAsync,
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
#include "asset/loader/json/assetjsonloader.h"
|
||||
#include "asset/loader/chunk/assetchunkloader.h"
|
||||
#include "asset/loader/cutscene/assetcutsceneloader.h"
|
||||
#include "asset/loader/wav/assetwavloader.h"
|
||||
#include "asset/loader/mp3/assetmp3loader.h"
|
||||
|
||||
@@ -27,7 +26,6 @@ typedef enum {
|
||||
ASSET_LOADER_TYPE_LOCALE,
|
||||
ASSET_LOADER_TYPE_JSON,
|
||||
ASSET_LOADER_TYPE_CHUNK,
|
||||
ASSET_LOADER_TYPE_CUTSCENE,
|
||||
ASSET_LOADER_TYPE_WAV,
|
||||
ASSET_LOADER_TYPE_MP3,
|
||||
|
||||
@@ -42,7 +40,6 @@ typedef union {
|
||||
assetlocaleloaderloading_t locale;
|
||||
assetjsonloaderloading_t json;
|
||||
assetchunkloaderloading_t chunk;
|
||||
assetcutsceneloaderloading_t cutscene;
|
||||
assetwavloaderloading_t wav;
|
||||
assetmp3loaderloading_t mp3;
|
||||
} assetloaderloading_t;
|
||||
@@ -55,7 +52,6 @@ typedef union {
|
||||
assetlocaleoutput_t locale;
|
||||
assetjsonoutput_t json;
|
||||
assetchunkoutput_t chunk;
|
||||
assetcutsceneoutput_t cutscene;
|
||||
assetwavoutput_t wav;
|
||||
assetmp3output_t mp3;
|
||||
} assetloaderoutput_t;
|
||||
@@ -66,7 +62,6 @@ typedef union {
|
||||
assetlocaleloaderinput_t locale;
|
||||
assetjsonloaderinput_t json;
|
||||
assetchunkloaderinput_t chunk;
|
||||
assetcutsceneloaderinput_t cutscene;
|
||||
assetwavloaderinput_t wav;
|
||||
assetmp3loaderinput_t mp3;
|
||||
} assetloaderinput_t;
|
||||
|
||||
@@ -8,41 +8,284 @@
|
||||
#include "assetchunkloader.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/endian.h"
|
||||
#include "util/string.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/json/assetjsonloader.h"
|
||||
#include "display/color.h"
|
||||
#include "display/texture/texture.h"
|
||||
#include "rpg/overworld/worldpos.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#define ASSET_CHUNK_TERRAIN_TEXTURE "tiles.png"
|
||||
|
||||
// Per-shape corner Z offsets (sw, se, ne, nw) as offsets from a tile's own
|
||||
// base Z, ported 1:1 from tools/asset/chunk/__main__.py's _RAMP_CORNERS.
|
||||
// TILE_SHAPE_NULL's row is never read - assetChunkTilesHaveTerrain/
|
||||
// assetChunkBuildTerrain skip those tiles outright.
|
||||
static const float_t CHUNK_RAMP_CORNERS[TILE_SHAPE_COUNT][4] = {
|
||||
[TILE_SHAPE_GROUND] = {0.0f, 0.0f, 0.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_NORTH] = {0.0f, 0.0f, 1.0f, 1.0f},
|
||||
[TILE_SHAPE_RAMP_SOUTH] = {1.0f, 1.0f, 0.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_EAST] = {0.0f, 1.0f, 1.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_WEST] = {1.0f, 0.0f, 0.0f, 1.0f},
|
||||
[TILE_SHAPE_RAMP_NORTHEAST] = {0.0f, 0.0f, 1.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_NORTHWEST] = {0.0f, 0.0f, 0.0f, 1.0f},
|
||||
[TILE_SHAPE_RAMP_SOUTHEAST] = {0.0f, 1.0f, 0.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_SOUTHWEST] = {1.0f, 0.0f, 0.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_NORTHEAST_INNER] = {0.0f, 1.0f, 1.0f, 1.0f},
|
||||
[TILE_SHAPE_RAMP_NORTHWEST_INNER] = {1.0f, 0.0f, 1.0f, 1.0f},
|
||||
[TILE_SHAPE_RAMP_SOUTHEAST_INNER] = {1.0f, 1.0f, 1.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_SOUTHWEST_INNER] = {1.0f, 1.0f, 0.0f, 1.0f},
|
||||
};
|
||||
|
||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
errorOk();
|
||||
}
|
||||
|
||||
if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) {
|
||||
errorOk();
|
||||
static bool_t assetChunkTilesHaveTerrain(const tile_t *tiles) {
|
||||
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
|
||||
if(tiles[i].shape != TILE_SHAPE_NULL) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reads the "tiles" array - a dense, fixed-size CHUNK_TILE_COUNT grid,
|
||||
// one slot per (x, y) column at index x + y * CHUNK_WIDTH (position is
|
||||
// implicit in the index, not stored). Each slot is either `null` (empty
|
||||
// column, already zeroed = TILE_SHAPE_NULL) or a 2-element [type, z]
|
||||
// array.
|
||||
static void assetChunkParseTiles(yyjson_val *root, tile_t *tiles) {
|
||||
memoryZero(tiles, CHUNK_TILE_COUNT * sizeof(tile_t));
|
||||
|
||||
yyjson_val *tilesArr = yyjson_obj_get(root, "tiles");
|
||||
if(!yyjson_is_arr(tilesArr)) return;
|
||||
|
||||
size_t idx, len;
|
||||
yyjson_val *tileVal;
|
||||
yyjson_arr_foreach(tilesArr, idx, len, tileVal) {
|
||||
if(idx >= CHUNK_TILE_COUNT) break;
|
||||
if(!yyjson_is_arr(tileVal) || yyjson_arr_size(tileVal) < 2) continue;
|
||||
|
||||
int32_t type = (int32_t)yyjson_get_int(yyjson_arr_get(tileVal, 0));
|
||||
int32_t z = (int32_t)yyjson_get_int(yyjson_arr_get(tileVal, 1));
|
||||
tiles[idx] = (tile_t){ .shape = (tileshape_t)type, .z = (uint8_t)z };
|
||||
}
|
||||
}
|
||||
|
||||
// Resolves "meshes" (external building references, by bare filename) into
|
||||
// out->modelNames/meshOffsets starting at *ioIndex, advancing it as
|
||||
// entries are written. Filenames are resolved as
|
||||
// "models/buildings/<basename>.json" - a hardcoded stand-in for
|
||||
// tools/asset/chunk/__main__.py's find_model() recursive search, valid
|
||||
// as long as every building model lives flat under that one directory
|
||||
// (true today - see the raw-json-maps plan notes for the tradeoff).
|
||||
static void assetChunkParseMeshes(
|
||||
yyjson_val *root, assetchunkoutput_t *out, uint8_t *ioIndex
|
||||
) {
|
||||
yyjson_val *meshesArr = yyjson_obj_get(root, "meshes");
|
||||
if(!yyjson_is_arr(meshesArr)) return;
|
||||
|
||||
size_t idx, len;
|
||||
yyjson_val *meshVal;
|
||||
yyjson_arr_foreach(meshesArr, idx, len, meshVal) {
|
||||
if(*ioIndex >= CHUNK_MESH_COUNT_MAX) break;
|
||||
|
||||
yyjson_val *fileVal = yyjson_obj_get(meshVal, "file");
|
||||
if(!fileVal || !yyjson_is_str(fileVal)) continue;
|
||||
|
||||
char_t baseName[CHUNK_MESH_NAME_MAX];
|
||||
stringCopy(baseName, yyjson_get_str(fileVal), sizeof(baseName));
|
||||
char_t *dot = stringFindLastChar(baseName, '.');
|
||||
if(dot != NULL) *dot = '\0';
|
||||
|
||||
uint8_t m = *ioIndex;
|
||||
stringFormat(
|
||||
out->modelNames[m], CHUNK_MESH_NAME_MAX,
|
||||
"models/buildings/%s.json", baseName
|
||||
);
|
||||
|
||||
vec3 offset = {0.0f, 0.0f, 0.0f};
|
||||
yyjson_val *posArr = yyjson_obj_get(meshVal, "pos");
|
||||
if(yyjson_is_arr(posArr) && yyjson_arr_size(posArr) >= 3) {
|
||||
offset[0] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 0));
|
||||
offset[1] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 1));
|
||||
offset[2] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 2));
|
||||
}
|
||||
memoryCopy(out->meshOffsets[m], offset, sizeof(vec3));
|
||||
|
||||
(*ioIndex)++;
|
||||
}
|
||||
}
|
||||
|
||||
static void assetChunkParseEntities(yyjson_val *root, assetchunkoutput_t *out) {
|
||||
out->entitySpawnCount = 0;
|
||||
|
||||
yyjson_val *arr = yyjson_obj_get(root, "entities");
|
||||
if(!yyjson_is_arr(arr)) return;
|
||||
|
||||
size_t idx, len;
|
||||
yyjson_val *val;
|
||||
yyjson_arr_foreach(arr, idx, len, val) {
|
||||
if(out->entitySpawnCount >= CHUNK_ENTITY_SPAWN_COUNT_MAX) break;
|
||||
|
||||
const char_t *type = yyjson_get_str(yyjson_obj_get(val, "type"));
|
||||
yyjson_val *posArr = yyjson_obj_get(val, "pos");
|
||||
if(type == NULL || !yyjson_is_arr(posArr) || yyjson_arr_size(posArr) < 3) {
|
||||
continue;
|
||||
}
|
||||
worldpos_t pos = {
|
||||
.x = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 0)),
|
||||
.y = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 1)),
|
||||
.z = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 2)),
|
||||
};
|
||||
|
||||
chunkentityspawn_t *spawn = &out->entitySpawns[out->entitySpawnCount];
|
||||
if(stringEquals(type, "global")) {
|
||||
spawn->kind = CHUNK_ENTITY_SPAWN_KIND_GLOBAL;
|
||||
spawn->globalId =
|
||||
(uint16_t)yyjson_get_int(yyjson_obj_get(val, "globalId"));
|
||||
spawn->itemId = 0;
|
||||
spawn->itemQuantity = 0;
|
||||
} else if(stringEquals(type, "item")) {
|
||||
spawn->kind = CHUNK_ENTITY_SPAWN_KIND_ITEM;
|
||||
spawn->globalId = 0;
|
||||
spawn->itemId = (uint16_t)yyjson_get_int(yyjson_obj_get(val, "itemId"));
|
||||
spawn->itemQuantity =
|
||||
(uint8_t)yyjson_get_int(yyjson_obj_get(val, "quantity"));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
spawn->position = pos;
|
||||
out->entitySpawnCount++;
|
||||
}
|
||||
}
|
||||
|
||||
static void assetChunkParseAreas(yyjson_val *root, assetchunkoutput_t *out) {
|
||||
out->areaSpawnCount = 0;
|
||||
|
||||
yyjson_val *arr = yyjson_obj_get(root, "areas");
|
||||
if(!yyjson_is_arr(arr)) return;
|
||||
|
||||
size_t idx, len;
|
||||
yyjson_val *val;
|
||||
yyjson_arr_foreach(arr, idx, len, val) {
|
||||
if(out->areaSpawnCount >= CHUNK_AREA_COUNT_MAX) break;
|
||||
|
||||
yyjson_val *minArr = yyjson_obj_get(val, "min");
|
||||
yyjson_val *maxArr = yyjson_obj_get(val, "max");
|
||||
if(
|
||||
!yyjson_is_arr(minArr) || yyjson_arr_size(minArr) < 3 ||
|
||||
!yyjson_is_arr(maxArr) || yyjson_arr_size(maxArr) < 3
|
||||
) continue;
|
||||
|
||||
chunkareaspawn_t *area = &out->areaSpawns[out->areaSpawnCount];
|
||||
area->min = (worldpos_t){
|
||||
.x = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 0)),
|
||||
.y = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 1)),
|
||||
.z = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 2)),
|
||||
};
|
||||
area->max = (worldpos_t){
|
||||
.x = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 0)),
|
||||
.y = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 1)),
|
||||
.z = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 2)),
|
||||
};
|
||||
area->callbackId =
|
||||
(uint16_t)yyjson_get_int(yyjson_obj_get(val, "callbackId"));
|
||||
area->notify = (uint8_t)yyjson_get_int(yyjson_obj_get(val, "notify"));
|
||||
area->trigger = (uint8_t)yyjson_get_int(yyjson_obj_get(val, "trigger"));
|
||||
out->areaSpawnCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Six vertices (2 CCW triangles) for one tile's quad, matching
|
||||
// tools/asset/chunk/__main__.py's _tile_quad. Corner order: SW=(x,y),
|
||||
// SE=(x+1,y), NE=(x+1,y+1), NW=(x,y+1). NORTH=+Y, EAST=+X.
|
||||
static void assetChunkAppendTileQuad(
|
||||
meshvertex_t *verts, uint32_t *vertCount,
|
||||
const uint32_t x, const uint32_t y,
|
||||
const float_t sw, const float_t se, const float_t ne, const float_t nw
|
||||
) {
|
||||
const float_t u0 = (float_t)x / (float_t)CHUNK_WIDTH;
|
||||
const float_t u1 = (float_t)(x + 1) / (float_t)CHUNK_WIDTH;
|
||||
const float_t v0 = (float_t)y / (float_t)CHUNK_HEIGHT;
|
||||
const float_t v1 = (float_t)(y + 1) / (float_t)CHUNK_HEIGHT;
|
||||
const float_t fx = (float_t)x;
|
||||
const float_t fy = (float_t)y;
|
||||
|
||||
meshvertex_t *v = verts + *vertCount;
|
||||
v[0] = (meshvertex_t){ .uv = {u0, v0}, .pos = {fx, fy, sw} };
|
||||
v[1] = (meshvertex_t){ .uv = {u1, v0}, .pos = {fx + 1, fy, se} };
|
||||
v[2] = (meshvertex_t){ .uv = {u1, v1}, .pos = {fx + 1, fy + 1, ne} };
|
||||
v[3] = (meshvertex_t){ .uv = {u0, v0}, .pos = {fx, fy, sw} };
|
||||
v[4] = (meshvertex_t){ .uv = {u1, v1}, .pos = {fx + 1, fy + 1, ne} };
|
||||
v[5] = (meshvertex_t){ .uv = {u0, v1}, .pos = {fx, fy + 1, nw} };
|
||||
*vertCount += 6;
|
||||
}
|
||||
|
||||
// Builds a mesh_t straight from the tile grid - one quad per non-null
|
||||
// tile column, shaped by CHUNK_RAMP_CORNERS - the runtime equivalent of
|
||||
// tools/asset/chunk/__main__.py's build_terrain_verts. Only called once
|
||||
// assetChunkTilesHaveTerrain has confirmed at least one real tile exists.
|
||||
// Mirrors assetMeshLoaderSync's meshInit/meshFlush/free-after-upload
|
||||
// sequence (assetmeshloader.c) since this produces the same
|
||||
// assetmeshoutput_t shape, just from generated verts instead of a file.
|
||||
static errorret_t assetChunkBuildTerrain(
|
||||
const tile_t *tiles, assetmeshoutput_t *out
|
||||
) {
|
||||
meshvertex_t *verts = memoryAllocate(
|
||||
CHUNK_TILE_COUNT * 6 * sizeof(meshvertex_t)
|
||||
);
|
||||
uint32_t vertCount = 0;
|
||||
|
||||
for(uint32_t y = 0; y < CHUNK_HEIGHT; y++) {
|
||||
for(uint32_t x = 0; x < CHUNK_WIDTH; x++) {
|
||||
const tile_t *tile = &tiles[x + y * CHUNK_WIDTH];
|
||||
if(tile->shape == TILE_SHAPE_NULL) continue;
|
||||
|
||||
const float_t *corners = CHUNK_RAMP_CORNERS[tile->shape];
|
||||
const float_t fz = (float_t)tile->z;
|
||||
assetChunkAppendTileQuad(
|
||||
verts, &vertCount, x, y,
|
||||
(fz + corners[0]) * WORLD_LAYER_HEIGHT,
|
||||
(fz + corners[1]) * WORLD_LAYER_HEIGHT,
|
||||
(fz + corners[2]) * WORLD_LAYER_HEIGHT,
|
||||
(fz + corners[3]) * WORLD_LAYER_HEIGHT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assertNull(loading->loading.chunk.data, "Data already defined?");
|
||||
assertTrue(vertCount > 0, "Terrain build called with no terrain tiles.");
|
||||
|
||||
assetfile_t *file = &loading->loading.chunk.file;
|
||||
assetLoaderErrorChain(loading,
|
||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||
out->vertices = verts;
|
||||
errorret_t ret = meshInit(
|
||||
&out->mesh, MESH_PRIMITIVE_TYPE_TRIANGLES, (int32_t)vertCount, verts
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
errorChain(ret);
|
||||
}
|
||||
out->meshInitialized = true;
|
||||
|
||||
uint8_t *data = memoryAllocate(file->size);
|
||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
|
||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||
assertTrue(
|
||||
file->lastRead == file->size,
|
||||
"Failed to read entire chunk file."
|
||||
);
|
||||
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
|
||||
if(errorIsNotOk(ret)) {
|
||||
meshDispose(&out->mesh);
|
||||
out->meshInitialized = false;
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
#if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
#endif
|
||||
|
||||
loading->loading.chunk.data = data;
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -55,8 +298,8 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
|
||||
switch(loading->loading.chunk.state) {
|
||||
case ASSET_CHUNK_LOADING_STATE_INITIAL:
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
|
||||
case ASSET_CHUNK_LOADING_STATE_PARSE:
|
||||
@@ -69,9 +312,7 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
out->modelEntries[m] = assetLock(
|
||||
out->modelNames[m], ASSET_LOADER_TYPE_MODEL, NULL
|
||||
);
|
||||
assertNotNull(
|
||||
out->modelEntries[m], "Failed to lock model."
|
||||
);
|
||||
assertNotNull(out->modelEntries[m], "Failed to lock model.");
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
@@ -93,126 +334,103 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t *data = loading->loading.chunk.data;
|
||||
assertNotNull(data, "Chunk data should have been loaded by now.");
|
||||
|
||||
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'F') {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(loading, "Invalid chunk file header");
|
||||
}
|
||||
|
||||
uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4));
|
||||
if(version != ASSET_CHUNK_FILE_VERSION) {
|
||||
memoryFree(data);
|
||||
// --- PARSE: lock+require the chunk's own JSON as a sub-asset, exactly
|
||||
// like assetmodelloader.c's ASSET_MODEL_LOADING_STATE_LOCK_ASSETS does
|
||||
// for its model descriptor, then read everything out of it before
|
||||
// unlocking (the yyjson_doc/vals only stay valid while jsonEntry does).
|
||||
char_t jsonKey[ASSET_FILE_NAME_MAX];
|
||||
stringFormat(jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name);
|
||||
assetloaderinput_t jsonInput;
|
||||
memoryZero(&jsonInput, sizeof(jsonInput));
|
||||
stringCopy(
|
||||
jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX
|
||||
);
|
||||
assetentry_t *jsonEntry =
|
||||
assetLock(jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput);
|
||||
errorret_t jsonRet = assetRequireLoaded(jsonEntry);
|
||||
if(errorIsNotOk(jsonRet)) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Unsupported chunk file version %u", version
|
||||
loading, "Failed to load chunk JSON: %s", loading->entry->name
|
||||
);
|
||||
}
|
||||
|
||||
size_t offset = 8;
|
||||
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
|
||||
|
||||
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
|
||||
out->tiles = memoryAllocate(tileSize);
|
||||
memoryCopy(out->tiles, data + offset, tileSize);
|
||||
offset += tileSize;
|
||||
tile_t tiles[CHUNK_TILE_COUNT];
|
||||
assetChunkParseTiles(root, tiles);
|
||||
out->hasTerrain = assetChunkTilesHaveTerrain(tiles);
|
||||
|
||||
for(size_t t = 0; t < CHUNK_TILE_COUNT; t++) {
|
||||
uint32_t *shape = (uint32_t *)&out->tiles[t].shape;
|
||||
*shape = endianLittleToHost32(*shape);
|
||||
}
|
||||
uint8_t meshIndex = out->hasTerrain ? 1 : 0;
|
||||
assetChunkParseMeshes(root, out, &meshIndex);
|
||||
out->meshCount = meshIndex;
|
||||
|
||||
out->meshCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->meshCount <= CHUNK_MESH_COUNT_MAX,
|
||||
"Chunk mesh count exceeds maximum."
|
||||
);
|
||||
assetChunkParseEntities(root, out);
|
||||
assetChunkParseAreas(root, out);
|
||||
|
||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
||||
uint8_t nameLen = 0;
|
||||
while(
|
||||
data[offset + nameLen] != '\0' &&
|
||||
nameLen < CHUNK_MESH_NAME_MAX - 1
|
||||
) {
|
||||
nameLen++;
|
||||
}
|
||||
memoryCopy(out->modelNames[m], data + offset, nameLen);
|
||||
out->modelNames[m][nameLen] = '\0';
|
||||
offset += nameLen + 1;
|
||||
assetUnlockEntry(jsonEntry);
|
||||
|
||||
memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3));
|
||||
offset += sizeof(vec3);
|
||||
out->meshOffsets[m][0] = endianLittleToHostFloat(out->meshOffsets[m][0]);
|
||||
out->meshOffsets[m][1] = endianLittleToHostFloat(out->meshOffsets[m][1]);
|
||||
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
|
||||
}
|
||||
out->tiles = memoryAllocate(CHUNK_TILE_COUNT * sizeof(tile_t));
|
||||
memoryCopy(out->tiles, tiles, CHUNK_TILE_COUNT * sizeof(tile_t));
|
||||
|
||||
out->entitySpawnCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->entitySpawnCount <= CHUNK_ENTITY_SPAWN_COUNT_MAX,
|
||||
"Chunk entity spawn count exceeds maximum."
|
||||
);
|
||||
|
||||
for(uint8_t s = 0; s < out->entitySpawnCount; s++) {
|
||||
chunkentityspawn_t *spawn = &out->entitySpawns[s];
|
||||
spawn->kind = (chunkentityspawnkind_t)data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
|
||||
uint16_t a;
|
||||
memoryCopy(&a, data + offset, sizeof(uint16_t));
|
||||
a = endianLittleToHost16(a);
|
||||
offset += sizeof(uint16_t);
|
||||
|
||||
uint8_t b = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
|
||||
if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_ITEM) {
|
||||
spawn->globalId = 0;
|
||||
spawn->itemId = a;
|
||||
spawn->itemQuantity = b;
|
||||
} else {
|
||||
spawn->globalId = a;
|
||||
spawn->itemId = 0;
|
||||
spawn->itemQuantity = 0;
|
||||
// Terrain: build the mesh, then wrap it in synthetic (non-cache,
|
||||
// non-refcounted) mesh/model entries so modelEntries[0] behaves exactly
|
||||
// like a real file-backed model entry for every downstream consumer -
|
||||
// see the header's doc comment on hasTerrain/terrainMeshEntry.
|
||||
if(out->hasTerrain) {
|
||||
assetmeshoutput_t terrainMesh;
|
||||
memoryZero(&terrainMesh, sizeof(terrainMesh));
|
||||
errorret_t terrainRet = assetChunkBuildTerrain(out->tiles, &terrainMesh);
|
||||
if(errorIsNotOk(terrainRet)) {
|
||||
memoryFree(out->tiles);
|
||||
out->tiles = NULL;
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Failed to build chunk terrain mesh: %s",
|
||||
loading->entry->name
|
||||
);
|
||||
}
|
||||
|
||||
spawn->position = worldPosReadLE(data, &offset);
|
||||
assetloaderinput_t texInput = { .texture = TEXTURE_FORMAT_RGBA };
|
||||
assetentry_t *texEntry = assetLock(
|
||||
ASSET_CHUNK_TERRAIN_TEXTURE, ASSET_LOADER_TYPE_TEXTURE, &texInput
|
||||
);
|
||||
errorret_t texRet = assetRequireLoaded(texEntry);
|
||||
if(errorIsNotOk(texRet)) {
|
||||
assetUnlockEntry(texEntry);
|
||||
meshDispose(&terrainMesh.mesh);
|
||||
memoryFree(out->tiles);
|
||||
out->tiles = NULL;
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Failed to load chunk terrain texture: %s",
|
||||
loading->entry->name
|
||||
);
|
||||
}
|
||||
out->terrainTexEntry = texEntry;
|
||||
|
||||
out->terrainMeshEntry = memoryAllocate(sizeof(assetentry_t));
|
||||
memoryZero(out->terrainMeshEntry, sizeof(assetentry_t));
|
||||
out->terrainMeshEntry->type = ASSET_LOADER_TYPE_MESH;
|
||||
out->terrainMeshEntry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
out->terrainMeshEntry->data.mesh = terrainMesh;
|
||||
|
||||
out->terrainModelEntry = memoryAllocate(sizeof(assetentry_t));
|
||||
memoryZero(out->terrainModelEntry, sizeof(assetentry_t));
|
||||
out->terrainModelEntry->type = ASSET_LOADER_TYPE_MODEL;
|
||||
out->terrainModelEntry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
out->terrainModelEntry->data.model.meshEntry = out->terrainMeshEntry;
|
||||
out->terrainModelEntry->data.model.texEntry = out->terrainTexEntry;
|
||||
out->terrainModelEntry->data.model.color = COLOR_WHITE;
|
||||
|
||||
out->modelEntries[0] = out->terrainModelEntry;
|
||||
}
|
||||
|
||||
out->areaSpawnCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->areaSpawnCount <= CHUNK_AREA_COUNT_MAX,
|
||||
"Chunk area spawn count exceeds maximum."
|
||||
);
|
||||
|
||||
for(uint8_t s = 0; s < out->areaSpawnCount; s++) {
|
||||
chunkareaspawn_t *area = &out->areaSpawns[s];
|
||||
area->min = worldPosReadLE(data, &offset);
|
||||
area->max = worldPosReadLE(data, &offset);
|
||||
|
||||
uint16_t callbackId;
|
||||
memoryCopy(&callbackId, data + offset, sizeof(uint16_t));
|
||||
area->callbackId = endianLittleToHost16(callbackId);
|
||||
offset += sizeof(uint16_t);
|
||||
|
||||
area->notify = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
area->trigger = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
}
|
||||
|
||||
memoryFree(data);
|
||||
loading->loading.chunk.data = NULL;
|
||||
|
||||
if(out->meshCount == 0) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
|
||||
loading->loading.chunk.modelIndex = 0;
|
||||
loading->loading.chunk.modelIndex = out->hasTerrain ? 1 : 0;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
@@ -231,8 +449,36 @@ errorret_t assetChunkDispose(assetentry_t *entry) {
|
||||
|
||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
||||
if(out->modelEntries[m] == NULL) continue;
|
||||
if(out->hasTerrain && m == 0) continue; // synthetic - freed below.
|
||||
assetUnlockEntry(out->modelEntries[m]);
|
||||
out->modelEntries[m] = NULL;
|
||||
}
|
||||
|
||||
if(out->hasTerrain) {
|
||||
if(out->terrainMeshEntry != NULL) {
|
||||
assetmeshoutput_t *meshOut = &out->terrainMeshEntry->data.mesh;
|
||||
if(meshOut->meshInitialized) {
|
||||
meshDispose(&meshOut->mesh);
|
||||
meshOut->meshInitialized = false;
|
||||
}
|
||||
if(meshOut->vertices != NULL) {
|
||||
memoryFree(meshOut->vertices);
|
||||
meshOut->vertices = NULL;
|
||||
}
|
||||
memoryFree(out->terrainMeshEntry);
|
||||
out->terrainMeshEntry = NULL;
|
||||
}
|
||||
if(out->terrainModelEntry != NULL) {
|
||||
memoryFree(out->terrainModelEntry);
|
||||
out->terrainModelEntry = NULL;
|
||||
}
|
||||
if(out->terrainTexEntry != NULL) {
|
||||
assetUnlockEntry(out->terrainTexEntry);
|
||||
out->terrainTexEntry = NULL;
|
||||
}
|
||||
out->modelEntries[0] = NULL;
|
||||
out->hasTerrain = false;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
#include "asset/assetfile.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
|
||||
#define ASSET_CHUNK_FILE_VERSION 5
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
@@ -20,16 +18,17 @@ typedef struct {
|
||||
|
||||
typedef enum {
|
||||
ASSET_CHUNK_LOADING_STATE_INITIAL,
|
||||
ASSET_CHUNK_LOADING_STATE_READ_FILE,
|
||||
ASSET_CHUNK_LOADING_STATE_PARSE,
|
||||
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS,
|
||||
ASSET_CHUNK_LOADING_STATE_DONE
|
||||
} assetchunkloadingstate_t;
|
||||
|
||||
typedef struct {
|
||||
assetfile_t file;
|
||||
assetchunkloadingstate_t state;
|
||||
uint8_t *data;
|
||||
|
||||
// Index into assetchunkoutput_t.modelNames/modelEntries the external
|
||||
// (non-terrain) model polling loop has reached - see
|
||||
// ASSET_CHUNK_LOADING_STATE_LOAD_MODELS.
|
||||
uint8_t modelIndex;
|
||||
} assetchunkloaderloading_t;
|
||||
|
||||
@@ -61,6 +60,27 @@ typedef struct {
|
||||
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
||||
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
||||
|
||||
// Terrain mesh generated at load time from the tile grid (no baked
|
||||
// .dmf/.json - see assetChunkBuildTerrain in assetchunkloader.c). NULL
|
||||
// (with hasTerrain false) for a chunk with no non-null tiles, in which
|
||||
// case modelEntries[0] (if meshCount > 0) is the first external mesh
|
||||
// instead - matches the old baked pipeline's "only write a terrain
|
||||
// model if it has vertices" behavior.
|
||||
//
|
||||
// terrainMeshEntry/terrainModelEntry are heap-allocated, plain
|
||||
// assetentry_t values that are never passed to assetLock/assetGetEntry/
|
||||
// assetUnlockEntry (only a real, name-keyed, ref-counted entry may be)
|
||||
// - they exist purely so modelEntries[0] can point at something with
|
||||
// the same ->state/->data.model/->data.mesh.mesh shape every other
|
||||
// model/mesh entry has, so chunk.c/map.c/sceneoverworld.c don't need to
|
||||
// know terrain isn't file-backed. Freed directly (not assetUnlockEntry)
|
||||
// in assetChunkDispose. terrainTexEntry is a real, ref-counted texture
|
||||
// entry (the shared "tiles.png") and must be assetUnlockEntry'd there.
|
||||
bool_t hasTerrain;
|
||||
assetentry_t *terrainMeshEntry;
|
||||
assetentry_t *terrainModelEntry;
|
||||
assetentry_t *terrainTexEntry;
|
||||
|
||||
uint8_t entitySpawnCount;
|
||||
chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX];
|
||||
|
||||
@@ -69,9 +89,10 @@ typedef struct {
|
||||
} assetchunkoutput_t;
|
||||
|
||||
/**
|
||||
* Asynchronous loader for chunk assets. Reads the raw DCF file bytes into
|
||||
* the loading buffer so the sync phase can parse without blocking the
|
||||
* main thread on I/O.
|
||||
* Async loader stub for chunk assets. Chunk loading is performed entirely
|
||||
* on the main thread (it delegates to the JSON asset loader and to
|
||||
* meshInit/meshFlush, both main-thread-only); this callback is never
|
||||
* reached.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
@@ -79,9 +100,11 @@ typedef struct {
|
||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for chunk assets. Validates the DCF binary previously
|
||||
* read by the async phase and populates the output assetchunkoutput_t with
|
||||
* tile data and model paths.
|
||||
* Synchronous loader for chunk assets. Locks the chunk's JSON descriptor
|
||||
* as a sub-asset, parses tiles/external mesh references/entity spawns/
|
||||
* area spawns, generates the terrain mesh directly from the tile grid,
|
||||
* and locks/waits for every referenced model (and the terrain's shared
|
||||
* tileset texture) to finish loading.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "assetcutsceneloader.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/asset.h"
|
||||
#include "rpg/cutscene/item/json/cutscenejsonpauseflags.h"
|
||||
|
||||
errorret_t assetCutsceneParseItem(
|
||||
assetloading_t *loading,
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset,
|
||||
const uint8_t index
|
||||
) {
|
||||
if(!yyjson_is_obj(itemObj)) {
|
||||
assetLoaderErrorThrow(
|
||||
loading,
|
||||
"Cutscene item %u is not an object",
|
||||
(uint32_t)index
|
||||
);
|
||||
}
|
||||
|
||||
const char_t *typeName = yyjson_get_str(yyjson_obj_get(itemObj, "type"));
|
||||
if(!cutsceneItemLookupType(typeName, &item->type)) {
|
||||
assetLoaderErrorThrow(
|
||||
loading,
|
||||
"Cutscene item %u: missing/unsupported item type %s",
|
||||
(uint32_t)index,
|
||||
typeName
|
||||
);
|
||||
}
|
||||
|
||||
cutsceneitemloadcallback_t *load = CUTSCENE_ITEM_CALLBACKS[item->type].load;
|
||||
if(load == NULL) {
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Cutscene item %u: unhandled item type %u", (uint32_t)index,
|
||||
(uint32_t)item->type
|
||||
);
|
||||
}
|
||||
|
||||
errorret_t result = load(itemObj, item, pool, poolOffset);
|
||||
if(errorIsNotOk(result)) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorChain(result);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
size_t assetCutsceneComputePoolSize(yyjson_val *itemsArr) {
|
||||
size_t size = 0;
|
||||
yyjson_arr_iter iter = yyjson_arr_iter_with(itemsArr);
|
||||
yyjson_val *itemVal;
|
||||
while((itemVal = yyjson_arr_iter_next(&iter)) != NULL) {
|
||||
const char_t *typeName = yyjson_get_str(yyjson_obj_get(itemVal, "type"));
|
||||
cutsceneitemtype_t type;
|
||||
if(!cutsceneItemLookupType(typeName, &type)) continue;
|
||||
|
||||
cutsceneitempoolsizecallback_t *poolSize =
|
||||
CUTSCENE_ITEM_CALLBACKS[type].poolSize;
|
||||
if(poolSize == NULL) continue;
|
||||
|
||||
size = cutsceneJsonPoolAlign(size) + poolSize(itemVal);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
void assetCutsceneFreeParsed(assetcutsceneoutput_t *out) {
|
||||
if(out->items != NULL) {
|
||||
memoryFree(out->items);
|
||||
out->items = NULL;
|
||||
}
|
||||
if(out->pool != NULL) {
|
||||
memoryFree(out->pool);
|
||||
out->pool = NULL;
|
||||
}
|
||||
if(out->doc != NULL) {
|
||||
yyjson_doc_free(out->doc);
|
||||
out->doc = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
|
||||
if(
|
||||
loading->loading.cutscene.state != ASSET_CUTSCENE_LOADING_STATE_READ_FILE
|
||||
) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assertNull(loading->loading.cutscene.buffer, "Buffer already defined?");
|
||||
|
||||
assetfile_t *file = &loading->loading.cutscene.file;
|
||||
assetLoaderErrorChain(loading,
|
||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||
);
|
||||
|
||||
if(file->size > ASSET_CUTSCENE_FILE_SIZE_MAX) {
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Cutscene JSON exceeds maximum allowed size"
|
||||
);
|
||||
}
|
||||
|
||||
size_t fileSize = (size_t)file->size;
|
||||
uint8_t *buffer = memoryAllocate(fileSize);
|
||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||
assetLoaderErrorChain(loading, assetFileRead(file, buffer, fileSize));
|
||||
assertTrue(
|
||||
file->lastRead == file->size,
|
||||
"Failed to read entire cutscene file."
|
||||
);
|
||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||
|
||||
loading->loading.cutscene.buffer = buffer;
|
||||
loading->loading.cutscene.size = fileSize;
|
||||
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_PARSE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetCutsceneLoaderSync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertTrue(loading->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
if(loading->loading.cutscene.state == ASSET_CUTSCENE_LOADING_STATE_INITIAL) {
|
||||
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_READ_FILE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assetcutsceneoutput_t *out = &loading->entry->data.cutscene;
|
||||
uint8_t *buffer = loading->loading.cutscene.buffer;
|
||||
assertNotNull(buffer, "Cutscene data should have been loaded by now.");
|
||||
size_t bufferSize = loading->loading.cutscene.size;
|
||||
|
||||
yyjson_doc *doc = yyjson_read(
|
||||
(char *)buffer, bufferSize,
|
||||
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
|
||||
);
|
||||
memoryFree(buffer);
|
||||
loading->loading.cutscene.buffer = NULL;
|
||||
|
||||
if(doc == NULL) {
|
||||
assetLoaderErrorThrow(loading, "Failed to parse cutscene JSON");
|
||||
}
|
||||
out->doc = doc;
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(doc);
|
||||
yyjson_val *itemsArr = yyjson_obj_get(root, "items");
|
||||
if(!yyjson_is_arr(itemsArr)) {
|
||||
assetCutsceneFreeParsed(out);
|
||||
assetLoaderErrorThrow(loading, "Cutscene JSON is missing an 'items' array");
|
||||
}
|
||||
|
||||
size_t itemCount = yyjson_arr_size(itemsArr);
|
||||
if(itemCount > UINT8_MAX) {
|
||||
assetCutsceneFreeParsed(out);
|
||||
assetLoaderErrorThrow(loading, "Cutscene has more than 255 items");
|
||||
}
|
||||
|
||||
cutscenepause_t pause = CUTSCENE_PAUSE_DEFAULT;
|
||||
yyjson_val *pauseArr = yyjson_obj_get(root, "pause");
|
||||
if(yyjson_is_arr(pauseArr)) {
|
||||
if(!cutsceneJsonParsePauseFlags(pauseArr, &pause)) {
|
||||
assetCutsceneFreeParsed(out);
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Cutscene JSON has an invalid 'pause' flag"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
out->items = memoryAllocate(itemCount * sizeof(cutsceneitem_t));
|
||||
memoryZero(out->items, itemCount * sizeof(cutsceneitem_t));
|
||||
|
||||
size_t poolSize = assetCutsceneComputePoolSize(itemsArr);
|
||||
out->pool = poolSize > 0 ? memoryAllocate(poolSize) : NULL;
|
||||
|
||||
size_t poolOffset = 0;
|
||||
yyjson_arr_iter itemIter = yyjson_arr_iter_with(itemsArr);
|
||||
yyjson_val *itemVal;
|
||||
uint8_t index = 0;
|
||||
while((itemVal = yyjson_arr_iter_next(&itemIter)) != NULL) {
|
||||
errorret_t itemResult = assetCutsceneParseItem(
|
||||
loading, itemVal, &out->items[index], out->pool, &poolOffset, index
|
||||
);
|
||||
if(errorIsNotOk(itemResult)) {
|
||||
assetCutsceneFreeParsed(out);
|
||||
errorChain(itemResult);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
out->cutscene.items = out->items;
|
||||
out->cutscene.itemCount = (uint8_t)itemCount;
|
||||
out->cutscene.pause = pause;
|
||||
out->cutscene.dataSize = 0;
|
||||
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetCutsceneDispose(assetentry_t *entry) {
|
||||
assertNotNull(entry, "Entry cannot be NULL");
|
||||
assertTrue(entry->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
assetCutsceneFreeParsed(&entry->data.cutscene);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "asset/assetfile.h"
|
||||
#include "rpg/cutscene/cutscene.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/item/json/cutscenejsonpool.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#define ASSET_CUTSCENE_FILE_SIZE_MAX (1024 * 64)
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
typedef struct {
|
||||
void *nothing;
|
||||
} assetcutsceneloaderinput_t;
|
||||
|
||||
typedef enum {
|
||||
ASSET_CUTSCENE_LOADING_STATE_INITIAL,
|
||||
ASSET_CUTSCENE_LOADING_STATE_READ_FILE,
|
||||
ASSET_CUTSCENE_LOADING_STATE_PARSE
|
||||
} assetcutsceneloadingstate_t;
|
||||
|
||||
typedef struct {
|
||||
assetfile_t file;
|
||||
assetcutsceneloadingstate_t state;
|
||||
uint8_t *buffer;
|
||||
size_t size;
|
||||
} assetcutsceneloaderloading_t;
|
||||
|
||||
typedef struct {
|
||||
cutscene_t cutscene;
|
||||
cutsceneitem_t *items;
|
||||
uint8_t *pool;
|
||||
yyjson_doc *doc;
|
||||
} assetcutsceneoutput_t;
|
||||
|
||||
/**
|
||||
* Parses one cutscene item object into item, writing any pool-backed
|
||||
* array data (entityWalkTo positions, mapAreaWait areaIds) into pool at
|
||||
* *poolOffset and advancing it. Every field/enum name is validated as
|
||||
* real (untrusted, authored) file content - failures are real errors, not
|
||||
* asserts. Dispatches to the matching type's Load callback (see
|
||||
* CUTSCENE_ITEM_CALLBACKS in rpg/cutscene/item/cutsceneitem.h) - the
|
||||
* per-type parsing logic itself lives there now, colocated with that
|
||||
* type's runtime behavior.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param pool Base pointer of the entry's pool allocation (may be NULL if
|
||||
* assetCutsceneComputePoolSize returned 0).
|
||||
* @param poolOffset In/out cursor into pool.
|
||||
* @param index Index of this item within the cutscene, for error messages.
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t assetCutsceneParseItem(
|
||||
assetloading_t *loading,
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset,
|
||||
const uint8_t index
|
||||
);
|
||||
|
||||
/**
|
||||
* Frees whatever of out->items/out->pool/out->doc is currently non-NULL
|
||||
* and clears them - shared by both the normal disposer and every parse
|
||||
* failure path's cleanup.
|
||||
*
|
||||
* @param out The output to free.
|
||||
*/
|
||||
void assetCutsceneFreeParsed(assetcutsceneoutput_t *out);
|
||||
|
||||
/**
|
||||
* Asynchronous loader for cutscene assets. Reads the raw JSONC file bytes
|
||||
* into the loading buffer so the sync phase can parse without blocking the
|
||||
* main thread on I/O.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
*/
|
||||
errorret_t assetCutsceneLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for cutscene assets. Parses the JSONC previously read
|
||||
* by the async phase into a heap-allocated cutsceneitem_t array + pool.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
*/
|
||||
errorret_t assetCutsceneLoaderSync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Disposer for cutscene assets.
|
||||
*
|
||||
* @param entry Asset entry containing the cutscene data to dispose.
|
||||
* @return Error code indicating success or failure of the dispose operation.
|
||||
*/
|
||||
errorret_t assetCutsceneDispose(assetentry_t *entry);
|
||||
@@ -12,11 +12,10 @@
|
||||
|
||||
// Forward-declared rather than including asset/asset.h - this header only
|
||||
// ever needs a pointer to it, and pulling in the full asset module here
|
||||
// creates a real include cycle for anything that reaches this header via
|
||||
// asset/loader/cutscene/assetcutsceneloader.h (asset.h -> ... ->
|
||||
// assetloader.h -> assetcutsceneloader.h -> cutscene.h -> cutsceneitem.h
|
||||
// -> the audio cutscene items -> this header -> asset/asset.h again,
|
||||
// mid-parse). audiomixerchannel.c itself still includes the full header.
|
||||
// risks an include cycle for anything that reaches this header via a
|
||||
// cutscene item (cutscene.h -> cutsceneitem.h -> the audio cutscene items
|
||||
// -> this header -> asset/asset.h, mid-parse). audiomixerchannel.c itself
|
||||
// still includes the full header.
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
typedef struct audiostream_s audiostream_t;
|
||||
|
||||
@@ -87,10 +87,10 @@ errorret_t audioStreamPcmSeek(audiostream_t *stream, const size_t frame) {
|
||||
|
||||
const size_t currentByte = (size_t) stream->pcm.file.position;
|
||||
if(targetByte < currentByte) {
|
||||
// Only a full rewind can move a read cursor earlier once it's already
|
||||
// advanced past a point - a compressed archive entry can't be decoded
|
||||
// backward. See this function's own doc comment for the performance
|
||||
// implications of a deep loopTo.
|
||||
// A rewind is needed to move the read cursor earlier once it's
|
||||
// already advanced past a point - assetFileRewind() takes the cheap
|
||||
// in-place seek path since every dusk.dsk entry is stored
|
||||
// uncompressed.
|
||||
errorChain(assetFileRewind(&stream->pcm.file));
|
||||
errorChain(assetFileRead(&stream->pcm.file, NULL, targetByte));
|
||||
} else if(targetByte > currentByte) {
|
||||
|
||||
@@ -63,11 +63,8 @@ size_t audioStreamPcmGetTotalFrames(const audiostream_t *stream);
|
||||
* (relative to the start of the PCM data - the same units as
|
||||
* audiostream_t's startFrame/loopStart/loopTo, once converted from
|
||||
* seconds). Seeking backward re-reads from the start of the underlying
|
||||
* asset file (see assetFileRewind()) since a compressed archive entry can
|
||||
* only be decoded forward - this makes a loop with a deep loopTo more
|
||||
* expensive to restart than one near the start, which is a real
|
||||
* performance caveat, not just a theoretical one, for anything backed by
|
||||
* a compressed (not stored) asset archive entry.
|
||||
* asset file (see assetFileRewind()), which is a cheap in-place seek
|
||||
* since every dusk.dsk entry is stored uncompressed.
|
||||
*
|
||||
* @param stream The audio stream to seek. Must be AUDIO_STREAM_TYPE_PCM.
|
||||
* @param frame Frame offset to seek to, relative to the start of the PCM
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "engine.h"
|
||||
#include "util/memory.h"
|
||||
#include "perf/perf.h"
|
||||
#include "time/time.h"
|
||||
#include "input/input.h"
|
||||
#include "locale/localemanager.h"
|
||||
@@ -36,6 +37,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
ENGINE.version = DUSK_VERSION;
|
||||
|
||||
// Init systems. Order is important.
|
||||
perfInit();
|
||||
timeInit();
|
||||
consoleInit();
|
||||
errorChain(systemInit());
|
||||
@@ -45,8 +47,8 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
errorChain(localeManagerInit());
|
||||
errorChain(displayInit());
|
||||
errorChain(audioInit());
|
||||
errorChain(uiInit());
|
||||
errorChain(rpgInit());
|
||||
errorChain(uiInit());
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkInit());
|
||||
#endif
|
||||
@@ -104,6 +106,9 @@ errorret_t engineDispose(void) {
|
||||
errorChain(displayDispose());
|
||||
errorChain(saveDispose());
|
||||
errorChain(assetDispose());
|
||||
perfDispose();
|
||||
|
||||
memoryCheckUnfreed();
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
testbattle.c
|
||||
perf.c
|
||||
)
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "perf.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "console/console.h"
|
||||
|
||||
#if defined(DUSK_LINUX)
|
||||
#include <time.h>
|
||||
#elif defined(DUSK_PSP)
|
||||
#include <pspthreadman.h>
|
||||
#endif
|
||||
|
||||
perf_t PERF;
|
||||
|
||||
void perfInit(void) {
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
memoryZero(&PERF, sizeof(perf_t));
|
||||
}
|
||||
|
||||
void perfDispose(void) {
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
}
|
||||
|
||||
uint64_t perfGetTick(void) {
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
#if defined(DUSK_LINUX)
|
||||
// TEST - CLOCK_MONOTONIC via clock_gettime(), Linux desktop only for
|
||||
// now. Not a cross-platform answer yet (see perfGetTick's doc
|
||||
// comment in perf.h) - just something real to look at while that
|
||||
// gets settled. Returns nanoseconds since some unspecified starting
|
||||
// point, so only differences between two calls are meaningful.
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
|
||||
#elif defined(DUSK_PSP)
|
||||
// TEST - sceKernelGetSystemTimeWide(), microseconds since boot at
|
||||
// ~1us resolution (the standard PSP homebrew idiom for elapsed-time
|
||||
// measurement - psprtc.h's sceRtcGetCurrentTick is for wall-clock
|
||||
// date/time instead, already used by timepsp.c for that purpose).
|
||||
// Scaled to nanoseconds to match every other platform's tick unit.
|
||||
return (uint64_t)sceKernelGetSystemTimeWide() * 1000ull;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void perfPushItem(
|
||||
const char_t *label, const char_t *file, const int32_t line
|
||||
) {
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
assertNotNull(label, "Perf item label cannot be NULL.");
|
||||
assertNotNull(file, "Perf item file cannot be NULL.");
|
||||
assertTrue(PERF.stackSize < PERF_STACK_SIZE, "Perf stack overflow.");
|
||||
|
||||
PERF.stack[PERF.stackSize++] = (perfitem_t){
|
||||
.label = label,
|
||||
.file = file,
|
||||
.line = line,
|
||||
.tick = perfGetTick()
|
||||
};
|
||||
}
|
||||
|
||||
void perfPopItem(void) {
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
assertTrue(PERF.stackSize > 0, "Perf stack underflow.");
|
||||
PERF.stackSize--;
|
||||
}
|
||||
|
||||
void perfReport(void) {
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
if(PERF.stackSize == 0) return;
|
||||
|
||||
const perfitem_t *item = &PERF.stack[PERF.stackSize - 1];
|
||||
uint64_t elapsedNs = perfGetTick() - item->tick;
|
||||
double_t elapsedMs = (double_t)elapsedNs / 1000000.0;
|
||||
consolePrint(
|
||||
"perf: %s (%s:%d, %.4fms)",
|
||||
item->label, item->file, item->line, elapsedMs
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "perfitem.h"
|
||||
|
||||
#define PERF_STACK_SIZE 256
|
||||
|
||||
typedef struct {
|
||||
perfitem_t stack[PERF_STACK_SIZE];
|
||||
uint32_t stackSize;
|
||||
} perf_t;
|
||||
|
||||
extern perf_t PERF;
|
||||
|
||||
/**
|
||||
* Initializes the perf system.
|
||||
*/
|
||||
void perfInit(void);
|
||||
|
||||
/**
|
||||
* Disposes/cleans up the perf system. Currently a no-op - here so
|
||||
* engine.c has a symmetric call to make once this needs real cleanup.
|
||||
*/
|
||||
void perfDispose(void);
|
||||
|
||||
/**
|
||||
* Returns a tick value representing "now", for measuring the duration
|
||||
* between a perfPush/perfPushItem and its matching perfPopItem. Always
|
||||
* nanoseconds since some unspecified starting point, on every platform
|
||||
* that implements it, so perfReport's ms conversion stays correct
|
||||
* everywhere without per-platform special-casing.
|
||||
*
|
||||
* PLACEHOLDER - currently only reads a real value on DUSK_LINUX desktop
|
||||
* builds (via clock_gettime(CLOCK_MONOTONIC)) and DUSK_PSP (via
|
||||
* sceKernelGetSystemTimeWide(), scaled from microseconds), as a test;
|
||||
* every other platform still gets a meaningless 0. A common,
|
||||
* cross-platform way of reading CPU cycles/time hasn't been settled on
|
||||
* yet - don't treat results as comparable across platforms until this is
|
||||
* replaced with a real per-platform implementation. Only the difference
|
||||
* between two calls is meaningful, never the raw value itself.
|
||||
*
|
||||
* @return A tick value, in nanoseconds. 0 on unimplemented platforms.
|
||||
*/
|
||||
uint64_t perfGetTick(void);
|
||||
|
||||
/**
|
||||
* Pushes a new item onto the perf stack, capturing the current tick (see
|
||||
* perfGetTick) as its start time. Prefer the perfPush() macro, which
|
||||
* supplies file/line automatically.
|
||||
*
|
||||
* @param label Short caller-supplied memo of what this item represents
|
||||
* (e.g. "frame start") - stored by pointer, not copied, so pass a
|
||||
* string literal or something that otherwise outlives the push.
|
||||
* @param file Source file the push originated from (e.g. __FILE__).
|
||||
* @param line Source line the push originated from (e.g. __LINE__).
|
||||
*/
|
||||
void perfPushItem(
|
||||
const char_t *label, const char_t *file, const int32_t line
|
||||
);
|
||||
|
||||
/**
|
||||
* Pops the most recently pushed perf item off the stack.
|
||||
*/
|
||||
void perfPopItem(void);
|
||||
|
||||
/**
|
||||
* Reports on the item currently at the top of the perf stack (the most
|
||||
* recently pushed one still awaiting its perfPopItem), if any.
|
||||
*/
|
||||
void perfReport(void);
|
||||
|
||||
/**
|
||||
* Pushes a new perf item onto the stack, tagged with the call site this
|
||||
* macro is expanded at.
|
||||
*
|
||||
* @param label Short memo of what this item represents (e.g. "frame
|
||||
* start") - see perfPushItem.
|
||||
*/
|
||||
#define perfPush(label) perfPushItem(label, __FILE__, __LINE__)
|
||||
|
||||
/**
|
||||
* Pops the most recently pushed perf item off the stack.
|
||||
*/
|
||||
#define perfPop() perfPopItem()
|
||||
@@ -8,4 +8,9 @@
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef uint8_t storyflagvalue_t;
|
||||
typedef struct {
|
||||
const char_t *label;
|
||||
int32_t line;
|
||||
const char_t *file;
|
||||
uint64_t tick;
|
||||
} perfitem_t;
|
||||
@@ -7,9 +7,9 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
battle.c
|
||||
battlefighter.c
|
||||
party.c
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(testbattle)
|
||||
add_subdirectory(fighter)
|
||||
add_subdirectory(move)
|
||||
add_subdirectory(state)
|
||||
|
||||
+42
-291
@@ -9,320 +9,71 @@
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
battle_t BATTLE;
|
||||
|
||||
void battleInit(void) {
|
||||
memoryZero(&BATTLE, sizeof(battle_t));
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
BATTLE.fighters[i].id = i;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t battleGetAvailableFighter(void) {
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(BATTLE.fighters[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
||||
}
|
||||
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
battlefighter_t *battleAddFighter(
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
const uint8_t index = battleGetAvailableFighter();
|
||||
if(index == 0xFF) return NULL;
|
||||
|
||||
battlefighter_t *fighter = &BATTLE.fighters[index];
|
||||
battleFighterInit(fighter, team, controller, stats, healthMax, mpMax);
|
||||
return fighter;
|
||||
}
|
||||
|
||||
void battleStart(
|
||||
const battleencountertype_t encounterType,
|
||||
const bool_t fleeAvailable
|
||||
battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT]
|
||||
) {
|
||||
assertTrue(encounterType < BATTLE_ENCOUNTER_COUNT, "Invalid encounter type");
|
||||
assertNotNull(fighters, "Fighters cannot be NULL");
|
||||
|
||||
BATTLE.encounterType = encounterType;
|
||||
BATTLE.fleeAvailable = fleeAvailable;
|
||||
BATTLE.result = BATTLE_RESULT_NONE;
|
||||
BATTLE.round = 1;
|
||||
|
||||
battleSetState(BATTLE_STATE_OPENING);
|
||||
}
|
||||
|
||||
void battleDispose(void) {
|
||||
battleInit();
|
||||
}
|
||||
|
||||
battlefighter_t *battleGetCurrentFighter(void) {
|
||||
if(BATTLE.state != BATTLE_STATE_PLAYER_SELECTION) return NULL;
|
||||
if(BATTLE.selectionIndex >= BATTLE.executionCount) return NULL;
|
||||
return &BATTLE.fighters[BATTLE.executionOrder[BATTLE.selectionIndex]];
|
||||
}
|
||||
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
|
||||
uint8_t count = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(BATTLE.fighters[i].team != team) continue;
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void battleResolveAttack(
|
||||
battlefighter_t *attacker,
|
||||
battlefighter_t *defender
|
||||
) {
|
||||
assertNotNull(attacker, "Attacker cannot be NULL");
|
||||
assertNotNull(defender, "Defender cannot be NULL");
|
||||
|
||||
const int32_t rawDamage =
|
||||
(int32_t)attacker->stats.attack - (int32_t)defender->stats.defense;
|
||||
const uint16_t damage = rawDamage > 0 ? (uint16_t)rawDamage : 1;
|
||||
|
||||
defender->health = damage >= defender->health ? 0 : defender->health - damage;
|
||||
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
|
||||
}
|
||||
|
||||
battleresult_t battleCheckResult(void) {
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
|
||||
|
||||
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
|
||||
battleSetResult(BATTLE_RESULT_LOSS);
|
||||
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
|
||||
battleSetResult(BATTLE_RESULT_WIN);
|
||||
}
|
||||
|
||||
return BATTLE.result;
|
||||
}
|
||||
|
||||
void battleQueueAction(
|
||||
const uint8_t fighterIndex,
|
||||
const battleactiontype_t type,
|
||||
const uint8_t targetIndex
|
||||
) {
|
||||
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
||||
action->type = type;
|
||||
action->targetIndex = targetIndex;
|
||||
|
||||
if(BATTLE.onActionDecided != NULL) {
|
||||
BATTLE.onActionDecided(&BATTLE.fighters[fighterIndex], action);
|
||||
}
|
||||
}
|
||||
|
||||
void battlePlayerAttack(const uint8_t targetIndex) {
|
||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[targetIndex])) return;
|
||||
|
||||
battleQueueAction(fighter->id, BATTLE_ACTION_ATTACK, targetIndex);
|
||||
BATTLE.selectionIndex++;
|
||||
battleAdvanceSelection();
|
||||
}
|
||||
|
||||
void battlePlayerFlee(void) {
|
||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
if(!BATTLE.fleeAvailable) return;
|
||||
|
||||
battleSetResult(BATTLE_RESULT_FLED);
|
||||
}
|
||||
|
||||
void battleUpdate(void) {
|
||||
if(BATTLE.state == BATTLE_STATE_NONE) return;
|
||||
if(BATTLE.state == BATTLE_STATE_ENDED) return;
|
||||
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_BATTLE) return;
|
||||
|
||||
switch(BATTLE.state) {
|
||||
case BATTLE_STATE_OPENING:
|
||||
battleSetState(BATTLE_STATE_PRE_ROUND);
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_PRE_ROUND:
|
||||
battleUpdatePreRound();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_AI_SELECTION:
|
||||
battleUpdateAiSelection();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_MOVES_EXECUTING:
|
||||
battleUpdateMovesExecuting();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_POST_ROUND:
|
||||
battleUpdatePostRound();
|
||||
break;
|
||||
|
||||
default:
|
||||
// BATTLE_STATE_PLAYER_SELECTION: waits on battlePlayerAttack/Flee.
|
||||
// BATTLE_STATE_NONE/ENDED: handled above.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void battleBuildExecutionOrder(const bool_t applyEncounterBias) {
|
||||
BATTLE.executionCount = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
BATTLE.executionOrder[BATTLE.executionCount++] = i;
|
||||
}
|
||||
|
||||
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
|
||||
for(uint8_t i = 1; i < BATTLE.executionCount; i++) {
|
||||
const uint8_t key = BATTLE.executionOrder[i];
|
||||
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
|
||||
|
||||
int8_t j = (int8_t)i - 1;
|
||||
while(
|
||||
j >= 0 &&
|
||||
BATTLE.fighters[BATTLE.executionOrder[j]].stats.speed < keySpeed
|
||||
) {
|
||||
BATTLE.executionOrder[j + 1] = BATTLE.executionOrder[j];
|
||||
j--;
|
||||
#if DUSK_ASSERTIONS
|
||||
// Must be at least two non null fighter present, may need to ensure they
|
||||
// are from seperate teams also.
|
||||
uint8_t nonNullCount = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
|
||||
if(fighters[i] != NULL) nonNullCount++;
|
||||
}
|
||||
BATTLE.executionOrder[j + 1] = key;
|
||||
}
|
||||
assertTrue(nonNullCount >= 2, "Must have at least two fighters");
|
||||
#endif
|
||||
|
||||
if(!applyEncounterBias) return;
|
||||
|
||||
if(BATTLE.encounterType == BATTLE_ENCOUNTER_PLAYER_ADVANTAGE) {
|
||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ALLY);
|
||||
} else if(BATTLE.encounterType == BATTLE_ENCOUNTER_BACK_ATTACK) {
|
||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ENEMY);
|
||||
}
|
||||
}
|
||||
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team) {
|
||||
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t count = 0;
|
||||
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.executionOrder[i]].team != team) continue;
|
||||
sorted[count++] = BATTLE.executionOrder[i];
|
||||
}
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.executionOrder[i]].team == team) continue;
|
||||
sorted[count++] = BATTLE.executionOrder[i];
|
||||
}
|
||||
|
||||
// TODO: I may not need to zero entire battle.
|
||||
memoryZero(&BATTLE, sizeof(battle_t));
|
||||
memoryCopy(
|
||||
BATTLE.executionOrder, sorted, sizeof(uint8_t) * BATTLE.executionCount
|
||||
BATTLE.fighters,
|
||||
fighters,
|
||||
sizeof(battlefighter_t *) * BATTLE_FIGHTER_POS_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
||||
const battlefighterteam_t enemyTeam =
|
||||
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
|
||||
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
|
||||
// Enter the opening state.
|
||||
battleSetState(BATTLE_STATE_OPENING);
|
||||
|
||||
battlefighter_t *weakest = NULL;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
battlefighter_t *candidate = &BATTLE.fighters[i];
|
||||
if(candidate->team != enemyTeam) continue;
|
||||
if(!battleFighterIsAlive(candidate)) continue;
|
||||
if(weakest == NULL || candidate->health < weakest->health) {
|
||||
weakest = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return weakest;
|
||||
// Change scene
|
||||
sceneSet(SCENE_TYPE_BATTLE);
|
||||
}
|
||||
|
||||
void battleSetState(const battlestate_t next) {
|
||||
const battlestate_t previous = BATTLE.state;
|
||||
assertTrue(next < BATTLE_STATE_COUNT, "Invalid battle state");
|
||||
assertNotNull(BATTLE_STATE_FUNCTIONS[next].init, "Missing state init function");
|
||||
assertNotNull(BATTLE_STATE_FUNCTIONS[next].update, "Missing state update function");
|
||||
assertNotNull(BATTLE_STATE_FUNCTIONS[next].dispose, "Missing state dispose function");
|
||||
|
||||
if(BATTLE.state != BATTLE_STATE_NULL) {
|
||||
BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose();
|
||||
}
|
||||
BATTLE.state = next;
|
||||
if(BATTLE.onStateChanged != NULL) BATTLE.onStateChanged(previous, next);
|
||||
BATTLE_STATE_FUNCTIONS[BATTLE.state].init();
|
||||
}
|
||||
|
||||
void battleSetResult(const battleresult_t result) {
|
||||
BATTLE.result = result;
|
||||
battleSetState(BATTLE_STATE_ENDED);
|
||||
void battleUpdate(void) {
|
||||
if(BATTLE.state == BATTLE_STATE_NULL) return;
|
||||
assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state");
|
||||
assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].update, "Missing state update function");
|
||||
|
||||
BATTLE_STATE_FUNCTIONS[BATTLE.state].update();
|
||||
}
|
||||
|
||||
bool_t battleFighterNeedsDecision(
|
||||
const uint8_t fighterIndex,
|
||||
const battlefightercontroller_t controller
|
||||
) {
|
||||
return battleFighterIsAlive(&BATTLE.fighters[fighterIndex])
|
||||
&& BATTLE.fighters[fighterIndex].controller == controller
|
||||
&& BATTLE.actions[fighterIndex].type == BATTLE_ACTION_NONE;
|
||||
}
|
||||
void battleDispose(void) {
|
||||
if(BATTLE.state == BATTLE_STATE_NULL) return;
|
||||
assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state");
|
||||
assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose, "Missing state dispose function");
|
||||
|
||||
void battleAdvanceSelection(void) {
|
||||
while(BATTLE.selectionIndex < BATTLE.executionCount) {
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.selectionIndex];
|
||||
if(
|
||||
battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_PLAYER)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
BATTLE.selectionIndex++;
|
||||
}
|
||||
|
||||
battleSetState(BATTLE_STATE_AI_SELECTION);
|
||||
}
|
||||
|
||||
void battleUpdatePreRound(void) {
|
||||
// No need to clear BATTLE.actions here: every living fighter's action is
|
||||
// unconditionally reset to BATTLE_ACTION_NONE as it's processed in
|
||||
// battleUpdateMovesExecuting, and round 1 starts pre-zeroed by
|
||||
// battleInit(). Clearing it here would also wipe out any action a
|
||||
// cutscene force-queued while parked at BATTLE_STATE_PRE_ROUND.
|
||||
battleBuildExecutionOrder(BATTLE.round == 1);
|
||||
|
||||
BATTLE.selectionIndex = 0;
|
||||
battleSetState(BATTLE_STATE_PLAYER_SELECTION);
|
||||
battleAdvanceSelection();
|
||||
}
|
||||
|
||||
void battleUpdateAiSelection(void) {
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[i];
|
||||
if(
|
||||
!battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_AI)
|
||||
) continue;
|
||||
|
||||
battlefighter_t *target =
|
||||
battleAIChooseTarget(&BATTLE.fighters[fighterIndex]);
|
||||
if(target == NULL) continue;
|
||||
|
||||
battleQueueAction(fighterIndex, BATTLE_ACTION_ATTACK, target->id);
|
||||
}
|
||||
|
||||
BATTLE.executionIndex = 0;
|
||||
battleSetState(BATTLE_STATE_MOVES_EXECUTING);
|
||||
}
|
||||
|
||||
void battleUpdateMovesExecuting(void) {
|
||||
if(BATTLE.executionIndex >= BATTLE.executionCount) {
|
||||
battleSetState(BATTLE_STATE_POST_ROUND);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.executionIndex++];
|
||||
battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
|
||||
if(!battleFighterIsAlive(fighter)) return;
|
||||
|
||||
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
||||
if(action->type == BATTLE_ACTION_ATTACK) {
|
||||
battlefighter_t *target = &BATTLE.fighters[action->targetIndex];
|
||||
if(battleFighterIsAlive(target)) battleResolveAttack(fighter, target);
|
||||
}
|
||||
action->type = BATTLE_ACTION_NONE;
|
||||
|
||||
battleCheckResult();
|
||||
}
|
||||
|
||||
void battleUpdatePostRound(void) {
|
||||
BATTLE.round++;
|
||||
battleSetState(BATTLE_STATE_PRE_ROUND);
|
||||
}
|
||||
BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose();
|
||||
memoryZero(&BATTLE, sizeof(battle_t));
|
||||
}
|
||||
+26
-264
@@ -6,89 +6,38 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "battlefighter.h"
|
||||
|
||||
#define BATTLE_FIGHTER_COUNT_MAX 8
|
||||
|
||||
typedef enum {
|
||||
BATTLE_ENCOUNTER_REGULAR,
|
||||
BATTLE_ENCOUNTER_PLAYER_ADVANTAGE,
|
||||
BATTLE_ENCOUNTER_BACK_ATTACK,
|
||||
|
||||
BATTLE_ENCOUNTER_COUNT
|
||||
} battleencountertype_t;
|
||||
#include "rpg/battle/fighter/battlefighter.h"
|
||||
#include "rpg/battle/move/battlemove.h"
|
||||
#include "rpg/battle/move/battlemoveaction.h"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
#include "rpg/battle/state/battlestate.h"
|
||||
|
||||
typedef enum {
|
||||
BATTLE_RESULT_NONE,
|
||||
BATTLE_RESULT_WIN,
|
||||
BATTLE_RESULT_LOSS,
|
||||
BATTLE_RESULT_FLED,
|
||||
|
||||
BATTLE_RESULT_COUNT
|
||||
} battleresult_t;
|
||||
|
||||
// Where BATTLE currently is within a round. A cutscene can pause progression
|
||||
// (CUTSCENE_PAUSE_BATTLE) and use CUTSCENE_BATTLE_WAIT_STATE to synchronize
|
||||
// with any of these, or CUTSCENE_BATTLE_FORCE_ACTION to decide a fighter's
|
||||
// action ahead of PLAYER_SELECTION/AI_SELECTION reaching them.
|
||||
typedef enum {
|
||||
BATTLE_STATE_NONE, // Battle inactive.
|
||||
|
||||
BATTLE_STATE_OPENING, // Entered once by battleStart().
|
||||
BATTLE_STATE_PRE_ROUND, // Rebuilds execution order, clears the action queue.
|
||||
BATTLE_STATE_PLAYER_SELECTION, // Waits on battlePlayerAttack/Flee.
|
||||
BATTLE_STATE_AI_SELECTION, // Auto-queues every undecided AI fighter.
|
||||
BATTLE_STATE_MOVES_EXECUTING, // Resolves one queued action per update.
|
||||
BATTLE_STATE_POST_ROUND, // Round wrap-up; loops back to PRE_ROUND.
|
||||
|
||||
BATTLE_STATE_ENDED, // Terminal for WIN/LOSS/FLED alike -- see BATTLE.result.
|
||||
|
||||
BATTLE_STATE_COUNT
|
||||
} battlestate_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_ACTION_NONE, // No action decided yet for this fighter this round.
|
||||
BATTLE_ACTION_ATTACK,
|
||||
|
||||
BATTLE_ACTION_COUNT
|
||||
} battleactiontype_t;
|
||||
|
||||
typedef struct {
|
||||
battleactiontype_t type;
|
||||
uint8_t targetIndex; // Meaningful for BATTLE_ACTION_ATTACK.
|
||||
} battleaction_t;
|
||||
battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT];
|
||||
|
||||
typedef void (*battlestatechangedcallback_t)(
|
||||
const battlestate_t previous,
|
||||
const battlestate_t next
|
||||
);
|
||||
// Order of who gets to act.
|
||||
uint8_t fighterOrder[BATTLE_FIGHTER_POS_COUNT];
|
||||
uint8_t fighterOrderCount;
|
||||
|
||||
typedef void (*battleactiondecidedcallback_t)(
|
||||
const battlefighter_t *fighter,
|
||||
const battleaction_t *action
|
||||
);
|
||||
// Actions to be executed.
|
||||
battlemoveaction_t actionsPending[BATTLE_FIGHTER_POS_COUNT];
|
||||
uint8_t actionsPendingCount;
|
||||
|
||||
typedef struct {
|
||||
battlestate_t state;
|
||||
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
|
||||
battleaction_t actions[BATTLE_FIGHTER_COUNT_MAX];
|
||||
battlestatedata_t stateData;
|
||||
|
||||
battleencountertype_t encounterType;
|
||||
bool_t fleeAvailable;
|
||||
// Set once a side has been wiped out (see cutsceneBattlePostMoveStart)
|
||||
// or the player has fled (see battleStateSelectionPlayerFlee). Stays
|
||||
// BATTLE_RESULT_NONE for the rest of the battle otherwise.
|
||||
battleresult_t result;
|
||||
|
||||
// Fighter indices (into fighters[]), sorted for the current round.
|
||||
uint8_t executionOrder[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t executionCount;
|
||||
uint8_t executionIndex;
|
||||
uint16_t round;
|
||||
|
||||
// Cursor into executionOrder used by BATTLE_STATE_PLAYER_SELECTION to find
|
||||
// the next player-controlled fighter that still needs a decision.
|
||||
uint8_t selectionIndex;
|
||||
|
||||
battlestatechangedcallback_t onStateChanged;
|
||||
battleactiondecidedcallback_t onActionDecided;
|
||||
} battle_t;
|
||||
|
||||
extern battle_t BATTLE;
|
||||
@@ -99,215 +48,28 @@ extern battle_t BATTLE;
|
||||
void battleInit(void);
|
||||
|
||||
/**
|
||||
* Gets an available (unused) fighter slot index.
|
||||
*
|
||||
* @return The index of an available fighter slot, or 0xFF if none are
|
||||
* available.
|
||||
*/
|
||||
uint8_t battleGetAvailableFighter(void);
|
||||
|
||||
/**
|
||||
* Adds a fighter to the battle in the next available slot.
|
||||
*
|
||||
* @param team The team the fighter belongs to.
|
||||
* @param controller Who makes decisions for the fighter.
|
||||
* @param stats The fighter's base combat stats.
|
||||
* @param healthMax The fighter's maximum health.
|
||||
* @param mpMax The fighter's maximum mp.
|
||||
* @return Pointer to the newly added fighter, or NULL if the battle is
|
||||
* already full.
|
||||
*/
|
||||
battlefighter_t *battleAddFighter(
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Starts the battle: enters BATTLE_STATE_OPENING and marks the battle
|
||||
* active. Call once every fighter has been added via battleAddFighter.
|
||||
*
|
||||
* @param encounterType Determines the opening round's execution order.
|
||||
* @param fleeAvailable Whether the party may attempt to flee this battle.
|
||||
* Starts a battle.
|
||||
*
|
||||
* @param fighters Fighters in the battle.
|
||||
*/
|
||||
void battleStart(
|
||||
const battleencountertype_t encounterType,
|
||||
const bool_t fleeAvailable
|
||||
battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT]
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes of the battle, clearing all fighters and marking it inactive.
|
||||
*/
|
||||
void battleDispose(void);
|
||||
|
||||
/**
|
||||
* Returns the fighter currently awaiting a player decision.
|
||||
*
|
||||
* @return Pointer to the fighter awaiting a decision, or NULL if the battle
|
||||
* isn't in BATTLE_STATE_PLAYER_SELECTION or every player-controlled
|
||||
* fighter has already decided.
|
||||
*/
|
||||
battlefighter_t *battleGetCurrentFighter(void);
|
||||
|
||||
/**
|
||||
* Returns the number of living fighters on a team.
|
||||
*
|
||||
* @param team The team to count.
|
||||
* @return Count of living fighters on that team.
|
||||
*/
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team);
|
||||
|
||||
/**
|
||||
* Resolves a physical attack from attacker onto defender: damage is the
|
||||
* attacker's attack stat minus the defender's defense stat (minimum 1),
|
||||
* subtracted from the defender's health. The defender is marked dead
|
||||
* once health reaches 0.
|
||||
*
|
||||
* @param attacker The attacking fighter.
|
||||
* @param defender The defending fighter.
|
||||
*/
|
||||
void battleResolveAttack(
|
||||
battlefighter_t *attacker,
|
||||
battlefighter_t *defender
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks whether the battle has been won or lost, transitioning to
|
||||
* BATTLE_STATE_ENDED and updating BATTLE.result if so. Does nothing if a
|
||||
* result has already been set (e.g. by a successful flee).
|
||||
*
|
||||
* @return The battle's current result.
|
||||
*/
|
||||
battleresult_t battleCheckResult(void);
|
||||
|
||||
/**
|
||||
* Queues an action for a fighter to perform once BATTLE_STATE_MOVES_EXECUTING
|
||||
* reaches them this round, overwriting any action already queued for that
|
||||
* fighter. Fires BATTLE.onActionDecided.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
|
||||
* @param type The type of action to perform.
|
||||
* @param targetIndex Index into BATTLE.fighters of the target, meaningful
|
||||
* for BATTLE_ACTION_ATTACK.
|
||||
*/
|
||||
void battleQueueAction(
|
||||
const uint8_t fighterIndex,
|
||||
const battleactiontype_t type,
|
||||
const uint8_t targetIndex
|
||||
);
|
||||
|
||||
/**
|
||||
* Submits the currently-selecting fighter's attack against a target, if the
|
||||
* battle is in BATTLE_STATE_PLAYER_SELECTION and awaiting a decision.
|
||||
* Queues the action and advances the selection cursor.
|
||||
*
|
||||
* @param targetIndex Index into BATTLE.fighters of the target.
|
||||
*/
|
||||
void battlePlayerAttack(const uint8_t targetIndex);
|
||||
|
||||
/**
|
||||
* Submits a flee attempt for the currently-selecting fighter, if the battle
|
||||
* is in BATTLE_STATE_PLAYER_SELECTION and fleeing is available for this
|
||||
* battle. Always succeeds, ending the battle with BATTLE_RESULT_FLED.
|
||||
*/
|
||||
void battlePlayerFlee(void);
|
||||
|
||||
/**
|
||||
* Updates the battle simulation for one frame, dispatching on BATTLE.state.
|
||||
* No-op if the battle isn't active, has already ended, or
|
||||
* CUTSCENE_PAUSE_BATTLE is set.
|
||||
*/
|
||||
void battleUpdate(void);
|
||||
|
||||
/**
|
||||
* Rebuilds BATTLE.executionOrder/executionCount from every currently living
|
||||
* fighter, sorted by speed descending.
|
||||
*
|
||||
* @param applyEncounterBias If true, reorders the freshly speed-sorted
|
||||
* queue so BATTLE.encounterType's favoured team goes first (used only
|
||||
* for the opening round).
|
||||
*/
|
||||
void battleBuildExecutionOrder(const bool_t applyEncounterBias);
|
||||
|
||||
/**
|
||||
* Stably partitions BATTLE.executionOrder so every fighter on the given team
|
||||
* comes first, preserving each side's relative (speed-sorted) order.
|
||||
*
|
||||
* @param team The team to move to the front of the execution order.
|
||||
*/
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team);
|
||||
|
||||
/**
|
||||
* Picks an AI target for fighter: the lowest-health living fighter on
|
||||
* the opposing team.
|
||||
*
|
||||
* @param fighter The AI-controlled fighter choosing a target.
|
||||
* @return The chosen target, or NULL if the opposing team has no
|
||||
* living fighters.
|
||||
*/
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
|
||||
|
||||
/**
|
||||
* Sets BATTLE.state and fires BATTLE.onStateChanged with the previous and
|
||||
* new state.
|
||||
* Transitions the battle to a new state: disposes the current state (if
|
||||
* any) and initializes next.
|
||||
*
|
||||
* @param next The state to transition to.
|
||||
*/
|
||||
void battleSetState(const battlestate_t next);
|
||||
|
||||
/**
|
||||
* Sets BATTLE.result and transitions to BATTLE_STATE_ENDED.
|
||||
*
|
||||
* @param result The result to end the battle with.
|
||||
* Updates the battle system.
|
||||
*/
|
||||
void battleSetResult(const battleresult_t result);
|
||||
void battleUpdate(void);
|
||||
|
||||
/**
|
||||
* Checks whether a fighter is a live, undecided candidate for the given
|
||||
* controller -- i.e. whether PLAYER_SELECTION or AI_SELECTION should still
|
||||
* be deciding an action for it this round.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters to check.
|
||||
* @param controller The controller PLAYER_SELECTION/AI_SELECTION is
|
||||
* currently deciding for.
|
||||
* @return True if the fighter is alive, matches controller, and has no
|
||||
* action queued yet.
|
||||
* Disposes of the battle, clearing all fighters and marking it inactive.
|
||||
*/
|
||||
bool_t battleFighterNeedsDecision(
|
||||
const uint8_t fighterIndex,
|
||||
const battlefightercontroller_t controller
|
||||
);
|
||||
|
||||
/**
|
||||
* Advances BATTLE.selectionIndex to the next player-controlled fighter that
|
||||
* still needs a decision, or transitions to BATTLE_STATE_AI_SELECTION once
|
||||
* none remain.
|
||||
*/
|
||||
void battleAdvanceSelection(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_PRE_ROUND: rebuilds the execution order and moves on
|
||||
* to BATTLE_STATE_PLAYER_SELECTION, positioning the selection cursor.
|
||||
*/
|
||||
void battleUpdatePreRound(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_AI_SELECTION: queues an attack for every undecided
|
||||
* AI-controlled fighter, then moves on to BATTLE_STATE_MOVES_EXECUTING.
|
||||
*/
|
||||
void battleUpdateAiSelection(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_MOVES_EXECUTING: resolves one queued action from
|
||||
* BATTLE.executionOrder per call, or transitions to BATTLE_STATE_POST_ROUND
|
||||
* once every fighter this round has been processed.
|
||||
*/
|
||||
void battleUpdateMovesExecuting(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_POST_ROUND: advances BATTLE.round and transitions
|
||||
* back to BATTLE_STATE_PRE_ROUND.
|
||||
*/
|
||||
void battleUpdatePostRound(void);
|
||||
void battleDispose(void);
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlefighter.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team");
|
||||
assertTrue(
|
||||
controller < BATTLE_FIGHTER_CONTROLLER_COUNT,
|
||||
"Invalid fighter controller"
|
||||
);
|
||||
|
||||
const uint8_t id = fighter->id;
|
||||
memoryZero(fighter, sizeof(battlefighter_t));
|
||||
fighter->id = id;
|
||||
fighter->status = BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
fighter->team = team;
|
||||
fighter->controller = controller;
|
||||
fighter->stats = stats;
|
||||
fighter->healthMax = healthMax;
|
||||
fighter->health = healthMax;
|
||||
fighter->mpMax = mpMax;
|
||||
fighter->mp = mpMax;
|
||||
}
|
||||
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
// An empty status means the slot in BATTLE.fighters is unused.
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_STATUS_NULL,
|
||||
|
||||
BATTLE_FIGHTER_STATUS_NORMAL,
|
||||
BATTLE_FIGHTER_STATUS_DEAD,
|
||||
|
||||
BATTLE_FIGHTER_STATUS_COUNT
|
||||
} battlefighterstatus_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_TEAM_ALLY,
|
||||
BATTLE_FIGHTER_TEAM_ENEMY,
|
||||
|
||||
BATTLE_FIGHTER_TEAM_COUNT
|
||||
} battlefighterteam_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
|
||||
BATTLE_FIGHTER_CONTROLLER_COUNT
|
||||
} battlefightercontroller_t;
|
||||
|
||||
// Base combat stats, kept separate from the resource pools (health/mp) on
|
||||
// battlefighter_t so that equipment/buffs can later modify them without
|
||||
// touching current health/mp state.
|
||||
typedef struct {
|
||||
uint16_t attack;
|
||||
uint16_t defense;
|
||||
uint16_t magic;
|
||||
uint16_t speed;
|
||||
uint16_t luck;
|
||||
} battlefighterstats_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t id;
|
||||
battlefighterstatus_t status;
|
||||
battlefighterteam_t team;
|
||||
battlefightercontroller_t controller;
|
||||
|
||||
uint16_t health;
|
||||
uint16_t healthMax;
|
||||
uint16_t mp;
|
||||
uint16_t mpMax;
|
||||
|
||||
battlefighterstats_t stats;
|
||||
} battlefighter_t;
|
||||
|
||||
/**
|
||||
* Initializes a battle fighter in place, filling health/mp to their maximum
|
||||
* values and setting its status to normal.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to initialize.
|
||||
* @param team The team the fighter belongs to.
|
||||
* @param controller Who makes decisions for the fighter.
|
||||
* @param stats The fighter's base combat stats.
|
||||
* @param healthMax The fighter's maximum health.
|
||||
* @param mpMax The fighter's maximum mp.
|
||||
*/
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the fighter is in a state where it can still act (i.e.
|
||||
* is not dead).
|
||||
*
|
||||
* @param fighter Pointer to the fighter to check.
|
||||
* @returns True if the fighter can act.
|
||||
*/
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter);
|
||||
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
battlefighter.c
|
||||
)
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlefighter.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
errorret_t battleFighterParseStatField(
|
||||
yyjson_val *obj,
|
||||
const char_t *key,
|
||||
uint16_t *out
|
||||
) {
|
||||
yyjson_val *val = yyjson_obj_get(obj, key);
|
||||
if(!yyjson_is_int(val)) {
|
||||
errorThrow("Fighter: missing/invalid '%s'", key);
|
||||
}
|
||||
|
||||
const int64_t num = yyjson_get_sint(val);
|
||||
if(num < 0 || num > UINT16_MAX) {
|
||||
errorThrow("Fighter: '%s' must be between 0 and %u", key, UINT16_MAX);
|
||||
}
|
||||
|
||||
*out = (uint16_t)num;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t battleFighterParseOptionalIntField(
|
||||
yyjson_val *obj,
|
||||
const char_t *key,
|
||||
const int32_t defaultValue,
|
||||
int32_t *out
|
||||
) {
|
||||
yyjson_val *val = yyjson_obj_get(obj, key);
|
||||
if(val == NULL) {
|
||||
*out = defaultValue;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
if(!yyjson_is_int(val)) errorThrow("Fighter: invalid '%s'", key);
|
||||
*out = (int32_t)yyjson_get_sint(val);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t battleFighterParse(battlefighter_t *fighter, yyjson_val *obj) {
|
||||
assertNotNull(fighter, "Fighter cannot be NULL");
|
||||
assertNotNull(obj, "JSON object cannot be NULL");
|
||||
|
||||
int32_t teamNum;
|
||||
errorChain(battleFighterParseOptionalIntField(
|
||||
obj, "team", BATTLE_FIGHTER_TEAM_ALLY, &teamNum
|
||||
));
|
||||
if(teamNum < 0 || teamNum >= BATTLE_FIGHTER_TEAM_COUNT) {
|
||||
errorThrow("Fighter: invalid 'team'");
|
||||
}
|
||||
|
||||
int32_t controllerNum;
|
||||
errorChain(battleFighterParseOptionalIntField(
|
||||
obj, "controller", BATTLE_FIGHTER_CONTROLLER_PLAYER, &controllerNum
|
||||
));
|
||||
if(controllerNum < 0 || controllerNum >= BATTLE_FIGHTER_CONTROLLER_COUNT) {
|
||||
errorThrow("Fighter: invalid 'controller'");
|
||||
}
|
||||
|
||||
battlefighterstats_t stats;
|
||||
errorChain(battleFighterParseStatField(obj, "attack", &stats.attack));
|
||||
errorChain(battleFighterParseStatField(obj, "defense", &stats.defense));
|
||||
errorChain(battleFighterParseStatField(obj, "magic", &stats.magic));
|
||||
errorChain(battleFighterParseStatField(obj, "speed", &stats.speed));
|
||||
errorChain(battleFighterParseStatField(obj, "luck", &stats.luck));
|
||||
errorChain(battleFighterParseStatField(obj, "healthMax", &stats.healthMax));
|
||||
errorChain(battleFighterParseStatField(obj, "mpMax", &stats.mpMax));
|
||||
|
||||
battleFighterInit(
|
||||
fighter, (battlefighterteam_t)teamNum,
|
||||
(battlefightercontroller_t)controllerNum, stats
|
||||
);
|
||||
|
||||
int32_t statusNum;
|
||||
errorChain(battleFighterParseOptionalIntField(
|
||||
obj, "status", BATTLE_FIGHTER_STATUS_NORMAL, &statusNum
|
||||
));
|
||||
if(statusNum < 0 || statusNum >= BATTLE_FIGHTER_STATUS_COUNT) {
|
||||
errorThrow("Fighter: invalid 'status'");
|
||||
}
|
||||
fighter->status = (battlefighterstatus_t)statusNum;
|
||||
|
||||
int32_t health;
|
||||
errorChain(
|
||||
battleFighterParseOptionalIntField(obj, "health", stats.healthMax, &health)
|
||||
);
|
||||
if(health < 0 || health > stats.healthMax) {
|
||||
errorThrow("Fighter: 'health' out of range");
|
||||
}
|
||||
fighter->stats.health = (uint16_t)health;
|
||||
|
||||
int32_t mp;
|
||||
errorChain(battleFighterParseOptionalIntField(obj, "mp", stats.mpMax, &mp));
|
||||
if(mp < 0 || mp > stats.mpMax) {
|
||||
errorThrow("Fighter: 'mp' out of range");
|
||||
}
|
||||
fighter->stats.mp = (uint16_t)mp;
|
||||
|
||||
yyjson_val *abilities = yyjson_obj_get(obj, "abilities");
|
||||
if(abilities != NULL) {
|
||||
if(
|
||||
!yyjson_is_arr(abilities) ||
|
||||
yyjson_arr_size(abilities) > BATTLE_FIGHTER_ABILITY_COUNT_MAX
|
||||
) {
|
||||
errorThrow(
|
||||
"Fighter: 'abilities' must have 0-%u entries",
|
||||
BATTLE_FIGHTER_ABILITY_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
yyjson_arr_iter iter = yyjson_arr_iter_with(abilities);
|
||||
yyjson_val *abilityVal;
|
||||
while((abilityVal = yyjson_arr_iter_next(&iter)) != NULL) {
|
||||
const char_t *name = yyjson_get_str(abilityVal);
|
||||
battlemoveid_t ability =
|
||||
name == NULL ? BATTLE_MOVE_ID_NULL : battleMoveGetIdByName(name);
|
||||
if(ability == BATTLE_MOVE_ID_NULL) {
|
||||
errorThrow("Fighter: invalid entry in 'abilities'");
|
||||
}
|
||||
battleFighterAbilityAdd(fighter, ability);
|
||||
}
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
yyjson_mut_val *battleFighterSerialize(
|
||||
const battlefighter_t *fighter,
|
||||
yyjson_mut_doc *doc
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter cannot be NULL");
|
||||
assertNotNull(doc, "Doc cannot be NULL");
|
||||
|
||||
yyjson_mut_val *obj = yyjson_mut_obj(doc);
|
||||
|
||||
yyjson_mut_obj_add_uint(doc, obj, "team", fighter->team);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "controller", fighter->controller);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "status", fighter->status);
|
||||
|
||||
yyjson_mut_obj_add_uint(doc, obj, "attack", fighter->stats.attack);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "defense", fighter->stats.defense);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "magic", fighter->stats.magic);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "speed", fighter->stats.speed);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "luck", fighter->stats.luck);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "healthMax", fighter->stats.healthMax);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "mpMax", fighter->stats.mpMax);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "health", fighter->stats.health);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "mp", fighter->stats.mp);
|
||||
|
||||
yyjson_mut_val *abilities = yyjson_mut_arr(doc);
|
||||
for(uint8_t i = 0; i < fighter->abilityCount; i++) {
|
||||
yyjson_mut_arr_add_str(doc, abilities, BATTLE_MOVES[fighter->abilities[i]].name);
|
||||
}
|
||||
yyjson_mut_obj_add_val(doc, obj, "abilities", abilities);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team");
|
||||
assertTrue(
|
||||
controller < BATTLE_FIGHTER_CONTROLLER_COUNT,
|
||||
"Invalid fighter controller"
|
||||
);
|
||||
|
||||
const uint8_t id = fighter->id;
|
||||
memoryZero(fighter, sizeof(battlefighter_t));
|
||||
fighter->id = id;
|
||||
fighter->status = BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
fighter->team = team;
|
||||
fighter->controller = controller;
|
||||
fighter->stats = stats;
|
||||
fighter->stats.health = stats.healthMax;
|
||||
fighter->stats.mp = stats.mpMax;
|
||||
}
|
||||
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
}
|
||||
|
||||
void battleFighterAbilityAdd(
|
||||
battlefighter_t *fighter,
|
||||
const battlemoveid_t ability
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
assertTrue(
|
||||
fighter->abilityCount < BATTLE_FIGHTER_ABILITY_COUNT_MAX,
|
||||
"Fighter already has the maximum number of abilities"
|
||||
);
|
||||
|
||||
fighter->abilities[fighter->abilityCount++] = ability;
|
||||
}
|
||||
|
||||
void battleFighterDamage(
|
||||
battlefighter_t *fighter,
|
||||
const uint16_t amount
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
|
||||
fighter->stats.health =
|
||||
amount >= fighter->stats.health ? 0 : fighter->stats.health - amount;
|
||||
if(fighter->stats.health == 0) fighter->status = BATTLE_FIGHTER_STATUS_DEAD;
|
||||
}
|
||||
|
||||
void battleFighterHeal(
|
||||
battlefighter_t *fighter,
|
||||
const uint16_t amount
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
if(!battleFighterIsAlive(fighter)) return;
|
||||
|
||||
const uint32_t healed = (uint32_t)fighter->stats.health + amount;
|
||||
fighter->stats.health = healed > fighter->stats.healthMax
|
||||
? fighter->stats.healthMax : (uint16_t)healed;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "yyjson.h"
|
||||
#include "rpg/battle/move/battlemove.h"
|
||||
#include "rpg/battle/fighter/battlefighterstatus.h"
|
||||
#include "rpg/battle/fighter/battlefighterteam.h"
|
||||
#include "rpg/battle/fighter/battlefightercontroller.h"
|
||||
#include "rpg/battle/fighter/battlefighterstats.h"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
|
||||
#define BATTLE_FIGHTER_ABILITY_COUNT_MAX 8
|
||||
|
||||
typedef struct battlefighter_s {
|
||||
uint8_t id;
|
||||
battlefighterstatus_t status;
|
||||
battlefighterteam_t team;
|
||||
battlefightercontroller_t controller;
|
||||
|
||||
|
||||
battlefighterstats_t stats;
|
||||
|
||||
battlemoveid_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX];
|
||||
battlemoveid_t abilityCount;
|
||||
} battlefighter_t;
|
||||
|
||||
/**
|
||||
* Initializes a battle fighter in place, filling health/mp to the maximum
|
||||
* values carried on stats and setting its status to normal.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to initialize.
|
||||
* @param team The team the fighter belongs to.
|
||||
* @param controller Who makes decisions for the fighter.
|
||||
* @param stats The fighter's base combat stats, including healthMax/mpMax.
|
||||
*/
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats
|
||||
);
|
||||
|
||||
/**
|
||||
* Generic battlefighter_t parser, shared by every place that builds a
|
||||
* fighter from JSON (party.jsonc roster members, BATTLE_START enemy
|
||||
* definitions, save-file party members) - see battleFighterSerialize for
|
||||
* the inverse. Reads 'team'/'controller'/'status' as ints (each optional,
|
||||
* defaulting to ALLY/PLAYER/NORMAL respectively - a caller that needs a
|
||||
* different team/controller, e.g. BATTLE_START's enemies, sets those
|
||||
* fields on fighter after this returns), 'attack'/'defense'/'magic'/
|
||||
* 'speed'/'luck'/'healthMax'/'mpMax' as required ints in [0, UINT16_MAX],
|
||||
* 'health'/'mp' as optional ints defaulting to healthMax/mpMax (a fresh
|
||||
* fighter), and an optional 'abilities' array of move name strings. Does
|
||||
* not touch fighter->id - set it (or leave it) before calling, same as
|
||||
* battleFighterInit.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to fill in.
|
||||
* @param obj The JSON object to read fields from.
|
||||
* @return Error if a required field is missing/invalid, or an abilities
|
||||
* entry doesn't name a real move.
|
||||
*/
|
||||
errorret_t battleFighterParse(battlefighter_t *fighter, yyjson_val *obj);
|
||||
|
||||
/**
|
||||
* Inverse of battleFighterParse: builds a new JSON object (not yet
|
||||
* attached anywhere - the caller attaches it, e.g. via
|
||||
* yyjson_mut_arr_add_val/yyjson_mut_obj_add_val) with every field
|
||||
* battleFighterParse reads except id (see its doc comment for why).
|
||||
*
|
||||
* @param fighter The fighter to serialize.
|
||||
* @param doc The mutable document to allocate the object in.
|
||||
* @return The newly built object.
|
||||
*/
|
||||
yyjson_mut_val *battleFighterSerialize(
|
||||
const battlefighter_t *fighter,
|
||||
yyjson_mut_doc *doc
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the fighter is in a state where it can still act (i.e.
|
||||
* is not dead).
|
||||
*
|
||||
* @param fighter Pointer to the fighter to check.
|
||||
* @returns True if the fighter can act.
|
||||
*/
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter);
|
||||
|
||||
/**
|
||||
* Appends an ability to the fighter's ability list.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to add the ability to.
|
||||
* @param ability The ability to add.
|
||||
*/
|
||||
void battleFighterAbilityAdd(
|
||||
battlefighter_t *fighter,
|
||||
const battlemoveid_t ability
|
||||
);
|
||||
|
||||
/**
|
||||
* Subtracts amount from the fighter's health (floored at 0), marking it
|
||||
* dead once health reaches 0.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to damage.
|
||||
* @param amount The amount of damage to apply.
|
||||
*/
|
||||
void battleFighterDamage(
|
||||
battlefighter_t *fighter,
|
||||
const uint16_t amount
|
||||
);
|
||||
|
||||
/**
|
||||
* Adds amount to the fighter's health (capped at healthMax). No-op if the
|
||||
* fighter isn't alive -- healing can't revive a dead fighter.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to heal.
|
||||
* @param amount The amount to heal.
|
||||
*/
|
||||
void battleFighterHeal(
|
||||
battlefighter_t *fighter,
|
||||
const uint16_t amount
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_CONTROLLER_NONE,
|
||||
|
||||
BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
|
||||
BATTLE_FIGHTER_CONTROLLER_COUNT
|
||||
} battlefightercontroller_t;
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef uint8_t battlefighterpos_t;
|
||||
|
||||
#define BATTLE_FIGHTER_POS(x) ((battlefighterpos_t)(x))
|
||||
|
||||
// Left (Enemy side)
|
||||
#define BATTLE_FIGHTER_POS_0 BATTLE_FIGHTER_POS(0)
|
||||
#define BATTLE_FIGHTER_POS_1 BATTLE_FIGHTER_POS(1)
|
||||
#define BATTLE_FIGHTER_POS_2 BATTLE_FIGHTER_POS(2)
|
||||
#define BATTLE_FIGHTER_POS_3 BATTLE_FIGHTER_POS(3)
|
||||
|
||||
// Right (Ally side)
|
||||
#define BATTLE_FIGHTER_POS_4 BATTLE_FIGHTER_POS(4)
|
||||
#define BATTLE_FIGHTER_POS_5 BATTLE_FIGHTER_POS(5)
|
||||
#define BATTLE_FIGHTER_POS_6 BATTLE_FIGHTER_POS(6)
|
||||
#define BATTLE_FIGHTER_POS_7 BATTLE_FIGHTER_POS(7)
|
||||
|
||||
#define BATTLE_FIGHTER_POS_COUNT (BATTLE_FIGHTER_POS_7 + 1)
|
||||
|
||||
// Sentinels resolved by battleStateExecutingInit against the currently
|
||||
// resolving action, for cutscene items authored inside a move's own
|
||||
// cutscene (e.g. cutscenes/battle/moves/slash.jsonc) that need to refer
|
||||
// to "whoever is being hit"/"whoever is attacking" without knowing their
|
||||
// concrete BATTLE.fighters index ahead of time.
|
||||
#define BATTLE_FIGHTER_POS_TARGET BATTLE_FIGHTER_POS(0xFE)
|
||||
#define BATTLE_FIGHTER_POS_USER BATTLE_FIGHTER_POS(0xFD)
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "rpg/battle/fighter/battlefighterstats.h"
|
||||
|
||||
#define BATTLE_FIGHTER_SPECIES_NAME_MAX 16
|
||||
#define BATTLE_FIGHTER_SPECIES_COUNT_MAX 250
|
||||
|
||||
typedef uint8_t battlespeciesid_t;
|
||||
|
||||
#define BATTLE_SPECIES_ID_NULL ((battlespeciesid_t)0)
|
||||
|
||||
typedef struct {
|
||||
char_t name[BATTLE_FIGHTER_SPECIES_NAME_MAX];
|
||||
battlefighterstats_t baseStats;
|
||||
} battlefighterspecies_t;
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct {
|
||||
uint16_t health;
|
||||
uint16_t mp;
|
||||
|
||||
uint16_t healthMax;
|
||||
uint16_t mpMax;
|
||||
uint16_t attack;
|
||||
uint16_t defense;
|
||||
uint16_t magic;
|
||||
uint16_t speed;
|
||||
uint16_t luck;
|
||||
} battlefighterstats_t;
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_STATUS_NULL,
|
||||
|
||||
BATTLE_FIGHTER_STATUS_NORMAL,
|
||||
BATTLE_FIGHTER_STATUS_DEAD,
|
||||
|
||||
// Poisoned, burned, confused, etc.
|
||||
|
||||
BATTLE_FIGHTER_STATUS_COUNT
|
||||
} battlefighterstatus_t;
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_TEAM_ALLY,
|
||||
BATTLE_FIGHTER_TEAM_ENEMY,
|
||||
|
||||
BATTLE_FIGHTER_TEAM_COUNT
|
||||
} battlefighterteam_t;
|
||||
+2
-1
@@ -3,7 +3,8 @@
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
assetcutsceneloader.c
|
||||
battlemove.c
|
||||
)
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlemove.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include <string.h>
|
||||
|
||||
battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX];
|
||||
uint8_t BATTLE_MOVE_COUNT;
|
||||
|
||||
errorret_t battleMoveInit(void) {
|
||||
memoryZero(BATTLE_MOVES, sizeof(BATTLE_MOVES));
|
||||
BATTLE_MOVE_COUNT = BATTLE_MOVE_ID_NULL + 1;// Reserve NULL.
|
||||
|
||||
// Get moves JSON.
|
||||
assetentry_t *entry = assetLock(
|
||||
"battle/moves.jsonc", ASSET_LOADER_TYPE_JSON, NULL
|
||||
);
|
||||
errorret_t ret = assetRequireLoaded(entry);
|
||||
if(errorIsNotOk(ret)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(entry->data.json);
|
||||
yyjson_val *moves = yyjson_obj_get(root, "moves");
|
||||
yyjson_obj_iter iter = yyjson_obj_iter_with(moves);
|
||||
yyjson_val *key;
|
||||
while((key = yyjson_obj_iter_next(&iter))) {
|
||||
if(BATTLE_MOVE_COUNT >= BATTLE_MOVE_COUNT_MAX) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow(
|
||||
"Battle move: too many moves defined (max %u)",
|
||||
(uint32_t)BATTLE_MOVE_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
battlemoveid_t id = BATTLE_MOVE_COUNT++;
|
||||
battleability_t *move = &BATTLE_MOVES[id];
|
||||
|
||||
// Name
|
||||
if(!yyjson_is_str(key)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move: keys in 'moves' must be strings");
|
||||
}
|
||||
const char_t *name = yyjson_get_str(key);
|
||||
stringCopy(move->name, name, BATTLE_MOVE_NAME_MAX);
|
||||
|
||||
if(strlen(name) >= BATTLE_MOVE_NAME_MAX) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow(
|
||||
"Battle move '%s': name must be less than %u characters",
|
||||
name, (uint32_t)BATTLE_MOVE_NAME_MAX
|
||||
);
|
||||
}
|
||||
|
||||
// Must me object
|
||||
yyjson_val *val = yyjson_obj_iter_get_val(key);
|
||||
if(!yyjson_is_obj(val)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': value must be an object", name);
|
||||
}
|
||||
|
||||
// Valid targets
|
||||
yyjson_val *enemies = yyjson_obj_get(val, "enemies");
|
||||
if(enemies != NULL && !yyjson_is_bool(enemies)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'enemies' must be a boolean", name);
|
||||
}
|
||||
yyjson_val *allies = yyjson_obj_get(val, "allies");
|
||||
if(allies != NULL && !yyjson_is_bool(allies)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'allies' must be a boolean", name);
|
||||
}
|
||||
yyjson_val *multiple = yyjson_obj_get(val, "multiple");
|
||||
if(multiple != NULL && !yyjson_is_bool(multiple)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'multiple' must be a boolean", name);
|
||||
}
|
||||
yyjson_val *self = yyjson_obj_get(val, "self");
|
||||
if(self != NULL && !yyjson_is_bool(self)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'self' must be a boolean", name);
|
||||
}
|
||||
|
||||
battletarget_t target = 0;
|
||||
if(yyjson_get_bool(enemies)) target |= BATTLE_TARGET_ENEMIES;
|
||||
if(yyjson_get_bool(allies)) target |= BATTLE_TARGET_ALLIES;
|
||||
if(yyjson_get_bool(multiple)) target |= BATTLE_TARGET_MULTIPLE;
|
||||
if(yyjson_get_bool(self)) target |= BATTLE_TARGET_SELF;
|
||||
move->target = target;
|
||||
if(move->target == 0) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': missing target type", name);
|
||||
}
|
||||
|
||||
// Default target (optional)
|
||||
yyjson_val *defaultVal = yyjson_obj_get(val, "default");
|
||||
if(defaultVal != NULL && !yyjson_is_str(defaultVal)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move '%s': 'default' must be a string", name);
|
||||
}
|
||||
|
||||
const char_t *defaultTarget = yyjson_get_str(defaultVal);
|
||||
if(defaultTarget == NULL) {
|
||||
move->targetDefault = 0;
|
||||
} else if(stringCompareInsensitive(defaultTarget, "enemy") == 0) {
|
||||
move->targetDefault = BATTLE_TARGET_ENEMIES;
|
||||
} else if(stringCompareInsensitive(defaultTarget, "ally") == 0) {
|
||||
move->targetDefault = BATTLE_TARGET_ALLIES;
|
||||
} else if(stringCompareInsensitive(defaultTarget, "self") == 0) {
|
||||
move->targetDefault = BATTLE_TARGET_SELF;
|
||||
} else {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("Battle move %s has invalid default target.", name);
|
||||
}
|
||||
}
|
||||
|
||||
assetUnlockEntry(entry);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
battlemoveid_t battleMoveGetIdByName(const char_t *name) {
|
||||
assertNotNull(name, "name must not be NULL");
|
||||
|
||||
for(battlemoveid_t id = BATTLE_MOVE_ID_NULL + 1; id < BATTLE_MOVE_COUNT; id++) {
|
||||
if(stringEquals(BATTLE_MOVES[id].name, name)) return id;
|
||||
}
|
||||
|
||||
return BATTLE_MOVE_ID_NULL;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "asset/asset.h"
|
||||
#include "battletarget.h"
|
||||
|
||||
typedef uint8_t battlemoveid_t;
|
||||
|
||||
#define BATTLE_MOVE_NAME_MAX 16
|
||||
#define BATTLE_MOVE_COUNT_MAX 250
|
||||
|
||||
#define BATTLE_MOVE_ID_NULL ((battlemoveid_t)0)
|
||||
|
||||
extern uint8_t BATTLE_MOVE_COUNT;
|
||||
|
||||
typedef struct {
|
||||
char_t name[BATTLE_MOVE_NAME_MAX];
|
||||
battletarget_t target;
|
||||
battletarget_t targetDefault;
|
||||
} battleability_t;
|
||||
|
||||
extern battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX];
|
||||
|
||||
/**
|
||||
* Initializes the battle move subsystem, loading assets/battle/moves.jsonc.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t battleMoveInit(void);
|
||||
|
||||
/**
|
||||
* Finds a move's id by name (exact match).
|
||||
*
|
||||
* @param name The move name to search for.
|
||||
* @return The matching move's id, or BATTLE_MOVE_ID_NULL if not found.
|
||||
*/
|
||||
battlemoveid_t battleMoveGetIdByName(const char_t *name);
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/move/battlemove.h"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
|
||||
typedef struct battlefighter_s battlefighter_t;
|
||||
|
||||
typedef struct {
|
||||
battlemoveid_t move;
|
||||
battlefighterpos_t targets[BATTLE_FIGHTER_POS_COUNT];
|
||||
battlefighter_t *user;
|
||||
} battlemoveaction_t;
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef uint8_t battletarget_t;
|
||||
|
||||
#define BATTLE_TARGET_ENEMIES (1 << 0)
|
||||
#define BATTLE_TARGET_ALLIES (1 << 1)
|
||||
#define BATTLE_TARGET_MULTIPLE (1 << 2)
|
||||
#define BATTLE_TARGET_SELF (1 << 3)
|
||||
+147
-30
@@ -8,56 +8,134 @@
|
||||
#include "party.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "save/save.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
|
||||
party_t PARTY;
|
||||
errorret_t partyInit(party_t *party) {
|
||||
assertNotNull(party, "Party cannot be NULL");
|
||||
|
||||
void partyInit(void) {
|
||||
memoryZero(&PARTY, sizeof(party_t));
|
||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||
PARTY.members[i].id = i;
|
||||
}
|
||||
memoryZero(party, sizeof(party_t));
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
PARTY.order[i] = PARTY_ORDER_EMPTY;
|
||||
party->order[i] = PARTY_ORDER_EMPTY;
|
||||
}
|
||||
|
||||
assetentry_t *entry = assetLock("party.jsonc", ASSET_LOADER_TYPE_JSON, NULL);
|
||||
errorret_t result = assetRequireLoaded(entry);
|
||||
if(errorIsNotOk(result)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(result);
|
||||
}
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(entry->data.json);
|
||||
yyjson_val *members = yyjson_obj_get(root, "members");
|
||||
if(!yyjson_is_obj(members)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow("party.jsonc: missing/invalid 'members' object");
|
||||
}
|
||||
|
||||
const size_t memberCount = yyjson_obj_size(members);
|
||||
if(memberCount > PARTY_MEMBER_COUNT_MAX) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow(
|
||||
"party.jsonc: defines more than PARTY_MEMBER_COUNT_MAX (%u) members",
|
||||
PARTY_MEMBER_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
uint8_t idx = 0;
|
||||
yyjson_val *key;
|
||||
yyjson_val *memberObj;
|
||||
yyjson_obj_iter iter = yyjson_obj_iter_with(members);
|
||||
while((key = yyjson_obj_iter_next(&iter))) {
|
||||
memberObj = yyjson_obj_iter_get_val(key);
|
||||
|
||||
if(yyjson_get_len(key) >= PARTY_KEY_LENGTH_MAX) {
|
||||
assetUnlockEntry(entry);
|
||||
errorThrow(
|
||||
"party.jsonc: member key '%s' exceeds PARTY_KEY_LENGTH_MAX (%u)",
|
||||
yyjson_get_str(key), PARTY_KEY_LENGTH_MAX
|
||||
);
|
||||
}
|
||||
|
||||
stringCopy(party->keys[idx], yyjson_get_str(key), PARTY_KEY_LENGTH_MAX - 1);
|
||||
|
||||
battlefighter_t *member = &party->members[idx];
|
||||
member->id = idx;
|
||||
errorret_t parseResult = battleFighterParse(member, memberObj);
|
||||
if(errorIsNotOk(parseResult)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(parseResult);
|
||||
}
|
||||
|
||||
idx++;
|
||||
}
|
||||
|
||||
assetUnlockEntry(entry);
|
||||
|
||||
// TEST: only the first defined member starts active, in the first party
|
||||
// position - replace once there's a real starting-party/character-
|
||||
// creation flow. Deliberately separate from the roster seeding above:
|
||||
// order (not a member's presence in the roster) is what determines who's
|
||||
// currently in the party.
|
||||
if(memberCount > 0) party->order[0] = 0;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t partyGetAvailableMember(void) {
|
||||
uint8_t partyGetMemberIndexByKey(const char_t *key) {
|
||||
assertNotNull(key, "Key cannot be NULL");
|
||||
|
||||
const party_t *party = &SAVE.slot.party;
|
||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||
if(PARTY.members[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
||||
if(stringEquals(party->keys[i], key)) return i;
|
||||
}
|
||||
|
||||
return 0xFF;
|
||||
return PARTY_MEMBER_NOT_FOUND;
|
||||
}
|
||||
|
||||
battlefighter_t *partyAddMember(
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
const uint8_t index = partyGetAvailableMember();
|
||||
if(index == 0xFF) return NULL;
|
||||
const char_t *partyGetMemberKey(const uint8_t memberIndex) {
|
||||
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||
return SAVE.slot.party.keys[memberIndex];
|
||||
}
|
||||
|
||||
battlefighter_t *member = &PARTY.members[index];
|
||||
battleFighterInit(
|
||||
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
stats, healthMax, mpMax
|
||||
bool_t partyIsMemberActive(const uint8_t memberIndex) {
|
||||
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||
|
||||
const party_t *party = &SAVE.slot.party;
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
if(party->order[i] == memberIndex) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void partyAddMember(const uint8_t memberIndex) {
|
||||
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||
|
||||
party_t *party = &SAVE.slot.party;
|
||||
assertTrue(
|
||||
party->members[memberIndex].status != BATTLE_FIGHTER_STATUS_NULL,
|
||||
"Member does not exist in the roster"
|
||||
);
|
||||
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
if(PARTY.order[i] != PARTY_ORDER_EMPTY) continue;
|
||||
PARTY.order[i] = index;
|
||||
break;
|
||||
}
|
||||
if(partyIsMemberActive(memberIndex)) return;
|
||||
|
||||
return member;
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
if(party->order[i] != PARTY_ORDER_EMPTY) continue;
|
||||
party->order[i] = memberIndex;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
battlefighter_t *partyGetOrderMember(const uint8_t slot) {
|
||||
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
|
||||
|
||||
const uint8_t index = PARTY.order[slot];
|
||||
party_t *party = &SAVE.slot.party;
|
||||
const uint8_t index = party->order[slot];
|
||||
if(index == PARTY_ORDER_EMPTY) return NULL;
|
||||
return &PARTY.members[index];
|
||||
return &party->members[index];
|
||||
}
|
||||
|
||||
void partySetOrder(const uint8_t slot, const uint8_t memberIndex) {
|
||||
@@ -67,5 +145,44 @@ void partySetOrder(const uint8_t slot, const uint8_t memberIndex) {
|
||||
"Invalid party member index"
|
||||
);
|
||||
|
||||
PARTY.order[slot] = memberIndex;
|
||||
SAVE.slot.party.order[slot] = memberIndex;
|
||||
}
|
||||
|
||||
void partyRemoveMember(const uint8_t memberIndex) {
|
||||
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||
|
||||
party_t *party = &SAVE.slot.party;
|
||||
|
||||
uint8_t removedSlot = PARTY_ORDER_EMPTY;
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
if(party->order[i] != memberIndex) continue;
|
||||
removedSlot = i;
|
||||
break;
|
||||
}
|
||||
if(removedSlot == PARTY_ORDER_EMPTY) return;
|
||||
|
||||
for(uint8_t i = removedSlot; i < PARTY_ACTIVE_SIZE_MAX - 1; i++) {
|
||||
party->order[i] = party->order[i + 1];
|
||||
}
|
||||
party->order[PARTY_ACTIVE_SIZE_MAX - 1] = PARTY_ORDER_EMPTY;
|
||||
}
|
||||
|
||||
void partyHealMember(const uint8_t memberIndex) {
|
||||
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||
|
||||
battlefighter_t *member = &SAVE.slot.party.members[memberIndex];
|
||||
if(member->status == BATTLE_FIGHTER_STATUS_NULL) return;
|
||||
|
||||
member->status = BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
member->stats.health = member->stats.healthMax;
|
||||
member->stats.mp = member->stats.mpMax;
|
||||
}
|
||||
|
||||
void partyHealAll(void) {
|
||||
const party_t *party = &SAVE.slot.party;
|
||||
|
||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||
if(party->members[i].status == BATTLE_FIGHTER_STATUS_NULL) continue;
|
||||
partyHealMember(i);
|
||||
}
|
||||
}
|
||||
|
||||
+87
-34
@@ -6,55 +6,80 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "battlefighter.h"
|
||||
#include "rpg/battle/fighter/battlefighter.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#define PARTY_MEMBER_COUNT_MAX 4
|
||||
#define PARTY_ACTIVE_SIZE_MAX 3
|
||||
#define PARTY_MEMBER_COUNT_MAX 10
|
||||
#define PARTY_ACTIVE_SIZE_MAX 4
|
||||
#define PARTY_KEY_LENGTH_MAX 32
|
||||
#define PARTY_ORDER_EMPTY 0xFF
|
||||
#define PARTY_MEMBER_NOT_FOUND 0xFF
|
||||
|
||||
typedef struct {
|
||||
battlefighter_t members[PARTY_MEMBER_COUNT_MAX];
|
||||
|
||||
// Maps an active battle slot to the roster member filling it, or
|
||||
// PARTY_ORDER_EMPTY if the slot is unfilled. Only the first
|
||||
// PARTY_ACTIVE_SIZE_MAX of the PARTY_MEMBER_COUNT_MAX roster members
|
||||
// can be in the active lineup at once.
|
||||
char_t keys[PARTY_MEMBER_COUNT_MAX][PARTY_KEY_LENGTH_MAX];
|
||||
uint8_t order[PARTY_ACTIVE_SIZE_MAX];
|
||||
} party_t;
|
||||
|
||||
extern party_t PARTY;
|
||||
|
||||
/**
|
||||
* Initializes the party system with an empty roster and order.
|
||||
*/
|
||||
void partyInit(void);
|
||||
|
||||
/**
|
||||
* Gets an available (unused) party member slot index.
|
||||
* Initializes party with an empty order, then seeds every roster slot
|
||||
* directly from party.jsonc's "members" object (each key/value pair fills
|
||||
* the next members[]/keys[] slot in the object's iteration order, key
|
||||
* copied into keys[] and value parsed via battleFighterParse) - unlike
|
||||
* partyAddMember, this never touches order itself except for one
|
||||
* placeholder default: if party.jsonc defines at least one member,
|
||||
* order[0] is set to 0 (the first member starts active in the first party
|
||||
* position). Replace this default once there's a real starting-party/
|
||||
* character-creation flow.
|
||||
*
|
||||
* @return The index of an available slot, or 0xFF if the party is full.
|
||||
* @param party The party to initialize.
|
||||
* @return Error if party.jsonc fails to load/parse, defines more members
|
||||
* than PARTY_MEMBER_COUNT_MAX has room for, or a key exceeds
|
||||
* PARTY_KEY_LENGTH_MAX.
|
||||
*/
|
||||
uint8_t partyGetAvailableMember(void);
|
||||
errorret_t partyInit(party_t *party);
|
||||
|
||||
/**
|
||||
* Adds a member to the party roster in the next available slot. Party
|
||||
* members are always allies controlled by the player. If there is a
|
||||
* free active order slot, the new member is placed into it.
|
||||
* Looks up a roster member's index by its party.jsonc key (see
|
||||
* party_t.keys).
|
||||
*
|
||||
* @param stats The member's base combat stats.
|
||||
* @param healthMax The member's maximum health.
|
||||
* @param mpMax The member's maximum mp.
|
||||
* @return Pointer to the newly added party member, or NULL if the party is
|
||||
* already full.
|
||||
* @param key The member's key to search for.
|
||||
* @return The matching roster index, or PARTY_MEMBER_NOT_FOUND if no
|
||||
* member has that key.
|
||||
*/
|
||||
battlefighter_t *partyAddMember(
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
uint8_t partyGetMemberIndexByKey(const char_t *key);
|
||||
|
||||
/**
|
||||
* Gets the roster member currently occupying an active order slot.
|
||||
* Gets a roster member's party.jsonc key by index (see party_t.keys).
|
||||
*
|
||||
* @param memberIndex The roster member index to query.
|
||||
* @return The member's key - "" if memberIndex is past however many
|
||||
* members party.jsonc actually defines.
|
||||
*/
|
||||
const char_t *partyGetMemberKey(const uint8_t memberIndex);
|
||||
|
||||
/**
|
||||
* Checks whether memberIndex currently occupies a slot in the current save
|
||||
* slot's party order (i.e. is actively in the party right now).
|
||||
*
|
||||
* @param memberIndex The roster member index to check.
|
||||
* @return True if memberIndex appears anywhere in order.
|
||||
*/
|
||||
bool_t partyIsMemberActive(const uint8_t memberIndex);
|
||||
|
||||
/**
|
||||
* Adds an existing roster member (see partyInit - every roster slot
|
||||
* always exists once initialized) to the current save slot's active party
|
||||
* order, in the first free order slot. No-op if memberIndex is already
|
||||
* active, or if there is no free order slot.
|
||||
*
|
||||
* @param memberIndex The roster member index to activate.
|
||||
*/
|
||||
void partyAddMember(const uint8_t memberIndex);
|
||||
|
||||
/**
|
||||
* Gets the roster member currently occupying an active order slot in the
|
||||
* current save slot's party.
|
||||
*
|
||||
* @param slot The active order slot to query.
|
||||
* @return Pointer to the member in that slot, or NULL if the slot is
|
||||
@@ -63,11 +88,39 @@ battlefighter_t *partyAddMember(
|
||||
battlefighter_t *partyGetOrderMember(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Assigns a roster member to an active order slot, replacing whatever
|
||||
* was there. Use PARTY_ORDER_EMPTY to clear a slot.
|
||||
* Assigns a roster member to an active order slot in the current save
|
||||
* slot's party, replacing whatever was there. Use PARTY_ORDER_EMPTY to
|
||||
* clear a slot.
|
||||
*
|
||||
* @param slot The active order slot to assign.
|
||||
* @param memberIndex The roster member index to place there, or
|
||||
* PARTY_ORDER_EMPTY to clear the slot.
|
||||
*/
|
||||
void partySetOrder(const uint8_t slot, const uint8_t memberIndex);
|
||||
|
||||
/**
|
||||
* Removes memberIndex from the current save slot's active party order -
|
||||
* the roster member itself is untouched (it still exists, per partyInit;
|
||||
* it's simply no longer active) - shifting every later order entry down
|
||||
* one slot to close the gap, so order stays packed with no holes between
|
||||
* active members. No-op if memberIndex isn't currently active.
|
||||
*
|
||||
* @param memberIndex The roster member index to deactivate.
|
||||
*/
|
||||
void partyRemoveMember(const uint8_t memberIndex);
|
||||
|
||||
/**
|
||||
* Fully restores a roster member's health/mp to their max and clears a
|
||||
* dead status back to normal - unlike battleFighterHeal, this can revive
|
||||
* a dead member (there's no combat context here to keep them down). No-op
|
||||
* if the slot is empty.
|
||||
*
|
||||
* @param memberIndex The roster member index to heal.
|
||||
*/
|
||||
void partyHealMember(const uint8_t memberIndex);
|
||||
|
||||
/**
|
||||
* Calls partyHealMember for every occupied slot in the current save
|
||||
* slot's party roster.
|
||||
*/
|
||||
void partyHealAll(void);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
battlestate.c
|
||||
battlestateopening.c
|
||||
battlestatepreround.c
|
||||
battlestateselection.c
|
||||
battlestateexecuting.c
|
||||
battlestatepostround.c
|
||||
battlestatelost.c
|
||||
battlestatewon.c
|
||||
battlestatefled.c
|
||||
battlestateending.c
|
||||
)
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestate.h"
|
||||
|
||||
battlestatefunctions_t BATTLE_STATE_FUNCTIONS[BATTLE_STATE_COUNT] = {
|
||||
[BATTLE_STATE_NULL] = { 0 },
|
||||
|
||||
[BATTLE_STATE_OPENING] = {
|
||||
.init = battleStateOpeningInit,
|
||||
.update = battleStateOpeningUpdate,
|
||||
.dispose = battleStateOpeningDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_PRE_ROUND] = {
|
||||
.init = battleStatePreRoundInit,
|
||||
.update = battleStatePreRoundUpdate,
|
||||
.dispose = battleStatePreRoundDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_SELECTION] = {
|
||||
.init = battleStateSelectionInit,
|
||||
.update = battleStateSelectionUpdate,
|
||||
.dispose = battleStateSelectionDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_EXECUTING] = {
|
||||
.init = battleStateExecutingInit,
|
||||
.update = battleStateExecutingUpdate,
|
||||
.dispose = battleStateExecutingDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_POST_ROUND] = {
|
||||
.init = battleStatePostRoundInit,
|
||||
.update = battleStatePostRoundUpdate,
|
||||
.dispose = battleStatePostRoundDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_LOST] = {
|
||||
.init = battleStateLostInit,
|
||||
.update = battleStateLostUpdate,
|
||||
.dispose = battleStateLostDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_WON] = {
|
||||
.init = battleStateWonInit,
|
||||
.update = battleStateWonUpdate,
|
||||
.dispose = battleStateWonDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_FLED] = {
|
||||
.init = battleStateFledInit,
|
||||
.update = battleStateFledUpdate,
|
||||
.dispose = battleStateFledDispose
|
||||
},
|
||||
|
||||
[BATTLE_STATE_ENDING] = {
|
||||
.init = battleStateEndingInit,
|
||||
.update = battleStateEndingUpdate,
|
||||
.dispose = battleStateEndingDispose
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "battlestateopening.h"
|
||||
#include "battlestatepreround.h"
|
||||
#include "battlestateselection.h"
|
||||
#include "battlestateexecuting.h"
|
||||
#include "battlestatepostround.h"
|
||||
#include "battlestatelost.h"
|
||||
#include "battlestatewon.h"
|
||||
#include "battlestatefled.h"
|
||||
#include "battlestateending.h"
|
||||
|
||||
typedef enum {
|
||||
// No active battle.
|
||||
BATTLE_STATE_NULL,
|
||||
|
||||
// Battle hasn't actually started yet, equiv to trainer intro in Pokemon.
|
||||
BATTLE_STATE_OPENING,
|
||||
// Pre round, anything that happens before AI or Player can act.
|
||||
BATTLE_STATE_PRE_ROUND,
|
||||
// Player is deciding what to do, waiting for their input
|
||||
BATTLE_STATE_SELECTION,
|
||||
// Currently executing the moves.
|
||||
BATTLE_STATE_EXECUTING,
|
||||
// Post round, anything that happens after AI or Player has acted, loops back
|
||||
// after this point
|
||||
BATTLE_STATE_POST_ROUND,
|
||||
// Battle has ended with a player loss
|
||||
BATTLE_STATE_LOST,
|
||||
// Battle has ended with a player win
|
||||
BATTLE_STATE_WON,
|
||||
// Battle has ended with a player fleeing
|
||||
BATTLE_STATE_FLED,
|
||||
// Battle state is done, this is fade out or whatever.
|
||||
BATTLE_STATE_ENDING,
|
||||
|
||||
BATTLE_STATE_COUNT
|
||||
} battlestate_t;
|
||||
|
||||
typedef union {
|
||||
battlestateopening_t opening;
|
||||
battlestatepreround_t preRound;
|
||||
battlestateselection_t selection;
|
||||
battlestateexecuting_t executing;
|
||||
battlestatepostround_t postRound;
|
||||
battlestatelost_t lost;
|
||||
battlestatewon_t won;
|
||||
battlestatefled_t fled;
|
||||
battlestateending_t ending;
|
||||
} battlestatedata_t;
|
||||
|
||||
typedef struct {
|
||||
void (*init)();
|
||||
void (*update)();
|
||||
void (*dispose)();
|
||||
} battlestatefunctions_t;
|
||||
|
||||
extern battlestatefunctions_t BATTLE_STATE_FUNCTIONS[BATTLE_STATE_COUNT];
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestateending.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
void battleStateEndingInit(void) {
|
||||
// Tear down the battle and hand control back to the overworld. The
|
||||
// overworld scene keeps no state of its own in scenedata_t (player/map
|
||||
// state lives in the separate global entity/map systems, untouched by
|
||||
// switching to/from SCENE_TYPE_BATTLE), so there's nothing else to
|
||||
// restore here.
|
||||
battleDispose();
|
||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||
}
|
||||
|
||||
void battleStateEndingUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateEndingDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestateending_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_ENDING. Disposes the battle
|
||||
* (see battleDispose) and switches back to SCENE_TYPE_OVERWORLD.
|
||||
*/
|
||||
void battleStateEndingInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_ENDING for one frame.
|
||||
*/
|
||||
void battleStateEndingUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_ENDING.
|
||||
*/
|
||||
void battleStateEndingDispose(void);
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestateexecuting.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/cutscene/cutscene.h"
|
||||
#include "asset/assetfile.h"
|
||||
#include "ui/overlay/uifatalerror.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/string.h"
|
||||
|
||||
|
||||
void battleStateExecutingResolveFighterIndex(
|
||||
battlefighterpos_t *fighterIndex,
|
||||
const uint8_t userIndex,
|
||||
const battlemoveaction_t *action
|
||||
) {
|
||||
if(*fighterIndex == BATTLE_FIGHTER_POS_TARGET) {
|
||||
*fighterIndex = action->targets[0];
|
||||
} else if(*fighterIndex == BATTLE_FIGHTER_POS_USER) {
|
||||
*fighterIndex = userIndex;
|
||||
}
|
||||
}
|
||||
|
||||
void battleStateExecutingInit(void) {
|
||||
cutsceneitem_t items[BATTLE_STATE_EXECUTING_ITEMS_MAX];
|
||||
uint8_t itemsCount = 0;
|
||||
|
||||
// For each action (just queued during selection). actionsPending is
|
||||
// indexed by fighter position, not sequentially, so scan every position
|
||||
// and stop early once every pending action has been processed.
|
||||
uint8_t processed = 0;
|
||||
for(
|
||||
uint8_t i = 0;
|
||||
i < BATTLE_FIGHTER_POS_COUNT && processed < BATTLE.actionsPendingCount;
|
||||
i++
|
||||
) {
|
||||
// Get action.
|
||||
battlemoveaction_t *action = &BATTLE.actionsPending[i];
|
||||
if(action->move == BATTLE_MOVE_ID_NULL) continue;
|
||||
processed++;
|
||||
|
||||
assertTrue(action->move < BATTLE_MOVE_COUNT, "Invalid move id");
|
||||
|
||||
// Get ability.
|
||||
battleability_t *ability = &BATTLE_MOVES[action->move];
|
||||
assertStrLenMin(ability->name, 1, "Invalid move name");
|
||||
|
||||
// Determine cutscene path
|
||||
char_t path[ASSET_FILE_NAME_MAX];
|
||||
stringFormat(
|
||||
path, ASSET_FILE_NAME_MAX - 1,
|
||||
"cutscenes/battle/moves/%s.jsonc", ability->name
|
||||
);
|
||||
|
||||
// Load and parse the move's cutscene straight into the tail of items.
|
||||
const uint8_t moveItemsStart = itemsCount;
|
||||
cutscene_t scene;
|
||||
errorret_t result = cutsceneLoadParse(
|
||||
path, &scene, &items[itemsCount],
|
||||
BATTLE_STATE_EXECUTING_ITEMS_MAX - itemsCount
|
||||
);
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(errorPrint(result));
|
||||
uiFatalErrorOpen(NULL);
|
||||
continue;
|
||||
}
|
||||
itemsCount += scene.itemCount;
|
||||
|
||||
// Resolve BATTLE_FIGHTER_POS_TARGET/USER sentinels against this
|
||||
// action, now that its user/target fighters are known.
|
||||
for(uint8_t j = moveItemsStart; j < itemsCount; j++) {
|
||||
cutsceneitem_t *moveItem = &items[j];
|
||||
if(moveItem->type == CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_DAMAGE) {
|
||||
battleStateExecutingResolveFighterIndex(
|
||||
&moveItem->battleFighterDamage.fighterIndex, i, action
|
||||
);
|
||||
} else if(moveItem->type == CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_HEAL) {
|
||||
battleStateExecutingResolveFighterIndex(
|
||||
&moveItem->battleFighterHeal.fighterIndex, i, action
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert end of attack cutscene item.
|
||||
items[itemsCount++] = (cutsceneitem_t){
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE
|
||||
};
|
||||
|
||||
// This action has now been resolved - clear it so it isn't
|
||||
// re-executed (and doesn't block re-selection) next round.
|
||||
action->move = BATTLE_MOVE_ID_NULL;
|
||||
}
|
||||
BATTLE.actionsPendingCount = 0;
|
||||
|
||||
// After all moves completed, transition to pre-round for next round.
|
||||
items[itemsCount++] = (cutsceneitem_t){
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = { .state = BATTLE_STATE_PRE_ROUND }
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, itemsCount);
|
||||
}
|
||||
|
||||
void battleStateExecutingUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateExecutingDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "rpg/battle/fighter/battlefighterpos.h"
|
||||
#include "rpg/battle/move/battlemoveaction.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestateexecuting_t;
|
||||
|
||||
#define BATTLE_STATE_EXECUTING_ITEMS_MAX 64
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_EXECUTING.
|
||||
*/
|
||||
void battleStateExecutingInit(void);
|
||||
|
||||
/**
|
||||
* Resolves a BATTLE_FIGHTER_POS_TARGET/BATTLE_FIGHTER_POS_USER sentinel
|
||||
* (as parsed onto a BATTLE_FIGHTER_DAMAGE/BATTLE_FIGHTER_HEAL item inside
|
||||
* a move's own cutscene) against the action currently being resolved
|
||||
* into cutscene items. Leaves fighterIndex untouched if it's already a
|
||||
* concrete BATTLE.fighters index.
|
||||
*
|
||||
* @param fighterIndex The field to resolve in place.
|
||||
* @param userIndex Index into BATTLE.fighters of the fighter performing
|
||||
* the action.
|
||||
* @param action The action being resolved.
|
||||
*/
|
||||
void battleStateExecutingResolveFighterIndex(
|
||||
battlefighterpos_t *fighterIndex,
|
||||
const uint8_t userIndex,
|
||||
const battlemoveaction_t *action
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_EXECUTING for one frame.
|
||||
*/
|
||||
void battleStateExecutingUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_EXECUTING.
|
||||
*/
|
||||
void battleStateExecutingDispose(void);
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestatefled.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
void battleStateFledInit(void) {
|
||||
const cutsceneitem_t items[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT,
|
||||
.text = { .text = "Got away safely!" }
|
||||
},
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = { .state = BATTLE_STATE_ENDING }
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
void battleStateFledUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateFledDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestatefled_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_FLED. Queues a textbox
|
||||
* announcing the escape, then transitions to BATTLE_STATE_ENDING once
|
||||
* dismissed.
|
||||
*/
|
||||
void battleStateFledInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_FLED for one frame.
|
||||
*/
|
||||
void battleStateFledUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_FLED.
|
||||
*/
|
||||
void battleStateFledDispose(void);
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestatelost.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
void battleStateLostInit(void) {
|
||||
const cutsceneitem_t items[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_TEXT,
|
||||
.text = { .text = "You were defeated..." }
|
||||
},
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = { .state = BATTLE_STATE_ENDING }
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
void battleStateLostUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateLostDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestatelost_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_LOST. Queues a textbox
|
||||
* announcing the loss, then transitions to BATTLE_STATE_ENDING once
|
||||
* dismissed.
|
||||
*/
|
||||
void battleStateLostInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_LOST for one frame.
|
||||
*/
|
||||
void battleStateLostUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_LOST.
|
||||
*/
|
||||
void battleStateLostDispose(void);
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlestateopening.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
void battleStateOpeningInit(void) {
|
||||
const cutsceneitem_t items[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||
.battleSetState = { .state = BATTLE_STATE_PRE_ROUND }
|
||||
}
|
||||
};
|
||||
cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t));
|
||||
}
|
||||
|
||||
void battleStateOpeningUpdate(void) {
|
||||
|
||||
}
|
||||
|
||||
void battleStateOpeningDispose(void) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} battlestateopening_t;
|
||||
|
||||
/**
|
||||
* Called when the battle enters BATTLE_STATE_OPENING.
|
||||
*/
|
||||
void battleStateOpeningInit(void);
|
||||
|
||||
/**
|
||||
* Updates BATTLE_STATE_OPENING for one frame.
|
||||
*/
|
||||
void battleStateOpeningUpdate(void);
|
||||
|
||||
/**
|
||||
* Called when the battle leaves BATTLE_STATE_OPENING.
|
||||
*/
|
||||
void battleStateOpeningDispose(void);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user