Skip to main content

Project Context

Project knowledge. Agent-facing; read this before any work. Not the doc for humans.

Overview

A Final Fantasy Tactics-like tactical RPG (grid-based, turn-based combat with height, movement, and class/job systems) built in Godot 4.x with GDScript. Codename: operation_tactics — the real name is TBD. Nothing about the codename should leak into user-facing strings.

Goals

  • Faithful FFT-style tactical combat: isometric(ish) grid, move/act/end-turn flow, terrain height, facing, and line-of-sight.
  • Job/class system with learning abilities from action (JP), not just leveling.
  • Branching/unique characters, story-driven campaign.
  • One cohesive game, not a tech demo — a single unified codebase.

Current State

  • Engine files exist (project.godot, directory skeleton). Map data core (registry, schema, binary codec), the 3D heightfield renderer (MapView/MapTheme), the seeded cosmetic layer (MapDetails), a demo map asset, and the custom test pipeline are in place. main.tscn boots the demo map with an FFT-style isometric camera (WASD pan / QE rotate / wheel zoom) and a 60-second day-night cycle (src/render/day_night.gd, cycle_seconds var).
  • Rules layer + pathfinding pipeline built — facts-vs-rules split is done (TileType = facts; MaterialRules/MovementRules/UnitProfile = judgments) with a budgeted Dijkstra pathfinder (Pathfinder/PathEvaluator) providing FFT movement-bubble reachability, lowest-cost paths, hazard reports, and fly/float/swim/status support. See "Rules & Pathfinding pipeline".
  • Combat designed; multiplayer foundation builtCOMBAT.md is the authoritative battle spec (deterministic CT turn order, streamlined stats, the eight elements, living-map mutations as commits). Foundation primitives exist: Elements, DeterministicRng, RulesetVersion, Commit/CommitLog (with the commit vocabulary), and streamlined combat stats + affinities on UnitProfile. See "Combat & multiplayer foundation".
