79 Commits

Author SHA1 Message Date
YourWishes e3f10e0926 Fix GameCube memory card slot B detection and free-space check
CARD_Mount() alone reliably reports CARD_ERROR_NOCARD for a card physically
present in slot B - unlike slot A, which the IPL polls automatically at
boot, slot B needs an explicit CARD_Probe() first (the standard pattern in
every official devkitPro CARD sample). Also fixes
saveDeviceDolphinCardHasFreeSpace() treating CARD_GetDirectory()'s
CARD_ERROR_NOFILE (a totally empty card, not an actual error) as a hard
failure, which made any card with zero existing save files - slot B's in
this case, since slot A already had leftover test data on it - get
reported as full.

Confirmed via Dolphin with slot A disabled/slot B set to a memory card:
previously reported "no save device found", now correctly detects it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 16:49:18 -05:00
YourWishes d7223d7387 Add PSP/GameCube/Wii save-device availability checks
PSP checks memory-stick reachability via sceIoGetstat. GameCube gets two
devices (CARD_SLOTA/CARD_SLOTB), each mounted via CARD_Init/CARD_Mount and
checked for free space via CARD_GetDirectory/CARD_GetBlockCount. Wii picks
between three storage methods at compile time (DUSK_SAVE_WII_METHOD =
NAND/CARD/SD in wii.cmake, default NAND via ISFS) since real hardware
behavior for the default is unverified.

Also fixes two pre-existing bugs found while build/runtime-testing this
across PPSSPP and Dolphin: wrong libogc language macros in
systemGetLocaleDolphin, and a missing SYS_STDIO_Report(true) call that
silently swallowed all guest console output in Dolphin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 16:20:28 -05:00
YourWishes 45331c2a60 First pass of save 2026-08-14 14:19:44 -05:00
YourWishes 33f50a2c69 Strip legacy save/settings systems for a unified savemanager rewrite; split uielement list out of uielement.c
Removes the per-platform save/savestream/autosave implementations, the settings UI frames, and the autosave overlay, replacing them with a savemanager.c/h + savefile.h/savedevice.h scaffold to build the new save system on top of. Also separates the concrete UI_ELEMENTS registration into uielementlist.h/.c so uielement.c only holds the generic per-element lifecycle logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 12:56:02 -05:00
YourWishes 6c8e4d5cbd Route remaining hardcoded UI strings through the locale system
Main menu buttons, the confirm dialog, battle menu/HUD text, backpack
tab labels, the loading/autosave indicators, and the settings tab
placeholders were all raw English literals bypassing translation.
Loads each through assetLocaleGetString like the rest of the UI, adding
new Init hooks for uibattlehud/uiautosave/uiloading where none existed,
and adds the corresponding message ids to en_US/es_MX/jp_JP.po.

Also widens the settings placeholder buffers to 64 bytes - the es_MX
and jp_JP translations overflowed the previous 32-byte size.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 09:12:57 -05:00
YourWishes 3b7215876a Move test battle setup out of uimainmenu into rpg/battle/testbattle
Keeps the temporary hardcoded-battle hook alongside the rest of the
battle module instead of the main menu UI, so the UI frame only
triggers it rather than owning battle setup code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 16:28:00 -05:00
YourWishes c74f5890bd Remove event for input 2026-08-08 16:25:36 -05:00
YourWishes fbd3c71ba7 Move network code into its own dusknetwork module, gate behind DUSK_NETWORK
Splits the platform-agnostic network core out of dusk into a top-level
dusknetwork module, mirroring the duskgl/dusksdl2 pattern. Adds a
DUSK_NETWORK cmake option (default ON) so builds can exclude all
networking code, including the per-platform implementations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 16:13:09 -05:00
YourWishes 128f9ab9d4 some random fixes 2026-08-08 01:18:07 -05:00
YourWishes 36fb359aa2 Finish animation update loop with loop/pingpong/reverse/stop flags, clean up keyframe sampling, add coverage
- animationUpdate now advances and resolves boundary crossings for
  ANIMATION_FLAG_LOOP, ANIMATION_FLAG_PINGPONG, ANIMATION_FLAG_REVERSE, and
  the STOP_BEGINNING/STOP_END flags, firing onLoop/onComplete appropriately;
  guards against LOOP+PINGPONG being set together and moves the
  duration-must-be-positive check into animationInit
- keyframeGetValue clamps to the last keyframe's value instead of dividing
  by zero once time reaches it, and its keyframe walk drops a branch that's
  unreachable after that clamp
- Adds test/animation/test_animation.c covering init, per-layer sampling,
  and the full animationUpdate flag matrix
2026-08-06 15:29:14 -05:00
YourWishes 1bd73d69fe Clamp keyframe interpolation to last value, add main menu scene/UI, battle HUD, and expanded test coverage
- keyframeGetValue now returns the last keyframe's value for times at or
  beyond it, fixes a missing util/math.h include, and asserts keyframes are
  sorted by time; adds test/animation/test_keyframe.c
