Compare commits
89 Commits
progressio
...
2518f4f14a
| Author | SHA1 | Date | |
|---|---|---|---|
| 2518f4f14a | |||
| 561c95d0dd | |||
| b4be06ed5e | |||
| d1051607b2 | |||
| 79b79ab7c3 | |||
| 0bba7686e6 | |||
| f766ae4a86 | |||
| 8b71fe1a03 | |||
| 11daa61714 | |||
| 60d6767d93 | |||
| 7c1455b8a0 | |||
| e20a0bba67 | |||
| b2ce20e6ad | |||
| a9082c57f3 | |||
| a8a6a04f1e | |||
| a75222f111 | |||
| 9b63af6ccb | |||
| 4ca5b332cd | |||
| 0c4eb480be | |||
| b2c1ea34fd | |||
| 1cbc695bc5 | |||
| c7ecce6ac4 | |||
| be475e2836 | |||
| 9622fa4345 | |||
| 1cdafb7bcd | |||
| f205136e21 | |||
| b708e1b29d | |||
| c76ba07bc7 | |||
| b2e7e4897a | |||
| c21af63e84 | |||
| a4267a4760 | |||
| d412c69f82 | |||
| 08752aeced | |||
| d465671cfc | |||
| 76812ba0c5 | |||
| 9c1e948ab4 | |||
| 23ff406101 | |||
| 2ea0cb815d | |||
| 876b344b20 | |||
| cd75492796 | |||
| 486296feee | |||
| c9f14970a1 | |||
| 6a8c456aa1 | |||
| af8a2224c0 | |||
| 4b149d97a7 | |||
| 6a8b6acd3b | |||
| 3ce6e6d599 | |||
| 80a2622267 | |||
| 535d4f8f24 | |||
| 8c4fb78fc9 | |||
| 698dd4d13d | |||
| 9d28175b17 | |||
| ac4d56764c | |||
| 69fe607157 | |||
| 92e896b973 | |||
| dd7c997816 | |||
| ad3e73fdd8 | |||
| 177809fe1a | |||
| 69f655d179 | |||
| 271855781c | |||
| 5f6ecbf6c8 | |||
| cd966daba9 | |||
| 88bc4f2170 | |||
| 4986c1bac8 | |||
| e110e7e413 | |||
| 6274b5ce60 | |||
| 16ed6a5695 | |||
| 13dacab274 | |||
| 2ddf13238c | |||
| cc38bf95fa | |||
| b58bcfe272 | |||
| de1ebb8a5f | |||
| 24c18f8ac6 | |||
| 2cedd5d433 | |||
| f11db0c072 | |||
| 808e0c6a7b | |||
| e4ea4ca4b4 | |||
| fef22b9f86 | |||
| cdf89ce0dd | |||
| e8786c3922 | |||
| fc622670d2 | |||
| e32d384c99 | |||
| 24e0999d8a | |||
| fcaee000fa | |||
| 751ef27a7b | |||
| 3c1376828c | |||
| bd0675db66 | |||
| 5b86b15c71 | |||
| c6db4bf24a |
@@ -4,17 +4,27 @@ message(STATUS "Using CMake ${CMAKE_VERSION}")
|
|||||||
|
|
||||||
include(cmake/add_files.cmake)
|
include(cmake/add_files.cmake)
|
||||||
include(cmake/create_source_groups.cmake)
|
include(cmake/create_source_groups.cmake)
|
||||||
|
include(cmake/version.cmake)
|
||||||
|
|
||||||
# Project ----------------------------------------------------------------------
|
# Project ----------------------------------------------------------------------
|
||||||
|
|
||||||
project(DotaFactory)
|
# Product identity — anything that depends on the product/project name is defined
|
||||||
|
# here so it lives in a single place and can change in the future. These values
|
||||||
|
# feed the build targets in src/CMakeLists.txt and the Windows version resource
|
||||||
|
# (see cmake/version.rc.in).
|
||||||
|
set(PRODUCT_NAME "DotaFactory") # internal name and executable base name
|
||||||
|
set(PRODUCT_DISPLAY_NAME "Dota Factory") # human-readable product / file description
|
||||||
|
set(PRODUCT_COMPANY "TODO: company") # placeholder
|
||||||
|
set(PRODUCT_COPYRIGHT "TODO: copyright") # placeholder
|
||||||
|
|
||||||
|
project(${PRODUCT_NAME})
|
||||||
|
|
||||||
set(CMAKE_BUILD_TYPE_INIT "Release")
|
set(CMAKE_BUILD_TYPE_INIT "Release")
|
||||||
|
|
||||||
|
|
||||||
# Qt ---------------------------------------------------------------------------
|
# Qt ---------------------------------------------------------------------------
|
||||||
|
|
||||||
find_package(Qt5 COMPONENTS Widgets Network Multimedia Charts REQUIRED)
|
find_package(Qt5 COMPONENTS Widgets Network Multimedia Charts Svg REQUIRED)
|
||||||
|
|
||||||
if(Qt5Widgets_FOUND)
|
if(Qt5Widgets_FOUND)
|
||||||
message(STATUS "Found Qt ${Qt5Widgets_VERSION_STRING}")
|
message(STATUS "Found Qt ${Qt5Widgets_VERSION_STRING}")
|
||||||
@@ -54,6 +64,7 @@ function(COPY_QT_BINARIES TARGET_DIR IS_DEBUG)
|
|||||||
configure_file("${QT_BINARY_DIR}/Qt5Network${SUFFIX}.dll" "${TARGET_DIR}/Qt5Network${SUFFIX}.dll" COPYONLY)
|
configure_file("${QT_BINARY_DIR}/Qt5Network${SUFFIX}.dll" "${TARGET_DIR}/Qt5Network${SUFFIX}.dll" COPYONLY)
|
||||||
configure_file("${QT_BINARY_DIR}/Qt5Widgets${SUFFIX}.dll" "${TARGET_DIR}/Qt5Widgets${SUFFIX}.dll" COPYONLY)
|
configure_file("${QT_BINARY_DIR}/Qt5Widgets${SUFFIX}.dll" "${TARGET_DIR}/Qt5Widgets${SUFFIX}.dll" COPYONLY)
|
||||||
configure_file("${QT_BINARY_DIR}/Qt5Multimedia${SUFFIX}.dll" "${TARGET_DIR}/Qt5Multimedia${SUFFIX}.dll" COPYONLY)
|
configure_file("${QT_BINARY_DIR}/Qt5Multimedia${SUFFIX}.dll" "${TARGET_DIR}/Qt5Multimedia${SUFFIX}.dll" COPYONLY)
|
||||||
|
configure_file("${QT_BINARY_DIR}/Qt5Svg${SUFFIX}.dll" "${TARGET_DIR}/Qt5Svg${SUFFIX}.dll" COPYONLY)
|
||||||
endfunction(COPY_QT_BINARIES)
|
endfunction(COPY_QT_BINARIES)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
[[building]]
|
[[building]]
|
||||||
id = "belt"
|
id = "belt"
|
||||||
|
tooltip = "Transports items one tile at a time in the direction it faces."
|
||||||
cost = 2
|
cost = 2
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 0.2
|
construction_time_seconds = 0.2
|
||||||
@@ -7,6 +8,7 @@ surface_mask = ["A>"]
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "splitter"
|
id = "splitter"
|
||||||
|
tooltip = "Splits an incoming item stream between two outputs, with optional per-output filters."
|
||||||
cost = 3
|
cost = 3
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 0.5
|
construction_time_seconds = 0.5
|
||||||
@@ -14,6 +16,7 @@ surface_mask = ["<A>"]
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "tunnel_entry"
|
id = "tunnel_entry"
|
||||||
|
tooltip = "Sends items underground so belts can cross. Places an entry, or an exit when it would connect to a matching entry under the cursor."
|
||||||
cost = 5
|
cost = 5
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 0.5
|
construction_time_seconds = 0.5
|
||||||
@@ -21,6 +24,7 @@ surface_mask = ["A>"]
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "tunnel_exit"
|
id = "tunnel_exit"
|
||||||
|
tooltip = "Receives items from a matching tunnel entry and pushes them onward."
|
||||||
cost = 5
|
cost = 5
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 0.5
|
construction_time_seconds = 0.5
|
||||||
@@ -28,6 +32,7 @@ surface_mask = ["A>"]
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "miner"
|
id = "miner"
|
||||||
|
tooltip = "Extracts a selected ore from the asteroid; every tile yields any ore."
|
||||||
cost = 15
|
cost = 15
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
@@ -37,6 +42,7 @@ surface_mask = [
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "smelter"
|
id = "smelter"
|
||||||
|
tooltip = "Melts ore or scrap into basic materials. No recipe selection needed."
|
||||||
cost = 20
|
cost = 20
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
@@ -47,6 +53,7 @@ surface_mask = [
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "assembler"
|
id = "assembler"
|
||||||
|
tooltip = "Crafts a selected recipe from the production tree into intermediate or final parts."
|
||||||
cost = 35
|
cost = 35
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
@@ -57,6 +64,7 @@ surface_mask = [
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "reprocessing_plant"
|
id = "reprocessing_plant"
|
||||||
|
tooltip = "Consumes scrap and yields one random higher-tier product per cycle."
|
||||||
cost = 40
|
cost = 40
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
@@ -68,6 +76,7 @@ surface_mask = [
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "shipyard"
|
id = "shipyard"
|
||||||
|
tooltip = "Builds autonomous combat ships from a selected schematic and module layout."
|
||||||
cost = 60
|
cost = 60
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
@@ -78,10 +87,12 @@ surface_mask = [
|
|||||||
|
|
||||||
[[building]]
|
[[building]]
|
||||||
id = "salvage_bay"
|
id = "salvage_bay"
|
||||||
|
tooltip = "Drop-off point where salvage ships unload collected scrap onto belts."
|
||||||
cost = 25
|
cost = 25
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
|
output_buffer_capacity = 20
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"SAA",
|
"<AAS",
|
||||||
"SAA>",
|
" AAS",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,10 +7,9 @@
|
|||||||
# Combat stats are placeholders until the arena balancing pass;
|
# Combat stats are placeholders until the arena balancing pass;
|
||||||
# production_time_seconds values come from the numbers pass.
|
# production_time_seconds values come from the numbers pass.
|
||||||
#
|
#
|
||||||
# Unlock progression is mostly disabled (unlock_at_station_level = -1) pending
|
# Unlock gating is defined in unlocks.toml, not here (REQ-LOCK-EXPLICIT): a
|
||||||
# the pacing pass. The railguns are the exception: railgun_m is gated to
|
# module id granted by an unlock group starts locked and is awarded via a
|
||||||
# station level 1, and railgun_l requires railgun_m to be unlocked first
|
# defence station drop; ids absent from unlocks.toml (railgun_s) start unlocked.
|
||||||
# (unlock_requires) — a demonstration of the prerequisite chain.
|
|
||||||
#
|
#
|
||||||
# Surface mask footprint ladder — footprints gate which hulls can mount a
|
# Surface mask footprint ladder — footprints gate which hulls can mount a
|
||||||
# module, purely through geometry (see ships.toml for the matching hull
|
# module, purely through geometry (see ships.toml for the matching hull
|
||||||
@@ -32,7 +31,7 @@
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "railgun_s"
|
id = "railgun_s"
|
||||||
unlock_at_station_level = -1
|
tooltip = "Small railgun. Fast-firing, short range, low damage; fits any hull."
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "railgun_s_module", amount = 1}]
|
materials = [{item = "railgun_s_module", amount = 1}]
|
||||||
production_time_seconds = 1
|
production_time_seconds = 1
|
||||||
@@ -47,7 +46,7 @@ attack_rate_hz = 2.0
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "railgun_m"
|
id = "railgun_m"
|
||||||
unlock_at_station_level = 2
|
tooltip = "Medium railgun. Higher damage at longer range; needs a 2x2 slot."
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
"OO"]
|
"OO"]
|
||||||
@@ -56,19 +55,15 @@ production_time_seconds = 3
|
|||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "Rm"
|
glyph = "Rm"
|
||||||
|
|
||||||
# damage 14 keeps a 60 HP drone at 5 hits — 15+ crosses a breakpoint that
|
|
||||||
# silently adds ~25% effective DPS vs drones (docs/balancing/history.md,
|
|
||||||
# round 6).
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
damage = 14
|
damage = 14
|
||||||
attack_range_m = 80
|
attack_range_m = 70
|
||||||
attack_rate_hz = 1.5
|
attack_rate_hz = 1.5
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "railgun_l"
|
id = "railgun_l"
|
||||||
unlock_at_station_level = 6
|
tooltip = "Large railgun. Heavy damage at long range; needs a 3x3 slot."
|
||||||
unlock_requires = ["railgun_m"]
|
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OOO",
|
"OOO",
|
||||||
"OOO",
|
"OOO",
|
||||||
@@ -78,12 +73,9 @@ production_time_seconds = 4
|
|||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "Rl"
|
glyph = "Rl"
|
||||||
|
|
||||||
# attack_range_m 130 deliberately exceeds the station range of 120
|
|
||||||
# (stations.toml) — the l gun is the only weapon that can besiege stations
|
|
||||||
# without tanking their fire (docs/balancing/history.md, playtest 1).
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
damage = 52
|
damage = 52
|
||||||
attack_range_m = 130
|
attack_range_m = 100
|
||||||
attack_rate_hz = 0.8
|
attack_rate_hz = 0.8
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -92,7 +84,7 @@ attack_rate_hz = 0.8
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "salvager"
|
id = "salvager"
|
||||||
unlock_at_station_level = -1
|
tooltip = "Collects scrap from wrecks and stores it in the ship's cargo hold."
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "salvager_module", amount = 1}]
|
materials = [{item = "salvager_module", amount = 1}]
|
||||||
production_time_seconds = 1
|
production_time_seconds = 1
|
||||||
@@ -107,7 +99,7 @@ collection_rate_hz = 0.5
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "repair_tool"
|
id = "repair_tool"
|
||||||
unlock_at_station_level = 0
|
tooltip = "Repairs damaged friendly ships and defence stations within range."
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "repair_tool_module", amount = 1}]
|
materials = [{item = "repair_tool_module", amount = 1}]
|
||||||
production_time_seconds = 1
|
production_time_seconds = 1
|
||||||
@@ -125,7 +117,7 @@ repair_range_m = 80
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "afterburner"
|
id = "afterburner"
|
||||||
unlock_at_station_level = 2
|
tooltip = "Greatly boosts top speed and forward acceleration."
|
||||||
surface_mask = ["OOO"]
|
surface_mask = ["OOO"]
|
||||||
materials = [{item = "afterburner_module", amount = 1}]
|
materials = [{item = "afterburner_module", amount = 1}]
|
||||||
production_time_seconds = 1
|
production_time_seconds = 1
|
||||||
@@ -139,7 +131,7 @@ added_main_acceleration_mpss = 60
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "maneuvering_thrusters"
|
id = "maneuvering_thrusters"
|
||||||
unlock_at_station_level = 1
|
tooltip = "Improves top speed and lateral/braking acceleration."
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
|
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
|
||||||
production_time_seconds = 1
|
production_time_seconds = 1
|
||||||
@@ -156,7 +148,7 @@ added_maneuvering_acceleration_mpss = 10
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "armor_plates"
|
id = "armor_plates"
|
||||||
unlock_at_station_level = 0
|
tooltip = "Adds a large flat bonus to the ship's hit points."
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "armor_plates_module", amount = 1}]
|
materials = [{item = "armor_plates_module", amount = 1}]
|
||||||
production_time_seconds = 1
|
production_time_seconds = 1
|
||||||
@@ -169,7 +161,7 @@ added_hp = 1200
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "sensor_booster"
|
id = "sensor_booster"
|
||||||
unlock_at_station_level = 1
|
tooltip = "Extends the ship's sensor range."
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "sensor_booster_module", amount = 1}]
|
materials = [{item = "sensor_booster_module", amount = 1}]
|
||||||
production_time_seconds = 1
|
production_time_seconds = 1
|
||||||
@@ -185,7 +177,7 @@ added_sensor_range_m = 50
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_upgrade"
|
id = "weapon_upgrade"
|
||||||
unlock_at_station_level = 4
|
tooltip = "Increases the damage of all weapons on the ship."
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
"OX",
|
"OX",
|
||||||
@@ -201,7 +193,7 @@ multiplied_damage = 1.2
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_primer"
|
id = "weapon_primer"
|
||||||
unlock_at_station_level = 4
|
tooltip = "Increases the fire rate of all weapons on the ship."
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
"OX",
|
"OX",
|
||||||
@@ -217,7 +209,7 @@ multiplied_attack_rate_hz = 1.2
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_stabilizer"
|
id = "weapon_stabilizer"
|
||||||
unlock_at_station_level = 3
|
tooltip = "Extends weapon range at the cost of some fire rate."
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
"OX",
|
"OX",
|
||||||
@@ -240,7 +232,7 @@ multiplied_attack_rate_hz = 0.8
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "drone_bay"
|
id = "drone_bay"
|
||||||
unlock_at_station_level = 5
|
tooltip = "Drone launch bay (capability not yet implemented)."
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
"OO"]
|
"OO"]
|
||||||
@@ -252,7 +244,7 @@ glyph = "Db"
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "drone_hangar"
|
id = "drone_hangar"
|
||||||
unlock_at_station_level = 9
|
tooltip = "Large drone hangar (capability not yet implemented)."
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OOOOOO",
|
"OOOOOO",
|
||||||
"OOOOOO"]
|
"OOOOOO"]
|
||||||
|
|||||||
@@ -142,12 +142,12 @@ duration_seconds = 1.5
|
|||||||
|
|
||||||
# Depth-3 chain (ore -> ingot -> plate -> block) is the factory's
|
# Depth-3 chain (ore -> ingot -> plate -> block) is the factory's
|
||||||
# doubling-time knob; see the block economy rules in docs/balancing/rules.md.
|
# doubling-time knob; see the block economy rules in docs/balancing/rules.md.
|
||||||
# Explicitly unlocked at start (-1): building blocks appear in no
|
# unlocked_at_start: building blocks appear in no schematic's materials, so the
|
||||||
# schematic's materials, so implicit unlocking can never reach this recipe.
|
# implicit item graph can never reach this recipe (REQ-LOCK-IMPLICIT).
|
||||||
[[recipe]]
|
[[recipe]]
|
||||||
id = "building_block"
|
id = "building_block"
|
||||||
unlock_at_station_level = -1
|
|
||||||
building = "assembler"
|
building = "assembler"
|
||||||
|
unlocked_at_start = true
|
||||||
inputs = [{item = "steel_plate", amount = 2}]
|
inputs = [{item = "steel_plate", amount = 2}]
|
||||||
outputs = [{item = "building_block", amount = 4}]
|
outputs = [{item = "building_block", amount = 4}]
|
||||||
duration_seconds = 2.0
|
duration_seconds = 2.0
|
||||||
@@ -220,15 +220,14 @@ outputs = [{item = "capital_core", amount = 1}]
|
|||||||
duration_seconds = 10.0
|
duration_seconds = 10.0
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Shortcut recipes — drop-only assembler recipe schematics
|
# Shortcut recipes — drop-only assembler recipes, gated by unlock groups in
|
||||||
# (unlock_at_station_level >= 0). Pure rewards: item threat stays defined by
|
# unlocks.toml (REQ-LOCK-EXPLICIT). Pure rewards: item threat stays defined by
|
||||||
# the base (expensive) path via the max rule, so shortcuts give real factory
|
# the base (expensive) path via the max rule, so shortcuts give real factory
|
||||||
# efficiency without shifting any balance.
|
# efficiency without shifting any balance.
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
[[recipe]]
|
[[recipe]]
|
||||||
id = "shortcut_steel_plate"
|
id = "shortcut_steel_plate"
|
||||||
unlock_at_station_level = 1
|
|
||||||
building = "assembler"
|
building = "assembler"
|
||||||
inputs = [{item = "iron_ore", amount = 3}]
|
inputs = [{item = "iron_ore", amount = 3}]
|
||||||
outputs = [{item = "steel_plate", amount = 1}]
|
outputs = [{item = "steel_plate", amount = 1}]
|
||||||
@@ -236,7 +235,6 @@ duration_seconds = 2.0
|
|||||||
|
|
||||||
[[recipe]]
|
[[recipe]]
|
||||||
id = "shortcut_control_chip"
|
id = "shortcut_control_chip"
|
||||||
unlock_at_station_level = 2
|
|
||||||
building = "assembler"
|
building = "assembler"
|
||||||
inputs = [{item = "quartz", amount = 2}]
|
inputs = [{item = "quartz", amount = 2}]
|
||||||
outputs = [{item = "control_chip", amount = 1}]
|
outputs = [{item = "control_chip", amount = 1}]
|
||||||
@@ -244,7 +242,6 @@ duration_seconds = 4.0
|
|||||||
|
|
||||||
[[recipe]]
|
[[recipe]]
|
||||||
id = "shortcut_hardened_steel"
|
id = "shortcut_hardened_steel"
|
||||||
unlock_at_station_level = 2
|
|
||||||
building = "assembler"
|
building = "assembler"
|
||||||
inputs = [{item = "iron_ingot", amount = 4}]
|
inputs = [{item = "iron_ingot", amount = 4}]
|
||||||
outputs = [{item = "hardened_steel", amount = 1}]
|
outputs = [{item = "hardened_steel", amount = 1}]
|
||||||
|
|||||||
@@ -4,10 +4,9 @@
|
|||||||
# content; stats, materials, and production times are placeholders until the
|
# content; stats, materials, and production times are placeholders until the
|
||||||
# recipe and balancing passes.
|
# recipe and balancing passes.
|
||||||
#
|
#
|
||||||
# Unlock progression is mostly disabled (unlock_at_station_level = -1) pending
|
# Unlock gating is defined in unlocks.toml, not here (REQ-LOCK-EXPLICIT): a ship
|
||||||
# the balancing pass. The capital hulls are the exception: battleship is gated
|
# id granted by an unlock group starts locked and is awarded via a defence
|
||||||
# to station level 1, and dreadnought (level 2) requires battleship to be
|
# station drop; ids absent from unlocks.toml (drone, frigate) start unlocked.
|
||||||
# unlocked first (unlock_requires) — a demonstration of the prerequisite chain.
|
|
||||||
#
|
#
|
||||||
# Size classes:
|
# Size classes:
|
||||||
# xs drone 1 cell — exactly one 1x1 module
|
# xs drone 1 cell — exactly one 1x1 module
|
||||||
@@ -22,7 +21,6 @@
|
|||||||
|
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "drone"
|
id = "drone"
|
||||||
unlock_at_station_level = -1
|
|
||||||
layout = ["O"]
|
layout = ["O"]
|
||||||
default_modules = [{type = "railgun_s", x = 0, y = 0, rotation = "east"}]
|
default_modules = [{type = "railgun_s", x = 0, y = 0, rotation = "east"}]
|
||||||
|
|
||||||
@@ -49,7 +47,6 @@ sensor_range_m = 150
|
|||||||
# L-shaped weapon modifier, or an afterburner spanning the full center line.
|
# L-shaped weapon modifier, or an afterburner spanning the full center line.
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "frigate"
|
id = "frigate"
|
||||||
unlock_at_station_level = -1
|
|
||||||
layout = [
|
layout = [
|
||||||
"XOX",
|
"XOX",
|
||||||
"OOO",
|
"OOO",
|
||||||
@@ -84,7 +81,6 @@ sensor_range_m = 200
|
|||||||
# mount medium hardware.
|
# mount medium hardware.
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "destroyer"
|
id = "destroyer"
|
||||||
unlock_at_station_level = 0
|
|
||||||
layout = [
|
layout = [
|
||||||
"OXOXO",
|
"OXOXO",
|
||||||
"OOOOO",
|
"OOOOO",
|
||||||
@@ -120,7 +116,6 @@ sensor_range_m = 220
|
|||||||
# supports; no 3x3 area exists for an l gun.
|
# supports; no 3x3 area exists for an l gun.
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "cruiser"
|
id = "cruiser"
|
||||||
unlock_at_station_level = 2
|
|
||||||
layout = [
|
layout = [
|
||||||
"XOOX",
|
"XOOX",
|
||||||
"OOOO",
|
"OOOO",
|
||||||
@@ -158,8 +153,6 @@ sensor_range_m = 250
|
|||||||
# stern leave no 3x3 area for an l gun and no 2x6 area for a drone hangar.
|
# stern leave no 3x3 area for an l gun and no 2x6 area for a drone hangar.
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "battlecruiser"
|
id = "battlecruiser"
|
||||||
unlock_at_station_level = 4
|
|
||||||
unlock_requires = ["cruiser"]
|
|
||||||
layout = [
|
layout = [
|
||||||
"OOXXOO",
|
"OOXXOO",
|
||||||
"OOOOOO",
|
"OOOOOO",
|
||||||
@@ -201,8 +194,6 @@ sensor_range_m = 260
|
|||||||
# so no 2x6 drone hangar fits.
|
# so no 2x6 drone hangar fits.
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "battleship"
|
id = "battleship"
|
||||||
unlock_at_station_level = 6
|
|
||||||
unlock_requires = ["battlecruiser"]
|
|
||||||
layout = [
|
layout = [
|
||||||
"XOOOOX",
|
"XOOOOX",
|
||||||
"OOOOOO",
|
"OOOOOO",
|
||||||
@@ -244,8 +235,6 @@ sensor_range_m = 280
|
|||||||
# stay the only hangar hull. Bow and stern strips hold supports.
|
# stay the only hangar hull. Bow and stern strips hold supports.
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "dreadnought"
|
id = "dreadnought"
|
||||||
unlock_at_station_level = 8
|
|
||||||
unlock_requires = ["battleship"]
|
|
||||||
layout = [
|
layout = [
|
||||||
"XXXOOOOOXXX",
|
"XXXOOOOOXXX",
|
||||||
"OOOXOOOXOOO",
|
"OOOXOOOXOOO",
|
||||||
@@ -288,8 +277,6 @@ sensor_range_m = 300
|
|||||||
# the lower decks hold supports and 2x2 point-defense m guns.
|
# the lower decks hold supports and 2x2 point-defense m guns.
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "carrier"
|
id = "carrier"
|
||||||
unlock_at_station_level = 9
|
|
||||||
unlock_requires = ["battleship"]
|
|
||||||
layout = [
|
layout = [
|
||||||
"XOOOOOOOOX",
|
"XOOOOOOOOX",
|
||||||
"OOOOOOOOOO",
|
"OOOOOOOOOO",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# a fresh player defence station holds one early parity wave unaided; the
|
# a fresh player defence station holds one early parity wave unaided; the
|
||||||
# enemy station at level 0 matches the player station exactly and scales
|
# enemy station at level 0 matches the player station exactly and scales
|
||||||
# with the push level x. Station scrap drops stay authored (pushing rewards
|
# with the push level x. Station scrap drops stay authored (pushing rewards
|
||||||
# are tuned independently of ship production costs, REQ-RES-SCRAP-DROP).
|
# are tuned independently of ship production costs, REQ-RES-DEBRIS-DROP).
|
||||||
|
|
||||||
[hq]
|
[hq]
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
|
|||||||
140
bin/app/data/config/unlocks.toml
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Unlock groups (REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP).
|
||||||
|
#
|
||||||
|
# Each [[unlock]] is a group of ships/modules/buildings/recipes awarded together
|
||||||
|
# from a single defence station drop. Anything NOT granted by any group is
|
||||||
|
# available from game start. `station_level` gates when a group becomes eligible;
|
||||||
|
# `requires` lists prerequisite unlock-group ids (REQ-LOCK-PREREQ).
|
||||||
|
#
|
||||||
|
# Most entries below are single-item groups that reproduce the previous per-item
|
||||||
|
# progression. The salvage_operations and reprocessing groups are the grouped
|
||||||
|
# unlocks: they lock the salvager module + salvage bay, and the reprocessing
|
||||||
|
# plant, from game start.
|
||||||
|
|
||||||
|
# --- Grouped unlocks -------------------------------------------------------
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "salvage_operations"
|
||||||
|
station_level = 1
|
||||||
|
modules = ["salvager"]
|
||||||
|
buildings = ["salvage_bay"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "reprocessing"
|
||||||
|
station_level = 2
|
||||||
|
buildings = ["reprocessing_plant"]
|
||||||
|
|
||||||
|
# --- Ships -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "destroyer"
|
||||||
|
station_level = 0
|
||||||
|
ships = ["destroyer"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "cruiser"
|
||||||
|
station_level = 2
|
||||||
|
ships = ["cruiser"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "battlecruiser"
|
||||||
|
station_level = 4
|
||||||
|
requires = ["cruiser"]
|
||||||
|
ships = ["battlecruiser"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "battleship"
|
||||||
|
station_level = 6
|
||||||
|
requires = ["battlecruiser"]
|
||||||
|
ships = ["battleship"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "dreadnought"
|
||||||
|
station_level = 8
|
||||||
|
requires = ["battleship"]
|
||||||
|
ships = ["dreadnought"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "carrier"
|
||||||
|
station_level = 9
|
||||||
|
requires = ["battleship"]
|
||||||
|
ships = ["carrier"]
|
||||||
|
|
||||||
|
# --- Modules ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "repair_tool"
|
||||||
|
station_level = 0
|
||||||
|
modules = ["repair_tool"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "armor_plates"
|
||||||
|
station_level = 0
|
||||||
|
modules = ["armor_plates"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "maneuvering_thrusters"
|
||||||
|
station_level = 1
|
||||||
|
modules = ["maneuvering_thrusters"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "sensor_booster"
|
||||||
|
station_level = 1
|
||||||
|
modules = ["sensor_booster"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "railgun_m"
|
||||||
|
station_level = 2
|
||||||
|
modules = ["railgun_m"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "afterburner"
|
||||||
|
station_level = 2
|
||||||
|
modules = ["afterburner"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "weapon_stabilizer"
|
||||||
|
station_level = 3
|
||||||
|
modules = ["weapon_stabilizer"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "weapon_upgrade"
|
||||||
|
station_level = 4
|
||||||
|
modules = ["weapon_upgrade"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "weapon_primer"
|
||||||
|
station_level = 4
|
||||||
|
modules = ["weapon_primer"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "drone_bay"
|
||||||
|
station_level = 5
|
||||||
|
modules = ["drone_bay"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "railgun_l"
|
||||||
|
station_level = 6
|
||||||
|
requires = ["railgun_m"]
|
||||||
|
modules = ["railgun_l"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "drone_hangar"
|
||||||
|
station_level = 9
|
||||||
|
modules = ["drone_hangar"]
|
||||||
|
|
||||||
|
# --- Assembler recipes -----------------------------------------------------
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "shortcut_steel_plate"
|
||||||
|
station_level = 1
|
||||||
|
recipes = ["shortcut_steel_plate"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "shortcut_control_chip"
|
||||||
|
station_level = 2
|
||||||
|
recipes = ["shortcut_control_chip"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "shortcut_hardened_steel"
|
||||||
|
station_level = 2
|
||||||
|
recipes = ["shortcut_hardened_steel"]
|
||||||
@@ -63,7 +63,7 @@ outline = "#ffffff"
|
|||||||
glyph = "Sb"
|
glyph = "Sb"
|
||||||
|
|
||||||
[buildings.belt]
|
[buildings.belt]
|
||||||
fill = "#5a5a5a"
|
fill = "#1a1a1a"
|
||||||
outline = "#7a7a7a"
|
outline = "#7a7a7a"
|
||||||
glyph = ""
|
glyph = ""
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ salvage_color = "#33ccff"
|
|||||||
width_px = 2
|
width_px = 2
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Build / demolish / selection overlays
|
# Build / deconstruct / selection overlays
|
||||||
#
|
#
|
||||||
# All overlay colors carry an alpha channel so they composite over the
|
# All overlay colors carry an alpha channel so they composite over the
|
||||||
# underlying scene.
|
# underlying scene.
|
||||||
@@ -344,10 +344,14 @@ width_px = 2
|
|||||||
[overlays]
|
[overlays]
|
||||||
ghost_valid = "#ffffff44" # builder-mode ghost, placement allowed (REQ-BLD-GHOST)
|
ghost_valid = "#ffffff44" # builder-mode ghost, placement allowed (REQ-BLD-GHOST)
|
||||||
ghost_invalid = "#ff000044" # builder-mode ghost, placement invalid (REQ-BLD-PLACE-VALID)
|
ghost_invalid = "#ff000044" # builder-mode ghost, placement invalid (REQ-BLD-PLACE-VALID)
|
||||||
demolish_tint = "#ff000033" # demolish-mode hover tint
|
deconstruct_tint = "#ff000033" # deconstruct-mode hover tint
|
||||||
selection_rect = "#00ff00" # box-drag selection rectangle (REQ-UI-MULTI-SELECT)
|
selection_rect = "#00ff00" # box-drag selection rectangle (REQ-UI-MULTI-SELECT)
|
||||||
tile_highlight = "#ffffff22" # tile under cursor
|
tile_highlight = "#ffffff22" # tile under cursor
|
||||||
selected_outline = "#ffff00" # outline drawn around currently-selected building(s)
|
selected_outline = "#ffff00" # outline drawn around currently-selected building(s)
|
||||||
|
copy_config = "#33ccff66" # copy-settings eligible-target tint + copy/paste flash (REQ-BLD-COPY-CONFIG-FEEDBACK)
|
||||||
|
locked_asteroid = "#0000007f" # tint over the asteroid left of the buildable edge (not yet unlocked by expansion)
|
||||||
|
modal_dim = "#00000099" # semi-transparent black dim behind modal dialogs/menus (REQ-UI-MODAL-DIM)
|
||||||
|
tunnel_preview = "#00ff0055" # tunnel connection preview: matched end + tiles between (REQ-BLD-TUNNEL-MODE)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Schematic-drop toasts (REQ-UI-SCHEMATIC-TOAST)
|
# Schematic-drop toasts (REQ-UI-SCHEMATIC-TOAST)
|
||||||
@@ -357,3 +361,17 @@ selected_outline = "#ffff00" # outline drawn around currently-selected buildin
|
|||||||
bg = "#000000cc"
|
bg = "#000000cc"
|
||||||
fg = "#ffffff"
|
fg = "#ffffff"
|
||||||
font_size = 14
|
font_size = 14
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Building status light (REQ-UI-STATUS-LIGHT)
|
||||||
|
#
|
||||||
|
# Fill color per production state, drawn as a small circle in the building's
|
||||||
|
# upper-right corner, plus the constant outline color.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[status_light]
|
||||||
|
grey = "#808080" # no recipe/schematic selected
|
||||||
|
green = "#33cc33" # producing (Salvage Bay: holding scrap)
|
||||||
|
red = "#cc3333" # idle, input missing (Salvage Bay: empty)
|
||||||
|
yellow = "#e6c619" # idle, output buffer full
|
||||||
|
outline = "#000000"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
[world]
|
[world]
|
||||||
height_tiles = 40
|
height_tiles = 40
|
||||||
refund_percentage = 100
|
refund_percentage = 100
|
||||||
|
deconstruction_time_seconds = 0.1
|
||||||
starting_building_blocks = 200
|
starting_building_blocks = 200
|
||||||
scrap_despawn_seconds = 120
|
debris_despawn_seconds = 120
|
||||||
scrap_per_threat = 0.25
|
scrap_per_threat = 0.25
|
||||||
tile_size_m = 10
|
tile_size_m = 10
|
||||||
belt_speed_mps = 20
|
belt_speed_mps = 20
|
||||||
@@ -10,6 +11,8 @@ tunnel_max_distance_tiles = 10
|
|||||||
departure_interval_seconds = 20
|
departure_interval_seconds = 20
|
||||||
orbit_factor = 0.8
|
orbit_factor = 0.8
|
||||||
rally_orbit_radius_tiles = 5.0
|
rally_orbit_radius_tiles = 5.0
|
||||||
|
building_blocks_tooltip = "Building blocks are the currency for construction. Spend them to place buildings and to expand the asteroid. Produce building blocks in your assemblers and deliver them to the HQ on a belt to grow your stock."
|
||||||
|
artifact_tooltip = "Artifacts are the key to victory. Earn one by choosing the artifact reward when you destroy a set of enemy defence stations. Collect enough of them to win the game."
|
||||||
|
|
||||||
[regions]
|
[regions]
|
||||||
asteroid_width_tiles = 60
|
asteroid_width_tiles = 60
|
||||||
@@ -17,6 +20,13 @@ player_buffer_width_tiles = 20
|
|||||||
contest_zone_width_tiles = 60
|
contest_zone_width_tiles = 60
|
||||||
enemy_buffer_width_tiles = 20
|
enemy_buffer_width_tiles = 20
|
||||||
|
|
||||||
|
[scroll]
|
||||||
|
# View pan speed (REQ-UI-SCROLL-SPEED): slow near the asteroid, fast across the
|
||||||
|
# contest zone, with a linear ramp of the given width straddling each boundary.
|
||||||
|
pan_speed_slow_tiles_per_second = 16.0
|
||||||
|
pan_speed_fast_tiles_per_second = 32.0
|
||||||
|
pan_ramp_band_width_tiles = 16
|
||||||
|
|
||||||
[expansion]
|
[expansion]
|
||||||
columns_per_expansion_tiles = 10
|
columns_per_expansion_tiles = 10
|
||||||
# x = expansions already purchased; ~1 per cycle mid-game, decelerating
|
# x = expansions already purchased; ~1 per cycle mid-game, decelerating
|
||||||
|
|||||||
6
bin/app/data/icons/buildings/assembler.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#3a6fa8"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="16" cy="16" r="6.2"/><circle cx="16" cy="16" r="2.2"/><path d="M16 5.5v3M16 23.5v3M5.5 16h3M23.5 16h3M8.6 8.6 10.7 10.7M23.4 8.6 21.3 10.7M8.6 23.4 10.7 21.3M23.4 23.4 21.3 21.3"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 499 B |
6
bin/app/data/icons/buildings/belt.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#6a6a6a"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="4" y="10" width="24" height="12" rx="3"/><path d="M9 13 12.5 16 9 19"/><path d="M14.5 13 18 16 14.5 19"/><path d="M20 13 23.5 16 20 19"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 453 B |
6
bin/app/data/icons/buildings/deconstruct.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#cc3333"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M7 10h18"/><path d="M9.5 10 11 26h10l1.5-16"/><path d="M13 10V6.5h6V10"/><path d="M13.5 14v8M18.5 14v8"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 421 B |
6
bin/app/data/icons/buildings/hq.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#2e5fb8"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="7" y="15" width="18" height="11" rx="1"/><path d="M16 15V5"/><path d="M16 6h6l-2.2 2 2.2 2h-6"/><path d="M11 19h2.5M15.5 19h2.5M20 19h1.5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 456 B |
6
bin/app/data/icons/buildings/miner.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#6b4a2c"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="9" y="5" width="14" height="8" rx="1.5"/><path d="M11 13 16 26 21 13"/><path d="M13 17.5h6"/><path d="M14.5 21.5h3"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 433 B |
6
bin/app/data/icons/buildings/reprocessing_plant.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#6a3a8a"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="18" r="4"/><circle cx="20" cy="18" r="4"/><path d="M10.6 16.6 13.4 19.4M13.4 16.6 10.6 19.4M18.6 16.6 21.4 19.4M21.4 16.6 18.6 19.4"/><path d="M16 4v8"/><path d="M13 9 16 12 19 9"/><path d="M16 24v4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 528 B |
6
bin/app/data/icons/buildings/salvage_bay.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#b8a23a"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M7 13h18l-2 13H9z"/><path d="M16 3v7"/><path d="M12 7 16 11 20 7"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 383 B |
6
bin/app/data/icons/buildings/shipyard.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#3f6580"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M16 4c2 3 2.6 6 2.6 9.5V16h-5.2v-2.5C13.4 10 14 7 16 4z"/><circle cx="16" cy="10" r="1.3"/><path d="M13.4 15 11 18h2.4z"/><path d="M18.6 15 21 18h-2.4z"/><path d="M14.7 17 16 21 17.3 17"/><path d="M9 25h14"/><path d="M12 25v-2M20 25v-2"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 554 B |
6
bin/app/data/icons/buildings/smelter.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#b85a1e"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M9 9h14l-2.5 8h-9z"/><path d="M8 9h16"/><path d="M11 26c-1-1.8.4-3 .4-4 .9 1 1.6 2.2 1.6 4"/><path d="M16 26c-1-2 .4-3.4.4-4.4 .9 1 1.6 2.6 1.6 4.4"/><path d="M21 26c-1-1.8.4-3 .4-4 .9 1 1.6 2.2 1.6 4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 519 B |
6
bin/app/data/icons/buildings/splitter.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#7a7a5a"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5 16h11"/><path d="M16 8v16"/><path d="M13.5 10.5 16 8 18.5 10.5"/><path d="M13.5 21.5 16 24 18.5 21.5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 422 B |
6
bin/app/data/icons/buildings/station.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#55606f"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M16 4 26 8v7c0 7-5 11-10 13-5-2-10-6-10-13V8z"/><path d="M16 11v9M11.5 15.5h9"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 396 B |
6
bin/app/data/icons/buildings/tunnel_entry.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<rect width="100" height="100" rx="22" fill="#4f7562"/>
|
||||||
|
<g transform="translate(13,13) scale(2.3125)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 13H29"/><path d="M4 7H8Q10 7 10 9.5V21.5Q10 24 12.5 24H19.5Q22 24 22 21.5V9.5Q22 7 24 7H27"/><path d="M24.5 4.5 27 7 24.5 9.5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 447 B |
6
bin/app/data/icons/items/afterburner_module.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="11.6 19.6 76.9 76.9" width="100" height="100">
|
||||||
|
<rect x="34" y="24" width="32" height="26" rx="5" fill="#6080c0" stroke="#182030" stroke-width="3.5"/>
|
||||||
|
<path d="M36 50 L64 50 L58 62 L42 62 Z" fill="#3a5090" stroke="#182030" stroke-width="2.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M42 62 Q46 80 50 62 Q54 80 58 62 Q56 90 50 92 Q44 90 42 62 Z" fill="#ff9a3a"/>
|
||||||
|
<path d="M47 64 Q50 82 53 64 Z" fill="#ffe08a"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 468 B |
5
bin/app/data/icons/items/armor_plates_module.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="10.5 11.5 79.0 79.0" width="100" height="100">
|
||||||
|
<path d="M50 16 L80 26 L80 50 Q80 74 50 86 Q20 74 20 50 L20 26 Z" fill="#808080" stroke="#202020" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M50 16 L50 86" stroke="#5a5a5a" stroke-width="3"/>
|
||||||
|
<path d="M20 42 Q50 52 80 42" fill="none" stroke="#5a5a5a" stroke-width="3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 390 B |
5
bin/app/data/icons/items/battlecruiser_hull.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7.1 6.1 85.9 85.9" width="100" height="100">
|
||||||
|
<path d="M50 12 L66 40 L88 60 L64 58 L60 86 L40 86 L36 58 L12 60 L34 40 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="40" r="7" fill="#cccc33" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
<g fill="#3a4450"><circle cx="44" cy="82" r="3.2"/><circle cx="56" cy="82" r="3.2"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 429 B |
5
bin/app/data/icons/items/battleship_hull.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="9.2 9.2 81.6 81.6" width="100" height="100">
|
||||||
|
<path d="M50 14 L70 44 L82 78 L60 70 L58 86 L42 86 L40 70 L18 78 L30 44 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="44" r="7" fill="#ff9933" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
<g fill="#3a4450"><circle cx="44" cy="82" r="3.2"/><circle cx="56" cy="82" r="3.2"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 429 B |
12
bin/app/data/icons/items/building_block.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="14.9 15.9 70.2 70.2" width="100" height="100">
|
||||||
|
<g stroke="#302810" stroke-width="3.2" stroke-linejoin="round">
|
||||||
|
<path d="M50 20 L78 36 L50 52 L22 36 Z" fill="#ddc98f"/>
|
||||||
|
<path d="M50 52 L78 36 L78 66 L50 82 Z" fill="#b39a5c"/>
|
||||||
|
<path d="M50 52 L22 36 L22 66 L50 82 Z" fill="#c8b070"/>
|
||||||
|
</g>
|
||||||
|
<g stroke="#302810" stroke-width="1.8" stroke-linejoin="round">
|
||||||
|
<path d="M50 26.4 L66.8 36 L50 45.6 L33.2 36 Z" fill="#cbb772"/>
|
||||||
|
<path d="M55.3 54.7 L72.7 44.7 L72.7 63.3 L55.3 73.3 Z" fill="#a2894f"/>
|
||||||
|
<path d="M44.7 54.7 L27.3 44.7 L27.3 63.3 L44.7 73.3 Z" fill="#b7a465"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 655 B |
12
bin/app/data/icons/items/capacitor_bank.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="17.0 16.0 65.9 65.9" width="100" height="100">
|
||||||
|
<g stroke="#302408" stroke-width="3.2" stroke-linecap="round">
|
||||||
|
<line x1="37" y1="20" x2="37" y2="30"/>
|
||||||
|
<line x1="63" y1="20" x2="63" y2="30"/>
|
||||||
|
</g>
|
||||||
|
<g stroke="#302408" stroke-width="3.2" stroke-linejoin="round">
|
||||||
|
<rect x="28" y="28" width="18" height="50" rx="8" fill="#d0a030"/>
|
||||||
|
<rect x="54" y="28" width="18" height="50" rx="8" fill="#e0b040"/>
|
||||||
|
</g>
|
||||||
|
<line x1="30" y1="42" x2="44" y2="42" stroke="#6b5410" stroke-width="2.6"/>
|
||||||
|
<line x1="56" y1="42" x2="70" y2="42" stroke="#6b5410" stroke-width="2.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 634 B |
5
bin/app/data/icons/items/capital_core.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.5 17.5 68.9 68.9" width="100" height="100">
|
||||||
|
<circle cx="50" cy="52" r="30" fill="#b040d0" stroke="#280c30" stroke-width="4"/>
|
||||||
|
<path d="M50 26 L68 46 L50 78 L32 46 Z" fill="#d06ae8" stroke="#280c30" stroke-width="2.5" stroke-linejoin="round"/>
|
||||||
|
<circle cx="43" cy="44" r="6" fill="#f0c0f8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 355 B |
5
bin/app/data/icons/items/carrier_hull.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="13.4 17.4 73.1 73.1" width="100" height="100">
|
||||||
|
<path d="M28 24 L72 24 L82 50 L72 84 L28 84 L18 50 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<line x1="50" y1="26" x2="50" y2="82" stroke="#4a5560" stroke-width="4"/>
|
||||||
|
<circle cx="50" cy="38" r="6" fill="#cc66ff" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 395 B |
4
bin/app/data/icons/items/ceramic_plate.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.8 15.8 68.4 68.4" width="100" height="100">
|
||||||
|
<path d="M50 20 L76 35 L76 65 L50 80 L24 65 L24 35 Z" fill="#e0d8c8" stroke="#3a3428" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M50 32 L64 40 L64 60 L50 68 L36 60 L36 40 Z" fill="#efe9dd" stroke="#3a3428" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 343 B |
6
bin/app/data/icons/items/control_chip.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.5 15.5 68.9 68.9" width="100" height="100">
|
||||||
|
<rect x="20" y="20" width="60" height="60" rx="11" fill="#1f7a44" stroke="#0e3320" stroke-width="4"/>
|
||||||
|
<rect x="33" y="33" width="34" height="34" rx="5" fill="#3fbf6a" stroke="#0e3320" stroke-width="3"/>
|
||||||
|
<path d="M50 33 L50 24 M50 67 L50 76 M33 50 L24 50 M67 50 L76 50" stroke="#186036" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<circle cx="50" cy="50" r="6" fill="#c9a227" stroke="#0e3320" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 520 B |
7
bin/app/data/icons/items/copper_coil.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0.0 2.0 100.0 100.0" width="100" height="100">
|
||||||
|
<circle cx="50" cy="52" r="42" fill="none" stroke="#3a1e08" stroke-width="2.5"/>
|
||||||
|
<circle cx="50" cy="52" r="26" fill="none" stroke="#3a1e08" stroke-width="2.5"/>
|
||||||
|
<circle cx="50" cy="52" r="34" fill="none" stroke="#cf7a2c" stroke-width="16"/>
|
||||||
|
<circle cx="50" cy="52" r="34" fill="none" stroke="#8a4e18" stroke-width="16" stroke-dasharray="3 11"/>
|
||||||
|
<circle cx="50" cy="52" r="34" fill="none" stroke="#eaa85f" stroke-width="16" stroke-dasharray="2 12" stroke-dashoffset="6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 585 B |
8
bin/app/data/icons/items/copper_ingot.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="24.0 28.0 62.0 62.0" width="100" height="100">
|
||||||
|
<g stroke="#3a1e08" stroke-width="3.5" stroke-linejoin="round">
|
||||||
|
<path d="M28 72 L72 72 L64 54 L36 54 Z" fill="#cf7a2c"/>
|
||||||
|
<path d="M64 54 L72 72 L82 64 L74 46 Z" fill="#a85e20"/>
|
||||||
|
<path d="M36 54 L64 54 L74 46 L46 46 Z" fill="#e59a52"/>
|
||||||
|
</g>
|
||||||
|
<path d="M40 63 L60 63" stroke="#f0b878" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 444 B |
8
bin/app/data/icons/items/copper_ore.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="11.5 11.5 79.0 79.0" width="100" height="100">
|
||||||
|
<path d="M50 16 L76 28 L86 54 L70 84 L36 86 L16 58 L24 30 Z" fill="#5f8f78" stroke="#16241d" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M50 16 L58 47 L24 30 Z" fill="#79a890"/>
|
||||||
|
<path d="M58 47 L70 84 L36 86 Z" fill="#4a7060"/>
|
||||||
|
<path d="M50 16 L58 47 M58 47 L86 54 M58 47 L70 84 M58 47 L36 86 M58 47 L24 30" fill="none" stroke="#2c463a" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
<circle cx="41" cy="56" r="4" fill="#d98a3e"/>
|
||||||
|
<circle cx="62" cy="66" r="3.2" fill="#d98a3e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 602 B |
10
bin/app/data/icons/items/copper_wire.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="8.1 4.1 83.7 83.7" width="100" height="100">
|
||||||
|
<g fill="none" stroke-linecap="round">
|
||||||
|
<path d="M16 38 q17 -12 34 0 t34 0" stroke="#3a1e08" stroke-width="10"/>
|
||||||
|
<path d="M16 52 q17 -12 34 0 t34 0" stroke="#3a1e08" stroke-width="10"/>
|
||||||
|
<path d="M16 66 q17 -12 34 0 t34 0" stroke="#3a1e08" stroke-width="10"/>
|
||||||
|
<path d="M16 38 q17 -12 34 0 t34 0" stroke="#cf7a2c" stroke-width="6"/>
|
||||||
|
<path d="M16 52 q17 -12 34 0 t34 0" stroke="#d98a3e" stroke-width="6"/>
|
||||||
|
<path d="M16 66 q17 -12 34 0 t34 0" stroke="#cf7a2c" stroke-width="6"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 608 B |
5
bin/app/data/icons/items/cruiser_hull.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="9.2 8.2 81.6 81.6" width="100" height="100">
|
||||||
|
<path d="M50 14 L62 40 L86 58 L64 56 L60 84 L40 84 L36 56 L14 58 L38 40 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="40" r="6.5" fill="#66cc33" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
<g fill="#3a4450"><circle cx="45" cy="80" r="3"/><circle cx="55" cy="80" r="3"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 427 B |
4
bin/app/data/icons/items/destroyer_hull.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="9.2 8.2 81.6 81.6" width="100" height="100">
|
||||||
|
<path d="M50 14 L62 40 L86 58 L64 56 L60 84 L40 84 L36 56 L14 58 L38 40 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="40" r="6.5" fill="#33ccaa" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 340 B |
5
bin/app/data/icons/items/dreadnought_hull.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7.1 7.1 85.9 85.9" width="100" height="100">
|
||||||
|
<path d="M50 12 L72 42 L86 80 L62 72 L58 88 L42 88 L38 72 L14 80 L28 42 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="42" r="7.5" fill="#ff5533" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
<g fill="#3a4450"><circle cx="42" cy="82" r="3.4"/><circle cx="50" cy="84" r="3.4"/><circle cx="58" cy="82" r="3.4"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 464 B |
6
bin/app/data/icons/items/drive_unit.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="9.2 17.2 81.6 81.6" width="100" height="100">
|
||||||
|
<path d="M40 22 L60 22 L58 42 L72 74 L28 74 L42 42 Z" fill="#4a6ad0" stroke="#101a38" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<ellipse cx="50" cy="74" rx="22" ry="6" fill="#2a3a80" stroke="#101a38" stroke-width="2.5"/>
|
||||||
|
<path d="M40 22 L60 22" stroke="#8aa0e8" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M44 80 Q50 94 56 80" fill="#ffb347"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 464 B |
6
bin/app/data/icons/items/drone_bay_module.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="16.9 15.9 66.2 66.2" width="100" height="100">
|
||||||
|
<rect x="22" y="52" width="56" height="26" rx="5" fill="#7a2a9a" stroke="#331040" stroke-width="3.5"/>
|
||||||
|
<line x1="22" y1="65" x2="78" y2="65" stroke="#4a1560" stroke-width="2.5"/>
|
||||||
|
<path d="M50 20 L61 42 L50 35 L39 42 Z" fill="#cc66ff" stroke="#331040" stroke-width="3" stroke-linejoin="round"/>
|
||||||
|
<line x1="50" y1="44" x2="50" y2="52" stroke="#331040" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 481 B |
9
bin/app/data/icons/items/drone_hangar_module.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="13.7 12.7 72.6 72.6" width="100" height="100">
|
||||||
|
<rect x="18" y="50" width="64" height="30" rx="5" fill="#5a1c7a" stroke="#260c33" stroke-width="3.5"/>
|
||||||
|
<line x1="18" y1="64" x2="82" y2="64" stroke="#3a1050" stroke-width="2.5"/>
|
||||||
|
<g fill="#b060e0" stroke="#260c33" stroke-width="2.5" stroke-linejoin="round">
|
||||||
|
<path d="M32 22 L40 38 L32 33 L24 38 Z"/>
|
||||||
|
<path d="M50 18 L58 34 L50 29 L42 34 Z"/>
|
||||||
|
<path d="M68 22 L76 38 L68 33 L60 38 Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 512 B |
4
bin/app/data/icons/items/drone_hull.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="20.8 19.8 58.3 58.3" width="100" height="100">
|
||||||
|
<path d="M50 24 L66 74 L50 64 L34 74 Z" fill="#8f9bb0" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="44" r="6" fill="#3366ff" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 305 B |
4
bin/app/data/icons/items/frigate_hull.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="10.2 9.2 79.5 79.5" width="100" height="100">
|
||||||
|
<path d="M50 14 L74 84 L50 70 L26 84 Z" fill="#9fb3c9" stroke="#232a33" stroke-width="4" stroke-linejoin="round"/>
|
||||||
|
<circle cx="50" cy="40" r="7" fill="#44aaff" stroke="#232a33" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 304 B |
8
bin/app/data/icons/items/hardened_steel.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.5 15.5 68.9 68.9" width="100" height="100">
|
||||||
|
<rect x="20" y="26" width="60" height="48" rx="6" fill="#6a7280" stroke="#181c22" stroke-width="4"/>
|
||||||
|
<rect x="29" y="35" width="42" height="30" rx="3" fill="#7c8593" stroke="#181c22" stroke-width="2.5"/>
|
||||||
|
<g fill="#2a2f38">
|
||||||
|
<circle cx="26" cy="32" r="3.2"/><circle cx="74" cy="32" r="3.2"/>
|
||||||
|
<circle cx="26" cy="68" r="3.2"/><circle cx="74" cy="68" r="3.2"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 481 B |
8
bin/app/data/icons/items/iron_ingot.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="24.0 28.0 62.0 62.0" width="100" height="100">
|
||||||
|
<g stroke="#2f343b" stroke-width="3.5" stroke-linejoin="round">
|
||||||
|
<path d="M28 72 L72 72 L64 54 L36 54 Z" fill="#a2a9b0"/>
|
||||||
|
<path d="M64 54 L72 72 L82 64 L74 46 Z" fill="#868d95"/>
|
||||||
|
<path d="M36 54 L64 54 L74 46 L46 46 Z" fill="#c8ced4"/>
|
||||||
|
</g>
|
||||||
|
<path d="M40 63 L60 63" stroke="#c2c7cd" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 444 B |
10
bin/app/data/icons/items/iron_ore.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="11.5 11.5 79.0 79.0" width="100" height="100">
|
||||||
|
<path d="M50 16 L76 28 L86 54 L70 84 L36 86 L16 58 L24 30 Z"
|
||||||
|
fill="#9a8a7a" stroke="#241c16" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<path d="M50 16 L58 47 L24 30 Z" fill="#b4a692"/>
|
||||||
|
<path d="M58 47 L70 84 L36 86 Z" fill="#7a6b5c"/>
|
||||||
|
<path d="M50 16 L58 47 M58 47 L86 54 M58 47 L70 84 M58 47 L36 86 M58 47 L24 30"
|
||||||
|
fill="none" stroke="#4a3d31" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
<circle cx="41" cy="56" r="4" fill="#c8752e"/>
|
||||||
|
<circle cx="62" cy="66" r="3.2" fill="#c8752e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 618 B |
11
bin/app/data/icons/items/maneuvering_thrusters_module.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="17.9 23.9 64.1 64.1" width="100" height="100">
|
||||||
|
<rect x="30" y="28" width="40" height="30" rx="5" fill="#5090e0" stroke="#142438" stroke-width="3.5"/>
|
||||||
|
<g fill="#3a6ab0" stroke="#142438" stroke-width="2.5" stroke-linejoin="round">
|
||||||
|
<path d="M34 58 L30 72 L44 72 L40 58 Z"/>
|
||||||
|
<path d="M60 58 L56 72 L70 72 L66 58 Z"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#ffcf6a">
|
||||||
|
<path d="M33 72 L37 84 L41 72 Z"/>
|
||||||
|
<path d="M59 72 L63 84 L67 72 Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 494 B |
7
bin/app/data/icons/items/quartz.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="8.6 7.6 82.9 82.9" width="100" height="100">
|
||||||
|
<path d="M50 12 L70 40 L58 86 L42 86 L30 40 Z" fill="#dcd3f0" stroke="#40345a" stroke-width="3.2" stroke-linejoin="round"/>
|
||||||
|
<path d="M30 40 L70 40" stroke="#40345a" stroke-width="2.2"/>
|
||||||
|
<path d="M50 12 L50 86 M42 40 L42 86 M58 40 L58 86" stroke="#9d90c2" stroke-width="1.8" stroke-linecap="round"/>
|
||||||
|
<path d="M58 40 L70 40 L58 86 Z" fill="#c3b8e2"/>
|
||||||
|
<circle cx="45" cy="29" r="3" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 507 B |
10
bin/app/data/icons/items/railgun_l_module.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7.2 8.4 91.2 91.2" width="100" height="100">
|
||||||
|
<rect x="22" y="76" width="22" height="8" rx="2" fill="#3f454d" stroke="#20242a" stroke-width="3"/>
|
||||||
|
<rect x="12" y="24" width="32" height="52" rx="6" fill="#565d66" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<rect x="42" y="30" width="46" height="10" rx="2" fill="#6b727b" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<rect x="42" y="45" width="46" height="10" rx="2" fill="#6b727b" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<rect x="42" y="60" width="46" height="10" rx="2" fill="#6b727b" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<circle cx="88" cy="35" r="5.5" fill="#e23b2b" stroke="#20242a" stroke-width="2.5"/>
|
||||||
|
<circle cx="88" cy="50" r="5.5" fill="#e23b2b" stroke="#20242a" stroke-width="2.5"/>
|
||||||
|
<circle cx="88" cy="65" r="5.5" fill="#e23b2b" stroke="#20242a" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 884 B |
8
bin/app/data/icons/items/railgun_m_module.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="9.2 8.2 89.6 89.6" width="100" height="100">
|
||||||
|
<rect x="22" y="68" width="20" height="8" rx="2" fill="#3f454d" stroke="#20242a" stroke-width="3"/>
|
||||||
|
<rect x="14" y="30" width="30" height="38" rx="6" fill="#565d66" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<rect x="42" y="38" width="46" height="11" rx="2" fill="#6b727b" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<rect x="42" y="51" width="46" height="11" rx="2" fill="#6b727b" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<circle cx="88" cy="43.5" r="6" fill="#e23b2b" stroke="#20242a" stroke-width="2.5"/>
|
||||||
|
<circle cx="88" cy="56.5" r="6" fill="#e23b2b" stroke="#20242a" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 692 B |
7
bin/app/data/icons/items/railgun_s_module.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="11.3 9.6 85.9 85.9" width="100" height="100">
|
||||||
|
<rect x="24" y="62" width="20" height="9" rx="2" fill="#3f454d" stroke="#20242a" stroke-width="3"/>
|
||||||
|
<rect x="16" y="34" width="30" height="30" rx="6" fill="#565d66" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<rect x="42" y="43" width="44" height="12" rx="2" fill="#6b727b" stroke="#20242a" stroke-width="3.5"/>
|
||||||
|
<path d="M45 46.5 L84 46.5 M45 51.5 L84 51.5" stroke="#e0b12a" stroke-width="2.6" stroke-linecap="round"/>
|
||||||
|
<circle cx="86" cy="49" r="6.5" fill="#e23b2b" stroke="#20242a" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 610 B |
8
bin/app/data/icons/items/repair_tool_module.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="9.3 10.3 78.4 78.4" width="100" height="100">
|
||||||
|
<line x1="30" y1="30" x2="66" y2="66" stroke="#2e9ba3" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<rect x="16" y="20" width="20" height="12" rx="3" fill="#1c6f76" stroke="#0f4247" stroke-width="2.5" transform="rotate(45 26 26)"/>
|
||||||
|
<path d="M64 64 L76 76 L72 80 L60 68 Z" fill="#bfe3e6" stroke="#0f4247" stroke-width="2"/>
|
||||||
|
<line x1="70" y1="30" x2="34" y2="66" stroke="#2e9ba3" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<circle cx="72" cy="28" r="9" fill="none" stroke="#2e9ba3" stroke-width="7"/>
|
||||||
|
<circle cx="32" cy="68" r="9" fill="none" stroke="#2e9ba3" stroke-width="7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 687 B |
10
bin/app/data/icons/items/salvager_module.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="10.8 9.3 78.4 78.4" width="100" height="100">
|
||||||
|
<circle cx="50" cy="28" r="13" fill="#b2cfdd" stroke="#1c4a30" stroke-width="3.5"/>
|
||||||
|
<circle cx="50" cy="28" r="5" fill="#5f8f78"/>
|
||||||
|
<path d="M50 41 L50 56" stroke="#1c4a30" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<g fill="none" stroke="#5f8f78" stroke-width="6" stroke-linecap="round">
|
||||||
|
<path d="M50 54 L30 78"/>
|
||||||
|
<path d="M50 54 L50 82"/>
|
||||||
|
<path d="M50 54 L70 78"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 494 B |
5
bin/app/data/icons/items/scrap.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.8 20.8 70.5 70.5" width="100" height="100">
|
||||||
|
<path d="M20 40 L44 30 L52 44 L74 34 L82 58 L60 66 L66 82 L38 78 L28 62 Z" fill="#8a8078" stroke="#241f1a" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<circle cx="46" cy="52" r="4" fill="#241f1a"/>
|
||||||
|
<path d="M34 46 L44 60 M58 50 L66 62" stroke="#5f574f" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 407 B |
9
bin/app/data/icons/items/sensor_booster_module.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="23.2 8.7 71.5 71.5" width="100" height="100">
|
||||||
|
<path d="M28 76 L46 40 A28 28 0 0 1 76 56 Z" fill="#40a0ff" stroke="#102840" stroke-width="3.5" stroke-linejoin="round"/>
|
||||||
|
<line x1="52" y1="52" x2="70" y2="30" stroke="#102840" stroke-width="3.5" stroke-linecap="round"/>
|
||||||
|
<circle cx="71" cy="29" r="5" fill="#bfe0ff" stroke="#102840" stroke-width="2.5"/>
|
||||||
|
<g stroke="#bfe0ff" stroke-width="3" fill="none" stroke-linecap="round">
|
||||||
|
<path d="M78 20 a12 12 0 0 1 6 10"/>
|
||||||
|
<path d="M82 13 a20 20 0 0 1 8 17"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 576 B |
6
bin/app/data/icons/items/silicon.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="12.4 14.4 75.3 75.3" width="100" height="100">
|
||||||
|
<circle cx="50" cy="52" r="33" fill="#33415e" stroke="#0e1420" stroke-width="3.5"/>
|
||||||
|
<line x1="35" y1="27" x2="52" y2="22" stroke="#0e1420" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<circle cx="50" cy="52" r="21" fill="none" stroke="#46567a" stroke-width="2.6"/>
|
||||||
|
<circle cx="42" cy="44" r="5" fill="#5d6f96"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 420 B |
8
bin/app/data/icons/items/steel_plate.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.8 15.8 68.4 68.4" width="100" height="100">
|
||||||
|
<rect x="20" y="30" width="60" height="40" rx="5" fill="#8a92a0" stroke="#22262c" stroke-width="3.5"/>
|
||||||
|
<line x1="26" y1="38" x2="74" y2="38" stroke="#aab1bb" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<g fill="#2f343b">
|
||||||
|
<circle cx="30" cy="40" r="3"/><circle cx="70" cy="40" r="3"/>
|
||||||
|
<circle cx="30" cy="60" r="3"/><circle cx="70" cy="60" r="3"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 469 B |
8
bin/app/data/icons/items/voidsteel.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="24.0 28.0 62.0 62.0" width="100" height="100">
|
||||||
|
<g stroke="#140a20" stroke-width="3.5" stroke-linejoin="round">
|
||||||
|
<path d="M28 72 L72 72 L64 54 L36 54 Z" fill="#3a2c52"/>
|
||||||
|
<path d="M64 54 L72 72 L82 64 L74 46 Z" fill="#291d3d"/>
|
||||||
|
<path d="M36 54 L64 54 L74 46 L46 46 Z" fill="#4d3a6e"/>
|
||||||
|
</g>
|
||||||
|
<path d="M40 63 L60 63" stroke="#8f6fc4" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 444 B |
8
bin/app/data/icons/items/voidsteel_plate.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.5 15.5 68.9 68.9" width="100" height="100">
|
||||||
|
<rect x="20" y="28" width="60" height="44" rx="6" fill="#7a5aaa" stroke="#1c1038" stroke-width="4"/>
|
||||||
|
<line x1="26" y1="37" x2="74" y2="37" stroke="#a98fd0" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<g fill="#180c30">
|
||||||
|
<circle cx="30" cy="39" r="3"/><circle cx="70" cy="39" r="3"/>
|
||||||
|
<circle cx="30" cy="61" r="3"/><circle cx="70" cy="61" r="3"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 467 B |
8
bin/app/data/icons/items/weapon_primer_module.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="17.0 16.0 65.9 65.9" width="100" height="100">
|
||||||
|
<g stroke="#380e0e" stroke-width="3.2" stroke-linejoin="round">
|
||||||
|
<path d="M42 20 L58 20 L58 58 L50 72 L42 58 Z" fill="#e03838"/>
|
||||||
|
<rect x="40" y="56" width="20" height="22" rx="2" fill="#c9a227"/>
|
||||||
|
</g>
|
||||||
|
<line x1="41" y1="63" x2="59" y2="63" stroke="#7a5f14" stroke-width="2.5"/>
|
||||||
|
<line x1="41" y1="71" x2="59" y2="71" stroke="#7a5f14" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 471 B |
7
bin/app/data/icons/items/weapon_stabilizer_module.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7.1 7.1 85.9 85.9" width="100" height="100">
|
||||||
|
<circle cx="50" cy="50" r="32" fill="none" stroke="#300c0c" stroke-width="8"/>
|
||||||
|
<circle cx="50" cy="50" r="32" fill="none" stroke="#c03030" stroke-width="5"/>
|
||||||
|
<ellipse cx="50" cy="50" rx="32" ry="12" fill="none" stroke="#e06060" stroke-width="4"/>
|
||||||
|
<line x1="50" y1="14" x2="50" y2="86" stroke="#c03030" stroke-width="4"/>
|
||||||
|
<circle cx="50" cy="50" r="6" fill="#c03030" stroke="#300c0c" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 513 B |
7
bin/app/data/icons/items/weapon_upgrade_module.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15.5 15.5 68.9 68.9" width="100" height="100">
|
||||||
|
<rect x="24" y="22" width="52" height="56" rx="10" fill="#7a1c1c" stroke="#3a0e0e" stroke-width="3.5"/>
|
||||||
|
<g fill="none" stroke="#ff6a6a" stroke-width="8" stroke-linejoin="round" stroke-linecap="round">
|
||||||
|
<path d="M34 52 L50 36 L66 52"/>
|
||||||
|
<path d="M34 66 L50 50 L66 66"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 389 B |
@@ -4,8 +4,7 @@
|
|||||||
# using the verified fitted threat values from tools/threat_report.py:
|
# using the verified fitted threat values from tools/threat_report.py:
|
||||||
# drone 10.5, frigate 47, destroyer 99, cruiser 233.5,
|
# drone 10.5, frigate 47, destroyer 99, cruiser 233.5,
|
||||||
# battlecruiser 354.5, battleship 722.5, dreadnought 1491.5,
|
# battlecruiser 354.5, battleship 722.5, dreadnought 1491.5,
|
||||||
# carrier 1436.5, glass destroyer (8 small guns) 92, repair drone 17,
|
# carrier 1436.5, glass destroyer (8 small guns) 92, repair drone 17.
|
||||||
# railgun_s-spam cruiser (12 small guns) 178.
|
|
||||||
# Module arrays mirror the ships' default_modules loadouts unless a
|
# Module arrays mirror the ships' default_modules loadouts unless a
|
||||||
# doctrine variant is the point of the arena.
|
# doctrine variant is the point of the arena.
|
||||||
#
|
#
|
||||||
@@ -349,50 +348,6 @@ enemy_buffer_width_tiles = 10
|
|||||||
{type = "railgun_s", x = 4, y = 1, rotation = "east"},
|
{type = "railgun_s", x = 4, y = 1, rotation = "east"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[arena]]
|
|
||||||
name = "Railgun_s-spam cruisers vs default cruisers (1424 vs 1401)"
|
|
||||||
# Tracks the playtest-1 meta: cruiser hulls filled with 12 small guns
|
|
||||||
# (max DPS/threat, no armor, range 50) against the default m-gun fit.
|
|
||||||
# The concentration tax means the spam side SHOULD win a brawl somewhat;
|
|
||||||
# this arena bounds its margin — a blowout here means the small-gun
|
|
||||||
# premium or the m-gun range edge needs retuning.
|
|
||||||
height_tiles = 10
|
|
||||||
player_buffer_width_tiles = 10
|
|
||||||
contest_zone_width_tiles = 50
|
|
||||||
enemy_buffer_width_tiles = 10
|
|
||||||
|
|
||||||
[[arena.team]]
|
|
||||||
name = "Spam"
|
|
||||||
[[arena.team.ship]]
|
|
||||||
schematic = "cruiser"
|
|
||||||
count = 8
|
|
||||||
modules = [
|
|
||||||
{type = "railgun_s", x = 1, y = 0, rotation = "east"},
|
|
||||||
{type = "railgun_s", x = 2, y = 0, rotation = "east"},
|
|
||||||
{type = "railgun_s", x = 0, y = 1, rotation = "east"},
|
|
||||||
{type = "railgun_s", x = 1, y = 1, rotation = "east"},
|
|
||||||
{type = "railgun_s", x = 2, y = 1, rotation = "east"},
|
|
||||||
{type = "railgun_s", x = 3, y = 1, rotation = "east"},
|
|
||||||
{type = "railgun_s", x = 0, y = 2, rotation = "east"},
|
|
||||||
{type = "railgun_s", x = 1, y = 2, rotation = "east"},
|
|
||||||
{type = "railgun_s", x = 2, y = 2, rotation = "east"},
|
|
||||||
{type = "railgun_s", x = 3, y = 2, rotation = "east"},
|
|
||||||
{type = "railgun_s", x = 1, y = 3, rotation = "east"},
|
|
||||||
{type = "railgun_s", x = 2, y = 3, rotation = "east"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[arena.team]]
|
|
||||||
name = "Default"
|
|
||||||
[[arena.team.ship]]
|
|
||||||
schematic = "cruiser"
|
|
||||||
count = 6
|
|
||||||
modules = [
|
|
||||||
{type = "railgun_m", x = 0, y = 1, rotation = "east"},
|
|
||||||
{type = "railgun_m", x = 2, y = 1, rotation = "east"},
|
|
||||||
{type = "armor_plates", x = 1, y = 0, rotation = "east"},
|
|
||||||
{type = "maneuvering_thrusters", x = 1, y = 3, rotation = "east"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[arena]]
|
[[arena]]
|
||||||
name = "Repair escort vs raw numbers (444 vs 444)"
|
name = "Repair escort vs raw numbers (444 vs 444)"
|
||||||
height_tiles = 10
|
height_tiles = 10
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ id = "salvage_bay"
|
|||||||
cost = 25
|
cost = 25
|
||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 15
|
construction_time_seconds = 15
|
||||||
|
output_buffer_capacity = 20
|
||||||
|
tooltip = "Drop-off point for salvage ships."
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"SAA",
|
"SAA",
|
||||||
"SAA>",
|
"SAA>",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[[module]]
|
[[module]]
|
||||||
id = "armor_plate"
|
id = "armor_plate"
|
||||||
unlock_at_station_level = -1
|
tooltip = "Adds a large flat bonus to hit points."
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "iron_ingot", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
production_time_seconds = 3
|
production_time_seconds = 3
|
||||||
@@ -12,7 +12,6 @@ multiplied_hp = 1.5
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "sensor_booster"
|
id = "sensor_booster"
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "circuit_board", amount = 1}]
|
materials = [{item = "circuit_board", amount = 1}]
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
@@ -24,7 +23,6 @@ added_sensor_range_m = 100
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_upgrade"
|
id = "weapon_upgrade"
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
@@ -36,7 +34,6 @@ multiplied_damage = 1.2
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "laser_cannon"
|
id = "laser_cannon"
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
@@ -50,7 +47,6 @@ attack_rate_hz = 2.0
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "salvager"
|
id = "salvager"
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "iron_ingot", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
@@ -64,7 +60,6 @@ collection_rate_hz = 0.5
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "repair_tool"
|
id = "repair_tool"
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "circuit_board", amount = 2}]
|
materials = [{item = "circuit_board", amount = 2}]
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
@@ -78,7 +73,6 @@ repair_range_m = 800
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_primer"
|
id = "weapon_primer"
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
@@ -90,7 +84,6 @@ multiplied_attack_rate_hz = 1.2
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_stabilizer"
|
id = "weapon_stabilizer"
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
@@ -103,7 +96,6 @@ multiplied_attack_rate_hz = 0.8
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "afterburner"
|
id = "afterburner"
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
@@ -116,7 +108,6 @@ added_main_acceleration_mpss = 60
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "maneuvering_thrusters"
|
id = "maneuvering_thrusters"
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ duration_seconds = 4.0
|
|||||||
[[recipe]]
|
[[recipe]]
|
||||||
id = "premium_circuit"
|
id = "premium_circuit"
|
||||||
building = "assembler"
|
building = "assembler"
|
||||||
unlock_at_station_level = -1
|
unlocked_at_start = true
|
||||||
inputs = [{item = "circuit_board", amount = 1}]
|
inputs = [{item = "circuit_board", amount = 1}]
|
||||||
outputs = [{item = "premium_circuit", amount = 1}]
|
outputs = [{item = "premium_circuit", amount = 1}]
|
||||||
duration_seconds = 8.0
|
duration_seconds = 8.0
|
||||||
@@ -51,7 +51,6 @@ duration_seconds = 8.0
|
|||||||
[[recipe]]
|
[[recipe]]
|
||||||
id = "quick_circuit"
|
id = "quick_circuit"
|
||||||
building = "assembler"
|
building = "assembler"
|
||||||
unlock_at_station_level = 0
|
|
||||||
inputs = [{item = "copper_ingot", amount = 3}]
|
inputs = [{item = "copper_ingot", amount = 3}]
|
||||||
outputs = [{item = "circuit_board", amount = 1}]
|
outputs = [{item = "circuit_board", amount = 1}]
|
||||||
duration_seconds = 3.0
|
duration_seconds = 3.0
|
||||||
@@ -59,7 +58,6 @@ duration_seconds = 3.0
|
|||||||
[[recipe]]
|
[[recipe]]
|
||||||
id = "advanced_circuit"
|
id = "advanced_circuit"
|
||||||
building = "assembler"
|
building = "assembler"
|
||||||
unlock_at_station_level = 1
|
|
||||||
inputs = [{item = "iron_ingot", amount = 5}]
|
inputs = [{item = "iron_ingot", amount = 5}]
|
||||||
outputs = [{item = "circuit_board", amount = 1}]
|
outputs = [{item = "circuit_board", amount = 1}]
|
||||||
duration_seconds = 6.0
|
duration_seconds = 6.0
|
||||||
@@ -67,7 +65,6 @@ duration_seconds = 6.0
|
|||||||
[[recipe]]
|
[[recipe]]
|
||||||
id = "exotic_alloy"
|
id = "exotic_alloy"
|
||||||
building = "assembler"
|
building = "assembler"
|
||||||
unlock_at_station_level = 0
|
|
||||||
inputs = [{item = "exotic_ore", amount = 2}]
|
inputs = [{item = "exotic_ore", amount = 2}]
|
||||||
outputs = [{item = "exotic_alloy", amount = 1}]
|
outputs = [{item = "exotic_alloy", amount = 1}]
|
||||||
duration_seconds = 10.0
|
duration_seconds = 10.0
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
[[ship]]
|
[[ship]]
|
||||||
id = "interceptor"
|
id = "interceptor"
|
||||||
unlock_at_station_level = -1
|
|
||||||
layout = ["XOX", "OOO", "XOX"]
|
layout = ["XOX", "OOO", "XOX"]
|
||||||
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
||||||
|
|
||||||
@@ -24,7 +23,6 @@ sensor_range_m = 2000
|
|||||||
|
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "destroyer"
|
id = "destroyer"
|
||||||
unlock_at_station_level = -1
|
|
||||||
layout = ["XOOX", "OOOO", "XOOX"]
|
layout = ["XOOX", "OOOO", "XOOX"]
|
||||||
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
||||||
|
|
||||||
@@ -48,7 +46,6 @@ sensor_range_m = 3000
|
|||||||
|
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "salvage_ship"
|
id = "salvage_ship"
|
||||||
unlock_at_station_level = -1
|
|
||||||
layout = ["OOO", "OOO"]
|
layout = ["OOO", "OOO"]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
@@ -71,7 +68,6 @@ sensor_range_m = 2500
|
|||||||
|
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "repair_ship"
|
id = "repair_ship"
|
||||||
unlock_at_station_level = 0
|
|
||||||
layout = ["XOX", "OOO", "XOX"]
|
layout = ["XOX", "OOO", "XOX"]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
|
|||||||
21
bin/test/data/config/unlocks.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Unlock groups for the test config (REQ-LOCK-EXPLICIT). Mirrors the previous
|
||||||
|
# per-item gating: repair_ship, quick_circuit, advanced_circuit start locked and
|
||||||
|
# are awarded via defence station drops. exotic_alloy is intentionally NOT here:
|
||||||
|
# it stays implicitly gated (its output/inputs are unreachable), so it is never
|
||||||
|
# unlocked. premium_circuit uses unlocked_at_start in recipes.toml. Everything
|
||||||
|
# else (interceptor, destroyer, salvage_ship, all modules) starts unlocked.
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "repair_ship"
|
||||||
|
station_level = 0
|
||||||
|
ships = ["repair_ship"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "quick_circuit"
|
||||||
|
station_level = 0
|
||||||
|
recipes = ["quick_circuit"]
|
||||||
|
|
||||||
|
[[unlock]]
|
||||||
|
id = "advanced_circuit"
|
||||||
|
station_level = 1
|
||||||
|
recipes = ["advanced_circuit"]
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
[world]
|
[world]
|
||||||
height_tiles = 60
|
height_tiles = 60
|
||||||
refund_percentage = 75
|
refund_percentage = 75
|
||||||
|
deconstruction_time_seconds = 0.1
|
||||||
starting_building_blocks = 100
|
starting_building_blocks = 100
|
||||||
scrap_despawn_seconds = 30
|
debris_despawn_seconds = 30
|
||||||
scrap_per_threat = 1.0
|
scrap_per_threat = 1.0
|
||||||
tile_size_m = 10
|
tile_size_m = 10
|
||||||
belt_speed_mps = 20
|
belt_speed_mps = 20
|
||||||
@@ -10,6 +11,8 @@ tunnel_max_distance_tiles = 10
|
|||||||
departure_interval_seconds = 20
|
departure_interval_seconds = 20
|
||||||
orbit_factor = 0.8
|
orbit_factor = 0.8
|
||||||
rally_orbit_radius_tiles = 5.0
|
rally_orbit_radius_tiles = 5.0
|
||||||
|
building_blocks_tooltip = "Spend building blocks to build; deliver them to the HQ to gain more."
|
||||||
|
artifact_tooltip = "Choose the artifact reward when destroying enemy stations; collect enough to win."
|
||||||
|
|
||||||
[regions]
|
[regions]
|
||||||
asteroid_width_tiles = 40
|
asteroid_width_tiles = 40
|
||||||
@@ -17,6 +20,11 @@ player_buffer_width_tiles = 10
|
|||||||
contest_zone_width_tiles = 30
|
contest_zone_width_tiles = 30
|
||||||
enemy_buffer_width_tiles = 15
|
enemy_buffer_width_tiles = 15
|
||||||
|
|
||||||
|
[scroll]
|
||||||
|
pan_speed_slow_tiles_per_second = 8.0
|
||||||
|
pan_speed_fast_tiles_per_second = 24.0
|
||||||
|
pan_ramp_band_width_tiles = 16
|
||||||
|
|
||||||
[expansion]
|
[expansion]
|
||||||
columns_per_expansion_tiles = 10
|
columns_per_expansion_tiles = 10
|
||||||
cost_building_blocks_formula = "400 * 2^x"
|
cost_building_blocks_formula = "400 * 2^x"
|
||||||
|
|||||||
60
cmake/version.cmake
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# simple check for a git repo
|
||||||
|
if(EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||||
|
find_package(Git)
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_BRANCH
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} log -1 --format=%h
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_COMMIT_HASH
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} log -1 --format=%ci
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_COMMIT_TIME
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} describe --long --match "[0-9]*" HEAD
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_VERSION_NUMBER
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
string(REGEX REPLACE "^([0-9]+)\\..*" "\\1" VERSION_MAJOR "${GIT_VERSION_NUMBER}")
|
||||||
|
string(REGEX REPLACE "^[0-9]+\\.([0-9]+).*" "\\1" VERSION_MINOR "${GIT_VERSION_NUMBER}")
|
||||||
|
string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" VERSION_PATCH "${GIT_VERSION_NUMBER}")
|
||||||
|
string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+).*" "\\1" VERSION_COMMIT "${GIT_VERSION_NUMBER}")
|
||||||
|
|
||||||
|
else(EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||||
|
set(GIT_BRANCH "")
|
||||||
|
set(GIT_COMMIT_HASH "")
|
||||||
|
set(GIT_VERSION_NUMBER "")
|
||||||
|
set(VERSION_MAJOR "0")
|
||||||
|
set(VERSION_MINOR "0")
|
||||||
|
set(VERSION_PATCH "0")
|
||||||
|
set(VERSION_COMMIT "0")
|
||||||
|
set(BUILD_TYPE "")
|
||||||
|
|
||||||
|
endif(EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||||
|
|
||||||
|
set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}.${VERSION_COMMIT}")
|
||||||
|
|
||||||
|
message(STATUS "Version: ${VERSION_STRING}")
|
||||||
|
|
||||||
|
# message(STATUS "Git current branch: ${GIT_BRANCH}")
|
||||||
|
# message(STATUS "Git version number: " ${GIT_VERSION_NUMBER} )
|
||||||
|
# message(STATUS "Git commit hash: ${GIT_COMMIT_HASH}")
|
||||||
|
# message(STATUS "Git commit time: ${GIT_COMMIT_TIME}")
|
||||||
|
# message(STATUS "Version major: ${VERSION_MAJOR}")
|
||||||
|
# message(STATUS "Version minor: ${VERSION_MINOR}")
|
||||||
|
# message(STATUS "Version patch: ${VERSION_PATCH}")
|
||||||
|
# message(STATUS "Version commit: ${VERSION_COMMIT}")
|
||||||
35
cmake/version.rc.in
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Windows version resource. Generated by CMake via configure_file() from this
|
||||||
|
// template; @VAR@ placeholders are filled from cmake/version.cmake (version
|
||||||
|
// numbers) and the product identity variables in the top-level CMakeLists.txt.
|
||||||
|
// Shows up on the executable's Details tab (right-click -> Properties).
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION @VERSION_MAJOR@,@VERSION_MINOR@,@VERSION_PATCH@,@VERSION_COMMIT@
|
||||||
|
PRODUCTVERSION @VERSION_MAJOR@,@VERSION_MINOR@,@VERSION_PATCH@,@VERSION_COMMIT@
|
||||||
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
FILEOS VOS_NT_WINDOWS32
|
||||||
|
FILETYPE VFT_APP
|
||||||
|
FILESUBTYPE VFT2_UNKNOWN
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904b0" // US English (0x0409), Unicode (0x04b0)
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "@PRODUCT_COMPANY@"
|
||||||
|
VALUE "FileDescription", "@PRODUCT_DISPLAY_NAME@"
|
||||||
|
VALUE "FileVersion", "@VERSION_STRING@"
|
||||||
|
VALUE "InternalName", "@PRODUCT_NAME@"
|
||||||
|
VALUE "OriginalFilename", "@PRODUCT_NAME@.exe"
|
||||||
|
VALUE "ProductName", "@PRODUCT_DISPLAY_NAME@"
|
||||||
|
VALUE "ProductVersion", "@VERSION_STRING@"
|
||||||
|
VALUE "LegalCopyright", "@PRODUCT_COPYRIGHT@"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1200 // 0x409 = en-US, 1200 = Unicode code page
|
||||||
|
END
|
||||||
|
END
|
||||||
@@ -15,7 +15,7 @@ This document captures the architectural decisions for the project. It is a comp
|
|||||||
A strict separation between the game simulation and the Qt Widgets UI.
|
A strict separation between the game simulation and the Qt Widgets UI.
|
||||||
|
|
||||||
- The **simulation** is a pure C++ library that depends only on Qt Core and Qt Gui (QPoint, QVector2D, QRect, etc., as required by the coding guidelines), toml++, and tinyexpr. It contains no QtWidgets, no painting, and no QApplication. Note: in Qt 5, vector math types such as QVector2D live in Qt::Gui rather than Qt::Core, so the lib links both.
|
- The **simulation** is a pure C++ library that depends only on Qt Core and Qt Gui (QPoint, QVector2D, QRect, etc., as required by the coding guidelines), toml++, and tinyexpr. It contains no QtWidgets, no painting, and no QApplication. Note: in Qt 5, vector math types such as QVector2D live in Qt::Gui rather than Qt::Core, so the lib links both.
|
||||||
- The **UI** reads simulation state and renders it. It owns all widgets, painting, and input handling, and drives the simulation via a small command interface (place building, demolish, clear belt tiles, change recipe, set game speed, etc.).
|
- The **UI** reads simulation state and renders it. It owns all widgets, painting, and input handling, and drives the simulation via a small command interface (place building, deconstruct, clear belt tiles, change recipe, set game speed, etc.).
|
||||||
|
|
||||||
This split is enforced at the CMake target level (see below). Tests link only against the simulation library and run without a display server.
|
This split is enforced at the CMake target level (see below). Tests link only against the simulation library and run without a display server.
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ The simulation advances in discrete ticks. All game quantities — production ti
|
|||||||
|
|
||||||
- Tick rate: fixed at 30 Hz; `tickDurationMs = 1000 / 30 ≈ 33.33`.
|
- Tick rate: fixed at 30 Hz; `tickDurationMs = 1000 / 30 ≈ 33.33`.
|
||||||
- Ticks are driven by an accumulator that is independent of the render rate. Each render frame, the driver adds `elapsedWallMs × gameSpeedMultiplier` to an accumulator and flushes one `tick()` per `tickDurationMs` of accumulated time (so multiple sim ticks may run between frames at high speeds, or a frame may run no ticks at low speeds). `gameSpeedMultiplier` ∈ {0, 0.5, 1, 2, 4} per REQ-UI-SPEED; 0× freezes the accumulator (pause). The concrete driver lives in the Rendering section.
|
- Ticks are driven by an accumulator that is independent of the render rate. Each render frame, the driver adds `elapsedWallMs × gameSpeedMultiplier` to an accumulator and flushes one `tick()` per `tickDurationMs` of accumulated time (so multiple sim ticks may run between frames at high speeds, or a frame may run no ticks at low speeds). `gameSpeedMultiplier` ∈ {0, 0.5, 1, 2, 4} per REQ-UI-SPEED; 0× freezes the accumulator (pause). The concrete driver lives in the Rendering section.
|
||||||
- Config-level durations given in seconds (recipe durations, wave gap ranges, scrap despawn, etc.) are converted to ticks at config-load time.
|
- Config-level durations given in seconds (recipe durations, wave gap ranges, debris despawn, etc.) are converted to ticks at config-load time.
|
||||||
|
|
||||||
Consequences: determinism, replayability, and the time-scale feature fall out for free. The simulation advances the same number of ticks over the same amount of game-time regardless of whether the game renders at 60 FPS, 30 FPS, or a stuttery mix.
|
Consequences: determinism, replayability, and the time-scale feature fall out for free. The simulation advances the same number of ticks over the same amount of game-time regardless of whether the game renders at 60 FPS, 30 FPS, or a stuttery mix.
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ See REQ-GW-COORDS for the authoritative tile-coordinate convention. This section
|
|||||||
|
|
||||||
- Tile coordinates are `QPoint(x, y)`. Origin `(0, 0)` is the first space tile (just right of the asteroid's right edge at game start). X grows right; Y grows down.
|
- Tile coordinates are `QPoint(x, y)`. Origin `(0, 0)` is the first space tile (just right of the asteroid's right edge at game start). X grows right; Y grows down.
|
||||||
- Asteroid tiles have `x < 0`. Asteroid left-expansions add tiles at increasingly negative X; the origin never shifts, so existing tile coordinates remain stable across expansions.
|
- Asteroid tiles have `x < 0`. Asteroid left-expansions add tiles at increasingly negative X; the origin never shifts, so existing tile coordinates remain stable across expansions.
|
||||||
- Continuous world positions (ship centers, scrap drops, projectiles) use `QVector2D` in tile units — one tile = 1.0 world unit. A ship center at `QVector2D(-3.5, 4.0)` sits at the center of the tile 3.5 tiles left of the asteroid's right edge and 4 tiles down from the top.
|
- Continuous world positions (ship centers, debris, projectiles) use `QVector2D` in tile units — one tile = 1.0 world unit. A ship center at `QVector2D(-3.5, 4.0)` sits at the center of the tile 3.5 tiles left of the asteroid's right edge and 4 tiles down from the top.
|
||||||
- Rendering multiplies world units by the tile size in pixels (20) at draw time.
|
- Rendering multiplies world units by the tile size in pixels (20) at draw time.
|
||||||
- Ship position always refers to the ship's center — this is the point used for sensor, attack-range, and hit-detection checks.
|
- Ship position always refers to the ship's center — this is the point used for sensor, attack-range, and hit-detection checks.
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ See REQ-GW-COORDS for the authoritative tile-coordinate convention. This section
|
|||||||
|
|
||||||
Simulation types shared across subsystems:
|
Simulation types shared across subsystems:
|
||||||
|
|
||||||
- `EntityId` — strictly increasing integer handle, allocated centrally by the simulation. Assigned to every targetable entity: ships, scrap drops, **and** buildings (including HQ and defence stations). Buildings additionally retain their anchor tile for spatial lookups and placement; the `EntityId` is the canonical reference used by ship-component target fields (`Weapon.currentTarget`, `RepairTool.currentTarget`, `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, debris, **and** buildings (including HQ and defence stations). Buildings additionally retain their anchor tile for spatial lookups and placement; the `EntityId` is the canonical reference used by ship-component target fields (`Weapon.currentTarget`, `RepairTool.currentTarget`, `AttackBehavior.currentTarget`, etc.), so a combat ship can target either another ship or a defence station uniformly.
|
||||||
- `Rotation` — enum `{ North, East, South, West }`. The rotation applied to a building's surface_mask when placed.
|
- `Rotation` — enum `{ North, East, South, West }`. The rotation applied to a building's surface_mask when placed.
|
||||||
- `BuildingType` — enum covering every building type in requirements.md (Miner, Smelter, Assembler, ReprocessingPlant, Shipyard, SalvageBay, Belt, Splitter, Hq, PlayerDefenceStation, EnemyDefenceStation). `Belt` and `Splitter` share the enum for cost, construction, placement, and `visuals.toml` lookup, but their runtime data lives inside the belt subsystem rather than in `Building` instances (see Belt Subsystem).
|
- `BuildingType` — enum covering every building type in requirements.md (Miner, Smelter, Assembler, ReprocessingPlant, Shipyard, SalvageBay, Belt, Splitter, Hq, PlayerDefenceStation, EnemyDefenceStation). `Belt` and `Splitter` share the enum for cost, construction, placement, and `visuals.toml` lookup, but their runtime data lives inside the belt subsystem rather than in `Building` instances (see Belt Subsystem).
|
||||||
- `ItemType` — tagged id of every transportable material (ores, ingots, intermediates, building_blocks, scrap).
|
- `ItemType` — tagged id of every transportable material (ores, ingots, intermediates, building_blocks, scrap).
|
||||||
@@ -93,10 +93,16 @@ Schematic drops: when an enemy station set is destroyed, the simulation generate
|
|||||||
|
|
||||||
### UI Events
|
### UI Events
|
||||||
|
|
||||||
All UI interactions — building selection, builder/blueprint mode transitions, speed changes, demolish mode, escape menu, layout dialog requests — are communicated via EventManager events rather than Qt signals/slots. Each event is a small struct inheriting `Event` (e.g., `SelectionChangedEvent`, `BuildingTypeSelectedEvent`, `SpeedChangeRequestedEvent`). Widgets register as `CombinedEventHandler` for the events they care about and emit events via `EventManager::sendEventImmediately()`.
|
All UI interactions — building selection, builder/blueprint mode transitions, speed changes, deconstruct mode, escape menu, layout dialog requests — are communicated via EventManager events rather than Qt signals/slots. Each event is a small struct inheriting `Event` (e.g., `SelectionChangedEvent`, `BuildingTypeSelectedEvent`, `SpeedChangeRequestedEvent`). Widgets register as `CombinedEventHandler` for the events they care about and emit events via `EventManager::sendEventImmediately()`.
|
||||||
|
|
||||||
Bidirectional interactions use separate request/notification event types to avoid infinite recursion (e.g., `ExitBuilderModeRequestedEvent` from `BuildButtonGrid` → `GameWorldView`, vs. `BuilderModeExitedEvent` from `GameWorldView` → `BuildButtonGrid`).
|
Bidirectional interactions use separate request/notification event types to avoid infinite recursion (e.g., `ExitBuilderModeRequestedEvent` from `BuildButtonGrid` → `GameWorldView`, vs. `BuilderModeExitedEvent` from `GameWorldView` → `BuildButtonGrid`).
|
||||||
|
|
||||||
|
### Reading Simulation State
|
||||||
|
|
||||||
|
The simulation is the single source of truth for every game value (building block stock, expansion cost, threat level, tick, etc.). A UI widget that needs such a value holds the `Simulation*` it was constructed with and **pulls the value on demand** via the corresponding getter (e.g., `m_sim->getBuildingBlocksStock()`), rather than caching its own copy.
|
||||||
|
|
||||||
|
State-change events (e.g., `BuildingBlocksChangedEvent`) are treated as *refresh signals*, not as carriers of truth: a widget subscribes to the event to learn *when* the value changed and then re-reads it from the simulation to learn *what* it now is. The value carried in the event payload is not authoritative and should not be stored. This keeps a single copy of each value and avoids stale-cache bugs (a widget acting on a value that has since moved on because nothing refreshed its local copy).
|
||||||
|
|
||||||
## Tick Order
|
## Tick Order
|
||||||
|
|
||||||
Within a single simulation tick, subsystems run in this fixed order. The order is load-bearing for determinism and for avoiding one-tick-delay artifacts (e.g., items landing on a belt but not advancing in the same tick).
|
Within a single simulation tick, subsystems run in this fixed order. The order is load-bearing for determinism and for avoiding one-tick-delay artifacts (e.g., items landing on a belt but not advancing in the same tick).
|
||||||
@@ -109,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).
|
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.
|
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.
|
8. **Combat resolution** — ships and defence stations validate/acquire targets, fire, apply damage; queue deaths. Each fire appends a `BeamFiredEvent` to the sim's beam-fired-event queue (REQ-SHP-FIRING-BEAM). The repair and salvage module systems (tick step 7d) append their own `BeamFiredEvent`s to the same queue when they start a cycle.
|
||||||
9. **Deaths & loot** — process queued deaths: drop scrap (REQ-RES-SCRAP-DROP); if a full enemy-defence-station set was destroyed this tick, 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 debris (REQ-RES-DEBRIS-DROP); if a full enemy-defence-station set was destroyed this tick, generate up to 3 schematic choice options (REQ-DEF-SCHEMATIC-DROP) stored as pending state for the UI to present; remove entities.
|
||||||
10. **`tickMovement`** — advance ship positions based on final `MovementIntent`.
|
10. **`tickMovement`** — advance ship positions based on final `MovementIntent`.
|
||||||
11. **Scrap despawn** — decrement scrap timers; remove expired scrap (REQ-RES-SCRAP-DROP).
|
11. **Debris despawn** — decrement debris timers; remove expired debris (REQ-RES-DEBRIS-DROP).
|
||||||
|
|
||||||
## CMake Target Layout
|
## CMake Target Layout
|
||||||
|
|
||||||
@@ -187,20 +193,21 @@ struct Building {
|
|||||||
- Belts and splitters are separate types owned by the belt subsystem, not general `Building` instances.
|
- Belts and splitters are separate types owned by the belt subsystem, not general `Building` instances.
|
||||||
- No ECS for buildings. A miner is never also an assembler; there is no composition benefit to decomposing buildings into components.
|
- No ECS for buildings. A miner is never also an assembler; there is no composition benefit to decomposing buildings into components.
|
||||||
|
|
||||||
## Scrap
|
## Debris
|
||||||
|
|
||||||
Scrap is the only non-ship, non-building entity in the simulation:
|
Debris — the salvageable object dropped by destroyed ships and defence stations — is the
|
||||||
|
only non-ship, non-building entity in the simulation. Each piece carries a scrap amount:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
struct Scrap {
|
struct Debris {
|
||||||
EntityId id;
|
EntityId id;
|
||||||
QVector2D position; // world units, tile-fractional; ship-center convention
|
QVector2D position; // world units, tile-fractional; ship-center convention
|
||||||
int amount;
|
int amount; // scrap the piece still holds
|
||||||
Tick despawnAt; // absolute tick at which the scrap is removed
|
Tick despawnAt; // absolute tick at which the debris is removed
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Created in tick step 9 (Deaths & loot) per REQ-RES-SCRAP-DROP, consumed by salvage ships in tick step 7 (ScrapCollector), and removed in tick step 11 when the current tick reaches `despawnAt`.
|
Created in tick step 9 (Deaths & loot) per REQ-RES-DEBRIS-DROP, drained one scrap per cycle by salvage ships in tick step 7 (SalvagerSystem), and removed in tick step 11 when the current tick reaches `despawnAt`.
|
||||||
|
|
||||||
## Ships
|
## Ships
|
||||||
|
|
||||||
@@ -228,7 +235,7 @@ struct RetreatBehavior { float retreatHpFraction; QVector2D retreatPoint;
|
|||||||
struct AttackBehavior { std::optional<EntityId> currentTarget; float score; };
|
struct AttackBehavior { std::optional<EntityId> currentTarget; float score; };
|
||||||
struct RepairBehavior { std::optional<EntityId> currentTarget;
|
struct RepairBehavior { std::optional<EntityId> currentTarget;
|
||||||
float maxRepairRange_tiles; float score; };
|
float maxRepairRange_tiles; float score; };
|
||||||
struct SalvageScrapBehavior { std::optional<QVector2D> scrapTarget;
|
struct SalvageScrapBehavior { std::optional<QVector2D> debrisTarget;
|
||||||
float maxCollectionRange_tiles; float score; };
|
float maxCollectionRange_tiles; float score; };
|
||||||
struct DeliverScrapBehavior { BuildingId deliveryBay; float score; };
|
struct DeliverScrapBehavior { BuildingId deliveryBay; float score; };
|
||||||
struct SelectedBehaviorComponent { BehaviorKind winner; float bestScore; }; // selection result
|
struct SelectedBehaviorComponent { BehaviorKind winner; float bestScore; }; // selection result
|
||||||
@@ -319,7 +326,7 @@ Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly
|
|||||||
4. **Scrap** — glyphs at world positions.
|
4. **Scrap** — glyphs at world positions.
|
||||||
5. **Ships** — colored arrows oriented by velocity; color keyed to role (player combat / salvage / repair / enemy).
|
5. **Ships** — colored arrows oriented by velocity; color keyed to role (player combat / salvage / repair / enemy).
|
||||||
6. **Laser beams** — lines derived from live `BeamFiredEvent`s kept by the renderer for 0.3 s, colored per `BeamKind` (weapon/repair/salvage) (REQ-SHP-FIRING-BEAM).
|
6. **Laser beams** — lines derived from live `BeamFiredEvent`s kept by the renderer for 0.3 s, colored per `BeamKind` (weapon/repair/salvage) (REQ-SHP-FIRING-BEAM).
|
||||||
7. **Build overlays** — ghost in builder mode (REQ-BLD-GHOST), demolish-mode tint, tile highlight under cursor, box-drag selection rectangle.
|
7. **Build overlays** — ghost in builder mode (REQ-BLD-GHOST), deconstruct-mode tint, tile highlight under cursor, box-drag selection rectangle.
|
||||||
8. **Screen-space UI** — screen-anchored elements, drawn after resetting the world-space transform.
|
8. **Screen-space UI** — screen-anchored elements, drawn after resetting the world-space transform.
|
||||||
|
|
||||||
### Coordinates and Scrolling
|
### Coordinates and Scrolling
|
||||||
@@ -366,7 +373,7 @@ width_px = 2
|
|||||||
[overlays]
|
[overlays]
|
||||||
ghost_valid = "#ffffff44"
|
ghost_valid = "#ffffff44"
|
||||||
ghost_invalid = "#ff000044"
|
ghost_invalid = "#ff000044"
|
||||||
demolish_tint = "#ff000033"
|
deconstruct_tint = "#ff000033"
|
||||||
selection_rect = "#00ff00"
|
selection_rect = "#00ff00"
|
||||||
|
|
||||||
[toast]
|
[toast]
|
||||||
|
|||||||
@@ -21,12 +21,8 @@ REQ-* ids in [../requirements.md](../requirements.md).
|
|||||||
|
|
||||||
First full balancing round complete (2026-07-06): targets → tree →
|
First full balancing round complete (2026-07-06): targets → tree →
|
||||||
numbers → threat-calculator parity → combat stats (arena-converged) →
|
numbers → threat-calculator parity → combat stats (arena-converged) →
|
||||||
pacing. Playtesting in progress: playtest 1 (two full ~40-min wins,
|
pacing. Next step: full-game playtests against the run-shape targets in
|
||||||
cruisers only) found the railgun_s-spam-cruiser meta, confirmed repair
|
`targets.md`.
|
||||||
as overpowered, and exposed a 2.5–3× run-length gap; combat stats
|
|
||||||
adjusted and re-checked in arena round 6 (see `history.md`). Next:
|
|
||||||
playtest 2 with the new stats — pacing knobs (station scaling, threat
|
|
||||||
rate, win pacing) wait for its result.
|
|
||||||
|
|
||||||
## Open action items
|
## Open action items
|
||||||
|
|
||||||
|
|||||||
@@ -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 /
|
- Reprocessing: 4 scrap per cycle, 4 s; full-pool weights iron_ingot 30 /
|
||||||
copper_ingot 30 / silicon 20 / voidsteel 20 → threat(voidsteel)
|
copper_ingot 30 / silicon 20 / voidsteel 20 → threat(voidsteel)
|
||||||
= (4·4 + 4)/0.2 = 100.
|
= (4·4 + 4)/0.2 = 100.
|
||||||
- `scrap_despawn_seconds = 120` (a capital kill drops hundreds of scrap,
|
- `debris_despawn_seconds = 120` (a capital kill drops hundreds of scrap,
|
||||||
collected one per salvage cycle).
|
collected one per salvage cycle).
|
||||||
|
|
||||||
## Recipes and item threats
|
## Recipes and item threats
|
||||||
@@ -94,16 +94,10 @@ geometry-validated against the hull grids)
|
|||||||
|
|
||||||
## Combat stats
|
## Combat stats
|
||||||
|
|
||||||
(arena-converged 2026-07, rounds 1–5; playtest-1 adjustments 2026-07-06 —
|
(arena-converged, 2026-07; see `history.md` rounds 1–5)
|
||||||
see `history.md`)
|
|
||||||
|
|
||||||
**Weapons:** railgun_s 2 dmg × 2.0 Hz (4.0 DPS), range 50 m;
|
**Weapons:** railgun_s 2 dmg × 2.0 Hz (4.0 DPS), range 50 m;
|
||||||
railgun_m 14 × 1.5 (21), range 80; railgun_l 52 × 0.8 (41.6), range 130.
|
railgun_m 14 × 1.5 (21), range 70; railgun_l 52 × 0.8 (41.6), range 100.
|
||||||
DPS per threat: s 0.62, m 0.48, l 0.41 — the concentration tax stands;
|
|
||||||
reach is the bigger guns' compensation (ranges raised after playtest 1,
|
|
||||||
which alone priced out the small-gun-spam meta). railgun_l deliberately
|
|
||||||
outranges stations (120 m) to buy the siege role. railgun_m damage is
|
|
||||||
breakpoint-sensitive: 15+ drops a 60 HP drone from 5 hits to 4.
|
|
||||||
|
|
||||||
**Hull HP** (15/threat prior + empirical trims): drone 60, frigate 300,
|
**Hull HP** (15/threat prior + empirical trims): drone 60, frigate 300,
|
||||||
destroyer 550, cruiser 1500, battlecruiser 2400, battleship 6300,
|
destroyer 550, cruiser 1500, battlecruiser 2400, battleship 6300,
|
||||||
@@ -115,9 +109,8 @@ destroyer 30|35|18|6|3, cruiser 24|25|12|4|2, battlecruiser 20|20|10|3|1.5,
|
|||||||
battleship 15|14|7|2|1, dreadnought/carrier 10|8|4|1|0.5.
|
battleship 15|14|7|2|1, dreadnought/carrier 10|8|4|1|0.5.
|
||||||
Sensors: 150/200/220/250/260/280/300/350 m.
|
Sensors: 150/200/220/250/260/280/300/350 m.
|
||||||
|
|
||||||
**Other modules:** armor_plates +1200 HP; repair_tool 4 HP × 1 Hz,
|
**Other modules:** armor_plates +1200 HP; repair_tool 9 HP × 1 Hz,
|
||||||
range 80 (halved after playtest 1 — free between-wave top-offs were never
|
range 80; salvager range 60, cargo 20, 0.5 collections/s; afterburner
|
||||||
priced by the arena escort test); salvager range 60, cargo 20, 0.5 collections/s; afterburner
|
|
||||||
×1.6 speed +60 accel; maneuvering_thrusters ×1.2 speed +10 maneuvering;
|
×1.6 speed +60 accel; maneuvering_thrusters ×1.2 speed +10 maneuvering;
|
||||||
sensor_booster +50 m; weapon_upgrade ×1.2 damage; weapon_primer ×1.2
|
sensor_booster +50 m; weapon_upgrade ×1.2 damage; weapon_primer ×1.2
|
||||||
rate; weapon_stabilizer ×1.3 range ×0.8 rate.
|
rate; weapon_stabilizer ×1.3 range ×0.8 rate.
|
||||||
|
|||||||
@@ -94,68 +94,3 @@ range 200).
|
|||||||
— ~1 expansion per cycle mid-game, 2–3 cycles apart late.
|
— ~1 expansion per cycle mid-game, 2–3 cycles apart late.
|
||||||
- **First full balancing round complete.** Next: full-game playtests
|
- **First full balancing round complete.** Next: full-game playtests
|
||||||
against the run-shape targets.
|
against the run-shape targets.
|
||||||
|
|
||||||
## 2026-07-06 — playtest 1 (two full playthroughs)
|
|
||||||
|
|
||||||
Two complete runs, WON in ~40 min each with cruiser fleets only — never
|
|
||||||
needing capitals. (Initially misread as "two pushes in 40 min, pacing on
|
|
||||||
target"; corrected in round 6.) That is roughly cycle 8 against the
|
|
||||||
win-cycle target of 20–24: a 2.5–3× pacing gap. Pacing knobs deliberately
|
|
||||||
untouched this round — the runs rode 9 HP/s repair and stations nothing
|
|
||||||
outranged, both nerfed below; playtest 2 measures the remaining gap.
|
|
||||||
|
|
||||||
- **Meta finding: cruisers filled with 12× railgun_s dominate.** Predicted
|
|
||||||
by the numbers in hindsight: the concentration tax makes railgun_s the
|
|
||||||
best DPS/threat (0.62 vs m 0.48, l 0.41), range is the big guns' only
|
|
||||||
mechanical edge (armor is added HP, not damage reduction — no anti-swarm
|
|
||||||
mechanic), repair sustain covers the closing distance, and stations
|
|
||||||
outranged every ship gun (120 vs railgun_l's 100), so even capitals had
|
|
||||||
to tank-and-brawl. The cruiser compounds it: first hull with a large
|
|
||||||
1×1 canvas (12 cells) and a nearly quartz-free chain.
|
|
||||||
- **Repair tool confirmed overpowered** (second signal after the
|
|
||||||
persistent +24% arena escort margin): the arena only prices in-fight
|
|
||||||
sustain; real runs add free full top-offs in every 15–45 s wave gap
|
|
||||||
across the whole swarm. The 0.7 HP/s-per-threat prior is wrong for
|
|
||||||
wave defence.
|
|
||||||
- **Changes:** repair_tool 9→4 HP/s; railgun_l range 100→130 (now
|
|
||||||
outranges stations — buys the siege role the capital ladder promises);
|
|
||||||
railgun_m 14→16 dmg and range 70→80 (tax softened: m sits at 0.55
|
|
||||||
DPS/threat, between s and l). Module threats unchanged (costs
|
|
||||||
untouched), so no ladder recalculation needed.
|
|
||||||
- New tracked arena added: railgun_s-spam cruisers (8× 178) vs default
|
|
||||||
cruisers (6× 233.5) — the spam side should win a brawl somewhat, but a
|
|
||||||
blowout means the small-gun premium needs retuning.
|
|
||||||
- **Open:** re-run the arena suite to check the range/damage changes
|
|
||||||
against the round 1–5 results; next playtest should verify big guns now
|
|
||||||
feel worth climbing to and repair is merely good.
|
|
||||||
|
|
||||||
## 2026-07-06 — arena round 6 (checking the playtest-1 adjustments)
|
|
||||||
|
|
||||||
Mirrors healthy (5–8% margins, durations 24/66/91 s). Results:
|
|
||||||
|
|
||||||
- **Spam-cruiser arena: default cruisers +9% — the meta is priced out**,
|
|
||||||
and the range buff alone did the work.
|
|
||||||
- **Regression: drone swarm vs cruisers +47% for cruisers** (was +14%
|
|
||||||
swarm in round 3). Besides the wider range gap, the damage buff crossed
|
|
||||||
a breakpoint: 14 dmg kills a 60 HP drone in 5 hits, 16 in 4 — a hidden
|
|
||||||
~25% effective-DPS gain vs drones. Change: **railgun_m damage 16→14**
|
|
||||||
(range stays 80); the tax stands, reach is the compensation.
|
|
||||||
- Battleship +30% and dreadnought +29% vs pure railgun_s fleets:
|
|
||||||
**accepted as reach-doctrine texture** (BS was already accepted at
|
|
||||||
+23%) — the l gun's 130 m standoff is exactly what the range buff
|
|
||||||
bought; the counter is your own reach or 2:1 numbers, not equal-threat
|
|
||||||
small guns. Watch, don't tune.
|
|
||||||
- Repair escort flipped to raw +16%: **kept at 4 HP/s deliberately** —
|
|
||||||
the arena cannot price the free between-wave top-offs, so slightly
|
|
||||||
below par in-fight is the correct price for a module whose run-value
|
|
||||||
includes them. Playtest 2 decides; 6 is the fallback if repair feels
|
|
||||||
dead.
|
|
||||||
- Station assault: the 3× swarm cracked the fortified position keeping
|
|
||||||
45% EHP. No knob this round touched it; together with playtest 1's
|
|
||||||
trivially easy pushes it flags **station strength as the first pacing
|
|
||||||
lever** for the next pass.
|
|
||||||
|
|
||||||
**Pacing deferred:** playtest 1's 40-min wins predate the repair nerf
|
|
||||||
and the l-gun siege range. If playtest 2 still wins by ~cycle 10, the
|
|
||||||
levers are enemy station scaling (`3000 + 1500*x` likely too shallow),
|
|
||||||
the threat rate, and possibly `artifact_win_count`.
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ data, not the UI gesture. Example: placing a miner records
|
|||||||
One command per sim-mutating operation (the complete mutation surface):
|
One command per sim-mutating operation (the complete mutation surface):
|
||||||
|
|
||||||
- `PlaceBuilding`
|
- `PlaceBuilding`
|
||||||
- `Demolish`
|
- `Deconstruct`
|
||||||
- `RotateInPlace`
|
- `RotateInPlace`
|
||||||
- `SetRecipe`
|
- `SetRecipe`
|
||||||
- `SetShipLayout`
|
- `SetShipLayout`
|
||||||
@@ -125,7 +125,7 @@ the only way production code can reach them is `apply(command)`.
|
|||||||
> `BeltSystem` directly, and every production `buildings()`/`belts()` call is a const query.
|
> `BeltSystem` directly, and every production `buildings()`/`belts()` call is a const query.
|
||||||
> So:
|
> So:
|
||||||
>
|
>
|
||||||
> - `Simulation::tryPlaceBuilding`, `demolish`, and `applySchematicChoice` are **private**.
|
> - `Simulation::tryPlaceBuilding`, `deconstruct`, and `applySchematicChoice` are **private**.
|
||||||
> - The mutable subsystem accessors are private and renamed `buildingsMutable()` /
|
> - The mutable subsystem accessors are private and renamed `buildingsMutable()` /
|
||||||
> `beltsMutable()`; only `const BuildingSystem& buildings() const` / `belts() const` are
|
> `beltsMutable()`; only `const BuildingSystem& buildings() const` / `belts() const` are
|
||||||
> public (queries). UI query sites bind to the const overload unchanged.
|
> public (queries). UI query sites bind to the const overload unchanged.
|
||||||
@@ -377,7 +377,7 @@ The whole feature rests on a deterministic sim, so prove that before building on
|
|||||||
|
|
||||||
Reshape mutations to flow through one path; behaviour unchanged.
|
Reshape mutations to flow through one path; behaviour unchanged.
|
||||||
|
|
||||||
- Defined `Command` base + derived types (`PlaceBuilding`, `Demolish`, `RotateInPlace`,
|
- Defined `Command` base + derived types (`PlaceBuilding`, `Deconstruct`, `RotateInPlace`,
|
||||||
`SetRecipe`, `SetShipLayout`, `SetSiteSplitterFilters`, `SetSplitterFilters`,
|
`SetRecipe`, `SetShipLayout`, `SetSiteSplitterFilters`, `SetSplitterFilters`,
|
||||||
`ClearBeltTiles`, `ApplySchematicChoice`, `Reset`) in `lib`, each with a `playerId` (always 0
|
`ClearBeltTiles`, `ApplySchematicChoice`, `Reset`) in `lib`, each with a `playerId` (always 0
|
||||||
now). `PlaceBuilding` is atomic (carries optional config — see the refinement note above).
|
now). `PlaceBuilding` is atomic (carries optional config — see the refinement note above).
|
||||||
|
|||||||
@@ -4,13 +4,14 @@
|
|||||||
|
|
||||||
Config files use the TOML format. The following config files drive game parameters:
|
Config files use the TOML format. The following config files drive game parameters:
|
||||||
|
|
||||||
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, 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, and artifact win count.
|
- **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.
|
- **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 `unlock_at_station_level` (integer): -1 means the recipe is explicitly unlocked at game start; a value ≥ 0 means the recipe starts locked and a schematic for it can be awarded via defence station destruction (see REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP). An assembler recipe schematic entry may also define an optional `unlock_requires` list of prerequisite schematic ids (REQ-LOCK-PREREQ).
|
- **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.
|
||||||
- **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, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the schematic already unlocked), an optional `unlock_requires` prerequisite list (REQ-LOCK-PREREQ), a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
|
- **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, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the module schematic already unlocked), an optional `unlock_requires` prerequisite list (REQ-LOCK-PREREQ), and an optional capability section and/or stat modifier formulas. 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).
|
- **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.
|
- **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 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. 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 (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.
|
||||||
- **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.
|
- **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.
|
- 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.
|
||||||
@@ -65,10 +66,32 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- `O` — module cell: must be placed on an unoccupied buildable cell (`O`) of the ship's layout.
|
- `O` — module cell: must be placed on an unoccupied buildable cell (`O`) of the ship's layout.
|
||||||
- `X` — ignored cell: may overlap any cell (non-buildable, unoccupied buildable, or occupied buildable) or extend outside the layout grid entirely.
|
- `X` — ignored cell: may overlap any cell (non-buildable, unoccupied buildable, or occupied buildable) or extend outside the layout grid entirely.
|
||||||
|
|
||||||
|
### Unlock Group Format
|
||||||
|
|
||||||
|
Unlock groups in `unlocks.toml` define what the player can be awarded from defence station drops (REQ-DEF-SCHEMATIC-DROP); by their absence they also define what is available from game start (REQ-LOCK-EXPLICIT). Each entry:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[unlock]]
|
||||||
|
id = "salvage_operations" # unique unlock-group id
|
||||||
|
station_level = 2 # eligible once a destroyed station set's level >= this
|
||||||
|
requires = [] # prerequisite unlock-group ids (REQ-LOCK-PREREQ); default empty
|
||||||
|
ships = [] # ship schematic ids granted (from ships.toml)
|
||||||
|
modules = ["salvager"] # module schematic ids granted (from modules.toml)
|
||||||
|
buildings = ["salvage_bay"] # building type ids granted (from buildings.toml)
|
||||||
|
recipes = [] # assembler recipe ids granted (from recipes.toml)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `id` — unique identifier of the unlock group; referenced by other groups' `requires`. Its display name in the schematic choice dialog is derived from the id (same convention as building, module, and recipe ids); there is no separate name field for now.
|
||||||
|
- `station_level` — the minimum destroyed enemy defence station level at which this group becomes eligible to drop (REQ-DEF-SCHEMATIC-DROP).
|
||||||
|
- `requires` — optional list of prerequisite unlock-group ids that must already have been awarded before this group can drop (REQ-LOCK-PREREQ). Defaults to empty.
|
||||||
|
- `ships`, `modules`, `buildings`, `recipes` — the ids granted when this group is awarded. Each list defaults to empty, but a group must grant at least one item overall. Every id must resolve to a definition in the corresponding config file, and `recipes` ids must name **assembler** recipes. Each grantable id (ship, module, building, or assembler recipe) may be granted by **at most one** unlock group; violations fail config load (REQ-LOCK-EXPLICIT).
|
||||||
|
|
||||||
|
Any ship, module, building, or assembler recipe id that appears in no unlock group's grant lists is available from game start (REQ-LOCK-EXPLICIT).
|
||||||
|
|
||||||
## Game World
|
## 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-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; 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).
|
- 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-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-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-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`.
|
- REQ-GW-HEIGHT: The world height (in tiles) is read from `world.toml [world].height_tiles`.
|
||||||
@@ -100,16 +123,36 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- REQ-BLD-COST: The player places buildings from a build menu. Placement costs building blocks from the global stock. The cost per building type is read from `buildings.toml [[building]].cost`.
|
- REQ-BLD-COST: The player places buildings from a build menu. Placement costs building blocks from the global stock. The cost per building type is read from `buildings.toml [[building]].cost`.
|
||||||
- REQ-BLD-QUEUE: Placed buildings enter a construction queue and are built one at a time. Each building takes a duration defined in `buildings.toml [[building]].construction_time_seconds` to construct.
|
- REQ-BLD-QUEUE: Placed buildings enter a construction queue and are built one at a time. Each building takes a duration defined in `buildings.toml [[building]].construction_time_seconds` to construct.
|
||||||
- REQ-BLD-ASTEROID-ONLY: Buildings can only be placed on asteroid tiles (per surface_mask; tiles marked `S` may extend into space).
|
- REQ-BLD-ASTEROID-ONLY: Buildings can only be placed on asteroid tiles (per surface_mask; tiles marked `S` may extend into space).
|
||||||
- REQ-BLD-BUILDER-MODE: Clicking a build button activates builder mode for that building type. Builder mode is exited by right-clicking in the game world or clicking the same build button again.
|
- REQ-BLD-BUILDER-MODE: Clicking a build button activates builder mode for that building type. Builder mode is exited by right-clicking in the game world or clicking the same build button again. (Exception: while a belt drag placement is in progress, right-clicking cancels that drag instead of exiting, and builder mode stays active — REQ-BLD-BELT-DRAG.)
|
||||||
- REQ-BLD-GHOST: While in builder mode, a ghost of the building is rendered at the tile under the cursor, showing where it would be placed. The ghost is drawn semi-transparently in the building type's own visuals — its `fill` and `outline` colors and `glyph` from `visuals.toml` — so that different building types are visually distinguishable in builder mode rather than all looking alike. When the current cursor position is invalid, the ghost instead uses the distinct "invalid" color (REQ-BLD-PLACE-VALID), which overrides the per-building coloring.
|
- REQ-BLD-GHOST: While in builder mode, a ghost of the building is rendered at the tile under the cursor, showing where it would be placed. The ghost is drawn semi-transparently in the building type's own visuals — its `fill` and `outline` colors and `glyph` from `visuals.toml` — so that different building types are visually distinguishable in builder mode rather than all looking alike. When the current cursor position is invalid, the ghost instead uses the distinct "invalid" color (REQ-BLD-PLACE-VALID), which overrides the per-building coloring.
|
||||||
- REQ-BLD-ROTATE: While in builder mode, pressing Shift+R rotates the ghost 90° clockwise and R rotates it 90° counter-clockwise. Rotation affects the direction of the output port.
|
- REQ-BLD-ROTATE: While in builder mode, pressing Shift+R rotates the ghost 90° clockwise and R rotates it 90° counter-clockwise. Rotation affects the direction of the output port.
|
||||||
- REQ-BLD-PLACE: Clicking a valid tile in builder mode places a construction site and adds it to the build queue, consuming building blocks from the global stock.
|
- REQ-BLD-PLACE: Clicking a valid tile in builder mode places a construction site and adds it to the build queue, consuming building blocks from the global stock. (For belts, placement is instead deferred to a drag gesture and happens on mouse release — REQ-BLD-BELT-DRAG.)
|
||||||
- REQ-BLD-PLACE-VALID: A placement position is valid only if (a) every footprint cell in the rotated `surface_mask` is satisfied by the underlying terrain — `A` cells coincide with asteroid tiles, `S` cells coincide with space tiles — (b) no footprint cell overlaps an existing placed building or construction site, except as allowed by REQ-BLD-ROTATE-IN-PLACE, and (c) the player has enough building blocks to afford the building. The ghost (REQ-BLD-GHOST) is rendered in a distinct "invalid" color — overriding its per-building coloring (REQ-BLD-GHOST) — when the current cursor position fails any of these conditions.
|
- REQ-BLD-PLACE-VALID: A placement position is valid only if (a) every footprint cell in the rotated `surface_mask` is satisfied by the underlying terrain — `A` cells coincide with asteroid tiles, `S` cells coincide with space tiles — (b) no footprint cell overlaps an existing placed building or construction site, except as allowed by REQ-BLD-ROTATE-IN-PLACE, and (c) the player has enough building blocks to afford the building. The ghost (REQ-BLD-GHOST) is rendered in a distinct "invalid" color — overriding its per-building coloring (REQ-BLD-GHOST) — when the current cursor position fails any of these conditions.
|
||||||
- REQ-BLD-ROTATE-IN-PLACE: If the ghost's footprint exactly coincides with the footprint of an existing placed building or construction site of the same building type, clicking places no new construction site and consumes no building blocks. Instead, the existing building or site is rotated to match the ghost's rotation. If the target is a construction site, its construction progress is preserved. This applies in both normal builder mode and blueprint placement mode; in blueprint placement mode it is evaluated per building in the blueprint independently — buildings in the blueprint whose footprint coincides with an existing same-type building or site are rotated in place, while the remaining buildings in the blueprint are placed as normal construction sites (subject to the usual validity checks and total cost).
|
- REQ-BLD-ROTATE-IN-PLACE: If the ghost's footprint exactly coincides with the footprint of an existing placed building or construction site of the same building type, clicking places no new construction site and consumes no building blocks. Instead, the existing building or site is rotated to match the ghost's rotation. If the target is a construction site, its construction progress is preserved. **Exception:** Tunnel Entries and Tunnel Exits are never rotated in place — re-orienting a tunnel requires deconstructing and re-placing it (REQ-BLD-TUNNEL-MODE). A tunnel ghost whose footprint coincides with an existing tunnel is therefore treated as an ordinary occupied-tile placement (invalid in normal builder mode; skipped in blueprint placement mode). This applies in both normal builder mode and blueprint placement mode; in blueprint placement mode it is evaluated per building in the blueprint independently — buildings in the blueprint whose footprint coincides with an existing same-type building or site are rotated in place, while the remaining buildings in the blueprint are placed as normal construction sites (subject to the usual validity checks and total cost).
|
||||||
- REQ-BLD-BELT-DRAG: For belts, the player can click and drag across multiple tiles to place a construction site on each tile in one gesture.
|
- REQ-BLD-BELT-DRAG: **Belt drag placement.** For belts, placement is a deferred drag gesture rather than immediate per-tile placement: construction sites are not placed while the cursor hovers new tiles, but only once the player releases the left mouse button. Pressing the left mouse button in the game world while in belt builder mode starts a drag anchored at the tile under the cursor. As the cursor moves, a **rectilinear (L-shaped) path** of belt tiles is computed from the anchor tile to the tile under the cursor: the path first runs along the axis **parallel to the belt's current orientation** (REQ-BLD-ROTATE) — stepping toward the cursor's coordinate on that axis to a corner tile — and then runs along the orthogonal axis to the cursor tile. When the cursor shares the anchor's row or column the path degenerates to a straight line, and when it is on the anchor tile the path is a single tile.
|
||||||
- REQ-BLD-TUNNEL-AUTO-SWITCH: After the player successfully places a Tunnel Entry construction site, builder mode automatically switches to Tunnel Exit (and vice versa), preserving the current ghost rotation. This makes it easy to immediately place the paired end without manually selecting the complementary type.
|
- **Snapping to a building.** When the tile under the cursor is occupied by a non-belt building or construction site (the **target**), the path does not end on that occupied tile. Instead the end tile is the tile **closest to the cursor** (by distance from the cursor position to the tile) among the tiles orthogonally adjacent to the target across one of its **input-capable edges** — any footprint edge that is not one of the target's output ports, i.e. an edge on which the target can accept an incoming item (REQ-MAT-INPUT-PORTS for buildings, REQ-MAT-ACCEPT-DIR for splitters and tunnels). The geometrically closest such tile is **always** used, even if it turns out not to be a valid belt endpoint — in that case it is previewed and applied by the ordinary rules below (invalid color and skipped if occupied by a non-belt building or invalid terrain; re-oriented if it already holds a belt). The rest of the L-shaped path is computed from the anchor to this end tile exactly as above. The end tile's belt direction points **toward the target** (across the shared input edge), overriding the "final tile keeps its incoming step" rule; this applies whether the end tile is a newly placed belt or an existing belt re-oriented in place, and is reflected both in the ghost preview and in the placement on release.
|
||||||
- REQ-BLD-DEMOLISH: The player can demolish a placed factory building. Demolition returns `world.toml [world].refund_percentage` percent of the original building block cost (default 75%) to the global stock. Exception: if the building is still in the construction queue (not yet fully built, including the one currently being constructed), it is removed from the queue and the **full** building block cost is refunded. The HQ and player defence stations cannot be demolished.
|
- **Rotating during the drag.** Rotating the belt with R / Shift+R (REQ-BLD-ROTATE) while a drag is in progress re-picks the path's primary axis immediately from the new orientation and re-derives the whole path from the anchor to the current cursor tile, without waiting for the next cursor movement.
|
||||||
|
- **Ghosts.** While dragging, a belt ghost (REQ-BLD-GHOST) is rendered on every path tile that would be acted on, instead of a single ghost under the cursor. Each ghost is oriented to point toward the next tile along the path toward the cursor, so the path forms one connected belt run that turns at the corner (curved belts along the path auto-derive per REQ-BLD-BELT); the final tile keeps the direction of its incoming step (unless the end tile is snapped to a building, in which case it points into the target — see **Snapping to a building**), and a single-tile path keeps the belt's current orientation. A tile occupied by only an existing belt or belt construction site is a valid target — its belt is re-oriented to follow the path — and shows a normal belt ghost. A tile occupied by a non-belt building or construction site, or otherwise an invalid belt position (REQ-BLD-PLACE-VALID), is drawn in the distinct invalid color, overriding the belt coloring. A tile whose new belt is unaffordable — the cumulative cost of the belts newly placed up to and including it exceeds the global stock — shows **no ghost at all**.
|
||||||
|
- **Placement on release.** No construction site is placed while dragging. On releasing the left mouse button, the path is applied in order (anchor to cursor): each cell occupied by only an existing belt or belt construction site has that belt re-oriented in place to its path direction, consuming no building blocks and preserving any construction progress (REQ-BLD-ROTATE-IN-PLACE); each empty, valid cell gets a new belt construction site, consuming building blocks from the global stock (REQ-BLD-COST). Cells occupied by a non-belt building or construction site, cells that are otherwise invalid (REQ-BLD-PLACE-VALID), and cells whose new belt can no longer be afforded once the running total has been spent are skipped. This supersedes the click-to-place of REQ-BLD-PLACE for belts, including both the single-tile case and multi-tile drags that pass over existing belts.
|
||||||
|
- **Right-click cancels the drag.** Right-clicking while a belt drag is in progress cancels it: the path is discarded, no construction site is placed, and belt builder mode stays active (the exception to REQ-BLD-BUILDER-MODE). Right-clicking when no drag is in progress exits builder mode as usual (REQ-BLD-BUILDER-MODE).
|
||||||
|
- REQ-BLD-TUNNEL-MODE: **Unified tunnel build mode.** The build button grid contains a single **Tunnel** button rather than separate Tunnel Entry and Tunnel Exit buttons (REQ-UI-BUILD-GRID), activated by that button or by hotkey 3 (REQ-UI-HOTKEYS). This one builder mode places either a Tunnel Entry or a Tunnel Exit construction site depending on the hovered position, so the player never manually chooses between the two ends. Both remain distinct building types (REQ-BLD-TUNNEL-ENTRY, REQ-BLD-TUNNEL-EXIT) with their own costs and construction; only their build-menu entry point is unified.
|
||||||
|
- **Default type.** The ghost (REQ-BLD-GHOST) is a **Tunnel Entry** by default; clicking places a Tunnel Entry construction site (REQ-BLD-PLACE). Rotation (REQ-BLD-ROTATE) sets the ghost's facing direction as for any building.
|
||||||
|
- **Exit-completion match.** While the ghost is at a valid position, the game tests whether placing a **Tunnel Exit** at the hovered tile with the current ghost rotation would pair — per the pairing rules of REQ-BLD-TUNNEL-PAIR (same facing direction, within `tunnel_max_distance`, first same-direction building along the search, nearest-claim semantics) — with an existing Tunnel Entry. If so, that Entry is the **exit-completion match** and the ghost turns into a **Tunnel Exit**; clicking then places a Tunnel Exit construction site.
|
||||||
|
- **Entry-completion match.** The game also tests whether placing a **Tunnel Entry** at the hovered tile with the current ghost rotation would pair — again per REQ-BLD-TUNNEL-PAIR — with an existing Tunnel Exit. If so, that Exit is the **entry-completion match** and the ghost stays a Tunnel Entry.
|
||||||
|
- **Resolving the type.** If neither match exists, the ghost is a Tunnel Entry (the default). If only one kind of match exists, the ghost is the kind that produces it (Tunnel Exit for an exit-completion match, Tunnel Entry for an entry-completion match). If **both** an exit-completion match (an existing Entry) and an entry-completion match (an existing Exit) exist, the mode resolves to the completion whose **existing partner building is closer to the mouse cursor position** — the actual sub-tile cursor position, not the hovered tile's center — and the ghost becomes the corresponding type (a Tunnel Exit to complete the nearer Entry, or a Tunnel Entry to complete the nearer Exit). Because the comparison uses the sub-tile cursor position, when the two partners are at the same tile distance the player can move the cursor within the hovered tile to switch which end is placed. When more than one candidate qualifies on a side, the nearest qualifying partner on that side is used.
|
||||||
|
- **Connection preview (green).** Whenever a completion match is resolved, the matched existing partner building is highlighted green, and every tile strictly between that partner and the hovered ghost tile (along the tunnel's straight run) is marked green, previewing the connection that placing the ghost would create.
|
||||||
|
- **Invalid positions.** The completion tests, type switch, and green preview apply only while the hovered position is a valid placement (REQ-BLD-PLACE-VALID). At an invalid position the ordinary invalid-colored ghost is shown (REQ-BLD-GHOST) with no green preview and no switch away from the default Tunnel Entry.
|
||||||
|
- REQ-BLD-DECONSTRUCT: The player can deconstruct a placed factory building. Deconstructing a **fully-built** factory building does not remove it instantly: it is added to the deconstruction queue (REQ-BLD-DECON-QUEUE) and, once its deconstruction completes, `world.toml [world].refund_percentage` percent of the original building block cost (default 75%) is returned to the global stock. Exception: if the building is still in the construction queue (not yet fully built, including the one currently being constructed), it is **not** queued for deconstruction but removed instantly from the construction queue, and the **full** building block cost is refunded immediately. The HQ and player defence stations cannot be deconstructed.
|
||||||
|
- REQ-BLD-DECON-QUEUE: Fully-built factory buildings marked for demolition (REQ-BLD-DECONSTRUCT) enter a **deconstruction queue** that is processed one building at a time and runs in parallel with the construction queue (REQ-BLD-QUEUE) — the two queues advance independently and simultaneously. Each building takes `world.toml [world].deconstruction_time_seconds` (default 0.1) to deconstruct, the same duration for every building type. When a building's deconstruction completes it is removed from the world and its refund is credited (REQ-BLD-DECONSTRUCT). A building **stops operating the moment it enters the queue**: it runs no production and transports no items, and no longer participates as a live building (its tunnel pairing is re-evaluated as if it were gone, REQ-BLD-TUNNEL-PAIR), but it still physically occupies its tiles until removed, so those tiles stay blocked for placement. A queued building can be taken back out of the deconstruction queue before it is removed (REQ-BLD-DECONSTRUCT-CLICK, REQ-BLD-DECONSTRUCT-BOX) — including the one currently being deconstructed; doing so discards any deconstruction progress, credits no refund, and the building resumes operating (and re-pairs, REQ-BLD-TUNNEL-PAIR). Construction sites never enter the deconstruction queue (REQ-BLD-DECONSTRUCT). Every building in the deconstruction queue is rendered with the deconstruct tint — the `visuals.toml [overlays].deconstruct_tint` color, the same tint applied to a building hovered in deconstruct mode (REQ-UI-DECONSTRUCT-BORDER) — so queued buildings are visually distinct.
|
||||||
|
- REQ-BLD-DECONSTRUCT-CLICK: While in deconstruct mode (REQ-UI-HOTKEYS, REQ-UI-DECONSTRUCT-BUTTON), left-clicking a placed factory building or construction site in the game world marks it for demolition, following the rules of REQ-BLD-DECONSTRUCT: a fully-built building is added to the deconstruction queue (REQ-BLD-DECON-QUEUE), and a construction site is removed instantly with the full refund. Left-clicking a fully-built building that is **already in the deconstruction queue** instead removes it from the queue (un-queues it, REQ-BLD-DECON-QUEUE), with no refund; repeated clicks on the same building therefore alternate between queueing and un-queueing it. Clicking a building that cannot be deconstructed (the HQ or a player defence station, per REQ-BLD-DECONSTRUCT), or clicking empty world space, has no effect. Deconstruct mode stays active after each action so the player can continue without re-entering the mode; it is exited via the Q toggle (REQ-UI-HOTKEYS) or the Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON).
|
||||||
|
- REQ-BLD-DECONSTRUCT-BOX: While in deconstruct mode (REQ-UI-HOTKEYS, REQ-UI-DECONSTRUCT-BUTTON), the player can click and drag a selection box in the game world. A selection rectangle is drawn while dragging, using the same box-drag gesture and coverage semantics as the multi-select box (REQ-UI-MULTI-SELECT). On mouse up, following the rules of REQ-BLD-DECONSTRUCT: every construction site covered by the box is removed instantly with the full refund; and among the fully-built deconstructible buildings covered by the box, if **all** of them are already in the deconstruction queue they are all removed from it (un-queued, REQ-BLD-DECON-QUEUE), otherwise every covered building not yet in the queue is added to the deconstruction queue (already-queued ones stay). Buildings that cannot be deconstructed (the HQ and player defence stations, per REQ-BLD-DECONSTRUCT) are excluded from the box demolition; ships and defence stations are never affected.
|
||||||
- REQ-BLD-SITE-CONFIG: A construction site — a building that has been placed but is still queued or under construction (REQ-BLD-QUEUE) — can be selected and configured exactly like the equivalent operational building, before it finishes building. Whatever configuration the building type supports is available on the site: the recipe for a Miner or Assembler (REQ-UI-SELECT-BUTTON), the produced-ship schematic and its module layout for a Shipyard (REQ-UI-SELECT-BUTTON, REQ-MOD-UI-PREVIEW, REQ-MOD-UI-DIALOG), and the output filters for a Splitter (REQ-BLD-SPLITTER) — all set through the same Selected Building Panel controls (REQ-UI-CONFIG-INLINE). Only currently unlocked recipes and schematics are offered, exactly as for operational buildings (REQ-LOCK-UI-RECIPE, REQ-LOCK-UI-SCHEMATIC, REQ-LOCK-UI-SPLITTER). The configuration is stored on the construction site and carries over unchanged when construction completes, so the building becomes operational already configured. A construction site has no input/output buffers and runs no production cycle, so the buffer and production-progress portions of the panel (REQ-UI-SINGLE-SELECTION, REQ-UI-PRODUCTION-PROGRESS) are not shown for it; only its construction progress (REQ-UI-CONSTRUCTION-PROGRESS) and its configuration controls appear. (Blueprint placement already applies a stored recipe or schematic to a construction site on placement per REQ-UI-BLUEPRINT-PLACE; this requirement additionally lets the player set or change that configuration directly on an existing site.)
|
- REQ-BLD-SITE-CONFIG: A construction site — a building that has been placed but is still queued or under construction (REQ-BLD-QUEUE) — can be selected and configured exactly like the equivalent operational building, before it finishes building. Whatever configuration the building type supports is available on the site: the recipe for a Miner or Assembler (REQ-UI-SELECT-BUTTON), the produced-ship schematic and its module layout for a Shipyard (REQ-UI-SELECT-BUTTON, REQ-MOD-UI-PREVIEW, REQ-MOD-UI-DIALOG), and the output filters for a Splitter (REQ-BLD-SPLITTER) — all set through the same Selected Building Panel controls (REQ-UI-CONFIG-INLINE). Only currently unlocked recipes and schematics are offered, exactly as for operational buildings (REQ-LOCK-UI-RECIPE, REQ-LOCK-UI-SCHEMATIC, REQ-LOCK-UI-SPLITTER). The configuration is stored on the construction site and carries over unchanged when construction completes, so the building becomes operational already configured. A construction site has no input/output buffers and runs no production cycle, so the buffer and production-progress portions of the panel (REQ-UI-SINGLE-SELECTION, REQ-UI-PRODUCTION-PROGRESS) are not shown for it; only its construction progress (REQ-UI-CONSTRUCTION-PROGRESS) and its configuration controls appear. (Blueprint placement already applies a stored recipe or schematic to a construction site on placement per REQ-UI-BLUEPRINT-PLACE; this requirement additionally lets the player set or change that configuration directly on an existing site.)
|
||||||
|
- REQ-BLD-COPY-CONFIG: **Copy building settings (hold Shift).** While the Shift key is held, the player can copy one building's settings onto other buildings of the same type, so several identical machines can be set up without opening each one's panel. This gesture is available only in the default selection mode; while a builder, blueprint placement, or deconstruct mode is active it is disabled, so it never clashes with placement or demolition clicks.
|
||||||
|
- **Shift + right-click** a building copies its current settings into a temporary cache, along with the building's type. The settings copied are whatever that building type supports: the selected recipe (Miner, Assembler), the selected schematic together with its module layout (Shipyard), or the two output filters (Splitter, REQ-BLD-SPLITTER). Copying succeeds only when there is something to copy — a Miner or Assembler with a recipe selected, a Shipyard with a schematic selected, or any Splitter (whose output filters, even when empty/accept-all, always constitute valid settings). Shift + right-clicking a configurable building with nothing yet selected, a building type that has no settings at all (Smelter, Reprocessing Plant, Salvage Bay, belt/tunnel tiles, the HQ), or empty world space, has no effect and leaves any existing cache unchanged.
|
||||||
|
- **Shift + left-click** a building of the **same type** as the cached one applies the cached settings to it, exactly as if the player had made that selection through the selected building panel — with the same effects as a normal selection change (buffer clearing per REQ-MAT-INPUT-BUFFER and REQ-MAT-OUTPUT-BUFFER, and, for a Shipyard, in-progress cycle cancellation per REQ-BLD-SHIPYARD). This can be repeated on any number of same-type buildings while Shift stays held. Shift + left-clicking a building of a different type than the cached one, any building while the cache is empty, or empty world space, has no effect.
|
||||||
|
- Both operational buildings and construction sites take part as source and target (REQ-BLD-SITE-CONFIG); settings applied to a construction site carry over unchanged when it finishes building.
|
||||||
|
- **Releasing Shift clears the temporary cache.** It is never persisted and does not survive Shift being released; the next copy starts fresh.
|
||||||
|
- Because the cached settings were already valid on a same-type source building, they remain valid and available on the target (a selected recipe/schematic stays unlocked per REQ-LOCK-UI-RECIPE and REQ-LOCK-UI-SCHEMATIC; splitter filter item types stay unlocked per REQ-LOCK-UI-SPLITTER).
|
||||||
|
|
||||||
## Building Types
|
## Building Types
|
||||||
|
|
||||||
@@ -118,29 +161,48 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- REQ-BLD-ASSEMBLER: **Assembler** (3×3): The player selects a recipe from the config-defined crafting tree. Produces the selected output item at the rate defined in the corresponding `recipes.toml [[recipe]]` entry with `building = "assembler"`. Only implicitly unlocked recipes are available for selection (REQ-LOCK-UI-RECIPE).
|
- REQ-BLD-ASSEMBLER: **Assembler** (3×3): The player selects a recipe from the config-defined crafting tree. Produces the selected output item at the rate defined in the corresponding `recipes.toml [[recipe]]` entry with `building = "assembler"`. Only implicitly unlocked recipes are available for selection (REQ-LOCK-UI-RECIPE).
|
||||||
- REQ-BLD-REPROCESSING: **Reprocessing Plant** (3×3): Consumes scrap per cycle and produces exactly one higher-level intermediate product per cycle via weighted random pick. The input quantity, possible output items, per-output weights, and amounts are defined in `recipes.toml [[recipe]]` entries with `building = "reprocessing_plant"` (`inputs`, `outputs[].item`, `outputs[].amount`, `outputs[].weight`). Weights are normalized at load time; their sum does not need to equal 1. The output is rolled at cycle start (see REQ-MAT-CYCLE); the pool of eligible outputs is restricted to implicitly unlocked item types (REQ-LOCK-REPROCESSING-POOL). The output buffer holds at most one cycle's output — see REQ-MAT-OUTPUT-BUFFER-REPROCESSING.
|
- REQ-BLD-REPROCESSING: **Reprocessing Plant** (3×3): Consumes scrap per cycle and produces exactly one higher-level intermediate product per cycle via weighted random pick. The input quantity, possible output items, per-output weights, and amounts are defined in `recipes.toml [[recipe]]` entries with `building = "reprocessing_plant"` (`inputs`, `outputs[].item`, `outputs[].amount`, `outputs[].weight`). Weights are normalized at load time; their sum does not need to equal 1. The output is rolled at cycle start (see REQ-MAT-CYCLE); the pool of eligible outputs is restricted to implicitly unlocked item types (REQ-LOCK-REPROCESSING-POOL). The output buffer holds at most one cycle's output — see REQ-MAT-OUTPUT-BUFFER-REPROCESSING.
|
||||||
- REQ-BLD-SHIPYARD: **Shipyard** (4×2): The player selects a schematic. When all required materials — the ship's base materials (`[ship.schematic].materials`) plus the materials of all modules in the configured layout (REQ-MOD-MATERIALS) — are present in its input buffer, the shipyard consumes them and begins a production cycle lasting the ship's base `[ship.schematic].production_time_seconds` plus the sum of production times contributed by all module instances in the configured layout (REQ-MOD-PRODUCTION-TIME). One ship of that type is spawned with the configured modules when the cycle completes. The shipyard cannot start a new cycle while one is in progress. If the player confirms a layout change (REQ-MOD-UI-DIALOG) while a production cycle is in progress, the current cycle is cancelled and all consumed materials are discarded; the shipyard returns to idle with the new layout configuration.
|
- REQ-BLD-SHIPYARD: **Shipyard** (4×2): The player selects a schematic. When all required materials — the ship's base materials (`[ship.schematic].materials`) plus the materials of all modules in the configured layout (REQ-MOD-MATERIALS) — are present in its input buffer, the shipyard consumes them and begins a production cycle lasting the ship's base `[ship.schematic].production_time_seconds` plus the sum of production times contributed by all module instances in the configured layout (REQ-MOD-PRODUCTION-TIME). One ship of that type is spawned with the configured modules when the cycle completes. The shipyard cannot start a new cycle while one is in progress. If the player confirms a layout change (REQ-MOD-UI-DIALOG) while a production cycle is in progress, the current cycle is cancelled and all consumed materials are discarded; the shipyard returns to idle with the new layout configuration.
|
||||||
- REQ-BLD-SALVAGE-BAY: **Salvage Bay** (3×2): A dedicated drop-off point for salvage ships. Scrap delivered here is placed onto connected output belts.
|
- REQ-BLD-SALVAGE-BAY: **Salvage Bay** (3×2): A dedicated drop-off point for salvage ships. It has an output buffer whose holding capacity is defined by the `output_buffer_capacity` field of the `salvage_bay` entry in `buildings.toml` (rather than by a production cycle, since the Salvage Bay has no recipe). A ship at the bay hands over one unit of scrap per tick while the buffer has free space; a full buffer blocks further drop-off until space frees up (consistent with the buffer-full semantics of REQ-MAT-OUTPUT-BUFFER). Held scrap is pushed onto connected output belts.
|
||||||
- REQ-BLD-BELT: **Belt** (1×1): Transports items. A belt tile has one direction (N, S, E, W) set at placement (modified by rotation). Curved belts are auto-derived: when a belt tile's outgoing direction leads into another belt whose direction is orthogonal, the downstream belt is rendered and behaves as a curve. Belt speed is defined in `world.toml [world].belt_speed_tiles_per_second` (REQ-GW-BELT-SPEED).
|
- REQ-BLD-BELT: **Belt** (1×1): Transports items. A belt tile has one direction (N, S, E, W) set at placement (modified by rotation). Curved belts are auto-derived: when a belt tile's outgoing direction leads into another belt whose direction is orthogonal, the downstream belt is rendered and behaves as a curve. Belt speed is defined in `world.toml [world].belt_speed_tiles_per_second` (REQ-GW-BELT-SPEED). A belt accepts items only through a non-output edge (REQ-MAT-ACCEPT-DIR).
|
||||||
- REQ-BLD-SPLITTER: **Splitter** (1×1): Distributes incoming items between two output directions. Each output can optionally have a filter (a list of item types), configurable via the selected building panel; only implicitly unlocked item types are available as filter options (REQ-LOCK-UI-SPLITTER). Routing rules:
|
- REQ-BLD-SPLITTER: **Splitter** (1×1): Distributes incoming items between two output directions. Incoming items are accepted only through the splitter's non-output edges (REQ-MAT-ACCEPT-DIR). Each output can optionally have a filter (a list of item types), configurable via the selected building panel; only implicitly unlocked item types are available as filter options (REQ-LOCK-UI-SPLITTER). Routing rules:
|
||||||
- An item matching only one output's filter is routed to that output.
|
- An item matching only one output's filter is routed to that output.
|
||||||
- An item matching both outputs' filters is distributed by strict alternation between those outputs.
|
- An item matching both outputs' filters is distributed by strict alternation between those outputs.
|
||||||
- An item matching neither output's filter is routed to the unfiltered output. If both outputs have a filter and the item matches neither, the splitter stalls and moves no items until the situation is resolved.
|
- An item matching neither output's filter is routed to the unfiltered output. If both outputs have a filter and the item matches neither, the splitter stalls and moves no items until the situation is resolved.
|
||||||
- If neither output has a filter, items are distributed by strict alternation.
|
- If neither output has a filter, items are distributed by strict alternation.
|
||||||
- In all alternation cases, if one output is blocked the item goes to the other output until it unblocks.
|
- In all alternation cases, if one output is blocked the item goes to the other output until it unblocks.
|
||||||
- REQ-BLD-TUNNEL-ENTRY: **Tunnel Entry** (1×1): The sending end of a tunnel pair. The player sets a direction (N, S, E, W) at placement, rotatable with R/Shift+R. Items arriving from an adjacent belt tile whose direction points into the entry are forwarded through the tunnel to the paired Tunnel Exit (see REQ-BLD-TUNNEL-PAIR, REQ-BLD-TUNNEL-TRANSIT). If the entry is unpaired, or if the paired exit's output is blocked, the entry blocks like a full belt tile.
|
- REQ-BLD-TUNNEL-ENTRY: **Tunnel Entry** (1×1): The sending end of a tunnel pair. The player sets a direction (N, S, E, W) at placement, rotatable with R/Shift+R. Items arriving from an adjacent belt tile on a non-output edge (i.e. not the mouth edge in the entry's facing direction — see REQ-MAT-ACCEPT-DIR) whose direction points into the entry are forwarded through the tunnel to the paired Tunnel Exit (see REQ-BLD-TUNNEL-PAIR, REQ-BLD-TUNNEL-TRANSIT). If the entry is unpaired, or if the paired exit's output is blocked, the entry blocks like a full belt tile.
|
||||||
- REQ-BLD-TUNNEL-EXIT: **Tunnel Exit** (1×1): The receiving end of a tunnel pair. The player sets a direction at placement, rotatable with R/Shift+R. Items received from the paired Tunnel Entry emerge from the output side of the exit tile — the tile adjacent in the exit's facing direction — continuing in that direction. If the exit is unpaired or its output is blocked, it holds received items until they can advance.
|
- REQ-BLD-TUNNEL-EXIT: **Tunnel Exit** (1×1): The receiving end of a tunnel pair. The player sets a direction at placement, rotatable with R/Shift+R. Items received from the paired Tunnel Entry emerge from the output side of the exit tile — the tile adjacent in the exit's facing direction — continuing in that direction. If the exit is unpaired or its output is blocked, it holds received items until they can advance.
|
||||||
- REQ-BLD-TUNNEL-PAIR: **Tunnel pairing rules.** Pairing is re-evaluated for all Tunnel Entries whenever any Tunnel Entry or Tunnel Exit is placed or demolished.
|
- REQ-BLD-TUNNEL-PAIR: **Tunnel pairing rules.** Pairing is re-evaluated for all Tunnel Entries whenever any Tunnel Entry or Tunnel Exit is placed, deconstructed, or enters or leaves the deconstruction queue (REQ-BLD-DECON-QUEUE; a tunnel end that is queued for deconstruction counts as removed for pairing).
|
||||||
- A Tunnel Entry searches tile-by-tile in its facing direction for a partner. Any tunnel building (entry or exit) that faces a *different* direction is ignored and skipped. The search stops at the first tunnel building that faces the *same* direction as the searching entry.
|
- A Tunnel Entry searches tile-by-tile in its facing direction for a partner. Any tunnel building (entry or exit) that faces a *different* direction is ignored and skipped. The search stops at the first tunnel building that faces the *same* direction as the searching entry.
|
||||||
- If that first same-direction tunnel building is a Tunnel Exit, is within `tunnel_max_distance` tiles of the entry, and is not already paired with a closer entry, the two form a pair.
|
- If that first same-direction tunnel building is a Tunnel Exit, is within `tunnel_max_distance` tiles of the entry, and is not already paired with a closer entry, the two form a pair.
|
||||||
- Otherwise the entry is unpaired.
|
- Otherwise the entry is unpaired.
|
||||||
- Pairing is one-to-one: each Tunnel Entry pairs with at most one Tunnel Exit, and vice versa. A Tunnel Exit is claimed by the nearest Tunnel Entry that can validly reach it; all other entries for which it would otherwise qualify are unpaired.
|
- Pairing is one-to-one: each Tunnel Entry pairs with at most one Tunnel Exit, and vice versa. A Tunnel Exit is claimed by the nearest Tunnel Entry that can validly reach it; all other entries for which it would otherwise qualify are unpaired.
|
||||||
- When one end of a pair is demolished, the pair is dissolved and any items currently in transit are discarded.
|
- When one end of a pair is deconstructed, the pair is dissolved and any items currently in transit are discarded.
|
||||||
- REQ-BLD-TUNNEL-TRANSIT: **Tunnel transit.** Items inside a tunnel are not rendered (they travel invisibly). Transit time equals the tile-coordinate distance between entry and exit divided by `world.toml [world].belt_speed_tiles_per_second`, matching the time a chain of belt tiles of equivalent length would take. Multiple items may be in transit simultaneously, spaced as they would be on a belt chain of the same length. Clearing a tunnel entry or exit tile (REQ-UI-BELT-CLEAR) also discards all items currently in transit through that tunnel.
|
- REQ-BLD-TUNNEL-TRANSIT: **Tunnel transit.** Items inside a tunnel are not rendered (they travel invisibly). Transit time equals the tile-coordinate distance between entry and exit divided by `world.toml [world].belt_speed_tiles_per_second`, matching the time a chain of belt tiles of equivalent length would take. Multiple items may be in transit simultaneously, spaced as they would be on a belt chain of the same length. Clearing a tunnel entry or exit tile (REQ-UI-BELT-CLEAR) also discards all items currently in transit through that tunnel.
|
||||||
|
- REQ-BLD-TUNNEL-SELECT-HIGHLIGHT: **Selected-tunnel connection highlight.** While a Tunnel Entry or Tunnel Exit — operational building or construction site — is part of the current selection (single selection or multi-selection, REQ-UI-MULTI-SELECT), its tunnel connection is marked green in the game world, using the same `visuals.toml [overlays].tunnel_preview` green as the placement connection preview (REQ-BLD-TUNNEL-MODE). The matching end is found by applying the pairing scan of REQ-BLD-TUNNEL-PAIR over both built tunnels **and** construction-site tunnels (site-inclusive, matching the placement preview): for a selected entry, the first same-direction tunnel within `tunnel_max_distance` along its facing direction, if it is a Tunnel Exit; for a selected exit, the first same-direction tunnel within `tunnel_max_distance` opposite its facing direction, if it is a Tunnel Entry. When a matching end is found, the entry tile, the exit tile, and every tile strictly between them (along the tunnel's straight run) are marked green. A selected tunnel with no matching end shows no green highlight (it still receives the normal selection outline). In multi-selection each selected tunnel end that has a matching end contributes its connection, and a given connection is shown whenever either of its ends is selected. The highlight is presentation-only and has no effect on the simulation.
|
||||||
|
|
||||||
## Material Transport & Buffers
|
## Material Transport & Buffers
|
||||||
|
|
||||||
- REQ-MAT-BELT-ONLY: Materials are transported exclusively via belts, splitters, and tunnels.
|
- REQ-MAT-BELT-ONLY: Materials are transported exclusively via belts, splitters, and tunnels, with one exception: two directly adjacent buildings whose output and input ports meet transfer items straight between them without an intervening transport tile (REQ-MAT-DIRECT-COUPLE).
|
||||||
- REQ-MAT-INPUT-PORTS: A building accepts items from any adjacent belt tile on any edge of its footprint (excluding cells occupied by output port(s)) whose direction points toward the building, provided the item is an input required by the currently selected recipe and the matching per-material input buffer has free space.
|
- REQ-MAT-INPUT-PORTS: A building accepts items from any adjacent belt tile on any edge of its footprint (excluding cells occupied by output port(s)) whose direction points toward the building, provided the item is an input required by the currently selected recipe and the matching per-material input buffer has free space. An accepted item does not enter the building instantly; it is removed from the belt and travels inward across the input port's footprint cell on that port's own input belt before being added to the buffer (REQ-MAT-INPUT-INTAKE).
|
||||||
- REQ-MAT-OUTPUT-PORT: Each building has one or more fixed output port(s) defined by its surface_mask (direction determined by rotation). Produced items are placed onto the belt at the output port tile regardless of that belt's direction.
|
- REQ-MAT-INPUT-INTAKE: Accepted input items travel into a building as an animation rather than vanishing off the belt instantly — the input-side mirror of REQ-MAT-OUTPUT-EMERGE. Each input port has its own **input belt** — a virtual belt tile occupying the input port's footprint cell (the body cell the feeding belt points into), oriented in the port's inward flow direction, with progress 0.0 at the outer edge adjacent to the feeding belt and 0.5 at the tile centre. It reuses the belt subsystem: movement at belt speed (REQ-GW-BELT-SPEED), item rendering and spacing (REQ-GW-TILE-SIZE), and capacity/packing (REQ-GW-BELT-CAPACITY), but restricted to the 0.0→0.5 half of the tile. This applies to every building that pulls items from adjacent belts into an input buffer (Smelter, Assembler, Reprocessing Plant, Shipyard); a building may run several input belts at once when belts feed it from more than one side. The HQ is included with the one difference noted below.
|
||||||
|
- **Acceptance & reservation.** The acceptance test of REQ-MAT-INPUT-PORTS is unchanged — an item is accepted only if it is a required input whose per-material input buffer has space — except that "has space" now counts both the items already buffered **and** the items of that material currently travelling on the building's input belts (reserved but not yet arrived), so the total (buffered + in-transit) never exceeds that material's buffer cap (REQ-MAT-INPUT-BUFFER). An item that fails this test is not placed on an input belt and stays on the feeding belt exactly as before, so items that are not required inputs never enter the building.
|
||||||
|
- **Feeding.** An accepted item is removed from the feeding belt on the same tick it would have been taken without this animation, and placed on the input belt at progress 0.0, reserving a slot in its per-material buffer. (An input belt may also be fed directly by an adjacent producer's output belt rather than by a real belt — see REQ-MAT-DIRECT-COUPLE — with the same reservation and entry rules.) A new item is placed only when the input belt's entry slot at progress 0.0 is free (per REQ-GW-BELT-CAPACITY spacing — no in-transit item within a quarter tile of 0.0). The 0.0→0.5 span holds at most three in-transit items (progress 0.0, 0.25, 0.5); the reservation limit above may permit fewer.
|
||||||
|
- **Travel & arrival.** An in-transit item advances from progress 0.0 to 0.5 at belt speed. On reaching progress 0.5 it leaves the input belt and is added to its per-material input buffer, turning its reservation into buffered stock; only then does it count toward starting a production cycle (REQ-MAT-CYCLE). Because the slot was reserved on entry, arrival always succeeds — there is no deadlock.
|
||||||
|
- **Reservation may delay production.** A reserved item occupies buffer capacity for its whole 0.0→0.5 travel without yet being consumable, so an input-starved building may briefly wait for an in-transit item to arrive before it can start a cycle. This is accepted.
|
||||||
|
- **Clearing.** Clearing the input buffers on a recipe or schematic change (REQ-MAT-INPUT-BUFFER) also discards any items currently travelling on the input belts and releases their reservations.
|
||||||
|
- **HQ.** The HQ has no input buffer (REQ-HQ-BELT-INPUT); a building block accepted at an HQ input port travels its input belt the same way but reserves nothing, and is added to the global building blocks stock (REQ-MAT-GLOBAL-STOCK) on reaching progress 0.5.
|
||||||
|
- **Intake rendering (no pop-out).** Mirror of the emergence rendering in REQ-MAT-OUTPUT-EMERGE: the building is rendered over the input belt, so an in-transit item is occluded while inside the footprint and is only visible as it crosses the outer edge — appearing to sink into the port. The portion inside the footprint is hidden, and the item disappears at the tile centre (progress 0.5) as it enters the buffer.
|
||||||
|
- REQ-MAT-OUTPUT-PORT: Each building has one or more fixed output port(s) defined by its surface_mask (direction determined by rotation). Produced items do not appear on the outgoing belt instantly; each item leaves the building by first emerging across the output port tile on that port's own output belt and then transferring onto the adjacent real belt tile (REQ-MAT-OUTPUT-EMERGE). The adjacent belt's direction is otherwise unconstrained (it may flow away from the building or perpendicular to it), except that a belt oriented with its own output edge facing back into the building refuses the transfer and the item stays stuck at the port (REQ-MAT-ACCEPT-DIR, REQ-MAT-OUTPUT-EMERGE).
|
||||||
|
- REQ-MAT-OUTPUT-EMERGE: Items emerge from a building output port as an animation rather than popping directly onto the outgoing belt. Each output port has its own **output belt** — a virtual belt tile occupying the output port tile, oriented in the port's facing direction, with progress 0.0 at the tile's inner edge and 1.0 at the outer (port) edge adjacent to the next real belt tile. It reuses the belt subsystem: movement at belt speed (REQ-GW-BELT-SPEED), item rendering and spacing (REQ-GW-TILE-SIZE), and capacity/packing (REQ-GW-BELT-CAPACITY), but restricted to the 0.5→1.0 half of the tile. This applies to every building that outputs items onto belts (Miner, Smelter, Assembler, Reprocessing Plant, Salvage Bay); it does not apply to the Shipyard, which spawns a ship rather than a belt item (REQ-SHP-SPAWN-PLAYER).
|
||||||
|
- **Feeding.** While the output buffer (REQ-MAT-OUTPUT-BUFFER) holds an item that has not yet begun emerging and the output belt's entry slot at progress 0.5 is free (per REQ-GW-BELT-CAPACITY spacing — no emerging item within a quarter tile of progress 0.5), the next buffered item is placed on the output belt at progress 0.5. Because only the 0.5→1.0 span is used, the output belt holds at most three emerging items (progress 0.5, 0.75, 1.0); once that span is full the building places no further items on it even if the output buffer still holds more.
|
||||||
|
- **Cosmetic hold.** An emerging item still counts as residing in the output buffer (REQ-MAT-GLOBAL-STOCK) for the whole animation; it only leaves the building when it transfers onto a real belt tile at progress 1.0. The output belt therefore adds no inventory capacity beyond the output buffer, and clearing the output buffer on a recipe or schematic change (REQ-MAT-OUTPUT-BUFFER) also removes any items currently emerging.
|
||||||
|
- **Travel & handoff.** An emerging item advances from progress 0.5 to 1.0 at belt speed. At progress 1.0 it attempts to transfer onto the adjacent real belt tile using the normal belt hand-off and accept-direction rules (REQ-MAT-OUTPUT-PORT, REQ-MAT-ACCEPT-DIR): the transfer succeeds only if a transport tile exists there, is not oriented with its output edge facing back into the building, and has free space. On success the item leaves the output buffer and becomes an ordinary item on that belt tile. If instead the output port tile is a directly adjacent building's input edge, the item transfers straight into that building (REQ-MAT-DIRECT-COUPLE).
|
||||||
|
- **Stuck items.** If there is no next real belt tile and no directly-coupled building (REQ-MAT-DIRECT-COUPLE), or the transfer is refused or blocked, the emerging item stops at progress 1.0 and is rendered there (still counted in the output buffer). Following items pile up behind it at progress 0.75 and 0.5 per the packing above, and once the 0.5→1.0 span is full no further items emerge until the front item transfers.
|
||||||
|
- **Emergence rendering (no pop-in).** An emerging item must not simply appear at progress 0.5. The output port tile's building is rendered over the output belt, so an emerging item is occluded while inside the footprint and is revealed progressively as it slides past the port edge — appearing to physically emerge from the building. The portion of the item still within the output port tile is hidden; the portion past the outer edge is drawn.
|
||||||
|
- REQ-MAT-DIRECT-COUPLE: **Direct port coupling.** Two directly adjacent buildings whose ports meet transfer items between them with no intervening transport tile. A direct coupling exists at a shared edge where a producer building's output port tile (the tile it pushes toward, REQ-MAT-OUTPUT-PORT) is a body cell of a consumer building, and the producer's output direction carries the item across that edge into the consumer through one of the consumer's input edges (any perimeter edge other than the consumer's own output port, per REQ-MAT-INPUT-PORTS). Over a direct coupling the two virtual belts chain end to end: an item that reaches progress 1.0 on the producer's output belt at the shared edge (REQ-MAT-OUTPUT-EMERGE) is handed, instead of onto a real belt tile, directly onto the consumer's input belt at progress 0.0 (REQ-MAT-INPUT-INTAKE) and continues inward to the consumer's buffer — so the item appears to slide continuously across the shared edge from one building into the next.
|
||||||
|
- **Acceptance.** The hand-off obeys the consumer's normal input rules (REQ-MAT-INPUT-PORTS, REQ-MAT-INPUT-INTAKE): it succeeds only if the item is a required input of the consumer whose per-material buffer has space (reservation-aware — buffered + in-transit below the cap) and the consumer's input belt entry at progress 0.0 is free. On success the item leaves the producer's output buffer and reserves a slot in the consumer's input buffer, exactly as a belt-fed intake would. If the consumer does not accept the item — it is not one of its inputs, or the buffer is full, or the input-belt entry is occupied — the item stays stuck at the producer's output port at progress 1.0, exactly as when a downstream belt is blocked (REQ-MAT-OUTPUT-EMERGE stuck items).
|
||||||
|
- **Scope.** Direct coupling is the only case in which materials move between buildings without a belt, splitter, or tunnel (REQ-MAT-BELT-ONLY); it bridges only two buildings that are directly adjacent with meeting output/input ports. Transport tiles feeding a building (belt, splitter, or tunnel exit) continue to work through the normal pull, and a producer still hands off to a transport tile placed in the gap as before; a single such tile between two buildings is unaffected by this requirement.
|
||||||
|
- REQ-MAT-ACCEPT-DIR: A transport tile (belt, splitter, tunnel entry, or tunnel exit) accepts an incoming item only through a non-output edge; an item that would enter through one of the tile's output edges is refused. For a belt or a tunnel entry/exit the sole output edge is the one in its facing direction; for a splitter either of its two output directions is an output edge. This applies both to items pushed from an adjacent transport tile and to items deposited by a building's output port (REQ-MAT-OUTPUT-PORT).
|
||||||
- REQ-MAT-INPUT-BUFFER: Each building has one input buffer per required input material. Each per-material buffer holds up to twice that material's per-cycle requirement. When the player selects a new recipe or schematic, all items in all input buffers are cleared.
|
- REQ-MAT-INPUT-BUFFER: Each building has one input buffer per required input material. Each per-material buffer holds up to twice that material's per-cycle requirement. When the player selects a new recipe or schematic, all items in all input buffers are cleared.
|
||||||
- REQ-MAT-OUTPUT-BUFFER: Each building has an output buffer that holds up to twice the quantity produced by one production cycle. If the output buffer is full, production stops until space is available. When the player selects a new recipe or schematic, all items in the output buffer are cleared (relevant when the adjacent belt is jammed and items have accumulated).
|
- REQ-MAT-OUTPUT-BUFFER: Each building has an output buffer that holds up to twice the quantity produced by one production cycle. If the output buffer is full, production stops until space is available. When the player selects a new recipe or schematic, all items in the output buffer are cleared (relevant when the adjacent belt is jammed and items have accumulated).
|
||||||
- REQ-MAT-OUTPUT-BUFFER-REPROCESSING: Exception to REQ-MAT-OUTPUT-BUFFER — the Reprocessing Plant's output buffer holds at most one cycle's output. This prevents exploits where the player stalls the output belt to force the plant to reroll.
|
- REQ-MAT-OUTPUT-BUFFER-REPROCESSING: Exception to REQ-MAT-OUTPUT-BUFFER — the Reprocessing Plant's output buffer holds at most one cycle's output. This prevents exploits where the player stalls the output belt to force the plant to reroll.
|
||||||
@@ -149,13 +211,13 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
- 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-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 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-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.
|
||||||
|
|
||||||
## Ships
|
## Ships
|
||||||
|
|
||||||
- REQ-SHP-AUTONOMOUS: Ships are produced by shipyards and are fully autonomous once produced.
|
- REQ-SHP-AUTONOMOUS: Ships are produced by shipyards and are fully autonomous once produced.
|
||||||
- REQ-SHP-STATS: Base hull stats are defined as plain values in `ships.toml`: HP (`[ship.health].hp`), max linear speed (`[ship.movement].speed`), sensor range (`[ship.sensors].range`), main acceleration (`[ship.movement].main_acceleration`, tiles/s²), maneuvering acceleration (`[ship.movement].maneuvering_acceleration`, tiles/s²), angular acceleration (`[ship.movement].angular_acceleration`, rad/s²), max rotation speed (`[ship.movement].max_rotation_speed`, rad/s). Required build materials (`[ship.schematic].materials`) and the station level at which the schematic becomes available for unlock (`[[ship]].unlock_at_station_level`; -1 = player starts with the schematic already unlocked) are also defined there. Combat, salvage, and repair capabilities are provided by modules (see REQ-MOD-CONFIG). Final hull stats incorporate passive module modifiers per REQ-MOD-STAT-CALC.
|
- REQ-SHP-STATS: Base hull stats are defined as plain values in `ships.toml`: HP (`[ship.health].hp`), max linear speed (`[ship.movement].speed`), sensor range (`[ship.sensors].range`), main acceleration (`[ship.movement].main_acceleration`, tiles/s²), maneuvering acceleration (`[ship.movement].maneuvering_acceleration`, tiles/s²), angular acceleration (`[ship.movement].angular_acceleration`, rad/s²), max rotation speed (`[ship.movement].max_rotation_speed`, rad/s). Required build materials (`[ship.schematic].materials`) are also defined there; whether the schematic starts unlocked or must be awarded during play is defined in `unlocks.toml` (REQ-LOCK-EXPLICIT). Combat, salvage, and repair capabilities are provided by modules (see REQ-MOD-CONFIG). Final hull stats incorporate passive module modifiers per REQ-MOD-STAT-CALC.
|
||||||
- REQ-SHP-SPAWN-PLAYER: A ship produced by a shipyard spawns centered on the shipyard's output port tile.
|
- REQ-SHP-SPAWN-PLAYER: A ship produced by a shipyard spawns centered on the shipyard's output port tile.
|
||||||
- REQ-SHP-SPAWN-ENEMY: Enemy ships spawn at a uniformly random position within the current enemy buffer zone — random X across the buffer's width and random Y across the world height.
|
- REQ-SHP-SPAWN-ENEMY: Enemy ships spawn at a uniformly random position within the current enemy buffer zone — random X across the buffer's width and random Y across the world height.
|
||||||
- REQ-SHP-MOVEMENT: Ships move using a physics-based model. Each ship has a velocity and a facing direction, both updated each tick. The main acceleration (`main_acceleration`) is applied along the ship's current facing direction only. The maneuvering acceleration (`maneuvering_acceleration`) can be applied in any direction independently of the facing direction, enabling lateral or braking movement without rotating. The angular acceleration (`angular_acceleration`) controls how quickly the ship rotates. Linear speed is capped at the ship's `speed` value; rotation rate is capped at the ship's `max_rotation_speed` value. Ship position refers to the ship's center for all range, sensor, and attack checks.
|
- REQ-SHP-MOVEMENT: Ships move using a physics-based model. Each ship has a velocity and a facing direction, both updated each tick. The main acceleration (`main_acceleration`) is applied along the ship's current facing direction only. The maneuvering acceleration (`maneuvering_acceleration`) can be applied in any direction independently of the facing direction, enabling lateral or braking movement without rotating. The angular acceleration (`angular_acceleration`) controls how quickly the ship rotates. Linear speed is capped at the ship's `speed` value; rotation rate is capped at the ship's `max_rotation_speed` value. Ship position refers to the ship's center for all range, sensor, and attack checks.
|
||||||
@@ -169,14 +231,14 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- REQ-SHP-NO-COLLISION: Ships do not collide with each other or with defence stations; they may visually overlap.
|
- 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-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: 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 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-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-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-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-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 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.
|
- 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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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).
|
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).
|
||||||
- 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.
|
- 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.
|
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.
|
||||||
@@ -196,8 +258,6 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- `id` — unique identifier, also used as the display name in the UI.
|
- `id` — unique identifier, also used as the display name in the UI.
|
||||||
- `surface_mask` — footprint within the ship layout grid (see Module Surface Mask Format).
|
- `surface_mask` — footprint within the ship layout grid (see Module Surface Mask Format).
|
||||||
- `materials` — list of materials required per instance (added to the ship's build cost).
|
- `materials` — list of materials required per instance (added to the ship's build cost).
|
||||||
- `unlock_at_station_level` — the enemy defence station level at which this module's schematic becomes available for unlock; -1 means the player starts with the module schematic already unlocked.
|
|
||||||
- `unlock_requires` — optional list of prerequisite schematic ids that must already be unlocked before this module's schematic can drop (REQ-LOCK-PREREQ). Defaults to empty.
|
|
||||||
- `production_time_seconds` — time added to the ship's production cycle per instance.
|
- `production_time_seconds` — time added to the ship's production cycle per instance.
|
||||||
- `fill_color` — fill color used to render this module's cells in the layout grid.
|
- `fill_color` — fill color used to render this module's cells in the layout grid.
|
||||||
- `glyph` — single character rendered on this module's cells in the layout grid and preview widget.
|
- `glyph` — single character rendered on this module's cells in the layout grid and preview widget.
|
||||||
@@ -232,7 +292,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- **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.
|
- **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.
|
- **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-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-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-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:
|
- 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).
|
- `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.
|
- `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.
|
||||||
@@ -251,7 +311,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
### Module UI
|
### Module UI
|
||||||
|
|
||||||
- REQ-MOD-UI-PREVIEW: When a schematic is selected in a shipyard's selected building panel, a small non-interactive **ship layout preview** widget is shown below the schematic selection button (REQ-UI-SELECT-BUTTON). The preview renders the ship's layout grid at a reduced scale: buildable cells without a module are shown as white, non-buildable cells are shown as black, and cells occupied by a module are shown in that module's `fill_color` with the module's `glyph` character. Below the preview, a "Configure" button is shown.
|
- REQ-MOD-UI-PREVIEW: For a selected shipyard (operational building or construction site), the selected building panel always shows a small non-interactive **ship layout preview** widget below the schematic selection button (REQ-UI-SELECT-BUTTON) and a "Configure" button below the preview. Both are **disabled while no schematic is selected**, and enabled once one is; the preview then shows an empty placeholder in place of a layout grid. When a schematic is selected, the preview renders the ship's layout grid at a reduced scale: buildable cells without a module are shown as white, non-buildable cells are shown as black, and cells occupied by a module are shown in that module's `fill_color` with the module's `glyph` character. For non-shipyard buildings, neither the preview nor the "Configure" button is shown.
|
||||||
- REQ-MOD-UI-DIALOG: Clicking the "Configure" button opens the **layout configuration dialog** as a modal. While the dialog is open, the game is paused (speed set to 0×). On close, the game speed is restored to what it was before the dialog was opened.
|
- REQ-MOD-UI-DIALOG: Clicking the "Configure" button opens the **layout configuration dialog** as a modal. While the dialog is open, the game is paused (speed set to 0×). On close, the game speed is restored to what it was before the dialog was opened.
|
||||||
|
|
||||||
The dialog contains:
|
The dialog contains:
|
||||||
@@ -261,6 +321,12 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- **Right** (below the grid): The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD).
|
- **Right** (below the grid): The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD).
|
||||||
- **Bottom**: A "Confirm" button and a "Cancel" button. Cancel discards all changes made in this dialog session and closes the dialog. Confirm applies the changes: the shipyard's configured layout is updated, the required materials and cycle time displayed in the selected building panel are recalculated, and the ship layout preview is refreshed.
|
- **Bottom**: A "Confirm" button and a "Cancel" button. Cancel discards all changes made in this dialog session and closes the dialog. Confirm applies the changes: the shipyard's configured layout is updated, the required materials and cycle time displayed in the selected building panel are recalculated, and the ship layout preview is refreshed.
|
||||||
|
|
||||||
|
- REQ-MOD-UI-EMPTY-PULSE: While a module is selected for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG), the empty buildable cells of the layout grid pulse smoothly around their normal fill shade, oscillating between a slightly darker and a slightly brighter shade at approximately 1 Hz (one full cycle per second), to draw the player's attention to where the module can be placed. All empty buildable cells pulse in phase. When no module is selected for placement (including remove mode), empty buildable cells render at their normal static shade. Non-buildable cells and cells occupied by a placed module do not pulse.
|
||||||
|
|
||||||
|
- REQ-MOD-UI-AUTO-DIALOG: When the player selects a schematic for a shipyard (operational building or construction site) through the schematic selection dialog (REQ-UI-SELECT-BUTTON), and the chosen schematic **differs** from the shipyard's current schematic, the layout configuration dialog (REQ-MOD-UI-DIALOG) opens automatically and immediately once the selection dialog closes — exactly as if the player had then clicked "Configure". Re-selecting the schematic already set does not reopen the dialog. This auto-open applies only to the manual schematic selection dialog; schematic changes applied via the copy-settings gesture (REQ-BLD-COPY-CONFIG) or blueprint placement (REQ-UI-BLUEPRINT-PLACE) do **not** auto-open the dialog. The player may still cancel the auto-opened dialog (REQ-MOD-UI-DIALOG), which leaves the newly selected schematic in place with its default empty layout; the "Configure" button (REQ-MOD-UI-PREVIEW) remains available to open the dialog again later.
|
||||||
|
|
||||||
|
- REQ-MOD-UI-MODULE-TOOLTIP: Each module selection button in the layout configuration dialog (REQ-MOD-UI-DIALOG) shows a hover tooltip with the descriptive text defined for that module type in `modules.toml` (the optional per-module tooltip field). If a module type defines no tooltip text, its button shows no tooltip. The "Remove" button is not a module type and has no config-defined tooltip.
|
||||||
|
|
||||||
- REQ-MOD-UI-STATS-PANEL: The **ship stats panel** in the layout configuration dialog shows the stats of the currently configured ship layout as they would be computed, incorporating all passive module modifiers per REQ-MOD-STAT-CALC. The panel updates in real time whenever modules are placed or removed in the layout grid.
|
- REQ-MOD-UI-STATS-PANEL: The **ship stats panel** in the layout configuration dialog shows the stats of the currently configured ship layout as they would be computed, incorporating all passive module modifiers per REQ-MOD-STAT-CALC. The panel updates in real time whenever modules are placed or removed in the layout grid.
|
||||||
|
|
||||||
The panel always shows all hull stats as final computed values:
|
The panel always shows all hull stats as final computed values:
|
||||||
@@ -305,35 +371,27 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- REQ-DEF-ENEMY-FIRE: Enemy defence stations automatically fire at player ships within range.
|
- REQ-DEF-ENEMY-FIRE: Enemy defence stations automatically fire at player ships within range.
|
||||||
- REQ-DEF-NO-CROSSFIRE: Enemy and player defence stations are never in each other's firing range.
|
- REQ-DEF-NO-CROSSFIRE: Enemy and player defence stations are never in each other's firing range.
|
||||||
- REQ-DEF-PUSH: When both enemy defence stations in a set are destroyed, the boss countdown is advanced (REQ-WAV-BOSS-ADVANCE), the scrollable area is extended (REQ-GW-PUSH-EXPAND), a new set of enemy defence stations is placed at the new boundary, and exactly one schematic drop is awarded for the destroyed set (REQ-DEF-SCHEMATIC-DROP).
|
- REQ-DEF-PUSH: When both enemy defence stations in a set are destroyed, the boss countdown is advanced (REQ-WAV-BOSS-ADVANCE), the scrollable area is extended (REQ-GW-PUSH-EXPAND), a new set of enemy defence stations is placed at the new boundary, and exactly one schematic drop is awarded for the destroyed set (REQ-DEF-SCHEMATIC-DROP).
|
||||||
- REQ-DEF-SCHEMATIC-DROP: Each destroyed set of enemy defence stations awards exactly one schematic drop (not one per station). The drop opens a **schematic choice dialog** — a modal dialog that pauses the game (speed set to 0×; on close, speed is restored to what it was before the dialog opened). Before drawing schematic picks, an artifact roll is made: evaluate `world.toml [world].artifact_chance_formula` with `x` set to the level of the destroyed station set, clamp the result to [0, 1], then compare against a uniform random value in [0, 1). If the roll succeeds, the dialog presents one **artifact option** plus two schematic picks drawn from the eligible pool; otherwise it presents three schematic picks. Up to three (or two, if an artifact option is present) schematic options are drawn uniformly at random **without replacement** from the eligible drop pool. If the pool contains fewer than the required number of entries, only that many schematic options are shown (the artifact option is always shown if the roll succeeded). The eligible drop pool contains:
|
- REQ-DEF-SCHEMATIC-DROP: Each destroyed set of enemy defence stations awards exactly one drop (not one per station). The drop opens a **schematic choice dialog** — a modal dialog that pauses the game (speed set to 0×; on close, speed is restored to what it was before the dialog opened). Before drawing picks, an artifact roll is made: evaluate `world.toml [world].artifact_chance_formula` with `x` set to the level of the destroyed station set, clamp the result to [0, 1], then compare against a uniform random value in [0, 1). If the roll succeeds, the dialog presents one **artifact option** plus two unlock picks drawn from the eligible pool; otherwise it presents three unlock picks. Up to three (or two, if an artifact option is present) unlock options are drawn uniformly at random **without replacement** from the eligible pool. If the pool contains fewer than the required number of entries, only that many unlock options are shown (the artifact option is always shown if the roll succeeded).
|
||||||
- All **ship schematics** and **module schematics** whose `unlock_at_station_level` is ≥ 0 and ≤ the level of the destroyed station set, and which have not yet been unlocked.
|
|
||||||
- All **assembler recipe schematics** whose `unlock_at_station_level` is ≥ 0 and ≤ the level of the destroyed station set, whose output item is currently implicitly unlocked (REQ-LOCK-IMPLICIT), and which have not yet been awarded.
|
|
||||||
|
|
||||||
In addition to the conditions above, a schematic is included in the eligible drop pool only when every prerequisite in its optional `unlock_requires` list is currently satisfied (REQ-LOCK-PREREQ). Because the pool is rebuilt for each drop, a schematic gated behind prerequisites first appears only after all of its prerequisites have themselves been unlocked.
|
The eligible pool contains every **unlock group** (REQ-LOCK-EXPLICIT) that (a) has not yet been awarded, (b) whose `station_level` is ≤ the level of the destroyed station set, and (c) every prerequisite in its `requires` list is currently satisfied (REQ-LOCK-PREREQ). Because the pool is rebuilt for each drop, an unlock group gated behind prerequisites first appears only after all of its prerequisites have themselves been awarded.
|
||||||
|
|
||||||
Each option in the dialog displays: the schematic name (ship `display_name` from `ships.toml`, module `id` from `modules.toml`, or the output item type for assembler recipes) and the schematic type (ship, module, or assembler recipe). The artifact option (if present) is displayed as a distinct entry with the name "Artifact".
|
Each option in the dialog displays the unlock group's display name — derived from its `id` (same display convention as building, module, and recipe ids) — and the list of items it would grant: its ship, module, building, and assembler-recipe ids (each shown with the same display convention as its respective selection dialog). The artifact option (if present) is displayed as a distinct entry with the name "Artifact".
|
||||||
|
|
||||||
Each option additionally displays a vertical list of item names labeled "Unlocks recipes for:", showing which recipes would newly become implicitly unlocked (REQ-LOCK-IMPLICIT) if this option were selected — specifically, the output items of miner recipes and assembler recipes (without `unlock_at_station_level`) that are not currently implicitly unlocked but would become so after applying this option's effect:
|
Each option additionally displays a vertical list of recipe names labeled "Unlocks recipes:", showing which miner and assembler recipes would newly become implicitly unlocked (REQ-LOCK-IMPLICIT) if this option were selected — specifically, the miner recipes and implicitly-gated assembler recipes that are not currently implicitly unlocked but would become so after applying this option's effect. To compute this, all `materials` of the group's granted ship and module schematics are added to the base set per REQ-LOCK-IMPLICIT step 1a, and the output items of the group's granted assembler recipes are added per step 1b, before recomputation.
|
||||||
- For a ship or module schematic, its `materials` are added to the base set per REQ-LOCK-IMPLICIT step 1a before recomputation.
|
|
||||||
- For an assembler recipe schematic, its output item is added to the base set per REQ-LOCK-IMPLICIT step 1b before recomputation.
|
|
||||||
|
|
||||||
Item names are deduplicated and sorted alphabetically. If no recipes would be newly unlocked, the list shows "None".
|
Each recipe is listed by its `id` (using the same display convention as the assembler recipe-selection dialog), sorted alphabetically. Hovering a recipe in this list displays the recipe info tooltip described for a recipe in REQ-UI-SELECT-TOOLTIP (the recipe name; the name and quantity of each input item; the completion time; and the name and quantity of the produced output item). If no recipes would be newly unlocked, the list shows "None".
|
||||||
|
|
||||||
The player selects one option by clicking it. If the player selects the artifact option, the player's artifact count is incremented by 1 (REQ-WIN-ARTIFACT-COUNT) and the dialog closes; no schematic is applied. Otherwise, the selected schematic is applied and the dialog closes:
|
The player selects one option by clicking it. If the player selects the artifact option, the player's artifact count is incremented by 1 (REQ-WIN-ARTIFACT-COUNT) and the dialog closes; no unlock is applied. Otherwise the selected unlock group is awarded and the dialog closes: every ship, module, building, and assembler recipe the group grants becomes unlocked at once — ship schematics unlock the corresponding shipyard selection; module schematics unlock the module type for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG); building types become available in the build menu (REQ-LOCK-BUILDING); assembler recipes become available in the assembler recipe-selection dialog (subject to REQ-LOCK-UI-RECIPE). The unlock group is removed from the pool permanently (REQ-LOCK-EXPLICIT), and the implicit unlock set is recomputed (REQ-LOCK-IMPLICIT).
|
||||||
|
|
||||||
For a **ship or module schematic**: it is unlocked (ship schematics unlock the corresponding shipyard selection; module schematics unlock the module type for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG)).
|
|
||||||
|
|
||||||
For an **assembler recipe schematic**: the recipe is explicitly unlocked and becomes available in the assembler recipe-selection dialog (subject to REQ-LOCK-UI-RECIPE). The schematic is removed from the drop pool permanently (REQ-LOCK-EXPLICIT). The implicit unlock set is recomputed (REQ-LOCK-IMPLICIT).
|
|
||||||
|
|
||||||
## Progression & Locking
|
## Progression & Locking
|
||||||
|
|
||||||
- REQ-LOCK-EXPLICIT: Ship schematics, module schematics, and **assembler recipe schematics** (assembler recipes in `recipes.toml` that define `unlock_at_station_level`) are **explicitly** locked or unlocked. A schematic starts unlocked if its `unlock_at_station_level` is -1; all others start locked. Locked schematics are unlocked only by REQ-DEF-SCHEMATIC-DROP. Once unlocked, a schematic is never re-locked within a run; lock states reset to their initial values on Restart (REQ-CFG-RELOAD). Unlike ship and module schematics, an assembler recipe schematic is removed from the drop pool permanently once awarded and cannot be dropped again.
|
- REQ-LOCK-EXPLICIT: The unit of unlocking is an **unlock group**, defined by an `[[unlock]]` entry in `unlocks.toml` (see Unlock Group Format). Each unlock group grants a set of ship schematics, module schematics, building types, and/or assembler recipes. A ship, module, building, or assembler recipe is **locked at game start if and only if some unlock group grants it**; anything not granted by any unlock group starts unlocked. (For assembler recipes this "starts unlocked" is further governed by implicit gating — see REQ-LOCK-IMPLICIT; an assembler recipe granted by an unlock group is explicitly gated and never subject to implicit unlocking, while one flagged `unlocked_at_start` is always available.) A locked item is unlocked only by awarding its unlock group via REQ-DEF-SCHEMATIC-DROP, which grants all of the group's members at once. Once awarded, an unlock group and its members are never re-locked within a run, and the group is removed from the drop pool permanently; lock states reset to their initial values on Restart (REQ-CFG-RELOAD). Each grantable id may be granted by **at most one** unlock group; a grant id that names no defined ship/module/building/assembler-recipe, that names a non-assembler recipe, or that is granted by more than one unlock group, is a configuration error that fails config load with a descriptive message (REQ-CFG-RELOAD).
|
||||||
|
|
||||||
- REQ-LOCK-PREREQ: A ship schematic, module schematic, or assembler recipe schematic may optionally define `unlock_requires` — a list of prerequisite schematic ids (a ship `id`, module `id`, or assembler recipe `id`) that must already be unlocked before this schematic may enter the drop pool. A prerequisite is **satisfied** only when the schematic it names is currently **explicitly unlocked** (REQ-LOCK-EXPLICIT) — that is, the referenced schematic either started unlocked with `unlock_at_station_level = -1` or has been awarded via a drop. This prerequisite check is applied in addition to the per-schematic conditions in REQ-DEF-SCHEMATIC-DROP: a schematic enters the eligible drop pool only when its `unlock_at_station_level` condition is met, it has not yet been unlocked/awarded, and every id in its `unlock_requires` is satisfied. `unlock_requires` defaults to empty (no prerequisites), which reproduces the prior behaviour. The check is re-evaluated against the current explicit-unlock set every time a drop pool is built (after each REQ-DEF-SCHEMATIC-DROP and on Restart per REQ-CFG-RELOAD), so a gated schematic becomes eligible in the first drop after its last prerequisite is unlocked. Every id listed in any `unlock_requires` must resolve to a schematic that is itself explicitly unlockable (a ship, module, or assembler recipe schematic defined in config); an id that names no such schematic is a configuration error that fails config load with a descriptive message (config is loaded at startup and reloaded on Restart, REQ-CFG-RELOAD). A schematic that lists itself, or a cycle of mutually dependent prerequisites, is not a load error but can never become eligible, since no schematic in the cycle can be the first to unlock.
|
- REQ-LOCK-PREREQ: An unlock group may optionally define `requires` — a list of prerequisite **unlock-group ids** that must already have been awarded before this group may enter the drop pool. A prerequisite is **satisfied** only when the unlock group it names has been awarded (REQ-LOCK-EXPLICIT). This check is applied in addition to the conditions in REQ-DEF-SCHEMATIC-DROP: a group enters the eligible pool only when its `station_level` condition is met, it has not yet been awarded, and every id in its `requires` is satisfied. `requires` defaults to empty (no prerequisites). The check is re-evaluated against the current set of awarded unlock groups every time a drop pool is built (after each REQ-DEF-SCHEMATIC-DROP and on Restart per REQ-CFG-RELOAD), so a gated group becomes eligible in the first drop after its last prerequisite is awarded. Every id listed in any `requires` must resolve to an unlock group defined in `unlocks.toml`; an id that names no such group is a configuration error that fails config load with a descriptive message (config is loaded at startup and reloaded on Restart, REQ-CFG-RELOAD). An unlock group that lists itself, or a cycle of mutually dependent prerequisites, is not a load error but can never become eligible, since no group in the cycle can be the first to be awarded.
|
||||||
|
|
||||||
- REQ-LOCK-IMPLICIT: Item types and miner/assembler recipes are **implicitly** unlocked or locked based on the current set of unlocked ship, module, and assembler recipe schematics. The implicit unlock set is recomputed whenever any schematic changes lock state (on Restart or after REQ-DEF-SCHEMATIC-DROP). Computation:
|
- REQ-LOCK-IMPLICIT: Item types and miner/assembler recipes are **implicitly** unlocked or locked based on the current set of unlocked ship, module, and assembler recipe schematics. The implicit unlock set is recomputed whenever any schematic changes lock state (on Restart or after REQ-DEF-SCHEMATIC-DROP). Computation:
|
||||||
1. Start with the union of: (a) all item types listed in `materials` across all currently unlocked ship schematics and all currently unlocked module schematics, and (b) the output item type of every currently explicitly unlocked assembler recipe schematic (REQ-LOCK-EXPLICIT).
|
1. Start with the union of: (a) all item types listed in `materials` across all currently unlocked ship schematics and all currently unlocked module schematics, and (b) the output item type of every assembler recipe that is currently **explicitly available** — that is, either flagged `unlocked_at_start` in `recipes.toml`, or granted by an unlock group that has been awarded (REQ-LOCK-EXPLICIT).
|
||||||
2. For each item type in the current set: for every recipe (miner, smelter, or assembler) that produces it — skipping any assembler recipe schematic that defines `unlock_at_station_level` and is not yet explicitly unlocked — add each of that recipe's input item types to the set. If the recipe is a miner recipe or an assembler recipe that does not define `unlock_at_station_level`, mark it as implicitly unlocked. Explicitly unlocked assembler recipe schematics are available in the assembler recipe-selection dialog by virtue of REQ-LOCK-EXPLICIT; their inputs are also added to the implicit set in this step.
|
2. For each item type in the current set: for every recipe (miner, smelter, or assembler) that produces it — skipping any assembler recipe that is granted by an unlock group whose group has not yet been awarded — add each of that recipe's input item types to the set. If the recipe is a miner recipe, or an assembler recipe that is not granted by any unlock group, mark it as implicitly unlocked. Assembler recipes that are explicitly available (flagged `unlocked_at_start`, or granted by an awarded unlock group) are available in the assembler recipe-selection dialog by virtue of REQ-LOCK-EXPLICIT; their inputs are also added to the implicit set in this step.
|
||||||
3. Repeat step 2 until no new item types are added.
|
3. Repeat step 2 until no new item types are added.
|
||||||
Item types and miner/assembler recipes not reached by this process (and not explicitly unlocked) are locked. Smelter recipes participate in the traversal to propagate unlocking to their inputs but are never themselves shown in any UI dropdown.
|
Item types and miner/assembler recipes not reached by this process (and not explicitly unlocked) are locked. Smelter recipes participate in the traversal to propagate unlocking to their inputs but are never themselves shown in any UI dropdown.
|
||||||
|
|
||||||
@@ -343,9 +401,11 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
- REQ-LOCK-UI-SCHEMATIC: Locked ship schematics are not shown in the shipyard's schematic-selection dialog (REQ-UI-SELECT-BUTTON).
|
- REQ-LOCK-UI-SCHEMATIC: Locked ship schematics are not shown in the shipyard's schematic-selection dialog (REQ-UI-SELECT-BUTTON).
|
||||||
|
|
||||||
|
- REQ-LOCK-BUILDING: A building type granted by an unlock group (REQ-LOCK-EXPLICIT) is **locked** until that group is awarded. A locked building type has no button in the build button grid (REQ-UI-BUILD-GRID) and cannot be placed, selected as a build tool, or triggered by its build hotkey (REQ-UI-HOTKEYS); its button appears in the grid only once the building type is unlocked. Building types not granted by any unlock group are available from game start. Lock state resets on Restart (REQ-CFG-RELOAD).
|
||||||
|
|
||||||
- REQ-LOCK-UI-SPLITTER: Item types that are not implicitly unlocked are excluded from splitter filter dropdowns (REQ-BLD-SPLITTER).
|
- REQ-LOCK-UI-SPLITTER: Item types that are not implicitly unlocked are excluded from splitter filter dropdowns (REQ-BLD-SPLITTER).
|
||||||
|
|
||||||
- REQ-LOCK-UI-BLUEPRINT: When a blueprint is placed (REQ-UI-BLUEPRINT-PLACE): if a stored recipe ID for a miner or assembler is currently locked, that building's recipe is left unset rather than applied; if a stored splitter filter entry refers to a locked item type, that entry is silently removed. (The analogous rule for locked ship schematics is defined in REQ-UI-BLUEPRINT-PLACE.)
|
- REQ-LOCK-UI-BLUEPRINT: When a blueprint is placed (REQ-UI-BLUEPRINT-PLACE): if a building in the blueprint is of a currently locked building type (REQ-LOCK-BUILDING), that building is silently skipped — no ghost, no validity check, no construction site, and its cost is excluded from the total — exactly as if it were not part of the blueprint; if a stored recipe ID for a miner or assembler is currently locked, that building's recipe is left unset rather than applied; if a stored splitter filter entry refers to a locked item type, that entry is silently removed. (The analogous rule for locked ship schematics is defined in REQ-UI-BLUEPRINT-PLACE.)
|
||||||
|
|
||||||
## Threat Level & Enemy Waves
|
## Threat Level & Enemy Waves
|
||||||
|
|
||||||
@@ -393,29 +453,54 @@ 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-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-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 4× 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-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-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-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-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-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-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.
|
||||||
|
|
||||||
### Game World
|
### Game World
|
||||||
|
|
||||||
- REQ-UI-SCROLL: The player scrolls the view horizontally across the scrollable area by pressing A (scroll left) and D (scroll right).
|
- REQ-UI-SCROLL: The player scrolls the view horizontally across the scrollable area by pressing A (scroll left) and D (scroll right). The pan speed is not constant; it varies with the view's position per REQ-UI-SCROLL-SPEED.
|
||||||
- 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-SCROLL-SPEED: The horizontal pan speed varies with position so the player crosses the empty middle of the world quickly while retaining fine control near the asteroid and near the front line. Two pan speeds are read from `world.toml [scroll]`: `pan_speed_slow_tiles_per_second` (the base speed, used while the view is over the asteroid and player buffer zone) and `pan_speed_fast_tiles_per_second` (the faster speed, used while the view is over the contest zone). Both are expressed in tiles per second and apply equally to the A and D scroll directions. The current pan speed is a function of the view's horizontal center X (REQ-GW-REGIONS defines the regions):
|
||||||
|
- **Flat regions:** while the view center is left of the contest zone (over the asteroid or player buffer zone) and outside any ramp band, the pan speed is the slow speed; while the view center is inside the contest zone and outside any ramp band, the pan speed is the fast speed.
|
||||||
|
- **Ramp bands:** a transition ramp band of width `world.toml [scroll].pan_ramp_band_width_tiles` tiles straddles each contest-zone boundary (the player defence stations on the left, the enemy defence stations on the right), centered on the boundary with half the band width on each side. While the view center is within a ramp band, the pan speed is linearly interpolated between the slow speed at the band's outer (non-contest-zone) edge and the fast speed at the band's inner (contest-zone) edge, by the view center's fractional position across the band. This produces smooth speed changes when entering and exiting the fast contest-zone range rather than an abrupt jump.
|
||||||
|
- **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-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-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.
|
||||||
|
- For the five production buildings (Miner, Smelter, Assembler, Reprocessing Plant, Shipyard), the fill color is determined by evaluating, in order:
|
||||||
|
- **Grey** — no recipe or schematic is selected. This applies only to buildings with a player-facing selection (Miner, Assembler, Shipyard); the Smelter and Reprocessing Plant always run an implicit recipe (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) and are never grey.
|
||||||
|
- **Green** — the building is currently producing: a production cycle is active (REQ-MAT-CYCLE; for the Shipyard, an in-progress production cycle per REQ-BLD-SHIPYARD).
|
||||||
|
- **Red** — the building is idle because a required input is missing from its input buffers, so it cannot start a cycle. Missing input takes precedence over a full output buffer: if any required input is missing the light is red even when the output buffer is also full.
|
||||||
|
- **Yellow** — the building is idle with all required inputs present but its output buffer full, so no new cycle can start (REQ-MAT-OUTPUT-BUFFER, REQ-MAT-CYCLE).
|
||||||
|
- A configured building that is momentarily idle yet blocked by neither condition (all inputs present and the output buffer has room — a transient state that resolves into a started cycle on the same or the next tick per REQ-MAT-CYCLE) shows green.
|
||||||
|
- The Salvage Bay has no recipe and no production cycle (REQ-BLD-SALVAGE-BAY); its status light uses only two states: **green** while its output buffer holds at least one unit of scrap, and **red** while its output buffer is empty. The Salvage Bay's status light is never grey or yellow.
|
||||||
|
- The four fill colors (grey, green, red, yellow) and the outline color are read from `visuals.toml [status_light]`, consistent with the other rendering-only colors. The status light is presentation-only and has no effect on the simulation.
|
||||||
- REQ-UI-HP-BARS: All entities with HP — the HQ, player and enemy defence stations, and player and enemy ships — render an HP bar below them. The bar is always visible regardless of current HP. The bar's filled portion represents the fraction of current HP to maximum HP.
|
- REQ-UI-HP-BARS: All entities with HP — the HQ, player and enemy defence stations, and player and enemy ships — render an HP bar below them. The bar is always visible regardless of current HP. The bar's filled portion represents the fraction of current HP to maximum HP.
|
||||||
- REQ-UI-NO-ZOOM: The view has a fixed zoom level; the player cannot zoom in or out.
|
- REQ-UI-NO-ZOOM: The view has a fixed zoom level; the player cannot zoom in or out.
|
||||||
- REQ-UI-HOTKEYS: Global keyboard shortcuts:
|
- REQ-UI-HOTKEYS: Global keyboard shortcuts:
|
||||||
- **Space** — toggles pause. Pressing Space pauses (sets speed to 0×) and stores the previously selected non-zero speed; pressing Space again restores that speed.
|
- **Space** — toggles pause. Pressing Space pauses (sets speed to 0×) and stores the previously selected non-zero speed; pressing Space again restores that speed.
|
||||||
- **W** — increases game speed by one step in the sequence 0×, 0.5×, 1×, 2×, 4× (no wrap-around past 4×).
|
- **W** — increases game speed by one step in the sequence 0×, 0.5×, 1×, 2×, 10× (no wrap-around past 10×).
|
||||||
- **S** — decreases game speed by one step in the same sequence (no wrap-around past 0×).
|
- **S** — decreases game speed by one step in the same sequence (no wrap-around past 0×).
|
||||||
- **A / D** — scroll the view left / right (REQ-UI-SCROLL).
|
- **A / D** — scroll the view left / right (REQ-UI-SCROLL).
|
||||||
- **Q** — context-sensitive. If a build mode is active (builder mode or blueprint placement mode), pressing Q exits it. Otherwise, pressing Q toggles demolish mode: it enters demolish mode if inactive, or exits demolish mode if already active. (See also REQ-UI-DEMOLISH-BUTTON for the equivalent button.)
|
- **Q** — context-sensitive. If a build mode is active (builder mode or blueprint placement mode), pressing Q exits it. Otherwise, pressing Q toggles deconstruct mode: it enters deconstruct mode if inactive, or exits deconstruct mode if already active. (See also REQ-UI-DECONSTRUCT-BUTTON for the equivalent button.)
|
||||||
- **R / Shift+R** — in builder mode, rotate the ghost counter-clockwise / clockwise (REQ-BLD-ROTATE).
|
- **R / Shift+R** — in builder mode, rotate the ghost counter-clockwise / clockwise (REQ-BLD-ROTATE).
|
||||||
|
- **T** — create a temporary blueprint from the current selection and enter its placement mode (REQ-UI-BLUEPRINT-TEMP).
|
||||||
- **Escape** — opens the escape menu (REQ-UI-GAME-MENU).
|
- **Escape** — opens the escape menu (REQ-UI-GAME-MENU).
|
||||||
- **Build mode selection** — pressing a build hotkey activates builder mode for the corresponding building type, equivalent to clicking its build button (REQ-BLD-BUILDER-MODE):
|
- **Build mode selection** — pressing a build hotkey activates builder mode for the corresponding building type, equivalent to clicking its build button (REQ-BLD-BUILDER-MODE):
|
||||||
- **1** — Belt, **2** — Splitter, **3** — Tunnel Entry, **4** — Tunnel Exit.
|
- **1** — Belt, **2** — Splitter, **3** — Tunnel (the unified tunnel build mode, REQ-BLD-TUNNEL-MODE). Hotkey 4 is unused.
|
||||||
- **Shift+1** — Miner, **Shift+2** — Smelter, **Shift+3** — Assembler, **Shift+4** — Shipyard, **Shift+5** — Salvage Bay, **Shift+6** — Reprocessing Plant.
|
- **Shift+1** — Miner, **Shift+2** — Smelter, **Shift+3** — Assembler, **Shift+4** — Shipyard, **Shift+5** — Salvage Bay, **Shift+6** — Reprocessing Plant.
|
||||||
|
|
||||||
### Debug Draw
|
### Debug Draw
|
||||||
@@ -439,47 +524,65 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
|
|
||||||
### Selected Building Panel
|
### Selected Building Panel
|
||||||
|
|
||||||
- REQ-UI-EMPTY-SELECTION: When no building is selected, the panel is empty.
|
- 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-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-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-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 buildings by box-drag or by Ctrl+clicking individual buildings to add or remove them from the selection.
|
- 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-SELECTION: When multiple buildings are selected, the panel shows how many of each building type are selected. No per-building detail is shown.
|
- 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-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-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:
|
- 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 **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>".
|
- 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-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. Clicking a ship or defence station clears any existing selection and establishes a single-entity selection containing only that entity. Ships and defence stations cannot participate in multi-select together with buildings. Clicking empty world space (no building, ship, or defence station) clears the selection.
|
- 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 a single ship is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **ship stats panel**. 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-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-STATION-STATS-PANEL: When a single defence station is selected (REQ-UI-ENTITY-CLICK-SELECT), 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.
|
- 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).
|
||||||
|
- **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).
|
||||||
|
|
||||||
### Build Button Grid
|
### Build Button Grid
|
||||||
|
|
||||||
- REQ-UI-BUILD-GRID: All placeable building types are shown as a flat grid of buttons with no grouping.
|
- 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, e.g. "Belt: 2 Blocks".
|
- 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-DISABLED: Buttons for buildings the player cannot currently afford are shown as disabled.
|
- 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-DEMOLISH-BUTTON: A dedicated **Demolish** button is shown in the build button grid. Clicking it toggles demolish mode on and off, equivalent to the Q demolish toggle (REQ-UI-HOTKEYS). The button is shown in a visually active/pressed state while demolish mode is active.
|
- 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.
|
||||||
|
|
||||||
### Blueprint Panel
|
### Blueprint Panel
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-PANEL: The blueprint panel is shown to the right of the build button grid. It contains, from top to bottom: a "Create Blueprint" button, and a list of blueprint entries (one per saved blueprint, in creation order).
|
- REQ-UI-BLUEPRINT-PANEL: The blueprint panel is shown to the right of the build button grid. It contains, from top to bottom: a "Create Blueprint" button, and a list of blueprint entries (one per saved blueprint, in creation order). The panel has no Save or Load buttons; blueprints are persisted automatically (REQ-UI-BLUEPRINT-SAVE) and restored at startup (REQ-UI-BLUEPRINT-LOAD).
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-CREATE: The "Create Blueprint" button is enabled only when at least one player-placeable building (i.e. a building with a button in the build button grid) is currently selected; non-player-placeable buildings (HQ, defence stations) in the selection do not count toward this condition. When clicked, a modal dialog appears prompting the player to enter a name. The dialog has Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm with a non-empty name creates a blueprint from the current selection, silently excluding any non-player-placeable buildings, and appends its button to the blueprint list.
|
- REQ-UI-BLUEPRINT-CREATE: The "Create Blueprint" button is enabled only when at least one player-placeable building (i.e. a building with a button in the build button grid) is currently selected; non-player-placeable buildings (HQ, defence stations) in the selection do not count toward this condition. A selected player-placeable building may be either an operational building or a construction site (a building placed but not yet fully built, REQ-BLD-SITE-CONFIG); both count toward this condition and are captured identically (REQ-UI-BLUEPRINT-STORAGE). When clicked, a modal dialog appears prompting the player to enter a name. The dialog has Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm with a non-empty name creates a blueprint from the current selection, silently excluding any non-player-placeable buildings, and appends its button to the blueprint list.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-STORAGE: A blueprint stores its name and, for each building in the selection, the building type, its rotation, its tile offset (integer dx, dy) from the center of the bounding box of all selected buildings' footprints, and — where applicable — the selected recipe ID (miners and assemblers) or schematic ID (shipyards), and for splitters the two output filters (each a list of item types; an empty list means accept-all), at the time of capture. If no recipe or schematic was selected at capture time, none is stored; for a splitter with no filters set, no filter lists are stored. This structure maps directly to a TOML representation (e.g. one `[[building]]` array entry per constituent building, with the splitter filters as `filter_a`/`filter_b` arrays of item-type ids).
|
- REQ-UI-BLUEPRINT-TEMP: Pressing the **T** key (REQ-UI-HOTKEYS) creates a **temporary blueprint** from the current selection and immediately enters blueprint placement mode for it, without opening the naming dialog. It has effect only when at least one player-placeable building is currently selected — the same condition as REQ-UI-BLUEPRINT-CREATE; pressing T with an empty selection, or a selection containing only non-player-placeable buildings (HQ, defence stations), does nothing. Entering this mode replaces any currently active build, blueprint placement, or deconstruct mode. The temporary blueprint is captured exactly as a saved blueprint (REQ-UI-BLUEPRINT-STORAGE), silently excluding any non-player-placeable buildings from the selection, but it is never named, never shown in the blueprint panel (REQ-UI-BLUEPRINT-PANEL), and never persisted to `blueprints.toml` (REQ-UI-BLUEPRINT-SAVE). Placement behaves identically to a saved blueprint's placement mode (REQ-UI-BLUEPRINT-MODE, REQ-UI-BLUEPRINT-PLACE): a ghost is rendered per building, R / Shift+R rotate the entire constellation, placement follows the same per-building validity and total-cost rules, and after a successful placement the mode stays active so the blueprint can be placed again. Right-clicking in the game world exits placement mode, at which point the temporary blueprint is discarded.
|
||||||
|
|
||||||
|
- REQ-UI-BLUEPRINT-STORAGE: A blueprint stores its name and, for each building in the selection, the building type, its rotation, its tile offset (integer dx, dy) from the center of the bounding box of all selected buildings' footprints, and — where applicable — the selected recipe ID (miners and assemblers) or schematic ID (shipyards), and for splitters the two output filters (each a list of item types; an empty list means accept-all), at the time of capture. A source building may be either an operational building or a construction site (REQ-BLD-SITE-CONFIG); a construction site is captured identically, storing whatever configuration it currently holds and never any buffer or construction-progress state. If no recipe or schematic was selected at capture time, none is stored; for a splitter with no filters set, no filter lists are stored. This structure maps directly to a TOML representation (e.g. one `[[building]]` array entry per constituent building, with the splitter filters as `filter_a`/`filter_b` arrays of item-type ids).
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-BUTTON: Each blueprint entry consists of a blueprint button and a dedicated delete icon ("×") placed to the right of the button. The blueprint button displays the blueprint name and, below it, the total building block cost of the blueprint (sum of the individual costs of all constituent buildings). A blueprint button is disabled when the player cannot afford the total cost. Clicking an enabled blueprint button enters blueprint placement mode for that blueprint. The delete icon is always enabled regardless of whether the player can afford the blueprint.
|
- REQ-UI-BLUEPRINT-BUTTON: Each blueprint entry consists of a blueprint button and a dedicated delete icon ("×") placed to the right of the button. The blueprint button displays the blueprint name and, below it, the total building block cost of the blueprint (sum of the individual costs of all constituent buildings). A blueprint button is disabled when the player cannot afford the total cost. Clicking an enabled blueprint button enters blueprint placement mode for that blueprint. The delete icon is always enabled regardless of whether the player can afford the blueprint.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-MODE: In blueprint placement mode a ghost is rendered for every building in the blueprint at the position determined by its stored tile offset from the bounding-box center, which is anchored to the tile under the cursor. Each ghost is rendered individually as valid or invalid, applying REQ-BLD-PLACE-VALID conditions (a) and (b) per building (the other ghosts in the same blueprint do not count as existing buildings for the overlap check). A valid ghost uses its building type's semi-transparent per-building coloring (REQ-BLD-GHOST); an invalid ghost uses the distinct "invalid" color, as in single-building builder mode. Pressing R / Shift+R rotates the entire constellation 90° counter-clockwise / clockwise: each building's tile offset is rotated around the bounding-box center and each building's own rotation is updated, consistent with REQ-BLD-ROTATE. Blueprint placement mode is exited by right-clicking in the game world. Clicking a different blueprint button exits the current mode and enters blueprint placement mode for the newly clicked blueprint.
|
- REQ-UI-BLUEPRINT-MODE: In blueprint placement mode a ghost is rendered for every building in the blueprint (excluding any of a currently locked building type, REQ-LOCK-BUILDING, which is omitted entirely per REQ-LOCK-UI-BLUEPRINT) at the position determined by its stored tile offset from the bounding-box center, which is anchored to the tile under the cursor. Each ghost is rendered individually as valid or invalid, applying REQ-BLD-PLACE-VALID conditions (a) and (b) per building (the other ghosts in the same blueprint do not count as existing buildings for the overlap check). A valid ghost uses its building type's semi-transparent per-building coloring (REQ-BLD-GHOST); an invalid ghost uses the distinct "invalid" color, as in single-building builder mode. Pressing R / Shift+R rotates the entire constellation 90° counter-clockwise / clockwise: each building's tile offset is rotated around the bounding-box center and each building's own rotation is updated, consistent with REQ-BLD-ROTATE. Blueprint placement mode is exited by right-clicking in the game world. Clicking a different blueprint button exits the current mode and enters blueprint placement mode for the newly clicked blueprint.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-PLACE: Left-clicking in blueprint placement mode places the blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. If both conditions are met, a construction site is added to the build queue for each building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. If splitter output filters are stored, they are applied to the construction site immediately and carry over when it finishes building (REQ-BLD-SITE-CONFIG). Locked recipe IDs and splitter filter entries for locked item types are handled on placement per REQ-LOCK-UI-BLUEPRINT. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately.
|
- REQ-UI-BLUEPRINT-PLACE: Buildings of a currently locked building type (REQ-LOCK-BUILDING) are first excluded from the blueprint for this placement, per REQ-LOCK-UI-BLUEPRINT — they are not ghosted, not validity-checked, not placed, and their cost is excluded from the total. Left-clicking in blueprint placement mode then places the (remaining) blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. If both conditions are met, a construction site is added to the build queue for each building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. If splitter output filters are stored, they are applied to the construction site immediately and carry over when it finishes building (REQ-BLD-SITE-CONFIG). Locked recipe IDs and splitter filter entries for locked item types are handled on placement per REQ-LOCK-UI-BLUEPRINT. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-DELETE: Clicking the delete icon ("×") on a blueprint entry immediately removes that blueprint from the list. If the deleted blueprint was active in blueprint placement mode, that mode is exited.
|
- REQ-UI-BLUEPRINT-DELETE: Clicking the delete icon ("×") on a blueprint entry immediately removes that blueprint from the list. If the deleted blueprint was active in blueprint placement mode, that mode is exited.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-SAVE: A "Save" button is shown at the bottom of the blueprint panel. Clicking it serializes all current blueprints to a file named `blueprints.toml` located in the same directory as the application executable. The TOML structure matches REQ-UI-BLUEPRINT-STORAGE. If writing fails, a modal error dialog is shown describing the failure.
|
- REQ-UI-BLUEPRINT-SAVE: On application shutdown, all current blueprints are serialized to a file named `blueprints.toml` located in the same directory as the application executable. The TOML structure matches REQ-UI-BLUEPRINT-STORAGE. Write errors are silently ignored on shutdown (no button, no dialog).
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-LOAD: A "Load" button is shown at the bottom of the blueprint panel, to the right of the "Save" button. Clicking it shows a confirmation dialog ("Load blueprints? This will replace all current blueprints.") with Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm reads `blueprints.toml` from the same directory as the application executable, replaces all current blueprints with those from the file (in the order they appear in the file), and exits any active blueprint-related mode (blueprint placement mode, delete mode). If the file does not exist or cannot be parsed, a modal error dialog is shown describing the failure and the current blueprint list is left unchanged.
|
- REQ-UI-BLUEPRINT-LOAD: At application startup, blueprints are loaded from `blueprints.toml` in the same directory as the application executable, populating the blueprint list (in the order they appear in the file). If the file does not exist, the blueprint list starts empty with no error. If the file exists but cannot be parsed (malformed TOML), a modal error dialog describes the failure and the blueprint list starts empty. There is no Load button and no runtime reload.
|
||||||
|
|
||||||
## Balancing Tool
|
## Balancing Tool
|
||||||
|
|
||||||
@@ -507,7 +610,7 @@ A separate executable target (`balancing`) that links against `lib` but contains
|
|||||||
|
|
||||||
- REQ-BAL-SIM-ENV: Each arena simulates a pure-space environment using the same tick-based simulation as the main game. There is no asteroid, no buildings, no belts, no wave system, and no threat accumulation. Only ships, HQs, defence stations, and combat are active.
|
- REQ-BAL-SIM-ENV: Each arena simulates a pure-space environment using the same tick-based simulation as the main game. There is no asteroid, no buildings, no belts, no wave system, and no threat accumulation. Only ships, HQs, defence stations, and combat are active.
|
||||||
- REQ-BAL-SIM-AI: Ships use the same AI and stats as in the main game. Ships with no target in sensor range advance toward the enemy team's HQ. Ships that detect an enemy in sensor range engage it as in the normal game (REQ-SHP-COMBAT, REQ-SHP-ENEMY-AI).
|
- REQ-BAL-SIM-AI: Ships use the same AI and stats as in the main game. Ships with no target in sensor range advance toward the enemy team's HQ. Ships that detect an enemy in sensor range engage it as in the normal game (REQ-SHP-COMBAT, REQ-SHP-ENEMY-AI).
|
||||||
- REQ-BAL-SIM-SPEED: Each arena that is not being inspected runs its simulation at maximum tick rate (as many ticks per second as the hardware allows), with no rendering. An inspected arena runs at a player-controllable game speed (same speed steps as the main game: 0×, 0.5×, 1×, 2×, 4×) with full rendering in the inspect window, defaulting to 1× on open.
|
- REQ-BAL-SIM-SPEED: Each arena that is not being inspected runs its simulation at maximum tick rate (as many ticks per second as the hardware allows), with no rendering. An inspected arena runs at a player-controllable game speed (same speed steps as the main game: 0×, 0.5×, 1×, 2×, 10×) with full rendering in the inspect window, defaulting to 1× on open.
|
||||||
- REQ-BAL-SIM-PARALLEL: All arenas are simulated in parallel, each on its own thread.
|
- REQ-BAL-SIM-PARALLEL: All arenas are simulated in parallel, each on its own thread.
|
||||||
- REQ-BAL-SIM-END: An arena fight ends when either team's HQ is destroyed or all ships and defence stations of one team have been destroyed. If a team has no defence stations, destroying all its ships is sufficient. When the fight ends, the simulation for that arena stops.
|
- REQ-BAL-SIM-END: An arena fight ends when either team's HQ is destroyed or all ships and defence stations of one team have been destroyed. If a team has no defence stations, destroying all its ships is sufficient. When the fight ends, the simulation for that arena stops.
|
||||||
|
|
||||||
@@ -524,6 +627,6 @@ A separate executable target (`balancing`) that links against `lib` but contains
|
|||||||
- REQ-BAL-UI-WIDGET-START: Each arena widget contains a "Start" button that starts the simulation for that arena. The button is disabled while the arena's simulation is running. When a finished arena's Start button is clicked, a fresh simulation is created and started (the widget resets to initial unit counts, the border returns to blue, and the previous results are replaced).
|
- REQ-BAL-UI-WIDGET-START: Each arena widget contains a "Start" button that starts the simulation for that arena. The button is disabled while the arena's simulation is running. When a finished arena's Start button is clicked, a fresh simulation is created and started (the widget resets to initial unit counts, the border returns to blue, and the previous results are replaced).
|
||||||
- REQ-BAL-UI-WIDGET-BORDER: Each arena widget has a colored border indicating its state: grey when not yet started, blue while its simulation is running, and green when the fight has ended.
|
- REQ-BAL-UI-WIDGET-BORDER: Each arena widget has a colored border indicating its state: grey when not yet started, blue while its simulation is running, and green when the fight has ended.
|
||||||
- REQ-BAL-UI-INSPECT: Clicking an arena widget's "Inspect" button opens a new inspect window for that arena. Any previously open inspect window is closed first (its arena's simulation is aborted and its widget border returns to grey). The inspected arena is restarted with a fresh simulation that runs at controllable game speed with full rendering (REQ-BAL-SIM-SPEED). The arena widget updates live during inspection (surviving counts, border color, `[WON]` prefix) as it does for non-inspected arenas. Only one inspect window may be open at a time.
|
- REQ-BAL-UI-INSPECT: Clicking an arena widget's "Inspect" button opens a new inspect window for that arena. Any previously open inspect window is closed first (its arena's simulation is aborted and its widget border returns to grey). The inspected arena is restarted with a fresh simulation that runs at controllable game speed with full rendering (REQ-BAL-SIM-SPEED). The arena widget updates live during inspection (surviving counts, border color, `[WON]` prefix) as it does for non-inspected arenas. Only one inspect window may be open at a time.
|
||||||
- REQ-BAL-UI-INSPECT-WINDOW: The inspect window consists of three sections, top to bottom: a title bar area containing the arena name and game speed controls (same buttons as the main game: 0×, 0.5×, 1×, 2×, 4×, with Space to toggle pause — see REQ-UI-SPEED and REQ-UI-HOTKEYS), the arena view in the center, and an info panel at the bottom displaying the same team columns and entry format as the arena widget in the main window (REQ-BAL-UI-WIDGET), updated live, including the arena's battle duration once the fight has ended (REQ-BAL-UI-WIDGET).
|
- REQ-BAL-UI-INSPECT-WINDOW: The inspect window consists of three sections, top to bottom: a title bar area containing the arena name and game speed controls (same buttons as the main game: 0×, 0.5×, 1×, 2×, 10×, with Space to toggle pause — see REQ-UI-SPEED and REQ-UI-HOTKEYS), the arena view in the center, and an info panel at the bottom displaying the same team columns and entry format as the arena widget in the main window (REQ-BAL-UI-WIDGET), updated live, including the arena's battle duration once the fight has ended (REQ-BAL-UI-WIDGET).
|
||||||
- REQ-BAL-UI-INSPECT-VIEW: The arena view renders all tiles of the arena and displays ships, HQs, defence stations, and laser beams using the same visual elements and `visuals.toml` colors as the main game. Team 1 uses player visual styles; team 2 uses enemy visual styles. The view has a fixed zoom level — no zoom or scroll is possible. The tile size is derived so that the full arena (all tiles) fits within the view.
|
- REQ-BAL-UI-INSPECT-VIEW: The arena view renders all tiles of the arena and displays ships, HQs, defence stations, and laser beams using the same visual elements and `visuals.toml` colors as the main game. Team 1 uses player visual styles; team 2 uses enemy visual styles. The view has a fixed zoom level — no zoom or scroll is possible. The tile size is derived so that the full arena (all tiles) fits within the view.
|
||||||
- REQ-BAL-UI-INSPECT-CLOSE: Closing the inspect window (via the window's close button) aborts the inspected arena's simulation. The arena widget's border returns to grey and its surviving counts are left as they were at the moment of closing. All main window buttons and controls are re-enabled.
|
- REQ-BAL-UI-INSPECT-CLOSE: Closing the inspect window (via the window's close button) aborts the inspected arena's simulation. The arena widget's border returns to grey and its surviving counts are left as they were at the moment of closing. All main window buttons and controls are re-enabled.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
set(TARGET_BASE_NAME "DotaFactory")
|
set(TARGET_BASE_NAME "${PRODUCT_NAME}")
|
||||||
|
|
||||||
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
||||||
set(TARGET_LIB_NAME "${TARGET_BASE_NAME}_lib")
|
set(TARGET_LIB_NAME "${TARGET_BASE_NAME}_lib")
|
||||||
@@ -117,6 +117,7 @@ target_link_libraries(${TARGET_UI_NAME}
|
|||||||
Qt5::Network
|
Qt5::Network
|
||||||
Qt5::Multimedia
|
Qt5::Multimedia
|
||||||
Qt5::Charts
|
Qt5::Charts
|
||||||
|
Qt5::Svg
|
||||||
)
|
)
|
||||||
target_compile_definitions(${TARGET_UI_NAME} PRIVATE TOML_FLOAT_CHARCONV=0)
|
target_compile_definitions(${TARGET_UI_NAME} PRIVATE TOML_FLOAT_CHARCONV=0)
|
||||||
|
|
||||||
@@ -183,6 +184,19 @@ target_compile_definitions(${TARGET_APP_NAME} PRIVATE
|
|||||||
)
|
)
|
||||||
target_link_libraries(${TARGET_APP_NAME} ${TARGET_UI_NAME})
|
target_link_libraries(${TARGET_APP_NAME} ${TARGET_UI_NAME})
|
||||||
|
|
||||||
|
# Embed the Windows version resource so the version shows on the executable's
|
||||||
|
# Details tab (right-click -> Properties). Values come from cmake/version.cmake
|
||||||
|
# (version numbers) and the product identity variables in the top-level
|
||||||
|
# CMakeLists.txt. MSVC compiles the .rc automatically once it is a target source.
|
||||||
|
if (WIN32)
|
||||||
|
configure_file(
|
||||||
|
"${CMAKE_SOURCE_DIR}/cmake/version.rc.in"
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/version.rc"
|
||||||
|
@ONLY
|
||||||
|
)
|
||||||
|
target_sources(${TARGET_APP_NAME} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/version.rc")
|
||||||
|
endif ()
|
||||||
|
|
||||||
unset(APP_FILES)
|
unset(APP_FILES)
|
||||||
unset(RELATIVE_HDRS)
|
unset(RELATIVE_HDRS)
|
||||||
unset(RELATIVE_SRCS)
|
unset(RELATIVE_SRCS)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "RepairSystem.h"
|
#include "RepairSystem.h"
|
||||||
#include "SalvagerSystem.h"
|
#include "SalvagerSystem.h"
|
||||||
#include "ScrapSystem.h"
|
#include "DebrisSystem.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
@@ -44,7 +44,6 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
|||||||
, m_team1HqEntity(entt::null)
|
, m_team1HqEntity(entt::null)
|
||||||
, m_team2HqEntity(entt::null)
|
, m_team2HqEntity(entt::null)
|
||||||
, m_finished(false)
|
, m_finished(false)
|
||||||
, m_winnerTeam(-1)
|
|
||||||
, m_stopRequested(false)
|
, m_stopRequested(false)
|
||||||
{
|
{
|
||||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
m_buildingSystem = std::make_unique<BuildingSystem>(
|
||||||
@@ -64,7 +63,7 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
|||||||
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
||||||
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
|
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
|
||||||
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
|
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
|
||||||
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
|
m_debrisSystem = std::make_unique<DebrisSystem>(m_admin);
|
||||||
m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin);
|
m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin);
|
||||||
m_repairSystem = std::make_unique<RepairSystem>(m_admin);
|
m_repairSystem = std::make_unique<RepairSystem>(m_admin);
|
||||||
|
|
||||||
@@ -95,7 +94,7 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
|||||||
updateStatus();
|
updateStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ArenaStatus::TeamStatus::ehpPercentText() const
|
std::string ArenaStatus::TeamStatus::getEhpPercentText() const
|
||||||
{
|
{
|
||||||
if (maxEhp <= 0.0)
|
if (maxEhp <= 0.0)
|
||||||
{
|
{
|
||||||
@@ -312,7 +311,7 @@ void ArenaSimulation::requestStop()
|
|||||||
m_stopRequested.store(true, std::memory_order_relaxed);
|
m_stopRequested.store(true, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
ArenaStatus ArenaSimulation::status() const
|
ArenaStatus ArenaSimulation::getStatus() const
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_statusMutex);
|
std::lock_guard<std::mutex> lock(m_statusMutex);
|
||||||
return m_status;
|
return m_status;
|
||||||
@@ -323,9 +322,9 @@ void ArenaSimulation::tick()
|
|||||||
// Ship behavior systems (tick step 7): evaluate, select winner, execute.
|
// Ship behavior systems (tick step 7): evaluate, select winner, execute.
|
||||||
// Module + combat systems emit their tool beams into a shared buffer.
|
// Module + combat systems emit their tool beams into a shared buffer.
|
||||||
m_shipSystem->clearMovementIntents();
|
m_shipSystem->clearMovementIntents();
|
||||||
m_aiSystem->tick(m_admin, *m_buildingSystem, *m_scrapSystem);
|
m_aiSystem->tick(m_admin, *m_buildingSystem, *m_debrisSystem);
|
||||||
std::vector<BeamFiredEvent> beamFiredEvents;
|
std::vector<BeamFiredEvent> beamFiredEvents;
|
||||||
m_salvagerSystem->tick(m_currentTick, *m_scrapSystem, *m_buildingSystem, beamFiredEvents);
|
m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, *m_buildingSystem, beamFiredEvents);
|
||||||
m_repairSystem->tick(m_currentTick, beamFiredEvents);
|
m_repairSystem->tick(m_currentTick, beamFiredEvents);
|
||||||
|
|
||||||
// Combat resolution (tick step 8).
|
// Combat resolution (tick step 8).
|
||||||
@@ -341,7 +340,7 @@ void ArenaSimulation::tick()
|
|||||||
m_dynamicBodySystem->tick(m_admin);
|
m_dynamicBodySystem->tick(m_admin);
|
||||||
|
|
||||||
// Scrap despawn (tick step 11).
|
// Scrap despawn (tick step 11).
|
||||||
m_scrapSystem->tickDespawn(m_currentTick);
|
m_debrisSystem->tickDespawn(m_currentTick);
|
||||||
|
|
||||||
++m_currentTick;
|
++m_currentTick;
|
||||||
|
|
||||||
@@ -372,8 +371,8 @@ void ArenaSimulation::tickDeaths()
|
|||||||
if (si.scrapDrop > 0)
|
if (si.scrapDrop > 0)
|
||||||
{
|
{
|
||||||
const Tick despawnAt = m_currentTick
|
const Tick despawnAt = m_currentTick
|
||||||
+ secondsToTicks(m_gameConfig.world.scrapDespawnSeconds);
|
+ secondsToTicks(m_gameConfig.world.debrisDespawnSeconds);
|
||||||
m_scrapSystem->spawn(pos.value, si.scrapDrop, despawnAt);
|
m_debrisSystem->spawn(pos.value, si.scrapDrop, despawnAt);
|
||||||
}
|
}
|
||||||
m_shipSystem->despawn(deadEntity);
|
m_shipSystem->despawn(deadEntity);
|
||||||
}
|
}
|
||||||
@@ -473,42 +472,42 @@ bool ArenaSimulation::isFinished() const
|
|||||||
return m_finished;
|
return m_finished;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ArenaSimulation::winnerTeam() const
|
std::optional<int> ArenaSimulation::getWinnerTeam() const
|
||||||
{
|
{
|
||||||
return m_winnerTeam;
|
return m_winnerTeam;
|
||||||
}
|
}
|
||||||
|
|
||||||
Tick ArenaSimulation::currentTick() const
|
Tick ArenaSimulation::getCurrentTick() const
|
||||||
{
|
{
|
||||||
return m_currentTick;
|
return m_currentTick;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArenaConfig& ArenaSimulation::arenaConfig() const
|
const ArenaConfig& ArenaSimulation::getArenaConfig() const
|
||||||
{
|
{
|
||||||
return m_arenaConfig;
|
return m_arenaConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BuildingSystem& ArenaSimulation::buildings() const
|
const BuildingSystem& ArenaSimulation::getBuildings() const
|
||||||
{
|
{
|
||||||
return *m_buildingSystem;
|
return *m_buildingSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShipSystem& ArenaSimulation::ships() const
|
const ShipSystem& ArenaSimulation::getShips() const
|
||||||
{
|
{
|
||||||
return *m_shipSystem;
|
return *m_shipSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ScrapSystem& ArenaSimulation::scraps() const
|
const DebrisSystem& ArenaSimulation::getDebrisSystem() const
|
||||||
{
|
{
|
||||||
return *m_scrapSystem;
|
return *m_debrisSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
EntityAdmin& ArenaSimulation::admin()
|
EntityAdmin& ArenaSimulation::getAdmin()
|
||||||
{
|
{
|
||||||
return m_admin;
|
return m_admin;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EntityAdmin& ArenaSimulation::admin() const
|
const EntityAdmin& ArenaSimulation::getAdmin() const
|
||||||
{
|
{
|
||||||
return m_admin;
|
return m_admin;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class MovementIntentSystem;
|
|||||||
class RepairSystem;
|
class RepairSystem;
|
||||||
class SalvagerSystem;
|
class SalvagerSystem;
|
||||||
class ShipSystem;
|
class ShipSystem;
|
||||||
class ScrapSystem;
|
class DebrisSystem;
|
||||||
|
|
||||||
struct ArenaStatus
|
struct ArenaStatus
|
||||||
{
|
{
|
||||||
@@ -46,19 +46,19 @@ struct ArenaStatus
|
|||||||
double threatLevel = 0.0; // accumulated threat of the team's configured ships
|
double threatLevel = 0.0; // accumulated threat of the team's configured ships
|
||||||
// Remaining durability of the team's ships and defence stations (HQ
|
// Remaining durability of the team's ships and defence stations (HQ
|
||||||
// excluded). currentEhp is summed live; maxEhp is the fixed full-HP
|
// excluded). currentEhp is summed live; maxEhp is the fixed full-HP
|
||||||
// baseline. See ehpPercentText() for the displayed value.
|
// baseline. See getEhpPercentText() for the displayed value.
|
||||||
double currentEhp = 0.0;
|
double currentEhp = 0.0;
|
||||||
double maxEhp = 0.0;
|
double maxEhp = 0.0;
|
||||||
std::vector<Entry> entries; // HQ first, then ships, then stations
|
std::vector<Entry> entries; // HQ first, then ships, then stations
|
||||||
|
|
||||||
// Remaining EHP as a whole-number percentage ("NN%"), or "n/a" when the
|
// Remaining EHP as a whole-number percentage ("NN%"), or "n/a" when the
|
||||||
// team has no ships or stations (maxEhp == 0).
|
// team has no ships or stations (maxEhp == 0).
|
||||||
std::string ehpPercentText() const;
|
std::string getEhpPercentText() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
TeamStatus teams[2];
|
TeamStatus teams[2];
|
||||||
bool finished = false;
|
bool finished = false;
|
||||||
int winnerTeam = -1; // 0 or 1 when finished; -1 while running
|
std::optional<int> winnerTeam; // 0 or 1 when finished; nullopt while running
|
||||||
// Game time the fight has lasted (simulated ticks * fixed tick duration).
|
// Game time the fight has lasted (simulated ticks * fixed tick duration).
|
||||||
// Meaningful once finished; the battle duration shown for completed runs.
|
// Meaningful once finished; the battle duration shown for completed runs.
|
||||||
double durationSeconds = 0.0;
|
double durationSeconds = 0.0;
|
||||||
@@ -78,17 +78,17 @@ public:
|
|||||||
void tickOnce();
|
void tickOnce();
|
||||||
std::vector<BeamFiredEvent> drainBeamFiredEvents();
|
std::vector<BeamFiredEvent> drainBeamFiredEvents();
|
||||||
|
|
||||||
ArenaStatus status() const;
|
ArenaStatus getStatus() const;
|
||||||
bool isFinished() const;
|
bool isFinished() const;
|
||||||
int winnerTeam() const;
|
std::optional<int> getWinnerTeam() const;
|
||||||
Tick currentTick() const;
|
Tick getCurrentTick() const;
|
||||||
|
|
||||||
const ArenaConfig& arenaConfig() const;
|
const ArenaConfig& getArenaConfig() const;
|
||||||
const BuildingSystem& buildings() const;
|
const BuildingSystem& getBuildings() const;
|
||||||
const ShipSystem& ships() const;
|
const ShipSystem& getShips() const;
|
||||||
const ScrapSystem& scraps() const;
|
const DebrisSystem& getDebrisSystem() const;
|
||||||
EntityAdmin& admin();
|
EntityAdmin& getAdmin();
|
||||||
const EntityAdmin& admin() const;
|
const EntityAdmin& getAdmin() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
BuildingId allocateBuildingId();
|
BuildingId allocateBuildingId();
|
||||||
@@ -114,7 +114,7 @@ private:
|
|||||||
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
|
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
|
||||||
std::unique_ptr<DynamicBodySystem> m_dynamicBodySystem;
|
std::unique_ptr<DynamicBodySystem> m_dynamicBodySystem;
|
||||||
std::unique_ptr<CombatSystem> m_combatSystem;
|
std::unique_ptr<CombatSystem> m_combatSystem;
|
||||||
std::unique_ptr<ScrapSystem> m_scrapSystem;
|
std::unique_ptr<DebrisSystem> m_debrisSystem;
|
||||||
std::unique_ptr<SalvagerSystem> m_salvagerSystem;
|
std::unique_ptr<SalvagerSystem> m_salvagerSystem;
|
||||||
std::unique_ptr<RepairSystem> m_repairSystem;
|
std::unique_ptr<RepairSystem> m_repairSystem;
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ private:
|
|||||||
entt::entity m_team2HqEntity;
|
entt::entity m_team2HqEntity;
|
||||||
|
|
||||||
bool m_finished;
|
bool m_finished;
|
||||||
int m_winnerTeam;
|
std::optional<int> m_winnerTeam;
|
||||||
std::atomic<bool> m_stopRequested;
|
std::atomic<bool> m_stopRequested;
|
||||||
|
|
||||||
// Static accumulated threat per team, computed once from the configured roster.
|
// Static accumulated threat per team, computed once from the configured roster.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "EntityHitTest.h"
|
#include "EntityHitTest.h"
|
||||||
#include "EntitySelectedEvent.h"
|
#include "EntitySelectionChangedEvent.h"
|
||||||
#include "EventManager.h"
|
#include "EventManager.h"
|
||||||
#include "FacingComponent.h"
|
#include "FacingComponent.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
@@ -24,11 +24,11 @@
|
|||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "RepairBehavior.h"
|
#include "RepairBehavior.h"
|
||||||
#include "SalvageScrapBehavior.h"
|
#include "SalvageScrapBehavior.h"
|
||||||
#include "ScrapSystem.h"
|
#include "DebrisSystem.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "StationBodyComponent.h"
|
#include "StationBodyComponent.h"
|
||||||
#include "ScrapDataComponent.h"
|
#include "DebrisComponent.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -73,7 +73,7 @@ void ArenaView::setGameSpeed(double multiplier)
|
|||||||
std::make_shared<GameSpeedChangedEvent>(multiplier));
|
std::make_shared<GameSpeedChangedEvent>(multiplier));
|
||||||
}
|
}
|
||||||
|
|
||||||
double ArenaView::gameSpeed() const
|
double ArenaView::getGameSpeed() const
|
||||||
{
|
{
|
||||||
return m_gameSpeedMultiplier;
|
return m_gameSpeedMultiplier;
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ void ArenaView::onFrame()
|
|||||||
// Expire old beams. Lifetime is measured in game ticks so beams stay
|
// Expire old beams. Lifetime is measured in game ticks so beams stay
|
||||||
// visible while the simulation is paused or slowed (REQ-SHP-FIRING-BEAM).
|
// visible while the simulation is paused or slowed (REQ-SHP-FIRING-BEAM).
|
||||||
{
|
{
|
||||||
const Tick now = m_sim->currentTick();
|
const Tick now = m_sim->getCurrentTick();
|
||||||
std::vector<ActiveBeam> live;
|
std::vector<ActiveBeam> live;
|
||||||
for (const ActiveBeam& b : m_activeBeams)
|
for (const ActiveBeam& b : m_activeBeams)
|
||||||
{
|
{
|
||||||
@@ -144,16 +144,16 @@ void ArenaView::onFrame()
|
|||||||
void ArenaView::handleEvent(std::shared_ptr<const BeamFiredEvent> event)
|
void ArenaView::handleEvent(std::shared_ptr<const BeamFiredEvent> event)
|
||||||
{
|
{
|
||||||
float maxRadius = 0.125f;
|
float maxRadius = 0.125f;
|
||||||
if (m_sim->admin().isValid(event->target)
|
if (m_sim->getAdmin().isValid(event->target)
|
||||||
&& m_sim->admin().hasAll<StationBodyComponent>(event->target))
|
&& m_sim->getAdmin().hasAll<StationBodyComponent>(event->target))
|
||||||
{
|
{
|
||||||
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(event->target);
|
const StationBodyComponent& sb = m_sim->getAdmin().get<StationBodyComponent>(event->target);
|
||||||
const int shorter = std::min(sb.footprint.width(),
|
const int shorter = std::min(sb.footprint.width(),
|
||||||
sb.footprint.height());
|
sb.footprint.height());
|
||||||
maxRadius = shorter / 2.0f;
|
maxRadius = shorter / 2.0f;
|
||||||
}
|
}
|
||||||
else if (m_sim->admin().isValid(event->target)
|
else if (m_sim->getAdmin().isValid(event->target)
|
||||||
&& m_sim->admin().hasAll<ScrapDataComponent>(event->target))
|
&& m_sim->getAdmin().hasAll<DebrisComponent>(event->target))
|
||||||
{
|
{
|
||||||
maxRadius = 0.1f;
|
maxRadius = 0.1f;
|
||||||
}
|
}
|
||||||
@@ -178,7 +178,7 @@ void ArenaView::paintGL()
|
|||||||
drawTiles(painter);
|
drawTiles(painter);
|
||||||
drawBuildings(painter);
|
drawBuildings(painter);
|
||||||
drawStations(painter);
|
drawStations(painter);
|
||||||
drawScrap(painter);
|
drawDebris(painter);
|
||||||
if (m_debugDraw)
|
if (m_debugDraw)
|
||||||
{
|
{
|
||||||
drawDebugSensorRanges(painter);
|
drawDebugSensorRanges(painter);
|
||||||
@@ -192,9 +192,9 @@ void ArenaView::paintGL()
|
|||||||
// Coordinate helpers
|
// Coordinate helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
float ArenaView::tilePx() const
|
float ArenaView::getTilePx() const
|
||||||
{
|
{
|
||||||
const ArenaConfig& ac = m_sim->arenaConfig();
|
const ArenaConfig& ac = m_sim->getArenaConfig();
|
||||||
const int totalWidth = ac.playerBufferWidth_tiles
|
const int totalWidth = ac.playerBufferWidth_tiles
|
||||||
+ ac.contestZoneWidth_tiles
|
+ ac.contestZoneWidth_tiles
|
||||||
+ ac.enemyBufferWidth_tiles;
|
+ ac.enemyBufferWidth_tiles;
|
||||||
@@ -209,8 +209,8 @@ float ArenaView::tilePx() const
|
|||||||
QPointF ArenaView::worldToWidget(QVector2D worldPos) const
|
QPointF ArenaView::worldToWidget(QVector2D worldPos) const
|
||||||
{
|
{
|
||||||
return QPointF(
|
return QPointF(
|
||||||
static_cast<qreal>(worldPos.x() * tilePx()),
|
static_cast<qreal>(worldPos.x() * getTilePx()),
|
||||||
static_cast<qreal>(worldPos.y() * tilePx()));
|
static_cast<qreal>(worldPos.y() * getTilePx()));
|
||||||
}
|
}
|
||||||
|
|
||||||
QPointF ArenaView::tileToWidget(QPoint tile) const
|
QPointF ArenaView::tileToWidget(QPoint tile) const
|
||||||
@@ -223,21 +223,21 @@ QRectF ArenaView::tileRect(QPoint tile) const
|
|||||||
{
|
{
|
||||||
const QPointF tl = tileToWidget(tile);
|
const QPointF tl = tileToWidget(tile);
|
||||||
return QRectF(tl.x(), tl.y(),
|
return QRectF(tl.x(), tl.y(),
|
||||||
static_cast<qreal>(tilePx()), static_cast<qreal>(tilePx()));
|
static_cast<qreal>(getTilePx()), static_cast<qreal>(getTilePx()));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<QVector2D> ArenaView::entityPosition(entt::entity entity) const
|
std::optional<QVector2D> ArenaView::entityPosition(entt::entity entity) const
|
||||||
{
|
{
|
||||||
if (!m_sim->admin().isValid(entity) || !m_sim->admin().hasAll<PositionComponent>(entity))
|
if (!m_sim->getAdmin().isValid(entity) || !m_sim->getAdmin().hasAll<PositionComponent>(entity))
|
||||||
{
|
{
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
return m_sim->admin().get<PositionComponent>(entity).value;
|
return m_sim->getAdmin().get<PositionComponent>(entity).value;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector2D ArenaView::widgetToWorld(QPoint widgetPt) const
|
QVector2D ArenaView::widgetToWorld(QPoint widgetPt) const
|
||||||
{
|
{
|
||||||
const float px = tilePx();
|
const float px = getTilePx();
|
||||||
if (px < 0.001f) { return QVector2D(0.0f, 0.0f); }
|
if (px < 0.001f) { return QVector2D(0.0f, 0.0f); }
|
||||||
return QVector2D(static_cast<float>(widgetPt.x()) / px,
|
return QVector2D(static_cast<float>(widgetPt.x()) / px,
|
||||||
static_cast<float>(widgetPt.y()) / px);
|
static_cast<float>(widgetPt.y()) / px);
|
||||||
@@ -248,7 +248,7 @@ void ArenaView::mousePressEvent(QMouseEvent* event)
|
|||||||
if (event->button() == Qt::LeftButton)
|
if (event->button() == Qt::LeftButton)
|
||||||
{
|
{
|
||||||
const QVector2D worldPos = widgetToWorld(event->pos());
|
const QVector2D worldPos = widgetToWorld(event->pos());
|
||||||
entt::entity hit = entityAtWorldPos(m_sim->admin(), worldPos);
|
entt::entity hit = entityAtWorldPos(m_sim->getAdmin(), worldPos);
|
||||||
|
|
||||||
if (hit != entt::null)
|
if (hit != entt::null)
|
||||||
{
|
{
|
||||||
@@ -259,8 +259,14 @@ void ArenaView::mousePressEvent(QMouseEvent* event)
|
|||||||
m_selectedEntity = std::nullopt;
|
m_selectedEntity = std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The arena is strictly single-select; emit a vector of size 0 or 1.
|
||||||
|
std::vector<entt::entity> selection;
|
||||||
|
if (m_selectedEntity.has_value())
|
||||||
|
{
|
||||||
|
selection.push_back(*m_selectedEntity);
|
||||||
|
}
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<EntitySelectedEvent>(m_selectedEntity));
|
std::make_shared<EntitySelectionChangedEvent>(selection));
|
||||||
}
|
}
|
||||||
|
|
||||||
QOpenGLWidget::mousePressEvent(event);
|
QOpenGLWidget::mousePressEvent(event);
|
||||||
@@ -282,7 +288,7 @@ void ArenaView::keyPressEvent(QKeyEvent* event)
|
|||||||
|
|
||||||
void ArenaView::drawTiles(QPainter& painter)
|
void ArenaView::drawTiles(QPainter& painter)
|
||||||
{
|
{
|
||||||
const ArenaConfig& ac = m_sim->arenaConfig();
|
const ArenaConfig& ac = m_sim->getArenaConfig();
|
||||||
const int totalWidth = ac.playerBufferWidth_tiles
|
const int totalWidth = ac.playerBufferWidth_tiles
|
||||||
+ ac.contestZoneWidth_tiles
|
+ ac.contestZoneWidth_tiles
|
||||||
+ ac.enemyBufferWidth_tiles;
|
+ ac.enemyBufferWidth_tiles;
|
||||||
@@ -300,7 +306,7 @@ void ArenaView::drawTiles(QPainter& painter)
|
|||||||
|
|
||||||
void ArenaView::drawBuildings(QPainter& painter)
|
void ArenaView::drawBuildings(QPainter& painter)
|
||||||
{
|
{
|
||||||
for (const Building& b : m_sim->buildings().allBuildings())
|
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
||||||
{
|
{
|
||||||
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
||||||
m_visuals->buildings.find(b.type);
|
m_visuals->buildings.find(b.type);
|
||||||
@@ -315,8 +321,8 @@ void ArenaView::drawBuildings(QPainter& painter)
|
|||||||
|
|
||||||
const QPointF tl = tileToWidget(b.anchor);
|
const QPointF tl = tileToWidget(b.anchor);
|
||||||
const QRectF bboxRect(tl.x(), tl.y(),
|
const QRectF bboxRect(tl.x(), tl.y(),
|
||||||
b.footprint.width() * static_cast<qreal>(tilePx()),
|
b.footprint.width() * static_cast<qreal>(getTilePx()),
|
||||||
b.footprint.height() * static_cast<qreal>(tilePx()));
|
b.footprint.height() * static_cast<qreal>(getTilePx()));
|
||||||
|
|
||||||
painter.setPen(QPen(bv.outline, 1));
|
painter.setPen(QPen(bv.outline, 1));
|
||||||
painter.setBrush(Qt::NoBrush);
|
painter.setBrush(Qt::NoBrush);
|
||||||
@@ -330,12 +336,12 @@ void ArenaView::drawBuildings(QPainter& painter)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArenaView::drawScrap(QPainter& painter)
|
void ArenaView::drawDebris(QPainter& painter)
|
||||||
{
|
{
|
||||||
const float r = tilePx() * 0.2f;
|
const float r = getTilePx() * 0.2f;
|
||||||
for (const ScrapInfo& scrap : m_sim->scraps().allScrapInfo())
|
for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
|
||||||
{
|
{
|
||||||
const QPointF center = worldToWidget(scrap.position);
|
const QPointF center = worldToWidget(debris.position);
|
||||||
painter.setBrush(QColor(128, 110, 90));
|
painter.setBrush(QColor(128, 110, 90));
|
||||||
painter.setPen(QPen(QColor(50, 40, 30), 1));
|
painter.setPen(QPen(QColor(50, 40, 30), 1));
|
||||||
painter.drawEllipse(center,
|
painter.drawEllipse(center,
|
||||||
@@ -345,7 +351,7 @@ void ArenaView::drawScrap(QPainter& painter)
|
|||||||
|
|
||||||
void ArenaView::drawStations(QPainter& painter)
|
void ArenaView::drawStations(QPainter& painter)
|
||||||
{
|
{
|
||||||
m_sim->admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
m_sim->getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||||
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f, const HealthComponent& h)
|
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f, const HealthComponent& h)
|
||||||
{
|
{
|
||||||
const BuildingType visType = f.isEnemy
|
const BuildingType visType = f.isEnemy
|
||||||
@@ -364,8 +370,8 @@ void ArenaView::drawStations(QPainter& painter)
|
|||||||
|
|
||||||
const QPointF tl = tileToWidget(sb.anchor);
|
const QPointF tl = tileToWidget(sb.anchor);
|
||||||
const QRectF bboxRect(tl.x(), tl.y(),
|
const QRectF bboxRect(tl.x(), tl.y(),
|
||||||
sb.footprint.width() * static_cast<qreal>(tilePx()),
|
sb.footprint.width() * static_cast<qreal>(getTilePx()),
|
||||||
sb.footprint.height() * static_cast<qreal>(tilePx()));
|
sb.footprint.height() * static_cast<qreal>(getTilePx()));
|
||||||
|
|
||||||
painter.setPen(QPen(bv.outline, 1));
|
painter.setPen(QPen(bv.outline, 1));
|
||||||
painter.setBrush(Qt::NoBrush);
|
painter.setBrush(Qt::NoBrush);
|
||||||
@@ -374,7 +380,7 @@ void ArenaView::drawStations(QPainter& painter)
|
|||||||
if (h.maxHp > 0.0f)
|
if (h.maxHp > 0.0f)
|
||||||
{
|
{
|
||||||
const float fraction = std::max(0.0f, h.hp / h.maxHp);
|
const float fraction = std::max(0.0f, h.hp / h.maxHp);
|
||||||
const qreal barH = static_cast<qreal>(tilePx()) * 0.12;
|
const qreal barH = static_cast<qreal>(getTilePx()) * 0.12;
|
||||||
const qreal barY = bboxRect.bottom() + 1.0;
|
const qreal barY = bboxRect.bottom() + 1.0;
|
||||||
const qreal barW = bboxRect.width();
|
const qreal barW = bboxRect.width();
|
||||||
painter.fillRect(QRectF(bboxRect.left(), barY, barW, barH),
|
painter.fillRect(QRectF(bboxRect.left(), barY, barW, barH),
|
||||||
@@ -394,7 +400,7 @@ void ArenaView::drawStations(QPainter& painter)
|
|||||||
|
|
||||||
void ArenaView::drawShips(QPainter& painter)
|
void ArenaView::drawShips(QPainter& painter)
|
||||||
{
|
{
|
||||||
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
|
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
|
||||||
FactionComponent, HealthComponent>(
|
FactionComponent, HealthComponent>(
|
||||||
[&](entt::entity e, const ShipIdentityComponent& si,
|
[&](entt::entity e, const ShipIdentityComponent& si,
|
||||||
const PositionComponent& pos, const FacingComponent& facing,
|
const PositionComponent& pos, const FacingComponent& facing,
|
||||||
@@ -408,8 +414,8 @@ void ArenaView::drawShips(QPainter& painter)
|
|||||||
const QVector2D dir(std::cos(facing.radians), std::sin(facing.radians));
|
const QVector2D dir(std::cos(facing.radians), std::sin(facing.radians));
|
||||||
const QVector2D perp(-dir.y(), dir.x());
|
const QVector2D perp(-dir.y(), dir.x());
|
||||||
|
|
||||||
const float fwd = tilePx() * 0.45f;
|
const float fwd = getTilePx() * 0.45f;
|
||||||
const float side = tilePx() * 0.25f;
|
const float side = getTilePx() * 0.25f;
|
||||||
|
|
||||||
QPolygonF tri;
|
QPolygonF tri;
|
||||||
tri << QPointF(center.x() + static_cast<qreal>(dir.x() * fwd),
|
tri << QPointF(center.x() + static_cast<qreal>(dir.x() * fwd),
|
||||||
@@ -427,7 +433,7 @@ void ArenaView::drawShips(QPainter& painter)
|
|||||||
{
|
{
|
||||||
const float fraction = std::max(0.0f, h.hp / h.maxHp);
|
const float fraction = std::max(0.0f, h.hp / h.maxHp);
|
||||||
const qreal barW = static_cast<qreal>(fwd) * 2.0;
|
const qreal barW = static_cast<qreal>(fwd) * 2.0;
|
||||||
const qreal barH = static_cast<qreal>(tilePx()) * 0.12;
|
const qreal barH = static_cast<qreal>(getTilePx()) * 0.12;
|
||||||
const qreal barX = center.x() - static_cast<qreal>(fwd);
|
const qreal barX = center.x() - static_cast<qreal>(fwd);
|
||||||
const qreal barY = center.y() + static_cast<qreal>(fwd) + 1.0;
|
const qreal barY = center.y() + static_cast<qreal>(fwd) + 1.0;
|
||||||
painter.fillRect(QRectF(barX, barY, barW, barH), QColor(60, 60, 60));
|
painter.fillRect(QRectF(barX, barY, barW, barH), QColor(60, 60, 60));
|
||||||
@@ -437,7 +443,7 @@ void ArenaView::drawShips(QPainter& painter)
|
|||||||
|
|
||||||
if (m_selectedEntity.has_value() && *m_selectedEntity == e)
|
if (m_selectedEntity.has_value() && *m_selectedEntity == e)
|
||||||
{
|
{
|
||||||
const qreal radius = static_cast<qreal>(tilePx()) * 0.55;
|
const qreal radius = static_cast<qreal>(getTilePx()) * 0.55;
|
||||||
painter.setPen(QPen(QColor(255, 255, 0), 2));
|
painter.setPen(QPen(QColor(255, 255, 0), 2));
|
||||||
painter.setBrush(Qt::NoBrush);
|
painter.setBrush(Qt::NoBrush);
|
||||||
painter.drawEllipse(center, radius, radius);
|
painter.drawEllipse(center, radius, radius);
|
||||||
@@ -448,7 +454,7 @@ void ArenaView::drawShips(QPainter& painter)
|
|||||||
void ArenaView::drawDebugSensorRanges(QPainter& painter)
|
void ArenaView::drawDebugSensorRanges(QPainter& painter)
|
||||||
{
|
{
|
||||||
painter.setBrush(Qt::NoBrush);
|
painter.setBrush(Qt::NoBrush);
|
||||||
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, SensorRangeComponent>(
|
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, SensorRangeComponent>(
|
||||||
[&](entt::entity /*e*/, const ShipIdentityComponent& si,
|
[&](entt::entity /*e*/, const ShipIdentityComponent& si,
|
||||||
const PositionComponent& pos, const SensorRangeComponent& sensor)
|
const PositionComponent& pos, const SensorRangeComponent& sensor)
|
||||||
{
|
{
|
||||||
@@ -458,7 +464,7 @@ void ArenaView::drawDebugSensorRanges(QPainter& painter)
|
|||||||
|
|
||||||
const QPointF center = worldToWidget(pos.value);
|
const QPointF center = worldToWidget(pos.value);
|
||||||
const qreal radiusPx = static_cast<qreal>(sensor.value_tiles)
|
const qreal radiusPx = static_cast<qreal>(sensor.value_tiles)
|
||||||
* static_cast<qreal>(tilePx());
|
* static_cast<qreal>(getTilePx());
|
||||||
QColor circleColor = it->second.outline;
|
QColor circleColor = it->second.outline;
|
||||||
circleColor.setAlpha(77);
|
circleColor.setAlpha(77);
|
||||||
painter.setPen(QPen(circleColor, 1));
|
painter.setPen(QPen(circleColor, 1));
|
||||||
@@ -487,7 +493,7 @@ void ArenaView::drawDebugTargetLines(QPainter& painter)
|
|||||||
painter.drawLine(worldToWidget(from), worldToWidget(to));
|
painter.drawLine(worldToWidget(from), worldToWidget(to));
|
||||||
};
|
};
|
||||||
|
|
||||||
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent,
|
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent,
|
||||||
FactionComponent, AttackBehavior>(
|
FactionComponent, AttackBehavior>(
|
||||||
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
||||||
const PositionComponent& pos, const FactionComponent& fac,
|
const PositionComponent& pos, const FactionComponent& fac,
|
||||||
@@ -502,7 +508,7 @@ void ArenaView::drawDebugTargetLines(QPainter& painter)
|
|||||||
drawTargetLine(fac.isEnemy, pos.value, *targetPos);
|
drawTargetLine(fac.isEnemy, pos.value, *targetPos);
|
||||||
});
|
});
|
||||||
|
|
||||||
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent,
|
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent,
|
||||||
FactionComponent, RepairBehavior>(
|
FactionComponent, RepairBehavior>(
|
||||||
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
||||||
const PositionComponent& pos, const FactionComponent& fac,
|
const PositionComponent& pos, const FactionComponent& fac,
|
||||||
@@ -517,15 +523,15 @@ void ArenaView::drawDebugTargetLines(QPainter& painter)
|
|||||||
drawTargetLine(fac.isEnemy, pos.value, *targetPos);
|
drawTargetLine(fac.isEnemy, pos.value, *targetPos);
|
||||||
});
|
});
|
||||||
|
|
||||||
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent,
|
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent,
|
||||||
FactionComponent, SalvageScrapBehavior>(
|
FactionComponent, SalvageScrapBehavior>(
|
||||||
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
[&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
||||||
const PositionComponent& pos, const FactionComponent& fac,
|
const PositionComponent& pos, const FactionComponent& fac,
|
||||||
const SalvageScrapBehavior& salvage)
|
const SalvageScrapBehavior& salvage)
|
||||||
{
|
{
|
||||||
if (!salvage.scrapTarget.has_value()) { return; }
|
if (!salvage.debrisTarget.has_value()) { return; }
|
||||||
|
|
||||||
drawTargetLine(fac.isEnemy, pos.value, *salvage.scrapTarget);
|
drawTargetLine(fac.isEnemy, pos.value, *salvage.debrisTarget);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
#include "BeamFiredEvent.h"
|
#include "BeamFiredEvent.h"
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
#include "EntitySelectedEvent.h"
|
#include "EntitySelectionChangedEvent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "TickDriver.h"
|
#include "TickDriver.h"
|
||||||
#include "VisualsConfig.h"
|
#include "VisualsConfig.h"
|
||||||
@@ -32,7 +32,7 @@ public:
|
|||||||
~ArenaView() override;
|
~ArenaView() override;
|
||||||
|
|
||||||
void setGameSpeed(double multiplier);
|
void setGameSpeed(double multiplier);
|
||||||
double gameSpeed() const;
|
double getGameSpeed() const;
|
||||||
void togglePause();
|
void togglePause();
|
||||||
void stopRendering();
|
void stopRendering();
|
||||||
|
|
||||||
@@ -50,13 +50,13 @@ private:
|
|||||||
void drawTiles(QPainter& painter);
|
void drawTiles(QPainter& painter);
|
||||||
void drawBuildings(QPainter& painter);
|
void drawBuildings(QPainter& painter);
|
||||||
void drawStations(QPainter& painter);
|
void drawStations(QPainter& painter);
|
||||||
void drawScrap(QPainter& painter);
|
void drawDebris(QPainter& painter);
|
||||||
void drawShips(QPainter& painter);
|
void drawShips(QPainter& painter);
|
||||||
void drawDebugSensorRanges(QPainter& painter);
|
void drawDebugSensorRanges(QPainter& painter);
|
||||||
void drawDebugTargetLines(QPainter& painter);
|
void drawDebugTargetLines(QPainter& painter);
|
||||||
void drawBeams(QPainter& painter);
|
void drawBeams(QPainter& painter);
|
||||||
|
|
||||||
float tilePx() const;
|
float getTilePx() const;
|
||||||
QPointF worldToWidget(QVector2D worldPos) const;
|
QPointF worldToWidget(QVector2D worldPos) const;
|
||||||
QPointF tileToWidget(QPoint tile) const;
|
QPointF tileToWidget(QPoint tile) const;
|
||||||
QRectF tileRect(QPoint tile) const;
|
QRectF tileRect(QPoint tile) const;
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ void ArenaWidget::updateStatus(const ArenaStatus& status)
|
|||||||
}
|
}
|
||||||
|
|
||||||
threat->setText(tr("Threat: %1").arg(QString::number(team.threatLevel, 'f', 0)));
|
threat->setText(tr("Threat: %1").arg(QString::number(team.threatLevel, 'f', 0)));
|
||||||
ehp->setText(tr("EHP: %1").arg(QString::fromStdString(team.ehpPercentText())));
|
ehp->setText(tr("EHP: %1").arg(QString::fromStdString(team.getEhpPercentText())));
|
||||||
|
|
||||||
QString lines;
|
QString lines;
|
||||||
for (const ArenaStatus::Entry& entry : team.entries)
|
for (const ArenaStatus::Entry& entry : team.entries)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ namespace
|
|||||||
header = QStringLiteral("[WON] ") + header;
|
header = QStringLiteral("[WON] ") + header;
|
||||||
}
|
}
|
||||||
header += QStringLiteral(" - threat %1").arg(QString::number(team.threatLevel, 'f', 0));
|
header += QStringLiteral(" - threat %1").arg(QString::number(team.threatLevel, 'f', 0));
|
||||||
header += QStringLiteral(" - EHP %1").arg(QString::fromStdString(team.ehpPercentText()));
|
header += QStringLiteral(" - EHP %1").arg(QString::fromStdString(team.getEhpPercentText()));
|
||||||
return escapeCell(header);
|
return escapeCell(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,6 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
|||||||
, m_balancingConfigPath(balancingConfigPath)
|
, m_balancingConfigPath(balancingConfigPath)
|
||||||
, m_nextSeed(0)
|
, m_nextSeed(0)
|
||||||
, m_inspectWindow(nullptr)
|
, m_inspectWindow(nullptr)
|
||||||
, m_inspectedArenaIndex(-1)
|
|
||||||
{
|
{
|
||||||
m_visuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
m_visuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||||
setWindowTitle(tr("DotaFactory — Balancing Tool"));
|
setWindowTitle(tr("DotaFactory — Balancing Tool"));
|
||||||
@@ -147,7 +146,7 @@ void BalancingWindow::populateArenas(const BalancingConfig& balancingConfig)
|
|||||||
entry.widget = new ArenaWidget(index, arenaConfig.name, scrollContent);
|
entry.widget = new ArenaWidget(index, arenaConfig.name, scrollContent);
|
||||||
contentLayout->addWidget(entry.widget);
|
contentLayout->addWidget(entry.widget);
|
||||||
|
|
||||||
entry.widget->updateStatus(entry.simulation->status());
|
entry.widget->updateStatus(entry.simulation->getStatus());
|
||||||
|
|
||||||
m_arenas.push_back(std::move(entry));
|
m_arenas.push_back(std::move(entry));
|
||||||
}
|
}
|
||||||
@@ -179,15 +178,15 @@ void BalancingWindow::pollStatuses()
|
|||||||
{
|
{
|
||||||
if (entry.worker.joinable())
|
if (entry.worker.joinable())
|
||||||
{
|
{
|
||||||
const ArenaStatus status = entry.simulation->status();
|
const ArenaStatus status = entry.simulation->getStatus();
|
||||||
entry.widget->updateStatus(status);
|
entry.widget->updateStatus(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_inspectedSim && m_inspectedArenaIndex >= 0)
|
if (m_inspectedSim && m_inspectedArenaIndex.has_value())
|
||||||
{
|
{
|
||||||
const ArenaStatus status = m_inspectedSim->status();
|
const ArenaStatus status = m_inspectedSim->getStatus();
|
||||||
m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->updateStatus(status);
|
m_arenas[static_cast<std::size_t>(*m_inspectedArenaIndex)].widget->updateStatus(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateButtons();
|
updateButtons();
|
||||||
@@ -242,7 +241,7 @@ void BalancingWindow::startArena(int index)
|
|||||||
entry.simulation = std::make_unique<ArenaSimulation>(
|
entry.simulation = std::make_unique<ArenaSimulation>(
|
||||||
m_gameConfig, entry.config, m_nextSeed++);
|
m_gameConfig, entry.config, m_nextSeed++);
|
||||||
entry.widget->startSimulation();
|
entry.widget->startSimulation();
|
||||||
entry.widget->updateStatus(entry.simulation->status());
|
entry.widget->updateStatus(entry.simulation->getStatus());
|
||||||
ArenaSimulation* sim = entry.simulation.get();
|
ArenaSimulation* sim = entry.simulation.get();
|
||||||
entry.worker = std::thread([sim]() { sim->run(); });
|
entry.worker = std::thread([sim]() { sim->run(); });
|
||||||
updateButtons();
|
updateButtons();
|
||||||
@@ -255,13 +254,13 @@ void BalancingWindow::inspectArena(int index)
|
|||||||
delete m_inspectWindow;
|
delete m_inspectWindow;
|
||||||
m_inspectWindow = nullptr;
|
m_inspectWindow = nullptr;
|
||||||
|
|
||||||
if (m_inspectedSim && m_inspectedArenaIndex >= 0
|
if (m_inspectedSim && m_inspectedArenaIndex.has_value()
|
||||||
&& !m_inspectedSim->isFinished())
|
&& !m_inspectedSim->isFinished())
|
||||||
{
|
{
|
||||||
m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->resetToGrey();
|
m_arenas[static_cast<std::size_t>(*m_inspectedArenaIndex)].widget->resetToGrey();
|
||||||
}
|
}
|
||||||
m_inspectedSim.reset();
|
m_inspectedSim.reset();
|
||||||
m_inspectedArenaIndex = -1;
|
m_inspectedArenaIndex = std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
ArenaEntry& entry = m_arenas[static_cast<std::size_t>(index)];
|
ArenaEntry& entry = m_arenas[static_cast<std::size_t>(index)];
|
||||||
@@ -278,7 +277,7 @@ void BalancingWindow::inspectArena(int index)
|
|||||||
|
|
||||||
entry.widget->resetToGrey();
|
entry.widget->resetToGrey();
|
||||||
entry.widget->startSimulation();
|
entry.widget->startSimulation();
|
||||||
entry.widget->updateStatus(m_inspectedSim->status());
|
entry.widget->updateStatus(m_inspectedSim->getStatus());
|
||||||
|
|
||||||
m_inspectWindow = new InspectWindow(
|
m_inspectWindow = new InspectWindow(
|
||||||
m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr);
|
m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr);
|
||||||
@@ -297,16 +296,16 @@ void BalancingWindow::closeInspectWindow()
|
|||||||
m_inspectWindow->deleteLater();
|
m_inspectWindow->deleteLater();
|
||||||
m_inspectWindow = nullptr;
|
m_inspectWindow = nullptr;
|
||||||
|
|
||||||
if (m_inspectedArenaIndex >= 0 && m_inspectedSim)
|
if (m_inspectedArenaIndex.has_value() && m_inspectedSim)
|
||||||
{
|
{
|
||||||
if (!m_inspectedSim->isFinished())
|
if (!m_inspectedSim->isFinished())
|
||||||
{
|
{
|
||||||
m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->resetToGrey();
|
m_arenas[static_cast<std::size_t>(*m_inspectedArenaIndex)].widget->resetToGrey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_inspectedSim.reset();
|
m_inspectedSim.reset();
|
||||||
m_inspectedArenaIndex = -1;
|
m_inspectedArenaIndex = std::nullopt;
|
||||||
setMainControlsEnabled(true);
|
setMainControlsEnabled(true);
|
||||||
updateButtons();
|
updateButtons();
|
||||||
}
|
}
|
||||||
@@ -336,7 +335,7 @@ void BalancingWindow::updateButtons()
|
|||||||
bool allRunning = true;
|
bool allRunning = true;
|
||||||
for (ArenaEntry& entry : m_arenas)
|
for (ArenaEntry& entry : m_arenas)
|
||||||
{
|
{
|
||||||
if (entry.worker.joinable() && !entry.simulation->status().finished)
|
if (entry.worker.joinable() && !entry.simulation->getStatus().finished)
|
||||||
{
|
{
|
||||||
anyRunning = true;
|
anyRunning = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -78,6 +79,6 @@ private:
|
|||||||
QTimer* m_pollTimer;
|
QTimer* m_pollTimer;
|
||||||
|
|
||||||
InspectWindow* m_inspectWindow;
|
InspectWindow* m_inspectWindow;
|
||||||
int m_inspectedArenaIndex;
|
std::optional<int> m_inspectedArenaIndex; // nullopt = no arena inspected
|
||||||
std::unique_ptr<ArenaSimulation> m_inspectedSim;
|
std::unique_ptr<ArenaSimulation> m_inspectedSim;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "InspectWindowClosedEvent.h"
|
#include "InspectWindowClosedEvent.h"
|
||||||
#include "ModuleOwnerComponent.h"
|
#include "ModuleOwnerComponent.h"
|
||||||
|
#include "SelectedBehaviorComponent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipStatsCalculator.h"
|
#include "ShipStatsCalculator.h"
|
||||||
#include "ShipStatsPanel.h"
|
#include "ShipStatsPanel.h"
|
||||||
@@ -198,7 +199,7 @@ void InspectWindow::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> eve
|
|||||||
|
|
||||||
void InspectWindow::pollStatus()
|
void InspectWindow::pollStatus()
|
||||||
{
|
{
|
||||||
const ArenaStatus status = m_sim->status();
|
const ArenaStatus status = m_sim->getStatus();
|
||||||
updateInfoPanel(status);
|
updateInfoPanel(status);
|
||||||
refreshEntityStats();
|
refreshEntityStats();
|
||||||
}
|
}
|
||||||
@@ -227,7 +228,7 @@ void InspectWindow::updateInfoPanel(const ArenaStatus& status)
|
|||||||
}
|
}
|
||||||
|
|
||||||
threat->setText(tr("Threat: %1").arg(QString::number(team.threatLevel, 'f', 0)));
|
threat->setText(tr("Threat: %1").arg(QString::number(team.threatLevel, 'f', 0)));
|
||||||
ehp->setText(tr("EHP: %1").arg(QString::fromStdString(team.ehpPercentText())));
|
ehp->setText(tr("EHP: %1").arg(QString::fromStdString(team.getEhpPercentText())));
|
||||||
|
|
||||||
QString lines;
|
QString lines;
|
||||||
for (const ArenaStatus::Entry& entry : team.entries)
|
for (const ArenaStatus::Entry& entry : team.entries)
|
||||||
@@ -249,13 +250,14 @@ void InspectWindow::updateInfoPanel(const ArenaStatus& status)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InspectWindow::handleEvent(std::shared_ptr<const EntitySelectedEvent> event)
|
void InspectWindow::handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event)
|
||||||
{
|
{
|
||||||
if (event->entity.has_value())
|
if (!event->entities.empty())
|
||||||
{
|
{
|
||||||
m_selectedEntity = event->entity;
|
// The arena is single-select, so only the first entity is inspected.
|
||||||
|
m_selectedEntity = event->entities.front();
|
||||||
|
|
||||||
EntityAdmin& admin = m_sim->admin();
|
EntityAdmin& admin = m_sim->getAdmin();
|
||||||
entt::entity entity = *m_selectedEntity;
|
entt::entity entity = *m_selectedEntity;
|
||||||
|
|
||||||
if (!admin.isValid(entity))
|
if (!admin.isValid(entity))
|
||||||
@@ -278,6 +280,8 @@ void InspectWindow::handleEvent(std::shared_ptr<const EntitySelectedEvent> event
|
|||||||
|
|
||||||
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
||||||
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
||||||
|
m_entityStatsPanel->setBehavior(
|
||||||
|
admin.get<SelectedBehaviorComponent>(entity).winner);
|
||||||
m_entityStatsPanel->show();
|
m_entityStatsPanel->show();
|
||||||
m_stationStatsLabel->hide();
|
m_stationStatsLabel->hide();
|
||||||
}
|
}
|
||||||
@@ -329,7 +333,7 @@ void InspectWindow::refreshEntityStats()
|
|||||||
{
|
{
|
||||||
if (!m_selectedEntity.has_value()) { return; }
|
if (!m_selectedEntity.has_value()) { return; }
|
||||||
|
|
||||||
EntityAdmin& admin = m_sim->admin();
|
EntityAdmin& admin = m_sim->getAdmin();
|
||||||
entt::entity entity = *m_selectedEntity;
|
entt::entity entity = *m_selectedEntity;
|
||||||
|
|
||||||
if (!admin.isValid(entity))
|
if (!admin.isValid(entity))
|
||||||
@@ -355,6 +359,8 @@ void InspectWindow::refreshEntityStats()
|
|||||||
{
|
{
|
||||||
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
||||||
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
||||||
|
m_entityStatsPanel->setBehavior(
|
||||||
|
admin.get<SelectedBehaviorComponent>(entity).winner);
|
||||||
}
|
}
|
||||||
else if (admin.hasAll<StationBodyComponent>(entity))
|
else if (admin.hasAll<StationBodyComponent>(entity))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
#include "ArenaSimulation.h"
|
#include "ArenaSimulation.h"
|
||||||
#include "EntitySelectedEvent.h"
|
#include "EntitySelectionChangedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "GameSpeedChangedEvent.h"
|
#include "GameSpeedChangedEvent.h"
|
||||||
@@ -22,7 +22,7 @@ class ArenaView;
|
|||||||
class ShipStatsPanel;
|
class ShipStatsPanel;
|
||||||
|
|
||||||
class InspectWindow : public QWidget,
|
class InspectWindow : public QWidget,
|
||||||
public CombinedEventHandler<EntitySelectedEvent,
|
public CombinedEventHandler<EntitySelectionChangedEvent,
|
||||||
GameSpeedChangedEvent>
|
GameSpeedChangedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -38,7 +38,7 @@ protected:
|
|||||||
void keyPressEvent(QKeyEvent* event) override;
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
|
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -18,9 +19,30 @@ struct BuildingDef
|
|||||||
// Stored as raw strings here; parsing into per-cell tiles + output ports
|
// Stored as raw strings here; parsing into per-cell tiles + output ports
|
||||||
// happens when buildings are placed, not at load time.
|
// happens when buildings are placed, not at load time.
|
||||||
std::vector<std::string> surfaceMask;
|
std::vector<std::string> surfaceMask;
|
||||||
|
|
||||||
|
// Output-buffer holding size for buildings without a recipe-driven buffer.
|
||||||
|
// Only the Salvage Bay sets this (REQ-BLD-SALVAGE-BAY).
|
||||||
|
std::optional<int> outputBufferCapacity;
|
||||||
|
|
||||||
|
// Optional hover-tooltip text for the build button (REQ-UI-BUILD-TOOLTIP).
|
||||||
|
std::optional<std::string> tooltip;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BuildingsConfig
|
struct BuildingsConfig
|
||||||
{
|
{
|
||||||
std::vector<BuildingDef> buildings;
|
std::vector<BuildingDef> buildings;
|
||||||
|
|
||||||
|
// Returns the definition for the given building type, or nullptr if the
|
||||||
|
// type has no entry in buildings.toml.
|
||||||
|
const BuildingDef* findBuildingDef(BuildingType type) const
|
||||||
|
{
|
||||||
|
for (const BuildingDef& def : buildings)
|
||||||
|
{
|
||||||
|
if (def.type == type)
|
||||||
|
{
|
||||||
|
return &def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/StationsConfig.h
|
${CMAKE_CURRENT_SOURCE_DIR}/StationsConfig.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/GameConfig.h
|
${CMAKE_CURRENT_SOURCE_DIR}/GameConfig.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ModulesConfig.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ModulesConfig.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/UnlocksConfig.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.h
|
||||||
|
|||||||
@@ -263,8 +263,9 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
|||||||
|
|
||||||
cfg.heightTiles = static_cast<int>(requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles"));
|
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.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.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.debrisDespawnSeconds = requireDouble(tbl["world"]["debris_despawn_seconds"], file, "world.debris_despawn_seconds");
|
||||||
cfg.scrapPerThreat = requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat");
|
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.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.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m;
|
||||||
@@ -273,6 +274,18 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
|||||||
cfg.orbitFactor = requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor");
|
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");
|
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.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.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.contestZoneWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles"));
|
||||||
@@ -305,6 +318,10 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
|||||||
cfg.artifacts.artifactChanceFormula = requireFormula(tbl["artifacts"]["artifact_chance_formula"], file, "artifacts.artifact_chance_formula");
|
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.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;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,6 +350,17 @@ BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
|
|||||||
def.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
|
def.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
|
||||||
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
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);
|
const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
|
||||||
if (!parsedType)
|
if (!parsedType)
|
||||||
{
|
{
|
||||||
@@ -377,18 +405,10 @@ RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
|
|||||||
}
|
}
|
||||||
def.building = *parsedType;
|
def.building = *parsedType;
|
||||||
|
|
||||||
if (def.building == BuildingType::Assembler)
|
if (def.building == BuildingType::Assembler && mt.contains("unlocked_at_start"))
|
||||||
{
|
{
|
||||||
const auto level = mt["unlock_at_station_level"].value<int64_t>();
|
def.unlockedAtStart = requireBool(mt["unlocked_at_start"], file,
|
||||||
if (level)
|
elemPath + ".unlocked_at_start");
|
||||||
{
|
|
||||||
def.unlockAtStationLevel = static_cast<int>(*level);
|
|
||||||
}
|
|
||||||
if (mt.contains("unlock_requires"))
|
|
||||||
{
|
|
||||||
def.unlockRequires = requireStringArray(mt["unlock_requires"], file,
|
|
||||||
elemPath + ".unlock_requires");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// inputs may be omitted (e.g. miner recipes). An empty array is fine.
|
// inputs may be omitted (e.g. miner recipes). An empty array is fine.
|
||||||
@@ -401,6 +421,14 @@ RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
|
|||||||
const toml::array& outputs = requireArray(mt["outputs"], file, elemPath + ".outputs");
|
const toml::array& outputs = requireArray(mt["outputs"], file, elemPath + ".outputs");
|
||||||
def.outputs = parseRecipeOutputs(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 = requireString(mt["icon"], file, elemPath + ".icon");
|
||||||
|
}
|
||||||
|
|
||||||
cfg.recipes.push_back(std::move(def));
|
cfg.recipes.push_back(std::move(def));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,11 +455,6 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
|||||||
|
|
||||||
ShipDef def;
|
ShipDef def;
|
||||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||||
def.unlockAtStationLevel = static_cast<int>(requireInt(mt["unlock_at_station_level"], file, elemPath + ".unlock_at_station_level"));
|
|
||||||
if (mt.contains("unlock_requires"))
|
|
||||||
{
|
|
||||||
def.unlockRequires = requireStringArray(mt["unlock_requires"], file, elemPath + ".unlock_requires");
|
|
||||||
}
|
|
||||||
def.layout = requireStringArray(mt["layout"], file, elemPath + ".layout");
|
def.layout = requireStringArray(mt["layout"], file, elemPath + ".layout");
|
||||||
|
|
||||||
// Schematic
|
// Schematic
|
||||||
@@ -581,18 +604,17 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|||||||
|
|
||||||
ModuleDef def;
|
ModuleDef def;
|
||||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||||
def.unlockAtStationLevel = static_cast<int>(
|
|
||||||
mt["unlock_at_station_level"].value_or<int64_t>(-1));
|
|
||||||
if (mt.contains("unlock_requires"))
|
|
||||||
{
|
|
||||||
def.unlockRequires = requireStringArray(mt["unlock_requires"], file, elemPath + ".unlock_requires");
|
|
||||||
}
|
|
||||||
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||||
def.productionTimeSeconds = requireDouble(
|
def.productionTimeSeconds = requireDouble(
|
||||||
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
||||||
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
||||||
def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph");
|
def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph");
|
||||||
|
|
||||||
|
if (mt.contains("tooltip"))
|
||||||
|
{
|
||||||
|
def.tooltip = requireString(mt["tooltip"], file, elemPath + ".tooltip");
|
||||||
|
}
|
||||||
|
|
||||||
// Materials
|
// Materials
|
||||||
{
|
{
|
||||||
const toml::array& materials = requireArray(mt["materials"], file, elemPath + ".materials");
|
const toml::array& materials = requireArray(mt["materials"], file, elemPath + ".materials");
|
||||||
@@ -699,62 +721,141 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|||||||
return cfg;
|
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
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
// Throws if any id in requiredIds is not a valid explicitly-unlockable schematic.
|
// Validates unlocks.toml against the rest of the config (REQ-LOCK-EXPLICIT,
|
||||||
void checkUnlockRequires(const std::vector<std::string>& requiredIds,
|
// REQ-LOCK-PREREQ): each granted id resolves to the right kind of definition
|
||||||
const std::unordered_set<std::string>& schematicIds,
|
// (recipe grants must name assembler recipes), each grantable id is granted by
|
||||||
const std::string& file,
|
// at most one group, group ids are unique, every group grants at least one
|
||||||
const std::string& path)
|
// item, and every `requires` id names a defined group.
|
||||||
|
void validateUnlocks(const GameConfig& cfg)
|
||||||
{
|
{
|
||||||
for (const std::string& requiredId : requiredIds)
|
const std::string file = "unlocks.toml";
|
||||||
{
|
|
||||||
if (schematicIds.count(requiredId) == 0)
|
|
||||||
{
|
|
||||||
throw makeError(file, path,
|
|
||||||
"references unknown schematic '" + requiredId + "'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validates that every id listed in an `unlock_requires` (REQ-LOCK-PREREQ)
|
std::unordered_set<std::string> shipIds;
|
||||||
// resolves to an explicitly-unlockable schematic: a ship, a module, or an
|
for (const ShipDef& def : cfg.ships.ships) { shipIds.insert(def.id); }
|
||||||
// assembler recipe that carries `unlock_at_station_level` (a recipe schematic,
|
std::unordered_set<std::string> moduleIds;
|
||||||
// per REQ-LOCK-EXPLICIT). An unresolved id is a config error surfaced at load.
|
for (const ModuleDef& def : cfg.modules.modules) { moduleIds.insert(def.id); }
|
||||||
void validateUnlockRequires(const GameConfig& cfg)
|
std::unordered_set<std::string> buildingIds;
|
||||||
{
|
for (const BuildingDef& def : cfg.buildings.buildings) { buildingIds.insert(def.id); }
|
||||||
std::unordered_set<std::string> schematicIds;
|
std::unordered_set<std::string> assemblerRecipeIds;
|
||||||
for (const ShipDef& def : cfg.ships.ships)
|
|
||||||
{
|
|
||||||
schematicIds.insert(def.id);
|
|
||||||
}
|
|
||||||
for (const ModuleDef& def : cfg.modules.modules)
|
|
||||||
{
|
|
||||||
schematicIds.insert(def.id);
|
|
||||||
}
|
|
||||||
for (const RecipeDef& def : cfg.recipes.recipes)
|
for (const RecipeDef& def : cfg.recipes.recipes)
|
||||||
{
|
{
|
||||||
if (def.building == BuildingType::Assembler && def.unlockAtStationLevel.has_value())
|
if (def.building == BuildingType::Assembler) { assemblerRecipeIds.insert(def.id); }
|
||||||
{
|
|
||||||
schematicIds.insert(def.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const ShipDef& def : cfg.ships.ships)
|
std::unordered_set<std::string> groupIds;
|
||||||
|
std::unordered_set<std::string> grantedShipIds;
|
||||||
|
std::unordered_set<std::string> grantedModuleIds;
|
||||||
|
std::unordered_set<std::string> grantedBuildingIds;
|
||||||
|
std::unordered_set<std::string> grantedRecipeIds;
|
||||||
|
|
||||||
|
const auto checkGrants = [&](const std::vector<std::string>& ids,
|
||||||
|
const std::unordered_set<std::string>& valid,
|
||||||
|
std::unordered_set<std::string>& granted,
|
||||||
|
const std::string& kind,
|
||||||
|
const std::string& gPath)
|
||||||
{
|
{
|
||||||
checkUnlockRequires(def.unlockRequires, schematicIds, "ships.toml",
|
for (const std::string& id : ids)
|
||||||
"ship '" + def.id + "'.unlock_requires");
|
{
|
||||||
|
if (valid.count(id) == 0)
|
||||||
|
{
|
||||||
|
throw makeError(file, gPath, "grants unknown " + kind + " '" + id + "'");
|
||||||
|
}
|
||||||
|
if (!granted.insert(id).second)
|
||||||
|
{
|
||||||
|
throw makeError(file, gPath,
|
||||||
|
"grants " + kind + " '" + id + "' which is already granted by another unlock group");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const UnlockGroupDef& group : cfg.unlocks.groups)
|
||||||
|
{
|
||||||
|
const std::string gPath = "unlock '" + group.id + "'";
|
||||||
|
if (!groupIds.insert(group.id).second)
|
||||||
|
{
|
||||||
|
throw makeError(file, gPath, "duplicate unlock group id");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.ships.empty() && group.modules.empty()
|
||||||
|
&& group.buildings.empty() && group.recipes.empty())
|
||||||
|
{
|
||||||
|
throw makeError(file, gPath, "grants no items (must grant at least one)");
|
||||||
|
}
|
||||||
|
|
||||||
|
checkGrants(group.ships, shipIds, grantedShipIds, "ship", gPath);
|
||||||
|
checkGrants(group.modules, moduleIds, grantedModuleIds, "module", gPath);
|
||||||
|
checkGrants(group.buildings, buildingIds, grantedBuildingIds, "building", gPath);
|
||||||
|
checkGrants(group.recipes, assemblerRecipeIds, grantedRecipeIds, "assembler recipe", gPath);
|
||||||
}
|
}
|
||||||
for (const ModuleDef& def : cfg.modules.modules)
|
|
||||||
|
// requires must reference defined group ids (checked once all group ids known).
|
||||||
|
for (const UnlockGroupDef& group : cfg.unlocks.groups)
|
||||||
{
|
{
|
||||||
checkUnlockRequires(def.unlockRequires, schematicIds, "modules.toml",
|
for (const std::string& req : group.requiredGroupIds)
|
||||||
"module '" + def.id + "'.unlock_requires");
|
{
|
||||||
}
|
if (groupIds.count(req) == 0)
|
||||||
for (const RecipeDef& def : cfg.recipes.recipes)
|
{
|
||||||
{
|
throw makeError(file, "unlock '" + group.id + "'.requires",
|
||||||
checkUnlockRequires(def.unlockRequires, schematicIds, "recipes.toml",
|
"references unknown unlock group '" + req + "'");
|
||||||
"recipe '" + def.id + "'.unlock_requires");
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,7 +870,8 @@ GameConfig ConfigLoader::loadFromDirectory(const std::string& configDir)
|
|||||||
cfg.ships = loadShips(configDir + "/ships.toml");
|
cfg.ships = loadShips(configDir + "/ships.toml");
|
||||||
cfg.stations = loadStations(configDir + "/stations.toml");
|
cfg.stations = loadStations(configDir + "/stations.toml");
|
||||||
cfg.modules = loadModules(configDir + "/modules.toml");
|
cfg.modules = loadModules(configDir + "/modules.toml");
|
||||||
validateUnlockRequires(cfg);
|
cfg.unlocks = loadUnlocks(configDir + "/unlocks.toml");
|
||||||
|
validateUnlocks(cfg);
|
||||||
cfg.threatCosts = computeThreatCostTable(cfg);
|
cfg.threatCosts = computeThreatCostTable(cfg);
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ public:
|
|||||||
static ShipsConfig loadShips(const std::string& path);
|
static ShipsConfig loadShips(const std::string& path);
|
||||||
static StationsConfig loadStations(const std::string& path);
|
static StationsConfig loadStations(const std::string& path);
|
||||||
static ModulesConfig loadModules(const std::string& path);
|
static ModulesConfig loadModules(const std::string& path);
|
||||||
|
static UnlocksConfig loadUnlocks(const std::string& path);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public:
|
|||||||
// Evaluates the expression at the given x. Requires a compiled formula.
|
// Evaluates the expression at the given x. Requires a compiled formula.
|
||||||
double evaluate(double x) const;
|
double evaluate(double x) const;
|
||||||
|
|
||||||
const std::string& source() const { return m_source; }
|
const std::string& getSource() const { return m_source; }
|
||||||
bool isValid() const { return m_expr != nullptr; }
|
bool isValid() const { return m_expr != nullptr; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
#include "StationsConfig.h"
|
#include "StationsConfig.h"
|
||||||
#include "ModulesConfig.h"
|
#include "ModulesConfig.h"
|
||||||
|
#include "UnlocksConfig.h"
|
||||||
#include "ThreatCostCalculator.h"
|
#include "ThreatCostCalculator.h"
|
||||||
|
|
||||||
// Aggregate of all simulation config files. Loaded at startup and reloaded
|
// Aggregate of all simulation config files. Loaded at startup and reloaded
|
||||||
@@ -18,5 +19,6 @@ struct GameConfig
|
|||||||
ShipsConfig ships;
|
ShipsConfig ships;
|
||||||
StationsConfig stations;
|
StationsConfig stations;
|
||||||
ModulesConfig modules;
|
ModulesConfig modules;
|
||||||
|
UnlocksConfig unlocks;
|
||||||
ThreatCostTable threatCosts;
|
ThreatCostTable threatCosts;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,10 +40,6 @@ struct ModuleRepairCapability
|
|||||||
struct ModuleDef
|
struct ModuleDef
|
||||||
{
|
{
|
||||||
std::string id;
|
std::string id;
|
||||||
int unlockAtStationLevel;
|
|
||||||
// Prerequisite schematic ids that must be explicitly unlocked before this
|
|
||||||
// schematic can enter the drop pool (REQ-LOCK-PREREQ). Empty = none.
|
|
||||||
std::vector<std::string> unlockRequires;
|
|
||||||
std::vector<std::string> surfaceMask;
|
std::vector<std::string> surfaceMask;
|
||||||
std::vector<RecipeIngredient> materials;
|
std::vector<RecipeIngredient> materials;
|
||||||
double productionTimeSeconds;
|
double productionTimeSeconds;
|
||||||
@@ -54,6 +50,10 @@ struct ModuleDef
|
|||||||
std::optional<ModuleWeaponCapability> weaponCapability;
|
std::optional<ModuleWeaponCapability> weaponCapability;
|
||||||
std::optional<ModuleSalvageCapability> salvageCapability;
|
std::optional<ModuleSalvageCapability> salvageCapability;
|
||||||
std::optional<ModuleRepairCapability> repairCapability;
|
std::optional<ModuleRepairCapability> repairCapability;
|
||||||
|
|
||||||
|
// Optional hover-tooltip text for the module selection button
|
||||||
|
// (REQ-MOD-UI-MODULE-TOOLTIP).
|
||||||
|
std::optional<std::string> tooltip;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ModulesConfig
|
struct ModulesConfig
|
||||||
|
|||||||
@@ -32,14 +32,16 @@ struct RecipeDef
|
|||||||
std::vector<RecipeIngredient> inputs;
|
std::vector<RecipeIngredient> inputs;
|
||||||
std::vector<RecipeOutput> outputs;
|
std::vector<RecipeOutput> outputs;
|
||||||
double durationSeconds;
|
double durationSeconds;
|
||||||
// Assembler only. nullopt = implicit-only locking. -1 = explicitly unlocked
|
// Optional id of the item whose icon represents this recipe in the recipe-
|
||||||
// at game start. >= 0 = locked; schematic enters drop pool at that station
|
// selection dialog (REQ-UI-RECIPE-ICON). When unset, the first output item is
|
||||||
// level once the output item is implicitly unlocked (REQ-LOCK-EXPLICIT).
|
// used. A missing icon file for that item is not an error (REQ-UI-ITEM-ICON).
|
||||||
std::optional<int> unlockAtStationLevel;
|
std::optional<std::string> icon;
|
||||||
// Assembler recipe schematics only. Prerequisite schematic ids that must be
|
// Assembler only. When true, this recipe is available from game start
|
||||||
// explicitly unlocked before this schematic can enter the drop pool
|
// regardless of the implicit item graph — used for base recipes that no
|
||||||
// (REQ-LOCK-PREREQ). Empty = none.
|
// schematic's materials reach (e.g. building blocks). See REQ-LOCK-IMPLICIT.
|
||||||
std::vector<std::string> unlockRequires;
|
// Otherwise an assembler recipe is either explicitly gated (granted by an
|
||||||
|
// unlock group, REQ-LOCK-EXPLICIT) or implicitly gated via the item graph.
|
||||||
|
bool unlockedAtStart = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RecipesConfig
|
struct RecipesConfig
|
||||||
|
|||||||
@@ -35,10 +35,6 @@ struct ShipSensor
|
|||||||
struct ShipDef
|
struct ShipDef
|
||||||
{
|
{
|
||||||
std::string id;
|
std::string id;
|
||||||
int unlockAtStationLevel;
|
|
||||||
// Prerequisite schematic ids that must be explicitly unlocked before this
|
|
||||||
// schematic can enter the drop pool (REQ-LOCK-PREREQ). Empty = none.
|
|
||||||
std::vector<std::string> unlockRequires;
|
|
||||||
std::vector<std::string> layout;
|
std::vector<std::string> layout;
|
||||||
|
|
||||||
ShipSchematic schematic;
|
ShipSchematic schematic;
|
||||||
|
|||||||
26
src/lib/config/UnlocksConfig.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// A single [[unlock]] entry from unlocks.toml — an unlock group, the unit of
|
||||||
|
// loot awarded by defence station drops (REQ-DEF-SCHEMATIC-DROP,
|
||||||
|
// REQ-LOCK-EXPLICIT). Awarding a group unlocks all of its granted members at
|
||||||
|
// once. Anything not granted by any group is available from game start.
|
||||||
|
struct UnlockGroupDef
|
||||||
|
{
|
||||||
|
std::string id; // Unique unlock-group id.
|
||||||
|
int stationLevel; // Min destroyed station level to become eligible.
|
||||||
|
std::vector<std::string> requiredGroupIds; // Prerequisite unlock-group ids (REQ-LOCK-PREREQ).
|
||||||
|
|
||||||
|
// Granted ids by kind (each defaults to empty; a group grants >= 1 item).
|
||||||
|
std::vector<std::string> ships;
|
||||||
|
std::vector<std::string> modules;
|
||||||
|
std::vector<std::string> buildings;
|
||||||
|
std::vector<std::string> recipes; // Assembler recipe ids only.
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UnlocksConfig
|
||||||
|
{
|
||||||
|
std::vector<UnlockGroupDef> groups;
|
||||||
|
};
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "Formula.h"
|
#include "Formula.h"
|
||||||
|
|
||||||
// Region widths are in tiles (REQ-GW-REGIONS).
|
// Region widths are in tiles (REQ-GW-REGIONS).
|
||||||
@@ -53,13 +56,22 @@ struct WorldArtifacts
|
|||||||
int artifactWinCount;
|
int artifactWinCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// View pan speed (REQ-UI-SCROLL-SPEED). Presentation-only; the simulation ignores these.
|
||||||
|
struct WorldScroll
|
||||||
|
{
|
||||||
|
double panSpeedSlow_tps; // tiles/s, used outside the contest zone
|
||||||
|
double panSpeedFast_tps; // tiles/s, used inside the contest zone
|
||||||
|
int panRampBandWidth_tiles; // full width of the ramp band straddling each contest-zone boundary
|
||||||
|
};
|
||||||
|
|
||||||
struct WorldConfig
|
struct WorldConfig
|
||||||
{
|
{
|
||||||
int heightTiles; // REQ-GW-HEIGHT
|
int heightTiles; // REQ-GW-HEIGHT
|
||||||
int refundPercentage; // REQ-BLD-DEMOLISH
|
int refundPercentage; // REQ-BLD-DECONSTRUCT
|
||||||
|
double deconstructionTimeSeconds; // REQ-BLD-DECON-QUEUE
|
||||||
int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS
|
int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS
|
||||||
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP
|
double debrisDespawnSeconds; // REQ-RES-DEBRIS-DROP
|
||||||
double scrapPerThreat; // REQ-RES-SCRAP-DROP, REQ-THREAT-SCRAP (scrap dropped per unit threat)
|
double scrapPerThreat; // REQ-RES-DEBRIS-DROP, REQ-THREAT-SCRAP (scrap dropped per unit threat)
|
||||||
double tileSize_m; // metres per tile (REQ-GW-TILE-SIZE)
|
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)
|
double beltSpeed_tps; // REQ-GW-BELT-SPEED (tiles/s, converted from m/s in config)
|
||||||
int tunnelMaxDistance_tiles; // REQ-BLD-TUNNEL-PAIR
|
int tunnelMaxDistance_tiles; // REQ-BLD-TUNNEL-PAIR
|
||||||
@@ -67,10 +79,19 @@ struct WorldConfig
|
|||||||
double orbitFactor; // REQ-SHP-ORBIT (multiplies tool range for orbit radius)
|
double orbitFactor; // REQ-SHP-ORBIT (multiplies tool range for orbit radius)
|
||||||
double rallyOrbitRadius_tiles; // REQ-SHP-ORBIT (fixed orbit radius around the rally point)
|
double rallyOrbitRadius_tiles; // REQ-SHP-ORBIT (fixed orbit radius around the rally point)
|
||||||
|
|
||||||
|
// Optional hover-tooltip for the header building blocks stock display
|
||||||
|
// (REQ-UI-BLOCKS-TOOLTIP). Presentation-only; the simulation ignores it.
|
||||||
|
std::optional<std::string> buildingBlocksTooltip;
|
||||||
|
|
||||||
|
// Optional hover-tooltip for the header artifact count display
|
||||||
|
// (REQ-UI-ARTIFACTS-TOOLTIP). Presentation-only; the simulation ignores it.
|
||||||
|
std::optional<std::string> artifactTooltip;
|
||||||
|
|
||||||
WorldRegions regions;
|
WorldRegions regions;
|
||||||
WorldExpansion expansion;
|
WorldExpansion expansion;
|
||||||
WorldPush push;
|
WorldPush push;
|
||||||
WorldWaves waves;
|
WorldWaves waves;
|
||||||
WorldTargeting targeting;
|
WorldTargeting targeting;
|
||||||
WorldArtifacts artifacts;
|
WorldArtifacts artifacts;
|
||||||
|
WorldScroll scroll;
|
||||||
};
|
};
|
||||||
|
|||||||
94
src/lib/core/BeltDragPath.cpp
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#include "BeltDragPath.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int signOf(int value)
|
||||||
|
{
|
||||||
|
if (value > 0) { return 1; }
|
||||||
|
if (value < 0) { return -1; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direction stepping from one tile to an orthogonally adjacent tile.
|
||||||
|
Rotation directionBetween(QPoint from, QPoint to)
|
||||||
|
{
|
||||||
|
const QPoint delta = to - from;
|
||||||
|
if (delta.x() > 0) { return Rotation::East; }
|
||||||
|
if (delta.x() < 0) { return Rotation::West; }
|
||||||
|
if (delta.y() > 0) { return Rotation::South; }
|
||||||
|
return Rotation::North;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<BeltPathTile> computeBeltDragPath(QPoint anchor, QPoint cursor,
|
||||||
|
Rotation orientation)
|
||||||
|
{
|
||||||
|
const bool horizontalFirst =
|
||||||
|
(orientation == Rotation::East || orientation == Rotation::West);
|
||||||
|
|
||||||
|
// Build the ordered tile coordinates: first leg along the primary axis to the
|
||||||
|
// corner, then the orthogonal leg to the cursor (no duplicated corner tile).
|
||||||
|
std::vector<QPoint> coords;
|
||||||
|
if (horizontalFirst)
|
||||||
|
{
|
||||||
|
const int stepX = signOf(cursor.x() - anchor.x());
|
||||||
|
for (int x = anchor.x(); ; x += stepX)
|
||||||
|
{
|
||||||
|
coords.push_back(QPoint(x, anchor.y()));
|
||||||
|
if (x == cursor.x() || stepX == 0) { break; }
|
||||||
|
}
|
||||||
|
const int stepY = signOf(cursor.y() - anchor.y());
|
||||||
|
if (stepY != 0)
|
||||||
|
{
|
||||||
|
for (int y = anchor.y() + stepY; ; y += stepY)
|
||||||
|
{
|
||||||
|
coords.push_back(QPoint(cursor.x(), y));
|
||||||
|
if (y == cursor.y()) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const int stepY = signOf(cursor.y() - anchor.y());
|
||||||
|
for (int y = anchor.y(); ; y += stepY)
|
||||||
|
{
|
||||||
|
coords.push_back(QPoint(anchor.x(), y));
|
||||||
|
if (y == cursor.y() || stepY == 0) { break; }
|
||||||
|
}
|
||||||
|
const int stepX = signOf(cursor.x() - anchor.x());
|
||||||
|
if (stepX != 0)
|
||||||
|
{
|
||||||
|
for (int x = anchor.x() + stepX; ; x += stepX)
|
||||||
|
{
|
||||||
|
coords.push_back(QPoint(x, cursor.y()));
|
||||||
|
if (x == cursor.x()) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign each tile the direction toward the next tile; the last tile keeps its
|
||||||
|
// incoming step direction, and a single-tile path keeps the belt orientation.
|
||||||
|
std::vector<BeltPathTile> path;
|
||||||
|
path.reserve(coords.size());
|
||||||
|
const std::size_t count = coords.size();
|
||||||
|
for (std::size_t index = 0; index < count; ++index)
|
||||||
|
{
|
||||||
|
Rotation rotation;
|
||||||
|
if (count == 1)
|
||||||
|
{
|
||||||
|
rotation = orientation;
|
||||||
|
}
|
||||||
|
else if (index + 1 < count)
|
||||||
|
{
|
||||||
|
rotation = directionBetween(coords[index], coords[index + 1]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rotation = directionBetween(coords[index - 1], coords[index]);
|
||||||
|
}
|
||||||
|
path.push_back(BeltPathTile{ coords[index], rotation });
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
27
src/lib/core/BeltDragPath.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "Rotation.h"
|
||||||
|
|
||||||
|
// One tile of a belt drag-placement path: the tile coordinate and the belt
|
||||||
|
// orientation it should be given (REQ-BLD-BELT-DRAG).
|
||||||
|
struct BeltPathTile
|
||||||
|
{
|
||||||
|
QPoint tile;
|
||||||
|
Rotation rotation;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Computes the rectilinear (L-shaped) belt path from `anchor` to `cursor` for a
|
||||||
|
// belt whose current orientation is `orientation` (REQ-BLD-BELT-DRAG). The path
|
||||||
|
// first runs along the axis parallel to `orientation` (horizontal for East/West,
|
||||||
|
// vertical for North/South), stepping toward the cursor's coordinate on that axis
|
||||||
|
// to the corner tile, then runs along the orthogonal axis to the cursor tile. Each
|
||||||
|
// tile is oriented to point toward the next tile along the path; the final tile
|
||||||
|
// keeps the direction of its incoming step, and a single-tile path keeps
|
||||||
|
// `orientation`. Returned tiles are ordered from anchor to cursor with no duplicate
|
||||||
|
// corner tile.
|
||||||
|
std::vector<BeltPathTile> computeBeltDragPath(QPoint anchor, QPoint cursor,
|
||||||
|
Rotation orientation);
|
||||||