Compare commits
355 Commits
1e6d838258
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| ade15c19d5 | |||
| abe8c8f448 | |||
| 8292bc6134 | |||
| b51a9c5c77 | |||
| b7fe5cfcf2 | |||
| 1ffacae2fb | |||
| 44ec3c51f1 | |||
| dcc801f017 | |||
| 099d614395 | |||
| 1554d99739 | |||
| 0e64f45a5e | |||
| fb65cbabf4 | |||
| 9b88deff69 | |||
| f03003194f | |||
| 7327343b2a | |||
| da3f417464 | |||
| 01aa1a08b0 | |||
| 5968e5f40a | |||
| 1e24b87640 | |||
| b0fa0da813 | |||
| 731b8874c9 | |||
| 392a2b8d00 | |||
| abaccc45b5 | |||
| b2148f00ac | |||
| 9490a96e12 | |||
| fd0c246bc0 | |||
| 9c275e283c | |||
| 41c45d73ce | |||
| f39e4f3506 | |||
| 3c63205c6c | |||
| a1c567715e | |||
| 1cf7c264c1 | |||
| d36e59fd26 | |||
| 9bbade2420 | |||
| 4ee6438405 | |||
| 0b859bd1a4 | |||
| dd061082fb | |||
| b6b6acb99c | |||
| 7e670fc01c | |||
| 28656bea7e | |||
| bd8fe5157e | |||
| a3fddf63cc | |||
| 1ddd830476 | |||
| f255224ccd | |||
| 3274df91d4 | |||
| d1da4b2937 | |||
| 37378e3c1b | |||
| 37a7a7499a | |||
| 5794a51c9a | |||
| 09858f4b62 | |||
| ca49b9dbec | |||
| 79650ae211 | |||
| c0b009c548 | |||
| 7ae5f8c4dc | |||
| 9f574fa05e | |||
| e5d689c8d8 | |||
| 0b8f5eccd6 | |||
| 40415e663b | |||
| 7a098edf72 | |||
| 365641feb7 | |||
| 4b5e288632 | |||
| cab7244b00 | |||
| 980fad272f | |||
| 68a43a7d98 | |||
| 1f754de431 | |||
| f61f0bf761 | |||
| 075e44d295 | |||
| 2289277e12 | |||
| cc0ef856e3 | |||
| 603f8063e9 | |||
| 3b37b0ecf8 | |||
| 98deab932a | |||
| 08d8b0dd90 | |||
| 9a3b6c10d6 | |||
| cd31af2611 | |||
| fd6a7c5815 | |||
| 18cfe238f6 | |||
| 2cbcf1554f | |||
| c1af58d80c | |||
| f39fe9a118 | |||
| e5dcb9de5f | |||
| dc83add5c6 | |||
| 3a1951559d | |||
| 26d7448492 | |||
| 202f583067 | |||
| d5ab44b9bf | |||
| bb1ffab8fc | |||
| e1445fe508 | |||
| caa810f66d | |||
| f6df95abb2 | |||
| fa9dbd62ad | |||
| 2af09d9eb1 | |||
| 4f7fdb8a4c | |||
| 949937d2c2 | |||
| 4c166bf47f | |||
| 60260540cd | |||
| 1f4503176b | |||
| fd85e8e10a | |||
| 114a43b205 | |||
| d87d063b10 | |||
| 537597c854 | |||
| 9c3be0fbd0 | |||
| 58b94223f7 | |||
| 1fb63cce4e | |||
| 0b7e94b4e4 | |||
| 0408336cf9 | |||
| 46932e4abf | |||
| 0edea5d961 | |||
| 60cc187d92 | |||
| 3990351a16 | |||
| bd344e4fbe | |||
| 64c344c3a3 | |||
| 02c7fed9b4 | |||
| 5d4a975384 | |||
| 475df0e5fd | |||
| 61634f6fd2 | |||
| c8ff7da345 | |||
| d664ab54cc | |||
| 906000b0e9 | |||
| d9ef6aa728 | |||
| b44a85685e | |||
| edd1c31785 | |||
| 785ce3ebfe | |||
| 7017f8b4dc | |||
| 77a842f884 | |||
| d5ba72d554 | |||
| 83177729e9 | |||
| a8e933b7f2 | |||
| e02e323cb2 | |||
| b4e622daa5 | |||
| 1150985c1f | |||
| 594c3b93c5 | |||
| 932b57720c | |||
| ca727bef35 | |||
| 553a7e0701 | |||
| af6828c348 | |||
| 3671e1d7e6 | |||
| 370a3036c1 | |||
| f60f111ccc | |||
| 2518f4f14a | |||
| 561c95d0dd | |||
| b4be06ed5e | |||
| d1051607b2 | |||
| 79b79ab7c3 | |||
| 0bba7686e6 | |||
| f766ae4a86 | |||
| 8b71fe1a03 | |||
| 11daa61714 | |||
| 60d6767d93 | |||
| 7c1455b8a0 | |||
| e20a0bba67 | |||
| b2ce20e6ad | |||
| a9082c57f3 | |||
| a8a6a04f1e | |||
| a75222f111 | |||
| 9b63af6ccb | |||
| 4ca5b332cd | |||
| 0c4eb480be | |||
| b2c1ea34fd | |||
| 1cbc695bc5 | |||
| c7ecce6ac4 | |||
| be475e2836 | |||
| 9622fa4345 | |||
| 1cdafb7bcd | |||
| f205136e21 | |||
| b708e1b29d | |||
| c76ba07bc7 | |||
| b2e7e4897a | |||
| c21af63e84 | |||
| a4267a4760 | |||
| d412c69f82 | |||
| 08752aeced | |||
| d465671cfc | |||
| 76812ba0c5 | |||
| 9c1e948ab4 | |||
| 23ff406101 | |||
| 2ea0cb815d | |||
| 876b344b20 | |||
| cd75492796 | |||
| 486296feee | |||
| c9f14970a1 | |||
| 6a8c456aa1 | |||
| af8a2224c0 | |||
| 4b149d97a7 | |||
| 6a8b6acd3b | |||
| 3ce6e6d599 | |||
| 80a2622267 | |||
| 535d4f8f24 | |||
| 8c4fb78fc9 | |||
| 698dd4d13d | |||
| 9d28175b17 | |||
| ac4d56764c | |||
| 69fe607157 | |||
| 92e896b973 | |||
| dd7c997816 | |||
| ad3e73fdd8 | |||
| 177809fe1a | |||
| 69f655d179 | |||
| 271855781c | |||
| 5f6ecbf6c8 | |||
| cd966daba9 | |||
| 88bc4f2170 | |||
| 4986c1bac8 | |||
| e110e7e413 | |||
| 6274b5ce60 | |||
| 16ed6a5695 | |||
| 13dacab274 | |||
| 2ddf13238c | |||
| cc38bf95fa | |||
| b58bcfe272 | |||
| de1ebb8a5f | |||
| 24c18f8ac6 | |||
| 2cedd5d433 | |||
| f11db0c072 | |||
| 808e0c6a7b | |||
| e4ea4ca4b4 | |||
| fef22b9f86 | |||
| cdf89ce0dd | |||
| e8786c3922 | |||
| fc622670d2 | |||
| e32d384c99 | |||
| 24e0999d8a | |||
| fcaee000fa | |||
| 751ef27a7b | |||
| 3c1376828c | |||
| bd0675db66 | |||
| 5b86b15c71 | |||
| c6db4bf24a | |||
| 6ea0655eaf | |||
| 58d4586d00 | |||
| 18e732ae99 | |||
| 81b1c7a66b | |||
| 5bc581bcb8 | |||
| 0a7e9a34ef | |||
| d74ba5bfad | |||
| cf68ac2862 | |||
| b301db2008 | |||
| 31a8915b0f | |||
| f818c90af0 | |||
| 577927ef70 | |||
| d271d65678 | |||
| e5017ab3c5 | |||
| 59688e6532 | |||
| c7218c7c1e | |||
| c43225b6fa | |||
| 405206211e | |||
| 3d577a11db | |||
| a472ec196c | |||
| 665060bcd2 | |||
| 4818997164 | |||
| 9573b9789a | |||
| 7924e037aa | |||
| c371b43a6d | |||
| abab2bbb6e | |||
| 313fed02ca | |||
| b95eaaaded | |||
| 41c8ed2938 | |||
| 7f4ea93a70 | |||
| 1a682fdb79 | |||
| e0e11b7933 | |||
| 0cf3d64983 | |||
| 1324a320e2 | |||
| 5219b227c5 | |||
| 0e02d9ec4a | |||
| 74615f5293 | |||
| bd2391876c | |||
| ac97652c60 | |||
| 4153b7e2f5 | |||
| 6b7c3df64a | |||
| e8dd73bcb0 | |||
| 8451f5a281 | |||
| 0a1b58442c | |||
| 997a7778e0 | |||
| 282ace4c11 | |||
| 1ea1cc59fb | |||
| 123c544423 | |||
| 10c5ad678f | |||
| 3716c2b734 | |||
| 5317f35198 | |||
| ed17664ef1 | |||
| 49f7129bd5 | |||
| 1641189b75 | |||
| 54a6056b77 | |||
| 69b35d2bfc | |||
| af96b95f61 | |||
| aad094f842 | |||
| 26857e8414 | |||
| 510e37c37b | |||
| 121cd5407f | |||
| 7c663e29a6 | |||
| c64d31fa46 | |||
| f097e9a25f | |||
| 37a70ea321 | |||
| 8dad554800 | |||
| 6b95619806 | |||
| 66cf9ae23a | |||
| ef17b0ce42 | |||
| eeaa309c08 | |||
| 7669245229 | |||
| 4e3e3ac715 | |||
| 9677133c54 | |||
| abc261c03a | |||
| 17e9913c98 | |||
| 900b5fdec1 | |||
| 3e19e44f24 | |||
| 42b51cc6f4 | |||
| 15d8fa4f2c | |||
| b5185b0906 | |||
| 457fc47c75 | |||
| 090dc64bc4 | |||
| 64f7c9dcc1 | |||
| f921f00a0d | |||
| 9d0a60a93b | |||
| f363f7a67c | |||
| 9e36c13635 | |||
| 25ff3c56c5 | |||
| 8ad7530740 | |||
| fa714335dc | |||
| 0cd0529468 | |||
| f5f4453e2c | |||
| b57299fd2a | |||
| dc4ebd5f2d | |||
| ea79d76953 | |||
| bd488db8ef | |||
| 4e3dc51981 | |||
| 9d20048705 | |||
| ca07cbaf0e | |||
| c18c4e4804 | |||
| cc2cca2442 | |||
| dc344df457 | |||
| 452c26c8b3 | |||
| 34c6dea505 | |||
| d397b9969a | |||
| d08bf5d37b | |||
| b59e392461 | |||
| c0b6f8f778 | |||
| 3ef32ee6ba | |||
| afd8cd28fa | |||
| 6405ad6b3f | |||
| 55b42a03d9 | |||
| 426870158c | |||
| 4eaae5d940 | |||
| a4427f7f67 | |||
| 5153129909 | |||
| b30addab3d | |||
| 0ce7cd7ae8 | |||
| 1e7f602865 | |||
| ad49daa1f6 | |||
| 3f0e05fe7a | |||
| 51333f752b | |||
| 2770bf96be | |||
| 7e0104e9b8 | |||
| f37ed0507c | |||
| b0320ac117 | |||
| 58b7cda21c |
184
.claude/CLAUDE.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# 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
|
||||||
|
* keep the tone professional, brief and to the point — brevity applies to prose and
|
||||||
|
preamble, not to the substance of an objection or a design rationale
|
||||||
|
* be critical: where there is a concrete technical reason to disagree, name it once;
|
||||||
|
if the user reaffirms, proceed with their call without re-litigating
|
||||||
|
|
||||||
|
## 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
|
||||||
|
* when planning a change, weigh the long-term maintainability of the codebase instead of
|
||||||
|
defaulting to the lowest-effort patch — but no speculative generality: never build or
|
||||||
|
prepare for functionality we may never need. If the maintainable solution is much larger
|
||||||
|
than the request, say so and let the user decide the scope.
|
||||||
|
* class layout: static members first, then non-static; within each, public, then protected,
|
||||||
|
then private (Qt `slots:`/`signals:` are ordinary non-static access groups). Inside an
|
||||||
|
access group the order is: nested types, static constants, aliases, methods, fields,
|
||||||
|
friends. Out-of-line method definitions follow the declaration order. Applies to new
|
||||||
|
classes and to files being edited anyway — don't reorder existing headers just to comply.
|
||||||
|
* 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
|
||||||
|
* keep source files ASCII-only. A non-ASCII character needed at runtime (a glyph in a
|
||||||
|
UI string, a symbol drawn on a widget) is written as its code point with a comment
|
||||||
|
naming it (`const QChar shiftGlyph(0x21E7); // U+21E7 UPWARDS WHITE ARROW`), never
|
||||||
|
as a literal character: MSVC 2017 does not read the sources as UTF-8 by default and
|
||||||
|
silently mangles them. Never round-trip a source file through
|
||||||
|
`Get-Content`/`Set-Content` either: Windows PowerShell reads it as ANSI and writes
|
||||||
|
it back double-encoded with a BOM. Use the Edit/Write tools.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
**Visual verification is the user's job.** Screen-capturing the app window does not
|
||||||
|
work here: `CopyFromScreen` and `PrintWindow` both return a blank white client area
|
||||||
|
even while the app is running and rendering normally, because the capture cannot read
|
||||||
|
the composited surface of the `QOpenGLWidget`-backed window. A blank capture therefore
|
||||||
|
says nothing about whether the UI works, so do not read one as a regression and do not
|
||||||
|
try to work around it. To check a UI change: build, run the tests, launch the app, and
|
||||||
|
ask the user to look at it. Redirecting the process's stdout/stderr to a file does
|
||||||
|
work and is worth checking for Qt warnings.
|
||||||
|
|
||||||
|
## 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.
|
||||||
28
.claude/skills/bug/SKILL.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: bug
|
||||||
|
description: Investigate a reported bug, find and explain its root cause, and propose a fix — without implementing anything
|
||||||
|
argument-hint: <description of the buggy behavior>
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
A bug has been reported:
|
||||||
|
|
||||||
|
$ARGUMENTS
|
||||||
|
|
||||||
|
Investigate it and propose a solution. **Do not implement anything** — no edits, no new files, no fixes applied. The goal of this pass is understanding and a proposal the user can approve first.
|
||||||
|
|
||||||
|
Work through it like this:
|
||||||
|
|
||||||
|
1. **Pin down expected vs. actual.** Restate what the behavior should be and what it actually is. If the report is ambiguous about the conditions that trigger it, note your assumptions explicitly.
|
||||||
|
|
||||||
|
2. **Find the relevant code.** Search for the subsystem(s) involved (Grep/Glob, then Read the actual files). Don't reason from memory or from names alone — read the implementation that runs in this case.
|
||||||
|
|
||||||
|
3. **Trace the real execution path.** Follow the data/control flow step by step for the specific failing scenario. For the tick-based simulation, that means tracing the relevant systems in tick order, including the per-tick progress/cap arithmetic where it matters. Use the project's actual constants (tick rate, belt speed, etc.) rather than hand-waving.
|
||||||
|
|
||||||
|
4. **State the root cause precisely.** Name the exact mechanism, citing `file:line`. Explain *why* it produces the observed symptom — connect the cause to the visible effect concretely (e.g. "single-slot output serializes to one item per full-tile traversal, so items land ~1 tile apart"). Confirm it explains the specific trigger conditions in the report.
|
||||||
|
|
||||||
|
5. **Propose a solution.** Describe the change and where it would go (`file:line`), reusing existing patterns in the codebase. If the symptom has more than one contributing path, say so. If the fix involves a design or balance trade-off (correctness vs. throughput, lossless vs. capped, a visual side effect, etc.), surface it as a decision for the user — give a recommendation, but ask before assuming which behavior they want.
|
||||||
|
|
||||||
|
6. **Stop and hand back.** End with the proposal and any open questions. Offer to implement (and to add tests) only once the user has chosen a direction.
|
||||||
|
|
||||||
|
Keep the write-up grounded in what the code actually does — quote the lines that matter. Adhere to the repository's coding guidelines and architecture notes (see `.claude/CLAUDE.md`) when describing any proposed change.
|
||||||
91
.claude/skills/cpp-pro/SKILL.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
name: C++ Pro
|
||||||
|
description: Expert C++ developer specializing in modern C++20/23, systems programming, and high-performance computing. Masters template metaprogramming, zero-overhead abstractions, and low-level optimization with emphasis on safety and efficiency.
|
||||||
|
triggers:
|
||||||
|
- C++
|
||||||
|
- C++17
|
||||||
|
- C++20
|
||||||
|
- C++23
|
||||||
|
- modern C++
|
||||||
|
- template metaprogramming
|
||||||
|
- systems programming
|
||||||
|
- performance optimization
|
||||||
|
- SIMD
|
||||||
|
- memory management
|
||||||
|
- CMake
|
||||||
|
role: specialist
|
||||||
|
scope: implementation
|
||||||
|
output-format: code
|
||||||
|
---
|
||||||
|
|
||||||
|
# C++ Pro
|
||||||
|
|
||||||
|
Senior C++ developer with deep expertise in modern C++20/23, systems programming, high-performance computing, and zero-overhead abstractions.
|
||||||
|
|
||||||
|
## Role Definition
|
||||||
|
|
||||||
|
You are a senior C++ engineer with 15+ years of systems programming experience. You specialize in modern C++20/23, template metaprogramming, performance optimization, and building production-grade systems with emphasis on safety, efficiency, and maintainability. You follow C++ Core Guidelines and leverage cutting-edge language features.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
- Building high-performance C++ applications
|
||||||
|
- Implementing template metaprogramming solutions
|
||||||
|
- Optimizing memory-critical systems
|
||||||
|
- Developing concurrent and parallel algorithms
|
||||||
|
- Creating custom allocators and memory pools
|
||||||
|
- Systems programming and embedded development
|
||||||
|
|
||||||
|
## Core Workflow
|
||||||
|
|
||||||
|
1. **Analyze architecture** - Review build system, compiler flags, performance requirements
|
||||||
|
2. **Design with concepts** - Create type-safe interfaces using C++20 concepts
|
||||||
|
3. **Implement zero-cost** - Apply RAII, constexpr, and zero-overhead abstractions
|
||||||
|
4. **Verify quality** - Run sanitizers, static analysis, and performance benchmarks
|
||||||
|
5. **Optimize** - Profile, measure, and apply targeted optimizations
|
||||||
|
|
||||||
|
## Reference Guide
|
||||||
|
|
||||||
|
Load detailed guidance based on context:
|
||||||
|
|
||||||
|
| Topic | Reference | Load When |
|
||||||
|
|-------|-----------|-----------|
|
||||||
|
| Modern C++ Features | `references/modern-cpp.md` | C++20/23 features, concepts, ranges, coroutines |
|
||||||
|
| Template Metaprogramming | `references/templates.md` | Variadic templates, SFINAE, type traits, CRTP |
|
||||||
|
| Memory & Performance | `references/memory-performance.md` | Allocators, SIMD, cache optimization, move semantics |
|
||||||
|
| Concurrency | `references/concurrency.md` | Atomics, lock-free structures, thread pools, coroutines |
|
||||||
|
| Build & Tooling | `references/build-tooling.md` | CMake, sanitizers, static analysis, testing |
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
### MUST DO
|
||||||
|
- Follow C++ Core Guidelines
|
||||||
|
- Use concepts for template constraints
|
||||||
|
- Apply RAII universally
|
||||||
|
- Do not use `auto`
|
||||||
|
- Prefer `std::unique_ptr` and `std::shared_ptr`
|
||||||
|
- Write const-correct code
|
||||||
|
- Use forward declarations in header files if possible
|
||||||
|
- Use descriptive functions names and variable names instead of writing comments
|
||||||
|
|
||||||
|
### MUST NOT DO
|
||||||
|
- Use raw `new`/`delete` (prefer smart pointers)
|
||||||
|
- Ignore compiler warnings
|
||||||
|
- Use C-style casts (use static_cast, etc.)
|
||||||
|
- Mix exception and error code patterns inconsistently
|
||||||
|
- Write non-const-correct code
|
||||||
|
- Use `using namespace std` in headers
|
||||||
|
- Ignore undefined behavior
|
||||||
|
- Skip move semantics for expensive types
|
||||||
|
- Write lots of comments
|
||||||
|
|
||||||
|
## Output Templates
|
||||||
|
|
||||||
|
When implementing C++ features, provide:
|
||||||
|
1. Header file with interfaces and templates
|
||||||
|
2. Implementation file (when needed)
|
||||||
|
3. CMakeLists.txt updates (if applicable)
|
||||||
|
4. Test file demonstrating usage
|
||||||
|
|
||||||
|
## Knowledge Reference
|
||||||
|
|
||||||
|
C++20/23, concepts, ranges, coroutines, modules, template metaprogramming, SFINAE, type traits, CRTP, smart pointers, custom allocators, move semantics, RAII, SIMD, atomics, lock-free programming, CMake, Conan, sanitizers, clang-tidy, cppcheck, Catch2, GoogleTest
|
||||||
443
.claude/skills/cpp-pro/references/build-tooling.md
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
# Build Systems and Tooling
|
||||||
|
|
||||||
|
> Reference for: C++ Pro
|
||||||
|
> Load when: CMake, sanitizers, static analysis, testing frameworks, CI/CD
|
||||||
|
|
||||||
|
## Modern CMake
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
# Set C++ standard
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
# Export compile commands for tools
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
# Compiler warnings
|
||||||
|
if(MSVC)
|
||||||
|
add_compile_options(/W4 /WX)
|
||||||
|
else()
|
||||||
|
add_compile_options(-Wall -Wextra -Wpedantic -Werror)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Create library target
|
||||||
|
add_library(mylib
|
||||||
|
src/mylib.cpp
|
||||||
|
include/mylib.h
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(mylib
|
||||||
|
PUBLIC
|
||||||
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||||
|
$<INSTALL_INTERFACE:include>
|
||||||
|
PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(mylib PUBLIC cxx_std_20)
|
||||||
|
|
||||||
|
# Create executable
|
||||||
|
add_executable(myapp src/main.cpp)
|
||||||
|
target_link_libraries(myapp PRIVATE mylib)
|
||||||
|
|
||||||
|
# Dependencies with FetchContent
|
||||||
|
include(FetchContent)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
fmt
|
||||||
|
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
|
||||||
|
GIT_TAG 10.1.1
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(fmt)
|
||||||
|
|
||||||
|
target_link_libraries(mylib PUBLIC fmt::fmt)
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
enable_testing()
|
||||||
|
add_subdirectory(tests)
|
||||||
|
|
||||||
|
# Install rules
|
||||||
|
include(GNUInstallDirs)
|
||||||
|
install(TARGETS mylib myapp
|
||||||
|
EXPORT MyProjectTargets
|
||||||
|
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||||
|
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||||
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
install(DIRECTORY include/
|
||||||
|
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sanitizers
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
# AddressSanitizer (ASan) - memory errors
|
||||||
|
set(CMAKE_CXX_FLAGS_ASAN
|
||||||
|
"-g -O1 -fsanitize=address -fno-omit-frame-pointer"
|
||||||
|
CACHE STRING "Flags for ASan build"
|
||||||
|
)
|
||||||
|
|
||||||
|
# UndefinedBehaviorSanitizer (UBSan)
|
||||||
|
set(CMAKE_CXX_FLAGS_UBSAN
|
||||||
|
"-g -O1 -fsanitize=undefined -fno-omit-frame-pointer"
|
||||||
|
CACHE STRING "Flags for UBSan build"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ThreadSanitizer (TSan) - data races
|
||||||
|
set(CMAKE_CXX_FLAGS_TSAN
|
||||||
|
"-g -O1 -fsanitize=thread -fno-omit-frame-pointer"
|
||||||
|
CACHE STRING "Flags for TSan build"
|
||||||
|
)
|
||||||
|
|
||||||
|
# MemorySanitizer (MSan) - uninitialized reads
|
||||||
|
set(CMAKE_CXX_FLAGS_MSAN
|
||||||
|
"-g -O1 -fsanitize=memory -fno-omit-frame-pointer"
|
||||||
|
CACHE STRING "Flags for MSan build"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Usage: cmake -DCMAKE_BUILD_TYPE=ASAN ..
|
||||||
|
```
|
||||||
|
|
||||||
|
## Static Analysis
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .clang-tidy configuration
|
||||||
|
---
|
||||||
|
Checks: >
|
||||||
|
*,
|
||||||
|
-fuchsia-*,
|
||||||
|
-google-*,
|
||||||
|
-llvm-*,
|
||||||
|
-modernize-use-trailing-return-type,
|
||||||
|
-readability-identifier-length
|
||||||
|
|
||||||
|
WarningsAsErrors: '*'
|
||||||
|
|
||||||
|
CheckOptions:
|
||||||
|
- key: readability-identifier-naming.ClassCase
|
||||||
|
value: CamelCase
|
||||||
|
- key: readability-identifier-naming.FunctionCase
|
||||||
|
value: lower_case
|
||||||
|
- key: readability-identifier-naming.VariableCase
|
||||||
|
value: lower_case
|
||||||
|
- key: readability-identifier-naming.ConstantCase
|
||||||
|
value: UPPER_CASE
|
||||||
|
- key: readability-identifier-naming.MemberCase
|
||||||
|
value: lower_case
|
||||||
|
- key: readability-identifier-naming.MemberSuffix
|
||||||
|
value: '_'
|
||||||
|
- key: modernize-use-nullptr.NullMacros
|
||||||
|
value: 'NULL'
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run clang-tidy
|
||||||
|
clang-tidy src/*.cpp -p build/
|
||||||
|
|
||||||
|
# Run cppcheck
|
||||||
|
cppcheck --enable=all --std=c++20 --suppress=missingInclude src/
|
||||||
|
|
||||||
|
# Run include-what-you-use
|
||||||
|
include-what-you-use -std=c++20 src/main.cpp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with Catch2
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/benchmark/catch_benchmark.hpp>
|
||||||
|
#include "mylib.h"
|
||||||
|
|
||||||
|
TEST_CASE("Vector operations", "[vector]") {
|
||||||
|
std::vector<int> vec{1, 2, 3};
|
||||||
|
|
||||||
|
SECTION("push_back") {
|
||||||
|
vec.push_back(4);
|
||||||
|
REQUIRE(vec.size() == 4);
|
||||||
|
REQUIRE(vec.back() == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("pop_back") {
|
||||||
|
vec.pop_back();
|
||||||
|
REQUIRE(vec.size() == 2);
|
||||||
|
REQUIRE(vec.back() == 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Exception handling", "[exceptions]") {
|
||||||
|
REQUIRE_THROWS_AS(risky_function(), std::runtime_error);
|
||||||
|
REQUIRE_THROWS_WITH(risky_function(), "error message");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Floating point", "[math]") {
|
||||||
|
REQUIRE_THAT(compute_value(),
|
||||||
|
Catch::Matchers::WithinAbs(3.14, 0.01));
|
||||||
|
}
|
||||||
|
|
||||||
|
BENCHMARK("Vector creation") {
|
||||||
|
return std::vector<int>(1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
BENCHMARK("Vector fill") {
|
||||||
|
std::vector<int> vec(1000);
|
||||||
|
for (int i = 0; i < 1000; ++i) {
|
||||||
|
vec[i] = i;
|
||||||
|
}
|
||||||
|
return vec;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with GoogleTest
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <gmock/gmock.h>
|
||||||
|
#include "calculator.h"
|
||||||
|
|
||||||
|
class CalculatorTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
calc = std::make_unique<Calculator>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
calc.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Calculator> calc;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(CalculatorTest, Addition) {
|
||||||
|
EXPECT_EQ(calc->add(2, 3), 5);
|
||||||
|
EXPECT_EQ(calc->add(-1, 1), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CalculatorTest, Division) {
|
||||||
|
EXPECT_DOUBLE_EQ(calc->divide(10, 2), 5.0);
|
||||||
|
EXPECT_THROW(calc->divide(10, 0), std::invalid_argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameterized tests
|
||||||
|
class AdditionTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {};
|
||||||
|
|
||||||
|
TEST_P(AdditionTest, ValidAddition) {
|
||||||
|
auto [a, b, expected] = GetParam();
|
||||||
|
Calculator calc;
|
||||||
|
EXPECT_EQ(calc.add(a, b), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTANTIATE_TEST_SUITE_P(
|
||||||
|
AdditionSuite,
|
||||||
|
AdditionTest,
|
||||||
|
::testing::Values(
|
||||||
|
std::make_tuple(1, 2, 3),
|
||||||
|
std::make_tuple(-1, -2, -3),
|
||||||
|
std::make_tuple(0, 0, 0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock objects
|
||||||
|
class MockDatabase : public Database {
|
||||||
|
public:
|
||||||
|
MOCK_METHOD(void, connect, (const std::string&), (override));
|
||||||
|
MOCK_METHOD(std::string, query, (const std::string&), (override));
|
||||||
|
MOCK_METHOD(void, disconnect, (), (override));
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST(ServiceTest, UsesDatabase) {
|
||||||
|
MockDatabase mock_db;
|
||||||
|
EXPECT_CALL(mock_db, connect("localhost"))
|
||||||
|
.Times(1);
|
||||||
|
EXPECT_CALL(mock_db, query("SELECT *"))
|
||||||
|
.WillOnce(::testing::Return("result"));
|
||||||
|
|
||||||
|
Service service(mock_db);
|
||||||
|
service.process();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Profiling
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Benchmark with Google Benchmark
|
||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
static void BM_VectorPush(benchmark::State& state) {
|
||||||
|
for (auto _ : state) {
|
||||||
|
std::vector<int> vec;
|
||||||
|
for (int i = 0; i < state.range(0); ++i) {
|
||||||
|
vec.push_back(i);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_VectorPush)->Range(8, 8<<10);
|
||||||
|
|
||||||
|
static void BM_VectorReserve(benchmark::State& state) {
|
||||||
|
for (auto _ : state) {
|
||||||
|
std::vector<int> vec;
|
||||||
|
vec.reserve(state.range(0));
|
||||||
|
for (int i = 0; i < state.range(0); ++i) {
|
||||||
|
vec.push_back(i);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_VectorReserve)->Range(8, 8<<10);
|
||||||
|
|
||||||
|
BENCHMARK_MAIN();
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Profiling with perf (Linux)
|
||||||
|
perf record -g ./myapp
|
||||||
|
perf report
|
||||||
|
|
||||||
|
# Profiling with Instruments (macOS)
|
||||||
|
instruments -t "Time Profiler" ./myapp
|
||||||
|
|
||||||
|
# Valgrind callgrind
|
||||||
|
valgrind --tool=callgrind ./myapp
|
||||||
|
kcachegrind callgrind.out.*
|
||||||
|
|
||||||
|
# Memory profiling
|
||||||
|
valgrind --tool=massif ./myapp
|
||||||
|
ms_print massif.out.*
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conan Package Manager
|
||||||
|
|
||||||
|
```python
|
||||||
|
# conanfile.txt
|
||||||
|
[requires]
|
||||||
|
fmt/10.1.1
|
||||||
|
spdlog/1.12.0
|
||||||
|
catch2/3.4.0
|
||||||
|
|
||||||
|
[generators]
|
||||||
|
CMakeDeps
|
||||||
|
CMakeToolchain
|
||||||
|
|
||||||
|
[options]
|
||||||
|
fmt:header_only=True
|
||||||
|
```
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
# CMakeLists.txt with Conan
|
||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(MyProject)
|
||||||
|
|
||||||
|
find_package(fmt REQUIRED)
|
||||||
|
find_package(spdlog REQUIRED)
|
||||||
|
find_package(Catch2 REQUIRED)
|
||||||
|
|
||||||
|
add_executable(myapp src/main.cpp)
|
||||||
|
target_link_libraries(myapp
|
||||||
|
PRIVATE
|
||||||
|
fmt::fmt
|
||||||
|
spdlog::spdlog
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(tests test/main.cpp)
|
||||||
|
target_link_libraries(tests
|
||||||
|
PRIVATE
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
conan install . --output-folder=build --build=missing
|
||||||
|
cd build
|
||||||
|
cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
|
||||||
|
cmake --build .
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD with GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/ci.yml
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
compiler: [gcc, clang, msvc]
|
||||||
|
build_type: [Debug, Release]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install conan
|
||||||
|
conan install . --output-folder=build --build=missing
|
||||||
|
|
||||||
|
- name: Configure
|
||||||
|
run: |
|
||||||
|
cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cmake --build build --config ${{ matrix.build_type }}
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: ctest --test-dir build -C ${{ matrix.build_type }}
|
||||||
|
|
||||||
|
sanitizers:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
sanitizer: [asan, ubsan, tsan]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Build with sanitizer
|
||||||
|
run: |
|
||||||
|
cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.sanitizer }}
|
||||||
|
cmake --build build
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: ctest --test-dir build
|
||||||
|
|
||||||
|
static-analysis:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Run clang-tidy
|
||||||
|
run: |
|
||||||
|
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
|
||||||
|
clang-tidy src/*.cpp -p build/
|
||||||
|
|
||||||
|
- name: Run cppcheck
|
||||||
|
run: cppcheck --enable=all --error-exitcode=1 src/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Tool | Purpose | Command |
|
||||||
|
|------|---------|---------|
|
||||||
|
| CMake | Build system | `cmake -B build && cmake --build build` |
|
||||||
|
| Conan | Package manager | `conan install . --build=missing` |
|
||||||
|
| ASan | Memory errors | `-fsanitize=address` |
|
||||||
|
| UBSan | Undefined behavior | `-fsanitize=undefined` |
|
||||||
|
| TSan | Data races | `-fsanitize=thread` |
|
||||||
|
| clang-tidy | Static analysis | `clang-tidy src/*.cpp` |
|
||||||
|
| cppcheck | Static analysis | `cppcheck --enable=all src/` |
|
||||||
|
| Catch2 | Unit testing | `TEST_CASE("name") { REQUIRE(...); }` |
|
||||||
|
| GoogleTest | Unit testing | `TEST(Suite, Name) { EXPECT_EQ(...); }` |
|
||||||
|
| Google Benchmark | Performance | `BENCHMARK(func)->Range(...)` |
|
||||||
|
| Valgrind | Memory profiler | `valgrind --tool=memcheck ./app` |
|
||||||
440
.claude/skills/cpp-pro/references/concurrency.md
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
# Concurrency and Parallel Programming
|
||||||
|
|
||||||
|
> Reference for: C++ Pro
|
||||||
|
> Load when: Atomics, lock-free structures, thread pools, parallel algorithms, coroutines
|
||||||
|
|
||||||
|
## Atomics and Memory Ordering
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
// Basic atomics
|
||||||
|
std::atomic<int> counter{0};
|
||||||
|
std::atomic<bool> flag{false};
|
||||||
|
|
||||||
|
// Memory ordering
|
||||||
|
void producer(std::atomic<int>& data, std::atomic<bool>& ready) {
|
||||||
|
data.store(42, std::memory_order_relaxed);
|
||||||
|
ready.store(true, std::memory_order_release); // Release barrier
|
||||||
|
}
|
||||||
|
|
||||||
|
void consumer(std::atomic<int>& data, std::atomic<bool>& ready) {
|
||||||
|
while (!ready.load(std::memory_order_acquire)) { // Acquire barrier
|
||||||
|
std::this_thread::yield();
|
||||||
|
}
|
||||||
|
int value = data.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare-and-swap
|
||||||
|
bool try_acquire_lock(std::atomic<bool>& lock) {
|
||||||
|
bool expected = false;
|
||||||
|
return lock.compare_exchange_strong(expected, true,
|
||||||
|
std::memory_order_acquire,
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch-and-add
|
||||||
|
int increment_counter(std::atomic<int>& counter) {
|
||||||
|
return counter.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lock-Free Data Structures
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
// Lock-free stack
|
||||||
|
template<typename T>
|
||||||
|
class LockFreeStack {
|
||||||
|
struct Node {
|
||||||
|
T data;
|
||||||
|
Node* next;
|
||||||
|
Node(const T& value) : data(value), next(nullptr) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::atomic<Node*> head_{nullptr};
|
||||||
|
|
||||||
|
public:
|
||||||
|
void push(const T& value) {
|
||||||
|
Node* new_node = new Node(value);
|
||||||
|
new_node->next = head_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
while (!head_.compare_exchange_weak(new_node->next, new_node,
|
||||||
|
std::memory_order_release,
|
||||||
|
std::memory_order_relaxed)) {
|
||||||
|
// Retry with updated head
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pop(T& result) {
|
||||||
|
Node* old_head = head_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
while (old_head &&
|
||||||
|
!head_.compare_exchange_weak(old_head, old_head->next,
|
||||||
|
std::memory_order_acquire,
|
||||||
|
std::memory_order_relaxed)) {
|
||||||
|
// Retry
|
||||||
|
}
|
||||||
|
|
||||||
|
if (old_head) {
|
||||||
|
result = old_head->data;
|
||||||
|
delete old_head; // Note: ABA problem exists
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lock-free queue (single producer, single consumer)
|
||||||
|
template<typename T, size_t Size>
|
||||||
|
class SPSCQueue {
|
||||||
|
std::array<T, Size> buffer_;
|
||||||
|
alignas(64) std::atomic<size_t> head_{0};
|
||||||
|
alignas(64) std::atomic<size_t> tail_{0};
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool push(const T& item) {
|
||||||
|
size_t head = head_.load(std::memory_order_relaxed);
|
||||||
|
size_t next_head = (head + 1) % Size;
|
||||||
|
|
||||||
|
if (next_head == tail_.load(std::memory_order_acquire)) {
|
||||||
|
return false; // Queue full
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer_[head] = item;
|
||||||
|
head_.store(next_head, std::memory_order_release);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pop(T& item) {
|
||||||
|
size_t tail = tail_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
if (tail == head_.load(std::memory_order_acquire)) {
|
||||||
|
return false; // Queue empty
|
||||||
|
}
|
||||||
|
|
||||||
|
item = buffer_[tail];
|
||||||
|
tail_.store((tail + 1) % Size, std::memory_order_release);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Thread Pool
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <thread>
|
||||||
|
#include <queue>
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <functional>
|
||||||
|
#include <future>
|
||||||
|
|
||||||
|
class ThreadPool {
|
||||||
|
std::vector<std::thread> workers_;
|
||||||
|
std::queue<std::function<void()>> tasks_;
|
||||||
|
std::mutex queue_mutex_;
|
||||||
|
std::condition_variable condition_;
|
||||||
|
bool stop_ = false;
|
||||||
|
|
||||||
|
public:
|
||||||
|
ThreadPool(size_t num_threads) {
|
||||||
|
for (size_t i = 0; i < num_threads; ++i) {
|
||||||
|
workers_.emplace_back([this] {
|
||||||
|
while (true) {
|
||||||
|
std::function<void()> task;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(queue_mutex_);
|
||||||
|
condition_.wait(lock, [this] {
|
||||||
|
return stop_ || !tasks_.empty();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stop_ && tasks_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
task = std::move(tasks_.front());
|
||||||
|
tasks_.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
task();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~ThreadPool() {
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(queue_mutex_);
|
||||||
|
stop_ = true;
|
||||||
|
}
|
||||||
|
condition_.notify_all();
|
||||||
|
for (auto& worker : workers_) {
|
||||||
|
worker.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename F, typename... Args>
|
||||||
|
auto enqueue(F&& f, Args&&... args)
|
||||||
|
-> std::future<typename std::invoke_result_t<F, Args...>> {
|
||||||
|
|
||||||
|
using return_type = typename std::invoke_result_t<F, Args...>;
|
||||||
|
|
||||||
|
auto task = std::make_shared<std::packaged_task<return_type()>>(
|
||||||
|
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
|
||||||
|
);
|
||||||
|
|
||||||
|
std::future<return_type> result = task->get_future();
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(queue_mutex_);
|
||||||
|
if (stop_) {
|
||||||
|
throw std::runtime_error("enqueue on stopped ThreadPool");
|
||||||
|
}
|
||||||
|
tasks_.emplace([task]() { (*task)(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
condition_.notify_one();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parallel STL Algorithms
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <algorithm>
|
||||||
|
#include <execution>
|
||||||
|
#include <vector>
|
||||||
|
#include <numeric>
|
||||||
|
|
||||||
|
void parallel_algorithms_demo() {
|
||||||
|
std::vector<int> vec(1'000'000);
|
||||||
|
std::iota(vec.begin(), vec.end(), 0);
|
||||||
|
|
||||||
|
// Parallel sort
|
||||||
|
std::sort(std::execution::par, vec.begin(), vec.end());
|
||||||
|
|
||||||
|
// Parallel for_each
|
||||||
|
std::for_each(std::execution::par_unseq, vec.begin(), vec.end(),
|
||||||
|
[](int& x) { x *= 2; });
|
||||||
|
|
||||||
|
// Parallel transform
|
||||||
|
std::vector<int> result(vec.size());
|
||||||
|
std::transform(std::execution::par, vec.begin(), vec.end(),
|
||||||
|
result.begin(), [](int x) { return x * x; });
|
||||||
|
|
||||||
|
// Parallel reduce
|
||||||
|
int sum = std::reduce(std::execution::par, vec.begin(), vec.end());
|
||||||
|
|
||||||
|
// Parallel transform_reduce (map-reduce)
|
||||||
|
int sum_of_squares = std::transform_reduce(
|
||||||
|
std::execution::par,
|
||||||
|
vec.begin(), vec.end(),
|
||||||
|
0,
|
||||||
|
std::plus<>(),
|
||||||
|
[](int x) { return x * x; }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Synchronization Primitives
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <mutex>
|
||||||
|
#include <shared_mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
|
||||||
|
// Mutex types
|
||||||
|
std::mutex mtx;
|
||||||
|
std::recursive_mutex rec_mtx;
|
||||||
|
std::timed_mutex timed_mtx;
|
||||||
|
std::shared_mutex shared_mtx;
|
||||||
|
|
||||||
|
// RAII locks
|
||||||
|
void exclusive_access() {
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
// Critical section
|
||||||
|
}
|
||||||
|
|
||||||
|
void unique_lock_example() {
|
||||||
|
std::unique_lock<std::mutex> lock(mtx);
|
||||||
|
// Can unlock and relock
|
||||||
|
lock.unlock();
|
||||||
|
// Do some work
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reader-writer lock
|
||||||
|
class SharedData {
|
||||||
|
mutable std::shared_mutex mutex_;
|
||||||
|
std::string data_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::string read() const {
|
||||||
|
std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||||
|
return data_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write(std::string new_data) {
|
||||||
|
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||||
|
data_ = std::move(new_data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Condition variable
|
||||||
|
class Queue {
|
||||||
|
std::queue<int> queue_;
|
||||||
|
std::mutex mutex_;
|
||||||
|
std::condition_variable cv_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void push(int value) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
queue_.push(value);
|
||||||
|
}
|
||||||
|
cv_.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
int pop() {
|
||||||
|
std::unique_lock<std::mutex> lock(mutex_);
|
||||||
|
cv_.wait(lock, [this] { return !queue_.empty(); });
|
||||||
|
int value = queue_.front();
|
||||||
|
queue_.pop();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// std::scoped_lock - multiple mutexes
|
||||||
|
std::mutex mtx1, mtx2;
|
||||||
|
|
||||||
|
void transfer(Account& from, Account& to, int amount) {
|
||||||
|
std::scoped_lock lock(from.mutex, to.mutex); // Deadlock-free
|
||||||
|
from.balance -= amount;
|
||||||
|
to.balance += amount;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Async and Futures
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <future>
|
||||||
|
|
||||||
|
// std::async
|
||||||
|
auto future = std::async(std::launch::async, []() {
|
||||||
|
return expensive_computation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get result (blocks until ready)
|
||||||
|
auto result = future.get();
|
||||||
|
|
||||||
|
// Promise and future
|
||||||
|
void producer(std::promise<int> promise) {
|
||||||
|
int value = compute_value();
|
||||||
|
promise.set_value(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void consumer(std::future<int> future) {
|
||||||
|
int value = future.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::promise<int> promise;
|
||||||
|
std::future<int> future = promise.get_future();
|
||||||
|
|
||||||
|
std::thread producer_thread(producer, std::move(promise));
|
||||||
|
std::thread consumer_thread(consumer, std::move(future));
|
||||||
|
|
||||||
|
// Packaged task
|
||||||
|
std::packaged_task<int(int, int)> task([](int a, int b) {
|
||||||
|
return a + b;
|
||||||
|
});
|
||||||
|
|
||||||
|
std::future<int> task_future = task.get_future();
|
||||||
|
std::thread task_thread(std::move(task), 5, 3);
|
||||||
|
|
||||||
|
int sum = task_future.get(); // 8
|
||||||
|
task_thread.join();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coroutine-Based Concurrency
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <coroutine>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
// Async task coroutine
|
||||||
|
template<typename T>
|
||||||
|
struct AsyncTask {
|
||||||
|
struct promise_type {
|
||||||
|
std::optional<T> value;
|
||||||
|
std::exception_ptr exception;
|
||||||
|
|
||||||
|
AsyncTask get_return_object() {
|
||||||
|
return AsyncTask{
|
||||||
|
std::coroutine_handle<promise_type>::from_promise(*this)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::suspend_never initial_suspend() { return {}; }
|
||||||
|
std::suspend_always final_suspend() noexcept { return {}; }
|
||||||
|
|
||||||
|
void return_value(T v) {
|
||||||
|
value = std::move(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void unhandled_exception() {
|
||||||
|
exception = std::current_exception();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::coroutine_handle<promise_type> handle;
|
||||||
|
|
||||||
|
AsyncTask(std::coroutine_handle<promise_type> h) : handle(h) {}
|
||||||
|
~AsyncTask() { if (handle) handle.destroy(); }
|
||||||
|
|
||||||
|
T get() {
|
||||||
|
if (!handle.done()) {
|
||||||
|
handle.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle.promise().exception) {
|
||||||
|
std::rethrow_exception(handle.promise().exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
return *handle.promise().value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
AsyncTask<int> async_compute() {
|
||||||
|
co_return 42;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Primitive | Use Case | Performance |
|
||||||
|
|-----------|----------|-------------|
|
||||||
|
| std::atomic | Simple shared state | Lock-free |
|
||||||
|
| std::mutex | Exclusive access | Kernel call |
|
||||||
|
| std::shared_mutex | Read-heavy workload | Better than mutex |
|
||||||
|
| Lock-free structures | High contention | Best throughput |
|
||||||
|
| Thread pool | Task parallelism | Avoid thread overhead |
|
||||||
|
| Parallel STL | Data parallelism | Automatic scaling |
|
||||||
|
| std::async | Simple async tasks | Thread pool |
|
||||||
|
| Coroutines | Async I/O | Minimal overhead |
|
||||||
|
|
||||||
|
## Memory Ordering Guide
|
||||||
|
|
||||||
|
| Ordering | Guarantees | Use Case |
|
||||||
|
|----------|-----------|----------|
|
||||||
|
| relaxed | No synchronization | Counters |
|
||||||
|
| acquire | Load barrier | Consumer |
|
||||||
|
| release | Store barrier | Producer |
|
||||||
|
| acq_rel | Both | RMW operations |
|
||||||
|
| seq_cst | Total order | Default |
|
||||||
400
.claude/skills/cpp-pro/references/memory-performance.md
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
# Memory Management & Performance
|
||||||
|
|
||||||
|
> Reference for: C++ Pro
|
||||||
|
> Load when: Custom allocators, SIMD, cache optimization, move semantics, memory pools
|
||||||
|
|
||||||
|
## Smart Pointers
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
// unique_ptr - exclusive ownership
|
||||||
|
auto create_resource() {
|
||||||
|
return std::make_unique<Resource>("data");
|
||||||
|
}
|
||||||
|
|
||||||
|
// shared_ptr - reference counting
|
||||||
|
std::shared_ptr<Data> shared = std::make_shared<Data>(42);
|
||||||
|
std::weak_ptr<Data> weak = shared; // Non-owning reference
|
||||||
|
|
||||||
|
// Custom deleters
|
||||||
|
auto file_deleter = [](FILE* fp) { if (fp) fclose(fp); };
|
||||||
|
std::unique_ptr<FILE, decltype(file_deleter)> file(
|
||||||
|
fopen("data.txt", "r"),
|
||||||
|
file_deleter
|
||||||
|
);
|
||||||
|
|
||||||
|
// enable_shared_from_this
|
||||||
|
class Node : public std::enable_shared_from_this<Node> {
|
||||||
|
public:
|
||||||
|
std::shared_ptr<Node> get_shared() {
|
||||||
|
return shared_from_this();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Allocators
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Pool allocator for fixed-size objects
|
||||||
|
template<typename T, size_t PoolSize = 1024>
|
||||||
|
class PoolAllocator {
|
||||||
|
struct Block {
|
||||||
|
alignas(T) std::byte data[sizeof(T)];
|
||||||
|
Block* next;
|
||||||
|
};
|
||||||
|
|
||||||
|
Block pool_[PoolSize];
|
||||||
|
Block* free_list_ = nullptr;
|
||||||
|
|
||||||
|
public:
|
||||||
|
using value_type = T;
|
||||||
|
|
||||||
|
PoolAllocator() {
|
||||||
|
// Initialize free list
|
||||||
|
for (size_t i = 0; i < PoolSize - 1; ++i) {
|
||||||
|
pool_[i].next = &pool_[i + 1];
|
||||||
|
}
|
||||||
|
pool_[PoolSize - 1].next = nullptr;
|
||||||
|
free_list_ = &pool_[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
T* allocate(size_t n) {
|
||||||
|
if (n != 1 || !free_list_) {
|
||||||
|
throw std::bad_alloc();
|
||||||
|
}
|
||||||
|
Block* block = free_list_;
|
||||||
|
free_list_ = free_list_->next;
|
||||||
|
return reinterpret_cast<T*>(block->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void deallocate(T* p, size_t n) {
|
||||||
|
if (n != 1) return;
|
||||||
|
Block* block = reinterpret_cast<Block*>(p);
|
||||||
|
block->next = free_list_;
|
||||||
|
free_list_ = block;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
std::vector<int, PoolAllocator<int>> vec;
|
||||||
|
|
||||||
|
// Arena allocator - bump allocator
|
||||||
|
class Arena {
|
||||||
|
std::byte* buffer_;
|
||||||
|
size_t size_;
|
||||||
|
size_t offset_ = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
Arena(size_t size) : size_(size) {
|
||||||
|
buffer_ = new std::byte[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
~Arena() {
|
||||||
|
delete[] buffer_;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
T* allocate(size_t n = 1) {
|
||||||
|
size_t alignment = alignof(T);
|
||||||
|
size_t space = size_ - offset_;
|
||||||
|
void* ptr = buffer_ + offset_;
|
||||||
|
|
||||||
|
if (std::align(alignment, sizeof(T) * n, ptr, space)) {
|
||||||
|
offset_ = size_ - space + sizeof(T) * n;
|
||||||
|
return static_cast<T*>(ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::bad_alloc();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
offset_ = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Move Semantics
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <utility>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
class Buffer {
|
||||||
|
size_t size_;
|
||||||
|
char* data_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Constructor
|
||||||
|
Buffer(size_t size) : size_(size), data_(new char[size]) {}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
~Buffer() { delete[] data_; }
|
||||||
|
|
||||||
|
// Copy constructor
|
||||||
|
Buffer(const Buffer& other) : size_(other.size_), data_(new char[size_]) {
|
||||||
|
std::copy(other.data_, other.data_ + size_, data_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy assignment
|
||||||
|
Buffer& operator=(const Buffer& other) {
|
||||||
|
if (this != &other) {
|
||||||
|
delete[] data_;
|
||||||
|
size_ = other.size_;
|
||||||
|
data_ = new char[size_];
|
||||||
|
std::copy(other.data_, other.data_ + size_, data_);
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move constructor
|
||||||
|
Buffer(Buffer&& other) noexcept
|
||||||
|
: size_(other.size_), data_(other.data_) {
|
||||||
|
other.size_ = 0;
|
||||||
|
other.data_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move assignment
|
||||||
|
Buffer& operator=(Buffer&& other) noexcept {
|
||||||
|
if (this != &other) {
|
||||||
|
delete[] data_;
|
||||||
|
size_ = other.size_;
|
||||||
|
data_ = other.data_;
|
||||||
|
other.size_ = 0;
|
||||||
|
other.data_ = nullptr;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Perfect forwarding
|
||||||
|
template<typename T>
|
||||||
|
void wrapper(T&& arg) {
|
||||||
|
process(std::forward<T>(arg)); // Preserves lvalue/rvalue
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SIMD Optimization
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <immintrin.h> // AVX/AVX2
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
// Vectorized sum using AVX2
|
||||||
|
float simd_sum(const float* data, size_t size) {
|
||||||
|
__m256 sum_vec = _mm256_setzero_ps();
|
||||||
|
|
||||||
|
size_t i = 0;
|
||||||
|
// Process 8 floats at a time
|
||||||
|
for (; i + 8 <= size; i += 8) {
|
||||||
|
__m256 vec = _mm256_loadu_ps(&data[i]);
|
||||||
|
sum_vec = _mm256_add_ps(sum_vec, vec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal sum
|
||||||
|
alignas(32) float temp[8];
|
||||||
|
_mm256_store_ps(temp, sum_vec);
|
||||||
|
float result = 0.0f;
|
||||||
|
for (int j = 0; j < 8; ++j) {
|
||||||
|
result += temp[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle remaining elements
|
||||||
|
for (; i < size; ++i) {
|
||||||
|
result += data[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vectorized multiply-add
|
||||||
|
void fma_operation(float* result, const float* a, const float* b,
|
||||||
|
const float* c, size_t size) {
|
||||||
|
for (size_t i = 0; i + 8 <= size; i += 8) {
|
||||||
|
__m256 va = _mm256_loadu_ps(&a[i]);
|
||||||
|
__m256 vb = _mm256_loadu_ps(&b[i]);
|
||||||
|
__m256 vc = _mm256_loadu_ps(&c[i]);
|
||||||
|
|
||||||
|
// result[i] = a[i] * b[i] + c[i]
|
||||||
|
__m256 vr = _mm256_fmadd_ps(va, vb, vc);
|
||||||
|
_mm256_storeu_ps(&result[i], vr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cache-Friendly Design
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Structure of Arrays (SoA) - better cache locality
|
||||||
|
struct ParticlesAoS {
|
||||||
|
struct Particle {
|
||||||
|
float x, y, z;
|
||||||
|
float vx, vy, vz;
|
||||||
|
};
|
||||||
|
std::vector<Particle> particles;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParticlesSoA {
|
||||||
|
std::vector<float> x, y, z;
|
||||||
|
std::vector<float> vx, vy, vz;
|
||||||
|
|
||||||
|
void update_positions(float dt) {
|
||||||
|
// All x coordinates are contiguous - better cache usage
|
||||||
|
for (size_t i = 0; i < x.size(); ++i) {
|
||||||
|
x[i] += vx[i] * dt;
|
||||||
|
y[i] += vy[i] * dt;
|
||||||
|
z[i] += vz[i] * dt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache line padding to avoid false sharing
|
||||||
|
struct alignas(64) CacheLinePadded {
|
||||||
|
std::atomic<int> counter;
|
||||||
|
char padding[64 - sizeof(std::atomic<int>)];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prefetching
|
||||||
|
void process_with_prefetch(const int* data, size_t size) {
|
||||||
|
for (size_t i = 0; i < size; ++i) {
|
||||||
|
// Prefetch data for next iteration
|
||||||
|
if (i + 8 < size) {
|
||||||
|
__builtin_prefetch(&data[i + 8], 0, 1);
|
||||||
|
}
|
||||||
|
// Process current data
|
||||||
|
process(data[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Memory Pool
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
template<typename T, size_t ChunkSize = 256>
|
||||||
|
class MemoryPool {
|
||||||
|
struct Chunk {
|
||||||
|
alignas(T) std::byte data[sizeof(T) * ChunkSize];
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<Chunk>> chunks_;
|
||||||
|
std::vector<T*> free_list_;
|
||||||
|
size_t current_chunk_offset_ = ChunkSize;
|
||||||
|
|
||||||
|
public:
|
||||||
|
T* allocate() {
|
||||||
|
if (!free_list_.empty()) {
|
||||||
|
T* ptr = free_list_.back();
|
||||||
|
free_list_.pop_back();
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_chunk_offset_ >= ChunkSize) {
|
||||||
|
chunks_.push_back(std::make_unique<Chunk>());
|
||||||
|
current_chunk_offset_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Chunk* chunk = chunks_.back().get();
|
||||||
|
T* ptr = reinterpret_cast<T*>(
|
||||||
|
&chunk->data[sizeof(T) * current_chunk_offset_++]
|
||||||
|
);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void deallocate(T* ptr) {
|
||||||
|
free_list_.push_back(ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename... Args>
|
||||||
|
T* construct(Args&&... args) {
|
||||||
|
T* ptr = allocate();
|
||||||
|
new (ptr) T(std::forward<Args>(args)...);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void destroy(T* ptr) {
|
||||||
|
ptr->~T();
|
||||||
|
deallocate(ptr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copy Elision and RVO
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Return Value Optimization (RVO)
|
||||||
|
std::vector<int> create_vector() {
|
||||||
|
std::vector<int> vec{1, 2, 3, 4, 5};
|
||||||
|
return vec; // RVO applies, no copy/move
|
||||||
|
}
|
||||||
|
|
||||||
|
// Named Return Value Optimization (NRVO)
|
||||||
|
std::string build_string(bool condition) {
|
||||||
|
std::string result;
|
||||||
|
if (condition) {
|
||||||
|
result = "condition true";
|
||||||
|
} else {
|
||||||
|
result = "condition false";
|
||||||
|
}
|
||||||
|
return result; // NRVO may apply
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guaranteed copy elision (C++17)
|
||||||
|
struct NonMovable {
|
||||||
|
NonMovable() = default;
|
||||||
|
NonMovable(const NonMovable&) = delete;
|
||||||
|
NonMovable(NonMovable&&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
NonMovable create() {
|
||||||
|
return NonMovable{}; // Guaranteed no copy/move in C++17
|
||||||
|
}
|
||||||
|
|
||||||
|
auto obj = create(); // OK in C++17
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alignment and Memory Layout
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
// Control alignment
|
||||||
|
struct alignas(64) CacheAligned {
|
||||||
|
int data[16];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check alignment
|
||||||
|
static_assert(alignof(CacheAligned) == 64);
|
||||||
|
|
||||||
|
// Aligned allocation
|
||||||
|
void* aligned_alloc_wrapper(size_t alignment, size_t size) {
|
||||||
|
void* ptr = nullptr;
|
||||||
|
if (posix_memalign(&ptr, alignment, size) != 0) {
|
||||||
|
throw std::bad_alloc();
|
||||||
|
}
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placement new with alignment
|
||||||
|
alignas(32) std::byte buffer[sizeof(Data)];
|
||||||
|
Data* obj = new (buffer) Data();
|
||||||
|
obj->~Data(); // Manual destruction needed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Technique | Use Case | Benefit |
|
||||||
|
|-----------|----------|---------|
|
||||||
|
| Smart Pointers | Ownership management | Memory safety |
|
||||||
|
| Move Semantics | Avoid copies | Performance |
|
||||||
|
| Custom Allocators | Specialized allocation | Speed + control |
|
||||||
|
| SIMD | Parallel computation | 4-8x speedup |
|
||||||
|
| SoA Layout | Sequential access | Cache efficiency |
|
||||||
|
| Memory Pools | Frequent alloc/dealloc | Reduced fragmentation |
|
||||||
|
| Alignment | SIMD/cache optimization | Performance |
|
||||||
|
| RVO/NRVO | Return objects | Zero-copy |
|
||||||
307
.claude/skills/cpp-pro/references/modern-cpp.md
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
# Modern C++20/23 Features
|
||||||
|
|
||||||
|
> Reference for: C++ Pro
|
||||||
|
> Load when: Using C++20/23 features, concepts, ranges, coroutines, modules
|
||||||
|
|
||||||
|
## Concepts and Constraints
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <concepts>
|
||||||
|
|
||||||
|
// Define custom concepts
|
||||||
|
template<typename T>
|
||||||
|
concept Numeric = std::integral<T> || std::floating_point<T>;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
concept Hashable = requires(T a) {
|
||||||
|
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
concept Container = requires(T c) {
|
||||||
|
typename T::value_type;
|
||||||
|
typename T::iterator;
|
||||||
|
{ c.begin() } -> std::same_as<typename T::iterator>;
|
||||||
|
{ c.end() } -> std::same_as<typename T::iterator>;
|
||||||
|
{ c.size() } -> std::convertible_to<std::size_t>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use concepts for function constraints
|
||||||
|
template<Numeric T>
|
||||||
|
T add(T a, T b) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concept-based overloading
|
||||||
|
template<std::integral T>
|
||||||
|
void process(T value) {
|
||||||
|
std::cout << "Processing integer: " << value << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
template<std::floating_point T>
|
||||||
|
void process(T value) {
|
||||||
|
std::cout << "Processing float: " << value << '\n';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ranges and Views
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <ranges>
|
||||||
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
// Ranges-based algorithms
|
||||||
|
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
||||||
|
|
||||||
|
// Filter, transform, take - all lazy evaluation
|
||||||
|
auto result = numbers
|
||||||
|
| std::views::filter([](int n) { return n % 2 == 0; })
|
||||||
|
| std::views::transform([](int n) { return n * n; })
|
||||||
|
| std::views::take(3);
|
||||||
|
|
||||||
|
// Copy to vector only when needed
|
||||||
|
std::vector<int> materialized(result.begin(), result.end());
|
||||||
|
|
||||||
|
// Custom range adaptor
|
||||||
|
auto is_even = [](int n) { return n % 2 == 0; };
|
||||||
|
auto square = [](int n) { return n * n; };
|
||||||
|
|
||||||
|
auto pipeline = std::views::filter(is_even)
|
||||||
|
| std::views::transform(square);
|
||||||
|
|
||||||
|
auto processed = numbers | pipeline;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coroutines
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <coroutine>
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
// Generator coroutine
|
||||||
|
template<typename T>
|
||||||
|
struct Generator {
|
||||||
|
struct promise_type {
|
||||||
|
T current_value;
|
||||||
|
|
||||||
|
auto get_return_object() {
|
||||||
|
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::suspend_always initial_suspend() { return {}; }
|
||||||
|
std::suspend_always final_suspend() noexcept { return {}; }
|
||||||
|
|
||||||
|
std::suspend_always yield_value(T value) {
|
||||||
|
current_value = value;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void return_void() {}
|
||||||
|
void unhandled_exception() { std::terminate(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::coroutine_handle<promise_type> handle;
|
||||||
|
|
||||||
|
Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
|
||||||
|
~Generator() { if (handle) handle.destroy(); }
|
||||||
|
|
||||||
|
bool move_next() {
|
||||||
|
handle.resume();
|
||||||
|
return !handle.done();
|
||||||
|
}
|
||||||
|
|
||||||
|
T current_value() {
|
||||||
|
return handle.promise().current_value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
Generator<int> fibonacci() {
|
||||||
|
int a = 0, b = 1;
|
||||||
|
while (true) {
|
||||||
|
co_yield a;
|
||||||
|
auto next = a + b;
|
||||||
|
a = b;
|
||||||
|
b = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async coroutine
|
||||||
|
#include <future>
|
||||||
|
|
||||||
|
struct Task {
|
||||||
|
struct promise_type {
|
||||||
|
Task get_return_object() {
|
||||||
|
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
|
||||||
|
}
|
||||||
|
std::suspend_never initial_suspend() { return {}; }
|
||||||
|
std::suspend_never final_suspend() noexcept { return {}; }
|
||||||
|
void return_void() {}
|
||||||
|
void unhandled_exception() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::coroutine_handle<promise_type> handle;
|
||||||
|
};
|
||||||
|
|
||||||
|
Task async_operation() {
|
||||||
|
std::cout << "Starting async work\n";
|
||||||
|
co_await std::suspend_always{};
|
||||||
|
std::cout << "Resuming async work\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Three-Way Comparison (Spaceship)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <compare>
|
||||||
|
|
||||||
|
struct Point {
|
||||||
|
int x, y;
|
||||||
|
|
||||||
|
// Auto-generate all comparison operators
|
||||||
|
auto operator<=>(const Point&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom spaceship operator
|
||||||
|
struct Version {
|
||||||
|
int major, minor, patch;
|
||||||
|
|
||||||
|
std::strong_ordering operator<=>(const Version& other) const {
|
||||||
|
if (auto cmp = major <=> other.major; cmp != 0) return cmp;
|
||||||
|
if (auto cmp = minor <=> other.minor; cmp != 0) return cmp;
|
||||||
|
return patch <=> other.patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(const Version& other) const = default;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Designated Initializers
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct Config {
|
||||||
|
std::string host = "localhost";
|
||||||
|
int port = 8080;
|
||||||
|
bool ssl_enabled = false;
|
||||||
|
int timeout_ms = 5000;
|
||||||
|
};
|
||||||
|
|
||||||
|
// C++20 designated initializers
|
||||||
|
Config cfg {
|
||||||
|
.host = "example.com",
|
||||||
|
.port = 443,
|
||||||
|
.ssl_enabled = true
|
||||||
|
// timeout_ms uses default
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modules (C++20)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// math.cppm - module interface
|
||||||
|
export module math;
|
||||||
|
|
||||||
|
export namespace math {
|
||||||
|
template<typename T>
|
||||||
|
T add(T a, T b) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Calculator {
|
||||||
|
public:
|
||||||
|
int multiply(int a, int b);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementation
|
||||||
|
module math;
|
||||||
|
|
||||||
|
int math::Calculator::multiply(int a, int b) {
|
||||||
|
return a * b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in other files
|
||||||
|
import math;
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
auto result = math::add(5, 3);
|
||||||
|
math::Calculator calc;
|
||||||
|
auto product = calc.multiply(4, 7);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## constexpr Enhancements
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
// C++20: constexpr std::string and std::vector
|
||||||
|
constexpr auto compute_at_compile_time() {
|
||||||
|
std::vector<int> vec{1, 2, 3, 4, 5};
|
||||||
|
std::ranges::reverse(vec);
|
||||||
|
return vec[0]; // Returns 5
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int value = compute_at_compile_time();
|
||||||
|
|
||||||
|
// constexpr virtual functions (C++20)
|
||||||
|
struct Base {
|
||||||
|
constexpr virtual int get_value() const { return 42; }
|
||||||
|
constexpr virtual ~Base() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Derived : Base {
|
||||||
|
constexpr int get_value() const override { return 100; }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## std::format (C++20)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <format>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::string msg = std::format("Hello, {}!", "World");
|
||||||
|
|
||||||
|
// Positional arguments
|
||||||
|
auto text = std::format("{1} {0}", "World", "Hello");
|
||||||
|
|
||||||
|
// Formatting options
|
||||||
|
double pi = 3.14159265;
|
||||||
|
auto formatted = std::format("Pi: {:.2f}", pi); // "Pi: 3.14"
|
||||||
|
|
||||||
|
// Custom types
|
||||||
|
struct Point { int x, y; };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom formatter
|
||||||
|
template<>
|
||||||
|
struct std::formatter<Point> {
|
||||||
|
constexpr auto parse(format_parse_context& ctx) {
|
||||||
|
return ctx.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto format(const Point& p, format_context& ctx) const {
|
||||||
|
return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Feature | C++17 | C++20 | C++23 |
|
||||||
|
|---------|-------|-------|-------|
|
||||||
|
| Concepts | - | ✓ | ✓ |
|
||||||
|
| Ranges | - | ✓ | ✓ |
|
||||||
|
| Coroutines | - | ✓ | ✓ |
|
||||||
|
| Modules | - | ✓ | ✓ |
|
||||||
|
| Spaceship | - | ✓ | ✓ |
|
||||||
|
| std::format | - | ✓ | ✓ |
|
||||||
|
| std::expected | - | - | ✓ |
|
||||||
|
| std::print | - | - | ✓ |
|
||||||
|
| Deducing this | - | - | ✓ |
|
||||||
360
.claude/skills/cpp-pro/references/templates.md
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
# Template Metaprogramming
|
||||||
|
|
||||||
|
> Reference for: C++ Pro
|
||||||
|
> Load when: Variadic templates, SFINAE, type traits, CRTP, compile-time programming
|
||||||
|
|
||||||
|
## Variadic Templates
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <iostream>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
// Fold expressions (C++17)
|
||||||
|
template<typename... Args>
|
||||||
|
auto sum(Args... args) {
|
||||||
|
return (args + ...); // Unary right fold
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename... Args>
|
||||||
|
void print(Args&&... args) {
|
||||||
|
((std::cout << args << ' '), ...); // Binary left fold
|
||||||
|
std::cout << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive variadic template
|
||||||
|
template<typename T>
|
||||||
|
void log(T&& value) {
|
||||||
|
std::cout << value << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T, typename... Args>
|
||||||
|
void log(T&& first, Args&&... rest) {
|
||||||
|
std::cout << first << ", ";
|
||||||
|
log(std::forward<Args>(rest)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameter pack expansion
|
||||||
|
template<typename... Types>
|
||||||
|
struct TypeList {
|
||||||
|
static constexpr size_t size = sizeof...(Types);
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Args>
|
||||||
|
auto make_tuple_advanced(Args&&... args) {
|
||||||
|
return std::tuple<std::decay_t<Args>...>(std::forward<Args>(args)...);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SFINAE and if constexpr
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
// SFINAE with std::enable_if (older style)
|
||||||
|
template<typename T>
|
||||||
|
std::enable_if_t<std::is_integral_v<T>, T>
|
||||||
|
double_value(T value) {
|
||||||
|
return value * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
std::enable_if_t<std::is_floating_point_v<T>, T>
|
||||||
|
double_value(T value) {
|
||||||
|
return value * 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modern: if constexpr (C++17)
|
||||||
|
template<typename T>
|
||||||
|
auto process(T value) {
|
||||||
|
if constexpr (std::is_integral_v<T>) {
|
||||||
|
return value * 2;
|
||||||
|
} else if constexpr (std::is_floating_point_v<T>) {
|
||||||
|
return value * 2.0;
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detection idiom
|
||||||
|
template<typename T, typename = void>
|
||||||
|
struct has_serialize : std::false_type {};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
struct has_serialize<T, std::void_t<decltype(std::declval<T>().serialize())>>
|
||||||
|
: std::true_type {};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
constexpr bool has_serialize_v = has_serialize<T>::value;
|
||||||
|
|
||||||
|
// Use with if constexpr
|
||||||
|
template<typename T>
|
||||||
|
void save(const T& obj) {
|
||||||
|
if constexpr (has_serialize_v<T>) {
|
||||||
|
obj.serialize();
|
||||||
|
} else {
|
||||||
|
// Default serialization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Traits
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
// Custom type traits
|
||||||
|
template<typename T>
|
||||||
|
struct remove_all_pointers {
|
||||||
|
using type = T;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
struct remove_all_pointers<T*> {
|
||||||
|
using type = typename remove_all_pointers<T>::type;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
using remove_all_pointers_t = typename remove_all_pointers<T>::type;
|
||||||
|
|
||||||
|
// Conditional types
|
||||||
|
template<bool Condition, typename T, typename F>
|
||||||
|
struct conditional_type {
|
||||||
|
using type = T;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T, typename F>
|
||||||
|
struct conditional_type<false, T, F> {
|
||||||
|
using type = F;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compile-time type selection
|
||||||
|
template<size_t N>
|
||||||
|
struct best_integral_type {
|
||||||
|
using type = std::conditional_t<N <= 8, uint8_t,
|
||||||
|
std::conditional_t<N <= 16, uint16_t,
|
||||||
|
std::conditional_t<N <= 32, uint32_t, uint64_t>>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for member functions
|
||||||
|
template<typename T, typename = void>
|
||||||
|
struct has_reserve : std::false_type {};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
struct has_reserve<T, std::void_t<decltype(std::declval<T>().reserve(size_t{}))>>
|
||||||
|
: std::true_type {};
|
||||||
|
```
|
||||||
|
|
||||||
|
## CRTP (Curiously Recurring Template Pattern)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Static polymorphism with CRTP
|
||||||
|
template<typename Derived>
|
||||||
|
class Shape {
|
||||||
|
public:
|
||||||
|
double area() const {
|
||||||
|
return static_cast<const Derived*>(this)->area_impl();
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw() const {
|
||||||
|
static_cast<const Derived*>(this)->draw_impl();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Circle : public Shape<Circle> {
|
||||||
|
double radius_;
|
||||||
|
public:
|
||||||
|
Circle(double r) : radius_(r) {}
|
||||||
|
|
||||||
|
double area_impl() const {
|
||||||
|
return 3.14159 * radius_ * radius_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_impl() const {
|
||||||
|
std::cout << "Drawing circle\n";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Rectangle : public Shape<Rectangle> {
|
||||||
|
double width_, height_;
|
||||||
|
public:
|
||||||
|
Rectangle(double w, double h) : width_(w), height_(h) {}
|
||||||
|
|
||||||
|
double area_impl() const {
|
||||||
|
return width_ * height_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_impl() const {
|
||||||
|
std::cout << "Drawing rectangle\n";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// CRTP for mixin capabilities
|
||||||
|
template<typename Derived>
|
||||||
|
class Printable {
|
||||||
|
public:
|
||||||
|
void print() const {
|
||||||
|
std::cout << static_cast<const Derived*>(this)->to_string() << '\n';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class User : public Printable<User> {
|
||||||
|
std::string name_;
|
||||||
|
public:
|
||||||
|
User(std::string name) : name_(std::move(name)) {}
|
||||||
|
|
||||||
|
std::string to_string() const {
|
||||||
|
return "User: " + name_;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Template Parameters
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <vector>
|
||||||
|
#include <list>
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
|
// Template template parameter
|
||||||
|
template<typename T, template<typename, typename> class Container>
|
||||||
|
class Stack {
|
||||||
|
Container<T, std::allocator<T>> data_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void push(const T& value) {
|
||||||
|
data_.push_back(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
T pop() {
|
||||||
|
T value = data_.back();
|
||||||
|
data_.pop_back();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size() const {
|
||||||
|
return data_.size();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usage with different containers
|
||||||
|
Stack<int, std::vector> vector_stack;
|
||||||
|
Stack<int, std::deque> deque_stack;
|
||||||
|
Stack<int, std::list> list_stack;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compile-Time Computation
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
// Compile-time factorial
|
||||||
|
constexpr int factorial(int n) {
|
||||||
|
return n <= 1 ? 1 : n * factorial(n - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int fact_5 = factorial(5); // Computed at compile time
|
||||||
|
|
||||||
|
// Compile-time prime checking
|
||||||
|
constexpr bool is_prime(int n) {
|
||||||
|
if (n < 2) return false;
|
||||||
|
for (int i = 2; i * i <= n; ++i) {
|
||||||
|
if (n % i == 0) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate compile-time array of primes
|
||||||
|
template<size_t N>
|
||||||
|
constexpr auto generate_primes() {
|
||||||
|
std::array<int, N> primes{};
|
||||||
|
int count = 0;
|
||||||
|
int candidate = 2;
|
||||||
|
|
||||||
|
while (count < N) {
|
||||||
|
if (is_prime(candidate)) {
|
||||||
|
primes[count++] = candidate;
|
||||||
|
}
|
||||||
|
++candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return primes;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto first_10_primes = generate_primes<10>();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expression Templates
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Lazy evaluation with expression templates
|
||||||
|
template<typename E>
|
||||||
|
class VecExpression {
|
||||||
|
public:
|
||||||
|
double operator[](size_t i) const {
|
||||||
|
return static_cast<const E&>(*this)[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size() const {
|
||||||
|
return static_cast<const E&>(*this).size();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Vec : public VecExpression<Vec> {
|
||||||
|
std::vector<double> data_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
Vec(size_t n) : data_(n) {}
|
||||||
|
|
||||||
|
double operator[](size_t i) const { return data_[i]; }
|
||||||
|
double& operator[](size_t i) { return data_[i]; }
|
||||||
|
size_t size() const { return data_.size(); }
|
||||||
|
|
||||||
|
// Evaluate expression template
|
||||||
|
template<typename E>
|
||||||
|
Vec& operator=(const VecExpression<E>& expr) {
|
||||||
|
for (size_t i = 0; i < size(); ++i) {
|
||||||
|
data_[i] = expr[i];
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Binary operation expression
|
||||||
|
template<typename E1, typename E2>
|
||||||
|
class VecSum : public VecExpression<VecSum<E1, E2>> {
|
||||||
|
const E1& lhs_;
|
||||||
|
const E2& rhs_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
VecSum(const E1& lhs, const E2& rhs) : lhs_(lhs), rhs_(rhs) {}
|
||||||
|
|
||||||
|
double operator[](size_t i) const {
|
||||||
|
return lhs_[i] + rhs_[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size() const { return lhs_.size(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Operator overload
|
||||||
|
template<typename E1, typename E2>
|
||||||
|
VecSum<E1, E2> operator+(const VecExpression<E1>& lhs,
|
||||||
|
const VecExpression<E2>& rhs) {
|
||||||
|
return VecSum<E1, E2>(static_cast<const E1&>(lhs),
|
||||||
|
static_cast<const E2&>(rhs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage: a = b + c + d (no temporaries created!)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Technique | Use Case | Performance |
|
||||||
|
|-----------|----------|-------------|
|
||||||
|
| Variadic Templates | Variable arguments | Zero overhead |
|
||||||
|
| SFINAE | Conditional compilation | Compile-time |
|
||||||
|
| if constexpr | Type-based branching | Zero overhead |
|
||||||
|
| CRTP | Static polymorphism | No vtable cost |
|
||||||
|
| Expression Templates | Lazy evaluation | Eliminates temps |
|
||||||
|
| Type Traits | Type introspection | Compile-time |
|
||||||
|
| Fold Expressions | Parameter pack ops | Optimal |
|
||||||
|
| Template Specialization | Type-specific impl | Zero overhead |
|
||||||
12
.claude/skills/requirements/SKILL.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: requirements
|
||||||
|
description: Update the Dota Factory requirements document with new or changed requirements
|
||||||
|
argument-hint: <description of new requirement>
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Read `docs/requirements.md`, then help the user update the requirements with the following change:
|
||||||
|
|
||||||
|
$ARGUMENTS
|
||||||
|
|
||||||
|
Ask any clarifying questions if the request is ambiguous, or flag any conflicts with existing requirements before making changes. Do not make changes to the requirements file before the answers are clear.
|
||||||
4
.gitignore
vendored
@@ -1 +1,5 @@
|
|||||||
/build/
|
/build/
|
||||||
|
|
||||||
|
# local Claude Code config (machine-specific; .mcp.json holds credentials)
|
||||||
|
/.mcp.json
|
||||||
|
/.claude/settings.local.json
|
||||||
|
|||||||
@@ -4,17 +4,27 @@ message(STATUS "Using CMake ${CMAKE_VERSION}")
|
|||||||
|
|
||||||
include(cmake/add_files.cmake)
|
include(cmake/add_files.cmake)
|
||||||
include(cmake/create_source_groups.cmake)
|
include(cmake/create_source_groups.cmake)
|
||||||
|
include(cmake/version.cmake)
|
||||||
|
|
||||||
# Project ----------------------------------------------------------------------
|
# Project ----------------------------------------------------------------------
|
||||||
|
|
||||||
project(DotaFactory)
|
# Product identity — anything that depends on the product/project name is defined
|
||||||
|
# here so it lives in a single place and can change in the future. These values
|
||||||
|
# feed the build targets in src/CMakeLists.txt and the Windows version resource
|
||||||
|
# (see cmake/version.rc.in).
|
||||||
|
set(PRODUCT_NAME "DotaFactory") # internal name and executable base name
|
||||||
|
set(PRODUCT_DISPLAY_NAME "Dota Factory") # human-readable product / file description
|
||||||
|
set(PRODUCT_COMPANY "TODO: company") # placeholder
|
||||||
|
set(PRODUCT_COPYRIGHT "TODO: copyright") # placeholder
|
||||||
|
|
||||||
|
project(${PRODUCT_NAME})
|
||||||
|
|
||||||
set(CMAKE_BUILD_TYPE_INIT "Release")
|
set(CMAKE_BUILD_TYPE_INIT "Release")
|
||||||
|
|
||||||
|
|
||||||
# Qt ---------------------------------------------------------------------------
|
# Qt ---------------------------------------------------------------------------
|
||||||
|
|
||||||
find_package(Qt5 COMPONENTS Widgets Network Multimedia Charts REQUIRED)
|
find_package(Qt5 COMPONENTS Widgets Network Multimedia Charts Svg REQUIRED)
|
||||||
|
|
||||||
if(Qt5Widgets_FOUND)
|
if(Qt5Widgets_FOUND)
|
||||||
message(STATUS "Found Qt ${Qt5Widgets_VERSION_STRING}")
|
message(STATUS "Found Qt ${Qt5Widgets_VERSION_STRING}")
|
||||||
@@ -54,6 +64,7 @@ function(COPY_QT_BINARIES TARGET_DIR IS_DEBUG)
|
|||||||
configure_file("${QT_BINARY_DIR}/Qt5Network${SUFFIX}.dll" "${TARGET_DIR}/Qt5Network${SUFFIX}.dll" COPYONLY)
|
configure_file("${QT_BINARY_DIR}/Qt5Network${SUFFIX}.dll" "${TARGET_DIR}/Qt5Network${SUFFIX}.dll" COPYONLY)
|
||||||
configure_file("${QT_BINARY_DIR}/Qt5Widgets${SUFFIX}.dll" "${TARGET_DIR}/Qt5Widgets${SUFFIX}.dll" COPYONLY)
|
configure_file("${QT_BINARY_DIR}/Qt5Widgets${SUFFIX}.dll" "${TARGET_DIR}/Qt5Widgets${SUFFIX}.dll" COPYONLY)
|
||||||
configure_file("${QT_BINARY_DIR}/Qt5Multimedia${SUFFIX}.dll" "${TARGET_DIR}/Qt5Multimedia${SUFFIX}.dll" COPYONLY)
|
configure_file("${QT_BINARY_DIR}/Qt5Multimedia${SUFFIX}.dll" "${TARGET_DIR}/Qt5Multimedia${SUFFIX}.dll" COPYONLY)
|
||||||
|
configure_file("${QT_BINARY_DIR}/Qt5Svg${SUFFIX}.dll" "${TARGET_DIR}/Qt5Svg${SUFFIX}.dll" COPYONLY)
|
||||||
endfunction(COPY_QT_BINARIES)
|
endfunction(COPY_QT_BINARIES)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
98
bin/app/data/config/buildings.toml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
[[building]]
|
||||||
|
id = "belt"
|
||||||
|
tooltip = "Transports items one tile at a time in the direction it faces."
|
||||||
|
cost = 2
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 0.2
|
||||||
|
surface_mask = ["A>"]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "splitter"
|
||||||
|
tooltip = "Splits an incoming item stream between two outputs, with optional per-output filters."
|
||||||
|
cost = 3
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 0.5
|
||||||
|
surface_mask = ["<A>"]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "tunnel_entry"
|
||||||
|
tooltip = "Sends items underground so belts can cross. Places an entry, or an exit when it would connect to a matching entry under the cursor."
|
||||||
|
cost = 5
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 0.5
|
||||||
|
surface_mask = ["A>"]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "tunnel_exit"
|
||||||
|
tooltip = "Receives items from a matching tunnel entry and pushes them onward."
|
||||||
|
cost = 5
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 0.5
|
||||||
|
surface_mask = ["A>"]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "miner"
|
||||||
|
tooltip = "Extracts a selected ore from the asteroid; every tile yields any ore."
|
||||||
|
cost = 15
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 1
|
||||||
|
surface_mask = [
|
||||||
|
"A>",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "smelter"
|
||||||
|
tooltip = "Melts ore or scrap into basic materials. No recipe selection needed."
|
||||||
|
cost = 20
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 1
|
||||||
|
surface_mask = [
|
||||||
|
"AA",
|
||||||
|
" v",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "assembler"
|
||||||
|
tooltip = "Crafts a selected recipe from the production tree into intermediate or final parts."
|
||||||
|
cost = 35
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 1
|
||||||
|
surface_mask = [
|
||||||
|
"AA ",
|
||||||
|
"AA>",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "reprocessing_plant"
|
||||||
|
tooltip = "Consumes scrap and yields one random higher-tier product per cycle."
|
||||||
|
cost = 40
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 1
|
||||||
|
surface_mask = [
|
||||||
|
"AAA ",
|
||||||
|
"AAA>",
|
||||||
|
"AAA ",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "shipyard"
|
||||||
|
tooltip = "Builds autonomous combat ships from a selected schematic and module layout."
|
||||||
|
cost = 60
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 1
|
||||||
|
surface_mask = [
|
||||||
|
"AAAS>",
|
||||||
|
"AAAS ",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "salvage_bay"
|
||||||
|
tooltip = "Drop-off point where salvage ships unload collected scrap onto belts."
|
||||||
|
cost = 25
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 1
|
||||||
|
output_buffer_capacity = 20
|
||||||
|
surface_mask = [
|
||||||
|
"<AAS",
|
||||||
|
" AAS",
|
||||||
|
]
|
||||||
254
bin/app/data/config/modules.toml
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
# modules.toml
|
||||||
|
#
|
||||||
|
# Production tree v2: all weapons are railguns for now — the implementation
|
||||||
|
# (instant damage, no projectile, no ammunition) stays as-is and the beam
|
||||||
|
# visual reads as a tracer round. Lasers are reserved for a future distinct
|
||||||
|
# weapon type (see docs/content_design.md, "Production tree v2 — Weapons").
|
||||||
|
# Combat stats are placeholders until the arena balancing pass;
|
||||||
|
# production_time_seconds values come from the numbers pass.
|
||||||
|
#
|
||||||
|
# Unlock gating is defined in unlocks.toml, not here (REQ-LOCK-EXPLICIT): a
|
||||||
|
# module id granted by an unlock group starts locked and is awarded via a
|
||||||
|
# defence station drop; ids absent from unlocks.toml (railgun_s) start unlocked.
|
||||||
|
#
|
||||||
|
# Surface mask footprint ladder — footprints gate which hulls can mount a
|
||||||
|
# module, purely through geometry (see ships.toml for the matching hull
|
||||||
|
# grids):
|
||||||
|
#
|
||||||
|
# 1x1 railgun_s, salvager, repair_tool fits every hull, incl. drones
|
||||||
|
# 1x2 maneuvering_thrusters, sensor_booster,
|
||||||
|
# armor_plates frigate and up
|
||||||
|
# 1x3 afterburner frigate and up (eats most of a frigate)
|
||||||
|
# L-shape weapon_stabilizer, weapon_primer,
|
||||||
|
# weapon_upgrade frigate and up
|
||||||
|
# 2x2 railgun_m, drone_bay cruiser and up (no 2x2 area on s hulls)
|
||||||
|
# 3x3 railgun_l battleship and up (no 3x3 area on m hulls)
|
||||||
|
# 2x6 drone_hangar carrier only
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Weapons
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "railgun_s"
|
||||||
|
tooltip = "Small railgun. Fast-firing, short range, low damage; fits any hull."
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "railgun_s_module", amount = 1}]
|
||||||
|
production_time_seconds = 1
|
||||||
|
fill_color = "#FF8040"
|
||||||
|
glyph = "Rs"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
damage = 2
|
||||||
|
attack_range_m = 50
|
||||||
|
attack_rate_hz = 2.0
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "railgun_m"
|
||||||
|
tooltip = "Medium railgun. Higher damage at longer range; needs a 2x2 slot."
|
||||||
|
surface_mask = [
|
||||||
|
"OO",
|
||||||
|
"OO"]
|
||||||
|
materials = [{item = "railgun_m_module", amount = 1}]
|
||||||
|
production_time_seconds = 3
|
||||||
|
fill_color = "#FF8040"
|
||||||
|
glyph = "Rm"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
damage = 14
|
||||||
|
attack_range_m = 70
|
||||||
|
attack_rate_hz = 1.5
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "railgun_l"
|
||||||
|
tooltip = "Large railgun. Heavy damage at long range; needs a 3x3 slot."
|
||||||
|
surface_mask = [
|
||||||
|
"OOO",
|
||||||
|
"OOO",
|
||||||
|
"OOO"]
|
||||||
|
materials = [{item = "railgun_l_module", amount = 1}]
|
||||||
|
production_time_seconds = 4
|
||||||
|
fill_color = "#FF8040"
|
||||||
|
glyph = "Rl"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
damage = 52
|
||||||
|
attack_range_m = 100
|
||||||
|
attack_rate_hz = 0.8
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Utility tools
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "salvager"
|
||||||
|
tooltip = "Collects scrap from wrecks and stores it in the ship's cargo hold."
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "salvager_module", amount = 1}]
|
||||||
|
production_time_seconds = 1
|
||||||
|
fill_color = "#AACC44"
|
||||||
|
glyph = "Sv"
|
||||||
|
|
||||||
|
[module.salvage]
|
||||||
|
collection_range_m = 60
|
||||||
|
cargo_capacity = 20
|
||||||
|
collection_rate_hz = 0.5
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "repair_tool"
|
||||||
|
tooltip = "Repairs damaged friendly ships and defence stations within range."
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "repair_tool_module", amount = 1}]
|
||||||
|
production_time_seconds = 1
|
||||||
|
fill_color = "#66CCFF"
|
||||||
|
glyph = "Rp"
|
||||||
|
|
||||||
|
[module.repair]
|
||||||
|
repair_rate_hz = 1
|
||||||
|
repair_amount_hp = 4
|
||||||
|
repair_range_m = 80
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Propulsion
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "afterburner"
|
||||||
|
tooltip = "Greatly boosts top speed and forward acceleration."
|
||||||
|
surface_mask = ["OOO"]
|
||||||
|
materials = [{item = "afterburner_module", amount = 1}]
|
||||||
|
production_time_seconds = 1
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "Ab"
|
||||||
|
|
||||||
|
[module.movement]
|
||||||
|
multiplied_speed_mps = 1.6
|
||||||
|
added_main_acceleration_mpss = 60
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "maneuvering_thrusters"
|
||||||
|
tooltip = "Improves top speed and lateral/braking acceleration."
|
||||||
|
surface_mask = ["OO"]
|
||||||
|
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
|
||||||
|
production_time_seconds = 1
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "Mt"
|
||||||
|
|
||||||
|
[module.movement]
|
||||||
|
multiplied_speed_mps = 1.2
|
||||||
|
added_maneuvering_acceleration_mpss = 10
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Defense & sensors
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "armor_plates"
|
||||||
|
tooltip = "Adds a large flat bonus to the ship's hit points."
|
||||||
|
surface_mask = ["OO"]
|
||||||
|
materials = [{item = "armor_plates_module", amount = 1}]
|
||||||
|
production_time_seconds = 1
|
||||||
|
fill_color = "#808080"
|
||||||
|
glyph = "A"
|
||||||
|
|
||||||
|
[module.health]
|
||||||
|
added_hp = 1200
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "sensor_booster"
|
||||||
|
tooltip = "Extends the ship's sensor range."
|
||||||
|
surface_mask = ["OO"]
|
||||||
|
materials = [{item = "sensor_booster_module", amount = 1}]
|
||||||
|
production_time_seconds = 1
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "S"
|
||||||
|
|
||||||
|
[module.sensor]
|
||||||
|
added_sensor_range_m = 50
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Weapon modifiers
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_upgrade"
|
||||||
|
tooltip = "Increases the damage of all weapons on the ship."
|
||||||
|
surface_mask = [
|
||||||
|
"OO",
|
||||||
|
"OX",
|
||||||
|
]
|
||||||
|
materials = [{item = "weapon_upgrade_module", amount = 1}]
|
||||||
|
production_time_seconds = 2
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "Wu"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
multiplied_damage = 1.2
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_primer"
|
||||||
|
tooltip = "Increases the fire rate of all weapons on the ship."
|
||||||
|
surface_mask = [
|
||||||
|
"OO",
|
||||||
|
"OX",
|
||||||
|
]
|
||||||
|
materials = [{item = "weapon_primer_module", amount = 1}]
|
||||||
|
production_time_seconds = 2
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "Wp"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
multiplied_attack_rate_hz = 1.2
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_stabilizer"
|
||||||
|
tooltip = "Extends weapon range at the cost of some fire rate."
|
||||||
|
surface_mask = [
|
||||||
|
"OO",
|
||||||
|
"OX",
|
||||||
|
]
|
||||||
|
materials = [{item = "weapon_stabilizer_module", amount = 1}]
|
||||||
|
production_time_seconds = 1
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "Ws"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
multiplied_attack_range_m = 1.3
|
||||||
|
multiplied_attack_rate_hz = 0.8
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Drone modules
|
||||||
|
#
|
||||||
|
# Footprint-only placeholders: the drone launching capability is not
|
||||||
|
# implemented yet, so these modules define no capability section.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "drone_bay"
|
||||||
|
tooltip = "Drone launch bay (capability not yet implemented)."
|
||||||
|
surface_mask = [
|
||||||
|
"OO",
|
||||||
|
"OO"]
|
||||||
|
materials = [{item = "drone_bay_module", amount = 1}]
|
||||||
|
production_time_seconds = 3
|
||||||
|
fill_color = "#CC66FF"
|
||||||
|
glyph = "Db"
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "drone_hangar"
|
||||||
|
tooltip = "Large drone hangar (capability not yet implemented)."
|
||||||
|
surface_mask = [
|
||||||
|
"OOOOOO",
|
||||||
|
"OOOOOO"]
|
||||||
|
materials = [{item = "drone_hangar_module", amount = 1}]
|
||||||
|
production_time_seconds = 6
|
||||||
|
fill_color = "#9933CC"
|
||||||
|
glyph = "Dh"
|
||||||
444
bin/app/data/config/recipes.toml
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
# recipes.toml
|
||||||
|
#
|
||||||
|
# Production tree v2 (structure in docs/content_design.md, numbers with
|
||||||
|
# derivations in docs/balancing/derived.md). Quantities and durations are tuned so that every
|
||||||
|
# fitted ship lands on the threat-cost ladder and the ratio curve is
|
||||||
|
# realized: tier 1 ratios are 1:1, tier 2 ratios are 2:3, tier 3+ ratios
|
||||||
|
# are deliberately strange.
|
||||||
|
#
|
||||||
|
# A recipe id names the process, not the product — the display name shown in
|
||||||
|
# the UI is derived from it — so an item and the recipe that makes it never
|
||||||
|
# carry the same name.
|
||||||
|
#
|
||||||
|
# Input chain per game phase — each phase transition adds exactly one new
|
||||||
|
# base input:
|
||||||
|
#
|
||||||
|
# early iron_ore + copper_ore minable on every asteroid tile (the
|
||||||
|
# asteroid is an M-type body — its bulk
|
||||||
|
# rock IS ore)
|
||||||
|
# mid + quartz geode deposits in expansion territory
|
||||||
|
# (deposit gating pending — see action
|
||||||
|
# item 5 in docs/balancing/README.md;
|
||||||
|
# until then quartz mines anywhere)
|
||||||
|
# late + voidsteel battle-forged: ONLY from reprocessing
|
||||||
|
# salvaged scrap, so capital production
|
||||||
|
# requires combat
|
||||||
|
#
|
||||||
|
# Run tools/verify_recipes.py and tools/threat_report.py after editing.
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Mining (tier 0)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "iron_mining"
|
||||||
|
building = "miner"
|
||||||
|
inputs = []
|
||||||
|
outputs = [{item = "iron_ore", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "copper_mining"
|
||||||
|
building = "miner"
|
||||||
|
inputs = []
|
||||||
|
outputs = [{item = "copper_ore", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "quartz_mining"
|
||||||
|
building = "miner"
|
||||||
|
inputs = []
|
||||||
|
outputs = [{item = "quartz", amount = 1}]
|
||||||
|
duration_seconds = 2.0
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Smelting (tier 1) — one recipe per input item; ratios are 1:1 with miners.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "iron_smelting"
|
||||||
|
building = "smelter"
|
||||||
|
inputs = [{item = "iron_ore", amount = 1}]
|
||||||
|
outputs = [{item = "iron_ingot", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "copper_smelting"
|
||||||
|
building = "smelter"
|
||||||
|
inputs = [{item = "copper_ore", amount = 1}]
|
||||||
|
outputs = [{item = "copper_ingot", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "quartz_reduction"
|
||||||
|
building = "smelter"
|
||||||
|
inputs = [{item = "quartz", amount = 1}]
|
||||||
|
outputs = [{item = "silicon", amount = 1}]
|
||||||
|
duration_seconds = 2.0
|
||||||
|
|
||||||
|
# Scrap smelting: the safe, boring sink. Deliberately value-losing (4 threat
|
||||||
|
# of scrap becomes a 2-threat ingot) — reprocessing is the value-preserving
|
||||||
|
# path.
|
||||||
|
[[recipe]]
|
||||||
|
id = "scrap_smelting"
|
||||||
|
building = "smelter"
|
||||||
|
inputs = [{item = "scrap", amount = 1}]
|
||||||
|
outputs = [{item = "iron_ingot", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Reprocessing — the only source of voidsteel (battle-forged; formed when
|
||||||
|
# weapon plasma anneals hull metal in the violence of ship destruction).
|
||||||
|
# Weights are authored for the fully unlocked pool state; the pool
|
||||||
|
# renormalizes over implicitly unlocked items early game.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "scrap_reprocessing"
|
||||||
|
building = "reprocessing_plant"
|
||||||
|
inputs = [{item = "scrap", amount = 4}]
|
||||||
|
duration_seconds = 4.0
|
||||||
|
|
||||||
|
[[recipe.output_group]]
|
||||||
|
probability = 0.3
|
||||||
|
items = [{item = "iron_ingot", amount = 1}]
|
||||||
|
|
||||||
|
[[recipe.output_group]]
|
||||||
|
probability = 0.3
|
||||||
|
items = [{item = "copper_ingot", amount = 1}]
|
||||||
|
|
||||||
|
[[recipe.output_group]]
|
||||||
|
probability = 0.2
|
||||||
|
items = [{item = "silicon", amount = 1}]
|
||||||
|
|
||||||
|
[[recipe.output_group]]
|
||||||
|
probability = 0.2
|
||||||
|
items = [{item = "voidsteel", amount = 1}]
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Tier 2 — early intermediates (clean ratios, ~2:3)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "steel_rolling"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ingot", amount = 2}]
|
||||||
|
outputs = [{item = "steel_plate", amount = 1}]
|
||||||
|
duration_seconds = 3.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "wire_drawing"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "copper_ingot", amount = 1}]
|
||||||
|
outputs = [{item = "copper_wire", amount = 2}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "coil_winding"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "copper_wire", amount = 2}]
|
||||||
|
outputs = [{item = "copper_coil", amount = 1}]
|
||||||
|
duration_seconds = 1.5
|
||||||
|
|
||||||
|
# Depth-3 chain (ore -> ingot -> plate -> block) is the factory's
|
||||||
|
# doubling-time knob; see the block economy rules in docs/balancing/rules.md.
|
||||||
|
# unlocked_at_start: building blocks appear in no schematic's materials, so the
|
||||||
|
# implicit item graph can never reach this recipe (REQ-LOCK-IMPLICIT).
|
||||||
|
[[recipe]]
|
||||||
|
id = "block_pressing"
|
||||||
|
building = "assembler"
|
||||||
|
unlocked_at_start = true
|
||||||
|
inputs = [{item = "steel_plate", amount = 2}]
|
||||||
|
outputs = [{item = "building_block", amount = 4}]
|
||||||
|
duration_seconds = 2.0
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Tier 3 — mid intermediates (strange ratios begin; need quartz)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "chip_etching"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "silicon", amount = 1}, {item = "copper_wire", amount = 2}]
|
||||||
|
outputs = [{item = "control_chip", amount = 1}]
|
||||||
|
duration_seconds = 5.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "capacitor_assembly"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "copper_coil", amount = 2}, {item = "silicon", amount = 1}]
|
||||||
|
outputs = [{item = "capacitor_bank", amount = 1}]
|
||||||
|
duration_seconds = 5.0
|
||||||
|
|
||||||
|
# The quality gate for m+ hulls: a deliberately long-running recipe
|
||||||
|
# (time-heavy archetype).
|
||||||
|
[[recipe]]
|
||||||
|
id = "steel_hardening"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "steel_plate", amount = 3}]
|
||||||
|
outputs = [{item = "hardened_steel", amount = 1}]
|
||||||
|
duration_seconds = 12.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "ceramic_firing"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "quartz", amount = 2}]
|
||||||
|
outputs = [{item = "ceramic_plate", amount = 1}]
|
||||||
|
duration_seconds = 4.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "drive_assembly"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [
|
||||||
|
{item = "steel_plate", amount = 2},
|
||||||
|
{item = "copper_coil", amount = 2},
|
||||||
|
{item = "control_chip", amount = 1},
|
||||||
|
]
|
||||||
|
outputs = [{item = "drive_unit", amount = 1}]
|
||||||
|
duration_seconds = 8.0
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Tier 4 — late intermediates (need voidsteel)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "voidsteel_forging"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "voidsteel", amount = 1}, {item = "hardened_steel", amount = 1}]
|
||||||
|
outputs = [{item = "voidsteel_plate", amount = 1}]
|
||||||
|
duration_seconds = 8.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "core_assembly"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [
|
||||||
|
{item = "voidsteel", amount = 2},
|
||||||
|
{item = "capacitor_bank", amount = 1},
|
||||||
|
{item = "control_chip", amount = 1},
|
||||||
|
]
|
||||||
|
outputs = [{item = "capital_core", amount = 1}]
|
||||||
|
duration_seconds = 10.0
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Shortcut recipes — drop-only assembler recipes, gated by unlock groups in
|
||||||
|
# unlocks.toml (REQ-LOCK-EXPLICIT). Pure rewards: item threat stays defined by
|
||||||
|
# the base (expensive) path via the max rule, so shortcuts give real factory
|
||||||
|
# efficiency without shifting any balance. Named "direct_*" after the base
|
||||||
|
# process they skip a step of, so the reward reads as what it is.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "direct_rolling"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ore", amount = 3}]
|
||||||
|
outputs = [{item = "steel_plate", amount = 1}]
|
||||||
|
duration_seconds = 2.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "direct_etching"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "quartz", amount = 2}]
|
||||||
|
outputs = [{item = "control_chip", amount = 1}]
|
||||||
|
duration_seconds = 4.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "direct_hardening"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ingot", amount = 4}]
|
||||||
|
outputs = [{item = "hardened_steel", amount = 1}]
|
||||||
|
duration_seconds = 8.0
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Ship hulls
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "drone_framing"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ingot", amount = 1}]
|
||||||
|
outputs = [{item = "drone_hull", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "frigate_framing"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "steel_plate", amount = 2}, {item = "copper_wire", amount = 1}]
|
||||||
|
outputs = [{item = "frigate_hull", amount = 1}]
|
||||||
|
duration_seconds = 2.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "destroyer_framing"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "steel_plate", amount = 3}, {item = "copper_coil", amount = 2}]
|
||||||
|
outputs = [{item = "destroyer_hull", amount = 1}]
|
||||||
|
duration_seconds = 4.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "cruiser_framing"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "hardened_steel", amount = 2}, {item = "control_chip", amount = 2}]
|
||||||
|
outputs = [{item = "cruiser_hull", amount = 1}]
|
||||||
|
duration_seconds = 6.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "battlecruiser_framing"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [
|
||||||
|
{item = "hardened_steel", amount = 3},
|
||||||
|
{item = "control_chip", amount = 2},
|
||||||
|
{item = "drive_unit", amount = 1},
|
||||||
|
]
|
||||||
|
outputs = [{item = "battlecruiser_hull", amount = 1}]
|
||||||
|
duration_seconds = 8.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "battleship_framing"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [
|
||||||
|
{item = "voidsteel_plate", amount = 3},
|
||||||
|
{item = "drive_unit", amount = 1},
|
||||||
|
{item = "control_chip", amount = 2},
|
||||||
|
]
|
||||||
|
outputs = [{item = "battleship_hull", amount = 1}]
|
||||||
|
duration_seconds = 10.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "dreadnought_framing"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [
|
||||||
|
{item = "voidsteel_plate", amount = 5},
|
||||||
|
{item = "capital_core", amount = 1},
|
||||||
|
{item = "drive_unit", amount = 2},
|
||||||
|
]
|
||||||
|
outputs = [{item = "dreadnought_hull", amount = 1}]
|
||||||
|
duration_seconds = 12.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "carrier_framing"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [
|
||||||
|
{item = "voidsteel_plate", amount = 5},
|
||||||
|
{item = "capital_core", amount = 1},
|
||||||
|
{item = "drive_unit", amount = 2},
|
||||||
|
]
|
||||||
|
outputs = [{item = "carrier_hull", amount = 1}]
|
||||||
|
duration_seconds = 12.0
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Module prefabs
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "railgun_s_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "copper_coil", amount = 1}]
|
||||||
|
outputs = [{item = "railgun_s_module", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "salvager_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "steel_plate", amount = 1}, {item = "copper_wire", amount = 2}]
|
||||||
|
outputs = [{item = "salvager_module", amount = 1}]
|
||||||
|
duration_seconds = 2.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "repair_tool_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "steel_plate", amount = 1}, {item = "copper_wire", amount = 2}]
|
||||||
|
outputs = [{item = "repair_tool_module", amount = 1}]
|
||||||
|
duration_seconds = 2.0
|
||||||
|
|
||||||
|
# Material-heavy, fast: the armor archetype.
|
||||||
|
[[recipe]]
|
||||||
|
id = "armor_plates_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "steel_plate", amount = 4}]
|
||||||
|
outputs = [{item = "armor_plates_module", amount = 1}]
|
||||||
|
duration_seconds = 3.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "maneuvering_thrusters_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "steel_plate", amount = 1}, {item = "copper_coil", amount = 1}]
|
||||||
|
outputs = [{item = "maneuvering_thrusters_module", amount = 1}]
|
||||||
|
duration_seconds = 2.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "sensor_booster_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "copper_wire", amount = 2}, {item = "copper_coil", amount = 1}]
|
||||||
|
outputs = [{item = "sensor_booster_module", amount = 1}]
|
||||||
|
duration_seconds = 2.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "afterburner_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "copper_coil", amount = 2}, {item = "steel_plate", amount = 1}]
|
||||||
|
outputs = [{item = "afterburner_module", amount = 1}]
|
||||||
|
duration_seconds = 3.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "weapon_stabilizer_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "steel_plate", amount = 1}, {item = "copper_coil", amount = 1}]
|
||||||
|
outputs = [{item = "weapon_stabilizer_module", amount = 1}]
|
||||||
|
duration_seconds = 2.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "weapon_primer_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "capacitor_bank", amount = 1}, {item = "copper_coil", amount = 1}]
|
||||||
|
outputs = [{item = "weapon_primer_module", amount = 1}]
|
||||||
|
duration_seconds = 4.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "weapon_upgrade_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "control_chip", amount = 1}, {item = "copper_coil", amount = 1}]
|
||||||
|
outputs = [{item = "weapon_upgrade_module", amount = 1}]
|
||||||
|
duration_seconds = 4.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "railgun_m_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [
|
||||||
|
{item = "capacitor_bank", amount = 1},
|
||||||
|
{item = "steel_plate", amount = 2},
|
||||||
|
{item = "copper_coil", amount = 1},
|
||||||
|
]
|
||||||
|
outputs = [{item = "railgun_m_module", amount = 1}]
|
||||||
|
duration_seconds = 4.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "drone_bay_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [
|
||||||
|
{item = "control_chip", amount = 1},
|
||||||
|
{item = "steel_plate", amount = 2},
|
||||||
|
{item = "copper_coil", amount = 1},
|
||||||
|
]
|
||||||
|
outputs = [{item = "drone_bay_module", amount = 1}]
|
||||||
|
duration_seconds = 4.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "railgun_l_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [
|
||||||
|
{item = "capacitor_bank", amount = 1},
|
||||||
|
{item = "hardened_steel", amount = 2},
|
||||||
|
{item = "ceramic_plate", amount = 1},
|
||||||
|
]
|
||||||
|
outputs = [{item = "railgun_l_module", amount = 1}]
|
||||||
|
duration_seconds = 6.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "drone_hangar_kit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [
|
||||||
|
{item = "voidsteel_plate", amount = 1},
|
||||||
|
{item = "control_chip", amount = 2},
|
||||||
|
{item = "drive_unit", amount = 1},
|
||||||
|
]
|
||||||
|
outputs = [{item = "drone_hangar_module", amount = 1}]
|
||||||
|
duration_seconds = 10.0
|
||||||
311
bin/app/data/config/ships.toml
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
# ships.toml
|
||||||
|
#
|
||||||
|
# First real-content iteration: ship ids and layout grids are the designed
|
||||||
|
# content; stats, materials, and production times are placeholders until the
|
||||||
|
# recipe and balancing passes.
|
||||||
|
#
|
||||||
|
# Unlock gating is defined in unlocks.toml, not here (REQ-LOCK-EXPLICIT): a ship
|
||||||
|
# id granted by an unlock group starts locked and is awarded via a defence
|
||||||
|
# station drop; ids absent from unlocks.toml (drone, frigate) start unlocked.
|
||||||
|
#
|
||||||
|
# Size classes:
|
||||||
|
# xs drone 1 cell — exactly one 1x1 module
|
||||||
|
# s frigate, destroyer no 2x2 area anywhere: only 1x1/1x2/1x3/L modules fit
|
||||||
|
# m cruiser, battlecruiser 2x2 areas (m guns, drone bays) but no 3x3 area
|
||||||
|
# l battleship four m guns, or exactly one 3x3 l gun at heavy
|
||||||
|
# opportunity cost
|
||||||
|
# xl dreadnought, carrier dreadnought fits three l guns but no drone
|
||||||
|
# hangar; carrier fits one drone hangar (2x6)
|
||||||
|
# but no l gun (its deck rows are broken up
|
||||||
|
# by elevator shafts)
|
||||||
|
|
||||||
|
[[ship]]
|
||||||
|
id = "drone"
|
||||||
|
layout = ["O"]
|
||||||
|
default_modules = [{type = "railgun_s", x = 0, y = 0, rotation = "east"}]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "drone_hull", amount = 1}]
|
||||||
|
production_time_seconds = 1
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp = 60
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_mps = 45
|
||||||
|
main_acceleration_mpss = 60
|
||||||
|
maneuvering_acceleration_mpss = 30
|
||||||
|
angular_acceleration_radpss = 12
|
||||||
|
max_rotation_speed_radps = 6
|
||||||
|
|
||||||
|
[ship.sensor]
|
||||||
|
sensor_range_m = 150
|
||||||
|
|
||||||
|
|
||||||
|
# Frigate — 5 cells in a plus shape. Holds a couple of small guns plus at
|
||||||
|
# most one 1x2 support (every 1x2 placement crosses the center cell), or one
|
||||||
|
# L-shaped weapon modifier, or an afterburner spanning the full center line.
|
||||||
|
[[ship]]
|
||||||
|
id = "frigate"
|
||||||
|
layout = [
|
||||||
|
"XOX",
|
||||||
|
"OOO",
|
||||||
|
"XOX",
|
||||||
|
]
|
||||||
|
default_modules = [
|
||||||
|
{type = "railgun_s", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "maneuvering_thrusters", x = 0, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "frigate_hull", amount = 1}]
|
||||||
|
production_time_seconds = 2
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp = 300
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_mps = 35
|
||||||
|
main_acceleration_mpss = 45
|
||||||
|
maneuvering_acceleration_mpss = 22
|
||||||
|
angular_acceleration_radpss = 8
|
||||||
|
max_rotation_speed_radps = 4
|
||||||
|
|
||||||
|
[ship.sensor]
|
||||||
|
sensor_range_m = 200
|
||||||
|
|
||||||
|
|
||||||
|
# Destroyer — 8 cells: a long gun deck with three turret bumps on top.
|
||||||
|
# Still no 2x2 area, so it packs more small guns than a frigate but can never
|
||||||
|
# mount medium hardware.
|
||||||
|
[[ship]]
|
||||||
|
id = "destroyer"
|
||||||
|
layout = [
|
||||||
|
"OXOXO",
|
||||||
|
"OOOOO",
|
||||||
|
]
|
||||||
|
default_modules = [
|
||||||
|
{type = "railgun_s", x = 0, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 2, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 4, y = 0, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "sensor_booster", x = 3, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "destroyer_hull", amount = 1}]
|
||||||
|
production_time_seconds = 3
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp = 550
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_mps = 30
|
||||||
|
main_acceleration_mpss = 35
|
||||||
|
maneuvering_acceleration_mpss = 18
|
||||||
|
angular_acceleration_radpss = 6
|
||||||
|
max_rotation_speed_radps = 3
|
||||||
|
|
||||||
|
[ship.sensor]
|
||||||
|
sensor_range_m = 220
|
||||||
|
|
||||||
|
|
||||||
|
# Cruiser — 12 cells with notched corners. Fits at most two 2x2 m guns
|
||||||
|
# (stacked through the middle), leaving the four side cells for small
|
||||||
|
# supports; no 3x3 area exists for an l gun.
|
||||||
|
[[ship]]
|
||||||
|
id = "cruiser"
|
||||||
|
layout = [
|
||||||
|
"XOOX",
|
||||||
|
"OOOO",
|
||||||
|
"OOOO",
|
||||||
|
"XOOX",
|
||||||
|
]
|
||||||
|
default_modules = [
|
||||||
|
{type = "railgun_m", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "maneuvering_thrusters", x = 1, y = 3, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "cruiser_hull", amount = 1}]
|
||||||
|
production_time_seconds = 4
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp = 1500
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_mps = 24
|
||||||
|
main_acceleration_mpss = 25
|
||||||
|
maneuvering_acceleration_mpss = 12
|
||||||
|
angular_acceleration_radpss = 4
|
||||||
|
max_rotation_speed_radps = 2
|
||||||
|
|
||||||
|
[ship.sensor]
|
||||||
|
sensor_range_m = 250
|
||||||
|
|
||||||
|
|
||||||
|
# Battlecruiser — 16 cells: a wide bow split into two gun cheeks, tapering
|
||||||
|
# toward the stern. Fits three 2x2 m guns (two in the cheeks, one through
|
||||||
|
# the middle) with small support slots left over; the split bow and tapered
|
||||||
|
# stern leave no 3x3 area for an l gun and no 2x6 area for a drone hangar.
|
||||||
|
[[ship]]
|
||||||
|
id = "battlecruiser"
|
||||||
|
layout = [
|
||||||
|
"OOXXOO",
|
||||||
|
"OOOOOO",
|
||||||
|
"XOOOOX",
|
||||||
|
"XXOOXX",
|
||||||
|
]
|
||||||
|
default_modules = [
|
||||||
|
{type = "railgun_m", x = 0, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 4, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 2, y = 3, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 1, y = 2, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 4, y = 2, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "battlecruiser_hull", amount = 1}]
|
||||||
|
production_time_seconds = 5
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp = 2400
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_mps = 20
|
||||||
|
main_acceleration_mpss = 20
|
||||||
|
maneuvering_acceleration_mpss = 10
|
||||||
|
angular_acceleration_radpss = 3
|
||||||
|
max_rotation_speed_radps = 1.5
|
||||||
|
|
||||||
|
[ship.sensor]
|
||||||
|
sensor_range_m = 260
|
||||||
|
|
||||||
|
|
||||||
|
# Battleship — 24 cells: a broadside hull with notched flanks on every other
|
||||||
|
# row. Fits four 2x2 m guns (two per gun deck) with the bow, stern, and flank
|
||||||
|
# cells left for supports. All 3x3 placements crowd the center columns, so at
|
||||||
|
# most ONE l gun fits — and mounting it blocks every m gun mount, leaving
|
||||||
|
# only narrow support strips. The notched rows are never adjacent-and-full,
|
||||||
|
# so no 2x6 drone hangar fits.
|
||||||
|
[[ship]]
|
||||||
|
id = "battleship"
|
||||||
|
layout = [
|
||||||
|
"XOOOOX",
|
||||||
|
"OOOOOO",
|
||||||
|
"XOOOOX",
|
||||||
|
"OOOOOO",
|
||||||
|
"XOOOOX",
|
||||||
|
]
|
||||||
|
default_modules = [
|
||||||
|
{type = "railgun_l", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 1, y = 3, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 3, y = 3, rotation = "east"},
|
||||||
|
{type = "weapon_stabilizer", x = 4, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 4, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 0, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "battleship_hull", amount = 1}]
|
||||||
|
production_time_seconds = 6
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp = 6300
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_mps = 15
|
||||||
|
main_acceleration_mpss = 14
|
||||||
|
maneuvering_acceleration_mpss = 7
|
||||||
|
angular_acceleration_radpss = 2
|
||||||
|
max_rotation_speed_radps = 1
|
||||||
|
|
||||||
|
[ship.sensor]
|
||||||
|
sensor_range_m = 280
|
||||||
|
|
||||||
|
|
||||||
|
# Dreadnought — 36 cells: the main battery deck is split into three 3x3 gun
|
||||||
|
# slots by structural spacer columns, so exactly three l guns fit side by
|
||||||
|
# side (or m guns / supports in unused slots). The spacers cap every
|
||||||
|
# horizontal run at 5 cells, so the 2x6 drone hangar can never fit — carriers
|
||||||
|
# stay the only hangar hull. Bow and stern strips hold supports.
|
||||||
|
[[ship]]
|
||||||
|
id = "dreadnought"
|
||||||
|
layout = [
|
||||||
|
"XXXOOOOOXXX",
|
||||||
|
"OOOXOOOXOOO",
|
||||||
|
"OOOXOOOXOOO",
|
||||||
|
"OOOXOOOXOOO",
|
||||||
|
"XXOOXXXOOXX",
|
||||||
|
]
|
||||||
|
default_modules = [
|
||||||
|
{type = "railgun_l", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_l", x = 4, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_l", x = 8, y = 1, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 3, y = 0, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 5, y = 0, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 2, y = 4, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 7, y = 4, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 7, y = 0, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "dreadnought_hull", amount = 1}]
|
||||||
|
production_time_seconds = 8
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp = 24000
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_mps = 10
|
||||||
|
main_acceleration_mpss = 8
|
||||||
|
maneuvering_acceleration_mpss = 4
|
||||||
|
angular_acceleration_radpss = 1
|
||||||
|
max_rotation_speed_radps = 0.5
|
||||||
|
|
||||||
|
[ship.sensor]
|
||||||
|
sensor_range_m = 300
|
||||||
|
|
||||||
|
|
||||||
|
# Carrier — 37 cells: the top flight deck (rows 0-1) is the only place wide
|
||||||
|
# enough for the 2x6 drone hangar, and exactly one fits. The middle deck row
|
||||||
|
# is broken up by elevator shafts (the X cells) so no 3x3 l gun can ever fit;
|
||||||
|
# the lower decks hold supports and 2x2 point-defense m guns.
|
||||||
|
[[ship]]
|
||||||
|
id = "carrier"
|
||||||
|
layout = [
|
||||||
|
"XOOOOOOOOX",
|
||||||
|
"OOOOOOOOOO",
|
||||||
|
"OOXOOXOOXO",
|
||||||
|
"XOOOOOOOOX",
|
||||||
|
"XXXOOOOXXX",
|
||||||
|
]
|
||||||
|
default_modules = [
|
||||||
|
{type = "drone_hangar", x = 2, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 3, y = 2, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 6, y = 2, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 8, y = 1, rotation = "east"},
|
||||||
|
{type = "sensor_booster", x = 3, y = 4, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "carrier_hull", amount = 1}]
|
||||||
|
production_time_seconds = 8
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp = 24000
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_mps = 10
|
||||||
|
main_acceleration_mpss = 8
|
||||||
|
maneuvering_acceleration_mpss = 4
|
||||||
|
angular_acceleration_radpss = 1
|
||||||
|
max_rotation_speed_radps = 0.5
|
||||||
|
|
||||||
|
[ship.sensor]
|
||||||
|
sensor_range_m = 350
|
||||||
38
bin/app/data/config/stations.toml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# stations.toml
|
||||||
|
#
|
||||||
|
# Combat-pass anchors (see docs/balancing/targets.md, "Combat anchors"):
|
||||||
|
# a fresh player defence station holds one early parity wave unaided; the
|
||||||
|
# enemy station at level 0 matches the player station exactly and scales
|
||||||
|
# with the push level x. Station scrap drops stay authored (pushing rewards
|
||||||
|
# are tuned independently of ship production costs, REQ-RES-DEBRIS-DROP).
|
||||||
|
|
||||||
|
[hq]
|
||||||
|
surface_mask = [
|
||||||
|
"AAA",
|
||||||
|
"AAA",
|
||||||
|
"AAA",
|
||||||
|
]
|
||||||
|
hp_formula = "5000"
|
||||||
|
|
||||||
|
[player_station]
|
||||||
|
surface_mask = [
|
||||||
|
"SS",
|
||||||
|
"SS",
|
||||||
|
]
|
||||||
|
level = 1
|
||||||
|
hp_formula = "3000"
|
||||||
|
damage_formula = "25"
|
||||||
|
range_m_formula = "120"
|
||||||
|
fire_rate_hz_formula = "1"
|
||||||
|
scrap_drop_formula = "40"
|
||||||
|
|
||||||
|
[enemy_station]
|
||||||
|
surface_mask = [
|
||||||
|
"SS",
|
||||||
|
"SS",
|
||||||
|
]
|
||||||
|
hp_formula = "3000 + 1500*x"
|
||||||
|
damage_formula = "25 + 12*x"
|
||||||
|
range_m_formula = "120"
|
||||||
|
fire_rate_hz_formula = "1.0 + 0.1*x"
|
||||||
|
scrap_drop_formula = "40 + 30*x"
|
||||||
140
bin/app/data/config/unlocks.toml
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Unlock groups (REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP).
|
||||||
|
#
|
||||||
|
# Each [[unlock]] is a group of ships/modules/buildings/recipes awarded together
|
||||||
|
# from a single defence station drop. Anything NOT granted by any group is
|
||||||
|
# available from game start. `station_level` gates when a group becomes eligible;
|
||||||
|
# `requires` lists prerequisite unlock-group ids (REQ-LOCK-PREREQ).
|
||||||
|
#
|
||||||
|
# Most entries below are single-item groups that reproduce the previous per-item
|
||||||
|
# progression. The salvage_operations and reprocessing groups are the grouped
|
||||||
|
# unlocks: they lock the salvager module + salvage bay, and the reprocessing
|
||||||
|
# plant, from game start.
|
||||||
|
|
||||||
|
# --- Grouped unlocks -------------------------------------------------------
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "salvage_operations"
|
||||||
|
station_level = 1
|
||||||
|
modules = ["salvager"]
|
||||||
|
buildings = ["salvage_bay"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "reprocessing"
|
||||||
|
station_level = 2
|
||||||
|
buildings = ["reprocessing_plant"]
|
||||||
|
|
||||||
|
# --- Ships -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "destroyer"
|
||||||
|
station_level = 0
|
||||||
|
ships = ["destroyer"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "cruiser"
|
||||||
|
station_level = 2
|
||||||
|
ships = ["cruiser"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "battlecruiser"
|
||||||
|
station_level = 4
|
||||||
|
requires = ["cruiser"]
|
||||||
|
ships = ["battlecruiser"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "battleship"
|
||||||
|
station_level = 6
|
||||||
|
requires = ["battlecruiser"]
|
||||||
|
ships = ["battleship"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "dreadnought"
|
||||||
|
station_level = 8
|
||||||
|
requires = ["battleship"]
|
||||||
|
ships = ["dreadnought"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "carrier"
|
||||||
|
station_level = 9
|
||||||
|
requires = ["battleship"]
|
||||||
|
ships = ["carrier"]
|
||||||
|
|
||||||
|
# --- Modules ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "repair_tool"
|
||||||
|
station_level = 0
|
||||||
|
modules = ["repair_tool"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "armor_plates"
|
||||||
|
station_level = 0
|
||||||
|
modules = ["armor_plates"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "maneuvering_thrusters"
|
||||||
|
station_level = 1
|
||||||
|
modules = ["maneuvering_thrusters"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "sensor_booster"
|
||||||
|
station_level = 1
|
||||||
|
modules = ["sensor_booster"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "railgun_m"
|
||||||
|
station_level = 2
|
||||||
|
modules = ["railgun_m"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "afterburner"
|
||||||
|
station_level = 2
|
||||||
|
modules = ["afterburner"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "weapon_stabilizer"
|
||||||
|
station_level = 3
|
||||||
|
modules = ["weapon_stabilizer"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "weapon_upgrade"
|
||||||
|
station_level = 4
|
||||||
|
modules = ["weapon_upgrade"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "weapon_primer"
|
||||||
|
station_level = 4
|
||||||
|
modules = ["weapon_primer"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "drone_bay"
|
||||||
|
station_level = 5
|
||||||
|
modules = ["drone_bay"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "railgun_l"
|
||||||
|
station_level = 6
|
||||||
|
requires = ["railgun_m"]
|
||||||
|
modules = ["railgun_l"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "drone_hangar"
|
||||||
|
station_level = 9
|
||||||
|
modules = ["drone_hangar"]
|
||||||
|
|
||||||
|
# --- Assembler recipes -----------------------------------------------------
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "direct_rolling"
|
||||||
|
station_level = 1
|
||||||
|
recipes = ["direct_rolling"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "direct_etching"
|
||||||
|
station_level = 2
|
||||||
|
recipes = ["direct_etching"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "direct_hardening"
|
||||||
|
station_level = 2
|
||||||
|
recipes = ["direct_hardening"]
|
||||||
380
bin/app/data/config/visuals.toml
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
# visuals.toml
|
||||||
|
#
|
||||||
|
# Rendering-only configuration, loaded once at startup by the UI.
|
||||||
|
# The simulation does not read this file.
|
||||||
|
#
|
||||||
|
# Every BuildingType, ItemType, ship role, and station type must have an
|
||||||
|
# entry here; missing or malformed entries abort startup (see architecture.md
|
||||||
|
# "Visual Parameters").
|
||||||
|
#
|
||||||
|
# Colors are "#rrggbb" or "#rrggbbaa" (alpha optional, last byte).
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Tile backgrounds
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[tiles.asteroid]
|
||||||
|
fill = "#4a4038"
|
||||||
|
|
||||||
|
[tiles.space]
|
||||||
|
fill = "#0a0a15"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Buildings
|
||||||
|
#
|
||||||
|
# One section per BuildingType enum value. `fill` colors every footprint tile
|
||||||
|
# of the building; `outline` is drawn around the building's bounding box;
|
||||||
|
# `glyph` is a short label drawn centered on the building.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[buildings.hq]
|
||||||
|
fill = "#2e5fb8"
|
||||||
|
outline = "#ffffff"
|
||||||
|
glyph = "HQ"
|
||||||
|
|
||||||
|
[buildings.miner]
|
||||||
|
fill = "#6b4a2c"
|
||||||
|
outline = "#ffffff"
|
||||||
|
glyph = "M"
|
||||||
|
|
||||||
|
[buildings.smelter]
|
||||||
|
fill = "#b85a1e"
|
||||||
|
outline = "#ffffff"
|
||||||
|
glyph = "Sm"
|
||||||
|
|
||||||
|
[buildings.assembler]
|
||||||
|
fill = "#3a6fa8"
|
||||||
|
outline = "#ffffff"
|
||||||
|
glyph = "A"
|
||||||
|
|
||||||
|
[buildings.reprocessing_plant]
|
||||||
|
fill = "#6a3a8a"
|
||||||
|
outline = "#ffffff"
|
||||||
|
glyph = "R"
|
||||||
|
|
||||||
|
[buildings.shipyard]
|
||||||
|
fill = "#385870"
|
||||||
|
outline = "#ffffff"
|
||||||
|
glyph = "Y"
|
||||||
|
|
||||||
|
[buildings.salvage_bay]
|
||||||
|
fill = "#b8a23a"
|
||||||
|
outline = "#ffffff"
|
||||||
|
glyph = "Sb"
|
||||||
|
|
||||||
|
[buildings.belt]
|
||||||
|
fill = "#1a1a1a"
|
||||||
|
outline = "#7a7a7a"
|
||||||
|
glyph = ""
|
||||||
|
|
||||||
|
[buildings.splitter]
|
||||||
|
fill = "#7a7a5a"
|
||||||
|
outline = "#9a9a7a"
|
||||||
|
glyph = ""
|
||||||
|
|
||||||
|
[buildings.tunnel_entry]
|
||||||
|
fill = "#4a6a5a"
|
||||||
|
outline = "#8aaa9a"
|
||||||
|
glyph = "Te"
|
||||||
|
|
||||||
|
[buildings.tunnel_exit]
|
||||||
|
fill = "#5a6a4a"
|
||||||
|
outline = "#9aaa8a"
|
||||||
|
glyph = "Tx"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Stations
|
||||||
|
#
|
||||||
|
# Player and enemy defence stations have their own entries rather than going
|
||||||
|
# through [buildings.*] so the two sides can be color-coded at a glance.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[stations.player]
|
||||||
|
fill = "#2e5fb8"
|
||||||
|
outline = "#ffffff"
|
||||||
|
glyph = "P"
|
||||||
|
|
||||||
|
[stations.enemy]
|
||||||
|
fill = "#b82e2e"
|
||||||
|
outline = "#ffffff"
|
||||||
|
glyph = "E"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Items
|
||||||
|
#
|
||||||
|
# Items on belts are 10x10 squares. `fill` is the square color; `outline` is
|
||||||
|
# drawn around it. One section per ItemType.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# --- ores ---
|
||||||
|
|
||||||
|
[items.iron_ore]
|
||||||
|
fill = "#47271c"
|
||||||
|
outline = "#a65b42"
|
||||||
|
|
||||||
|
[items.copper_ore]
|
||||||
|
fill = "#deb592"
|
||||||
|
outline = "#826a56"
|
||||||
|
|
||||||
|
[items.quartz]
|
||||||
|
fill = "#4f3d66"
|
||||||
|
outline = "#9776c4"
|
||||||
|
|
||||||
|
# --- smelted basics ---
|
||||||
|
|
||||||
|
[items.iron_ingot]
|
||||||
|
fill = "#535357"
|
||||||
|
outline = "#97979e"
|
||||||
|
|
||||||
|
[items.copper_ingot]
|
||||||
|
fill = "#663810"
|
||||||
|
outline = "#b07746"
|
||||||
|
|
||||||
|
[items.silicon]
|
||||||
|
fill = "#737f99"
|
||||||
|
outline = "#373f4f"
|
||||||
|
|
||||||
|
# --- salvage loop ---
|
||||||
|
|
||||||
|
[items.scrap]
|
||||||
|
fill = "#998e81"
|
||||||
|
outline = "#574b3d"
|
||||||
|
|
||||||
|
[items.voidsteel]
|
||||||
|
fill = "#82759c"
|
||||||
|
outline = "#3f374f"
|
||||||
|
|
||||||
|
# --- basic components ---
|
||||||
|
|
||||||
|
[items.copper_wire]
|
||||||
|
fill = "#e09a50"
|
||||||
|
outline = "#3a2008"
|
||||||
|
|
||||||
|
[items.steel_plate]
|
||||||
|
fill = "#202836"
|
||||||
|
outline = "#54698c"
|
||||||
|
|
||||||
|
[items.copper_coil]
|
||||||
|
fill = "#cca287"
|
||||||
|
outline = "#755d4d"
|
||||||
|
|
||||||
|
[items.building_block]
|
||||||
|
fill = "#544724"
|
||||||
|
outline = "#a18845"
|
||||||
|
|
||||||
|
# --- advanced components ---
|
||||||
|
|
||||||
|
[items.control_chip]
|
||||||
|
fill = "#74b08b"
|
||||||
|
outline = "#42634e"
|
||||||
|
|
||||||
|
[items.capacitor_bank]
|
||||||
|
fill = "#d0a030"
|
||||||
|
outline = "#302408"
|
||||||
|
|
||||||
|
[items.hardened_steel]
|
||||||
|
fill = "#818999"
|
||||||
|
outline = "#3e4859"
|
||||||
|
|
||||||
|
[items.ceramic_plate]
|
||||||
|
fill = "#e0d8c8"
|
||||||
|
outline = "#3a3428"
|
||||||
|
|
||||||
|
[items.drive_unit]
|
||||||
|
fill = "#92a4de"
|
||||||
|
outline = "#545f80"
|
||||||
|
|
||||||
|
# --- capital components ---
|
||||||
|
|
||||||
|
[items.voidsteel_plate]
|
||||||
|
fill = "#9986b5"
|
||||||
|
outline = "#544766"
|
||||||
|
|
||||||
|
[items.capital_core]
|
||||||
|
fill = "#420c52"
|
||||||
|
outline = "#9444ab"
|
||||||
|
|
||||||
|
# --- module items ---
|
||||||
|
|
||||||
|
[items.railgun_s_module]
|
||||||
|
fill = "#bf7e7e"
|
||||||
|
outline = "#664343"
|
||||||
|
|
||||||
|
[items.railgun_m_module]
|
||||||
|
fill = "#b07474"
|
||||||
|
outline = "#593b3b"
|
||||||
|
|
||||||
|
[items.railgun_l_module]
|
||||||
|
fill = "#b07474"
|
||||||
|
outline = "#593b3b"
|
||||||
|
|
||||||
|
[items.salvager_module]
|
||||||
|
fill = "#b2cfdd"
|
||||||
|
outline = "#236137"
|
||||||
|
|
||||||
|
[items.repair_tool_module]
|
||||||
|
fill = "#0b4347"
|
||||||
|
outline = "#38868c"
|
||||||
|
|
||||||
|
[items.armor_plates_module]
|
||||||
|
fill = "#999999"
|
||||||
|
outline = "#545454"
|
||||||
|
|
||||||
|
[items.sensor_booster_module]
|
||||||
|
fill = "#a0c9f2"
|
||||||
|
outline = "#607991"
|
||||||
|
|
||||||
|
[items.maneuvering_thrusters_module]
|
||||||
|
fill = "#92b4de"
|
||||||
|
outline = "#566982"
|
||||||
|
|
||||||
|
[items.afterburner_module]
|
||||||
|
fill = "#1d2e52"
|
||||||
|
outline = "#486db5"
|
||||||
|
|
||||||
|
[items.weapon_upgrade_module]
|
||||||
|
fill = "#ff4040"
|
||||||
|
outline = "#401010"
|
||||||
|
|
||||||
|
[items.weapon_primer_module]
|
||||||
|
fill = "#e69797"
|
||||||
|
outline = "#855858"
|
||||||
|
|
||||||
|
[items.weapon_stabilizer_module]
|
||||||
|
fill = "#c78383"
|
||||||
|
outline = "#6e4848"
|
||||||
|
|
||||||
|
[items.drone_bay_module]
|
||||||
|
fill = "#cc66ff"
|
||||||
|
outline = "#331040"
|
||||||
|
|
||||||
|
[items.drone_hangar_module]
|
||||||
|
fill = "#8c689e"
|
||||||
|
outline = "#42314a"
|
||||||
|
|
||||||
|
# --- ship hulls (outline matches the ship's fleet color in [ships.*]) ---
|
||||||
|
|
||||||
|
[items.drone_hull]
|
||||||
|
fill = "#1b1b1b"
|
||||||
|
outline = "#3366ff"
|
||||||
|
|
||||||
|
[items.frigate_hull]
|
||||||
|
fill = "#1b1b1b"
|
||||||
|
outline = "#44aaff"
|
||||||
|
|
||||||
|
[items.destroyer_hull]
|
||||||
|
fill = "#1b1b1b"
|
||||||
|
outline = "#33ccaa"
|
||||||
|
|
||||||
|
[items.cruiser_hull]
|
||||||
|
fill = "#1b1b1b"
|
||||||
|
outline = "#66cc33"
|
||||||
|
|
||||||
|
[items.battlecruiser_hull]
|
||||||
|
fill = "#1b1b1b"
|
||||||
|
outline = "#cccc33"
|
||||||
|
|
||||||
|
[items.battleship_hull]
|
||||||
|
fill = "#1b1b1b"
|
||||||
|
outline = "#ff9933"
|
||||||
|
|
||||||
|
[items.dreadnought_hull]
|
||||||
|
fill = "#1b1b1b"
|
||||||
|
outline = "#ff5533"
|
||||||
|
|
||||||
|
[items.carrier_hull]
|
||||||
|
fill = "#310f42"
|
||||||
|
outline = "#8542a6"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Ships
|
||||||
|
#
|
||||||
|
# Ships are drawn as oriented triangles/arrows. Color is keyed to schematic id.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[ships.drone]
|
||||||
|
fill = "#3366ff"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.frigate]
|
||||||
|
fill = "#44aaff"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.destroyer]
|
||||||
|
fill = "#33ccaa"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.cruiser]
|
||||||
|
fill = "#66cc33"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.battlecruiser]
|
||||||
|
fill = "#cccc33"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.battleship]
|
||||||
|
fill = "#ff9933"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.dreadnought]
|
||||||
|
fill = "#ff5533"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.carrier]
|
||||||
|
fill = "#cc66ff"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Laser beams (REQ-SHP-FIRING-BEAM)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[beams]
|
||||||
|
weapon_color = "#ff6600"
|
||||||
|
repair_color = "#33ff66"
|
||||||
|
salvage_color = "#33ccff"
|
||||||
|
width_px = 2
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Build / deconstruct / selection overlays
|
||||||
|
#
|
||||||
|
# All overlay colors carry an alpha channel so they composite over the
|
||||||
|
# underlying scene.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[overlays]
|
||||||
|
ghost_valid = "#ffffff44" # builder-mode ghost, placement allowed (REQ-BLD-GHOST)
|
||||||
|
ghost_invalid = "#ff000044" # builder-mode ghost, placement invalid (REQ-BLD-PLACE-VALID)
|
||||||
|
deconstruct_tint = "#ff000033" # deconstruct-mode hover tint; its RGB also draws the
|
||||||
|
# box-drag rectangle in deconstruct mode, opaque
|
||||||
|
# (REQ-UI-MULTI-SELECT, REQ-BLD-DECONSTRUCT-BOX)
|
||||||
|
tile_highlight = "#ffffff22" # tile under cursor
|
||||||
|
selected_outline = "#ffff00" # outline around currently-selected building(s), and the
|
||||||
|
# box-drag selection rectangle (REQ-UI-MULTI-SELECT)
|
||||||
|
config_transfer = "#33ccff66" # blueprint ghost over a configuration-transfer target (REQ-UI-BLUEPRINT-TRANSFER)
|
||||||
|
locked_asteroid = "#0000007f" # tint over the asteroid left of the buildable edge (not yet unlocked by expansion)
|
||||||
|
next_expansion = "#00000040" # lighter tint over the columns the next expansion unlocks (REQ-UI-LOCKED-ASTEROID)
|
||||||
|
modal_dim = "#00000099" # semi-transparent black dim behind modal dialogs/menus (REQ-UI-MODAL-DIM)
|
||||||
|
tunnel_preview = "#00ff0055" # tunnel connection preview: matched end + tiles between (REQ-BLD-TUNNEL-MODE)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Schematic-drop toasts (REQ-UI-SCHEMATIC-TOAST)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[toast]
|
||||||
|
bg = "#000000cc"
|
||||||
|
fg = "#ffffff"
|
||||||
|
font_size = 14
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Building status light (REQ-UI-STATUS-LIGHT)
|
||||||
|
#
|
||||||
|
# Fill color per production state, drawn as a small circle in the building's
|
||||||
|
# upper-right corner, plus the constant outline color.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[status_light]
|
||||||
|
grey = "#808080" # no recipe/schematic selected
|
||||||
|
green = "#33cc33" # producing (Salvage Bay: holding scrap)
|
||||||
|
red = "#cc3333" # idle, input missing (Salvage Bay: empty)
|
||||||
|
yellow = "#e6c619" # idle, output buffer full
|
||||||
|
outline = "#000000"
|
||||||
59
bin/app/data/config/world.toml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
[world]
|
||||||
|
height_tiles = 30
|
||||||
|
refund_percentage = 100
|
||||||
|
deconstruction_time_seconds = 0.1
|
||||||
|
starting_building_blocks = 200
|
||||||
|
debris_despawn_seconds = 120
|
||||||
|
scrap_per_threat = 0.25
|
||||||
|
tile_size_m = 10
|
||||||
|
belt_speed_mps = 20
|
||||||
|
tunnel_max_distance_tiles = 10
|
||||||
|
departure_interval_seconds = 20
|
||||||
|
orbit_factor = 0.8
|
||||||
|
rally_orbit_radius_tiles = 5.0
|
||||||
|
artifact_tooltip = "Artifacts are the key to victory. Earn one by choosing the artifact reward when you destroy a set of enemy defence stations. Collect enough of them to win the game."
|
||||||
|
|
||||||
|
[regions]
|
||||||
|
asteroid_width_tiles = 40
|
||||||
|
player_buffer_width_tiles = 20
|
||||||
|
contest_zone_width_tiles = 60
|
||||||
|
enemy_buffer_width_tiles = 20
|
||||||
|
|
||||||
|
[scroll]
|
||||||
|
# View pan speed (REQ-UI-SCROLL-SPEED): slow near the asteroid, fast across the
|
||||||
|
# contest zone, with a linear ramp of the given width straddling each boundary.
|
||||||
|
pan_speed_slow_tiles_per_second = 16.0
|
||||||
|
pan_speed_fast_tiles_per_second = 32.0
|
||||||
|
pan_ramp_band_width_tiles = 16
|
||||||
|
|
||||||
|
[expansion]
|
||||||
|
columns_per_expansion_tiles = 10
|
||||||
|
# x = expansions already purchased; ~1 per cycle mid-game, decelerating
|
||||||
|
# to 2-3 cycles late (docs/balancing/derived.md).
|
||||||
|
cost_building_blocks_formula = "300 + 50*x + 10*x*x"
|
||||||
|
|
||||||
|
[push]
|
||||||
|
push_expand_columns_tiles = 10
|
||||||
|
boss_advance_seconds = 60
|
||||||
|
|
||||||
|
[targeting]
|
||||||
|
target_score_formula = "1 / (1 + x)" # x = distance / max weapon range; higher = better, clamped to >=0
|
||||||
|
overclaim_penalty_formula = "max(0.5, 1 - 0.1*x)" # x = competing claim count; multiplies score, clamped to [0,1]
|
||||||
|
target_hysteresis = 0.40 # keep current target unless a challenger beats it by >10%
|
||||||
|
|
||||||
|
[artifacts]
|
||||||
|
artifact_chance_formula = "0.05 * x" # 5% chance per station level
|
||||||
|
artifact_win_count = 5
|
||||||
|
|
||||||
|
[waves]
|
||||||
|
# Tuned against the factory-size curve (docs/balancing/targets.md, balancing
|
||||||
|
# targets): stays below the player's achievable military output early,
|
||||||
|
# crosses it around the late boundary (~cycle 15), overwhelms by ~24.
|
||||||
|
threat_rate_formula = "2*x + 0.15*x*x"
|
||||||
|
gap_min_seconds = 15
|
||||||
|
gap_max_seconds = 45
|
||||||
|
spawn_duration_seconds = 10
|
||||||
|
boss_countdown_seconds = 300
|
||||||
|
boss_threat_duration_seconds = 60
|
||||||
|
boss_quiet_before_seconds = 20
|
||||||
|
boss_quiet_after_seconds = 20
|
||||||
6
bin/app/data/icons/buildings/assembler.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#3a6fa8"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="16" cy="16" r="6.2"/><circle cx="16" cy="16" r="2.2"/><path d="M16 5.5v3M16 23.5v3M5.5 16h3M23.5 16h3M8.6 8.6 10.7 10.7M23.4 8.6 21.3 10.7M8.6 23.4 10.7 21.3M23.4 23.4 21.3 21.3"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 499 B |
6
bin/app/data/icons/buildings/belt.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#6a6a6a"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="4" y="10" width="24" height="12" rx="3"/><path d="M9 13 12.5 16 9 19"/><path d="M14.5 13 18 16 14.5 19"/><path d="M20 13 23.5 16 20 19"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 453 B |
6
bin/app/data/icons/buildings/deconstruct.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#cc3333"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M7 10h18"/><path d="M9.5 10 11 26h10l1.5-16"/><path d="M13 10V6.5h6V10"/><path d="M13.5 14v8M18.5 14v8"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 421 B |
6
bin/app/data/icons/buildings/hq.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#2e5fb8"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="7" y="15" width="18" height="11" rx="1"/><path d="M16 15V5"/><path d="M16 6h6l-2.2 2 2.2 2h-6"/><path d="M11 19h2.5M15.5 19h2.5M20 19h1.5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 456 B |
6
bin/app/data/icons/buildings/miner.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#6b4a2c"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="9" y="5" width="14" height="8" rx="1.5"/><path d="M11 13 16 26 21 13"/><path d="M13 17.5h6"/><path d="M14.5 21.5h3"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 433 B |
6
bin/app/data/icons/buildings/reprocessing_plant.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#6a3a8a"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="18" r="4"/><circle cx="20" cy="18" r="4"/><path d="M10.6 16.6 13.4 19.4M13.4 16.6 10.6 19.4M18.6 16.6 21.4 19.4M21.4 16.6 18.6 19.4"/><path d="M16 4v8"/><path d="M13 9 16 12 19 9"/><path d="M16 24v4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 528 B |
6
bin/app/data/icons/buildings/salvage_bay.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#b8a23a"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M7 13h18l-2 13H9z"/><path d="M16 3v7"/><path d="M12 7 16 11 20 7"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 383 B |
6
bin/app/data/icons/buildings/shipyard.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#3f6580"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M16 4c2 3 2.6 6 2.6 9.5V16h-5.2v-2.5C13.4 10 14 7 16 4z"/><circle cx="16" cy="10" r="1.3"/><path d="M13.4 15 11 18h2.4z"/><path d="M18.6 15 21 18h-2.4z"/><path d="M14.7 17 16 21 17.3 17"/><path d="M9 25h14"/><path d="M12 25v-2M20 25v-2"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 554 B |
6
bin/app/data/icons/buildings/smelter.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#b85a1e"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M9 9h14l-2.5 8h-9z"/><path d="M8 9h16"/><path d="M11 26c-1-1.8.4-3 .4-4 .9 1 1.6 2.2 1.6 4"/><path d="M16 26c-1-2 .4-3.4.4-4.4 .9 1 1.6 2.6 1.6 4.4"/><path d="M21 26c-1-1.8.4-3 .4-4 .9 1 1.6 2.2 1.6 4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 519 B |
6
bin/app/data/icons/buildings/splitter.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#7a7a5a"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5 16h11"/><path d="M16 8v16"/><path d="M13.5 10.5 16 8 18.5 10.5"/><path d="M13.5 21.5 16 24 18.5 21.5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 422 B |
6
bin/app/data/icons/buildings/station.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#55606f"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M16 4 26 8v7c0 7-5 11-10 13-5-2-10-6-10-13V8z"/><path d="M16 11v9M11.5 15.5h9"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 396 B |
6
bin/app/data/icons/buildings/tunnel_entry.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#4f7562"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 13H29"/><path d="M4 7H8Q10 7 10 9.5V21.5Q10 24 12.5 24H19.5Q22 24 22 21.5V9.5Q22 7 24 7H27"/><path d="M24.5 4.5 27 7 24.5 9.5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 447 B |
6
bin/app/data/icons/items/afterburner_module.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="11.6 19.6 76.9 76.9" width="100" height="100">
|
||||||
|
<rect x="34" y="24" width="32" height="26" rx="5" fill="#6080c0" stroke="#182030" stroke-width="3.5"/>
|
||||||
|
<path d="M36 50 L64 50 L58 62 L42 62 Z" fill="#3a5090" stroke="#182030" stroke-width="2.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M42 62 Q46 80 50 62 Q54 80 58 62 Q56 90 50 92 Q44 90 42 62 Z" fill="#ff9a3a"/>
|
||||||
|
<path d="M47 64 Q50 82 53 64 Z" fill="#ffe08a"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 468 B |
5
bin/app/data/icons/items/armor_plates_module.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="10.5 11.5 79.0 79.0" width="100" height="100">
|
||||||
|
<path d="M50 16 L80 26 L80 50 Q80 74 50 86 Q20 74 20 50 L20 26 Z" fill="#808080" stroke="#202020" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M50 16 L50 86" stroke="#5a5a5a" stroke-width="3"/>
|
||||||
|
<path d="M20 42 Q50 52 80 42" fill="none" stroke="#5a5a5a" stroke-width="3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 390 B |
5
bin/app/data/icons/items/battlecruiser_hull.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7.1 6.1 85.9 85.9" width="100" height="100">
|
||||||
|
<path d="M50 12 L66 40 L88 60 L64 58 L60 86 L40 86 L36 58 L12 60 L34 40 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="40" r="7" fill="#cccc33" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
<g fill="#3a4450"><circle cx="44" cy="82" r="3.2"/><circle cx="56" cy="82" r="3.2"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 429 B |
5
bin/app/data/icons/items/battleship_hull.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="9.2 9.2 81.6 81.6" width="100" height="100">
|
||||||
|
<path d="M50 14 L70 44 L82 78 L60 70 L58 86 L42 86 L40 70 L18 78 L30 44 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="44" r="7" fill="#ff9933" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
<g fill="#3a4450"><circle cx="44" cy="82" r="3.2"/><circle cx="56" cy="82" r="3.2"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 429 B |
12
bin/app/data/icons/items/building_block.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="14.9 15.9 70.2 70.2" width="100" height="100">
|
||||||
|
<g stroke="#302810" stroke-width="3.2" stroke-linejoin="round">
|
||||||
|
<path d="M50 20 L78 36 L50 52 L22 36 Z" fill="#ddc98f"/>
|
||||||
|
<path d="M50 52 L78 36 L78 66 L50 82 Z" fill="#b39a5c"/>
|
||||||
|
<path d="M50 52 L22 36 L22 66 L50 82 Z" fill="#c8b070"/>
|
||||||
|
</g>
|
||||||
|
<g stroke="#302810" stroke-width="1.8" stroke-linejoin="round">
|
||||||
|
<path d="M50 26.4 L66.8 36 L50 45.6 L33.2 36 Z" fill="#cbb772"/>
|
||||||
|
<path d="M55.3 54.7 L72.7 44.7 L72.7 63.3 L55.3 73.3 Z" fill="#a2894f"/>
|
||||||
|
<path d="M44.7 54.7 L27.3 44.7 L27.3 63.3 L44.7 73.3 Z" fill="#b7a465"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 655 B |
12
bin/app/data/icons/items/capacitor_bank.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="17.0 16.0 65.9 65.9" width="100" height="100">
|
||||||
|
<g stroke="#302408" stroke-width="3.2" stroke-linecap="round">
|
||||||
|
<line x1="37" y1="20" x2="37" y2="30"/>
|
||||||
|
<line x1="63" y1="20" x2="63" y2="30"/>
|
||||||
|
</g>
|
||||||
|
<g stroke="#302408" stroke-width="3.2" stroke-linejoin="round">
|
||||||
|
<rect x="28" y="28" width="18" height="50" rx="8" fill="#d0a030"/>
|
||||||
|
<rect x="54" y="28" width="18" height="50" rx="8" fill="#e0b040"/>
|
||||||
|
</g>
|
||||||
|
<line x1="30" y1="42" x2="44" y2="42" stroke="#6b5410" stroke-width="2.6"/>
|
||||||
|
<line x1="56" y1="42" x2="70" y2="42" stroke="#6b5410" stroke-width="2.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 634 B |
5
bin/app/data/icons/items/capital_core.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.5 17.5 68.9 68.9" width="100" height="100">
|
||||||
|
<circle cx="50" cy="52" r="30" fill="#b040d0" stroke="#280c30" stroke-width="4"/>
|
||||||
|
<path d="M50 26 L68 46 L50 78 L32 46 Z" fill="#d06ae8" stroke="#280c30" stroke-width="2.5" stroke-linejoin="round"/>
|
||||||
|
<circle cx="43" cy="44" r="6" fill="#f0c0f8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 355 B |
5
bin/app/data/icons/items/carrier_hull.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="13.4 17.4 73.1 73.1" width="100" height="100">
|
||||||
|
<path d="M28 24 L72 24 L82 50 L72 84 L28 84 L18 50 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<line x1="50" y1="26" x2="50" y2="82" stroke="#4a5560" stroke-width="4"/>
|
||||||
|
<circle cx="50" cy="38" r="6" fill="#cc66ff" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 395 B |
4
bin/app/data/icons/items/ceramic_plate.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.8 15.8 68.4 68.4" width="100" height="100">
|
||||||
|
<path d="M50 20 L76 35 L76 65 L50 80 L24 65 L24 35 Z" fill="#e0d8c8" stroke="#3a3428" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M50 32 L64 40 L64 60 L50 68 L36 60 L36 40 Z" fill="#efe9dd" stroke="#3a3428" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 343 B |
6
bin/app/data/icons/items/control_chip.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.5 15.5 68.9 68.9" width="100" height="100">
|
||||||
|
<rect x="20" y="20" width="60" height="60" rx="11" fill="#1f7a44" stroke="#0e3320" stroke-width="4"/>
|
||||||
|
<rect x="33" y="33" width="34" height="34" rx="5" fill="#3fbf6a" stroke="#0e3320" stroke-width="3"/>
|
||||||
|
<path d="M50 33 L50 24 M50 67 L50 76 M33 50 L24 50 M67 50 L76 50" stroke="#186036" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<circle cx="50" cy="50" r="6" fill="#c9a227" stroke="#0e3320" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 520 B |
7
bin/app/data/icons/items/copper_coil.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0.0 2.0 100.0 100.0" width="100" height="100">
|
||||||
|
<circle cx="50" cy="52" r="42" fill="none" stroke="#3a1e08" stroke-width="2.5"/>
|
||||||
|
<circle cx="50" cy="52" r="26" fill="none" stroke="#3a1e08" stroke-width="2.5"/>
|
||||||
|
<circle cx="50" cy="52" r="34" fill="none" stroke="#cf7a2c" stroke-width="16"/>
|
||||||
|
<circle cx="50" cy="52" r="34" fill="none" stroke="#8a4e18" stroke-width="16" stroke-dasharray="3 11"/>
|
||||||
|
<circle cx="50" cy="52" r="34" fill="none" stroke="#eaa85f" stroke-width="16" stroke-dasharray="2 12" stroke-dashoffset="6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 585 B |
8
bin/app/data/icons/items/copper_ingot.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="24.0 28.0 62.0 62.0" width="100" height="100">
|
||||||
|
<g stroke="#3a1e08" stroke-width="3.5" stroke-linejoin="round">
|
||||||
|
<path d="M28 72 L72 72 L64 54 L36 54 Z" fill="#cf7a2c"/>
|
||||||
|
<path d="M64 54 L72 72 L82 64 L74 46 Z" fill="#a85e20"/>
|
||||||
|
<path d="M36 54 L64 54 L74 46 L46 46 Z" fill="#e59a52"/>
|
||||||
|
</g>
|
||||||
|
<path d="M40 63 L60 63" stroke="#f0b878" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 444 B |
8
bin/app/data/icons/items/copper_ore.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="11.5 11.5 79.0 79.0" width="100" height="100">
|
||||||
|
<path d="M50 16 L76 28 L86 54 L70 84 L36 86 L16 58 L24 30 Z" fill="#5f8f78" stroke="#16241d" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M50 16 L58 47 L24 30 Z" fill="#79a890"/>
|
||||||
|
<path d="M58 47 L70 84 L36 86 Z" fill="#4a7060"/>
|
||||||
|
<path d="M50 16 L58 47 M58 47 L86 54 M58 47 L70 84 M58 47 L36 86 M58 47 L24 30" fill="none" stroke="#2c463a" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
<circle cx="41" cy="56" r="4" fill="#d98a3e"/>
|
||||||
|
<circle cx="62" cy="66" r="3.2" fill="#d98a3e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 602 B |
10
bin/app/data/icons/items/copper_wire.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="8.1 4.1 83.7 83.7" width="100" height="100">
|
||||||
|
<g fill="none" stroke-linecap="round">
|
||||||
|
<path d="M16 38 q17 -12 34 0 t34 0" stroke="#3a1e08" stroke-width="10"/>
|
||||||
|
<path d="M16 52 q17 -12 34 0 t34 0" stroke="#3a1e08" stroke-width="10"/>
|
||||||
|
<path d="M16 66 q17 -12 34 0 t34 0" stroke="#3a1e08" stroke-width="10"/>
|
||||||
|
<path d="M16 38 q17 -12 34 0 t34 0" stroke="#cf7a2c" stroke-width="6"/>
|
||||||
|
<path d="M16 52 q17 -12 34 0 t34 0" stroke="#d98a3e" stroke-width="6"/>
|
||||||
|
<path d="M16 66 q17 -12 34 0 t34 0" stroke="#cf7a2c" stroke-width="6"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 608 B |
5
bin/app/data/icons/items/cruiser_hull.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="9.2 8.2 81.6 81.6" width="100" height="100">
|
||||||
|
<path d="M50 14 L62 40 L86 58 L64 56 L60 84 L40 84 L36 56 L14 58 L38 40 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="40" r="6.5" fill="#66cc33" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
<g fill="#3a4450"><circle cx="45" cy="80" r="3"/><circle cx="55" cy="80" r="3"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 427 B |
4
bin/app/data/icons/items/destroyer_hull.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="9.2 8.2 81.6 81.6" width="100" height="100">
|
||||||
|
<path d="M50 14 L62 40 L86 58 L64 56 L60 84 L40 84 L36 56 L14 58 L38 40 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="40" r="6.5" fill="#33ccaa" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 340 B |
5
bin/app/data/icons/items/dreadnought_hull.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7.1 7.1 85.9 85.9" width="100" height="100">
|
||||||
|
<path d="M50 12 L72 42 L86 80 L62 72 L58 88 L42 88 L38 72 L14 80 L28 42 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="42" r="7.5" fill="#ff5533" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
<g fill="#3a4450"><circle cx="42" cy="82" r="3.4"/><circle cx="50" cy="84" r="3.4"/><circle cx="58" cy="82" r="3.4"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 464 B |
6
bin/app/data/icons/items/drive_unit.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="9.2 17.2 81.6 81.6" width="100" height="100">
|
||||||
|
<path d="M40 22 L60 22 L58 42 L72 74 L28 74 L42 42 Z" fill="#4a6ad0" stroke="#101a38" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<ellipse cx="50" cy="74" rx="22" ry="6" fill="#2a3a80" stroke="#101a38" stroke-width="2.5"/>
|
||||||
|
<path d="M40 22 L60 22" stroke="#8aa0e8" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M44 80 Q50 94 56 80" fill="#ffb347"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 464 B |
6
bin/app/data/icons/items/drone_bay_module.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="16.9 15.9 66.2 66.2" width="100" height="100">
|
||||||
|
<rect x="22" y="52" width="56" height="26" rx="5" fill="#7a2a9a" stroke="#331040" stroke-width="3.5"/>
|
||||||
|
<line x1="22" y1="65" x2="78" y2="65" stroke="#4a1560" stroke-width="2.5"/>
|
||||||
|
<path d="M50 20 L61 42 L50 35 L39 42 Z" fill="#cc66ff" stroke="#331040" stroke-width="3" stroke-linejoin="round"/>
|
||||||
|
<line x1="50" y1="44" x2="50" y2="52" stroke="#331040" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 481 B |
9
bin/app/data/icons/items/drone_hangar_module.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="13.7 12.7 72.6 72.6" width="100" height="100">
|
||||||
|
<rect x="18" y="50" width="64" height="30" rx="5" fill="#5a1c7a" stroke="#260c33" stroke-width="3.5"/>
|
||||||
|
<line x1="18" y1="64" x2="82" y2="64" stroke="#3a1050" stroke-width="2.5"/>
|
||||||
|
<g fill="#b060e0" stroke="#260c33" stroke-width="2.5" stroke-linejoin="round">
|
||||||
|
<path d="M32 22 L40 38 L32 33 L24 38 Z"/>
|
||||||
|
<path d="M50 18 L58 34 L50 29 L42 34 Z"/>
|
||||||
|
<path d="M68 22 L76 38 L68 33 L60 38 Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 512 B |
4
bin/app/data/icons/items/drone_hull.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="20.8 19.8 58.3 58.3" width="100" height="100">
|
||||||
|
<path d="M50 24 L66 74 L50 64 L34 74 Z" fill="#8f9bb0" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="44" r="6" fill="#3366ff" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 305 B |
4
bin/app/data/icons/items/frigate_hull.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="10.2 9.2 79.5 79.5" width="100" height="100">
|
||||||
|
<path d="M50 14 L74 84 L50 70 L26 84 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="40" r="7" fill="#44aaff" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 304 B |
8
bin/app/data/icons/items/hardened_steel.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.5 15.5 68.9 68.9" width="100" height="100">
|
||||||
|
<rect x="20" y="26" width="60" height="48" rx="6" fill="#6a7280" stroke="#181c22" stroke-width="4"/>
|
||||||
|
<rect x="29" y="35" width="42" height="30" rx="3" fill="#7c8593" stroke="#181c22" stroke-width="2.5"/>
|
||||||
|
<g fill="#2a2f38">
|
||||||
|
<circle cx="26" cy="32" r="3.2"/><circle cx="74" cy="32" r="3.2"/>
|
||||||
|
<circle cx="26" cy="68" r="3.2"/><circle cx="74" cy="68" r="3.2"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 481 B |
8
bin/app/data/icons/items/iron_ingot.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="24.0 28.0 62.0 62.0" width="100" height="100">
|
||||||
|
<g stroke="#2f343b" stroke-width="3.5" stroke-linejoin="round">
|
||||||
|
<path d="M28 72 L72 72 L64 54 L36 54 Z" fill="#a2a9b0"/>
|
||||||
|
<path d="M64 54 L72 72 L82 64 L74 46 Z" fill="#868d95"/>
|
||||||
|
<path d="M36 54 L64 54 L74 46 L46 46 Z" fill="#c8ced4"/>
|
||||||
|
</g>
|
||||||
|
<path d="M40 63 L60 63" stroke="#c2c7cd" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 444 B |
10
bin/app/data/icons/items/iron_ore.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="11.5 11.5 79.0 79.0" width="100" height="100">
|
||||||
|
<path d="M50 16 L76 28 L86 54 L70 84 L36 86 L16 58 L24 30 Z"
|
||||||
|
fill="#9a8a7a" stroke="#241c16" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M50 16 L58 47 L24 30 Z" fill="#b4a692"/>
|
||||||
|
<path d="M58 47 L70 84 L36 86 Z" fill="#7a6b5c"/>
|
||||||
|
<path d="M50 16 L58 47 M58 47 L86 54 M58 47 L70 84 M58 47 L36 86 M58 47 L24 30"
|
||||||
|
fill="none" stroke="#4a3d31" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
<circle cx="41" cy="56" r="4" fill="#c8752e"/>
|
||||||
|
<circle cx="62" cy="66" r="3.2" fill="#c8752e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 618 B |
11
bin/app/data/icons/items/maneuvering_thrusters_module.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="17.9 23.9 64.1 64.1" width="100" height="100">
|
||||||
|
<rect x="30" y="28" width="40" height="30" rx="5" fill="#5090e0" stroke="#142438" stroke-width="3.5"/>
|
||||||
|
<g fill="#3a6ab0" stroke="#142438" stroke-width="2.5" stroke-linejoin="round">
|
||||||
|
<path d="M34 58 L30 72 L44 72 L40 58 Z"/>
|
||||||
|
<path d="M60 58 L56 72 L70 72 L66 58 Z"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#ffcf6a">
|
||||||
|
<path d="M33 72 L37 84 L41 72 Z"/>
|
||||||
|
<path d="M59 72 L63 84 L67 72 Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 494 B |
7
bin/app/data/icons/items/quartz.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="8.6 7.6 82.9 82.9" width="100" height="100">
|
||||||
|
<path d="M50 12 L70 40 L58 86 L42 86 L30 40 Z" fill="#dcd3f0" stroke="#40345a" stroke-width="3.2" stroke-linejoin="round"/>
|
||||||
|
<path d="M30 40 L70 40" stroke="#40345a" stroke-width="2.2"/>
|
||||||
|
<path d="M50 12 L50 86 M42 40 L42 86 M58 40 L58 86" stroke="#9d90c2" stroke-width="1.8" stroke-linecap="round"/>
|
||||||
|
<path d="M58 40 L70 40 L58 86 Z" fill="#c3b8e2"/>
|
||||||
|
<circle cx="45" cy="29" r="3" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 507 B |
10
bin/app/data/icons/items/railgun_l_module.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7.2 8.4 91.2 91.2" width="100" height="100">
|
||||||
|
<rect x="22" y="76" width="22" height="8" rx="2" fill="#3f454d" stroke="#20242a" stroke-width="3"/>
|
||||||
|
<rect x="12" y="24" width="32" height="52" rx="6" fill="#565d66" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<rect x="42" y="30" width="46" height="10" rx="2" fill="#6b727b" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<rect x="42" y="45" width="46" height="10" rx="2" fill="#6b727b" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<rect x="42" y="60" width="46" height="10" rx="2" fill="#6b727b" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<circle cx="88" cy="35" r="5.5" fill="#e23b2b" stroke="#20242a" stroke-width="2.5"/>
|
||||||
|
<circle cx="88" cy="50" r="5.5" fill="#e23b2b" stroke="#20242a" stroke-width="2.5"/>
|
||||||
|
<circle cx="88" cy="65" r="5.5" fill="#e23b2b" stroke="#20242a" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 884 B |
8
bin/app/data/icons/items/railgun_m_module.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="9.2 8.2 89.6 89.6" width="100" height="100">
|
||||||
|
<rect x="22" y="68" width="20" height="8" rx="2" fill="#3f454d" stroke="#20242a" stroke-width="3"/>
|
||||||
|
<rect x="14" y="30" width="30" height="38" rx="6" fill="#565d66" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<rect x="42" y="38" width="46" height="11" rx="2" fill="#6b727b" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<rect x="42" y="51" width="46" height="11" rx="2" fill="#6b727b" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<circle cx="88" cy="43.5" r="6" fill="#e23b2b" stroke="#20242a" stroke-width="2.5"/>
|
||||||
|
<circle cx="88" cy="56.5" r="6" fill="#e23b2b" stroke="#20242a" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 692 B |
7
bin/app/data/icons/items/railgun_s_module.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="11.3 9.6 85.9 85.9" width="100" height="100">
|
||||||
|
<rect x="24" y="62" width="20" height="9" rx="2" fill="#3f454d" stroke="#20242a" stroke-width="3"/>
|
||||||
|
<rect x="16" y="34" width="30" height="30" rx="6" fill="#565d66" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<rect x="42" y="43" width="44" height="12" rx="2" fill="#6b727b" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<path d="M45 46.5 L84 46.5 M45 51.5 L84 51.5" stroke="#e0b12a" stroke-width="2.6" stroke-linecap="round"/>
|
||||||
|
<circle cx="86" cy="49" r="6.5" fill="#e23b2b" stroke="#20242a" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 610 B |
8
bin/app/data/icons/items/repair_tool_module.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="9.3 10.3 78.4 78.4" width="100" height="100">
|
||||||
|
<line x1="30" y1="30" x2="66" y2="66" stroke="#2e9ba3" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<rect x="16" y="20" width="20" height="12" rx="3" fill="#1c6f76" stroke="#0f4247" stroke-width="2.5" transform="rotate(45 26 26)"/>
|
||||||
|
<path d="M64 64 L76 76 L72 80 L60 68 Z" fill="#bfe3e6" stroke="#0f4247" stroke-width="2"/>
|
||||||
|
<line x1="70" y1="30" x2="34" y2="66" stroke="#2e9ba3" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<circle cx="72" cy="28" r="9" fill="none" stroke="#2e9ba3" stroke-width="7"/>
|
||||||
|
<circle cx="32" cy="68" r="9" fill="none" stroke="#2e9ba3" stroke-width="7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 687 B |
10
bin/app/data/icons/items/salvager_module.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="10.8 9.3 78.4 78.4" width="100" height="100">
|
||||||
|
<circle cx="50" cy="28" r="13" fill="#b2cfdd" stroke="#1c4a30" stroke-width="3.5"/>
|
||||||
|
<circle cx="50" cy="28" r="5" fill="#5f8f78"/>
|
||||||
|
<path d="M50 41 L50 56" stroke="#1c4a30" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<g fill="none" stroke="#5f8f78" stroke-width="6" stroke-linecap="round">
|
||||||
|
<path d="M50 54 L30 78"/>
|
||||||
|
<path d="M50 54 L50 82"/>
|
||||||
|
<path d="M50 54 L70 78"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 494 B |
5
bin/app/data/icons/items/scrap.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.8 20.8 70.5 70.5" width="100" height="100">
|
||||||
|
<path d="M20 40 L44 30 L52 44 L74 34 L82 58 L60 66 L66 82 L38 78 L28 62 Z" fill="#8a8078" stroke="#241f1a" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<circle cx="46" cy="52" r="4" fill="#241f1a"/>
|
||||||
|
<path d="M34 46 L44 60 M58 50 L66 62" stroke="#5f574f" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 407 B |
9
bin/app/data/icons/items/sensor_booster_module.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="23.2 8.7 71.5 71.5" width="100" height="100">
|
||||||
|
<path d="M28 76 L46 40 A28 28 0 0 1 76 56 Z" fill="#40a0ff" stroke="#102840" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<line x1="52" y1="52" x2="70" y2="30" stroke="#102840" stroke-width="3.5" stroke-linecap="round"/>
|
||||||
|
<circle cx="71" cy="29" r="5" fill="#bfe0ff" stroke="#102840" stroke-width="2.5"/>
|
||||||
|
<g stroke="#bfe0ff" stroke-width="3" fill="none" stroke-linecap="round">
|
||||||
|
<path d="M78 20 a12 12 0 0 1 6 10"/>
|
||||||
|
<path d="M82 13 a20 20 0 0 1 8 17"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 576 B |
6
bin/app/data/icons/items/silicon.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="12.4 14.4 75.3 75.3" width="100" height="100">
|
||||||
|
<circle cx="50" cy="52" r="33" fill="#33415e" stroke="#0e1420" stroke-width="3.5"/>
|
||||||
|
<line x1="35" y1="27" x2="52" y2="22" stroke="#0e1420" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<circle cx="50" cy="52" r="21" fill="none" stroke="#46567a" stroke-width="2.6"/>
|
||||||
|
<circle cx="42" cy="44" r="5" fill="#5d6f96"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 420 B |
8
bin/app/data/icons/items/steel_plate.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.8 15.8 68.4 68.4" width="100" height="100">
|
||||||
|
<rect x="20" y="30" width="60" height="40" rx="5" fill="#8a92a0" stroke="#22262c" stroke-width="3.5"/>
|
||||||
|
<line x1="26" y1="38" x2="74" y2="38" stroke="#aab1bb" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<g fill="#2f343b">
|
||||||
|
<circle cx="30" cy="40" r="3"/><circle cx="70" cy="40" r="3"/>
|
||||||
|
<circle cx="30" cy="60" r="3"/><circle cx="70" cy="60" r="3"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 469 B |
8
bin/app/data/icons/items/voidsteel.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="24.0 28.0 62.0 62.0" width="100" height="100">
|
||||||
|
<g stroke="#140a20" stroke-width="3.5" stroke-linejoin="round">
|
||||||
|
<path d="M28 72 L72 72 L64 54 L36 54 Z" fill="#3a2c52"/>
|
||||||
|
<path d="M64 54 L72 72 L82 64 L74 46 Z" fill="#291d3d"/>
|
||||||
|
<path d="M36 54 L64 54 L74 46 L46 46 Z" fill="#4d3a6e"/>
|
||||||
|
</g>
|
||||||
|
<path d="M40 63 L60 63" stroke="#8f6fc4" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 444 B |
8
bin/app/data/icons/items/voidsteel_plate.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.5 15.5 68.9 68.9" width="100" height="100">
|
||||||
|
<rect x="20" y="28" width="60" height="44" rx="6" fill="#7a5aaa" stroke="#1c1038" stroke-width="4"/>
|
||||||
|
<line x1="26" y1="37" x2="74" y2="37" stroke="#a98fd0" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<g fill="#180c30">
|
||||||
|
<circle cx="30" cy="39" r="3"/><circle cx="70" cy="39" r="3"/>
|
||||||
|
<circle cx="30" cy="61" r="3"/><circle cx="70" cy="61" r="3"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 467 B |
8
bin/app/data/icons/items/weapon_primer_module.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="17.0 16.0 65.9 65.9" width="100" height="100">
|
||||||
|
<g stroke="#380e0e" stroke-width="3.2" stroke-linejoin="round">
|
||||||
|
<path d="M42 20 L58 20 L58 58 L50 72 L42 58 Z" fill="#e03838"/>
|
||||||
|
<rect x="40" y="56" width="20" height="22" rx="2" fill="#c9a227"/>
|
||||||
|
</g>
|
||||||
|
<line x1="41" y1="63" x2="59" y2="63" stroke="#7a5f14" stroke-width="2.5"/>
|
||||||
|
<line x1="41" y1="71" x2="59" y2="71" stroke="#7a5f14" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 471 B |
7
bin/app/data/icons/items/weapon_stabilizer_module.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7.1 7.1 85.9 85.9" width="100" height="100">
|
||||||
|
<circle cx="50" cy="50" r="32" fill="none" stroke="#300c0c" stroke-width="8"/>
|
||||||
|
<circle cx="50" cy="50" r="32" fill="none" stroke="#c03030" stroke-width="5"/>
|
||||||
|
<ellipse cx="50" cy="50" rx="32" ry="12" fill="none" stroke="#e06060" stroke-width="4"/>
|
||||||
|
<line x1="50" y1="14" x2="50" y2="86" stroke="#c03030" stroke-width="4"/>
|
||||||
|
<circle cx="50" cy="50" r="6" fill="#c03030" stroke="#300c0c" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 513 B |
7
bin/app/data/icons/items/weapon_upgrade_module.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.5 15.5 68.9 68.9" width="100" height="100">
|
||||||
|
<rect x="24" y="22" width="52" height="56" rx="10" fill="#7a1c1c" stroke="#3a0e0e" stroke-width="3.5"/>
|
||||||
|
<g fill="none" stroke="#ff6a6a" stroke-width="8" stroke-linejoin="round" stroke-linecap="round">
|
||||||
|
<path d="M34 52 L50 36 L66 52"/>
|
||||||
|
<path d="M34 66 L50 50 L66 66"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 389 B |
425
bin/balancing/data/balancing.toml
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
# balancing.toml — canonical arena suite for the combat stats pass.
|
||||||
|
#
|
||||||
|
# Ship counts are chosen so both teams have (near-)equal total threat,
|
||||||
|
# using the verified fitted threat values from tools/threat_report.py:
|
||||||
|
# drone 10.5, frigate 47, destroyer 99, cruiser 233.5,
|
||||||
|
# battlecruiser 354.5, battleship 722.5, dreadnought 1491.5,
|
||||||
|
# carrier 1436.5, glass destroyer (8 small guns) 92, repair drone 17.
|
||||||
|
# Module arrays mirror the ships' default_modules loadouts unless a
|
||||||
|
# doctrine variant is the point of the arena.
|
||||||
|
#
|
||||||
|
# Expectations: mirror matches and equal-threat cross-tier matchups should
|
||||||
|
# be near-draws (power-per-threat rule); the two-to-one arena must be a
|
||||||
|
# decisive win for the larger team. "Cruisers vs carrier" is EXPECTED to be
|
||||||
|
# a loss for the carrier until the drone-launching capability exists — the
|
||||||
|
# hangar is 224 threat of dead weight.
|
||||||
|
|
||||||
|
# --- mirrors (sanity: symmetric outcomes, fight duration in the 30-60 s band) ---
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Mirror: drones 20v20"
|
||||||
|
height_tiles = 10
|
||||||
|
player_buffer_width_tiles = 10
|
||||||
|
contest_zone_width_tiles = 50
|
||||||
|
enemy_buffer_width_tiles = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Alpha"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "drone"
|
||||||
|
count = 20
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 0, y = 0, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Beta"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "drone"
|
||||||
|
count = 20
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 0, y = 0, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Mirror: cruisers 6v6"
|
||||||
|
height_tiles = 10
|
||||||
|
player_buffer_width_tiles = 10
|
||||||
|
contest_zone_width_tiles = 50
|
||||||
|
enemy_buffer_width_tiles = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Alpha"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "cruiser"
|
||||||
|
count = 6
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_m", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "maneuvering_thrusters", x = 1, y = 3, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Beta"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "cruiser"
|
||||||
|
count = 6
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_m", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "maneuvering_thrusters", x = 1, y = 3, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Mirror: battleships 2v2"
|
||||||
|
height_tiles = 10
|
||||||
|
player_buffer_width_tiles = 10
|
||||||
|
contest_zone_width_tiles = 50
|
||||||
|
enemy_buffer_width_tiles = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Alpha"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "battleship"
|
||||||
|
count = 2
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_l", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 1, y = 3, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 3, y = 3, rotation = "east"},
|
||||||
|
{type = "weapon_stabilizer", x = 4, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 4, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 0, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Beta"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "battleship"
|
||||||
|
count = 2
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_l", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 1, y = 3, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 3, y = 3, rotation = "east"},
|
||||||
|
{type = "weapon_stabilizer", x = 4, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 4, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 0, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- equal-threat cross-tier matchups (power-per-threat: expect near-draws) ---
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Drone swarm vs cruisers (462 vs 467)"
|
||||||
|
height_tiles = 10
|
||||||
|
player_buffer_width_tiles = 10
|
||||||
|
contest_zone_width_tiles = 50
|
||||||
|
enemy_buffer_width_tiles = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Swarm"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "drone"
|
||||||
|
count = 44
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 0, y = 0, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Cruisers"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "cruiser"
|
||||||
|
count = 2
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_m", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "maneuvering_thrusters", x = 1, y = 3, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Frigates vs battleship (705 vs 723)"
|
||||||
|
height_tiles = 10
|
||||||
|
player_buffer_width_tiles = 10
|
||||||
|
contest_zone_width_tiles = 50
|
||||||
|
enemy_buffer_width_tiles = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Frigates"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "frigate"
|
||||||
|
count = 15
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "maneuvering_thrusters", x = 0, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Battleship"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "battleship"
|
||||||
|
count = 1
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_l", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 1, y = 3, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 3, y = 3, rotation = "east"},
|
||||||
|
{type = "weapon_stabilizer", x = 4, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 4, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 0, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Destroyers vs dreadnought (1485 vs 1492)"
|
||||||
|
height_tiles = 10
|
||||||
|
player_buffer_width_tiles = 10
|
||||||
|
contest_zone_width_tiles = 50
|
||||||
|
enemy_buffer_width_tiles = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Destroyers"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "destroyer"
|
||||||
|
count = 15
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 0, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 2, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 4, y = 0, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "sensor_booster", x = 3, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Dreadnought"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "dreadnought"
|
||||||
|
count = 1
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_l", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_l", x = 4, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_l", x = 8, y = 1, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 3, y = 0, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 5, y = 0, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 2, y = 4, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 7, y = 4, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 7, y = 0, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Cruisers vs carrier (1401 vs 1437, carrier expected to lose)"
|
||||||
|
height_tiles = 10
|
||||||
|
player_buffer_width_tiles = 10
|
||||||
|
contest_zone_width_tiles = 50
|
||||||
|
enemy_buffer_width_tiles = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Cruisers"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "cruiser"
|
||||||
|
count = 6
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_m", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "maneuvering_thrusters", x = 1, y = 3, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Carrier"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "carrier"
|
||||||
|
count = 1
|
||||||
|
modules = [
|
||||||
|
{type = "drone_hangar", x = 2, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 3, y = 2, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 6, y = 2, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 8, y = 1, rotation = "east"},
|
||||||
|
{type = "sensor_booster", x = 3, y = 4, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Mixed mid vs battlecruisers (1394 vs 1418)"
|
||||||
|
height_tiles = 10
|
||||||
|
player_buffer_width_tiles = 10
|
||||||
|
contest_zone_width_tiles = 50
|
||||||
|
enemy_buffer_width_tiles = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Mixed"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "destroyer"
|
||||||
|
count = 7
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 0, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 2, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 4, y = 0, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "sensor_booster", x = 3, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "cruiser"
|
||||||
|
count = 3
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_m", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "maneuvering_thrusters", x = 1, y = 3, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Battlecruisers"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "battlecruiser"
|
||||||
|
count = 4
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_m", x = 0, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 4, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_m", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 2, y = 3, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 1, y = 2, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 4, y = 2, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- asymmetric checks ---
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Two to one (must be decisive)"
|
||||||
|
height_tiles = 10
|
||||||
|
player_buffer_width_tiles = 10
|
||||||
|
contest_zone_width_tiles = 50
|
||||||
|
enemy_buffer_width_tiles = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Six"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "frigate"
|
||||||
|
count = 6
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "maneuvering_thrusters", x = 0, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Three"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "frigate"
|
||||||
|
count = 3
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "maneuvering_thrusters", x = 0, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Armored vs glass destroyers (1188 vs 1196)"
|
||||||
|
height_tiles = 10
|
||||||
|
player_buffer_width_tiles = 10
|
||||||
|
contest_zone_width_tiles = 50
|
||||||
|
enemy_buffer_width_tiles = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Armored"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "destroyer"
|
||||||
|
count = 12
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 0, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 2, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 4, y = 0, rotation = "east"},
|
||||||
|
{type = "armor_plates", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "sensor_booster", x = 3, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Glass"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "destroyer"
|
||||||
|
count = 13
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 0, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 2, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 4, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 0, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 1, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 3, y = 1, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 4, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Repair escort vs raw numbers (444 vs 444)"
|
||||||
|
height_tiles = 10
|
||||||
|
player_buffer_width_tiles = 10
|
||||||
|
contest_zone_width_tiles = 50
|
||||||
|
enemy_buffer_width_tiles = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Escorted"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "frigate"
|
||||||
|
count = 8
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "maneuvering_thrusters", x = 0, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "drone"
|
||||||
|
count = 4
|
||||||
|
modules = [
|
||||||
|
{type = "repair_tool", x = 0, y = 0, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Raw"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "frigate"
|
||||||
|
count = 9
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 1, y = 0, rotation = "east"},
|
||||||
|
{type = "railgun_s", x = 2, y = 1, rotation = "east"},
|
||||||
|
{type = "maneuvering_thrusters", x = 0, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "drone"
|
||||||
|
count = 2
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 0, y = 0, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Station assault (2 stations + 105 vs 315)"
|
||||||
|
height_tiles = 10
|
||||||
|
player_buffer_width_tiles = 15
|
||||||
|
contest_zone_width_tiles = 40
|
||||||
|
enemy_buffer_width_tiles = 15
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Fortified"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "drone"
|
||||||
|
count = 10
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 0, y = 0, rotation = "east"},
|
||||||
|
]
|
||||||
|
[[arena.team.station]]
|
||||||
|
type = "player_station"
|
||||||
|
x = 8
|
||||||
|
y = 15
|
||||||
|
level = 1
|
||||||
|
[[arena.team.station]]
|
||||||
|
type = "player_station"
|
||||||
|
x = 8
|
||||||
|
y = 45
|
||||||
|
level = 1
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Swarm"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "drone"
|
||||||
|
count = 30
|
||||||
|
modules = [
|
||||||
|
{type = "railgun_s", x = 0, y = 0, rotation = "east"},
|
||||||
|
]
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
[[recipe]]
|
|
||||||
id = "mine_iron_ore"
|
|
||||||
building = "miner"
|
|
||||||
inputs = []
|
|
||||||
outputs = [{item = "iron_ore", amount = 1}]
|
|
||||||
duration_seconds = 1.0
|
|
||||||
|
|
||||||
[[recipe]]
|
|
||||||
id = "mine_copper_ore"
|
|
||||||
building = "miner"
|
|
||||||
inputs = []
|
|
||||||
outputs = [{item = "copper_ore", amount = 1}]
|
|
||||||
duration_seconds = 1.5
|
|
||||||
|
|
||||||
[[recipe]]
|
|
||||||
id = "iron_ingot"
|
|
||||||
building = "smelter"
|
|
||||||
inputs = [{item = "iron_ore", amount = 2}]
|
|
||||||
outputs = [{item = "iron_ingot", amount = 1}]
|
|
||||||
duration_seconds = 2.0
|
|
||||||
|
|
||||||
[[recipe]]
|
|
||||||
id = "copper_ingot"
|
|
||||||
building = "smelter"
|
|
||||||
inputs = [{item = "copper_ore", amount = 2}]
|
|
||||||
outputs = [{item = "copper_ingot", amount = 1}]
|
|
||||||
duration_seconds = 2.5
|
|
||||||
|
|
||||||
[[recipe]]
|
|
||||||
id = "circuit_board"
|
|
||||||
building = "assembler"
|
|
||||||
inputs = [{item = "iron_ingot", amount = 3}, {item = "copper_ingot", amount = 2}]
|
|
||||||
outputs = [{item = "circuit_board", amount = 1}]
|
|
||||||
duration_seconds = 5.0
|
|
||||||
|
|
||||||
[[recipe]]
|
|
||||||
id = "building_blocks"
|
|
||||||
building = "assembler"
|
|
||||||
inputs = [{item = "iron_ingot", amount = 4}]
|
|
||||||
outputs = [{item = "building_block", amount = 10}]
|
|
||||||
duration_seconds = 4.0
|
|
||||||
|
|
||||||
[[recipe]]
|
|
||||||
id = "reprocessing_cycle"
|
|
||||||
building = "reprocessing_plant"
|
|
||||||
inputs = [{item = "scrap", amount = 5}]
|
|
||||||
duration_seconds = 3.0
|
|
||||||
|
|
||||||
[[recipe.outputs]]
|
|
||||||
item = "iron_ingot"
|
|
||||||
amount = 2
|
|
||||||
probability = 0.6
|
|
||||||
|
|
||||||
[[recipe.outputs]]
|
|
||||||
item = "circuit_board"
|
|
||||||
amount = 1
|
|
||||||
probability = 0.3
|
|
||||||
|
|
||||||
[[recipe.outputs]]
|
|
||||||
item = "advanced_alloy"
|
|
||||||
amount = 1
|
|
||||||
probability = 0.1
|
|
||||||
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
[[ship]]
|
|
||||||
id = "interceptor"
|
|
||||||
available_from_start = true
|
|
||||||
|
|
||||||
[ship.schematic]
|
|
||||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
|
||||||
player_production_level = 3
|
|
||||||
production_time_seconds = 10
|
|
||||||
|
|
||||||
[ship.threat]
|
|
||||||
cost_formula = "5 + 1*x"
|
|
||||||
|
|
||||||
[ship.health]
|
|
||||||
hp_formula = "40 + 5*x"
|
|
||||||
|
|
||||||
[ship.movement]
|
|
||||||
speed_formula = "200 + 5*x"
|
|
||||||
|
|
||||||
[ship.combat]
|
|
||||||
damage_formula = "10 + 2*x"
|
|
||||||
attack_range_formula = "150"
|
|
||||||
attack_rate_formula = "2.0"
|
|
||||||
|
|
||||||
[ship.loot]
|
|
||||||
scrap_drop = 2
|
|
||||||
|
|
||||||
|
|
||||||
[[ship]]
|
|
||||||
id = "destroyer"
|
|
||||||
available_from_start = true
|
|
||||||
|
|
||||||
[ship.schematic]
|
|
||||||
materials = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 2}]
|
|
||||||
player_production_level = 5
|
|
||||||
production_time_seconds = 20
|
|
||||||
|
|
||||||
[ship.threat]
|
|
||||||
cost_formula = "10 + 2*x"
|
|
||||||
|
|
||||||
[ship.health]
|
|
||||||
hp_formula = "120 + 15*x"
|
|
||||||
|
|
||||||
[ship.movement]
|
|
||||||
speed_formula = "120"
|
|
||||||
|
|
||||||
[ship.combat]
|
|
||||||
damage_formula = "12 + 2*x"
|
|
||||||
attack_range_formula = "250"
|
|
||||||
attack_rate_formula = "1.0"
|
|
||||||
|
|
||||||
[ship.loot]
|
|
||||||
scrap_drop = 4
|
|
||||||
|
|
||||||
|
|
||||||
[[ship]]
|
|
||||||
id = "salvage_ship"
|
|
||||||
available_from_start = true
|
|
||||||
|
|
||||||
[ship.schematic]
|
|
||||||
materials = [{item = "iron_ingot", amount = 4}]
|
|
||||||
player_production_level = 3
|
|
||||||
production_time_seconds = 10
|
|
||||||
|
|
||||||
[ship.threat]
|
|
||||||
cost_formula = "0"
|
|
||||||
|
|
||||||
[ship.health]
|
|
||||||
hp_formula = "40 + 4*x"
|
|
||||||
|
|
||||||
[ship.movement]
|
|
||||||
speed_formula = "110"
|
|
||||||
|
|
||||||
[ship.salvage]
|
|
||||||
collection_range = 50
|
|
||||||
cargo_capacity = 10
|
|
||||||
|
|
||||||
[ship.loot]
|
|
||||||
scrap_drop = 2
|
|
||||||
|
|
||||||
|
|
||||||
[[ship]]
|
|
||||||
id = "repair_ship"
|
|
||||||
available_from_start = false
|
|
||||||
|
|
||||||
[ship.schematic]
|
|
||||||
materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount = 2}]
|
|
||||||
player_production_level = 3
|
|
||||||
production_time_seconds = 15
|
|
||||||
|
|
||||||
[ship.threat]
|
|
||||||
cost_formula = "0"
|
|
||||||
|
|
||||||
[ship.health]
|
|
||||||
hp_formula = "60 + 5*x"
|
|
||||||
|
|
||||||
[ship.movement]
|
|
||||||
speed_formula = "130"
|
|
||||||
|
|
||||||
[ship.repair]
|
|
||||||
repair_rate_formula = "5 + x"
|
|
||||||
repair_range_formula = "80"
|
|
||||||
|
|
||||||
[ship.loot]
|
|
||||||
scrap_drop = 2
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[hq]
|
|
||||||
surface_mask = [
|
|
||||||
"AAA",
|
|
||||||
"AAA",
|
|
||||||
"AAA",
|
|
||||||
]
|
|
||||||
hp_formula = "1000"
|
|
||||||
|
|
||||||
[player_station]
|
|
||||||
surface_mask = [
|
|
||||||
"SS",
|
|
||||||
"SS",
|
|
||||||
]
|
|
||||||
level = 5
|
|
||||||
hp_formula = "300 + 40*x"
|
|
||||||
damage_formula = "5 + 4*x"
|
|
||||||
range_formula = "300 + 20*x"
|
|
||||||
fire_rate_formula = "0.5 + 0.2*x"
|
|
||||||
scrap_drop_formula = "x"
|
|
||||||
|
|
||||||
[enemy_station]
|
|
||||||
surface_mask = [
|
|
||||||
"SS",
|
|
||||||
"SS",
|
|
||||||
]
|
|
||||||
hp_formula = "300 + 150*x"
|
|
||||||
damage_formula = "20 + 10*x"
|
|
||||||
range_formula = "350 + 20*x"
|
|
||||||
fire_rate_formula = "1.0 + 0.2*x"
|
|
||||||
scrap_drop_formula = "10 + 5*x"
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
# visuals.toml
|
|
||||||
#
|
|
||||||
# Rendering-only configuration, loaded once at startup by the UI.
|
|
||||||
# The simulation does not read this file.
|
|
||||||
#
|
|
||||||
# Every BuildingType, ItemType, ship role, and station type must have an
|
|
||||||
# entry here; missing or malformed entries abort startup (see architecture.md
|
|
||||||
# "Visual Parameters").
|
|
||||||
#
|
|
||||||
# Colors are "#rrggbb" or "#rrggbbaa" (alpha optional, last byte).
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Tile backgrounds
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
[tiles.asteroid]
|
|
||||||
fill = "#4a4038"
|
|
||||||
|
|
||||||
[tiles.space]
|
|
||||||
fill = "#0a0a15"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Buildings
|
|
||||||
#
|
|
||||||
# One section per BuildingType enum value. `fill` colors every footprint tile
|
|
||||||
# of the building; `outline` is drawn around the building's bounding box;
|
|
||||||
# `glyph` is a short label drawn centered on the building.
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
[buildings.hq]
|
|
||||||
fill = "#2e5fb8"
|
|
||||||
outline = "#ffffff"
|
|
||||||
glyph = "HQ"
|
|
||||||
|
|
||||||
[buildings.miner]
|
|
||||||
fill = "#6b4a2c"
|
|
||||||
outline = "#ffffff"
|
|
||||||
glyph = "M"
|
|
||||||
|
|
||||||
[buildings.smelter]
|
|
||||||
fill = "#b85a1e"
|
|
||||||
outline = "#ffffff"
|
|
||||||
glyph = "Sm"
|
|
||||||
|
|
||||||
[buildings.assembler]
|
|
||||||
fill = "#3a6fa8"
|
|
||||||
outline = "#ffffff"
|
|
||||||
glyph = "A"
|
|
||||||
|
|
||||||
[buildings.reprocessing_plant]
|
|
||||||
fill = "#6a3a8a"
|
|
||||||
outline = "#ffffff"
|
|
||||||
glyph = "R"
|
|
||||||
|
|
||||||
[buildings.shipyard]
|
|
||||||
fill = "#385870"
|
|
||||||
outline = "#ffffff"
|
|
||||||
glyph = "Y"
|
|
||||||
|
|
||||||
[buildings.salvage_bay]
|
|
||||||
fill = "#b8a23a"
|
|
||||||
outline = "#ffffff"
|
|
||||||
glyph = "Sb"
|
|
||||||
|
|
||||||
[buildings.belt]
|
|
||||||
fill = "#5a5a5a"
|
|
||||||
outline = "#7a7a7a"
|
|
||||||
glyph = ""
|
|
||||||
|
|
||||||
[buildings.splitter]
|
|
||||||
fill = "#7a7a5a"
|
|
||||||
outline = "#9a9a7a"
|
|
||||||
glyph = ""
|
|
||||||
|
|
||||||
[buildings.tunnel_entry]
|
|
||||||
fill = "#4a6a5a"
|
|
||||||
outline = "#8aaa9a"
|
|
||||||
glyph = "Te"
|
|
||||||
|
|
||||||
[buildings.tunnel_exit]
|
|
||||||
fill = "#5a6a4a"
|
|
||||||
outline = "#9aaa8a"
|
|
||||||
glyph = "Tx"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Stations
|
|
||||||
#
|
|
||||||
# Player and enemy defence stations have their own entries rather than going
|
|
||||||
# through [buildings.*] so the two sides can be color-coded at a glance.
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
[stations.player]
|
|
||||||
fill = "#2e5fb8"
|
|
||||||
outline = "#ffffff"
|
|
||||||
glyph = "P"
|
|
||||||
|
|
||||||
[stations.enemy]
|
|
||||||
fill = "#b82e2e"
|
|
||||||
outline = "#ffffff"
|
|
||||||
glyph = "E"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Items
|
|
||||||
#
|
|
||||||
# Items on belts are 10x10 squares. `fill` is the square color; `outline` is
|
|
||||||
# drawn around it. One section per ItemType.
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
[items.iron_ore]
|
|
||||||
fill = "#8a5a4a"
|
|
||||||
outline = "#201010"
|
|
||||||
|
|
||||||
[items.copper_ore]
|
|
||||||
fill = "#c47a3a"
|
|
||||||
outline = "#3a1a0a"
|
|
||||||
|
|
||||||
[items.iron_ingot]
|
|
||||||
fill = "#b0b0b8"
|
|
||||||
outline = "#202028"
|
|
||||||
|
|
||||||
[items.copper_ingot]
|
|
||||||
fill = "#d48a4a"
|
|
||||||
outline = "#402010"
|
|
||||||
|
|
||||||
[items.circuit_board]
|
|
||||||
fill = "#2ea35a"
|
|
||||||
outline = "#0a2a14"
|
|
||||||
|
|
||||||
[items.advanced_alloy]
|
|
||||||
fill = "#a06acc"
|
|
||||||
outline = "#201030"
|
|
||||||
|
|
||||||
[items.building_block]
|
|
||||||
fill = "#c8b070"
|
|
||||||
outline = "#302810"
|
|
||||||
|
|
||||||
[items.scrap]
|
|
||||||
fill = "#7a7268"
|
|
||||||
outline = "#201a14"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Ships
|
|
||||||
#
|
|
||||||
# Ships are drawn as oriented triangles/arrows. Color is keyed to role, not
|
|
||||||
# schematic (architecture.md, "Layer Order").
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
[ships.player_combat]
|
|
||||||
fill = "#3366ff"
|
|
||||||
outline = "#ffffff"
|
|
||||||
|
|
||||||
[ships.salvage]
|
|
||||||
fill = "#33cc66"
|
|
||||||
outline = "#ffffff"
|
|
||||||
|
|
||||||
[ships.repair]
|
|
||||||
fill = "#66ccff"
|
|
||||||
outline = "#ffffff"
|
|
||||||
|
|
||||||
[ships.enemy]
|
|
||||||
fill = "#cc3333"
|
|
||||||
outline = "#ffffff"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Laser beams (REQ-SHP-FIRING-BEAM)
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
[beams]
|
|
||||||
color = "#ff6600"
|
|
||||||
width_px = 2
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Build / demolish / selection overlays
|
|
||||||
#
|
|
||||||
# All overlay colors carry an alpha channel so they composite over the
|
|
||||||
# underlying scene.
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
[overlays]
|
|
||||||
ghost_valid = "#ffffff44" # builder-mode ghost, placement allowed (REQ-BLD-GHOST)
|
|
||||||
ghost_invalid = "#ff000044" # builder-mode ghost, placement invalid (REQ-BLD-PLACE-VALID)
|
|
||||||
demolish_tint = "#ff000033" # demolish-mode hover tint
|
|
||||||
selection_rect = "#00ff00" # box-drag selection rectangle (REQ-UI-MULTI-SELECT)
|
|
||||||
tile_highlight = "#ffffff22" # tile under cursor
|
|
||||||
selected_outline = "#ffff00" # outline drawn around currently-selected building(s)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Schematic-drop toasts (REQ-UI-SCHEMATIC-TOAST)
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
[toast]
|
|
||||||
bg = "#000000cc"
|
|
||||||
fg = "#ffffff"
|
|
||||||
font_size = 14
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
[world]
|
|
||||||
height_tiles = 30
|
|
||||||
refund_percentage = 75
|
|
||||||
starting_building_blocks = 1000
|
|
||||||
scrap_despawn_seconds = 30
|
|
||||||
belt_speed_tiles_per_second = 2
|
|
||||||
tunnel_max_distance = 10
|
|
||||||
departure_interval_seconds = 20
|
|
||||||
|
|
||||||
[regions]
|
|
||||||
asteroid_width = 40
|
|
||||||
player_buffer_width = 20
|
|
||||||
contest_zone_width = 60
|
|
||||||
enemy_buffer_width = 20
|
|
||||||
|
|
||||||
[expansion]
|
|
||||||
columns_per_expansion = 10
|
|
||||||
cost_building_blocks = 200
|
|
||||||
|
|
||||||
[push]
|
|
||||||
push_expand_columns = 10
|
|
||||||
scaling_factor = 1.2
|
|
||||||
|
|
||||||
[waves]
|
|
||||||
threat_rate_formula = "0.1*x - 60"
|
|
||||||
ship_level_formula = "1"
|
|
||||||
gap_min_seconds = 15
|
|
||||||
gap_max_seconds = 45
|
|
||||||
spawn_duration_seconds = 10
|
|
||||||
@@ -83,6 +83,8 @@ id = "salvage_bay"
|
|||||||
cost = 25
|
cost = 25
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 15
|
construction_time_seconds = 15
|
||||||
|
output_buffer_capacity = 20
|
||||||
|
tooltip = "Drop-off point for salvage ships."
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"SAA",
|
"SAA",
|
||||||
"SAA>",
|
"SAA>",
|
||||||
119
bin/test/data/config/modules.toml
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
[[module]]
|
||||||
|
id = "armor_plate"
|
||||||
|
tooltip = "Adds a large flat bonus to hit points."
|
||||||
|
surface_mask = ["OO"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
|
production_time_seconds = 3
|
||||||
|
fill_color = "#808080"
|
||||||
|
glyph = "A"
|
||||||
|
|
||||||
|
[module.health]
|
||||||
|
multiplied_hp = 1.5
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "sensor_booster"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "circuit_board", amount = 1}]
|
||||||
|
production_time_seconds = 2
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "S"
|
||||||
|
|
||||||
|
[module.sensor]
|
||||||
|
added_sensor_range_m = 100
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_upgrade"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
||||||
|
production_time_seconds = 4
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "W"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
multiplied_damage = 1.2
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "laser_cannon"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
|
production_time_seconds = 5
|
||||||
|
fill_color = "#FF8040"
|
||||||
|
glyph = "L"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
damage = 2
|
||||||
|
attack_range_m = 50
|
||||||
|
attack_rate_hz = 2.0
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "salvager"
|
||||||
|
surface_mask = ["OO"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
|
production_time_seconds = 5
|
||||||
|
fill_color = "#AACC44"
|
||||||
|
glyph = "Sv"
|
||||||
|
|
||||||
|
[module.salvage]
|
||||||
|
collection_range_m = 500
|
||||||
|
cargo_capacity = 10
|
||||||
|
collection_rate_hz = 0.5
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "repair_tool"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "circuit_board", amount = 2}]
|
||||||
|
production_time_seconds = 5
|
||||||
|
fill_color = "#66CCFF"
|
||||||
|
glyph = "Rp"
|
||||||
|
|
||||||
|
[module.repair]
|
||||||
|
repair_rate_hz = 1
|
||||||
|
repair_amount_hp = 6
|
||||||
|
repair_range_m = 800
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_primer"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
|
production_time_seconds = 4
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "Wp"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
multiplied_attack_rate_hz = 1.2
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_stabilizer"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
|
production_time_seconds = 4
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "Ws"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
multiplied_attack_range_m = 1.5
|
||||||
|
multiplied_attack_rate_hz = 0.8
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "afterburner"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
|
production_time_seconds = 2
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "Ab"
|
||||||
|
|
||||||
|
[module.movement]
|
||||||
|
multiplied_speed_mps = 1.6
|
||||||
|
added_main_acceleration_mpss = 60
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "maneuvering_thrusters"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
|
production_time_seconds = 2
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "Mt"
|
||||||
|
|
||||||
|
[module.movement]
|
||||||
|
multiplied_speed_mps = 1.2
|
||||||
|
added_maneuvering_acceleration_mpss = 10
|
||||||
145
bin/test/data/config/recipes.toml
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
[[recipe]]
|
||||||
|
id = "mine_iron_ore"
|
||||||
|
building = "miner"
|
||||||
|
inputs = []
|
||||||
|
outputs = [{item = "iron_ore", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "mine_copper_ore"
|
||||||
|
building = "miner"
|
||||||
|
inputs = []
|
||||||
|
outputs = [{item = "copper_ore", amount = 1}]
|
||||||
|
duration_seconds = 1.5
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "iron_ingot"
|
||||||
|
building = "smelter"
|
||||||
|
inputs = [{item = "iron_ore", amount = 2}]
|
||||||
|
outputs = [{item = "iron_ingot", amount = 1}]
|
||||||
|
duration_seconds = 2.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "copper_ingot"
|
||||||
|
building = "smelter"
|
||||||
|
inputs = [{item = "copper_ore", amount = 2}]
|
||||||
|
outputs = [{item = "copper_ingot", amount = 1}]
|
||||||
|
duration_seconds = 2.5
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "circuit_board"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ingot", amount = 3}, {item = "copper_ingot", amount = 2}]
|
||||||
|
outputs = [{item = "circuit_board", amount = 1}]
|
||||||
|
duration_seconds = 5.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "building_blocks"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ingot", amount = 4}]
|
||||||
|
outputs = [{item = "building_block", amount = 10}]
|
||||||
|
duration_seconds = 4.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "premium_circuit"
|
||||||
|
building = "assembler"
|
||||||
|
unlocked_at_start = true
|
||||||
|
inputs = [{item = "circuit_board", amount = 1}]
|
||||||
|
outputs = [{item = "premium_circuit", amount = 1}]
|
||||||
|
duration_seconds = 8.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "quick_circuit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "copper_ingot", amount = 3}]
|
||||||
|
outputs = [{item = "circuit_board", amount = 1}]
|
||||||
|
duration_seconds = 3.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "advanced_circuit"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ingot", amount = 5}]
|
||||||
|
outputs = [{item = "circuit_board", amount = 1}]
|
||||||
|
duration_seconds = 6.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "exotic_alloy"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "exotic_ore", amount = 2}]
|
||||||
|
outputs = [{item = "exotic_alloy", amount = 1}]
|
||||||
|
duration_seconds = 10.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "reprocessing_cycle"
|
||||||
|
building = "reprocessing_plant"
|
||||||
|
inputs = [{item = "scrap", amount = 5}]
|
||||||
|
duration_seconds = 3.0
|
||||||
|
|
||||||
|
[[recipe.output_group]]
|
||||||
|
probability = 0.6
|
||||||
|
items = [{item = "iron_ingot", amount = 2}]
|
||||||
|
|
||||||
|
[[recipe.output_group]]
|
||||||
|
probability = 0.3
|
||||||
|
items = [{item = "circuit_board", amount = 1}]
|
||||||
|
|
||||||
|
[[recipe.output_group]]
|
||||||
|
probability = 0.1
|
||||||
|
items = [{item = "advanced_alloy", amount = 1}]
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Extra recipes for ThreatCostCalculator unit tests (fixes 6-9)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Fix 6: scrap-consuming smelter recipe for iron_ingot. Because iron_ingot
|
||||||
|
# already has a scrap-free smelter recipe above, this recipe must be excluded
|
||||||
|
# from iron_ingot's threat computation.
|
||||||
|
[[recipe]]
|
||||||
|
id = "scrap_iron"
|
||||||
|
building = "smelter"
|
||||||
|
inputs = [{item = "scrap", amount = 1}]
|
||||||
|
outputs = [{item = "iron_ingot", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
|
||||||
|
# Fix 7: a recipe that produces 2 items per cycle. Per-unit threat must
|
||||||
|
# divide by the output amount.
|
||||||
|
# dual_wire: (duration=3.0 + iron_ore(1.0)*1) / 2 = 4.0 / 2 = 2.0 per unit.
|
||||||
|
[[recipe]]
|
||||||
|
id = "dual_wire"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ore", amount = 1}]
|
||||||
|
outputs = [{item = "dual_wire", amount = 2}]
|
||||||
|
duration_seconds = 3.0
|
||||||
|
|
||||||
|
# Fix 8: an item downstream of a reprocessing-only item (advanced_alloy).
|
||||||
|
# advanced_alloy is resolved only by the reprocessing pass; downstream_product
|
||||||
|
# can only resolve in a non-reprocessing pass that runs AFTER the reprocessing
|
||||||
|
# pass, requiring proper fixpoint iteration.
|
||||||
|
# downstream_product: 2.0 + advanced_alloy(80.0)*1 = 82.0
|
||||||
|
[[recipe]]
|
||||||
|
id = "downstream_product"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "advanced_alloy", amount = 1}]
|
||||||
|
outputs = [{item = "downstream_product", amount = 1}]
|
||||||
|
duration_seconds = 2.0
|
||||||
|
|
||||||
|
# Fix 9: two recipes producing the same staggered_item. The cheap recipe
|
||||||
|
# resolves before circuit_board is known; the expensive one requires
|
||||||
|
# circuit_board. The item must be committed only once BOTH are computable,
|
||||||
|
# so the result is max(cheap, expensive).
|
||||||
|
# staggered_item_cheap: 1.0 + iron_ore(1.0)*1 = 2.0 (resolves early)
|
||||||
|
# staggered_item_expensive: 1.0 + circuit_board(28.0)*1 = 29.0 (resolves later)
|
||||||
|
# expected: max = 29.0
|
||||||
|
[[recipe]]
|
||||||
|
id = "staggered_item_cheap"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ore", amount = 1}]
|
||||||
|
outputs = [{item = "staggered_item", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "staggered_item_expensive"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "circuit_board", amount = 1}]
|
||||||
|
outputs = [{item = "staggered_item", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
88
bin/test/data/config/ships.toml
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
[[ship]]
|
||||||
|
id = "interceptor"
|
||||||
|
layout = ["XOX", "OOO", "XOX"]
|
||||||
|
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||||
|
production_time_seconds = 10
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp = 45
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_mps = 2050
|
||||||
|
main_acceleration_mpss = 1000000
|
||||||
|
maneuvering_acceleration_mpss = 1000000
|
||||||
|
angular_acceleration_radpss = 100000
|
||||||
|
max_rotation_speed_radps = 100000
|
||||||
|
|
||||||
|
[ship.sensor]
|
||||||
|
sensor_range_m = 2000
|
||||||
|
|
||||||
|
|
||||||
|
[[ship]]
|
||||||
|
id = "destroyer"
|
||||||
|
layout = ["XOOX", "OOOO", "XOOX"]
|
||||||
|
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 2}]
|
||||||
|
production_time_seconds = 20
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp = 135
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_mps = 1200
|
||||||
|
main_acceleration_mpss = 1000000
|
||||||
|
maneuvering_acceleration_mpss = 1000000
|
||||||
|
angular_acceleration_radpss = 100000
|
||||||
|
max_rotation_speed_radps = 100000
|
||||||
|
|
||||||
|
[ship.sensor]
|
||||||
|
sensor_range_m = 3000
|
||||||
|
|
||||||
|
|
||||||
|
[[ship]]
|
||||||
|
id = "salvage_ship"
|
||||||
|
layout = ["OOO", "OOO"]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "iron_ingot", amount = 4}]
|
||||||
|
production_time_seconds = 10
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp = 44
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_mps = 1100
|
||||||
|
main_acceleration_mpss = 1000000
|
||||||
|
maneuvering_acceleration_mpss = 1000000
|
||||||
|
angular_acceleration_radpss = 100000
|
||||||
|
max_rotation_speed_radps = 100000
|
||||||
|
|
||||||
|
[ship.sensor]
|
||||||
|
sensor_range_m = 2500
|
||||||
|
|
||||||
|
|
||||||
|
[[ship]]
|
||||||
|
id = "repair_ship"
|
||||||
|
layout = ["XOX", "OOO", "XOX"]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount = 2}]
|
||||||
|
production_time_seconds = 15
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp = 65
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_mps = 1300
|
||||||
|
main_acceleration_mpss = 1000000
|
||||||
|
maneuvering_acceleration_mpss = 1000000
|
||||||
|
angular_acceleration_radpss = 100000
|
||||||
|
max_rotation_speed_radps = 100000
|
||||||
|
|
||||||
|
[ship.sensor]
|
||||||
|
sensor_range_m = 2500
|
||||||
@@ -14,8 +14,8 @@ surface_mask = [
|
|||||||
level = 5
|
level = 5
|
||||||
hp_formula = "300 + 40*x"
|
hp_formula = "300 + 40*x"
|
||||||
damage_formula = "5 + 4*x"
|
damage_formula = "5 + 4*x"
|
||||||
range_formula = "300 + 20*x"
|
range_m_formula = "3000 + 200*x"
|
||||||
fire_rate_formula = "0.5 + 0.2*x"
|
fire_rate_hz_formula = "0.5 + 0.2*x"
|
||||||
scrap_drop_formula = "x"
|
scrap_drop_formula = "x"
|
||||||
|
|
||||||
[enemy_station]
|
[enemy_station]
|
||||||
@@ -25,6 +25,6 @@ surface_mask = [
|
|||||||
]
|
]
|
||||||
hp_formula = "300 + 150*x"
|
hp_formula = "300 + 150*x"
|
||||||
damage_formula = "20 + 10*x"
|
damage_formula = "20 + 10*x"
|
||||||
range_formula = "350 + 20*x"
|
range_m_formula = "3500 + 200*x"
|
||||||
fire_rate_formula = "1.0 + 0.2*x"
|
fire_rate_hz_formula = "1.0 + 0.2*x"
|
||||||
scrap_drop_formula = "10 + 5*x"
|
scrap_drop_formula = "10 + 5*x"
|
||||||
21
bin/test/data/config/unlocks.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Unlock groups for the test config (REQ-LOCK-EXPLICIT). Mirrors the previous
|
||||||
|
# per-item gating: repair_ship, quick_circuit, advanced_circuit start locked and
|
||||||
|
# are awarded via defence station drops. exotic_alloy is intentionally NOT here:
|
||||||
|
# it stays implicitly gated (its output/inputs are unreachable), so it is never
|
||||||
|
# unlocked. premium_circuit uses unlocked_at_start in recipes.toml. Everything
|
||||||
|
# else (interceptor, destroyer, salvage_ship, all modules) starts unlocked.
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "repair_ship"
|
||||||
|
station_level = 0
|
||||||
|
ships = ["repair_ship"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "quick_circuit"
|
||||||
|
station_level = 0
|
||||||
|
recipes = ["quick_circuit"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "advanced_circuit"
|
||||||
|
station_level = 1
|
||||||
|
recipes = ["advanced_circuit"]
|
||||||
52
bin/test/data/config/world.toml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
[world]
|
||||||
|
height_tiles = 60
|
||||||
|
refund_percentage = 75
|
||||||
|
deconstruction_time_seconds = 0.1
|
||||||
|
starting_building_blocks = 100
|
||||||
|
debris_despawn_seconds = 30
|
||||||
|
scrap_per_threat = 1.0
|
||||||
|
tile_size_m = 10
|
||||||
|
belt_speed_mps = 20
|
||||||
|
tunnel_max_distance_tiles = 10
|
||||||
|
departure_interval_seconds = 20
|
||||||
|
orbit_factor = 0.8
|
||||||
|
rally_orbit_radius_tiles = 5.0
|
||||||
|
artifact_tooltip = "Choose the artifact reward when destroying enemy stations; collect enough to win."
|
||||||
|
|
||||||
|
[regions]
|
||||||
|
asteroid_width_tiles = 40
|
||||||
|
player_buffer_width_tiles = 10
|
||||||
|
contest_zone_width_tiles = 30
|
||||||
|
enemy_buffer_width_tiles = 15
|
||||||
|
|
||||||
|
[scroll]
|
||||||
|
pan_speed_slow_tiles_per_second = 8.0
|
||||||
|
pan_speed_fast_tiles_per_second = 24.0
|
||||||
|
pan_ramp_band_width_tiles = 16
|
||||||
|
|
||||||
|
[expansion]
|
||||||
|
columns_per_expansion_tiles = 10
|
||||||
|
cost_building_blocks_formula = "400 * 2^x"
|
||||||
|
|
||||||
|
[push]
|
||||||
|
push_expand_columns_tiles = 20
|
||||||
|
boss_advance_seconds = 60
|
||||||
|
|
||||||
|
[targeting]
|
||||||
|
target_score_formula = "1 / (1 + x)" # x = distance / max weapon range; higher = better, clamped to >=0
|
||||||
|
overclaim_penalty_formula = "max(0.5, 1 - 0.1*x)" # x = competing claim count; multiplies score, clamped to [0,1]
|
||||||
|
target_hysteresis = 0.10 # keep current target unless a challenger beats it by >10%
|
||||||
|
|
||||||
|
[artifacts]
|
||||||
|
artifact_chance_formula = "0.05 * x" # 5% chance per station level
|
||||||
|
artifact_win_count = 3
|
||||||
|
|
||||||
|
[waves]
|
||||||
|
threat_rate_formula = "x"
|
||||||
|
gap_min_seconds = 15
|
||||||
|
gap_max_seconds = 45
|
||||||
|
spawn_duration_seconds = 10
|
||||||
|
boss_countdown_seconds = 300
|
||||||
|
boss_threat_duration_seconds = 60
|
||||||
|
boss_quiet_before_seconds = 60
|
||||||
|
boss_quiet_after_seconds = 60
|
||||||
60
cmake/version.cmake
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# simple check for a git repo
|
||||||
|
if(EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||||
|
find_package(Git)
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_BRANCH
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} log -1 --format=%h
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_COMMIT_HASH
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} log -1 --format=%ci
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_COMMIT_TIME
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} describe --long --match "[0-9]*" HEAD
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_VERSION_NUMBER
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
string(REGEX REPLACE "^([0-9]+)\\..*" "\\1" VERSION_MAJOR "${GIT_VERSION_NUMBER}")
|
||||||
|
string(REGEX REPLACE "^[0-9]+\\.([0-9]+).*" "\\1" VERSION_MINOR "${GIT_VERSION_NUMBER}")
|
||||||
|
string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" VERSION_PATCH "${GIT_VERSION_NUMBER}")
|
||||||
|
string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+).*" "\\1" VERSION_COMMIT "${GIT_VERSION_NUMBER}")
|
||||||
|
|
||||||
|
else(EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||||
|
set(GIT_BRANCH "")
|
||||||
|
set(GIT_COMMIT_HASH "")
|
||||||
|
set(GIT_VERSION_NUMBER "")
|
||||||
|
set(VERSION_MAJOR "0")
|
||||||
|
set(VERSION_MINOR "0")
|
||||||
|
set(VERSION_PATCH "0")
|
||||||
|
set(VERSION_COMMIT "0")
|
||||||
|
set(BUILD_TYPE "")
|
||||||
|
|
||||||
|
endif(EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||||
|
|
||||||
|
set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}.${VERSION_COMMIT}")
|
||||||
|
|
||||||
|
message(STATUS "Version: ${VERSION_STRING}")
|
||||||
|
|
||||||
|
# message(STATUS "Git current branch: ${GIT_BRANCH}")
|
||||||
|
# message(STATUS "Git version number: " ${GIT_VERSION_NUMBER} )
|
||||||
|
# message(STATUS "Git commit hash: ${GIT_COMMIT_HASH}")
|
||||||
|
# message(STATUS "Git commit time: ${GIT_COMMIT_TIME}")
|
||||||
|
# message(STATUS "Version major: ${VERSION_MAJOR}")
|
||||||
|
# message(STATUS "Version minor: ${VERSION_MINOR}")
|
||||||
|
# message(STATUS "Version patch: ${VERSION_PATCH}")
|
||||||
|
# message(STATUS "Version commit: ${VERSION_COMMIT}")
|
||||||
35
cmake/version.rc.in
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Windows version resource. Generated by CMake via configure_file() from this
|
||||||
|
// template; @VAR@ placeholders are filled from cmake/version.cmake (version
|
||||||
|
// numbers) and the product identity variables in the top-level CMakeLists.txt.
|
||||||
|
// Shows up on the executable's Details tab (right-click -> Properties).
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION @VERSION_MAJOR@,@VERSION_MINOR@,@VERSION_PATCH@,@VERSION_COMMIT@
|
||||||
|
PRODUCTVERSION @VERSION_MAJOR@,@VERSION_MINOR@,@VERSION_PATCH@,@VERSION_COMMIT@
|
||||||
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
FILEOS VOS_NT_WINDOWS32
|
||||||
|
FILETYPE VFT_APP
|
||||||
|
FILESUBTYPE VFT2_UNKNOWN
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904b0" // US English (0x0409), Unicode (0x04b0)
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "@PRODUCT_COMPANY@"
|
||||||
|
VALUE "FileDescription", "@PRODUCT_DISPLAY_NAME@"
|
||||||
|
VALUE "FileVersion", "@VERSION_STRING@"
|
||||||
|
VALUE "InternalName", "@PRODUCT_NAME@"
|
||||||
|
VALUE "OriginalFilename", "@PRODUCT_NAME@.exe"
|
||||||
|
VALUE "ProductName", "@PRODUCT_DISPLAY_NAME@"
|
||||||
|
VALUE "ProductVersion", "@VERSION_STRING@"
|
||||||
|
VALUE "LegalCopyright", "@PRODUCT_COPYRIGHT@"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1200 // 0x409 = en-US, 1200 = Unicode code page
|
||||||
|
END
|
||||||
|
END
|
||||||
@@ -15,7 +15,7 @@ This document captures the architectural decisions for the project. It is a comp
|
|||||||
A strict separation between the game simulation and the Qt Widgets UI.
|
A strict separation between the game simulation and the Qt Widgets UI.
|
||||||
|
|
||||||
- The **simulation** is a pure C++ library that depends only on Qt Core and Qt Gui (QPoint, QVector2D, QRect, etc., as required by the coding guidelines), toml++, and tinyexpr. It contains no QtWidgets, no painting, and no QApplication. Note: in Qt 5, vector math types such as QVector2D live in Qt::Gui rather than Qt::Core, so the lib links both.
|
- The **simulation** is a pure C++ library that depends only on Qt Core and Qt Gui (QPoint, QVector2D, QRect, etc., as required by the coding guidelines), toml++, and tinyexpr. It contains no QtWidgets, no painting, and no QApplication. Note: in Qt 5, vector math types such as QVector2D live in Qt::Gui rather than Qt::Core, so the lib links both.
|
||||||
- The **UI** reads simulation state and renders it. It owns all widgets, painting, and input handling, and drives the simulation via a small command interface (place building, demolish, clear belt tiles, change recipe, set game speed, etc.).
|
- The **UI** reads simulation state and renders it. It owns all widgets, painting, and input handling, and drives the simulation via a small command interface (place building, deconstruct, clear belt tiles, change recipe, set game speed, etc.).
|
||||||
|
|
||||||
This split is enforced at the CMake target level (see below). Tests link only against the simulation library and run without a display server.
|
This split is enforced at the CMake target level (see below). Tests link only against the simulation library and run without a display server.
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ The simulation advances in discrete ticks. All game quantities — production ti
|
|||||||
|
|
||||||
- Tick rate: fixed at 30 Hz; `tickDurationMs = 1000 / 30 ≈ 33.33`.
|
- Tick rate: fixed at 30 Hz; `tickDurationMs = 1000 / 30 ≈ 33.33`.
|
||||||
- Ticks are driven by an accumulator that is independent of the render rate. Each render frame, the driver adds `elapsedWallMs × gameSpeedMultiplier` to an accumulator and flushes one `tick()` per `tickDurationMs` of accumulated time (so multiple sim ticks may run between frames at high speeds, or a frame may run no ticks at low speeds). `gameSpeedMultiplier` ∈ {0, 0.5, 1, 2, 4} per REQ-UI-SPEED; 0× freezes the accumulator (pause). The concrete driver lives in the Rendering section.
|
- Ticks are driven by an accumulator that is independent of the render rate. Each render frame, the driver adds `elapsedWallMs × gameSpeedMultiplier` to an accumulator and flushes one `tick()` per `tickDurationMs` of accumulated time (so multiple sim ticks may run between frames at high speeds, or a frame may run no ticks at low speeds). `gameSpeedMultiplier` ∈ {0, 0.5, 1, 2, 4} per REQ-UI-SPEED; 0× freezes the accumulator (pause). The concrete driver lives in the Rendering section.
|
||||||
- Config-level durations given in seconds (recipe durations, wave gap ranges, scrap despawn, etc.) are converted to ticks at config-load time.
|
- Config-level durations given in seconds (recipe durations, wave gap ranges, debris despawn, etc.) are converted to ticks at config-load time.
|
||||||
|
|
||||||
Consequences: determinism, replayability, and the time-scale feature fall out for free. The simulation advances the same number of ticks over the same amount of game-time regardless of whether the game renders at 60 FPS, 30 FPS, or a stuttery mix.
|
Consequences: determinism, replayability, and the time-scale feature fall out for free. The simulation advances the same number of ticks over the same amount of game-time regardless of whether the game renders at 60 FPS, 30 FPS, or a stuttery mix.
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ See REQ-GW-COORDS for the authoritative tile-coordinate convention. This section
|
|||||||
|
|
||||||
- Tile coordinates are `QPoint(x, y)`. Origin `(0, 0)` is the first space tile (just right of the asteroid's right edge at game start). X grows right; Y grows down.
|
- Tile coordinates are `QPoint(x, y)`. Origin `(0, 0)` is the first space tile (just right of the asteroid's right edge at game start). X grows right; Y grows down.
|
||||||
- Asteroid tiles have `x < 0`. Asteroid left-expansions add tiles at increasingly negative X; the origin never shifts, so existing tile coordinates remain stable across expansions.
|
- Asteroid tiles have `x < 0`. Asteroid left-expansions add tiles at increasingly negative X; the origin never shifts, so existing tile coordinates remain stable across expansions.
|
||||||
- Continuous world positions (ship centers, scrap drops, projectiles) use `QVector2D` in tile units — one tile = 1.0 world unit. A ship center at `QVector2D(-3.5, 4.0)` sits at the center of the tile 3.5 tiles left of the asteroid's right edge and 4 tiles down from the top.
|
- Continuous world positions (ship centers, debris, projectiles) use `QVector2D` in tile units — one tile = 1.0 world unit. A ship center at `QVector2D(-3.5, 4.0)` sits at the center of the tile 3.5 tiles left of the asteroid's right edge and 4 tiles down from the top.
|
||||||
- Rendering multiplies world units by the tile size in pixels (20) at draw time.
|
- Rendering multiplies world units by the tile size in pixels (20) at draw time.
|
||||||
- Ship position always refers to the ship's center — this is the point used for sensor, attack-range, and hit-detection checks.
|
- Ship position always refers to the ship's center — this is the point used for sensor, attack-range, and hit-detection checks.
|
||||||
|
|
||||||
@@ -52,30 +52,56 @@ See REQ-GW-COORDS for the authoritative tile-coordinate convention. This section
|
|||||||
|
|
||||||
Simulation types shared across subsystems:
|
Simulation types shared across subsystems:
|
||||||
|
|
||||||
- `EntityId` — strictly increasing integer handle, allocated centrally by the simulation. Assigned to every targetable entity: ships, scrap drops, **and** buildings (including HQ and defence stations). Buildings additionally retain their anchor tile for spatial lookups and placement; the `EntityId` is the canonical reference used by ship-component target fields (`Weapon.currentTarget`, `RepairTool.currentTarget`, `ThreatResponse.currentTarget`, etc.), so a combat ship can target either another ship or a defence station uniformly.
|
- `EntityId` — strictly increasing integer handle, allocated centrally by the simulation. Assigned to every targetable entity: ships, debris, **and** buildings (including HQ and defence stations). Buildings additionally retain their anchor tile for spatial lookups and placement; the `EntityId` is the canonical reference used by ship-component target fields (`Weapon.currentTarget`, `RepairTool.currentTarget`, `AttackBehavior.currentTarget`, etc.), so a combat ship can target either another ship or a defence station uniformly.
|
||||||
- `Rotation` — enum `{ North, East, South, West }`. The rotation applied to a building's surface_mask when placed.
|
- `Rotation` — enum `{ North, East, South, West }`. The rotation applied to a building's surface_mask when placed.
|
||||||
- `BuildingType` — enum covering every building type in requirements.md (Miner, Smelter, Assembler, ReprocessingPlant, Shipyard, SalvageBay, Belt, Splitter, Hq, PlayerDefenceStation, EnemyDefenceStation). `Belt` and `Splitter` share the enum for cost, construction, placement, and `visuals.toml` lookup, but their runtime data lives inside the belt subsystem rather than in `Building` instances (see Belt Subsystem).
|
- `BuildingType` — enum covering every building type in requirements.md (Miner, Smelter, Assembler, ReprocessingPlant, Shipyard, SalvageBay, Belt, Splitter, Hq, PlayerDefenceStation, EnemyDefenceStation). `Belt` and `Splitter` share the enum for cost, construction, placement, and `visuals.toml` lookup, but their runtime data lives inside the belt subsystem rather than in `Building` instances (see Belt Subsystem).
|
||||||
- `ItemType` — tagged id of every transportable material (ores, ingots, intermediates, building_blocks, scrap).
|
- `ItemType` — tagged id of every transportable material (ores, ingots, intermediates, building_blocks, scrap).
|
||||||
- `Item` — `struct Item { ItemType type; }`. Items on belts have no persistent identity across ticks.
|
- `Item` — `struct Item { ItemType type; }`. Items on belts have no persistent identity across ticks.
|
||||||
- `Port` — `struct Port { QPoint tile; Rotation direction; }`. Identifies a belt-adjacent cell and the direction of flow across that cell.
|
- `Port` — `struct Port { QPoint tile; Rotation direction; }`. Identifies a belt-adjacent cell and the direction of flow across that cell.
|
||||||
- `MovementIntent` — `struct MovementIntent { int priority; QVector2D target; }`. Priority follows the order declared under Movement Arbitration. Cleared at the start of each tick; the highest-priority write wins; `tickMovement` reads the winner.
|
- `MovementIntent` — `struct MovementIntent { bool active; QVector2D target; }`. Written by the winning behavior's executor (see Movement Arbitration). Cleared (`active = false`) at the start of each tick; `tickMovement` brakes when inactive, otherwise drives toward `target`.
|
||||||
- `FireEvent` — `struct FireEvent { EntityId shooter; EntityId target; Tick emittedAt; }`. Transient record emitted each time a weapon fires (REQ-SHP-FIRING, REQ-SHP-FIRING-BEAM). Buffered in a sim-owned queue and drained by the renderer; see Sim → UI Events.
|
- `BeamFiredEvent` — `struct BeamFiredEvent : public Event { BeamKind kind; entt::entity shooter; entt::entity target; Tick emittedAt; }`. Transient record emitted each time a weapon fires, a repair tool starts a heal cycle, or a salvage module starts a collection cycle (REQ-SHP-FIRING, REQ-SHP-FIRING-BEAM). `BeamKind` (`Weapon`/`Repair`/`Salvage`) selects the beam color. Buffered in a sim-owned vector during the tick, then drained and re-emitted via EventManager by the UI frame handler; see Sim → UI Events.
|
||||||
- `SchematicDropEvent` — `struct SchematicDropEvent { ShipSchematicId schematic; int newLevel; bool wasNewUnlock; }`. Emitted when a destroyed enemy-defence-station set awards a schematic (REQ-DEF-SCHEMATIC-DROP). The UI renders a toast (REQ-UI-SCHEMATIC-TOAST); `wasNewUnlock` chooses between the "unlocked" and "level → N" wording.
|
- `SchematicChoiceOption` — `struct SchematicChoiceOption { string schematicId; SchematicType type; string displayName; bool isNewUnlock; int targetLevel; }`. Describes one option in the schematic choice dialog (REQ-DEF-SCHEMATIC-DROP). Up to three are generated when an enemy station set is destroyed. `SchematicType` is `Ship`, `Module`, or `Recipe`.
|
||||||
|
- `SchematicChoicesAvailableEvent` — EventManager event carrying a `vector<SchematicChoiceOption>`. Sent by the UI each frame when pending choices are detected; handled by `MainWindow` which opens the schematic choice dialog.
|
||||||
|
|
||||||
## Sim → UI Events
|
## Event System
|
||||||
|
|
||||||
The sim owns a small set of per-frame event queues that the UI drains on each render. These carry one-shot signals that are not derivable from persistent state — currently weapon fires (REQ-SHP-FIRING-BEAM) and schematic drops (REQ-UI-SCHEMATIC-TOAST). Additional event types can be added here later (e.g., building-complete, unit-death flashes) without changing the pattern.
|
All inter-component communication — both sim→UI and UI→UI — uses a unified `EventManager`/`EventHandler` system. No custom Qt signals/slots are used for inter-widget communication.
|
||||||
|
|
||||||
Implementation: a plain `std::vector<FireEvent>` owned by `Simulation`, one vector per event type. Combat resolution (tick-order step 8) appends to it. The UI calls `simulation.drainFireEvents()` once per rendered frame, which returns the accumulated vector by move and clears the internal one. Beams are tracked by the renderer for 0.3 s of wall time (9 ticks at 30 Hz) using the events' `emittedAt` tick, then discarded. If either the shooter or target entity is gone when the renderer looks them up, the beam is dropped early.
|
### EventManager
|
||||||
|
|
||||||
We deliberately do **not** use `QObject` signals/slots or `QEvent`:
|
`EventManager` is a singleton (`EventManager::getInstance()`) that routes events to registered handlers.
|
||||||
|
|
||||||
- **Determinism.** A plain ordered vector preserves tick-order exactly; the queue is part of per-tick state, inspectable in tests.
|
- `sendEventImmediately(shared_ptr<Event>)` — synchronous dispatch to all handlers of the event's type.
|
||||||
- **Sim/UI seam.** The sim exposes pull-style access only; the UI never subscribes into the sim, keeping the simulation/presentation split clean.
|
- `addEvent(shared_ptr<Event>)` — queues the event for later batch processing.
|
||||||
- **Headless testability.** Catch2 tests read the queue directly after `tick()`; no event loop, no `QApplication`.
|
- `processEvents()` — drains the queue, dispatching each event to its handlers.
|
||||||
- **Zero overhead.** Sim types remain plain structs — no `QObject`, no moc, no signal dispatch machinery.
|
|
||||||
|
|
||||||
If the number of event types grows past a handful, we can wrap them in a small `EventQueue<T>` template, still owned by the sim. Signals/slots would only be warranted if we needed multiple independent subscribers or cross-thread dispatch, and we need neither.
|
The EventManager is thread-safe (mutex-guarded).
|
||||||
|
|
||||||
|
### EventHandler
|
||||||
|
|
||||||
|
`EventHandler<T>` is a CRTP-style template that a class inherits to receive events of type `T`. It provides `registerForEvent()` / `unregisterForEvent()` and requires an override of `handleEvent(shared_ptr<const T>)`.
|
||||||
|
|
||||||
|
`CombinedEventHandler<Ts...>` is a variadic template for classes that handle multiple event types. It provides `registerForEvents()` / `unregisterForEvents()` and requires one `handleEvent` override per type.
|
||||||
|
|
||||||
|
### Sim → UI Events
|
||||||
|
|
||||||
|
The simulation layer stays free of EventManager — it uses a plain `std::vector<BeamFiredEvent>` internally (owned by `Simulation`, filled by the combat, repair, and salvage systems). This preserves determinism, tick-order fidelity, and headless testability (Catch2 tests read the queue directly via `drainBeamFiredEvents()` after `tick()`).
|
||||||
|
|
||||||
|
The UI frame handler (`GameWorldView::onFrame` / `ArenaView::onFrame`) bridges the gap: each frame it calls `simulation.drainBeamFiredEvents()`, then re-emits each `BeamFiredEvent` via `EventManager::sendEventImmediately()`. Subscribers (the same view's `handleEvent(BeamFiredEvent)`) create `ActiveBeam` records tracked for 0.3 s of wall time, then discarded. If either the shooter or target entity is gone when the renderer looks them up, the beam is dropped early.
|
||||||
|
|
||||||
|
Schematic drops: when an enemy station set is destroyed, the simulation generates up to 3 `SchematicChoiceOption` entries and stores them as pending state. The UI polls `hasSchematicChoicesPending()` each frame and, when true, sends a `SchematicChoicesAvailableEvent` via EventManager. `MainWindow` handles this event by pausing the game and opening a modal `SchematicChoiceDialog`. The player's selection is fed back via `applySchematicChoice(index)`.
|
||||||
|
|
||||||
|
### UI Events
|
||||||
|
|
||||||
|
All UI interactions — building selection, builder/blueprint mode transitions, speed changes, deconstruct mode, escape menu, layout dialog requests — are communicated via EventManager events rather than Qt signals/slots. Each event is a small struct inheriting `Event` (e.g., `SelectionChangedEvent`, `BuildingTypeSelectedEvent`, `SpeedChangeRequestedEvent`). Widgets register as `CombinedEventHandler` for the events they care about and emit events via `EventManager::sendEventImmediately()`.
|
||||||
|
|
||||||
|
Bidirectional interactions use separate request/notification event types to avoid infinite recursion (e.g., `ExitBuilderModeRequestedEvent` from `BuildButtonBar` → `GameWorldView`, vs. `BuilderModeExitedEvent` from `GameWorldView` → `BuildButtonBar`).
|
||||||
|
|
||||||
|
### Reading Simulation State
|
||||||
|
|
||||||
|
The simulation is the single source of truth for every game value (building block stock, expansion cost, threat level, tick, etc.). A UI widget that needs such a value holds the `Simulation*` it was constructed with and **pulls the value on demand** via the corresponding getter (e.g., `m_sim->getBuildingBlocksStock()`), rather than caching its own copy.
|
||||||
|
|
||||||
|
State-change events (e.g., `BuildingBlocksChangedEvent`) are treated as *refresh signals*, not as carriers of truth: a widget subscribes to the event to learn *when* the value changed and then re-reads it from the simulation to learn *what* it now is. The value carried in the event payload is not authoritative and should not be stored. This keeps a single copy of each value and avoids stale-cache bugs (a widget acting on a value that has since moved on because nothing refreshed its local copy).
|
||||||
|
|
||||||
## Tick Order
|
## Tick Order
|
||||||
|
|
||||||
@@ -87,40 +113,85 @@ Within a single simulation tick, subsystems run in this fixed order. The order i
|
|||||||
4. **Building production** — advance production timers; start new cycles when inputs and output-buffer space permit (REQ-MAT-CYCLE); on completion, deposit output.
|
4. **Building production** — advance production timers; start new cycles when inputs and output-buffer space permit (REQ-MAT-CYCLE); on completion, deposit output.
|
||||||
5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT).
|
5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT).
|
||||||
6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER).
|
6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER).
|
||||||
7. **Ship behavior systems** — clear `MovementIntent` on each ship, then run `tickThreatResponse`, `tickScrapCollector`, `tickRepairBehavior`, `tickHomeReturn` in any order (arbitration is via intent priority).
|
7. **Ship behavior systems** — clear `MovementIntent` on each ship, then the `AiSystem` runs three batched phases: every behavior **evaluator** scores its behavior and sets its target data; a **selection** pass records the highest-scoring behavior per ship in `SelectedBehaviorComponent`; each behavior **executor** runs for the winner, writing `MovementIntent` and preferred module targets. The module systems then perform world mutation: `SalvagerSystem` (scrap collection/delivery) and `RepairSystem` (healing). See Movement Arbitration.
|
||||||
8. **Combat resolution** — ships and defence stations acquire targets, fire, apply damage; queue deaths. Each fire appends a `FireEvent` to the sim's fire-event queue (REQ-SHP-FIRING-BEAM).
|
8. **Combat resolution** — ships and defence stations validate/acquire targets, fire, apply damage; queue deaths. Each fire appends a `BeamFiredEvent` to the sim's beam-fired-event queue (REQ-SHP-FIRING-BEAM). The repair and salvage module systems (tick step 7d) append their own `BeamFiredEvent`s to the same queue when they start a cycle.
|
||||||
9. **Deaths & loot** — process queued deaths: drop scrap (REQ-RES-SCRAP-DROP); if a full enemy-defence-station set was destroyed this tick, award one schematic (REQ-DEF-SCHEMATIC-DROP) and append a `SchematicDropEvent`; remove entities.
|
9. **Deaths & loot** — process queued deaths: drop debris (REQ-RES-DEBRIS-DROP); if a full enemy-defence-station set was destroyed this tick, generate up to 3 schematic choice options (REQ-DEF-SCHEMATIC-DROP) stored as pending state for the UI to present; remove entities.
|
||||||
10. **`tickMovement`** — advance ship positions based on final `MovementIntent`.
|
10. **`tickMovement`** — advance ship positions based on final `MovementIntent`.
|
||||||
11. **Scrap despawn** — decrement scrap timers; remove expired scrap (REQ-RES-SCRAP-DROP).
|
11. **Debris despawn** — decrement debris timers; remove expired debris (REQ-RES-DEBRIS-DROP).
|
||||||
|
|
||||||
## CMake Target Layout
|
## CMake Target Layout
|
||||||
|
|
||||||
Three product targets plus tests:
|
Three product targets plus tests:
|
||||||
|
|
||||||
- `lib/` — simulation + config. Depends on Qt Core + Qt Gui, toml++, tinyexpr. No QtWidgets.
|
- `lib/` — simulation + config. Depends on Qt Core + Qt Gui, toml++, tinyexpr. No QtWidgets.
|
||||||
- `ui/` — QtWidgets + `QOpenGLWidget` code: header bar, game world view, selected building panel, build button grid. Depends on `lib` and on Qt's OpenGL widgets module.
|
- `ui/` — QtWidgets + `QOpenGLWidget` code: header bar, game world view, selection panel, build button bar, controls panel. Depends on `lib` and on Qt's OpenGL widgets module.
|
||||||
|
- `ui/selection/` — the selection panel's contents. `SelectionPanel` itself only arbitrates between the two selection categories, picks a card from the catalog (`SelectionContentFactory`), and hosts one at a time; each kind of selection has its own `SelectionContent` subclass assembled from shared parts (REQ-UI-SELECTION-CARD, REQ-UI-SELECTION-CONTENT).
|
||||||
- `app/` — thin `main()` that creates the simulation, the UI, and wires them together. Depends on `ui`.
|
- `app/` — thin `main()` that creates the simulation, the UI, and wires them together. Depends on `ui`.
|
||||||
- `tests/` — Catch2 tests. Links only against `lib`.
|
- `tests/` — Catch2 tests. Links only against `lib`.
|
||||||
|
|
||||||
Directory discipline inside `lib/` keeps the internal sim/config seam clear; sim code must not reach into config parsing and vice versa.
|
Directory discipline inside `lib/` keeps the internal sim/config seam clear; sim code must not reach into config parsing and vice versa.
|
||||||
|
|
||||||
|
## Player Input
|
||||||
|
|
||||||
|
Every player control is declared once, in `lib/core/ControlAction.h`, and read by three consumers that must never disagree about it:
|
||||||
|
|
||||||
|
* **`ControlsPanel`** asks which actions apply and draws a row per action (REQ-UI-CONTROLS-CONTENT).
|
||||||
|
* **`InputMapper`** resolves a key press to an action and fires the event that action stands for.
|
||||||
|
* **`GameWorldView`** resolves a mouse gesture to an action and runs the branch that carries it out.
|
||||||
|
|
||||||
|
The file declares; it never performs. It holds no simulation access, fires no events, and names nothing — display strings live in `ui/ControlActionText.h`, which renders each badge from the binding the resolver actually matches, so a chip cannot claim a key that does nothing. What an action *does* stays in the widget that always did it: the drag state machines, hit-testing, and command enqueuing were not moved.
|
||||||
|
|
||||||
|
Three invariants are easy to break here:
|
||||||
|
|
||||||
|
* **Do not add a shortcut straight to `InputMapper`'s switch or `mousePressEvent`'s branches.** Add the action and its binding to the table; the handler switches on the resolved action. A binding added directly is invisible to the panel, which is the drift the table exists to prevent. (Build hotkeys and `F3`/`F4` are deliberate exceptions, documented in the header and in REQ-UI-CONTROLS-ACCURACY.)
|
||||||
|
* **Availability and display are different questions.** An action can be live in a context the panel does not advertise it in — `Ctrl`+click with an empty selection is the standing example. `isControlActionAvailable` answers the first, the per-context row lists answer the second, and `ControlActionTest` asserts the pairing that matters: every row's bindings resolve back to that row's action.
|
||||||
|
* **Gesture state is shared, not owned by an action.** Whether a belt drag is in progress decides what the right mouse button means, so it lives on `BuildModeController` where the resolver can see it — as does the hovered-transfer flag. `m_boxSelecting` is likewise one gesture serving two actions (box select and deconstruct area).
|
||||||
|
|
||||||
|
`ControlContext` is a plain snapshot rather than references to the live controllers, which is what keeps the rules testable without a world and stops an action reaching into the simulation: if a rule needs a fact, the fact is named in the struct and the caller supplies it. When bindings become player-configurable, only the binding tables in `ControlAction.cpp` turn from hard-coded data into loaded data.
|
||||||
|
|
||||||
## Belt Subsystem
|
## Belt Subsystem
|
||||||
|
|
||||||
Belts and splitters are their own specialized subsystem. Belt items are **not** entities — they are transient data flowing through the belt representation. They do not have identities that persist across ticks.
|
Belts and splitters are their own specialized subsystem. Belt items are **not** entities — they are transient data flowing through the belt representation. They do not have identities that persist across ticks.
|
||||||
|
|
||||||
### Public Interface
|
### Public Interface
|
||||||
|
|
||||||
Narrow and representation-agnostic:
|
`BeltSystem.h` is authoritative. The surface is wider than the original design sketch — 15 public methods in five groups, not the 5-method port interface this section used to describe:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
class BeltSystem {
|
class BeltSystem {
|
||||||
public:
|
public:
|
||||||
bool tryPutItem(Port port, Item item);
|
// Placement — belts/splitters/tunnels are Buildings for cost and
|
||||||
std::optional<Item> tryTakeItem(Port port);
|
// construction, so BuildingSystem registers and unregisters their tiles.
|
||||||
|
void placeBelt(QPoint tile, Rotation direction);
|
||||||
|
void placeTunnelEntry(QPoint tile, Rotation direction, int maxDistance);
|
||||||
|
void placeTunnelExit(QPoint tile, Rotation direction);
|
||||||
|
void placeSplitter(QPoint tile, Rotation outputA, Rotation outputB);
|
||||||
|
void removeTile(QPoint tile);
|
||||||
|
|
||||||
|
// Splitter filter configuration (REQ-BLD-SPLITTER). A splitter's filters
|
||||||
|
// live here, not on Building, so callers that re-register a tile must
|
||||||
|
// carry them across (see BuildingSystem::reregisterBeltTile).
|
||||||
|
void setSplitterFilters(QPoint tile, const std::vector<ItemType>& filterA,
|
||||||
|
const std::vector<ItemType>& filterB);
|
||||||
|
std::optional<SplitterInfo> getSplitterInfo(QPoint tile) const;
|
||||||
|
|
||||||
|
// Port interface (buildings <-> belts)
|
||||||
|
bool tryPutItem(QPoint tile, Item item, Rotation fromDir = Rotation::West);
|
||||||
|
std::optional<Item> tryTakeItem(Port port);
|
||||||
|
std::optional<ItemType> peekItem(Port port) const;
|
||||||
|
double getProgressPerTick_tpt() const; // shared so building output items
|
||||||
|
// travel at belt speed (REQ-MAT-OUTPUT-EMERGE)
|
||||||
|
|
||||||
|
// Maintenance
|
||||||
void clearTiles(const std::vector<QPoint>& tiles); // REQ-UI-BELT-CLEAR
|
void clearTiles(const std::vector<QPoint>& tiles); // REQ-UI-BELT-CLEAR
|
||||||
void tick();
|
void tick();
|
||||||
|
|
||||||
|
// Rendering
|
||||||
void forEachVisualItem(QRect viewportTiles,
|
void forEachVisualItem(QRect viewportTiles,
|
||||||
std::function<void(VisualItem)> visit) const;
|
std::function<void(VisualItem)> visit) const;
|
||||||
|
|
||||||
|
// Determinism (docs/replay_design.md)
|
||||||
|
void appendChecksum(Hasher& hasher) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VisualItem {
|
struct VisualItem {
|
||||||
@@ -129,12 +200,12 @@ struct VisualItem {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Buildings interact with belts only through port-level push and pull. Rendering reads only through `forEachVisualItem`. No other system ever asks "what is on tile X".
|
Item *transport* is still reached only through push and pull: `tryPutItem` / `tryTakeItem` move items, `peekItem` reveals the leading item's type but never an identity, and rendering reads only through `forEachVisualItem`. The growth is in tile **topology** — placement, removal and splitter filters — which `BuildingSystem` drives because belts are `Building`s for cost, construction and deconstruction. That coupling is real and is not going away.
|
||||||
|
|
||||||
### Implementation Strategy
|
### Implementation Strategy
|
||||||
|
|
||||||
- v1: per-tile representation. Each belt tile stores up to 2 items with a progress value in `[0, 1]` along the tile's belt direction. Sufficient for the scale this game targets.
|
- v1: per-tile representation. Each belt tile stores up to 2 items with a progress value in `[0, 1]` along the tile's belt direction. Sufficient for the scale this game targets.
|
||||||
- v2 (optional, only if v1 profiles poorly): Factorio-style belt-segment compression. Because the public interface never exposes tile-level item identity, migration is internal to the subsystem.
|
- v2 (optional, only if v1 profiles poorly): Factorio-style belt-segment compression. The migration argument still holds for the item representation, since no method exposes tile-level item identity — but a v2 would have to keep the placement and splitter-filter methods working per tile, which is a stronger constraint than this section originally implied.
|
||||||
|
|
||||||
### Rendering Note
|
### Rendering Note
|
||||||
|
|
||||||
@@ -167,20 +238,21 @@ struct Building {
|
|||||||
- Belts and splitters are separate types owned by the belt subsystem, not general `Building` instances.
|
- Belts and splitters are separate types owned by the belt subsystem, not general `Building` instances.
|
||||||
- No ECS for buildings. A miner is never also an assembler; there is no composition benefit to decomposing buildings into components.
|
- No ECS for buildings. A miner is never also an assembler; there is no composition benefit to decomposing buildings into components.
|
||||||
|
|
||||||
## Scrap
|
## Debris
|
||||||
|
|
||||||
Scrap is the only non-ship, non-building entity in the simulation:
|
Debris — the salvageable object dropped by destroyed ships and defence stations — is the
|
||||||
|
only non-ship, non-building entity in the simulation. Each piece carries a scrap amount:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
struct Scrap {
|
struct Debris {
|
||||||
EntityId id;
|
EntityId id;
|
||||||
QVector2D position; // world units, tile-fractional; ship-center convention
|
QVector2D position; // world units, tile-fractional; ship-center convention
|
||||||
int amount;
|
int amount; // scrap the piece still holds
|
||||||
Tick despawnAt; // absolute tick at which the scrap is removed
|
Tick despawnAt; // absolute tick at which the debris is removed
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Created in tick step 9 (Deaths & loot) per REQ-RES-SCRAP-DROP, consumed by salvage ships in tick step 7 (ScrapCollector), and removed in tick step 11 when the current tick reaches `despawnAt`.
|
Created in tick step 9 (Deaths & loot) per REQ-RES-DEBRIS-DROP, drained one scrap per cycle by salvage ships in tick step 7 (SalvagerSystem), and removed in tick step 11 when the current tick reaches `despawnAt`.
|
||||||
|
|
||||||
## Ships
|
## Ships
|
||||||
|
|
||||||
@@ -192,21 +264,26 @@ Ships follow a component-composition model using `std::optional<Component>` memb
|
|||||||
struct Weapon { float damage; float range; float fireRateHz; float cooldownTicks;
|
struct Weapon { float damage; float range; float fireRateHz; float cooldownTicks;
|
||||||
std::optional<EntityId> currentTarget; };
|
std::optional<EntityId> currentTarget; };
|
||||||
struct SalvageCargo { int capacity; int current; };
|
struct SalvageCargo { int capacity; int current; };
|
||||||
struct RepairTool { float ratePerTick; std::optional<EntityId> currentTarget; };
|
struct RepairTool { float repairAmountHp; int repairIntervalTicks; int cooldownTicksRemaining;
|
||||||
|
float range; std::optional<EntityId> currentTarget; };
|
||||||
```
|
```
|
||||||
|
|
||||||
### Behavior Components
|
### Behavior Components
|
||||||
|
|
||||||
Behaviors are decomposed, not bundled into per-role monolithic AIs. This is the critical modeling choice: adding a capability (e.g., putting a `Weapon` on a repair ship) must not require rewriting AI code.
|
Behaviors are decomposed, not bundled into per-role monolithic AIs. This is the critical modeling choice: adding a capability (e.g., putting a `Weapon` on a repair ship) must not require rewriting AI code. Each behavior is a small component carrying its own target data plus a `float score` written by its evaluator each tick.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
struct ThreatResponse { float engagementRange; CombatStance stance;
|
struct AdvanceBehavior { float score; }; // baseline fallback, all ships
|
||||||
CombatTargetPriority priority;
|
struct RallyBehavior { QVector2D rallyPoint; float score; }; // player combat ships
|
||||||
std::optional<EntityId> currentTarget; };
|
struct RetreatBehavior { float retreatHpFraction; QVector2D retreatPoint; // player ships
|
||||||
struct ScrapCollector { std::optional<QVector2D> scrapTarget; EntityId deliveryBay; };
|
float score; };
|
||||||
struct RepairBehavior { RepairTargetPriority priority;
|
struct AttackBehavior { std::optional<EntityId> currentTarget; float score; };
|
||||||
std::optional<EntityId> currentTarget; };
|
struct RepairBehavior { std::optional<EntityId> currentTarget;
|
||||||
struct HomeReturn { float retreatHpFraction; QVector2D homePos; };
|
float maxRepairRange_tiles; float score; };
|
||||||
|
struct SalvageScrapBehavior { std::optional<QVector2D> debrisTarget;
|
||||||
|
float maxCollectionRange_tiles; float score; };
|
||||||
|
struct DeliverScrapBehavior { BuildingId deliveryBay; float score; };
|
||||||
|
struct SelectedBehaviorComponent { BehaviorKind winner; float bestScore; }; // selection result
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ship
|
### Ship
|
||||||
@@ -226,38 +303,42 @@ struct Ship {
|
|||||||
std::optional<SalvageCargo> cargo;
|
std::optional<SalvageCargo> cargo;
|
||||||
std::optional<RepairTool> repairTool;
|
std::optional<RepairTool> repairTool;
|
||||||
|
|
||||||
// Behaviors
|
// Behaviors (attached per capability; AdvanceBehavior + SelectedBehaviorComponent
|
||||||
std::optional<ThreatResponse> threatResponse;
|
// on every ship, RetreatBehavior on player ships, etc.)
|
||||||
std::optional<ScrapCollector> scrapCollector;
|
std::optional<AttackBehavior> attackBehavior;
|
||||||
std::optional<RepairBehavior> repairBehavior;
|
std::optional<SalvageScrapBehavior> salvageScrapBehavior;
|
||||||
std::optional<HomeReturn> homeReturn;
|
std::optional<DeliverScrapBehavior> deliverScrapBehavior;
|
||||||
|
std::optional<RepairBehavior> repairBehavior;
|
||||||
|
|
||||||
// Written by behavior systems, read by movement.
|
// Written by the winning behavior's executor, read by movement.
|
||||||
MovementIntent intent;
|
MovementIntent intent;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Systems
|
### Systems
|
||||||
|
|
||||||
Each behavior has its own tick system. A system iterates a flat `std::vector<Ship>` and skips ships that do not have the relevant components.
|
Each behavior is split into a stateless **evaluator** and **executor** class (one per behavior, e.g. `AttackEvaluator`/`AttackExecutor`), orchestrated by `AiSystem`. Evaluators and executors only read/write behavior components and module target fields — they never mutate the game world. World mutation lives in dedicated module systems that run every tick, independent of which behavior won:
|
||||||
|
|
||||||
- `tickThreatResponse` — requires `threatResponse` + `weapon`. Acquires target, fires, manages cooldown.
|
- `CombatSystem` — validates each weapon's executor-set target, falls back to nearest-target acquisition, fires, applies damage.
|
||||||
- `tickScrapCollector` — requires `scrapCollector` + `cargo`. Flies to scrap, picks up, returns to delivery bay.
|
- `SalvagerSystem` — collects scrap into cargo and delivers full cargo at a `SalvageBay`.
|
||||||
- `tickRepairBehavior` — requires `repairBehavior` + `repairTool`. Finds damaged target, moves to range, repairs.
|
- `RepairSystem` — validates each repair tool's target, falls back to nearest damaged friendly, applies healing.
|
||||||
- `tickHomeReturn` — requires `homeReturn`. Overrides movement if hp drops below threshold.
|
- `MovementIntentSystem` (`tickMovement`) — reads `MovementIntent`, advances `position`; brakes when inactive.
|
||||||
- `tickMovement` — reads `intent`, advances `position`.
|
|
||||||
|
|
||||||
### Movement Arbitration
|
### Movement Arbitration
|
||||||
|
|
||||||
When multiple behaviors want to drive movement, a fixed global priority resolves the conflict. Each behavior system writes a `MovementIntent` carrying its priority; a higher-priority write overwrites a lower-priority one. `tickMovement` reads the final winner.
|
Arbitration is **score-based**, not fixed-priority. In a single tick `AiSystem` runs three phases:
|
||||||
|
|
||||||
Initial priority order (subject to tuning):
|
1. **Evaluate** — every behavior's evaluator iterates the ships that have its component, sets its target data, and writes a `float score` (see `BehaviorScores.h`). An evaluator returns an inactive score when its behavior does not apply.
|
||||||
|
2. **Select** — `selectWinningBehaviors` resets each `SelectedBehaviorComponent`, then compares every behavior's score per ship, recording the highest as `winner`. Behaviors are considered highest-band first so a strict `>` breaks ties toward the more urgent behavior.
|
||||||
|
3. **Execute** — each behavior's executor runs only for ships where it is the `winner`, writing the single `MovementIntent` and any preferred module targets.
|
||||||
|
|
||||||
|
`AdvanceBehavior` is present on every ship with the lowest score, guaranteeing a winner. The resulting band order:
|
||||||
|
|
||||||
```
|
```
|
||||||
HomeReturn > ThreatResponse > RepairBehavior > ScrapCollector
|
Retreat > Attack / Repair / SalvageScrap / DeliverScrap > Rally > Advance
|
||||||
```
|
```
|
||||||
|
|
||||||
`tickMovement` runs last. Intents are cleared at the start of each tick.
|
`MovementIntent` is cleared (inactive) at the start of each tick; `tickMovement` runs last.
|
||||||
|
|
||||||
### Why Not ECS
|
### Why Not ECS
|
||||||
|
|
||||||
@@ -267,7 +348,11 @@ Buildings and the belt subsystem stay outside any entity model regardless of wha
|
|||||||
|
|
||||||
## Rendering
|
## Rendering
|
||||||
|
|
||||||
The game world is rendered by a single `GameWorldView` widget that inherits `QOpenGLWidget` and uses `QPainter` for all drawing. This gives the same imperative paint API as a plain `QWidget` with GPU acceleration, comfortably handling the expected scale (hundreds of ships, thousands of belt items) without blocking the main thread on CPU rasterization.
|
The game world is drawn into a single `GameWorldView` widget that inherits `QOpenGLWidget` and uses `QPainter` for all drawing. This gives the same imperative paint API as a plain `QWidget` with GPU acceleration, comfortably handling the expected scale (hundreds of ships, thousands of belt items) without blocking the main thread on CPU rasterization.
|
||||||
|
|
||||||
|
The drawing itself lives in `WorldRenderer`, not in the widget. `paintGL` is a call sequence: build the frame's `WorldCoordinates`, hand the renderer a `WorldRenderFrame`, then draw the screen-anchored chrome. The split is the world-space / screen-space line, and it is exact: the renderer draws everything positioned in tiles, while everything positioned in pixels — the pause and deconstruct vignettes, the replay overlay, the debug stats panel — stays with the widget. A useful consequence is that the renderer draws no translatable text at all (its text is config-driven glyphs, ASCII port arrows, and numbers), so it needs no `tr()` and no tie to the meta-object system.
|
||||||
|
|
||||||
|
`WorldRenderFrame` is what makes the renderer independent of the widget. The renderer reads the simulation directly, but everything else it draws is interaction state the widget owns — the selection, the active build mode, live beams, the box-select rectangle. Those are gathered into the frame each `paintGL` and passed by reference, so the renderer keeps no copy that a later click could invalidate. The renderer knows nothing about input: the widget resolves clicks and hit-tests, and the renderer only draws the result.
|
||||||
|
|
||||||
### Render Loop
|
### Render Loop
|
||||||
|
|
||||||
@@ -280,7 +365,7 @@ The game world is rendered by a single `GameWorldView` widget that inherits `QOp
|
|||||||
|
|
||||||
### Threading
|
### Threading
|
||||||
|
|
||||||
Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly without locks. If profiling later justifies moving the sim to a worker thread, the pull-style `drainFireEvents()` / `drainSchematicDropEvents()` / `forEachVisualItem()` APIs already support a clean snapshot-and-render split; a single mutex at the sim boundary would suffice.
|
Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly without locks. If profiling later justifies moving the sim to a worker thread, the pull-style `drainBeamFiredEvents()` / `getPendingSchematicChoices()` / `applySchematicChoice()` / `forEachVisualItem()` APIs already support a clean snapshot-and-render split; a single mutex at the sim boundary would suffice. The `ArenaSimulation` used by the balancing tool runs headlessly on a worker thread; fire events accumulate in its internal vector and are only drained when `ArenaView` drives `tickOnce()` on the main thread during interactive inspection.
|
||||||
|
|
||||||
### Layer Order (back to front)
|
### Layer Order (back to front)
|
||||||
|
|
||||||
@@ -289,15 +374,17 @@ Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly
|
|||||||
3. **Belt items** — 10×10 colored squares emitted by `BeltSystem::forEachVisualItem`.
|
3. **Belt items** — 10×10 colored squares emitted by `BeltSystem::forEachVisualItem`.
|
||||||
4. **Scrap** — glyphs at world positions.
|
4. **Scrap** — glyphs at world positions.
|
||||||
5. **Ships** — colored arrows oriented by velocity; color keyed to role (player combat / salvage / repair / enemy).
|
5. **Ships** — colored arrows oriented by velocity; color keyed to role (player combat / salvage / repair / enemy).
|
||||||
6. **Laser beams** — lines derived from live `FireEvent`s kept by the renderer for 0.3 s (REQ-SHP-FIRING-BEAM).
|
6. **Laser beams** — lines derived from live `BeamFiredEvent`s kept by the renderer for 0.3 s, colored per `BeamKind` (weapon/repair/salvage) (REQ-SHP-FIRING-BEAM).
|
||||||
7. **Build overlays** — ghost in builder mode (REQ-BLD-GHOST), demolish-mode tint, tile highlight under cursor, box-drag selection rectangle.
|
7. **Build overlays** — ghost in builder mode (REQ-BLD-GHOST), deconstruct-mode tint, tile highlight under cursor, box-drag selection rectangle.
|
||||||
8. **Screen-space UI** — schematic toasts (REQ-UI-SCHEMATIC-TOAST) and any other screen-anchored elements, drawn after resetting the world-space transform.
|
8. **Screen-space UI** — screen-anchored elements, drawn after resetting the world-space transform.
|
||||||
|
|
||||||
### Coordinates and Scrolling
|
### Coordinates and Scrolling
|
||||||
|
|
||||||
- `GameWorldView` holds a continuous `scrollXTiles` (float). A / D input pans this smoothly (REQ-UI-SCROLL).
|
- The horizontal view position lives in `WorldCamera` (`lib/core/`) as a continuous view-center X in tiles. A / D input pans it smoothly (REQ-UI-SCROLL) at a position-dependent speed (REQ-UI-SCROLL-SPEED). The camera works purely in world units — tiles and tiles/second, never pixels — which is what keeps it independent of `WorldCoordinates`; the two meet only where `GameWorldView` feeds `getViewCenterXTiles()` into the transform.
|
||||||
- At the start of `paintEvent`, a single `painter.translate(-scrollXTiles * tilePx, 0)` maps world tile units into widget pixels (`tilePx = 20`, per REQ-GW-TILE-SIZE).
|
- The camera takes no simulation dependency. Its pan limits move with asteroid expansion and with pushes, so `GameWorldView` reads them from the sim each frame and passes them in as `ScrollBounds`; the camera clamps on every `advance()`, not only when panning, so the view follows the bounds inward when they shrink. Pan *intent* is likewise passed in as a `PanDirection` rather than read from key state, so the camera is unaffected if controls later become rebindable. Both properties are what make it a plain value with unit tests (`WorldCameraTest`) — notably over the two-ramp pan-speed curve, whose overlapping-band and zero-width-band cases are otherwise easy to break unnoticed.
|
||||||
- Mouse input converts the other way: `worldX = mouseX / tilePx + scrollXTiles`; apply `floor` for a tile. Asteroid tiles (`x < 0`) need no special casing — they share the coordinate system with space tiles.
|
- The world↔widget transform itself lives in `WorldCoordinates` (`lib/core/`), not in the view. It is an immutable value, built through one of two named factories that differ only in how `tilePx` and the left edge are derived; everything downstream is shared. `scrolling(...)` is the game world: `tilePx` makes the world height fill the viewport (REQ-GW-TILE-SIZE) and the view pans horizontally. `fitToWorld(...)` is the balancing tool's arena: a fixed world shown whole, so `tilePx` is the tighter of the two axis fits and there is no scroll. Being a plain value with no Qt Widgets dependency, it is unit-tested (`WorldCoordinatesTest`) even though the widgets around it are not.
|
||||||
|
- `GameWorldView::getCoordinates()` and `ArenaView::getCoordinates()` each build one per frame in `paintGL` and per event in the mouse handlers, and pass it down: every world-space `draw<X>` takes a `const WorldCoordinates&`, while the screen-space draws (vignette borders, replay overlay, debug text) take none. The snapshot is deliberately never cached in a member — a resize or a scroll would silently invalidate it.
|
||||||
|
- Conversions are per-call arithmetic rather than a `painter.translate`, because hit-testing needs the inverse (`widgetToWorld` / `widgetToTile`, flooring for a tile) as often as drawing needs the forward direction. Asteroid tiles (`x < 0`) need no special casing — they share the coordinate system with space tiles, which is why the flooring must not be truncation.
|
||||||
|
|
||||||
### Culling
|
### Culling
|
||||||
|
|
||||||
@@ -307,6 +394,8 @@ The renderer iterates only entities and tiles whose world X lies within the visi
|
|||||||
|
|
||||||
Shapes are hardcoded in the renderer — a building is a rectangle per footprint tile, a ship is an oriented arrow/triangle, a belt item is a 10×10 square, scrap is a small circle, a beam is a line. These structural choices live in the `draw<X>(painter, entity)` functions of the UI and are not expected to change frequently.
|
Shapes are hardcoded in the renderer — a building is a rectangle per footprint tile, a ship is an oriented arrow/triangle, a belt item is a 10×10 square, scrap is a small circle, a beam is a line. These structural choices live in the `draw<X>(painter, entity)` functions of the UI and are not expected to change frequently.
|
||||||
|
|
||||||
|
The few shapes the game view and the balancing tool's arena view draw *identically* — the ship body, the health bar, the debris marker, the sensor-range circle — live in `ui/WorldPrimitives` as free functions over explicit values. The arena exists to eyeball combat, so it only works while a ship there looks like a ship in the game; keeping these in one place means a retuned ship shape cannot silently stop applying to the tool that measures it. The balancing target does not link the `ui` library, so it compiles that file into itself, the same way it already does for `VisualsLoader` and `ShipStatsPanel` (see `balancing/CMakeLists.txt`). Everything the two views draw differently — selection highlights, beams, target lines, and all of the factory — stays with each view; the shared set is deliberately not grown beyond shapes that are genuinely the same.
|
||||||
|
|
||||||
Colors, outline widths, glyph text, and tile tints live in a separate config file, `visuals.toml`, loaded once by the UI at startup using the same pattern and lifetime as the sim config files (see Config Loading). The file is UI-scoped: the sim does not read it and does not depend on it.
|
Colors, outline widths, glyph text, and tile tints live in a separate config file, `visuals.toml`, loaded once by the UI at startup using the same pattern and lifetime as the sim config files (see Config Loading). The file is UI-scoped: the sim does not read it and does not depend on it.
|
||||||
|
|
||||||
Sketch of `visuals.toml`:
|
Sketch of `visuals.toml`:
|
||||||
@@ -337,8 +426,8 @@ width_px = 2
|
|||||||
[overlays]
|
[overlays]
|
||||||
ghost_valid = "#ffffff44"
|
ghost_valid = "#ffffff44"
|
||||||
ghost_invalid = "#ff000044"
|
ghost_invalid = "#ff000044"
|
||||||
demolish_tint = "#ff000033"
|
deconstruct_tint = "#ff000033"
|
||||||
selection_rect = "#00ff00"
|
selected_outline = "#ffff00"
|
||||||
|
|
||||||
[toast]
|
[toast]
|
||||||
bg = "#000000cc"
|
bg = "#000000cc"
|
||||||
|
|||||||
56
docs/balancing/README.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Balancing Documentation
|
||||||
|
|
||||||
|
Everything about balancing Dota Factory, separated by role:
|
||||||
|
|
||||||
|
- **[rules.md](rules.md)** — the design rules and principles. Timeless;
|
||||||
|
changes only when the design changes.
|
||||||
|
- **[targets.md](targets.md)** — the base numbers (roots/anchors) chosen
|
||||||
|
by design. Change these first; everything else re-derives.
|
||||||
|
- **[derived.md](derived.md)** — the current tuned state of all derived
|
||||||
|
numbers, mirroring the configs. Updated whenever configs change.
|
||||||
|
- **[process.md](process.md)** — how balancing is done: the pass order,
|
||||||
|
tuning discipline, tools, and the checklist for the next round.
|
||||||
|
- **[history.md](history.md)** — chronological record of decisions,
|
||||||
|
findings, bugs, and arena rounds.
|
||||||
|
|
||||||
|
Related: game content (hull grids, footprint gating, tree design and
|
||||||
|
fiction) in [../content_design.md](../content_design.md); rules with
|
||||||
|
REQ-* ids in [../requirements.md](../requirements.md).
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
First full balancing round complete (2026-07-06): targets → tree →
|
||||||
|
numbers → threat-calculator parity → combat stats (arena-converged) →
|
||||||
|
pacing. Next step: full-game playtests against the run-shape targets in
|
||||||
|
`targets.md`.
|
||||||
|
|
||||||
|
## Open action items
|
||||||
|
|
||||||
|
Agreed changes that require edits to `requirements.md`, the code, or the
|
||||||
|
configs. Completed items are removed (their outcomes live in
|
||||||
|
`requirements.md`, `history.md`, and the git history).
|
||||||
|
|
||||||
|
1. **Fill unfillable schematic slots with artifacts.** With duplicates
|
||||||
|
removed, the schematic drop pool can run dry — previously unreachable.
|
||||||
|
Decision: every slot in the choice dialog that cannot be filled with a
|
||||||
|
schematic because the eligible pool is exhausted is filled with an
|
||||||
|
artifact option instead (in addition to any artifact option granted by
|
||||||
|
the regular artifact roll). A push therefore always awards a full
|
||||||
|
dialog. Update REQ-DEF-SCHEMATIC-DROP.
|
||||||
|
2. **Confirm wave scaling in playtests.** `threat_rate_formula` is the
|
||||||
|
only time-scaling axis; verify the tuned curve (see `derived.md`)
|
||||||
|
produces the intended difficulty race in real runs.
|
||||||
|
3. **Gate shortcut-recipe drops on their inputs.** Extend the assembler
|
||||||
|
recipe schematic pool eligibility in REQ-DEF-SCHEMATIC-DROP: in
|
||||||
|
addition to the existing station-level and output-item checks, all of
|
||||||
|
the recipe's input item types must be implicitly unlocked as well.
|
||||||
|
4. **Resource deposits.** Add a terrain deposit layer per the Resource
|
||||||
|
deposits rules (`rules.md`): deposit patches generated in expansion
|
||||||
|
columns (deterministic content per expansion, randomized placement
|
||||||
|
within the new columns), deposit rendering, and a miner condition (a
|
||||||
|
resource recipe is selectable only if the miner's footprint overlaps
|
||||||
|
at least one matching deposit tile). Touches REQ-BLD-MINER ("every
|
||||||
|
asteroid tile is equivalent" no longer holds),
|
||||||
|
REQ-GW-ASTEROID-EXPAND / REQ-EXP-*, `world.toml`, and `visuals.toml`.
|
||||||
|
Until this lands, quartz mines anywhere and the mid-game is
|
||||||
|
knowledge-gated only.
|
||||||
164
docs/balancing/derived.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Derived Values (current tuned state)
|
||||||
|
|
||||||
|
Everything here is derived from `targets.md` under the rules in
|
||||||
|
`rules.md`, and mirrors the config files. Item threats, ship threats,
|
||||||
|
ratios, and belt checks are verified by `tools/threat_report.py` — re-run
|
||||||
|
it after any recipe or material change and update this file when values
|
||||||
|
move. Combat stats were tuned empirically against the arena suite in
|
||||||
|
`bin/balancing/data/balancing.toml` (round-by-round record in
|
||||||
|
`history.md`).
|
||||||
|
|
||||||
|
## Economy constants
|
||||||
|
|
||||||
|
- `scrap_per_threat = 0.25` — 1 scrap per 4 threat destroyed (a cruiser
|
||||||
|
kill drops ~59 scrap); threat(scrap) = 4.
|
||||||
|
- Scrap smelting: 1 scrap → 1 iron_ingot, 1 s — deliberately
|
||||||
|
value-losing; reprocessing is the value-preserving path.
|
||||||
|
- Reprocessing: 4 scrap per cycle, 4 s; full-pool weights iron_ingot 30 /
|
||||||
|
copper_ingot 30 / silicon 20 / voidsteel 20 → threat(voidsteel)
|
||||||
|
= (4·4 + 4)/0.2 = 100.
|
||||||
|
- `debris_despawn_seconds = 120` (a capital kill drops hundreds of scrap,
|
||||||
|
collected one per salvage cycle).
|
||||||
|
|
||||||
|
## Recipes and item threats
|
||||||
|
|
||||||
|
(dur in seconds; threat is per output unit)
|
||||||
|
|
||||||
|
| item | recipe | dur | out | threat |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| iron_ore / copper_ore | miner | 1 | 1 | 1 |
|
||||||
|
| quartz | miner (deposit) | 2 | 1 | 2 |
|
||||||
|
| iron_ingot | 1 iron_ore | 1 | 1 | 2 |
|
||||||
|
| copper_ingot | 1 copper_ore | 1 | 1 | 2 |
|
||||||
|
| silicon | 1 quartz | 2 | 1 | 4 |
|
||||||
|
| steel_plate | 2 iron_ingot | 3 | 1 | 7 |
|
||||||
|
| copper_wire | 1 copper_ingot | 1 | 2 | 1.5 |
|
||||||
|
| copper_coil | 2 copper_wire | 1.5 | 1 | 4.5 |
|
||||||
|
| building_block | 2 steel_plate | 2 | 4 | 4 |
|
||||||
|
| control_chip | 1 silicon + 2 copper_wire | 5 | 1 | 12 |
|
||||||
|
| capacitor_bank | 2 copper_coil + 1 silicon | 5 | 1 | 18 |
|
||||||
|
| hardened_steel | 3 steel_plate | 12 | 1 | 33 |
|
||||||
|
| ceramic_plate | 2 quartz | 4 | 1 | 8 |
|
||||||
|
| drive_unit | 2 steel_plate + 2 copper_coil + 1 control_chip | 8 | 1 | 43 |
|
||||||
|
| voidsteel_plate | 1 voidsteel + 1 hardened_steel | 8 | 1 | 141 |
|
||||||
|
| capital_core | 2 voidsteel + 1 capacitor_bank + 1 control_chip | 10 | 1 | 240 |
|
||||||
|
|
||||||
|
Shortcut recipes (drop-only; item threat stays defined by the base path
|
||||||
|
via the max rule): `direct_rolling` 3 iron_ore → 1 plate (2 s,
|
||||||
|
level 1), `direct_etching` 2 quartz → 1 chip (4 s, level 2),
|
||||||
|
`direct_hardening` 4 iron_ingot → 1 hardened (8 s, level 2).
|
||||||
|
|
||||||
|
Ratio curve realized: t1 all 1:1 (miner:smelter); t2 clean 2:3
|
||||||
|
(ingot→plate, wire→coil); t3 strange — 2:5 (silicon→chip), 3:5
|
||||||
|
(coil→capacitor), 3:4 (plate→hardened, plate→drive); t4 inverted 3:2
|
||||||
|
(hardened→voidsteel_plate). Belt check: worst input demand 1.33 items/s,
|
||||||
|
under the ~2/s single-belt cap everywhere.
|
||||||
|
|
||||||
|
## Module prefabs
|
||||||
|
|
||||||
|
(contribution = item threat + module production time)
|
||||||
|
|
||||||
|
| module | recipe | dur | mod. time | contribution |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| railgun_s | 1 copper_coil | 1 | 1 | 6.5 |
|
||||||
|
| salvager | 1 steel_plate + 2 copper_wire | 2 | 1 | 13 |
|
||||||
|
| repair_tool | 1 steel_plate + 2 copper_wire | 2 | 1 | 13 |
|
||||||
|
| armor_plates | 4 steel_plate | 3 | 1 | 32 |
|
||||||
|
| maneuvering_thrusters | 1 steel_plate + 1 copper_coil | 2 | 1 | 14.5 |
|
||||||
|
| sensor_booster | 2 copper_wire + 1 copper_coil | 2 | 1 | 10.5 |
|
||||||
|
| afterburner | 2 copper_coil + 1 steel_plate | 3 | 1 | 20 |
|
||||||
|
| weapon_stabilizer | 1 steel_plate + 1 copper_coil | 2 | 1 | 14.5 |
|
||||||
|
| weapon_primer | 1 capacitor_bank + 1 copper_coil | 4 | 2 | 28.5 |
|
||||||
|
| weapon_upgrade | 1 control_chip + 1 copper_coil | 4 | 2 | 22.5 |
|
||||||
|
| railgun_m | 1 capacitor_bank + 2 steel_plate + 1 copper_coil | 4 | 3 | 43.5 |
|
||||||
|
| drone_bay | 1 control_chip + 2 steel_plate + 1 copper_coil | 4 | 3 | 37.5 |
|
||||||
|
| railgun_l | 1 capacitor_bank + 2 hardened_steel + 1 ceramic_plate | 6 | 4 | 102 |
|
||||||
|
| drone_hangar | 1 voidsteel_plate + 2 control_chip + 1 drive_unit | 10 | 6 | 224 |
|
||||||
|
|
||||||
|
## Ships
|
||||||
|
|
||||||
|
(fitted = hull item + ship base time + default loadout; the default
|
||||||
|
loadouts are the `default_modules` used by enemy waves and are
|
||||||
|
geometry-validated against the hull grids)
|
||||||
|
|
||||||
|
| ship | hull recipe | dur | base | default loadout | fitted |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| drone | 1 iron_ingot | 1 | 1 | railgun_s | 10.5 |
|
||||||
|
| frigate | 2 steel_plate + 1 copper_wire | 2 | 2 | 2× railgun_s, maneuvering_thrusters | 47 |
|
||||||
|
| destroyer | 3 steel_plate + 2 copper_coil | 4 | 3 | 3× railgun_s, armor_plates, sensor_booster | 99 |
|
||||||
|
| cruiser | 2 hardened_steel + 2 control_chip | 6 | 4 | 2× railgun_m, armor_plates, maneuvering_thrusters | 233.5 |
|
||||||
|
| battlecruiser | 3 hardened_steel + 2 control_chip + 1 drive_unit | 8 | 5 | 3× railgun_m, armor_plates, 2× railgun_s | 354.5 |
|
||||||
|
| battleship | 3 voidsteel_plate + 1 drive_unit + 2 control_chip | 10 | 6 | railgun_l, 2× railgun_m, weapon_stabilizer, 2× railgun_s | 722.5 |
|
||||||
|
| dreadnought | 5 voidsteel_plate + 1 capital_core + 2 drive_unit | 12 | 8 | 3× railgun_l, 4× armor_plates, railgun_s | 1491.5 |
|
||||||
|
| carrier | 5 voidsteel_plate + 1 capital_core + 2 drive_unit | 12 | 8 | drone_hangar, 2× railgun_m, 2× armor_plates, sensor_booster | 1436.5 |
|
||||||
|
|
||||||
|
## Combat stats
|
||||||
|
|
||||||
|
(arena-converged, 2026-07; see `history.md` rounds 1–5)
|
||||||
|
|
||||||
|
**Weapons:** railgun_s 2 dmg × 2.0 Hz (4.0 DPS), range 50 m;
|
||||||
|
railgun_m 14 × 1.5 (21), range 70; railgun_l 52 × 0.8 (41.6), range 100.
|
||||||
|
|
||||||
|
**Hull HP** (15/threat prior + empirical trims): drone 60, frigate 300,
|
||||||
|
destroyer 550, cruiser 1500, battlecruiser 2400, battleship 6300,
|
||||||
|
dreadnought/carrier 24000.
|
||||||
|
|
||||||
|
**Mobility ladder** (speed m/s | main accel | maneuvering | angular |
|
||||||
|
max rot): drone 45|60|30|12|6, frigate 35|45|22|8|4,
|
||||||
|
destroyer 30|35|18|6|3, cruiser 24|25|12|4|2, battlecruiser 20|20|10|3|1.5,
|
||||||
|
battleship 15|14|7|2|1, dreadnought/carrier 10|8|4|1|0.5.
|
||||||
|
Sensors: 150/200/220/250/260/280/300/350 m.
|
||||||
|
|
||||||
|
**Other modules:** armor_plates +1200 HP; repair_tool 9 HP × 1 Hz,
|
||||||
|
range 80; salvager range 60, cargo 20, 0.5 collections/s; afterburner
|
||||||
|
×1.6 speed +60 accel; maneuvering_thrusters ×1.2 speed +10 maneuvering;
|
||||||
|
sensor_booster +50 m; weapon_upgrade ×1.2 damage; weapon_primer ×1.2
|
||||||
|
rate; weapon_stabilizer ×1.3 range ×0.8 rate.
|
||||||
|
|
||||||
|
**Stations:** HQ 5000 HP. Player station 3000 HP, 25 dmg × 1 Hz,
|
||||||
|
range 120, scrap 40. Enemy station: 3000+1500x HP, 25+12x dmg,
|
||||||
|
1.0+0.1x Hz, range 120, scrap 40+30x (x = push level).
|
||||||
|
|
||||||
|
## Pacing
|
||||||
|
|
||||||
|
**Unlock ladder** (level → unlocks; ← marks `unlock_requires`; starting
|
||||||
|
set at −1: drone, frigate, railgun_s, salvager, block_pressing recipe):
|
||||||
|
|
||||||
|
| level | ships | modules | recipes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0 | destroyer | repair_tool, armor_plates | |
|
||||||
|
| 1 | | maneuvering_thrusters, sensor_booster | direct_rolling |
|
||||||
|
| 2 | cruiser | railgun_m, afterburner | direct_etching, direct_hardening |
|
||||||
|
| 3 | | weapon_stabilizer | |
|
||||||
|
| 4 | battlecruiser ← cruiser | weapon_primer, weapon_upgrade | |
|
||||||
|
| 5 | | drone_bay | |
|
||||||
|
| 6 | battleship ← battlecruiser | railgun_l ← railgun_m | |
|
||||||
|
| 8 | dreadnought ← battleship | | |
|
||||||
|
| 9 | carrier ← battleship | drone_hangar | |
|
||||||
|
|
||||||
|
Level 0's pool has exactly three entries (a full first dialog). Level 2
|
||||||
|
is the quartz gate: cruiser and railgun_m are the first schematics whose
|
||||||
|
chains reach quartz; the shortcut outputs only become implicitly
|
||||||
|
unlocked alongside them, so shortcuts cannot drop early.
|
||||||
|
|
||||||
|
**Threat rate** `2*x + 0.15*x*x` (x = boss cycle counter), against the
|
||||||
|
factory-size curve with ~half the player's output assumed military:
|
||||||
|
|
||||||
|
| cycle x | rate (threat/s) | player military (≈ curve/2) |
|
||||||
|
|---|---|---|
|
||||||
|
| 2 | 4.6 | ~12 |
|
||||||
|
| 6 | 17.4 | ~30 |
|
||||||
|
| 15 | 63.8 | ~60 |
|
||||||
|
| 20 | 100 | ~75 |
|
||||||
|
| 24 | 134 | — |
|
||||||
|
|
||||||
|
**Economy:** `starting_building_blocks = 200`; expansion cost formula
|
||||||
|
`300 + 50*x + 10*x*x` (x = expansions already purchased: ~1 affordable
|
||||||
|
per cycle mid-game at ~1/3 of block income, stretching to 2–3 cycles
|
||||||
|
late — quadratic so costs outrun the roughly linear block income
|
||||||
|
gradually, never with a hard wall); `artifact_win_count = 5` with
|
||||||
|
`artifact_chance_formula = 0.05*x`. Building costs: belt 2, splitter 3,
|
||||||
|
tunnels 5, miner 15, smelter 20, assembler 35, reprocessing plant 40,
|
||||||
|
salvage bay 25, shipyard 60 — averaging ≈18 blocks per placed building
|
||||||
|
(belts included), which meets the 4-minute doubling target at block
|
||||||
|
threat 4.
|
||||||
96
docs/balancing/history.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# Balancing History
|
||||||
|
|
||||||
|
Chronological record of the balancing work: what was decided, what was
|
||||||
|
found, what changed. Current values live in `derived.md`; this file
|
||||||
|
explains how they got there.
|
||||||
|
|
||||||
|
## 2026-07-02/03 — rules and structural decisions
|
||||||
|
|
||||||
|
- Rules document written (now `rules.md`): ratio curve, shortcut
|
||||||
|
recipes, refactorability, cost archetypes, threat model, growth curve.
|
||||||
|
- Scrap derived from threat (`scrap_per_threat`), replacing authored
|
||||||
|
per-ship scrap drops; scrap threat became the constant
|
||||||
|
`1/scrap_per_threat`, removing the old min-scrap_drop derivation and
|
||||||
|
its circularity.
|
||||||
|
- Duplicate schematic drops removed (no level-ups); ship/module levels
|
||||||
|
removed entirely — all time scaling lives in the threat rate, push
|
||||||
|
scaling stays on stations. Mk2 upgrade recipes noted as the future
|
||||||
|
per-item progression.
|
||||||
|
- Growth-curve rules added: escalating expansion costs, designed
|
||||||
|
doubling time, growth limited by economy not waiting; resource
|
||||||
|
deposits designed (deposit-gated mid resource in expansion territory).
|
||||||
|
- Production tree v2 decided: iron/copper everywhere (M-type asteroid),
|
||||||
|
quartz in geodes (mid), voidsteel battle-forged from scrap (late);
|
||||||
|
titanium dropped; lasers renamed to railguns, lasers reserved as a
|
||||||
|
future weapon type.
|
||||||
|
|
||||||
|
## 2026-07-03 — targets, tree, numbers
|
||||||
|
|
||||||
|
- Balancing targets fixed: ≤2 h run, phases 1–5/6–14/15+, factory curve
|
||||||
|
25/60/120/150, threat ladder, 25-ship swarm, block roots.
|
||||||
|
- Tree structure drafted and numbers computed (recursive threat
|
||||||
|
calculator); ratio curve realized; fitted ships within 96–124% of the
|
||||||
|
strawman ladder (small end hot from fixed chain overhead — ladder
|
||||||
|
later adopted the achieved values).
|
||||||
|
- **Rule bugs found by the numbers work:** the scrap→ingot smelter
|
||||||
|
recipe would inflate basic materials via the max rule (fixed:
|
||||||
|
scrap-consuming recipes are threat fallback only); recipe output
|
||||||
|
amounts were ignored (fixed: per-unit division); items downstream of
|
||||||
|
reprocessing-only items never resolved (fixed: fixpoint resolution);
|
||||||
|
a shortcut recipe resolving earlier than the base path silently
|
||||||
|
underpriced items (fixed: commit only when all eligible recipes are
|
||||||
|
computable). All four fixed in `ThreatCostCalculator` with tests, and
|
||||||
|
implemented in `tools/threat_report.py`.
|
||||||
|
- v2 tree written into the configs; `default_modules` loadouts
|
||||||
|
geometry-validated (the numbers-pass loadouts for battlecruiser and
|
||||||
|
dreadnought were geometrically impossible — L-modifiers don't fit
|
||||||
|
beside full gun complements; corrected loadouts landed closer to the
|
||||||
|
ladder).
|
||||||
|
|
||||||
|
## 2026-07-04 — combat stats, arena rounds 1–5
|
||||||
|
|
||||||
|
Initial stats derived from the anchors (weapon DPS ≈0.6/threat flat,
|
||||||
|
hull 15 HP/threat, armor 20/threat, repair 2 HP/s/threat, station
|
||||||
|
range 200).
|
||||||
|
|
||||||
|
- **Round 1:** concentrated fleets won all equal-threat cross-tier
|
||||||
|
matchups flawlessly; glass beat armored; repair escort flawless; two
|
||||||
|
stations shrugged off a 3× swarm. Changes: concentration tax on m/l
|
||||||
|
gun damage (railgun_m 17→14, railgun_l 70→52), armor 640→1000,
|
||||||
|
repair 25→12, station range 200→120. (Team-1 "bias" in mirrors later
|
||||||
|
shown to be noise.)
|
||||||
|
- **Round 2 (EHP-margin logging added):** battleship +33% while
|
||||||
|
dreadnought −37% (stabilizer range + opposing armor); glass still
|
||||||
|
+11%. Changes: stabilizer range ×1.5→×1.3; per-hull trims introduced
|
||||||
|
(BC 2700→2500, BS 7500→7000, DN/CV 15500→19000).
|
||||||
|
- **Round 3 (narrow lanes — geometry fixed into the fixture):**
|
||||||
|
DN closed to −11%, BS +22%, swarm flipped to +14% over cruisers,
|
||||||
|
glass +12% third time. Changes: armor 1000→1200, BC 2500→2000,
|
||||||
|
BS 7000→6300, DN/CV 19000→22500.
|
||||||
|
- **Round 4:** glass-vs-armored resolved (+3% armored); noise floor
|
||||||
|
established (~±10%/run: BS ignored a −10% EHP cut; repair drifted
|
||||||
|
14→24% untouched). Convergence policy adopted: two-round signals only,
|
||||||
|
±20% converged. Changes: BC 2000→2200, DN/CV 22500→24000; BS +23%
|
||||||
|
accepted as doctrine texture (mechanical range edge vs. pure small
|
||||||
|
fleets).
|
||||||
|
- **Round 5 (durations logged; end-condition bug fixed upstream):**
|
||||||
|
TTK anchor validated (mirrors 23/71/95 s; DN-vs-swarm 214 s outlier
|
||||||
|
accepted); dreadnought +3%, everything else inside band. Final
|
||||||
|
changes: BC 2200→2400, repair 12→9 (persistent +24% escort margin).
|
||||||
|
**Combat pass declared converged.**
|
||||||
|
|
||||||
|
## 2026-07-05/06 — pacing pass
|
||||||
|
|
||||||
|
- Unlock ladder set (starting set drone/frigate/railgun_s/salvager;
|
||||||
|
quartz gate at level 2; capitals at 8–9 with `unlock_requires`
|
||||||
|
chains); threat rate `2*x + 0.15*x*x`; starting blocks 1000→200;
|
||||||
|
expansion 400 flat pending the cost formula; artifacts 3→5.
|
||||||
|
- **Bug found:** the building_block recipe was silently locked at game
|
||||||
|
start (building blocks appear in no schematic's materials, so implicit
|
||||||
|
unlocking could never reach the recipe) — fixed with an explicit
|
||||||
|
`unlock_at_station_level = -1`.
|
||||||
|
- Expansion cost formula implemented and set (`300 + 50*x + 10*x*x`):
|
||||||
|
quadratic, so costs outrun the roughly linear block income gradually
|
||||||
|
— ~1 expansion per cycle mid-game, 2–3 cycles apart late.
|
||||||
|
- **First full balancing round complete.** Next: full-game playtests
|
||||||
|
against the run-shape targets.
|
||||||
98
docs/balancing/process.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# Balancing Process
|
||||||
|
|
||||||
|
How balancing is done in this project: the pass order, the tuning
|
||||||
|
discipline, and the tools. Refer to this when starting the next
|
||||||
|
balancing round.
|
||||||
|
|
||||||
|
## The pass order
|
||||||
|
|
||||||
|
Each pass depends on the ones before it; a change in an earlier pass
|
||||||
|
invalidates the later ones (but not vice versa). Redo from the earliest
|
||||||
|
pass whose inputs changed.
|
||||||
|
|
||||||
|
1. **Targets** (`targets.md`) — choose the root numbers: run shape,
|
||||||
|
factory curve, threat-cost ladder, fleet size, block roots, combat
|
||||||
|
anchors, pacing anchors. These are design decisions, not
|
||||||
|
measurements. Everything else is derived from them.
|
||||||
|
2. **Tree structure** (`../content_design.md`) — items, chains,
|
||||||
|
what-consumes-what, per the production tree rules (one input per
|
||||||
|
phase transition, generic parts, archetypes, refactorability).
|
||||||
|
Structure only, no quantities.
|
||||||
|
3. **Numbers** (`derived.md`, recipes/materials in the configs) —
|
||||||
|
quantities and durations so every fitted ship sums to its ladder
|
||||||
|
value, the ratio curve is realized, and the belt/buffer guardrails
|
||||||
|
hold. Verified computationally by `tools/threat_report.py`.
|
||||||
|
4. **Calculator/tooling parity** — the game's `ThreatCostCalculator`
|
||||||
|
and `tools/threat_report.py` must produce identical values; the
|
||||||
|
Python tool is the design reference. Any semantic change to
|
||||||
|
REQ-THREAT-* needs both updated plus tests.
|
||||||
|
5. **Combat stats** (arena-driven) — derive stats from the combat
|
||||||
|
anchors, then iterate against the arena suite
|
||||||
|
(`bin/balancing/data/balancing.toml`) until equal-threat matchups are
|
||||||
|
near-draws. Threat costs are stat-independent, so arena ship counts
|
||||||
|
stay valid across stat changes.
|
||||||
|
6. **Pacing** — unlock ladder, `unlock_requires` edges, threat rate,
|
||||||
|
block/artifact/expansion values, per the pacing anchors.
|
||||||
|
|
||||||
|
Then: **full-game playtests**, which are the only check for the pacing
|
||||||
|
pass and feed back into targets.
|
||||||
|
|
||||||
|
## Tuning discipline (learned in arena rounds 1–5)
|
||||||
|
|
||||||
|
- **Change anchors, not symptoms.** When a class of results is off,
|
||||||
|
adjust the anchor that explains all of them (e.g. the concentration
|
||||||
|
tax) rather than individual stats.
|
||||||
|
- **Fewest knobs per round.** Attribution dies when many knobs move at
|
||||||
|
once. Prefer one anchor change plus its mechanical compensations.
|
||||||
|
- **Shared vs. local knobs.** Guns and module stats are shared across
|
||||||
|
many hulls — changing them moves many matchups. Per-hull HP moves
|
||||||
|
exactly one matchup; it is the designated per-ship trim knob on top of
|
||||||
|
the HP-per-threat prior.
|
||||||
|
- **Mind the ride-alongs.** A module buff lands on every default loadout
|
||||||
|
containing it (e.g. an armor buff strengthens the destroyer swarm that
|
||||||
|
opposes the dreadnought). Compute the net effect per matchup before
|
||||||
|
choosing step sizes.
|
||||||
|
- **Two-round signal policy.** Single arena runs re-roll by ~±10% EHP
|
||||||
|
margin; a margin inside ±20% counts as converged for v1. Only act on
|
||||||
|
signals that persist across two rounds.
|
||||||
|
- **Arena geometry is part of the fixture.** Lane width/height changes
|
||||||
|
the results (full engagement vs. fleets slipping past); margins are
|
||||||
|
only comparable within the same geometry.
|
||||||
|
- **Accept mechanical texture.** Not every deviation is a bug: a margin
|
||||||
|
that survives a stat change is mechanical (usually range/kiting under
|
||||||
|
the orbit AI) and may be desirable doctrine texture. Document the
|
||||||
|
acceptance in `targets.md` instead of chasing it.
|
||||||
|
- **Range is the strongest stat** under the orbit AI — free approach
|
||||||
|
fire. Price range modifiers conservatively; station dominance is
|
||||||
|
controlled via range, not HP.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
- `tools/threat_report.py` — item threats, module contributions,
|
||||||
|
hull/fitted ship threats, producer:consumer ratios, belt feasibility;
|
||||||
|
reads the real configs. The design reference for threat semantics.
|
||||||
|
- `tools/verify_recipes.py` — recipe tree closure, visuals coverage,
|
||||||
|
orphans, reprocessing-only items.
|
||||||
|
- `tools/verify_layouts.py` — module footprint gating matrix per hull.
|
||||||
|
- **Balancing tool** (`balancing` target) — parallel arena simulation of
|
||||||
|
`bin/balancing/data/balancing.toml`; logs winner, surviving counts,
|
||||||
|
team EHP %, and fight duration per arena. The suite covers: class
|
||||||
|
mirrors (expect near-mutual annihilation, symmetric winners),
|
||||||
|
equal-threat cross-tier matchups (expect near-draws — power-per-threat
|
||||||
|
made empirical), a 2:1 decisiveness check, doctrine matchups
|
||||||
|
(armored-vs-glass, repair-escort), and station assault.
|
||||||
|
|
||||||
|
## Checklist for the next balancing round
|
||||||
|
|
||||||
|
1. Pull; run `verify_recipes.py`, `verify_layouts.py`,
|
||||||
|
`threat_report.py`; compare against the tables in `derived.md`.
|
||||||
|
2. If recipes/materials changed: re-check fitted threats vs. the ladder
|
||||||
|
in `targets.md`; update arena suite ship counts if fitted values
|
||||||
|
moved.
|
||||||
|
3. Run the arena suite; read EHP margins and durations against the
|
||||||
|
expectations noted in `balancing.toml` and the anchors.
|
||||||
|
4. Apply changes per the tuning discipline (two-round signals only);
|
||||||
|
record the round and its knob changes in `history.md`.
|
||||||
|
5. Update `derived.md` where values moved; if an anchor moved, update
|
||||||
|
`targets.md` and state why.
|
||||||
|
6. Commit and push (the review workflow reads the remote).
|
||||||
333
docs/balancing/rules.md
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
# Balancing & Progression Rules
|
||||||
|
|
||||||
|
Rules and principles that govern the production tree, progression pacing,
|
||||||
|
and balancing. This document contains **rules only** — the chosen base
|
||||||
|
numbers live in `targets.md`, everything derived from them in
|
||||||
|
`derived.md`, and the concrete content in the config files and
|
||||||
|
`../content_design.md`. All of those must follow the rules stated here.
|
||||||
|
|
||||||
|
## Player-experience goals
|
||||||
|
|
||||||
|
What each phase of a run should feel like:
|
||||||
|
|
||||||
|
- **Early:** learning belts and ratios with forgiving chains. The building
|
||||||
|
block economy is the main constraint; the player bootstraps a
|
||||||
|
self-sustaining factory from the starting stock.
|
||||||
|
- **Mid:** deeper chains, the first real ratio puzzles, and the first
|
||||||
|
meaningful drop decisions (which schematic, when to push).
|
||||||
|
- **Late:** combat feeds the factory — capital production requires salvage.
|
||||||
|
Progress means extending and refactoring the existing factory, not
|
||||||
|
rebuilding it. Strange ratios are deliberate optimization puzzles.
|
||||||
|
|
||||||
|
Overarching: an experienced player gains efficiency through **knowledge** —
|
||||||
|
layout foresight, understanding chains, exploiting shortcut recipes — never
|
||||||
|
through hidden mechanics. An inexperienced setup should not cost much more
|
||||||
|
than an experienced one; experience pays off in how easily the factory
|
||||||
|
adapts later (see Refactorability).
|
||||||
|
|
||||||
|
## Resource phases
|
||||||
|
|
||||||
|
- A run has exactly **four base inputs**:
|
||||||
|
1. Two mined resources available from the start, minable on **every**
|
||||||
|
asteroid tile.
|
||||||
|
2. A third mined resource unlocked mid-game, minable **only on deposit
|
||||||
|
patches** found in expansion territory (see Resource deposits).
|
||||||
|
3. A fourth input unlocked late-game, obtainable **only** from
|
||||||
|
reprocessing salvaged scrap.
|
||||||
|
- The fourth input is the core loop hook: capital ship production requires
|
||||||
|
fighting (salvaging and reprocessing), not just mining.
|
||||||
|
- Every gating has a fictional reason (concrete fiction in
|
||||||
|
`../content_design.md`): the asteroid is a metal-rich body, so its bulk
|
||||||
|
rock is minable anywhere; the mid resource sits in rare pockets; the
|
||||||
|
late input is battle-forged — created only in the violence of ship
|
||||||
|
destruction, which is why any wreck (including the player's own)
|
||||||
|
yields it and no foundry can make it.
|
||||||
|
- The mid resource is **dual-gated**: schematics (knowledge, via drops)
|
||||||
|
and territory (deposits, via expansions). Tuning must guarantee the
|
||||||
|
deposit-bearing expansion is comfortably affordable by the time the
|
||||||
|
first mid-tier schematics drop, or those drops are dead picks.
|
||||||
|
- There is no direct "resource unlock" mechanism. Miner recipes unlock
|
||||||
|
**implicitly** (REQ-LOCK-IMPLICIT) when some unlocked schematic's material
|
||||||
|
chain reaches that resource. Resource pacing is therefore controlled
|
||||||
|
through the `unlock_at_station_level` values of ships, modules, and
|
||||||
|
assembler recipe schematics — and the content must guarantee that the
|
||||||
|
chains actually connect (a mid-game schematic must require an item whose
|
||||||
|
chain reaches the mid resource, or it never unlocks).
|
||||||
|
|
||||||
|
### Resource deposits
|
||||||
|
|
||||||
|
- **Rule: freedom first, geography later.** The starting resources are
|
||||||
|
minable everywhere, so the player has full layout freedom while
|
||||||
|
learning. Later mined resources are bound to deposit patches — fixed
|
||||||
|
geography as a layout puzzle, introduced once the player is competent.
|
||||||
|
- **Rule: deposits exist only in expansion territory.** Expansions buy
|
||||||
|
space *and* access to resource tiers — the second leg of the growth
|
||||||
|
curve (see Building block economy).
|
||||||
|
- **Rule: patch area is the throughput cap.** Deposits never deplete but
|
||||||
|
are finite in area; the number of deposit tiles caps how many miners
|
||||||
|
the chain supports. Buying deeper expansions raises the throughput
|
||||||
|
ceiling of high-tier chains.
|
||||||
|
- **Rule: no empty expansions.** Deposit content per expansion is
|
||||||
|
deterministic and config-defined; only the placement within the new
|
||||||
|
columns is randomized. Buying an expansion never rolls "nothing".
|
||||||
|
- **Rule: mining is binary.** A miner whose footprint overlaps at least
|
||||||
|
one deposit tile of a resource can select that resource's recipe; no
|
||||||
|
partial-coverage rate scaling.
|
||||||
|
- Deposits arrive at the periphery (expansions add columns on the left),
|
||||||
|
so each new chain starts in fresh space — supporting the
|
||||||
|
refactorability property — and high-tier chains have the longest belt
|
||||||
|
runs to the shipyards, escalating the logistics puzzle with tier.
|
||||||
|
|
||||||
|
## Production tree rules
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
- **Each phase transition adds exactly one new base input chain.** A base
|
||||||
|
input is a bottom-level resource entering the factory from outside — a
|
||||||
|
mined resource or the scrap-only input. The early game starts with two
|
||||||
|
ores as the baseline; the transition to mid adds one (the deposit-bound
|
||||||
|
mid resource), the transition to late adds one (the scrap-only input).
|
||||||
|
No transition ever introduces more than one unfamiliar bottom-level
|
||||||
|
chain, so the factory grows in one direction at a time.
|
||||||
|
- **Intermediates are generic shared parts.** Keep the item count low —
|
||||||
|
modules and hulls of a tier draw from a shared pool of that tier's and
|
||||||
|
lower tiers' intermediates rather than each having bespoke inputs.
|
||||||
|
- **Thematic naming over thematic items.** Inputs should be plausible for
|
||||||
|
what the recipe produces (crystals for lasers, heat sinks for bigger
|
||||||
|
lasers). Achieve this through naming and chain membership, not by adding
|
||||||
|
item types: rename a generic part, don't add a parallel one.
|
||||||
|
|
||||||
|
### Ratios
|
||||||
|
|
||||||
|
- **Ratio "niceness" degrades with tier.** The producer:consumer ratios
|
||||||
|
needed for 100% throughput follow a curve:
|
||||||
|
- Tier 1 (ore → basic material): trivially nice (e.g. 1:1 or 1:2
|
||||||
|
miner:smelter).
|
||||||
|
- Tier 2: slightly complex but still clean (e.g. 2:3).
|
||||||
|
- Higher tiers: increasingly strange ratios, as deliberate optimization
|
||||||
|
puzzles.
|
||||||
|
- Exceptions in both directions are allowed when there is a reason — a
|
||||||
|
clean late chain as a breather, an odd early chain as a teaser — but the
|
||||||
|
curve is the default.
|
||||||
|
|
||||||
|
### Shortcut recipes
|
||||||
|
|
||||||
|
- Some strange chains get a **shortcut recipe**: an explicitly unlockable
|
||||||
|
assembler recipe schematic (`unlock_at_station_level ≥ 0`, drop-only per
|
||||||
|
REQ-LOCK-EXPLICIT) that skips a step (e.g. t1 → t3 directly) and yields
|
||||||
|
nice ratios for a chain whose base path is strange.
|
||||||
|
- **Not every strange chain gets a shortcut.** Some strangeness is
|
||||||
|
permanent; the absence of a fix is a valid design choice.
|
||||||
|
- **Shortcuts drop only for known chains.** A shortcut recipe enters the
|
||||||
|
drop pool only when both its input items and its output item are
|
||||||
|
already unlocked (in addition to the station level check). The player
|
||||||
|
is never offered a shortcut for a chain they have not built yet. The
|
||||||
|
output-item half of this check already exists in
|
||||||
|
REQ-DEF-SCHEMATIC-DROP; the input half is an open action item (see
|
||||||
|
`README.md`).
|
||||||
|
- **Shortcuts are pure rewards, never balance factors.** An item's threat
|
||||||
|
value is the *maximum* across its producing recipes (REQ-THREAT-ITEM), so
|
||||||
|
unlocking a cheaper recipe does not lower the item's threat accounting —
|
||||||
|
the player gains real factory efficiency without their ships being
|
||||||
|
valued cheaper and without enemy wave budgets shifting. Consequently:
|
||||||
|
**balance every chain around its base (expensive) path**; the shortcut's
|
||||||
|
savings define the size of the reward.
|
||||||
|
|
||||||
|
### Refactorability
|
||||||
|
|
||||||
|
- **Rule (the property):** unlocking the next tier or size of a thing must
|
||||||
|
be a *local edit* of the existing production line — adding assemblers
|
||||||
|
and belts, or replacing a machine or two in place — never a rebuild of
|
||||||
|
the line.
|
||||||
|
- **What this buys the player:** foresight pays off in space, not blocks.
|
||||||
|
An experienced player leaves a little slack in the middle of a line,
|
||||||
|
knowing the next size or tier upgrade means tearing out one assembler
|
||||||
|
and a few belts there and inserting the new step — plus maybe swapping
|
||||||
|
a recipe or two elsewhere — while the rest of the line keeps running
|
||||||
|
untouched.
|
||||||
|
- **Default technique:** the bigger version introduces one new intermediate
|
||||||
|
that is produced from a subset of the smaller version's inputs (possibly
|
||||||
|
plus one additional low-tier material), and otherwise reuses the smaller
|
||||||
|
version's inputs. Existing lines keep running and feed the new
|
||||||
|
intermediate's assemblers.
|
||||||
|
- The property is the rule; the technique is only the default. It may be
|
||||||
|
broken where it fights thematic plausibility, as long as the property
|
||||||
|
still holds.
|
||||||
|
|
||||||
|
## Cost archetypes
|
||||||
|
|
||||||
|
Every item has two cost knobs: **material quantity** and **cycle time**.
|
||||||
|
Both feed the threat value identically (threat = recursive
|
||||||
|
production-seconds, REQ-MOD-THREAT), so the split between them does not
|
||||||
|
change what an item is *worth* — it changes what kind of **factory
|
||||||
|
pressure** it creates:
|
||||||
|
|
||||||
|
- **Material-heavy, fast** (e.g. armor plates): simple items; stress belt
|
||||||
|
throughput, splitter logistics, and miner/smelter counts.
|
||||||
|
- **Time-heavy, lean** (e.g. shield modules): technically complex items;
|
||||||
|
few inputs — possibly higher-tier ones — but long cycles; stress
|
||||||
|
assembler counts and parallelization.
|
||||||
|
|
||||||
|
**Rule:** each module family commits to a clear archetype, so factories
|
||||||
|
supporting different fleet doctrines feel structurally different to build.
|
||||||
|
|
||||||
|
## Threat model (balancing backbone)
|
||||||
|
|
||||||
|
- Threat cost = total recursive production-seconds (REQ-MOD-THREAT). One
|
||||||
|
factory-second equals one threat; player output and enemy wave budgets
|
||||||
|
are denominated in the same currency.
|
||||||
|
- **Rule: combat power per threat is roughly constant** across all ships,
|
||||||
|
modules, and tiers. Higher tiers are better per *ship* and per *module
|
||||||
|
slot*, not per invested factory-second — their advantage is
|
||||||
|
concentration (fewer, bigger things; slot geometry per
|
||||||
|
`../content_design.md`) and qualitative capabilities, not a better
|
||||||
|
exchange rate. Deviations from this rule are deliberate and documented.
|
||||||
|
- **Difficulty race:** the enemy threat rate (`threat_rate_formula`) is
|
||||||
|
tuned against the factory output (threat/s) achievable by a competent
|
||||||
|
player — slightly below it early, crossing above it eventually. The game
|
||||||
|
is endless; enemy scaling must ultimately outpace any factory, and
|
||||||
|
player skill shifts *when*, not *whether*.
|
||||||
|
- **All time scaling lives in the threat rate** — waves get bigger, ships
|
||||||
|
of a given schematic never get individually stronger. There is no ship
|
||||||
|
level dimension: stat formulas are plain values, and per-ship level
|
||||||
|
scaling does not exist. Push scaling on enemy defence stations is the
|
||||||
|
separate, player-triggered difficulty axis and keeps its level formulas.
|
||||||
|
|
||||||
|
## Unlock & drop pacing
|
||||||
|
|
||||||
|
- **Starting set rule:** the schematics unlocked at game start
|
||||||
|
(`unlock_at_station_level = -1`) must be exactly enough to reach the
|
||||||
|
first push unaided — a functioning block loop, small hulls, a basic
|
||||||
|
weapon, and the salvage loop. Nothing more.
|
||||||
|
- The `unlock_at_station_level` ladder mirrors the resource phases:
|
||||||
|
mid-tier hulls/modules/recipes at low station levels, capital content at
|
||||||
|
higher levels. A schematic must not become available before the chains
|
||||||
|
its materials need can be unlocked alongside it.
|
||||||
|
- **Schematics can require other schematics.** Beyond the station-level
|
||||||
|
gate, a schematic (ship, module, or assembler recipe) may list
|
||||||
|
prerequisite schematics (`unlock_requires`, REQ-LOCK-PREREQ) that must
|
||||||
|
already be unlocked before it enters the drop pool — e.g. the medium
|
||||||
|
gun requires the small gun; a future Mk2 requires its base version.
|
||||||
|
Station level gates the earliest *when*; prerequisites gate the
|
||||||
|
*order*, keeping drop offers coherent with what the player already
|
||||||
|
owns.
|
||||||
|
- **No duplicate drops.** Ship and module schematics leave the drop pool
|
||||||
|
once owned, exactly as assembler recipe schematics already do. There are
|
||||||
|
no schematic level-ups; player power grows through unlock breadth and
|
||||||
|
factory scale only, which keeps power-per-threat exact on both sides.
|
||||||
|
The pool therefore shrinks over a run and late pushes increasingly offer
|
||||||
|
artifacts — intended: the late game is a race for the win condition.
|
||||||
|
Per-item progression may return later as Mk2 upgrade recipes (see Future
|
||||||
|
work), never as free level-ups.
|
||||||
|
- **Artifacts trade power for progress.** Artifact options compete with
|
||||||
|
schematic picks in the same choice dialog; the artifact chance must be
|
||||||
|
tuned so that taking one is a real decision (giving up an unlock), not
|
||||||
|
automatic in either direction.
|
||||||
|
|
||||||
|
## Scrap & reprocessing economy
|
||||||
|
|
||||||
|
- Scrap is the bridge from combat back into the factory, with two sinks:
|
||||||
|
**smelting** (same basic materials as ore — the safe, boring option) and
|
||||||
|
**reprocessing** (probabilistic higher intermediates, including the
|
||||||
|
late-game input — the gamble that eventually becomes mandatory).
|
||||||
|
- The reprocessing output pool renormalizes over the output groups whose items
|
||||||
|
are implicitly unlocked (REQ-LOCK-OUTPUT-POOL), so its output quality improves
|
||||||
|
automatically as the run progresses. **Rule:** weights are authored for
|
||||||
|
the *fully unlocked* pool state; early-game behavior falls out of
|
||||||
|
renormalization for free and needs no separate staging.
|
||||||
|
- **Rule: ship scrap drops are derived, never authored.** A destroyed ship
|
||||||
|
drops `threat cost × scrap_per_threat` (a `world.toml` key), with the
|
||||||
|
threat cost computed from its actual hull plus installed modules
|
||||||
|
(REQ-MOD-THREAT) — a kitted-out ship drops more scrap than a bare hull
|
||||||
|
automatically. `ships.toml` carries no scrap value. Defence stations are
|
||||||
|
the exception: they keep authored `scrap_drop_formula`s, because pushing
|
||||||
|
rewards are tuned independently of ship production costs.
|
||||||
|
- Consequence: the threat value of scrap is the constant
|
||||||
|
`1 / scrap_per_threat` (REQ-THREAT-SCRAP). The former min-`scrap_drop`
|
||||||
|
schematic derivation and its potential circularity are gone.
|
||||||
|
- **Rule:** the late-game input's income rate meaningfully gates capital
|
||||||
|
production — unlocking a capital hull must not mean spamming it; the
|
||||||
|
input trickles in slowly enough that every capital ship is a noticeable
|
||||||
|
investment. The tuning target is relative, not absolute: assume a
|
||||||
|
reference player who destroys and salvages roughly the threat the game
|
||||||
|
spawns ("fighting at parity"), and tune `scrap_per_threat`, the
|
||||||
|
reprocessing weights, and capital material costs so that this player
|
||||||
|
affords roughly N capital ships per boss cycle. An absolute income rate
|
||||||
|
would be meaningless (income depends entirely on how much the player
|
||||||
|
fights) and would not self-scale; per boss cycle, the target tracks the
|
||||||
|
threat rate as it steps up.
|
||||||
|
|
||||||
|
## Building block economy
|
||||||
|
|
||||||
|
- Building blocks are the only global currency and the early game's
|
||||||
|
central constraint. The early game is a bootstrap problem: convert the
|
||||||
|
starting stock into a self-sustaining block loop before the first waves
|
||||||
|
bite.
|
||||||
|
- **Rule:** the starting stock suffices for a minimal block loop plus the
|
||||||
|
first shipyard — with a little slack for beginner mistakes, but not
|
||||||
|
enough to skip the loop entirely.
|
||||||
|
- **Rule: the growth curve lives here.** A saturated building produces
|
||||||
|
exactly 1 threat/s, so the player's output curve *is* their
|
||||||
|
building-count curve — shaping growth over a run means shaping the
|
||||||
|
block and space economy, there is nowhere else it can live. Intended
|
||||||
|
shape: exponential bootstrap (block-limited) → ramp
|
||||||
|
(expansion-limited) → asymptotic squeeze as expansion costs outrun
|
||||||
|
income, racing the enemy threat rate throughout.
|
||||||
|
- **Rule: escalating expansion costs.** Expansion cost is a formula of
|
||||||
|
the number of expansions already purchased, rising steeply enough that
|
||||||
|
expansions eventually outrun any block income. The starting asteroid
|
||||||
|
is deliberately small — filled within the first boss cycle or two, so
|
||||||
|
the early exponential burst is a satisfying ramp, not a balance hole —
|
||||||
|
and from then on the output curve is the expansion curve. Blocks keep
|
||||||
|
a meaningful sink for the entire run, and "grow vs. army" stays a live
|
||||||
|
decision at every moment.
|
||||||
|
- **Rule: designed doubling time.** Block production is a positive
|
||||||
|
feedback loop (blocks buy assemblers, assemblers make blocks); its
|
||||||
|
time constant is a designed quantity, never an accident of quantity
|
||||||
|
choice. The block chain's depth and the per-building costs are tuned
|
||||||
|
against a stated target of the form: "a factory spending X% of its
|
||||||
|
capacity on blocks doubles in ~T minutes."
|
||||||
|
- **Rule: growth is limited by economy, never by waiting.** Construction
|
||||||
|
times stay short; the serial build queue must not be used as a growth
|
||||||
|
brake. Waiting for placed buildings to become operational — especially
|
||||||
|
at the start of a run — is frustration, not gameplay. All growth
|
||||||
|
limiting comes from block income and expansion pricing.
|
||||||
|
- Note: block income has a structural ceiling — blocks enter the stock
|
||||||
|
through the HQ's single belt port, so income is capped at belt
|
||||||
|
throughput regardless of assembler count. Per-building costs should be
|
||||||
|
high enough that this cap can bind late-game (see the condensed-block
|
||||||
|
idea under Future work).
|
||||||
|
|
||||||
|
## Numeric guardrails
|
||||||
|
|
||||||
|
Constraints that every recipe must respect, independent of tuning:
|
||||||
|
|
||||||
|
- **Belt throughput:** belt speed and per-tile capacity cap how fast a
|
||||||
|
single belt can feed an input. A recipe whose per-cycle inputs cannot be
|
||||||
|
sustained by one belt per input at 100% duty cycle is a *deliberate*
|
||||||
|
design (forcing parallel belts/splitters as part of a high-tier puzzle)
|
||||||
|
— never an accident of quantity choice.
|
||||||
|
- **Buffer burstiness:** input buffers hold 2× the per-cycle amount
|
||||||
|
(REQ-MAT-INPUT-BUFFER), so large per-cycle quantities create bursty belt
|
||||||
|
demand. Low tiers prefer small quantities with short cycles; big-batch
|
||||||
|
recipes are reserved for high tiers where burstiness is part of the
|
||||||
|
puzzle.
|
||||||
|
- **Cycle times scale with tier** monotonically — a higher-tier item never
|
||||||
|
has a shorter total chain time than a lower-tier item of the same role.
|
||||||
|
|
||||||
|
## Future work
|
||||||
|
|
||||||
|
- **Condensed building blocks** — a drop-unlockable shortcut-style
|
||||||
|
recipe that packs several blocks' worth of value into one belt item,
|
||||||
|
relieving the HQ intake ceiling (see Building block economy) as a
|
||||||
|
late-game reward. The ceiling is the puzzle, the drop is the fix —
|
||||||
|
same philosophy as shortcut recipes.
|
||||||
|
- **Mk2 upgrade recipes** — the deferred design for per-item progression,
|
||||||
|
to revisit once the config has stabilized. A duplicate-style drop
|
||||||
|
unlocks a distinct `*_mk2` item whose recipe consumes the Mk1 item plus
|
||||||
|
higher-tier parts. This preserves power-per-threat (the extra power is
|
||||||
|
paid in real production-seconds, since threat is recursive), satisfies
|
||||||
|
the refactorability rule (the Mk1 line keeps running and feeds one new
|
||||||
|
assembler), and keeps balancing one-dimensional (no level variable
|
||||||
|
anywhere). Enemy-side progression happens via `default_modules`
|
||||||
|
variants per era instead of a level formula.
|
||||||
104
docs/balancing/targets.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Balancing Targets (base numbers)
|
||||||
|
|
||||||
|
The root numbers of the balancing. Everything in `derived.md` is tuned to
|
||||||
|
hit these; when rebalancing, **change these first and re-derive — never
|
||||||
|
patch derived values directly**. The rules these numbers follow live in
|
||||||
|
`rules.md`.
|
||||||
|
|
||||||
|
All time targets are in **game time**. The player can pause and
|
||||||
|
accelerate, so real session length differs; playtests measure both. The
|
||||||
|
time unit is the boss cycle (`world.toml boss_countdown_seconds`, 300 s).
|
||||||
|
Destroying a station set advances the boss countdown by
|
||||||
|
`boss_advance_seconds` (60 s), so cycles run shorter than nominal when
|
||||||
|
pushing actively — targets deliberately ignore that.
|
||||||
|
|
||||||
|
## Run shape
|
||||||
|
|
||||||
|
1. **Run length** — a winning run takes up to 2 hours of game time: win
|
||||||
|
around boss cycle 20–24. Losing runs end earlier.
|
||||||
|
2. **Phase boundaries** — early = cycles 1–5 (iron/copper, small hulls),
|
||||||
|
mid = cycles 6–14 (quartz, medium hulls), late = cycles 15+
|
||||||
|
(voidsteel, capitals). Push cadence: first station set around cycle
|
||||||
|
2–3, roughly one per cycle from mid onward — so the destroyed set's
|
||||||
|
level ℓ is reached around cycle ℓ+2.
|
||||||
|
3. **Factory size curve** — producing buildings over time; when
|
||||||
|
saturated, output threat/s equals this count, so this curve IS the
|
||||||
|
player power curve: ~25 when the starting asteroid is full (end of
|
||||||
|
cycle 2), ~60 at the start of mid (cycle 6), ~120 at the start of
|
||||||
|
late (cycle 15), ~150 near the win. `threat_rate_formula` must remain
|
||||||
|
a fraction of this curve; buildings plus belts must physically fit
|
||||||
|
the asteroid plus affordable expansions.
|
||||||
|
4. **Threat-cost ladder** — total production-seconds per *fitted* hull
|
||||||
|
(including the typical/default module loadout): drone 10.5,
|
||||||
|
frigate 47, destroyer 99, cruiser 233.5, battlecruiser 354.5,
|
||||||
|
battleship 722.5, dreadnought 1491.5, carrier 1436.5. Every
|
||||||
|
production chain must sum to its ladder value. (The original strawman
|
||||||
|
was 10/40/80/200/350/700/1500; the small end runs ~10–20% hot because
|
||||||
|
fixed chain overhead dominates small hulls — accepted, and the
|
||||||
|
achieved values adopted as the ladder. The ~×2-per-class curve shape
|
||||||
|
is the invariant.)
|
||||||
|
5. **Fleet size** — swarm-leaning: ~25 player combat ships as the
|
||||||
|
standing mid-game fleet. Standing fleet = build cadence (4) × average
|
||||||
|
ship lifetime, so this target drives time-to-kill and therefore all
|
||||||
|
combat stat magnitudes.
|
||||||
|
6. **Block economy roots** — bootstrap complete (starting asteroid full)
|
||||||
|
by the end of cycle 2; a factory spending ~30% of its capacity on
|
||||||
|
blocks doubles in ~4 minutes early game; one expansion affordable per
|
||||||
|
cycle at ~1/3 of block income mid-game, decelerating to one per 2–3
|
||||||
|
cycles late as escalating costs outrun income.
|
||||||
|
|
||||||
|
## Combat anchors
|
||||||
|
|
||||||
|
All combat stats derive from these; per-hull HP additionally carries
|
||||||
|
empirical trims from arena rounds (values in `derived.md`).
|
||||||
|
|
||||||
|
- **Weapon DPS per threat pays a concentration tax that grows with gun
|
||||||
|
size**: small ≈ 0.62, medium ≈ 0.48, large ≈ 0.41 DPS per threat of
|
||||||
|
weapon contribution, compensated by the range ladder 50/70/100 m.
|
||||||
|
Rationale: concentration itself (focus fire, no DPS loss to attrition,
|
||||||
|
range) is worth paying for — with a flat curve, concentrated fleets
|
||||||
|
win equal-threat fights outright (arena round 1).
|
||||||
|
- **Hull HP = 15 per threat of hull contribution** as the prior; per-hull
|
||||||
|
HP is the empirical trim knob (guns are shared across hulls, hull HP
|
||||||
|
moves exactly one matchup). The arena consistently prices capitals as
|
||||||
|
*tanks with taxed guns* — capital hulls sit well above the prior.
|
||||||
|
- **Armor HP ≈ 37 per threat** — a strong premium over hull HP because
|
||||||
|
armor is pure HP with no capability, and fights snowball: killing
|
||||||
|
removes enemy DPS, surviving merely delays — HP must be cheaper than
|
||||||
|
DPS.
|
||||||
|
- **Repair ≈ 0.7 HP/s per threat** — in-combat sustain effectively
|
||||||
|
removes enemy DPS and must be priced like DPS, not like HP.
|
||||||
|
- **TTK / fight duration**: parity fights in the 30–60 s band at
|
||||||
|
mid-game scale; capital mirrors ~90 s deliberately; the extreme
|
||||||
|
tank-vs-chip-damage matchup (dreadnought vs destroyer swarm, ~3.5 min)
|
||||||
|
is an accepted outlier.
|
||||||
|
- **Mobility is monotone in size** — the smallest hulls are the fastest
|
||||||
|
and nimblest. Sensor ranges (150→350 m) always exceed weapon ranges.
|
||||||
|
- **Weapon modifiers are capital economy**: a ×1.2 damage modifier at
|
||||||
|
~22.5 threat beats adding a gun once a ship carries more than ~68
|
||||||
|
threat of weapons — modifiers pay off on gun-heavy big hulls, waste on
|
||||||
|
small ones. Range modifiers are the strongest and are priced/kept
|
||||||
|
small (×1.3): range is the dominant stat under the orbit AI (free
|
||||||
|
approach fire).
|
||||||
|
- **Stations**: a fresh player station holds one early parity wave
|
||||||
|
unaided; the enemy station at level 0 matches the player station
|
||||||
|
exactly and scales per push level. Station range is the dominance
|
||||||
|
lever, not HP (at 4× a small gun's range, two stations annihilated a
|
||||||
|
3× threat swarm through approach fire alone).
|
||||||
|
- **Accepted imbalances**: the carrier loses its equal-threat fights
|
||||||
|
until the drone-launching capability exists (the hangar is dead
|
||||||
|
threat) — fix by implementing drones, not stats. A pure smallest-ship
|
||||||
|
fleet modestly loses (~15–25%) to a range-fitted capital — desirable
|
||||||
|
doctrine texture; the fair anti-capital answer is the mixed fleet.
|
||||||
|
|
||||||
|
## Pacing anchors
|
||||||
|
|
||||||
|
- **Starting set** is the rule-minimum: drone, frigate, small gun,
|
||||||
|
salvager (plus the explicitly unlocked building-block recipe).
|
||||||
|
- **Threat rate shape**: below the player's achievable military output
|
||||||
|
(≈ half the factory curve) early, crossing at the late boundary
|
||||||
|
(~cycle 15), overwhelming by ~cycle 24.
|
||||||
|
- **Winning = five real decisions**: `artifact_win_count` is set so that
|
||||||
|
across a winning run's ~15–18 pushes (~7 cumulative artifact offers at
|
||||||
|
the current chance formula), the player must choose the artifact over
|
||||||
|
a schematic about five times.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
A single-player asymmetric game inspired by DOTA's wave/tower structure, combined with a Factorio-style factory builder. The player builds a factory on an asteroid to supply shipyards that produce autonomous combat ships. Those ships fight off endless enemy waves advancing from the right. The goal is to survive as long as possible; elapsed time is always displayed.
|
A single-player asymmetric game inspired by DOTA's wave/tower structure, combined with a Factorio-style factory builder. The player builds a factory on an asteroid to supply shipyards that produce autonomous combat ships. Those ships fight off enemy waves advancing from the right, with tougher boss waves arriving periodically. Pushing into enemy territory and destroying their defence stations occasionally yields artifacts, which are used to upgrade the HQ; the goal is to upgrade the HQ enough to launch it into space, winning the game.
|
||||||
|
|
||||||
## Setting & Visuals
|
## Setting & Visuals
|
||||||
|
|
||||||
@@ -62,21 +62,26 @@ Two sources feed the same production tree:
|
|||||||
|
|
||||||
- Waves consist of a single enemy ship type whose stats scale with difficulty.
|
- Waves consist of a single enemy ship type whose stats scale with difficulty.
|
||||||
- Waves spawn over several seconds; a gap follows before the next wave begins spawning. The previous wave may still be approaching or fighting during the gap.
|
- Waves spawn over several seconds; a gap follows before the next wave begins spawning. The previous wave may still be approaching or fighting during the gap.
|
||||||
- Difficulty scales multiplicatively from two sources:
|
- A tougher **boss wave** spawns periodically on its own countdown, on top of normal waves.
|
||||||
- **Time scaling** — enemy strength increases gradually over elapsed time.
|
- Enemy strength increases gradually over time and with each boss wave that occurs.
|
||||||
- **Push scaling** — destroying a set of enemy defence stations multiplies enemy strength by a configurable factor. The replacement stations are scaled by the same factor.
|
|
||||||
|
|
||||||
## Push Mechanic
|
## Push Mechanic
|
||||||
|
|
||||||
- The player is not forced to push; purely defensive play is valid.
|
- The player must push — destroying enemy defence stations is the only way to earn artifacts, which are required to win.
|
||||||
- Destroying enemy defence stations applies the push scaling multiplier to all future waves, extends the scrollable area, and places a new (stronger) set of stations at the new boundary.
|
- Destroying a set of enemy defence stations advances the boss countdown (bringing the next, stronger boss wave sooner), extends the scrollable area, and places a new (stronger) set of stations at the new boundary.
|
||||||
- Destroyed enemy defence stations drop ship schematics.
|
- Destroyed enemy defence stations drop either a ship/module schematic or, occasionally, an artifact.
|
||||||
|
|
||||||
|
## Win Condition
|
||||||
|
|
||||||
|
- Artifacts are gathered by defeating enemy defence stations instead of taking a schematic reward.
|
||||||
|
- Artifacts are used to upgrade the HQ. Once the HQ is upgraded enough, the player can launch it into space — this is how the game is won.
|
||||||
|
|
||||||
## Starting Conditions & Game Over
|
## Starting Conditions & Game Over
|
||||||
|
|
||||||
- The player starts with the HQ and player defence stations pre-placed and a stock of building blocks; no other buildings are pre-placed.
|
- The player starts with the HQ and player defence stations pre-placed and a stock of building blocks; no other buildings are pre-placed.
|
||||||
- There is a grace period before the first wave to allow initial setup.
|
- There is a grace period before the first wave to allow initial setup.
|
||||||
- If all ships and player defence stations are destroyed, enemies attack the HQ. The game is lost when the HQ is destroyed. Factory buildings are never targeted.
|
- If all ships and player defence stations are destroyed, enemies attack the HQ. The game is lost when the HQ is destroyed. Factory buildings are never targeted.
|
||||||
|
- The game is won when the player launches the HQ into space (see Win Condition).
|
||||||
|
|
||||||
## Asteroid Expansion
|
## Asteroid Expansion
|
||||||
|
|
||||||
|
|||||||
294
docs/content_design.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
# Content Design — Ships, Modules & Production Tree
|
||||||
|
|
||||||
|
The designed game content: hull layout grids, module footprints and the
|
||||||
|
gating between them, and the production tree (items, chains, fiction).
|
||||||
|
All numbers — quantities, durations, threat values, stats, unlock levels
|
||||||
|
— live in the config files and are documented with their derivations in
|
||||||
|
`docs/balancing/` (see `balancing/README.md` for the index).
|
||||||
|
|
||||||
|
## Design principle: footprint gating
|
||||||
|
|
||||||
|
Which module fits on which hull is controlled purely by geometry — no
|
||||||
|
explicit allow-lists. Each hull grid is shaped so that it physically cannot
|
||||||
|
contain the footprint of modules from a larger size class. This keeps the
|
||||||
|
rules transparent to the player ("it doesn't fit because there is no room")
|
||||||
|
and makes them trivially moddable through the config files alone.
|
||||||
|
|
||||||
|
### Module footprint ladder
|
||||||
|
|
||||||
|
| Footprint | Modules | Smallest hull that fits it |
|
||||||
|
|-----------|---------|----------------------------|
|
||||||
|
| 1x1 | railgun_s, salvager, repair_tool | drone |
|
||||||
|
| 1x2 | maneuvering_thrusters, sensor_booster, armor_plates | frigate |
|
||||||
|
| 1x3 | afterburner | frigate (eats most of it) |
|
||||||
|
| L-shape (3 cells) | weapon_stabilizer, weapon_primer, weapon_upgrade | frigate |
|
||||||
|
| 2x2 | railgun_m, drone_bay | cruiser |
|
||||||
|
| 3x3 | railgun_l | battleship |
|
||||||
|
| 2x6 | drone_hangar | carrier (only) |
|
||||||
|
|
||||||
|
### Hull grids
|
||||||
|
|
||||||
|
`O` = buildable cell, `X` = hull structure (not buildable).
|
||||||
|
|
||||||
|
**drone (xs, 1 cell)** — exactly one 1x1 module: a small gun, a salvager, or
|
||||||
|
a repair tool. This is what makes drone roles swappable.
|
||||||
|
|
||||||
|
O
|
||||||
|
|
||||||
|
**frigate (s, 5 cells)** — plus shape. Every 1x2 placement crosses the center
|
||||||
|
cell, so at most ONE 1x2 support fits; alternatively one L-shaped weapon
|
||||||
|
modifier or one afterburner through the center line. Gun-boat with one or two
|
||||||
|
support modules, as intended.
|
||||||
|
|
||||||
|
XOX
|
||||||
|
OOO
|
||||||
|
XOX
|
||||||
|
|
||||||
|
**destroyer (s, 8 cells)** — gun deck with three turret bumps. More cells
|
||||||
|
than the frigate (more small guns), but still no 2x2 area anywhere, so medium
|
||||||
|
hardware can never be mounted.
|
||||||
|
|
||||||
|
OXOXO
|
||||||
|
OOOOO
|
||||||
|
|
||||||
|
**cruiser (m, 12 cells)** — notched corners. Fits at most two 2x2 m guns
|
||||||
|
(stacked through the middle), leaving the side cells for supports. No 3x3
|
||||||
|
area.
|
||||||
|
|
||||||
|
XOOX
|
||||||
|
OOOO
|
||||||
|
OOOO
|
||||||
|
XOOX
|
||||||
|
|
||||||
|
**battlecruiser (m, 16 cells)** — split bow with two gun cheeks, tapered
|
||||||
|
stern. Fits three 2x2 m guns — one more than the cruiser — with small support
|
||||||
|
slots left over. The bow split and stern taper prevent any 3x3 area (no l
|
||||||
|
gun) and any 2x6 area (no drone hangar).
|
||||||
|
|
||||||
|
OOXXOO
|
||||||
|
OOOOOO
|
||||||
|
XOOOOX
|
||||||
|
XXOOXX
|
||||||
|
|
||||||
|
**battleship (l, 24 cells)** — broadside hull with notched flanks on every
|
||||||
|
other row. Fits four 2x2 m guns (two per gun deck) — one more than the
|
||||||
|
battlecruiser — with bow, stern, and flank cells for supports. All 3x3
|
||||||
|
placements crowd the center columns, so at most ONE l gun fits: mounted
|
||||||
|
center it blocks every m gun mount (pure support strips remain), mounted
|
||||||
|
offset it still allows two m guns. The notched rows are never adjacent-and-
|
||||||
|
full, so no 2x6 drone hangar fits.
|
||||||
|
|
||||||
|
XOOOOX
|
||||||
|
OOOOOO
|
||||||
|
XOOOOX
|
||||||
|
OOOOOO
|
||||||
|
XOOOOX
|
||||||
|
|
||||||
|
**dreadnought (xl, 36 cells)** — the main battery deck is split into three
|
||||||
|
3x3 gun slots by structural spacer columns, so exactly three l guns fit side
|
||||||
|
by side (or m guns / supports in unused slots), plus bow/stern strips for
|
||||||
|
supports. The spacers cap every horizontal run at 5 cells, so the 2x6 drone
|
||||||
|
hangar can never fit — the carrier stays the only hangar hull.
|
||||||
|
|
||||||
|
XXXOOOOOXXX
|
||||||
|
OOOXOOOXOOO
|
||||||
|
OOOXOOOXOOO
|
||||||
|
OOOXOOOXOOO
|
||||||
|
XXOOXXXOOXX
|
||||||
|
|
||||||
|
**carrier (xl, 37 cells)** — the top flight deck (rows 0–1) is the only
|
||||||
|
region wide enough for the 2x6 drone hangar, and exactly one fits. The middle
|
||||||
|
deck row is broken up by elevator shafts (X cells placed so every 3-column
|
||||||
|
window hits one), which is what prevents any 3x3 l gun from ever fitting.
|
||||||
|
Lower decks hold supports and 2x2 point-defense m guns.
|
||||||
|
|
||||||
|
XOOOOOOOOX
|
||||||
|
OOOOOOOOOO
|
||||||
|
OOXOOXOOXO
|
||||||
|
XOOOOOOOOX
|
||||||
|
XXXOOOOXXX
|
||||||
|
|
||||||
|
### Verified gating matrix
|
||||||
|
|
||||||
|
Checked programmatically against the configs (all four mask rotations,
|
||||||
|
all placements) with `tools/verify_layouts.py` — re-run it after editing
|
||||||
|
layout grids or surface masks:
|
||||||
|
|
||||||
|
python dota_factory/tools/verify_layouts.py
|
||||||
|
|
||||||
|
| Footprint | drone | frigate | destroyer | cruiser | battlecruiser | battleship | dreadnought | carrier |
|
||||||
|
|-----------|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||||
|
| 1x1 | x | x | x | x | x | x | x | x |
|
||||||
|
| 1x2 | | x | x | x | x | x | x | x |
|
||||||
|
| 1x3 | | x | x | x | x | x | x | x |
|
||||||
|
| L-shape | | x | x | x | x | x | x | x |
|
||||||
|
| 2x2 | | | | x | x | x | x | x |
|
||||||
|
| 3x3 | | | | | | x | x | |
|
||||||
|
| 2x6 | | | | | | | | x |
|
||||||
|
|
||||||
|
Maximum simultaneous (disjoint) placements: m guns — cruiser 2,
|
||||||
|
battlecruiser 3, battleship 4; l guns — battleship 1, dreadnought 3;
|
||||||
|
drone hangar — carrier 1.
|
||||||
|
|
||||||
|
## Production tree
|
||||||
|
|
||||||
|
Designed against the rules in `docs/balancing/rules.md` (ratio curve,
|
||||||
|
cost ladder, cost archetypes, refactorability). Quantities, durations,
|
||||||
|
and threat values live in `docs/balancing/derived.md`.
|
||||||
|
|
||||||
|
### Base inputs (4) and fiction
|
||||||
|
|
||||||
|
- **iron_ore, copper_ore** — from the start, minable on every asteroid
|
||||||
|
tile. Fiction: the asteroid is an M-type (metal) body — its bulk rock
|
||||||
|
*is* ore, which is why the shipyard operation was built here at all.
|
||||||
|
- **quartz** — mid-game, minable only on geode deposit patches in
|
||||||
|
expansion territory (see the Resource deposits rules in
|
||||||
|
`docs/balancing/rules.md`; the deposit mechanic itself is an open
|
||||||
|
action item — until it lands, quartz mines anywhere). Fiction:
|
||||||
|
ordinary silicate dust is everywhere and worthless; chips and optics
|
||||||
|
need rare, pocket-bound optical-grade crystal.
|
||||||
|
- **voidsteel** — late-game, obtained only by reprocessing scrap.
|
||||||
|
Fiction: battle-forged — formed when weapon plasma anneals hull metal
|
||||||
|
in the violence of ship destruction. Any wreck yields it, including the
|
||||||
|
player's own; no foundry can replicate it.
|
||||||
|
- **titanium was dropped** (v1 tree). Its hull-gating role moved to
|
||||||
|
quartz-era control systems ("you can smelt all the steel you want, but
|
||||||
|
you cannot steer a battlecruiser without electronics") plus the
|
||||||
|
hardened-steel quality step (a deliberately long-running, time-heavy
|
||||||
|
recipe) — explicitly not sheer steel quantity alone.
|
||||||
|
|
||||||
|
### Material palette (fingerprints per family)
|
||||||
|
|
||||||
|
- **iron/steel** — structure.
|
||||||
|
- **copper** — conduction and heat: wiring, coils, heat sinks.
|
||||||
|
- **silicon family** (all derived from quartz): silicon (logic,
|
||||||
|
sensors), ceramics (heat shielding, insulators); glass/optics are cut
|
||||||
|
from v1 — their only consumers would be lasers, which are deferred.
|
||||||
|
- **voidsteel** — capital-tier structure and exotics.
|
||||||
|
- Deliberately skipped: carbon (mostly redundant with copper/ceramics),
|
||||||
|
plastics (drags in Factorio-style chemical chains; ceramics read more
|
||||||
|
sci-fi anyway), volatiles/ice (materials are build costs only — no
|
||||||
|
consumption mechanic to justify fuel).
|
||||||
|
|
||||||
|
### Weapons
|
||||||
|
|
||||||
|
- All v1 weapons are **railguns** (`railgun_s/m/l`, renamed from the
|
||||||
|
laser placeholders; footprints and the gating matrix unchanged).
|
||||||
|
Implementation is instant damage application with no projectile and no
|
||||||
|
ammunition — the beam visual reads as a tracer round. Materials: iron
|
||||||
|
slugs, copper coils, steel rails — the starting-metal fingerprint.
|
||||||
|
- **Lasers are reserved for later** as a genuinely distinct weapon type
|
||||||
|
(e.g. once projectile/ammunition mechanics exist for other families),
|
||||||
|
arriving with quartz optics. More weapon types are planned; railguns
|
||||||
|
are simply the baseline tech that ships with v1.
|
||||||
|
- `drone_bay` and `drone_hangar` are footprint-only placeholders: the
|
||||||
|
drone-launching capability does not exist in the simulation yet, so
|
||||||
|
they define no capability section. The carrier is deliberately weak
|
||||||
|
until that capability lands (see the accepted imbalances in
|
||||||
|
`docs/balancing/targets.md`).
|
||||||
|
|
||||||
|
### Tree structure
|
||||||
|
|
||||||
|
Input lists only — quantities, durations, and per-item threat values are
|
||||||
|
in `docs/balancing/derived.md` and the configs.
|
||||||
|
|
||||||
|
**Mined (miner):** `iron_ore`, `copper_ore` (every tile), `quartz`
|
||||||
|
(geode deposits in expansion territory).
|
||||||
|
|
||||||
|
**Smelted (smelter — exactly one recipe per input item):**
|
||||||
|
|
||||||
|
| output | input | ratio class |
|
||||||
|
|---|---|---|
|
||||||
|
| iron_ingot | iron_ore | nice (1:1) |
|
||||||
|
| copper_ingot | copper_ore | nice |
|
||||||
|
| silicon | quartz | mid entry |
|
||||||
|
| iron_ingot | scrap | the safe, boring scrap sink |
|
||||||
|
|
||||||
|
**Reprocessing pool (scrap):** `iron_ingot`, `copper_ingot`, `silicon`,
|
||||||
|
`voidsteel` — the only source of voidsteel. Weights authored for the
|
||||||
|
fully unlocked pool state.
|
||||||
|
|
||||||
|
**Tier 2 — early intermediates (clean ratios, ~2:3):**
|
||||||
|
|
||||||
|
| item | inputs | role |
|
||||||
|
|---|---|---|
|
||||||
|
| steel_plate | iron_ingot | structure backbone, highest volume |
|
||||||
|
| copper_wire | copper_ingot | conductors |
|
||||||
|
| copper_coil | copper_wire | electromagnets: railguns, thrusters |
|
||||||
|
| building_block | steel_plate | depth-3 chain = the doubling-time knob |
|
||||||
|
|
||||||
|
**Tier 3 — mid intermediates (strange ratios begin, need quartz):**
|
||||||
|
|
||||||
|
| item | inputs | role |
|
||||||
|
|---|---|---|
|
||||||
|
| control_chip | silicon + copper_wire | electronics gate for m+ hulls |
|
||||||
|
| capacitor_bank | copper_coil + silicon | power for railgun m/l |
|
||||||
|
| hardened_steel | steel_plate (long cycle) | quality gate for m+ hulls; time-heavy |
|
||||||
|
| ceramic_plate | quartz | heat shielding: drives, l guns, capitals |
|
||||||
|
| drive_unit | steel_plate + copper_coil + control_chip | propulsion for m+ hulls |
|
||||||
|
|
||||||
|
**Tier 4 — late intermediates (need voidsteel):**
|
||||||
|
|
||||||
|
| item | inputs | role |
|
||||||
|
|---|---|---|
|
||||||
|
| voidsteel_plate | voidsteel + hardened_steel | capital structure |
|
||||||
|
| capital_core | voidsteel + capacitor_bank + control_chip | capital heart |
|
||||||
|
|
||||||
|
**Hull items** (`<ship>_hull`, assembler-made; the shipyard consumes the
|
||||||
|
hull item plus module materials). The m+ hull gate is **both**
|
||||||
|
hardened_steel (quality steel, the time-heavy step) *and* control_chip
|
||||||
|
(electronics):
|
||||||
|
|
||||||
|
| hull | inputs |
|
||||||
|
|---|---|
|
||||||
|
| drone_hull | iron_ingot |
|
||||||
|
| frigate_hull | steel_plate + copper_wire |
|
||||||
|
| destroyer_hull | steel_plate + copper_coil |
|
||||||
|
| cruiser_hull | hardened_steel + control_chip |
|
||||||
|
| battlecruiser_hull | hardened_steel + control_chip + drive_unit |
|
||||||
|
| battleship_hull | voidsteel_plate + drive_unit + control_chip |
|
||||||
|
| dreadnought_hull | voidsteel_plate + capital_core + drive_unit |
|
||||||
|
| carrier_hull | voidsteel_plate + capital_core + drive_unit |
|
||||||
|
|
||||||
|
**Module items** (`<module>_module`, assembler-made prefabs — kept as
|
||||||
|
items so shipyard belt inputs stay simple and module production can be
|
||||||
|
stockpiled):
|
||||||
|
|
||||||
|
| module | inputs | archetype |
|
||||||
|
|---|---|---|
|
||||||
|
| railgun_s | copper_coil | lean |
|
||||||
|
| salvager | steel_plate + copper_wire | balanced |
|
||||||
|
| repair_tool | steel_plate + copper_wire | balanced |
|
||||||
|
| armor_plates | steel_plate (many) | material-heavy, fast |
|
||||||
|
| maneuvering_thrusters | steel_plate + copper_coil | balanced |
|
||||||
|
| sensor_booster | copper_wire + copper_coil | lean (an antenna, no chip) |
|
||||||
|
| afterburner | copper_coil + steel_plate | balanced |
|
||||||
|
| weapon_stabilizer | steel_plate + copper_coil | balanced |
|
||||||
|
| weapon_primer | capacitor_bank + copper_coil | mid; time-heavy |
|
||||||
|
| weapon_upgrade | control_chip + copper_coil | mid; time-heavy |
|
||||||
|
| railgun_m | capacitor_bank + steel_plate + copper_coil | mid |
|
||||||
|
| drone_bay | control_chip + steel_plate + copper_coil | mid |
|
||||||
|
| railgun_l | capacitor_bank + hardened_steel + ceramic_plate | late |
|
||||||
|
| drone_hangar | voidsteel_plate + control_chip + drive_unit | late (carrier only) |
|
||||||
|
|
||||||
|
**Refactorability check** (the default technique holds): railgun_s → m
|
||||||
|
introduces capacitor_bank, built from a subset of the small gun's inputs
|
||||||
|
(copper_coil) plus the new base resource (silicon); the m gun otherwise
|
||||||
|
reuses the small gun's inputs. Hulls likewise: the cruiser adds
|
||||||
|
hardening (fed by the existing steel line) and chips (fed by the new
|
||||||
|
quartz territory) without touching the iron/copper core.
|
||||||
|
|
||||||
|
**Shortcut recipes** (drop-only assembler schematics; not every strange
|
||||||
|
chain gets one): `iron_ore → steel_plate` (skips the ingot step on the
|
||||||
|
highest-volume chain), `quartz → control_chip` (skips silicon),
|
||||||
|
`iron_ingot → hardened_steel` (a nicer-ratio route past the deliberately
|
||||||
|
awkward hardening step).
|
||||||
|
|
||||||
|
Consistency is checked by `tools/verify_recipes.py` — re-run it after
|
||||||
|
editing recipes, ship/module materials, or visuals:
|
||||||
|
|
||||||
|
python dota_factory/tools/verify_recipes.py
|
||||||
|
|
||||||
|
It verifies every consumed item has a producer, every item has a visuals
|
||||||
|
entry, flags orphaned items, and prints which items are
|
||||||
|
reprocessing-only (currently exactly voidsteel).
|
||||||
482
docs/replay_design.md
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
# Replay — Design
|
||||||
|
|
||||||
|
This document captures the design for the replay record/playback feature. It records the
|
||||||
|
decisions made during design discussion; it is a complement to `architecture.md`. No
|
||||||
|
implementation exists yet — this is the agreed design to implement against.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Record every play session and allow it to be played back later. Playback is **view-only**
|
||||||
|
(no interaction) with **manual game-speed selection** (including pause). Playback is launched
|
||||||
|
via a command-line argument to the executable.
|
||||||
|
|
||||||
|
## Approach: deterministic command-replay (re-simulation)
|
||||||
|
|
||||||
|
We record **player intent** (commands) plus the inputs needed to reproduce the run, and on
|
||||||
|
playback we **re-run the real simulation**, injecting the recorded commands at their recorded
|
||||||
|
ticks. We do **not** record per-tick state snapshots.
|
||||||
|
|
||||||
|
This is viable because the simulation is already built for it (see `architecture.md`:
|
||||||
|
"determinism, replayability ... fall out for free"):
|
||||||
|
|
||||||
|
- Fixed 30 Hz tick-based simulation, decoupled from render rate via `TickDriver`.
|
||||||
|
- Game speed (0/0.5/1/2/4×) and pause are tick-rate multipliers — they change *how many*
|
||||||
|
ticks run per frame, never the *outcome* of a tick. So speed, pause, camera scroll, and
|
||||||
|
selection are pure presentation and are **not recorded**.
|
||||||
|
- A single deterministic RNG stream: `Simulation::m_rng` (one `std::mt19937`) is passed by
|
||||||
|
reference into `WaveSystem` and `BuildingSystem`, the only two consumers. ECS combat/AI/
|
||||||
|
movement/scrap systems use no RNG. The `utility::getRandom*` global is not used by the sim.
|
||||||
|
- Config is immutable after load; a replay is pinned to the config it was recorded with.
|
||||||
|
|
||||||
|
A replay run is therefore a pure function of `(seed, config, ordered commands)`.
|
||||||
|
|
||||||
|
### What we do NOT do (now)
|
||||||
|
|
||||||
|
- No per-tick / keyframe state snapshots.
|
||||||
|
- No backward seek / scrubbing (would require snapshots).
|
||||||
|
- No save/load. (See "Future direction".)
|
||||||
|
- No interactive playback (no taking over a replay mid-run).
|
||||||
|
|
||||||
|
## Replay commands
|
||||||
|
|
||||||
|
A *replay command* is the resolved, serializable **intent** behind a player action — the
|
||||||
|
data, not the UI gesture. Example: placing a miner records
|
||||||
|
`PlaceBuilding{type=Miner, anchor=(3,5), rotation=East}`, not the mouse pixel that produced it.
|
||||||
|
|
||||||
|
- Commands are at **intent level, resolved to tile coordinates / domain ids** — independent
|
||||||
|
of window size, camera scroll, and DPI.
|
||||||
|
- Command payloads reference **stable, deterministic domain ids** (`BuildingId`, tile
|
||||||
|
coordinates, choice indices) — never raw `entt::entity` handles. These ids are sim-allocated
|
||||||
|
deterministically, so a recorded command resolves to the same entity on replay.
|
||||||
|
- Camera scroll, selection, game speed, and pause are **not** commands.
|
||||||
|
|
||||||
|
### Command vocabulary
|
||||||
|
|
||||||
|
One command per sim-mutating operation (the complete mutation surface):
|
||||||
|
|
||||||
|
- `PlaceBuilding`
|
||||||
|
- `Deconstruct`
|
||||||
|
- `RotateInPlace`
|
||||||
|
- `SetRecipe`
|
||||||
|
- `SetShipLayout`
|
||||||
|
- `SetSplitterFilters` (building-site and belt variants)
|
||||||
|
- `ClearBeltTiles`
|
||||||
|
- `ApplySchematicChoice`
|
||||||
|
- `Reset` / restart — see "Restart is a boundary".
|
||||||
|
|
||||||
|
### Command representation
|
||||||
|
|
||||||
|
Commands use a **base class + derived classes** (mirroring the existing `Event` hierarchy
|
||||||
|
idiom, so it is native to this codebase). They are routed through a dedicated command path,
|
||||||
|
**not** through `EventManager` (see next section).
|
||||||
|
|
||||||
|
> **Implementation refinement (Phase 1).** `PlaceBuilding` is **atomic**: it carries the
|
||||||
|
> optional recipe / ship-layout / splitter-filters to configure the new building in the same
|
||||||
|
> command. This is forced by the deferred-drain timing — commands apply at a later tick
|
||||||
|
> boundary, so the caller never sees the new `BuildingId` and therefore cannot issue a
|
||||||
|
> follow-up `SetRecipe`/`SetShipLayout` against it. The standalone `SetRecipe`,
|
||||||
|
> `SetShipLayout`, and the two `SetSplitterFilters` commands remain for the dialog-driven
|
||||||
|
> edits on *existing* buildings (which reference a known id). `Reset` carries the (move-only)
|
||||||
|
> `GameConfig` via `shared_ptr` and is moved into the sim on apply; a null config means "keep
|
||||||
|
> current config".
|
||||||
|
|
||||||
|
## Command system: reuse the *pattern*, not the EventManager singleton
|
||||||
|
|
||||||
|
We reuse the **pattern** of the existing event system (a polymorphic base + small derived
|
||||||
|
types), but the sim-mutating command path is a **dedicated, ordered queue**, not the
|
||||||
|
`EventManager` pub/sub bus. Reasons:
|
||||||
|
|
||||||
|
1. **Determinism / ordering.** Sim mutations must apply in a strict, tick-pinned, recorded
|
||||||
|
order. `architecture.md` deliberately keeps the sim free of `EventManager` for exactly this
|
||||||
|
reason (determinism, tick-order fidelity, headless testability — why `BeamFiredEvent` uses a
|
||||||
|
plain vector). Routing commands into the sim via the singleton would break that.
|
||||||
|
2. **Single consumer.** A command has exactly one recipient (the `Simulation`); pub/sub
|
||||||
|
N-handler fan-out is the wrong shape.
|
||||||
|
3. **Recording chokepoint.** One place must see every command, stamp its tick, append it to the
|
||||||
|
file, and apply it. A direct queue gives that; a multi-handler bus does not.
|
||||||
|
4. **Headless tests.** Tests link only `lib` and build a `Simulation` directly; the command
|
||||||
|
type and apply path live in `lib` and must work with no UI and no singleton.
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
- **In `lib`:** a `Command` base class + derived command types, plus a `CommandManager`
|
||||||
|
(ordered queue) and a single `Simulation::apply(command)` chokepoint.
|
||||||
|
- **UI fan-in still uses `EventManager`:** widgets emit a UI event as today; a single
|
||||||
|
dispatcher/recorder catches it, builds the `lib` command, and hands it to the
|
||||||
|
`CommandManager`. This keeps widgets decoupled (consistent with current architecture).
|
||||||
|
- **Replay** skips the UI half and feeds commands straight into the same `CommandManager` /
|
||||||
|
`Simulation::apply` chokepoint.
|
||||||
|
|
||||||
|
### The completeness invariant (enforced structurally)
|
||||||
|
|
||||||
|
**Every** sim mutation must flow through the single `CommandManager → Simulation::apply`
|
||||||
|
chokepoint. Any path that mutates the sim directly would not be recorded and would silently
|
||||||
|
desync the replay.
|
||||||
|
|
||||||
|
This is enforced **structurally**: the `Simulation` player-action mutators are **private**, so
|
||||||
|
the only way production code can reach them is `apply(command)`.
|
||||||
|
|
||||||
|
> **Implementation decision (Phase 1, revised post-Phase 4).** Structural enforcement was
|
||||||
|
> initially deferred in favour of convention, because the test suite legitimately drives the
|
||||||
|
> same mutators directly and relies on their return values (notably the `BuildingId` from
|
||||||
|
> placement, which `apply()` cannot hand back to a caller). It was later restored once a key
|
||||||
|
> observation made the change cheap: **the UI's only handle to a mutable subsystem is through
|
||||||
|
> `Simulation`** — no production code in `ui`/`app`/`balancing` holds a `BuildingSystem`/
|
||||||
|
> `BeltSystem` directly, and every production `buildings()`/`belts()` call is a const query.
|
||||||
|
> So:
|
||||||
|
>
|
||||||
|
> - `Simulation::tryPlaceBuilding`, `deconstruct`, and `applySchematicChoice` are **private**.
|
||||||
|
> - The mutable subsystem accessors are private and renamed `buildingsMutable()` /
|
||||||
|
> `beltsMutable()`; only `const BuildingSystem& buildings() const` / `belts() const` are
|
||||||
|
> public (queries). UI query sites bind to the const overload unchanged.
|
||||||
|
> - `Simulation::apply` still mutates through the private members directly, so the chokepoint
|
||||||
|
> itself is unaffected.
|
||||||
|
> - Tests reach the private mutators through `SimulationTestAccess` (src/test, a `friend struct`
|
||||||
|
> of `Simulation`), so they keep calling the real mutators **and keep getting return values**
|
||||||
|
> — no id-by-position recovery needed. This header is not on the lib/ui/app include path, so
|
||||||
|
> only test translation units can use it.
|
||||||
|
>
|
||||||
|
> The `BuildingSystem` subsystem mutators (`place`, `setRecipe`, `placeImmediate`,
|
||||||
|
> `forEachBuilding`, …) stay **public**: `BuildingTest` unit-tests a bare `BuildingSystem` with
|
||||||
|
> no `Simulation`/command layer, and that surface is unreachable from production anyway (you
|
||||||
|
> cannot obtain a mutable subsystem without the private accessor). A `[command]` Catch2 suite
|
||||||
|
> still asserts `apply(...)` produces byte-identical state to the direct mutator path, guarding
|
||||||
|
> the equivalence the replay relies on. Tests are not gameplay (they never record), so direct
|
||||||
|
> mutator use there does not affect replay correctness.
|
||||||
|
|
||||||
|
Recording happens **at the apply chokepoint**, not at the UI gesture — so only commands that
|
||||||
|
actually reached the sim are recorded, and they replay through the identical apply path.
|
||||||
|
UI-side validation (placement validity, affordability) remains a pre-filter that simply does
|
||||||
|
not produce a command unless the action reaches the sim.
|
||||||
|
|
||||||
|
## Command timing: drain once per frame, before the tick batch
|
||||||
|
|
||||||
|
- During live play, input pushes commands onto the `CommandManager` queue (not applied
|
||||||
|
synchronously).
|
||||||
|
- The queue is drained at **one defined point: once per frame, before stepping the tick
|
||||||
|
batch.** The whole queue is drained in FIFO order (not one-per-tick), so bursts (e.g. laying
|
||||||
|
many belts quickly) apply immediately instead of dribbling across ticks, and it matches the
|
||||||
|
lockstep model wanted later.
|
||||||
|
- Each drained command is **tagged with the current completed-tick count**, recorded at drain
|
||||||
|
time (so record-order == apply-order canonically), and applied.
|
||||||
|
|
||||||
|
### Build-while-paused is preserved
|
||||||
|
|
||||||
|
The drain runs every frame including at 0× (the tick batch is simply empty when paused). So a
|
||||||
|
player can place buildings while paused and **see the construction sites immediately**. This is
|
||||||
|
still fully deterministic: replay applies each command at its recorded tick regardless of the
|
||||||
|
frame cadence that produced it.
|
||||||
|
|
||||||
|
On replay, there is no input; the player applies each pre-filled command at its recorded tick
|
||||||
|
through the same drain path, preserving order.
|
||||||
|
|
||||||
|
The Qt single-threaded event loop guarantees input events and the `onFrame` tick-batch never
|
||||||
|
interleave, so the completed-tick count at drain time is unambiguous. (If the sim is ever moved
|
||||||
|
to a worker thread, this needs a lock at the sim boundary.)
|
||||||
|
|
||||||
|
## Determinism: checksums and verification
|
||||||
|
|
||||||
|
We do not verify EnTT iteration order statically. EnTT view iteration is a pure function of the
|
||||||
|
sequence of spawn/destroy/add/remove operations, so on a fixed binary it contributes zero
|
||||||
|
run-to-run nondeterminism. Instead we verify **end-to-end determinism** with a state checksum,
|
||||||
|
and any divergence (EnTT order, float, container ordering, etc.) surfaces loudly.
|
||||||
|
|
||||||
|
### What is checksummed (now)
|
||||||
|
|
||||||
|
- **RNG state only**, for now. The `mt19937` state is fingerprinted into a 64-bit value.
|
||||||
|
- The hash can be extended later (entity positions/HP, belt items, building buffers, scalars)
|
||||||
|
without changing the format.
|
||||||
|
|
||||||
|
### Cadence
|
||||||
|
|
||||||
|
- **In the replay file:** every **30 ticks**, **and** after **every command** is applied. The
|
||||||
|
per-command checksum pins any divergence to the action that triggered it; the periodic one
|
||||||
|
localizes drift to a ~1 s window. On playback the recomputed checksum is compared; a mismatch
|
||||||
|
reports "desync at tick N".
|
||||||
|
- **In tests:** the Catch2 **double-run determinism test** hashes **full sim state every tick**
|
||||||
|
(not just RNG). It runs a scripted command sequence twice from the same seed and asserts
|
||||||
|
per-tick checksums match. This keeps the file lean while still catching non-RNG determinism
|
||||||
|
bugs during development.
|
||||||
|
|
||||||
|
### Known limitation of the RNG-only file checksum (accepted)
|
||||||
|
|
||||||
|
An RNG-only checksum only catches divergences that change **how much randomness is consumed**
|
||||||
|
(wave composition, recipe rolls, scrap). Float or iteration drift that does **not** alter RNG
|
||||||
|
draw counts passes the checksum undetected. This is acceptable for same-binary Windows replay
|
||||||
|
(no float drift expected on an identical binary; the checksum's real job there is catching
|
||||||
|
determinism *bugs*). When cross-platform replay becomes a goal, the **file** hash must be
|
||||||
|
expanded to include entity state.
|
||||||
|
|
||||||
|
## Cross-platform: Windows-first, portable by construction
|
||||||
|
|
||||||
|
The replay file is platform-neutral data; `std::mt19937` is bit-identical across platforms, so
|
||||||
|
RNG is not a cross-platform problem. The only real cross-platform issue is **floating-point
|
||||||
|
reproducibility** — the sim does heavy `QVector2D` float math, and a 1-ULP difference (compiler
|
||||||
|
/ CPU / SIMD / FMA contraction) can flip an in-range comparison and cascade into different ship
|
||||||
|
behavior (the classic lockstep-RTS problem).
|
||||||
|
|
||||||
|
Decision: **Windows-only first**, but make the later swap cheap and bounded by, from day one:
|
||||||
|
|
||||||
|
- a **per-period state checksum** in the file (above), and
|
||||||
|
- a **build/version + config-hash identity tag** in the header.
|
||||||
|
|
||||||
|
Then cross-platform later is a contained float-hardening pass (`/fp:strict`, no FMA contraction,
|
||||||
|
possibly fixed-point positions) guided by the checksums — **not** a redesign of the
|
||||||
|
command-replay architecture.
|
||||||
|
|
||||||
|
Note: even a new Windows *build* of the game can desync old replays for the same float reasons,
|
||||||
|
so the version tag + "warn on mismatch" is needed regardless of cross-platform ambitions.
|
||||||
|
|
||||||
|
## Seed and config
|
||||||
|
|
||||||
|
- **Seed:** a **random** seed is generated at the start of each run, **outside** the sim (e.g.
|
||||||
|
`std::random_device` in `main`/reset), so the `Simulation` stays a pure function of
|
||||||
|
`(seed, config, commands)`. The seed is written to the replay header.
|
||||||
|
- **Config:** the header stores a **config hash** (not a full config snapshot). On playback the
|
||||||
|
current config is hashed and compared; a mismatch warns/refuses. The hash is taken over the
|
||||||
|
actually-loaded config (so editing config files and restarting yields a new, consistent
|
||||||
|
replay).
|
||||||
|
|
||||||
|
## File format: line-oriented append-friendly text
|
||||||
|
|
||||||
|
Non-binary, chosen for readability and crash-safety. Size is a non-issue: the command log is
|
||||||
|
sparse (only ticks with a player action), so even a multi-hour game is tens of KB in any text
|
||||||
|
format.
|
||||||
|
|
||||||
|
- A small keyed/header section: seed, config hash, build/version, start timestamp.
|
||||||
|
- One line per command, e.g. `1234 place miner 3 5 E`.
|
||||||
|
- Periodic checksum lines interleaved, e.g. `# checksum 9000 a1b2c3...`.
|
||||||
|
|
||||||
|
Why line-oriented text:
|
||||||
|
|
||||||
|
- **Append-friendly** — the recorder stream-appends as the game runs, so a crash does not lose
|
||||||
|
the replay (a crash is exactly when you would want it). A format that must be rewritten/closed
|
||||||
|
as a whole is rejected for this reason.
|
||||||
|
- **No new dependency** — the project has no JSON lib; toml++ is parse-oriented and clunky for a
|
||||||
|
long event stream (fine for the header, awkward as an array-of-tables of thousands of
|
||||||
|
entries).
|
||||||
|
- Greppable, diffable, tiny.
|
||||||
|
- Aligns with the project's existing text-serialization idiom (`BlueprintSerializer`,
|
||||||
|
`ShipLayoutBlueprintSerializer`).
|
||||||
|
|
||||||
|
## Recording lifecycle
|
||||||
|
|
||||||
|
- **Record every run.** A new replay file is created at `Simulation` construction and at each
|
||||||
|
`reset()`.
|
||||||
|
- **Restart is a boundary.** Restart (escape menu → restart, which reloads config and resets)
|
||||||
|
closes the current file and opens a new one with a fresh seed and header. One replay file =
|
||||||
|
one contiguous run from tick 0 to game-over/quit.
|
||||||
|
- **Retention: keep everything.** Files live in the existing `data/` directory, named by
|
||||||
|
timestamp + seed. (No automatic pruning for now.)
|
||||||
|
|
||||||
|
## Playback
|
||||||
|
|
||||||
|
Launched via a command-line argument, e.g. `DotaFactory.exe --replay <file>`.
|
||||||
|
|
||||||
|
`main` for the `--replay` path:
|
||||||
|
|
||||||
|
1. Read the header → validate config hash and build/version (warn on mismatch).
|
||||||
|
2. Construct the `Simulation` from the recorded seed + config.
|
||||||
|
3. Construct the `CommandManager` in **Replay mode**, **pre-filled** with the whole command list
|
||||||
|
from the file. (Pre-fill memory is trivial; streaming-read is a later optimization if files
|
||||||
|
ever get huge — not needed now.)
|
||||||
|
4. Run the driver in replay mode: each frame, drain commands due at the reached tick (same drain
|
||||||
|
path as live), step ticks, compare checksums.
|
||||||
|
|
||||||
|
### Replay mode rules
|
||||||
|
|
||||||
|
Reframe: the schematic-choice modal is **an input source** (the device that produces an
|
||||||
|
`ApplySchematicChoice` command in live play), exactly like the mouse. Replay's single rule is
|
||||||
|
"**disable live input sources**", which the modal falls under.
|
||||||
|
|
||||||
|
- **`CommandManager` in replay mode:** `addCommand` is a no-op; the queue is pre-filled from the
|
||||||
|
file. Live input therefore produces nothing.
|
||||||
|
- **Only two reactions need explicit gating** — the sim-state *polls* in `onFrame` that emit
|
||||||
|
`SchematicChoicesAvailableEvent` and `GameOverEvent`. In replay these polls do not run, so no
|
||||||
|
modal opens, no auto-pause occurs, and there is no deadlock against the recorded command.
|
||||||
|
- **Everything else falls away for free** because it is click-driven, not sim-state-driven: the
|
||||||
|
recipe dialog (`RecipeSelectionRequestedEvent`), ship-layout dialog
|
||||||
|
(`LayoutDialogRequestedEvent`), and escape menu are all triggered by player input, which is
|
||||||
|
disabled — so they never open and need no special handling.
|
||||||
|
- **Schematic choice still resolves with no UI:** the sim regenerates identical choices
|
||||||
|
deterministically (same seed + prior commands), and the pre-filled `ApplySchematicChoice`
|
||||||
|
applies itself at its recorded tick through the normal drain path. The tick-tag invariant
|
||||||
|
places it correctly relative to when the choices became pending, in both record and replay.
|
||||||
|
- **Game-over is replaced, not just suppressed:** instead of the live restart/quit dialog,
|
||||||
|
playback detects the end condition (command stream exhausted / recorded game-over reached) and
|
||||||
|
stops, showing a passive "replay ended" state.
|
||||||
|
- **Kept in replay:** the renderer/view and **manual game-speed selection** (including pause /
|
||||||
|
0× and fast-forward via high speed). Playback only ever moves forward.
|
||||||
|
|
||||||
|
## Future direction (informs the design, not built now)
|
||||||
|
|
||||||
|
Save/load and (deterministic lockstep) multiplayer are wanted later. The command bus is the
|
||||||
|
shared foundation; two cheap shaping decisions now keep that path open:
|
||||||
|
|
||||||
|
1. **Each command carries a source/player id** (always "player 0" in single-player). Lockstep
|
||||||
|
multiplayer is just commands from multiple sources merged into one ordered stream.
|
||||||
|
2. **Commands are applied at a defined tick boundary** (already required for replay). Multiplayer
|
||||||
|
schedules them a few ticks in the future to hide latency; single-player uses the next drain.
|
||||||
|
|
||||||
|
Implications to note:
|
||||||
|
|
||||||
|
- Multiplayer makes cross-platform float determinism mandatory and promotes the checksum to
|
||||||
|
load-bearing desync-detection (rather than a test aid) — reinforcing doing the checksum now.
|
||||||
|
- **Save/load** is the one feature that needs a *different* mechanism: either "replay to current
|
||||||
|
tick" on load (reuses 100% of replay machinery; load time grows with game length, though
|
||||||
|
fast-forward usually replays hours in seconds), or a full **state-snapshot serializer**
|
||||||
|
(EnTT registry + belts + buildings + scalars). The snapshot serializer is also what
|
||||||
|
backward-seek/scrubbing would need. Building the command bus now does not block adding it
|
||||||
|
later; it is explicitly out of scope here.
|
||||||
|
|
||||||
|
## Summary of decisions
|
||||||
|
|
||||||
|
- Approach: **A — deterministic command-replay** (re-simulation), no snapshots.
|
||||||
|
- Scope: **view-only** playback + **manual speed selection**; launched via CLI argument.
|
||||||
|
- Commands: **base class + derived types**, routed through a dedicated `CommandManager` queue
|
||||||
|
and a single `Simulation::apply` chokepoint; sim mutators made non-public to **enforce** the
|
||||||
|
chokepoint. UI fan-in still uses `EventManager`.
|
||||||
|
- Timing: queue **drained once per frame before the tick batch**, whole queue FIFO, each command
|
||||||
|
tick-tagged; **build-while-paused preserved**.
|
||||||
|
- Determinism: **RNG-state checksum** in the file every **30 ticks + after each command**;
|
||||||
|
**full-state per-tick hashing** in the Catch2 double-run test. Known RNG-only blind spot
|
||||||
|
accepted for now.
|
||||||
|
- Platform: **Windows-first**; file format + version/config-hash make a later cross-platform
|
||||||
|
pass contained.
|
||||||
|
- Seed: **random**, generated outside the sim, written to the header.
|
||||||
|
- Config: **config hash** in the header, validated on playback.
|
||||||
|
- File: **line-oriented append-friendly text**, kept in `data/`, **one file per run**,
|
||||||
|
**retain everything**.
|
||||||
|
- Restart: **a boundary** — new file, new seed.
|
||||||
|
- Replay mode: `CommandManager` `addCommand` is a no-op + pre-filled; gate the two sim-state
|
||||||
|
polls (schematic choices, game-over); passive "replay ended" instead of the game-over dialog;
|
||||||
|
keep view + speed.
|
||||||
|
|
||||||
|
## Implementation plan
|
||||||
|
|
||||||
|
Ordered to de-risk: prove determinism first, then build the command path, then recording, then
|
||||||
|
playback. Each phase is independently testable and leaves the game in a working state. Phases
|
||||||
|
0 → 1 → 2 → 3 are strictly sequential; Phase 4 tests can start as soon as their subject exists.
|
||||||
|
|
||||||
|
### Phase 0 — Determinism foundation & verification (no replay yet)
|
||||||
|
|
||||||
|
The whole feature rests on a deterministic sim, so prove that before building on it.
|
||||||
|
|
||||||
|
- Add a `mt19937` state **fingerprint** (fold its serialized state into a 64-bit value).
|
||||||
|
- Add a **full-state checksum** path (positions, HP, velocities, belt items, building buffers,
|
||||||
|
scalars), used by tests; each subsystem contributes via its own `appendChecksum(Hasher&)` so
|
||||||
|
no state knowledge is duplicated.
|
||||||
|
- Add a Catch2 **double-run determinism test**: run a scripted sequence twice from the same
|
||||||
|
seed, assert per-tick **full-state** checksums match.
|
||||||
|
- **Files:** new `lib/sim` checksum helper; small additions to `Simulation`, `BeltSystem`,
|
||||||
|
`BuildingSystem`, ECS state; new test.
|
||||||
|
- **Exit criteria:** the double-run test passes. If it fails, fix the nondeterminism here before
|
||||||
|
proceeding.
|
||||||
|
|
||||||
|
### Phase 1 — Command model + chokepoint (no recording yet) — DONE
|
||||||
|
|
||||||
|
Reshape mutations to flow through one path; behaviour unchanged.
|
||||||
|
|
||||||
|
- Defined `Command` base + derived types (`PlaceBuilding`, `Deconstruct`, `RotateInPlace`,
|
||||||
|
`SetRecipe`, `SetShipLayout`, `SetSiteSplitterFilters`, `SetSplitterFilters`,
|
||||||
|
`ClearBeltTiles`, `ApplySchematicChoice`, `Reset`) in `lib`, each with a `playerId` (always 0
|
||||||
|
now). `PlaceBuilding` is atomic (carries optional config — see the refinement note above).
|
||||||
|
- Added `CommandManager` (FIFO queue, `enqueue`/`drain`) in `lib`, holding a `Simulation&`.
|
||||||
|
- Added `Simulation::apply(const Command&)` dispatching by `CommandKind` to the underlying
|
||||||
|
mutators — the single chokepoint. The `Simulation` player-action mutators are **private**
|
||||||
|
(compile-time enforced; tests reach them via the `SimulationTestAccess` friend) — see the
|
||||||
|
decision note above.
|
||||||
|
- Wired the drain: `GameWorldView::onFrame` calls `CommandManager::drain()` once per frame,
|
||||||
|
before the tick batch (runs even at 0× → build-while-paused preserved). A drained `Reset`
|
||||||
|
triggers the view reset.
|
||||||
|
- Refactored every UI mutation site: `GameWorldView` owns the `CommandManager` and enqueues
|
||||||
|
directly; `MainWindow` and `SelectionPanel` emit `CommandRequestedEvent` (carrying a
|
||||||
|
`shared_ptr<const Command>`) which `GameWorldView` subscribes to and enqueues.
|
||||||
|
- **Files:** new `lib/sim/Command.h`, `CommandManager.{h,cpp}`; `CommandRequestedEvent.h`;
|
||||||
|
`Simulation.{h,cpp}` (`apply`); `GameWorldView.{h,cpp}`, `MainWindow.cpp`,
|
||||||
|
`SelectionPanel.cpp`; new `CommandTest.cpp`.
|
||||||
|
- **Exit criteria:** game plays identically (including build-while-paused); determinism test
|
||||||
|
still passes; `[command]` equivalence tests pass; no production call site can mutate the sim
|
||||||
|
directly (compile-enforced: the `Simulation` mutators are private, tests excepted via
|
||||||
|
`SimulationTestAccess`).
|
||||||
|
|
||||||
|
### Phase 2 — Recording — DONE
|
||||||
|
|
||||||
|
- `ReplayRecorder` (lib) writes the **line-oriented append file**: header (`version`, `build`,
|
||||||
|
`seed`, `config_hash`, `timestamp`) then `---`, then one tick-tagged line per command
|
||||||
|
interleaved with `# checksum <tick> <hex>` lines. Each line is flushed, so a crash mid-run
|
||||||
|
leaves a valid partial file. `CommandSerializer` produces the per-command text (length-prefixed
|
||||||
|
variable parts; `ShipLayoutConfig`/filters serialized inline). The build tag is
|
||||||
|
`__DATE__ " " __TIME__`; the config hash is a 64-bit FNV over the `*.toml` files in the config
|
||||||
|
dir (re-hashed on playback to detect mismatch).
|
||||||
|
- **Random seed** generated in `main` (and on each restart in `MainWindow`) via
|
||||||
|
`std::random_device`; `Simulation` retains it (`getSeed()`) for the header.
|
||||||
|
- **Recorder hooked at the chokepoint:** `CommandManager` owns an optional `ReplayRecorder`;
|
||||||
|
`drain()` records each applied command (tick-tagged) + a post-apply RNG checksum, and
|
||||||
|
`recordTickCheckpoint()` (called per tick from the `onFrame` loop) writes a checksum every 30
|
||||||
|
ticks. A drained `Reset` rolls the recorder to a new file (restart = boundary).
|
||||||
|
- **Lifecycle:** `GameWorldView` attaches the recorder at construction (opens the first file with
|
||||||
|
the initial seed + a tick-0 checksum); files live in `<data>/replays`, named
|
||||||
|
`<timestamp>_<seed>.replay`; everything is retained.
|
||||||
|
- **Files:** new `lib/sim/ReplayRecorder.{h,cpp}`, `CommandSerializer.{h,cpp}`; `Simulation`
|
||||||
|
(`getSeed`); `CommandManager` (recorder + tick checkpoint); `main.cpp` (seed);
|
||||||
|
`MainWindow.cpp` / `GameWorldView.{h,cpp}` (wiring); new `ReplayRecorderTest.cpp`.
|
||||||
|
- **Exit criteria met:** recorder + serializer + drain-integration tests pass; the format is
|
||||||
|
well-formed and flushed per line. (Live GUI recording is wired but not auto-tested here.)
|
||||||
|
|
||||||
|
### Phase 3 — Playback — DONE
|
||||||
|
|
||||||
|
- `ReplayReader` (lib) parses the file into `{ header, entries }`, where each entry is a command
|
||||||
|
(with its tick) or a checksum (with its tick), kept in **file order**. `CommandSerializer`
|
||||||
|
gained the inverse `parseCommand` (round-tripping every verb).
|
||||||
|
- `--replay <file>` CLI path in `main`: reads the file, **warns** on version / config-hash
|
||||||
|
mismatch (proceeds anyway), constructs the `Simulation` from the header seed, and threads the
|
||||||
|
parsed replay through `MainWindow` to `GameWorldView`.
|
||||||
|
- `ReplayPlayer` (lib) is the playback driver. Rather than reproduce frame batching, it applies
|
||||||
|
each command at its **exact recorded tick** and verifies checksums **in file order**:
|
||||||
|
`start()` processes the tick-0 entries, then after every `sim.tick()` `advanceTo(tick)`
|
||||||
|
consumes that tick's entries (periodic checksum first, then command + its checksum — the order
|
||||||
|
the file already has). This makes playback independent of replay-time speed/pause.
|
||||||
|
- `GameWorldView` runs the player in `onFrame` when in replay mode (manual speed/pause kept,
|
||||||
|
forward-only); `CommandManager` is put in **replay mode** so live input is a no-op. The two
|
||||||
|
sim-state polls (schematic-choices, game-over) are **gated off**; dialog/escape paths are
|
||||||
|
input-driven and fall away. A **"REPLAY"** tag plus a passive **"Replay ended"** /
|
||||||
|
**"Desync at tick N"** overlay replaces the restart dialog.
|
||||||
|
- **Files:** new `lib/sim/ReplayReader.{h,cpp}`, `ReplayPlayer.{h,cpp}`; `CommandSerializer`
|
||||||
|
(`parseCommand`); `ReplayRecorder` (shared `computeReplayConfigHash`); `CommandManager`
|
||||||
|
(replay mode); `main.cpp`; `MainWindow.{h,cpp}`; `GameWorldView.{h,cpp}`; new
|
||||||
|
`ReplayPlaybackTest.cpp`.
|
||||||
|
- **Exit criteria met:** the headless `ReplayPlaybackTest` records a scripted run, reads it back,
|
||||||
|
replays it, and asserts **no desync** and a **byte-identical final state checksum** — including
|
||||||
|
the periodic-checksum-then-command ordering at a shared tick. (Live GUI playback is wired but
|
||||||
|
not auto-tested here.)
|
||||||
|
|
||||||
|
### Phase 4 — Closing tests & polish — DONE
|
||||||
|
|
||||||
|
- **Round-trip:** every command verb serializes → parses → re-serializes identically;
|
||||||
|
malformed input is rejected (`parseCommand` returns nullptr).
|
||||||
|
- **Replay-equivalence (headless):** a short scripted run and a **long ~2400-tick run through
|
||||||
|
waves/combat** each record → read → replay with **no desync** and a **byte-identical final
|
||||||
|
state checksum**.
|
||||||
|
- **Desync detection:** corrupting one recorded checksum makes the player report the exact
|
||||||
|
desync tick.
|
||||||
|
- **Reset boundary:** a `Reset` drained through `CommandManager` rolls the recorder to a new
|
||||||
|
file (named by the new seed).
|
||||||
|
- **Polish:** the end-of-replay / desync overlay dims the world behind the message for
|
||||||
|
readability; config/version mismatch is warned to the log on launch (its visible consequence,
|
||||||
|
a desync, is already surfaced by the overlay).
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
Record + playback is functionally complete and covered by headless tests. Still deferred (per
|
||||||
|
this design): snapshots, save/load, backward-seek, cross-platform float hardening, expanding the
|
||||||
|
file checksum beyond RNG. Known minor rough edge: in replay mode the recipe/layout dialogs and
|
||||||
|
escape→restart can still open but do nothing (their commands hit the no-op enqueue); fully
|
||||||
|
disabling that input UI is polish, not correctness.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- Phase 1 is the largest (the mutation-site refactor); Phase 0 is the riskiest (it may surface
|
||||||
|
latent nondeterminism that must be fixed first).
|
||||||
|
- Still deferred (per this design): snapshots, save/load, backward-seek, cross-platform float
|
||||||
|
hardening, expanding the file checksum beyond RNG.
|
||||||
@@ -1,16 +1,23 @@
|
|||||||
set(TARGET_BASE_NAME "DotaFactory")
|
set(TARGET_BASE_NAME "${PRODUCT_NAME}")
|
||||||
|
|
||||||
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
||||||
set(TARGET_LIB_NAME "${TARGET_BASE_NAME}_lib")
|
set(TARGET_LIB_NAME "${TARGET_BASE_NAME}_lib")
|
||||||
set(TARGET_UI_NAME "${TARGET_BASE_NAME}_ui")
|
set(TARGET_UI_NAME "${TARGET_BASE_NAME}_ui")
|
||||||
set(TARGET_TEST_NAME "${TARGET_BASE_NAME}_test")
|
set(TARGET_TEST_NAME "${TARGET_BASE_NAME}_test")
|
||||||
|
set(TARGET_BALANCING_NAME "${TARGET_BASE_NAME}_balancing")
|
||||||
|
|
||||||
set(TARGET_LIB_INCLUDE_DIRS
|
set(TARGET_LIB_INCLUDE_DIRS
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/lib"
|
"${CMAKE_CURRENT_SOURCE_DIR}/lib"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/external"
|
"${CMAKE_CURRENT_SOURCE_DIR}/external"
|
||||||
)
|
)
|
||||||
set(TARGET_UI_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/ui")
|
set(TARGET_UI_INCLUDE_DIRS
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/ui"
|
||||||
|
# The balancing target compiles a few ui files into itself rather than linking the
|
||||||
|
# ui library, and the ship stats panel is built from the selection card's parts.
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/ui/selection"
|
||||||
|
)
|
||||||
set(TARGET_TEST_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/test")
|
set(TARGET_TEST_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/test")
|
||||||
|
set(TARGET_BALANCING_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/balancing")
|
||||||
|
|
||||||
set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
|
set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
|
|
||||||
@@ -76,6 +83,7 @@ unset(SRCS)
|
|||||||
|
|
||||||
set(HDRS)
|
set(HDRS)
|
||||||
set(SRCS)
|
set(SRCS)
|
||||||
|
set(UI_INCLUDE_PATH)
|
||||||
|
|
||||||
add_subdirectory(ui)
|
add_subdirectory(ui)
|
||||||
|
|
||||||
@@ -104,6 +112,7 @@ set_target_properties(${TARGET_UI_NAME} PROPERTIES
|
|||||||
)
|
)
|
||||||
target_include_directories(${TARGET_UI_NAME} PUBLIC
|
target_include_directories(${TARGET_UI_NAME} PUBLIC
|
||||||
"${TARGET_UI_INCLUDE_DIRS}"
|
"${TARGET_UI_INCLUDE_DIRS}"
|
||||||
|
"${UI_INCLUDE_PATH}"
|
||||||
"${TARGET_LIB_INCLUDE_DIRS}"
|
"${TARGET_LIB_INCLUDE_DIRS}"
|
||||||
"${LIB_INCLUDE_PATH}"
|
"${LIB_INCLUDE_PATH}"
|
||||||
)
|
)
|
||||||
@@ -115,6 +124,7 @@ target_link_libraries(${TARGET_UI_NAME}
|
|||||||
Qt5::Network
|
Qt5::Network
|
||||||
Qt5::Multimedia
|
Qt5::Multimedia
|
||||||
Qt5::Charts
|
Qt5::Charts
|
||||||
|
Qt5::Svg
|
||||||
)
|
)
|
||||||
target_compile_definitions(${TARGET_UI_NAME} PRIVATE TOML_FLOAT_CHARCONV=0)
|
target_compile_definitions(${TARGET_UI_NAME} PRIVATE TOML_FLOAT_CHARCONV=0)
|
||||||
|
|
||||||
@@ -177,10 +187,23 @@ set_target_properties(${TARGET_APP_NAME} PROPERTIES
|
|||||||
VS_DEBUGGER_WORKING_DIRECTORY "${OUTPUT_ROOT_PATH}/$(Configuration)/app/"
|
VS_DEBUGGER_WORKING_DIRECTORY "${OUTPUT_ROOT_PATH}/$(Configuration)/app/"
|
||||||
)
|
)
|
||||||
target_compile_definitions(${TARGET_APP_NAME} PRIVATE
|
target_compile_definitions(${TARGET_APP_NAME} PRIVATE
|
||||||
DOTA_FACTORY_CONFIG_DIR="${CMAKE_SOURCE_DIR}/bin/config"
|
CONFIG_DIR="${CMAKE_SOURCE_DIR}/bin/app/data/config"
|
||||||
)
|
)
|
||||||
target_link_libraries(${TARGET_APP_NAME} ${TARGET_UI_NAME})
|
target_link_libraries(${TARGET_APP_NAME} ${TARGET_UI_NAME})
|
||||||
|
|
||||||
|
# Embed the Windows version resource so the version shows on the executable's
|
||||||
|
# Details tab (right-click -> Properties). Values come from cmake/version.cmake
|
||||||
|
# (version numbers) and the product identity variables in the top-level
|
||||||
|
# CMakeLists.txt. MSVC compiles the .rc automatically once it is a target source.
|
||||||
|
if (WIN32)
|
||||||
|
configure_file(
|
||||||
|
"${CMAKE_SOURCE_DIR}/cmake/version.rc.in"
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/version.rc"
|
||||||
|
@ONLY
|
||||||
|
)
|
||||||
|
target_sources(${TARGET_APP_NAME} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/version.rc")
|
||||||
|
endif ()
|
||||||
|
|
||||||
unset(APP_FILES)
|
unset(APP_FILES)
|
||||||
unset(RELATIVE_HDRS)
|
unset(RELATIVE_HDRS)
|
||||||
unset(RELATIVE_SRCS)
|
unset(RELATIVE_SRCS)
|
||||||
@@ -188,6 +211,74 @@ unset(HDRS)
|
|||||||
unset(SRCS)
|
unset(SRCS)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# balancing — ship balancing tool; depends on lib + QtWidgets
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
COPY_QT_BINARIES("${OUTPUT_ROOT_PATH}/Debug/balancing/" True)
|
||||||
|
COPY_QT_BINARIES("${OUTPUT_ROOT_PATH}/Release/balancing/" False)
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND "cmd.exe" "/k" "rmdir" "${BACKSLASHED_OUTPUT_ROOT_PATH}\\Debug\\balancing\\data" & "mklink" "/d" "/j" "${BACKSLASHED_OUTPUT_ROOT_PATH}\\Debug\\balancing\\data" "${BACKSLASHED_CMAKE_SOURCE_DIR}\\bin\\balancing\\data" & exit
|
||||||
|
COMMAND "cmd.exe" "/k" "rmdir" "${BACKSLASHED_OUTPUT_ROOT_PATH}\\Release\\balancing\\data" & "mklink" "/d" "/j" "${BACKSLASHED_OUTPUT_ROOT_PATH}\\Release\\balancing\\data" "${BACKSLASHED_CMAKE_SOURCE_DIR}\\bin\\balancing\\data" & exit
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(HDRS)
|
||||||
|
set(SRCS)
|
||||||
|
|
||||||
|
add_subdirectory(balancing)
|
||||||
|
|
||||||
|
set(RELATIVE_HDRS)
|
||||||
|
foreach (_file ${HDRS})
|
||||||
|
file(RELATIVE_PATH _relPath "${SRC_DIR}" "${_file}")
|
||||||
|
list(APPEND RELATIVE_HDRS "${_relPath}")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
set(RELATIVE_SRCS)
|
||||||
|
foreach (_file ${SRCS})
|
||||||
|
file(RELATIVE_PATH _relPath "${SRC_DIR}" "${_file}")
|
||||||
|
list(APPEND RELATIVE_SRCS "${_relPath}")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
add_files(BALANCING_FILES ${RELATIVE_HDRS} ${RELATIVE_SRCS})
|
||||||
|
|
||||||
|
add_executable(${TARGET_BALANCING_NAME} ${BALANCING_FILES})
|
||||||
|
create_source_groups(${BALANCING_FILES})
|
||||||
|
|
||||||
|
foreach (OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES})
|
||||||
|
string(TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG)
|
||||||
|
set_target_properties(${TARGET_BALANCING_NAME} PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "${OUTPUT_ROOT_PATH}/${OUTPUTCONFIG}/balancing/"
|
||||||
|
)
|
||||||
|
endforeach ()
|
||||||
|
|
||||||
|
set_target_properties(${TARGET_BALANCING_NAME} PROPERTIES
|
||||||
|
AUTOMOC ON
|
||||||
|
CXX_STANDARD 17
|
||||||
|
VS_DEBUGGER_WORKING_DIRECTORY "${OUTPUT_ROOT_PATH}/$(Configuration)/balancing/"
|
||||||
|
)
|
||||||
|
target_include_directories(${TARGET_BALANCING_NAME} PRIVATE
|
||||||
|
"${TARGET_BALANCING_INCLUDE_DIRS}"
|
||||||
|
"${TARGET_LIB_INCLUDE_DIRS}"
|
||||||
|
"${TARGET_UI_INCLUDE_DIRS}"
|
||||||
|
"${LIB_INCLUDE_PATH}"
|
||||||
|
)
|
||||||
|
target_compile_definitions(${TARGET_BALANCING_NAME} PRIVATE
|
||||||
|
CONFIG_DIR="${CMAKE_SOURCE_DIR}/bin/app/data/config"
|
||||||
|
BALANCING_CONFIG="${CMAKE_SOURCE_DIR}/bin/balancing/data/balancing.toml"
|
||||||
|
TOML_FLOAT_CHARCONV=0
|
||||||
|
)
|
||||||
|
target_link_libraries(${TARGET_BALANCING_NAME} ${TARGET_LIB_NAME} Qt5::Widgets ${OPENGL_LIBRARIES})
|
||||||
|
|
||||||
|
unset(BALANCING_FILES)
|
||||||
|
unset(RELATIVE_HDRS)
|
||||||
|
unset(RELATIVE_SRCS)
|
||||||
|
unset(HDRS)
|
||||||
|
unset(SRCS)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# tests — Catch2 tests; links against lib only (no QtWidgets)
|
# tests — Catch2 tests; links against lib only (no QtWidgets)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -206,7 +297,7 @@ set_property(TARGET ${TARGET_TEST_NAME} PROPERTY INCLUDE_DIRECTORIES
|
|||||||
"${LIB_INCLUDE_PATH}"
|
"${LIB_INCLUDE_PATH}"
|
||||||
)
|
)
|
||||||
target_compile_definitions(${TARGET_TEST_NAME} PRIVATE
|
target_compile_definitions(${TARGET_TEST_NAME} PRIVATE
|
||||||
DOTA_FACTORY_CONFIG_DIR="${CMAKE_SOURCE_DIR}/src/test/config"
|
CONFIG_DIR="${CMAKE_SOURCE_DIR}/bin/test/data/config"
|
||||||
)
|
)
|
||||||
target_link_libraries(${TARGET_TEST_NAME} ${TARGET_LIB_NAME})
|
target_link_libraries(${TARGET_TEST_NAME} ${TARGET_LIB_NAME})
|
||||||
|
|
||||||
@@ -218,3 +309,4 @@ target_link_libraries(${TARGET_TEST_NAME} ${TARGET_LIB_NAME})
|
|||||||
set(TARGET_LIB_NAME "${TARGET_LIB_NAME}" PARENT_SCOPE)
|
set(TARGET_LIB_NAME "${TARGET_LIB_NAME}" PARENT_SCOPE)
|
||||||
set(TARGET_UI_NAME "${TARGET_UI_NAME}" PARENT_SCOPE)
|
set(TARGET_UI_NAME "${TARGET_UI_NAME}" PARENT_SCOPE)
|
||||||
set(TARGET_APP_NAME "${TARGET_APP_NAME}" PARENT_SCOPE)
|
set(TARGET_APP_NAME "${TARGET_APP_NAME}" PARENT_SCOPE)
|
||||||
|
set(TARGET_BALANCING_NAME "${TARGET_BALANCING_NAME}" PARENT_SCOPE)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <random>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
@@ -8,6 +11,8 @@
|
|||||||
#include "logging.h"
|
#include "logging.h"
|
||||||
#include "LogManager.h"
|
#include "LogManager.h"
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
|
#include "ReplayReader.h"
|
||||||
|
#include "ReplayRecorder.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
@@ -31,10 +36,54 @@ int main(int argc, char *argv[])
|
|||||||
QDir().mkdir(dataDir.dirName());
|
QDir().mkdir(dataDir.dirName());
|
||||||
}
|
}
|
||||||
|
|
||||||
GameConfig config = ConfigLoader::loadFromDirectory(DOTA_FACTORY_CONFIG_DIR);
|
// Optional "--replay <file>" launches view-only playback of a recorded run.
|
||||||
std::unique_ptr<Simulation> sim = std::make_unique<Simulation>(std::move(config));
|
std::optional<std::string> replayPath;
|
||||||
|
for (int i = 1; i + 1 < argc; ++i)
|
||||||
|
{
|
||||||
|
if (std::string(argv[i]) == "--replay")
|
||||||
|
{
|
||||||
|
replayPath = argv[i + 1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MainWindow window(sim.get(), std::string(DOTA_FACTORY_CONFIG_DIR));
|
GameConfig config = ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||||
|
|
||||||
|
unsigned int seed = 0;
|
||||||
|
std::shared_ptr<ParsedReplay> replay;
|
||||||
|
if (replayPath.has_value())
|
||||||
|
{
|
||||||
|
std::optional<ParsedReplay> parsed = readReplayFile(*replayPath);
|
||||||
|
if (!parsed.has_value())
|
||||||
|
{
|
||||||
|
LOG_ERROR("Failed to read replay file: " + *replayPath);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// Warn (but proceed) on identity mismatches: a different config or build
|
||||||
|
// can desync playback (see docs/replay_design.md).
|
||||||
|
if (parsed->header.version != 1)
|
||||||
|
{
|
||||||
|
LOG_WARNING_STREAM(<< "Replay format version " << parsed->header.version
|
||||||
|
<< " differs from 1; playback may fail");
|
||||||
|
}
|
||||||
|
if (computeReplayConfigHash(CONFIG_DIR) != parsed->header.configHash)
|
||||||
|
{
|
||||||
|
LOG_WARNING("Replay config hash mismatch; playback may desync");
|
||||||
|
}
|
||||||
|
seed = parsed->header.seed;
|
||||||
|
replay = std::make_shared<ParsedReplay>(std::move(*parsed));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Random seed generated outside the sim so the Simulation stays a pure
|
||||||
|
// function of (seed, config, commands); written to the replay header
|
||||||
|
// (see docs/replay_design.md "Seed and config").
|
||||||
|
seed = std::random_device{}();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Simulation> sim = std::make_unique<Simulation>(std::move(config), seed);
|
||||||
|
|
||||||
|
MainWindow window(sim.get(), std::string(CONFIG_DIR), replay);
|
||||||
window.show();
|
window.show();
|
||||||
|
|
||||||
const int ret = application.exec();
|
const int ret = application.exec();
|
||||||
|
|||||||