- Adds mainmenu scene/UI and a battle HUD UI frame
- Adds save autosave-related fields and battle scene tweaks
- Adds headless test coverage for cutscenes, entities, and map areas
2026-08-06 12:58:07 -05:00
YourWishes fb48285143 Rebuild battle flow as an explicit state machine with cutscene hooks
Replace the one-fighter-at-a-time turn model with an OPENING/PRE_ROUND/
PLAYER_SELECTION/AI_SELECTION/MOVES_EXECUTING/POST_ROUND/ENDED state
machine and a per-fighter action queue, so actions are decided before any
of them execute (needed for speed-ordered resolution) and so a cutscene
can pause the battle, wait for a specific state, and force a fighter's
action -- enabling automated, fully-scripted, and partially-scripted
battles. Adds CUTSCENE_PAUSE_BATTLE plus CUTSCENE_BATTLE_WAIT_STATE and
CUTSCENE_BATTLE_FORCE_ACTION cutscene items, and generic onStateChanged/
onActionDecided callbacks on battle_t. Re-enables the long-dormant
test/rpg suite and adds test/rpg/battle covering the state machine and
the new cutscene hooks end-to-end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 08:49:42 -05:00
YourWishes 3de50b8370 Lock in Chapter 1's literal opening scene
The village square at the mountain's foot, party roles established
(tank/paladin leader, ranged ranger, support mage, melee rogue), the
funds bicker, and the cowgirl teleporter's too-good-to-question,
one-way-only offer up the mountain - the innocent-sounding line that
seeds the whole chapter's central complication. Adds character stubs
for the three other party members and the teleporter, and confirms
the bubbly ranger as the first to abandon the mission.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 08:11:54 -05:00
YourWishes e61cbe25b7 Add three of the mentor's other former squadron members
Introduces Strider (a merc who protects those who can't afford real
protection, and has found genuine peace in it), Downes (retired to
farming and family, forced back into one last fight), and Egor (Royal
Guard, honor curdled into blind devotion, dies still certain of
himself). Together with the party leader, they give the same
mentor/same night four distinct answers to what honor means. Updates
themes and open questions accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 06:54:49 -05:00
YourWishes 8395830be6 Introduce the Detective, kept deliberately thin by design
Reveals him as the presence following the party since Chapter 1 - a
king's agent who frees the protagonist from execution in early
Chapter 2, drops a small, cagey lead to the newly-dismissed party
leader, and then vanishes to chase other threads. Documents these
early Chapter 2 beats, notes culinary magic is now branded "the
forbidden magic" per the king's cover story, and updates open
questions/themes accordingly. His entry is intentionally sparse -
the character is meant to stay a mystery for as long as possible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 23:47:50 -05:00
YourWishes 6e4ec2b9d8 Add the party leader's backstory and his mentor
Ties the party leader's rigid, orders-above-all sense of honor back
to a mentor he idolized as a young soldier - one who broke orders on
the night of the Great Explosion to keep his squadron out of the
massacre, then died in it. The party leader doesn't know why at the
time; his eventual realization that his mentor's disobedience was
itself the honorable act becomes his arc's throughline. Also notes
the rest of that squadron, now scattered, as a future plot thread.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 23:37:18 -05:00
YourWishes f501bb8e28 Detail the true events of the Great Explosion
Replaces the vague placeholder with the full account: the king's
personal, in-person purge of the culinary mages' hometown, the failed
hostage surrender, the town's collective final spell (built on a
shared folk song) that becomes the Great Explosion, and the
protagonist's survival via her mother's protective spell. Also adds
the king's official cover story (framing the mages as insurgents),
new characters (the Great Mage and the protagonist's mother), and
updates themes/open questions accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 23:29:39 -05:00
YourWishes d07cd3397d Start documenting the story: premise, structure, and Chapter 1
Adds story.md covering the overarching premise (a prophecy caused by
the attempt to prevent it), the eight-movement story structure, the
Great Explosion backstory, core characters, and a detailed breakdown
of Chapter 1's opening - the bounty party's retrieval of the
protagonist, the mountain teleportation-logistics trap that splits
the party, and the confrontation with the king.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 23:16:46 -05:00
YourWishes e008fb108a Embed default font as a hard-coded bitmap instead of loading it as an asset
Moves FONT_DEFAULT and its init/dispose into their own font.h/font.c,
and rebuilds it from a static glyph bit array + a runtime-generated
texture/tileset rather than loading ui/minogram.png/.dtf through the
asset system. The engine now always has a usable font to render with,
even if asset loading fails. The now-unused minogram assets are removed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 22:37:22 -05:00
YourWishes 9aaffff7a8 Add autosave system with forced no-save-device prompt on failure
autoSaveQueue() is a single callable entry point for requesting an
autosave; autoSaveUpdate() pumps the queue each frame and, if the write
fails (e.g. no memory card), forces the existing no-card modal back
open and pauses world simulation until the player retries or accepts a
temporary session. A small overlay shows "SAVING" while a write is in
flight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 22:05:43 -05:00
YourWishes 7357b4a5df Localize remaining hardcoded UI strings and persist language preference
Replaces the last hardcoded English strings (initial-scene modals, game
menu save status messages) with locale-loaded text, adds a shared
LOCALE_LIST so the settings dropdown and locale manager stay in sync,
and persists the chosen language into savemeta_t across all platforms.
Also fixes two message buffers that were too small for their longest
translations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 21:39:24 -05:00
YourWishes 1ddc298a74 Only update overworld entities while the overworld scene is active
Entities (and the player's pause-to-open-game-menu handling with them)
previously kept updating every frame regardless of the active scene,
matching an existing TODO in rpgUpdate(). Gating the entity loop itself
means the game menu (and any other entity-driven input) naturally can't
trigger mid-battle or before the initial scene hands off to the
overworld, without needing a scene check at each individual call site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:54:58 -05:00
YourWishes e2a9442aa6 Add initial boot scene to check/prompt for save data before overworld
A new SCENE_TYPE_INITIAL now runs before the overworld: it checks
save-device availability and existing save data, then shows one of two
new dedicated modals - "no save device found" (Retry / Continue Anyway)
or "no save data found, create one?" (Yes / No) - before handing off to
the overworld. Both modals are self-contained UI elements mirroring
uiconfirm.h's shape, registered like any other global UI element.

Choosing "Continue Anyway" marks the session temporary (SAVE.temporary,
folded into saveIsAvailable()) so saving stays disabled for the rest of
the session instead of silently retrying, and the game menu's Save
action now reports that distinctly instead of the generic "no device"
message.

Also removes rpg.c's leftover TEST block (unconditional player-name
stamp + save write on every boot) now that this real flow owns save
creation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:53:01 -05:00
YourWishes aa0180571e Unify save system into one save.h/.c; diverge storage format per platform
Renames savefile_t to saveslot_t and folds last session's standalone
settings.h/.c module back in as savemeta_t, so there's one save system
(SAVE.slots[] + SAVE.meta) instead of two parallel ones - while letting
each platform pick its own physical format for the two concepts:

- Linux now writes human-editable JSON (slot0.json, settings.json, ...)
  via yyjson's mutable writer API, so players can hand-fix a bad setting.
- PSP folds meta into the same sceUtilitySavedata binary payload as its
  one save slot (SAVE_SLOT_COUNT_MAX=1 there - a future save picker will
  let players manage multiple named saves via the OS's own browser).
- GameCube consolidates the 3 per-slot memory card files and the separate
  settings file into one combined card file.

Also fixes two bugs surfaced while building this: the CRC finalize step
seeked to a hardcoded offset (only safe for one section per file, breaks
once meta+slots share a buffer), and save.c's async/sync dispatch left an
unconditional fallback call that doesn't exist on PSP-only platforms.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 18:35:06 -05:00
YourWishes 7f7be39230 Split device settings out of the per-slot save file
Deadzone (and future prefs like locale) now live in their own
settingsfile_t/settings.c, loaded eagerly at boot and saved immediately
on Apply, instead of inside savefile_t - a setting shouldn't reset or
diverge just because the player is on a different save slot, and this
also fixes settings changes not actually reaching disk until the next
full game Save.

PSP settings use a new plain sceIo path rather than sceUtilitySavedata,
since that dialog would flash its native icon on every settings tweak.
GameCube reuses the save system's existing memory card mount rather than
mounting it twice (settingsInit() now runs after saveInit()).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 13:54:15 -05:00
YourWishes 4d95415232 Move global item collected-state, deadzone, and story flags into save file
Adds three new pieces of save-file state, all following the same shape:
the save file is the single source of truth, not a separate live runtime
copy that gets synced in/out.

- globalitemstore.h/.c: per-global-entity-ID "collected" flags
  (savefile_t.globalItemCollected), so a global item entity's init
  callback can check whether it was already picked up in a prior session
  without needing to keep the entity itself alive to remember that.

- Gamepad deadzone: removed input_t.deadzone entirely. The setting UI and
  every platform's actual deadzone-applying code (inputGetDeadzoneDolphin/
  SDL2, previously hardcoded per-platform literals that the settings menu
  didn't actually affect) now read savefile_t.deadzone directly via
  saveGet(SAVE_ACTIVE_SLOT). Default lives in savefile.h
  (SAVE_DEADZONE_DEFAULT), stamped onto every slot in saveInit().

- Story flags: STORY_FLAG_VALUES (a live, codegen-initialized array) is
  replaced by savefile_t.storyFlags, read/written via the existing
  storyFlagGet()/storyFlagSet() call sites (now macros/functions over the
  active save file instead of a separate array). tools/story.py now
  generates STORY_FLAG_DEFAULTS (const) instead; storyFlagInitDefaults()
  stamps those onto a save the first time it's used (file->exists false),
  called from rpgInit().

Added SAVE_ACTIVE_SLOT (0) to savefile.h as the one shared "which slot is
actually being played" constant, replacing three different local/implicit
0s (uigamemenu.c, rpg.c, and now the settings/input call sites).

Verified round-trip on Linux (all three together in one save/load cycle);
both Linux and PSP build clean.
2026-08-04 11:10:44 -05:00
YourWishes 2cbd80a004 Check for existing save data before saving, confirm before creating new
uiGameMenuSave() now attempts a real load first (via the existing generic
saveLoad()/saveExists() primitives) instead of writing blind. If a save
already exists, it saves straight over it as before. If not, it prompts
via the existing uiConfirm dialog ("No save data found. Create a new
save?") before writing - this is exactly the flow GameCube needs (no
native OS save browser to lean on, unlike PSP), but implemented generically
so it also applies correctly on every other platform without any
platform-specific UI code: saveIsAvailable()/saveExists() already reflect
each platform's real state (e.g. Dolphin's memory-card presence and
existing-file checks), so the same logic just does the right thing
everywhere.

Verified the two branches directly on Linux (temporarily wiring the same
saveLoad -> check -> uiConfirmOpen sequence into rpgInit): with no save
file, saveExists() is false and the confirm dialog opens; with one already
written, it's true and the confirm dialog is correctly skipped. Not
verified via actual menu navigation (no input-injection tooling available
here) or on Dolphin (no devkitPPC toolchain in this environment).
2026-08-04 10:33:57 -05:00
YourWishes 9abf8101da PSP: save through the real sceUtilitySavedata API, not raw file I/O
Rewrote savepsp.c/savestreampsp.c to use sceUtilitySavedataInitStart/
Update/GetStatus/ShutdownStart instead of sceIoOpen/Read/Write, so PSP
saves get a proper OS-generated PARAM.SFO (title/savedataTitle/detail) and
show up correctly in the native save browser.

This dialog spans multiple frames and, per this project's prior experience
with the network config dialog, must be pumped non-blocking one step per
real engine frame rather than blocked on synchronously - a raw-sceGu
blocking loop already froze the app on real hardware for that dialog,
since pspGL owns the GU context. So save.h's saveWrite()/saveLoad() are
now callback-based (savecallback_t onComplete) instead of returning a
result directly, mirroring networkRequestConnection()'s shape, with a new
saveUpdate() (wired into engineUpdate()) pumping the active op each frame.
Linux/Dolphin behavior is unchanged - their fallback path in save.c still
completes synchronously, just via an immediate callback call instead of a
direct return.

Two real bugs found via PPSSPP testing (not just code review): SAVE/LOAD
modes show a confirm screen even for brand-new data, which blocks forever
headlessly - switched to AUTOSAVE/AUTOLOAD, which write/read silently and
generate the identical PARAM.SFO. And PPSSPP's dialog status goes straight
from QUIT to NONE without a separately observable FINISHED in between,
which the first version misread as "disappeared without a result" even on
a successful save - fixed by tracking whether QUIT was already seen.

Confirmed end-to-end in PPSSPP: write, dialog completes, PARAM.SFO +
encrypted save.bin appear on the virtual memory stick, and a subsequent
load decrypts/deserializes back to the exact original data. Not tested on
real PSP hardware.
2026-08-04 09:54:45 -05:00
YourWishes 24badd06a5 Add player name field to save file, save it out as a round-trip test
Adds savefile_t.playerName (SAVE_PLAYER_NAME_MAX) serialized via the
existing saveFileReadString/WriteString helpers, and stamps + saves it in
rpgInit() as a test that the save system now persists actual game data,
not just the header/version. Verified manually: written bytes end in
"Dusk\0" immediately after the version field, and loading it back returns
the same string.
2026-08-04 08:59:29 -05:00
YourWishes 7a03ef8eaf Re-enable save system, fix header/version stamping, handle missing media
- Fixed the actual reason saving never worked on any platform: saveWrite()
  never stamped file->header/file->version before serializing, so every
  written save file had a zeroed magic header and failed its own
  validation on the next load. Confirmed via a manual write/load round
  trip that this alone fully explains "saving doesn't work."
- Re-enabled saveInit()/saveDispose() in engine.c (previously commented out
  under "Temporarily disable save code").
- Added SAVE.available + saveIsAvailable(), refreshed by every real
  save/load/delete attempt. saveInit() no longer treats an unreachable
  save medium as fatal to booting - it logs and continues, since a missing
  memory card/stick shouldn't prevent playing.
- Hardened PSP's saveInitPSP() to actually detect a missing memory stick
  (sceIoGetstat on ms0:/) instead of assuming success, and fixed
  single-level sceIoMkdir to build the full PSP/SAVEDATA directory chain.
- Added busy-retry (CARD_ERROR_BUSY) and a not-mounted guard to Dolphin's
  live savestreamdolphin.c path, extending the same handling already
  backported into savedolphin.c.
- Added a "Save" entry to the game menu wired to saveWrite(0), showing a
  clear message on success, on failure, and when saveIsAvailable() is false.
2026-08-04 08:30:24 -05:00
YourWishes f3ea507313 Fixed save crash
Backported from branch ac2 (commit 85b61097) - CARD_Mount was being called
without CARD_Init first, leaving per-channel control blocks and the DSP
unlock sequence unset. On real Dolphin/hardware this surfaced as a hard
MMIO crash instead of a clean CARD_ERROR_* failure.

Co-Authored-By: Dominic Masters <dominic@domsplace.com>
2026-08-04 08:12:57 -05:00
YourWishes 4b0388a0e1 Chunk streaming concurrency, entity slot fix, and map-data-driven spawns
- Allow 2 chunks to be mid-load concurrently instead of 1 (MAP_CHUNK_LOAD_CONCURRENCY).
- Fix entitySetChunk silently losing track of an entity when its target chunk's
  entity slots are full - it now stays detached (and retries later) instead of
  claiming a chunk that never actually registered it.
- DCF format bumped to v5: chunks can now declare entity spawns (global/NPC via
  the existing entityglobal registry, or one-shot item pickups) and map area
  triggers, resolved via a new callback-ID registry (mapareagloballist.h)
  mirroring the entity one. rpg.c's hardcoded TEST entity/item/area spawns are
  gone - chunk_0_0_0.json now carries that data instead. The player is still
  bootstrapped in code since it isn't map-authored content.
2026-08-04 07:37:48 -05:00
YourWishes a84137b5ff del md 2026-08-04 06:58:03 -05:00
YourWishes ca02ee0352 Add camera shake 2026-07-11 11:02:09 -05:00
YourWishes fbaa54145e Fixed dolphin rendering. 2026-07-10 20:15:26 -05:00
YourWishes 28754ffbf2 Removed old scripted types 2026-07-10 13:24:21 -05:00
YourWishes 470c0eba7a Whatever, some minor map chunking improvements 2026-07-10 12:57:31 -05:00
YourWishes 7098dcec43 Cleaned some log 2026-07-09 23:25:43 -05:00
YourWishes 07137f57af Assets are slightly optimized 2026-07-09 23:25:26 -05:00
YourWishes 8b7491a3d3 Emoji support to characters 2026-07-09 13:18:48 -05:00
YourWishes 8cfa8ddfeb Weaather baseline 2026-07-08 12:57:13 -05:00
YourWishes ef284a15a1 pre-cache shader matrices 2026-07-08 12:36:40 -05:00
YourWishes 3723921573 Render culling on sceneoverworld.h 2026-07-08 12:02:41 -05:00
YourWishes 195399635e Updating mini textbox 2026-07-08 11:45:10 -05:00
YourWishes 46e2a924d3 Mini textboxes 2026-07-08 10:53:53 -05:00
YourWishes b693ea4102 Starting item and battle stuff 2026-07-08 10:05:21 -05:00
YourWishes a73f55beb0 Battle stuff
Build Dusk / build-linux (push) Successful in 7m5s
Build Dusk / build-psp (push) Successful in 1m33s
Build Dusk / build-knulli (push) Successful in 4m0s
Build Dusk / build-gamecube (push) Successful in 3m31s
Build Dusk / build-gamecube-iso (push) Successful in 3m39s
Build Dusk / build-wii (push) Successful in 3m5s
Build Dusk / build-wii-iso (push) Successful in 3m25s
2026-07-08 07:55:15 -05:00
YourWishes 0bd2491ab7 Dropdown test. 2026-07-07 15:18:33 -05:00
YourWishes 860c797c9c Last round of cleanup for settings for now. 2026-07-07 14:56:08 -05:00
YourWishes 38c5080f9f Collapsing more ui settings code 2026-07-07 14:36:19 -05:00
YourWishes fae191d8fe Settings improvements 2026-07-07 14:00:37 -05:00
YourWishes d83a953e2d First pass of language 2026-07-07 11:14:02 -05:00
YourWishes 2dcf0d0f0d UI Confirm 2026-07-07 10:12:22 -05:00
YourWishes 6dee37d8c1 Slider widget and some cleanup 2026-07-07 09:58:10 -05:00
YourWishes 3bad03afb3 Item entity 2026-07-07 07:52:22 -05:00
YourWishes 6a6d8448f7 Area cutscene controls 2026-07-07 07:28:12 -05:00
YourWishes 189babd2cf Trigger types on areas 2026-07-07 07:07:32 -05:00
YourWishes 988a0f2294 Area basics 2026-07-06 23:19:05 -05:00
YourWishes 85bf455731 entity pos sets chunk now 2026-07-04 18:43:09 -05:00
YourWishes 589e4224f3 Example global ent 2026-07-04 13:03:24 -05:00
YourWishes bd7fa154b4 Add user data to callbacks 2026-07-04 09:13:37 -05:00
YourWishes a292f1992b Change to use the Z tile system 2026-07-04 08:40:53 -05:00
YourWishes 88ddf429b7 add some entity management in prep for global entities. 2026-07-03 12:25:24 -05:00
YourWishes 7a1f6662df More cutscene tools 2026-07-03 09:17:49 -05:00
YourWishes afc68bccc6 Update test runner
Build Dusk / build-linux (push) Successful in 5m36s
Build Dusk / build-psp (push) Successful in 1m15s
Build Dusk / build-knulli (push) Successful in 4m53s
Build Dusk / build-gamecube (push) Successful in 3m51s
Build Dusk / build-gamecube-iso (push) Successful in 3m52s
Build Dusk / build-wii (push) Successful in 3m17s
Build Dusk / build-wii-iso (push) Successful in 4m54s
2026-07-02 19:20:17 -05:00
YourWishes b4cdc4a64f Add remove event
Build Dusk / run-tests (push) Failing after 5m7s
Build Dusk / build-linux (push) Successful in 5m41s
Build Dusk / build-psp (push) Successful in 1m17s
Build Dusk / build-knulli (push) Successful in 4m22s
Build Dusk / build-gamecube (push) Successful in 3m43s
Build Dusk / build-gamecube-iso (push) Successful in 3m36s
Build Dusk / build-wii (push) Successful in 3m36s
Build Dusk / build-wii-iso (push) Successful in 3m22s
2026-07-02 16:10:47 -05:00
YourWishes 3ad5afb81c Fix item crash 2026-07-02 16:04:41 -05:00
YourWishes bed3f20118 Lot of cutscene cleanup 2026-07-02 15:53:02 -05:00
YourWishes de67315178 Cutscene cleanup 2026-07-02 14:59:41 -05:00
YourWishes 8d5c0c7cad Path finding (first pass) 2026-07-02 14:08:59 -05:00
YourWishes a8271e01bd Fade cutscene items 2026-07-02 13:26:41 -05:00
YourWishes 900b3f8558 More cutscene stuff 2026-07-02 12:54:12 -05:00
YourWishes fdc4e056f9 Cutscene defs 2026-07-02 11:32:31 -05:00
YourWishes 7b98e40ccf Cutscene tests 2026-07-02 11:18:36 -05:00
YourWishes 01d89cf22c Map tweaks 2026-07-01 21:07:24 -05:00
YourWishes 503a3c799a Grid to editor 2026-07-01 20:51:46 -05:00
YourWishes 0b21388844 Fixing bugs, one at a time 2026-07-01 20:23:44 -05:00
YourWishes 117bdf0c00 Fov tweaks 2026-07-01 20:18:32 -05:00
YourWishes 172dc5d37b Tile Z is now hypotenused to 1 rather than stretching to have Z of 1. 2026-07-01 16:27:59 -05:00
421 changed files with 17317 additions and 5949 deletions
-30
View File
@@ -4,36 +4,6 @@ on:
tags:
- '*'
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:
runs-on: ubuntu-latest
steps:
+35
View File
@@ -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
-455
View File
@@ -1,455 +0,0 @@
# Dusk — Claude Code rules
## File headers
Every C, H, and JS file starts with:
```c
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
```
JS files use `//` comment style instead.
---
## C conventions
### Types
Always use the project-defined aliases instead of bare C primitives:
| Use | Not |
|-----------|--------------|
| `bool_t` | `bool` |
| `int_t` | `int` |
| `float_t` | `float` |
| `char_t` | `char` |
Use `uint8_t`, `uint16_t`, `int32_t`, etc. for fixed-width integers.
All struct and enum types end in `_t` (`animation_t`, `errorret_t`, …).
### Naming
- **Functions** — snake_case, prefixed with their module:
`assetLock()`, `entityPositionInit()`, `moduleAssetBatchCtor()`
- **Struct fields** — camelCase: `keyframeCount`, `localPosition`
- **Macros / constants** — UPPER_SNAKE_CASE:
`ENTITY_ID_INVALID`, `ERROR_OK`, `COMPONENT_TYPE_COUNT`
- **Files** — snake_case matching the primary type: `entityposition.c`,
`moduleassetbatch.c`
### Header files (`.h`)
- Use `#pragma once` — no include guards.
- Declare every public function, `#define`, and `extern` global.
- Write a JSDoc block (`/** … */`) above every declaration explaining
purpose, `@param`s, and `@returns`.
- Only include headers that the `.h` file itself strictly requires for
the types it exposes. Move everything else to the `.c` file.
Do not use forward declarations as a workaround — use the real
include in the `.c` file instead.
### Implementation files (`.c`)
- Contain function bodies only; no declarations.
- Pull in whatever additional includes the implementation needs.
- Do not use `static` or `inline` on **functions**. Every function,
including internal helpers, must be declared in the matching `.h` and
defined in the `.c` file. Internal helpers belong near the bottom of
the `.c` file, not at the top with a `static` qualifier.
`static` and `inline` on functions are only appropriate when the
function body is written directly inside a `.h` file.
`static` on **variables** (file-scope state) is fine and expected.
### Formatting
- Hard-wrap all lines at **80 characters**.
### Error handling
Return `errorret_t` from fallible functions. Use these macros:
```c
errorOk(); // return success
errorThrow("msg %d", val); // return failure with message
errorChain(someCall()); // propagate failure, continue on success
errorIsOk(ret) / errorIsNotOk(ret) // test a result
errorCatch(ret); // handle + free an error
```
Never return raw error codes or use `errno` for in-engine errors.
### Memory
Use the project allocator — never raw `malloc`/`free`:
```c
memoryAllocate(size) // allocate
memoryFree(ptr) // free
memoryZero(dest, size) // zero a block
memoryCopy(dest, src, size) // copy
```
### Asserts
Prefer specific assert macros over bare `assert()`:
```c
assertNotNull(ptr, "msg");
assertTrue(cond, "msg");
assertFalse(cond, "msg");
assertUnreachable("msg");
assertIsMainThread("msg");
```
---
## Build system
Each subdirectory has its own `CMakeLists.txt` that adds sources with:
```cmake
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
myfile.c
)
```
Never add source files to the root `CMakeLists.txt` directly.
---
## Platform support
### Targets
Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
| `DUSK_TARGET_SYSTEM` | Macro defined | Platform |
|----------------------|-------------------|------------------|
| `linux` | `DUSK_LINUX` | Linux desktop |
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
| `psp` | `DUSK_PSP` | Sony PSP |
| `vita` | `DUSK_VITA` | PlayStation Vita |
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
| `wii` | `DUSK_WII` | Nintendo Wii |
### Layer structure
```
src/dusk/ core, platform-agnostic game logic
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita)
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita)
src/dusklinux/ Linux + Knulli platform impl
src/duskpsp/ PSP platform impl
src/duskvita/ Vita platform impl
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
```
Dolphin is the only target that bypasses SDL2 and OpenGL entirely —
it uses native GameCube/Wii rendering and input APIs.
### Platform guards
Use the compile-time macros for platform-specific code:
```c
#ifdef DUSK_PSP
// PSP-only path
#elif defined(DUSK_GAMECUBE) || defined(DUSK_WII)
// GameCube / Wii path
#else
// Generic / Linux fallback
#endif
```
Additional capability macros set per-target:
`DUSK_SDL2`, `DUSK_OPENGL`, `DUSK_OPENGL_ES`, `DUSK_OPENGL_LEGACY`,
`DUSK_INPUT_GAMEPAD`, `DUSK_INPUT_KEYBOARD`, `DUSK_INPUT_POINTER`,
`DUSK_PLATFORM_ENDIAN_BIG` / `DUSK_PLATFORM_ENDIAN_LITTLE`.
### Abstraction pattern
Platform-specific implementations are wired in via `#define` macros in
each platform's `displayplatform.h` / `inputplatform.h` etc., which
the core calls through. Functions that a platform does not support are
simply left undefined — the core guards calls with `#ifdef`.
### Adding platform-specific code
- Put it under `src/dusk<platform>/` in the matching subsystem folder.
- Gate any core call-site with the appropriate `#ifdef DUSK_<PLATFORM>`
or capability macro.
- Keep the `src/dusk/` core free of platform ifdefs — delegate through
the platform header macros instead.
---
## Adding a new asset loader type
1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in
`src/dusk/asset/loader/assetloader.h`.
2. Add fields to the input/loading/output unions in `assetloader.h`.
3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and
`assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory.
4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in
`src/dusk/asset/loader/assetloader.c`.
5. If user-facing, create a JS module (see below) and a `.d.ts` file.
---
## Adding a new entity component
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
`entityMyCompDispose()`.
2. Add the include to `src/dusk/entity/componentlist.h` header block.
3. Add a row to `src/dusk/entity/componentlist.h`:
```c
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
```
This auto-generates the enum, union field, and definition entry.
4. If JS-facing, create the script module and `.d.ts` (see below).
---
## Adding a new script (JS) module
1. Create `src/dusk/script/module/<category>/moduleMyMod.h/.c`.
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
- Use `moduleBaseFunction(name)` to define JS-callable functions.
- Register props/funcs in `moduleMyModInit()` with
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
`scriptProtoDefineStaticFunc`.
2. `#include` the header in
`src/dusk/script/module/modulelist.c` and call
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
`moduleListDispose()`).
3. For component modules also register in
`src/dusk/script/module/entity/component/modulecomponentlist.c`
so `entity.add()` returns the typed wrapper.
4. Create `types/<category>/mymod.d.ts` and add a
`/// <reference path="..." />` line to `types/index.d.ts`.
---
## Script module type declarations
Whenever a `src/dusk/script/module/**/*.c` file is created or modified,
check whether the corresponding `types/**/*.d.ts` needs updating and
apply any changes before finishing the task.
---
## JavaScript (asset scripts)
- Use `var` for module-level state; `const` for values that never
change.
- Always use semicolons.
- Scene objects are plain objects (`var scene = {}`) with assigned
methods.
- Export via `module.exports = scene`.
- Async scene init should use `async function` and `await`.
---
## Coding style
### ASCII only
Source files (`.c`, `.h`, `.js`) must contain only ASCII characters (U+0000U+007F).
Non-ASCII characters are banned even in comments and string literals.
Use ASCII-only substitutes instead:
- `--` or `-` instead of `` (em dash)
- `->` instead of `` (arrow)
- `x` or `*` instead of `×` (multiplication)
Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text.
### Indentation
2 spaces. No tabs.
### Keyword and operator spacing
No space between a keyword or function name and its opening parenthesis:
```c
if(!ptr) return;
for(uint8_t i = 0; i < count; i++) {
while(entry->state != DONE) {
switch(type) {
sizeof(assetbatch_t)
memoryZero(ptr, size)
```
Spaces around all binary operators and after every comma:
```c
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
(size_t)end - (size_t)start
foo(a, b, c)
```
### Braces
Opening brace on the **same line** as the statement (K&R style) for all
constructs — functions, `if`, `else`, `for`, `while`, `switch`:
```c
void assetEntryLock(assetentry_t *entry) {
...
}
if(dirty) {
...
} else {
...
}
```
### Guard returns
Short guards go on one line with no braces:
```c
if(!ptr) return;
if(!b || !b->batch) return jerry_undefined();
if(!(flags & DIRTY)) return;
```
### Blank lines
- One blank line between functions; no blank line at the start or end of
a function body.
- One blank line between logical blocks inside a function body.
- No trailing blank lines at the end of a file.
### Pointer placement
`*` is attached to the variable name, not the type:
```c
assetentry_t *entry
const char_t *name
void *ptr
uint8_t *d = (uint8_t *)dest;
```
### Casts
Space between cast and operand:
```c
(assetbatch_t *)user
(uint8_t *)dest
(textureformat_t)v
```
### Return
No parentheses around the return value:
```c
return ptr;
return MEMORY_POINTERS_IN_USE;
```
### switch / case
`case` indented 2 spaces from `switch`; body indented 2 more from `case`:
```c
switch(type) {
case ASSET_LOADER_TYPE_TEXTURE:
descs[i].input.texture = (textureformat_t)v;
break;
default:
break;
}
```
### Multi-line function signatures
When parameters don't fit on one line, put each on its own line indented
2 spaces; the closing `) {` (definition) or `);` (declaration) goes on
its own line at column 0:
```c
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
errorret_t memoryCompare(
const void *a,
const void *b,
const size_t size
);
```
### Structs and enums
Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing
brace and name on the same line:
```c
typedef struct {
errorcode_t code;
char_t *message;
} errorstate_t;
typedef enum {
ASSET_LOADER_TYPE_NULL,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
```
### Designated initialisers
Spaces inside braces; `.field = value`:
```c
jsassetentry_t e = { .entry = entry };
assetbatchloadedpend_t init = { .batch = batch };
```
### Ternary operator
Spaces around `?` and `:`:
```c
const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;
```
### const placement
`const` before the type, `*` attached to the variable:
```c
const char_t *name
const void *src
const size_t size
```
### Comments in `.c` files
- Do not use section dividers (`/* ---- ... ---- */`). Just let the
functions follow one another with a single blank line between them.
- Multi-line explanatory comments inside function bodies use `//` lines:
```c
// Script modules are freed; orphaned JS wrapper objects now get GC'd
// so their finalizers fire before assetDispose() checks ref counts.
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
```
- Do not use `/* */` for inline or inline-block comments inside `.c`
function bodies.
### Comments in `.h` files
Every public declaration gets a Javadoc block (`/** … */`) with
`@param` and `@returns` where relevant. Keep it on the lines immediately
above the declaration with no blank line in between.
---
## Color system
Colors are defined in `src/dusk/display/color.csv` and code-generated
into a `color.h` header by `tools/color/csv/__main__.py`.
Each row in the CSV has `name,r,g,b,a` with channel values in `[0.0, 1.0]`.
The script emits four `#define` variants per color plus a bare alias:
```
COLOR_<NAME>_4B color4b(r8, g8, b8, a8) // default alias target
COLOR_<NAME>_3B color3b(r8, g8, b8)
COLOR_<NAME>_3F color3f(rf, gf, bf)
COLOR_<NAME>_4F color4f(rf, gf, bf, af)
COLOR_<NAME> COLOR_<NAME>_4B
```
`color_t` is `color4b_t` (four `uint8_t` channels).
To add a new color, append a row to `color.csv` and rebuild — do not
hand-edit the generated header.
---
## Tests
- Tests live in `test/` mirroring `src/dusk/` structure.
- Use cmocka; include `dusktest.h`.
- Test functions: `static void test_something(void **state)`.
- After each test, assert `memoryGetAllocatedCount() == 0` to catch
leaks.
- Build with `-DDUSK_BUILD_TESTS=ON`.
+7
View File
@@ -13,6 +13,7 @@ cmake_policy(SET CMP0079 NEW)
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
option(DUSK_BUILD_TESTS "Enable tests" OFF)
option(DUSK_NETWORK "Enable network support" ON)
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
@@ -90,6 +91,12 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
DUSK_VERSION="${DUSK_VERSION}"
)
if(DUSK_NETWORK)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_NETWORK
)
endif()
# Toolchains
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-21
View File
@@ -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();
});
+176 -43
View File
@@ -10,51 +10,184 @@ msgid "ui.title"
msgstr ""
"Welcome"
#: ui/user.c:22
msgid "ui.greeting"
msgstr "Hello, %s!"
msgid "save.linux.mkdirp_failed"
msgstr "Failed to create save directory, check the disk is not full or write-protected."
#: ui/files.c:40
msgid "ui.file_status"
msgstr "%s has %d files."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general"
msgstr "General"
#: ui/cart.c:55
msgid "cart.item_count"
msgid_plural "cart.item_count"
msgstr[0] "%d item"
msgstr[1] "%d items (dual)"
msgstr[2] "%d items (few)"
msgstr[3] "%d items (many)"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.input"
msgstr "Input"
#: ui/notifications.c:71
msgid ""
"ui.multiline_help"
msgstr ""
"Line one of the help text.\n"
"Line two continues here.\n"
"Line three ends here."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.display"
msgstr "Display"
#: ui/errors.c:90
msgid ""
"error.upload_failed.long"
msgstr ""
"Upload failed for file \"%s\".\n"
"Please try again later or contact support."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.audio"
msgstr "Audio"
#: ui/messages.c:110
msgid ""
"user.invite_status"
msgid_plural ""
"user.invite_status"
msgstr[0] ""
"%s invited %d user.\n"
"Please review the request."
msgstr[1] ""
"%s invited %d users (dual).\n"
"Please review the requests."
msgstr[2] ""
"%s invited %d users (few).\n"
"Please review the requests."
msgstr[3] ""
"%s invited %d users (many).\n"
"Please review the requests."
msgid "ui.settings.input.deadzone"
msgstr "Deadzone"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language"
msgstr "Language"
msgid "ui.settings.general.language_detail"
msgstr "Takes effect after restarting the application."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.apply"
msgstr "Apply"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes"
msgstr "Discard unsaved changes?"
#: src/dusk/ui/frame/game/uigamemenu.c
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"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save"
msgstr "Save"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_success"
msgstr "Game saved."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_cancelled"
msgstr "Save cancelled."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_unavailable"
msgstr "Can't save - no save device found."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_temporary"
msgstr "This session is temporary - no save device was found, so saving is disabled."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_create_confirm"
msgstr "No save data found. Create a new save?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_failed_format"
msgstr "Save failed: %s"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_check_failed_format"
msgstr "Can't save: %s"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.message"
msgstr "No save device found. You can continue, but\nprogress will not be saved."
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.retry"
msgstr "Retry"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.continue"
msgstr "Continue Anyway"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.message"
msgstr "No save data found. Create a new save?"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.yes"
msgstr "Yes"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.no"
msgstr "No"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.new_game"
msgstr "New Game"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.load_game"
msgstr "Load Game"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.options"
msgstr "Options"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.quit"
msgstr "Quit Game"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.confirm"
msgstr "Confirm"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.cancel"
msgstr "Cancel"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.attack"
msgstr "Attack"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.flee"
msgstr "Flee"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.target_format"
msgstr "Enemy %u (%u/%u HP)"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.hp_format"
msgstr "HP %u/%u"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.mp_format"
msgstr "MP %u/%u"
#: src/dusk/ui/frame/settings/uisettingsaudio.c
msgid "ui.settings.audio.placeholder"
msgstr "No audio settings yet"
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
msgid "ui.settings.display.placeholder"
msgstr "No display settings yet"
#: src/dusk/ui/frame/settings/uisettingsinput.c
msgid "ui.settings.input.placeholder"
msgstr "No input settings yet"
#: src/dusk/ui/frame/backpack/uibackpack.c
msgid "ui.backpack.category_format"
msgstr "Category %u"
#: src/dusk/ui/overlay/uiloading.c
msgid "ui.loading.text"
msgstr "loading"
#: src/dusk/ui/overlay/uiautosave.c
msgid "ui.autosave.saving"
msgstr "SAVING"
msgid "item.potion.name"
msgstr "Potion"
msgid "item.potato.name"
msgstr "Potato"
msgid "item.apple.name"
msgstr "Apple"
+194
View File
@@ -0,0 +1,194 @@
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/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save"
msgstr "Guardar"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_success"
msgstr "Partida guardada."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_cancelled"
msgstr "Guardado cancelado."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_unavailable"
msgstr "No se puede guardar: no se encontró ningún dispositivo de guardado."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_temporary"
msgstr "Esta sesión es temporal - no se encontró ningún dispositivo de guardado, por lo que guardar está deshabilitado."
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_create_confirm"
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_failed_format"
msgstr "Error al guardar: %s"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_check_failed_format"
msgstr "No se puede guardar: %s"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.message"
msgstr "No se encontró ningún dispositivo de guardado. Puedes continuar, pero\nel progreso no se guardará."
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.retry"
msgstr "Reintentar"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.continue"
msgstr "Continuar de todos modos"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.message"
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.yes"
msgstr "Sí"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.no"
msgstr "No"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.new_game"
msgstr "Nueva Partida"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.load_game"
msgstr "Cargar Partida"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.options"
msgstr "Opciones"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.quit"
msgstr "Salir del Juego"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.confirm"
msgstr "Confirmar"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.cancel"
msgstr "Cancelar"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.attack"
msgstr "Atacar"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.flee"
msgstr "Huir"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.target_format"
msgstr "Enemigo %u (%u/%u PS)"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.hp_format"
msgstr "PS %u/%u"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.mp_format"
msgstr "PM %u/%u"
#: src/dusk/ui/frame/settings/uisettingsaudio.c
msgid "ui.settings.audio.placeholder"
msgstr "Aún no hay opciones de audio"
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
msgid "ui.settings.display.placeholder"
msgstr "Aún no hay opciones de pantalla"
#: src/dusk/ui/frame/settings/uisettingsinput.c
msgid "ui.settings.input.placeholder"
msgstr "Aún no hay opciones de entrada"
#: src/dusk/ui/frame/backpack/uibackpack.c
msgid "ui.backpack.category_format"
msgstr "Categoría %u"
#: src/dusk/ui/overlay/uiloading.c
msgid "ui.loading.text"
msgstr "cargando"
#: src/dusk/ui/overlay/uiautosave.c
msgid "ui.autosave.saving"
msgstr "GUARDANDO"
#: 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"
+194
View File
@@ -0,0 +1,194 @@
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/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save"
msgstr "セーブ"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_success"
msgstr "セーブしました。"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_cancelled"
msgstr "セーブをキャンセルしました。"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_unavailable"
msgstr "セーブできません - セーブデバイスが見つかりません。"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_temporary"
msgstr "このセッションは一時的です - セーブデバイスが見つからなかったため、セーブは無効になっています。"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_create_confirm"
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_failed_format"
msgstr "セーブに失敗しました: %s"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.save_check_failed_format"
msgstr "セーブできません: %s"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.message"
msgstr "セーブデバイスが見つかりません。続行できますが、\n進行状況は保存されません。"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.retry"
msgstr "再試行"
#: src/dusk/ui/frame/initial/uiinitialnocard.c
msgid "ui.initial.no_card.continue"
msgstr "続行する"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.message"
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.yes"
msgstr "はい"
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
msgid "ui.initial.create_save.no"
msgstr "いいえ"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.new_game"
msgstr "ニューゲーム"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.load_game"
msgstr "ロードゲーム"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.options"
msgstr "オプション"
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
msgid "ui.main_menu.quit"
msgstr "ゲームを終了"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.confirm"
msgstr "確認"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.cancel"
msgstr "キャンセル"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.attack"
msgstr "攻撃"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.flee"
msgstr "逃げる"
#: src/dusk/ui/frame/battle/uibattlemenu.c
msgid "ui.battle.menu.target_format"
msgstr "敵%u (%u/%u HP)"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.hp_format"
msgstr "HP %u/%u"
#: src/dusk/ui/frame/battle/uibattlehud.c
msgid "ui.battle.hud.mp_format"
msgstr "MP %u/%u"
#: src/dusk/ui/frame/settings/uisettingsaudio.c
msgid "ui.settings.audio.placeholder"
msgstr "オーディオ設定はまだありません"
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
msgid "ui.settings.display.placeholder"
msgstr "表示設定はまだありません"
#: src/dusk/ui/frame/settings/uisettingsinput.c
msgid "ui.settings.input.placeholder"
msgstr "入力設定はまだありません"
#: src/dusk/ui/frame/backpack/uibackpack.c
msgid "ui.backpack.category_format"
msgstr "カテゴリー%u"
#: src/dusk/ui/overlay/uiloading.c
msgid "ui.loading.text"
msgstr "読み込み中"
#: src/dusk/ui/overlay/uiautosave.c
msgid "ui.autosave.saving"
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 "リンゴ"
Binary file not shown.
Binary file not shown.
-63
View File
@@ -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;
-42
View File
@@ -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;
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

