Files
dota_factory/.claude/CLAUDE.md

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 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 BeamFiredEvents 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.