Compare commits
7 Commits
refactorin
...
c479291c1b
| Author | SHA1 | Date | |
|---|---|---|---|
| c479291c1b | |||
| 614dda8daf | |||
| ae41f677e6 | |||
| 1914705ab9 | |||
| aec65446d5 | |||
| 1f339bd8d7 | |||
| 8dc76284e6 |
@@ -1,155 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Interaction
|
||||
|
||||
* ONLY modify code or other files if explicitly asked to do so
|
||||
|
||||
## Project Overview
|
||||
|
||||
Dota Factory is a single-player game that blends a Factorio-style factory builder with
|
||||
DOTA-style wave defence. The player builds a factory on an asteroid — mining ores,
|
||||
transporting materials over belts and splitters, and crafting through a config-defined
|
||||
production tree — to supply shipyards that produce autonomous combat ships. Those ships
|
||||
fight off endless enemy waves advancing from the right. See `docs/concept.md` for the full design.
|
||||
|
||||
|
||||
## Project Structure
|
||||
|
||||
* the project root and the git repository root are the same directory
|
||||
* project requirements can be found at `docs/requirements.md`
|
||||
* architecture decisions can be found at `docs/architecture.md`
|
||||
* game content design (ship/module roster, layout grids, footprint gating) can be found at `docs/content_design.md`
|
||||
* replay/determinism design can be found at `docs/replay_design.md`
|
||||
* balancing rules, targets, tuned numbers, process, and history live under `docs/balancing/`
|
||||
|
||||
Requirements carry stable `REQ-<AREA>-<NAME>` ids. They are cited throughout the code in
|
||||
comments — when changing behavior, find the governing REQ id first and
|
||||
keep the citation accurate.
|
||||
|
||||
## Coding Guidelines
|
||||
|
||||
* avoid duplicate code
|
||||
* do not use the "auto" keyword, with two exceptions:
|
||||
* **named local lambdas** — a lambda's type is unnameable, and `std::function`
|
||||
is not an acceptable substitute in per-tick code because it adds a heap
|
||||
allocation and an indirect call
|
||||
* **iterator types** — `auto it = m_buildings.find(id)` is allowed where
|
||||
spelling the iterator out adds length without adding information
|
||||
* everywhere else the type is written out; in particular `auto` is not used
|
||||
for plain values, return values, or range-for element types
|
||||
* use Qt utility data types (like QPoint, QVector3D, QString, etc.)
|
||||
* wrap strings that appear in the UI with Qt's "tr()"
|
||||
* use the EventManager/EventHandler instead of defining own signals and slots
|
||||
* use std::optional if a variable can be "not set"
|
||||
* start the name of a getter method with "get"
|
||||
* don't use abbreviations, except very common ones ("s" for seconds, "min", "max", etc.)
|
||||
* if a variable holds a value that has a unit or if a function returns a value that has a unit, append that unit to the name (e.g. "m_shipVelocity_mps", "getAcceleration_mpss()")
|
||||
* always enclose scopes in braces
|
||||
|
||||
## Build
|
||||
|
||||
Requires CMake 3.14.4+, a C++17 compiler, and Qt 5 (developed against Qt 5.12.3,
|
||||
MSVC 2017 x64; `Qt5_DIR` is cached in `build/CMakeCache.txt`). Needs Qt components
|
||||
Widgets, Network, Multimedia, Charts, Svg, plus OpenGL.
|
||||
|
||||
External dependencies vendored under `src/external/`:
|
||||
* **toml++** — reading TOML config files
|
||||
* **tinyexpr** — evaluating formula strings from config files
|
||||
* **EnTT** — entity registry backing the ship/station/debris simulation
|
||||
* **Catch2** — test framework
|
||||
|
||||
Configure and build (a configured `build/` tree already exists):
|
||||
|
||||
```sh
|
||||
cmake -S . -B build # configure (multi-config VS generator)
|
||||
cmake --build build --config Debug # all targets
|
||||
cmake --build build --config Debug --target DotaFactory_test
|
||||
```
|
||||
|
||||
Targets: `DotaFactory` (app), `DotaFactory_lib`, `DotaFactory_ui`, `DotaFactory_test`,
|
||||
`DotaFactory_balancing`. Executables land in `build/DotaFactory/<Config>/{app,balancing}/`.
|
||||
|
||||
**Adding a source file requires editing CMake.** Every directory under `src/` has its own
|
||||
`CMakeLists.txt` listing files explicitly in `HDRS`/`SRCS` (or `TEST_FILES` for tests) —
|
||||
there is no globbing. A new file that is not registered simply will not compile.
|
||||
|
||||
Config data is not copied: `CONFIG_DIR` is a compile definition pointing at
|
||||
`bin/app/data/config` for the app and balancing tool, and `bin/test/data/config` for
|
||||
tests (a separate fixture set). On Windows the build also junctions `bin/*/data` into the
|
||||
output directories and copies the Qt DLLs.
|
||||
|
||||
Run the app: `build/DotaFactory/Debug/app/DotaFactory.exe`, optionally
|
||||
`--replay <file>` for view-only playback of a recorded run.
|
||||
|
||||
## Tests
|
||||
|
||||
Catch2, single executable, links `lib` only — no QApplication, no display.
|
||||
|
||||
```sh
|
||||
build/DotaFactory/Debug/app/DotaFactory_test.exe # all
|
||||
build/DotaFactory/Debug/app/DotaFactory_test.exe "[belt],[building]" # by tag
|
||||
build/DotaFactory/Debug/app/DotaFactory_test.exe "BeltSystem: *" # by name pattern
|
||||
build/DotaFactory/Debug/app/DotaFactory_test.exe --reporter compact
|
||||
```
|
||||
|
||||
Common tags: `[building] [belt] [behavior] [blueprint] [modules] [config] [wave] [combat]
|
||||
[replay] [determinism] [ship] [debris] [threat] [unlock]`.
|
||||
|
||||
`src/test/SimulationTestAccess.h` is a friend-struct backdoor to `Simulation`'s private
|
||||
mutators; tests use it instead of duplicating the command path. It lives under `src/test`
|
||||
and is deliberately off the lib/ui/app include path.
|
||||
|
||||
## Verification Tools
|
||||
|
||||
Python scripts in `tools/` read the real configs and are the first check
|
||||
after config edits (see `docs/balancing/process.md`):
|
||||
|
||||
* `verify_recipes.py` — recipe-tree closure, visuals coverage, orphan items
|
||||
* `verify_layouts.py` — module footprint gating per hull layout
|
||||
* `threat_report.py` — item/module/ship threat values, ratios, belt feasibility
|
||||
|
||||
The `DotaFactory_balancing` target runs parallel arena simulations from
|
||||
`bin/balancing/data/balancing.toml` for combat-stat tuning.
|
||||
|
||||
## Architecture
|
||||
|
||||
See `docs/architecture.md` for the full write-up. Highlights and the
|
||||
invariants that are easy to break:
|
||||
|
||||
* Strict simulation/presentation split, enforced at the CMake target level: `lib`
|
||||
(sim + config, Qt Core/Gui only — no QtWidgets), `ui` (QtWidgets + QOpenGLWidget),
|
||||
`app` (thin main), `test` (Catch2 against `lib`).
|
||||
* Fixed 30 Hz tick simulation, 60 FPS render, accumulator-driven; game speed is a
|
||||
tick-rate multiplier. All sim quantities are in ticks, never wall-clock seconds.
|
||||
* The tick order in `Simulation::tick()` is load-bearing for determinism — see the
|
||||
Tick Order section of `architecture.md` before reordering systems.
|
||||
* **Command chokepoint:** every sim mutation during play flows through
|
||||
`Simulation::apply(const Command&)` (see `sim/Command.h`, `CommandManager`), so runs can
|
||||
be recorded and replayed. Commands reference stable ids (`BuildingId`, tile coords,
|
||||
choice indices) — never raw `entt::entity` handles. UI code must not call sim mutators
|
||||
directly. Determinism is checksummed (`StateChecksum`) and covered by
|
||||
`DeterminismTest` / `ReplayPlaybackTest`.
|
||||
* Config is loaded once at startup, formulas compiled once via tinyexpr, immutable
|
||||
afterwards; malformed config aborts startup rather than failing mid-game. Restart
|
||||
reloads config from disk (REQ-CFG-RELOAD).
|
||||
* **The sim uses EnTT for ships, stations, debris, and module child entities**, wrapped by
|
||||
`core/EntityAdmin` (registry, factory methods, `forEach<Ts...>` views). Components live
|
||||
in `lib/ecs/component/`, systems in `lib/ecs/system/`. Note: `architecture.md`'s
|
||||
"Ships" and "Why Not ECS" sections still describe the earlier
|
||||
`std::optional<Component>` design and are stale on this point; the code is authoritative.
|
||||
Buildings and the belt subsystem stay outside the entity model.
|
||||
* Ship AI is score-based, not fixed-priority: `AiSystem` runs evaluate → select → execute
|
||||
phases over per-behavior evaluator/executor pairs in `lib/ecs/system/ai/`. Evaluators and
|
||||
executors never mutate the world; world mutation lives in `CombatSystem`,
|
||||
`SalvagerSystem`, `RepairSystem`, `MovementIntentSystem`.
|
||||
* Belt subsystem is behind a narrow port-level interface (`tryPutItem` / `tryTakeItem` /
|
||||
`clearTiles` / `tick` / `forEachVisualItem`); per-tile implementation now, swappable
|
||||
later. No other system asks "what is on tile X".
|
||||
* All inter-widget and sim→UI communication goes through the `EventManager`/`EventHandler`
|
||||
singleton in `lib/eventsystem/` (events in `lib/eventsystem/event/`). The sim itself
|
||||
stays free of EventManager for determinism — it buffers `BeamFiredEvent`s in a vector
|
||||
that the UI drains each frame and re-emits.
|
||||
* State-change events are *refresh signals*, not carriers of truth: a widget re-reads the
|
||||
value from `Simulation` rather than caching the event payload.
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,91 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,443 +0,0 @@
|
||||
# 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` |
|
||||
@@ -1,440 +0,0 @@
|
||||
# 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 |
|
||||
@@ -1,400 +0,0 @@
|
||||
# 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 |
|
||||
@@ -1,307 +0,0 @@
|
||||
# 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 | - | - | ✓ |
|
||||
@@ -1,360 +0,0 @@
|
||||
# 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 |
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
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,5 +1 @@
|
||||
/build/
|
||||
|
||||
# local Claude Code config (machine-specific; .mcp.json holds credentials)
|
||||
/.mcp.json
|
||||
/.claude/settings.local.json
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# 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).
|
||||
# are tuned independently of ship production costs, REQ-RES-SCRAP-DROP).
|
||||
|
||||
[hq]
|
||||
surface_mask = [
|
||||
|
||||
@@ -63,7 +63,7 @@ outline = "#ffffff"
|
||||
glyph = "Sb"
|
||||
|
||||
[buildings.belt]
|
||||
fill = "#1a1a1a"
|
||||
fill = "#5a5a5a"
|
||||
outline = "#7a7a7a"
|
||||
glyph = ""
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ height_tiles = 40
|
||||
refund_percentage = 100
|
||||
deconstruction_time_seconds = 0.1
|
||||
starting_building_blocks = 200
|
||||
debris_despawn_seconds = 120
|
||||
scrap_despawn_seconds = 120
|
||||
scrap_per_threat = 0.25
|
||||
tile_size_m = 10
|
||||
belt_speed_mps = 20
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 456 B |
@@ -1,6 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 396 B |
@@ -1,6 +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"/>
|
||||
<path d="M4 9h24"/><path d="M8 9C8 24 24 24 24 9"/><path d="M14.5 18.5 17.5 20 14.5 21.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 447 B After Width: | Height: | Size: 398 B |
@@ -1,6 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 468 B |
@@ -1,5 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 390 B |
@@ -1,5 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 429 B |
@@ -1,5 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 429 B |
@@ -1,12 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 655 B |
@@ -1,12 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 634 B |
@@ -1,5 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 355 B |
@@ -1,5 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 395 B |
@@ -1,4 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 343 B |
@@ -1,6 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 520 B |
@@ -1,7 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 585 B |
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 444 B |
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 602 B |
@@ -1,10 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 608 B |
@@ -1,5 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 427 B |
@@ -1,4 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 340 B |
@@ -1,5 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 464 B |
@@ -1,6 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 464 B |
@@ -1,6 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 481 B |
@@ -1,9 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 512 B |
@@ -1,4 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 305 B |
@@ -1,4 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 304 B |
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 481 B |
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 444 B |
@@ -1,10 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 618 B |
@@ -1,11 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 494 B |
@@ -1,7 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 507 B |
@@ -1,10 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 884 B |
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 692 B |
@@ -1,7 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 610 B |
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 687 B |
@@ -1,10 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 494 B |
@@ -1,5 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 407 B |
@@ -1,9 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 576 B |
@@ -1,6 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 420 B |
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 469 B |
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 444 B |
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 467 B |
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 471 B |
@@ -1,7 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 513 B |
@@ -1,7 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 389 B |
@@ -3,7 +3,7 @@ height_tiles = 60
|
||||
refund_percentage = 75
|
||||
deconstruction_time_seconds = 0.1
|
||||
starting_building_blocks = 100
|
||||
debris_despawn_seconds = 30
|
||||
scrap_despawn_seconds = 30
|
||||
scrap_per_threat = 1.0
|
||||
tile_size_m = 10
|
||||
belt_speed_mps = 20
|
||||
|
||||
@@ -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`.
|
||||
- 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, debris despawn, etc.) are converted to ticks at config-load time.
|
||||
- Config-level durations given in seconds (recipe durations, wave gap ranges, scrap 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.
|
||||
|
||||
@@ -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.
|
||||
- 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, 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -52,7 +52,7 @@ See REQ-GW-COORDS for the authoritative tile-coordinate convention. This section
|
||||
|
||||
Simulation types shared across subsystems:
|
||||
|
||||
- `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.
|
||||
- `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`, `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.
|
||||
- `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).
|
||||
@@ -115,9 +115,9 @@ Within a single simulation tick, subsystems run in this fixed order. The order i
|
||||
6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER).
|
||||
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 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 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.
|
||||
9. **Deaths & loot** — process queued deaths: drop scrap (REQ-RES-SCRAP-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`.
|
||||
11. **Debris despawn** — decrement debris timers; remove expired debris (REQ-RES-DEBRIS-DROP).
|
||||
11. **Scrap despawn** — decrement scrap timers; remove expired scrap (REQ-RES-SCRAP-DROP).
|
||||
|
||||
## CMake Target Layout
|
||||
|
||||
@@ -136,43 +136,17 @@ Belts and splitters are their own specialized subsystem. Belt items are **not**
|
||||
|
||||
### Public Interface
|
||||
|
||||
`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:
|
||||
Narrow and representation-agnostic:
|
||||
|
||||
```cpp
|
||||
class BeltSystem {
|
||||
public:
|
||||
// Placement — belts/splitters/tunnels are Buildings for cost and
|
||||
// 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
|
||||
bool tryPutItem(Port port, Item item);
|
||||
std::optional<Item> tryTakeItem(Port port);
|
||||
void clearTiles(const std::vector<QPoint>& tiles); // REQ-UI-BELT-CLEAR
|
||||
void tick();
|
||||
|
||||
// Rendering
|
||||
void forEachVisualItem(QRect viewportTiles,
|
||||
std::function<void(VisualItem)> visit) const;
|
||||
|
||||
// Determinism (docs/replay_design.md)
|
||||
void appendChecksum(Hasher& hasher) const;
|
||||
};
|
||||
|
||||
struct VisualItem {
|
||||
@@ -181,12 +155,12 @@ struct VisualItem {
|
||||
};
|
||||
```
|
||||
|
||||
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.
|
||||
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".
|
||||
|
||||
### 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
### Rendering Note
|
||||
|
||||
@@ -219,21 +193,20 @@ struct Building {
|
||||
- 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.
|
||||
|
||||
## Debris
|
||||
## Scrap
|
||||
|
||||
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:
|
||||
Scrap is the only non-ship, non-building entity in the simulation:
|
||||
|
||||
```cpp
|
||||
struct Debris {
|
||||
struct Scrap {
|
||||
EntityId id;
|
||||
QVector2D position; // world units, tile-fractional; ship-center convention
|
||||
int amount; // scrap the piece still holds
|
||||
Tick despawnAt; // absolute tick at which the debris is removed
|
||||
int amount;
|
||||
Tick despawnAt; // absolute tick at which the scrap is removed
|
||||
};
|
||||
```
|
||||
|
||||
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`.
|
||||
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`.
|
||||
|
||||
## Ships
|
||||
|
||||
@@ -261,7 +234,7 @@ struct RetreatBehavior { float retreatHpFraction; QVector2D retreatPoint;
|
||||
struct AttackBehavior { std::optional<EntityId> currentTarget; float score; };
|
||||
struct RepairBehavior { std::optional<EntityId> currentTarget;
|
||||
float maxRepairRange_tiles; float score; };
|
||||
struct SalvageScrapBehavior { std::optional<QVector2D> debrisTarget;
|
||||
struct SalvageScrapBehavior { std::optional<QVector2D> scrapTarget;
|
||||
float maxCollectionRange_tiles; float score; };
|
||||
struct DeliverScrapBehavior { BuildingId deliveryBay; float score; };
|
||||
struct SelectedBehaviorComponent { BehaviorKind winner; float bestScore; }; // selection result
|
||||
|
||||
@@ -17,7 +17,7 @@ move. Combat stats were tuned empirically against the arena suite in
|
||||
- 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,
|
||||
- `scrap_despawn_seconds = 120` (a capital kill drops hundreds of scrap,
|
||||
collected one per salvage cycle).
|
||||
|
||||
## Recipes and item threats
|
||||
|
||||
@@ -6,12 +6,12 @@ Config files use the TOML format. The following config files drive game paramete
|
||||
|
||||
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, building deconstruction time, wave timing, boss wave timing, belt speed, starting building blocks, departure interval, ship orbit factor, rally orbit radius, scrap-per-threat conversion, combat target-selection parameters (target score formula, overclaim penalty formula, target hysteresis), artifact chance formula, artifact win count, view pan speeds (slow and fast horizontal pan speed and pan ramp band width), an optional building blocks tooltip string (shown as the header bar's building blocks stock hover tooltip, REQ-UI-BLOCKS-TOOLTIP; omitted when unset), and an optional artifact tooltip string (shown as the header bar's artifact count hover tooltip, REQ-UI-ARTIFACTS-TOOLTIP; omitted when unset).
|
||||
- **buildings.toml** — building block cost and construction time per building type, plus an optional tooltip description string per building type (shown as the build button's hover tooltip, REQ-UI-BUILD-TOOLTIP; omitted when unset). Whether a building type is available from game start or must be unlocked during play is not defined here but in **unlocks.toml** (REQ-LOCK-EXPLICIT): a building type granted by an unlock group starts locked and is hidden from the build menu until its group is awarded (REQ-LOCK-BUILDING).
|
||||
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. Assembler recipe entries may optionally define `unlocked_at_start` (boolean, default false): when true the recipe is available from game start regardless of the implicit item graph — used for base recipes that no schematic's materials reach (such as building blocks; see REQ-LOCK-IMPLICIT). Which assembler recipes must instead be awarded during play (explicitly gated) is defined in **unlocks.toml**, not here (REQ-LOCK-EXPLICIT); every remaining assembler recipe is implicitly unlocked through the item graph (REQ-LOCK-IMPLICIT). Any recipe entry may optionally define `icon` (string): the id of an item whose icon represents the recipe in the recipe-selection dialog (REQ-UI-RECIPE-ICON); when omitted, the recipe's first output item is used.
|
||||
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. Assembler recipe entries may optionally define `unlocked_at_start` (boolean, default false): when true the recipe is available from game start regardless of the implicit item graph — used for base recipes that no schematic's materials reach (such as building blocks; see REQ-LOCK-IMPLICIT). Which assembler recipes must instead be awarded during play (explicitly gated) is defined in **unlocks.toml**, not here (REQ-LOCK-EXPLICIT); every remaining assembler recipe is implicitly unlocked through the item graph (REQ-LOCK-IMPLICIT).
|
||||
- **ships.toml** — per schematic: a human-readable display name (used in the UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as plain values, required build materials, a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES). Whether a ship schematic is available from game start or must be unlocked during play is defined in **unlocks.toml** (REQ-LOCK-EXPLICIT), not here.
|
||||
- **modules.toml** — per module type: id, surface mask, materials list, production time, fill color, glyph, an optional tooltip description string (shown as the module selection button's hover tooltip, REQ-MOD-UI-MODULE-TOOLTIP; omitted when unset), and an optional capability section and/or stat modifier formulas. Whether a module schematic is available from game start or must be unlocked during play is defined in **unlocks.toml** (REQ-LOCK-EXPLICIT), not here. A module with a capability section (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas is a **capability module** that grants the ship a weapon, salvage bay, or repair tool per instance (see REQ-MOD-CONFIG for the full list of formulas per capability type). A module with only `added_*`/`multiplied_*` formulas is a **passive module** that modifies stats on the ship or on capability module instances (see REQ-MOD-STAT-CALC).
|
||||
- **unlocks.toml** — unlock groups: each `[[unlock]]` entry names a group of ship schematics, module schematics, building types, and/or assembler recipes that are awarded together from a single defence station drop (see Unlock Group Format, REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP). Anything not granted by any unlock group is available from game start.
|
||||
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
|
||||
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs (identity labels; used in the world for building types not covered by an icon and as the fallback when an icon file is missing — REQ-UI-WORLD-ICON) for every building type, item type, ship schematic, and station type; for items, the `fill` and `outline` colors are drawn as the item's belt/port square and serve as the fallback when the item's icon file is missing (REQ-UI-ITEM-ICON); a distinct beam color per tool type (weapon, repair, salvage) and beam width; overlay and toast colors; and building status light colors (grey, green, red, and yellow fills plus the outline color, REQ-UI-STATUS-LIGHT). Loaded by the UI at startup; the simulation does not read it.
|
||||
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship schematic, and station type; a distinct beam color per tool type (weapon, repair, salvage) and beam width; overlay and toast colors; and building status light colors (grey, green, red, and yellow fills plus the outline color, REQ-UI-STATUS-LIGHT). Loaded by the UI at startup; the simulation does not read it.
|
||||
- **ship_layouts.toml** — named layout blueprints per ship type; written and read by the application to persist the layout blueprint panel (REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD). Not a game parameter file; the simulation does not read it.
|
||||
|
||||
- REQ-CFG-RELOAD: When the player triggers a Restart (REQ-UI-GAME-MENU), all config files are reloaded from disk before the simulation is reset to its initial state. Formula strings are recompiled at that point. This allows config edits made while the application is running to take effect without a full application restart.
|
||||
@@ -91,7 +91,7 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
||||
## Game World
|
||||
|
||||
- REQ-GW-COORDS: Tile coordinates are integer `(x, y)`. The origin `(0, 0)` is the first column of space — the tile immediately to the right of the asteroid's right edge at game start, at the top of the world. X grows right; Y grows down. All asteroid tiles have `x < 0`; asteroid left-expansions add tiles at increasingly negative X. The origin never shifts.
|
||||
- REQ-GW-TILE-SIZE: Tiles are square. The tile size in pixels is derived automatically so that the world height (in tiles) exactly fills the game world view's height in pixels. Items on belts are rendered at half-tile size (drawn as an item icon, or a colored square as the fallback — REQ-UI-ITEM-ICON); when multiple items occupy the same tile they are spaced quarter-tile apart along the direction of travel and overlap, rendered in ascending order of progress — the least-progressed item is drawn first (bottom) and the furthest-progressed item is drawn last (on top). Items emerging from a building's output port are rendered by these same rules on that port's output belt (REQ-MAT-OUTPUT-EMERGE).
|
||||
- REQ-GW-TILE-SIZE: Tiles are square. The tile size in pixels is derived automatically so that the world height (in tiles) exactly fills the game world view's height in pixels. Items on belts are rendered at half-tile size; when multiple items occupy the same tile they are spaced quarter-tile apart along the direction of travel and overlap, rendered in ascending order of progress — the least-progressed item is drawn first (bottom) and the furthest-progressed item is drawn last (on top). Items emerging from a building's output port are rendered by these same rules on that port's output belt (REQ-MAT-OUTPUT-EMERGE).
|
||||
- REQ-GW-BELT-CAPACITY: Belt tiles and tunnel entry/exit tiles each hold up to four items simultaneously, queued one behind the other in the direction of travel. Splitter tiles hold up to four items: two unassigned items (progress < 0.5, not yet routed to an output) and one item per output slot (progress ≥ 0.5, committed to a specific output direction). Output-slot items are rendered on top of unassigned items; when both output slots are occupied, their rendering order follows the clockwise port order starting from East.
|
||||
- REQ-GW-BELT-SPEED: Items on belts move at `world.toml [world].belt_speed_tiles_per_second` tiles per second (default 2).
|
||||
- REQ-GW-HEIGHT: The world height (in tiles) is read from `world.toml [world].height_tiles`.
|
||||
@@ -211,8 +211,8 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
||||
|
||||
## Resources
|
||||
|
||||
- REQ-RES-DEBRIS-DROP: Destroyed ships (both player and enemy) and destroyed defence stations (both player and enemy) drop a piece of **debris** at their location. A piece of debris carries a scrap amount. For a ship this amount is derived from the ship's threat cost (REQ-MOD-THREAT) for its as-built layout, multiplied by `world.toml [world].scrap_per_threat` (default 0.01) and rounded to the nearest integer (at least 1 for any ship whose threat cost is greater than 0); for stations it is defined as `stations.toml [player_station].scrap_drop_formula` and `[enemy_station].scrap_drop_formula`. Salvage modules collect from a piece of debris one scrap per cycle (REQ-SHP-SALVAGE), and the debris is removed from the world once its remaining scrap amount reaches zero or `world.toml [world].debris_despawn_seconds` seconds have elapsed since it was dropped, whichever comes first.
|
||||
- REQ-RES-SCRAP-COLLECT: Scrap is collected from debris by salvage ships and delivered to a Salvage Bay on the asteroid. From there it can be fed via belt into a smelter (same output as ore) or a Reprocessing Plant.
|
||||
- REQ-RES-SCRAP-DROP: Destroyed ships (both player and enemy) and destroyed defence stations (both player and enemy) drop scrap at their location. The scrap amount per ship is derived from the ship's threat cost (REQ-MOD-THREAT) for its as-built layout, multiplied by `world.toml [world].scrap_per_threat` (default 0.01) and rounded to the nearest integer (at least 1 for any ship whose threat cost is greater than 0); for stations it is defined as `stations.toml [player_station].scrap_drop_formula` and `[enemy_station].scrap_drop_formula`. A scrap drop carries an amount; salvage modules collect it one scrap per cycle (REQ-SHP-SALVAGE), and the drop is removed from the world once its remaining amount reaches zero or `world.toml [world].scrap_despawn_seconds` seconds have elapsed since it was dropped, whichever comes first.
|
||||
- REQ-RES-SCRAP-COLLECT: Scrap is collected by salvage ships and delivered to a Salvage Bay on the asteroid. From there it can be fed via belt into a smelter (same output as ore) or a Reprocessing Plant.
|
||||
|
||||
## Ships
|
||||
|
||||
@@ -231,14 +231,14 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
||||
- REQ-SHP-NO-COLLISION: Ships do not collide with each other or with defence stations; they may visually overlap.
|
||||
- REQ-SHP-SENSOR: A ship perceives only entities within its sensor range. Behavior is driven by what is in sensor range; entities outside sensor range are ignored.
|
||||
- REQ-SHP-FIRING: All weapons — on ships and on defence stations — fire when off cooldown and the target is within attack range. Firing emits a fire event and starts a 0.15-second damage delay (half the beam duration). When that delay expires, damage is applied to the target — unless the target has already been destroyed, in which case the damage is silently dropped. If the shooter is destroyed before the delay expires, damage is still applied when the delay expires. There is no projectile entity and no intervening collision. The weapon's cooldown begins at the moment of firing, not at damage application.
|
||||
- REQ-SHP-FIRING-BEAM: Each weapon fire event (REQ-SHP-FIRING), repair-tool activation (REQ-SHP-REPAIR), and salvage activation (REQ-SHP-SALVAGE) produces a visual beam drawn from the acting ship's position to the target for 0.3 seconds; repair and salvage beams have the same duration as weapon beams. The beam is rendered in the tool type's beam color from `visuals.toml` (a distinct color for weapon, repair, and salvage beams). The beam endpoint is not the target's center but a point randomly offset from it: the offset direction is uniformly random and the offset magnitude is uniformly random up to half the target's visual size (for ships: half their rendered radius; for buildings/stations: half the shorter side of their tile footprint, in world units; for a piece of debris: half its rendered size). The offset is chosen once per activation event and held fixed for the beam's lifetime. The beam is a pure rendering effect and has no simulation state (does not block movement, does not re-apply its effect over its lifetime). Beams follow the acting ship and target positions if either moves during the 0.3-second window. The beam is rendered for its full 0.3-second duration even if the acting ship or target is destroyed before it expires.
|
||||
- REQ-SHP-FIRING-BEAM: Each weapon fire event (REQ-SHP-FIRING), repair-tool activation (REQ-SHP-REPAIR), and salvage activation (REQ-SHP-SALVAGE) produces a visual beam drawn from the acting ship's position to the target for 0.3 seconds; repair and salvage beams have the same duration as weapon beams. The beam is rendered in the tool type's beam color from `visuals.toml` (a distinct color for weapon, repair, and salvage beams). The beam endpoint is not the target's center but a point randomly offset from it: the offset direction is uniformly random and the offset magnitude is uniformly random up to half the target's visual size (for ships: half their rendered radius; for buildings/stations: half the shorter side of their tile footprint, in world units; for a scrap pile: half its rendered size). The offset is chosen once per activation event and held fixed for the beam's lifetime. The beam is a pure rendering effect and has no simulation state (does not block movement, does not re-apply its effect over its lifetime). Beams follow the acting ship and target positions if either moves during the 0.3-second window. The beam is rendered for its full 0.3-second duration even if the acting ship or target is destroyed before it expires.
|
||||
- REQ-SHP-COMBAT: Ships with at least one **weapon module** (player) — engage enemy ships within sensor range. When engaging an enemy, the ship orbits it at the combat orbit radius (REQ-SHP-ORBIT) rather than approaching its center.
|
||||
- REQ-SHP-RALLY: After spawning, ships with weapon modules move to and orbit the **rally point** — the midpoint between the two player defence stations (center of their Y-span, at the player defence stations' X position) — at the rally orbit radius (REQ-SHP-ORBIT). While orbiting the rally point, ships still engage any enemy that enters sensor range (switching to the combat orbit per REQ-SHP-COMBAT). Every `world.toml [world].departure_interval_seconds` seconds (default 20), all ships with weapon modules currently at the rally point depart simultaneously and begin their normal aggressive advance toward the enemy. The departure timer is global and shared across all shipyards; it is not reset by individual ship arrivals at the rally point.
|
||||
- REQ-SHP-SALVAGE: Ships with at least one **salvage module** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If debris enters sensor range, navigate toward it by orbiting it at the salvage orbit radius (REQ-SHP-ORBIT); when it is within a module's `collection_range`, that module begins collecting from it, one scrap per cycle (see below). Once the ship's cargo pool is full, fly to a Salvage Bay and deliver (a direct approach, not an orbit — the ship must reach the bay); after delivery, resume patrol. If an enemy ship enters sensor range, the ship retreats (REQ-SHP-RETREAT) until no enemy is in sensor range, then resumes patrol — this applies regardless of whether the ship is targeting debris or carrying scrap. Ships with salvage modules are vulnerable to enemy ships while operating.
|
||||
- REQ-SHP-SALVAGE: Ships with at least one **salvage module** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If scrap enters sensor range, navigate toward it by orbiting it at the salvage orbit radius (REQ-SHP-ORBIT); when it is within a module's `collection_range`, that module begins collecting from it, one scrap per cycle (see below). Once the ship's cargo pool is full, fly to a Salvage Bay and deliver (a direct approach, not an orbit — the ship must reach the bay); after delivery, resume patrol. If an enemy ship enters sensor range, the ship retreats (REQ-SHP-RETREAT) until no enemy is in sensor range, then resumes patrol — this applies regardless of whether the ship is targeting or carrying scrap. Ships with salvage modules are vulnerable to enemy ships while operating.
|
||||
|
||||
All salvage modules on a ship deposit into a single shared **cargo pool** whose size is the ship's cargo capacity stat (REQ-MOD-CARGO-CAPACITY). Each salvage module instance still runs its own collection cycle independently, with its own collection range (`collection_range`) and collection rate (`collection_rate`, in collection cycles per second). A module starts a collection cycle when it is off cooldown, the shared cargo pool has free space, and a piece of debris is within its `collection_range`. Free space is measured against the pool's current contents **plus the collection cycles already in flight toward the pool** (scrap claimed by cycles whose effect delay has not yet elapsed); each in-flight cycle is registered against the ship so that concurrent modules on the same ship never start more cycles than the remaining capacity can hold. Starting a cycle emits a collection beam toward that debris (REQ-SHP-FIRING-BEAM) and begins a 0.15-second effect delay (half the beam duration); the module's cooldown of `1 / collection_rate` seconds begins at cycle start, not at effect application. When the delay expires, exactly 1 scrap is removed from the targeted debris and added to the ship's cargo pool — unless the debris has already been fully depleted or despawned, or the pool is now full, in which case the collection is silently dropped. A piece of debris worth more than 1 (REQ-RES-DEBRIS-DROP) is depleted one scrap per cycle and persists, with its remaining scrap amount decremented, until it is fully collected or despawns. A ship with multiple salvage modules can therefore run multiple collection cycles concurrently (one per ready module), and instances of different module types may have different ranges and rates. The ship navigates based on the maximum collection range across all installed salvage modules.
|
||||
All salvage modules on a ship deposit into a single shared **cargo pool** whose size is the ship's cargo capacity stat (REQ-MOD-CARGO-CAPACITY). Each salvage module instance still runs its own collection cycle independently, with its own collection range (`collection_range`) and collection rate (`collection_rate`, in collection cycles per second). A module starts a collection cycle when it is off cooldown, the shared cargo pool has free space, and a scrap pile is within its `collection_range`. Free space is measured against the pool's current contents **plus the collection cycles already in flight toward the pool** (scrap claimed by cycles whose effect delay has not yet elapsed); each in-flight cycle is registered against the ship so that concurrent modules on the same ship never start more cycles than the remaining capacity can hold. Starting a cycle emits a collection beam toward that scrap pile (REQ-SHP-FIRING-BEAM) and begins a 0.15-second effect delay (half the beam duration); the module's cooldown of `1 / collection_rate` seconds begins at cycle start, not at effect application. When the delay expires, exactly 1 scrap is removed from the targeted pile and added to the ship's cargo pool — unless the pile has already been fully depleted or despawned, or the pool is now full, in which case the collection is silently dropped. A scrap pile worth more than 1 (REQ-RES-SCRAP-DROP) is depleted one scrap per cycle and persists, with its remaining amount decremented, until it is fully collected or despawns. A ship with multiple salvage modules can therefore run multiple collection cycles concurrently (one per ready module), and instances of different module types may have different ranges and rates. The ship navigates based on the maximum collection range across all installed salvage modules.
|
||||
|
||||
Salvage collection cycles and delivery are processed regardless of which behavior the ship is currently executing; the salvage behavior only governs where the ship navigates (toward debris, toward a Salvage Bay, or — when retreating — toward the rally point).
|
||||
Salvage collection cycles and delivery are processed regardless of which behavior the ship is currently executing; the salvage behavior only governs where the ship navigates (toward scrap, toward a Salvage Bay, or — when retreating — toward the rally point).
|
||||
- REQ-SHP-REPAIR: Ships with at least one **repair module** (player) — when no more urgent behavior applies, hold with the fleet (REQ-SHP-STANDBY) rather than charging the enemy, so damaged allies stay within sensor range. If a damaged player defence station or player ship enters sensor range, navigate toward it by orbiting it at the repair orbit radius (REQ-SHP-ORBIT) and repair. If an enemy ship enters sensor range, the ship retreats (REQ-SHP-RETREAT) until no enemy is in sensor range — except that it holds its ground and keeps repairing while a damaged friendly remains within sensor range (REQ-SHP-RETREAT), retreating only once there is nothing left to repair — then resumes patrol.
|
||||
|
||||
Each repair module instance operates independently: it has its own repair rate (`repair_rate`, in repair cycles per second), per-cycle heal amount (`repair_amount_hp`), and repair range (`repair_range`). A module starts a repair cycle when it is off cooldown and a valid repair target is in range. To choose the target, the module first considers the ship's current behavior-level navigation target if that target is within the module's `repair_range` and is damaged (HP above zero and below maximum HP). If those conditions are not met — because the target is out of the module's `repair_range`, already at full health, or destroyed — the module independently searches for the nearest damaged friendly (player ship or player defence station) within its own `repair_range`. If no valid target is found within range, the module idles and starts no cycle. On starting a cycle, the module emits a repair beam toward the chosen target (REQ-SHP-FIRING-BEAM) and begins a 0.15-second effect delay (half the beam duration); the module's cooldown of `1 / repair_rate` seconds begins at cycle start, not at effect application. When the delay expires, `repair_amount_hp` HP is restored to the targeted entity, clamped to its maximum HP — unless that entity is no longer damaged or has been destroyed, in which case the heal is silently dropped. A ship with multiple repair modules can therefore run multiple repair cycles concurrently, healing different targets. Navigation is driven solely by the behavior-level target; individual module fallback targets do not affect which direction the ship moves. Repair cycles are processed regardless of which behavior the ship is currently executing.
|
||||
@@ -292,7 +292,7 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
||||
- **Multiple recipes**: if an item type can be produced by more than one non-reprocessing recipe (miner, smelter, or assembler), its threat value is the **maximum** across **all** such eligible recipes, and the threat is committed only once every eligible recipe is computable (so a shallow shortcut recipe that resolves earlier than a deeper base recipe cannot lower the item's threat). The reprocessing path is only used when no other recipe exists. If recipe cycles prevent full resolution, the max over the currently computable subset is used as a fallback.
|
||||
- **Scrap-consuming recipe fallback**: a non-reprocessing recipe that takes `scrap` as an input participates in an item's threat computation only if no scrap-free recipe (miner, smelter, or assembler) produces that item. This mirrors the reprocessing fallback rule and prevents the scrap-to-ingot smelter recipe from inflating basic material threats via the max rule.
|
||||
|
||||
- REQ-THREAT-SCRAP: The threat value of scrap is the constant `1 / world.toml [world].scrap_per_threat`. This is the exact inverse of the scrap conversion in REQ-RES-DEBRIS-DROP, so a destroyed ship drops debris worth precisely its own threat cost. Because scrap threat is now a fixed constant, it no longer depends on any ship's threat cost, removing the potential circularity with REQ-MOD-THREAT for ships built from reprocessing-only materials.
|
||||
- REQ-THREAT-SCRAP: The threat value of scrap is the constant `1 / world.toml [world].scrap_per_threat`. This is the exact inverse of the scrap-drop conversion in REQ-RES-SCRAP-DROP, so a destroyed ship drops scrap worth precisely its own threat cost. Because scrap threat is now a fixed constant, it no longer depends on any ship's threat cost, removing the potential circularity with REQ-MOD-THREAT for ships built from reprocessing-only materials.
|
||||
- REQ-MOD-STAT-CALC: For each stat (on the ship hull or on a capability module instance), the final value is computed as: `final = base × total_multiplier + total_additive`, where:
|
||||
- `base` is the stat's base value — the hull stat value (for hull stats) or the capability module's base stat value (for capability module stats).
|
||||
- `total_multiplier` = 1 + sum of (m_i − 1) for each multiplicative modifier m_i from all passive module instances. Each m_i is the module's multiplicative modifier value.
|
||||
@@ -453,14 +453,13 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
```
|
||||
|
||||
- REQ-UI-HEADER: The header bar spans the width of the game world column (75% of the screen width) and always shows the elapsed survival time, the current global building blocks stock, and the artifact count (REQ-WIN-ARTIFACT-COUNT) displayed as `Artifacts: x/y` (where `x` is the current artifact count and `y` is `world.toml [world].artifact_win_count`) on the left, the boss wave counter and boss countdown (REQ-UI-BOSS-STATUS) and an asteroid expansion button (REQ-UI-EXPAND-BUTTON) to the left of the speed buttons, and game speed controls on the right.
|
||||
- REQ-UI-BLOCKS-ICON: In the header bar (REQ-UI-HEADER), the global building blocks stock is displayed as `Stock: <n>` followed by the `building_block` item icon (REQ-UI-ITEM-ICON) — e.g. `Stock: 200` then a small block icon — replacing the `Building Blocks: <n>` text label. The icon is sized to the header text height. When no icon file exists for `building_block` (a missing icon is not an error, REQ-UI-ITEM-ICON), the display falls back to the `Stock: <n> Blocks` text. The hover tooltip (REQ-UI-BLOCKS-TOOLTIP) applies in either form.
|
||||
- REQ-UI-BLOCKS-TOOLTIP: The header bar's building blocks stock display (REQ-UI-HEADER) shows a hover tooltip with the descriptive text defined in `world.toml [world].building_blocks_tooltip` — intended to tell the player what building blocks are used for and how to obtain them. If the field is unset, the stock display shows no tooltip. This tooltip is distinct from the build/module button tooltips (REQ-UI-BUILD-TOOLTIP, REQ-MOD-UI-MODULE-TOOLTIP).
|
||||
- REQ-UI-ARTIFACTS-TOOLTIP: The header bar's artifact count display (REQ-UI-HEADER) shows a hover tooltip with the descriptive text defined in `world.toml [world].artifact_tooltip` — intended to tell the player what artifacts are, how they are obtained (REQ-DEF-SCHEMATIC-DROP), and that collecting `world.toml [world].artifact_win_count` of them wins the game (REQ-WIN-ARTIFACT-COUNT). If the field is unset, the artifact count display shows no tooltip. This tooltip is distinct from the building blocks tooltip (REQ-UI-BLOCKS-TOOLTIP) and the build/module button tooltips (REQ-UI-BUILD-TOOLTIP, REQ-MOD-UI-MODULE-TOOLTIP).
|
||||
- REQ-UI-BOSS-STATUS: The header bar displays, to the left of the speed buttons, the current boss wave counter (REQ-WAV-BOSS-COUNTER) and the time remaining on the boss countdown (REQ-WAV-BOSS-COUNTDOWN). The boss wave counter is shown as `Boss Wave #<x>` and the countdown as `Next boss: <M:SS>`, where `<M:SS>` is the remaining seconds formatted as whole minutes and two-digit seconds. Both values update continuously as the simulation runs.
|
||||
- REQ-UI-SPEED: The game speed controls in the header bar are buttons for 0×, 0.5×, 1×, 2×, and 10× speed. The currently active speed is shown as selected. All game simulation (production, movement, threat accumulation, wave timing) scales with the selected speed. 0× pauses the game.
|
||||
- REQ-UI-PAUSE-BORDER: While the game is paused (speed 0×, whether set via the speed controls (REQ-UI-SPEED), the Space toggle (REQ-UI-HOTKEYS), or an auto-pausing modal), a vignette border is drawn around the edges of the game world view to make the paused state hard to miss. The border is black and fades in the alpha channel from fully transparent at its inner (center-facing) edge to 50% opacity at the viewport edge, over a thickness of 100 pixels (capped at half the smaller viewport dimension on very small views).
|
||||
- REQ-UI-DECONSTRUCT-BORDER: While deconstruct mode is active (REQ-UI-DECONSTRUCT-BUTTON, REQ-UI-HOTKEYS), a vignette border is drawn around the edges of the game world view to signal the mode, matching the geometry of the paused-state vignette (REQ-UI-PAUSE-BORDER): a 100-pixel thickness (capped at half the smaller viewport dimension on very small views) with the four sides meeting along mitred corner diagonals. It fades in the alpha channel from fully transparent at its inner (center-facing) edge to the deconstruct tint color at the viewport edge. The color — including its alpha, which sets the peak opacity at the viewport edge — is read from `visuals.toml [overlays].deconstruct_tint`, the same deconstruct-mode color used for the hover tint. The border is presentation-only and has no effect on the simulation. If the game is both paused and in deconstruct mode, both vignettes are drawn and compose over each other.
|
||||
- REQ-UI-EXPAND-BUTTON: The header bar shows an asteroid expansion button captioned `Expand: <x>` followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON) in place of the trailing `Blocks` word, where `<x>` is the current expansion cost computed from `world.toml [expansion].cost_building_blocks_formula` at the current number of purchased expansions (REQ-EXP-COST). When no icon file exists for `building_block`, the caption falls back to the `Expand: <x> Blocks` text. Clicking the button unlocks the next asteroid expansion (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND), spending that many building blocks from the global stock. The button is disabled when the player cannot currently afford the cost (consistent with REQ-UI-BUILD-DISABLED). The caption updates as the cost changes with each purchased expansion.
|
||||
- REQ-UI-EXPAND-BUTTON: The header bar shows an asteroid expansion button captioned `Expand: <x> Blocks`, where `<x>` is the current expansion cost computed from `world.toml [expansion].cost_building_blocks_formula` at the current number of purchased expansions (REQ-EXP-COST). Clicking the button unlocks the next asteroid expansion (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND), spending that many building blocks from the global stock. The button is disabled when the player cannot currently afford the cost (consistent with REQ-UI-BUILD-DISABLED). The caption updates as the cost changes with each purchased expansion.
|
||||
- REQ-UI-WORLD-SIZE: The game world view occupies the full height below the header bar in the main column (75% of the screen width).
|
||||
- REQ-UI-PANEL-COLUMN: The side panel column occupies 25% of the screen width and the full screen height. It is divided into three equal-height panels stacked top to bottom: selected building panel (top), build button grid (middle), and blueprint panel (bottom).
|
||||
- REQ-UI-MODAL-DIM: While a modal dialog, menu, or full-screen state screen is open on top of the game, a transparent black overlay (a dim/scrim) is drawn over the **entire game window** — the header bar, the game world view, and the side panel column — behind that modal, so the game reads as inactive while the modal holds focus. The overlay is shown for every modal that auto-pauses the simulation — the escape menu (REQ-UI-GAME-MENU), the recipe/schematic selection dialog (REQ-UI-SELECT-BUTTON), the layout configuration dialog (REQ-MOD-UI-DIALOG), and the schematic choice dialog (REQ-DEF-SCHEMATIC-DROP) — as well as the game-over screen (REQ-HQ-GAME-OVER) and the win screen (REQ-WIN-SCREEN), which end rather than pause the game. When modals are nested (for example the Create Blueprint name dialog (REQ-MOD-UI-BLUEPRINT-CREATE) opened from the layout configuration dialog), only a single dim is shown over the game window; nested modals do not stack additional overlays. The dim color and opacity are read from `visuals.toml [overlays]` (a semi-transparent black modal-dim color), consistent with the other overlay colors. The overlay is presentation-only and has no effect on the simulation.
|
||||
@@ -474,9 +473,7 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
- **Narrow contest zone:** should the two ramp bands overlap (a contest zone narrower than the band width), each ramp is clamped at the contest-zone center so the bands do not cross; the fast plateau then reduces to a single point at the center and the peak speed there may be below the fast speed.
|
||||
|
||||
Because the contest-zone boundaries shift as the scrollable area grows with each push (REQ-GW-PUSH-EXPAND, REQ-GW-SCROLL-LIMIT), the ramp bands are recomputed from the current contest-zone boundaries. This is a presentation-only concern and does not affect the simulation, consistent with REQ-UI-NO-ZOOM.
|
||||
- REQ-UI-WORLD-ICON: In the game world, a building is drawn with an icon's glyph symbol centered on its footprint, in place of the letter identity glyph. The icon is an SVG loaded from `data/icons/buildings/`; only the icon's glyph is drawn in the world — in a contrasting ink (white over dark fills, dark over light fills) so it stays legible — and its colored chip background is omitted, because the footprint is already filled with the building's `visuals.toml` fill color. This applies to the production buildings (Miner, Smelter, Assembler, Reprocessing Plant, Shipyard, Salvage Bay), the HQ, and the player and enemy defence stations, wherever the identity label appears: operational buildings, construction sites (REQ-UI-CONSTRUCTION-PROGRESS), and the builder-mode and blueprint-placement ghosts. **Belts, splitters, and tunnels are excluded** — they keep their existing tile rendering so their orientation and flow stay readable (a centered icon would obscure direction). Their build-menu buttons still use icons (REQ-UI-BUILD-ICON); in particular the shared Tunnel button's `tunnel_entry.svg` is a build-button icon only, not a world icon. The directional output-port glyphs (REQ-UI-PORT-GLYPH, REQ-UI-PORT-TARGET-GLYPH) are a separate indicator and are unaffected. A building or station with no icon file falls back to its `visuals.toml` text glyph; a type with neither icon nor glyph shows no identity label. A missing icon is not an error, consistent with REQ-UI-BUILD-ICON.
|
||||
- REQ-UI-ITEM-ICON: In the game world, an item is drawn with its **item icon** in place of the colored square of REQ-GW-TILE-SIZE. The icon is a self-contained, full-color SVG (rendered as-is, unlike the glyph-only building icons of REQ-UI-WORLD-ICON), loaded at runtime from `data/icons/items/` — a sibling of the config directory, read the same way as the building icons (REQ-UI-BUILD-ICON) — one file per item type named after the item's id (e.g. `iron_ore.svg`). It fills the item's half-tile rect, keeping the size, spacing, and draw-order rules of REQ-GW-TILE-SIZE, and applies wherever an item is drawn: on belts, splitters, and tunnel ends, and while emerging from or sinking into a building port (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-INPUT-INTAKE). An item type with no icon file falls back to its `visuals.toml` colored square (`fill` + `outline`); a missing icon is not an error, consistent with REQ-UI-BUILD-ICON. For performance, each item icon is rasterized to a pixmap cached per target pixel size — re-rasterized only when the tile pixel size changes (e.g. on view resize) — rather than re-rendered from vector every frame.
|
||||
- REQ-UI-CONSTRUCTION-PROGRESS: Construction sites display the building's identity symbol centered on the footprint (same as an operational building) — its icon glyph, or the text glyph as a fallback (REQ-UI-WORLD-ICON). Below the symbol — or centered on the footprint if the building has neither an icon nor a glyph — a construction progress percentage is shown (integer, e.g. `42%`), increasing from 0% to 100% as construction completes.
|
||||
- REQ-UI-CONSTRUCTION-PROGRESS: Construction sites display the building's glyph centered on the footprint (same as an operational building). Below the glyph — or centered on the footprint if the building has no glyph — a construction progress percentage is shown (integer, e.g. `42%`), increasing from 0% to 100% as construction completes.
|
||||
- REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port.
|
||||
- REQ-UI-PORT-TARGET-GLYPH: While in builder mode (REQ-BLD-BUILDER-MODE), the builder-mode ghost additionally shows, for each of the building's output ports, a directional glyph drawn centered in the port's **target cell** — the cell immediately outside the footprint that the port pushes into, i.e. the cell the surface-mask output-port indicator occupies (see Surface Mask Format). As in REQ-UI-PORT-GLYPH the glyph is a `>` rotated to face the port's exit direction (`>` East, `^` North, `<` West, `v` South), previewing where the port's output will go before placement. This is in addition to the on-tile port glyph of REQ-UI-PORT-GLYPH, and — unlike that indicator — is shown only for the builder-mode ghost, not for operational buildings, construction sites, or the blueprint-placement ghost (REQ-UI-BLUEPRINT-PLACE). A building with multiple output ports (e.g. a splitter) shows one target-cell glyph per port. The target-cell glyph is drawn larger than the on-tile port glyph so it stands out as the flow-direction preview. Exceptions: the Tunnel Entry shows no target-cell glyph, because it receives items (which may arrive from any of its non-mouth edges, REQ-BLD-TUNNEL-ENTRY) rather than emitting into a single adjacent cell; the Shipyard shows none either, because its output port is a ship-spawn point (REQ-SHP-SPAWN-PLAYER) rather than a belt-item output (REQ-MAT-OUTPUT-EMERGE).
|
||||
- REQ-UI-STATUS-LIGHT: Every operational production building — Miner, Smelter, Assembler, Reprocessing Plant, Shipyard, and Salvage Bay — renders a small **status light**: a filled circle with a black outline drawn in the building's upper-right corner, letting the player read a building's production state without selecting it. The light is anchored to the footprint corner that is the upper-right corner in the building's default orientation and rotates with the building — like the output-port glyph (REQ-UI-PORT-GLYPH) — so it stays on the same physical corner of the building as it is rotated. The status light is rendered only for operational buildings; construction sites (which instead show construction progress, REQ-UI-CONSTRUCTION-PROGRESS) and the builder-mode ghost do not render it. Buildings that are not production buildings — belts, splitters, tunnel entries/exits, and the HQ — have no status light. The black outline is constant; the fill color reflects the building's current production state.
|
||||
@@ -524,43 +521,41 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
|
||||
### Selected Building Panel
|
||||
|
||||
- REQ-UI-EMPTY-SELECTION: When nothing is selected (no building, construction site, ship, defence station, or piece of debris), the panel is empty.
|
||||
- REQ-UI-SELECTION-CATEGORIES: **Selection categories and precedence.** Every selectable object belongs to one of two mutually exclusive selection categories: **buildings** (buildings and construction sites) and **field objects** (ships and defence stations — player or enemy — together with debris). A single selection holds objects from only one category at a time. Field objects of different kinds may be selected together (e.g. several ships plus debris, freely mixing player and enemy actors). Buildings are exclusive and take precedence — **buildings win**: selecting a building (by click, Ctrl+click, or a box-drag covering at least one building) clears any field selection and yields a buildings-only selection, and conversely selecting any field object clears any building selection. Point hit-testing prefers a building over a coincident field object, and among field objects prefers an actor (ship or defence station) over a coincident piece of debris (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-CLICK-SELECT).
|
||||
- REQ-UI-EMPTY-SELECTION: When nothing is selected (no building, construction site, ship, defence station, or scrap pile), the panel is empty.
|
||||
- REQ-UI-SELECTION-CATEGORIES: **Selection categories and precedence.** Every selectable object belongs to one of two mutually exclusive selection categories: **buildings** (buildings and construction sites) and **field objects** (ships and defence stations — player or enemy — together with scrap piles). A single selection holds objects from only one category at a time. Field objects of different kinds may be selected together (e.g. several ships plus scrap piles, freely mixing player and enemy actors). Buildings are exclusive and take precedence — **buildings win**: selecting a building (by click, Ctrl+click, or a box-drag covering at least one building) clears any field selection and yields a buildings-only selection, and conversely selecting any field object clears any building selection. Point hit-testing prefers a building over a coincident field object, and among field objects prefers an actor (ship or defence station) over a coincident scrap pile (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-SCRAP-CLICK-SELECT).
|
||||
- REQ-UI-SINGLE-SELECTION: When one building is selected, the panel shows: building name, current recipe or schematic selection, input buffer contents, and output buffer contents. Buffer counts are displayed as `a/b` where `a` is the current item count and `b` is the per-cycle amount (items consumed per run for inputs; items produced per run for outputs). For a selected construction site, the recipe/schematic selection (and, for a shipyard, the layout preview and "Configure" button) are shown but the buffer rows are omitted (REQ-BLD-SITE-CONFIG).
|
||||
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the selected building panel also shows: (a) the cycle time of the currently selected recipe or schematic in seconds, and (b) the completion percentage of the active production cycle as an integer (e.g. `42%`), or the text `idle` when no production cycle is active. When no recipe or schematic is selected, neither the cycle time nor the progress indicator is shown.
|
||||
- REQ-UI-MULTI-SELECT: The player selects multiple objects by box-drag or by Ctrl+clicking individual objects to add or remove them from the selection. Multi-select operates within a single category (REQ-UI-SELECTION-CATEGORIES). A box-drag that covers at least one building selects buildings (any field objects within the box are ignored — buildings win); a box-drag that covers no building but does cover ships, defence stations, or debris selects all of those field objects together (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-MULTI-SELECT).
|
||||
- REQ-UI-MULTI-SELECT: The player selects multiple objects by box-drag or by Ctrl+clicking individual objects to add or remove them from the selection. Multi-select operates within a single category (REQ-UI-SELECTION-CATEGORIES). A box-drag that covers at least one building selects buildings (any field objects within the box are ignored — buildings win); a box-drag that covers no building but does cover ships, defence stations, or scrap piles selects all of those field objects together (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-SCRAP-MULTI-SELECT).
|
||||
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected, the panel shows how many of each building type are selected. No per-building detail is shown. The panel additionally shows the **total building block cost** of the selection — the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button grid); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-BUTTON). Construction sites count at their building type's full placement cost regardless of construction progress.
|
||||
- REQ-UI-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel. Recipe selection (miner, assembler) and schematic selection (shipyard) use the selection button and dialog (REQ-UI-SELECT-BUTTON) rather than an inline control. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic selection button (REQ-MOD-UI-PREVIEW).
|
||||
- REQ-UI-SELECT-BUTTON: **Recipe and schematic selection control.** Recipe selection (Miner ore type, Assembler recipe) and schematic selection (Shipyard) are each presented in the selected building panel as a single **selection button** whose caption is the name of the currently selected recipe or schematic, or a placeholder ("Select recipe" / "Select schematic") when none is selected. Clicking the button opens a modal **selection dialog** that pauses the game (speed set to 0×; on close, the speed is restored to what it was before the dialog was opened). The dialog contains a grid of option buttons, one per selectable option — only options that are currently unlocked are shown (REQ-LOCK-UI-RECIPE for recipes, REQ-LOCK-UI-SCHEMATIC for schematics). Hovering an option button shows the selection info tooltip (REQ-UI-SELECT-TOOLTIP). Clicking an option button selects that recipe/schematic, closes the dialog, and updates the selection button's caption in the selected building panel. The dialog can be dismissed without changing the current selection (e.g. closing it without clicking an option). Selecting a new recipe or schematic has the same effects as before (REQ-MAT-INPUT-BUFFER, REQ-MAT-OUTPUT-BUFFER, REQ-BLD-SHIPYARD).
|
||||
- REQ-UI-SELECT-TOOLTIP: **Selection info tooltip.** Hovering an option button in the selection dialog (REQ-UI-SELECT-BUTTON), and hovering the selection button in the selected building panel when a selection is set, displays an info tooltip:
|
||||
- For a **recipe** (Miner or Assembler): the recipe name; the name and quantity of each input item (no inputs are listed for miner recipes, which consume nothing); the completion time (`duration_seconds`); and the name and quantity of the produced output item.
|
||||
- For a **ship schematic** (Shipyard): the ship's `display_name`; the name and quantity of each base required material (`[ship.schematic].materials`, excluding any module contributions); the base production time (`[ship.schematic].production_time_seconds`); and "Produces: 1 <ship display name>".
|
||||
- REQ-UI-RECIPE-ICON: In the recipe-selection dialog (REQ-UI-SELECT-BUTTON) for a Miner or Assembler, each recipe option button shows the icon of the recipe's produced item **instead of** its name caption (icon-only). The item shown is the recipe's `icon` field if set, otherwise its first output item; the icon is that item's icon per REQ-UI-ITEM-ICON. When the item has no icon file, the button falls back to the recipe/item name caption. The recipe name and details remain available on hover via the selection info tooltip (REQ-UI-SELECT-TOOLTIP). The `(None)` option keeps its text caption. This applies only to recipe options; the Shipyard schematic-selection dialog is unaffected and continues to show ship name captions.
|
||||
- REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel shows a "Clear" button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels.
|
||||
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. A plain click on a ship or defence station makes it the sole selection, clearing any previous selection. Ships and defence stations can be multi-selected — by Ctrl+clicking individual actors to add or remove them, or by box-drag (REQ-UI-MULTI-SELECT) — and can be selected together with debris and with one another in a single field selection (REQ-UI-SELECTION-CATEGORIES), freely mixing player and enemy actors. Actors cannot be selected together with buildings: selecting a ship or defence station clears any building selection, and selecting a building clears the actors (buildings win). Clicking a piece of debris adds to or establishes a field selection (REQ-UI-DEBRIS-CLICK-SELECT). Clicking empty world space (no building, ship, defence station, or piece of debris) clears the selection.
|
||||
- REQ-UI-SHIP-STATS-PANEL: When exactly one ship is selected (REQ-UI-ENTITY-CLICK-SELECT) and no debris is selected, the selected building panel shows a **ship stats panel**. (If debris is also selected, the panel shows the compact count summary instead, per REQ-UI-FIELD-MULTI-SELECTION.) The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed from its installed modules per REQ-MOD-STAT-CALC. The panel always shows all hull stats: HP (current / maximum), max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed. In addition, capability module summaries are shown conditioned on which module types are installed, using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each section appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT).
|
||||
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. A plain click on a ship or defence station makes it the sole selection, clearing any previous selection. Ships and defence stations can be multi-selected — by Ctrl+clicking individual actors to add or remove them, or by box-drag (REQ-UI-MULTI-SELECT) — and can be selected together with scrap piles and with one another in a single field selection (REQ-UI-SELECTION-CATEGORIES), freely mixing player and enemy actors. Actors cannot be selected together with buildings: selecting a ship or defence station clears any building selection, and selecting a building clears the actors (buildings win). Clicking a scrap pile adds to or establishes a field selection (REQ-UI-SCRAP-CLICK-SELECT). Clicking empty world space (no building, ship, defence station, or scrap pile) clears the selection.
|
||||
- REQ-UI-SHIP-STATS-PANEL: When exactly one ship is selected (REQ-UI-ENTITY-CLICK-SELECT) and no scrap is selected, the selected building panel shows a **ship stats panel**. (If scrap is also selected, the panel shows the compact count summary instead, per REQ-UI-FIELD-MULTI-SELECTION.) The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed from its installed modules per REQ-MOD-STAT-CALC. The panel always shows all hull stats: HP (current / maximum), max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed. In addition, capability module summaries are shown conditioned on which module types are installed, using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each section appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT).
|
||||
- REQ-UI-SHIP-BEHAVIOR: The ship stats panel (REQ-UI-SHIP-STATS-PANEL) additionally displays the selected ship's **current behavior** — a single label naming the top-priority behavior currently governing the ship's navigation, as resolved by the fixed-priority behavior arbitration. Only the winning behavior is named; lower-priority behaviors that are suppressed are not shown, and neither are the salvage/repair cycles that run regardless of the active behavior (REQ-SHP-SALVAGE, REQ-SHP-REPAIR). The label updates live as the ship's behavior changes, and it is always shown (independent of debug draw mode, unlike the threat-cost line of REQ-UI-SHIP-STATS-PANEL). This applies to both player and enemy ships (REQ-UI-ENTITY-CLICK-SELECT); enemy ships only ever show **Engaging** or **Advancing**. The behavior labels (all wrapped in `tr()`) are:
|
||||
- **Retreating** — the ship is retreating (REQ-SHP-RETREAT).
|
||||
- **Engaging** — the ship is engaging a combat target (player: REQ-SHP-COMBAT; enemy: REQ-SHP-ENEMY-AI).
|
||||
- **Salvaging** — the ship is executing salvage navigation: seeking debris, collecting, or delivering to a Salvage Bay (REQ-SHP-SALVAGE).
|
||||
- **Salvaging** — the ship is executing salvage navigation: seeking scrap, collecting, or delivering to a Salvage Bay (REQ-SHP-SALVAGE).
|
||||
- **Repairing** — the ship is navigating to a repair target (REQ-SHP-REPAIR).
|
||||
- **Rallying** — the ship is moving to or orbiting the rally point (REQ-SHP-RALLY).
|
||||
- **Standby** — the ship is holding with its fleet (REQ-SHP-STANDBY).
|
||||
- **Advancing** — the ship is executing the baseline forward advance with no higher-priority behavior active (player: REQ-SHP-COMBAT advance toward the enemy; enemy: REQ-SHP-ENEMY-AI advance toward the asteroid).
|
||||
- REQ-UI-STATION-STATS-PANEL: When exactly one defence station is selected (REQ-UI-ENTITY-CLICK-SELECT) and no debris is selected, the selected building panel shows a **station stats panel** displaying the station's stats computed at its current level: HP (current / maximum), damage, range, and fire rate. (If debris is also selected, the panel shows the compact count summary instead, per REQ-UI-FIELD-MULTI-SELECTION.)
|
||||
- REQ-UI-FIELD-MULTI-SELECTION: A full single-object stats panel (REQ-UI-SHIP-STATS-PANEL, REQ-UI-STATION-STATS-PANEL, REQ-UI-DEBRIS-PANEL) is shown only when the field selection holds exactly one object — one ship, one defence station, or one piece of debris. Whenever the selection holds more than one field object — multiple actors, multiple pieces of debris, or any mix of actors and debris — the panel shows a **compact summary** instead: a count per type, one line per type rendered as "<type> x <count>" (the same `x`-count notation as the recipe tooltip and the building multi-selection, REQ-UI-MULTI-SELECTION). Ships are grouped by schematic display name and defence stations as a group, distinguishing player from enemy; all selected pieces of debris are grouped into a single "Debris x <count>" line whose count is the number of selected debris pieces. No per-object detail and no total-object-count header are shown (consistent with the building panel). If debris is part of the selection, a final "Scrap x <total>" line is appended after the "Debris" line, summing the remaining scrap across all selected debris (REQ-UI-DEBRIS-PANEL), so all lines share uniform spacing. Building selections use REQ-UI-SINGLE-SELECTION / REQ-UI-MULTI-SELECTION instead.
|
||||
- REQ-UI-DEBRIS-CLICK-SELECT: The player can click any piece of debris (REQ-RES-DEBRIS-DROP) in the game world to select it. Debris are field objects (REQ-UI-SELECTION-CATEGORIES) and can be selected together with ships and defence stations, but not with buildings. A plain click on a piece of debris makes it the sole selection, clearing any previous selection; selecting a building clears any debris (buildings win), and selecting a piece of debris clears any building selection. Hit-testing prefers a building over a coincident actor or piece of debris, and an actor (ship or defence station) over a coincident piece of debris: a piece of debris is selected only when no building or actor is under the cursor. A selected piece of debris that despawns or is fully collected (REQ-RES-DEBRIS-DROP) is removed from the selection; if no selected object remains, the panel becomes empty (REQ-UI-EMPTY-SELECTION).
|
||||
- REQ-UI-DEBRIS-MULTI-SELECT: Multiple pieces of debris can be selected by box-drag or by Ctrl+clicking individual pieces to add or remove them, mirroring building multi-select (REQ-UI-MULTI-SELECT). Debris shares the field-object category with ships and defence stations (REQ-UI-SELECTION-CATEGORIES), so a field selection may hold debris and actors together. Ctrl+clicking a piece of debris while a field selection is active adds or removes that piece within the same selection; Ctrl+clicking a piece of debris while a building selection is active first clears the buildings and begins a field selection (buildings win). Conversely, selecting a building while a field selection is active clears it. Box-drag disambiguation follows REQ-UI-MULTI-SELECT (a box covering any building selects buildings; a box covering no building selects the ships, defence stations, and debris within it).
|
||||
- REQ-UI-DEBRIS-PANEL: When exactly one piece of debris is selected (and no actors, REQ-UI-FIELD-MULTI-SELECTION), the selected building panel shows a **debris stats panel** structured like the ship and station stats panels (REQ-UI-SHIP-STATS-PANEL, REQ-UI-STATION-STATS-PANEL): a **"Debris"** heading followed by a single stat row, **"Scrap"**, showing that piece's current remaining scrap amount (REQ-RES-DEBRIS-DROP), rendered in the same label/value style as a ship hull stat row. When more than one field object is selected — multiple pieces of debris, or debris together with actors — the debris are instead summarized within the compact count summary (REQ-UI-FIELD-MULTI-SELECTION): a "Debris x <count>" line giving the number of selected debris pieces, followed by a "Scrap x <total>" line summing the remaining scrap across all selected debris. The displayed scrap value(s) update as selected debris are partially collected or despawn (REQ-UI-DEBRIS-CLICK-SELECT).
|
||||
- REQ-UI-STATION-STATS-PANEL: When exactly one defence station is selected (REQ-UI-ENTITY-CLICK-SELECT) and no scrap is selected, the selected building panel shows a **station stats panel** displaying the station's stats computed at its current level: HP (current / maximum), damage, range, and fire rate. (If scrap is also selected, the panel shows the compact count summary instead, per REQ-UI-FIELD-MULTI-SELECTION.)
|
||||
- REQ-UI-FIELD-MULTI-SELECTION: The full single-actor stats panel (REQ-UI-SHIP-STATS-PANEL, REQ-UI-STATION-STATS-PANEL) is shown only when the field selection holds exactly one actor and no scrap. Whenever the selection holds more than one object — multiple actors, or a single actor together with scrap — the panel shows a **compact summary** instead: a count per actor type, one line per type rendered as "<type> x <count>" (the same `x`-count notation as the recipe tooltip and the building multi-selection, REQ-UI-MULTI-SELECTION). Ships are grouped by schematic display name and defence stations as a group, distinguishing player from enemy. No per-actor detail and no total-actor-count header are shown (consistent with the building panel). If scrap piles are also part of the field selection (REQ-UI-SELECTION-CATEGORIES) their total is appended as a final line of the same summary (REQ-UI-SCRAP-PANEL), so all lines share uniform spacing. Building selections use REQ-UI-SINGLE-SELECTION / REQ-UI-MULTI-SELECTION instead.
|
||||
- REQ-UI-SCRAP-CLICK-SELECT: The player can click any scrap pile (REQ-RES-SCRAP-DROP) in the game world to select it. Scrap piles are field objects (REQ-UI-SELECTION-CATEGORIES) and can be selected together with ships and defence stations, but not with buildings. A plain click on a scrap pile makes it the sole selection, clearing any previous selection; selecting a building clears any scrap (buildings win), and selecting a scrap pile clears any building selection. Hit-testing prefers a building over a coincident actor or scrap pile, and an actor (ship or defence station) over a coincident scrap pile: a scrap pile is selected only when no building or actor is under the cursor. A selected scrap pile that despawns or is fully collected (REQ-RES-SCRAP-DROP) is removed from the selection; if no selected object remains, the panel becomes empty (REQ-UI-EMPTY-SELECTION).
|
||||
- REQ-UI-SCRAP-MULTI-SELECT: Multiple scrap piles can be selected by box-drag or by Ctrl+clicking individual piles to add or remove them, mirroring building multi-select (REQ-UI-MULTI-SELECT). Scrap shares the field-object category with ships and defence stations (REQ-UI-SELECTION-CATEGORIES), so a field selection may hold scrap piles and actors together. Ctrl+clicking a scrap pile while a field selection is active adds or removes that pile within the same selection; Ctrl+clicking a scrap pile while a building selection is active first clears the buildings and begins a field selection (buildings win). Conversely, selecting a building while a field selection is active clears it. Box-drag disambiguation follows REQ-UI-MULTI-SELECT (a box covering any building selects buildings; a box covering no building selects the ships, defence stations, and scrap piles within it).
|
||||
- REQ-UI-SCRAP-PANEL: When one or more scrap piles are selected, the selected building panel shows the **total remaining scrap amount** across all selected piles — the sum of the piles' current remaining amounts (REQ-RES-SCRAP-DROP), e.g. "Scrap x 47". The same summed-amount display is used whether one pile or many are selected; no per-pile detail and no pile count are shown. The displayed total updates as selected piles are partially collected or despawn (REQ-UI-SCRAP-CLICK-SELECT). When actors are also selected, this scrap total is shown as an additional line of the actor count summary rather than alongside a single-actor stats panel (REQ-UI-FIELD-MULTI-SELECTION).
|
||||
|
||||
### Build Button Grid
|
||||
|
||||
- REQ-UI-BUILD-GRID: All placeable building types are shown as a flat grid of buttons with no grouping. Tunnel Entry and Tunnel Exit share a single **Tunnel** button (REQ-BLD-TUNNEL-MODE) rather than one button each.
|
||||
- REQ-UI-BUILD-COST: Each button caption shows the building name and its building block cost with the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON) in place of the trailing `Blocks` word, e.g. `Belt: 2` then a small block icon. When no icon file exists for `building_block`, the caption falls back to the text form, e.g. "Belt: 2 Blocks".
|
||||
- REQ-UI-BUILD-ICON: Each build button shows an icon alongside its caption. Icons are SVG files loaded at runtime from `data/icons/buildings/` (a sibling of the config directory, read the same way as `visuals.toml`), one file per button named after the building's id (e.g. `belt.svg`, `reprocessing_plant.svg`). The shared Tunnel button (REQ-UI-BUILD-GRID) uses `tunnel_entry.svg`; the Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) uses `deconstruct.svg`. Each icon is a rounded colored "chip" bearing a white line glyph, the chip color following the building's fill color in `visuals.toml`. A missing icon file leaves the button with its caption and no icon; it is not an error.
|
||||
- REQ-UI-BUILD-TOOLTIP: Each building-type button shows a hover tooltip with the descriptive text defined for that building type in `buildings.toml` (the optional per-building tooltip field). This tooltip is distinct from the recipe/schematic selection tooltip (REQ-UI-SELECT-TOOLTIP). If a building type defines no tooltip text, its button shows no tooltip. The Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) is not a building type and so has no config-defined tooltip; it instead shows its own refund tooltip defined in REQ-UI-DECONSTRUCT-BUTTON.
|
||||
- REQ-UI-BUILD-DISABLED: Buttons for buildings the player cannot currently afford are shown as disabled. A disabled button's icon (REQ-UI-BUILD-ICON) is rendered in a greyed variant, with its colored chip background recolored grey while the white glyph is retained.
|
||||
- REQ-UI-DECONSTRUCT-BUTTON: A dedicated **Deconstruct** button is shown in the build button grid. Clicking it toggles deconstruct mode on and off, equivalent to the Q deconstruct toggle (REQ-UI-HOTKEYS). The button is shown in a visually active/pressed state while deconstruct mode is active. The button shows a hover tooltip stating the deconstruction refund (REQ-BLD-DECONSTRUCT): that deconstructing a fully-built building returns `world.toml [world].refund_percentage` percent of its building block cost once deconstruction completes, and that a construction site removed before it finishes building is refunded in full. When `refund_percentage` is 100% both cases yield the same refund, and the tooltip is simplified to state the single refund percentage without distinguishing the two cases. Unlike the building-type button tooltips (REQ-UI-BUILD-TOOLTIP), this tooltip is not config-defined text but is composed from the refund percentage.
|
||||
- REQ-UI-BUILD-COST: Each button caption shows the building name and its building block cost, e.g. "Belt: 2 Blocks".
|
||||
- REQ-UI-BUILD-TOOLTIP: Each building-type button shows a hover tooltip with the descriptive text defined for that building type in `buildings.toml` (the optional per-building tooltip field). This tooltip is distinct from the recipe/schematic selection tooltip (REQ-UI-SELECT-TOOLTIP). If a building type defines no tooltip text, its button shows no tooltip. The Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) is not a building type and has no config-defined tooltip.
|
||||
- REQ-UI-BUILD-DISABLED: Buttons for buildings the player cannot currently afford are shown as disabled.
|
||||
- REQ-UI-DECONSTRUCT-BUTTON: A dedicated **Deconstruct** button is shown in the build button grid. Clicking it toggles deconstruct mode on and off, equivalent to the Q deconstruct toggle (REQ-UI-HOTKEYS). The button is shown in a visually active/pressed state while deconstruct mode is active.
|
||||
|
||||
### Blueprint Panel
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
|
||||
|
||||
|
||||
set(TARGET_BASE_NAME "${PRODUCT_NAME}")
|
||||
|
||||
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include "PositionComponent.h"
|
||||
#include "RepairSystem.h"
|
||||
#include "SalvagerSystem.h"
|
||||
#include "DebrisSystem.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "ShipSystem.h"
|
||||
#include "ShipsConfig.h"
|
||||
@@ -46,8 +46,6 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
||||
, m_finished(false)
|
||||
, m_stopRequested(false)
|
||||
{
|
||||
m_factoryState = makeFactoryState(m_gameConfig);
|
||||
|
||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
||||
m_gameConfig,
|
||||
m_beltSystem,
|
||||
@@ -65,7 +63,7 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
||||
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
||||
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
|
||||
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
|
||||
m_debrisSystem = std::make_unique<DebrisSystem>(m_admin);
|
||||
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
|
||||
m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin);
|
||||
m_repairSystem = std::make_unique<RepairSystem>(m_admin);
|
||||
|
||||
@@ -164,7 +162,7 @@ void ArenaSimulation::placeStructures()
|
||||
hp, hp, false);
|
||||
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
||||
m_admin.addComponent<HqProxyComponent>(m_team1HqEntity);
|
||||
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||
}
|
||||
|
||||
// Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon.
|
||||
@@ -185,7 +183,7 @@ void ArenaSimulation::placeStructures()
|
||||
hp, hp, true);
|
||||
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
||||
m_admin.addComponent<HqProxyComponent>(m_team2HqEntity);
|
||||
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||
}
|
||||
|
||||
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
|
||||
@@ -239,7 +237,7 @@ void ArenaSimulation::placeStructures()
|
||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||
ModuleOwnerComponent{stationEntity});
|
||||
}
|
||||
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||
};
|
||||
|
||||
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)
|
||||
@@ -324,13 +322,13 @@ void ArenaSimulation::tick()
|
||||
// Ship behavior systems (tick step 7): evaluate, select winner, execute.
|
||||
// Module + combat systems emit their tool beams into a shared buffer.
|
||||
m_shipSystem->clearMovementIntents();
|
||||
m_aiSystem->tick(m_admin, m_factoryState);
|
||||
m_aiSystem->tick(m_admin, *m_buildingSystem, *m_scrapSystem);
|
||||
std::vector<BeamFiredEvent> beamFiredEvents;
|
||||
m_salvagerSystem->tick(m_currentTick, m_factoryState, beamFiredEvents);
|
||||
m_salvagerSystem->tick(m_currentTick, *m_scrapSystem, *m_buildingSystem, beamFiredEvents);
|
||||
m_repairSystem->tick(m_currentTick, beamFiredEvents);
|
||||
|
||||
// Combat resolution (tick step 8).
|
||||
m_combatSystem->tick(m_currentTick, m_admin, beamFiredEvents);
|
||||
m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, beamFiredEvents);
|
||||
m_beamFiredEvents.insert(m_beamFiredEvents.end(), beamFiredEvents.begin(), beamFiredEvents.end());
|
||||
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
||||
|
||||
@@ -342,7 +340,7 @@ void ArenaSimulation::tick()
|
||||
m_dynamicBodySystem->tick(m_admin);
|
||||
|
||||
// Scrap despawn (tick step 11).
|
||||
m_debrisSystem->tickDespawn(m_currentTick);
|
||||
m_scrapSystem->tickDespawn(m_currentTick);
|
||||
|
||||
++m_currentTick;
|
||||
|
||||
@@ -373,8 +371,8 @@ void ArenaSimulation::tickDeaths()
|
||||
if (si.scrapDrop > 0)
|
||||
{
|
||||
const Tick despawnAt = m_currentTick
|
||||
+ secondsToTicks(m_gameConfig.world.debrisDespawnSeconds);
|
||||
m_debrisSystem->spawn(pos.value, si.scrapDrop, despawnAt);
|
||||
+ secondsToTicks(m_gameConfig.world.scrapDespawnSeconds);
|
||||
m_scrapSystem->spawn(pos.value, si.scrapDrop, despawnAt);
|
||||
}
|
||||
m_shipSystem->despawn(deadEntity);
|
||||
}
|
||||
@@ -394,7 +392,7 @@ void ArenaSimulation::tickDeaths()
|
||||
for (entt::entity deadEntity : deadStations)
|
||||
{
|
||||
const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
|
||||
m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells);
|
||||
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
|
||||
{
|
||||
std::vector<entt::entity> stationChildren;
|
||||
m_admin.forEach<ModuleOwnerComponent>(
|
||||
@@ -489,11 +487,6 @@ const ArenaConfig& ArenaSimulation::getArenaConfig() const
|
||||
return m_arenaConfig;
|
||||
}
|
||||
|
||||
const FactoryState& ArenaSimulation::getFactoryState() const
|
||||
{
|
||||
return m_factoryState;
|
||||
}
|
||||
|
||||
const BuildingSystem& ArenaSimulation::getBuildings() const
|
||||
{
|
||||
return *m_buildingSystem;
|
||||
@@ -504,9 +497,9 @@ const ShipSystem& ArenaSimulation::getShips() const
|
||||
return *m_shipSystem;
|
||||
}
|
||||
|
||||
const DebrisSystem& ArenaSimulation::getDebrisSystem() const
|
||||
const ScrapSystem& ArenaSimulation::getScraps() const
|
||||
{
|
||||
return *m_debrisSystem;
|
||||
return *m_scrapSystem;
|
||||
}
|
||||
|
||||
EntityAdmin& ArenaSimulation::getAdmin()
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
#include "BalancingConfig.h"
|
||||
#include "BeltSystem.h"
|
||||
#include "FactoryState.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "BuildingId.h"
|
||||
|
||||
@@ -27,7 +26,7 @@ class MovementIntentSystem;
|
||||
class RepairSystem;
|
||||
class SalvagerSystem;
|
||||
class ShipSystem;
|
||||
class DebrisSystem;
|
||||
class ScrapSystem;
|
||||
|
||||
struct ArenaStatus
|
||||
{
|
||||
@@ -86,9 +85,8 @@ public:
|
||||
|
||||
const ArenaConfig& getArenaConfig() const;
|
||||
const BuildingSystem& getBuildings() const;
|
||||
const FactoryState& getFactoryState() const;
|
||||
const ShipSystem& getShips() const;
|
||||
const DebrisSystem& getDebrisSystem() const;
|
||||
const ScrapSystem& getScraps() const;
|
||||
EntityAdmin& getAdmin();
|
||||
const EntityAdmin& getAdmin() const;
|
||||
|
||||
@@ -109,7 +107,6 @@ private:
|
||||
BuildingId m_nextBuildingId;
|
||||
|
||||
EntityAdmin m_admin;
|
||||
FactoryState m_factoryState;
|
||||
BeltSystem m_beltSystem;
|
||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||
@@ -117,7 +114,7 @@ private:
|
||||
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
|
||||
std::unique_ptr<DynamicBodySystem> m_dynamicBodySystem;
|
||||
std::unique_ptr<CombatSystem> m_combatSystem;
|
||||
std::unique_ptr<DebrisSystem> m_debrisSystem;
|
||||
std::unique_ptr<ScrapSystem> m_scrapSystem;
|
||||
std::unique_ptr<SalvagerSystem> m_salvagerSystem;
|
||||
std::unique_ptr<RepairSystem> m_repairSystem;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "ArenaView.h"
|
||||
#include "FactoryQueries.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -25,11 +24,11 @@
|
||||
#include "PositionComponent.h"
|
||||
#include "RepairBehavior.h"
|
||||
#include "SalvageScrapBehavior.h"
|
||||
#include "DebrisSystem.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "SensorRangeComponent.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "StationBodyComponent.h"
|
||||
#include "DebrisComponent.h"
|
||||
#include "ScrapDataComponent.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -154,7 +153,7 @@ void ArenaView::handleEvent(std::shared_ptr<const BeamFiredEvent> event)
|
||||
maxRadius = shorter / 2.0f;
|
||||
}
|
||||
else if (m_sim->getAdmin().isValid(event->target)
|
||||
&& m_sim->getAdmin().hasAll<DebrisComponent>(event->target))
|
||||
&& m_sim->getAdmin().hasAll<ScrapDataComponent>(event->target))
|
||||
{
|
||||
maxRadius = 0.1f;
|
||||
}
|
||||
@@ -179,7 +178,7 @@ void ArenaView::paintGL()
|
||||
drawTiles(painter);
|
||||
drawBuildings(painter);
|
||||
drawStations(painter);
|
||||
drawDebris(painter);
|
||||
drawScrap(painter);
|
||||
if (m_debugDraw)
|
||||
{
|
||||
drawDebugSensorRanges(painter);
|
||||
@@ -307,7 +306,7 @@ void ArenaView::drawTiles(QPainter& painter)
|
||||
|
||||
void ArenaView::drawBuildings(QPainter& painter)
|
||||
{
|
||||
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
|
||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
||||
{
|
||||
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
||||
m_visuals->buildings.find(b.type);
|
||||
@@ -337,12 +336,12 @@ void ArenaView::drawBuildings(QPainter& painter)
|
||||
}
|
||||
}
|
||||
|
||||
void ArenaView::drawDebris(QPainter& painter)
|
||||
void ArenaView::drawScrap(QPainter& painter)
|
||||
{
|
||||
const float r = getTilePx() * 0.2f;
|
||||
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
|
||||
for (const ScrapInfo& scrap : m_sim->getScraps().getAllScrapInfo())
|
||||
{
|
||||
const QPointF center = worldToWidget(debris.position);
|
||||
const QPointF center = worldToWidget(scrap.position);
|
||||
painter.setBrush(QColor(128, 110, 90));
|
||||
painter.setPen(QPen(QColor(50, 40, 30), 1));
|
||||
painter.drawEllipse(center,
|
||||
@@ -530,9 +529,9 @@ void ArenaView::drawDebugTargetLines(QPainter& painter)
|
||||
const PositionComponent& pos, const FactionComponent& fac,
|
||||
const SalvageScrapBehavior& salvage)
|
||||
{
|
||||
if (!salvage.debrisTarget.has_value()) { return; }
|
||||
if (!salvage.scrapTarget.has_value()) { return; }
|
||||
|
||||
drawTargetLine(fac.isEnemy, pos.value, *salvage.debrisTarget);
|
||||
drawTargetLine(fac.isEnemy, pos.value, *salvage.scrapTarget);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ private:
|
||||
void drawTiles(QPainter& painter);
|
||||
void drawBuildings(QPainter& painter);
|
||||
void drawStations(QPainter& painter);
|
||||
void drawDebris(QPainter& painter);
|
||||
void drawScrap(QPainter& painter);
|
||||
void drawShips(QPainter& painter);
|
||||
void drawDebugSensorRanges(QPainter& painter);
|
||||
void drawDebugTargetLines(QPainter& painter);
|
||||
|
||||
@@ -13,7 +13,6 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprintSerializer.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TomlHelpers.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -21,17 +20,9 @@ SET(SRCS
|
||||
${SRCS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Formula.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderWorld.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderBuildings.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderRecipes.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderShips.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderStations.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderModules.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderUnlocks.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprintSerializer.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TomlHelpers.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,771 @@
|
||||
#include "ConfigLoader.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "TomlHelpers.h"
|
||||
#include <QPoint>
|
||||
|
||||
#include "toml.hpp"
|
||||
|
||||
#include "Rotation.h"
|
||||
#include "ShipLayout.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// --- Error helpers --------------------------------------------------------
|
||||
|
||||
std::runtime_error makeError(const std::string& file,
|
||||
const std::string& path,
|
||||
const std::string& why)
|
||||
{
|
||||
return std::runtime_error("Config: " + file + ": '" + path + "' " + why);
|
||||
}
|
||||
|
||||
// --- Typed accessors (throw on missing or wrong type) ---------------------
|
||||
|
||||
int64_t requireInt(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const std::optional<int64_t> value = node.value<int64_t>();
|
||||
if (!value)
|
||||
{
|
||||
throw makeError(file, path, "missing or not an integer");
|
||||
}
|
||||
return *value;
|
||||
}
|
||||
|
||||
double requireDouble(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
if (const std::optional<double> v = node.value<double>())
|
||||
{
|
||||
return *v;
|
||||
}
|
||||
if (const std::optional<int64_t> v = node.value<int64_t>())
|
||||
{
|
||||
return static_cast<double>(*v);
|
||||
}
|
||||
throw makeError(file, path, "missing or not a number");
|
||||
}
|
||||
|
||||
std::string requireString(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const std::optional<std::string> value = node.value<std::string>();
|
||||
if (!value)
|
||||
{
|
||||
throw makeError(file, path, "missing or not a string");
|
||||
}
|
||||
return *value;
|
||||
}
|
||||
|
||||
bool requireBool(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const std::optional<bool> value = node.value<bool>();
|
||||
if (!value)
|
||||
{
|
||||
throw makeError(file, path, "missing or not a boolean");
|
||||
}
|
||||
return *value;
|
||||
}
|
||||
|
||||
const toml::array& requireArray(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const toml::array* arr = node.as_array();
|
||||
if (arr == nullptr)
|
||||
{
|
||||
throw makeError(file, path, "missing or not an array");
|
||||
}
|
||||
return *arr;
|
||||
}
|
||||
|
||||
const toml::table& requireTable(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const toml::table* tbl = node.as_table();
|
||||
if (tbl == nullptr)
|
||||
{
|
||||
throw makeError(file, path, "missing or not a table");
|
||||
}
|
||||
return *tbl;
|
||||
}
|
||||
|
||||
Formula requireFormula(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const std::string source = requireString(node, file, path);
|
||||
try
|
||||
{
|
||||
return Formula::compile(source);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
throw makeError(file, path, std::string("formula error: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> requireStringArray(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const toml::array& arr = requireArray(node, file, path);
|
||||
std::vector<std::string> result;
|
||||
result.reserve(arr.size());
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||
const std::optional<std::string> s = arr[i].value<std::string>();
|
||||
if (!s)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a string");
|
||||
}
|
||||
result.push_back(*s);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<RecipeIngredient> parseIngredients(const toml::array& arr,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
std::vector<RecipeIngredient> result;
|
||||
result.reserve(arr.size());
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||
const toml::table* t = arr[i].as_table();
|
||||
if (t == nullptr)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a table");
|
||||
}
|
||||
|
||||
// We need a mutable node_view to reuse our helpers, which is fine
|
||||
// because the helpers never mutate.
|
||||
toml::table& mt = const_cast<toml::table&>(*t);
|
||||
|
||||
RecipeIngredient ing;
|
||||
ing.item = requireString(mt["item"], file, elemPath + ".item");
|
||||
ing.amount = static_cast<int>(requireInt(mt["amount"], file, elemPath + ".amount"));
|
||||
result.push_back(std::move(ing));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<RecipeOutput> parseRecipeOutputs(const toml::array& arr,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
std::vector<RecipeOutput> result;
|
||||
result.reserve(arr.size());
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||
const toml::table* t = arr[i].as_table();
|
||||
if (t == nullptr)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*t);
|
||||
|
||||
RecipeOutput out;
|
||||
out.item = requireString(mt["item"], file, elemPath + ".item");
|
||||
out.amount = static_cast<int>(requireInt(mt["amount"], file, elemPath + ".amount"));
|
||||
if (const std::optional<double> p = mt["probability"].value<double>())
|
||||
{
|
||||
out.probability = *p;
|
||||
}
|
||||
else if (const std::optional<int64_t> p = mt["probability"].value<int64_t>())
|
||||
{
|
||||
out.probability = static_cast<double>(*p);
|
||||
}
|
||||
result.push_back(std::move(out));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
toml::table parseFile(const std::string& path, const std::string& file)
|
||||
{
|
||||
try
|
||||
{
|
||||
return toml::parse_file(path);
|
||||
}
|
||||
catch (const toml::parse_error& e)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "Config: " << file << ": TOML parse error: " << e.description()
|
||||
<< " at " << e.source().begin;
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
}
|
||||
|
||||
Rotation parseRotationString(const std::string& s)
|
||||
{
|
||||
if (s == "east") { return Rotation::East; }
|
||||
if (s == "south") { return Rotation::South; }
|
||||
if (s == "west") { return Rotation::West; }
|
||||
return Rotation::North;
|
||||
}
|
||||
|
||||
std::vector<PlacedModule> parsePlacedModules(const toml::array& arr,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
std::vector<PlacedModule> result;
|
||||
result.reserve(arr.size());
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||
const toml::table* t = arr[i].as_table();
|
||||
if (t == nullptr) { continue; }
|
||||
toml::table& mt = const_cast<toml::table&>(*t);
|
||||
|
||||
const std::optional<std::string> type = mt["type"].value<std::string>();
|
||||
const std::optional<int64_t> x = mt["x"].value<int64_t>();
|
||||
const std::optional<int64_t> y = mt["y"].value<int64_t>();
|
||||
const std::optional<std::string> rot = mt["rotation"].value<std::string>();
|
||||
if (!type || !x || !y || !rot) { continue; }
|
||||
|
||||
PlacedModule pm;
|
||||
pm.moduleId = *type;
|
||||
pm.position = QPoint(static_cast<int>(*x), static_cast<int>(*y));
|
||||
pm.rotation = parseRotationString(*rot);
|
||||
result.push_back(std::move(pm));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
// --- Per-file loaders -----------------------------------------------------
|
||||
|
||||
WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
||||
{
|
||||
const std::string file = "world.toml";
|
||||
toml::table tbl = parseFile(path, file);
|
||||
|
||||
WorldConfig cfg;
|
||||
|
||||
cfg.heightTiles = static_cast<int>(requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles"));
|
||||
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
|
||||
cfg.deconstructionTimeSeconds = requireDouble(tbl["world"]["deconstruction_time_seconds"], file, "world.deconstruction_time_seconds");
|
||||
cfg.startingBuildingBlocks = static_cast<int>(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks"));
|
||||
cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds");
|
||||
cfg.scrapPerThreat = requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat");
|
||||
cfg.tileSize_m = requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m");
|
||||
cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m;
|
||||
cfg.tunnelMaxDistance_tiles = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles"));
|
||||
cfg.departureIntervalSeconds = requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds");
|
||||
cfg.orbitFactor = requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor");
|
||||
cfg.rallyOrbitRadius_tiles = requireDouble(tbl["world"]["rally_orbit_radius_tiles"], file, "world.rally_orbit_radius_tiles");
|
||||
|
||||
if (const std::optional<std::string> tip =
|
||||
tbl["world"]["building_blocks_tooltip"].value<std::string>())
|
||||
{
|
||||
cfg.buildingBlocksTooltip = *tip;
|
||||
}
|
||||
|
||||
if (const std::optional<std::string> tip =
|
||||
tbl["world"]["artifact_tooltip"].value<std::string>())
|
||||
{
|
||||
cfg.artifactTooltip = *tip;
|
||||
}
|
||||
|
||||
cfg.regions.asteroidWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles"));
|
||||
cfg.regions.playerBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles"));
|
||||
cfg.regions.contestZoneWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles"));
|
||||
cfg.regions.enemyBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles"));
|
||||
|
||||
cfg.expansion.columnsPerExpansion_tiles = static_cast<int>(requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles"));
|
||||
cfg.expansion.costBuildingBlocksFormula = requireFormula(tbl["expansion"]["cost_building_blocks_formula"], file, "expansion.cost_building_blocks_formula");
|
||||
|
||||
cfg.push.pushExpandColumns_tiles = static_cast<int>(requireInt(tbl["push"]["push_expand_columns_tiles"], file, "push.push_expand_columns_tiles"));
|
||||
cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds");
|
||||
|
||||
cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula");
|
||||
cfg.waves.gapMinSeconds = requireDouble(tbl["waves"]["gap_min_seconds"], file, "waves.gap_min_seconds");
|
||||
cfg.waves.gapMaxSeconds = requireDouble(tbl["waves"]["gap_max_seconds"], file, "waves.gap_max_seconds");
|
||||
cfg.waves.spawnDurationSeconds = requireDouble(tbl["waves"]["spawn_duration_seconds"], file, "waves.spawn_duration_seconds");
|
||||
cfg.waves.bossCountdownSeconds = requireDouble(tbl["waves"]["boss_countdown_seconds"], file, "waves.boss_countdown_seconds");
|
||||
cfg.waves.bossThreatDurationSeconds = requireDouble(tbl["waves"]["boss_threat_duration_seconds"], file, "waves.boss_threat_duration_seconds");
|
||||
cfg.waves.bossQuietBeforeSeconds = requireDouble(tbl["waves"]["boss_quiet_before_seconds"], file, "waves.boss_quiet_before_seconds");
|
||||
cfg.waves.bossQuietAfterSeconds = requireDouble(tbl["waves"]["boss_quiet_after_seconds"], file, "waves.boss_quiet_after_seconds");
|
||||
|
||||
if (cfg.waves.gapMinSeconds > cfg.waves.gapMaxSeconds)
|
||||
{
|
||||
throw makeError(file, "waves", "gap_min_seconds > gap_max_seconds");
|
||||
}
|
||||
|
||||
cfg.targeting.targetScoreFormula = requireFormula(tbl["targeting"]["target_score_formula"], file, "targeting.target_score_formula");
|
||||
cfg.targeting.overclaimPenaltyFormula = requireFormula(tbl["targeting"]["overclaim_penalty_formula"], file, "targeting.overclaim_penalty_formula");
|
||||
cfg.targeting.hysteresis = requireDouble(tbl["targeting"]["target_hysteresis"], file, "targeting.target_hysteresis");
|
||||
|
||||
cfg.artifacts.artifactChanceFormula = requireFormula(tbl["artifacts"]["artifact_chance_formula"], file, "artifacts.artifact_chance_formula");
|
||||
cfg.artifacts.artifactWinCount = static_cast<int>(requireInt(tbl["artifacts"]["artifact_win_count"], file, "artifacts.artifact_win_count"));
|
||||
|
||||
cfg.scroll.panSpeedSlow_tps = requireDouble(tbl["scroll"]["pan_speed_slow_tiles_per_second"], file, "scroll.pan_speed_slow_tiles_per_second");
|
||||
cfg.scroll.panSpeedFast_tps = requireDouble(tbl["scroll"]["pan_speed_fast_tiles_per_second"], file, "scroll.pan_speed_fast_tiles_per_second");
|
||||
cfg.scroll.panRampBandWidth_tiles = static_cast<int>(requireInt(tbl["scroll"]["pan_ramp_band_width_tiles"], file, "scroll.pan_ramp_band_width_tiles"));
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
|
||||
{
|
||||
const std::string file = "buildings.toml";
|
||||
toml::table tbl = parseFile(path, file);
|
||||
|
||||
BuildingsConfig cfg;
|
||||
const toml::array& arr = requireArray(tbl["building"], file, "building");
|
||||
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = "building[" + std::to_string(i) + "]";
|
||||
const toml::table* bt = arr[i].as_table();
|
||||
if (bt == nullptr)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*bt);
|
||||
|
||||
BuildingDef def;
|
||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||
def.cost = static_cast<int>(requireInt(mt["cost"], file, elemPath + ".cost"));
|
||||
def.playerPlaceable = requireBool(mt["player_placeable"], file, elemPath + ".player_placeable");
|
||||
def.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
|
||||
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||
|
||||
if (mt.contains("output_buffer_capacity"))
|
||||
{
|
||||
def.outputBufferCapacity = static_cast<int>(
|
||||
requireInt(mt["output_buffer_capacity"], file, elemPath + ".output_buffer_capacity"));
|
||||
}
|
||||
|
||||
if (mt.contains("tooltip"))
|
||||
{
|
||||
def.tooltip = requireString(mt["tooltip"], file, elemPath + ".tooltip");
|
||||
}
|
||||
|
||||
const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
|
||||
if (!parsedType)
|
||||
{
|
||||
throw makeError(file, elemPath + ".id", "unknown building id '" + def.id + "'");
|
||||
}
|
||||
def.type = *parsedType;
|
||||
|
||||
cfg.buildings.push_back(std::move(def));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
|
||||
{
|
||||
const std::string file = "recipes.toml";
|
||||
toml::table tbl = parseFile(path, file);
|
||||
|
||||
RecipesConfig cfg;
|
||||
const toml::array& arr = requireArray(tbl["recipe"], file, "recipe");
|
||||
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = "recipe[" + std::to_string(i) + "]";
|
||||
const toml::table* rt = arr[i].as_table();
|
||||
if (rt == nullptr)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*rt);
|
||||
|
||||
RecipeDef def;
|
||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||
def.durationSeconds = requireDouble(mt["duration_seconds"], file, elemPath + ".duration_seconds");
|
||||
|
||||
const std::string buildingId = requireString(mt["building"], file, elemPath + ".building");
|
||||
const std::optional<BuildingType> parsedType = parseBuildingType(buildingId);
|
||||
if (!parsedType)
|
||||
{
|
||||
throw makeError(file, elemPath + ".building",
|
||||
"unknown building id '" + buildingId + "'");
|
||||
}
|
||||
def.building = *parsedType;
|
||||
|
||||
if (def.building == BuildingType::Assembler && mt.contains("unlocked_at_start"))
|
||||
{
|
||||
def.unlockedAtStart = requireBool(mt["unlocked_at_start"], file,
|
||||
elemPath + ".unlocked_at_start");
|
||||
}
|
||||
|
||||
// inputs may be omitted (e.g. miner recipes). An empty array is fine.
|
||||
if (mt.contains("inputs"))
|
||||
{
|
||||
const toml::array& inputs = requireArray(mt["inputs"], file, elemPath + ".inputs");
|
||||
def.inputs = parseIngredients(inputs, file, elemPath + ".inputs");
|
||||
}
|
||||
|
||||
const toml::array& outputs = requireArray(mt["outputs"], file, elemPath + ".outputs");
|
||||
def.outputs = parseRecipeOutputs(outputs, file, elemPath + ".outputs");
|
||||
|
||||
cfg.recipes.push_back(std::move(def));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
{
|
||||
const std::string file = "ships.toml";
|
||||
toml::table tbl = parseFile(path, file);
|
||||
|
||||
ShipsConfig cfg;
|
||||
const toml::array& arr = requireArray(tbl["ship"], file, "ship");
|
||||
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = "ship[" + std::to_string(i) + "]";
|
||||
const toml::table* st = arr[i].as_table();
|
||||
if (st == nullptr)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*st);
|
||||
|
||||
ShipDef def;
|
||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||
def.layout = requireStringArray(mt["layout"], file, elemPath + ".layout");
|
||||
|
||||
// Schematic
|
||||
{
|
||||
const std::string bpPath = elemPath + ".schematic";
|
||||
const toml::table& bpTable = requireTable(mt["schematic"], file, bpPath);
|
||||
toml::table& bpMt = const_cast<toml::table&>(bpTable);
|
||||
|
||||
const toml::array& materials = requireArray(bpMt["materials"], file, bpPath + ".materials");
|
||||
def.schematic.materials = parseIngredients(materials, file, bpPath + ".materials");
|
||||
def.schematic.productionTimeSeconds = requireDouble(
|
||||
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
|
||||
}
|
||||
|
||||
// Health
|
||||
{
|
||||
const std::string hPath = elemPath + ".health";
|
||||
const toml::table& hTable = requireTable(mt["health"], file, hPath);
|
||||
toml::table& hMt = const_cast<toml::table&>(hTable);
|
||||
def.health.hp = static_cast<float>(requireDouble(hMt["hp"], file, hPath + ".hp"));
|
||||
}
|
||||
|
||||
// Movement
|
||||
{
|
||||
const std::string mPath = elemPath + ".movement";
|
||||
const toml::table& mTable = requireTable(mt["movement"], file, mPath);
|
||||
toml::table& mMt = const_cast<toml::table&>(mTable);
|
||||
def.movement.speed_mps = static_cast<float>(requireDouble(mMt["speed_mps"], file, mPath + ".speed_mps"));
|
||||
def.movement.mainAcceleration_mpss = static_cast<float>(requireDouble(mMt["main_acceleration_mpss"], file, mPath + ".main_acceleration_mpss"));
|
||||
def.movement.maneuveringAcceleration_mpss = static_cast<float>(requireDouble(mMt["maneuvering_acceleration_mpss"], file, mPath + ".maneuvering_acceleration_mpss"));
|
||||
def.movement.angularAcceleration_radpss = static_cast<float>(requireDouble(mMt["angular_acceleration_radpss"], file, mPath + ".angular_acceleration_radpss"));
|
||||
def.movement.maxRotationSpeed_radps = static_cast<float>(requireDouble(mMt["max_rotation_speed_radps"], file, mPath + ".max_rotation_speed_radps"));
|
||||
}
|
||||
|
||||
// Sensor
|
||||
{
|
||||
const std::string snsPath = elemPath + ".sensor";
|
||||
const toml::table& snsTable = requireTable(mt["sensor"], file, snsPath);
|
||||
toml::table& snsMt = const_cast<toml::table&>(snsTable);
|
||||
def.sensor.sensorRange_m = static_cast<float>(requireDouble(snsMt["sensor_range_m"], file, snsPath + ".sensor_range_m"));
|
||||
}
|
||||
|
||||
// Optional: default_modules (REQ-WAV-DEFAULT-MODULES)
|
||||
if (mt.contains("default_modules"))
|
||||
{
|
||||
const toml::array& modArr = requireArray(mt["default_modules"], file,
|
||||
elemPath + ".default_modules");
|
||||
def.defaultModules = parsePlacedModules(modArr, file,
|
||||
elemPath + ".default_modules");
|
||||
}
|
||||
|
||||
cfg.ships.push_back(std::move(def));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
StationsConfig ConfigLoader::loadStations(const std::string& path)
|
||||
{
|
||||
const std::string file = "stations.toml";
|
||||
toml::table tbl = parseFile(path, file);
|
||||
|
||||
StationsConfig cfg;
|
||||
|
||||
// HQ
|
||||
{
|
||||
const std::string p = "hq";
|
||||
cfg.hq.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
||||
cfg.hq.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||
}
|
||||
|
||||
// Player station
|
||||
{
|
||||
const std::string p = "player_station";
|
||||
cfg.playerStation.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
||||
cfg.playerStation.level = static_cast<int>(requireInt(tbl[p]["level"], file, p + ".level"));
|
||||
cfg.playerStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||
cfg.playerStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
||||
cfg.playerStation.rangeFormula = requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
|
||||
cfg.playerStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
|
||||
cfg.playerStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
||||
}
|
||||
|
||||
// Enemy station
|
||||
{
|
||||
const std::string p = "enemy_station";
|
||||
cfg.enemyStation.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
||||
cfg.enemyStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||
cfg.enemyStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
||||
cfg.enemyStation.rangeFormula = requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
|
||||
cfg.enemyStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
|
||||
cfg.enemyStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Known category→stat mappings for module stat modifier discovery.
|
||||
// addedKeySuffix: unit suffix appended before "_formula" for additive modifier keys only.
|
||||
// Multiplicative modifier keys are always dimensionless and carry no suffix.
|
||||
struct StatEntry
|
||||
{
|
||||
const char* category;
|
||||
const char* stat;
|
||||
const char* addedKeySuffix;
|
||||
};
|
||||
|
||||
static const StatEntry kKnownStats[] = {
|
||||
{"health", "hp", ""},
|
||||
{"movement", "speed", "_mps"},
|
||||
{"movement", "main_acceleration", "_mpss"},
|
||||
{"movement", "maneuvering_acceleration", "_mpss"},
|
||||
{"sensor", "sensor_range", "_m"},
|
||||
{"weapon", "damage", ""},
|
||||
{"weapon", "attack_range", "_m"},
|
||||
{"weapon", "attack_rate", "_hz"},
|
||||
{"salvage", "collection_range", "_m"},
|
||||
{"salvage", "collection_rate", "_hz"},
|
||||
{"cargo", "cargo_capacity", ""},
|
||||
{"repair", "repair_rate", "_hz"},
|
||||
{"repair", "repair_range", "_m"},
|
||||
};
|
||||
|
||||
ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||
{
|
||||
const std::string file = "modules.toml";
|
||||
toml::table tbl = parseFile(path, file);
|
||||
|
||||
ModulesConfig cfg;
|
||||
|
||||
if (!tbl.contains("module"))
|
||||
{
|
||||
return cfg;
|
||||
}
|
||||
|
||||
const toml::array& arr = requireArray(tbl["module"], file, "module");
|
||||
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = "module[" + std::to_string(i) + "]";
|
||||
const toml::table* st = arr[i].as_table();
|
||||
if (st == nullptr)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*st);
|
||||
|
||||
ModuleDef def;
|
||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||
def.productionTimeSeconds = requireDouble(
|
||||
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
||||
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
||||
def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph");
|
||||
|
||||
if (mt.contains("tooltip"))
|
||||
{
|
||||
def.tooltip = requireString(mt["tooltip"], file, elemPath + ".tooltip");
|
||||
}
|
||||
|
||||
// Materials
|
||||
{
|
||||
const toml::array& materials = requireArray(mt["materials"], file, elemPath + ".materials");
|
||||
def.materials = parseIngredients(materials, file, elemPath + ".materials");
|
||||
}
|
||||
|
||||
// Stat modifiers from [module.<category>] sub-tables
|
||||
for (const StatEntry& se : kKnownStats)
|
||||
{
|
||||
if (!mt.contains(se.category))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const toml::table& catTable = requireTable(mt[se.category], file,
|
||||
elemPath + "." + se.category);
|
||||
toml::table& catMt = const_cast<toml::table&>(catTable);
|
||||
|
||||
const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix;
|
||||
const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix;
|
||||
|
||||
if (catMt.contains(addedKey))
|
||||
{
|
||||
ModuleStatModifier mod;
|
||||
mod.stat = se.stat;
|
||||
mod.modifierType = "additive";
|
||||
mod.value = requireDouble(catMt[addedKey], file,
|
||||
elemPath + "." + se.category + "." + addedKey);
|
||||
def.statModifiers.push_back(std::move(mod));
|
||||
}
|
||||
|
||||
if (catMt.contains(multipliedKey))
|
||||
{
|
||||
ModuleStatModifier mod;
|
||||
mod.stat = se.stat;
|
||||
mod.modifierType = "multiplicative";
|
||||
mod.value = requireDouble(catMt[multipliedKey], file,
|
||||
elemPath + "." + se.category + "." + multipliedKey);
|
||||
def.statModifiers.push_back(std::move(mod));
|
||||
}
|
||||
}
|
||||
|
||||
// Weapon capability section: [module.weapon] with base stat formulas
|
||||
if (mt.contains("weapon"))
|
||||
{
|
||||
const std::string wPath = elemPath + ".weapon";
|
||||
const toml::table& wTable = requireTable(mt["weapon"], file, wPath);
|
||||
toml::table& wMt = const_cast<toml::table&>(wTable);
|
||||
if (wMt.contains("damage") || wMt.contains("attack_range_m")
|
||||
|| wMt.contains("attack_rate_hz"))
|
||||
{
|
||||
ModuleWeaponCapability cap;
|
||||
cap.damage = static_cast<float>(requireDouble(wMt["damage"],
|
||||
file, wPath + ".damage"));
|
||||
cap.attackRange_m = static_cast<float>(requireDouble(wMt["attack_range_m"],
|
||||
file, wPath + ".attack_range_m"));
|
||||
cap.attackRate_hz = static_cast<float>(requireDouble(wMt["attack_rate_hz"],
|
||||
file, wPath + ".attack_rate_hz"));
|
||||
def.weaponCapability = std::move(cap);
|
||||
}
|
||||
}
|
||||
|
||||
// Salvage capability section: [module.salvage] with base stat formulas
|
||||
if (mt.contains("salvage"))
|
||||
{
|
||||
const std::string sPath = elemPath + ".salvage";
|
||||
const toml::table& sTable = requireTable(mt["salvage"], file, sPath);
|
||||
toml::table& sMt = const_cast<toml::table&>(sTable);
|
||||
if (sMt.contains("collection_range_m") || sMt.contains("cargo_capacity")
|
||||
|| sMt.contains("collection_rate_hz"))
|
||||
{
|
||||
ModuleSalvageCapability cap;
|
||||
cap.collectionRange_m = static_cast<float>(requireDouble(sMt["collection_range_m"],
|
||||
file, sPath + ".collection_range_m"));
|
||||
cap.cargoCapacity = static_cast<float>(requireDouble(sMt["cargo_capacity"],
|
||||
file, sPath + ".cargo_capacity"));
|
||||
cap.collectionRate_hz = static_cast<float>(requireDouble(sMt["collection_rate_hz"],
|
||||
file, sPath + ".collection_rate_hz"));
|
||||
def.salvageCapability = std::move(cap);
|
||||
}
|
||||
}
|
||||
|
||||
// Repair capability section: [module.repair] with base stat formulas
|
||||
if (mt.contains("repair"))
|
||||
{
|
||||
const std::string rPath = elemPath + ".repair";
|
||||
const toml::table& rTable = requireTable(mt["repair"], file, rPath);
|
||||
toml::table& rMt = const_cast<toml::table&>(rTable);
|
||||
if (rMt.contains("repair_rate_hz") || rMt.contains("repair_range_m"))
|
||||
{
|
||||
ModuleRepairCapability cap;
|
||||
cap.repairRate_hz = static_cast<float>(requireDouble(rMt["repair_rate_hz"],
|
||||
file, rPath + ".repair_rate_hz"));
|
||||
cap.repairAmountHp = static_cast<float>(requireDouble(rMt["repair_amount_hp"],
|
||||
file, rPath + ".repair_amount_hp"));
|
||||
cap.repairRange_m = static_cast<float>(requireDouble(rMt["repair_range_m"],
|
||||
file, rPath + ".repair_range_m"));
|
||||
def.repairCapability = std::move(cap);
|
||||
}
|
||||
}
|
||||
|
||||
cfg.modules.push_back(std::move(def));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
UnlocksConfig ConfigLoader::loadUnlocks(const std::string& path)
|
||||
{
|
||||
const std::string file = "unlocks.toml";
|
||||
toml::table tbl = parseFile(path, file);
|
||||
|
||||
UnlocksConfig cfg;
|
||||
if (!tbl.contains("unlock"))
|
||||
{
|
||||
return cfg;
|
||||
}
|
||||
|
||||
const toml::array& arr = requireArray(tbl["unlock"], file, "unlock");
|
||||
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = "unlock[" + std::to_string(i) + "]";
|
||||
const toml::table* ut = arr[i].as_table();
|
||||
if (ut == nullptr)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*ut);
|
||||
|
||||
UnlockGroupDef def;
|
||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||
def.stationLevel = static_cast<int>(
|
||||
requireInt(mt["station_level"], file, elemPath + ".station_level"));
|
||||
if (mt.contains("requires"))
|
||||
{
|
||||
def.requiredGroupIds = requireStringArray(mt["requires"], file, elemPath + ".requires");
|
||||
}
|
||||
if (mt.contains("ships"))
|
||||
{
|
||||
def.ships = requireStringArray(mt["ships"], file, elemPath + ".ships");
|
||||
}
|
||||
if (mt.contains("modules"))
|
||||
{
|
||||
def.modules = requireStringArray(mt["modules"], file, elemPath + ".modules");
|
||||
}
|
||||
if (mt.contains("buildings"))
|
||||
{
|
||||
def.buildings = requireStringArray(mt["buildings"], file, elemPath + ".buildings");
|
||||
}
|
||||
if (mt.contains("recipes"))
|
||||
{
|
||||
def.recipes = requireStringArray(mt["recipes"], file, elemPath + ".recipes");
|
||||
}
|
||||
|
||||
cfg.groups.push_back(std::move(def));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -46,11 +807,11 @@ void validateUnlocks(const GameConfig& cfg)
|
||||
{
|
||||
if (valid.count(id) == 0)
|
||||
{
|
||||
throw utility::makeError(file, gPath, "grants unknown " + kind + " '" + id + "'");
|
||||
throw makeError(file, gPath, "grants unknown " + kind + " '" + id + "'");
|
||||
}
|
||||
if (!granted.insert(id).second)
|
||||
{
|
||||
throw utility::makeError(file, gPath,
|
||||
throw makeError(file, gPath,
|
||||
"grants " + kind + " '" + id + "' which is already granted by another unlock group");
|
||||
}
|
||||
}
|
||||
@@ -61,13 +822,13 @@ void validateUnlocks(const GameConfig& cfg)
|
||||
const std::string gPath = "unlock '" + group.id + "'";
|
||||
if (!groupIds.insert(group.id).second)
|
||||
{
|
||||
throw utility::makeError(file, gPath, "duplicate unlock group id");
|
||||
throw makeError(file, gPath, "duplicate unlock group id");
|
||||
}
|
||||
|
||||
if (group.ships.empty() && group.modules.empty()
|
||||
&& group.buildings.empty() && group.recipes.empty())
|
||||
{
|
||||
throw utility::makeError(file, gPath, "grants no items (must grant at least one)");
|
||||
throw makeError(file, gPath, "grants no items (must grant at least one)");
|
||||
}
|
||||
|
||||
checkGrants(group.ships, shipIds, grantedShipIds, "ship", gPath);
|
||||
@@ -83,7 +844,7 @@ void validateUnlocks(const GameConfig& cfg)
|
||||
{
|
||||
if (groupIds.count(req) == 0)
|
||||
{
|
||||
throw utility::makeError(file, "unlock '" + group.id + "'.requires",
|
||||
throw makeError(file, "unlock '" + group.id + "'.requires",
|
||||
"references unknown unlock group '" + req + "'");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
#include "ConfigLoader.h"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "toml.hpp"
|
||||
|
||||
#include "TomlHelpers.h"
|
||||
|
||||
BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
|
||||
{
|
||||
const std::string file = "buildings.toml";
|
||||
toml::table tbl = utility::parseFile(path, file);
|
||||
|
||||
BuildingsConfig cfg;
|
||||
const toml::array& arr = utility::requireArray(tbl["building"], file, "building");
|
||||
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = "building[" + std::to_string(i) + "]";
|
||||
const toml::table* bt = arr[i].as_table();
|
||||
if (bt == nullptr)
|
||||
{
|
||||
throw utility::makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*bt);
|
||||
|
||||
BuildingDef def;
|
||||
def.id = utility::requireString(mt["id"], file, elemPath + ".id");
|
||||
def.cost = static_cast<int>(utility::requireInt(mt["cost"], file, elemPath + ".cost"));
|
||||
def.playerPlaceable = utility::requireBool(mt["player_placeable"], file, elemPath + ".player_placeable");
|
||||
def.constructionTimeSeconds = utility::requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
|
||||
def.surfaceMask = utility::requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||
|
||||
if (mt.contains("output_buffer_capacity"))
|
||||
{
|
||||
def.outputBufferCapacity = static_cast<int>(
|
||||
utility::requireInt(mt["output_buffer_capacity"], file, elemPath + ".output_buffer_capacity"));
|
||||
}
|
||||
|
||||
if (mt.contains("tooltip"))
|
||||
{
|
||||
def.tooltip = utility::requireString(mt["tooltip"], file, elemPath + ".tooltip");
|
||||
}
|
||||
|
||||
const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
|
||||
if (!parsedType)
|
||||
{
|
||||
throw utility::makeError(file, elemPath + ".id", "unknown building id '" + def.id + "'");
|
||||
}
|
||||
def.type = *parsedType;
|
||||
|
||||
cfg.buildings.push_back(std::move(def));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
#include "ConfigLoader.h"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "toml.hpp"
|
||||
|
||||
#include "TomlHelpers.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Known category→stat mappings for module stat modifier discovery.
|
||||
// addedKeySuffix: unit suffix appended before "_formula" for additive modifier keys only.
|
||||
// Multiplicative modifier keys are always dimensionless and carry no suffix.
|
||||
struct StatEntry
|
||||
{
|
||||
const char* category;
|
||||
const char* stat;
|
||||
const char* addedKeySuffix;
|
||||
};
|
||||
|
||||
static const StatEntry kKnownStats[] = {
|
||||
{"health", "hp", ""},
|
||||
{"movement", "speed", "_mps"},
|
||||
{"movement", "main_acceleration", "_mpss"},
|
||||
{"movement", "maneuvering_acceleration", "_mpss"},
|
||||
{"sensor", "sensor_range", "_m"},
|
||||
{"weapon", "damage", ""},
|
||||
{"weapon", "attack_range", "_m"},
|
||||
{"weapon", "attack_rate", "_hz"},
|
||||
{"salvage", "collection_range", "_m"},
|
||||
{"salvage", "collection_rate", "_hz"},
|
||||
{"cargo", "cargo_capacity", ""},
|
||||
{"repair", "repair_rate", "_hz"},
|
||||
{"repair", "repair_range", "_m"},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||
{
|
||||
const std::string file = "modules.toml";
|
||||
toml::table tbl = utility::parseFile(path, file);
|
||||
|
||||
ModulesConfig cfg;
|
||||
|
||||
if (!tbl.contains("module"))
|
||||
{
|
||||
return cfg;
|
||||
}
|
||||
|
||||
const toml::array& arr = utility::requireArray(tbl["module"], file, "module");
|
||||
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = "module[" + std::to_string(i) + "]";
|
||||
const toml::table* st = arr[i].as_table();
|
||||
if (st == nullptr)
|
||||
{
|
||||
throw utility::makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*st);
|
||||
|
||||
ModuleDef def;
|
||||
def.id = utility::requireString(mt["id"], file, elemPath + ".id");
|
||||
def.surfaceMask = utility::requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||
def.productionTimeSeconds = utility::requireDouble(
|
||||
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
||||
def.fillColor = utility::requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
||||
def.glyph = utility::requireString(mt["glyph"], file, elemPath + ".glyph");
|
||||
|
||||
if (mt.contains("tooltip"))
|
||||
{
|
||||
def.tooltip = utility::requireString(mt["tooltip"], file, elemPath + ".tooltip");
|
||||
}
|
||||
|
||||
// Materials
|
||||
{
|
||||
const toml::array& materials = utility::requireArray(mt["materials"], file, elemPath + ".materials");
|
||||
def.materials = utility::parseIngredients(materials, file, elemPath + ".materials");
|
||||
}
|
||||
|
||||
// Stat modifiers from [module.<category>] sub-tables
|
||||
for (const StatEntry& se : kKnownStats)
|
||||
{
|
||||
if (!mt.contains(se.category))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const toml::table& catTable = utility::requireTable(mt[se.category], file,
|
||||
elemPath + "." + se.category);
|
||||
toml::table& catMt = const_cast<toml::table&>(catTable);
|
||||
|
||||
const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix;
|
||||
const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix;
|
||||
|
||||
if (catMt.contains(addedKey))
|
||||
{
|
||||
ModuleStatModifier mod;
|
||||
mod.stat = se.stat;
|
||||
mod.modifierType = "additive";
|
||||
mod.value = utility::requireDouble(catMt[addedKey], file,
|
||||
elemPath + "." + se.category + "." + addedKey);
|
||||
def.statModifiers.push_back(std::move(mod));
|
||||
}
|
||||
|
||||
if (catMt.contains(multipliedKey))
|
||||
{
|
||||
ModuleStatModifier mod;
|
||||
mod.stat = se.stat;
|
||||
mod.modifierType = "multiplicative";
|
||||
mod.value = utility::requireDouble(catMt[multipliedKey], file,
|
||||
elemPath + "." + se.category + "." + multipliedKey);
|
||||
def.statModifiers.push_back(std::move(mod));
|
||||
}
|
||||
}
|
||||
|
||||
// Weapon capability section: [module.weapon] with base stat formulas
|
||||
if (mt.contains("weapon"))
|
||||
{
|
||||
const std::string wPath = elemPath + ".weapon";
|
||||
const toml::table& wTable = utility::requireTable(mt["weapon"], file, wPath);
|
||||
toml::table& wMt = const_cast<toml::table&>(wTable);
|
||||
if (wMt.contains("damage") || wMt.contains("attack_range_m")
|
||||
|| wMt.contains("attack_rate_hz"))
|
||||
{
|
||||
ModuleWeaponCapability cap;
|
||||
cap.damage = static_cast<float>(utility::requireDouble(wMt["damage"],
|
||||
file, wPath + ".damage"));
|
||||
cap.attackRange_m = static_cast<float>(utility::requireDouble(wMt["attack_range_m"],
|
||||
file, wPath + ".attack_range_m"));
|
||||
cap.attackRate_hz = static_cast<float>(utility::requireDouble(wMt["attack_rate_hz"],
|
||||
file, wPath + ".attack_rate_hz"));
|
||||
def.weaponCapability = std::move(cap);
|
||||
}
|
||||
}
|
||||
|
||||
// Salvage capability section: [module.salvage] with base stat formulas
|
||||
if (mt.contains("salvage"))
|
||||
{
|
||||
const std::string sPath = elemPath + ".salvage";
|
||||
const toml::table& sTable = utility::requireTable(mt["salvage"], file, sPath);
|
||||
toml::table& sMt = const_cast<toml::table&>(sTable);
|
||||
if (sMt.contains("collection_range_m") || sMt.contains("cargo_capacity")
|
||||
|| sMt.contains("collection_rate_hz"))
|
||||
{
|
||||
ModuleSalvageCapability cap;
|
||||
cap.collectionRange_m = static_cast<float>(utility::requireDouble(sMt["collection_range_m"],
|
||||
file, sPath + ".collection_range_m"));
|
||||
cap.cargoCapacity = static_cast<float>(utility::requireDouble(sMt["cargo_capacity"],
|
||||
file, sPath + ".cargo_capacity"));
|
||||
cap.collectionRate_hz = static_cast<float>(utility::requireDouble(sMt["collection_rate_hz"],
|
||||
file, sPath + ".collection_rate_hz"));
|
||||
def.salvageCapability = std::move(cap);
|
||||
}
|
||||
}
|
||||
|
||||
// Repair capability section: [module.repair] with base stat formulas
|
||||
if (mt.contains("repair"))
|
||||
{
|
||||
const std::string rPath = elemPath + ".repair";
|
||||
const toml::table& rTable = utility::requireTable(mt["repair"], file, rPath);
|
||||
toml::table& rMt = const_cast<toml::table&>(rTable);
|
||||
if (rMt.contains("repair_rate_hz") || rMt.contains("repair_range_m"))
|
||||
{
|
||||
ModuleRepairCapability cap;
|
||||
cap.repairRate_hz = static_cast<float>(utility::requireDouble(rMt["repair_rate_hz"],
|
||||
file, rPath + ".repair_rate_hz"));
|
||||
cap.repairAmountHp = static_cast<float>(utility::requireDouble(rMt["repair_amount_hp"],
|
||||
file, rPath + ".repair_amount_hp"));
|
||||
cap.repairRange_m = static_cast<float>(utility::requireDouble(rMt["repair_range_m"],
|
||||
file, rPath + ".repair_range_m"));
|
||||
def.repairCapability = std::move(cap);
|
||||
}
|
||||
}
|
||||
|
||||
cfg.modules.push_back(std::move(def));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
#include "ConfigLoader.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "toml.hpp"
|
||||
|
||||
#include "TomlHelpers.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::vector<RecipeOutput> parseRecipeOutputs(const toml::array& arr,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
std::vector<RecipeOutput> result;
|
||||
result.reserve(arr.size());
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||
const toml::table* t = arr[i].as_table();
|
||||
if (t == nullptr)
|
||||
{
|
||||
throw utility::makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*t);
|
||||
|
||||
RecipeOutput out;
|
||||
out.item = utility::requireString(mt["item"], file, elemPath + ".item");
|
||||
out.amount = static_cast<int>(utility::requireInt(mt["amount"], file, elemPath + ".amount"));
|
||||
if (const std::optional<double> p = mt["probability"].value<double>())
|
||||
{
|
||||
out.probability = *p;
|
||||
}
|
||||
else if (const std::optional<int64_t> p = mt["probability"].value<int64_t>())
|
||||
{
|
||||
out.probability = static_cast<double>(*p);
|
||||
}
|
||||
result.push_back(std::move(out));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
|
||||
{
|
||||
const std::string file = "recipes.toml";
|
||||
toml::table tbl = utility::parseFile(path, file);
|
||||
|
||||
RecipesConfig cfg;
|
||||
const toml::array& arr = utility::requireArray(tbl["recipe"], file, "recipe");
|
||||
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = "recipe[" + std::to_string(i) + "]";
|
||||
const toml::table* rt = arr[i].as_table();
|
||||
if (rt == nullptr)
|
||||
{
|
||||
throw utility::makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*rt);
|
||||
|
||||
RecipeDef def;
|
||||
def.id = utility::requireString(mt["id"], file, elemPath + ".id");
|
||||
def.durationSeconds = utility::requireDouble(mt["duration_seconds"], file, elemPath + ".duration_seconds");
|
||||
|
||||
const std::string buildingId = utility::requireString(mt["building"], file, elemPath + ".building");
|
||||
const std::optional<BuildingType> parsedType = parseBuildingType(buildingId);
|
||||
if (!parsedType)
|
||||
{
|
||||
throw utility::makeError(file, elemPath + ".building",
|
||||
"unknown building id '" + buildingId + "'");
|
||||
}
|
||||
def.building = *parsedType;
|
||||
|
||||
if (def.building == BuildingType::Assembler && mt.contains("unlocked_at_start"))
|
||||
{
|
||||
def.unlockedAtStart = utility::requireBool(mt["unlocked_at_start"], file,
|
||||
elemPath + ".unlocked_at_start");
|
||||
}
|
||||
|
||||
// inputs may be omitted (e.g. miner recipes). An empty array is fine.
|
||||
if (mt.contains("inputs"))
|
||||
{
|
||||
const toml::array& inputs = utility::requireArray(mt["inputs"], file, elemPath + ".inputs");
|
||||
def.inputs = utility::parseIngredients(inputs, file, elemPath + ".inputs");
|
||||
}
|
||||
|
||||
const toml::array& outputs = utility::requireArray(mt["outputs"], file, elemPath + ".outputs");
|
||||
def.outputs = parseRecipeOutputs(outputs, file, elemPath + ".outputs");
|
||||
|
||||
// Optional icon item id (REQ-UI-RECIPE-ICON); defaults to the first output
|
||||
// in the UI when unset. Not validated against known items here — a missing
|
||||
// icon is not an error (REQ-UI-ITEM-ICON).
|
||||
if (mt.contains("icon"))
|
||||
{
|
||||
def.icon = utility::requireString(mt["icon"], file, elemPath + ".icon");
|
||||
}
|
||||
|
||||
cfg.recipes.push_back(std::move(def));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
#include "ConfigLoader.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QPoint>
|
||||
|
||||
#include "toml.hpp"
|
||||
|
||||
#include "Rotation.h"
|
||||
#include "ShipLayout.h"
|
||||
#include "TomlHelpers.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
Rotation parseRotationString(const std::string& s)
|
||||
{
|
||||
if (s == "east") { return Rotation::East; }
|
||||
if (s == "south") { return Rotation::South; }
|
||||
if (s == "west") { return Rotation::West; }
|
||||
return Rotation::North;
|
||||
}
|
||||
|
||||
std::vector<PlacedModule> parsePlacedModules(const toml::array& arr,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
std::vector<PlacedModule> result;
|
||||
result.reserve(arr.size());
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||
const toml::table* t = arr[i].as_table();
|
||||
if (t == nullptr) { continue; }
|
||||
toml::table& mt = const_cast<toml::table&>(*t);
|
||||
|
||||
const std::optional<std::string> type = mt["type"].value<std::string>();
|
||||
const std::optional<int64_t> x = mt["x"].value<int64_t>();
|
||||
const std::optional<int64_t> y = mt["y"].value<int64_t>();
|
||||
const std::optional<std::string> rot = mt["rotation"].value<std::string>();
|
||||
if (!type || !x || !y || !rot) { continue; }
|
||||
|
||||
PlacedModule pm;
|
||||
pm.moduleId = *type;
|
||||
pm.position = QPoint(static_cast<int>(*x), static_cast<int>(*y));
|
||||
pm.rotation = parseRotationString(*rot);
|
||||
result.push_back(std::move(pm));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
{
|
||||
const std::string file = "ships.toml";
|
||||
toml::table tbl = utility::parseFile(path, file);
|
||||
|
||||
ShipsConfig cfg;
|
||||
const toml::array& arr = utility::requireArray(tbl["ship"], file, "ship");
|
||||
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = "ship[" + std::to_string(i) + "]";
|
||||
const toml::table* st = arr[i].as_table();
|
||||
if (st == nullptr)
|
||||
{
|
||||
throw utility::makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*st);
|
||||
|
||||
ShipDef def;
|
||||
def.id = utility::requireString(mt["id"], file, elemPath + ".id");
|
||||
def.layout = utility::requireStringArray(mt["layout"], file, elemPath + ".layout");
|
||||
|
||||
// Schematic
|
||||
{
|
||||
const std::string bpPath = elemPath + ".schematic";
|
||||
const toml::table& bpTable = utility::requireTable(mt["schematic"], file, bpPath);
|
||||
toml::table& bpMt = const_cast<toml::table&>(bpTable);
|
||||
|
||||
const toml::array& materials = utility::requireArray(bpMt["materials"], file, bpPath + ".materials");
|
||||
def.schematic.materials = utility::parseIngredients(materials, file, bpPath + ".materials");
|
||||
def.schematic.productionTimeSeconds = utility::requireDouble(
|
||||
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
|
||||
}
|
||||
|
||||
// Health
|
||||
{
|
||||
const std::string hPath = elemPath + ".health";
|
||||
const toml::table& hTable = utility::requireTable(mt["health"], file, hPath);
|
||||
toml::table& hMt = const_cast<toml::table&>(hTable);
|
||||
def.health.hp = static_cast<float>(utility::requireDouble(hMt["hp"], file, hPath + ".hp"));
|
||||
}
|
||||
|
||||
// Movement
|
||||
{
|
||||
const std::string mPath = elemPath + ".movement";
|
||||
const toml::table& mTable = utility::requireTable(mt["movement"], file, mPath);
|
||||
toml::table& mMt = const_cast<toml::table&>(mTable);
|
||||
def.movement.speed_mps = static_cast<float>(utility::requireDouble(mMt["speed_mps"], file, mPath + ".speed_mps"));
|
||||
def.movement.mainAcceleration_mpss = static_cast<float>(utility::requireDouble(mMt["main_acceleration_mpss"], file, mPath + ".main_acceleration_mpss"));
|
||||
def.movement.maneuveringAcceleration_mpss = static_cast<float>(utility::requireDouble(mMt["maneuvering_acceleration_mpss"], file, mPath + ".maneuvering_acceleration_mpss"));
|
||||
def.movement.angularAcceleration_radpss = static_cast<float>(utility::requireDouble(mMt["angular_acceleration_radpss"], file, mPath + ".angular_acceleration_radpss"));
|
||||
def.movement.maxRotationSpeed_radps = static_cast<float>(utility::requireDouble(mMt["max_rotation_speed_radps"], file, mPath + ".max_rotation_speed_radps"));
|
||||
}
|
||||
|
||||
// Sensor
|
||||
{
|
||||
const std::string snsPath = elemPath + ".sensor";
|
||||
const toml::table& snsTable = utility::requireTable(mt["sensor"], file, snsPath);
|
||||
toml::table& snsMt = const_cast<toml::table&>(snsTable);
|
||||
def.sensor.sensorRange_m = static_cast<float>(utility::requireDouble(snsMt["sensor_range_m"], file, snsPath + ".sensor_range_m"));
|
||||
}
|
||||
|
||||
// Optional: default_modules (REQ-WAV-DEFAULT-MODULES)
|
||||
if (mt.contains("default_modules"))
|
||||
{
|
||||
const toml::array& modArr = utility::requireArray(mt["default_modules"], file,
|
||||
elemPath + ".default_modules");
|
||||
def.defaultModules = parsePlacedModules(modArr, file,
|
||||
elemPath + ".default_modules");
|
||||
}
|
||||
|
||||
cfg.ships.push_back(std::move(def));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
#include "ConfigLoader.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "toml.hpp"
|
||||
|
||||
#include "TomlHelpers.h"
|
||||
|
||||
StationsConfig ConfigLoader::loadStations(const std::string& path)
|
||||
{
|
||||
const std::string file = "stations.toml";
|
||||
toml::table tbl = utility::parseFile(path, file);
|
||||
|
||||
StationsConfig cfg;
|
||||
|
||||
// HQ
|
||||
{
|
||||
const std::string p = "hq";
|
||||
cfg.hq.surfaceMask = utility::requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
||||
cfg.hq.hpFormula = utility::requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||
}
|
||||
|
||||
// Player station
|
||||
{
|
||||
const std::string p = "player_station";
|
||||
cfg.playerStation.surfaceMask = utility::requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
||||
cfg.playerStation.level = static_cast<int>(utility::requireInt(tbl[p]["level"], file, p + ".level"));
|
||||
cfg.playerStation.hpFormula = utility::requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||
cfg.playerStation.damageFormula = utility::requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
||||
cfg.playerStation.rangeFormula = utility::requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
|
||||
cfg.playerStation.fireRateFormula = utility::requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
|
||||
cfg.playerStation.scrapDropFormula = utility::requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
||||
}
|
||||
|
||||
// Enemy station
|
||||
{
|
||||
const std::string p = "enemy_station";
|
||||
cfg.enemyStation.surfaceMask = utility::requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
||||
cfg.enemyStation.hpFormula = utility::requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||
cfg.enemyStation.damageFormula = utility::requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
||||
cfg.enemyStation.rangeFormula = utility::requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
|
||||
cfg.enemyStation.fireRateFormula = utility::requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
|
||||
cfg.enemyStation.scrapDropFormula = utility::requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
#include "ConfigLoader.h"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "toml.hpp"
|
||||
|
||||
#include "TomlHelpers.h"
|
||||
|
||||
UnlocksConfig ConfigLoader::loadUnlocks(const std::string& path)
|
||||
{
|
||||
const std::string file = "unlocks.toml";
|
||||
toml::table tbl = utility::parseFile(path, file);
|
||||
|
||||
UnlocksConfig cfg;
|
||||
if (!tbl.contains("unlock"))
|
||||
{
|
||||
return cfg;
|
||||
}
|
||||
|
||||
const toml::array& arr = utility::requireArray(tbl["unlock"], file, "unlock");
|
||||
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = "unlock[" + std::to_string(i) + "]";
|
||||
const toml::table* ut = arr[i].as_table();
|
||||
if (ut == nullptr)
|
||||
{
|
||||
throw utility::makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*ut);
|
||||
|
||||
UnlockGroupDef def;
|
||||
def.id = utility::requireString(mt["id"], file, elemPath + ".id");
|
||||
def.stationLevel = static_cast<int>(
|
||||
utility::requireInt(mt["station_level"], file, elemPath + ".station_level"));
|
||||
if (mt.contains("requires"))
|
||||
{
|
||||
def.requiredGroupIds = utility::requireStringArray(mt["requires"], file, elemPath + ".requires");
|
||||
}
|
||||
if (mt.contains("ships"))
|
||||
{
|
||||
def.ships = utility::requireStringArray(mt["ships"], file, elemPath + ".ships");
|
||||
}
|
||||
if (mt.contains("modules"))
|
||||
{
|
||||
def.modules = utility::requireStringArray(mt["modules"], file, elemPath + ".modules");
|
||||
}
|
||||
if (mt.contains("buildings"))
|
||||
{
|
||||
def.buildings = utility::requireStringArray(mt["buildings"], file, elemPath + ".buildings");
|
||||
}
|
||||
if (mt.contains("recipes"))
|
||||
{
|
||||
def.recipes = utility::requireStringArray(mt["recipes"], file, elemPath + ".recipes");
|
||||
}
|
||||
|
||||
cfg.groups.push_back(std::move(def));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
#include "ConfigLoader.h"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "toml.hpp"
|
||||
|
||||
#include "TomlHelpers.h"
|
||||
|
||||
WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
||||
{
|
||||
const std::string file = "world.toml";
|
||||
toml::table tbl = utility::parseFile(path, file);
|
||||
|
||||
WorldConfig cfg;
|
||||
|
||||
cfg.heightTiles = static_cast<int>(utility::requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles"));
|
||||
cfg.refundPercentage = static_cast<int>(utility::requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
|
||||
cfg.deconstructionTimeSeconds = utility::requireDouble(tbl["world"]["deconstruction_time_seconds"], file, "world.deconstruction_time_seconds");
|
||||
cfg.startingBuildingBlocks = static_cast<int>(utility::requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks"));
|
||||
cfg.debrisDespawnSeconds = utility::requireDouble(tbl["world"]["debris_despawn_seconds"], file, "world.debris_despawn_seconds");
|
||||
cfg.scrapPerThreat = utility::requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat");
|
||||
cfg.tileSize_m = utility::requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m");
|
||||
cfg.beltSpeed_tps = utility::requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m;
|
||||
cfg.tunnelMaxDistance_tiles = static_cast<int>(utility::requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles"));
|
||||
cfg.departureIntervalSeconds = utility::requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds");
|
||||
cfg.orbitFactor = utility::requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor");
|
||||
cfg.rallyOrbitRadius_tiles = utility::requireDouble(tbl["world"]["rally_orbit_radius_tiles"], file, "world.rally_orbit_radius_tiles");
|
||||
|
||||
if (const std::optional<std::string> tip =
|
||||
tbl["world"]["building_blocks_tooltip"].value<std::string>())
|
||||
{
|
||||
cfg.buildingBlocksTooltip = *tip;
|
||||
}
|
||||
|
||||
if (const std::optional<std::string> tip =
|
||||
tbl["world"]["artifact_tooltip"].value<std::string>())
|
||||
{
|
||||
cfg.artifactTooltip = *tip;
|
||||
}
|
||||
|
||||
cfg.regions.asteroidWidth_tiles = static_cast<int>(utility::requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles"));
|
||||
cfg.regions.playerBufferWidth_tiles = static_cast<int>(utility::requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles"));
|
||||
cfg.regions.contestZoneWidth_tiles = static_cast<int>(utility::requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles"));
|
||||
cfg.regions.enemyBufferWidth_tiles = static_cast<int>(utility::requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles"));
|
||||
|
||||
cfg.expansion.columnsPerExpansion_tiles = static_cast<int>(utility::requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles"));
|
||||
cfg.expansion.costBuildingBlocksFormula = utility::requireFormula(tbl["expansion"]["cost_building_blocks_formula"], file, "expansion.cost_building_blocks_formula");
|
||||
|
||||
cfg.push.pushExpandColumns_tiles = static_cast<int>(utility::requireInt(tbl["push"]["push_expand_columns_tiles"], file, "push.push_expand_columns_tiles"));
|
||||
cfg.push.bossAdvanceSeconds = utility::requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds");
|
||||
|
||||
cfg.waves.threatRateFormula = utility::requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula");
|
||||
cfg.waves.gapMinSeconds = utility::requireDouble(tbl["waves"]["gap_min_seconds"], file, "waves.gap_min_seconds");
|
||||
cfg.waves.gapMaxSeconds = utility::requireDouble(tbl["waves"]["gap_max_seconds"], file, "waves.gap_max_seconds");
|
||||
cfg.waves.spawnDurationSeconds = utility::requireDouble(tbl["waves"]["spawn_duration_seconds"], file, "waves.spawn_duration_seconds");
|
||||
cfg.waves.bossCountdownSeconds = utility::requireDouble(tbl["waves"]["boss_countdown_seconds"], file, "waves.boss_countdown_seconds");
|
||||
cfg.waves.bossThreatDurationSeconds = utility::requireDouble(tbl["waves"]["boss_threat_duration_seconds"], file, "waves.boss_threat_duration_seconds");
|
||||
cfg.waves.bossQuietBeforeSeconds = utility::requireDouble(tbl["waves"]["boss_quiet_before_seconds"], file, "waves.boss_quiet_before_seconds");
|
||||
cfg.waves.bossQuietAfterSeconds = utility::requireDouble(tbl["waves"]["boss_quiet_after_seconds"], file, "waves.boss_quiet_after_seconds");
|
||||
|
||||
if (cfg.waves.gapMinSeconds > cfg.waves.gapMaxSeconds)
|
||||
{
|
||||
throw utility::makeError(file, "waves", "gap_min_seconds > gap_max_seconds");
|
||||
}
|
||||
|
||||
cfg.targeting.targetScoreFormula = utility::requireFormula(tbl["targeting"]["target_score_formula"], file, "targeting.target_score_formula");
|
||||
cfg.targeting.overclaimPenaltyFormula = utility::requireFormula(tbl["targeting"]["overclaim_penalty_formula"], file, "targeting.overclaim_penalty_formula");
|
||||
cfg.targeting.hysteresis = utility::requireDouble(tbl["targeting"]["target_hysteresis"], file, "targeting.target_hysteresis");
|
||||
|
||||
cfg.artifacts.artifactChanceFormula = utility::requireFormula(tbl["artifacts"]["artifact_chance_formula"], file, "artifacts.artifact_chance_formula");
|
||||
cfg.artifacts.artifactWinCount = static_cast<int>(utility::requireInt(tbl["artifacts"]["artifact_win_count"], file, "artifacts.artifact_win_count"));
|
||||
|
||||
cfg.scroll.panSpeedSlow_tps = utility::requireDouble(tbl["scroll"]["pan_speed_slow_tiles_per_second"], file, "scroll.pan_speed_slow_tiles_per_second");
|
||||
cfg.scroll.panSpeedFast_tps = utility::requireDouble(tbl["scroll"]["pan_speed_fast_tiles_per_second"], file, "scroll.pan_speed_fast_tiles_per_second");
|
||||
cfg.scroll.panRampBandWidth_tiles = static_cast<int>(utility::requireInt(tbl["scroll"]["pan_ramp_band_width_tiles"], file, "scroll.pan_ramp_band_width_tiles"));
|
||||
|
||||
return cfg;
|
||||
}
|
||||
@@ -59,18 +59,4 @@ struct ModuleDef
|
||||
struct ModulesConfig
|
||||
{
|
||||
std::vector<ModuleDef> modules;
|
||||
|
||||
// Returns the definition for the given module id, or nullptr if the id has
|
||||
// no entry in modules.toml.
|
||||
const ModuleDef* findModuleDef(const std::string& id) const
|
||||
{
|
||||
for (const ModuleDef& def : modules)
|
||||
{
|
||||
if (def.id == id)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,10 +32,6 @@ struct RecipeDef
|
||||
std::vector<RecipeIngredient> inputs;
|
||||
std::vector<RecipeOutput> outputs;
|
||||
double durationSeconds;
|
||||
// Optional id of the item whose icon represents this recipe in the recipe-
|
||||
// selection dialog (REQ-UI-RECIPE-ICON). When unset, the first output item is
|
||||
// used. A missing icon file for that item is not an error (REQ-UI-ITEM-ICON).
|
||||
std::optional<std::string> icon;
|
||||
// Assembler only. When true, this recipe is available from game start
|
||||
// regardless of the implicit item graph — used for base recipes that no
|
||||
// schematic's materials reach (e.g. building blocks). See REQ-LOCK-IMPLICIT.
|
||||
@@ -47,32 +43,4 @@ struct RecipeDef
|
||||
struct RecipesConfig
|
||||
{
|
||||
std::vector<RecipeDef> recipes;
|
||||
|
||||
// Returns the definition for the given recipe id, or nullptr if the id has
|
||||
// no entry in recipes.toml.
|
||||
const RecipeDef* findRecipeDef(const std::string& id) const
|
||||
{
|
||||
for (const RecipeDef& recipe : recipes)
|
||||
{
|
||||
if (recipe.id == id)
|
||||
{
|
||||
return &recipe;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Same, but additionally requires the recipe to belong to the given building
|
||||
// type — recipe ids are only unique per building type.
|
||||
const RecipeDef* findRecipeDef(const std::string& id, BuildingType building) const
|
||||
{
|
||||
for (const RecipeDef& recipe : recipes)
|
||||
{
|
||||
if (recipe.id == id && recipe.building == building)
|
||||
{
|
||||
return &recipe;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,18 +49,4 @@ struct ShipDef
|
||||
struct ShipsConfig
|
||||
{
|
||||
std::vector<ShipDef> ships;
|
||||
|
||||
// Returns the definition for the given ship schematic id, or nullptr if the
|
||||
// id has no entry in ships.toml.
|
||||
const ShipDef* findShipDef(const std::string& id) const
|
||||
{
|
||||
for (const ShipDef& def : ships)
|
||||
{
|
||||
if (def.id == id)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
#include "TomlHelpers.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace utility
|
||||
{
|
||||
|
||||
// --- Error helpers --------------------------------------------------------
|
||||
|
||||
std::runtime_error makeError(const std::string& file,
|
||||
const std::string& path,
|
||||
const std::string& why)
|
||||
{
|
||||
return std::runtime_error("Config: " + file + ": '" + path + "' " + why);
|
||||
}
|
||||
|
||||
// --- Typed accessors (throw on missing or wrong type) ---------------------
|
||||
|
||||
int64_t requireInt(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const std::optional<int64_t> value = node.value<int64_t>();
|
||||
if (!value)
|
||||
{
|
||||
throw makeError(file, path, "missing or not an integer");
|
||||
}
|
||||
return *value;
|
||||
}
|
||||
|
||||
double requireDouble(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
if (const std::optional<double> v = node.value<double>())
|
||||
{
|
||||
return *v;
|
||||
}
|
||||
if (const std::optional<int64_t> v = node.value<int64_t>())
|
||||
{
|
||||
return static_cast<double>(*v);
|
||||
}
|
||||
throw makeError(file, path, "missing or not a number");
|
||||
}
|
||||
|
||||
std::string requireString(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const std::optional<std::string> value = node.value<std::string>();
|
||||
if (!value)
|
||||
{
|
||||
throw makeError(file, path, "missing or not a string");
|
||||
}
|
||||
return *value;
|
||||
}
|
||||
|
||||
bool requireBool(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const std::optional<bool> value = node.value<bool>();
|
||||
if (!value)
|
||||
{
|
||||
throw makeError(file, path, "missing or not a boolean");
|
||||
}
|
||||
return *value;
|
||||
}
|
||||
|
||||
const toml::array& requireArray(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const toml::array* arr = node.as_array();
|
||||
if (arr == nullptr)
|
||||
{
|
||||
throw makeError(file, path, "missing or not an array");
|
||||
}
|
||||
return *arr;
|
||||
}
|
||||
|
||||
const toml::table& requireTable(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const toml::table* tbl = node.as_table();
|
||||
if (tbl == nullptr)
|
||||
{
|
||||
throw makeError(file, path, "missing or not a table");
|
||||
}
|
||||
return *tbl;
|
||||
}
|
||||
|
||||
Formula requireFormula(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const std::string source = requireString(node, file, path);
|
||||
try
|
||||
{
|
||||
return Formula::compile(source);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
throw makeError(file, path, std::string("formula error: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> requireStringArray(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const toml::array& arr = requireArray(node, file, path);
|
||||
std::vector<std::string> result;
|
||||
result.reserve(arr.size());
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||
const std::optional<std::string> s = arr[i].value<std::string>();
|
||||
if (!s)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a string");
|
||||
}
|
||||
result.push_back(*s);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<RecipeIngredient> parseIngredients(const toml::array& arr,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
std::vector<RecipeIngredient> result;
|
||||
result.reserve(arr.size());
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||
const toml::table* t = arr[i].as_table();
|
||||
if (t == nullptr)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a table");
|
||||
}
|
||||
|
||||
// We need a mutable node_view to reuse our helpers, which is fine
|
||||
// because the helpers never mutate.
|
||||
toml::table& mt = const_cast<toml::table&>(*t);
|
||||
|
||||
RecipeIngredient ing;
|
||||
ing.item = requireString(mt["item"], file, elemPath + ".item");
|
||||
ing.amount = static_cast<int>(requireInt(mt["amount"], file, elemPath + ".amount"));
|
||||
result.push_back(std::move(ing));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
toml::table parseFile(const std::string& path, const std::string& file)
|
||||
{
|
||||
try
|
||||
{
|
||||
return toml::parse_file(path);
|
||||
}
|
||||
catch (const toml::parse_error& e)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "Config: " << file << ": TOML parse error: " << e.description()
|
||||
<< " at " << e.source().begin;
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace utility
|
||||
@@ -1,70 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "toml.hpp"
|
||||
|
||||
#include "Formula.h"
|
||||
#include "RecipesConfig.h" // for RecipeIngredient
|
||||
|
||||
// Shared TOML-parsing helpers used by two or more ConfigLoader per-domain
|
||||
// loaders. Helpers used by exactly one domain stay local to that domain's
|
||||
// .cpp file instead.
|
||||
//
|
||||
// Namespaced because the names are generic: VisualsLoader.cpp and
|
||||
// BalancingConfig.cpp each have their own same-named helpers in anonymous
|
||||
// namespaces, and unqualified globals here would form an overload set with
|
||||
// them the moment either file includes this header.
|
||||
namespace utility
|
||||
{
|
||||
|
||||
// --- Error helpers ----------------------------------------------------------
|
||||
|
||||
std::runtime_error makeError(const std::string& file,
|
||||
const std::string& path,
|
||||
const std::string& why);
|
||||
|
||||
// --- Typed accessors (throw on missing or wrong type) -----------------------
|
||||
|
||||
int64_t requireInt(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path);
|
||||
|
||||
double requireDouble(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path);
|
||||
|
||||
std::string requireString(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path);
|
||||
|
||||
bool requireBool(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path);
|
||||
|
||||
const toml::array& requireArray(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path);
|
||||
|
||||
const toml::table& requireTable(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path);
|
||||
|
||||
Formula requireFormula(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path);
|
||||
|
||||
std::vector<std::string> requireStringArray(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path);
|
||||
|
||||
std::vector<RecipeIngredient> parseIngredients(const toml::array& arr,
|
||||
const std::string& file,
|
||||
const std::string& path);
|
||||
|
||||
toml::table parseFile(const std::string& path, const std::string& file);
|
||||
|
||||
} // namespace utility
|
||||
@@ -70,8 +70,8 @@ struct WorldConfig
|
||||
int refundPercentage; // REQ-BLD-DECONSTRUCT
|
||||
double deconstructionTimeSeconds; // REQ-BLD-DECON-QUEUE
|
||||
int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS
|
||||
double debrisDespawnSeconds; // REQ-RES-DEBRIS-DROP
|
||||
double scrapPerThreat; // REQ-RES-DEBRIS-DROP, REQ-THREAT-SCRAP (scrap dropped per unit threat)
|
||||
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP
|
||||
double scrapPerThreat; // REQ-RES-SCRAP-DROP, REQ-THREAT-SCRAP (scrap dropped per unit threat)
|
||||
double tileSize_m; // metres per tile (REQ-GW-TILE-SIZE)
|
||||
double beltSpeed_tps; // REQ-GW-BELT-SPEED (tiles/s, converted from m/s in config)
|
||||
int tunnelMaxDistance_tiles; // REQ-BLD-TUNNEL-PAIR
|
||||
|
||||
@@ -38,32 +38,3 @@ std::string buildingTypeId(BuildingType type)
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool isAutoRecipeBuildingType(BuildingType type)
|
||||
{
|
||||
return type == BuildingType::Smelter
|
||||
|| type == BuildingType::ReprocessingPlant;
|
||||
}
|
||||
|
||||
bool isBeltSubsystemType(BuildingType type)
|
||||
{
|
||||
return type == BuildingType::Belt
|
||||
|| type == BuildingType::Splitter
|
||||
|| type == BuildingType::TunnelEntry
|
||||
|| type == BuildingType::TunnelExit;
|
||||
}
|
||||
|
||||
bool isProductionBuildingType(BuildingType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case BuildingType::Miner:
|
||||
case BuildingType::Smelter:
|
||||
case BuildingType::Assembler:
|
||||
case BuildingType::ReprocessingPlant:
|
||||
case BuildingType::Shipyard:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,17 +29,3 @@ std::optional<BuildingType> parseBuildingType(const std::string& id);
|
||||
|
||||
// Canonical id string for a BuildingType. The inverse of parseBuildingType.
|
||||
std::string buildingTypeId(BuildingType type);
|
||||
|
||||
// Smelter and Reprocessing Plant have no player-selected recipe
|
||||
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). They auto-process whatever inputs
|
||||
// they receive, matching against every recipe of their building type.
|
||||
bool isAutoRecipeBuildingType(BuildingType type);
|
||||
|
||||
// Buildings that run a production cycle: Miner, Smelter, Assembler, Reprocessing
|
||||
// Plant and Shipyard (REQ-UI-DEBUG-OVERLAY counts these).
|
||||
bool isProductionBuildingType(BuildingType type);
|
||||
|
||||
// Belts, splitters, and tunnel ends keep their runtime data in the belt subsystem
|
||||
// rather than in the Building instance, so placing/removing them must register or
|
||||
// unregister a tile with BeltSystem.
|
||||
bool isBeltSubsystemType(BuildingType type);
|
||||
|
||||
@@ -9,7 +9,6 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Item.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Port.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/PortGeometry.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h
|
||||
@@ -20,7 +19,6 @@ SET(HDRS
|
||||
SET(SRCS
|
||||
${SRCS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/PortGeometry.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include "HqProxyComponent.h"
|
||||
#include "MovementIntentComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "DebrisComponent.h"
|
||||
#include "ScrapDataComponent.h"
|
||||
#include "SensorRangeComponent.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "StationBodyComponent.h"
|
||||
@@ -45,11 +45,11 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
|
||||
const std::string& schematicId, bool isEnemy)
|
||||
{
|
||||
entt::entity entity = createEntity();
|
||||
addComponent<PositionComponent>(entity, PositionComponent{position});
|
||||
addComponent<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||
addComponent<FactionComponent>(entity, FactionComponent{isEnemy});
|
||||
addComponent<FacingComponent>(entity, FacingComponent{0.0f});
|
||||
addComponent<DynamicBodyComponent>(entity, DynamicBodyComponent{
|
||||
add<PositionComponent>(entity, PositionComponent{position});
|
||||
add<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||
add<FactionComponent>(entity, FactionComponent{isEnemy});
|
||||
add<FacingComponent>(entity, FacingComponent{0.0f});
|
||||
add<DynamicBodyComponent>(entity, DynamicBodyComponent{
|
||||
maxSpeed_tpt,
|
||||
mainAcceleration_tptt,
|
||||
maneuveringAcceleration_tptt,
|
||||
@@ -60,9 +60,9 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
|
||||
QVector2D(0.0f, 0.0f), // linearAcceleration_tptt
|
||||
0.0f // angularAcceleration_rptt
|
||||
});
|
||||
addComponent<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange_tiles});
|
||||
addComponent<ShipIdentityComponent>(entity, ShipIdentityComponent{schematicId});
|
||||
addComponent<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)});
|
||||
add<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange_tiles});
|
||||
add<ShipIdentityComponent>(entity, ShipIdentityComponent{schematicId});
|
||||
add<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)});
|
||||
return entity;
|
||||
}
|
||||
|
||||
@@ -73,28 +73,28 @@ entt::entity EntityAdmin::spawnStation(QPoint anchor, QSize footprint,
|
||||
entt::entity entity = createEntity();
|
||||
QVector2D center(anchor.x() + footprint.width() / 2.0f,
|
||||
anchor.y() + footprint.height() / 2.0f);
|
||||
addComponent<PositionComponent>(entity, PositionComponent{center});
|
||||
addComponent<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||
addComponent<FactionComponent>(entity, FactionComponent{isEnemy});
|
||||
addComponent<StationBodyComponent>(entity, StationBodyComponent{anchor, footprint, bodyCells});
|
||||
add<PositionComponent>(entity, PositionComponent{center});
|
||||
add<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||
add<FactionComponent>(entity, FactionComponent{isEnemy});
|
||||
add<StationBodyComponent>(entity, StationBodyComponent{anchor, footprint, bodyCells});
|
||||
return entity;
|
||||
}
|
||||
|
||||
entt::entity EntityAdmin::spawnDebris(QVector2D position, int amount, Tick despawnAt)
|
||||
entt::entity EntityAdmin::spawnScrap(QVector2D position, int amount, Tick despawnAt)
|
||||
{
|
||||
entt::entity entity = createEntity();
|
||||
addComponent<PositionComponent>(entity, PositionComponent{position});
|
||||
addComponent<DebrisComponent>(entity, DebrisComponent{amount});
|
||||
addComponent<DespawnAtComponent>(entity, DespawnAtComponent{despawnAt});
|
||||
add<PositionComponent>(entity, PositionComponent{position});
|
||||
add<ScrapDataComponent>(entity, ScrapDataComponent{amount});
|
||||
add<DespawnAtComponent>(entity, DespawnAtComponent{despawnAt});
|
||||
return entity;
|
||||
}
|
||||
|
||||
entt::entity EntityAdmin::spawnHqProxy(QVector2D position, float hp, float maxHp)
|
||||
{
|
||||
entt::entity entity = createEntity();
|
||||
addComponent<PositionComponent>(entity, PositionComponent{position});
|
||||
addComponent<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||
addComponent<FactionComponent>(entity, FactionComponent{false});
|
||||
addComponent<HqProxyComponent>(entity);
|
||||
add<PositionComponent>(entity, PositionComponent{position});
|
||||
add<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||
add<FactionComponent>(entity, FactionComponent{false});
|
||||
add<HqProxyComponent>(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public:
|
||||
const std::vector<QPoint>& bodyCells,
|
||||
float hp, float maxHp, bool isEnemy);
|
||||
|
||||
entt::entity spawnDebris(QVector2D position, int amount, Tick despawnAt);
|
||||
entt::entity spawnScrap(QVector2D position, int amount, Tick despawnAt);
|
||||
|
||||
entt::entity spawnHqProxy(QVector2D position, float hp, float maxHp);
|
||||
|
||||
@@ -73,6 +73,9 @@ public:
|
||||
private:
|
||||
entt::entity createEntity();
|
||||
|
||||
template <typename T, typename... Args>
|
||||
void add(entt::entity entity, Args&&... args);
|
||||
|
||||
entt::registry m_registry;
|
||||
};
|
||||
|
||||
@@ -130,4 +133,10 @@ void EntityAdmin::removeComponent(entt::entity entity)
|
||||
m_registry.remove<T>(entity);
|
||||
}
|
||||
|
||||
template <typename T, typename... Args>
|
||||
void EntityAdmin::add(entt::entity entity, Args&&... args)
|
||||
{
|
||||
m_registry.emplace<T>(entity, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
#endif // ENTITY_ADMIN_H
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#include "PortGeometry.h"
|
||||
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
std::vector<Port> computeInputPorts(
|
||||
const std::vector<QPoint>& bodyCells,
|
||||
const std::vector<Port>& outputPorts)
|
||||
{
|
||||
// Build lookup sets for quick membership checks.
|
||||
std::set<std::pair<int, int>> bodySet;
|
||||
for (const QPoint& cell : bodyCells)
|
||||
{
|
||||
bodySet.insert({cell.x(), cell.y()});
|
||||
}
|
||||
|
||||
std::set<std::pair<int, int>> outputPortTiles;
|
||||
for (const Port& port : outputPorts)
|
||||
{
|
||||
outputPortTiles.insert({port.tile.x(), port.tile.y()});
|
||||
}
|
||||
|
||||
// Neighbour deltas and the corresponding "inward" belt direction.
|
||||
const int dx[4] = {-1, 1, 0, 0};
|
||||
const int dy[4] = { 0, 0, -1, 1};
|
||||
const Rotation inward[4] = {
|
||||
Rotation::East, // neighbour is to the West; belt flows East toward building
|
||||
Rotation::West, // neighbour is to the East; belt flows West toward building
|
||||
Rotation::South, // neighbour is above (row-1); belt flows South toward building
|
||||
Rotation::North // neighbour is below (row+1); belt flows North toward building
|
||||
};
|
||||
|
||||
std::set<std::pair<int, int>> seen;
|
||||
std::vector<Port> inputPorts;
|
||||
|
||||
for (const QPoint& cell : bodyCells)
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
const int nx = cell.x() + dx[i];
|
||||
const int ny = cell.y() + dy[i];
|
||||
const std::pair<int, int> neighbor = {nx, ny};
|
||||
|
||||
if (bodySet.count(neighbor)) { continue; }
|
||||
if (outputPortTiles.count(neighbor)){ continue; }
|
||||
if (seen.count(neighbor)) { continue; }
|
||||
|
||||
seen.insert(neighbor);
|
||||
Port port;
|
||||
port.tile = QPoint(nx, ny);
|
||||
port.direction = inward[i];
|
||||
inputPorts.push_back(port);
|
||||
}
|
||||
}
|
||||
|
||||
return inputPorts;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QPoint>
|
||||
|
||||
#include "Port.h"
|
||||
#include "Rotation.h"
|
||||
|
||||
// Geometry of a building's input/output ports. A Port names the tile *outside* the
|
||||
// building together with the direction items flow across it; these helpers give the
|
||||
// building body tile on the other side of that edge, which is where the virtual
|
||||
// input/output belt lives.
|
||||
//
|
||||
// Shared by the simulation (which moves items across the edge) and the renderer
|
||||
// (which draws the virtual belt), so the two cannot disagree about which tile a
|
||||
// port belongs to.
|
||||
|
||||
// The building body tile that owns an output port, given the port's outside tile
|
||||
// (port.tile) and its facing direction. The virtual output belt occupies this tile
|
||||
// and flows toward port.tile (REQ-MAT-OUTPUT-EMERGE).
|
||||
inline QPoint outputBodyTile(QPoint portTile, Rotation direction)
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case Rotation::East: return portTile + QPoint(-1, 0);
|
||||
case Rotation::West: return portTile + QPoint( 1, 0);
|
||||
case Rotation::North: return portTile + QPoint( 0, 1);
|
||||
case Rotation::South: return portTile + QPoint( 0, -1);
|
||||
}
|
||||
return portTile;
|
||||
}
|
||||
|
||||
// The building body tile an input port feeds into, given the port's outside belt
|
||||
// tile (port.tile) and its inward flow direction. The virtual input belt occupies
|
||||
// this tile and flows from the outer edge (progress 0.0) to the centre (0.5)
|
||||
// (REQ-MAT-INPUT-INTAKE).
|
||||
inline QPoint inputBodyTile(QPoint portTile, Rotation inwardDirection)
|
||||
{
|
||||
switch (inwardDirection)
|
||||
{
|
||||
case Rotation::East: return portTile + QPoint( 1, 0);
|
||||
case Rotation::West: return portTile + QPoint(-1, 0);
|
||||
case Rotation::North: return portTile + QPoint( 0, -1);
|
||||
case Rotation::South: return portTile + QPoint( 0, 1);
|
||||
}
|
||||
return portTile;
|
||||
}
|
||||
|
||||
// Every belt-facing edge of a footprint that is not already an output port — the
|
||||
// tiles a belt can feed the building from, with the direction items must flow to
|
||||
// enter (REQ-MAT-INPUT-PORTS, REQ-BLD-BELT-DRAG). bodyCells and outputPorts are
|
||||
// in absolute tile coordinates, and so is the result.
|
||||
std::vector<Port> computeInputPorts(const std::vector<QPoint>& bodyCells,
|
||||
const std::vector<Port>& outputPorts);
|
||||
@@ -20,7 +20,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RetreatBehavior.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SalvagerComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SalvageScrapBehavior.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DebrisComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ScrapDataComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBehaviorComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SensorRangeComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipIdentityComponent.h
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// Marks a piece of debris and holds the amount of scrap it still contains
|
||||
// (REQ-RES-DEBRIS-DROP). Salvage modules collect one scrap per cycle (REQ-SHP-SALVAGE).
|
||||
struct DebrisComponent
|
||||
{
|
||||
int amount;
|
||||
};
|
||||
@@ -5,10 +5,10 @@
|
||||
#include <QVector2D>
|
||||
|
||||
// Collect-scrap behavior (one half of the old SalvageBehaviorComponent). The
|
||||
// evaluator finds the nearest debris and sets debrisTarget when cargo is not full.
|
||||
// evaluator finds the nearest scrap and sets scrapTarget when cargo is not full.
|
||||
struct SalvageScrapBehavior
|
||||
{
|
||||
std::optional<QVector2D> debrisTarget;
|
||||
std::optional<QVector2D> scrapTarget;
|
||||
float maxCollectionRange_tiles = 0.0f;
|
||||
float orbitRadius_tiles = 0.0f; // REQ-SHP-ORBIT
|
||||
float score = 0.0f;
|
||||
|
||||
6
src/lib/ecs/component/ScrapDataComponent.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
struct ScrapDataComponent
|
||||
{
|
||||
int amount;
|
||||
};
|
||||
@@ -6,6 +6,6 @@ struct ShipIdentityComponent
|
||||
{
|
||||
std::string schematicId;
|
||||
// Scrap dropped on destruction, derived from the ship's as-built threat cost
|
||||
// at spawn time (REQ-RES-DEBRIS-DROP).
|
||||
// at spawn time (REQ-RES-SCRAP-DROP).
|
||||
int scrapDrop = 0;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "AiSystem.h"
|
||||
#include "FactoryQueries.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
@@ -43,7 +42,8 @@ AiSystem::AiSystem(const GameConfig& config)
|
||||
{
|
||||
}
|
||||
|
||||
void AiSystem::tick(EntityAdmin& admin, const FactoryState& state)
|
||||
void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
|
||||
const ScrapSystem& scraps)
|
||||
{
|
||||
TRACE();
|
||||
|
||||
@@ -54,8 +54,8 @@ void AiSystem::tick(EntityAdmin& admin, const FactoryState& state)
|
||||
m_retreatEvaluator.evaluate(admin);
|
||||
m_attackEvaluator.evaluate(admin);
|
||||
m_repairEvaluator.evaluate(admin);
|
||||
m_salvageScrapEvaluator.evaluate(admin);
|
||||
m_deliverScrapEvaluator.evaluate(admin, state);
|
||||
m_salvageScrapEvaluator.evaluate(admin, scraps);
|
||||
m_deliverScrapEvaluator.evaluate(admin, buildings);
|
||||
|
||||
// Phase 2: pick the highest-scoring behavior per ship.
|
||||
selectWinningBehaviors(admin);
|
||||
@@ -68,7 +68,7 @@ void AiSystem::tick(EntityAdmin& admin, const FactoryState& state)
|
||||
m_attackExecutor.execute(admin);
|
||||
m_repairExecutor.execute(admin);
|
||||
m_salvageScrapExecutor.execute(admin);
|
||||
m_deliverScrapExecutor.execute(admin, state);
|
||||
m_deliverScrapExecutor.execute(admin, buildings);
|
||||
}
|
||||
|
||||
void AiSystem::selectWinningBehaviors(EntityAdmin& admin)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "FactoryQueries.h"
|
||||
|
||||
#include "AdvanceEvaluator.h"
|
||||
#include "AdvanceExecutor.h"
|
||||
#include "AttackEvaluator.h"
|
||||
@@ -19,7 +17,9 @@
|
||||
#include "StandbyEvaluator.h"
|
||||
#include "StandbyExecutor.h"
|
||||
|
||||
class BuildingSystem;
|
||||
class EntityAdmin;
|
||||
class ScrapSystem;
|
||||
struct GameConfig;
|
||||
|
||||
// Orchestrates ship-behavior decision-making in three batched phases:
|
||||
@@ -34,7 +34,7 @@ class AiSystem
|
||||
public:
|
||||
explicit AiSystem(const GameConfig& config);
|
||||
|
||||
void tick(EntityAdmin& admin, const FactoryState& state);
|
||||
void tick(EntityAdmin& admin, const BuildingSystem& buildings, const ScrapSystem& scraps);
|
||||
|
||||
private:
|
||||
void selectWinningBehaviors(EntityAdmin& admin);
|
||||
|
||||
@@ -5,10 +5,8 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AttackEvaluator.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AttackExecutor.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/BehaviorTargeting.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/Centroid.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapEvaluator.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapExecutor.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/OrbitAndAssignExecutor.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyEvaluator.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyExecutor.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RepairEvaluator.h
|
||||
@@ -25,7 +23,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RepairSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SalvagerSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DebrisSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -55,7 +53,7 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RepairSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SalvagerSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DebrisSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ CombatSystem::CombatSystem(const GameConfig& config)
|
||||
|
||||
void CombatSystem::tick(Tick currentTick,
|
||||
EntityAdmin& admin,
|
||||
BuildingSystem& /*buildings*/,
|
||||
std::vector<BeamFiredEvent>& outBeamFiredEvents)
|
||||
{
|
||||
TRACE();
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
class BuildingSystem;
|
||||
class EntityAdmin;
|
||||
|
||||
class CombatSystem
|
||||
@@ -24,6 +25,7 @@ public:
|
||||
|
||||
void tick(Tick currentTick,
|
||||
EntityAdmin& admin,
|
||||
BuildingSystem& buildings,
|
||||
std::vector<BeamFiredEvent>& outBeamFiredEvents);
|
||||
|
||||
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
#include "DebrisSystem.h"
|
||||
|
||||
#include "DespawnAtComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "DebrisComponent.h"
|
||||
#include "tracing.h"
|
||||
|
||||
DebrisSystem::DebrisSystem(EntityAdmin& admin)
|
||||
: m_admin(admin)
|
||||
{
|
||||
}
|
||||
|
||||
entt::entity DebrisSystem::spawn(QVector2D position, int amount, Tick despawnAt)
|
||||
{
|
||||
return m_admin.spawnDebris(position, amount, despawnAt);
|
||||
}
|
||||
|
||||
void DebrisSystem::tickDespawn(Tick currentTick)
|
||||
{
|
||||
TRACE();
|
||||
std::vector<entt::entity> expired;
|
||||
m_admin.forEach<DespawnAtComponent>(
|
||||
[&expired, currentTick](entt::entity e, DespawnAtComponent& d)
|
||||
{
|
||||
if (d.tick <= currentTick)
|
||||
{
|
||||
expired.push_back(e);
|
||||
}
|
||||
});
|
||||
|
||||
for (entt::entity e : expired)
|
||||
{
|
||||
m_admin.destroy(e);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<int> DebrisSystem::consume(entt::entity entity)
|
||||
{
|
||||
if (!m_admin.isValid(entity) || !m_admin.hasAll<DebrisComponent>(entity))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
int amount = m_admin.get<DebrisComponent>(entity).amount;
|
||||
m_admin.destroy(entity);
|
||||
return amount;
|
||||
}
|
||||
|
||||
bool collectOne(EntityAdmin& admin, entt::entity entity)
|
||||
{
|
||||
if (!admin.isValid(entity) || !admin.hasAll<DebrisComponent>(entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
DebrisComponent& data = admin.get<DebrisComponent>(entity);
|
||||
if (data.amount <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
--data.amount;
|
||||
if (data.amount <= 0)
|
||||
{
|
||||
admin.destroy(entity);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<DebrisInfo> getAllDebrisInfo(const EntityAdmin& admin)
|
||||
{
|
||||
std::vector<DebrisInfo> result;
|
||||
admin.forEach<DebrisComponent>(
|
||||
[&result, &admin](entt::entity e, const DebrisComponent& sd)
|
||||
{
|
||||
result.push_back(DebrisInfo{e, admin.get<PositionComponent>(e).value, sd.amount});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "Tick.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
class EntityAdmin;
|
||||
|
||||
// A piece of debris and the scrap amount it still holds (REQ-RES-DEBRIS-DROP).
|
||||
struct DebrisInfo
|
||||
{
|
||||
entt::entity entity;
|
||||
QVector2D position;
|
||||
int amount;
|
||||
};
|
||||
|
||||
// Manages debris entities: the salvageable objects dropped by destroyed ships and
|
||||
// defence stations (REQ-RES-DEBRIS-DROP). Each piece carries a scrap amount that
|
||||
// salvage modules collect one unit at a time (REQ-SHP-SALVAGE).
|
||||
class DebrisSystem
|
||||
{
|
||||
public:
|
||||
explicit DebrisSystem(EntityAdmin& admin);
|
||||
|
||||
entt::entity spawn(QVector2D position, int amount, Tick despawnAt);
|
||||
void tickDespawn(Tick currentTick);
|
||||
|
||||
// Removes the debris and returns its remaining scrap amount, or nullopt if not found.
|
||||
std::optional<int> consume(entt::entity entity);
|
||||
|
||||
// Collects a single scrap unit from the debris: decrements its amount by one,
|
||||
// destroying the entity once depleted. Returns true if a scrap was collected,
|
||||
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
|
||||
bool collectOne(entt::entity entity);
|
||||
|
||||
|
||||
private:
|
||||
EntityAdmin& m_admin;
|
||||
};
|
||||
|
||||
// Debris state read and changed straight off the registry — no system needed.
|
||||
|
||||
// Lightweight snapshot for callers that need to iterate all debris.
|
||||
std::vector<DebrisInfo> getAllDebrisInfo(const EntityAdmin& admin);
|
||||
|
||||
// Collects a single scrap unit from the debris: decrements its amount by one,
|
||||
// destroying the entity once depleted. Returns true if a scrap was collected,
|
||||
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
|
||||
bool collectOne(EntityAdmin& admin, entt::entity entity);
|
||||