-6
View File
@@ -1,6 +0,0 @@
module = {
render() {
Text.draw(0, 0, "Hello World");
SpriteBatch.flush();
}
};
+22
View File
@@ -2179,5 +2179,27 @@
0
]
}
],
"entities": [
{
"type": "global",
"globalId": 3,
"pos": [8, 8, 1]
},
{
"type": "item",
"itemId": 1,
"quantity": 1,
"pos": [12, 2, 0]
}
],
"areas": [
{
"min": [11, 3, 0],
"max": [16, 9, 10],
"callbackId": 1,
"notify": 3,
"trigger": 6
}
]
}
+276 -6
View File
@@ -1464,7 +1464,7 @@
10,
0
],
"type": 1,
"type": 4,
"tile": 0
},
{
@@ -1473,7 +1473,7 @@
10,
0
],
"type": 1,
"type": 4,
"tile": 0
},
{
@@ -1482,7 +1482,7 @@
10,
0
],
"type": 1,
"type": 4,
"tile": 0
},
{
@@ -1491,7 +1491,7 @@
10,
0
],
"type": 1,
"type": 4,
"tile": 0
},
{
@@ -1500,7 +1500,7 @@
10,
0
],
"type": 1,
"type": 4,
"tile": 0
},
{
@@ -1509,7 +1509,7 @@
10,
0
],
"type": 1,
"type": 4,
"tile": 0
},
{
@@ -2267,6 +2267,276 @@
],
"type": 1,
"tile": 0
},
{
"pos": [
6,
9,
1
],
"type": 4,
"tile": 0
},
{
"pos": [
7,
9,
1
],
"type": 4,
"tile": 0
},
{
"pos": [
8,
9,
1
],
"type": 4,
"tile": 0
},
{
"pos": [
9,
9,
1
],
"type": 4,
"tile": 0
},
{
"pos": [
10,
9,
1
],
"type": 4,
"tile": 0
},
{
"pos": [
11,
9,
1
],
"type": 4,
"tile": 0
},
{
"pos": [
6,
8,
2
],
"type": 4,
"tile": 0
},
{
"pos": [
7,
8,
2
],
"type": 4,
"tile": 0
},
{
"pos": [
8,
8,
2
],
"type": 4,
"tile": 0
},
{
"pos": [
9,
8,
2
],
"type": 4,
"tile": 0
},
{
"pos": [
10,
8,
2
],
"type": 4,
"tile": 0
},
{
"pos": [
11,
8,
2
],
"type": 4,
"tile": 0
},
{
"pos": [
6,
5,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
7,
5,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
8,
5,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
9,
5,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
10,
5,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
11,
5,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
6,
6,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
7,
6,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
8,
6,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
9,
6,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
10,
6,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
11,
6,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
6,
7,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
7,
7,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
8,
7,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
9,
7,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
10,
7,
3
],
"type": 1,
"tile": 0
},
{
"pos": [
11,
7,
3
],
"type": 1,
"tile": 0
}
],
"meshes": []
+3
View File
@@ -16,6 +16,9 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DOL=1
ISO=2
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
+1 -1
View File
@@ -36,7 +36,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
# DUSK_OPENGL_LEGACY
DUSK_LINUX
DUSK_DISPLAY_SIZE_DYNAMIC
DUSK_DISPLAY_WIDTH_DEFAULT=640
DUSK_DISPLAY_WIDTH_DEFAULT=854
DUSK_DISPLAY_HEIGHT_DEFAULT=480
DUSK_DISPLAY_SCREEN_HEIGHT=240
DUSK_INPUT_KEYBOARD
+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")
+11
View File
@@ -4,6 +4,17 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_WII
)
# Wii save storage method - see src/duskdolphin/save/savedeviceplatform.h.
set(DUSK_SAVE_WII_METHOD "NAND" CACHE STRING
"Wii save storage: NAND (internal storage via ISFS), CARD (GameCube-\
compatible memory card emulation), or SD (SD card via libfat)"
)
set_property(CACHE DUSK_SAVE_WII_METHOD PROPERTY STRINGS "NAND" "CARD" "SD")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SAVE_WII_METHOD_${DUSK_SAVE_WII_METHOD}
)
# Generate Homebrew Channel meta.xml from project identity variables
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
configure_file(
+53 -3
View File
@@ -17,6 +17,10 @@ const ChunkTerrain = (() => {
const CHUNK_DEPTH = 4;
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_GROUND = 1;
const TILE_SHAPE_RAMP_NORTH = 2;
@@ -88,7 +92,53 @@ const ChunkTerrain = (() => {
const v1 = (y + 1) / CHUNK_HEIGHT;
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({
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_RAMP_NORTH, TILE_SHAPE_RAMP_EAST,
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_NORTHEAST_INNER, TILE_SHAPE_RAMP_NORTHWEST_INNER,
TILE_SHAPE_RAMP_SOUTHEAST_INNER, TILE_SHAPE_RAMP_SOUTHWEST_INNER,
RAMP_CORNERS, tileIndex, buildTerrainVerts,
RAMP_CORNERS, tileIndex, buildTerrainVerts, buildGridLines,
});
})();
+1 -1
View File
@@ -9,7 +9,7 @@
flex-direction: column;
flex-wrap: wrap;
gap: 6px;
max-height: 512px;
max-height: 600px;
}
.tile-swatch {
+78 -20
View File
@@ -41,16 +41,15 @@
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER]: { dx: -1, dy: 1 },
};
// 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.
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;
const tipX = cx + ux * len, tipY = cy + uy * len;
const tailX = cx - ux * len, tailY = cy - uy * len;
const INNER_CORNER_SHAPES = new Set([
ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER,
ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER,
ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER,
ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER,
]);
// Draws a single-headed arrow from (tailX, tailY) to (tipX, tipY).
function drawArrowSegment(ctx, tailX, tailY, tipX, tipY, size) {
ctx.strokeStyle = "#ffffff";
ctx.fillStyle = "#ffffff";
ctx.lineWidth = Math.max(1, size * 0.08);
@@ -62,7 +61,7 @@
ctx.stroke();
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 rightAngle = angle - Math.PI * 0.8;
ctx.beginPath();
@@ -73,6 +72,54 @@
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.
function drawPencilIcon(ctx, size) {
ctx.save();
@@ -146,6 +193,7 @@
let mapRenderer = null;
let terrainMesh = null;
let gridLinesMesh = null;
let terrainTexturePromise = null;
let modelIndex = null;
let neighborChunks = [];
@@ -262,6 +310,13 @@
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
// its model JSON by basename, mirroring find_model() in
// tools/asset/chunk/__main__.py - the chunk JSON only ever stores a bare
@@ -330,7 +385,7 @@
for(const m of neighborMeshes) {
try {
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) {
console.warn("Failed to load neighbor mesh preview", m.file, e);
}
@@ -426,13 +481,12 @@
buildPalette();
});
const dir = SHAPE_DIRECTIONS[s.type];
if(dir) {
if(SHAPE_DIRECTIONS[s.type]) {
const icon = document.createElement("canvas");
icon.width = 32;
icon.height = 32;
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);
}
@@ -489,11 +543,10 @@
const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - y) * cell;
ctx.fillStyle = SHAPE_COLORS[type] || "#15171b";
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);
const dir = SHAPE_DIRECTIONS[type];
if(dir) drawArrow(ctx, x * cell + cell / 2, sy + cell / 2, cell, dir.dx, dir.dy);
drawShapeIcon(ctx, x * cell + cell / 2, sy + cell / 2, cell, type);
}
}
@@ -592,6 +645,9 @@
function rebuildTerrainMesh() {
const floats = ChunkTerrain.buildTerrainVerts(tiles);
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
@@ -619,7 +675,7 @@
for(const m of meshes) {
try {
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) {
console.warn("Failed to load mesh preview", m.file, e);
}
@@ -637,13 +693,15 @@
models: n.models,
}));
const zLevelHeight = currentZLevel * ChunkTerrain.WORLD_LAYER_HEIGHT;
mapRenderer.render({
target: [ChunkTerrain.CHUNK_WIDTH / 2, ChunkTerrain.CHUNK_HEIGHT / 2, currentZLevel],
target: [ChunkTerrain.CHUNK_WIDTH / 2, ChunkTerrain.CHUNK_HEIGHT / 2, zLevelHeight],
worldH: zoomWorldH,
terrain: terrainMesh ? { mesh: terrainMesh, texture: terrainTexture } : null,
gridLines: gridLinesMesh ? { mesh: gridLinesMesh } : null,
models,
neighbors,
highlight: hoverTile ? { x: hoverTile.x, y: hoverTile.y, z: currentZLevel } : null,
highlight: hoverTile ? { x: hoverTile.x, y: hoverTile.y, z: zLevelHeight } : null,
});
}
+5
View File
@@ -221,6 +221,7 @@ const MapRenderer = (() => {
// scene = {
// target: [x, y, z], worldH: number,
// terrain: { mesh, texture } | null,
// gridLines: { mesh } | null,
// models: [{ mesh, texture, color: [r,g,b,a in 0..1], offset: [x,y,z] }],
// neighbors: [{
// offset: [x,y,z],
@@ -248,6 +249,10 @@ const MapRenderer = (() => {
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) {
const modelMatrix = mat4Translation(new Float32Array(16), model.offset);
drawMesh(model.mesh, modelMatrix, model.texture, model.color, true);
+4
View File
@@ -5,6 +5,10 @@
add_subdirectory(dusk)
if(DUSK_NETWORK)
add_subdirectory(dusknetwork)
endif()
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
add_subdirectory(dusklinux)
add_subdirectory(dusksdl2)
-1
View File
@@ -68,7 +68,6 @@ add_subdirectory(scene)
add_subdirectory(system)
add_subdirectory(time)
add_subdirectory(ui)
add_subdirectory(network)
add_subdirectory(save)
add_subdirectory(util)
add_subdirectory(thread)
+1
View File
@@ -7,4 +7,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
easing.c
animation.c
keyframe.c
)
+109 -29
View File
@@ -11,42 +11,122 @@
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
uint16_t *keyframeCounts,
const uint16_t layerCount
) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
assertNotNull(keyframeCounts, "Keyframe counts pointer cannot be null.");
assertTrue(layerCount > 0, "Layer count must be greater than zero.");
memoryZero(anim, sizeof(animation_t));
anim->keyframes = keyframes;
anim->keyframeCount = keyframeCount;
anim->keyframeCounts = keyframeCounts;
anim->layerCount = layerCount;
// Determine duration
float_t duration = 0.0f;
for(uint16_t layer = 0; layer < layerCount; layer++) {
uint16_t keyframeCount = keyframeCounts[layer];
assertTrue(keyframeCount > 0, "Keyframe count invalid.");
keyframe_t *layerKeyframes = keyframes + layer * keyframeCount;
#ifdef DUSK_ASSERTIONS
// Check that the keyframes are sorted by time.
for(uint16_t i = 1; i < keyframeCount; i++) {
assertTrue(
layerKeyframes[i].time >= layerKeyframes[i - 1].time,
"Keyframes must be sorted by time."
);
}
#endif
keyframe_t *lastKeyframe = layerKeyframes + keyframeCount - 1;
duration = mathMax(duration, lastKeyframe->time);
}
assertTrue(duration > 0, "Animation duration must be greater than 0.");
anim->duration = duration;
}
float_t animationGetValue(animation_t *anim, const float_t time) {
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
assertTrue(time >= 0, "Time must be non-negative.");
assertTrue(layer < anim->layerCount, "Layer index out of bounds.");
keyframe_t *start;
keyframe_t *end;
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
keyframe_t *current = anim->keyframes;
start = current;
do {
if(current->time > time) {
end = current;
break;
}
start = current;
current++;
if(current > last) {
end = start;
break;
}
} while(true);
float_t t = (time - start->time) / (end->time - start->time);
return mathLerp(start->value, end->value, easingApply(start->easing, t));
uint16_t keyframeCount = anim->keyframeCounts[layer];
keyframe_t *layerKeyframes = anim->keyframes + layer * keyframeCount;
return keyframeGetValue(layerKeyframes, keyframeCount, anim->time);
}
void animationUpdate(
animation_t *anim,
const float_t deltaTime
) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertTrue(deltaTime >= 0, "Delta time must be non-negative.");
bool_t justCompleted = false;
if(!(anim->flags & ANIMATION_FLAG_INTERNAL_COMPLETED)) {
bool_t loop = (anim->flags & ANIMATION_FLAG_LOOP) != 0;
bool_t pingpong = (anim->flags & ANIMATION_FLAG_PINGPONG) != 0;
assertFalse(
loop && pingpong,
"Cannot set both ANIMATION_FLAG_LOOP and ANIMATION_FLAG_PINGPONG."
);
bool_t backward;
if(pingpong) {
backward = (anim->flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0;
} else {
backward = (anim->flags & ANIMATION_FLAG_REVERSE) != 0;
}
// Resolve boundary crossings one at a time, so a single large deltaTime
// can correctly loop/pingpong across multiple boundaries in one call.
float_t remaining = deltaTime;
while(remaining > 0.0f) {
float_t toBoundary = (
backward ? anim->time : (anim->duration - anim->time)
);
if(remaining < toBoundary) {
anim->time += backward ? -remaining : remaining;
break;
}
remaining -= toBoundary;
anim->time = backward ? 0.0f : anim->duration;
bool_t stopHere;
if(backward) {
stopHere = (anim->flags & ANIMATION_FLAG_STOP_BEGINNING) != 0;
} else {
stopHere = (anim->flags & ANIMATION_FLAG_STOP_END) != 0;
}
if(stopHere) {
justCompleted = true;
break;
} else if(pingpong) {
backward = !backward;
if(backward) anim->flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
else anim->flags &= ~ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
} else if(loop) {
anim->time = backward ? anim->duration : 0.0f;
if(anim->onLoop) anim->onLoop(anim->user);
} else {
justCompleted = true;
break;
}
}
if(justCompleted) anim->flags |= ANIMATION_FLAG_INTERNAL_COMPLETED;
}
// Call onUpdate for each layer.
for(uint16_t layer = 0; layer < anim->layerCount; layer++) {
float_t value = animationGetLayerValue(anim, layer);
if(anim->onUpdate) anim->onUpdate(layer, value, anim->user);
}
if(justCompleted && anim->onComplete) anim->onComplete(anim->user);
}
+71 -11
View File
@@ -6,29 +6,89 @@
#pragma once
#include "keyframe.h"
#define ANIMATION_FLAG_LOOP (1 << 0)
#define ANIMATION_FLAG_REVERSE (1 << 1)
#define ANIMATION_FLAG_PINGPONG (1 << 2)
#define ANIMATION_FLAG_STOP_BEGINNING (1 << 3)
#define ANIMATION_FLAG_STOP_END (1 << 4)
// Internal - tracks which direction a pingponging animation is currently
// travelling. Do not set this manually, it is managed by animationUpdate().
#define ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD (1 << 7)
// Internal - set once the animation has stopped advancing (see
// animationUpdate()). Do not set this manually. There is currently no way to
// restart a completed animation short of clearing this bit and resetting
// anim->time by hand.
#define ANIMATION_FLAG_INTERNAL_COMPLETED (1 << 6)
typedef struct {
keyframe_t *keyframes;
uint16_t keyframeCount;
uint16_t *keyframeCounts;
uint16_t layerCount;
float_t time;
float_t duration;
uint8_t flags;
void *user;
void (*onUpdate)(const uint16_t layer, const float_t value, void *user);
void (*onComplete)(void *user);
void (*onLoop)(void *user);
} animation_t;
/**
* Initializes an animation.
* Initializes an animation with the given keyframes and layer count.
*
* @param anim The animation to initialize.
* @param keyframes The keyframes to use for the animation.
* @param keyframeCount The number of keyframes in the animation.
* @param anim Pointer to the animation to initialize.
* @param keyframes Pointer to the array of keyframes for each layer.
* @param keyframeCount Number of keyframes in each layer.
* @param layerCount Number of layers in the animation.
*/
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
uint16_t *keyframeCounts,
const uint16_t layerCount
);
/**
* Gets the value of the animation at a given time.
* Sets the current time of the animation, clamping it to the valid range.
* This will call the onUpdate callback but none of the other callbacks.
*
* @param anim The animation to get the value from.
* @param time The time at which to get the value, in seconds.
* @return The value of the animation at the given time.
* @param anim Pointer to the animation to set the time for.
* @param time The new time to set for the animation.
*/
float_t animationGetValue(animation_t *anim, const float_t time);
void animationSetTime(animation_t *anim, const float_t time);
/**
* Gets the current value of a specific layer in the animation based on the
* current animation time.
*/
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer);
/**
* Updates the animation state based on the elapsed time. Advances anim->time
* by deltaTime (or against it, if ANIMATION_FLAG_REVERSE is set), then
* resolves whatever happens when it reaches the 0 or duration boundary:
*
* - ANIMATION_FLAG_PINGPONG: reflects off the boundary and continues playing
* in the opposite direction, forever, unless stopped (see below).
* - ANIMATION_FLAG_LOOP: wraps back around to the other boundary and keeps
* playing in the same direction, forever, unless stopped (see below).
* - ANIMATION_FLAG_STOP_BEGINNING / ANIMATION_FLAG_STOP_END: when the
* animation reaches that specific boundary, it clamps there and stops
* (firing onComplete) instead of looping/pingponging past it.
* - If none of the above apply at a boundary, the animation clamps there and
* stops, firing onComplete.
*
* onUpdate is called for every layer on every call. onLoop is called each
* time a loop wraps around. onComplete is called at most once, the moment
* the animation stops advancing.
*
* @param anim Pointer to the animation to update.
* @param deltaTime Time elapsed since the last update (in seconds).
*/
void animationUpdate(
animation_t *anim,
const float_t deltaTime
);
+45
View File
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "keyframe.h"
#include "assert/assert.h"
#include "util/math.h"
float_t keyframeGetValue(
const keyframe_t *keyframes,
const uint32_t keyframeCount,
const float_t time
) {
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
assertTrue(time >= 0, "Time must be non-negative.");
#ifdef DUSK_ASSERTIONS
// Checks that the keyframes are sorted by time.
for(uint32_t i = 1; i < keyframeCount; i++) {
assertTrue(
keyframes[i].time >= keyframes[i - 1].time,
"Keyframes must be sorted by time."
);
}
#endif
keyframe_t *last = (keyframe_t *)(keyframes + keyframeCount - 1);
if(time >= last->time) return last->value;
// Since time < last->time (checked above), current is guaranteed to stop
// at or before reaching last, so no separate end-of-array check is needed.
keyframe_t *current = (keyframe_t *)keyframes;
keyframe_t *start = current;
while(current->time <= time) {
start = current;
current++;
}
keyframe_t *end = current;
float_t t = (time - start->time) / (end->time - start->time);
return mathLerp(start->value, end->value, easingApply(start->easing, t));
}
+14
View File
@@ -11,3 +11,17 @@ typedef struct {
float_t value;
easingtype_t easing;
} keyframe_t;
/**
* Gets the value of a keyframe at a given time.
*
* @param keyframes The keyframes to get the value from.
* @param keyframeCount The number of keyframes in the array.
* @param time The time at which to get the value, in seconds.
* @return The value of the keyframe at the given time.
*/
float_t keyframeGetValue(
const keyframe_t *keyframes,
const uint32_t keyframeCount,
const float_t time
);
+2
View File
@@ -22,6 +22,8 @@
#endif
#ifndef DUSK_ASSERTIONS_FAKED
#define DUSK_ASSERTIONS 1
/**
* Initializes the assert system. Must be the very first call in engine
* startup.
+48 -57
View File
@@ -59,20 +59,28 @@ assetentry_t * assetGetEntry(
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
// We did not find one existing, Find first available slot.
entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
// We did not find one existing. Find first available slot, reaping
// zero-ref entries to make room if none are immediately available.
bool_t reaped = false;
for(;;) {
entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
assetEntryInit(entry, name, type, input);
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
assetEntryInit(entry, name, type, input);
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
if(reaped) break;
reaped = true;
errorCatch(assetReapUnused());
}
assertUnreachable("No available asset entry slots.");
return NULL;
@@ -191,6 +199,32 @@ void assetUnlockEntry(assetentry_t *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) {
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);
// 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();
}
@@ -413,26 +423,7 @@ errorret_t assetDispose(void) {
assertIsMainThread("Must be called from the main thread.");
threadStop(&ASSET.loadThread);
// Drain-dispose: 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 *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);
errorChain(assetReapUnused());
// Cleanup zip file.
if(ASSET.zip != NULL) {
+12 -2
View File
@@ -23,8 +23,8 @@
#define ASSET_FILE_NAME "dusk.dsk"
#define ASSET_HEADER_SIZE 3
#define ASSET_LOADING_COUNT_MAX 20
#define ASSET_ENTRY_COUNT_MAX 128
#define ASSET_LOADING_COUNT_MAX 10
#define ASSET_ENTRY_COUNT_MAX 64
typedef struct asset_s {
zip_t *zip;
@@ -112,6 +112,16 @@ void assetUnlock(const char_t *name);
*/
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
* is fully loaded.
@@ -14,6 +14,26 @@
#include "asset/loader/assetloader.h"
#include "asset/asset.h"
// Reads a little-endian int16 from a potentially-unaligned offset into a
// worldunit_t, advancing *offset past it.
static worldunit_t assetChunkReadWorldUnit(
const uint8_t *data,
size_t *offset
) {
int16_t value;
memoryCopy(&value, data + *offset, sizeof(int16_t));
*offset += sizeof(int16_t);
return (worldunit_t)endianLittleToHost16((uint16_t)value);
}
static worldpos_t assetChunkReadWorldPos(const uint8_t *data, size_t *offset) {
worldpos_t pos;
pos.x = assetChunkReadWorldUnit(data, offset);
pos.y = assetChunkReadWorldUnit(data, offset);
pos.z = assetChunkReadWorldUnit(data, offset);
return pos;
}
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
@@ -111,9 +131,15 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
size_t offset = 8;
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
out->tiles = memoryAllocate(tileSize);
memoryCopy(out->tiles, data + offset, tileSize);
offset += tileSize;
for(size_t t = 0; t < CHUNK_TILE_COUNT; t++) {
uint32_t *shape = (uint32_t *)&out->tiles[t].shape;
*shape = endianLittleToHost32(*shape);
}
out->meshCount = data[offset];
offset += sizeof(uint8_t);
assertTrue(
@@ -135,6 +161,65 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3));
offset += sizeof(vec3);
out->meshOffsets[m][0] = endianLittleToHostFloat(out->meshOffsets[m][0]);
out->meshOffsets[m][1] = endianLittleToHostFloat(out->meshOffsets[m][1]);
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
}
out->entitySpawnCount = data[offset];
offset += sizeof(uint8_t);
assertTrue(
out->entitySpawnCount <= CHUNK_ENTITY_SPAWN_COUNT_MAX,
"Chunk entity spawn count exceeds maximum."
);
for(uint8_t s = 0; s < out->entitySpawnCount; s++) {
chunkentityspawn_t *spawn = &out->entitySpawns[s];
spawn->kind = (chunkentityspawnkind_t)data[offset];
offset += sizeof(uint8_t);
uint16_t a;
memoryCopy(&a, data + offset, sizeof(uint16_t));
a = endianLittleToHost16(a);
offset += sizeof(uint16_t);
uint8_t b = data[offset];
offset += sizeof(uint8_t);
if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_ITEM) {
spawn->globalId = 0;
spawn->itemId = a;
spawn->itemQuantity = b;
} else {
spawn->globalId = a;
spawn->itemId = 0;
spawn->itemQuantity = 0;
}
spawn->position = assetChunkReadWorldPos(data, &offset);
}
out->areaSpawnCount = data[offset];
offset += sizeof(uint8_t);
assertTrue(
out->areaSpawnCount <= CHUNK_AREA_COUNT_MAX,
"Chunk area spawn count exceeds maximum."
);
for(uint8_t s = 0; s < out->areaSpawnCount; s++) {
chunkareaspawn_t *area = &out->areaSpawns[s];
area->min = assetChunkReadWorldPos(data, &offset);
area->max = assetChunkReadWorldPos(data, &offset);
uint16_t callbackId;
memoryCopy(&callbackId, data + offset, sizeof(uint16_t));
area->callbackId = endianLittleToHost16(callbackId);
offset += sizeof(uint16_t);
area->notify = data[offset];
offset += sizeof(uint8_t);
area->trigger = data[offset];
offset += sizeof(uint8_t);
}
memoryFree(data);
@@ -157,6 +242,12 @@ errorret_t assetChunkDispose(assetentry_t *entry) {
assertIsMainThread("Must be called from the main thread.");
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++) {
if(out->modelEntries[m] == NULL) continue;
assetUnlockEntry(out->modelEntries[m]);
+29 -2
View File
@@ -9,7 +9,7 @@
#include "asset/assetfile.h"
#include "rpg/overworld/chunk.h"
#define ASSET_CHUNK_FILE_VERSION 3
#define ASSET_CHUNK_FILE_VERSION 5
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
@@ -33,12 +33,39 @@ typedef struct {
uint8_t modelIndex;
} assetchunkloaderloading_t;
typedef enum {
CHUNK_ENTITY_SPAWN_KIND_GLOBAL,
CHUNK_ENTITY_SPAWN_KIND_ITEM
} chunkentityspawnkind_t;
typedef struct {
tile_t tiles[CHUNK_TILE_COUNT];
chunkentityspawnkind_t kind;
uint16_t globalId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL.
uint16_t itemId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
uint8_t itemQuantity; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
worldpos_t position;
} chunkentityspawn_t;
typedef struct {
worldpos_t min;
worldpos_t max;
uint16_t callbackId; // Index into MAP_AREA_CALLBACK_LIST.
uint8_t notify;
uint8_t trigger;
} chunkareaspawn_t;
typedef struct {
tile_t *tiles;
uint8_t meshCount;
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
uint8_t entitySpawnCount;
chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX];
uint8_t areaSpawnCount;
chunkareaspawn_t areaSpawns[CHUNK_AREA_COUNT_MAX];
} assetchunkoutput_t;
/**
+23 -4
View File
@@ -48,8 +48,20 @@ errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8));
meshvertex_t *vertices = NULL;
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));
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);
@@ -103,18 +115,22 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
out->vertices = NULL;
errorChain(ret);
}
out->meshInitialized = true;
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
if(errorIsNotOk(ret)) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
meshDispose(&out->mesh);
out->meshInitialized = false;
memoryFree(out->vertices);
out->vertices = NULL;
errorChain(ret);
}
#ifndef DUSK_OPENGL_LEGACY
// VBO owns the data now; CPU copy is no longer needed.
#if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
// 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);
out->vertices = NULL;
#endif
@@ -129,8 +145,11 @@ errorret_t assetMeshDispose(assetentry_t *entry) {
assertIsMainThread("Must be called from the main thread.");
assetmeshoutput_t *out = &entry->data.mesh;
if(out->vertices != NULL) {
if(out->meshInitialized) {
errorChain(meshDispose(&out->mesh));
out->meshInitialized = false;
}
if(out->vertices != NULL) {
memoryFree(out->vertices);
out->vertices = NULL;
}
@@ -30,6 +30,7 @@ typedef struct {
typedef struct {
mesh_t mesh;
bool_t meshInitialized;
meshvertex_t *vertices;
} assetmeshoutput_t;
@@ -506,12 +506,17 @@ errorret_t assetLocaleGetString(
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
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
while(!reader.eof) {
while(true) {
// Is this msgid?
if(memoryCompare(lineBuffer, "msgid", 5) != 0) {
if(reader.eof) break;
errorChain(assetFileLineReaderNext(&reader));
msgidBuffer[0] = '\0';
continue;
@@ -535,7 +540,7 @@ errorret_t assetLocaleGetString(
}
// We are either going to see a msgstr or a msgid_plural
while(!reader.eof) {
while(true) {
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
// Is msgid_plural?
+5 -16
View File
@@ -17,11 +17,8 @@ console_t CONSOLE;
void consoleInit(void) {
memoryZero(&CONSOLE, sizeof(console_t));
CONSOLE.visible = true;
#ifdef DUSK_CONSOLE_POSIX
threadMutexInit(&CONSOLE.printMutex);
#endif
CONSOLE.visible = false;
threadMutexInit(&CONSOLE.printMutex);
}
void consolePrint(const char_t *message, ...) {
@@ -32,20 +29,14 @@ void consolePrint(const char_t *message, ...) {
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
va_end(args);
#ifdef DUSK_CONSOLE_POSIX
threadMutexLock(&CONSOLE.printMutex);
#endif
threadMutexLock(&CONSOLE.printMutex);
memoryMove(
CONSOLE.line[0],
CONSOLE.line[1],
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
);
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
#ifdef DUSK_CONSOLE_POSIX
threadMutexUnlock(&CONSOLE.printMutex);
#endif
threadMutexUnlock(&CONSOLE.printMutex);
logDebug("%s\n", buffer);
}
@@ -61,7 +52,5 @@ void consoleUpdate(void) {
}
void consoleDispose(void) {
#ifdef DUSK_CONSOLE_POSIX
threadMutexDispose(&CONSOLE.printMutex);
#endif
threadMutexDispose(&CONSOLE.printMutex);
}
+5 -11
View File
@@ -6,24 +6,18 @@
*/
#pragma once
#include "consoledefs.h"
#include "error/error.h"
#include "dusk.h"
#include "thread/thread.h"
#ifdef DUSK_CONSOLE_POSIX
#include "thread/thread.h"
#include <poll.h>
#include <unistd.h>
#define CONSOLE_POSIX_POLL_RATE 75
#endif
#define CONSOLE_LINE_MAX 512
#define CONSOLE_HISTORY_MAX 16
#define CONSOLE_EXEC_BUFFER_MAX 32
typedef struct {
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
bool_t visible;
#ifdef DUSK_CONSOLE_POSIX
threadmutex_t printMutex;
#endif
threadmutex_t printMutex;
} console_t;
extern console_t CONSOLE;
+7 -3
View File
@@ -33,13 +33,17 @@ errorret_t displayInit(void) {
#ifdef displayPlatformInit
errorChain(displayPlatformInit());
#endif
// Set initial state
errorChain(displaySetState((displaystate_t){ .flags = 0 }));
// Init the fixed textures
errorChain(textureInit(
&TEXTURE_WHITE, 4, 4,
&TEXTURE_WHITE, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT,
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS }
));
errorChain(textureInit(
&TEXTURE_TEST, 4, 4,
&TEXTURE_TEST, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT,
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_TEST_PIXELS }
));
@@ -51,13 +55,13 @@ errorret_t displayInit(void) {
errorChain(capsuleInit());
errorChain(triPrismInit());
// Init the subsystems
errorChain(frameBufferInitBackBuffer());
errorChain(spriteBatchInit());
errorChain(textInit());
errorChain(screenInit());
// Setup initial shader with default values
errorChain(shaderListInit());
errorOk();
+1
View File
@@ -7,4 +7,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
text.c
font.c
)
+168
View File
@@ -0,0 +1,168 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "font.h"
#include "util/memory.h"
#include "util/math.h"
#include "display/color.h"
font_t FONT_DEFAULT;
static texture_t FONT_DEFAULT_TEXTURE;
static tileset_t FONT_DEFAULT_TILESET;
const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
FONT_DEFAULT_TILE_HEIGHT
] = {
{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x00, 0x00 }, // !
{ 0x00, 0x14, 0x14, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // "
{ 0x00, 0x14, 0x14, 0x3E, 0x14, 0x3E, 0x14, 0x14, 0x00, 0x00 }, // #
{ 0x00, 0x08, 0x1E, 0x28, 0x1C, 0x0A, 0x3C, 0x08, 0x00, 0x00 }, // $
{ 0x00, 0x00, 0x22, 0x24, 0x08, 0x12, 0x22, 0x00, 0x00, 0x00 }, // %
{ 0x00, 0x08, 0x14, 0x14, 0x1A, 0x24, 0x24, 0x1A, 0x00, 0x00 }, // &
{ 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '
{ 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08, 0x04, 0x00, 0x00 }, // (
{ 0x00, 0x10, 0x08, 0x08, 0x08, 0x08, 0x08, 0x10, 0x00, 0x00 }, // )
{ 0x00, 0x00, 0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00, 0x00, 0x00 }, // *
{ 0x00, 0x00, 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00, 0x00, 0x00 }, // +
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x20, 0x00 }, // ,
{ 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00 }, // -
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00 }, // .
{ 0x00, 0x04, 0x04, 0x08, 0x08, 0x08, 0x10, 0x10, 0x00, 0x00 }, // /
{ 0x00, 0x1C, 0x22, 0x26, 0x2A, 0x32, 0x22, 0x1C, 0x00, 0x00 }, // 0
{ 0x00, 0x08, 0x18, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // 1
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // 2
{ 0x00, 0x1C, 0x22, 0x02, 0x0C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 3
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x02, 0x02, 0x02, 0x00, 0x00 }, // 4
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 5
{ 0x00, 0x1C, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 6
{ 0x00, 0x3E, 0x02, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00, 0x00 }, // 7
{ 0x00, 0x1C, 0x22, 0x22, 0x1C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 8
{ 0x00, 0x1C, 0x22, 0x22, 0x1E, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 9
{ 0x00, 0x00, 0x10, 0x10, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00 }, // :
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ;
{ 0x00, 0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00, 0x00 }, // <
{ 0x00, 0x00, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00 }, // =
{ 0x00, 0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00 }, // >
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x00, 0x08, 0x00, 0x00 }, // ?
{ 0x00, 0x1C, 0x26, 0x2A, 0x2A, 0x26, 0x20, 0x1C, 0x00, 0x00 }, // @
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x00, 0x00 }, // A
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // B
{ 0x00, 0x1C, 0x22, 0x20, 0x20, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // C
{ 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // D
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // E
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // F
{ 0x00, 0x1C, 0x22, 0x20, 0x2E, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // G
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x22, 0x00, 0x00 }, // H
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // I
{ 0x00, 0x02, 0x02, 0x02, 0x02, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // J
{ 0x00, 0x22, 0x24, 0x28, 0x30, 0x28, 0x24, 0x22, 0x00, 0x00 }, // K
{ 0x00, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // L
{ 0x00, 0x22, 0x36, 0x2A, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // M
{ 0x00, 0x22, 0x22, 0x32, 0x2A, 0x26, 0x22, 0x22, 0x00, 0x00 }, // N
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // O
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // P
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x06, 0x00 }, // Q
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x22, 0x00, 0x00 }, // R
{ 0x00, 0x1C, 0x22, 0x20, 0x1C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // S
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // T
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // U
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x14, 0x14, 0x08, 0x00, 0x00 }, // V
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x2A, 0x36, 0x22, 0x00, 0x00 }, // W
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x14, 0x22, 0x22, 0x00, 0x00 }, // X
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y
{ 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z
{ 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // backslash (not drawn in source font)
{ 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x00, 0x00 }, // ]
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ^ (not drawn in source font)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // _ (not drawn in source font)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ` (not drawn in source font)
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // a
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // b
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // c
{ 0x00, 0x02, 0x02, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // d
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x3E, 0x20, 0x1C, 0x00, 0x00 }, // e
{ 0x00, 0x0C, 0x12, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x00, 0x00 }, // f
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // g
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // h
{ 0x00, 0x08, 0x00, 0x18, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // i
{ 0x00, 0x02, 0x00, 0x06, 0x02, 0x02, 0x02, 0x02, 0x22, 0x1C }, // j
{ 0x00, 0x20, 0x20, 0x22, 0x24, 0x38, 0x24, 0x22, 0x00, 0x00 }, // k
{ 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // l
{ 0x00, 0x00, 0x00, 0x3C, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00 }, // m
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // n
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // o
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x20, 0x20 }, // p
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x02 }, // q
{ 0x00, 0x00, 0x00, 0x2C, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00 }, // r
{ 0x00, 0x00, 0x00, 0x1E, 0x20, 0x1C, 0x02, 0x3C, 0x00, 0x00 }, // s
{ 0x00, 0x10, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // t
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // u
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x14, 0x08, 0x00, 0x00 }, // v
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x2A, 0x2A, 0x14, 0x00, 0x00 }, // w
{ 0x00, 0x00, 0x00, 0x22, 0x14, 0x08, 0x14, 0x22, 0x00, 0x00 }, // x
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // y
{ 0x00, 0x00, 0x00, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z
{ 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // {
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // | (not drawn in source font)
{ 0x00, 0x10, 0x08, 0x08, 0x04, 0x08, 0x08, 0x10, 0x00, 0x00 }, // }
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ~ (not drawn in source font)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
};
errorret_t fontDefaultInit(void) {
const int32_t width = (int32_t)mathNextPowTwo(
FONT_DEFAULT_COLUMNS * FONT_DEFAULT_TILE_WIDTH
);
const int32_t height = (int32_t)mathNextPowTwo(
FONT_DEFAULT_ROWS * FONT_DEFAULT_TILE_HEIGHT
);
color_t *pixels = memoryAllocate(sizeof(color_t) * width * height);
memoryZero(pixels, sizeof(color_t) * width * height);
for(uint16_t i = 0; i < FONT_DEFAULT_TILE_COUNT; i++) {
const uint16_t tileX = (i % FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_WIDTH;
const uint16_t tileY = (i / FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_HEIGHT;
for(uint8_t row = 0; row < FONT_DEFAULT_TILE_HEIGHT; row++) {
const uint8_t bits = FONT_DEFAULT_GLYPHS[i][row];
for(uint8_t col = 0; col < FONT_DEFAULT_TILE_WIDTH; col++) {
if(!((bits >> (FONT_DEFAULT_TILE_WIDTH - 1 - col)) & 1)) continue;
pixels[((tileY + row) * width) + (tileX + col)] = COLOR_WHITE;
}
}
}
FONT_DEFAULT_TILESET.tileWidth = FONT_DEFAULT_TILE_WIDTH;
FONT_DEFAULT_TILESET.tileHeight = FONT_DEFAULT_TILE_HEIGHT;
FONT_DEFAULT_TILESET.columns = FONT_DEFAULT_COLUMNS;
FONT_DEFAULT_TILESET.rows = FONT_DEFAULT_ROWS;
FONT_DEFAULT_TILESET.tileCount = FONT_DEFAULT_TILE_COUNT;
FONT_DEFAULT_TILESET.uv[0] = (float_t)FONT_DEFAULT_TILE_WIDTH / (float_t)width;
FONT_DEFAULT_TILESET.uv[1] = (float_t)FONT_DEFAULT_TILE_HEIGHT / (float_t)height;
const texturedata_t data = { .rgbaColors = pixels };
errorret_t textureResult = textureInit(
&FONT_DEFAULT_TEXTURE, width, height, TEXTURE_FORMAT_RGBA, data
);
memoryFree(pixels);
errorChain(textureResult);
FONT_DEFAULT.texture = &FONT_DEFAULT_TEXTURE;
FONT_DEFAULT.tileset = &FONT_DEFAULT_TILESET;
errorOk();
}
errorret_t fontDefaultDispose(void) {
errorChain(textureDispose(&FONT_DEFAULT_TEXTURE));
FONT_DEFAULT.texture = NULL;
FONT_DEFAULT.tileset = NULL;
errorOk();
}
+49
View File
@@ -6,6 +6,7 @@
*/
#pragma once
#include "error/error.h"
#include "display/texture/texture.h"
#include "display/texture/tileset.h"
@@ -13,3 +14,51 @@ typedef struct {
texture_t *texture;
tileset_t *tileset;
} font_t;
/**
* Pixel width/height of a single default-font glyph tile.
*/
#define FONT_DEFAULT_TILE_WIDTH 6
#define FONT_DEFAULT_TILE_HEIGHT 10
/** Grid layout of the generated default-font texture, in tiles. */
#define FONT_DEFAULT_COLUMNS 16
#define FONT_DEFAULT_ROWS 6
/**
* Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS *
* FONT_DEFAULT_ROWS), covering the printable ASCII range starting at
* TEXT_CHAR_START ('!') plus a couple of unused trailing tiles.
*/
#define FONT_DEFAULT_TILE_COUNT (FONT_DEFAULT_COLUMNS * FONT_DEFAULT_ROWS)
extern font_t FONT_DEFAULT;
/**
* Hard coded bitmap data for the built-in default font. Indexed
* [glyph][row], where glyph 0 corresponds to TEXT_CHAR_START ('!') and
* glyphs run consecutively through the printable ASCII range. Each row
* byte holds FONT_DEFAULT_TILE_WIDTH bit flags, one per pixel column:
* bit (FONT_DEFAULT_TILE_WIDTH - 1) is the leftmost pixel and bit 0 is
* the rightmost; 1 means the pixel is set, 0 means it is not.
*/
extern const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
FONT_DEFAULT_TILE_HEIGHT
];
/**
* Builds the default font's texture + tileset directly from
* FONT_DEFAULT_GLYPHS, without going through the asset system - so the
* engine always has a usable font to render with regardless of whether
* asset loading (e.g. the packed .dsk archive) succeeds.
*
* @return Either an error or success result.
*/
errorret_t fontDefaultInit(void);
/**
* Disposes of the default font created by fontDefaultInit().
*
* @return Either an error or success result.
*/
errorret_t fontDefaultDispose(void);
+2 -21
View File
@@ -9,34 +9,15 @@
#include "assert/assert.h"
#include "util/memory.h"
#include "display/spritebatch/spritebatch.h"
#include "asset/asset.h"
#include "asset/loader/display/assettextureloader.h"
#include "asset/loader/display/assettilesetloader.h"
#include "display/shader/shaderunlit.h"
font_t FONT_DEFAULT;
errorret_t textInit(void) {
assetloaderinput_t input = { .texture = TEXTURE_FORMAT_RGBA };
assetentry_t *entryTexture = assetLock(
"ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input
);
assetentry_t *entryTileset = assetLock(
"ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL
);
errorChain(assetRequireLoaded(entryTexture));
errorChain(assetRequireLoaded(entryTileset));
FONT_DEFAULT.texture = &entryTexture->data.texture;
FONT_DEFAULT.tileset = &entryTileset->data.tileset;
errorChain(fontDefaultInit());
errorOk();
}
errorret_t textDispose(void) {
FONT_DEFAULT.texture = NULL;
FONT_DEFAULT.tileset = NULL;
assetUnlock("ui/minogram.png");
assetUnlock("ui/minogram.dtf");
errorChain(fontDefaultDispose());
errorOk();
}
-2
View File
@@ -12,8 +12,6 @@
#define TEXT_CHAR_START '!'
extern font_t FONT_DEFAULT;
/**
* Initializes the text system.
*
+2 -2
View File
@@ -12,7 +12,7 @@
#include "display/display.h"
texture_t TEXTURE_WHITE;
color_t TEXTURE_WHITE_PIXELS[4*4] = {
color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = {
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
@@ -20,7 +20,7 @@ color_t TEXTURE_WHITE_PIXELS[4*4] = {
};
texture_t TEXTURE_TEST;
color_t TEXTURE_TEST_PIXELS[4*4] = {
color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = {
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK,
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
+5 -2
View File
@@ -17,6 +17,9 @@
#error "textureDisposePlatform should not be defined."
#endif
#define TEXTURE_FIXED_WIDTH 4
#define TEXTURE_FIXED_HEIGHT 4
typedef textureformatplatform_t textureformat_t;
typedef textureplatform_t texture_t;
@@ -29,9 +32,9 @@ typedef union texturedata_u {
} texturedata_t;
extern texture_t TEXTURE_WHITE;
extern color_t TEXTURE_WHITE_PIXELS[4*4];
extern color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT];
extern texture_t TEXTURE_TEST;
extern color_t TEXTURE_TEST_PIXELS[4*4];
extern color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT];
/**
* Initializes a texture.
+17 -9
View File
@@ -16,10 +16,12 @@
#include "asset/asset.h"
#include "ui/ui.h"
#include "assert/assert.h"
#include "network/network.h"
#ifdef DUSK_NETWORK
#include "network/network.h"
#endif
#include "system/system.h"
#include "console/console.h"
#include "save/save.h"
#include "save/savemanager.h"\
engine_t ENGINE;
@@ -37,12 +39,14 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorChain(systemInit());
errorChain(inputInit());
errorChain(assetInit());
// errorChain(saveInit());
errorChain(saveManagerInit());
errorChain(localeManagerInit());
errorChain(displayInit());
errorChain(uiInit());
errorChain(rpgInit());
errorChain(networkInit());
#ifdef DUSK_NETWORK
errorChain(networkInit());
#endif
errorChain(sceneInit());
consolePrint("Engine initialized");
@@ -53,15 +57,17 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
consolePrint("Assertions real");
#endif
sceneSet(SCENE_TYPE_OVERWORLD);
sceneSet(SCENE_TYPE_INITIAL);
errorOk();
}
errorret_t engineUpdate(void) {
// Order here is important.
errorChain(networkUpdate());
#ifdef DUSK_NETWORK
errorChain(networkUpdate());
#endif
errorChain(saveManagerUpdate());
timeUpdate();
inputUpdate();
consoleUpdate();
@@ -82,13 +88,15 @@ void engineExit(void) {
errorret_t engineDispose(void) {
errorChain(sceneDispose());
errorChain(networkDispose());
#ifdef DUSK_NETWORK
errorChain(networkDispose());
#endif
errorChain(rpgDispose());
localeManagerDispose();
errorChain(uiDispose());
consoleDispose();
errorChain(displayDispose());
// errorChain(saveDispose());
errorChain(saveManagerDispose());
errorChain(assetDispose());
errorOk();
-16
View File
@@ -22,20 +22,6 @@ errorret_t inputInit(void) {
INPUT.actions[i].action = (inputaction_t)i;
INPUT.actions[i].lastValue = 0.0f;
INPUT.actions[i].currentValue = 0.0f;
eventInit(
&INPUT.actions[i].onPressed,
INPUT.actions[i].onPressedCallbacks,
INPUT.actions[i].onPressedUsers,
INPUT_ACTION_CALLBACK_COUNT_MAX
);
eventInit(
&INPUT.actions[i].onReleased,
INPUT.actions[i].onReleasedCallbacks,
INPUT.actions[i].onReleasedUsers,
INPUT_ACTION_CALLBACK_COUNT_MAX
);
}
#ifdef inputInitPlatform
@@ -106,8 +92,6 @@ void inputUpdate(void) {
inputactiondata_t *act = &INPUT.actions[i];
bool_t isDown = act->currentValue > 0.0f;
bool_t wasDown = act->lastValue > 0.0f;
if(isDown && !wasDown) eventInvoke(&act->onPressed, act);
if(!isDown && wasDown) eventInvoke(&act->onReleased, act);
}
}
-4
View File
@@ -10,12 +10,8 @@
#include "inputbutton.h"
#include "inputaction.h"
#define INPUT_LISTENER_PRESSED_MAX 16
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
typedef struct {
inputactiondata_t actions[INPUT_ACTION_COUNT];
inputplatform_t platform;
} input_t;
-8
View File
@@ -8,7 +8,6 @@
#pragma once
#include "time/time.h"
#include "input/inputactiondefs.h"
#include "event/event.h"
#define INPUT_ACTION_CALLBACK_COUNT_MAX 4
@@ -21,13 +20,6 @@ typedef struct {
float_t lastDynamicValue;
float_t currentDynamicValue;
#endif
eventcallback_t onPressedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
void *onPressedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
event_t onPressed;
eventcallback_t onReleasedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
void *onReleasedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
event_t onReleased;
} inputactiondata_t;
/**
+25 -1
View File
@@ -13,7 +13,31 @@ typedef struct {
const char_t *file;
} localeinfo_t;
static const localeinfo_t LOCALE_EN_US = {
static const localeinfo_t LOCALE_INFO_EN_US = {
.name = "en-US",
.file = "locale/en_US.po",
};
static const localeinfo_t LOCALE_INFO_JP_JP = {
.name = "ja-JP",
.file = "locale/jp_JP.po",
};
static const localeinfo_t LOCALE_INFO_ES_MX = {
.name = "es-MX",
.file = "locale/es_MX.po",
};
static const localeinfo_t * const LOCALE_INFO_LIST[] = {
&LOCALE_INFO_EN_US,
&LOCALE_INFO_JP_JP,
&LOCALE_INFO_ES_MX
};
#define LOCALE_INFO_LIST_COUNT ( \
sizeof(LOCALE_INFO_LIST) / sizeof(LOCALE_INFO_LIST[0]) \
)
#define LOCALE_DEFAULT LOCALE_INFO_EN_US
// EOF
+13 -1
View File
@@ -7,13 +7,22 @@
#include "localemanager.h"
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
#include "ui/ui.h"
#include "system/system.h"
#include "console/console.h"
localemanager_t LOCALE;
errorret_t localeManagerInit() {
memoryZero(&LOCALE, sizeof(localemanager_t));
errorChain(localeManagerSetLocale(&LOCALE_EN_US));
// TODO: Set locale based on system locale.
const localeinfo_t *locale = systemGetLocale();
errorChain(localeManagerSetLocale(locale));
consolePrint("Locale set to: %s", locale->name);
errorOk();
}
@@ -30,6 +39,9 @@ errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
assetEntryLock(LOCALE.entry);
errorChain(assetRequireLoaded(LOCALE.entry));
// TODO : Trigger UI update.
errorChain(uiUpdateTranslations());
errorOk();
}
+1 -1
View File
@@ -7,7 +7,7 @@
#pragma once
#include "error/error.h"
#include "localemanager.h"
#include "locale/localemanager.h"
#include "locale/localeinfo.h"
#include "asset/asset.h"
+1 -1
View File
@@ -8,10 +8,10 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
rpg.c
rpgcamera.c
rpgtextbox.c
)
# Subdirs
add_subdirectory(battle)
add_subdirectory(cutscene)
add_subdirectory(entity)
add_subdirectory(overworld)
+15
View File
@@ -0,0 +1,15 @@
# 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
)
# Subdirs
add_subdirectory(testbattle)
+326
View File
@@ -0,0 +1,326 @@
/**
* 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"
#include "rpg/cutscene/cutscenesystem.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;
battleSetState(BATTLE_STATE_OPENING);
}
void battleDispose(void) {
battleInit();
}
battlefighter_t *battleGetCurrentFighter(void) {
if(BATTLE.state != BATTLE_STATE_PLAYER_SELECTION) return NULL;
if(BATTLE.selectionIndex >= BATTLE.executionCount) return NULL;
return &BATTLE.fighters[BATTLE.executionOrder[BATTLE.selectionIndex]];
}
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
uint8_t count = 0;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
if(BATTLE.fighters[i].team != team) continue;
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
count++;
}
return count;
}
void battleResolveAttack(
battlefighter_t *attacker,
battlefighter_t *defender
) {
assertNotNull(attacker, "Attacker cannot be NULL");
assertNotNull(defender, "Defender cannot be NULL");
const int32_t rawDamage =
(int32_t)attacker->stats.attack - (int32_t)defender->stats.defense;
const uint16_t damage = rawDamage > 0 ? (uint16_t)rawDamage : 1;
defender->health = damage >= defender->health ? 0 : defender->health - damage;
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
}
battleresult_t battleCheckResult(void) {
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
battleSetResult(BATTLE_RESULT_LOSS);
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
battleSetResult(BATTLE_RESULT_WIN);
}
return BATTLE.result;
}
void battleQueueAction(
const uint8_t fighterIndex,
const battleactiontype_t type,
const uint8_t targetIndex
) {
battleaction_t *action = &BATTLE.actions[fighterIndex];
action->type = type;
action->targetIndex = targetIndex;
if(BATTLE.onActionDecided != NULL) {
BATTLE.onActionDecided(&BATTLE.fighters[fighterIndex], action);
}
}
void battlePlayerAttack(const uint8_t targetIndex) {
battlefighter_t *fighter = battleGetCurrentFighter();
if(fighter == NULL) return;
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
if(!battleFighterIsAlive(&BATTLE.fighters[targetIndex])) return;
battleQueueAction(fighter->id, BATTLE_ACTION_ATTACK, targetIndex);
BATTLE.selectionIndex++;
battleAdvanceSelection();
}
void battlePlayerFlee(void) {
battlefighter_t *fighter = battleGetCurrentFighter();
if(fighter == NULL) return;
if(!BATTLE.fleeAvailable) return;
battleSetResult(BATTLE_RESULT_FLED);
}
void battleUpdate(void) {
if(BATTLE.state == BATTLE_STATE_NONE) return;
if(BATTLE.state == BATTLE_STATE_ENDED) return;
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_BATTLE) return;
switch(BATTLE.state) {
case BATTLE_STATE_OPENING:
battleSetState(BATTLE_STATE_PRE_ROUND);
break;
case BATTLE_STATE_PRE_ROUND:
battleUpdatePreRound();
break;
case BATTLE_STATE_AI_SELECTION:
battleUpdateAiSelection();
break;
case BATTLE_STATE_MOVES_EXECUTING:
battleUpdateMovesExecuting();
break;
case BATTLE_STATE_POST_ROUND:
battleUpdatePostRound();
break;
default:
// BATTLE_STATE_PLAYER_SELECTION: waits on battlePlayerAttack/Flee.
// BATTLE_STATE_NONE/ENDED: handled above.
break;
}
}
void battleBuildExecutionOrder(const bool_t applyEncounterBias) {
BATTLE.executionCount = 0;
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
BATTLE.executionOrder[BATTLE.executionCount++] = i;
}
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
for(uint8_t i = 1; i < BATTLE.executionCount; i++) {
const uint8_t key = BATTLE.executionOrder[i];
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
int8_t j = (int8_t)i - 1;
while(
j >= 0 &&
BATTLE.fighters[BATTLE.executionOrder[j]].stats.speed < keySpeed
) {
BATTLE.executionOrder[j + 1] = BATTLE.executionOrder[j];
j--;
}
BATTLE.executionOrder[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.executionCount; i++) {
if(BATTLE.fighters[BATTLE.executionOrder[i]].team != team) continue;
sorted[count++] = BATTLE.executionOrder[i];
}
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
if(BATTLE.fighters[BATTLE.executionOrder[i]].team == team) continue;
sorted[count++] = BATTLE.executionOrder[i];
}
memoryCopy(BATTLE.executionOrder, sorted, sizeof(uint8_t) * BATTLE.executionCount);
}
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;
}
void battleSetState(const battlestate_t next) {
const battlestate_t previous = BATTLE.state;
BATTLE.state = next;
if(BATTLE.onStateChanged != NULL) BATTLE.onStateChanged(previous, next);
}
void battleSetResult(const battleresult_t result) {
BATTLE.result = result;
battleSetState(BATTLE_STATE_ENDED);
}
bool_t battleFighterNeedsDecision(
const uint8_t fighterIndex,
const battlefightercontroller_t controller
) {
return battleFighterIsAlive(&BATTLE.fighters[fighterIndex])
&& BATTLE.fighters[fighterIndex].controller == controller
&& BATTLE.actions[fighterIndex].type == BATTLE_ACTION_NONE;
}
void battleAdvanceSelection(void) {
while(BATTLE.selectionIndex < BATTLE.executionCount) {
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.selectionIndex];
if(
battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_PLAYER)
) {
return;
}
BATTLE.selectionIndex++;
}
battleSetState(BATTLE_STATE_AI_SELECTION);
}
void battleUpdatePreRound(void) {
// No need to clear BATTLE.actions here: every living fighter's action is
// unconditionally reset to BATTLE_ACTION_NONE as it's processed in
// battleUpdateMovesExecuting, and round 1 starts pre-zeroed by
// battleInit(). Clearing it here would also wipe out any action a
// cutscene force-queued while parked at BATTLE_STATE_PRE_ROUND.
battleBuildExecutionOrder(BATTLE.round == 1);
BATTLE.selectionIndex = 0;
battleSetState(BATTLE_STATE_PLAYER_SELECTION);
battleAdvanceSelection();
}
void battleUpdateAiSelection(void) {
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
const uint8_t fighterIndex = BATTLE.executionOrder[i];
if(
!battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_AI)
) continue;
battlefighter_t *target =
battleAIChooseTarget(&BATTLE.fighters[fighterIndex]);
if(target == NULL) continue;
battleQueueAction(fighterIndex, BATTLE_ACTION_ATTACK, target->id);
}
BATTLE.executionIndex = 0;
battleSetState(BATTLE_STATE_MOVES_EXECUTING);
}
void battleUpdateMovesExecuting(void) {
if(BATTLE.executionIndex >= BATTLE.executionCount) {
battleSetState(BATTLE_STATE_POST_ROUND);
return;
}
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.executionIndex++];
battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
if(!battleFighterIsAlive(fighter)) return;
battleaction_t *action = &BATTLE.actions[fighterIndex];
if(action->type == BATTLE_ACTION_ATTACK) {
battlefighter_t *target = &BATTLE.fighters[action->targetIndex];
if(battleFighterIsAlive(target)) battleResolveAttack(fighter, target);
}
action->type = BATTLE_ACTION_NONE;
battleCheckResult();
}
void battleUpdatePostRound(void) {
BATTLE.round++;
battleSetState(BATTLE_STATE_PRE_ROUND);
}
+313
View File
@@ -0,0 +1,313 @@
/**
* 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;
// Where BATTLE currently is within a round. A cutscene can pause progression
// (CUTSCENE_PAUSE_BATTLE) and use CUTSCENE_BATTLE_WAIT_STATE to synchronize
// with any of these, or CUTSCENE_BATTLE_FORCE_ACTION to decide a fighter's
// action ahead of PLAYER_SELECTION/AI_SELECTION reaching them.
typedef enum {
BATTLE_STATE_NONE, // Battle inactive.
BATTLE_STATE_OPENING, // Entered once by battleStart().
BATTLE_STATE_PRE_ROUND, // Rebuilds execution order, clears the action queue.
BATTLE_STATE_PLAYER_SELECTION, // Waits on battlePlayerAttack/Flee.
BATTLE_STATE_AI_SELECTION, // Auto-queues every undecided AI fighter.
BATTLE_STATE_MOVES_EXECUTING, // Resolves one queued action per update.
BATTLE_STATE_POST_ROUND, // Round wrap-up; loops back to PRE_ROUND.
BATTLE_STATE_ENDED, // Terminal for WIN/LOSS/FLED alike -- see BATTLE.result.
BATTLE_STATE_COUNT
} battlestate_t;
typedef enum {
BATTLE_ACTION_NONE, // No action decided yet for this fighter this round.
BATTLE_ACTION_ATTACK,
BATTLE_ACTION_COUNT
} battleactiontype_t;
typedef struct {
battleactiontype_t type;
uint8_t targetIndex; // Meaningful for BATTLE_ACTION_ATTACK.
} battleaction_t;
typedef void (*battlestatechangedcallback_t)(
const battlestate_t previous,
const battlestate_t next
);
typedef void (*battleactiondecidedcallback_t)(
const battlefighter_t *fighter,
const battleaction_t *action
);
typedef struct {
battlestate_t state;
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
battleaction_t actions[BATTLE_FIGHTER_COUNT_MAX];
battleencountertype_t encounterType;
bool_t fleeAvailable;
battleresult_t result;
// Fighter indices (into fighters[]), sorted for the current round.
uint8_t executionOrder[BATTLE_FIGHTER_COUNT_MAX];
uint8_t executionCount;
uint8_t executionIndex;
uint16_t round;
// Cursor into executionOrder used by BATTLE_STATE_PLAYER_SELECTION to find
// the next player-controlled fighter that still needs a decision.
uint8_t selectionIndex;
battlestatechangedcallback_t onStateChanged;
battleactiondecidedcallback_t onActionDecided;
} battle_t;
extern battle_t BATTLE;
/**
* 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: enters BATTLE_STATE_OPENING and marks the battle
* active. Call once every fighter has been added via battleAddFighter.
*
* @param encounterType Determines the opening round's execution order.
* @param fleeAvailable Whether the party may attempt to flee this battle.
*/
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 currently awaiting a player decision.
*
* @return Pointer to the fighter awaiting a decision, or NULL if the battle
* isn't in BATTLE_STATE_PLAYER_SELECTION or every player-controlled
* fighter has already decided.
*/
battlefighter_t *battleGetCurrentFighter(void);
/**
* Returns the number of living fighters on a team.
*
* @param team The team to count.
* @return Count of living fighters on that team.
*/
uint8_t battleGetAliveCount(const battlefighterteam_t team);
/**
* Resolves a physical attack from attacker onto defender: damage is the
* attacker's attack stat minus the defender's defense stat (minimum 1),
* subtracted from the defender's health. The defender is marked dead
* once health reaches 0.
*
* @param attacker The attacking fighter.
* @param defender The defending fighter.
*/
void battleResolveAttack(
battlefighter_t *attacker,
battlefighter_t *defender
);
/**
* Checks whether the battle has been won or lost, transitioning to
* BATTLE_STATE_ENDED and updating BATTLE.result if so. Does nothing if a
* result has already been set (e.g. by a successful flee).
*
* @return The battle's current result.
*/
battleresult_t battleCheckResult(void);
/**
* Queues an action for a fighter to perform once BATTLE_STATE_MOVES_EXECUTING
* reaches them this round, overwriting any action already queued for that
* fighter. Fires BATTLE.onActionDecided.
*
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
* @param type The type of action to perform.
* @param targetIndex Index into BATTLE.fighters of the target, meaningful
* for BATTLE_ACTION_ATTACK.
*/
void battleQueueAction(
const uint8_t fighterIndex,
const battleactiontype_t type,
const uint8_t targetIndex
);
/**
* Submits the currently-selecting fighter's attack against a target, if the
* battle is in BATTLE_STATE_PLAYER_SELECTION and awaiting a decision.
* Queues the action and advances the selection cursor.
*
* @param targetIndex Index into BATTLE.fighters of the target.
*/
void battlePlayerAttack(const uint8_t targetIndex);
/**
* Submits a flee attempt for the currently-selecting fighter, if the battle
* is in BATTLE_STATE_PLAYER_SELECTION and fleeing is available for this
* battle. Always succeeds, ending the battle with BATTLE_RESULT_FLED.
*/
void battlePlayerFlee(void);
/**
* Updates the battle simulation for one frame, dispatching on BATTLE.state.
* No-op if the battle isn't active, has already ended, or
* CUTSCENE_PAUSE_BATTLE is set.
*/
void battleUpdate(void);
/**
* Rebuilds BATTLE.executionOrder/executionCount from every currently living
* fighter, sorted by speed descending.
*
* @param applyEncounterBias If true, reorders the freshly speed-sorted
* queue so BATTLE.encounterType's favoured team goes first (used only
* for the opening round).
*/
void battleBuildExecutionOrder(const bool_t applyEncounterBias);
/**
* Stably partitions BATTLE.executionOrder so every fighter on the given team
* comes first, preserving each side's relative (speed-sorted) order.
*
* @param team The team to move to the front of the execution order.
*/
void battleMoveTeamFirst(const battlefighterteam_t team);
/**
* Picks an AI target for fighter: the lowest-health living fighter on
* the opposing team.
*
* @param fighter The AI-controlled fighter choosing a target.
* @return The chosen target, or NULL if the opposing team has no
* living fighters.
*/
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
/**
* Sets BATTLE.state and fires BATTLE.onStateChanged with the previous and
* new state.
*
* @param next The state to transition to.
*/
void battleSetState(const battlestate_t next);
/**
* Sets BATTLE.result and transitions to BATTLE_STATE_ENDED.
*
* @param result The result to end the battle with.
*/
void battleSetResult(const battleresult_t result);
/**
* Checks whether a fighter is a live, undecided candidate for the given
* controller -- i.e. whether PLAYER_SELECTION or AI_SELECTION should still
* be deciding an action for it this round.
*
* @param fighterIndex Index into BATTLE.fighters to check.
* @param controller The controller PLAYER_SELECTION/AI_SELECTION is
* currently deciding for.
* @return True if the fighter is alive, matches controller, and has no
* action queued yet.
*/
bool_t battleFighterNeedsDecision(
const uint8_t fighterIndex,
const battlefightercontroller_t controller
);
/**
* Advances BATTLE.selectionIndex to the next player-controlled fighter that
* still needs a decision, or transitions to BATTLE_STATE_AI_SELECTION once
* none remain.
*/
void battleAdvanceSelection(void);
/**
* Handles BATTLE_STATE_PRE_ROUND: rebuilds the execution order and moves on
* to BATTLE_STATE_PLAYER_SELECTION, positioning the selection cursor.
*/
void battleUpdatePreRound(void);
/**
* Handles BATTLE_STATE_AI_SELECTION: queues an attack for every undecided
* AI-controlled fighter, then moves on to BATTLE_STATE_MOVES_EXECUTING.
*/
void battleUpdateAiSelection(void);
/**
* Handles BATTLE_STATE_MOVES_EXECUTING: resolves one queued action from
* BATTLE.executionOrder per call, or transitions to BATTLE_STATE_POST_ROUND
* once every fighter this round has been processed.
*/
void battleUpdateMovesExecuting(void);
/**
* Handles BATTLE_STATE_POST_ROUND: advances BATTLE.round and transitions
* back to BATTLE_STATE_PRE_ROUND.
*/
void battleUpdatePostRound(void);
+43
View File
@@ -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;
}
+87
View File
@@ -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);
+71
View File
@@ -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;
}
+73
View File
@@ -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,6 +6,5 @@
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
savevita.c
savestreamvita.c
testbattle.c
)
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "testbattle.h"
#include "rpg/battle/battle.h"
#include "scene/scene.h"
void testBattleStart(void) {
battleInit();
const battlefighterstats_t allyOneStats =
{ .attack = 10, .defense = 5, .magic = 0, .speed = 10, .luck = 0 };
const battlefighterstats_t allyTwoStats =
{ .attack = 8, .defense = 4, .magic = 0, .speed = 8, .luck = 0 };
const battlefighterstats_t enemyOneStats =
{ .attack = 6, .defense = 3, .magic = 0, .speed = 6, .luck = 0 };
const battlefighterstats_t enemyTwoStats =
{ .attack = 7, .defense = 3, .magic = 0, .speed = 5, .luck = 0 };
battleAddFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
allyOneStats, 30, 10
);
battleAddFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
allyTwoStats, 25, 10
);
battleAddFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
enemyOneStats, 20, 5
);
battleAddFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
enemyTwoStats, 20, 5
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
sceneSet(SCENE_TYPE_BATTLE);
}
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
/**
* TEMPORARY test hook: sets up a hardcoded mock battle and switches to the
* battle scene, so the battle scene (camera/fighters/HUD) can be seen and
* played without a real encounter trigger yet.
*/
void testBattleStart(void);
-1
View File
@@ -7,7 +7,6 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
cutscenesystem.c
cutscenemode.c
)
# Subdirs
+240
View File
@@ -7,8 +7,248 @@
#pragma once
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenepause.h"
typedef struct cutscene_s {
const cutsceneitem_t *items;
uint8_t itemCount;
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 } \
}
// Waits until BATTLE.state reaches STATE. Put this BEFORE
// CUTSCENE_SET_PAUSE(CUTSCENE_PAUSE_BATTLE), not after -- pausing first
// freezes BATTLE.state wherever it already is, so it would never reach
// STATE on its own to satisfy the wait. Waiting unpaused, then pausing the
// moment it's satisfied, catches the battle right at STATE before it can
// advance further.
#define CUTSCENE_BATTLE_WAIT_STATE(STATE) \
{ \
.type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE, \
.battleWaitState = { .state = STATE } \
}
// Immediately queues an attack for FIGHTER_INDEX against TARGET_INDEX,
// bypassing normal player/AI selection for that fighter this round.
#define CUTSCENE_BATTLE_FORCE_ACTION(FIGHTER_INDEX, TARGET_INDEX) \
{ \
.type = CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION, \
.battleForceAction = { \
.fighterIndex = FIGHTER_INDEX, .targetIndex = TARGET_INDEX \
} \
}
#define CUTSCENE_SET_PAUSE(FLAGS) \
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
#define CUTSCENE_ITEM_GIVE(ITEM_ID, QUANTITY) \
{ \
.type = CUTSCENE_ITEM_TYPE_ITEM_GIVE, \
.itemGive = { .item = ITEM_ID, .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)
-19
View File
@@ -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;
}
}
-26
View File
@@ -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();
+26
View File
@@ -0,0 +1,26 @@
/**
* 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_BATTLE ((cutscenepause_t)(1 << 3))
#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 | \
CUTSCENE_PAUSE_BATTLE \
))
+100 -5
View File
@@ -6,7 +6,9 @@
*/
#include "cutscenesystem.h"
#include "rpg/entity/entity.h"
#include "util/memory.h"
#include "assert/assert.h"
cutscenesystem_t CUTSCENE_SYSTEM;
@@ -15,9 +17,28 @@ void cutsceneSystemInit() {
}
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.mode = CUTSCENE_MODE_INITIAL;
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so start wraps.
CUTSCENE_SYSTEM.pause = cutscene->pause;
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();
}
@@ -25,7 +46,7 @@ void cutsceneSystemUpdate() {
if(CUTSCENE_SYSTEM.scene == NULL) return;
const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
cutsceneItemUpdate(item, &CUTSCENE_SYSTEM.data);
if(cutsceneItemUpdate(item, &CUTSCENE_SYSTEM.data)) cutsceneSystemNext();
}
void cutsceneSystemNext() {
@@ -39,7 +60,13 @@ void cutsceneSystemNext() {
) {
CUTSCENE_SYSTEM.scene = NULL;
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;
}
@@ -55,8 +82,76 @@ const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
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() {
CUTSCENE_SYSTEM.scene = NULL;
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;
}
+68 -3
View File
@@ -7,15 +7,37 @@
#pragma once
#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 {
const cutscene_t *scene;
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).
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;
extern cutscenesystem_t CUTSCENE_SYSTEM;
@@ -26,12 +48,55 @@ extern cutscenesystem_t CUTSCENE_SYSTEM;
void cutsceneSystemInit();
/**
* Start a cutscene.
* Start a cutscene with no bound entities.
*
* @param cutscene Pointer to the cutscene to start.
*/
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.
*/
+8 -2
View File
@@ -3,9 +3,15 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
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,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
cutscenestartbattle.c
cutscenebattlewaitstate.c
cutscenebattleforceaction.c
)
@@ -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"
void cutsceneBattleForceActionStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
battleQueueAction(
item->battleForceAction.fighterIndex, BATTLE_ACTION_ATTACK,
item->battleForceAction.targetIndex
);
}
bool_t cutsceneBattleForceActionUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -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/battle/battle.h"
typedef struct {
uint8_t fighterIndex;
uint8_t targetIndex;
} cutscenebattleforceaction_t;
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
/**
* Starts a battle force-action step: immediately queues an attack for the
* given fighter against the given target, bypassing normal player/AI
* selection for that fighter this round.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneBattleForceActionStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a battle force-action step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneBattleForceActionUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,15 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
bool_t cutsceneBattleWaitStateUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return BATTLE.state == item->battleWaitState.state;
}
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/battle/battle.h"
typedef struct {
battlestate_t state;
} cutscenebattlewaitstate_t;
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
/**
* Updates a battle wait-state step, completing once BATTLE.state reaches
* the watched state. Has no Start callback -- there's nothing to do until
* the state is actually reached.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once BATTLE.state equals the watched state.
*/
bool_t cutsceneBattleWaitStateUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -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;
}
+27 -1
View File
@@ -8,4 +8,30 @@
#pragma once
#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);

Some files were not shown because too many files have changed in this diff Show More