From ca02ee035217fde8e74a84d6f706da256103e185 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sat, 11 Jul 2026 11:02:09 -0500 Subject: [PATCH] Add camera shake --- assets/locale/es_MX.po | 6 +- assets/locale/jp_JP.po | 6 +- cmake/targets/psp.cmake | 1 + src/dusk/rpg/cutscene/cutscene.h | 8 + src/dusk/rpg/cutscene/item/cutsceneitem.c | 290 +++++++++--------- src/dusk/rpg/cutscene/item/cutsceneitem.h | 52 +++- src/dusk/rpg/cutscene/item/ui/CMakeLists.txt | 1 + src/dusk/rpg/cutscene/item/ui/cutsceneshake.c | 23 ++ src/dusk/rpg/cutscene/item/ui/cutsceneshake.h | 41 +++ src/dusk/rpg/item/CMakeLists.txt | 6 +- src/dusk/rpg/item/item.csv | 4 - src/dusk/rpg/item/item.json | 5 + src/dusk/rpg/rpgcamera.c | 24 ++ src/dusk/rpg/rpgcamera.h | 15 + tools/item.py | 38 +-- 15 files changed, 335 insertions(+), 185 deletions(-) create mode 100644 src/dusk/rpg/cutscene/item/ui/cutsceneshake.c create mode 100644 src/dusk/rpg/cutscene/item/ui/cutsceneshake.h delete mode 100644 src/dusk/rpg/item/item.csv create mode 100644 src/dusk/rpg/item/item.json diff --git a/assets/locale/es_MX.po b/assets/locale/es_MX.po index 3e43adef..fe196221 100644 --- a/assets/locale/es_MX.po +++ b/assets/locale/es_MX.po @@ -57,14 +57,14 @@ msgstr "Objetos" msgid "ui.game_menu.settings" msgstr "Configuración" -#: src/dusk/rpg/item/item.csv +#: src/dusk/rpg/item/item.json msgid "item.potion.name" msgstr "Poción" -#: src/dusk/rpg/item/item.csv +#: src/dusk/rpg/item/item.json msgid "item.potato.name" msgstr "Papa" -#: src/dusk/rpg/item/item.csv +#: src/dusk/rpg/item/item.json msgid "item.apple.name" msgstr "Manzana" diff --git a/assets/locale/jp_JP.po b/assets/locale/jp_JP.po index ed503e2f..6e4250c3 100644 --- a/assets/locale/jp_JP.po +++ b/assets/locale/jp_JP.po @@ -57,14 +57,14 @@ msgstr "アイテム" msgid "ui.game_menu.settings" msgstr "設定" -#: src/dusk/rpg/item/item.csv +#: src/dusk/rpg/item/item.json msgid "item.potion.name" msgstr "ポーション" -#: src/dusk/rpg/item/item.csv +#: src/dusk/rpg/item/item.json msgid "item.potato.name" msgstr "ジャガイモ" -#: src/dusk/rpg/item/item.csv +#: src/dusk/rpg/item/item.json msgid "item.apple.name" msgstr "リンゴ" diff --git a/cmake/targets/psp.cmake b/cmake/targets/psp.cmake index c1b33fda..32a2b5a5 100644 --- a/cmake/targets/psp.cmake +++ b/cmake/targets/psp.cmake @@ -56,6 +56,7 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC DUSK_DISPLAY_HEIGHT=272 DUSK_THREAD_PTHREAD DUSK_TIME_DYNAMIC + DUSK_DISPLAY_OVERSCAN=6 ) if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") diff --git a/src/dusk/rpg/cutscene/cutscene.h b/src/dusk/rpg/cutscene/cutscene.h index bf9d322e..f954455e 100644 --- a/src/dusk/rpg/cutscene/cutscene.h +++ b/src/dusk/rpg/cutscene/cutscene.h @@ -154,6 +154,14 @@ typedef struct cutscene_s { } \ } +// 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) } diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index 95432dda..348de65f 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -7,160 +7,146 @@ #include "rpg/cutscene/cutscenesystem.h" +cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { + [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) { - switch(item->type) { - case CUTSCENE_ITEM_TYPE_TEXT: - cutsceneTextStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_TEXT_MINI: - cutsceneTextMiniStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE: - cutsceneTextMiniHideStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_CALLBACK: - cutsceneCallbackStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_WAIT: - cutsceneWaitStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_CUTSCENE: - if(item->cutscene != NULL) cutsceneSystemStartCutscene(item->cutscene); - break; - - case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT: - cutsceneEntityTeleportStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO: - cutsceneEntityWalkToStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_FADE: - cutsceneFadeStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_SET_PAUSE: - cutsceneSetPauseStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_CONCURRENT: - cutsceneConcurrentStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_ITEM_GIVE: - cutsceneItemGiveStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE: - cutsceneEntityRemoveStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_ENTITY_ADD: - cutsceneEntityAddStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_ENTITY_TURN: - cutsceneEntityTurnStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY: - cutsceneEntityWalkToEntityStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_MAP_AREA_ADD: - cutsceneMapAreaAddStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE: - cutsceneMapAreaRemoveStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT: - cutsceneMapAreaWaitStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_START_BATTLE: - cutsceneStartBattleStart(item, data); - break; - - case CUTSCENE_ITEM_TYPE_EMOJI: - cutsceneEmojiStart(item, data); - break; - - default: - break; - } + cutsceneiteminitcallback_t *init = CUTSCENE_ITEM_CALLBACKS[item->type].init; + if(init != NULL) init(item, data); } -bool_t cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data) { - switch(item->type) { - case CUTSCENE_ITEM_TYPE_TEXT: - return cutsceneTextUpdate(item, data); +bool_t cutsceneItemUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + cutsceneitemupdatecallback_t *update = + CUTSCENE_ITEM_CALLBACKS[item->type].update; + if(update == NULL) return false; - case CUTSCENE_ITEM_TYPE_TEXT_MINI: - return cutsceneTextMiniUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE: - return cutsceneTextMiniHideUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_CALLBACK: - return cutsceneCallbackUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_WAIT: - return cutsceneWaitUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT: - return cutsceneEntityTeleportUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO: - return cutsceneEntityWalkToUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_FADE: - return cutsceneFadeUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_SET_PAUSE: - return cutsceneSetPauseUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_CONCURRENT: - return cutsceneConcurrentUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_ITEM_GIVE: - return cutsceneItemGiveUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE: - return cutsceneEntityRemoveUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_ENTITY_ADD: - return cutsceneEntityAddUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_ENTITY_TURN: - return cutsceneEntityTurnUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY: - return cutsceneEntityWalkToEntityUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_MAP_AREA_ADD: - return cutsceneMapAreaAddUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE: - return cutsceneMapAreaRemoveUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT: - return cutsceneMapAreaWaitUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_START_BATTLE: - return cutsceneStartBattleUpdate(item, data); - - case CUTSCENE_ITEM_TYPE_EMOJI: - return cutsceneEmojiUpdate(item, data); - - default: - return false; - } + return update(item, data); +} + +void cutsceneCutsceneStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + if(item->cutscene != NULL) cutsceneSystemStartCutscene(item->cutscene); +} + +bool_t cutsceneCutsceneUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return false; } diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index 84164c50..46ee3be1 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -21,6 +21,7 @@ #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" @@ -31,6 +32,7 @@ typedef struct cutscene_s cutscene_t; typedef enum { CUTSCENE_ITEM_TYPE_NULL, + CUTSCENE_ITEM_TYPE_TEXT, CUTSCENE_ITEM_TYPE_TEXT_MINI, CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE, @@ -51,7 +53,10 @@ typedef enum { CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, CUTSCENE_ITEM_TYPE_START_BATTLE, - CUTSCENE_ITEM_TYPE_EMOJI + CUTSCENE_ITEM_TYPE_EMOJI, + CUTSCENE_ITEM_TYPE_SHAKE, + + CUTSCENE_ITEM_TYPE_COUNT } cutsceneitemtype_t; struct cutsceneitem_s { @@ -79,6 +84,7 @@ struct cutsceneitem_s { cutscenemapareawait_t mapAreaWait; cutscenestartbattle_t startBattle; cutsceneemoji_t emoji; + cutsceneshake_t shake; }; }; @@ -89,6 +95,24 @@ typedef union cutsceneitemdata_u { cutscenemapareawaitdata_t mapAreaWait; } 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. * @@ -111,3 +135,29 @@ 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 +); diff --git a/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt b/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt index 2cee571a..8f2e44fc 100644 --- a/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt @@ -10,4 +10,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} cutscenetextminihide.c cutscenefade.c cutsceneemoji.c + cutsceneshake.c ) diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneshake.c b/src/dusk/rpg/cutscene/item/ui/cutsceneshake.c new file mode 100644 index 00000000..eb1eca92 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/ui/cutsceneshake.c @@ -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/rpgcamera.h" + +void cutsceneShakeStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + rpgCameraShake(item->shake.amount, item->shake.duration); +} + +bool_t cutsceneShakeUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return true; +} diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneshake.h b/src/dusk/rpg/cutscene/item/ui/cutsceneshake.h new file mode 100644 index 00000000..cc0c0f24 --- /dev/null +++ b/src/dusk/rpg/cutscene/item/ui/cutsceneshake.h @@ -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 "dusk.h" + +typedef struct cutsceneitem_s cutsceneitem_t; +typedef union cutsceneitemdata_u cutsceneitemdata_t; + +typedef struct { + uint8_t amount; + float_t duration; +} cutsceneshake_t; + +/** + * Starts a camera shake item (kicks off the shake on the RPG camera). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneShakeStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates a camera shake item. The shake itself runs asynchronously on + * the RPG camera, so this always completes immediately. + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns true always. + */ +bool_t cutsceneShakeUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); diff --git a/src/dusk/rpg/item/CMakeLists.txt b/src/dusk/rpg/item/CMakeLists.txt index eacbfffe..cbd4416d 100644 --- a/src/dusk/rpg/item/CMakeLists.txt +++ b/src/dusk/rpg/item/CMakeLists.txt @@ -14,9 +14,9 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} # Item Definitions dusk_run_python( - dusk_item_csv_defs + dusk_item_json_defs tools.item - --csv ${CMAKE_CURRENT_SOURCE_DIR}/item.csv + --json ${CMAKE_CURRENT_SOURCE_DIR}/item.json --output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/itemdef.h ) -add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_csv_defs) \ No newline at end of file +add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_json_defs) \ No newline at end of file diff --git a/src/dusk/rpg/item/item.csv b/src/dusk/rpg/item/item.csv deleted file mode 100644 index 56f5453d..00000000 --- a/src/dusk/rpg/item/item.csv +++ /dev/null @@ -1,4 +0,0 @@ -id,type,weight,name -POTION,MEDICINE,1.0,potion -POTATO,FOOD,0.5,potato -APPLE,FOOD,0.3,apple \ No newline at end of file diff --git a/src/dusk/rpg/item/item.json b/src/dusk/rpg/item/item.json new file mode 100644 index 00000000..35aceb4d --- /dev/null +++ b/src/dusk/rpg/item/item.json @@ -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" } +] diff --git a/src/dusk/rpg/rpgcamera.c b/src/dusk/rpg/rpgcamera.c index f717ac9d..d3742d88 100644 --- a/src/dusk/rpg/rpgcamera.c +++ b/src/dusk/rpg/rpgcamera.c @@ -7,12 +7,18 @@ #include "rpgcamera.h" #include "util/memory.h" +#include "util/random.h" #include "rpg/entity/entity.h" #include "rpg/overworld/map.h" #include "assert/assert.h" +#include "time/time.h" #include "display/screen/screen.h" +static const float_t RPG_CAMERA_SHAKE_AMOUNTS[] = { + 0.0f, 0.5f, 1.0f, 2.0f, 3.0f +}; + rpgcamera_t RPG_CAMERA; void rpgCameraInit(void) { @@ -20,6 +26,13 @@ void rpgCameraInit(void) { RPG_CAMERA.projectionDirty = true; } +void rpgCameraShake(uint8_t amount, float_t duration) { + assertTrue(amount <= 4, "Camera shake amount must be between 0 and 4"); + RPG_CAMERA.shakeAmount = RPG_CAMERA_SHAKE_AMOUNTS[amount]; + RPG_CAMERA.shakeDuration = duration; + RPG_CAMERA.shakeTime = 0.0f; +} + void rpgCameraGetPosition(vec3 out) { switch(RPG_CAMERA.mode) { case RPG_CAMERA_MODE_FREE: @@ -67,6 +80,13 @@ void rpgCameraUpdateEye(void) { rpgCameraGetPosition(target); glm_vec3_add(target, (vec3){ 0.5f, 0.5f, 0.5f }, target); + if(RPG_CAMERA.shakeTime < RPG_CAMERA.shakeDuration) { + float_t t = 1.0f - (RPG_CAMERA.shakeTime / RPG_CAMERA.shakeDuration); + float_t magnitude = RPG_CAMERA.shakeAmount * t; + target[0] += randomFloat(-magnitude, magnitude); + target[2] += randomFloat(-magnitude, magnitude); + } + glm_lookat( (vec3){ target[0], target[1] + offset, target[2] + z }, target, @@ -90,6 +110,10 @@ void rpgCameraToScreen(vec3 worldPos, vec2 out) { } errorret_t rpgCameraUpdate(void) { + if(RPG_CAMERA.shakeTime < RPG_CAMERA.shakeDuration) { + RPG_CAMERA.shakeTime += TIME.delta; + } + if(!mapIsLoaded()) errorOk(); vec3 pos; diff --git a/src/dusk/rpg/rpgcamera.h b/src/dusk/rpg/rpgcamera.h index 056cdb55..12742ff7 100644 --- a/src/dusk/rpg/rpgcamera.h +++ b/src/dusk/rpg/rpgcamera.h @@ -30,6 +30,10 @@ typedef struct { mat4 eye; mat4 projection; bool_t projectionDirty; + + float_t shakeAmount; + float_t shakeDuration; + float_t shakeTime; } rpgcamera_t; extern rpgcamera_t RPG_CAMERA; @@ -69,6 +73,17 @@ void rpgCameraUpdateProjection(void); */ void rpgCameraUpdateEye(void); +/** + * Shakes the RPG camera, randomly offsetting its position by a + * decreasing amount over the given duration. + * + * @param amount Shake strength from 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. + * @param duration How long the shake lasts, in seconds. + */ +void rpgCameraShake(uint8_t amount, float_t duration); + /** * Converts a world-space position to screen-space pixel coordinates, * using the camera's current eye and projection matrices. diff --git a/tools/item.py b/tools/item.py index c0637237..f5d8b9c7 100644 --- a/tools/item.py +++ b/tools/item.py @@ -1,9 +1,9 @@ import argparse -import csv +import json import os -parser = argparse.ArgumentParser(description="Item CSV to .h defines") -parser.add_argument("--csv", required=True, help="Path to item CSV file") +parser = argparse.ArgumentParser(description="Item JSON to .h defines") +parser.add_argument("--json", required=True, help="Path to item JSON file") parser.add_argument("--output", required=True, help="Path to output .h file") args = parser.parse_args() @@ -13,26 +13,26 @@ def type_enum(name): def id_enum(name): return "ITEM_ID_" + name.upper() -# Load CSV +# Load JSON item_ids = [] item_types = [] rows = {} -with open(args.csv, newline="", encoding="utf-8") as f: - reader = csv.DictReader(f) - if ( - "id" not in reader.fieldnames or - "type" not in reader.fieldnames or - "name" not in reader.fieldnames - ): - raise ValueError("CSV must have 'id', 'type', and 'name' columns") - for row in reader: - item_id, item_type = row["id"], row["type"] - if item_id not in item_ids: - item_ids.append(item_id) - if item_type not in item_types: - item_types.append(item_type) - rows[item_id] = row +with open(args.json, encoding="utf-8") as f: + entries = json.load(f) + +if not all( + "id" in row and "type" in row and "name" in row for row in entries +): + raise ValueError("Each item must have 'id', 'type', and 'name' fields") + +for row in entries: + item_id, item_type = row["id"], row["type"] + if item_id not in item_ids: + item_ids.append(item_id) + if item_type not in item_types: + item_types.append(item_type) + rows[item_id] = row # Assign enum values: types and IDs each start from 1 with NULL = 0. type_values = {}