Add camera shake

This commit is contained in:
2026-07-11 11:02:09 -05:00
parent fbaa54145e
commit ca02ee0352
15 changed files with 335 additions and 185 deletions
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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 "リンゴ"
+1
View File
@@ -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")
+8
View File
@@ -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) }
+134 -148
View File
@@ -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;
cutsceneiteminitcallback_t *init = CUTSCENE_ITEM_CALLBACKS[item->type].init;
if(init != NULL) init(item, data);
}
case CUTSCENE_ITEM_TYPE_TEXT_MINI:
cutsceneTextMiniStart(item, data);
break;
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_HIDE:
cutsceneTextMiniHideStart(item, data);
break;
return update(item, data);
}
case CUTSCENE_ITEM_TYPE_CALLBACK:
cutsceneCallbackStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_WAIT:
cutsceneWaitStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_CUTSCENE:
void cutsceneCutsceneStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
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;
}
}
bool_t cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
switch(item->type) {
case CUTSCENE_ITEM_TYPE_TEXT:
return cutsceneTextUpdate(item, data);
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:
bool_t cutsceneCutsceneUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return false;
}
}
+51 -1
View File
@@ -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
);
@@ -10,4 +10,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
cutscenetextminihide.c
cutscenefade.c
cutsceneemoji.c
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;
}
@@ -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
);
+3 -3
View File
@@ -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)
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_json_defs)
-4
View File
@@ -1,4 +0,0 @@
id,type,weight,name
POTION,MEDICINE,1.0,potion
POTATO,FOOD,0.5,potato
APPLE,FOOD,0.3,apple
1 id type weight name
2 POTION MEDICINE 1.0 potion
3 POTATO FOOD 0.5 potato
4 APPLE FOOD 0.3 apple
+5
View File
@@ -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" }
]
+24
View File
@@ -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;
+15
View File
@@ -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.
+12 -12
View File
@@ -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,20 +13,20 @@ 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
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("CSV must have 'id', 'type', and 'name' columns")
for row in reader:
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)