README.adoc (9926B)
1 = phantasia 2 3 2D SDL3 RPG prototype. 4 5 == Direction 6 7 * FFVI-like top-down movement and camera feel. 8 * Visible enemies instead of random encounters. 9 * FFX-inspired CTB combat whose turn order responds to agility and action speed. 10 * Loot-heavy progression with meaningful build choices. 11 * Optional, discoverable story that does not interrupt play. 12 * A small set of clear rules that gains depth through interaction, like chess. 13 14 == Principles 15 16 Phantasia's details may change while these principles remain stable: 17 18 * Put performance, simplicity, and the core gameplay loop first. 19 * Prefer the smallest clear implementation. Remove, reuse, or combine before 20 adding abstractions, special cases, files, or substantial code. 21 * Build foundations that expand through typed data and configuration. New 22 content should not require initialization or rendering special cases. 23 * Treat file count and lines of code as costs, but never trade clarity or 24 correctness for a smaller number. 25 * Verify changes carefully and preserve existing behavior deliberately. 26 * Keep compile-time settings visible through the suckless 27 `config.def.h`/`config.h` model. 28 29 == Architecture 30 31 Phantasia uses a small, reusable 2D RPG engine. Its systems operate on entities, 32 items, interactions, area markers, and caller-supplied catalogs rather than named 33 game content. The headless engine builds as `bin/libph-engine.a`, shared by 34 Phantasia and the independent Parcel Run example. It is not yet a standalone 35 SDK with a stable public API. 36 37 `src/engine` contains reusable systems and must never include `game/*`. 38 `src/game` owns Phantasia's content, menus and overlays, and application startup; 39 it may depend on the engine. Generic code belongs in the engine whenever it can 40 operate entirely on engine types and supplied data. 41 42 Core logic and game flow remain headless-testable. `game/game` owns menu state, 43 action routing, and world/battle transitions; `game/presentation` draws it. 44 `game/main` translates SDL input into game actions and owns application lifecycle 45 and media playback. Escape and Backspace remain separate actions. 46 47 Rendering and audio use opaque engine contexts, asset IDs, and engine-owned 48 rectangles, colors, and flip modes. SDL textures, streams, and devices live in 49 the SDL backend implementation; `engine/sdl.h` is the host's construction bridge. 50 The renderer owns its loaded textures and borrows the host's SDL render target, 51 so it must be destroyed before that target. Audio owns its clips and device. 52 53 World drawing and game presentation compile without SDL headers. The host supplies 54 configured key labels as text, and only backend code issues SDL drawing/audio 55 calls. The current backend is SDL; this boundary permits another implementation 56 without requiring a runtime plugin system. Headless executables do not link SDL, 57 even when it is installed. 58 The host advances simulation at a fixed tick rate, configured by 59 `PH_SIMULATION_HZ`. `PH_MAX_CATCHUP_STEPS` bounds work after a slow frame; excess 60 time is discarded rather than left as a growing backlog. Integer clock credit 61 retains fractional ticks without floating-point scheduling drift. Rendering runs 62 once per host frame and currently displays the latest simulation state without 63 interpolation. Held input is sampled per host frame; brief interaction presses 64 are retained until a simulation tick runs. 65 66 Menus discard paused time. Input-driven menu/mode/battle-phase changes discard 67 the current frame's elapsed time so a resumed game cannot catch up paused time. 68 Interactions and presentation events are handled after each simulation tick. 69 Identical input per tick and random seeds reproduce simulation within the same 70 build; this does not promise identical results across platforms. 71 72 Record a fresh session and reproduce it headlessly with: 73 74 [source,sh] 75 ---- 76 ./bin/phantasia --record /tmp/bug.phr --seed 12345 77 ./bin/phantasia --replay /tmp/bug.phr 78 ---- 79 80 Recording starts directly in a new world and leaves the normal autosave alone. 81 The optional seed defaults to `0x5048414e`. Quit normally to finish the recording; 82 truncated or malformed streams fail replay with a nonzero exit status. Replay 83 requires no window or audio device, including in builds without SDL, and reports 84 the tick count and final area, mode, and random-generator state. 85 86 The versioned binary stream stores the seed and simulation rate, movement and 87 interaction input for every executed tick, and ordered menu/battle actions before 88 the next tick. Paused menus therefore preserve action order without advancing 89 simulation. Rendering, debug controls, and wall-clock delays are excluded. Keep 90 the same build, maps, content, and simulation configuration when reproducing a 91 bug: the header checks format version and tick rate, not content compatibility. 92 This is a debugging format, not a portable save format or a cross-platform 93 floating-point determinism guarantee. `make game-smoke` exercises replay through 94 seeded combat and paused menus, compares resulting game state, and checks corrupt 95 streams. Tests can supply a specific starting fixture to `ph_replay_run` for a 96 focused regression; command-line recordings always start from a fresh world. 97 98 Camera state belongs to presentation. World drawing accepts a caller-supplied 99 camera, and player following reads the world without changing simulation. World 100 startup and save loading are independent of viewport dimensions. Compile-time 101 simulation configuration remains in the engine; this is an incremental 102 separation, not a general-purpose SDK. 103 104 For the current implementation, start with `src/engine/*.h` for the engine's 105 public data types and operations, `src/game/content.c` for a concrete game setup, 106 and `src/tests/` for headless and SDL integration examples. Source remains 107 authoritative for exact systems, types, and content as they evolve. 108 109 == Content and configuration 110 111 The first build copies tracked defaults from `config.def.h` to ignored local 112 `config.h`. Later builds preserve `config.h`, which is the sole source of active 113 settings. A configuration API version rejects stale files so missing values are 114 never replaced by silent core-code fallbacks. 115 116 Configuration owns adjustable settings; `game/content` owns typed game data. 117 Maps own placement, while content assigns semantics to their markers. Adding an 118 instance should normally change only a map; adding content should change the 119 catalog and map rather than engine code. 120 121 Before loading a save or starting a game, a headless preflight validates catalogs 122 and every configured map. It checks IDs, starting equipment, rewards, spells, 123 shops and interactions, tile indices, marker placements, spawn limits, and portal 124 arrivals. Diagnostics identify the offending catalog entry or area, with map 125 coordinates where applicable (zero-based). Maps are loaded one at a time and 126 released after inspection; validation does not populate world state. Asset 127 references are checked against the catalogs; media decoding remains the backend's 128 responsibility. 129 130 Vendored media is grouped by role under `assets/vendor/graphics` and 131 `assets/vendor/audio`. Each upstream asset directory keeps its own source and 132 license record. 133 134 == Build 135 136 [source,sh] 137 ---- 138 make 139 ---- 140 141 Without SDL3, build and run the headless smoke suite: 142 143 [source,sh] 144 ---- 145 make smoke 146 ---- 147 148 Exercise SDL rendering in a headless environment: 149 150 [source,sh] 151 ---- 152 make render-smoke 153 ---- 154 155 Validate content and exercise invalid-data regression fixtures without SDL: 156 157 [source,sh] 158 ---- 159 make content-check 160 ---- 161 162 Run all checks, including content validation, headless menu/battle flow, the 163 independent example, SDL-free presentation compilation, and media pixel/lifecycle 164 checks: 165 166 [source,sh] 167 ---- 168 make check 169 ---- 170 171 Run the SDL-free Parcel Run example, which supplies its own map, content, terminal 172 UI, and delivery rule without linking any `game/*` code: 173 174 [source,sh] 175 ---- 176 make engine-example # builds and checks a scripted delivery 177 ./bin/ph-courier # WASD then Enter; Q to quit 178 ---- 179 180 `make smoke game-smoke engine-example` requires no SDL or graphics conversion. 181 `make render-smoke` additionally exercises the SDL backend using dummy drivers. 182 183 With SDL3 available: 184 185 [source,sh] 186 ---- 187 ./bin/phantasia 188 man ./phantasia.6 189 ---- 190 191 At startup, choose Continue to restore the autosave or New Game to replace it. 192 The game saves atomically on area transitions and clean exit. The save path is 193 configured by `PH_SAVE_PATH`; static map tiles are reloaded from game data while 194 player progression and each visited area's mutable actors and ground items are 195 persisted. 196 Starting combat creates a stable pre-combat checkpoint. Exiting during combat 197 does not overwrite it with a partial battle; victory saves the finalized result. 198 199 == Debugging 200 201 Press F3 (`PH_KEY_DEBUG`) to toggle the debug overlay in any mode. In the world, 202 red outlines mark blocking tiles, green circles show actor collision radii, and 203 blue circles show reserved movement destinations. Blue squares label non-player 204 movement targets with their active entity index. These use simulation positions 205 and the same camera as world drawing; collision uses center distance, not sprite 206 bounds. Colors and the toggle key are configured in `config.h`. 207 208 The header shows the previous completed frame's duration (including frame pacing), 209 update and draw times in milliseconds, and executed simulation ticks. Update time 210 includes tick work, audio, and autosaves; draw time includes presentation. It also 211 shows current world-event queue occupancy and a cumulative count of events lost 212 to a full queue. The loss counter survives queue draining and area travel, resets 213 when a world starts or loads, and is not saved. The overlay starts disabled and 214 never advances simulation or consumes events. 215 216 == Maps 217 218 Sources live in `data/maps/*.txt`. `make` compiles them with `bin/ph-mapc` into 219 `obj/maps/*.phmap`. Tile and marker meanings are typed data in `game/content.c`, 220 which is the authoritative reference as maps evolve.