156 lines
8.1 KiB
Markdown
156 lines
8.1 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Interaction
|
|
|
|
* ONLY modify code or other files if explicitly asked to do so
|
|
|
|
## Project Overview
|
|
|
|
Dota Factory is a single-player game that blends a Factorio-style factory builder with
|
|
DOTA-style wave defence. The player builds a factory on an asteroid — mining ores,
|
|
transporting materials over belts and splitters, and crafting through a config-defined
|
|
production tree — to supply shipyards that produce autonomous combat ships. Those ships
|
|
fight off endless enemy waves advancing from the right. See `docs/concept.md` for the full design.
|
|
|
|
|
|
## Project Structure
|
|
|
|
* the project root and the git repository root are the same directory
|
|
* project requirements can be found at `docs/requirements.md`
|
|
* architecture decisions can be found at `docs/architecture.md`
|
|
* game content design (ship/module roster, layout grids, footprint gating) can be found at `docs/content_design.md`
|
|
* replay/determinism design can be found at `docs/replay_design.md`
|
|
* balancing rules, targets, tuned numbers, process, and history live under `docs/balancing/`
|
|
|
|
Requirements carry stable `REQ-<AREA>-<NAME>` ids. They are cited throughout the code in
|
|
comments — when changing behavior, find the governing REQ id first and
|
|
keep the citation accurate.
|
|
|
|
## Coding Guidelines
|
|
|
|
* avoid duplicate code
|
|
* do not use the "auto" keyword, with two exceptions:
|
|
* **named local lambdas** — a lambda's type is unnameable, and `std::function`
|
|
is not an acceptable substitute in per-tick code because it adds a heap
|
|
allocation and an indirect call
|
|
* **iterator types** — `auto it = m_buildings.find(id)` is allowed where
|
|
spelling the iterator out adds length without adding information
|
|
* everywhere else the type is written out; in particular `auto` is not used
|
|
for plain values, return values, or range-for element types
|
|
* use Qt utility data types (like QPoint, QVector3D, QString, etc.)
|
|
* wrap strings that appear in the UI with Qt's "tr()"
|
|
* use the EventManager/EventHandler instead of defining own signals and slots
|
|
* use std::optional if a variable can be "not set"
|
|
* start the name of a getter method with "get"
|
|
* don't use abbreviations, except very common ones ("s" for seconds, "min", "max", etc.)
|
|
* if a variable holds a value that has a unit or if a function returns a value that has a unit, append that unit to the name (e.g. "m_shipVelocity_mps", "getAcceleration_mpss()")
|
|
* always enclose scopes in braces
|
|
|
|
## Build
|
|
|
|
Requires CMake 3.14.4+, a C++17 compiler, and Qt 5 (developed against Qt 5.12.3,
|
|
MSVC 2017 x64; `Qt5_DIR` is cached in `build/CMakeCache.txt`). Needs Qt components
|
|
Widgets, Network, Multimedia, Charts, Svg, plus OpenGL.
|
|
|
|
External dependencies vendored under `src/external/`:
|
|
* **toml++** — reading TOML config files
|
|
* **tinyexpr** — evaluating formula strings from config files
|
|
* **EnTT** — entity registry backing the ship/station/debris simulation
|
|
* **Catch2** — test framework
|
|
|
|
Configure and build (a configured `build/` tree already exists):
|
|
|
|
```sh
|
|
cmake -S . -B build # configure (multi-config VS generator)
|
|
cmake --build build --config Debug # all targets
|
|
cmake --build build --config Debug --target DotaFactory_test
|
|
```
|
|
|
|
Targets: `DotaFactory` (app), `DotaFactory_lib`, `DotaFactory_ui`, `DotaFactory_test`,
|
|
`DotaFactory_balancing`. Executables land in `build/DotaFactory/<Config>/{app,balancing}/`.
|
|
|
|
**Adding a source file requires editing CMake.** Every directory under `src/` has its own
|
|
`CMakeLists.txt` listing files explicitly in `HDRS`/`SRCS` (or `TEST_FILES` for tests) —
|
|
there is no globbing. A new file that is not registered simply will not compile.
|
|
|
|
Config data is not copied: `CONFIG_DIR` is a compile definition pointing at
|
|
`bin/app/data/config` for the app and balancing tool, and `bin/test/data/config` for
|
|
tests (a separate fixture set). On Windows the build also junctions `bin/*/data` into the
|
|
output directories and copies the Qt DLLs.
|
|
|
|
Run the app: `build/DotaFactory/Debug/app/DotaFactory.exe`, optionally
|
|
`--replay <file>` for view-only playback of a recorded run.
|
|
|
|
## Tests
|
|
|
|
Catch2, single executable, links `lib` only — no QApplication, no display.
|
|
|
|
```sh
|
|
build/DotaFactory/Debug/app/DotaFactory_test.exe # all
|
|
build/DotaFactory/Debug/app/DotaFactory_test.exe "[belt],[building]" # by tag
|
|
build/DotaFactory/Debug/app/DotaFactory_test.exe "BeltSystem: *" # by name pattern
|
|
build/DotaFactory/Debug/app/DotaFactory_test.exe --reporter compact
|
|
```
|
|
|
|
Common tags: `[building] [belt] [behavior] [blueprint] [modules] [config] [wave] [combat]
|
|
[replay] [determinism] [ship] [debris] [threat] [unlock]`.
|
|
|
|
`src/test/SimulationTestAccess.h` is a friend-struct backdoor to `Simulation`'s private
|
|
mutators; tests use it instead of duplicating the command path. It lives under `src/test`
|
|
and is deliberately off the lib/ui/app include path.
|
|
|
|
## Verification Tools
|
|
|
|
Python scripts in `tools/` read the real configs and are the first check
|
|
after config edits (see `docs/balancing/process.md`):
|
|
|
|
* `verify_recipes.py` — recipe-tree closure, visuals coverage, orphan items
|
|
* `verify_layouts.py` — module footprint gating per hull layout
|
|
* `threat_report.py` — item/module/ship threat values, ratios, belt feasibility
|
|
|
|
The `DotaFactory_balancing` target runs parallel arena simulations from
|
|
`bin/balancing/data/balancing.toml` for combat-stat tuning.
|
|
|
|
## Architecture
|
|
|
|
See `docs/architecture.md` for the full write-up. Highlights and the
|
|
invariants that are easy to break:
|
|
|
|
* Strict simulation/presentation split, enforced at the CMake target level: `lib`
|
|
(sim + config, Qt Core/Gui only — no QtWidgets), `ui` (QtWidgets + QOpenGLWidget),
|
|
`app` (thin main), `test` (Catch2 against `lib`).
|
|
* Fixed 30 Hz tick simulation, 60 FPS render, accumulator-driven; game speed is a
|
|
tick-rate multiplier. All sim quantities are in ticks, never wall-clock seconds.
|
|
* The tick order in `Simulation::tick()` is load-bearing for determinism — see the
|
|
Tick Order section of `architecture.md` before reordering systems.
|
|
* **Command chokepoint:** every sim mutation during play flows through
|
|
`Simulation::apply(const Command&)` (see `sim/Command.h`, `CommandManager`), so runs can
|
|
be recorded and replayed. Commands reference stable ids (`BuildingId`, tile coords,
|
|
choice indices) — never raw `entt::entity` handles. UI code must not call sim mutators
|
|
directly. Determinism is checksummed (`StateChecksum`) and covered by
|
|
`DeterminismTest` / `ReplayPlaybackTest`.
|
|
* Config is loaded once at startup, formulas compiled once via tinyexpr, immutable
|
|
afterwards; malformed config aborts startup rather than failing mid-game. Restart
|
|
reloads config from disk (REQ-CFG-RELOAD).
|
|
* **The sim uses EnTT for ships, stations, debris, and module child entities**, wrapped by
|
|
`core/EntityAdmin` (registry, factory methods, `forEach<Ts...>` views). Components live
|
|
in `lib/ecs/component/`, systems in `lib/ecs/system/`. Note: `architecture.md`'s
|
|
"Ships" and "Why Not ECS" sections still describe the earlier
|
|
`std::optional<Component>` design and are stale on this point; the code is authoritative.
|
|
Buildings and the belt subsystem stay outside the entity model.
|
|
* Ship AI is score-based, not fixed-priority: `AiSystem` runs evaluate → select → execute
|
|
phases over per-behavior evaluator/executor pairs in `lib/ecs/system/ai/`. Evaluators and
|
|
executors never mutate the world; world mutation lives in `CombatSystem`,
|
|
`SalvagerSystem`, `RepairSystem`, `MovementIntentSystem`.
|
|
* Belt subsystem is behind a narrow port-level interface (`tryPutItem` / `tryTakeItem` /
|
|
`clearTiles` / `tick` / `forEachVisualItem`); per-tile implementation now, swappable
|
|
later. No other system asks "what is on tile X".
|
|
* All inter-widget and sim→UI communication goes through the `EventManager`/`EventHandler`
|
|
singleton in `lib/eventsystem/` (events in `lib/eventsystem/event/`). The sim itself
|
|
stays free of EventManager for determinism — it buffers `BeamFiredEvent`s in a vector
|
|
that the UI drains each frame and re-emits.
|
|
* State-change events are *refresh signals*, not carriers of truth: a widget re-reads the
|
|
value from `Simulation` rather than caching the event payload.
|