phantasia

Phantasia - 2D SDL3 RPG prototype.
git clone git://git.beep.wimdupont.com/phantasia.git
Log | Files | Refs | README | LICENSE

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.