Project Context
operation_tactics — CONTEXT.md
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.tscnboots 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_secondsvar). - 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". Test suite is 84 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.shruns 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 (seeres://tests/run_tests.gd). Tests extend theTestCasebase and use itseq/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.versionguards against breaking changes; loaders rejectcatalog_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): magicOTMP+ schema version + catalog version + dims + spawns + per-cell: flags u8, elevation u16, ground u16, optional layer list. Map files use the.otmapextension.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.gdfeedshover/selectedcell+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 ofTileTypeso map data stays renderer-agnostic; swapping the theme restyles the whole game.MapView(src/render/map_view.gd) — the single data→rendering bridge: one combinedArrayMesh. Two view modes (MapView.ViewMode, toggled with F1 inmain.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 perhover_cell/selected_celluniforms.main.gdpicks 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 viaMultiMesh). Purely visual — never feeds back into map data or rules.- Two-layer model + seed contract —
MapData.seedis 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 bytools/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 (seetools/build_test_map.gd— its coordinates are the contract;tests/map/test_map_test.gdspot-checks each zone). View it withgodot --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 validMapData(indices within the registry, in-bounds elevations/layers). - In-game map editor (planned) — mutates in-memory
MapDataand 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. OneTerrainRuleper type id:passable,move_cost,hazard/hazard_damage, behavioralflags,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).PathResultcarries 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-4heightbyte lands.
Conventions
- Godot 4.x GDScript; follow the
godot4skill (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.
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/pkgbuildsrepo at~/opencode/pkgbuilds/(one dir per package,-gitvariant recommended for an in-development game). - Workflow: bump
pkgver=(rev-count.commit), regeneratemakepkg --printsrcinfo > <pkg>/.SRCINFO, commit.SRCINFOalongside, push. Full detail in the pkgbuilds project's CONTEXT.md/AGENTS.md.