Procedural arena — voxel_arena.vmap is the seeded showcase/test map: rolling noise terrain + stamped features covering all 10 materials, all 5 shapes, rotations 0-3, hazards, real void, multi-level climbing, and a pathable plaza. Generated by src/map/arena_generator.gd (ArenaGenerator), written by tools/build_voxel_arena.tscn, validated by tests/map/voxel_arena_test.gd (determinism + asset-in-sync + feature coverage). Now the default map in main.gd. Test suite is 107117 green. Repository created on Forgejo and pushed. Repo uses the SHA-256 object format — never recreate it as sha1, and never rely on push-to-create for this repo (Forgejo's push-to-create ignores object format and would make a sha1 repo).

Engineering Standards

Applies to every agent and human working here. Goal: a codebase humans can maintain long-term, not one that merely "works."

Code quality

  • Human-first. Idiomatic GDScript, clear names, logical file layout. Readability first, efficiency second — but never accept needless waste (hot paths: avoid per-frame allocations).
  • Concise. Smallest correct change. No speculative generality, no backward-compat shims without a concrete need.
  • Reuse over copy. Shared frameworks (tile registry, map codec; later combat math, pathfinding) live once and are used everywhere. Rule of three: third copy → extract.
  • Modular & layered. Scripts stay focused (one responsibility); layers point one way — data/logic must not import rendering; cross-cutting concerns use signals. No spaghetti, no circular references.
  • Debuggable. Keep logic in deterministic functions (pure input → output) where practical; avoid hidden global mutation.

Comment policy

  • Comment the WHY and the intent: design decisions, non-obvious data formats, invariants, cross-module contracts. A reviewer should understand your intent.
  • Never narrate WHAT the code does; no AI-style filler (e.g. # get the player position). No emoji. Code should look hand-written.

Testing (mandatory, Factorio-style)

  • Before every commit: ./run_tests.sh runs the full suite headless and must pass; a pre-commit hook enforces it. Never commit red.
  • Suite covers: codec round-trip (byte-exact), registry append-only guard, and every subsystem's core behavior as it lands.
  • Benchmark mode (--bench, planned) for perf-sensitive paths (combat sim, map load); checked for regressions, not just pass/fail.
  • Test layout: res://tests/**/*_test.gd, custom runner (see res://tests/run_tests.gd). Tests extend the TestCase base and use its eq/is_true/... helpers.

Growing-project guardrails

  • Format/version drift → schema version in every binary format; loaders reject mismatches loudly, never silently.
  • Duplication creep → rule of three + review.
  • Scope creep → anything new must fit the modular layout or the plan changes (and this doc gets updated).
  • Dead code → no "just in case" branches; keep the tree lean.
  • Conventions drift → this doc is the arbiter; update it when rules change.
  • Single-file bloat → split when a script exceeds ~300 lines or has more than one job.

Map Data Architecture

MAP_RULES.md is the authoritative world-model spec — read it before touching anything map-related. It defines the voxel model, tile types, shapes, terrain/blocks, movement thresholds, fluids, dynamic edits, generation, validation, and the agent rules. This section is the quick-reference; MAP_RULES.md wins on any conflict.

Direction: voxel pivot. VoxelMap (src/map/voxel_map.gd): a chunked 16³ grid of 1×1×1 voxels (open-world ready; columns are subsumed as vertical runs of solid voxels). Each voxel is {type, shape, rot} (2 bytes packed; TileType.Shape = FULL/HALF/SLAB/SLOPE/WEDGE…, per-voxel shape + rotation for the editor). Serialized by VoxelCodec (voxel_codec.gd, schema 3, .vmap files). VoxelMesh (src/render/voxel_mesh.gd) builds the combined shape mesh with face culling (interior faces shared with a full solid neighbor are dropped); VoxelView renders it. main.gd defaults to the voxel demo (assets/maps/voxel_demo.vmap, built by tools/build_voxel_demo.tscn) and loads .vmap as voxels vs .otmap as the legacy column model (still present until the column code is retired). Cosmetic layer, hover/select and picking are column-only for now.

Global design (see res://src/map/ for implementation). Maps are 2D grids of columns: each cell owns its entire z column, so interior-under-roof and bridges are expressible as stacked walkable surfaces.

  • TileType (tile_type.gd) — one shared entry per tile kind: material facts only (id, block_height, shape, solid, fluid). Behavior (walkability, cost, hazards, flags) lives in the rules layer (MaterialRules), never here. Logic only — visuals live in a separate theme table (type_id → scene/texture).
  • TileTypeRegistry (tile_type_registry.gd) — the global catalog. Append-only: never reorder/delete entries, maps reference types by index, so adding types is always safe. version guards against breaking changes; loaders reject catalog_version > registry.version.
  • MapData (map_data.gd) — size, catalog_version, spawn points, and a column per cell: { elevation, ground (type index), layers: [{z, type, rot}] }.
  • MapCodec (map_codec.gd) — StreamPeerBuffer (little-endian) ↔ PackedByteArray. Binary layout is the contract (see file header): magic OTMP + schema version + catalog version + dims + spawns + per-cell: flags u8, elevation u16, ground u16, optional layer list. Map files use the .otmap extension.
  • MapPicker (src/map/map_picker.gd) — resolves a cursor ray to the specific block hit (cell + surface height), not just the column: DDA over the grid + ray-vs-AABB on every block. View-aware (RENDER fills/ thin layers vs RAW data heights). main.gd feeds hover/selected cell+height into the top-grid shader, so a click on a roof highlights the roof, a click on a cliff face highlights that block.
  • MapTheme (src/render/map_theme.gd) — visual theme, type_id → color (later scene/texture). Kept out of TileType so map data stays renderer-agnostic; swapping the theme restyles the whole game.
  • MapView (src/render/map_view.gd) — the single data→rendering bridge: one combined ArrayMesh. Two view modes (MapView.ViewMode, toggled with F1 in main.gd): RENDER = the game look (terrain fills to the map base — the bedrock rule, no terrain voids; layers drawn thin; cosmetic layer on; no wireframe) and RAW = the map data literally (ground blocks at [elevation, elevation + block_height] with voids intact, layers at full data height, flat colors + wireframe, no cosmetics) — the geometry rules/pathfinding are written against the RAW view. Renderer direction is 3D heightfield. The RENDER top grid is a fragment shader (src/render/top_grid.gdshader) on a duplicate of the terrain mesh: + crosses at lattice corners + dashed edges, uniform and anti-aliased, tinted per hover_cell/selected_cell uniforms. main.gd picks the cell under the cursor (hover on mouse move, blue select on left-click) and feeds the uniforms.
  • MapDetails (src/render/map_details.gd) — the cosmetic layer (two-layer model): a pure deterministic function of (map, seed) producing per-cell tint variance, C0-continuous surface jitter (no seams), and scattered props (rocks/bushes/tufts via MultiMesh). Purely visual — never feeds back into map data or rules.
  • Two-layer model + seed contractMapData.seed is part of the map (codec field, schema 2). Ground truth is the column blocks (drives rules/pathfinding later); the cosmetic layer is derived from (map, seed) and regenerated freely — same seed, same result, everywhere. Determinism rules: integer-hash noise keyed by (seed, x, y, z), fixed iteration order, no engine RNG. The seed can later drive other derived content (spawns, variants).
  • DemoRegistry (src/map/demo_registry.gd) — canonical demo catalog; stand-in for a future on-disk catalog file (append-only by index). The demo map asset (res://assets/maps/demo.otmap, seed 12345) is generated by tools/build_demo_map.tscn.
  • TestRegistry (src/map/test_registry.gd) + test map (assets/maps/test_map.otmap, 53×27, seed 20260815) — the scenario museum: every hazard, elevation shape, layered structure, wall/corridor, rotation, and spawn surface the model can express, laid out in 8×6 zones (see tools/build_test_map.gd — its coordinates are the contract; tests/map/test_map_test.gd spot-checks each zone). View it with godot --path . -- --map=res://assets/maps/test_map.otmap.
  • Procedural maps — a generator is just another producer of MapData (same shape as the demo builder); it flows through the same codec and renderer. Generators must produce valid MapData (indices within the registry, in-bounds elevations/layers).
  • In-game map editor (planned) — mutates in-memory MapData and re-packs through the same codec; palette is the registry. Editor stays an orthogonal consumer of the schema.

Rules & Pathfinding pipeline

  • MaterialRules (src/rules/material_rules.gd) — the rules layer: how the world treats each material. One TerrainRule per type id: passable, move_cost, hazard/hazard_damage, behavioral flags, status_on_enter. for_registry() yields the rule set for a catalog (defaults + per-id overrides — the single behavioral source for demo/test ids). All editor-editable as Resources.
  • MovementRules (src/rules/movement_rules.gd) — the single tunable module for MAP_RULES §8.3 thresholds: free_step (0.25), step_ceiling (1.0), climb_cost_per_step, water_cost_extra.
  • UnitProfile (src/rules/unit_profile.gd) — unit mobility: move_budget, traits (FLOAT/FLY/SWIM), status effects (slow/stop/immobilize modify the budget; float/swim statuses grant traits).
  • PathEvaluator (src/pathfinding/path_evaluator.gd) — composes map facts (column surfaces from voxel shape geometry) × rules × unit state into per-step verdicts (step_cost, hazard_at).
  • Pathfinder (src/pathfinding/pathfinder.gd) — budgeted 4-directional Dijkstra over the surface graph. find_path() → lowest-cost path to a destination; reachability() → the FFT movement bubble (cost field + parents). PathResult carries the ordered path, total cost, and every hazardous cell crossed (movement/combat applies the rule's damage/status as the unit traverses it).
  • Pathfinding operates on (x, y, surface) nodes derived from the voxel grid; inter-surface transitions (stairs, ledges) are edge costs per MAP_RULES §8.3. Surface heights come from shape geometry until the schema-4 height byte lands.

Combat & multiplayer foundation

COMBAT.md is the authoritative battle spec — read it before touching combat. It locks the FFT baseline (turn order, turn flow, occupancy, targeting) and defines our divergences: deterministic CT scheduling, the eight-element system with affinities, and the living map (destruction/placement/field events/collapse, all as commits).

  • Elements (src/rules/elements.gd) — the eight elements (fire/ice/lightning/water/wind/earth/holy/dark) + affinity helpers (effective_multiplier, standing_multiplier).
  • UnitProfile (src/rules/unit_profile.gd) — now carries the streamlined stats (hp/mp/atk/def/mag/mdf/spd) and per-element affinities alongside mobility.
  • DeterministicRng (src/rules/deterministic_rng.gd) — SplitMix64-seeded PRNG; the ONLY randomness source allowed in simulation paths (COMBAT.md §7.3, MAP_RULES §14.8).
  • RulesetVersion (src/rules/ruleset_version.gd) — FNV-1a hash of catalog + material rules + movement rules; a match pins it, mismatched clients refuse to join.
  • Commit/CommitLog (src/net/) — the append-only, sequence-numbered state-change stream (the shared clock of a match). The kind vocabulary (move_unit, set_block, remove_block, apply_damage, …) is COMBAT.md §10.1, reflected as Commit.KIND_* constants.
  • Turn controller, damage math, targeting, field effects, collapse, and the snapshot codec: [planned] (specified in COMBAT.md).

Conventions

  • Godot 4.x GDScript; follow the godot4 skill (theme/font override quirks, ConfigFile gotchas, explicit type annotations — type warnings treated as errors). Comments explain intent, not narration (see Engineering Standards).
  • snake_case for scripts/vars, PascalCase for node names.
  • Branch main; commits concise, imperative mood, lowercase, focused on "why"; push frequently.

Docs & Wiki

  • Canonical docs live in the repo: GAME_BIBLE.md (the game bible — vision, systems, pipelines, baselines, glossary; read it first), MAP_RULES.md (authoritative world-model spec), COMBAT.md (authoritative battle spec), and this file.
  • Mirrored to the wiki (https://wiki.fifthdread.com/books/operation-tactics) by tools/sync_wiki.py — the wiki is a rendered view, the repo is the source of truth.
  • Sync (idempotent; creates missing pages, updates existing): source ~/opencode/api_secrets.sh && python3 tools/sync_wiki.py. --check previews without writing. Which docs map to which pages is declared in tools/wiki_manifest.json.

Shipping

Shipping = commit and push to the project repo.

  • Remote: Forgejo — ssh://git@forgejo.fifthdread.com:223/Fifthdread/operation_tactics.git. SHA-256 object format — the repo must be pre-created on the server as sha256; push-to-create will not work for this repo.
  • If the game is ready for distribution on Arch-based Linux: also update the PKGBUILD so paru -S <pkg> installs it.
  • PKGBUILD lives in the Fifthdread/pkgbuilds repo at ~/opencode/pkgbuilds/ (one dir per package, -git variant recommended for an in-development game).
  • Workflow: bump pkgver= (rev-count.commit), regenerate makepkg --printsrcinfo > <pkg>/.SRCINFO, commit .SRCINFO alongside, push. Full detail in the pkgbuilds project's CONTEXT.md/AGENTS.md.