7.6 KiB
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
- 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):
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.
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 itemsverify_layouts.py— module footprint gating per hull layoutthreat_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 againstlib). - 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 ofarchitecture.mdbefore reordering systems. - Command chokepoint: every sim mutation during play flows through
Simulation::apply(const Command&)(seesim/Command.h,CommandManager), so runs can be recorded and replayed. Commands reference stable ids (BuildingId, tile coords, choice indices) — never rawentt::entityhandles. UI code must not call sim mutators directly. Determinism is checksummed (StateChecksum) and covered byDeterminismTest/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 inlib/ecs/component/, systems inlib/ecs/system/. Note:architecture.md's "Ships" and "Why Not ECS" sections still describe the earlierstd::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:
AiSystemruns evaluate → select → execute phases over per-behavior evaluator/executor pairs inlib/ecs/system/ai/. Evaluators and executors never mutate the world; world mutation lives inCombatSystem,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/EventHandlersingleton inlib/eventsystem/(events inlib/eventsystem/event/). The sim itself stays free of EventManager for determinism — it buffersBeamFiredEvents 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
Simulationrather than caching the event payload.