Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9551e222dc | |||
| 60dfb89b53 | |||
| b08ad308e4 | |||
| ca02ee0352 | |||
| fbaa54145e | |||
| 28754ffbf2 | |||
| 470c0eba7a | |||
| 7098dcec43 | |||
| 07137f57af | |||
| 8b7491a3d3 | |||
| 8cfa8ddfeb | |||
| ef284a15a1 | |||
| 3723921573 | |||
| 195399635e | |||
| 46e2a924d3 | |||
| b693ea4102 | |||
| a73f55beb0 | |||
| 0bd2491ab7 | |||
| 860c797c9c | |||
| 38c5080f9f | |||
| fae191d8fe | |||
| d83a953e2d | |||
| 2dcf0d0f0d | |||
| 6dee37d8c1 | |||
| 3bad03afb3 | |||
| 6a6d8448f7 | |||
| 189babd2cf | |||
| 988a0f2294 | |||
| 85bf455731 | |||
| 589e4224f3 | |||
| bd7fa154b4 | |||
| a292f1992b | |||
| 88ddf429b7 | |||
| 7a1f6662df | |||
| afc68bccc6 | |||
| b4cdc4a64f | |||
| 3ad5afb81c | |||
| bed3f20118 | |||
| de67315178 | |||
| 8d5c0c7cad | |||
| a8271e01bd | |||
| 900b3f8558 | |||
| fdc4e056f9 | |||
| 7b98e40ccf | |||
| 01d89cf22c | |||
| 503a3c799a | |||
| 0b21388844 | |||
| 117bdf0c00 | |||
| 172dc5d37b |
@@ -4,36 +4,6 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
jobs:
|
jobs:
|
||||||
run-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
cmake \
|
|
||||||
python3 \
|
|
||||||
python3-pip \
|
|
||||||
python3-polib \
|
|
||||||
python3-pil \
|
|
||||||
libsdl2-dev \
|
|
||||||
libgl1-mesa-dev \
|
|
||||||
libzip-dev \
|
|
||||||
python3-dotenv \
|
|
||||||
python3-pyqt5 \
|
|
||||||
python3-opengl \
|
|
||||||
xz-utils \
|
|
||||||
liblzma-dev \
|
|
||||||
libbz2-dev \
|
|
||||||
zlib1g-dev \
|
|
||||||
git \
|
|
||||||
libssl-dev
|
|
||||||
- name: Run tests
|
|
||||||
run: ./scripts/test-linux.sh
|
|
||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: Test Dusk
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
cmake \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-polib \
|
||||||
|
python3-pil \
|
||||||
|
libsdl2-dev \
|
||||||
|
libgl1-mesa-dev \
|
||||||
|
libzip-dev \
|
||||||
|
python3-dotenv \
|
||||||
|
python3-pyqt5 \
|
||||||
|
python3-opengl \
|
||||||
|
xz-utils \
|
||||||
|
liblzma-dev \
|
||||||
|
libbz2-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
git \
|
||||||
|
libssl-dev
|
||||||
|
- name: Run tests
|
||||||
|
run: ./scripts/test-linux.sh
|
||||||
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,25 @@
|
|||||||
|
{
|
||||||
|
"pause": "DEFAULT",
|
||||||
|
"items": [
|
||||||
|
{ "type": "text", "text": "Test Two." },
|
||||||
|
{ "type": "entityAdd", "entityType": "npc", "position": [4, 4, 0] },
|
||||||
|
{
|
||||||
|
"type": "textMini",
|
||||||
|
"text": "Hello!",
|
||||||
|
"position": [4, 4, 0],
|
||||||
|
"duration": 3.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "emoji",
|
||||||
|
"entityIndex": "lastCreated",
|
||||||
|
"emojiType": "exclamation",
|
||||||
|
"duration": 2.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "entityWalkTo",
|
||||||
|
"entityIndex": "lastCreated",
|
||||||
|
"positions": [[8, 2, 0]]
|
||||||
|
},
|
||||||
|
{ "type": "text", "text": "Done." }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// Copyright (c) 2026 Dominic Masters
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
const platformNames = {
|
|
||||||
[System.PLATFORM_LINUX]: 'Linux',
|
|
||||||
[System.PLATFORM_KNULLI]: 'Knulli',
|
|
||||||
[System.PLATFORM_PSP]: 'PSP',
|
|
||||||
[System.PLATFORM_GAMECUBE]: 'GameCube',
|
|
||||||
[System.PLATFORM_WII]: 'Wii',
|
|
||||||
};
|
|
||||||
|
|
||||||
Console.print('Platform: ' + (platformNames[System.platform] || 'Unknown'));
|
|
||||||
|
|
||||||
UIFullboxOver.setColor(Color.BLACK);
|
|
||||||
|
|
||||||
requireAsync('testscene.js').then(Scene.set).catch(err => {
|
|
||||||
Console.print('Error loading scene: ' + err);
|
|
||||||
Engine.exit();
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
{ "id": "POTION", "type": "MEDICINE", "weight": 1.0, "name": "potion" },
|
||||||
|
{ "id": "POTATO", "type": "FOOD", "weight": 0.5, "name": "potato" },
|
||||||
|
{ "id": "APPLE", "type": "FOOD", "weight": 0.3, "name": "apple" }
|
||||||
|
]
|
||||||
+49
-43
@@ -10,51 +10,57 @@ msgid "ui.title"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Welcome"
|
"Welcome"
|
||||||
|
|
||||||
#: ui/user.c:22
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
msgid "ui.greeting"
|
msgid "ui.settings.tabs.general"
|
||||||
msgstr "Hello, %s!"
|
msgstr "General"
|
||||||
|
|
||||||
#: ui/files.c:40
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
msgid "ui.file_status"
|
msgid "ui.settings.tabs.input"
|
||||||
msgstr "%s has %d files."
|
msgstr "Input"
|
||||||
|
|
||||||
#: ui/cart.c:55
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
msgid "cart.item_count"
|
msgid "ui.settings.tabs.display"
|
||||||
msgid_plural "cart.item_count"
|
msgstr "Display"
|
||||||
msgstr[0] "%d item"
|
|
||||||
msgstr[1] "%d items (dual)"
|
|
||||||
msgstr[2] "%d items (few)"
|
|
||||||
msgstr[3] "%d items (many)"
|
|
||||||
|
|
||||||
#: ui/notifications.c:71
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
msgid ""
|
msgid "ui.settings.tabs.audio"
|
||||||
"ui.multiline_help"
|
msgstr "Audio"
|
||||||
msgstr ""
|
|
||||||
"Line one of the help text.\n"
|
|
||||||
"Line two continues here.\n"
|
|
||||||
"Line three ends here."
|
|
||||||
|
|
||||||
#: ui/errors.c:90
|
msgid "ui.settings.input.deadzone"
|
||||||
msgid ""
|
msgstr "Deadzone"
|
||||||
"error.upload_failed.long"
|
|
||||||
msgstr ""
|
|
||||||
"Upload failed for file \"%s\".\n"
|
|
||||||
"Please try again later or contact support."
|
|
||||||
|
|
||||||
#: ui/messages.c:110
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
msgid ""
|
msgid "ui.settings.general.language"
|
||||||
"user.invite_status"
|
msgstr "Language"
|
||||||
msgid_plural ""
|
|
||||||
"user.invite_status"
|
msgid "ui.settings.general.language_detail"
|
||||||
msgstr[0] ""
|
msgstr "Takes effect after restarting the application."
|
||||||
"%s invited %d user.\n"
|
|
||||||
"Please review the request."
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
msgstr[1] ""
|
msgid "ui.settings.apply"
|
||||||
"%s invited %d users (dual).\n"
|
msgstr "Apply"
|
||||||
"Please review the requests."
|
|
||||||
msgstr[2] ""
|
#: src/dusk/ui/frame/uiconfirm.c
|
||||||
"%s invited %d users (few).\n"
|
msgid "ui.confirm.discard_changes"
|
||||||
"Please review the requests."
|
msgstr "Discard unsaved changes?"
|
||||||
msgstr[3] ""
|
|
||||||
"%s invited %d users (many).\n"
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
"Please review the requests."
|
msgid "ui.game_menu.characters"
|
||||||
|
msgstr "Characters"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.items"
|
||||||
|
msgstr "Items"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.settings"
|
||||||
|
msgstr "Settings"
|
||||||
|
|
||||||
|
msgid "item.potion.name"
|
||||||
|
msgstr "Potion"
|
||||||
|
|
||||||
|
msgid "item.potato.name"
|
||||||
|
msgstr "Potato"
|
||||||
|
|
||||||
|
msgid "item.apple.name"
|
||||||
|
msgstr "Apple"
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: ExampleApp 1.0\n"
|
||||||
|
"Language: es\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n==1 ? 0 : 1);\n"
|
||||||
|
|
||||||
|
#: ui/menu.c:10
|
||||||
|
msgid "ui.title"
|
||||||
|
msgstr ""
|
||||||
|
"Bienvenido"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.general"
|
||||||
|
msgstr "General"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.input"
|
||||||
|
msgstr "Entrada"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.display"
|
||||||
|
msgstr "Pantalla"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.audio"
|
||||||
|
msgstr "Audio"
|
||||||
|
|
||||||
|
msgid "ui.settings.input.deadzone"
|
||||||
|
msgstr "Deadzone"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
|
msgid "ui.settings.general.language"
|
||||||
|
msgstr "Idioma"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
|
msgid "ui.settings.general.language_detail"
|
||||||
|
msgstr "Se aplica después de reiniciar la aplicación."
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.apply"
|
||||||
|
msgstr "Aplicar"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/uiconfirm.c
|
||||||
|
msgid "ui.confirm.discard_changes"
|
||||||
|
msgstr "¿Descartar los cambios no guardados?"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.characters"
|
||||||
|
msgstr "Personajes"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.items"
|
||||||
|
msgstr "Objetos"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.settings"
|
||||||
|
msgstr "Configuración"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.potion.name"
|
||||||
|
msgstr "Poción"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.potato.name"
|
||||||
|
msgstr "Papa"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.apple.name"
|
||||||
|
msgstr "Manzana"
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: ExampleApp 1.0\n"
|
||||||
|
"Language: ja\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Plural-Forms: nplurals=1; plural=(0);\n"
|
||||||
|
|
||||||
|
#: ui/menu.c:10
|
||||||
|
msgid "ui.title"
|
||||||
|
msgstr ""
|
||||||
|
"歓迎"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.general"
|
||||||
|
msgstr "一般"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.input"
|
||||||
|
msgstr "入力"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.display"
|
||||||
|
msgstr "表示"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.audio"
|
||||||
|
msgstr "オーディオ"
|
||||||
|
|
||||||
|
msgid "ui.settings.input.deadzone"
|
||||||
|
msgstr "デッドゾーン"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
|
msgid "ui.settings.general.language"
|
||||||
|
msgstr "言語"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
|
msgid "ui.settings.general.language_detail"
|
||||||
|
msgstr "アプリケーションを再起動すると適用されます。"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.apply"
|
||||||
|
msgstr "適用"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/uiconfirm.c
|
||||||
|
msgid "ui.confirm.discard_changes"
|
||||||
|
msgstr "未保存の変更を破棄しますか?"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.characters"
|
||||||
|
msgstr "キャラクター"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.items"
|
||||||
|
msgstr "アイテム"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.settings"
|
||||||
|
msgstr "設定"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.potion.name"
|
||||||
|
msgstr "ポーション"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.potato.name"
|
||||||
|
msgstr "ジャガイモ"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.apple.name"
|
||||||
|
msgstr "リンゴ"
|
||||||
@@ -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], [0, 0], [0, 0], [0, 0], [0, 0], [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], [0, 0], [0, 0], [0, 0], [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], [0, 0], [0, 0], [0, 0], [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], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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], [0, 0], [1, 0], [1, 0], [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], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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": [{"model": "models/chunks/chunk_-1_0_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_4_4.json", "offset": [2.0, 1.0, 0.0]}, {"model": "models/buildings/house_6_3.json", "offset": [1.0, 9.0, 0.0]}, {"model": "models/buildings/house_2_2.json", "offset": [12.0, 6.0, 0.0]}]}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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], [0, 0], [1, 0], [1, 0], [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], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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": [{"model": "models/chunks/chunk_0_1_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_8_4.json", "offset": [1.0, 1.0, 0.0]}, {"model": "models/buildings/house_3_3.json", "offset": [11.0, 2.0, 0.0]}, {"model": "models/buildings/house_2_2.json", "offset": [3.0, 11.0, 0.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], [0, 0], [0, 0], [0, 0], [1, 0], [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], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [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], [0, 0], [1, 0], [1, 0], [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], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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], [0, 0], [0, 0], [1, 0], [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], [0, 0], [0, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [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], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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": [{"model": "models/chunks/chunk_1_0_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_3_2.json", "offset": [1.0, 1.0, 0.0]}, {"model": "models/buildings/house_2_3.json", "offset": [7.0, 2.0, 0.0]}, {"model": "models/buildings/house_4_2.json", "offset": [1.0, 8.0, 0.0]}, {"model": "models/buildings/house_1_1.json", "offset": [11.0, 10.0, 0.0]}, {"model": "models/buildings/house_5_2.json", "offset": [13.0, 6.0, 0.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], [0, 0], [0, 0], [0, 0], [0, 0], [0, 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], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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": [{"model": "models/chunks/chunk_1_1_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_5_5.json", "offset": [3.0, 2.0, 0.0]}, {"model": "models/buildings/house_2_3.json", "offset": [10.0, 3.0, 0.0]}, {"model": "models/buildings/house_3_1.json", "offset": [1.0, 12.0, 0.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], [0, 0], [0, 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], [0, 0], [0, 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], [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": [{"model": "models/chunks/chunk_2_0_0_0.json", "offset": [0.0, 0.0, 0.0]}]}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "Test Map",
|
||||||
|
"entities": [
|
||||||
|
{ "type": "player", "position": [10, 2, 0], "direction": "north" },
|
||||||
|
{ "type": "item", "position": [12, 2, 0], "item": "POTION", "quantity": 1 },
|
||||||
|
{
|
||||||
|
"type": "npc",
|
||||||
|
"position": [8, 8, 1],
|
||||||
|
"path": [[4, 4, 0], [10, 10, 1], [4, 4, 0], [10, 10, 1]],
|
||||||
|
"cutscene": "test_npc"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,63 +0,0 @@
|
|||||||
// Copyright (c) 2026 Dominic Masters
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
const PLAYER_SPEED = 5.0;
|
|
||||||
// 1 world unit = 16 pixels.
|
|
||||||
const PIXEL_SCALE = 1.0 / 16.0;
|
|
||||||
// Player sprite is 32x32 px (test.png dimensions).
|
|
||||||
const PLAYER_W = 32 * PIXEL_SCALE;
|
|
||||||
const PLAYER_H = 32 * PIXEL_SCALE;
|
|
||||||
|
|
||||||
var player = {};
|
|
||||||
|
|
||||||
player.getAssets = () => {
|
|
||||||
return [
|
|
||||||
{ path: 'test.png', type: Asset.TYPE_TEXTURE, format: Texture.FORMAT_RGBA }
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
player.init = function(scene) {
|
|
||||||
var texture = scene.assets.getAssetByPath('test.png');
|
|
||||||
Console.print('Player init: got texture ' + texture);
|
|
||||||
|
|
||||||
_entity = Entity.create();
|
|
||||||
_position = _entity.add(Component.POSITION);
|
|
||||||
_physics = _entity.add(Component.PHYSICS);
|
|
||||||
|
|
||||||
_physics.bodyType = Physics.DYNAMIC;
|
|
||||||
_physics.shape = Physics.SHAPE_CUBE;
|
|
||||||
_physics.gravityScale = 1.0;
|
|
||||||
|
|
||||||
var r = _entity.add(Component.RENDERABLE);
|
|
||||||
r.texture = texture.texture;
|
|
||||||
r.type = Renderable.SPRITEBATCH;
|
|
||||||
r.color = new Color(220, 80, 80);
|
|
||||||
// Upright quad centered on X, bottom-aligned on Y.
|
|
||||||
r.sprites = [[-PLAYER_W/2, 0, 0, PLAYER_W/2, PLAYER_H, 0, 0, 1, 1, 0]];
|
|
||||||
|
|
||||||
_position.localPosition = new Vec3(0, PLAYER_H, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
player.getPosition = function() {
|
|
||||||
return _position;
|
|
||||||
};
|
|
||||||
|
|
||||||
player.update = function() {
|
|
||||||
if(!_physics) return;
|
|
||||||
var vx = Input.axis(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT) * PLAYER_SPEED;
|
|
||||||
var vz = Input.axis(INPUT_ACTION_DOWN, INPUT_ACTION_UP) * PLAYER_SPEED;
|
|
||||||
// Preserve vertical velocity so gravity and landing work correctly.
|
|
||||||
var vy = _physics.velocity.y;
|
|
||||||
_physics.velocity = new Vec3(vx, vy, vz);
|
|
||||||
};
|
|
||||||
|
|
||||||
player.dispose = function() {
|
|
||||||
Entity.dispose(_entity);
|
|
||||||
_entity = null;
|
|
||||||
_position = null;
|
|
||||||
_physics = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = player;
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// Copyright (c) 2026 Dominic Masters
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
var scene = {};
|
|
||||||
|
|
||||||
// Pokemon DS-style camera: ~34 degrees elevation (atan(6/9)).
|
|
||||||
// CAM_HEIGHT / CAM_DIST ratio controls the tilt - keep it under 0.7 for
|
|
||||||
// the characteristically shallow DS angle.
|
|
||||||
const CAM_HEIGHT = 6;
|
|
||||||
const CAM_DIST = 9;
|
|
||||||
|
|
||||||
scene.init = async function() {
|
|
||||||
// Camera
|
|
||||||
scene.cam = Entity.create();
|
|
||||||
var camPos = scene.cam.add(Component.POSITION);
|
|
||||||
var cam = scene.cam.add(Component.CAMERA);
|
|
||||||
camPos.localPosition = new Vec3(3, 3, 3);
|
|
||||||
camPos.lookAt(new Vec3(0, 0, 0));
|
|
||||||
|
|
||||||
// Floor - large flat slab, no texture needed.
|
|
||||||
scene.floor = Entity.create();
|
|
||||||
var floorPos = scene.floor.add(Component.POSITION);
|
|
||||||
var floorR = scene.floor.add(Component.RENDERABLE);
|
|
||||||
floorR.type = Renderable.SHADER_MATERIAL;
|
|
||||||
floorR.color = Color.BLUE;
|
|
||||||
// floorPos.localScale = new Vec3(16, 0.2, 16);
|
|
||||||
// floorPos.localPosition = new Vec3(0, -0.1, 0);
|
|
||||||
|
|
||||||
await UIFullboxOver.transition(Color.BLACK, Color.TRANSPARENT, 1.0);
|
|
||||||
};
|
|
||||||
|
|
||||||
scene.update = function() {
|
|
||||||
};
|
|
||||||
|
|
||||||
scene.dispose = function() {
|
|
||||||
Entity.dispose(scene.floor);
|
|
||||||
Entity.dispose(scene.cam);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = scene;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
module = {
|
|
||||||
render() {
|
|
||||||
Text.draw(0, 0, "Hello World");
|
|
||||||
SpriteBatch.flush();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
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
@@ -16,6 +16,9 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
DOL=1
|
DOL=1
|
||||||
ISO=2
|
ISO=2
|
||||||
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
|
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
|
||||||
|
# GameCube/Wii PowerPC is always big-endian; declare it at compile time
|
||||||
|
# like every other target instead of relying on endian.h's runtime probe.
|
||||||
|
DUSK_PLATFORM_ENDIAN_BIG
|
||||||
)
|
)
|
||||||
|
|
||||||
# Custom compiler flags
|
# Custom compiler flags
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
# DUSK_OPENGL_LEGACY
|
# DUSK_OPENGL_LEGACY
|
||||||
DUSK_LINUX
|
DUSK_LINUX
|
||||||
DUSK_DISPLAY_SIZE_DYNAMIC
|
DUSK_DISPLAY_SIZE_DYNAMIC
|
||||||
DUSK_DISPLAY_WIDTH_DEFAULT=640
|
DUSK_DISPLAY_WIDTH_DEFAULT=854
|
||||||
DUSK_DISPLAY_HEIGHT_DEFAULT=480
|
DUSK_DISPLAY_HEIGHT_DEFAULT=480
|
||||||
DUSK_DISPLAY_SCREEN_HEIGHT=240
|
DUSK_DISPLAY_SCREEN_HEIGHT=240
|
||||||
DUSK_INPUT_KEYBOARD
|
DUSK_INPUT_KEYBOARD
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
|||||||
DUSK_DISPLAY_HEIGHT=272
|
DUSK_DISPLAY_HEIGHT=272
|
||||||
DUSK_THREAD_PTHREAD
|
DUSK_THREAD_PTHREAD
|
||||||
DUSK_TIME_DYNAMIC
|
DUSK_TIME_DYNAMIC
|
||||||
|
DUSK_DISPLAY_OVERSCAN=6
|
||||||
)
|
)
|
||||||
|
|
||||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ const ChunkTerrain = (() => {
|
|||||||
const CHUNK_DEPTH = 4;
|
const CHUNK_DEPTH = 4;
|
||||||
const TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH;
|
const TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH;
|
||||||
|
|
||||||
|
// World-space Z distance of one Z-layer/story, in world-float space.
|
||||||
|
// Must match WORLD_LAYER_HEIGHT in src/dusk/rpg/overworld/worldpos.h.
|
||||||
|
const WORLD_LAYER_HEIGHT = Math.SQRT1_2;
|
||||||
|
|
||||||
const TILE_SHAPE_NULL = 0;
|
const TILE_SHAPE_NULL = 0;
|
||||||
const TILE_SHAPE_GROUND = 1;
|
const TILE_SHAPE_GROUND = 1;
|
||||||
const TILE_SHAPE_RAMP_NORTH = 2;
|
const TILE_SHAPE_RAMP_NORTH = 2;
|
||||||
@@ -88,7 +92,53 @@ const ChunkTerrain = (() => {
|
|||||||
const v1 = (y + 1) / CHUNK_HEIGHT;
|
const v1 = (y + 1) / CHUNK_HEIGHT;
|
||||||
const [sw, se, ne, nw] = corners;
|
const [sw, se, ne, nw] = corners;
|
||||||
|
|
||||||
pushTileQuad(out, u0, u1, v0, v1, x, y, z + sw, z + se, z + ne, z + nw);
|
pushTileQuad(
|
||||||
|
out, u0, u1, v0, v1, x, y,
|
||||||
|
(z + sw) * WORLD_LAYER_HEIGHT,
|
||||||
|
(z + se) * WORLD_LAYER_HEIGHT,
|
||||||
|
(z + ne) * WORLD_LAYER_HEIGHT,
|
||||||
|
(z + nw) * WORLD_LAYER_HEIGHT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Float32Array(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Four edges (SW-SE, SE-NE, NE-NW, NW-SW) of a tile's quad as line
|
||||||
|
// segments, lifted slightly above the terrain surface to avoid
|
||||||
|
// z-fighting - same corner-height convention as pushTileQuad().
|
||||||
|
function pushTileGridLines(out, fx, fy, swZ, seZ, neZ, nwZ) {
|
||||||
|
const eps = 0.01;
|
||||||
|
const sw = [fx, fy, swZ + eps];
|
||||||
|
const se = [fx + 1, fy, seZ + eps];
|
||||||
|
const ne = [fx + 1, fy + 1, neZ + eps];
|
||||||
|
const nw = [fx, fy + 1, nwZ + eps];
|
||||||
|
const edges = [sw, se, se, ne, ne, nw, nw, sw];
|
||||||
|
for(const [x, y, z] of edges) out.push(0, 0, x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate grid-outline line segments for every non-null tile, following
|
||||||
|
// the same per-corner heights as buildTerrainVerts() so lines hug ramps
|
||||||
|
// instead of cutting through them. Returns a Float32Array of interleaved
|
||||||
|
// [u, v, x, y, z] vertices meant to be drawn with gl.LINES (pairs).
|
||||||
|
function buildGridLines(tiles) {
|
||||||
|
const out = [];
|
||||||
|
for(let z = 0; z < CHUNK_DEPTH; z++) {
|
||||||
|
for(let y = 0; y < CHUNK_HEIGHT; y++) {
|
||||||
|
for(let x = 0; x < CHUNK_WIDTH; x++) {
|
||||||
|
const type = tiles[tileIndex(x, y, z)] || TILE_SHAPE_NULL;
|
||||||
|
const corners = RAMP_CORNERS[type];
|
||||||
|
if(!corners) continue;
|
||||||
|
|
||||||
|
const [sw, se, ne, nw] = corners;
|
||||||
|
pushTileGridLines(
|
||||||
|
out, x, y,
|
||||||
|
(z + sw) * WORLD_LAYER_HEIGHT,
|
||||||
|
(z + se) * WORLD_LAYER_HEIGHT,
|
||||||
|
(z + ne) * WORLD_LAYER_HEIGHT,
|
||||||
|
(z + nw) * WORLD_LAYER_HEIGHT
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +146,7 @@ const ChunkTerrain = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
CHUNK_WIDTH, CHUNK_HEIGHT, CHUNK_DEPTH, TILE_COUNT,
|
CHUNK_WIDTH, CHUNK_HEIGHT, CHUNK_DEPTH, TILE_COUNT, WORLD_LAYER_HEIGHT,
|
||||||
TILE_SHAPE_NULL, TILE_SHAPE_GROUND,
|
TILE_SHAPE_NULL, TILE_SHAPE_GROUND,
|
||||||
TILE_SHAPE_RAMP_NORTH, TILE_SHAPE_RAMP_EAST,
|
TILE_SHAPE_RAMP_NORTH, TILE_SHAPE_RAMP_EAST,
|
||||||
TILE_SHAPE_RAMP_SOUTH, TILE_SHAPE_RAMP_WEST,
|
TILE_SHAPE_RAMP_SOUTH, TILE_SHAPE_RAMP_WEST,
|
||||||
@@ -104,6 +154,6 @@ const ChunkTerrain = (() => {
|
|||||||
TILE_SHAPE_RAMP_SOUTHEAST, TILE_SHAPE_RAMP_SOUTHWEST,
|
TILE_SHAPE_RAMP_SOUTHEAST, TILE_SHAPE_RAMP_SOUTHWEST,
|
||||||
TILE_SHAPE_RAMP_NORTHEAST_INNER, TILE_SHAPE_RAMP_NORTHWEST_INNER,
|
TILE_SHAPE_RAMP_NORTHEAST_INNER, TILE_SHAPE_RAMP_NORTHWEST_INNER,
|
||||||
TILE_SHAPE_RAMP_SOUTHEAST_INNER, TILE_SHAPE_RAMP_SOUTHWEST_INNER,
|
TILE_SHAPE_RAMP_SOUTHEAST_INNER, TILE_SHAPE_RAMP_SOUTHWEST_INNER,
|
||||||
RAMP_CORNERS, tileIndex, buildTerrainVerts,
|
RAMP_CORNERS, tileIndex, buildTerrainVerts, buildGridLines,
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
max-height: 512px;
|
max-height: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-swatch {
|
.tile-swatch {
|
||||||
|
|||||||
@@ -41,16 +41,15 @@
|
|||||||
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER]: { dx: -1, dy: 1 },
|
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER]: { dx: -1, dy: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draws an arrow centered at (cx, cy) pointing toward the ramp's high
|
const INNER_CORNER_SHAPES = new Set([
|
||||||
// side, so ramp direction is visible at a glance on both the palette
|
ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER,
|
||||||
// swatches and the tile grid.
|
ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER,
|
||||||
function drawArrow(ctx, cx, cy, size, dx, dy) {
|
ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER,
|
||||||
const mag = Math.hypot(dx, dy) || 1;
|
ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER,
|
||||||
const ux = dx / mag, uy = dy / mag;
|
]);
|
||||||
const len = size * 0.32;
|
|
||||||
const tipX = cx + ux * len, tipY = cy + uy * len;
|
|
||||||
const tailX = cx - ux * len, tailY = cy - uy * len;
|
|
||||||
|
|
||||||
|
// Draws a single-headed arrow from (tailX, tailY) to (tipX, tipY).
|
||||||
|
function drawArrowSegment(ctx, tailX, tailY, tipX, tipY, size) {
|
||||||
ctx.strokeStyle = "#ffffff";
|
ctx.strokeStyle = "#ffffff";
|
||||||
ctx.fillStyle = "#ffffff";
|
ctx.fillStyle = "#ffffff";
|
||||||
ctx.lineWidth = Math.max(1, size * 0.08);
|
ctx.lineWidth = Math.max(1, size * 0.08);
|
||||||
@@ -62,7 +61,7 @@
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
const headLen = size * 0.18;
|
const headLen = size * 0.18;
|
||||||
const angle = Math.atan2(uy, ux);
|
const angle = Math.atan2(tipY - tailY, tipX - tailX);
|
||||||
const leftAngle = angle + Math.PI * 0.8;
|
const leftAngle = angle + Math.PI * 0.8;
|
||||||
const rightAngle = angle - Math.PI * 0.8;
|
const rightAngle = angle - Math.PI * 0.8;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -73,6 +72,54 @@
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draws an arrow centered at (cx, cy) pointing toward the ramp's high
|
||||||
|
// side, so ramp direction is visible at a glance on both the palette
|
||||||
|
// swatches and the tile grid. Used for cardinal ramps and outer-corner
|
||||||
|
// ramps (a single raised corner).
|
||||||
|
function drawArrow(ctx, cx, cy, size, dx, dy) {
|
||||||
|
const mag = Math.hypot(dx, dy) || 1;
|
||||||
|
const ux = dx / mag, uy = dy / mag;
|
||||||
|
const len = size * 0.32;
|
||||||
|
drawArrowSegment(ctx, cx - ux * len, cy - uy * len, cx + ux * len, cy + uy * len, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws a plain right-angle bracket - two line segments meeting at 90
|
||||||
|
// degrees at the tile's corner, one running along each of the two edges
|
||||||
|
// adjacent to it - so inner-corner ramps (three corners raised, one
|
||||||
|
// dropped) read as "the adjacent sides meeting in a 90-degree corner",
|
||||||
|
// distinct from the single diagonal arrow used for outer-corner ramps.
|
||||||
|
function drawCornerBracket(ctx, cx, cy, size, dx, dy) {
|
||||||
|
const cornerDist = size * 0.42;
|
||||||
|
const armLen = size * 0.34;
|
||||||
|
const cornerX = cx + dx * cornerDist;
|
||||||
|
const cornerY = cy + dy * cornerDist;
|
||||||
|
|
||||||
|
ctx.strokeStyle = "#ffffff";
|
||||||
|
ctx.lineWidth = Math.max(1, size * 0.08);
|
||||||
|
ctx.lineCap = "round";
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(cornerX - dx * armLen, cornerY);
|
||||||
|
ctx.lineTo(cornerX, cornerY);
|
||||||
|
ctx.lineTo(cornerX, cornerY - dy * armLen);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws the direction icon for a ramp tile `type` in a `size`x`size` area
|
||||||
|
// centered at (cx, cy): a diagonal arrow for cardinal/outer-corner ramps,
|
||||||
|
// or a right-angle bracket for inner-corner ramps. No-op for shapes with
|
||||||
|
// no direction (ground, erase).
|
||||||
|
function drawShapeIcon(ctx, cx, cy, size, type) {
|
||||||
|
const dir = SHAPE_DIRECTIONS[type];
|
||||||
|
if(!dir) return;
|
||||||
|
if(INNER_CORNER_SHAPES.has(type)) {
|
||||||
|
drawCornerBracket(ctx, cx, cy, size, dir.dx, dir.dy);
|
||||||
|
} else {
|
||||||
|
drawArrow(ctx, cx, cy, size, dir.dx, dir.dy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draws a small pencil icon centered in a `size`x`size` canvas.
|
// Draws a small pencil icon centered in a `size`x`size` canvas.
|
||||||
function drawPencilIcon(ctx, size) {
|
function drawPencilIcon(ctx, size) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
@@ -146,6 +193,7 @@
|
|||||||
|
|
||||||
let mapRenderer = null;
|
let mapRenderer = null;
|
||||||
let terrainMesh = null;
|
let terrainMesh = null;
|
||||||
|
let gridLinesMesh = null;
|
||||||
let terrainTexturePromise = null;
|
let terrainTexturePromise = null;
|
||||||
let modelIndex = null;
|
let modelIndex = null;
|
||||||
let neighborChunks = [];
|
let neighborChunks = [];
|
||||||
@@ -262,6 +310,13 @@
|
|||||||
return modelIndex;
|
return modelIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scales a mesh's stored [x, y, z] offset for preview, matching the
|
||||||
|
// Z scaling mapChunkLoaded() applies at runtime (chunk.meshOffsets[m][2]
|
||||||
|
// * WORLD_LAYER_HEIGHT) - x/y stay 1:1 since only Z is height-scaled.
|
||||||
|
function scaledMeshOffset(pos) {
|
||||||
|
return [pos[0], pos[1], pos[2] * ChunkTerrain.WORLD_LAYER_HEIGHT];
|
||||||
|
}
|
||||||
|
|
||||||
// Resolves a chunk mesh entry's bare filename (e.g. "house_5_3.dmf") to
|
// Resolves a chunk mesh entry's bare filename (e.g. "house_5_3.dmf") to
|
||||||
// its model JSON by basename, mirroring find_model() in
|
// its model JSON by basename, mirroring find_model() in
|
||||||
// tools/asset/chunk/__main__.py - the chunk JSON only ever stores a bare
|
// tools/asset/chunk/__main__.py - the chunk JSON only ever stores a bare
|
||||||
@@ -330,7 +385,7 @@
|
|||||||
for(const m of neighborMeshes) {
|
for(const m of neighborMeshes) {
|
||||||
try {
|
try {
|
||||||
const resolved = await resolveModelForMeshFile(m.file);
|
const resolved = await resolveModelForMeshFile(m.file);
|
||||||
if(resolved) models.push({ ...resolved, offset: m.pos });
|
if(resolved) models.push({ ...resolved, offset: scaledMeshOffset(m.pos) });
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.warn("Failed to load neighbor mesh preview", m.file, e);
|
console.warn("Failed to load neighbor mesh preview", m.file, e);
|
||||||
}
|
}
|
||||||
@@ -426,13 +481,12 @@
|
|||||||
buildPalette();
|
buildPalette();
|
||||||
});
|
});
|
||||||
|
|
||||||
const dir = SHAPE_DIRECTIONS[s.type];
|
if(SHAPE_DIRECTIONS[s.type]) {
|
||||||
if(dir) {
|
|
||||||
const icon = document.createElement("canvas");
|
const icon = document.createElement("canvas");
|
||||||
icon.width = 32;
|
icon.width = 32;
|
||||||
icon.height = 32;
|
icon.height = 32;
|
||||||
icon.className = "tile-swatch-icon";
|
icon.className = "tile-swatch-icon";
|
||||||
drawArrow(icon.getContext("2d"), 16, 16, 32, dir.dx, dir.dy);
|
drawShapeIcon(icon.getContext("2d"), 16, 16, 32, s.type);
|
||||||
btn.appendChild(icon);
|
btn.appendChild(icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,11 +543,10 @@
|
|||||||
const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - y) * cell;
|
const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - y) * cell;
|
||||||
ctx.fillStyle = SHAPE_COLORS[type] || "#15171b";
|
ctx.fillStyle = SHAPE_COLORS[type] || "#15171b";
|
||||||
ctx.fillRect(x * cell, sy, cell, cell);
|
ctx.fillRect(x * cell, sy, cell, cell);
|
||||||
ctx.strokeStyle = "rgba(255, 255, 255, 0.08)";
|
ctx.strokeStyle = "rgba(255, 255, 255, 0.25)";
|
||||||
ctx.strokeRect(x * cell, sy, cell, cell);
|
ctx.strokeRect(x * cell, sy, cell, cell);
|
||||||
|
|
||||||
const dir = SHAPE_DIRECTIONS[type];
|
drawShapeIcon(ctx, x * cell + cell / 2, sy + cell / 2, cell, type);
|
||||||
if(dir) drawArrow(ctx, x * cell + cell / 2, sy + cell / 2, cell, dir.dx, dir.dy);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,6 +645,9 @@
|
|||||||
function rebuildTerrainMesh() {
|
function rebuildTerrainMesh() {
|
||||||
const floats = ChunkTerrain.buildTerrainVerts(tiles);
|
const floats = ChunkTerrain.buildTerrainVerts(tiles);
|
||||||
terrainMesh = floats.length ? mapRenderer.createMesh(floats) : null;
|
terrainMesh = floats.length ? mapRenderer.createMesh(floats) : null;
|
||||||
|
|
||||||
|
const lineFloats = ChunkTerrain.buildGridLines(tiles);
|
||||||
|
gridLinesMesh = lineFloats.length ? mapRenderer.createMesh(lineFloats) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keeps the preview canvas's backing-store resolution matched to its
|
// Keeps the preview canvas's backing-store resolution matched to its
|
||||||
@@ -619,7 +675,7 @@
|
|||||||
for(const m of meshes) {
|
for(const m of meshes) {
|
||||||
try {
|
try {
|
||||||
const resolved = await resolveModelForMeshFile(m.file);
|
const resolved = await resolveModelForMeshFile(m.file);
|
||||||
if(resolved) models.push({ ...resolved, offset: m.pos });
|
if(resolved) models.push({ ...resolved, offset: scaledMeshOffset(m.pos) });
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.warn("Failed to load mesh preview", m.file, e);
|
console.warn("Failed to load mesh preview", m.file, e);
|
||||||
}
|
}
|
||||||
@@ -637,13 +693,15 @@
|
|||||||
models: n.models,
|
models: n.models,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const zLevelHeight = currentZLevel * ChunkTerrain.WORLD_LAYER_HEIGHT;
|
||||||
mapRenderer.render({
|
mapRenderer.render({
|
||||||
target: [ChunkTerrain.CHUNK_WIDTH / 2, ChunkTerrain.CHUNK_HEIGHT / 2, currentZLevel],
|
target: [ChunkTerrain.CHUNK_WIDTH / 2, ChunkTerrain.CHUNK_HEIGHT / 2, zLevelHeight],
|
||||||
worldH: zoomWorldH,
|
worldH: zoomWorldH,
|
||||||
terrain: terrainMesh ? { mesh: terrainMesh, texture: terrainTexture } : null,
|
terrain: terrainMesh ? { mesh: terrainMesh, texture: terrainTexture } : null,
|
||||||
|
gridLines: gridLinesMesh ? { mesh: gridLinesMesh } : null,
|
||||||
models,
|
models,
|
||||||
neighbors,
|
neighbors,
|
||||||
highlight: hoverTile ? { x: hoverTile.x, y: hoverTile.y, z: currentZLevel } : null,
|
highlight: hoverTile ? { x: hoverTile.x, y: hoverTile.y, z: zLevelHeight } : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ const MapRenderer = (() => {
|
|||||||
// scene = {
|
// scene = {
|
||||||
// target: [x, y, z], worldH: number,
|
// target: [x, y, z], worldH: number,
|
||||||
// terrain: { mesh, texture } | null,
|
// terrain: { mesh, texture } | null,
|
||||||
|
// gridLines: { mesh } | null,
|
||||||
// models: [{ mesh, texture, color: [r,g,b,a in 0..1], offset: [x,y,z] }],
|
// models: [{ mesh, texture, color: [r,g,b,a in 0..1], offset: [x,y,z] }],
|
||||||
// neighbors: [{
|
// neighbors: [{
|
||||||
// offset: [x,y,z],
|
// offset: [x,y,z],
|
||||||
@@ -248,6 +249,10 @@ const MapRenderer = (() => {
|
|||||||
drawMesh(scene.terrain.mesh, identity, scene.terrain.texture, [1, 1, 1, 1], true);
|
drawMesh(scene.terrain.mesh, identity, scene.terrain.texture, [1, 1, 1, 1], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(scene.gridLines) {
|
||||||
|
drawMesh(scene.gridLines.mesh, identity, null, [0, 0, 0, 0.35], false, gl.LINES);
|
||||||
|
}
|
||||||
|
|
||||||
for(const model of scene.models) {
|
for(const model of scene.models) {
|
||||||
const modelMatrix = mat4Translation(new Float32Array(16), model.offset);
|
const modelMatrix = mat4Translation(new Float32Array(16), model.offset);
|
||||||
drawMesh(model.mesh, modelMatrix, model.texture, model.color, true);
|
drawMesh(model.mesh, modelMatrix, model.texture, model.color, true);
|
||||||
|
|||||||
+48
-57
@@ -59,20 +59,28 @@ assetentry_t * assetGetEntry(
|
|||||||
entry++;
|
entry++;
|
||||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
||||||
|
|
||||||
// We did not find one existing, Find first available slot.
|
// We did not find one existing. Find first available slot, reaping
|
||||||
entry = ASSET.entries;
|
// zero-ref entries to make room if none are immediately available.
|
||||||
do {
|
bool_t reaped = false;
|
||||||
if(entry->type != ASSET_LOADER_TYPE_NULL) {
|
for(;;) {
|
||||||
entry++;
|
entry = ASSET.entries;
|
||||||
continue;
|
do {
|
||||||
}
|
if(entry->type != ASSET_LOADER_TYPE_NULL) {
|
||||||
|
entry++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
|
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
|
||||||
assetEntryInit(entry, name, type, input);
|
assetEntryInit(entry, name, type, input);
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
entry++;
|
entry++;
|
||||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
||||||
|
|
||||||
|
if(reaped) break;
|
||||||
|
reaped = true;
|
||||||
|
errorCatch(assetReapUnused());
|
||||||
|
}
|
||||||
|
|
||||||
assertUnreachable("No available asset entry slots.");
|
assertUnreachable("No available asset entry slots.");
|
||||||
return NULL;
|
return NULL;
|
||||||
@@ -191,6 +199,32 @@ void assetUnlockEntry(assetentry_t *entry) {
|
|||||||
assetEntryUnlock(entry);
|
assetEntryUnlock(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorret_t assetReapUnused(void) {
|
||||||
|
assertIsMainThread("assetReapUnused must be called from the main thread.");
|
||||||
|
|
||||||
|
// Repeatedly find and dispose zero-ref LOADED entries until none remain.
|
||||||
|
// This handles dependency chains where an entry (e.g. a model) holds refs
|
||||||
|
// on child entries (mesh, texture): dispose parents first so child ref
|
||||||
|
// counts drop to zero, then pick up the children on the next pass. Without
|
||||||
|
// this, a forward-only scan fails when a shared child entry appears before
|
||||||
|
// a parent that still holds a ref to it.
|
||||||
|
bool_t any;
|
||||||
|
do {
|
||||||
|
any = false;
|
||||||
|
assetentry_t *entry = ASSET.entries;
|
||||||
|
do {
|
||||||
|
if(entry->type == ASSET_LOADER_TYPE_NULL) { entry++; continue; }
|
||||||
|
if(entry->state != ASSET_ENTRY_STATE_LOADED) { entry++; continue; }
|
||||||
|
if(entry->refs.count > 0) { entry++; continue; }
|
||||||
|
errorChain(assetEntryDispose(entry));
|
||||||
|
any = true;
|
||||||
|
entry++;
|
||||||
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
||||||
|
} while(any);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t assetUpdate(void) {
|
errorret_t assetUpdate(void) {
|
||||||
assertIsMainThread("assetUpdate must be called from the main thread.");
|
assertIsMainThread("assetUpdate must be called from the main thread.");
|
||||||
|
|
||||||
@@ -324,30 +358,6 @@ errorret_t assetUpdate(void) {
|
|||||||
}
|
}
|
||||||
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
|
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
|
||||||
|
|
||||||
|
|
||||||
// Reap unused entries.
|
|
||||||
entry = ASSET.entries;
|
|
||||||
do {
|
|
||||||
if(entry->state != ASSET_ENTRY_STATE_LOADED) {
|
|
||||||
entry++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(entry->type == ASSET_LOADER_TYPE_NULL) {
|
|
||||||
entry++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(entry->refs.count > 0) {
|
|
||||||
entry++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// consolePrint("Reaping asset %s", entry->name);
|
|
||||||
errorChain(assetEntryDispose(entry));
|
|
||||||
entry++;
|
|
||||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,26 +423,7 @@ errorret_t assetDispose(void) {
|
|||||||
assertIsMainThread("Must be called from the main thread.");
|
assertIsMainThread("Must be called from the main thread.");
|
||||||
threadStop(&ASSET.loadThread);
|
threadStop(&ASSET.loadThread);
|
||||||
|
|
||||||
// Drain-dispose: repeatedly find and dispose zero-ref LOADED entries
|
errorChain(assetReapUnused());
|
||||||
// until none remain. This handles dependency chains where an entry
|
|
||||||
// (e.g. a model) holds refs on child entries (mesh, texture): dispose
|
|
||||||
// parents first so child ref counts drop to zero, then pick up the
|
|
||||||
// children on the next pass. Without this, a forward-only scan fails
|
|
||||||
// when a shared child entry appears before a parent that still holds a
|
|
||||||
// ref to it.
|
|
||||||
bool_t any;
|
|
||||||
do {
|
|
||||||
any = false;
|
|
||||||
assetentry_t *e = ASSET.entries;
|
|
||||||
do {
|
|
||||||
if(e->type == ASSET_LOADER_TYPE_NULL) { e++; continue; }
|
|
||||||
if(e->state != ASSET_ENTRY_STATE_LOADED) { e++; continue; }
|
|
||||||
if(e->refs.count > 0) { e++; continue; }
|
|
||||||
errorChain(assetEntryDispose(e));
|
|
||||||
any = true;
|
|
||||||
e++;
|
|
||||||
} while(e < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
||||||
} while(any);
|
|
||||||
|
|
||||||
// Cleanup zip file.
|
// Cleanup zip file.
|
||||||
if(ASSET.zip != NULL) {
|
if(ASSET.zip != NULL) {
|
||||||
|
|||||||
+12
-2
@@ -23,8 +23,8 @@
|
|||||||
#define ASSET_FILE_NAME "dusk.dsk"
|
#define ASSET_FILE_NAME "dusk.dsk"
|
||||||
#define ASSET_HEADER_SIZE 3
|
#define ASSET_HEADER_SIZE 3
|
||||||
|
|
||||||
#define ASSET_LOADING_COUNT_MAX 20
|
#define ASSET_LOADING_COUNT_MAX 10
|
||||||
#define ASSET_ENTRY_COUNT_MAX 128
|
#define ASSET_ENTRY_COUNT_MAX 64
|
||||||
|
|
||||||
typedef struct asset_s {
|
typedef struct asset_s {
|
||||||
zip_t *zip;
|
zip_t *zip;
|
||||||
@@ -112,6 +112,16 @@ void assetUnlock(const char_t *name);
|
|||||||
*/
|
*/
|
||||||
void assetUnlockEntry(assetentry_t *entry);
|
void assetUnlockEntry(assetentry_t *entry);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frees every currently unreferenced (zero-ref) loaded asset entry. Repeats
|
||||||
|
* until a full pass frees nothing further, since disposing a parent entry
|
||||||
|
* (e.g. a model) may drop a child entry's (e.g. a mesh) ref count to zero,
|
||||||
|
* making it eligible for reaping too.
|
||||||
|
*
|
||||||
|
* @return An error code if any entry could not be disposed properly.
|
||||||
|
*/
|
||||||
|
errorret_t assetReapUnused(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requires an asset entry to be loaded. This will block until the asset entry
|
* Requires an asset entry to be loaded. This will block until the asset entry
|
||||||
* is fully loaded.
|
* is fully loaded.
|
||||||
|
|||||||
@@ -8,40 +8,17 @@
|
|||||||
#include "assetchunkloader.h"
|
#include "assetchunkloader.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/endian.h"
|
#include "util/string.h"
|
||||||
#include "asset/loader/assetloading.h"
|
#include "asset/loader/assetloading.h"
|
||||||
#include "asset/loader/assetentry.h"
|
#include "asset/loader/assetentry.h"
|
||||||
#include "asset/loader/assetloader.h"
|
#include "asset/loader/assetloader.h"
|
||||||
|
#include "asset/loader/json/assetjsonloader.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
|
#include "yyjson.h"
|
||||||
|
|
||||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
||||||
assertNotNull(loading, "Loading cannot be NULL");
|
assertNotNull(loading, "Loading cannot be NULL");
|
||||||
assertNotMainThread("Should be called from an async thread.");
|
assertNotMainThread("Should be called from an async thread.");
|
||||||
|
|
||||||
if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) {
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNull(loading->loading.chunk.data, "Data already defined?");
|
|
||||||
|
|
||||||
assetfile_t *file = &loading->loading.chunk.file;
|
|
||||||
assetLoaderErrorChain(loading,
|
|
||||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
|
||||||
);
|
|
||||||
|
|
||||||
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."
|
|
||||||
);
|
|
||||||
|
|
||||||
loading->loading.chunk.data = data;
|
|
||||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,12 +31,141 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
|||||||
|
|
||||||
switch(loading->loading.chunk.state) {
|
switch(loading->loading.chunk.state) {
|
||||||
case ASSET_CHUNK_LOADING_STATE_INITIAL:
|
case ASSET_CHUNK_LOADING_STATE_INITIAL:
|
||||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE;
|
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_JSON;
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|
||||||
case ASSET_CHUNK_LOADING_STATE_PARSE:
|
case ASSET_CHUNK_LOADING_STATE_LOAD_JSON: {
|
||||||
break;
|
// Lock the chunk's JSON as a sub-asset. The entry key is prefixed
|
||||||
|
// with "json:" to avoid a type-collision with the chunk entry
|
||||||
|
// itself (both share the same filename). The JSON loader reads
|
||||||
|
// the real file path supplied in the input.
|
||||||
|
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 ret = assetRequireLoaded(jsonEntry);
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||||
|
errorChain(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
|
||||||
|
|
||||||
|
yyjson_val *tilesVal = yyjson_obj_get(root, "tiles");
|
||||||
|
if(
|
||||||
|
!tilesVal || !yyjson_is_arr(tilesVal) ||
|
||||||
|
yyjson_arr_size(tilesVal) != CHUNK_TILE_COUNT
|
||||||
|
) {
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||||
|
errorThrow(
|
||||||
|
"Chunk JSON 'tiles' must have exactly %d entries",
|
||||||
|
CHUNK_TILE_COUNT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
out->tiles = memoryAllocate(CHUNK_TILE_COUNT * sizeof(tile_t));
|
||||||
|
size_t tileIdx, tileMax;
|
||||||
|
yyjson_val *tileVal;
|
||||||
|
yyjson_arr_foreach(tilesVal, tileIdx, tileMax, tileVal) {
|
||||||
|
yyjson_val *shapeVal = yyjson_arr_get(tileVal, 0);
|
||||||
|
yyjson_val *zVal = yyjson_arr_get(tileVal, 1);
|
||||||
|
if(
|
||||||
|
!yyjson_is_arr(tileVal) || yyjson_arr_size(tileVal) != 2 ||
|
||||||
|
!yyjson_is_int(shapeVal) || !yyjson_is_int(zVal)
|
||||||
|
) {
|
||||||
|
memoryFree(out->tiles);
|
||||||
|
out->tiles = NULL;
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||||
|
errorThrow("Chunk JSON tile entries must be [shape, z] arrays");
|
||||||
|
}
|
||||||
|
out->tiles[tileIdx] = (tile_t){
|
||||||
|
.shape = (tileshape_t)yyjson_get_int(shapeVal),
|
||||||
|
.z = (uint8_t)yyjson_get_int(zVal)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_val *meshesVal = yyjson_obj_get(root, "meshes");
|
||||||
|
out->meshCount = 0;
|
||||||
|
if(meshesVal && yyjson_is_arr(meshesVal)) {
|
||||||
|
size_t meshCount = yyjson_arr_size(meshesVal);
|
||||||
|
if(meshCount > CHUNK_MESH_COUNT_MAX) {
|
||||||
|
memoryFree(out->tiles);
|
||||||
|
out->tiles = NULL;
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||||
|
errorThrow(
|
||||||
|
"Chunk JSON 'meshes' exceeds CHUNK_MESH_COUNT_MAX (%d)",
|
||||||
|
CHUNK_MESH_COUNT_MAX
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t meshIdx, meshMax;
|
||||||
|
yyjson_val *meshVal;
|
||||||
|
yyjson_arr_foreach(meshesVal, meshIdx, meshMax, meshVal) {
|
||||||
|
yyjson_val *modelVal = yyjson_obj_get(meshVal, "model");
|
||||||
|
if(!modelVal || !yyjson_is_str(modelVal)) {
|
||||||
|
memoryFree(out->tiles);
|
||||||
|
out->tiles = NULL;
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||||
|
errorThrow("Chunk JSON mesh entry missing 'model' string");
|
||||||
|
}
|
||||||
|
const char_t *modelStr = yyjson_get_str(modelVal);
|
||||||
|
size_t modelLen = yyjson_get_len(modelVal);
|
||||||
|
if(modelLen >= CHUNK_MESH_NAME_MAX) {
|
||||||
|
memoryFree(out->tiles);
|
||||||
|
out->tiles = NULL;
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||||
|
errorThrow(
|
||||||
|
"Chunk JSON model path '%s' exceeds max length", modelStr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
memoryCopy(out->modelNames[meshIdx], modelStr, modelLen + 1);
|
||||||
|
|
||||||
|
vec3 offset = { 0.0f, 0.0f, 0.0f };
|
||||||
|
yyjson_val *offsetVal = yyjson_obj_get(meshVal, "offset");
|
||||||
|
if(
|
||||||
|
offsetVal && yyjson_is_arr(offsetVal) &&
|
||||||
|
yyjson_arr_size(offsetVal) == 3
|
||||||
|
) {
|
||||||
|
size_t offIdx, offMax;
|
||||||
|
yyjson_val *offElem;
|
||||||
|
yyjson_arr_foreach(offsetVal, offIdx, offMax, offElem) {
|
||||||
|
if(yyjson_is_num(offElem)) {
|
||||||
|
offset[offIdx] = (float_t)yyjson_get_num(offElem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glm_vec3_copy(offset, out->meshOffsets[meshIdx]);
|
||||||
|
}
|
||||||
|
out->meshCount = (uint8_t)meshCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
|
||||||
|
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->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS:
|
case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS:
|
||||||
while(loading->loading.chunk.modelIndex < out->meshCount) {
|
while(loading->loading.chunk.modelIndex < out->meshCount) {
|
||||||
@@ -91,64 +197,6 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
|||||||
default:
|
default:
|
||||||
errorOk();
|
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);
|
|
||||||
assetLoaderErrorThrow(
|
|
||||||
loading, "Unsupported chunk file version %u", version
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t offset = 8;
|
|
||||||
|
|
||||||
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
|
|
||||||
memoryCopy(out->tiles, data + offset, tileSize);
|
|
||||||
offset += tileSize;
|
|
||||||
|
|
||||||
out->meshCount = data[offset];
|
|
||||||
offset += sizeof(uint8_t);
|
|
||||||
assertTrue(
|
|
||||||
out->meshCount <= CHUNK_MESH_COUNT_MAX,
|
|
||||||
"Chunk mesh count exceeds maximum."
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3));
|
|
||||||
offset += sizeof(vec3);
|
|
||||||
}
|
|
||||||
|
|
||||||
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->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
||||||
errorOk();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t assetChunkDispose(assetentry_t *entry) {
|
errorret_t assetChunkDispose(assetentry_t *entry) {
|
||||||
@@ -157,6 +205,12 @@ errorret_t assetChunkDispose(assetentry_t *entry) {
|
|||||||
assertIsMainThread("Must be called from the main thread.");
|
assertIsMainThread("Must be called from the main thread.");
|
||||||
|
|
||||||
assetchunkoutput_t *out = &entry->data.chunk;
|
assetchunkoutput_t *out = &entry->data.chunk;
|
||||||
|
|
||||||
|
if(out->tiles != NULL) {
|
||||||
|
memoryFree(out->tiles);
|
||||||
|
out->tiles = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
for(uint8_t m = 0; m < out->meshCount; m++) {
|
||||||
if(out->modelEntries[m] == NULL) continue;
|
if(out->modelEntries[m] == NULL) continue;
|
||||||
assetUnlockEntry(out->modelEntries[m]);
|
assetUnlockEntry(out->modelEntries[m]);
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
#include "asset/assetfile.h"
|
#include "asset/assetfile.h"
|
||||||
#include "rpg/overworld/chunk.h"
|
#include "rpg/overworld/chunk.h"
|
||||||
|
|
||||||
#define ASSET_CHUNK_FILE_VERSION 3
|
|
||||||
|
|
||||||
typedef struct assetloading_s assetloading_t;
|
typedef struct assetloading_s assetloading_t;
|
||||||
typedef struct assetentry_s assetentry_t;
|
typedef struct assetentry_s assetentry_t;
|
||||||
|
|
||||||
@@ -20,21 +18,17 @@ typedef struct {
|
|||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ASSET_CHUNK_LOADING_STATE_INITIAL,
|
ASSET_CHUNK_LOADING_STATE_INITIAL,
|
||||||
ASSET_CHUNK_LOADING_STATE_READ_FILE,
|
ASSET_CHUNK_LOADING_STATE_LOAD_JSON,
|
||||||
ASSET_CHUNK_LOADING_STATE_PARSE,
|
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS
|
||||||
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS,
|
|
||||||
ASSET_CHUNK_LOADING_STATE_DONE
|
|
||||||
} assetchunkloadingstate_t;
|
} assetchunkloadingstate_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
assetfile_t file;
|
|
||||||
assetchunkloadingstate_t state;
|
assetchunkloadingstate_t state;
|
||||||
uint8_t *data;
|
|
||||||
uint8_t modelIndex;
|
uint8_t modelIndex;
|
||||||
} assetchunkloaderloading_t;
|
} assetchunkloaderloading_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
tile_t tiles[CHUNK_TILE_COUNT];
|
tile_t *tiles;
|
||||||
uint8_t meshCount;
|
uint8_t meshCount;
|
||||||
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
||||||
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
||||||
@@ -42,9 +36,9 @@ typedef struct {
|
|||||||
} assetchunkoutput_t;
|
} assetchunkoutput_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronous loader for chunk assets. Reads the raw DCF file bytes into
|
* Asynchronous loader for chunk assets. No-op - the chunk's JSON file is
|
||||||
* the loading buffer so the sync phase can parse without blocking the
|
* loaded via a JSON sub-asset in the sync phase (see assetChunkLoaderSync),
|
||||||
* main thread on I/O.
|
* which handles its own async file I/O.
|
||||||
*
|
*
|
||||||
* @param loading Loading information for the asset being loaded.
|
* @param loading Loading information for the asset being loaded.
|
||||||
* @return Error code indicating success or failure of the load operation.
|
* @return Error code indicating success or failure of the load operation.
|
||||||
@@ -52,9 +46,22 @@ typedef struct {
|
|||||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading);
|
errorret_t assetChunkLoaderAsync(assetloading_t *loading);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronous loader for chunk assets. Validates the DCF binary previously
|
* Synchronous loader for chunk assets. Locks and parses the chunk's JSON
|
||||||
* read by the async phase and populates the output assetchunkoutput_t with
|
* file (tiles plus referenced model paths/offsets), then locks each
|
||||||
* tile data and model paths.
|
* referenced model asset before marking the entry loaded.
|
||||||
|
*
|
||||||
|
* Expected JSON shape:
|
||||||
|
* {
|
||||||
|
* "tiles": [ [shape, z], ... ] // exactly CHUNK_TILE_COUNT entries,
|
||||||
|
* // one per (x, y) column, x-major
|
||||||
|
* "meshes": [
|
||||||
|
* { "model": "models/chunks/chunk_0_0_0_0.json",
|
||||||
|
* "offset": [0.0, 0.0, 0.0] }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* "meshes" is optional; each entry's "offset" defaults to [0, 0, 0] if
|
||||||
|
* omitted. By convention mesh index 0 is the chunk's terrain and the
|
||||||
|
* rest are props (see sceneOverworldDrawChunksBase/Props).
|
||||||
*
|
*
|
||||||
* @param loading Loading information for the asset being loaded.
|
* @param loading Loading information for the asset being loaded.
|
||||||
* @return Error code indicating success or failure of the load operation.
|
* @return Error code indicating success or failure of the load operation.
|
||||||
|
|||||||
@@ -48,8 +48,20 @@ errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
|
|||||||
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8));
|
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8));
|
||||||
meshvertex_t *vertices = NULL;
|
meshvertex_t *vertices = NULL;
|
||||||
if(vertCount > 0) {
|
if(vertCount > 0) {
|
||||||
vertices = memoryAllocate(vertCount * sizeof(meshvertex_t));
|
// 32-byte (cache-line) aligned: GX_SetArray + DCFlushRange on Dolphin
|
||||||
|
// require this for the DMA'd vertex data to actually reach the GPU
|
||||||
|
// coherently. Static compiled-in vertex arrays happen to get this from
|
||||||
|
// the linker; a plain memoryAllocate here would not.
|
||||||
|
vertices = memoryAlign(32, vertCount * sizeof(meshvertex_t));
|
||||||
memoryCopy(vertices, raw + 12, vertCount * sizeof(meshvertex_t));
|
memoryCopy(vertices, raw + 12, vertCount * sizeof(meshvertex_t));
|
||||||
|
|
||||||
|
for(uint32_t v = 0; v < vertCount; v++) {
|
||||||
|
vertices[v].uv[0] = endianLittleToHostFloat(vertices[v].uv[0]);
|
||||||
|
vertices[v].uv[1] = endianLittleToHostFloat(vertices[v].uv[1]);
|
||||||
|
vertices[v].pos[0] = endianLittleToHostFloat(vertices[v].pos[0]);
|
||||||
|
vertices[v].pos[1] = endianLittleToHostFloat(vertices[v].pos[1]);
|
||||||
|
vertices[v].pos[2] = endianLittleToHostFloat(vertices[v].pos[2]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
memoryFree(raw);
|
memoryFree(raw);
|
||||||
|
|
||||||
@@ -103,18 +115,22 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
|
|||||||
out->vertices = NULL;
|
out->vertices = NULL;
|
||||||
errorChain(ret);
|
errorChain(ret);
|
||||||
}
|
}
|
||||||
|
out->meshInitialized = true;
|
||||||
|
|
||||||
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
|
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
|
||||||
if(errorIsNotOk(ret)) {
|
if(errorIsNotOk(ret)) {
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||||
meshDispose(&out->mesh);
|
meshDispose(&out->mesh);
|
||||||
|
out->meshInitialized = false;
|
||||||
memoryFree(out->vertices);
|
memoryFree(out->vertices);
|
||||||
out->vertices = NULL;
|
out->vertices = NULL;
|
||||||
errorChain(ret);
|
errorChain(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef DUSK_OPENGL_LEGACY
|
#if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
|
||||||
// VBO owns the data now; CPU copy is no longer needed.
|
// VBO owns the data now; CPU copy is no longer needed. The platform
|
||||||
|
// mesh object itself still needs meshDispose later - tracked via
|
||||||
|
// out->meshInitialized, independent of the CPU buffer's lifetime.
|
||||||
memoryFree(out->vertices);
|
memoryFree(out->vertices);
|
||||||
out->vertices = NULL;
|
out->vertices = NULL;
|
||||||
#endif
|
#endif
|
||||||
@@ -129,8 +145,11 @@ errorret_t assetMeshDispose(assetentry_t *entry) {
|
|||||||
assertIsMainThread("Must be called from the main thread.");
|
assertIsMainThread("Must be called from the main thread.");
|
||||||
|
|
||||||
assetmeshoutput_t *out = &entry->data.mesh;
|
assetmeshoutput_t *out = &entry->data.mesh;
|
||||||
if(out->vertices != NULL) {
|
if(out->meshInitialized) {
|
||||||
errorChain(meshDispose(&out->mesh));
|
errorChain(meshDispose(&out->mesh));
|
||||||
|
out->meshInitialized = false;
|
||||||
|
}
|
||||||
|
if(out->vertices != NULL) {
|
||||||
memoryFree(out->vertices);
|
memoryFree(out->vertices);
|
||||||
out->vertices = NULL;
|
out->vertices = NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ typedef struct {
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
mesh_t mesh;
|
mesh_t mesh;
|
||||||
|
bool_t meshInitialized;
|
||||||
meshvertex_t *vertices;
|
meshvertex_t *vertices;
|
||||||
} assetmeshoutput_t;
|
} assetmeshoutput_t;
|
||||||
|
|
||||||
|
|||||||
@@ -506,12 +506,17 @@ errorret_t assetLocaleGetString(
|
|||||||
sizeof(lineBuffer)
|
sizeof(lineBuffer)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Prime the reader with the first line before scanning; outBuffer holds
|
||||||
|
// uninitialized memory until the first Next() call fills it.
|
||||||
|
errorChain(assetFileLineReaderNext(&reader));
|
||||||
|
|
||||||
// Skip blanks, comments, etc and start looking for msgid's
|
// Skip blanks, comments, etc and start looking for msgid's
|
||||||
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
|
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
|
||||||
|
|
||||||
while(!reader.eof) {
|
while(true) {
|
||||||
// Is this msgid?
|
// Is this msgid?
|
||||||
if(memoryCompare(lineBuffer, "msgid", 5) != 0) {
|
if(memoryCompare(lineBuffer, "msgid", 5) != 0) {
|
||||||
|
if(reader.eof) break;
|
||||||
errorChain(assetFileLineReaderNext(&reader));
|
errorChain(assetFileLineReaderNext(&reader));
|
||||||
msgidBuffer[0] = '\0';
|
msgidBuffer[0] = '\0';
|
||||||
continue;
|
continue;
|
||||||
@@ -535,7 +540,7 @@ errorret_t assetLocaleGetString(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We are either going to see a msgstr or a msgid_plural
|
// We are either going to see a msgstr or a msgid_plural
|
||||||
while(!reader.eof) {
|
while(true) {
|
||||||
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
|
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
|
||||||
|
|
||||||
// Is msgid_plural?
|
// Is msgid_plural?
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ console_t CONSOLE;
|
|||||||
|
|
||||||
void consoleInit(void) {
|
void consoleInit(void) {
|
||||||
memoryZero(&CONSOLE, sizeof(console_t));
|
memoryZero(&CONSOLE, sizeof(console_t));
|
||||||
CONSOLE.visible = true;
|
CONSOLE.visible = false;
|
||||||
|
|
||||||
#ifdef DUSK_CONSOLE_POSIX
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
threadMutexInit(&CONSOLE.printMutex);
|
threadMutexInit(&CONSOLE.printMutex);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "input/input.h"
|
#include "input/input.h"
|
||||||
#include "locale/localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
|
#include "rpg/item/item.h"
|
||||||
#include "rpg/rpg.h"
|
#include "rpg/rpg.h"
|
||||||
#include "display/display.h"
|
#include "display/display.h"
|
||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
@@ -39,6 +40,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorChain(assetInit());
|
errorChain(assetInit());
|
||||||
// errorChain(saveInit());
|
// errorChain(saveInit());
|
||||||
errorChain(localeManagerInit());
|
errorChain(localeManagerInit());
|
||||||
|
errorChain(itemInit());
|
||||||
errorChain(displayInit());
|
errorChain(displayInit());
|
||||||
errorChain(uiInit());
|
errorChain(uiInit());
|
||||||
errorChain(rpgInit());
|
errorChain(rpgInit());
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ input_t INPUT;
|
|||||||
|
|
||||||
errorret_t inputInit(void) {
|
errorret_t inputInit(void) {
|
||||||
memoryZero(&INPUT, sizeof(input_t));
|
memoryZero(&INPUT, sizeof(input_t));
|
||||||
|
INPUT.deadzone = INPUT_DEADZONE_DEFAULT;
|
||||||
|
|
||||||
for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) {
|
for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) {
|
||||||
INPUT.actions[i].action = (inputaction_t)i;
|
INPUT.actions[i].action = (inputaction_t)i;
|
||||||
|
|||||||
@@ -12,11 +12,15 @@
|
|||||||
|
|
||||||
#define INPUT_LISTENER_PRESSED_MAX 16
|
#define INPUT_LISTENER_PRESSED_MAX 16
|
||||||
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
|
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
|
||||||
|
#define INPUT_DEADZONE_DEFAULT 0.1f
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
inputactiondata_t actions[INPUT_ACTION_COUNT];
|
inputactiondata_t actions[INPUT_ACTION_COUNT];
|
||||||
|
|
||||||
inputplatform_t platform;
|
inputplatform_t platform;
|
||||||
|
|
||||||
|
/** User-configured gamepad axis deadzone (0.0f to 1.0f). */
|
||||||
|
float_t deadzone;
|
||||||
} input_t;
|
} input_t;
|
||||||
|
|
||||||
extern input_t INPUT;
|
extern input_t INPUT;
|
||||||
|
|||||||
@@ -16,4 +16,14 @@ typedef struct {
|
|||||||
static const localeinfo_t LOCALE_EN_US = {
|
static const localeinfo_t LOCALE_EN_US = {
|
||||||
.name = "en-US",
|
.name = "en-US",
|
||||||
.file = "locale/en_US.po",
|
.file = "locale/en_US.po",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const localeinfo_t LOCALE_JP_JP = {
|
||||||
|
.name = "ja-JP",
|
||||||
|
.file = "locale/jp_JP.po",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const localeinfo_t LOCALE_ES_MX = {
|
||||||
|
.name = "es-MX",
|
||||||
|
.file = "locale/es_MX.po",
|
||||||
};
|
};
|
||||||
@@ -8,10 +8,10 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
rpg.c
|
rpg.c
|
||||||
rpgcamera.c
|
rpgcamera.c
|
||||||
rpgtextbox.c
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Subdirs
|
# Subdirs
|
||||||
|
add_subdirectory(battle)
|
||||||
add_subdirectory(cutscene)
|
add_subdirectory(cutscene)
|
||||||
add_subdirectory(entity)
|
add_subdirectory(entity)
|
||||||
add_subdirectory(overworld)
|
add_subdirectory(overworld)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 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
|
||||||
|
battle.c
|
||||||
|
battlefighter.c
|
||||||
|
party.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "battle.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.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
|
||||||
|
) {
|
||||||
|
assertTrue(encounterType < BATTLE_ENCOUNTER_COUNT, "Invalid encounter type");
|
||||||
|
|
||||||
|
BATTLE.encounterType = encounterType;
|
||||||
|
BATTLE.fleeAvailable = fleeAvailable;
|
||||||
|
BATTLE.result = BATTLE_RESULT_NONE;
|
||||||
|
BATTLE.round = 1;
|
||||||
|
BATTLE.turnIndex = 0;
|
||||||
|
battleBuildTurnOrder(true);
|
||||||
|
|
||||||
|
BATTLE.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void battleDispose(void) {
|
||||||
|
battleInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
battlefighter_t *battleGetCurrentFighter(void) {
|
||||||
|
if(!BATTLE.active) return NULL;
|
||||||
|
if(BATTLE.turnIndex >= BATTLE.turnCount) return NULL;
|
||||||
|
return &BATTLE.fighters[BATTLE.turnOrder[BATTLE.turnIndex]];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void battleNextTurn(void) {
|
||||||
|
BATTLE.turnIndex++;
|
||||||
|
if(BATTLE.turnIndex < BATTLE.turnCount) return;
|
||||||
|
|
||||||
|
BATTLE.round++;
|
||||||
|
BATTLE.turnIndex = 0;
|
||||||
|
battleBuildTurnOrder(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
battleresult_t battleCheckResult(void) {
|
||||||
|
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
|
||||||
|
|
||||||
|
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
|
||||||
|
BATTLE.result = BATTLE_RESULT_LOSS;
|
||||||
|
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
|
||||||
|
BATTLE.result = BATTLE_RESULT_WIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BATTLE.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void battlePlayerAttack(const uint8_t targetIndex) {
|
||||||
|
battlefighter_t *attacker = battleGetCurrentFighter();
|
||||||
|
if(attacker == NULL) return;
|
||||||
|
if(attacker->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
||||||
|
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
|
||||||
|
|
||||||
|
battlefighter_t *defender = &BATTLE.fighters[targetIndex];
|
||||||
|
if(!battleFighterIsAlive(defender)) return;
|
||||||
|
|
||||||
|
battleResolveAttack(attacker, defender);
|
||||||
|
battleCheckResult();
|
||||||
|
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
||||||
|
}
|
||||||
|
|
||||||
|
void battlePlayerFlee(void) {
|
||||||
|
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||||
|
if(fighter == NULL) return;
|
||||||
|
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
||||||
|
if(!BATTLE.fleeAvailable) return;
|
||||||
|
|
||||||
|
BATTLE.result = BATTLE_RESULT_FLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
void battleUpdate(void) {
|
||||||
|
if(!BATTLE.active) return;
|
||||||
|
if(BATTLE.result != BATTLE_RESULT_NONE) return;
|
||||||
|
|
||||||
|
battlefighter_t *current = battleGetCurrentFighter();
|
||||||
|
if(current == NULL) return;
|
||||||
|
|
||||||
|
if(!battleFighterIsAlive(current)) {
|
||||||
|
battleNextTurn();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(current->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
|
||||||
|
|
||||||
|
battlefighter_t *target = battleAIChooseTarget(current);
|
||||||
|
if(target != NULL) battleResolveAttack(current, target);
|
||||||
|
|
||||||
|
battleCheckResult();
|
||||||
|
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
||||||
|
}
|
||||||
|
|
||||||
|
void battleBuildTurnOrder(const bool_t applyEncounterBias) {
|
||||||
|
BATTLE.turnCount = 0;
|
||||||
|
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||||
|
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||||
|
BATTLE.turnOrder[BATTLE.turnCount++] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
|
||||||
|
for(uint8_t i = 1; i < BATTLE.turnCount; i++) {
|
||||||
|
const uint8_t key = BATTLE.turnOrder[i];
|
||||||
|
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
|
||||||
|
|
||||||
|
int8_t j = (int8_t)i - 1;
|
||||||
|
while(
|
||||||
|
j >= 0 && BATTLE.fighters[BATTLE.turnOrder[j]].stats.speed < keySpeed
|
||||||
|
) {
|
||||||
|
BATTLE.turnOrder[j + 1] = BATTLE.turnOrder[j];
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
BATTLE.turnOrder[j + 1] = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.turnCount; i++) {
|
||||||
|
if(BATTLE.fighters[BATTLE.turnOrder[i]].team != team) continue;
|
||||||
|
sorted[count++] = BATTLE.turnOrder[i];
|
||||||
|
}
|
||||||
|
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
|
||||||
|
if(BATTLE.fighters[BATTLE.turnOrder[i]].team == team) continue;
|
||||||
|
sorted[count++] = BATTLE.turnOrder[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
memoryCopy(BATTLE.turnOrder, sorted, sizeof(uint8_t) * BATTLE.turnCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
BATTLE_RESULT_NONE,
|
||||||
|
BATTLE_RESULT_WIN,
|
||||||
|
BATTLE_RESULT_LOSS,
|
||||||
|
BATTLE_RESULT_FLED,
|
||||||
|
|
||||||
|
BATTLE_RESULT_COUNT
|
||||||
|
} battleresult_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool_t active;
|
||||||
|
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
|
||||||
|
|
||||||
|
battleencountertype_t encounterType;
|
||||||
|
bool_t fleeAvailable;
|
||||||
|
battleresult_t result;
|
||||||
|
|
||||||
|
// Fighter indices (into fighters[]), sorted for the current round.
|
||||||
|
uint8_t turnOrder[BATTLE_FIGHTER_COUNT_MAX];
|
||||||
|
uint8_t turnCount;
|
||||||
|
uint8_t turnIndex;
|
||||||
|
uint16_t round;
|
||||||
|
} battle_t;
|
||||||
|
|
||||||
|
extern battle_t BATTLE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the battle system. Marks it as inactive with no fighters.
|
||||||
|
*/
|
||||||
|
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: builds the opening turn order (biased by
|
||||||
|
* encounterType for the first round only) and marks the battle active.
|
||||||
|
* Call once every fighter has been added via battleAddFighter.
|
||||||
|
*
|
||||||
|
* @param encounterType Determines the opening round's turn order.
|
||||||
|
* @param fleeAvailable Whether the party may attempt to flee this battle.
|
||||||
|
*/
|
||||||
|
void battleStart(
|
||||||
|
const battleencountertype_t encounterType,
|
||||||
|
const bool_t fleeAvailable
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the battle, clearing all fighters and marking it inactive.
|
||||||
|
*/
|
||||||
|
void battleDispose(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the fighter whose turn it currently is.
|
||||||
|
*
|
||||||
|
* @return Pointer to the active fighter, or NULL if the battle isn't
|
||||||
|
* active or has no living fighters left to act.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends the current fighter's turn and advances to the next fighter in
|
||||||
|
* the turn order, starting a new round (rebuilding turn order purely by
|
||||||
|
* speed) once every fighter in the current round has acted.
|
||||||
|
*/
|
||||||
|
void battleNextTurn(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the battle has been won or lost, updating and
|
||||||
|
* returning BATTLE.result. 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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the current fighter's attack against a target, if it is
|
||||||
|
* currently a player-controlled fighter's turn. Resolves the attack,
|
||||||
|
* checks for a battle result, and advances the turn.
|
||||||
|
*
|
||||||
|
* @param targetIndex Index into BATTLE.fighters of the target.
|
||||||
|
*/
|
||||||
|
void battlePlayerAttack(const uint8_t targetIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits a flee attempt for the current fighter's turn, if it is
|
||||||
|
* currently a player-controlled fighter's turn 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: resolves the current
|
||||||
|
* fighter's turn automatically if AI-controlled, otherwise waits for a
|
||||||
|
* player action via battlePlayerAttack/battlePlayerFlee. No-op if the
|
||||||
|
* battle isn't active or already has a result.
|
||||||
|
*/
|
||||||
|
void battleUpdate(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds BATTLE.turnOrder/turnCount 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 battleBuildTurnOrder(const bool_t applyEncounterBias);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stably partitions BATTLE.turnOrder 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 turn 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);
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* 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,71 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "party.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
party_t PARTY;
|
||||||
|
|
||||||
|
void partyInit(void) {
|
||||||
|
memoryZero(&PARTY, sizeof(party_t));
|
||||||
|
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||||
|
PARTY.members[i].id = i;
|
||||||
|
}
|
||||||
|
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||||
|
PARTY.order[i] = PARTY_ORDER_EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t partyGetAvailableMember(void) {
|
||||||
|
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||||
|
if(PARTY.members[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
battlefighter_t *member = &PARTY.members[index];
|
||||||
|
battleFighterInit(
|
||||||
|
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||||
|
stats, healthMax, mpMax
|
||||||
|
);
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||||
|
if(PARTY.order[i] != PARTY_ORDER_EMPTY) continue;
|
||||||
|
PARTY.order[i] = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
|
||||||
|
battlefighter_t *partyGetOrderMember(const uint8_t slot) {
|
||||||
|
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
|
||||||
|
|
||||||
|
const uint8_t index = PARTY.order[slot];
|
||||||
|
if(index == PARTY_ORDER_EMPTY) return NULL;
|
||||||
|
return &PARTY.members[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void partySetOrder(const uint8_t slot, const uint8_t memberIndex) {
|
||||||
|
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
|
||||||
|
assertTrue(
|
||||||
|
memberIndex == PARTY_ORDER_EMPTY || memberIndex < PARTY_MEMBER_COUNT_MAX,
|
||||||
|
"Invalid party member index"
|
||||||
|
);
|
||||||
|
|
||||||
|
PARTY.order[slot] = memberIndex;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "battlefighter.h"
|
||||||
|
|
||||||
|
#define PARTY_MEMBER_COUNT_MAX 4
|
||||||
|
#define PARTY_ACTIVE_SIZE_MAX 3
|
||||||
|
#define PARTY_ORDER_EMPTY 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.
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @return The index of an available slot, or 0xFF if the party is full.
|
||||||
|
*/
|
||||||
|
uint8_t partyGetAvailableMember(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
battlefighter_t *partyAddMember(
|
||||||
|
const battlefighterstats_t stats,
|
||||||
|
const uint16_t healthMax,
|
||||||
|
const uint16_t mpMax
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the roster member currently occupying an active order slot.
|
||||||
|
*
|
||||||
|
* @param slot The active order slot to query.
|
||||||
|
* @return Pointer to the member in that slot, or NULL if the slot is
|
||||||
|
* empty.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
cutscene.c
|
||||||
cutscenesystem.c
|
cutscenesystem.c
|
||||||
cutscenemode.c
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Subdirs
|
# Subdirs
|
||||||
|
|||||||
@@ -0,0 +1,444 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "cutscene.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "asset/asset.h"
|
||||||
|
#include "asset/loader/assetloader.h"
|
||||||
|
#include "asset/loader/assetentry.h"
|
||||||
|
#include "asset/loader/json/assetjsonloader.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "rpg/entity/entitytype.h"
|
||||||
|
#include "rpg/entity/entitydir.h"
|
||||||
|
#include "ui/rpg/uiemoji.h"
|
||||||
|
#include "yyjson.h"
|
||||||
|
|
||||||
|
static cutscenejsoncacheentry_t CUTSCENE_JSON_CACHE[CUTSCENE_JSON_CACHE_MAX];
|
||||||
|
static uint32_t CUTSCENE_JSON_CACHE_COUNT;
|
||||||
|
|
||||||
|
errorret_t cutsceneJsonParsePause(yyjson_val *val, cutscenepause_t *outPause) {
|
||||||
|
assertNotNull(outPause, "Output pause pointer cannot be NULL");
|
||||||
|
if(!val || !yyjson_is_str(val)) {
|
||||||
|
errorThrow("Cutscene pause value must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const char_t *str = yyjson_get_str(val);
|
||||||
|
if(stringEquals(str, "NONE")) {
|
||||||
|
*outPause = CUTSCENE_PAUSE_NONE;
|
||||||
|
} else if(stringEquals(str, "DEFAULT")) {
|
||||||
|
*outPause = CUTSCENE_PAUSE_DEFAULT;
|
||||||
|
} else if(stringEquals(str, "ALL")) {
|
||||||
|
*outPause = CUTSCENE_PAUSE_ALL;
|
||||||
|
} else {
|
||||||
|
errorThrow("Unknown cutscene pause value '%s'", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t cutsceneJsonParseEntityIndex(yyjson_val *val, uint8_t *outIndex) {
|
||||||
|
assertNotNull(outIndex, "Output entity index pointer cannot be NULL");
|
||||||
|
|
||||||
|
if(val && yyjson_is_str(val)) {
|
||||||
|
const char_t *str = yyjson_get_str(val);
|
||||||
|
if(stringEquals(str, "interact")) {
|
||||||
|
*outIndex = CUTSCENE_ENTITY_INTERACT;
|
||||||
|
} else if(stringEquals(str, "interacted")) {
|
||||||
|
*outIndex = CUTSCENE_ENTITY_INTERACTED;
|
||||||
|
} else if(stringEquals(str, "lastCreated")) {
|
||||||
|
*outIndex = CUTSCENE_ENTITY_LAST_CREATED;
|
||||||
|
} else {
|
||||||
|
errorThrow("Unknown entity index sentinel '%s'", str);
|
||||||
|
}
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!val || !yyjson_is_int(val)) {
|
||||||
|
errorThrow("Entity index must be a number or sentinel string");
|
||||||
|
}
|
||||||
|
*outIndex = (uint8_t)yyjson_get_int(val);
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t cutsceneJsonParseWorldPos(yyjson_val *val, worldpos_t *outPos) {
|
||||||
|
assertNotNull(outPos, "Output position pointer cannot be NULL");
|
||||||
|
if(!val || !yyjson_is_arr(val) || yyjson_arr_size(val) != 3) {
|
||||||
|
errorThrow("Position must be a [x, y, z] array");
|
||||||
|
}
|
||||||
|
|
||||||
|
worldunit_t comps[3];
|
||||||
|
size_t idx, max;
|
||||||
|
yyjson_val *elem;
|
||||||
|
yyjson_arr_foreach(val, idx, max, elem) {
|
||||||
|
if(!yyjson_is_num(elem)) {
|
||||||
|
errorThrow("Position elements must be numbers");
|
||||||
|
}
|
||||||
|
comps[idx] = (worldunit_t)yyjson_get_num(elem);
|
||||||
|
}
|
||||||
|
|
||||||
|
*outPos = (worldpos_t){ comps[0], comps[1], comps[2] };
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t cutsceneItemCreateFromJson(
|
||||||
|
yyjson_val *obj,
|
||||||
|
cutsceneitem_t *outItem,
|
||||||
|
worldpos_t *waypoints,
|
||||||
|
const uint8_t waypointsMax
|
||||||
|
) {
|
||||||
|
assertNotNull(obj, "Cutscene item JSON object cannot be NULL");
|
||||||
|
assertNotNull(outItem, "Output item pointer cannot be NULL");
|
||||||
|
|
||||||
|
yyjson_val *typeVal = yyjson_obj_get(obj, "type");
|
||||||
|
if(!typeVal || !yyjson_is_str(typeVal)) {
|
||||||
|
errorThrow("Cutscene item JSON missing 'type' string");
|
||||||
|
}
|
||||||
|
const char_t *typeStr = yyjson_get_str(typeVal);
|
||||||
|
|
||||||
|
memoryZero(outItem, sizeof(cutsceneitem_t));
|
||||||
|
|
||||||
|
if(stringEquals(typeStr, "text")) {
|
||||||
|
yyjson_val *textVal = yyjson_obj_get(obj, "text");
|
||||||
|
if(!textVal || !yyjson_is_str(textVal)) {
|
||||||
|
errorThrow("Cutscene 'text' item missing 'text' string");
|
||||||
|
}
|
||||||
|
|
||||||
|
outItem->type = CUTSCENE_ITEM_TYPE_TEXT;
|
||||||
|
stringCopy(
|
||||||
|
outItem->text.text, yyjson_get_str(textVal), CUTSCENE_TEXT_MAX_CHARS
|
||||||
|
);
|
||||||
|
} else if(stringEquals(typeStr, "textMini")) {
|
||||||
|
yyjson_val *textVal = yyjson_obj_get(obj, "text");
|
||||||
|
if(!textVal || !yyjson_is_str(textVal)) {
|
||||||
|
errorThrow("Cutscene 'textMini' item missing 'text' string");
|
||||||
|
}
|
||||||
|
|
||||||
|
worldpos_t pos;
|
||||||
|
errorChain(
|
||||||
|
cutsceneJsonParseWorldPos(yyjson_obj_get(obj, "position"), &pos)
|
||||||
|
);
|
||||||
|
|
||||||
|
yyjson_val *durVal = yyjson_obj_get(obj, "duration");
|
||||||
|
if(!durVal || !yyjson_is_num(durVal)) {
|
||||||
|
errorThrow("Cutscene 'textMini' item missing 'duration' number");
|
||||||
|
}
|
||||||
|
|
||||||
|
outItem->type = CUTSCENE_ITEM_TYPE_TEXT_MINI;
|
||||||
|
stringCopy(
|
||||||
|
outItem->textMini.text, yyjson_get_str(textVal),
|
||||||
|
CUTSCENE_TEXT_MINI_MAX_CHARS
|
||||||
|
);
|
||||||
|
outItem->textMini.position[0] = (float_t)pos.x;
|
||||||
|
outItem->textMini.position[1] = (float_t)pos.y;
|
||||||
|
outItem->textMini.position[2] = (float_t)pos.z;
|
||||||
|
outItem->textMini.duration = (float_t)yyjson_get_num(durVal);
|
||||||
|
} else if(stringEquals(typeStr, "textMiniHide")) {
|
||||||
|
yyjson_val *indexVal = yyjson_obj_get(obj, "index");
|
||||||
|
if(!indexVal || !yyjson_is_int(indexVal)) {
|
||||||
|
errorThrow("Cutscene 'textMiniHide' item missing 'index' number");
|
||||||
|
}
|
||||||
|
|
||||||
|
outItem->type = CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE;
|
||||||
|
outItem->textMiniHide.index = (uint8_t)yyjson_get_int(indexVal);
|
||||||
|
} else if(stringEquals(typeStr, "wait")) {
|
||||||
|
yyjson_val *durVal = yyjson_obj_get(obj, "duration");
|
||||||
|
if(!durVal || !yyjson_is_num(durVal)) {
|
||||||
|
errorThrow("Cutscene 'wait' item missing 'duration' number");
|
||||||
|
}
|
||||||
|
|
||||||
|
outItem->type = CUTSCENE_ITEM_TYPE_WAIT;
|
||||||
|
outItem->wait = (float_t)yyjson_get_num(durVal);
|
||||||
|
} else if(stringEquals(typeStr, "entityAdd")) {
|
||||||
|
yyjson_val *entityTypeVal = yyjson_obj_get(obj, "entityType");
|
||||||
|
if(!entityTypeVal || !yyjson_is_str(entityTypeVal)) {
|
||||||
|
errorThrow("Cutscene 'entityAdd' item missing 'entityType' string");
|
||||||
|
}
|
||||||
|
const char_t *entityTypeStr = yyjson_get_str(entityTypeVal);
|
||||||
|
|
||||||
|
entitytype_t entityType;
|
||||||
|
if(stringEquals(entityTypeStr, "player")) {
|
||||||
|
entityType = ENTITY_TYPE_PLAYER;
|
||||||
|
} else if(stringEquals(entityTypeStr, "npc")) {
|
||||||
|
entityType = ENTITY_TYPE_NPC;
|
||||||
|
} else if(stringEquals(entityTypeStr, "item")) {
|
||||||
|
entityType = ENTITY_TYPE_ITEM;
|
||||||
|
} else {
|
||||||
|
errorThrow(
|
||||||
|
"Cutscene 'entityAdd' has unknown entityType '%s'", entityTypeStr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
worldpos_t pos;
|
||||||
|
errorChain(
|
||||||
|
cutsceneJsonParseWorldPos(yyjson_obj_get(obj, "position"), &pos)
|
||||||
|
);
|
||||||
|
|
||||||
|
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_ADD;
|
||||||
|
outItem->entityAdd.entityType = entityType;
|
||||||
|
outItem->entityAdd.position = pos;
|
||||||
|
} else if(stringEquals(typeStr, "entityRemove")) {
|
||||||
|
uint8_t entityIndex;
|
||||||
|
errorChain(cutsceneJsonParseEntityIndex(
|
||||||
|
yyjson_obj_get(obj, "entityIndex"), &entityIndex
|
||||||
|
));
|
||||||
|
|
||||||
|
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_REMOVE;
|
||||||
|
outItem->entityRemove.entityIndex = entityIndex;
|
||||||
|
} else if(stringEquals(typeStr, "entityTurn")) {
|
||||||
|
uint8_t entityIndex;
|
||||||
|
errorChain(cutsceneJsonParseEntityIndex(
|
||||||
|
yyjson_obj_get(obj, "entityIndex"), &entityIndex
|
||||||
|
));
|
||||||
|
|
||||||
|
yyjson_val *dirVal = yyjson_obj_get(obj, "direction");
|
||||||
|
if(!dirVal || !yyjson_is_str(dirVal)) {
|
||||||
|
errorThrow("Cutscene 'entityTurn' item missing 'direction' string");
|
||||||
|
}
|
||||||
|
const char_t *dirStr = yyjson_get_str(dirVal);
|
||||||
|
|
||||||
|
entitydir_t direction;
|
||||||
|
if(stringEquals(dirStr, "north")) {
|
||||||
|
direction = ENTITY_DIR_NORTH;
|
||||||
|
} else if(stringEquals(dirStr, "east")) {
|
||||||
|
direction = ENTITY_DIR_EAST;
|
||||||
|
} else if(stringEquals(dirStr, "south")) {
|
||||||
|
direction = ENTITY_DIR_SOUTH;
|
||||||
|
} else if(stringEquals(dirStr, "west")) {
|
||||||
|
direction = ENTITY_DIR_WEST;
|
||||||
|
} else {
|
||||||
|
errorThrow("Cutscene 'entityTurn' has unknown direction '%s'", dirStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_TURN;
|
||||||
|
outItem->entityTurn.entityIndex = entityIndex;
|
||||||
|
outItem->entityTurn.direction = direction;
|
||||||
|
} else if(stringEquals(typeStr, "entityWalkTo")) {
|
||||||
|
uint8_t entityIndex;
|
||||||
|
errorChain(cutsceneJsonParseEntityIndex(
|
||||||
|
yyjson_obj_get(obj, "entityIndex"), &entityIndex
|
||||||
|
));
|
||||||
|
|
||||||
|
yyjson_val *positionsVal = yyjson_obj_get(obj, "positions");
|
||||||
|
if(!positionsVal || !yyjson_is_arr(positionsVal)) {
|
||||||
|
errorThrow("Cutscene 'entityWalkTo' item missing 'positions' array");
|
||||||
|
}
|
||||||
|
size_t count = yyjson_arr_size(positionsVal);
|
||||||
|
if(count == 0 || count > waypointsMax) {
|
||||||
|
errorThrow(
|
||||||
|
"Cutscene 'entityWalkTo' 'positions' must have 1-%d entries",
|
||||||
|
waypointsMax
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assertNotNull(waypoints, "Waypoint storage cannot be NULL");
|
||||||
|
|
||||||
|
size_t idx, max;
|
||||||
|
yyjson_val *posVal;
|
||||||
|
yyjson_arr_foreach(positionsVal, idx, max, posVal) {
|
||||||
|
errorChain(cutsceneJsonParseWorldPos(posVal, &waypoints[idx]));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t walkAround = true;
|
||||||
|
yyjson_val *walkAroundVal = yyjson_obj_get(obj, "walkAround");
|
||||||
|
if(walkAroundVal && yyjson_is_bool(walkAroundVal)) {
|
||||||
|
walkAround = yyjson_get_bool(walkAroundVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO;
|
||||||
|
outItem->entityWalkTo.entityIndex = entityIndex;
|
||||||
|
outItem->entityWalkTo.positions = waypoints;
|
||||||
|
outItem->entityWalkTo.count = (uint8_t)count;
|
||||||
|
outItem->entityWalkTo.walkAround = walkAround;
|
||||||
|
} else if(stringEquals(typeStr, "entityWalkToEntity")) {
|
||||||
|
uint8_t entityIndex, targetEntityIndex;
|
||||||
|
errorChain(cutsceneJsonParseEntityIndex(
|
||||||
|
yyjson_obj_get(obj, "entityIndex"), &entityIndex
|
||||||
|
));
|
||||||
|
errorChain(cutsceneJsonParseEntityIndex(
|
||||||
|
yyjson_obj_get(obj, "targetEntityIndex"), &targetEntityIndex
|
||||||
|
));
|
||||||
|
|
||||||
|
yyjson_val *offsetXVal = yyjson_obj_get(obj, "offsetX");
|
||||||
|
yyjson_val *offsetYVal = yyjson_obj_get(obj, "offsetY");
|
||||||
|
if(
|
||||||
|
!offsetXVal || !yyjson_is_num(offsetXVal) ||
|
||||||
|
!offsetYVal || !yyjson_is_num(offsetYVal)
|
||||||
|
) {
|
||||||
|
errorThrow(
|
||||||
|
"Cutscene 'entityWalkToEntity' item missing "
|
||||||
|
"'offsetX'/'offsetY' numbers"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY;
|
||||||
|
outItem->entityWalkToEntity.entityIndex = entityIndex;
|
||||||
|
outItem->entityWalkToEntity.targetEntityIndex = targetEntityIndex;
|
||||||
|
outItem->entityWalkToEntity.offsetX =
|
||||||
|
(worldunit_t)yyjson_get_num(offsetXVal);
|
||||||
|
outItem->entityWalkToEntity.offsetY =
|
||||||
|
(worldunit_t)yyjson_get_num(offsetYVal);
|
||||||
|
} else if(stringEquals(typeStr, "entityTeleport")) {
|
||||||
|
uint8_t entityIndex;
|
||||||
|
errorChain(cutsceneJsonParseEntityIndex(
|
||||||
|
yyjson_obj_get(obj, "entityIndex"), &entityIndex
|
||||||
|
));
|
||||||
|
|
||||||
|
worldpos_t pos;
|
||||||
|
errorChain(
|
||||||
|
cutsceneJsonParseWorldPos(yyjson_obj_get(obj, "position"), &pos)
|
||||||
|
);
|
||||||
|
|
||||||
|
outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT;
|
||||||
|
outItem->entityTeleport.entityIndex = entityIndex;
|
||||||
|
outItem->entityTeleport.target = pos;
|
||||||
|
} else if(stringEquals(typeStr, "emoji")) {
|
||||||
|
uint8_t entityIndex;
|
||||||
|
errorChain(cutsceneJsonParseEntityIndex(
|
||||||
|
yyjson_obj_get(obj, "entityIndex"), &entityIndex
|
||||||
|
));
|
||||||
|
|
||||||
|
yyjson_val *emojiTypeVal = yyjson_obj_get(obj, "emojiType");
|
||||||
|
if(!emojiTypeVal || !yyjson_is_str(emojiTypeVal)) {
|
||||||
|
errorThrow("Cutscene 'emoji' item missing 'emojiType' string");
|
||||||
|
}
|
||||||
|
const char_t *emojiTypeStr = yyjson_get_str(emojiTypeVal);
|
||||||
|
|
||||||
|
uiemojitype_t emojiType;
|
||||||
|
if(stringEquals(emojiTypeStr, "question")) {
|
||||||
|
emojiType = UI_EMOJI_QUESTION_MARK;
|
||||||
|
} else if(stringEquals(emojiTypeStr, "exclamation")) {
|
||||||
|
emojiType = UI_EMOJI_EXCLAMATION_MARK;
|
||||||
|
} else {
|
||||||
|
errorThrow(
|
||||||
|
"Cutscene 'emoji' has unknown emojiType '%s'", emojiTypeStr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_val *durVal = yyjson_obj_get(obj, "duration");
|
||||||
|
if(!durVal || !yyjson_is_num(durVal)) {
|
||||||
|
errorThrow("Cutscene 'emoji' item missing 'duration' number");
|
||||||
|
}
|
||||||
|
|
||||||
|
outItem->type = CUTSCENE_ITEM_TYPE_EMOJI;
|
||||||
|
outItem->emoji.entityIndex = entityIndex;
|
||||||
|
outItem->emoji.emojiType = emojiType;
|
||||||
|
outItem->emoji.duration = (float_t)yyjson_get_num(durVal);
|
||||||
|
} else if(stringEquals(typeStr, "shake")) {
|
||||||
|
yyjson_val *amountVal = yyjson_obj_get(obj, "amount");
|
||||||
|
yyjson_val *durVal = yyjson_obj_get(obj, "duration");
|
||||||
|
if(
|
||||||
|
!amountVal || !yyjson_is_int(amountVal) ||
|
||||||
|
!durVal || !yyjson_is_num(durVal)
|
||||||
|
) {
|
||||||
|
errorThrow("Cutscene 'shake' item missing 'amount'/'duration' numbers");
|
||||||
|
}
|
||||||
|
|
||||||
|
outItem->type = CUTSCENE_ITEM_TYPE_SHAKE;
|
||||||
|
outItem->shake.amount = (uint8_t)yyjson_get_int(amountVal);
|
||||||
|
outItem->shake.duration = (float_t)yyjson_get_num(durVal);
|
||||||
|
} else if(stringEquals(typeStr, "setPause")) {
|
||||||
|
cutscenepause_t pause;
|
||||||
|
errorChain(
|
||||||
|
cutsceneJsonParsePause(yyjson_obj_get(obj, "pause"), &pause)
|
||||||
|
);
|
||||||
|
|
||||||
|
outItem->type = CUTSCENE_ITEM_TYPE_SET_PAUSE;
|
||||||
|
outItem->setPause = pause;
|
||||||
|
} else {
|
||||||
|
errorThrow("Cutscene item JSON has unknown 'type': %s", typeStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t cutsceneGetByName(
|
||||||
|
const char_t *name, const cutscene_t **outCutscene
|
||||||
|
) {
|
||||||
|
assertNotNull(name, "Cutscene name cannot be NULL");
|
||||||
|
assertNotNull(outCutscene, "Output cutscene pointer cannot be NULL");
|
||||||
|
assertStrLenMax(name, CUTSCENE_JSON_NAME_MAX, "Cutscene name too long");
|
||||||
|
|
||||||
|
for(uint32_t i = 0; i < CUTSCENE_JSON_CACHE_COUNT; i++) {
|
||||||
|
if(!stringEquals(CUTSCENE_JSON_CACHE[i].name, name)) continue;
|
||||||
|
*outCutscene = &CUTSCENE_JSON_CACHE[i].cutscene;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(CUTSCENE_JSON_CACHE_COUNT >= CUTSCENE_JSON_CACHE_MAX) {
|
||||||
|
errorThrow(
|
||||||
|
"Too many cutscenes loaded: exceeds CUTSCENE_JSON_CACHE_MAX (%d)",
|
||||||
|
CUTSCENE_JSON_CACHE_MAX
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
char_t path[CUTSCENE_JSON_NAME_MAX + 24];
|
||||||
|
stringFormat(path, sizeof(path), "cutscene/%s.json", name);
|
||||||
|
|
||||||
|
assetentry_t *entry = assetLock(path, 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);
|
||||||
|
|
||||||
|
cutscenepause_t pause = CUTSCENE_PAUSE_DEFAULT;
|
||||||
|
yyjson_val *pauseVal = yyjson_obj_get(root, "pause");
|
||||||
|
if(pauseVal) {
|
||||||
|
errorret_t pauseRet = cutsceneJsonParsePause(pauseVal, &pause);
|
||||||
|
if(errorIsNotOk(pauseRet)) {
|
||||||
|
assetUnlockEntry(entry);
|
||||||
|
errorChain(pauseRet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_val *itemsVal = yyjson_obj_get(root, "items");
|
||||||
|
if(!itemsVal || !yyjson_is_arr(itemsVal)) {
|
||||||
|
assetUnlockEntry(entry);
|
||||||
|
errorThrow("Cutscene '%s' JSON missing 'items' array", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t itemCount = yyjson_arr_size(itemsVal);
|
||||||
|
if(itemCount == 0 || itemCount > CUTSCENE_JSON_ITEM_COUNT_MAX) {
|
||||||
|
assetUnlockEntry(entry);
|
||||||
|
errorThrow(
|
||||||
|
"Cutscene '%s' 'items' must have 1-%d entries",
|
||||||
|
name, CUTSCENE_JSON_ITEM_COUNT_MAX
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cutscenejsoncacheentry_t *cache =
|
||||||
|
&CUTSCENE_JSON_CACHE[CUTSCENE_JSON_CACHE_COUNT];
|
||||||
|
memoryZero(cache, sizeof(cutscenejsoncacheentry_t));
|
||||||
|
stringCopy(cache->name, name, CUTSCENE_JSON_NAME_MAX);
|
||||||
|
|
||||||
|
size_t idx, max;
|
||||||
|
yyjson_val *itemVal;
|
||||||
|
yyjson_arr_foreach(itemsVal, idx, max, itemVal) {
|
||||||
|
errorret_t itemRet = cutsceneItemCreateFromJson(
|
||||||
|
itemVal, &cache->items[idx], cache->waypoints[idx],
|
||||||
|
CUTSCENE_JSON_WAYPOINT_COUNT_MAX
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(itemRet)) {
|
||||||
|
assetUnlockEntry(entry);
|
||||||
|
errorChain(itemRet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assetUnlockEntry(entry);
|
||||||
|
|
||||||
|
cache->cutscene.items = cache->items;
|
||||||
|
cache->cutscene.itemCount = (uint8_t)itemCount;
|
||||||
|
cache->cutscene.pause = pause;
|
||||||
|
cache->cutscene.dataSize = 0;
|
||||||
|
|
||||||
|
CUTSCENE_JSON_CACHE_COUNT++;
|
||||||
|
*outCutscene = &cache->cutscene;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -7,8 +7,343 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenepause.h"
|
||||||
|
|
||||||
|
typedef struct yyjson_val yyjson_val;
|
||||||
|
|
||||||
typedef struct cutscene_s {
|
typedef struct cutscene_s {
|
||||||
const cutsceneitem_t *items;
|
const cutsceneitem_t *items;
|
||||||
uint8_t itemCount;
|
uint8_t itemCount;
|
||||||
} cutscene_t;
|
cutscenepause_t pause;
|
||||||
|
|
||||||
|
// Size in bytes of this cutscene's custom user data, carved out of
|
||||||
|
// CUTSCENE_SYSTEM.data while the cutscene is running.
|
||||||
|
size_t dataSize;
|
||||||
|
} cutscene_t;
|
||||||
|
|
||||||
|
#define CUTSCENE(NAME, SIZE, PAUSE_TYPE, ...) \
|
||||||
|
static const cutsceneitem_t CUTSCENE_##NAME##_ITEMS[] = { __VA_ARGS__ }; \
|
||||||
|
static const cutscene_t CUTSCENE_##NAME = { \
|
||||||
|
.items = CUTSCENE_##NAME##_ITEMS, \
|
||||||
|
.itemCount = sizeof(CUTSCENE_##NAME##_ITEMS) / sizeof(cutsceneitem_t), \
|
||||||
|
.pause = CUTSCENE_PAUSE_##PAUSE_TYPE, \
|
||||||
|
.dataSize = SIZE \
|
||||||
|
};
|
||||||
|
|
||||||
|
#define CUTSCENE_REFERENCE(CUTSCENE) \
|
||||||
|
&CUTSCENE_##CUTSCENE
|
||||||
|
|
||||||
|
#define CUTSCENE_TEXT(TEXT) \
|
||||||
|
{ .type = CUTSCENE_ITEM_TYPE_TEXT, .text = { .text = TEXT } }
|
||||||
|
|
||||||
|
#define CUTSCENE_TEXT_MINI(TEXT, X, Y, Z, DURATION) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI, \
|
||||||
|
.textMini = { \
|
||||||
|
.text = TEXT, \
|
||||||
|
.position = { X, Y, Z }, \
|
||||||
|
.duration = DURATION \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CUTSCENE_TEXT_MINI_HIDE(INDEX) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE, \
|
||||||
|
.textMiniHide = { .index = INDEX } \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CUTSCENE_WAIT(WAIT) \
|
||||||
|
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT }
|
||||||
|
|
||||||
|
#define CUTSCENE_CUTSCENE(CUTSCENE) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_CUTSCENE, \
|
||||||
|
.cutscene = CUTSCENE_REFERENCE(CUTSCENE) \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CUTSCENE_CALLBACK(CALLBACK) \
|
||||||
|
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = CALLBACK }
|
||||||
|
|
||||||
|
#define CUTSCENE_ENTITY_WALK_TO(ENTITY_INDEX, X, Y, Z) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \
|
||||||
|
.entityWalkTo = { \
|
||||||
|
.entityIndex = ENTITY_INDEX, \
|
||||||
|
.positions = (const worldpos_t[]){ { X, Y, Z } }, \
|
||||||
|
.count = 1, \
|
||||||
|
.walkAround = true \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CUTSCENE_ENTITY_WALK_PATH(NAME, ENTITY_INDEX, ...) \
|
||||||
|
static const worldpos_t CUTSCENE_##NAME##_POSITIONS[] = { __VA_ARGS__ }; \
|
||||||
|
static const cutsceneitem_t CUTSCENE_##NAME = { \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \
|
||||||
|
.entityWalkTo = { \
|
||||||
|
.entityIndex = ENTITY_INDEX, \
|
||||||
|
.positions = CUTSCENE_##NAME##_POSITIONS, \
|
||||||
|
.count = sizeof(CUTSCENE_##NAME##_POSITIONS) / sizeof(worldpos_t), \
|
||||||
|
.walkAround = true \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CUTSCENE_ENTITY_REMOVE(ENTITY_INDEX) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_ENTITY_REMOVE, \
|
||||||
|
.entityRemove = { .entityIndex = ENTITY_INDEX } \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CUTSCENE_ENTITY_ADD(TYPE, X, Y, Z) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_ENTITY_ADD, \
|
||||||
|
.entityAdd = { .entityType = TYPE, .position = { X, Y, Z } } \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CUTSCENE_ENTITY_TURN(ENTITY_INDEX, DIRECTION) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_ENTITY_TURN, \
|
||||||
|
.entityTurn = { .entityIndex = ENTITY_INDEX, .direction = DIRECTION } \
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walks ENTITY_INDEX to stand beside TARGET_ENTITY_INDEX, offset by
|
||||||
|
// (OFFSET_X, OFFSET_Y) on the 2D plane. The destination Z is resolved
|
||||||
|
// from nearby terrain each frame, so ramps between the two entities are
|
||||||
|
// accounted for automatically.
|
||||||
|
#define CUTSCENE_ENTITY_WALK_TO_ENTITY( \
|
||||||
|
ENTITY_INDEX, TARGET_ENTITY_INDEX, OFFSET_X, OFFSET_Y \
|
||||||
|
) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY, \
|
||||||
|
.entityWalkToEntity = { \
|
||||||
|
.entityIndex = ENTITY_INDEX, \
|
||||||
|
.targetEntityIndex = TARGET_ENTITY_INDEX, \
|
||||||
|
.offsetX = OFFSET_X, \
|
||||||
|
.offsetY = OFFSET_Y \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CUTSCENE_ENTITY_TELEPORT(ENTITY_INDEX, X, Y, Z) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT, \
|
||||||
|
.entityTeleport = { .entityIndex = ENTITY_INDEX, .target = { X, Y, Z } } \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CUTSCENE_FADE(FROM, TO, DURATION, EASING) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_FADE, \
|
||||||
|
.fade = { .from = FROM, .to = TO, .duration = DURATION, .easing = EASING } \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CUTSCENE_FADE_TO_BLACK(DURATION) \
|
||||||
|
CUTSCENE_FADE(COLOR_TRANSPARENT_BLACK, COLOR_BLACK, DURATION, EASING_LINEAR)
|
||||||
|
|
||||||
|
#define CUTSCENE_FADE_FROM_BLACK(DURATION) \
|
||||||
|
CUTSCENE_FADE(COLOR_BLACK, COLOR_TRANSPARENT_BLACK, DURATION, EASING_LINEAR)
|
||||||
|
|
||||||
|
#define CUTSCENE_FADE_TO_WHITE(DURATION) \
|
||||||
|
CUTSCENE_FADE(COLOR_TRANSPARENT_WHITE, COLOR_WHITE, DURATION, EASING_LINEAR)
|
||||||
|
|
||||||
|
#define CUTSCENE_FADE_FROM_WHITE(DURATION) \
|
||||||
|
CUTSCENE_FADE(COLOR_WHITE, COLOR_TRANSPARENT_WHITE, DURATION, EASING_LINEAR)
|
||||||
|
|
||||||
|
#define CUTSCENE_EMOJI(ENTITY_INDEX, EMOJI_TYPE, DURATION) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_EMOJI, \
|
||||||
|
.emoji = { \
|
||||||
|
.entityIndex = ENTITY_INDEX, \
|
||||||
|
.emojiType = EMOJI_TYPE, \
|
||||||
|
.duration = DURATION \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
// AMOUNT ranges 0 (no shake) to 4 (three tiles): 1 is half a tile, 2 is
|
||||||
|
// a full tile, 3 is two tiles, and 4 is three tiles.
|
||||||
|
#define CUTSCENE_SHAKE(AMOUNT, DURATION) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_SHAKE, \
|
||||||
|
.shake = { .amount = AMOUNT, .duration = DURATION } \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CUTSCENE_SET_PAUSE(FLAGS) \
|
||||||
|
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
|
||||||
|
|
||||||
|
#define CUTSCENE_ITEM_GIVE(ITEM_NAME, QUANTITY) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_ITEM_GIVE, \
|
||||||
|
.itemGive = { .itemName = ITEM_NAME, .quantity = QUANTITY } \
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs all listed items simultaneously and waits until all are done.
|
||||||
|
// Concurrent items cannot be nested inside another CUTSCENE_CONCURRENT.
|
||||||
|
#define CUTSCENE_CONCURRENT(...) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_CONCURRENT, \
|
||||||
|
.concurrent = { \
|
||||||
|
.items = (const cutsceneitem_t[]){ __VA_ARGS__ }, \
|
||||||
|
.count = (uint8_t)( \
|
||||||
|
sizeof((cutsceneitem_t[]){ __VA_ARGS__ }) / \
|
||||||
|
sizeof(cutsceneitem_t) \
|
||||||
|
) \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CUTSCENE_MAP_AREA_ADD( \
|
||||||
|
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, CALLBACK, NOTIFY, TRIGGER \
|
||||||
|
) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD, \
|
||||||
|
.mapAreaAdd = { \
|
||||||
|
.min = { MIN_X, MIN_Y, MIN_Z }, \
|
||||||
|
.max = { MAX_X, MAX_Y, MAX_Z }, \
|
||||||
|
.callback = CALLBACK, \
|
||||||
|
.notify = NOTIFY, \
|
||||||
|
.trigger = TRIGGER \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CUTSCENE_MAP_AREA_REMOVE(AREA_ID) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, \
|
||||||
|
.mapAreaRemove = { .areaId = AREA_ID } \
|
||||||
|
}
|
||||||
|
|
||||||
|
// Waits until any one of the given map area IDs has its callback invoked.
|
||||||
|
// Accepts CUTSCENE_AREA_LAST_CREATED in place of a literal area ID.
|
||||||
|
#define CUTSCENE_MAP_AREA_WAIT(...) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, \
|
||||||
|
.mapAreaWait = { \
|
||||||
|
.areaIds = (const uint8_t[]){ __VA_ARGS__ }, \
|
||||||
|
.count = (uint8_t)( \
|
||||||
|
sizeof((const uint8_t[]){ __VA_ARGS__ }) / sizeof(uint8_t) \
|
||||||
|
) \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a map area, waits for it to be triggered once, then removes it
|
||||||
|
// before the cutscene continues. Uses a no-op callback since the wait is
|
||||||
|
// driven by the area's trigger count rather than callback logic.
|
||||||
|
#define CUTSCENE_MAP_AREA_TRIGGER_ONCE( \
|
||||||
|
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, NOTIFY, TRIGGER \
|
||||||
|
) \
|
||||||
|
CUTSCENE_MAP_AREA_ADD( \
|
||||||
|
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, \
|
||||||
|
mapAreaNoopCallback, NOTIFY, TRIGGER \
|
||||||
|
), \
|
||||||
|
CUTSCENE_MAP_AREA_WAIT(CUTSCENE_AREA_LAST_CREATED), \
|
||||||
|
CUTSCENE_MAP_AREA_REMOVE(CUTSCENE_AREA_LAST_CREATED)
|
||||||
|
|
||||||
|
#define CUTSCENE_JSON_NAME_MAX 32
|
||||||
|
#define CUTSCENE_JSON_ITEM_COUNT_MAX 32
|
||||||
|
#define CUTSCENE_JSON_WAYPOINT_COUNT_MAX 8
|
||||||
|
#define CUTSCENE_JSON_CACHE_MAX 16
|
||||||
|
|
||||||
|
// One slot of the cutsceneGetByName cache: a parsed cutscene plus the
|
||||||
|
// backing storage its items point into (items themselves, and the
|
||||||
|
// waypoint lists any "entityWalkTo" items reference).
|
||||||
|
typedef struct {
|
||||||
|
char_t name[CUTSCENE_JSON_NAME_MAX];
|
||||||
|
cutscene_t cutscene;
|
||||||
|
cutsceneitem_t items[CUTSCENE_JSON_ITEM_COUNT_MAX];
|
||||||
|
worldpos_t waypoints
|
||||||
|
[CUTSCENE_JSON_ITEM_COUNT_MAX][CUTSCENE_JSON_WAYPOINT_COUNT_MAX];
|
||||||
|
} cutscenejsoncacheentry_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a cutscene pause value ("NONE", "DEFAULT" or "ALL") from a
|
||||||
|
* yyjson string value.
|
||||||
|
*
|
||||||
|
* @param val The yyjson value to parse.
|
||||||
|
* @param outPause Output pointer, set to the parsed pause flags.
|
||||||
|
* @return Any error that occurs (missing/invalid/unknown value).
|
||||||
|
*/
|
||||||
|
errorret_t cutsceneJsonParsePause(yyjson_val *val, cutscenepause_t *outPause);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an entityIndex-shaped yyjson value - a raw number, or one of
|
||||||
|
* "interact", "interacted", "lastCreated" - into a raw uint8_t index or
|
||||||
|
* the matching CUTSCENE_ENTITY_* sentinel (see cutscenesystem.h).
|
||||||
|
*
|
||||||
|
* @param val The yyjson value to parse.
|
||||||
|
* @param outIndex Output pointer, set to the parsed index.
|
||||||
|
* @return Any error that occurs (missing/invalid/unknown value).
|
||||||
|
*/
|
||||||
|
errorret_t cutsceneJsonParseEntityIndex(yyjson_val *val, uint8_t *outIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a [x, y, z] yyjson array into a worldpos_t.
|
||||||
|
*
|
||||||
|
* @param val The yyjson value to parse.
|
||||||
|
* @param outPos Output pointer, set to the parsed position.
|
||||||
|
* @return Any error that occurs (missing/invalid value).
|
||||||
|
*/
|
||||||
|
errorret_t cutsceneJsonParseWorldPos(yyjson_val *val, worldpos_t *outPos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a single cutscene item from a yyjson object into outItem.
|
||||||
|
* "type" selects the item shape (required):
|
||||||
|
* { "type": "text", "text": "Hello!" }
|
||||||
|
* { "type": "textMini", "text": "Hi", "position": [x, y, z],
|
||||||
|
* "duration": 3.0 }
|
||||||
|
* { "type": "textMiniHide", "index": 0 }
|
||||||
|
* { "type": "wait", "duration": 1.5 }
|
||||||
|
* { "type": "entityAdd", "entityType": "npc", "position": [x, y, z] }
|
||||||
|
* { "type": "entityRemove", "entityIndex": 0 }
|
||||||
|
* { "type": "entityTurn", "entityIndex": 0, "direction": "south" }
|
||||||
|
* { "type": "entityWalkTo", "entityIndex": 0,
|
||||||
|
* "positions": [[x, y, z], ...], "walkAround": true }
|
||||||
|
* { "type": "entityWalkToEntity", "entityIndex": 0,
|
||||||
|
* "targetEntityIndex": 1, "offsetX": 1, "offsetY": 0 }
|
||||||
|
* { "type": "entityTeleport", "entityIndex": 0, "position": [x, y, z] }
|
||||||
|
* { "type": "emoji", "entityIndex": 0, "emojiType": "exclamation",
|
||||||
|
* "duration": 2.0 }
|
||||||
|
* { "type": "shake", "amount": 2, "duration": 0.5 }
|
||||||
|
* { "type": "setPause", "pause": "ALL" }
|
||||||
|
* "entityIndex"/"targetEntityIndex" accept a raw number, or one of
|
||||||
|
* "interact", "interacted", "lastCreated" for the matching
|
||||||
|
* CUTSCENE_ENTITY_* sentinel. "entityType" uses the same strings as
|
||||||
|
* entityCreateFromJson ("player", "npc", "item"); "direction" is one of
|
||||||
|
* "north"/"east"/"south"/"west"; "emojiType" is "question" or
|
||||||
|
* "exclamation"; "pause" is "NONE", "DEFAULT" or "ALL".
|
||||||
|
*
|
||||||
|
* Not supported (would need persistent string/array storage or function
|
||||||
|
* pointers this parser doesn't provide): itemGive, concurrent, fade,
|
||||||
|
* map area items, callbacks, nested cutscene references.
|
||||||
|
*
|
||||||
|
* @param obj The yyjson object describing a single cutscene item.
|
||||||
|
* @param outItem Output pointer, filled with the parsed item.
|
||||||
|
* @param waypoints Backing storage for an "entityWalkTo" item's waypoint
|
||||||
|
* list; must remain valid for as long as the parsed item is used.
|
||||||
|
* Unused (may be NULL) for any other item type.
|
||||||
|
* @param waypointsMax Capacity of waypoints.
|
||||||
|
* @return Any error that occurs (missing/invalid/unknown fields).
|
||||||
|
*/
|
||||||
|
errorret_t cutsceneItemCreateFromJson(
|
||||||
|
yyjson_val *obj,
|
||||||
|
cutsceneitem_t *outItem,
|
||||||
|
worldpos_t *waypoints,
|
||||||
|
const uint8_t waypointsMax
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads (or returns the cached result of an earlier load of) the named
|
||||||
|
* cutscene from assets/cutscene/<name>.json, parsing each entry of its
|
||||||
|
* "items" array with cutsceneItemCreateFromJson. Parsed once per name
|
||||||
|
* and cached for the lifetime of the process; holds at most
|
||||||
|
* CUTSCENE_JSON_CACHE_MAX distinct names, each with at most
|
||||||
|
* CUTSCENE_JSON_ITEM_COUNT_MAX items.
|
||||||
|
*
|
||||||
|
* File shape:
|
||||||
|
* { "pause": "DEFAULT", "items": [ { "type": "text", ... }, ... ] }
|
||||||
|
* "pause" is optional (one of "NONE", "DEFAULT", "ALL"; defaults to
|
||||||
|
* "DEFAULT" when absent).
|
||||||
|
*
|
||||||
|
* @param name The cutscene's file name (without extension), under
|
||||||
|
* assets/cutscene/.
|
||||||
|
* @param outCutscene Output pointer, set to the loaded cutscene.
|
||||||
|
* @return Any error that occurs (missing/malformed file, cache full,
|
||||||
|
* unsupported/invalid item).
|
||||||
|
*/
|
||||||
|
errorret_t cutsceneGetByName(
|
||||||
|
const char_t *name, const cutscene_t **outCutscene
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/cutscene/cutscenesystem.h"
|
|
||||||
|
|
||||||
bool_t cutsceneModeIsInputAllowed() {
|
|
||||||
switch(CUTSCENE_SYSTEM.mode) {
|
|
||||||
case CUTSCENE_MODE_FULL_FREEZE:
|
|
||||||
case CUTSCENE_MODE_INPUT_FREEZE:
|
|
||||||
return false;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
CUTSCENE_MODE_NONE,
|
|
||||||
CUTSCENE_MODE_FULL_FREEZE,
|
|
||||||
CUTSCENE_MODE_INPUT_FREEZE,
|
|
||||||
CUTSCENE_MODE_GAMEPLAY
|
|
||||||
} cutscenemode_t;
|
|
||||||
|
|
||||||
// Default mode for all cutscenes.
|
|
||||||
#define CUTSCENE_MODE_INITIAL CUTSCENE_MODE_INPUT_FREEZE
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if input is allowed in the current cutscene mode.
|
|
||||||
*
|
|
||||||
* @return true if input is allowed, false otherwise.
|
|
||||||
*/
|
|
||||||
bool_t cutsceneModeIsInputAllowed();
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 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 cutscenepause_t;
|
||||||
|
|
||||||
|
#define CUTSCENE_PAUSE_NONE ((cutscenepause_t)0)
|
||||||
|
#define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0))
|
||||||
|
#define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1))
|
||||||
|
#define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2))
|
||||||
|
|
||||||
|
#define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \
|
||||||
|
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \
|
||||||
|
))
|
||||||
|
|
||||||
|
#define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \
|
||||||
|
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD \
|
||||||
|
))
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2025 Dominic Masters
|
* Copyright (c) 2025 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "cutscenesystem.h"
|
#include "cutscenesystem.h"
|
||||||
|
#include "rpg/entity/entity.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
cutscenesystem_t CUTSCENE_SYSTEM;
|
cutscenesystem_t CUTSCENE_SYSTEM;
|
||||||
|
|
||||||
@@ -15,9 +17,28 @@ void cutsceneSystemInit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void cutsceneSystemStartCutscene(const cutscene_t *cutscene) {
|
void cutsceneSystemStartCutscene(const cutscene_t *cutscene) {
|
||||||
|
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cutsceneSystemStartCutsceneWith(
|
||||||
|
const cutscene_t *cutscene,
|
||||||
|
entity_t *interact,
|
||||||
|
entity_t *interacted
|
||||||
|
) {
|
||||||
|
assertTrue(
|
||||||
|
cutscene->dataSize < CUTSCENE_SYSTEM_SIZE_MAX,
|
||||||
|
"Cutscene data size exceeds CUTSCENE_SYSTEM_SIZE_MAX"
|
||||||
|
);
|
||||||
|
|
||||||
CUTSCENE_SYSTEM.scene = cutscene;
|
CUTSCENE_SYSTEM.scene = cutscene;
|
||||||
CUTSCENE_SYSTEM.mode = CUTSCENE_MODE_INITIAL;
|
CUTSCENE_SYSTEM.pause = cutscene->pause;
|
||||||
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so start wraps.
|
CUTSCENE_SYSTEM.entityInteract = interact;
|
||||||
|
CUTSCENE_SYSTEM.entityInteracted = interacted;
|
||||||
|
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||||
|
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||||
|
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||||
|
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||||
|
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
|
||||||
cutsceneSystemNext();
|
cutsceneSystemNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +46,7 @@ void cutsceneSystemUpdate() {
|
|||||||
if(CUTSCENE_SYSTEM.scene == NULL) return;
|
if(CUTSCENE_SYSTEM.scene == NULL) return;
|
||||||
|
|
||||||
const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
|
const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
|
||||||
cutsceneItemUpdate(item, &CUTSCENE_SYSTEM.data);
|
if(cutsceneItemUpdate(item, &CUTSCENE_SYSTEM.data)) cutsceneSystemNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
void cutsceneSystemNext() {
|
void cutsceneSystemNext() {
|
||||||
@@ -39,7 +60,13 @@ void cutsceneSystemNext() {
|
|||||||
) {
|
) {
|
||||||
CUTSCENE_SYSTEM.scene = NULL;
|
CUTSCENE_SYSTEM.scene = NULL;
|
||||||
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
||||||
CUTSCENE_SYSTEM.mode = CUTSCENE_MODE_NONE;
|
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
|
||||||
|
CUTSCENE_SYSTEM.entityInteract = NULL;
|
||||||
|
CUTSCENE_SYSTEM.entityInteracted = NULL;
|
||||||
|
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||||
|
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||||
|
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||||
|
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +82,76 @@ const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
|
|||||||
return &CUTSCENE_SYSTEM.scene->items[CUTSCENE_SYSTEM.currentItem];
|
return &CUTSCENE_SYSTEM.scene->items[CUTSCENE_SYSTEM.currentItem];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex) {
|
||||||
|
entity_t *entity;
|
||||||
|
|
||||||
|
if(entityIndex == CUTSCENE_ENTITY_INTERACT) {
|
||||||
|
assertNotNull(
|
||||||
|
CUTSCENE_SYSTEM.entityInteract,
|
||||||
|
"CUTSCENE_ENTITY_INTERACT used but no interact entity is set"
|
||||||
|
);
|
||||||
|
entity = CUTSCENE_SYSTEM.entityInteract;
|
||||||
|
} else if(entityIndex == CUTSCENE_ENTITY_INTERACTED) {
|
||||||
|
assertNotNull(
|
||||||
|
CUTSCENE_SYSTEM.entityInteracted,
|
||||||
|
"CUTSCENE_ENTITY_INTERACTED used but no interacted entity is set"
|
||||||
|
);
|
||||||
|
entity = CUTSCENE_SYSTEM.entityInteracted;
|
||||||
|
} else if(entityIndex == CUTSCENE_ENTITY_LAST_CREATED) {
|
||||||
|
assertNotNull(
|
||||||
|
CUTSCENE_SYSTEM.entityLastCreated,
|
||||||
|
"CUTSCENE_ENTITY_LAST_CREATED used but no entity has been created"
|
||||||
|
);
|
||||||
|
entity = CUTSCENE_SYSTEM.entityLastCreated;
|
||||||
|
} else if(entityIndex == CUTSCENE_ENTITY_LAST_REF) {
|
||||||
|
assertNotNull(
|
||||||
|
CUTSCENE_SYSTEM.entityLastRef,
|
||||||
|
"CUTSCENE_ENTITY_LAST_REF used but no entity has been referenced"
|
||||||
|
);
|
||||||
|
entity = CUTSCENE_SYSTEM.entityLastRef;
|
||||||
|
} else {
|
||||||
|
assertTrue(
|
||||||
|
entityIndex < ENTITY_COUNT,
|
||||||
|
"Entity index is out of range"
|
||||||
|
);
|
||||||
|
entity = &ENTITIES[entityIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
CUTSCENE_SYSTEM.entityLastRef = entity;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId) {
|
||||||
|
if(areaId == CUTSCENE_AREA_LAST_CREATED) {
|
||||||
|
assertTrue(
|
||||||
|
CUTSCENE_SYSTEM.areaLastCreated != CUTSCENE_AREA_LAST_CREATED,
|
||||||
|
"CUTSCENE_AREA_LAST_CREATED used but no map area has been created"
|
||||||
|
);
|
||||||
|
return CUTSCENE_SYSTEM.areaLastCreated;
|
||||||
|
}
|
||||||
|
return areaId;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index) {
|
||||||
|
if(index == CUTSCENE_TEXT_MINI_LAST_CREATED) {
|
||||||
|
assertTrue(
|
||||||
|
CUTSCENE_SYSTEM.textMiniLastCreated != CUTSCENE_TEXT_MINI_LAST_CREATED,
|
||||||
|
"CUTSCENE_TEXT_MINI_LAST_CREATED used but no mini textbox has been "
|
||||||
|
"shown"
|
||||||
|
);
|
||||||
|
return CUTSCENE_SYSTEM.textMiniLastCreated;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
void cutsceneSystemDispose() {
|
void cutsceneSystemDispose() {
|
||||||
CUTSCENE_SYSTEM.scene = NULL;
|
CUTSCENE_SYSTEM.scene = NULL;
|
||||||
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
||||||
CUTSCENE_SYSTEM.mode = CUTSCENE_MODE_NONE;
|
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
|
||||||
}
|
CUTSCENE_SYSTEM.entityInteract = NULL;
|
||||||
|
CUTSCENE_SYSTEM.entityInteracted = NULL;
|
||||||
|
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||||
|
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||||
|
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||||
|
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,15 +7,37 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "cutscene.h"
|
#include "cutscene.h"
|
||||||
#include "cutscenemode.h"
|
|
||||||
|
typedef struct entity_s entity_t;
|
||||||
|
|
||||||
|
#define CUTSCENE_ENTITY_INTERACT ((uint8_t)0xFE)
|
||||||
|
#define CUTSCENE_ENTITY_INTERACTED ((uint8_t)0xFD)
|
||||||
|
#define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC)
|
||||||
|
#define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB)
|
||||||
|
#define CUTSCENE_AREA_LAST_CREATED ((uint8_t)0xFF)
|
||||||
|
#define CUTSCENE_TEXT_MINI_LAST_CREATED ((uint8_t)0xFA)
|
||||||
|
|
||||||
|
// Maximum number of bytes a running cutscene may request via
|
||||||
|
// cutscene_t.dataSize.
|
||||||
|
#define CUTSCENE_SYSTEM_SIZE_MAX 8192
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const cutscene_t *scene;
|
const cutscene_t *scene;
|
||||||
uint8_t currentItem;
|
uint8_t currentItem;
|
||||||
|
cutscenepause_t pause;
|
||||||
|
entity_t *entityInteract;
|
||||||
|
entity_t *entityInteracted;
|
||||||
|
entity_t *entityLastCreated;
|
||||||
|
entity_t *entityLastRef;
|
||||||
|
uint8_t areaLastCreated;
|
||||||
|
uint8_t textMiniLastCreated;
|
||||||
|
|
||||||
// Data (used by the current item).
|
// Data (used by the current item).
|
||||||
cutsceneitemdata_t data;
|
cutsceneitemdata_t data;
|
||||||
cutscenemode_t mode;
|
|
||||||
|
// Custom user data for the running cutscene, sized per-scene by
|
||||||
|
// cutscene_t.dataSize.
|
||||||
|
uint8_t userData[CUTSCENE_SYSTEM_SIZE_MAX];
|
||||||
} cutscenesystem_t;
|
} cutscenesystem_t;
|
||||||
|
|
||||||
extern cutscenesystem_t CUTSCENE_SYSTEM;
|
extern cutscenesystem_t CUTSCENE_SYSTEM;
|
||||||
@@ -26,12 +48,55 @@ extern cutscenesystem_t CUTSCENE_SYSTEM;
|
|||||||
void cutsceneSystemInit();
|
void cutsceneSystemInit();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a cutscene.
|
* Start a cutscene with no bound entities.
|
||||||
*
|
*
|
||||||
* @param cutscene Pointer to the cutscene to start.
|
* @param cutscene Pointer to the cutscene to start.
|
||||||
*/
|
*/
|
||||||
void cutsceneSystemStartCutscene(const cutscene_t *cutscene);
|
void cutsceneSystemStartCutscene(const cutscene_t *cutscene);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a cutscene with the two entities that triggered it.
|
||||||
|
*
|
||||||
|
* @param cutscene Pointer to the cutscene to start.
|
||||||
|
* @param interact The entity that initiated the interaction (player).
|
||||||
|
* @param interacted The entity that was interacted with (NPC).
|
||||||
|
*/
|
||||||
|
void cutsceneSystemStartCutsceneWith(
|
||||||
|
const cutscene_t *cutscene,
|
||||||
|
entity_t *interact,
|
||||||
|
entity_t *interacted
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a raw entity index (or sentinel) to an entity pointer.
|
||||||
|
* Handles CUTSCENE_ENTITY_INTERACT, CUTSCENE_ENTITY_INTERACTED,
|
||||||
|
* CUTSCENE_ENTITY_LAST_CREATED and CUTSCENE_ENTITY_LAST_REF.
|
||||||
|
* Updates CUTSCENE_SYSTEM.entityLastRef to the resolved entity.
|
||||||
|
* Asserts the resolved entity is within bounds.
|
||||||
|
*
|
||||||
|
* @param entityIndex Raw entity index or sentinel value.
|
||||||
|
* @returns Pointer to the resolved entity.
|
||||||
|
*/
|
||||||
|
entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a raw map area ID (or CUTSCENE_AREA_LAST_CREATED sentinel) to
|
||||||
|
* a concrete map area ID.
|
||||||
|
*
|
||||||
|
* @param areaId Raw map area ID or sentinel value.
|
||||||
|
* @returns The resolved map area ID.
|
||||||
|
*/
|
||||||
|
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a raw mini textbox slot index (or CUTSCENE_TEXT_MINI_LAST_CREATED
|
||||||
|
* sentinel) to a concrete UI_TEXTBOX_MINI_LIST slot index.
|
||||||
|
*
|
||||||
|
* @param index Raw slot index or sentinel value.
|
||||||
|
* @returns The resolved slot index.
|
||||||
|
*/
|
||||||
|
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Advance to the next item in the cutscene.
|
* Advance to the next item in the cutscene.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
# Copyright (c) 2025 Dominic Masters
|
# Copyright (c) 2025 Dominic Masters
|
||||||
#
|
#
|
||||||
# This software is released under the MIT License.
|
# This software is released under the MIT License.
|
||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
# Sources
|
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
cutsceneitem.c
|
cutsceneitem.c
|
||||||
cutsceneentitymove.c
|
cutscenecallback.c
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_subdirectory(control)
|
||||||
|
add_subdirectory(entity)
|
||||||
|
add_subdirectory(item)
|
||||||
|
add_subdirectory(maparea)
|
||||||
|
add_subdirectory(ui)
|
||||||
|
add_subdirectory(battle)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
cutscenestartbattle.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/battle/party.h"
|
||||||
|
#include "scene/scene.h"
|
||||||
|
|
||||||
|
void cutsceneStartBattleStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
const cutscenestartbattle_t *config = &item->startBattle;
|
||||||
|
|
||||||
|
battleInit();
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||||
|
battlefighter_t *member = partyGetOrderMember(i);
|
||||||
|
if(member == NULL) continue;
|
||||||
|
|
||||||
|
battlefighter_t *fighter = battleAddFighter(
|
||||||
|
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||||
|
member->stats, member->healthMax, member->mpMax
|
||||||
|
);
|
||||||
|
if(fighter == NULL) continue;
|
||||||
|
|
||||||
|
fighter->health = member->health;
|
||||||
|
fighter->mp = member->mp;
|
||||||
|
fighter->status = member->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < config->enemyCount; i++) {
|
||||||
|
const cutscenestartbattleenemy_t *enemy = &config->enemies[i];
|
||||||
|
battleAddFighter(
|
||||||
|
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||||
|
enemy->stats, enemy->healthMax, enemy->mpMax
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
battleStart(config->encounterType, config->fleeAvailable);
|
||||||
|
sceneSet(SCENE_TYPE_BATTLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneStartBattleUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
if(BATTLE.result == BATTLE_RESULT_NONE) return false;
|
||||||
|
|
||||||
|
// Sync ally HP/MP back to the persistent party roster. Relies on
|
||||||
|
// ally fighters having been added to BATTLE.fighters in the same
|
||||||
|
// order partyGetOrderMember() iterates, starting at index 0 (see
|
||||||
|
// cutsceneStartBattleStart).
|
||||||
|
uint8_t allySlot = 0;
|
||||||
|
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||||
|
battlefighter_t *member = partyGetOrderMember(i);
|
||||||
|
if(member == NULL) continue;
|
||||||
|
|
||||||
|
battlefighter_t *fighter = &BATTLE.fighters[allySlot++];
|
||||||
|
member->health = fighter->health;
|
||||||
|
member->mp = fighter->mp;
|
||||||
|
member->status = fighter->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||||
|
battleDispose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "rpg/battle/battle.h"
|
||||||
|
|
||||||
|
#define CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX 4
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
battlefighterstats_t stats;
|
||||||
|
uint16_t healthMax;
|
||||||
|
uint16_t mpMax;
|
||||||
|
} cutscenestartbattleenemy_t;
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
battleencountertype_t encounterType;
|
||||||
|
bool_t fleeAvailable;
|
||||||
|
uint8_t enemyCount;
|
||||||
|
cutscenestartbattleenemy_t enemies[CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX];
|
||||||
|
} cutscenestartbattle_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a battle: seeds BATTLE with the party's active order members
|
||||||
|
* and the item's configured enemies, then switches to the battle
|
||||||
|
* scene.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneStartBattleStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for the battle to produce a result, syncs ally HP/MP back to
|
||||||
|
* the party roster, then returns to the overworld scene.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true once the battle has ended.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneStartBattleUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
cutscenewait.c
|
||||||
|
cutscenesetpause.c
|
||||||
|
cutsceneconcurrent.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
void cutsceneConcurrentStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
assertTrue(
|
||||||
|
item->concurrent.count <= CUTSCENE_CONCURRENT_MAX,
|
||||||
|
"Too many items in CUTSCENE_CONCURRENT"
|
||||||
|
);
|
||||||
|
for(uint8_t i = 0; i < item->concurrent.count; i++) {
|
||||||
|
assertTrue(
|
||||||
|
item->concurrent.items[i].type != CUTSCENE_ITEM_TYPE_CONCURRENT,
|
||||||
|
"Concurrent items cannot be nested"
|
||||||
|
);
|
||||||
|
cutsceneItemStart(
|
||||||
|
&item->concurrent.items[i],
|
||||||
|
(cutsceneitemdata_t *)&data->concurrent.childData[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneConcurrentUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
for(uint8_t i = 0; i < item->concurrent.count; i++) {
|
||||||
|
if(data->concurrent.doneMask & (1u << i)) continue;
|
||||||
|
if(cutsceneItemUpdate(
|
||||||
|
&item->concurrent.items[i],
|
||||||
|
(cutsceneitemdata_t *)&data->concurrent.childData[i]
|
||||||
|
)) {
|
||||||
|
data->concurrent.doneMask |= (uint8_t)(1u << i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint8_t allDone = (uint8_t)((1u << item->concurrent.count) - 1u);
|
||||||
|
return data->concurrent.doneMask == allDone;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "cutscenewait.h"
|
||||||
|
#include "rpg/cutscene/item/entity/cutsceneentitywalkto.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
/** Maximum number of items that may run inside a CUTSCENE_CONCURRENT. */
|
||||||
|
#define CUTSCENE_CONCURRENT_MAX 8
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static (const) data for a concurrent cutscene item.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
const cutsceneitem_t *items;
|
||||||
|
uint8_t count;
|
||||||
|
} cutsceneconcurrent_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime data for one non-concurrent child item.
|
||||||
|
* Concurrent items cannot be nested.
|
||||||
|
*/
|
||||||
|
typedef union {
|
||||||
|
cutscenewaitdata_t wait;
|
||||||
|
cutsceneentitywalktodata_t entityWalkTo;
|
||||||
|
} cutsceneconcurrentchilddata_t;
|
||||||
|
|
||||||
|
/** Runtime data for a running concurrent item. */
|
||||||
|
typedef struct {
|
||||||
|
cutsceneconcurrentchilddata_t childData[CUTSCENE_CONCURRENT_MAX];
|
||||||
|
uint8_t doneMask;
|
||||||
|
} cutsceneconcurrentdata_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a concurrent item (starts all child items simultaneously).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneConcurrentStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a concurrent item (ticks all unfinished children).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true once every child has completed.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneConcurrentUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
|
||||||
|
void cutsceneSetPauseStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
CUTSCENE_SYSTEM.pause = item->setPause;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneSetPauseUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -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 "rpg/cutscene/cutscenepause.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a set-pause item (applies the new pause flags immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneSetPauseStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a set-pause item (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneSetPauseUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "time/time.h"
|
||||||
|
|
||||||
|
void cutsceneWaitStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
data->wait = item->wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneWaitUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
data->wait -= TIME.delta;
|
||||||
|
return data->wait <= 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef float_t cutscenewait_t;
|
||||||
|
typedef float_t cutscenewaitdata_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a wait item (stores the duration in data).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneWaitStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a wait item.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true when the wait has elapsed.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneWaitUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
|
||||||
|
void cutsceneCallbackStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
if(item->callback != NULL) item->callback(CUTSCENE_SYSTEM.userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneCallbackUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2025 Dominic Masters
|
* Copyright (c) 2025 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
@@ -8,4 +8,30 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "dusk.h"
|
#include "dusk.h"
|
||||||
|
|
||||||
typedef void (*cutscenecallback_t)(void);
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef void (*cutscenecallback_t)(void *userData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a callback item (invokes the callback immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneCallbackStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a callback item (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneCallbackUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "cutsceneentitymove.h"
|
|
||||||
#include "rpg/cutscene/cutscenesystem.h"
|
|
||||||
#include "rpg/entity/entity.h"
|
|
||||||
|
|
||||||
void cutsceneEntityMoveStart(const cutsceneitem_t *item) {
|
|
||||||
}
|
|
||||||
|
|
||||||
void cutsceneEntityMoveUpdate(const cutsceneitem_t *item) {
|
|
||||||
entity_t *entity = &ENTITIES[item->entityMove.entityIndex];
|
|
||||||
|
|
||||||
if(worldPosIsEqual(entity->position, item->entityMove.target)) {
|
|
||||||
cutsceneSystemNext();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
entitydir_t dir;
|
|
||||||
if(entity->position.x != item->entityMove.target.x) {
|
|
||||||
dir = entity->position.x < item->entityMove.target.x
|
|
||||||
? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
|
|
||||||
} else {
|
|
||||||
dir = entity->position.y < item->entityMove.target.y
|
|
||||||
? ENTITY_DIR_SOUTH : ENTITY_DIR_NORTH;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(entityCanTurn(entity)) entityTurn(entity, dir);
|
|
||||||
|
|
||||||
if(item->entityMove.run) {
|
|
||||||
if(entityCanRun(entity)) entityRun(entity, dir);
|
|
||||||
} else {
|
|
||||||
if(entityCanWalk(entity)) entityWalk(entity, dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "cutsceneitem.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the start of an entity move cutscene item.
|
|
||||||
*
|
|
||||||
* @param item The cutscene item.
|
|
||||||
*/
|
|
||||||
void cutsceneEntityMoveStart(const cutsceneitem_t *item);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates an entity move cutscene item, steering the entity one step
|
|
||||||
* per frame toward the target and advancing the cutscene on arrival.
|
|
||||||
*
|
|
||||||
* @param item The cutscene item.
|
|
||||||
*/
|
|
||||||
void cutsceneEntityMoveUpdate(const cutsceneitem_t *item);
|
|
||||||
@@ -1,63 +1,152 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2025 Dominic Masters
|
* Copyright (c) 2025 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "rpg/cutscene/cutscenesystem.h"
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
#include "cutsceneentitymove.h"
|
|
||||||
#include "input/input.h"
|
cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
||||||
#include "time/time.h"
|
[CUTSCENE_ITEM_TYPE_NULL] = { 0 },
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_TEXT] = {
|
||||||
|
.init = cutsceneTextStart,
|
||||||
|
.update = cutsceneTextUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_TEXT_MINI] = {
|
||||||
|
.init = cutsceneTextMiniStart,
|
||||||
|
.update = cutsceneTextMiniUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE] = {
|
||||||
|
.init = cutsceneTextMiniHideStart,
|
||||||
|
.update = cutsceneTextMiniHideUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_CALLBACK] = {
|
||||||
|
.init = cutsceneCallbackStart,
|
||||||
|
.update = cutsceneCallbackUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_WAIT] = {
|
||||||
|
.init = cutsceneWaitStart,
|
||||||
|
.update = cutsceneWaitUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_CUTSCENE] = {
|
||||||
|
.init = cutsceneCutsceneStart,
|
||||||
|
.update = cutsceneCutsceneUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT] = {
|
||||||
|
.init = cutsceneEntityTeleportStart,
|
||||||
|
.update = cutsceneEntityTeleportUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO] = {
|
||||||
|
.init = cutsceneEntityWalkToStart,
|
||||||
|
.update = cutsceneEntityWalkToUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_FADE] = {
|
||||||
|
.init = cutsceneFadeStart,
|
||||||
|
.update = cutsceneFadeUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_SET_PAUSE] = {
|
||||||
|
.init = cutsceneSetPauseStart,
|
||||||
|
.update = cutsceneSetPauseUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_CONCURRENT] = {
|
||||||
|
.init = cutsceneConcurrentStart,
|
||||||
|
.update = cutsceneConcurrentUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_ITEM_GIVE] = {
|
||||||
|
.init = cutsceneItemGiveStart,
|
||||||
|
.update = cutsceneItemGiveUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_ENTITY_REMOVE] = {
|
||||||
|
.init = cutsceneEntityRemoveStart,
|
||||||
|
.update = cutsceneEntityRemoveUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_ENTITY_ADD] = {
|
||||||
|
.init = cutsceneEntityAddStart,
|
||||||
|
.update = cutsceneEntityAddUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_ENTITY_TURN] = {
|
||||||
|
.init = cutsceneEntityTurnStart,
|
||||||
|
.update = cutsceneEntityTurnUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY] = {
|
||||||
|
.init = cutsceneEntityWalkToEntityStart,
|
||||||
|
.update = cutsceneEntityWalkToEntityUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_MAP_AREA_ADD] = {
|
||||||
|
.init = cutsceneMapAreaAddStart,
|
||||||
|
.update = cutsceneMapAreaAddUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE] = {
|
||||||
|
.init = cutsceneMapAreaRemoveStart,
|
||||||
|
.update = cutsceneMapAreaRemoveUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT] = {
|
||||||
|
.init = cutsceneMapAreaWaitStart,
|
||||||
|
.update = cutsceneMapAreaWaitUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_START_BATTLE] = {
|
||||||
|
.init = cutsceneStartBattleStart,
|
||||||
|
.update = cutsceneStartBattleUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_EMOJI] = {
|
||||||
|
.init = cutsceneEmojiStart,
|
||||||
|
.update = cutsceneEmojiUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_SHAKE] = {
|
||||||
|
.init = cutsceneShakeStart,
|
||||||
|
.update = cutsceneShakeUpdate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
|
void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
|
||||||
switch(item->type) {
|
cutsceneiteminitcallback_t *init = CUTSCENE_ITEM_CALLBACKS[item->type].init;
|
||||||
case CUTSCENE_ITEM_TYPE_TEXT: {
|
if(init != NULL) init(item, data);
|
||||||
rpgTextboxShow(
|
|
||||||
item->text.position,
|
|
||||||
item->text.text
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case CUTSCENE_ITEM_TYPE_WAIT:
|
|
||||||
data->wait = item->wait;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CUTSCENE_ITEM_TYPE_CALLBACK:
|
|
||||||
if(item->callback != NULL) item->callback();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CUTSCENE_ITEM_TYPE_CUTSCENE:
|
|
||||||
if(item->cutscene != NULL) cutsceneSystemStartCutscene(item->cutscene);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CUTSCENE_ITEM_TYPE_ENTITY_MOVE:
|
|
||||||
cutsceneEntityMoveStart(item);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
|
bool_t cutsceneItemUpdate(
|
||||||
switch(item->type) {
|
const cutsceneitem_t *item,
|
||||||
case CUTSCENE_ITEM_TYPE_TEXT:
|
cutsceneitemdata_t *data
|
||||||
if(rpgTextboxIsVisible()) return;
|
) {
|
||||||
cutsceneSystemNext();
|
cutsceneitemupdatecallback_t *update =
|
||||||
break;
|
CUTSCENE_ITEM_CALLBACKS[item->type].update;
|
||||||
|
if(update == NULL) return false;
|
||||||
|
|
||||||
case CUTSCENE_ITEM_TYPE_WAIT:
|
return update(item, data);
|
||||||
data->wait -= TIME.delta;
|
}
|
||||||
if(data->wait <= 0) cutsceneSystemNext();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CUTSCENE_ITEM_TYPE_ENTITY_MOVE:
|
void cutsceneCutsceneStart(
|
||||||
cutsceneEntityMoveUpdate(item);
|
const cutsceneitem_t *item,
|
||||||
break;
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
if(item->cutscene != NULL) cutsceneSystemStartCutscene(item->cutscene);
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
bool_t cutsceneCutsceneUpdate(
|
||||||
break;
|
const cutsceneitem_t *item,
|
||||||
}
|
cutsceneitemdata_t *data
|
||||||
}
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,60 +1,163 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2025 Dominic Masters
|
* Copyright (c) 2025 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "cutscenewait.h"
|
|
||||||
#include "cutscenecallback.h"
|
#include "cutscenecallback.h"
|
||||||
#include "cutscenetext.h"
|
#include "control/cutscenewait.h"
|
||||||
#include "rpg/overworld/worldpos.h"
|
#include "control/cutscenesetpause.h"
|
||||||
|
#include "control/cutsceneconcurrent.h"
|
||||||
|
#include "entity/cutsceneentityteleport.h"
|
||||||
|
#include "entity/cutsceneentitywalkto.h"
|
||||||
|
#include "entity/cutsceneentityremove.h"
|
||||||
|
#include "entity/cutsceneentityadd.h"
|
||||||
|
#include "entity/cutsceneentityturn.h"
|
||||||
|
#include "entity/cutsceneentitywalktoentity.h"
|
||||||
|
#include "ui/cutscenetext.h"
|
||||||
|
#include "ui/cutscenetextmini.h"
|
||||||
|
#include "ui/cutscenetextminihide.h"
|
||||||
|
#include "ui/cutscenefade.h"
|
||||||
|
#include "ui/cutsceneemoji.h"
|
||||||
|
#include "ui/cutsceneshake.h"
|
||||||
|
#include "item/cutsceneitemgive.h"
|
||||||
|
#include "maparea/cutscenemapareaadd.h"
|
||||||
|
#include "maparea/cutscenemaparearemove.h"
|
||||||
|
#include "maparea/cutscenemapareawait.h"
|
||||||
|
#include "battle/cutscenestartbattle.h"
|
||||||
|
|
||||||
typedef struct cutscene_s cutscene_t;
|
typedef struct cutscene_s cutscene_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
CUTSCENE_ITEM_TYPE_NULL,
|
CUTSCENE_ITEM_TYPE_NULL,
|
||||||
|
|
||||||
CUTSCENE_ITEM_TYPE_TEXT,
|
CUTSCENE_ITEM_TYPE_TEXT,
|
||||||
|
CUTSCENE_ITEM_TYPE_TEXT_MINI,
|
||||||
|
CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE,
|
||||||
CUTSCENE_ITEM_TYPE_CALLBACK,
|
CUTSCENE_ITEM_TYPE_CALLBACK,
|
||||||
CUTSCENE_ITEM_TYPE_WAIT,
|
CUTSCENE_ITEM_TYPE_WAIT,
|
||||||
CUTSCENE_ITEM_TYPE_CUTSCENE,
|
CUTSCENE_ITEM_TYPE_CUTSCENE,
|
||||||
CUTSCENE_ITEM_TYPE_ENTITY_MOVE
|
CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT,
|
||||||
|
CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO,
|
||||||
|
CUTSCENE_ITEM_TYPE_FADE,
|
||||||
|
CUTSCENE_ITEM_TYPE_SET_PAUSE,
|
||||||
|
CUTSCENE_ITEM_TYPE_CONCURRENT,
|
||||||
|
CUTSCENE_ITEM_TYPE_ITEM_GIVE,
|
||||||
|
CUTSCENE_ITEM_TYPE_ENTITY_REMOVE,
|
||||||
|
CUTSCENE_ITEM_TYPE_ENTITY_ADD,
|
||||||
|
CUTSCENE_ITEM_TYPE_ENTITY_TURN,
|
||||||
|
CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY,
|
||||||
|
CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
|
||||||
|
CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
|
||||||
|
CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
|
||||||
|
CUTSCENE_ITEM_TYPE_START_BATTLE,
|
||||||
|
CUTSCENE_ITEM_TYPE_EMOJI,
|
||||||
|
CUTSCENE_ITEM_TYPE_SHAKE,
|
||||||
|
|
||||||
|
CUTSCENE_ITEM_TYPE_COUNT
|
||||||
} cutsceneitemtype_t;
|
} cutsceneitemtype_t;
|
||||||
|
|
||||||
typedef struct cutsceneitem_s {
|
struct cutsceneitem_s {
|
||||||
cutsceneitemtype_t type;
|
cutsceneitemtype_t type;
|
||||||
|
|
||||||
// Arguments/Data that will be used when this item is invoked.
|
|
||||||
union {
|
union {
|
||||||
cutscenetext_t text;
|
cutscenetext_t text;
|
||||||
|
cutscenetextmini_t textMini;
|
||||||
|
cutscenetextminihide_t textMiniHide;
|
||||||
cutscenecallback_t callback;
|
cutscenecallback_t callback;
|
||||||
cutscenewait_t wait;
|
cutscenewait_t wait;
|
||||||
const cutscene_t *cutscene;
|
const cutscene_t *cutscene;
|
||||||
struct {
|
cutsceneentityteleport_t entityTeleport;
|
||||||
uint8_t entityIndex;
|
cutsceneentitywalkto_t entityWalkTo;
|
||||||
worldpos_t target;
|
cutscenefade_t fade;
|
||||||
bool_t run;
|
cutscenepause_t setPause;
|
||||||
} entityMove;
|
cutsceneconcurrent_t concurrent;
|
||||||
|
cutsceneitemgive_t itemGive;
|
||||||
|
cutsceneentityremove_t entityRemove;
|
||||||
|
cutsceneentityadd_t entityAdd;
|
||||||
|
cutsceneentityturn_t entityTurn;
|
||||||
|
cutsceneentitywalktoentity_t entityWalkToEntity;
|
||||||
|
cutscenemapareaadd_t mapAreaAdd;
|
||||||
|
cutscenemaparearemove_t mapAreaRemove;
|
||||||
|
cutscenemapareawait_t mapAreaWait;
|
||||||
|
cutscenestartbattle_t startBattle;
|
||||||
|
cutsceneemoji_t emoji;
|
||||||
|
cutsceneshake_t shake;
|
||||||
};
|
};
|
||||||
} cutsceneitem_t;
|
};
|
||||||
|
|
||||||
typedef union {
|
typedef union cutsceneitemdata_u {
|
||||||
cutscenewaitdata_t wait;
|
cutscenewaitdata_t wait;
|
||||||
|
cutsceneentitywalktodata_t entityWalkTo;
|
||||||
|
cutsceneconcurrentdata_t concurrent;
|
||||||
|
cutscenemapareawaitdata_t mapAreaWait;
|
||||||
} cutsceneitemdata_t;
|
} cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef void (cutsceneiteminitcallback_t)(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef bool_t (cutsceneitemupdatecallback_t)(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
cutsceneiteminitcallback_t *init;
|
||||||
|
cutsceneitemupdatecallback_t *update;
|
||||||
|
} cutsceneitemcallbacks_t;
|
||||||
|
|
||||||
|
extern cutsceneitemcallbacks_t
|
||||||
|
CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the given cutscene item.
|
* Start the given cutscene item.
|
||||||
*
|
*
|
||||||
* @param item The cutscene item to start.
|
* @param item The cutscene item to start.
|
||||||
* @param data The cutscene item data storage.
|
* @param data Runtime data storage (pre-zeroed by caller).
|
||||||
*/
|
*/
|
||||||
void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data);
|
void cutsceneItemStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tick the given cutscene item (one frame).
|
* Tick the given cutscene item (one frame).
|
||||||
*
|
*
|
||||||
* @param item The cutscene item to tick.
|
* @param item The cutscene item to tick.
|
||||||
* @param data The cutscene item data storage.
|
* @param data Runtime data storage.
|
||||||
|
* @returns true if the item is complete and the cutscene should advance.
|
||||||
*/
|
*/
|
||||||
void cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data);
|
bool_t cutsceneItemUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a nested-cutscene item, handing control over to the
|
||||||
|
* referenced cutscene.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneCutsceneStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a nested-cutscene item. By the time this would run, control
|
||||||
|
* has already moved on to the referenced cutscene, so this always
|
||||||
|
* reports incomplete.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns false always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneCutsceneUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "rpg/rpgtextbox.h"
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
char_t text[RPG_TEXTBOX_MAX_CHARS];
|
|
||||||
rpgtextboxpos_t position;
|
|
||||||
} cutscenetext_t;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
|
|
||||||
typedef float_t cutscenewait_t;
|
|
||||||
typedef float_t cutscenewaitdata_t;
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
cutsceneentityteleport.c
|
||||||
|
cutsceneentitywalkto.c
|
||||||
|
cutsceneentityremove.c
|
||||||
|
cutsceneentityadd.c
|
||||||
|
cutsceneentityturn.c
|
||||||
|
cutsceneentitywalktoentity.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "rpg/entity/entity.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
void cutsceneEntityAddStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
uint8_t entIndex = entityGetAvailable();
|
||||||
|
assertTrue(entIndex != 0xFF, "No available entity slots for CUTSCENE_ENTITY_ADD");
|
||||||
|
|
||||||
|
entity_t *entity = &ENTITIES[entIndex];
|
||||||
|
entityInit(entity, item->entityAdd.entityType);
|
||||||
|
entityPositionSet(entity, item->entityAdd.position);// Also assigns chunk.
|
||||||
|
|
||||||
|
CUTSCENE_SYSTEM.entityLastCreated = entity;
|
||||||
|
CUTSCENE_SYSTEM.entityLastRef = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneEntityAddUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "rpg/entity/entitytype.h"
|
||||||
|
#include "rpg/overworld/worldpos.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
entitytype_t entityType;
|
||||||
|
worldpos_t position;
|
||||||
|
} cutsceneentityadd_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an entity add step (spawns the entity into the world immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneEntityAddStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an entity add step (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneEntityAddUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "rpg/entity/entity.h"
|
||||||
|
|
||||||
|
void cutsceneEntityRemoveStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
cutsceneSystemGetEntity(item->entityRemove.entityIndex)->type = \
|
||||||
|
ENTITY_TYPE_NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneEntityRemoveUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 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 cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t entityIndex;
|
||||||
|
} cutsceneentityremove_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an entity remove step (removes the entity from the world immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneEntityRemoveStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an entity remove step (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneEntityRemoveUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "rpg/entity/entity.h"
|
||||||
|
|
||||||
|
void cutsceneEntityTeleportStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
entityPositionSet(
|
||||||
|
cutsceneSystemGetEntity(item->entityTeleport.entityIndex),
|
||||||
|
item->entityTeleport.target
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneEntityTeleportUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "rpg/overworld/worldpos.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t entityIndex;
|
||||||
|
worldpos_t target;
|
||||||
|
} cutsceneentityteleport_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an entity teleport item (teleports the entity immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneEntityTeleportStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an entity teleport item (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneEntityTeleportUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "rpg/entity/entity.h"
|
||||||
|
|
||||||
|
void cutsceneEntityTurnStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneEntityTurnUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
entity_t *entity = cutsceneSystemGetEntity(item->entityTurn.entityIndex);
|
||||||
|
if(
|
||||||
|
entity->direction == item->entityTurn.direction &&
|
||||||
|
entity->animation == ENTITY_ANIM_IDLE
|
||||||
|
) return true;
|
||||||
|
|
||||||
|
entityTurn(entity, item->entityTurn.direction);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -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 "rpg/entity/entitydir.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t entityIndex;
|
||||||
|
entitydir_t direction;
|
||||||
|
} cutsceneentityturn_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an entity turn step. The turn itself is driven from Update, since
|
||||||
|
* the entity may still be finishing a previous action.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneEntityTurnStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an entity turn step, retrying entityTurn until it takes effect
|
||||||
|
* and its animation completes.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true once the entity is idle and facing the target direction.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneEntityTurnUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "rpg/entity/entitypathstep.h"
|
||||||
|
|
||||||
|
void cutsceneEntityWalkToStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
data->entityWalkTo.currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneEntityWalkToUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
uint8_t i = data->entityWalkTo.currentIndex;
|
||||||
|
entity_t *e = cutsceneSystemGetEntity(item->entityWalkTo.entityIndex);
|
||||||
|
if(!entityPathStep(
|
||||||
|
e,
|
||||||
|
item->entityWalkTo.positions[i],
|
||||||
|
item->entityWalkTo.walkAround
|
||||||
|
)) return false;
|
||||||
|
i++;
|
||||||
|
if(i < item->entityWalkTo.count) {
|
||||||
|
data->entityWalkTo.currentIndex = i;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "rpg/overworld/worldpos.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t entityIndex;
|
||||||
|
const worldpos_t *positions;
|
||||||
|
uint8_t count;
|
||||||
|
bool_t walkAround;
|
||||||
|
} cutsceneentitywalkto_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t currentIndex;
|
||||||
|
} cutsceneentitywalktodata_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an entity walk-to item (resets the waypoint index).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneEntityWalkToStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an entity walk-to item (steps the entity toward the next waypoint).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true once all waypoints have been reached.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneEntityWalkToUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "rpg/entity/entity.h"
|
||||||
|
#include "rpg/entity/entitypathstep.h"
|
||||||
|
#include "rpg/overworld/chunk.h"
|
||||||
|
|
||||||
|
void cutsceneEntityWalkToEntityStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneEntityWalkToEntityUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
entity_t *entity = cutsceneSystemGetEntity(
|
||||||
|
item->entityWalkToEntity.entityIndex
|
||||||
|
);
|
||||||
|
entity_t *target = cutsceneSystemGetEntity(
|
||||||
|
item->entityWalkToEntity.targetEntityIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
worldpos_t dest = {
|
||||||
|
.x = (worldunit_t)(target->position.x + item->entityWalkToEntity.offsetX),
|
||||||
|
.y = (worldunit_t)(target->position.y + item->entityWalkToEntity.offsetY),
|
||||||
|
.z = target->position.z
|
||||||
|
};
|
||||||
|
|
||||||
|
worldunit_t z;
|
||||||
|
if(chunkGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z;
|
||||||
|
|
||||||
|
return entityPathStep(entity, dest, true);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "rpg/overworld/worldpos.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t entityIndex;
|
||||||
|
uint8_t targetEntityIndex;
|
||||||
|
worldunit_t offsetX;
|
||||||
|
worldunit_t offsetY;
|
||||||
|
} cutsceneentitywalktoentity_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an entity walk-to-entity item. No setup is needed, the destination
|
||||||
|
* is recomputed from the target's live position every Update.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneEntityWalkToEntityStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an entity walk-to-entity item. Re-reads the target entity's
|
||||||
|
* current position each frame, applies the X/Y offset, resolves the
|
||||||
|
* destination Z from nearby terrain (to account for ramps), and steps
|
||||||
|
* the entity toward it.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true once the entity has reached the target's side.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneEntityWalkToEntityUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
cutsceneitemgive.c
|
||||||
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user