Compare commits
46 Commits
9e36c13635
...
config
| Author | SHA1 | Date | |
|---|---|---|---|
| c44936d1fe | |||
| 68c1345660 | |||
| dbf334c829 | |||
| f225c1330e | |||
| fba98c928f | |||
| 282ace4c11 | |||
| 1ea1cc59fb | |||
| 123c544423 | |||
| 10c5ad678f | |||
| 3716c2b734 | |||
| 5317f35198 | |||
| ed17664ef1 | |||
| 49f7129bd5 | |||
| 1641189b75 | |||
| 54a6056b77 | |||
| 69b35d2bfc | |||
| af96b95f61 | |||
| aad094f842 | |||
| 26857e8414 | |||
| 510e37c37b | |||
| 121cd5407f | |||
| 7c663e29a6 | |||
| c64d31fa46 | |||
| f097e9a25f | |||
| 37a70ea321 | |||
| 8dad554800 | |||
| 6b95619806 | |||
| 66cf9ae23a | |||
| ef17b0ce42 | |||
| eeaa309c08 | |||
| 7669245229 | |||
| 4e3e3ac715 | |||
| 9677133c54 | |||
| abc261c03a | |||
| 17e9913c98 | |||
| 900b5fdec1 | |||
| 3e19e44f24 | |||
| 42b51cc6f4 | |||
| 15d8fa4f2c | |||
| b5185b0906 | |||
| 457fc47c75 | |||
| 090dc64bc4 | |||
| 64f7c9dcc1 | |||
| f921f00a0d | |||
| 9d0a60a93b | |||
| f363f7a67c |
@@ -32,7 +32,6 @@ cost = 15
|
||||
player_placeable = true
|
||||
construction_time_seconds = 1
|
||||
surface_mask = [
|
||||
"AA",
|
||||
"A>",
|
||||
]
|
||||
|
||||
@@ -42,8 +41,8 @@ cost = 20
|
||||
player_placeable = true
|
||||
construction_time_seconds = 1
|
||||
surface_mask = [
|
||||
"AA ",
|
||||
"AA>",
|
||||
"AA",
|
||||
" v",
|
||||
]
|
||||
|
||||
[[building]]
|
||||
@@ -52,9 +51,8 @@ cost = 35
|
||||
player_placeable = true
|
||||
construction_time_seconds = 1
|
||||
surface_mask = [
|
||||
"AAA ",
|
||||
"AAA>",
|
||||
"AAA ",
|
||||
"AA ",
|
||||
"AA>",
|
||||
]
|
||||
|
||||
[[building]]
|
||||
|
||||
@@ -1,51 +1,260 @@
|
||||
# modules.toml
|
||||
#
|
||||
# First real-content iteration: module ids and surface masks are the designed
|
||||
# content; stats, materials, and threat costs are placeholders until the
|
||||
# recipe and balancing passes.
|
||||
#
|
||||
# Surface mask footprint ladder — footprints gate which hulls can mount a
|
||||
# module, purely through geometry (see ships.toml for the matching hull
|
||||
# grids):
|
||||
#
|
||||
# 1x1 laser_cannon_s, salvager, repair_tool fits every hull, incl. drones
|
||||
# 1x2 maneuvering_thrusters, sensor_booster,
|
||||
# armor_plates frigate and up
|
||||
# 1x3 afterburner frigate and up (eats most of a frigate)
|
||||
# L-shape weapon_stabilizer, weapon_primer,
|
||||
# weapon_upgrade frigate and up
|
||||
# 2x2 laser_cannon_m, drone_bay cruiser and up (no 2x2 area on s hulls)
|
||||
# 3x3 laser_cannon_l battleship and up (no 3x3 area on m hulls)
|
||||
# 2x6 drone_hangar carrier only
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Weapons
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[module]]
|
||||
id = "armor_plate"
|
||||
surface_mask = ["OO", "OO"]
|
||||
materials = [{item = "iron_ingot", amount = 2}]
|
||||
id = "laser_cannon_s"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["O"]
|
||||
materials = [{item = "laser_cannon_s_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 0.5
|
||||
fill_color = "#FF8040"
|
||||
glyph = "Ls"
|
||||
|
||||
[module.weapon]
|
||||
damage_formula = "2"
|
||||
attack_range_m_formula = "50"
|
||||
attack_rate_hz_formula = "2.0"
|
||||
|
||||
|
||||
[[module]]
|
||||
id = "laser_cannon_m"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = [
|
||||
"OO",
|
||||
"OO"]
|
||||
materials = [{item = "laser_cannon_m_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 2
|
||||
fill_color = "#FF8040"
|
||||
glyph = "Lm"
|
||||
|
||||
[module.weapon]
|
||||
damage_formula = "10"
|
||||
attack_range_m_formula = "70"
|
||||
attack_rate_hz_formula = "1.5"
|
||||
|
||||
|
||||
[[module]]
|
||||
id = "laser_cannon_l"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = [
|
||||
"OOO",
|
||||
"OOO",
|
||||
"OOO"]
|
||||
materials = [{item = "laser_cannon_l_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 8
|
||||
fill_color = "#FF8040"
|
||||
glyph = "Ll"
|
||||
|
||||
[module.weapon]
|
||||
damage_formula = "40"
|
||||
attack_range_m_formula = "100"
|
||||
attack_rate_hz_formula = "0.8"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utility tools
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[module]]
|
||||
id = "salvager"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["O"]
|
||||
materials = [{item = "salvager_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 2
|
||||
fill_color = "#AACC44"
|
||||
glyph = "Sv"
|
||||
|
||||
[module.salvage]
|
||||
collection_range_m_formula = "500"
|
||||
cargo_capacity_formula = "10"
|
||||
collection_rate_hz_formula = "0.5"
|
||||
|
||||
|
||||
[[module]]
|
||||
id = "repair_tool"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["O"]
|
||||
materials = [{item = "repair_tool_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 2
|
||||
fill_color = "#66CCFF"
|
||||
glyph = "Rp"
|
||||
|
||||
[module.repair]
|
||||
repair_rate_hz_formula = "5 + x"
|
||||
repair_range_m_formula = "800"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Propulsion
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[module]]
|
||||
id = "afterburner"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["OOO"]
|
||||
materials = [{item = "afterburner_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 2
|
||||
fill_color = "#40A0FF"
|
||||
glyph = "Ab"
|
||||
|
||||
[module.movement]
|
||||
multiplied_speed_mps_formula = "1.6"
|
||||
added_main_acceleration_mpss_formula = "60"
|
||||
|
||||
|
||||
[[module]]
|
||||
id = "maneuvering_thrusters"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["OO"]
|
||||
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 2
|
||||
fill_color = "#40A0FF"
|
||||
glyph = "Mt"
|
||||
|
||||
[module.movement]
|
||||
multiplied_speed_mps_formula = "1.2"
|
||||
added_maneuvering_acceleration_mpss_formula = "10"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Defense & sensors
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[module]]
|
||||
id = "armor_plates"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["OO"]
|
||||
materials = [{item = "armor_plates_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 3
|
||||
threat_cost = 2.0
|
||||
fill_color = "#808080"
|
||||
glyph = "A"
|
||||
|
||||
[module.health]
|
||||
multiplied_hp_formula = "1.0 + 0.2 * x"
|
||||
added_hp_formula = "40"
|
||||
|
||||
|
||||
[[module]]
|
||||
id = "sensor_booster"
|
||||
surface_mask = ["O"]
|
||||
materials = [{item = "circuit_board", amount = 1}]
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["OO"]
|
||||
materials = [{item = "sensor_booster_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 2
|
||||
threat_cost = 1.0
|
||||
fill_color = "#40A0FF"
|
||||
glyph = "S"
|
||||
|
||||
[module.sensor]
|
||||
added_sensor_range_formula = "2 + x"
|
||||
added_sensor_range_m_formula = "50"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Weapon modifiers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[module]]
|
||||
id = "weapon_upgrade"
|
||||
surface_mask = ["OO"]
|
||||
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = [
|
||||
"OO",
|
||||
"OX",
|
||||
]
|
||||
materials = [{item = "weapon_upgrade_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 4
|
||||
threat_cost = 3.0
|
||||
fill_color = "#FF4040"
|
||||
glyph = "W"
|
||||
glyph = "Wu"
|
||||
|
||||
[module.weapon]
|
||||
multiplied_damage_formula = "1.2"
|
||||
|
||||
[module.combat]
|
||||
multiplied_damage_formula = "1.0 + 0.15 * x"
|
||||
|
||||
[[module]]
|
||||
id = "engine_booster"
|
||||
surface_mask = ["O", "O"]
|
||||
materials = [{item = "iron_ingot", amount = 2}]
|
||||
id = "weapon_primer"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = [
|
||||
"OO",
|
||||
"OX",
|
||||
]
|
||||
materials = [{item = "weapon_primer_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 3
|
||||
threat_cost = 1.5
|
||||
fill_color = "#40FF80"
|
||||
glyph = "E"
|
||||
production_time_seconds = 4
|
||||
fill_color = "#FF4040"
|
||||
glyph = "Wp"
|
||||
|
||||
[module.movement]
|
||||
added_speed_formula = "0.5 * x"
|
||||
[module.weapon]
|
||||
multiplied_attack_rate_hz_formula = "1.2"
|
||||
|
||||
|
||||
[[module]]
|
||||
id = "weapon_stabilizer"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = [
|
||||
"OO",
|
||||
"OX",
|
||||
]
|
||||
materials = [{item = "weapon_stabilizer_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 4
|
||||
fill_color = "#FF4040"
|
||||
glyph = "Ws"
|
||||
|
||||
[module.weapon]
|
||||
multiplied_attack_range_m_formula = "1.5"
|
||||
multiplied_attack_rate_hz_formula = "0.8"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Drone modules
|
||||
#
|
||||
# Footprint-only placeholders: the drone launching capability is not
|
||||
# implemented yet, so these modules define no capability section.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[module]]
|
||||
id = "drone_bay"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = [
|
||||
"OO",
|
||||
"OO"]
|
||||
materials = [{item = "drone_bay_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 5
|
||||
fill_color = "#CC66FF"
|
||||
glyph = "Db"
|
||||
|
||||
|
||||
[[module]]
|
||||
id = "drone_hangar"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = [
|
||||
"OOOOOO",
|
||||
"OOOOOO"]
|
||||
materials = [{item = "drone_hangar_module", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 20
|
||||
fill_color = "#9933CC"
|
||||
glyph = "Dh"
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
# recipes.toml
|
||||
#
|
||||
# First real-content iteration of the production tree. Quantities and
|
||||
# durations are a first guess; the balancing pass will tune them and assign
|
||||
# real unlock_at_station_level values (everything is unlocked for now so the
|
||||
# full tree is testable).
|
||||
#
|
||||
# Input chain per game phase — each phase adds exactly one new base input:
|
||||
#
|
||||
# early iron_ore + copper_ore -> ingots -> copper_wire, steel_plate,
|
||||
# circuit_board
|
||||
# mid + titanium_ore -> titanium_frame; assembler-made
|
||||
# mechanical_parts, targeting_unit,
|
||||
# drive_unit
|
||||
# late + advanced_alloy -> reinforced_plating, capital_core.
|
||||
# advanced_alloy CANNOT be mined; it only
|
||||
# comes from reprocessing salvaged scrap,
|
||||
# so capital production requires combat.
|
||||
#
|
||||
# Run tools/verify_recipes.py after editing to check that every consumed
|
||||
# item has a producer and every item has a visuals.toml entry.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Mining (tier 0)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[recipe]]
|
||||
id = "mine_iron_ore"
|
||||
building = "miner"
|
||||
@@ -12,6 +38,18 @@ inputs = []
|
||||
outputs = [{item = "copper_ore", amount = 1}]
|
||||
duration_seconds = 1.5
|
||||
|
||||
# Titanium is the midgame ore: mined three times slower than iron.
|
||||
[[recipe]]
|
||||
id = "mine_titanium_ore"
|
||||
building = "miner"
|
||||
inputs = []
|
||||
outputs = [{item = "titanium_ore", amount = 1}]
|
||||
duration_seconds = 3.0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Smelting (tier 1)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[recipe]]
|
||||
id = "iron_ingot"
|
||||
building = "smelter"
|
||||
@@ -27,19 +65,18 @@ outputs = [{item = "copper_ingot", amount = 1}]
|
||||
duration_seconds = 2.5
|
||||
|
||||
[[recipe]]
|
||||
id = "circuit_board"
|
||||
building = "assembler"
|
||||
inputs = [{item = "iron_ingot", amount = 3}, {item = "copper_ingot", amount = 2}]
|
||||
outputs = [{item = "circuit_board", amount = 1}]
|
||||
duration_seconds = 5.0
|
||||
|
||||
[[recipe]]
|
||||
id = "building_blocks"
|
||||
building = "assembler"
|
||||
inputs = [{item = "iron_ingot", amount = 4}]
|
||||
outputs = [{item = "building_block", amount = 10}]
|
||||
id = "titanium_ingot"
|
||||
building = "smelter"
|
||||
inputs = [{item = "titanium_ore", amount = 3}]
|
||||
outputs = [{item = "titanium_ingot", amount = 1}]
|
||||
duration_seconds = 4.0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Reprocessing
|
||||
#
|
||||
# The only source of advanced_alloy: salvaged scrap from destroyed ships.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[recipe]]
|
||||
id = "reprocessing_cycle"
|
||||
building = "reprocessing_plant"
|
||||
@@ -49,15 +86,354 @@ duration_seconds = 3.0
|
||||
[[recipe.outputs]]
|
||||
item = "iron_ingot"
|
||||
amount = 2
|
||||
probability = 0.6
|
||||
probability = 0.45
|
||||
|
||||
[[recipe.outputs]]
|
||||
item = "circuit_board"
|
||||
item = "copper_ingot"
|
||||
amount = 1
|
||||
probability = 0.3
|
||||
probability = 0.25
|
||||
|
||||
[[recipe.outputs]]
|
||||
item = "titanium_ingot"
|
||||
amount = 1
|
||||
probability = 0.15
|
||||
|
||||
[[recipe.outputs]]
|
||||
item = "advanced_alloy"
|
||||
amount = 1
|
||||
probability = 0.1
|
||||
probability = 0.15
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Basic components (tier 2, early game)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[recipe]]
|
||||
id = "copper_wire"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "copper_ingot", amount = 1}]
|
||||
outputs = [{item = "copper_wire", amount = 2}]
|
||||
duration_seconds = 1.5
|
||||
|
||||
[[recipe]]
|
||||
id = "steel_plate"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "iron_ingot", amount = 2}]
|
||||
outputs = [{item = "steel_plate", amount = 1}]
|
||||
duration_seconds = 2.0
|
||||
|
||||
[[recipe]]
|
||||
id = "circuit_board"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "iron_ingot", amount = 1}, {item = "copper_wire", amount = 2}]
|
||||
outputs = [{item = "circuit_board", amount = 1}]
|
||||
duration_seconds = 2.0
|
||||
|
||||
[[recipe]]
|
||||
id = "building_blocks"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "iron_ingot", amount = 4}]
|
||||
outputs = [{item = "building_block", amount = 10}]
|
||||
duration_seconds = 4.0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Advanced components (tier 3, midgame)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[recipe]]
|
||||
id = "mechanical_parts"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "steel_plate", amount = 1}, {item = "iron_ingot", amount = 1}]
|
||||
outputs = [{item = "mechanical_parts", amount = 2}]
|
||||
duration_seconds = 2.5
|
||||
|
||||
[[recipe]]
|
||||
id = "targeting_unit"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "circuit_board", amount = 2}, {item = "copper_wire", amount = 1}]
|
||||
outputs = [{item = "targeting_unit", amount = 1}]
|
||||
duration_seconds = 3.0
|
||||
|
||||
[[recipe]]
|
||||
id = "drive_unit"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [
|
||||
{item = "steel_plate", amount = 1},
|
||||
{item = "mechanical_parts", amount = 1},
|
||||
{item = "circuit_board", amount = 1},
|
||||
]
|
||||
outputs = [{item = "drive_unit", amount = 1}]
|
||||
duration_seconds = 4.0
|
||||
|
||||
[[recipe]]
|
||||
id = "titanium_frame"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "titanium_ingot", amount = 2}, {item = "steel_plate", amount = 1}]
|
||||
outputs = [{item = "titanium_frame", amount = 1}]
|
||||
duration_seconds = 4.0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Capital components (tier 4, lategame — gated on advanced_alloy)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[recipe]]
|
||||
id = "reinforced_plating"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "steel_plate", amount = 2}, {item = "advanced_alloy", amount = 1}]
|
||||
outputs = [{item = "reinforced_plating", amount = 1}]
|
||||
duration_seconds = 5.0
|
||||
|
||||
[[recipe]]
|
||||
id = "capital_core"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [
|
||||
{item = "targeting_unit", amount = 1},
|
||||
{item = "drive_unit", amount = 1},
|
||||
{item = "advanced_alloy", amount = 2},
|
||||
]
|
||||
outputs = [{item = "capital_core", amount = 1}]
|
||||
duration_seconds = 8.0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Module items — early game
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[recipe]]
|
||||
id = "laser_cannon_s_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "iron_ingot", amount = 2}, {item = "circuit_board", amount = 1}]
|
||||
outputs = [{item = "laser_cannon_s_module", amount = 1}]
|
||||
duration_seconds = 3.0
|
||||
|
||||
[[recipe]]
|
||||
id = "salvager_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "steel_plate", amount = 1}, {item = "circuit_board", amount = 1}]
|
||||
outputs = [{item = "salvager_module", amount = 1}]
|
||||
duration_seconds = 4.0
|
||||
|
||||
[[recipe]]
|
||||
id = "repair_tool_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "circuit_board", amount = 2}, {item = "copper_wire", amount = 1}]
|
||||
outputs = [{item = "repair_tool_module", amount = 1}]
|
||||
duration_seconds = 4.0
|
||||
|
||||
[[recipe]]
|
||||
id = "armor_plates_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "steel_plate", amount = 2}]
|
||||
outputs = [{item = "armor_plates_module", amount = 1}]
|
||||
duration_seconds = 3.0
|
||||
|
||||
[[recipe]]
|
||||
id = "sensor_booster_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "circuit_board", amount = 1}, {item = "copper_wire", amount = 2}]
|
||||
outputs = [{item = "sensor_booster_module", amount = 1}]
|
||||
duration_seconds = 3.0
|
||||
|
||||
[[recipe]]
|
||||
id = "maneuvering_thrusters_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "mechanical_parts", amount = 1}, {item = "copper_wire", amount = 1}]
|
||||
outputs = [{item = "maneuvering_thrusters_module", amount = 1}]
|
||||
duration_seconds = 3.0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Module items — midgame
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[recipe]]
|
||||
id = "afterburner_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "drive_unit", amount = 1}, {item = "steel_plate", amount = 1}]
|
||||
outputs = [{item = "afterburner_module", amount = 1}]
|
||||
duration_seconds = 4.0
|
||||
|
||||
[[recipe]]
|
||||
id = "weapon_upgrade_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "targeting_unit", amount = 1}, {item = "steel_plate", amount = 1}]
|
||||
outputs = [{item = "weapon_upgrade_module", amount = 1}]
|
||||
duration_seconds = 4.0
|
||||
|
||||
[[recipe]]
|
||||
id = "weapon_primer_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "targeting_unit", amount = 1}, {item = "copper_wire", amount = 2}]
|
||||
outputs = [{item = "weapon_primer_module", amount = 1}]
|
||||
duration_seconds = 4.0
|
||||
|
||||
[[recipe]]
|
||||
id = "weapon_stabilizer_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "targeting_unit", amount = 1}, {item = "mechanical_parts", amount = 1}]
|
||||
outputs = [{item = "weapon_stabilizer_module", amount = 1}]
|
||||
duration_seconds = 4.0
|
||||
|
||||
[[recipe]]
|
||||
id = "laser_cannon_m_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "targeting_unit", amount = 1}, {item = "titanium_frame", amount = 1}]
|
||||
outputs = [{item = "laser_cannon_m_module", amount = 1}]
|
||||
duration_seconds = 6.0
|
||||
|
||||
[[recipe]]
|
||||
id = "drone_bay_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [
|
||||
{item = "titanium_frame", amount = 1},
|
||||
{item = "mechanical_parts", amount = 1},
|
||||
{item = "circuit_board", amount = 1},
|
||||
]
|
||||
outputs = [{item = "drone_bay_module", amount = 1}]
|
||||
duration_seconds = 6.0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Module items — lategame
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[recipe]]
|
||||
id = "laser_cannon_l_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [
|
||||
{item = "targeting_unit", amount = 2},
|
||||
{item = "reinforced_plating", amount = 2},
|
||||
{item = "titanium_frame", amount = 1},
|
||||
]
|
||||
outputs = [{item = "laser_cannon_l_module", amount = 1}]
|
||||
duration_seconds = 12.0
|
||||
|
||||
[[recipe]]
|
||||
id = "drone_hangar_module"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [
|
||||
{item = "capital_core", amount = 1},
|
||||
{item = "titanium_frame", amount = 2},
|
||||
{item = "reinforced_plating", amount = 2},
|
||||
]
|
||||
outputs = [{item = "drone_hangar_module", amount = 1}]
|
||||
duration_seconds = 20.0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Ship hulls
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[[recipe]]
|
||||
id = "drone_hull"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 1}]
|
||||
outputs = [{item = "drone_hull", amount = 1}]
|
||||
duration_seconds = 4.0
|
||||
|
||||
[[recipe]]
|
||||
id = "frigate_hull"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [
|
||||
{item = "steel_plate", amount = 3},
|
||||
{item = "mechanical_parts", amount = 1},
|
||||
{item = "circuit_board", amount = 1},
|
||||
]
|
||||
outputs = [{item = "frigate_hull", amount = 1}]
|
||||
duration_seconds = 8.0
|
||||
|
||||
[[recipe]]
|
||||
id = "destroyer_hull"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [
|
||||
{item = "steel_plate", amount = 5},
|
||||
{item = "mechanical_parts", amount = 2},
|
||||
{item = "circuit_board", amount = 1},
|
||||
]
|
||||
outputs = [{item = "destroyer_hull", amount = 1}]
|
||||
duration_seconds = 10.0
|
||||
|
||||
[[recipe]]
|
||||
id = "cruiser_hull"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [
|
||||
{item = "titanium_frame", amount = 2},
|
||||
{item = "steel_plate", amount = 4},
|
||||
{item = "drive_unit", amount = 1},
|
||||
]
|
||||
outputs = [{item = "cruiser_hull", amount = 1}]
|
||||
duration_seconds = 15.0
|
||||
|
||||
[[recipe]]
|
||||
id = "battlecruiser_hull"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [
|
||||
{item = "titanium_frame", amount = 3},
|
||||
{item = "steel_plate", amount = 6},
|
||||
{item = "drive_unit", amount = 1},
|
||||
{item = "targeting_unit", amount = 1},
|
||||
]
|
||||
outputs = [{item = "battlecruiser_hull", amount = 1}]
|
||||
duration_seconds = 20.0
|
||||
|
||||
[[recipe]]
|
||||
id = "battleship_hull"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [
|
||||
{item = "titanium_frame", amount = 4},
|
||||
{item = "reinforced_plating", amount = 2},
|
||||
{item = "drive_unit", amount = 2},
|
||||
]
|
||||
outputs = [{item = "battleship_hull", amount = 1}]
|
||||
duration_seconds = 30.0
|
||||
|
||||
[[recipe]]
|
||||
id = "dreadnought_hull"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [
|
||||
{item = "capital_core", amount = 1},
|
||||
{item = "titanium_frame", amount = 6},
|
||||
{item = "reinforced_plating", amount = 4},
|
||||
{item = "drive_unit", amount = 2},
|
||||
]
|
||||
outputs = [{item = "dreadnought_hull", amount = 1}]
|
||||
duration_seconds = 60.0
|
||||
|
||||
[[recipe]]
|
||||
id = "carrier_hull"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [
|
||||
{item = "capital_core", amount = 1},
|
||||
{item = "titanium_frame", amount = 5},
|
||||
{item = "reinforced_plating", amount = 3},
|
||||
{item = "drive_unit", amount = 2},
|
||||
]
|
||||
outputs = [{item = "carrier_hull", amount = 1}]
|
||||
duration_seconds = 60.0
|
||||
|
||||
@@ -1,174 +1,295 @@
|
||||
# ships.toml
|
||||
#
|
||||
# First real-content iteration: ship ids and layout grids are the designed
|
||||
# content; stats, materials, and production times are placeholders until the
|
||||
# recipe and balancing passes.
|
||||
#
|
||||
# Size classes:
|
||||
# xs drone 1 cell — exactly one 1x1 module
|
||||
# s frigate, destroyer no 2x2 area anywhere: only 1x1/1x2/1x3/L modules fit
|
||||
# m cruiser, battlecruiser 2x2 areas (m guns, drone bays) but no 3x3 area
|
||||
# l battleship four m guns, or exactly one 3x3 l gun at heavy
|
||||
# opportunity cost
|
||||
# xl dreadnought, carrier dreadnought fits three l guns but no drone
|
||||
# hangar; carrier fits one drone hangar (2x6)
|
||||
# but no l gun (its deck rows are broken up
|
||||
# by elevator shafts)
|
||||
|
||||
[[ship]]
|
||||
id = "fighter"
|
||||
available_from_start = true
|
||||
layout = ["XOX", "OOO", "XOX"]
|
||||
id = "drone"
|
||||
unlock_at_station_level = -1
|
||||
layout = ["O"]
|
||||
default_modules = [{type = "laser_cannon_s", x = 0, y = 0, rotation = "east"}]
|
||||
|
||||
[ship.schematic]
|
||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||
materials = [{item = "iron_ore", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 10
|
||||
|
||||
[ship.threat]
|
||||
cost_formula = "10"
|
||||
production_time_seconds = 5
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "3"
|
||||
|
||||
[ship.movement]
|
||||
speed_formula = "4"
|
||||
main_acceleration_formula = "8"
|
||||
maneuvering_acceleration_formula = "4"
|
||||
angular_acceleration_formula = "12.56"
|
||||
max_rotation_speed_formula = "6.28"
|
||||
speed_mps_formula = "40"
|
||||
main_acceleration_mpss_formula = "80"
|
||||
maneuvering_acceleration_mpss_formula = "40"
|
||||
angular_acceleration_radpss_formula = "12.56"
|
||||
max_rotation_speed_radps_formula = "6.28"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "15"
|
||||
|
||||
[ship.combat]
|
||||
damage_formula = "2"
|
||||
attack_range_formula = "5"
|
||||
attack_rate_formula = "2.0"
|
||||
sensor_range_m_formula = "150"
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 2
|
||||
|
||||
|
||||
|
||||
# Frigate — 5 cells in a plus shape. Holds a couple of small guns plus at
|
||||
# most one 1x2 support (every 1x2 placement crosses the center cell), or one
|
||||
# L-shaped weapon modifier, or an afterburner spanning the full center line.
|
||||
[[ship]]
|
||||
id = "sniper"
|
||||
available_from_start = true
|
||||
layout = ["XOOX", "OOOO", "XOOX"]
|
||||
id = "frigate"
|
||||
unlock_at_station_level = -1
|
||||
layout = [
|
||||
"XOX",
|
||||
"OOO",
|
||||
"XOX",
|
||||
]
|
||||
|
||||
[ship.schematic]
|
||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||
materials = [{item = "frigate_hull", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 10
|
||||
|
||||
[ship.threat]
|
||||
cost_formula = "10"
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "8"
|
||||
hp_formula = "30"
|
||||
|
||||
[ship.movement]
|
||||
speed_formula = "1"
|
||||
main_acceleration_formula = "1.5"
|
||||
maneuvering_acceleration_formula = "0.5"
|
||||
angular_acceleration_formula = "9.42"
|
||||
max_rotation_speed_formula = "3.14"
|
||||
speed_mps_formula = "30"
|
||||
main_acceleration_mpss_formula = "50"
|
||||
maneuvering_acceleration_mpss_formula = "25"
|
||||
angular_acceleration_radpss_formula = "8"
|
||||
max_rotation_speed_radps_formula = "4"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "25"
|
||||
|
||||
[ship.combat]
|
||||
damage_formula = "10"
|
||||
attack_range_formula = "20"
|
||||
attack_rate_formula = "0.5"
|
||||
sensor_range_m_formula = "200"
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 2
|
||||
|
||||
scrap_drop = 5
|
||||
|
||||
|
||||
# Destroyer — 8 cells: a long gun deck with three turret bumps on top.
|
||||
# Still no 2x2 area, so it packs more small guns than a frigate but can never
|
||||
# mount medium hardware.
|
||||
[[ship]]
|
||||
id = "gunship"
|
||||
available_from_start = true
|
||||
layout = ["XOOOX", "OOOOO", "OOOOO", "XOOOX"]
|
||||
id = "destroyer"
|
||||
unlock_at_station_level = -1
|
||||
layout = [
|
||||
"OXOXO",
|
||||
"OOOOO",
|
||||
]
|
||||
|
||||
[ship.schematic]
|
||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||
materials = [{item = "destroyer_hull", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 10
|
||||
|
||||
[ship.threat]
|
||||
cost_formula = "10"
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "12"
|
||||
|
||||
[ship.movement]
|
||||
speed_formula = "1"
|
||||
main_acceleration_formula = "1.5"
|
||||
maneuvering_acceleration_formula = "0.5"
|
||||
angular_acceleration_formula = "15.7"
|
||||
max_rotation_speed_formula = "3.14"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "20"
|
||||
|
||||
[ship.combat]
|
||||
damage_formula = "1"
|
||||
attack_range_formula = "10"
|
||||
attack_rate_formula = "5"
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 2
|
||||
|
||||
|
||||
|
||||
[[ship]]
|
||||
id = "salvage_ship"
|
||||
available_from_start = true
|
||||
layout = ["OOO", "OOO"]
|
||||
|
||||
[ship.schematic]
|
||||
materials = [{item = "iron_ingot", amount = 4}]
|
||||
player_production_level = 3
|
||||
production_time_seconds = 10
|
||||
|
||||
[ship.threat]
|
||||
cost_formula = "0"
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "40 + 4*x"
|
||||
|
||||
[ship.movement]
|
||||
speed_formula = "110"
|
||||
main_acceleration_formula = "220"
|
||||
maneuvering_acceleration_formula = "110"
|
||||
angular_acceleration_formula = "12.56"
|
||||
max_rotation_speed_formula = "6.28"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "250"
|
||||
|
||||
[ship.salvage]
|
||||
collection_range = 50
|
||||
cargo_capacity = 10
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 2
|
||||
|
||||
|
||||
[[ship]]
|
||||
id = "repair_ship"
|
||||
available_from_start = false
|
||||
layout = ["XOX", "OOO", "XOX"]
|
||||
|
||||
[ship.schematic]
|
||||
materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount = 2}]
|
||||
player_production_level = 3
|
||||
production_time_seconds = 15
|
||||
|
||||
[ship.threat]
|
||||
cost_formula = "0"
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "60 + 5*x"
|
||||
hp_formula = "50"
|
||||
|
||||
[ship.movement]
|
||||
speed_formula = "130"
|
||||
main_acceleration_formula = "260"
|
||||
maneuvering_acceleration_formula = "130"
|
||||
angular_acceleration_formula = "12.56"
|
||||
max_rotation_speed_formula = "6.28"
|
||||
speed_mps_formula = "25"
|
||||
main_acceleration_mpss_formula = "40"
|
||||
maneuvering_acceleration_mpss_formula = "20"
|
||||
angular_acceleration_radpss_formula = "6"
|
||||
max_rotation_speed_radps_formula = "3"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "250"
|
||||
|
||||
[ship.repair]
|
||||
repair_rate_formula = "5 + x"
|
||||
repair_range_formula = "80"
|
||||
sensor_range_m_formula = "220"
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 2
|
||||
scrap_drop = 8
|
||||
|
||||
|
||||
# Cruiser — 12 cells with notched corners. Fits at most two 2x2 m guns
|
||||
# (stacked through the middle), leaving the four side cells for small
|
||||
# supports; no 3x3 area exists for an l gun.
|
||||
[[ship]]
|
||||
id = "cruiser"
|
||||
unlock_at_station_level = -1
|
||||
layout = [
|
||||
"XOOX",
|
||||
"OOOO",
|
||||
"OOOO",
|
||||
"XOOX",
|
||||
]
|
||||
|
||||
[ship.schematic]
|
||||
materials = [{item = "cruiser_hull", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 25
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "120"
|
||||
|
||||
[ship.movement]
|
||||
speed_mps_formula = "20"
|
||||
main_acceleration_mpss_formula = "30"
|
||||
maneuvering_acceleration_mpss_formula = "15"
|
||||
angular_acceleration_radpss_formula = "4"
|
||||
max_rotation_speed_radps_formula = "2"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_m_formula = "250"
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 15
|
||||
|
||||
|
||||
# Battlecruiser — 16 cells: a wide bow split into two gun cheeks, tapering
|
||||
# toward the stern. Fits three 2x2 m guns (two in the cheeks, one through
|
||||
# the middle) with small support slots left over; the split bow and tapered
|
||||
# stern leave no 3x3 area for an l gun and no 2x6 area for a drone hangar.
|
||||
[[ship]]
|
||||
id = "battlecruiser"
|
||||
unlock_at_station_level = -1
|
||||
layout = [
|
||||
"OOXXOO",
|
||||
"OOOOOO",
|
||||
"XOOOOX",
|
||||
"XXOOXX",
|
||||
]
|
||||
|
||||
[ship.schematic]
|
||||
materials = [{item = "battlecruiser_hull", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 35
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "180"
|
||||
|
||||
[ship.movement]
|
||||
speed_mps_formula = "18"
|
||||
main_acceleration_mpss_formula = "25"
|
||||
maneuvering_acceleration_mpss_formula = "12"
|
||||
angular_acceleration_radpss_formula = "3"
|
||||
max_rotation_speed_radps_formula = "1.5"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_m_formula = "260"
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 20
|
||||
|
||||
|
||||
# Battleship — 24 cells: a broadside hull with notched flanks on every other
|
||||
# row. Fits four 2x2 m guns (two per gun deck) with the bow, stern, and flank
|
||||
# cells left for supports. All 3x3 placements crowd the center columns, so at
|
||||
# most ONE l gun fits — and mounting it blocks every m gun mount, leaving
|
||||
# only narrow support strips. The notched rows are never adjacent-and-full,
|
||||
# so no 2x6 drone hangar fits.
|
||||
[[ship]]
|
||||
id = "battleship"
|
||||
unlock_at_station_level = -1
|
||||
layout = [
|
||||
"XOOOOX",
|
||||
"OOOOOO",
|
||||
"XOOOOX",
|
||||
"OOOOOO",
|
||||
"XOOOOX",
|
||||
]
|
||||
|
||||
[ship.schematic]
|
||||
materials = [{item = "battleship_hull", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 60
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "350"
|
||||
|
||||
[ship.movement]
|
||||
speed_mps_formula = "14"
|
||||
main_acceleration_mpss_formula = "18"
|
||||
maneuvering_acceleration_mpss_formula = "8"
|
||||
angular_acceleration_radpss_formula = "2"
|
||||
max_rotation_speed_radps_formula = "1"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_m_formula = "280"
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 35
|
||||
|
||||
|
||||
# Dreadnought — 36 cells: the main battery deck is split into three 3x3 gun
|
||||
# slots by structural spacer columns, so exactly three l guns fit side by
|
||||
# side (or m guns / supports in unused slots). The spacers cap every
|
||||
# horizontal run at 5 cells, so the 2x6 drone hangar can never fit — carriers
|
||||
# stay the only hangar hull. Bow and stern strips hold supports.
|
||||
[[ship]]
|
||||
id = "dreadnought"
|
||||
unlock_at_station_level = -1
|
||||
layout = [
|
||||
"XXXOOOOOXXX",
|
||||
"OOOXOOOXOOO",
|
||||
"OOOXOOOXOOO",
|
||||
"OOOXOOOXOOO",
|
||||
"XXOOXXXOOXX",
|
||||
]
|
||||
|
||||
[ship.schematic]
|
||||
materials = [{item = "dreadnought_hull", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 120
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "800"
|
||||
|
||||
[ship.movement]
|
||||
speed_mps_formula = "8"
|
||||
main_acceleration_mpss_formula = "10"
|
||||
maneuvering_acceleration_mpss_formula = "5"
|
||||
angular_acceleration_radpss_formula = "1"
|
||||
max_rotation_speed_radps_formula = "0.5"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_m_formula = "300"
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 60
|
||||
|
||||
|
||||
# Carrier — 37 cells: the top flight deck (rows 0-1) is the only place wide
|
||||
# enough for the 2x6 drone hangar, and exactly one fits. The middle deck row
|
||||
# is broken up by elevator shafts (the X cells) so no 3x3 l gun can ever fit;
|
||||
# the lower decks hold supports and 2x2 point-defense m guns.
|
||||
[[ship]]
|
||||
id = "carrier"
|
||||
unlock_at_station_level = -1
|
||||
layout = [
|
||||
"XOOOOOOOOX",
|
||||
"OOOOOOOOOO",
|
||||
"OOXOOXOOXO",
|
||||
"XOOOOOOOOX",
|
||||
"XXXOOOOXXX",
|
||||
]
|
||||
|
||||
[ship.schematic]
|
||||
materials = [{item = "carrier_hull", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 120
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "700"
|
||||
|
||||
[ship.movement]
|
||||
speed_mps_formula = "9"
|
||||
main_acceleration_mpss_formula = "10"
|
||||
maneuvering_acceleration_mpss_formula = "5"
|
||||
angular_acceleration_radpss_formula = "1"
|
||||
max_rotation_speed_radps_formula = "0.5"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_m_formula = "350"
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 60
|
||||
|
||||
@@ -14,8 +14,8 @@ surface_mask = [
|
||||
level = 1
|
||||
hp_formula = "300"
|
||||
damage_formula = "5"
|
||||
range_formula = "20"
|
||||
fire_rate_formula = "1"
|
||||
range_m_formula = "200"
|
||||
fire_rate_hz_formula = "1"
|
||||
scrap_drop_formula = "10"
|
||||
|
||||
[enemy_station]
|
||||
@@ -25,6 +25,6 @@ surface_mask = [
|
||||
]
|
||||
hp_formula = "300 + 150*x"
|
||||
damage_formula = "2 + 1*x"
|
||||
range_formula = "20"
|
||||
fire_rate_formula = "1.0 + 0.2*x"
|
||||
range_m_formula = "200"
|
||||
fire_rate_hz_formula = "1.0 + 0.2*x"
|
||||
scrap_drop_formula = "10 + 5*x"
|
||||
|
||||
@@ -106,6 +106,8 @@ glyph = "E"
|
||||
# drawn around it. One section per ItemType.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# --- ores ---
|
||||
|
||||
[items.iron_ore]
|
||||
fill = "#8a5a4a"
|
||||
outline = "#201010"
|
||||
@@ -114,6 +116,12 @@ outline = "#201010"
|
||||
fill = "#c47a3a"
|
||||
outline = "#3a1a0a"
|
||||
|
||||
[items.titanium_ore]
|
||||
fill = "#9aa3ad"
|
||||
outline = "#2a2e33"
|
||||
|
||||
# --- ingots ---
|
||||
|
||||
[items.iron_ingot]
|
||||
fill = "#b0b0b8"
|
||||
outline = "#202028"
|
||||
@@ -122,43 +130,194 @@ outline = "#202028"
|
||||
fill = "#d48a4a"
|
||||
outline = "#402010"
|
||||
|
||||
[items.circuit_board]
|
||||
fill = "#2ea35a"
|
||||
outline = "#0a2a14"
|
||||
[items.titanium_ingot]
|
||||
fill = "#c8d2dc"
|
||||
outline = "#3a4048"
|
||||
|
||||
[items.advanced_alloy]
|
||||
fill = "#a06acc"
|
||||
outline = "#201030"
|
||||
|
||||
[items.building_block]
|
||||
fill = "#c8b070"
|
||||
outline = "#302810"
|
||||
# --- salvage loop ---
|
||||
|
||||
[items.scrap]
|
||||
fill = "#7a7268"
|
||||
outline = "#201a14"
|
||||
|
||||
[items.advanced_alloy]
|
||||
fill = "#a06acc"
|
||||
outline = "#201030"
|
||||
|
||||
# --- basic components ---
|
||||
|
||||
[items.copper_wire]
|
||||
fill = "#e09a50"
|
||||
outline = "#3a2008"
|
||||
|
||||
[items.steel_plate]
|
||||
fill = "#8a92a0"
|
||||
outline = "#22262c"
|
||||
|
||||
[items.circuit_board]
|
||||
fill = "#2ea35a"
|
||||
outline = "#0a2a14"
|
||||
|
||||
[items.building_block]
|
||||
fill = "#c8b070"
|
||||
outline = "#302810"
|
||||
|
||||
# --- advanced components ---
|
||||
|
||||
[items.mechanical_parts]
|
||||
fill = "#6f7a66"
|
||||
outline = "#1c2018"
|
||||
|
||||
[items.targeting_unit]
|
||||
fill = "#3a9e8c"
|
||||
outline = "#0c2824"
|
||||
|
||||
[items.drive_unit]
|
||||
fill = "#4a6ad0"
|
||||
outline = "#101a38"
|
||||
|
||||
[items.titanium_frame]
|
||||
fill = "#b8c4d4"
|
||||
outline = "#343c48"
|
||||
|
||||
# --- capital components ---
|
||||
|
||||
[items.reinforced_plating]
|
||||
fill = "#8a6ad0"
|
||||
outline = "#1c1038"
|
||||
|
||||
[items.capital_core]
|
||||
fill = "#b040d0"
|
||||
outline = "#280c30"
|
||||
|
||||
# --- module items ---
|
||||
|
||||
[items.laser_cannon_s_module]
|
||||
fill = "#691313"
|
||||
outline = "#f3ff4f"
|
||||
|
||||
[items.laser_cannon_m_module]
|
||||
fill = "#892020"
|
||||
outline = "#f3ff4f"
|
||||
|
||||
[items.laser_cannon_l_module]
|
||||
fill = "#a92d2d"
|
||||
outline = "#f3ff4f"
|
||||
|
||||
[items.salvager_module]
|
||||
fill = "#b2cfdd"
|
||||
outline = "#236137"
|
||||
|
||||
[items.repair_tool_module]
|
||||
fill = "#2e9ba3"
|
||||
outline = "#689275"
|
||||
|
||||
[items.armor_plates_module]
|
||||
fill = "#808080"
|
||||
outline = "#202020"
|
||||
|
||||
[items.sensor_booster_module]
|
||||
fill = "#40a0ff"
|
||||
outline = "#102840"
|
||||
|
||||
[items.maneuvering_thrusters_module]
|
||||
fill = "#5090e0"
|
||||
outline = "#142438"
|
||||
|
||||
[items.afterburner_module]
|
||||
fill = "#6080c0"
|
||||
outline = "#182030"
|
||||
|
||||
[items.weapon_upgrade_module]
|
||||
fill = "#ff4040"
|
||||
outline = "#401010"
|
||||
|
||||
[items.weapon_primer_module]
|
||||
fill = "#e03838"
|
||||
outline = "#380e0e"
|
||||
|
||||
[items.weapon_stabilizer_module]
|
||||
fill = "#c03030"
|
||||
outline = "#300c0c"
|
||||
|
||||
[items.drone_bay_module]
|
||||
fill = "#cc66ff"
|
||||
outline = "#331040"
|
||||
|
||||
[items.drone_hangar_module]
|
||||
fill = "#9933cc"
|
||||
outline = "#260c33"
|
||||
|
||||
# --- ship hulls (outline matches the ship's fleet color in [ships.*]) ---
|
||||
|
||||
[items.drone_hull]
|
||||
fill = "#1b1b1b"
|
||||
outline = "#3366ff"
|
||||
|
||||
[items.frigate_hull]
|
||||
fill = "#1b1b1b"
|
||||
outline = "#44aaff"
|
||||
|
||||
[items.destroyer_hull]
|
||||
fill = "#1b1b1b"
|
||||
outline = "#33ccaa"
|
||||
|
||||
[items.cruiser_hull]
|
||||
fill = "#1b1b1b"
|
||||
outline = "#66cc33"
|
||||
|
||||
[items.battlecruiser_hull]
|
||||
fill = "#1b1b1b"
|
||||
outline = "#cccc33"
|
||||
|
||||
[items.battleship_hull]
|
||||
fill = "#1b1b1b"
|
||||
outline = "#ff9933"
|
||||
|
||||
[items.dreadnought_hull]
|
||||
fill = "#1b1b1b"
|
||||
outline = "#ff5533"
|
||||
|
||||
[items.carrier_hull]
|
||||
fill = "#1b1b1b"
|
||||
outline = "#cc66ff"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Ships
|
||||
#
|
||||
# Ships are drawn as oriented triangles/arrows. Color is keyed to role, not
|
||||
# schematic (architecture.md, "Layer Order").
|
||||
# Ships are drawn as oriented triangles/arrows. Color is keyed to schematic id.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
[ships.player_combat]
|
||||
[ships.drone]
|
||||
fill = "#3366ff"
|
||||
outline = "#ffffff"
|
||||
|
||||
[ships.salvage]
|
||||
fill = "#33cc66"
|
||||
[ships.frigate]
|
||||
fill = "#44aaff"
|
||||
outline = "#ffffff"
|
||||
|
||||
[ships.repair]
|
||||
fill = "#66ccff"
|
||||
[ships.destroyer]
|
||||
fill = "#33ccaa"
|
||||
outline = "#ffffff"
|
||||
|
||||
[ships.enemy]
|
||||
fill = "#cc3333"
|
||||
[ships.cruiser]
|
||||
fill = "#66cc33"
|
||||
outline = "#ffffff"
|
||||
|
||||
[ships.battlecruiser]
|
||||
fill = "#cccc33"
|
||||
outline = "#ffffff"
|
||||
|
||||
[ships.battleship]
|
||||
fill = "#ff9933"
|
||||
outline = "#ffffff"
|
||||
|
||||
[ships.dreadnought]
|
||||
fill = "#ff5533"
|
||||
outline = "#ffffff"
|
||||
|
||||
[ships.carrier]
|
||||
fill = "#cc66ff"
|
||||
outline = "#ffffff"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -3,27 +3,32 @@ height_tiles = 30
|
||||
refund_percentage = 75
|
||||
starting_building_blocks = 1000
|
||||
scrap_despawn_seconds = 30
|
||||
belt_speed_tiles_per_second = 2
|
||||
tunnel_max_distance = 10
|
||||
tile_size_m = 10
|
||||
belt_speed_mps = 20
|
||||
tunnel_max_distance_tiles = 10
|
||||
departure_interval_seconds = 20
|
||||
|
||||
[regions]
|
||||
asteroid_width = 40
|
||||
player_buffer_width = 20
|
||||
contest_zone_width = 60
|
||||
enemy_buffer_width = 20
|
||||
asteroid_width_tiles = 40
|
||||
player_buffer_width_tiles = 20
|
||||
contest_zone_width_tiles = 60
|
||||
enemy_buffer_width_tiles = 20
|
||||
|
||||
[expansion]
|
||||
columns_per_expansion = 10
|
||||
columns_per_expansion_tiles = 10
|
||||
cost_building_blocks = 200
|
||||
|
||||
[push]
|
||||
push_expand_columns = 10
|
||||
scaling_factor = 1.2
|
||||
push_expand_columns_tiles = 10
|
||||
boss_advance_seconds = 60
|
||||
|
||||
[waves]
|
||||
threat_rate_formula = "0.01*x"
|
||||
threat_rate_formula = "x"
|
||||
ship_level_formula = "1"
|
||||
gap_min_seconds = 15
|
||||
gap_max_seconds = 45
|
||||
spawn_duration_seconds = 10
|
||||
boss_countdown_seconds = 300
|
||||
boss_threat_duration_seconds = 60
|
||||
boss_quiet_before_seconds = 20
|
||||
boss_quiet_after_seconds = 20
|
||||
|
||||
@@ -1,111 +1,85 @@
|
||||
[[arena]]
|
||||
name = "Fighters vs Sniper"
|
||||
height_tiles = 20
|
||||
player_buffer_width = 10
|
||||
contest_zone_width = 60
|
||||
enemy_buffer_width = 10
|
||||
player_buffer_width_tiles = 10
|
||||
contest_zone_width_tiles = 60
|
||||
enemy_buffer_width_tiles = 10
|
||||
|
||||
[[arena.team]]
|
||||
name = "Alpha"
|
||||
[[arena.team.ship]]
|
||||
schematic = "fighter"
|
||||
schematic = "drone"
|
||||
level = 1
|
||||
count = 5
|
||||
modules = [
|
||||
{type = "weapon_upgrade", x = 0, y = 1, rotation = "east"},
|
||||
{type = "sensor_booster", x = 1, y = 0, rotation = "east"},
|
||||
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||
]
|
||||
|
||||
[[arena.team]]
|
||||
name = "Beta"
|
||||
[[arena.team.ship]]
|
||||
schematic = "sniper"
|
||||
schematic = "drone"
|
||||
level = 1
|
||||
count = 1
|
||||
count = 2
|
||||
modules = [
|
||||
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
|
||||
{type = "weapon_upgrade", x = 1, y = 2, rotation = "east"},
|
||||
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||
{type = "weapon_stabilizer", x = 1, y = 1, rotation = "east"},
|
||||
{type = "weapon_stabilizer", x = 1, y = 1, rotation = "east"},
|
||||
{type = "weapon_upgrade", x = 1, y = 1, rotation = "east"},
|
||||
{type = "sensor_booster", x = 1, y = 1, rotation = "east"},
|
||||
{type = "sensor_booster", x = 1, y = 1, rotation = "east"},
|
||||
]
|
||||
|
||||
|
||||
[[arena]]
|
||||
name = "Sniper vs Gunship"
|
||||
name = "Fighters vs Supported"
|
||||
height_tiles = 20
|
||||
player_buffer_width = 10
|
||||
contest_zone_width = 60
|
||||
enemy_buffer_width = 10
|
||||
player_buffer_width_tiles = 10
|
||||
contest_zone_width_tiles = 60
|
||||
enemy_buffer_width_tiles = 10
|
||||
|
||||
[[arena.team]]
|
||||
name = "Alpha"
|
||||
name = "Fighters"
|
||||
[[arena.team.ship]]
|
||||
schematic = "sniper"
|
||||
level = 1
|
||||
count = 1
|
||||
modules = [
|
||||
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
|
||||
{type = "sensor_booster", x = 0, y = 1, rotation = "east"},
|
||||
]
|
||||
|
||||
[[arena.team]]
|
||||
name = "Beta"
|
||||
[[arena.team.ship]]
|
||||
schematic = "gunship"
|
||||
level = 1
|
||||
count = 1
|
||||
modules = [
|
||||
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
|
||||
{type = "weapon_upgrade", x = 3, y = 1, rotation = "east"},
|
||||
{type = "engine_booster", x = 0, y = 1, rotation = "east"},
|
||||
]
|
||||
|
||||
|
||||
[[arena]]
|
||||
name = "Gunship vs Fighters"
|
||||
height_tiles = 20
|
||||
player_buffer_width = 10
|
||||
contest_zone_width = 60
|
||||
enemy_buffer_width = 10
|
||||
|
||||
[[arena.team]]
|
||||
name = "Alpha"
|
||||
[[arena.team.ship]]
|
||||
schematic = "gunship"
|
||||
level = 1
|
||||
count = 1
|
||||
modules = [
|
||||
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
|
||||
{type = "weapon_upgrade", x = 3, y = 2, rotation = "east"},
|
||||
{type = "engine_booster", x = 0, y = 1, rotation = "east"},
|
||||
]
|
||||
|
||||
[[arena.team]]
|
||||
name = "Beta"
|
||||
[[arena.team.ship]]
|
||||
schematic = "fighter"
|
||||
schematic = "drone"
|
||||
level = 1
|
||||
count = 5
|
||||
modules = [
|
||||
{type = "engine_booster", x = 1, y = 0, rotation = "east"},
|
||||
{type = "sensor_booster", x = 2, y = 1, rotation = "east"},
|
||||
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||
]
|
||||
|
||||
[[arena.team]]
|
||||
name = "Supported"
|
||||
[[arena.team.ship]]
|
||||
schematic = "drone"
|
||||
level = 1
|
||||
count = 3
|
||||
modules = [
|
||||
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||
]
|
||||
|
||||
[[arena.team.ship]]
|
||||
schematic = "drone"
|
||||
level = 1
|
||||
count = 2
|
||||
modules = [
|
||||
{type = "repair_tool", x = 1, y = 1, rotation = "east"},
|
||||
]
|
||||
|
||||
[[arena]]
|
||||
name = "Stations and Ships"
|
||||
height_tiles = 60
|
||||
player_buffer_width = 15
|
||||
contest_zone_width = 40
|
||||
enemy_buffer_width = 15
|
||||
player_buffer_width_tiles = 15
|
||||
contest_zone_width_tiles = 40
|
||||
enemy_buffer_width_tiles = 15
|
||||
|
||||
[[arena.team]]
|
||||
name = "Fortified"
|
||||
[[arena.team.ship]]
|
||||
schematic = "fighter"
|
||||
schematic = "drone"
|
||||
level = 1
|
||||
count = 3
|
||||
modules = [
|
||||
{type = "weapon_upgrade", x = 1, y = 1, rotation = "east"},
|
||||
{type = "sensor_booster", x = 1, y = 0, rotation = "east"},
|
||||
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||
]
|
||||
[[arena.team.station]]
|
||||
type = "player_station"
|
||||
@@ -121,9 +95,9 @@ enemy_buffer_width = 15
|
||||
[[arena.team]]
|
||||
name = "Swarm"
|
||||
[[arena.team.ship]]
|
||||
schematic = "fighter"
|
||||
schematic = "drone"
|
||||
level = 1
|
||||
count = 8
|
||||
modules = [
|
||||
{type = "engine_booster", x = 1, y = 0, rotation = "east"},
|
||||
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||
]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[[module]]
|
||||
id = "armor_plate"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["OO"]
|
||||
materials = [{item = "iron_ingot", amount = 2}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 3
|
||||
threat_cost = 2.0
|
||||
fill_color = "#808080"
|
||||
glyph = "A"
|
||||
|
||||
@@ -13,26 +13,125 @@ multiplied_hp_formula = "1.5"
|
||||
|
||||
[[module]]
|
||||
id = "sensor_booster"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["O"]
|
||||
materials = [{item = "circuit_board", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 2
|
||||
threat_cost = 1.0
|
||||
fill_color = "#40A0FF"
|
||||
glyph = "S"
|
||||
|
||||
[module.sensor]
|
||||
added_sensor_range_formula = "10"
|
||||
added_sensor_range_m_formula = "100"
|
||||
|
||||
[[module]]
|
||||
id = "weapon_upgrade"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["O"]
|
||||
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 4
|
||||
threat_cost = 3.0
|
||||
fill_color = "#FF4040"
|
||||
glyph = "W"
|
||||
|
||||
[module.combat]
|
||||
[module.weapon]
|
||||
multiplied_damage_formula = "1.2"
|
||||
|
||||
[[module]]
|
||||
id = "laser_cannon"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["O"]
|
||||
materials = [{item = "iron_ingot", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 5
|
||||
fill_color = "#FF8040"
|
||||
glyph = "L"
|
||||
|
||||
[module.weapon]
|
||||
damage_formula = "2"
|
||||
attack_range_m_formula = "50"
|
||||
attack_rate_hz_formula = "2.0"
|
||||
|
||||
[[module]]
|
||||
id = "salvager"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["OO"]
|
||||
materials = [{item = "iron_ingot", amount = 2}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 5
|
||||
fill_color = "#AACC44"
|
||||
glyph = "Sv"
|
||||
|
||||
[module.salvage]
|
||||
collection_range_m_formula = "500"
|
||||
cargo_capacity_formula = "10"
|
||||
collection_rate_hz_formula = "0.5"
|
||||
|
||||
[[module]]
|
||||
id = "repair_tool"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["O"]
|
||||
materials = [{item = "circuit_board", amount = 2}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 5
|
||||
fill_color = "#66CCFF"
|
||||
glyph = "Rp"
|
||||
|
||||
[module.repair]
|
||||
repair_rate_hz_formula = "5 + x"
|
||||
repair_range_m_formula = "800"
|
||||
|
||||
[[module]]
|
||||
id = "weapon_primer"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["O"]
|
||||
materials = [{item = "iron_ingot", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 4
|
||||
fill_color = "#FF4040"
|
||||
glyph = "Wp"
|
||||
|
||||
[module.weapon]
|
||||
multiplied_attack_rate_hz_formula = "1.2"
|
||||
|
||||
[[module]]
|
||||
id = "weapon_stabilizer"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["O"]
|
||||
materials = [{item = "iron_ingot", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 4
|
||||
fill_color = "#FF4040"
|
||||
glyph = "Ws"
|
||||
|
||||
[module.weapon]
|
||||
multiplied_attack_range_m_formula = "1.5"
|
||||
multiplied_attack_rate_hz_formula = "0.8"
|
||||
|
||||
[[module]]
|
||||
id = "afterburner"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["O"]
|
||||
materials = [{item = "iron_ingot", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 2
|
||||
fill_color = "#40A0FF"
|
||||
glyph = "Ab"
|
||||
|
||||
[module.movement]
|
||||
multiplied_speed_mps_formula = "1.6"
|
||||
added_main_acceleration_mpss_formula = "60"
|
||||
|
||||
[[module]]
|
||||
id = "maneuvering_thrusters"
|
||||
unlock_at_station_level = -1
|
||||
surface_mask = ["O"]
|
||||
materials = [{item = "iron_ingot", amount = 1}]
|
||||
player_production_level = 1
|
||||
production_time_seconds = 2
|
||||
fill_color = "#40A0FF"
|
||||
glyph = "Mt"
|
||||
|
||||
[module.movement]
|
||||
multiplied_speed_mps_formula = "1.2"
|
||||
added_maneuvering_acceleration_mpss_formula = "10"
|
||||
|
||||
@@ -40,6 +40,38 @@ inputs = [{item = "iron_ingot", amount = 4}]
|
||||
outputs = [{item = "building_block", amount = 10}]
|
||||
duration_seconds = 4.0
|
||||
|
||||
[[recipe]]
|
||||
id = "premium_circuit"
|
||||
building = "assembler"
|
||||
unlock_at_station_level = -1
|
||||
inputs = [{item = "circuit_board", amount = 1}]
|
||||
outputs = [{item = "premium_circuit", amount = 1}]
|
||||
duration_seconds = 8.0
|
||||
|
||||
[[recipe]]
|
||||
id = "quick_circuit"
|
||||
building = "assembler"
|
||||
unlock_at_station_level = 0
|
||||
inputs = [{item = "copper_ingot", amount = 3}]
|
||||
outputs = [{item = "circuit_board", amount = 1}]
|
||||
duration_seconds = 3.0
|
||||
|
||||
[[recipe]]
|
||||
id = "advanced_circuit"
|
||||
building = "assembler"
|
||||
unlock_at_station_level = 1
|
||||
inputs = [{item = "iron_ingot", amount = 5}]
|
||||
outputs = [{item = "circuit_board", amount = 1}]
|
||||
duration_seconds = 6.0
|
||||
|
||||
[[recipe]]
|
||||
id = "exotic_alloy"
|
||||
building = "assembler"
|
||||
unlock_at_station_level = 0
|
||||
inputs = [{item = "exotic_ore", amount = 2}]
|
||||
outputs = [{item = "exotic_alloy", amount = 1}]
|
||||
duration_seconds = 10.0
|
||||
|
||||
[[recipe]]
|
||||
id = "reprocessing_cycle"
|
||||
building = "reprocessing_plant"
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
[[ship]]
|
||||
id = "interceptor"
|
||||
available_from_start = true
|
||||
unlock_at_station_level = -1
|
||||
layout = ["XOX", "OOO", "XOX"]
|
||||
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
||||
|
||||
[ship.schematic]
|
||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||
player_production_level = 3
|
||||
production_time_seconds = 10
|
||||
|
||||
[ship.threat]
|
||||
cost_formula = "5 + 1*x"
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "40 + 5*x"
|
||||
|
||||
[ship.movement]
|
||||
speed_formula = "200 + 5*x"
|
||||
main_acceleration_formula = "100000"
|
||||
maneuvering_acceleration_formula = "100000"
|
||||
angular_acceleration_formula = "100000"
|
||||
max_rotation_speed_formula = "100000"
|
||||
speed_mps_formula = "2000 + 50*x"
|
||||
main_acceleration_mpss_formula = "1000000"
|
||||
maneuvering_acceleration_mpss_formula = "1000000"
|
||||
angular_acceleration_radpss_formula = "100000"
|
||||
max_rotation_speed_radps_formula = "100000"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "200"
|
||||
|
||||
[ship.combat]
|
||||
damage_formula = "10 + 2*x"
|
||||
attack_range_formula = "150"
|
||||
attack_rate_formula = "2.0"
|
||||
sensor_range_m_formula = "2000"
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 2
|
||||
@@ -35,34 +28,27 @@ scrap_drop = 2
|
||||
|
||||
[[ship]]
|
||||
id = "destroyer"
|
||||
available_from_start = true
|
||||
unlock_at_station_level = -1
|
||||
layout = ["XOOX", "OOOO", "XOOX"]
|
||||
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
||||
|
||||
[ship.schematic]
|
||||
materials = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 2}]
|
||||
player_production_level = 5
|
||||
production_time_seconds = 20
|
||||
|
||||
[ship.threat]
|
||||
cost_formula = "10 + 2*x"
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "120 + 15*x"
|
||||
|
||||
[ship.movement]
|
||||
speed_formula = "120"
|
||||
main_acceleration_formula = "100000"
|
||||
maneuvering_acceleration_formula = "100000"
|
||||
angular_acceleration_formula = "100000"
|
||||
max_rotation_speed_formula = "100000"
|
||||
speed_mps_formula = "1200"
|
||||
main_acceleration_mpss_formula = "1000000"
|
||||
maneuvering_acceleration_mpss_formula = "1000000"
|
||||
angular_acceleration_radpss_formula = "100000"
|
||||
max_rotation_speed_radps_formula = "100000"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "300"
|
||||
|
||||
[ship.combat]
|
||||
damage_formula = "12 + 2*x"
|
||||
attack_range_formula = "250"
|
||||
attack_rate_formula = "1.0"
|
||||
sensor_range_m_formula = "3000"
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 4
|
||||
@@ -70,7 +56,7 @@ scrap_drop = 4
|
||||
|
||||
[[ship]]
|
||||
id = "salvage_ship"
|
||||
available_from_start = true
|
||||
unlock_at_station_level = -1
|
||||
layout = ["OOO", "OOO"]
|
||||
|
||||
[ship.schematic]
|
||||
@@ -78,25 +64,18 @@ materials = [{item = "iron_ingot", amount = 4}]
|
||||
player_production_level = 3
|
||||
production_time_seconds = 10
|
||||
|
||||
[ship.threat]
|
||||
cost_formula = "0"
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "40 + 4*x"
|
||||
|
||||
[ship.movement]
|
||||
speed_formula = "110"
|
||||
main_acceleration_formula = "100000"
|
||||
maneuvering_acceleration_formula = "100000"
|
||||
angular_acceleration_formula = "100000"
|
||||
max_rotation_speed_formula = "100000"
|
||||
speed_mps_formula = "1100"
|
||||
main_acceleration_mpss_formula = "1000000"
|
||||
maneuvering_acceleration_mpss_formula = "1000000"
|
||||
angular_acceleration_radpss_formula = "100000"
|
||||
max_rotation_speed_radps_formula = "100000"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "250"
|
||||
|
||||
[ship.salvage]
|
||||
collection_range = 50
|
||||
cargo_capacity = 10
|
||||
sensor_range_m_formula = "2500"
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 2
|
||||
@@ -104,7 +83,7 @@ scrap_drop = 2
|
||||
|
||||
[[ship]]
|
||||
id = "repair_ship"
|
||||
available_from_start = false
|
||||
unlock_at_station_level = 0
|
||||
layout = ["XOX", "OOO", "XOX"]
|
||||
|
||||
[ship.schematic]
|
||||
@@ -112,25 +91,18 @@ materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount
|
||||
player_production_level = 3
|
||||
production_time_seconds = 15
|
||||
|
||||
[ship.threat]
|
||||
cost_formula = "0"
|
||||
|
||||
[ship.health]
|
||||
hp_formula = "60 + 5*x"
|
||||
|
||||
[ship.movement]
|
||||
speed_formula = "130"
|
||||
main_acceleration_formula = "100000"
|
||||
maneuvering_acceleration_formula = "100000"
|
||||
angular_acceleration_formula = "100000"
|
||||
max_rotation_speed_formula = "100000"
|
||||
speed_mps_formula = "1300"
|
||||
main_acceleration_mpss_formula = "1000000"
|
||||
maneuvering_acceleration_mpss_formula = "1000000"
|
||||
angular_acceleration_radpss_formula = "100000"
|
||||
max_rotation_speed_radps_formula = "100000"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "250"
|
||||
|
||||
[ship.repair]
|
||||
repair_rate_formula = "5 + x"
|
||||
repair_range_formula = "80"
|
||||
sensor_range_m_formula = "2500"
|
||||
|
||||
[ship.loot]
|
||||
scrap_drop = 2
|
||||
|
||||
@@ -14,8 +14,8 @@ surface_mask = [
|
||||
level = 5
|
||||
hp_formula = "300 + 40*x"
|
||||
damage_formula = "5 + 4*x"
|
||||
range_formula = "300 + 20*x"
|
||||
fire_rate_formula = "0.5 + 0.2*x"
|
||||
range_m_formula = "3000 + 200*x"
|
||||
fire_rate_hz_formula = "0.5 + 0.2*x"
|
||||
scrap_drop_formula = "x"
|
||||
|
||||
[enemy_station]
|
||||
@@ -25,6 +25,6 @@ surface_mask = [
|
||||
]
|
||||
hp_formula = "300 + 150*x"
|
||||
damage_formula = "20 + 10*x"
|
||||
range_formula = "350 + 20*x"
|
||||
fire_rate_formula = "1.0 + 0.2*x"
|
||||
range_m_formula = "3500 + 200*x"
|
||||
fire_rate_hz_formula = "1.0 + 0.2*x"
|
||||
scrap_drop_formula = "10 + 5*x"
|
||||
|
||||
@@ -3,27 +3,32 @@ height_tiles = 60
|
||||
refund_percentage = 75
|
||||
starting_building_blocks = 100
|
||||
scrap_despawn_seconds = 30
|
||||
belt_speed_tiles_per_second = 2
|
||||
tunnel_max_distance = 10
|
||||
tile_size_m = 10
|
||||
belt_speed_mps = 20
|
||||
tunnel_max_distance_tiles = 10
|
||||
departure_interval_seconds = 20
|
||||
|
||||
[regions]
|
||||
asteroid_width = 40
|
||||
player_buffer_width = 10
|
||||
contest_zone_width = 30
|
||||
enemy_buffer_width = 15
|
||||
asteroid_width_tiles = 40
|
||||
player_buffer_width_tiles = 10
|
||||
contest_zone_width_tiles = 30
|
||||
enemy_buffer_width_tiles = 15
|
||||
|
||||
[expansion]
|
||||
columns_per_expansion = 10
|
||||
columns_per_expansion_tiles = 10
|
||||
cost_building_blocks = 200
|
||||
|
||||
[push]
|
||||
push_expand_columns = 20
|
||||
scaling_factor = 1.2
|
||||
push_expand_columns_tiles = 20
|
||||
boss_advance_seconds = 60
|
||||
|
||||
[waves]
|
||||
threat_rate_formula = "1*x - 30"
|
||||
ship_level_formula = "1 + x / 120"
|
||||
threat_rate_formula = "x"
|
||||
ship_level_formula = "1 + x / 10"
|
||||
gap_min_seconds = 15
|
||||
gap_max_seconds = 45
|
||||
spawn_duration_seconds = 10
|
||||
boss_countdown_seconds = 300
|
||||
boss_threat_duration_seconds = 60
|
||||
boss_quiet_before_seconds = 60
|
||||
boss_quiet_after_seconds = 60
|
||||
|
||||
@@ -59,23 +59,43 @@ Simulation types shared across subsystems:
|
||||
- `Item` — `struct Item { ItemType type; }`. Items on belts have no persistent identity across ticks.
|
||||
- `Port` — `struct Port { QPoint tile; Rotation direction; }`. Identifies a belt-adjacent cell and the direction of flow across that cell.
|
||||
- `MovementIntent` — `struct MovementIntent { int priority; QVector2D target; }`. Priority follows the order declared under Movement Arbitration. Cleared at the start of each tick; the highest-priority write wins; `tickMovement` reads the winner.
|
||||
- `FireEvent` — `struct FireEvent { EntityId shooter; EntityId target; Tick emittedAt; }`. Transient record emitted each time a weapon fires (REQ-SHP-FIRING, REQ-SHP-FIRING-BEAM). Buffered in a sim-owned queue and drained by the renderer; see Sim → UI Events.
|
||||
- `SchematicDropEvent` — `struct SchematicDropEvent { ShipSchematicId schematic; int newLevel; bool wasNewUnlock; }`. Emitted when a destroyed enemy-defence-station set awards a schematic (REQ-DEF-SCHEMATIC-DROP). The UI renders a toast (REQ-UI-SCHEMATIC-TOAST); `wasNewUnlock` chooses between the "unlocked" and "level → N" wording.
|
||||
- `WeaponFiredEvent` — `struct WeaponFiredEvent : public Event { entt::entity shooter; entt::entity target; Tick emittedAt; }`. Transient record emitted each time a weapon fires (REQ-SHP-FIRING, REQ-SHP-FIRING-BEAM). Buffered in a sim-owned vector during the tick, then drained and re-emitted via EventManager by the UI frame handler; see Sim → UI Events.
|
||||
- `SchematicChoiceOption` — `struct SchematicChoiceOption { string schematicId; SchematicType type; string displayName; bool isNewUnlock; int targetLevel; }`. Describes one option in the schematic choice dialog (REQ-DEF-SCHEMATIC-DROP). Up to three are generated when an enemy station set is destroyed. `SchematicType` is `Ship`, `Module`, or `Recipe`.
|
||||
- `SchematicChoicesAvailableEvent` — EventManager event carrying a `vector<SchematicChoiceOption>`. Sent by the UI each frame when pending choices are detected; handled by `MainWindow` which opens the schematic choice dialog.
|
||||
|
||||
## Sim → UI Events
|
||||
## Event System
|
||||
|
||||
The sim owns a small set of per-frame event queues that the UI drains on each render. These carry one-shot signals that are not derivable from persistent state — currently weapon fires (REQ-SHP-FIRING-BEAM) and schematic drops (REQ-UI-SCHEMATIC-TOAST). Additional event types can be added here later (e.g., building-complete, unit-death flashes) without changing the pattern.
|
||||
All inter-component communication — both sim→UI and UI→UI — uses a unified `EventManager`/`EventHandler` system. No custom Qt signals/slots are used for inter-widget communication.
|
||||
|
||||
Implementation: a plain `std::vector<FireEvent>` owned by `Simulation`, one vector per event type. Combat resolution (tick-order step 8) appends to it. The UI calls `simulation.drainFireEvents()` once per rendered frame, which returns the accumulated vector by move and clears the internal one. Beams are tracked by the renderer for 0.3 s of wall time (9 ticks at 30 Hz) using the events' `emittedAt` tick, then discarded. If either the shooter or target entity is gone when the renderer looks them up, the beam is dropped early.
|
||||
### EventManager
|
||||
|
||||
We deliberately do **not** use `QObject` signals/slots or `QEvent`:
|
||||
`EventManager` is a singleton (`EventManager::getInstance()`) that routes events to registered handlers.
|
||||
|
||||
- **Determinism.** A plain ordered vector preserves tick-order exactly; the queue is part of per-tick state, inspectable in tests.
|
||||
- **Sim/UI seam.** The sim exposes pull-style access only; the UI never subscribes into the sim, keeping the simulation/presentation split clean.
|
||||
- **Headless testability.** Catch2 tests read the queue directly after `tick()`; no event loop, no `QApplication`.
|
||||
- **Zero overhead.** Sim types remain plain structs — no `QObject`, no moc, no signal dispatch machinery.
|
||||
- `sendEventImmediately(shared_ptr<Event>)` — synchronous dispatch to all handlers of the event's type.
|
||||
- `addEvent(shared_ptr<Event>)` — queues the event for later batch processing.
|
||||
- `processEvents()` — drains the queue, dispatching each event to its handlers.
|
||||
|
||||
If the number of event types grows past a handful, we can wrap them in a small `EventQueue<T>` template, still owned by the sim. Signals/slots would only be warranted if we needed multiple independent subscribers or cross-thread dispatch, and we need neither.
|
||||
The EventManager is thread-safe (mutex-guarded).
|
||||
|
||||
### EventHandler
|
||||
|
||||
`EventHandler<T>` is a CRTP-style template that a class inherits to receive events of type `T`. It provides `registerForEvent()` / `unregisterForEvent()` and requires an override of `handleEvent(shared_ptr<const T>)`.
|
||||
|
||||
`CombinedEventHandler<Ts...>` is a variadic template for classes that handle multiple event types. It provides `registerForEvents()` / `unregisterForEvents()` and requires one `handleEvent` override per type.
|
||||
|
||||
### Sim → UI Events
|
||||
|
||||
The simulation layer stays free of EventManager — it uses a plain `std::vector<WeaponFiredEvent>` internally (owned by `CombatSystem`). This preserves determinism, tick-order fidelity, and headless testability (Catch2 tests read the queue directly via `drainWeaponFiredEvents()` after `tick()`).
|
||||
|
||||
The UI frame handler (`GameWorldView::onFrame` / `ArenaView::onFrame`) bridges the gap: each frame it calls `simulation.drainWeaponFiredEvents()`, then re-emits each `WeaponFiredEvent` via `EventManager::sendEventImmediately()`. Subscribers (the same view's `handleEvent(WeaponFiredEvent)`) create `ActiveBeam` records tracked for 0.3 s of wall time, then discarded. If either the shooter or target entity is gone when the renderer looks them up, the beam is dropped early.
|
||||
|
||||
Schematic drops: when an enemy station set is destroyed, the simulation generates up to 3 `SchematicChoiceOption` entries and stores them as pending state. The UI polls `hasSchematicChoicesPending()` each frame and, when true, sends a `SchematicChoicesAvailableEvent` via EventManager. `MainWindow` handles this event by pausing the game and opening a modal `SchematicChoiceDialog`. The player's selection is fed back via `applySchematicChoice(index)`.
|
||||
|
||||
### UI Events
|
||||
|
||||
All UI interactions — building selection, builder/blueprint mode transitions, speed changes, 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()`.
|
||||
|
||||
Bidirectional interactions use separate request/notification event types to avoid infinite recursion (e.g., `ExitBuilderModeRequestedEvent` from `BuildButtonGrid` → `GameWorldView`, vs. `BuilderModeExitedEvent` from `GameWorldView` → `BuildButtonGrid`).
|
||||
|
||||
## Tick Order
|
||||
|
||||
@@ -88,8 +108,8 @@ Within a single simulation tick, subsystems run in this fixed order. The order i
|
||||
5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT).
|
||||
6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER).
|
||||
7. **Ship behavior systems** — clear `MovementIntent` on each ship, then run `tickThreatResponse`, `tickScrapCollector`, `tickRepairBehavior`, `tickHomeReturn` in any order (arbitration is via intent priority).
|
||||
8. **Combat resolution** — ships and defence stations acquire targets, fire, apply damage; queue deaths. Each fire appends a `FireEvent` to the sim's fire-event queue (REQ-SHP-FIRING-BEAM).
|
||||
9. **Deaths & loot** — process queued deaths: drop scrap (REQ-RES-SCRAP-DROP); if a full enemy-defence-station set was destroyed this tick, award one schematic (REQ-DEF-SCHEMATIC-DROP) and append a `SchematicDropEvent`; remove entities.
|
||||
8. **Combat resolution** — ships and defence stations acquire targets, fire, apply damage; queue deaths. Each fire appends a `WeaponFiredEvent` to the sim's weapon-fired-event queue (REQ-SHP-FIRING-BEAM).
|
||||
9. **Deaths & loot** — process queued deaths: drop scrap (REQ-RES-SCRAP-DROP); if a full enemy-defence-station set was destroyed this tick, generate up to 3 schematic choice options (REQ-DEF-SCHEMATIC-DROP) stored as pending state for the UI to present; remove entities.
|
||||
10. **`tickMovement`** — advance ship positions based on final `MovementIntent`.
|
||||
11. **Scrap despawn** — decrement scrap timers; remove expired scrap (REQ-RES-SCRAP-DROP).
|
||||
|
||||
@@ -280,7 +300,7 @@ The game world is rendered by a single `GameWorldView` widget that inherits `QOp
|
||||
|
||||
### Threading
|
||||
|
||||
Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly without locks. If profiling later justifies moving the sim to a worker thread, the pull-style `drainFireEvents()` / `drainSchematicDropEvents()` / `forEachVisualItem()` APIs already support a clean snapshot-and-render split; a single mutex at the sim boundary would suffice.
|
||||
Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly without locks. If profiling later justifies moving the sim to a worker thread, the pull-style `drainWeaponFiredEvents()` / `getPendingSchematicChoices()` / `applySchematicChoice()` / `forEachVisualItem()` APIs already support a clean snapshot-and-render split; a single mutex at the sim boundary would suffice. The `ArenaSimulation` used by the balancing tool runs headlessly on a worker thread; fire events accumulate in its internal vector and are only drained when `ArenaView` drives `tickOnce()` on the main thread during interactive inspection.
|
||||
|
||||
### Layer Order (back to front)
|
||||
|
||||
@@ -289,9 +309,9 @@ Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly
|
||||
3. **Belt items** — 10×10 colored squares emitted by `BeltSystem::forEachVisualItem`.
|
||||
4. **Scrap** — glyphs at world positions.
|
||||
5. **Ships** — colored arrows oriented by velocity; color keyed to role (player combat / salvage / repair / enemy).
|
||||
6. **Laser beams** — lines derived from live `FireEvent`s kept by the renderer for 0.3 s (REQ-SHP-FIRING-BEAM).
|
||||
6. **Laser beams** — lines derived from live `WeaponFiredEvent`s kept by the renderer for 0.3 s (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.
|
||||
8. **Screen-space UI** — schematic toasts (REQ-UI-SCHEMATIC-TOAST) and any other screen-anchored elements, drawn after resetting the world-space transform.
|
||||
8. **Screen-space UI** — screen-anchored elements, drawn after resetting the world-space transform.
|
||||
|
||||
### Coordinates and Scrolling
|
||||
|
||||
|
||||
194
docs/content_design.md
Normal file
194
docs/content_design.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# Content Design — Ships & Modules
|
||||
|
||||
First real-content iterations (June 2026). Pass 1 defined ship hull grids and
|
||||
module surface masks; pass 2 defined the production tree (recipes). Stats and
|
||||
threat costs in the config files are still placeholders for the balancing
|
||||
pass.
|
||||
|
||||
## Design principle: footprint gating
|
||||
|
||||
Which module fits on which hull is controlled purely by geometry — no
|
||||
explicit allow-lists. Each hull grid is shaped so that it physically cannot
|
||||
contain the footprint of modules from a larger size class. This keeps the
|
||||
rules transparent to the player ("it doesn't fit because there is no room")
|
||||
and makes them trivially moddable through the config files alone.
|
||||
|
||||
### Module footprint ladder
|
||||
|
||||
| Footprint | Modules | Smallest hull that fits it |
|
||||
|-----------|---------|----------------------------|
|
||||
| 1x1 | laser_cannon_s, salvager, repair_tool | drone |
|
||||
| 1x2 | maneuvering_thrusters, sensor_booster, armor_plates | frigate |
|
||||
| 1x3 | afterburner | frigate (eats most of it) |
|
||||
| L-shape (3 cells) | weapon_stabilizer, weapon_primer, weapon_upgrade | frigate |
|
||||
| 2x2 | laser_cannon_m, drone_bay | cruiser |
|
||||
| 3x3 | laser_cannon_l | battleship |
|
||||
| 2x6 | drone_hangar | carrier (only) |
|
||||
|
||||
### Hull grids
|
||||
|
||||
`O` = buildable cell, `X` = hull structure (not buildable).
|
||||
|
||||
**drone (xs, 1 cell)** — exactly one 1x1 module: a small gun, a salvager, or
|
||||
a repair tool. This is what makes drone roles swappable.
|
||||
|
||||
O
|
||||
|
||||
**frigate (s, 5 cells)** — plus shape. Every 1x2 placement crosses the center
|
||||
cell, so at most ONE 1x2 support fits; alternatively one L-shaped weapon
|
||||
modifier or one afterburner through the center line. Gun-boat with one or two
|
||||
support modules, as intended.
|
||||
|
||||
XOX
|
||||
OOO
|
||||
XOX
|
||||
|
||||
**destroyer (s, 8 cells)** — gun deck with three turret bumps. More cells
|
||||
than the frigate (more small guns), but still no 2x2 area anywhere, so medium
|
||||
hardware can never be mounted.
|
||||
|
||||
OXOXO
|
||||
OOOOO
|
||||
|
||||
**cruiser (m, 12 cells)** — notched corners. Fits at most two 2x2 m guns
|
||||
(stacked through the middle), leaving the side cells for supports. No 3x3
|
||||
area.
|
||||
|
||||
XOOX
|
||||
OOOO
|
||||
OOOO
|
||||
XOOX
|
||||
|
||||
**battlecruiser (m, 16 cells)** — split bow with two gun cheeks, tapered
|
||||
stern. Fits three 2x2 m guns — one more than the cruiser — with small support
|
||||
slots left over. The bow split and stern taper prevent any 3x3 area (no l
|
||||
gun) and any 2x6 area (no drone hangar).
|
||||
|
||||
OOXXOO
|
||||
OOOOOO
|
||||
XOOOOX
|
||||
XXOOXX
|
||||
|
||||
**battleship (l, 24 cells)** — broadside hull with notched flanks on every
|
||||
other row. Fits four 2x2 m guns (two per gun deck) — one more than the
|
||||
battlecruiser — with bow, stern, and flank cells for supports. All 3x3
|
||||
placements crowd the center columns, so at most ONE l gun fits: mounted
|
||||
center it blocks every m gun mount (pure support strips remain), mounted
|
||||
offset it still allows two m guns. The notched rows are never adjacent-and-
|
||||
full, so no 2x6 drone hangar fits.
|
||||
|
||||
XOOOOX
|
||||
OOOOOO
|
||||
XOOOOX
|
||||
OOOOOO
|
||||
XOOOOX
|
||||
|
||||
**dreadnought (xl, 36 cells)** — the main battery deck is split into three
|
||||
3x3 gun slots by structural spacer columns, so exactly three l guns fit side
|
||||
by side (or m guns / supports in unused slots), plus bow/stern strips for
|
||||
supports. The spacers cap every horizontal run at 5 cells, so the 2x6 drone
|
||||
hangar can never fit — the carrier stays the only hangar hull.
|
||||
|
||||
XXXOOOOOXXX
|
||||
OOOXOOOXOOO
|
||||
OOOXOOOXOOO
|
||||
OOOXOOOXOOO
|
||||
XXOOXXXOOXX
|
||||
|
||||
**carrier (xl, 37 cells)** — the top flight deck (rows 0–1) is the only
|
||||
region wide enough for the 2x6 drone hangar, and exactly one fits. The middle
|
||||
deck row is broken up by elevator shafts (X cells placed so every 3-column
|
||||
window hits one), which is what prevents any 3x3 l gun from ever fitting.
|
||||
Lower decks hold supports and 2x2 point-defense m guns.
|
||||
|
||||
XOOOOOOOOX
|
||||
OOOOOOOOOO
|
||||
OOXOOXOOXO
|
||||
XOOOOOOOOX
|
||||
XXXOOOOXXX
|
||||
|
||||
### Verified gating matrix
|
||||
|
||||
Checked programmatically against the configs (all four mask rotations,
|
||||
all placements) with `tools/verify_layouts.py` — re-run it after editing
|
||||
layout grids or surface masks:
|
||||
|
||||
python dota_factory/tools/verify_layouts.py
|
||||
|
||||
| Footprint | drone | frigate | destroyer | cruiser | battlecruiser | battleship | dreadnought | carrier |
|
||||
|-----------|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| 1x1 | x | x | x | x | x | x | x | x |
|
||||
| 1x2 | | x | x | x | x | x | x | x |
|
||||
| 1x3 | | x | x | x | x | x | x | x |
|
||||
| L-shape | | x | x | x | x | x | x | x |
|
||||
| 2x2 | | | | x | x | x | x | x |
|
||||
| 3x3 | | | | | | x | x | |
|
||||
| 2x6 | | | | | | | | x |
|
||||
|
||||
Maximum simultaneous (disjoint) placements: m guns — cruiser 2,
|
||||
battlecruiser 3, battleship 4; l guns — battleship 1, dreadnought 3;
|
||||
drone hangar — carrier 1.
|
||||
|
||||
## Production tree
|
||||
|
||||
Design principle: each game phase adds exactly one new base input chain, so
|
||||
factory complexity ramps alongside ship size.
|
||||
|
||||
| Phase | New input | How acquired | Unlocks |
|
||||
|-------|-----------|--------------|---------|
|
||||
| early | iron_ore, copper_ore | mined | drone, frigate, destroyer; small guns and basic supports |
|
||||
| mid | titanium_ore | mined (3x slower than iron) | cruiser, battlecruiser; m guns, drone bay, weapon modifiers |
|
||||
| late | advanced_alloy | ONLY from reprocessing salvaged scrap | battleship, dreadnought, carrier; l guns, drone hangar |
|
||||
|
||||
The advanced_alloy gate is the core loop hook: capital ship production
|
||||
requires fighting (salvaging scrap from kills and reprocessing it), not just
|
||||
mining. The reprocessing plant turns 5 scrap into iron/copper/titanium ingots
|
||||
or advanced_alloy probabilistically.
|
||||
|
||||
Intermediate components, by tier:
|
||||
|
||||
- **Tier 2 (early):** copper_wire (copper), steel_plate (iron), circuit_board
|
||||
(iron + wire), building_block (iron).
|
||||
- **Tier 3 (mid):** mechanical_parts (steel + iron), targeting_unit (circuits
|
||||
+ wire), drive_unit (steel + mechanical_parts + circuit), titanium_frame
|
||||
(titanium + steel).
|
||||
- **Tier 4 (late):** reinforced_plating (steel + advanced_alloy),
|
||||
capital_core (targeting_unit + drive_unit + 2 advanced_alloy).
|
||||
|
||||
Hulls and modules consume intermediates of their tier: early items are built
|
||||
from tier-2 parts, midgame items require tier-3 parts (deeper chains, more
|
||||
assemblers), capital items require tier-4 parts (and therefore combat). Hull
|
||||
items are named `<ship>_hull`; module items `<module>_module`. Every item has
|
||||
an `[items.*]` entry in visuals.toml; hull item outlines match the ship's
|
||||
fleet color from `[ships.*]`.
|
||||
|
||||
Consistency is checked by `tools/verify_recipes.py` — re-run it after editing
|
||||
recipes, ship/module materials, or visuals:
|
||||
|
||||
python dota_factory/tools/verify_recipes.py
|
||||
|
||||
It verifies every consumed item has a producer, every item has a visuals
|
||||
entry, flags orphaned items, and prints which items are reprocessing-only
|
||||
(currently exactly advanced_alloy).
|
||||
|
||||
## Deliberate placeholders / open questions for later passes
|
||||
|
||||
- All new hulls have `threat.cost_formula = "0"` so enemy waves do not spawn
|
||||
them yet (WaveSystem treats any ship with positive threat cost as wave-
|
||||
eligible, regardless of unlock level). The balancing pass should set real
|
||||
threat costs together with `default_modules` loadouts so waves spawn them
|
||||
armed.
|
||||
- All new hulls and all assembler recipes are `unlock_at_station_level = -1`
|
||||
(available from the start) to make testing easy; the balancing pass should
|
||||
stagger these so mid/lategame recipes drop as schematics from enemy defence
|
||||
stations.
|
||||
- Recipe quantities and durations are a first guess, deliberately roughly
|
||||
tiered (capital hulls ~60 s, drones 4 s); the balancing pass tunes them.
|
||||
- `drone_bay` and `drone_hangar` are footprint-only placeholders: the drone
|
||||
launching capability does not exist in the simulation yet, so they define
|
||||
no capability section.
|
||||
- Renames in this pass: `laser_cannon_xs` → `laser_cannon_s` (the old 2x2
|
||||
`laser_cannon_s` became `laser_cannon_m`), `armor_plate` → `armor_plates`,
|
||||
`manuvering_thrusters` → `maneuvering_thrusters` (typo fix). Test data
|
||||
under `bin/test/data/config` intentionally still uses the old ids — it is
|
||||
an independent fixture set.
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
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, enemy ship level formula, belt speed, starting building blocks, departure interval.
|
||||
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, boss wave timing, enemy ship level formula, belt speed, starting building blocks, departure interval.
|
||||
- **buildings.toml** — building block cost and construction time per building type.
|
||||
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities.
|
||||
- **ships.toml** — per schematic: a human-readable display name (used in toasts and UI), ship stats (HP, max linear speed, damage, attack range, attack rate, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as formulas of ship level, required build materials, threat cost formula, player production level, whether the schematic is available from game start, and a layout grid defining the ship's module slots.
|
||||
- **modules.toml** — per module type: id, surface mask, materials list, player production level, production time, threat cost, fill color, glyph, and stat modifier formulas (additive and/or multiplicative per stat).
|
||||
- **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).
|
||||
- **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 formulas of ship level, required build materials, player production level, 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), a layout grid defining the ship's module slots, a `scrap_drop` loot value, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
|
||||
- **modules.toml** — per module type: id, surface mask, materials list, initial player production level, 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), 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).
|
||||
- **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 role, and station type; beam color and 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 for every building type, item type, ship schematic, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
|
||||
- **ship_layouts.toml** — named layout blueprints per ship type; written and read by the application to persist the layout blueprint panel (REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD). Not a game parameter file; the simulation does not read it.
|
||||
|
||||
- REQ-CFG-RELOAD: When the player triggers a Restart (REQ-UI-GAME-MENU), all config files are reloaded from disk before the simulation is reset to its initial state. Formula strings are recompiled at that point. This allows config edits made while the application is running to take effect without a full application restart.
|
||||
@@ -107,14 +107,14 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
|
||||
## Building Types
|
||||
|
||||
- REQ-BLD-MINER: **Miner** (2×2): The player selects which ore type it extracts. Each ore type corresponds to a `recipes.toml [[recipe]]` entry with `building = "miner"`, defining the output item and `duration_seconds`. Every asteroid tile is equivalent for mining — any miner can produce any ore type based solely on its selected recipe. Ore never depletes.
|
||||
- REQ-BLD-MINER: **Miner** (2×2): The player selects which ore type it extracts. Each ore type corresponds to a `recipes.toml [[recipe]]` entry with `building = "miner"`, defining the output item and `duration_seconds`. Every asteroid tile is equivalent for mining — any miner can produce any ore type based solely on its selected recipe. Ore never depletes. Only implicitly unlocked ore-type recipes are available for selection (REQ-LOCK-UI-RECIPE).
|
||||
- REQ-BLD-SMELTER: **Smelter** (2×2): Converts ore or scrap into basic materials. No recipe selection required. Inputs, outputs, and rates are defined in `recipes.toml [[recipe]]` entries with `building = "smelter"`.
|
||||
- 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"`.
|
||||
- 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 output buffer holds at most one cycle's output — see REQ-MAT-OUTPUT-BUFFER-REPROCESSING.
|
||||
- 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-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 at `ships.toml [ship.schematic].player_production_level` (initial value 5, incremented by duplicate schematic drops per REQ-DEF-SCHEMATIC-DROP) 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-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-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. Routing rules:
|
||||
- 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:
|
||||
- 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 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.
|
||||
@@ -149,7 +149,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
## Ships
|
||||
|
||||
- REQ-SHP-AUTONOMOUS: Ships are produced by shipyards and are fully autonomous once produced.
|
||||
- REQ-SHP-STATS: Base ship stats are defined as formulas of ship level in `ships.toml`: HP (`[ship.health].hp_formula`), max linear speed (`[ship.movement].speed_formula`), damage (`[ship.combat].damage_formula`), attack range (`[ship.combat].attack_range_formula`), attack rate (`[ship.combat].attack_rate_formula`), sensor range (`[ship.sensors].range_formula`), main acceleration (`[ship.movement].main_acceleration_formula`, tiles/s²), maneuvering acceleration (`[ship.movement].maneuvering_acceleration_formula`, tiles/s²), angular acceleration (`[ship.movement].angular_acceleration_formula`, rad/s²), max rotation speed (`[ship.movement].max_rotation_speed_formula`, rad/s). Required build materials (`[ship.schematic].materials`) and availability from game start (`[[ship]].available_from_start`) are also defined there. Final ship stats incorporate module modifiers per REQ-MOD-STAT-CALC.
|
||||
- REQ-SHP-STATS: Base hull stats are defined as formulas of ship level in `ships.toml`: HP (`[ship.health].hp_formula`), max linear speed (`[ship.movement].speed_formula`), sensor range (`[ship.sensors].range_formula`), main acceleration (`[ship.movement].main_acceleration_formula`, tiles/s²), maneuvering acceleration (`[ship.movement].maneuvering_acceleration_formula`, tiles/s²), angular acceleration (`[ship.movement].angular_acceleration_formula`, rad/s²), max rotation speed (`[ship.movement].max_rotation_speed_formula`, 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-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-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_formula`) is applied along the ship's current facing direction only. The maneuvering acceleration (`maneuvering_acceleration_formula`) can be applied in any direction independently of the facing direction, enabling lateral or braking movement without rotating. The angular acceleration (`angular_acceleration_formula`) controls how quickly the ship rotates. Linear speed is capped at the ship's `speed_formula` value; rotation rate is capped at the ship's `max_rotation_speed_formula` value. Ship position refers to the ship's center for all range, sensor, and attack checks.
|
||||
@@ -157,15 +157,19 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
- REQ-SHP-SENSOR: A ship perceives only entities within its sensor range. Behavior is driven by what is in sensor range; entities outside sensor range are ignored.
|
||||
- REQ-SHP-FIRING: All weapons — on ships and on defence stations — fire when off cooldown and the target is within attack range. Firing emits a fire event and starts a 0.15-second damage delay (half the beam duration). When that delay expires, damage is applied to the target — unless the target has already been destroyed, in which case the damage is silently dropped. If the shooter is destroyed before the delay expires, damage is still applied when the delay expires. There is no projectile entity and no intervening collision. The weapon's cooldown begins at the moment of firing, not at damage application.
|
||||
- REQ-SHP-FIRING-BEAM: Each fire event produces a visual laser beam drawn from the shooter's position to the target for 0.3 seconds. 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). The offset is chosen once per fire 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 damage over its lifetime). Beams follow the shooter 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 shooter or target is destroyed before it expires.
|
||||
- REQ-SHP-COMBAT: **Combat ships** (player) — engage enemy ships within sensor range. The player can configure the following per shipyard (applied to all ships produced by that shipyard):
|
||||
- REQ-SHP-COMBAT: Ships with at least one **weapon module** (player) — engage enemy ships within sensor range. The player can configure the following per shipyard (applied to all ships produced by that shipyard):
|
||||
- Stance: aggressive (advance toward enemies) / defensive (hold position near asteroid).
|
||||
- Target priority: closest / highest HP / structures first.
|
||||
- REQ-SHP-RALLY: After spawning, aggressive-stance combat ships move to and loiter at the **rally point** — the midpoint between the two player defence stations (center of their Y-span, at the player defence stations' X position). While at the rally point, ships still engage any enemy that enters sensor range. Every `world.toml [world].departure_interval_seconds` seconds (default 20), all combat ships 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: **Salvage ships** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If scrap enters sensor range, move to it, collect, and deliver it to a Salvage Bay on the asteroid; after delivery, resume patrol. If an enemy ship enters sensor range while not currently targeting or carrying scrap, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. Salvage ships are vulnerable to enemy ships while operating.
|
||||
- REQ-SHP-REPAIR: **Repair ships** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If a damaged player defence station or player ship enters sensor range, move to it and repair. If an enemy ship enters sensor range while not currently repairing, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. The player can configure the target priority per shipyard:
|
||||
- REQ-SHP-RALLY: After spawning, aggressive-stance ships with weapon modules move to and loiter at the **rally point** — the midpoint between the two player defence stations (center of their Y-span, at the player defence stations' X position). While at the rally point, ships still engage any enemy that enters sensor range. 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, move to it; when it is within a module's `collection_range`, that module collects it (consuming the scrap entity). Once all cargo is full, fly to a Salvage Bay and deliver; after delivery, resume patrol. If an enemy ship enters sensor range while not currently targeting or carrying scrap, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. Ships with salvage modules are vulnerable to enemy ships while operating.
|
||||
|
||||
Each salvage module instance operates independently: it has its own cargo hold (`cargo_capacity`), collection range (`collection_range`), and collection rate (`collection_rate`, in collections per second). After collecting a piece of scrap, the module cannot collect again until `1 / collection_rate` seconds have elapsed. A ship with multiple salvage modules can therefore collect multiple pieces of scrap per tick (one per ready module), and installs of different module types may have different ranges and rates. The ship navigates based on the maximum collection range across all installed salvage modules.
|
||||
- REQ-SHP-REPAIR: Ships with at least one **repair module** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If a damaged player defence station or player ship enters sensor range, move to it and repair. If an enemy ship enters sensor range while not currently repairing, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. The player can configure the target priority per shipyard:
|
||||
- Defence stations first / ships first / nearest target.
|
||||
|
||||
Each repair module instance operates independently: it has its own repair rate (`repair_rate`) and repair range (`repair_range`). On each tick, a module first attempts to heal 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` and heals that instead. If no valid target is found within range, the module idles. A ship with multiple repair modules can therefore heal different targets simultaneously. Navigation is driven solely by the behavior-level target; individual module fallback targets do not affect which direction the ship moves.
|
||||
- REQ-SHP-ENEMY-AI: **Enemy ships** — engage the closest valid target (player defence station, HQ, or player ship) within their sensor range. If no target is in sensor range, they move toward the asteroid (leftward in world coordinates).
|
||||
- REQ-SHP-SCHEMATICS: The player selects a schematic per shipyard by clicking it. New schematics are unlocked automatically when an enemy defence station set is destroyed (REQ-DEF-SCHEMATIC-DROP) — there is no physical loot to collect.
|
||||
- REQ-SHP-SCHEMATICS: The player selects a schematic per shipyard by clicking it. New schematics are unlocked by destroying enemy defence station sets (REQ-DEF-SCHEMATIC-DROP) — there is no physical loot to collect.
|
||||
|
||||
## Ship Modules
|
||||
|
||||
@@ -175,12 +179,16 @@ 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.
|
||||
- `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).
|
||||
- `player_production_level` — fixed level for this module type; used as `x` in its stat modifier formulas.
|
||||
- `player_production_level` — initial level for this module type; used as `x` in its stat formulas. Incremented by 1 on each duplicate schematic drop (REQ-DEF-SCHEMATIC-DROP).
|
||||
- `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.
|
||||
- `production_time_seconds` — time added to the ship's production cycle per instance.
|
||||
- `threat_cost` — threat cost added to the ship's threat cost per instance.
|
||||
- `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.
|
||||
- Zero or more stat modifier formulas (see REQ-MOD-STAT-CALC).
|
||||
- An optional **capability section** (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas. A module with base stat formulas is a capability module — each placed instance grants the ship an independent weapon, salvage bay, or repair tool with its own state (cooldown, target, cargo). A ship may have multiple capability module instances of the same or different types. Base stat formulas per capability type:
|
||||
- **Weapon** (`[module.weapon]`): `damage_formula`, `attack_range_formula`, `attack_rate_formula`.
|
||||
- **Salvage** (`[module.salvage]`): `collection_range_formula` (tiles), `cargo_capacity_formula` (integer scrap units), `collection_rate_formula` (collections per second).
|
||||
- **Repair** (`[module.repair]`): `repair_rate_formula` (HP/s), `repair_range_formula` (tiles).
|
||||
- Zero or more **passive stat modifier formulas** (`added_*`/`multiplied_*`) that boost stats on the ship hull or on capability module instances (see REQ-MOD-STAT-CALC). A single module may be both a capability module and provide passive modifiers.
|
||||
|
||||
- REQ-MOD-LAYOUT: Each ship in `ships.toml` defines a `layout` — a list of strings representing the ship's module grid (see Ship Layout Format). All ships define a layout.
|
||||
|
||||
@@ -194,13 +202,31 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
|
||||
- REQ-MOD-MATERIALS: The total materials required to build a ship are the union of the ship's base `[ship.schematic].materials` and the `materials` of every module instance in the configured layout. Quantities of the same item type are summed.
|
||||
- REQ-MOD-PRODUCTION-TIME: The total production time is the ship's base `[ship.schematic].production_time_seconds` plus the sum of `production_time_seconds` for every module instance in the configured layout.
|
||||
- REQ-MOD-THREAT: The total threat cost of a ship is the ship's base `[ship.threat].cost_formula` evaluated at the ship's level, plus the sum of `threat_cost` for every module instance in the configured layout.
|
||||
- REQ-MOD-STAT-CALC: For each ship stat, the final value is computed as: `final = base × total_multiplier + total_additive`, where:
|
||||
- `base` is the ship's base stat formula evaluated at the ship's production level.
|
||||
- `total_multiplier` = 1 + sum of (m_i − 1) for each multiplicative modifier m_i from all module instances. Each m_i is evaluated from the module's multiplicative formula at the module's `player_production_level`.
|
||||
- `total_additive` = sum of all additive modifier values from all module instances. Each additive value is evaluated from the module's additive formula at the module's `player_production_level`.
|
||||
- REQ-MOD-THREAT: The threat cost of a ship is dynamically derived from the accumulated total production time required to produce that ship from scratch. One second of production time equals one threat. The total production time is the sum of:
|
||||
1. The ship's base `production_time_seconds`.
|
||||
2. The `production_time_seconds` of every module instance in the configured layout.
|
||||
3. For every material required (the union of the ship's base materials and all module instance materials, with quantities summed per item type): the recursive production time of that material multiplied by the required quantity (see REQ-THREAT-ITEM).
|
||||
|
||||
Module stat modifier formulas follow the naming convention: for a ship stat `ship.<category>.<stat>_formula`, a module may define `added_<stat>_formula` (additive) and/or `multiplied_<stat>_formula` (multiplicative) under `[module.<category>]`. Example: for `ship.sensor.sensor_range_formula`, a module may define `module.sensor.added_sensor_range_formula` and/or `module.sensor.multiplied_sensor_range_formula`.
|
||||
- REQ-THREAT-ITEM: The threat value of an item type (in seconds) is determined by the recipe that produces it:
|
||||
- **Miner recipe**: the recipe's `duration_seconds`.
|
||||
- **Smelter recipe**: the recipe's `duration_seconds` plus the sum of each input's threat value multiplied by that input's required quantity.
|
||||
- **Assembler recipe**: the recipe's `duration_seconds` plus the sum of each input's threat value multiplied by that input's required quantity.
|
||||
- **Reprocessing-only item** (an item type that has no miner, smelter, or assembler recipe producing it, and is only obtainable via reprocessing): `(scrap_threat × scrap_per_cycle + duration_seconds) / probability`, where `scrap_threat` is the threat value of scrap (see REQ-THREAT-SCRAP), `scrap_per_cycle` is the number of scrap consumed per reprocessing cycle, `duration_seconds` is the reprocessing cycle time, and `probability` is the normalized weight of that item in the reprocessing output pool.
|
||||
- **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 recipes. The reprocessing path is only used when no other recipe exists.
|
||||
|
||||
- REQ-THREAT-SCRAP: The threat value of scrap is derived from the ship schematic with the smallest configured `scrap_drop` value (from `ships.toml [ship.loot].scrap_drop`). Scrap threat = that ship's threat cost (REQ-MOD-THREAT) / that ship's `scrap_drop` value. If multiple schematics share the same smallest `scrap_drop`, any one of them may be used.
|
||||
- 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 formula evaluated at the ship's production level (for hull stats) or at the capability module's `player_production_level` (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 evaluated from the module's multiplicative formula at the module's `player_production_level`.
|
||||
- `total_additive` = sum of all additive modifier values from all passive module instances. Each additive value is evaluated from the module's additive formula at the module's `player_production_level`.
|
||||
|
||||
Passive modifier formulas follow the naming convention: a module may define `added_<stat>_formula` (additive) and/or `multiplied_<stat>_formula` (multiplicative) under `[module.<category>]`. The category determines what the modifier targets:
|
||||
- `[module.health]`, `[module.movement]`, `[module.sensor]` — modifiers apply to the ship hull's stats.
|
||||
- `[module.weapon]` — modifiers apply to every weapon module instance on the ship.
|
||||
- `[module.salvage]` — modifiers apply to every salvage module instance on the ship.
|
||||
- `[module.repair]` — modifiers apply to every repair module instance on the ship.
|
||||
|
||||
Example: `[module.sensor].added_sensor_range_formula` adds to the ship's sensor range. `[module.weapon].multiplied_damage_formula` multiplies the damage of every weapon module instance on the ship.
|
||||
|
||||
### Module UI
|
||||
|
||||
@@ -208,11 +234,32 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
- 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:
|
||||
- **Left**: The ship's layout grid rendered at full scale. Buildable cells are white; non-buildable cells are black. Placed modules are rendered with their `fill_color` and `glyph`. The ghost of the currently selected module is shown at the cursor position when in placement mode.
|
||||
- **Center**: A grid of module selection buttons (one per module type defined in `modules.toml`) plus a "Remove" button. Each module button shows the module id and its glyph.
|
||||
- **Right**: The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD).
|
||||
- **Top**: The ship's layout grid rendered at full scale. Buildable cells are white; non-buildable cells are black. Placed modules are rendered with their `fill_color` and `glyph`. The ghost of the currently selected module is shown at the cursor position when in placement mode.
|
||||
- **Left** (below the grid): The ship stats panel (see REQ-MOD-UI-STATS-PANEL).
|
||||
- **Center** (below the grid): A grid of module selection buttons (one per **unlocked** module type; see REQ-DEF-SCHEMATIC-DROP) plus a "Remove" button. Each module button shows the module id and its glyph.
|
||||
- **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.
|
||||
|
||||
- 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 at the schematic's `player_production_level`, 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:
|
||||
- HP
|
||||
- Max linear speed
|
||||
- Sensor range
|
||||
- Main acceleration
|
||||
- Maneuvering acceleration
|
||||
- Angular acceleration
|
||||
- Max rotation speed
|
||||
|
||||
In addition, the panel shows capability module stats conditioned on which capability module types are present in the current layout:
|
||||
- **Weapons** (shown only if at least one weapon module is placed): combined DPS = Σ(damage_i × attack_rate_i) across all weapon module instances; maximum range = max(attack_range_i) across all weapon module instances.
|
||||
- **Salvage** (shown only if at least one salvage module is placed): combined collection rate = Σ(collection_rate_i) across all salvage module instances; maximum range = max(collection_range_i) across all salvage module instances.
|
||||
- **Repair** (shown only if at least one repair module is placed): combined repair rate = Σ(repair_rate_i) across all repair module instances; maximum range = max(repair_range_i) across all repair module instances.
|
||||
|
||||
All capability module stat values incorporate passive modifiers targeting the relevant capability category per REQ-MOD-STAT-CALC. Each capability module instance uses its own `player_production_level` for formula evaluation.
|
||||
|
||||
While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT) for the current layout configuration. This value updates in real time as modules are placed or removed.
|
||||
|
||||
- REQ-MOD-UI-LAYOUT-SIZE: Ship layouts are small enough to display in the layout configuration dialog without scrolling (maximum grid size fits within the dialog).
|
||||
|
||||
### Layout Blueprints
|
||||
@@ -221,7 +268,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
|
||||
- REQ-MOD-UI-BLUEPRINT-CREATE: Clicking "Create Blueprint" opens a modal dialog prompting for 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 module layout currently shown in the left-side layout grid (the in-progress state of the dialog, not the previously confirmed shipyard layout) and appends it to the blueprint list.
|
||||
|
||||
- REQ-MOD-UI-BLUEPRINT-ENTRY: Each blueprint entry shows the blueprint name and a delete icon ("×") to the right of the name. Clicking the entry (name area) loads that blueprint's module list into the left-side layout grid, replacing all currently placed modules. Module instances that are invalid for the current ship layout (unknown module type, position outside the grid, position on a non-buildable cell, or overlapping another module in the same blueprint) are silently skipped; the remaining valid instances are placed. Clicking the delete icon ("×") removes that blueprint entry from the list immediately.
|
||||
- REQ-MOD-UI-BLUEPRINT-ENTRY: Each blueprint entry shows the blueprint name and a delete icon ("×") to the right of the name. Clicking the entry (name area) loads that blueprint's module list into the left-side layout grid, replacing all currently placed modules. Module instances that are invalid for the current ship layout (unknown module type, locked module type, position outside the grid, position on a non-buildable cell, or overlapping another module in the same blueprint) are silently skipped; the remaining valid instances are placed. Clicking the delete icon ("×") removes that blueprint entry from the list immediately.
|
||||
|
||||
- REQ-MOD-UI-BLUEPRINT-STARTUP: At application startup, layout blueprints are loaded from `ship_layouts.toml` in the same directory as the application executable. Blueprint entries missing required fields (`name` or `ship_type`) are silently skipped. 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.
|
||||
|
||||
@@ -235,21 +282,62 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
- REQ-DEF-ENEMY-PLACEMENT: 2 enemy defence stations are placed at the right boundary of the scrollable area at game start, and again each time a new set is spawned after a push. Stats scale with the station level (REQ-PSH-STATION-STATS).
|
||||
- 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-PUSH: When both enemy defence stations in a set are destroyed, the push scaling multiplier is applied (REQ-PSH-ACCUMULATION), 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 is automatic — no physical item to collect. A schematic is chosen uniformly at random from all schematics defined in `ships.toml`. If the player does not yet have that schematic, it is unlocked. If the player already has it, the schematic's `[ship.schematic].player_production_level` is incremented by 1 — so subsequent ships of that type are produced at a higher level. The player is notified via a toast (REQ-UI-SCHEMATIC-TOAST).
|
||||
- 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). Up to three schematic options are drawn uniformly at random **without replacement** from the eligible drop pool. If the pool contains fewer than three entries, only that many options are shown. The eligible drop pool contains:
|
||||
- All **ship schematics** and **module schematics** whose `unlock_at_station_level` is -1 or is ≤ the level of the destroyed station set.
|
||||
- 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.
|
||||
|
||||
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), the schematic type (ship, module, or assembler recipe), and whether selecting it would be a **new unlock** or a **level-up** (showing the target level for level-ups). Assembler recipe schematics are always new unlocks since they are removed from the pool once awarded.
|
||||
|
||||
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:
|
||||
- For a ship or module schematic that would be a **new unlock**, its `materials` are added to the base set per REQ-LOCK-IMPLICIT step 1a before recomputation.
|
||||
- For a ship or module schematic **level-up**, the implicit unlock set is unchanged, so the list is always empty.
|
||||
- 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".
|
||||
|
||||
The player selects one option by clicking it. The selected schematic is applied and the dialog closes:
|
||||
|
||||
For a **ship or module schematic**: if the player does not yet have the 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)). If the player already has it, the schematic's `player_production_level` is incremented by 1 — for ship schematics, subsequent ships of that type are produced at a higher level; for module schematics, all instances of that module type use the higher level in their stat formulas.
|
||||
|
||||
For an **assembler recipe schematic**: the recipe is explicitly unlocked and becomes available in the assembler recipe-selection dropdown (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
|
||||
|
||||
- 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-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).
|
||||
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 dropdown 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.
|
||||
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.
|
||||
|
||||
- REQ-LOCK-REPROCESSING-POOL: The pool of possible outputs for a Reprocessing Plant cycle (REQ-BLD-REPROCESSING) is restricted to item types that are currently implicitly unlocked (REQ-LOCK-IMPLICIT). Weights are renormalized over the eligible outputs. If no eligible outputs remain, the Reprocessing Plant cannot start a production cycle.
|
||||
|
||||
- REQ-LOCK-UI-RECIPE: Locked miner ore-type recipes and assembler recipes are not shown in their respective recipe-selection dropdowns.
|
||||
|
||||
- REQ-LOCK-UI-SCHEMATIC: Locked ship schematics are not shown in the shipyard's schematic-selection dropdown.
|
||||
|
||||
- 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.)
|
||||
|
||||
## Threat Level & Enemy Waves
|
||||
|
||||
- REQ-WAV-THREAT-RATE: A global **threat level** accumulates continuously over time. The rate of increase per second is determined by `world.toml [waves].threat_rate_formula` where x is elapsed game time in seconds, clamped to a minimum of 0 (negative formula values are treated as 0). Example: `1*x - 30` yields 0 threat/s for x ≤ 30s and increases linearly after that.
|
||||
- REQ-WAV-GAP: At game start and immediately after each wave is triggered, a random inter-wave gap is drawn uniformly from [`world.toml [waves].gap_min_seconds`, `gap_max_seconds`].
|
||||
- REQ-WAV-TRIGGER: When the gap expires, a wave is triggered. Ships are selected one at a time: from all schematics whose `threat.cost_formula` evaluates to > 0 at the current enemy ship level, uniformly randomly pick one whose cost fits the remaining threat budget. Repeat until no eligible schematic fits. Any remaining threat carries over to the next wave. A longer gap results in a larger wave. Because enemy ship level increases with time (REQ-WAV-SHIP-LEVEL), threat cost per ship rises naturally over the course of the game.
|
||||
- REQ-WAV-SHIP-LEVEL: Each wave's enemy ships are assigned a level determined by `world.toml [waves].ship_level_formula` where x is elapsed game time in seconds. This is the sole mechanism by which individual enemy ships become stronger over time. Wave *size* grows separately via threat accumulation (REQ-WAV-THREAT-RATE) and push scaling (REQ-PSH-ACCUMULATION). Per-ship stats and threat cost are computed from the ship level via the formulas in `ships.toml` (see REQ-SHP-STATS).
|
||||
- REQ-WAV-NO-MODULES: Enemy ships spawned by waves have no modules. Their stats are computed from the base ship formulas only, with no module modifiers applied.
|
||||
- REQ-WAV-BOSS-COUNTER: A global **boss wave counter** `x` starts at 1 at game start and increments by 1 immediately after each boss wave fires. It represents the current boss wave cycle number and is used as the variable in the threat rate and ship level formulas.
|
||||
- REQ-WAV-THREAT-RATE: A global **threat level** accumulates continuously over real game time. The rate of increase per second is determined by `world.toml [waves].threat_rate_formula` where `x` is the boss wave counter (REQ-WAV-BOSS-COUNTER), clamped to a minimum of 0 (negative formula values are treated as 0). The rate is constant within each boss wave cycle and steps up each time `x` increments. Threat accumulation is paused during quiet windows (REQ-WAV-QUIET). Example: `1*x - 30` yields 0 threat/s when x ≤ 30 and increases linearly beyond that.
|
||||
- REQ-WAV-GAP: At game start and immediately after each normal wave is triggered, a random inter-wave gap is drawn uniformly from [`world.toml [waves].gap_min_seconds`, `gap_max_seconds`]. The gap timer does not advance while inside a quiet window (REQ-WAV-QUIET); if a gap would expire inside a quiet window, its expiry is deferred until the quiet window ends.
|
||||
- REQ-WAV-TRIGGER: When the gap timer expires outside a quiet window, a normal wave is triggered. Ships are selected one at a time: from all schematics whose threat cost (REQ-MOD-THREAT) is > 0, uniformly randomly pick one whose cost fits the remaining threat budget. For wave ship selection, the threat cost is computed using the schematic's `default_modules` layout (REQ-WAV-DEFAULT-MODULES). Repeat until no eligible schematic fits. Any remaining threat carries over to the next normal wave. A longer gap results in a larger wave.
|
||||
- REQ-WAV-SHIP-LEVEL: Each wave's (normal and boss) enemy ships are assigned a level determined by `world.toml [waves].ship_level_formula` where `x` is the boss wave counter (REQ-WAV-BOSS-COUNTER). Per-ship stats are computed from the ship level via the formulas in `ships.toml` (see REQ-SHP-STATS). Threat cost is level-independent (REQ-MOD-THREAT).
|
||||
- REQ-WAV-BOSS-COUNTDOWN: A **boss countdown** timer starts at `world.toml [waves].boss_countdown_seconds` (default 300) at game start and counts down continuously in real game-time seconds. It is not paused during quiet windows. When it reaches 0, a boss wave is triggered (REQ-WAV-BOSS-TRIGGER). Immediately after the boss wave fires, `x` increments (REQ-WAV-BOSS-COUNTER) and a fresh countdown starts at the same configured value.
|
||||
- REQ-WAV-BOSS-ADVANCE: When the player destroys a set of enemy defence stations, the boss countdown is reduced by `world.toml [push].boss_advance_seconds` (default 60), clamped to a minimum of 0. Threat that would have accumulated during the skipped time is not added. If the countdown reaches 0 by this reduction, the boss wave is triggered immediately.
|
||||
- REQ-WAV-QUIET: A **quiet window** suppresses normal wave spawning around each boss wave. The pre-boss quiet window begins when the boss countdown falls to or below `world.toml [waves].boss_quiet_before_seconds` and ends when the countdown reaches 0. The post-boss quiet window begins immediately when the boss wave fires and lasts `world.toml [waves].boss_quiet_after_seconds` seconds. Threat accumulation is paused during both windows. The normal wave gap timer does not advance during either window (REQ-WAV-GAP). The new boss countdown runs during the post-boss quiet window.
|
||||
- REQ-WAV-BOSS-TRIGGER: When the boss countdown reaches 0, a boss wave is triggered. Its threat budget is the sum of: (a) `world.toml [waves].boss_threat_duration_seconds` (default 60) multiplied by the current threat rate, and (b) all unspent threat carried over from normal waves. Ships are selected using the same random process as normal waves (REQ-WAV-TRIGGER). Any threat remaining unspent after ship selection carries over to the first normal wave of the new cycle.
|
||||
- REQ-WAV-DEFAULT-MODULES: Enemy ships spawned by waves use the `default_modules` list defined per schematic in `ships.toml`. The `default_modules` array uses the same format as layout blueprints (see Layout Blueprint TOML Format). If `default_modules` is absent or empty, the ship spawns with no modules. Invalid module instances (unknown type, position outside the grid, position on a non-buildable cell, or overlapping another module) are silently skipped.
|
||||
- REQ-WAV-SPAWN-DURATION: Ships in a wave are spawned one at a time over `world.toml [waves].spawn_duration_seconds`.
|
||||
|
||||
## Push Scaling
|
||||
## Push Effects
|
||||
|
||||
- REQ-PSH-ACCUMULATION: Each time the player destroys a set of enemy defence stations, `world.toml [push].scaling_factor` is multiplied permanently into the threat level accumulation rate. Scaling factors stack multiplicatively with each other and with the time-based threat formula, causing all subsequent waves to be larger.
|
||||
- REQ-PSH-STATION-STATS: Enemy defence station stats are each defined as formulas in `stations.toml [enemy_station]`: `hp_formula`, `damage_formula`, `range_formula`, `fire_rate_formula`, `scrap_drop_formula`, where x is the station level — an integer starting at 0 for the initial set and incrementing by 1 each time a new set is placed.
|
||||
|
||||
## Asteroid Expansion
|
||||
@@ -261,38 +349,38 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
|
||||
### Layout
|
||||
|
||||
The screen is divided into three vertical sections:
|
||||
The screen is divided into two columns: a main column (75% width) containing the header bar and game world, and a side panel column (25% width) containing the three UI panels stacked vertically:
|
||||
|
||||
```
|
||||
+--------------------------------------------------+
|
||||
| Header Bar |
|
||||
+--------------------------------------------------+
|
||||
| |
|
||||
| Game World (70%) |
|
||||
| |
|
||||
+-----------------+-----------------+--------------+
|
||||
| Selected | Build Button | Blueprint |
|
||||
| Building Panel | Grid | Panel |
|
||||
| (left) | (center) | (right) |
|
||||
+-----------------+-----------------+--------------+
|
||||
+--------------------------------------+--------------+
|
||||
| Header Bar | |
|
||||
+--------------------------------------+ Selected |
|
||||
| | Building |
|
||||
| | Panel |
|
||||
| +--------------+
|
||||
| Game World | Build |
|
||||
| | Button |
|
||||
| | Grid |
|
||||
| +--------------+
|
||||
| | Blueprint |
|
||||
| | Panel |
|
||||
+--------------------------------------+--------------+
|
||||
(75% width) (25% width)
|
||||
```
|
||||
|
||||
- REQ-UI-HEADER: The header bar spans the full width above the game world and always shows the elapsed survival time and the current global building blocks stock on the left, 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 and the current global building blocks stock on the left, the boss wave counter and boss countdown (REQ-UI-BOSS-STATUS) to the left of the speed buttons, and game speed controls on the right.
|
||||
- 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-WORLD-HEIGHT: The game world view occupies 70% of the remaining screen height below the header bar.
|
||||
- REQ-UI-PANEL-HEIGHT: The UI panel occupies the remaining 30% of the screen height, split horizontally into a selected building panel (left), a build button grid (center), and a blueprint panel (right).
|
||||
- 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).
|
||||
|
||||
### 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-CONSTRUCTION-PROGRESS: Construction sites display the building's glyph centered on the footprint (same as an operational building). Below the glyph — or centered on the footprint if the building has no glyph — a construction progress percentage is shown (integer, e.g. `42%`), increasing from 0% to 100% as construction completes.
|
||||
- REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port.
|
||||
- REQ-UI-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-SCHEMATIC-TOAST: When a schematic is unlocked or leveled up (REQ-DEF-SCHEMATIC-DROP), a transient notification toast appears in the top-right corner of the game world view for 4 seconds and then fades out. `<Ship Name>` in the text below is the schematic's `ships.toml [ship.schematic].display_name`. Toast text:
|
||||
- **New unlock**: `Schematic unlocked: <Ship Name>`
|
||||
- **Level-up (duplicate drop)**: `<Ship Name> production level → N` (where N is the new level).
|
||||
|
||||
If multiple toasts arrive in close succession, they stack vertically in a queue (most recent at the top) and each fades out independently after its own 4-second lifetime.
|
||||
- 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.
|
||||
- **W** — increases game speed by one step in the sequence 0×, 0.5×, 1×, 2×, 4× (no wrap-around past 4×).
|
||||
@@ -304,7 +392,14 @@ The screen is divided into three vertical sections:
|
||||
|
||||
### Debug Draw
|
||||
|
||||
- REQ-UI-DEBUG-DRAW: A debug draw mode can be toggled on and off with the **M** key (REQ-UI-HOTKEYS). It is inactive by default. While active, the sensor range of every ship — both player and enemy — is drawn as a circle centered on the ship, using that ship role's outline color from `visuals.toml`.
|
||||
- REQ-UI-DEBUG-DRAW: A debug draw mode can be toggled on and off with the **M** key (REQ-UI-HOTKEYS). It is inactive by default. While active, the sensor range of every ship — both player and enemy — is drawn as a circle centered on the ship, using that ship schematic's outline color from `visuals.toml`.
|
||||
|
||||
- REQ-UI-DEBUG-OVERLAY: While debug draw mode is active (REQ-UI-DEBUG-DRAW), a text overlay is drawn in the upper left corner of the game world view. The overlay has a semi-transparent black background sized to fit its content. It displays the following lines of text:
|
||||
- `Accumulated Threat Level: <level>` — where `<level>` is the current accumulated threat level (see REQ-WAV-THREAT-RATE).
|
||||
- `Time until Wave: <time_s>` — where `<time_s>` is the remaining time in seconds on the normal-wave inter-wave gap timer (see REQ-WAV-GAP). During a quiet window the gap timer is frozen; the displayed value reflects that frozen state.
|
||||
- `Threat Accumulation Rate: <rate> threat/s` — the rate at which the accumulated threat level is currently increasing (see REQ-WAV-THREAT-RATE). During a quiet window (REQ-WAV-QUIET), this is 0, reflecting that accumulation is currently paused.
|
||||
- `Max Factory Production: <rate> threat/s` — the threat-equivalent of the factory's total possible production: 1 threat/second for each completed (operational, not under construction) miner, smelter, assembler, reprocessing plant, and shipyard. One second of production equals one threat (see REQ-MOD-THREAT).
|
||||
- `Current Factory Production: <rate> threat/s` — the threat-equivalent of the factory's current production: 1 threat/second for each completed miner, smelter, assembler, reprocessing plant, or shipyard that currently has an active production cycle (see REQ-MAT-CYCLE; for shipyards, an in-progress production cycle per REQ-BLD-SHIPYARD).
|
||||
|
||||
### Escape Menu
|
||||
|
||||
@@ -323,6 +418,9 @@ The screen is divided into three vertical sections:
|
||||
- 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-CONFIG-INLINE: Recipe, schematic, ship stance, and target priority configuration for a selected building is shown and changed inline within this panel. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic dropdown (REQ-MOD-UI-PREVIEW).
|
||||
- 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-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 at the ship's actual level with 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.
|
||||
|
||||
### Build Button Grid
|
||||
|
||||
@@ -343,7 +441,7 @@ The screen is divided into three vertical sections:
|
||||
|
||||
- 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). Pressing Q/E 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. 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: 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. 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.
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "MovementIntentSystem.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "ScrapSystem.h"
|
||||
@@ -46,6 +47,7 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
||||
[this]() { return allocateBuildingId(); },
|
||||
[](int) {},
|
||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||
[](const std::string&) -> bool { return true; },
|
||||
m_rng);
|
||||
|
||||
m_shipSystem = std::make_unique<ShipSystem>(m_gameConfig, m_admin);
|
||||
@@ -72,9 +74,9 @@ BuildingId ArenaSimulation::allocateBuildingId()
|
||||
|
||||
void ArenaSimulation::placeStructures()
|
||||
{
|
||||
const int totalWidth = m_arenaConfig.playerBufferWidth
|
||||
+ m_arenaConfig.contestZoneWidth
|
||||
+ m_arenaConfig.enemyBufferWidth;
|
||||
const int totalWidth = m_arenaConfig.playerBufferWidth_tiles
|
||||
+ m_arenaConfig.contestZoneWidth_tiles
|
||||
+ m_arenaConfig.enemyBufferWidth_tiles;
|
||||
const int midY = m_arenaConfig.heightTiles / 2;
|
||||
|
||||
// Team 1 HQ — ECS proxy entity, player faction (isEnemy=false).
|
||||
@@ -122,6 +124,7 @@ void ArenaSimulation::placeStructures()
|
||||
weapon.cooldownTicks = 0.0f;
|
||||
weapon.currentTarget = std::nullopt;
|
||||
const double lv = static_cast<double>(entry.level);
|
||||
const float tileSize = static_cast<float>(m_gameConfig.world.tileSize_m);
|
||||
|
||||
const std::vector<std::string>& mask = isEnemy
|
||||
? m_gameConfig.stations.enemyStation.surfaceMask
|
||||
@@ -133,8 +136,8 @@ void ArenaSimulation::placeStructures()
|
||||
m_gameConfig.stations.playerStation.hpFormula.evaluate(lv));
|
||||
weapon.damage = static_cast<float>(
|
||||
m_gameConfig.stations.playerStation.damageFormula.evaluate(lv));
|
||||
weapon.range = static_cast<float>(
|
||||
m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv));
|
||||
weapon.range_tiles = static_cast<float>(
|
||||
m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv)) / tileSize;
|
||||
weapon.fireRateHz = static_cast<float>(
|
||||
m_gameConfig.stations.playerStation.fireRateFormula.evaluate(lv));
|
||||
}
|
||||
@@ -144,8 +147,8 @@ void ArenaSimulation::placeStructures()
|
||||
m_gameConfig.stations.enemyStation.hpFormula.evaluate(lv));
|
||||
weapon.damage = static_cast<float>(
|
||||
m_gameConfig.stations.enemyStation.damageFormula.evaluate(lv));
|
||||
weapon.range = static_cast<float>(
|
||||
m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv));
|
||||
weapon.range_tiles = static_cast<float>(
|
||||
m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv)) / tileSize;
|
||||
weapon.fireRateHz = static_cast<float>(
|
||||
m_gameConfig.stations.enemyStation.fireRateFormula.evaluate(lv));
|
||||
}
|
||||
@@ -159,7 +162,12 @@ void ArenaSimulation::placeStructures()
|
||||
}
|
||||
const entt::entity stationEntity = m_admin.spawnStation(
|
||||
anchor, parsed.footprint, absCells, hp, hp, isEnemy);
|
||||
m_admin.addComponent<WeaponComponent>(stationEntity, weapon);
|
||||
{
|
||||
entt::entity wChild = m_admin.createModuleEntity();
|
||||
m_admin.addComponent<WeaponComponent>(wChild, weapon);
|
||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||
ModuleOwnerComponent{stationEntity});
|
||||
}
|
||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||
};
|
||||
|
||||
@@ -175,9 +183,9 @@ void ArenaSimulation::placeStructures()
|
||||
|
||||
void ArenaSimulation::spawnShips()
|
||||
{
|
||||
const int contestStart = m_arenaConfig.playerBufferWidth;
|
||||
const int team2Start = contestStart + m_arenaConfig.contestZoneWidth;
|
||||
const int totalWidth = team2Start + m_arenaConfig.enemyBufferWidth;
|
||||
const int contestStart = m_arenaConfig.playerBufferWidth_tiles;
|
||||
const int team2Start = contestStart + m_arenaConfig.contestZoneWidth_tiles;
|
||||
const int totalWidth = team2Start + m_arenaConfig.enemyBufferWidth_tiles;
|
||||
|
||||
std::uniform_real_distribution<float> yDist(0.0f,
|
||||
static_cast<float>(m_arenaConfig.heightTiles));
|
||||
@@ -185,7 +193,7 @@ void ArenaSimulation::spawnShips()
|
||||
// Team 1: isEnemy=false, spawn in player buffer zone.
|
||||
{
|
||||
std::uniform_real_distribution<float> xDist(0.0f,
|
||||
static_cast<float>(m_arenaConfig.playerBufferWidth));
|
||||
static_cast<float>(m_arenaConfig.playerBufferWidth_tiles));
|
||||
|
||||
for (const ArenaShipEntry& entry : m_arenaConfig.teams[0].ships)
|
||||
{
|
||||
@@ -247,12 +255,13 @@ void ArenaSimulation::tick()
|
||||
m_aiSystem->tickHomeReturnBehavior(m_admin);
|
||||
m_aiSystem->tickThreatResponseBehavior(m_admin, *m_buildingSystem);
|
||||
m_aiSystem->tickRepairBehavior(m_admin, *m_buildingSystem);
|
||||
m_aiSystem->tickRepairTools(m_admin);
|
||||
m_aiSystem->tickSalvageBehavior(m_admin, *m_scrapSystem, *m_buildingSystem);
|
||||
|
||||
// Combat resolution (tick step 8).
|
||||
std::vector<FireEvent> fireEvents;
|
||||
m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, fireEvents);
|
||||
m_fireEvents.insert(m_fireEvents.end(), fireEvents.begin(), fireEvents.end());
|
||||
std::vector<WeaponFiredEvent> weaponFiredEvents;
|
||||
m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, weaponFiredEvents);
|
||||
m_weaponFiredEvents.insert(m_weaponFiredEvents.end(), weaponFiredEvents.begin(), weaponFiredEvents.end());
|
||||
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
||||
|
||||
// Deaths (tick step 9, simplified).
|
||||
@@ -320,6 +329,15 @@ void ArenaSimulation::tickDeaths()
|
||||
{
|
||||
const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
|
||||
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
|
||||
{
|
||||
std::vector<entt::entity> stationChildren;
|
||||
m_admin.forEach<ModuleOwnerComponent>(
|
||||
[&](entt::entity ce, const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (o.owner == deadEntity) { stationChildren.push_back(ce); }
|
||||
});
|
||||
for (entt::entity ce : stationChildren) { m_admin.destroy(ce); }
|
||||
}
|
||||
m_admin.destroy(deadEntity);
|
||||
}
|
||||
|
||||
@@ -375,10 +393,10 @@ void ArenaSimulation::tickOnce()
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<FireEvent> ArenaSimulation::drainFireEvents()
|
||||
std::vector<WeaponFiredEvent> ArenaSimulation::drainWeaponFiredEvents()
|
||||
{
|
||||
std::vector<FireEvent> result;
|
||||
result.swap(m_fireEvents);
|
||||
std::vector<WeaponFiredEvent> result;
|
||||
result.swap(m_weaponFiredEvents);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#include "BuildingId.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
#include "FireEvent.h"
|
||||
#include "WeaponFiredEvent.h"
|
||||
#include "GameConfig.h"
|
||||
#include "Tick.h"
|
||||
|
||||
@@ -58,7 +58,7 @@ public:
|
||||
void requestStop();
|
||||
|
||||
void tickOnce();
|
||||
std::vector<FireEvent> drainFireEvents();
|
||||
std::vector<WeaponFiredEvent> drainWeaponFiredEvents();
|
||||
|
||||
ArenaStatus status() const;
|
||||
bool isFinished() const;
|
||||
@@ -104,7 +104,7 @@ private:
|
||||
int m_winnerTeam;
|
||||
std::atomic<bool> m_stopRequested;
|
||||
|
||||
std::vector<FireEvent> m_fireEvents;
|
||||
std::vector<WeaponFiredEvent> m_weaponFiredEvents;
|
||||
|
||||
mutable std::mutex m_statusMutex;
|
||||
ArenaStatus m_status;
|
||||
|
||||
@@ -4,33 +4,27 @@
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPoint>
|
||||
|
||||
#include "ArenaSimulation.h"
|
||||
#include "Building.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "EntityHitTest.h"
|
||||
#include "EntitySelectedEvent.h"
|
||||
#include "EventManager.h"
|
||||
#include "FacingComponent.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "GameSpeedChangedEvent.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "RepairToolComponent.h"
|
||||
#include "SalvageCargoComponent.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "StationBodyComponent.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
ShipRole shipRoleFromComponents(bool isEnemy, bool hasCargo, bool hasRepairTool)
|
||||
{
|
||||
if (isEnemy) { return ShipRole::Enemy; }
|
||||
if (hasCargo) { return ShipRole::Salvage; }
|
||||
if (hasRepairTool) { return ShipRole::Repair; }
|
||||
return ShipRole::PlayerCombat;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -52,6 +46,13 @@ ArenaView::ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals,
|
||||
connect(m_renderTimer, &QTimer::timeout, this, &ArenaView::onFrame);
|
||||
m_renderTimer->start();
|
||||
m_frameTimer.start();
|
||||
|
||||
registerForEvent();
|
||||
}
|
||||
|
||||
ArenaView::~ArenaView()
|
||||
{
|
||||
unregisterForEvent();
|
||||
}
|
||||
|
||||
void ArenaView::setGameSpeed(double multiplier)
|
||||
@@ -61,7 +62,8 @@ void ArenaView::setGameSpeed(double multiplier)
|
||||
m_prevNonZeroSpeed = multiplier;
|
||||
}
|
||||
m_gameSpeedMultiplier = multiplier;
|
||||
emit speedChanged(multiplier);
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<GameSpeedChangedEvent>(multiplier));
|
||||
}
|
||||
|
||||
double ArenaView::gameSpeed() const
|
||||
@@ -100,34 +102,17 @@ void ArenaView::onFrame()
|
||||
}
|
||||
}
|
||||
|
||||
// Emit fire events via EventManager
|
||||
{
|
||||
const std::vector<FireEvent> fires = m_sim->drainFireEvents();
|
||||
for (const FireEvent& fe : fires)
|
||||
const std::vector<WeaponFiredEvent> fires = m_sim->drainWeaponFiredEvents();
|
||||
for (const WeaponFiredEvent& fe : fires)
|
||||
{
|
||||
float maxRadius = 0.125f;
|
||||
if (m_sim->admin().isValid(fe.target)
|
||||
&& m_sim->admin().hasAll<StationBodyComponent>(fe.target))
|
||||
{
|
||||
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(fe.target);
|
||||
const int shorter = std::min(sb.footprint.width(),
|
||||
sb.footprint.height());
|
||||
maxRadius = shorter / 2.0f;
|
||||
}
|
||||
|
||||
std::uniform_real_distribution<float> angleDist(0.0f, 6.28318530f);
|
||||
std::uniform_real_distribution<float> radiusDist(0.0f, maxRadius);
|
||||
const float angle = angleDist(m_rng);
|
||||
const float radius = radiusDist(m_rng);
|
||||
|
||||
ActiveBeam beam;
|
||||
beam.event = fe;
|
||||
beam.emittedWallMs = m_wallMs;
|
||||
beam.targetOffset = QVector2D(radius * std::cos(angle),
|
||||
radius * std::sin(angle));
|
||||
m_activeBeams.push_back(beam);
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<WeaponFiredEvent>(fe));
|
||||
}
|
||||
}
|
||||
|
||||
// Expire old beams
|
||||
{
|
||||
std::vector<ActiveBeam> live;
|
||||
for (const ActiveBeam& b : m_activeBeams)
|
||||
@@ -143,12 +128,36 @@ void ArenaView::onFrame()
|
||||
if (m_sim->isFinished() && !m_finishedEmitted)
|
||||
{
|
||||
m_finishedEmitted = true;
|
||||
emit finished();
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void ArenaView::handleEvent(std::shared_ptr<const WeaponFiredEvent> event)
|
||||
{
|
||||
float maxRadius = 0.125f;
|
||||
if (m_sim->admin().isValid(event->target)
|
||||
&& m_sim->admin().hasAll<StationBodyComponent>(event->target))
|
||||
{
|
||||
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(event->target);
|
||||
const int shorter = std::min(sb.footprint.width(),
|
||||
sb.footprint.height());
|
||||
maxRadius = shorter / 2.0f;
|
||||
}
|
||||
|
||||
std::uniform_real_distribution<float> angleDist(0.0f, 6.28318530f);
|
||||
std::uniform_real_distribution<float> radiusDist(0.0f, maxRadius);
|
||||
const float angle = angleDist(m_rng);
|
||||
const float radius = radiusDist(m_rng);
|
||||
|
||||
ActiveBeam beam;
|
||||
beam.event = *event;
|
||||
beam.emittedWallMs = m_wallMs;
|
||||
beam.targetOffset = QVector2D(radius * std::cos(angle),
|
||||
radius * std::sin(angle));
|
||||
m_activeBeams.push_back(beam);
|
||||
}
|
||||
|
||||
void ArenaView::paintGL()
|
||||
{
|
||||
QPainter painter(this);
|
||||
@@ -169,9 +178,9 @@ void ArenaView::paintGL()
|
||||
float ArenaView::tilePx() const
|
||||
{
|
||||
const ArenaConfig& ac = m_sim->arenaConfig();
|
||||
const int totalWidth = ac.playerBufferWidth
|
||||
+ ac.contestZoneWidth
|
||||
+ ac.enemyBufferWidth;
|
||||
const int totalWidth = ac.playerBufferWidth_tiles
|
||||
+ ac.contestZoneWidth_tiles
|
||||
+ ac.enemyBufferWidth_tiles;
|
||||
const int totalHeight = ac.heightTiles;
|
||||
if (totalWidth <= 0 || totalHeight <= 0) { return 1.0f; }
|
||||
|
||||
@@ -209,6 +218,37 @@ std::optional<QVector2D> ArenaView::entityPosition(entt::entity entity) const
|
||||
return m_sim->admin().get<PositionComponent>(entity).value;
|
||||
}
|
||||
|
||||
QVector2D ArenaView::widgetToWorld(QPoint widgetPt) const
|
||||
{
|
||||
const float px = tilePx();
|
||||
if (px < 0.001f) { return QVector2D(0.0f, 0.0f); }
|
||||
return QVector2D(static_cast<float>(widgetPt.x()) / px,
|
||||
static_cast<float>(widgetPt.y()) / px);
|
||||
}
|
||||
|
||||
void ArenaView::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
{
|
||||
const QVector2D worldPos = widgetToWorld(event->pos());
|
||||
entt::entity hit = entityAtWorldPos(m_sim->admin(), worldPos);
|
||||
|
||||
if (hit != entt::null)
|
||||
{
|
||||
m_selectedEntity = hit;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_selectedEntity = std::nullopt;
|
||||
}
|
||||
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<EntitySelectedEvent>(m_selectedEntity));
|
||||
}
|
||||
|
||||
QOpenGLWidget::mousePressEvent(event);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -216,9 +256,9 @@ std::optional<QVector2D> ArenaView::entityPosition(entt::entity entity) const
|
||||
void ArenaView::drawTiles(QPainter& painter)
|
||||
{
|
||||
const ArenaConfig& ac = m_sim->arenaConfig();
|
||||
const int totalWidth = ac.playerBufferWidth
|
||||
+ ac.contestZoneWidth
|
||||
+ ac.enemyBufferWidth;
|
||||
const int totalWidth = ac.playerBufferWidth_tiles
|
||||
+ ac.contestZoneWidth_tiles
|
||||
+ ac.enemyBufferWidth_tiles;
|
||||
const int totalHeight = ac.heightTiles;
|
||||
|
||||
painter.setPen(Qt::NoPen);
|
||||
@@ -279,7 +319,7 @@ void ArenaView::drawScrap(QPainter& painter)
|
||||
void ArenaView::drawStations(QPainter& painter)
|
||||
{
|
||||
m_sim->admin().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
|
||||
? BuildingType::EnemyDefenceStation
|
||||
@@ -315,22 +355,26 @@ void ArenaView::drawStations(QPainter& painter)
|
||||
painter.fillRect(QRectF(bboxRect.left(), barY, barW * static_cast<qreal>(fraction), barH),
|
||||
f.isEnemy ? QColor(200, 60, 60) : QColor(60, 200, 60));
|
||||
}
|
||||
|
||||
if (m_selectedEntity.has_value() && *m_selectedEntity == e)
|
||||
{
|
||||
painter.setPen(QPen(QColor(255, 255, 0), 2));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(bboxRect.adjusted(-2, -2, 2, 2));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ArenaView::drawShips(QPainter& painter)
|
||||
{
|
||||
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
|
||||
FactionComponent>(
|
||||
[&](entt::entity e, const ShipIdentityComponent& /*si*/,
|
||||
FactionComponent, HealthComponent>(
|
||||
[&](entt::entity e, const ShipIdentityComponent& si,
|
||||
const PositionComponent& pos, const FacingComponent& facing,
|
||||
const FactionComponent& fac)
|
||||
const FactionComponent& fac, const HealthComponent& h)
|
||||
{
|
||||
const bool hasCargo = m_sim->admin().hasAll<SalvageCargoComponent>(e);
|
||||
const bool hasRepair = m_sim->admin().hasAll<RepairToolComponent>(e);
|
||||
const ShipRole role = shipRoleFromComponents(fac.isEnemy, hasCargo, hasRepair);
|
||||
const std::map<ShipRole, ShipVisuals>::const_iterator it =
|
||||
m_visuals->ships.find(role);
|
||||
const std::map<std::string, ShipVisuals>::const_iterator it =
|
||||
m_visuals->ships.find(si.schematicId);
|
||||
if (it == m_visuals->ships.end()) { return; }
|
||||
|
||||
const QPointF center = worldToWidget(pos.value);
|
||||
@@ -351,6 +395,26 @@ void ArenaView::drawShips(QPainter& painter)
|
||||
painter.setPen(QPen(it->second.outline, 1));
|
||||
painter.setBrush(it->second.fill);
|
||||
painter.drawPolygon(tri);
|
||||
|
||||
if (h.maxHp > 0.0f)
|
||||
{
|
||||
const float fraction = std::max(0.0f, h.hp / h.maxHp);
|
||||
const qreal barW = static_cast<qreal>(fwd) * 2.0;
|
||||
const qreal barH = static_cast<qreal>(tilePx()) * 0.12;
|
||||
const qreal barX = center.x() - static_cast<qreal>(fwd);
|
||||
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 * static_cast<qreal>(fraction), barH),
|
||||
fac.isEnemy ? QColor(200, 60, 60) : QColor(60, 200, 60));
|
||||
}
|
||||
|
||||
if (m_selectedEntity.has_value() && *m_selectedEntity == e)
|
||||
{
|
||||
const qreal radius = static_cast<qreal>(tilePx()) * 0.55;
|
||||
painter.setPen(QPen(QColor(255, 255, 0), 2));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawEllipse(center, radius, radius);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -366,4 +430,3 @@ void ArenaView::drawBeams(QPainter& painter)
|
||||
worldToWidget(*targetPos + beam.targetOffset));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
@@ -8,9 +9,11 @@
|
||||
#include <QTimer>
|
||||
#include <QVector2D>
|
||||
|
||||
#include "FireEvent.h"
|
||||
#include "EventHandler.h"
|
||||
#include "WeaponFiredEvent.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
#include "EntitySelectedEvent.h"
|
||||
#include "Tick.h"
|
||||
#include "TickDriver.h"
|
||||
#include "VisualsConfig.h"
|
||||
@@ -18,30 +21,31 @@
|
||||
class ArenaSimulation;
|
||||
class QPainter;
|
||||
|
||||
class ArenaView : public QOpenGLWidget
|
||||
class ArenaView : public QOpenGLWidget,
|
||||
public EventHandler<WeaponFiredEvent>
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals,
|
||||
QWidget* parent = nullptr);
|
||||
~ArenaView() override;
|
||||
|
||||
void setGameSpeed(double multiplier);
|
||||
double gameSpeed() const;
|
||||
void togglePause();
|
||||
void stopRendering();
|
||||
|
||||
signals:
|
||||
void speedChanged(double multiplier);
|
||||
void finished();
|
||||
|
||||
protected:
|
||||
void paintGL() override;
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
|
||||
private slots:
|
||||
void onFrame();
|
||||
|
||||
private:
|
||||
void handleEvent(std::shared_ptr<const WeaponFiredEvent> event) override;
|
||||
|
||||
void drawTiles(QPainter& painter);
|
||||
void drawBuildings(QPainter& painter);
|
||||
void drawStations(QPainter& painter);
|
||||
@@ -55,10 +59,11 @@ private:
|
||||
QRectF tileRect(QPoint tile) const;
|
||||
|
||||
std::optional<QVector2D> entityPosition(entt::entity entity) const;
|
||||
QVector2D widgetToWorld(QPoint widgetPt) const;
|
||||
|
||||
struct ActiveBeam
|
||||
{
|
||||
FireEvent event;
|
||||
WeaponFiredEvent event;
|
||||
qint64 emittedWallMs;
|
||||
QVector2D targetOffset;
|
||||
};
|
||||
@@ -79,4 +84,6 @@ private:
|
||||
|
||||
std::vector<ActiveBeam> m_activeBeams;
|
||||
bool m_finishedEmitted;
|
||||
|
||||
std::optional<entt::entity> m_selectedEntity;
|
||||
};
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
ArenaWidget::ArenaWidget(const std::string& arenaName, QWidget* parent)
|
||||
#include "ArenaInspectRequestedEvent.h"
|
||||
#include "ArenaStartRequestedEvent.h"
|
||||
#include "EventManager.h"
|
||||
|
||||
ArenaWidget::ArenaWidget(int arenaIndex, const std::string& arenaName, QWidget* parent)
|
||||
: QFrame(parent)
|
||||
, m_arenaIndex(arenaIndex)
|
||||
, m_running(false)
|
||||
, m_wasFinished(false)
|
||||
{
|
||||
@@ -30,12 +35,18 @@ void ArenaWidget::buildLayout(const std::string& arenaName)
|
||||
|
||||
titleRow->addStretch();
|
||||
|
||||
m_inspectButton = new QPushButton("Inspect", this);
|
||||
connect(m_inspectButton, &QPushButton::clicked, this, &ArenaWidget::inspectRequested);
|
||||
m_inspectButton = new QPushButton(tr("Inspect"), this);
|
||||
connect(m_inspectButton, &QPushButton::clicked, this, [this]() {
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ArenaInspectRequestedEvent>(m_arenaIndex));
|
||||
});
|
||||
titleRow->addWidget(m_inspectButton);
|
||||
|
||||
m_startButton = new QPushButton("Start", this);
|
||||
connect(m_startButton, &QPushButton::clicked, this, &ArenaWidget::startRequested);
|
||||
m_startButton = new QPushButton(tr("Start"), this);
|
||||
connect(m_startButton, &QPushButton::clicked, this, [this]() {
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ArenaStartRequestedEvent>(m_arenaIndex));
|
||||
});
|
||||
titleRow->addWidget(m_startButton);
|
||||
|
||||
outerLayout->addLayout(titleRow);
|
||||
@@ -94,7 +105,7 @@ void ArenaWidget::updateStatus(const ArenaStatus& status)
|
||||
|
||||
if (status.finished && status.winnerTeam == ti)
|
||||
{
|
||||
header->setText("[WON] " + QString::fromStdString(team.name));
|
||||
header->setText(tr("[WON] %1").arg(QString::fromStdString(team.name)));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -14,19 +14,16 @@ class ArenaWidget : public QFrame
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ArenaWidget(const std::string& arenaName, QWidget* parent = nullptr);
|
||||
ArenaWidget(int arenaIndex, const std::string& arenaName, QWidget* parent = nullptr);
|
||||
|
||||
void updateStatus(const ArenaStatus& status);
|
||||
void startSimulation();
|
||||
void resetToGrey();
|
||||
|
||||
signals:
|
||||
void startRequested();
|
||||
void inspectRequested();
|
||||
|
||||
private:
|
||||
void buildLayout(const std::string& arenaName);
|
||||
|
||||
int m_arenaIndex;
|
||||
QLabel* m_titleLabel;
|
||||
QLabel* m_team1Header;
|
||||
QLabel* m_team2Header;
|
||||
|
||||
@@ -81,12 +81,12 @@ BalancingConfig loadBalancingConfig(const std::string& path)
|
||||
arena.name = requireString((*arenaTbl)["name"], prefix + ".name");
|
||||
arena.heightTiles = static_cast<int>(
|
||||
requireInt((*arenaTbl)["height_tiles"], prefix + ".height_tiles"));
|
||||
arena.playerBufferWidth = static_cast<int>(
|
||||
requireInt((*arenaTbl)["player_buffer_width"], prefix + ".player_buffer_width"));
|
||||
arena.contestZoneWidth = static_cast<int>(
|
||||
requireInt((*arenaTbl)["contest_zone_width"], prefix + ".contest_zone_width"));
|
||||
arena.enemyBufferWidth = static_cast<int>(
|
||||
requireInt((*arenaTbl)["enemy_buffer_width"], prefix + ".enemy_buffer_width"));
|
||||
arena.playerBufferWidth_tiles = static_cast<int>(
|
||||
requireInt((*arenaTbl)["player_buffer_width_tiles"], prefix + ".player_buffer_width_tiles"));
|
||||
arena.contestZoneWidth_tiles = static_cast<int>(
|
||||
requireInt((*arenaTbl)["contest_zone_width_tiles"], prefix + ".contest_zone_width_tiles"));
|
||||
arena.enemyBufferWidth_tiles = static_cast<int>(
|
||||
requireInt((*arenaTbl)["enemy_buffer_width_tiles"], prefix + ".enemy_buffer_width_tiles"));
|
||||
|
||||
const toml::array* teamArray = (*arenaTbl)["team"].as_array();
|
||||
if (!teamArray || teamArray->size() != 2)
|
||||
|
||||
@@ -34,9 +34,9 @@ struct ArenaConfig
|
||||
{
|
||||
std::string name;
|
||||
int heightTiles;
|
||||
int playerBufferWidth;
|
||||
int contestZoneWidth;
|
||||
int enemyBufferWidth;
|
||||
int playerBufferWidth_tiles;
|
||||
int contestZoneWidth_tiles;
|
||||
int enemyBufferWidth_tiles;
|
||||
ArenaTeamConfig teams[2];
|
||||
};
|
||||
|
||||
|
||||
@@ -22,15 +22,15 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
||||
, m_inspectedArenaIndex(-1)
|
||||
{
|
||||
m_visuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||
setWindowTitle("DotaFactory — Balancing Tool");
|
||||
setWindowTitle(tr("DotaFactory — Balancing Tool"));
|
||||
resize(800, 600);
|
||||
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
QHBoxLayout* buttonRow = new QHBoxLayout();
|
||||
m_reloadButton = new QPushButton("Reload Config", this);
|
||||
m_startAllButton = new QPushButton("Start All", this);
|
||||
m_reloadButton = new QPushButton(tr("Reload Config"), this);
|
||||
m_startAllButton = new QPushButton(tr("Start All"), this);
|
||||
buttonRow->addWidget(m_reloadButton);
|
||||
buttonRow->addWidget(m_startAllButton);
|
||||
buttonRow->addStretch();
|
||||
@@ -48,14 +48,17 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
||||
m_pollTimer = new QTimer(this);
|
||||
connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses);
|
||||
m_pollTimer->start(100);
|
||||
|
||||
registerForEvents();
|
||||
}
|
||||
|
||||
BalancingWindow::~BalancingWindow()
|
||||
{
|
||||
unregisterForEvents();
|
||||
|
||||
m_pollTimer->stop();
|
||||
if (m_inspectWindow)
|
||||
{
|
||||
m_inspectWindow->disconnect(this);
|
||||
delete m_inspectWindow;
|
||||
m_inspectWindow = nullptr;
|
||||
}
|
||||
@@ -81,16 +84,11 @@ void BalancingWindow::populateArenas(const BalancingConfig& balancingConfig)
|
||||
entry.config = arenaConfig;
|
||||
entry.simulation = std::make_unique<ArenaSimulation>(
|
||||
m_gameConfig, arenaConfig, m_nextSeed++);
|
||||
entry.widget = new ArenaWidget(arenaConfig.name, scrollContent);
|
||||
entry.widget = new ArenaWidget(index, arenaConfig.name, scrollContent);
|
||||
contentLayout->addWidget(entry.widget);
|
||||
|
||||
entry.widget->updateStatus(entry.simulation->status());
|
||||
|
||||
connect(entry.widget, &ArenaWidget::startRequested,
|
||||
this, [this, index]() { startArena(index); });
|
||||
connect(entry.widget, &ArenaWidget::inspectRequested,
|
||||
this, [this, index]() { inspectArena(index); });
|
||||
|
||||
m_arenas.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
@@ -146,7 +144,7 @@ void BalancingWindow::reloadConfig()
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
QMessageBox::critical(this, "Reload Failed", QString::fromStdString(e.what()));
|
||||
QMessageBox::critical(this, tr("Reload Failed"), QString::fromStdString(e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +156,21 @@ void BalancingWindow::startAll()
|
||||
}
|
||||
}
|
||||
|
||||
void BalancingWindow::handleEvent(std::shared_ptr<const ArenaStartRequestedEvent> event)
|
||||
{
|
||||
startArena(event->arenaIndex);
|
||||
}
|
||||
|
||||
void BalancingWindow::handleEvent(std::shared_ptr<const ArenaInspectRequestedEvent> event)
|
||||
{
|
||||
inspectArena(event->arenaIndex);
|
||||
}
|
||||
|
||||
void BalancingWindow::handleEvent(std::shared_ptr<const InspectWindowClosedEvent> /*event*/)
|
||||
{
|
||||
closeInspectWindow();
|
||||
}
|
||||
|
||||
void BalancingWindow::startArena(int index)
|
||||
{
|
||||
ArenaEntry& entry = m_arenas[index];
|
||||
@@ -179,7 +192,6 @@ void BalancingWindow::inspectArena(int index)
|
||||
{
|
||||
if (m_inspectWindow)
|
||||
{
|
||||
m_inspectWindow->disconnect(this);
|
||||
delete m_inspectWindow;
|
||||
m_inspectWindow = nullptr;
|
||||
|
||||
@@ -209,9 +221,7 @@ void BalancingWindow::inspectArena(int index)
|
||||
entry.widget->updateStatus(m_inspectedSim->status());
|
||||
|
||||
m_inspectWindow = new InspectWindow(
|
||||
m_inspectedSim.get(), &m_visuals, entry.config.name, nullptr);
|
||||
connect(m_inspectWindow, &InspectWindow::closed,
|
||||
this, &BalancingWindow::closeInspectWindow);
|
||||
m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr);
|
||||
|
||||
setMainControlsEnabled(false);
|
||||
m_inspectWindow->show();
|
||||
@@ -224,7 +234,6 @@ void BalancingWindow::closeInspectWindow()
|
||||
return;
|
||||
}
|
||||
|
||||
m_inspectWindow->disconnect(this);
|
||||
m_inspectWindow->deleteLater();
|
||||
m_inspectWindow = nullptr;
|
||||
|
||||
|
||||
@@ -10,15 +10,22 @@
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
#include "ArenaInspectRequestedEvent.h"
|
||||
#include "ArenaStartRequestedEvent.h"
|
||||
#include "ArenaWidget.h"
|
||||
#include "ArenaSimulation.h"
|
||||
#include "BalancingConfig.h"
|
||||
#include "EventHandler.h"
|
||||
#include "GameConfig.h"
|
||||
#include "InspectWindowClosedEvent.h"
|
||||
#include "VisualsConfig.h"
|
||||
|
||||
class InspectWindow;
|
||||
|
||||
class BalancingWindow : public QWidget
|
||||
class BalancingWindow : public QWidget,
|
||||
public CombinedEventHandler<ArenaStartRequestedEvent,
|
||||
ArenaInspectRequestedEvent,
|
||||
InspectWindowClosedEvent>
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -30,15 +37,20 @@ public:
|
||||
QWidget* parent = nullptr);
|
||||
~BalancingWindow() override;
|
||||
|
||||
private:
|
||||
void handleEvent(std::shared_ptr<const ArenaStartRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const ArenaInspectRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const InspectWindowClosedEvent> event) override;
|
||||
|
||||
private slots:
|
||||
void pollStatuses();
|
||||
void reloadConfig();
|
||||
void startAll();
|
||||
|
||||
private:
|
||||
void startArena(int index);
|
||||
void inspectArena(int index);
|
||||
void closeInspectWindow();
|
||||
|
||||
private:
|
||||
void populateArenas(const BalancingConfig& balancingConfig);
|
||||
void stopAllArenas();
|
||||
void updateButtons();
|
||||
|
||||
@@ -6,6 +6,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/ShipStatsPanel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.h
|
||||
PARENT_SCOPE
|
||||
@@ -20,6 +21,7 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/ShipStatsPanel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -9,16 +9,28 @@
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "ArenaView.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "EventManager.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "InspectWindowClosedEvent.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "ShipStatsCalculator.h"
|
||||
#include "ShipStatsPanel.h"
|
||||
#include "StationBodyComponent.h"
|
||||
#include "WeaponComponent.h"
|
||||
|
||||
const double InspectWindow::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 4.0 };
|
||||
const double InspectWindow::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
||||
const int InspectWindow::kSpeedCount = 5;
|
||||
|
||||
InspectWindow::InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
|
||||
InspectWindow::InspectWindow(ArenaSimulation* sim, const GameConfig* config,
|
||||
const VisualsConfig* visuals,
|
||||
const std::string& arenaName, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_sim(sim)
|
||||
, m_config(config)
|
||||
{
|
||||
setWindowTitle(QString("Inspect \u2014 %1").arg(QString::fromStdString(arenaName)));
|
||||
setWindowTitle(tr("Inspect \u2014 %1").arg(QString::fromStdString(arenaName)));
|
||||
resize(900, 700);
|
||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||
|
||||
@@ -42,7 +54,7 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
|
||||
|
||||
headerLayout->addStretch();
|
||||
|
||||
const char* labels[] = { "0x", "0.5x", "1x", "2x", "4x" };
|
||||
const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" };
|
||||
QSignalMapper* mapper = new QSignalMapper(this);
|
||||
for (int i = 0; i < kSpeedCount; ++i)
|
||||
{
|
||||
@@ -66,9 +78,6 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
|
||||
m_arenaView = new ArenaView(sim, visuals, this);
|
||||
mainLayout->addWidget(m_arenaView, 1);
|
||||
|
||||
connect(m_arenaView, &ArenaView::speedChanged,
|
||||
this, &InspectWindow::onSpeedChanged);
|
||||
|
||||
// Info panel (bottom)
|
||||
{
|
||||
QWidget* infoPanel = new QWidget(this);
|
||||
@@ -96,6 +105,27 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
|
||||
team2Layout->addStretch();
|
||||
infoLayout->addLayout(team2Layout);
|
||||
|
||||
// Entity stats section (right side of info panel)
|
||||
QVBoxLayout* entityLayout = new QVBoxLayout();
|
||||
m_entityTitleLabel = new QLabel(infoPanel);
|
||||
QFont entityTitleFont = m_entityTitleLabel->font();
|
||||
entityTitleFont.setBold(true);
|
||||
m_entityTitleLabel->setFont(entityTitleFont);
|
||||
m_entityTitleLabel->hide();
|
||||
entityLayout->addWidget(m_entityTitleLabel);
|
||||
|
||||
m_entityStatsPanel = new ShipStatsPanel(config, infoPanel);
|
||||
m_entityStatsPanel->hide();
|
||||
entityLayout->addWidget(m_entityStatsPanel);
|
||||
|
||||
m_stationStatsLabel = new QLabel(infoPanel);
|
||||
m_stationStatsLabel->setWordWrap(true);
|
||||
m_stationStatsLabel->hide();
|
||||
entityLayout->addWidget(m_stationStatsLabel);
|
||||
|
||||
entityLayout->addStretch();
|
||||
infoLayout->addLayout(entityLayout);
|
||||
|
||||
mainLayout->addWidget(infoPanel);
|
||||
}
|
||||
|
||||
@@ -108,13 +138,21 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
|
||||
pollStatus();
|
||||
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
|
||||
registerForEvents();
|
||||
}
|
||||
|
||||
InspectWindow::~InspectWindow()
|
||||
{
|
||||
unregisterForEvents();
|
||||
}
|
||||
|
||||
void InspectWindow::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
m_arenaView->stopRendering();
|
||||
m_pollTimer->stop();
|
||||
emit closed();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<InspectWindowClosedEvent>());
|
||||
event->accept();
|
||||
}
|
||||
|
||||
@@ -138,11 +176,11 @@ void InspectWindow::onSpeedButton(int index)
|
||||
}
|
||||
}
|
||||
|
||||
void InspectWindow::onSpeedChanged(double multiplier)
|
||||
void InspectWindow::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event)
|
||||
{
|
||||
for (int i = 0; i < kSpeedCount; ++i)
|
||||
{
|
||||
const bool active = (std::abs(kSpeeds[i] - multiplier) < 0.001);
|
||||
const bool active = (std::abs(kSpeeds[i] - event->speed) < 0.001);
|
||||
m_speedButtons[static_cast<std::size_t>(i)]->setChecked(active);
|
||||
}
|
||||
}
|
||||
@@ -151,6 +189,7 @@ void InspectWindow::pollStatus()
|
||||
{
|
||||
const ArenaStatus status = m_sim->status();
|
||||
updateInfoPanel(status);
|
||||
refreshEntityStats();
|
||||
}
|
||||
|
||||
void InspectWindow::updateInfoPanel(const ArenaStatus& status)
|
||||
@@ -163,7 +202,7 @@ void InspectWindow::updateInfoPanel(const ArenaStatus& status)
|
||||
|
||||
if (status.finished && status.winnerTeam == ti)
|
||||
{
|
||||
header->setText("[WON] " + QString::fromStdString(team.name));
|
||||
header->setText(tr("[WON] %1").arg(QString::fromStdString(team.name)));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -186,3 +225,140 @@ void InspectWindow::updateInfoPanel(const ArenaStatus& status)
|
||||
content->setText(lines);
|
||||
}
|
||||
}
|
||||
|
||||
void InspectWindow::handleEvent(std::shared_ptr<const EntitySelectedEvent> event)
|
||||
{
|
||||
if (event->entity.has_value())
|
||||
{
|
||||
m_selectedEntity = event->entity;
|
||||
|
||||
EntityAdmin& admin = m_sim->admin();
|
||||
entt::entity entity = *m_selectedEntity;
|
||||
|
||||
if (!admin.isValid(entity))
|
||||
{
|
||||
m_selectedEntity = std::nullopt;
|
||||
m_entityTitleLabel->hide();
|
||||
m_entityStatsPanel->hide();
|
||||
m_stationStatsLabel->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (admin.hasAll<ShipIdentityComponent>(entity))
|
||||
{
|
||||
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity);
|
||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
||||
|
||||
m_entityTitleLabel->setText(tr("Ship: %1 (Lv %2)")
|
||||
.arg(QString::fromStdString(identity.schematicId))
|
||||
.arg(identity.level));
|
||||
m_entityTitleLabel->show();
|
||||
|
||||
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
||||
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
||||
m_entityStatsPanel->show();
|
||||
m_stationStatsLabel->hide();
|
||||
}
|
||||
else if (admin.hasAll<StationBodyComponent>(entity))
|
||||
{
|
||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
||||
|
||||
m_entityTitleLabel->setText(tr("Defence Station"));
|
||||
m_entityTitleLabel->show();
|
||||
|
||||
float totalDps = 0.0f;
|
||||
float maxRange = 0.0f;
|
||||
bool hasWeapons = false;
|
||||
|
||||
admin.forEach<ModuleOwnerComponent, WeaponComponent>(
|
||||
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const WeaponComponent& w)
|
||||
{
|
||||
if (owner.owner != entity) { return; }
|
||||
hasWeapons = true;
|
||||
totalDps += w.damage * w.fireRateHz;
|
||||
if (w.range_tiles > maxRange) { maxRange = w.range_tiles; }
|
||||
});
|
||||
|
||||
QString statsText = tr("HP: %1 / %2")
|
||||
.arg(static_cast<int>(health.hp + 0.5f))
|
||||
.arg(static_cast<int>(health.maxHp + 0.5f));
|
||||
|
||||
if (hasWeapons)
|
||||
{
|
||||
statsText += tr("\nDPS: %1").arg(QString::number(static_cast<double>(totalDps), 'f', 1));
|
||||
statsText += tr("\nRange: %1 tiles").arg(QString::number(static_cast<double>(maxRange), 'f', 1));
|
||||
}
|
||||
|
||||
m_stationStatsLabel->setText(statsText);
|
||||
m_stationStatsLabel->show();
|
||||
m_entityStatsPanel->hide();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_selectedEntity = std::nullopt;
|
||||
m_entityTitleLabel->hide();
|
||||
m_entityStatsPanel->hide();
|
||||
m_stationStatsLabel->hide();
|
||||
}
|
||||
}
|
||||
|
||||
void InspectWindow::refreshEntityStats()
|
||||
{
|
||||
if (!m_selectedEntity.has_value()) { return; }
|
||||
|
||||
EntityAdmin& admin = m_sim->admin();
|
||||
entt::entity entity = *m_selectedEntity;
|
||||
|
||||
if (!admin.isValid(entity))
|
||||
{
|
||||
m_selectedEntity = std::nullopt;
|
||||
m_entityTitleLabel->hide();
|
||||
m_entityStatsPanel->hide();
|
||||
m_stationStatsLabel->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
||||
if (health.hp <= 0.0f)
|
||||
{
|
||||
m_selectedEntity = std::nullopt;
|
||||
m_entityTitleLabel->hide();
|
||||
m_entityStatsPanel->hide();
|
||||
m_stationStatsLabel->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (admin.hasAll<ShipIdentityComponent>(entity))
|
||||
{
|
||||
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
||||
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
||||
}
|
||||
else if (admin.hasAll<StationBodyComponent>(entity))
|
||||
{
|
||||
float totalDps = 0.0f;
|
||||
float maxRange = 0.0f;
|
||||
bool hasWeapons = false;
|
||||
|
||||
admin.forEach<ModuleOwnerComponent, WeaponComponent>(
|
||||
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const WeaponComponent& w)
|
||||
{
|
||||
if (owner.owner != entity) { return; }
|
||||
hasWeapons = true;
|
||||
totalDps += w.damage * w.fireRateHz;
|
||||
if (w.range_tiles > maxRange) { maxRange = w.range_tiles; }
|
||||
});
|
||||
|
||||
QString statsText = tr("HP: %1 / %2")
|
||||
.arg(static_cast<int>(health.hp + 0.5f))
|
||||
.arg(static_cast<int>(health.maxHp + 0.5f));
|
||||
|
||||
if (hasWeapons)
|
||||
{
|
||||
statsText += tr("\nDPS: %1").arg(QString::number(static_cast<double>(totalDps), 'f', 1));
|
||||
statsText += tr("\nRange: %1 tiles").arg(QString::number(static_cast<double>(maxRange), 'f', 1));
|
||||
}
|
||||
|
||||
m_stationStatsLabel->setText(statsText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -8,35 +9,48 @@
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
#include "ArenaSimulation.h"
|
||||
#include "EntitySelectedEvent.h"
|
||||
#include "EventHandler.h"
|
||||
#include "GameConfig.h"
|
||||
#include "GameSpeedChangedEvent.h"
|
||||
#include "VisualsConfig.h"
|
||||
|
||||
class ArenaView;
|
||||
class ShipStatsPanel;
|
||||
|
||||
class InspectWindow : public QWidget
|
||||
class InspectWindow : public QWidget,
|
||||
public CombinedEventHandler<EntitySelectedEvent,
|
||||
GameSpeedChangedEvent>
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
|
||||
InspectWindow(ArenaSimulation* sim, const GameConfig* config,
|
||||
const VisualsConfig* visuals,
|
||||
const std::string& arenaName, QWidget* parent = nullptr);
|
||||
|
||||
signals:
|
||||
void closed();
|
||||
~InspectWindow() override;
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent* event) override;
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
|
||||
private:
|
||||
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event) override;
|
||||
|
||||
private slots:
|
||||
void onSpeedButton(int index);
|
||||
void onSpeedChanged(double multiplier);
|
||||
void pollStatus();
|
||||
|
||||
private:
|
||||
void updateInfoPanel(const ArenaStatus& status);
|
||||
void refreshEntityStats();
|
||||
|
||||
ArenaSimulation* m_sim;
|
||||
const GameConfig* m_config;
|
||||
ArenaView* m_arenaView;
|
||||
|
||||
std::vector<QPushButton*> m_speedButtons;
|
||||
@@ -46,6 +60,11 @@ private:
|
||||
QLabel* m_team2Content;
|
||||
QTimer* m_pollTimer;
|
||||
|
||||
std::optional<entt::entity> m_selectedEntity;
|
||||
QLabel* m_entityTitleLabel;
|
||||
ShipStatsPanel* m_entityStatsPanel;
|
||||
QLabel* m_stationStatsLabel;
|
||||
|
||||
static const double kSpeeds[];
|
||||
static const int kSpeedCount;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
SET(HDRS)
|
||||
SET(SRCS)
|
||||
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(config)
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(ecs)
|
||||
add_subdirectory(eventsystem)
|
||||
add_subdirectory(utility)
|
||||
add_subdirectory(sim)
|
||||
add_subdirectory(ecs)
|
||||
|
||||
SET(HDRS
|
||||
${HDRS}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "BuildingType.h"
|
||||
#include "Rotation.h"
|
||||
#include "ShipLayout.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -50,6 +51,20 @@ std::string serialize(const std::vector<Blueprint>& blueprints)
|
||||
bldTbl.insert("offset_x", static_cast<int64_t>(b.offset.x()));
|
||||
bldTbl.insert("offset_y", static_cast<int64_t>(b.offset.y()));
|
||||
bldTbl.insert("recipe_id", b.recipeId);
|
||||
if (b.shipLayout.has_value())
|
||||
{
|
||||
toml::array modArr;
|
||||
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||
{
|
||||
toml::table modTbl;
|
||||
modTbl.insert("type", pm.moduleId);
|
||||
modTbl.insert("x", static_cast<int64_t>(pm.position.x()));
|
||||
modTbl.insert("y", static_cast<int64_t>(pm.position.y()));
|
||||
modTbl.insert("rotation", rotationToString(pm.rotation));
|
||||
modArr.push_back(std::move(modTbl));
|
||||
}
|
||||
bldTbl.insert("modules", std::move(modArr));
|
||||
}
|
||||
bldArr.push_back(std::move(bldTbl));
|
||||
}
|
||||
|
||||
@@ -123,6 +138,27 @@ std::vector<Blueprint> deserialize(const std::string& tomlContent)
|
||||
bb.offset.setX(static_cast<int>((*bldTbl)["offset_x"].value_or(int64_t{0})));
|
||||
bb.offset.setY(static_cast<int>((*bldTbl)["offset_y"].value_or(int64_t{0})));
|
||||
bb.recipeId = (*bldTbl)["recipe_id"].value_or(std::string{});
|
||||
const toml::array* modArr = (*bldTbl)["modules"].as_array();
|
||||
if (modArr)
|
||||
{
|
||||
ShipLayoutConfig layout;
|
||||
for (std::size_t k = 0; k < modArr->size(); ++k)
|
||||
{
|
||||
const toml::table* modTbl = (*modArr)[k].as_table();
|
||||
if (!modTbl) { continue; }
|
||||
const std::optional<std::string> modType = (*modTbl)["type"].value<std::string>();
|
||||
const std::optional<int64_t> x = (*modTbl)["x"].value<int64_t>();
|
||||
const std::optional<int64_t> y = (*modTbl)["y"].value<int64_t>();
|
||||
const std::optional<std::string> rotStr = (*modTbl)["rotation"].value<std::string>();
|
||||
if (!modType || !x || !y || !rotStr) { continue; }
|
||||
PlacedModule pm;
|
||||
pm.moduleId = *modType;
|
||||
pm.position = QPoint(static_cast<int>(*x), static_cast<int>(*y));
|
||||
pm.rotation = parseRotation(*rotStr);
|
||||
layout.placedModules.push_back(std::move(pm));
|
||||
}
|
||||
bb.shipLayout = std::move(layout);
|
||||
}
|
||||
bp.buildings.push_back(std::move(bb));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,13 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QPoint>
|
||||
|
||||
#include "toml.hpp"
|
||||
|
||||
#include "Rotation.h"
|
||||
#include "ShipLayout.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
@@ -207,6 +212,42 @@ toml::table parseFile(const std::string& path, const std::string& file)
|
||||
}
|
||||
}
|
||||
|
||||
Rotation parseRotationString(const std::string& s)
|
||||
{
|
||||
if (s == "east") { return Rotation::East; }
|
||||
if (s == "south") { return Rotation::South; }
|
||||
if (s == "west") { return Rotation::West; }
|
||||
return Rotation::North;
|
||||
}
|
||||
|
||||
std::vector<PlacedModule> parsePlacedModules(const toml::array& arr,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
std::vector<PlacedModule> result;
|
||||
result.reserve(arr.size());
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||
const toml::table* t = arr[i].as_table();
|
||||
if (t == nullptr) { continue; }
|
||||
toml::table& mt = const_cast<toml::table&>(*t);
|
||||
|
||||
const std::optional<std::string> type = mt["type"].value<std::string>();
|
||||
const std::optional<int64_t> x = mt["x"].value<int64_t>();
|
||||
const std::optional<int64_t> y = mt["y"].value<int64_t>();
|
||||
const std::optional<std::string> rot = mt["rotation"].value<std::string>();
|
||||
if (!type || !x || !y || !rot) { continue; }
|
||||
|
||||
PlacedModule pm;
|
||||
pm.moduleId = *type;
|
||||
pm.position = QPoint(static_cast<int>(*x), static_cast<int>(*y));
|
||||
pm.rotation = parseRotationString(*rot);
|
||||
result.push_back(std::move(pm));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -223,26 +264,31 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
||||
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
|
||||
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.beltSpeedTilesPerSecond = requireDouble(tbl["world"]["belt_speed_tiles_per_second"], file, "world.belt_speed_tiles_per_second");
|
||||
cfg.tunnelMaxDistance = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance"], file, "world.tunnel_max_distance"));
|
||||
cfg.tileSize_m = requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m");
|
||||
cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m;
|
||||
cfg.tunnelMaxDistance_tiles = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles"));
|
||||
cfg.departureIntervalSeconds = requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds");
|
||||
|
||||
cfg.regions.asteroidWidth = static_cast<int>(requireInt(tbl["regions"]["asteroid_width"], file, "regions.asteroid_width"));
|
||||
cfg.regions.playerBufferWidth = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width"], file, "regions.player_buffer_width"));
|
||||
cfg.regions.contestZoneWidth = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width"], file, "regions.contest_zone_width"));
|
||||
cfg.regions.enemyBufferWidth = static_cast<int>(requireInt(tbl["regions"]["enemy_buffer_width"], file, "regions.enemy_buffer_width"));
|
||||
cfg.regions.asteroidWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles"));
|
||||
cfg.regions.playerBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles"));
|
||||
cfg.regions.contestZoneWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles"));
|
||||
cfg.regions.enemyBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles"));
|
||||
|
||||
cfg.expansion.columnsPerExpansion = static_cast<int>(requireInt(tbl["expansion"]["columns_per_expansion"], file, "expansion.columns_per_expansion"));
|
||||
cfg.expansion.columnsPerExpansion_tiles = static_cast<int>(requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles"));
|
||||
cfg.expansion.costBuildingBlocks = static_cast<int>(requireInt(tbl["expansion"]["cost_building_blocks"], file, "expansion.cost_building_blocks"));
|
||||
|
||||
cfg.push.pushExpandColumns = static_cast<int>(requireInt(tbl["push"]["push_expand_columns"], file, "push.push_expand_columns"));
|
||||
cfg.push.scalingFactor = requireDouble(tbl["push"]["scaling_factor"], file, "push.scaling_factor");
|
||||
cfg.push.pushExpandColumns_tiles = static_cast<int>(requireInt(tbl["push"]["push_expand_columns_tiles"], file, "push.push_expand_columns_tiles"));
|
||||
cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds");
|
||||
|
||||
cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula");
|
||||
cfg.waves.shipLevelFormula = requireFormula(tbl["waves"]["ship_level_formula"], file, "waves.ship_level_formula");
|
||||
cfg.waves.gapMinSeconds = requireDouble(tbl["waves"]["gap_min_seconds"], file, "waves.gap_min_seconds");
|
||||
cfg.waves.gapMaxSeconds = requireDouble(tbl["waves"]["gap_max_seconds"], file, "waves.gap_max_seconds");
|
||||
cfg.waves.spawnDurationSeconds = requireDouble(tbl["waves"]["spawn_duration_seconds"], file, "waves.spawn_duration_seconds");
|
||||
cfg.waves.bossCountdownSeconds = requireDouble(tbl["waves"]["boss_countdown_seconds"], file, "waves.boss_countdown_seconds");
|
||||
cfg.waves.bossThreatDurationSeconds = requireDouble(tbl["waves"]["boss_threat_duration_seconds"], file, "waves.boss_threat_duration_seconds");
|
||||
cfg.waves.bossQuietBeforeSeconds = requireDouble(tbl["waves"]["boss_quiet_before_seconds"], file, "waves.boss_quiet_before_seconds");
|
||||
cfg.waves.bossQuietAfterSeconds = requireDouble(tbl["waves"]["boss_quiet_after_seconds"], file, "waves.boss_quiet_after_seconds");
|
||||
|
||||
if (cfg.waves.gapMinSeconds > cfg.waves.gapMaxSeconds)
|
||||
{
|
||||
@@ -321,6 +367,15 @@ RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
|
||||
}
|
||||
def.building = *parsedType;
|
||||
|
||||
if (def.building == BuildingType::Assembler)
|
||||
{
|
||||
const auto level = mt["unlock_at_station_level"].value<int64_t>();
|
||||
if (level)
|
||||
{
|
||||
def.unlockAtStationLevel = static_cast<int>(*level);
|
||||
}
|
||||
}
|
||||
|
||||
// inputs may be omitted (e.g. miner recipes). An empty array is fine.
|
||||
if (mt.contains("inputs"))
|
||||
{
|
||||
@@ -357,7 +412,7 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
|
||||
ShipDef def;
|
||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||
def.availableFromStart = requireBool(mt["available_from_start"], file, elemPath + ".available_from_start");
|
||||
def.unlockAtStationLevel = static_cast<int>(requireInt(mt["unlock_at_station_level"], file, elemPath + ".unlock_at_station_level"));
|
||||
def.layout = requireStringArray(mt["layout"], file, elemPath + ".layout");
|
||||
|
||||
// Schematic
|
||||
@@ -374,14 +429,6 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
|
||||
}
|
||||
|
||||
// Threat
|
||||
{
|
||||
const std::string tPath = elemPath + ".threat";
|
||||
const toml::table& tTable = requireTable(mt["threat"], file, tPath);
|
||||
toml::table& tMt = const_cast<toml::table&>(tTable);
|
||||
def.threat.costFormula = requireFormula(tMt["cost_formula"], file, tPath + ".cost_formula");
|
||||
}
|
||||
|
||||
// Health
|
||||
{
|
||||
const std::string hPath = elemPath + ".health";
|
||||
@@ -395,11 +442,11 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
const std::string mPath = elemPath + ".movement";
|
||||
const toml::table& mTable = requireTable(mt["movement"], file, mPath);
|
||||
toml::table& mMt = const_cast<toml::table&>(mTable);
|
||||
def.movement.speedFormula = requireFormula(mMt["speed_formula"], file, mPath + ".speed_formula");
|
||||
def.movement.mainAccelerationFormula = requireFormula(mMt["main_acceleration_formula"], file, mPath + ".main_acceleration_formula");
|
||||
def.movement.maneuveringAccelerationFormula = requireFormula(mMt["maneuvering_acceleration_formula"], file, mPath + ".maneuvering_acceleration_formula");
|
||||
def.movement.angularAccelerationFormula = requireFormula(mMt["angular_acceleration_formula"], file, mPath + ".angular_acceleration_formula");
|
||||
def.movement.maxRotationSpeedFormula = requireFormula(mMt["max_rotation_speed_formula"], file, mPath + ".max_rotation_speed_formula");
|
||||
def.movement.speedFormula = requireFormula(mMt["speed_mps_formula"], file, mPath + ".speed_mps_formula");
|
||||
def.movement.mainAccelerationFormula = requireFormula(mMt["main_acceleration_mpss_formula"], file, mPath + ".main_acceleration_mpss_formula");
|
||||
def.movement.maneuveringAccelerationFormula = requireFormula(mMt["maneuvering_acceleration_mpss_formula"], file, mPath + ".maneuvering_acceleration_mpss_formula");
|
||||
def.movement.angularAccelerationFormula = requireFormula(mMt["angular_acceleration_radpss_formula"], file, mPath + ".angular_acceleration_radpss_formula");
|
||||
def.movement.maxRotationSpeedFormula = requireFormula(mMt["max_rotation_speed_radps_formula"], file, mPath + ".max_rotation_speed_radps_formula");
|
||||
}
|
||||
|
||||
// Sensor
|
||||
@@ -407,7 +454,7 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
const std::string snsPath = elemPath + ".sensor";
|
||||
const toml::table& snsTable = requireTable(mt["sensor"], file, snsPath);
|
||||
toml::table& snsMt = const_cast<toml::table&>(snsTable);
|
||||
def.sensor.sensorRangeFormula = requireFormula(snsMt["sensor_range_formula"], file, snsPath + ".sensor_range_formula");
|
||||
def.sensor.sensorRangeFormula = requireFormula(snsMt["sensor_range_m_formula"], file, snsPath + ".sensor_range_m_formula");
|
||||
}
|
||||
|
||||
// Loot
|
||||
@@ -418,43 +465,13 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
def.loot.scrapDrop = static_cast<int>(requireInt(lMt["scrap_drop"], file, lPath + ".scrap_drop"));
|
||||
}
|
||||
|
||||
// Optional: combat
|
||||
if (mt.contains("combat"))
|
||||
// Optional: default_modules (REQ-WAV-DEFAULT-MODULES)
|
||||
if (mt.contains("default_modules"))
|
||||
{
|
||||
const std::string cPath = elemPath + ".combat";
|
||||
const toml::table& cTable = requireTable(mt["combat"], file, cPath);
|
||||
toml::table& cMt = const_cast<toml::table&>(cTable);
|
||||
ShipCombat combat {
|
||||
requireFormula(cMt["damage_formula"], file, cPath + ".damage_formula"),
|
||||
requireFormula(cMt["attack_range_formula"], file, cPath + ".attack_range_formula"),
|
||||
requireFormula(cMt["attack_rate_formula"], file, cPath + ".attack_rate_formula"),
|
||||
};
|
||||
def.combat = std::move(combat);
|
||||
}
|
||||
|
||||
// Optional: salvage
|
||||
if (mt.contains("salvage"))
|
||||
{
|
||||
const std::string sPath = elemPath + ".salvage";
|
||||
const toml::table& sTable = requireTable(mt["salvage"], file, sPath);
|
||||
toml::table& sMt = const_cast<toml::table&>(sTable);
|
||||
ShipSalvage salvage;
|
||||
salvage.collectionRange = requireDouble(sMt["collection_range"], file, sPath + ".collection_range");
|
||||
salvage.cargoCapacity = static_cast<int>(requireInt(sMt["cargo_capacity"], file, sPath + ".cargo_capacity"));
|
||||
def.salvage = salvage;
|
||||
}
|
||||
|
||||
// Optional: repair
|
||||
if (mt.contains("repair"))
|
||||
{
|
||||
const std::string rPath = elemPath + ".repair";
|
||||
const toml::table& rTable = requireTable(mt["repair"], file, rPath);
|
||||
toml::table& rMt = const_cast<toml::table&>(rTable);
|
||||
ShipRepair repair {
|
||||
requireFormula(rMt["repair_rate_formula"], file, rPath + ".repair_rate_formula"),
|
||||
requireFormula(rMt["repair_range_formula"], file, rPath + ".repair_range_formula"),
|
||||
};
|
||||
def.repair = std::move(repair);
|
||||
const toml::array& modArr = requireArray(mt["default_modules"], file,
|
||||
elemPath + ".default_modules");
|
||||
def.defaultModules = parsePlacedModules(modArr, file,
|
||||
elemPath + ".default_modules");
|
||||
}
|
||||
|
||||
cfg.ships.push_back(std::move(def));
|
||||
@@ -484,8 +501,8 @@ StationsConfig ConfigLoader::loadStations(const std::string& path)
|
||||
cfg.playerStation.level = static_cast<int>(requireInt(tbl[p]["level"], file, p + ".level"));
|
||||
cfg.playerStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||
cfg.playerStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
||||
cfg.playerStation.rangeFormula = requireFormula(tbl[p]["range_formula"], file, p + ".range_formula");
|
||||
cfg.playerStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_formula"], file, p + ".fire_rate_formula");
|
||||
cfg.playerStation.rangeFormula = requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
|
||||
cfg.playerStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
|
||||
cfg.playerStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
||||
}
|
||||
|
||||
@@ -495,8 +512,8 @@ StationsConfig ConfigLoader::loadStations(const std::string& path)
|
||||
cfg.enemyStation.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
||||
cfg.enemyStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||
cfg.enemyStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
||||
cfg.enemyStation.rangeFormula = requireFormula(tbl[p]["range_formula"], file, p + ".range_formula");
|
||||
cfg.enemyStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_formula"], file, p + ".fire_rate_formula");
|
||||
cfg.enemyStation.rangeFormula = requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
|
||||
cfg.enemyStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
|
||||
cfg.enemyStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
||||
}
|
||||
|
||||
@@ -504,21 +521,29 @@ StationsConfig ConfigLoader::loadStations(const std::string& path)
|
||||
}
|
||||
|
||||
// Known category→stat mappings for module stat modifier discovery.
|
||||
// addedKeySuffix: unit suffix appended before "_formula" for additive modifier keys only.
|
||||
// Multiplicative modifier keys are always dimensionless and carry no suffix.
|
||||
struct StatEntry
|
||||
{
|
||||
const char* category;
|
||||
const char* stat;
|
||||
const char* addedKeySuffix;
|
||||
};
|
||||
|
||||
static const StatEntry kKnownStats[] = {
|
||||
{"health", "hp"},
|
||||
{"movement", "speed"},
|
||||
{"sensor", "sensor_range"},
|
||||
{"combat", "damage"},
|
||||
{"combat", "attack_range"},
|
||||
{"combat", "attack_rate"},
|
||||
{"repair", "repair_rate"},
|
||||
{"repair", "repair_range"},
|
||||
{"health", "hp", ""},
|
||||
{"movement", "speed", "_mps"},
|
||||
{"movement", "main_acceleration", "_mpss"},
|
||||
{"movement", "maneuvering_acceleration", "_mpss"},
|
||||
{"sensor", "sensor_range", "_m"},
|
||||
{"weapon", "damage", ""},
|
||||
{"weapon", "attack_range", "_m"},
|
||||
{"weapon", "attack_rate", "_hz"},
|
||||
{"salvage", "collection_range", "_m"},
|
||||
{"salvage", "cargo_capacity", ""},
|
||||
{"salvage", "collection_rate", "_hz"},
|
||||
{"repair", "repair_rate", "_hz"},
|
||||
{"repair", "repair_range", "_m"},
|
||||
};
|
||||
|
||||
ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||
@@ -547,12 +572,13 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||
|
||||
ModuleDef def;
|
||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||
def.unlockAtStationLevel = static_cast<int>(
|
||||
mt["unlock_at_station_level"].value_or<int64_t>(-1));
|
||||
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||
def.playerProductionLevel = static_cast<int>(requireInt(
|
||||
mt["player_production_level"], file, elemPath + ".player_production_level"));
|
||||
def.productionTimeSeconds = requireDouble(
|
||||
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
||||
def.threatCost = requireDouble(mt["threat_cost"], file, elemPath + ".threat_cost");
|
||||
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
||||
def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph");
|
||||
|
||||
@@ -573,8 +599,8 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||
elemPath + "." + se.category);
|
||||
toml::table& catMt = const_cast<toml::table&>(catTable);
|
||||
|
||||
const std::string addedKey = std::string("added_") + se.stat + "_formula";
|
||||
const std::string multipliedKey = std::string("multiplied_") + se.stat + "_formula";
|
||||
const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix + "_formula";
|
||||
const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix + "_formula";
|
||||
|
||||
if (catMt.contains(addedKey))
|
||||
{
|
||||
@@ -597,6 +623,63 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||
}
|
||||
}
|
||||
|
||||
// Weapon capability section: [module.weapon] with base stat formulas
|
||||
if (mt.contains("weapon"))
|
||||
{
|
||||
const std::string wPath = elemPath + ".weapon";
|
||||
const toml::table& wTable = requireTable(mt["weapon"], file, wPath);
|
||||
toml::table& wMt = const_cast<toml::table&>(wTable);
|
||||
if (wMt.contains("damage_formula") || wMt.contains("attack_range_m_formula")
|
||||
|| wMt.contains("attack_rate_hz_formula"))
|
||||
{
|
||||
ModuleWeaponCapability cap;
|
||||
cap.damageFormula = requireFormula(wMt["damage_formula"],
|
||||
file, wPath + ".damage_formula");
|
||||
cap.attackRangeFormula = requireFormula(wMt["attack_range_m_formula"],
|
||||
file, wPath + ".attack_range_m_formula");
|
||||
cap.attackRateFormula = requireFormula(wMt["attack_rate_hz_formula"],
|
||||
file, wPath + ".attack_rate_hz_formula");
|
||||
def.weaponCapability = std::move(cap);
|
||||
}
|
||||
}
|
||||
|
||||
// Salvage capability section: [module.salvage] with base stat formulas
|
||||
if (mt.contains("salvage"))
|
||||
{
|
||||
const std::string sPath = elemPath + ".salvage";
|
||||
const toml::table& sTable = requireTable(mt["salvage"], file, sPath);
|
||||
toml::table& sMt = const_cast<toml::table&>(sTable);
|
||||
if (sMt.contains("collection_range_m_formula") || sMt.contains("cargo_capacity_formula")
|
||||
|| sMt.contains("collection_rate_hz_formula"))
|
||||
{
|
||||
ModuleSalvageCapability cap;
|
||||
cap.collectionRangeFormula = requireFormula(sMt["collection_range_m_formula"],
|
||||
file, sPath + ".collection_range_m_formula");
|
||||
cap.cargoCapacityFormula = requireFormula(sMt["cargo_capacity_formula"],
|
||||
file, sPath + ".cargo_capacity_formula");
|
||||
cap.collectionRateFormula = requireFormula(sMt["collection_rate_hz_formula"],
|
||||
file, sPath + ".collection_rate_hz_formula");
|
||||
def.salvageCapability = std::move(cap);
|
||||
}
|
||||
}
|
||||
|
||||
// Repair capability section: [module.repair] with base stat formulas
|
||||
if (mt.contains("repair"))
|
||||
{
|
||||
const std::string rPath = elemPath + ".repair";
|
||||
const toml::table& rTable = requireTable(mt["repair"], file, rPath);
|
||||
toml::table& rMt = const_cast<toml::table&>(rTable);
|
||||
if (rMt.contains("repair_rate_hz_formula") || rMt.contains("repair_range_m_formula"))
|
||||
{
|
||||
ModuleRepairCapability cap;
|
||||
cap.repairRateFormula = requireFormula(rMt["repair_rate_hz_formula"],
|
||||
file, rPath + ".repair_rate_hz_formula");
|
||||
cap.repairRangeFormula = requireFormula(rMt["repair_range_m_formula"],
|
||||
file, rPath + ".repair_range_m_formula");
|
||||
def.repairCapability = std::move(cap);
|
||||
}
|
||||
}
|
||||
|
||||
cfg.modules.push_back(std::move(def));
|
||||
}
|
||||
|
||||
@@ -612,5 +695,6 @@ GameConfig ConfigLoader::loadFromDirectory(const std::string& configDir)
|
||||
cfg.ships = loadShips(configDir + "/ships.toml");
|
||||
cfg.stations = loadStations(configDir + "/stations.toml");
|
||||
cfg.modules = loadModules(configDir + "/modules.toml");
|
||||
cfg.threatCosts = computeThreatCostTable(cfg);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "ShipsConfig.h"
|
||||
#include "StationsConfig.h"
|
||||
#include "ModulesConfig.h"
|
||||
#include "ThreatCostCalculator.h"
|
||||
|
||||
// Aggregate of all simulation config files. Loaded at startup and reloaded
|
||||
// from disk on each game restart (REQ-CFG-RELOAD). See architecture.md "Config Loading".
|
||||
@@ -17,4 +18,5 @@ struct GameConfig
|
||||
ShipsConfig ships;
|
||||
StationsConfig stations;
|
||||
ModulesConfig modules;
|
||||
ThreatCostTable threatCosts;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -15,17 +16,42 @@ struct ModuleStatModifier
|
||||
Formula formula;
|
||||
};
|
||||
|
||||
// Capability sections — present when the module grants that capability.
|
||||
struct ModuleWeaponCapability
|
||||
{
|
||||
Formula damageFormula;
|
||||
Formula attackRangeFormula;
|
||||
Formula attackRateFormula;
|
||||
};
|
||||
|
||||
struct ModuleSalvageCapability
|
||||
{
|
||||
Formula collectionRangeFormula;
|
||||
Formula cargoCapacityFormula;
|
||||
Formula collectionRateFormula;
|
||||
};
|
||||
|
||||
struct ModuleRepairCapability
|
||||
{
|
||||
Formula repairRateFormula;
|
||||
Formula repairRangeFormula;
|
||||
};
|
||||
|
||||
struct ModuleDef
|
||||
{
|
||||
std::string id;
|
||||
int unlockAtStationLevel;
|
||||
std::vector<std::string> surfaceMask;
|
||||
std::vector<RecipeIngredient> materials;
|
||||
int playerProductionLevel;
|
||||
double productionTimeSeconds;
|
||||
double threatCost;
|
||||
std::string fillColor;
|
||||
std::string glyph;
|
||||
std::vector<ModuleStatModifier> statModifiers;
|
||||
|
||||
std::optional<ModuleWeaponCapability> weaponCapability;
|
||||
std::optional<ModuleSalvageCapability> salvageCapability;
|
||||
std::optional<ModuleRepairCapability> repairCapability;
|
||||
};
|
||||
|
||||
struct ModulesConfig
|
||||
|
||||
@@ -32,6 +32,10 @@ struct RecipeDef
|
||||
std::vector<RecipeIngredient> inputs;
|
||||
std::vector<RecipeOutput> outputs;
|
||||
double durationSeconds;
|
||||
// Assembler only. nullopt = implicit-only locking. -1 = explicitly unlocked
|
||||
// at game start. >= 0 = locked; schematic enters drop pool at that station
|
||||
// level once the output item is implicitly unlocked (REQ-LOCK-EXPLICIT).
|
||||
std::optional<int> unlockAtStationLevel;
|
||||
};
|
||||
|
||||
struct RecipesConfig
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Formula.h"
|
||||
#include "RecipesConfig.h" // for RecipeIngredient
|
||||
#include "ShipLayout.h" // for PlacedModule
|
||||
|
||||
// Build materials and initial per-schematic production level
|
||||
// (REQ-BLD-SHIPYARD, REQ-DEF-SCHEMATIC-DROP).
|
||||
@@ -16,13 +16,6 @@ struct ShipSchematic
|
||||
double productionTimeSeconds;
|
||||
};
|
||||
|
||||
// Wave scheduling cost (REQ-WAV-THREAT-COST). Ships with cost_formula that
|
||||
// always evaluates to 0 are ineligible as wave picks.
|
||||
struct ShipThreat
|
||||
{
|
||||
Formula costFormula;
|
||||
};
|
||||
|
||||
struct ShipHealth
|
||||
{
|
||||
Formula hpFormula; // REQ-SHP-STATS
|
||||
@@ -42,27 +35,6 @@ struct ShipSensor
|
||||
Formula sensorRangeFormula; // REQ-SHP-SENSOR, REQ-SHP-STATS
|
||||
};
|
||||
|
||||
struct ShipCombat
|
||||
{
|
||||
Formula damageFormula;
|
||||
Formula attackRangeFormula;
|
||||
Formula attackRateFormula; // shots per second
|
||||
};
|
||||
|
||||
// Optional; present only on salvage ships (REQ-SHP-SALVAGE).
|
||||
struct ShipSalvage
|
||||
{
|
||||
double collectionRange;
|
||||
int cargoCapacity;
|
||||
};
|
||||
|
||||
// Optional; present only on repair ships (REQ-SHP-REPAIR).
|
||||
struct ShipRepair
|
||||
{
|
||||
Formula repairRateFormula;
|
||||
Formula repairRangeFormula;
|
||||
};
|
||||
|
||||
// Scrap dropped on destruction (REQ-RES-SCRAP-DROP).
|
||||
struct ShipLoot
|
||||
{
|
||||
@@ -72,22 +44,17 @@ struct ShipLoot
|
||||
struct ShipDef
|
||||
{
|
||||
std::string id;
|
||||
bool availableFromStart;
|
||||
int unlockAtStationLevel;
|
||||
std::vector<std::string> layout;
|
||||
|
||||
ShipSchematic schematic;
|
||||
ShipThreat threat;
|
||||
ShipHealth health;
|
||||
ShipMovement movement;
|
||||
ShipSensor sensor;
|
||||
ShipLoot loot;
|
||||
|
||||
// Role-specific sections. A ship is a combat ship if combat is present,
|
||||
// a salvage ship if salvage is present, etc. A ship may have multiple
|
||||
// of these set (hybrid ships) once the behavior systems support it.
|
||||
std::optional<ShipCombat> combat;
|
||||
std::optional<ShipSalvage> salvage;
|
||||
std::optional<ShipRepair> repair;
|
||||
// Module layout used for enemy wave ships (REQ-WAV-DEFAULT-MODULES).
|
||||
std::vector<PlacedModule> defaultModules;
|
||||
};
|
||||
|
||||
struct ShipsConfig
|
||||
|
||||
@@ -5,34 +5,38 @@
|
||||
// Region widths are in tiles (REQ-GW-REGIONS).
|
||||
struct WorldRegions
|
||||
{
|
||||
int asteroidWidth;
|
||||
int playerBufferWidth;
|
||||
int contestZoneWidth;
|
||||
int enemyBufferWidth;
|
||||
int asteroidWidth_tiles;
|
||||
int playerBufferWidth_tiles;
|
||||
int contestZoneWidth_tiles;
|
||||
int enemyBufferWidth_tiles;
|
||||
};
|
||||
|
||||
// Asteroid expansion (REQ-EXP-UNLOCK, REQ-EXP-COST).
|
||||
struct WorldExpansion
|
||||
{
|
||||
int columnsPerExpansion;
|
||||
int columnsPerExpansion_tiles;
|
||||
int costBuildingBlocks;
|
||||
};
|
||||
|
||||
// Push scaling (REQ-PSH-*).
|
||||
// Push effects (REQ-PSH-*, REQ-WAV-BOSS-ADVANCE).
|
||||
struct WorldPush
|
||||
{
|
||||
int pushExpandColumns;
|
||||
double scalingFactor;
|
||||
int pushExpandColumns_tiles;
|
||||
double bossAdvanceSeconds; // boss countdown advanced by this much per push
|
||||
};
|
||||
|
||||
// Wave scheduling (REQ-WAV-*).
|
||||
struct WorldWaves
|
||||
{
|
||||
Formula threatRateFormula; // threat/s as a function of elapsed game-time seconds
|
||||
Formula shipLevelFormula; // enemy ship level as a function of elapsed game-time seconds
|
||||
Formula threatRateFormula; // threat/s as a function of boss wave counter x
|
||||
Formula shipLevelFormula; // enemy ship level as a function of boss wave counter x
|
||||
double gapMinSeconds;
|
||||
double gapMaxSeconds;
|
||||
double spawnDurationSeconds;
|
||||
double bossCountdownSeconds; // duration of each boss cycle (REQ-WAV-BOSS-COUNTDOWN)
|
||||
double bossThreatDurationSeconds; // boss budget = rate * this (REQ-WAV-BOSS-TRIGGER)
|
||||
double bossQuietBeforeSeconds; // suppress normal waves this long before boss (REQ-WAV-QUIET)
|
||||
double bossQuietAfterSeconds; // suppress normal waves this long after boss (REQ-WAV-QUIET)
|
||||
};
|
||||
|
||||
struct WorldConfig
|
||||
@@ -41,8 +45,9 @@ struct WorldConfig
|
||||
int refundPercentage; // REQ-BLD-DEMOLISH
|
||||
int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS
|
||||
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP
|
||||
double beltSpeedTilesPerSecond; // REQ-GW-BELT-SPEED
|
||||
int tunnelMaxDistance; // REQ-BLD-TUNNEL-PAIR
|
||||
double tileSize_m; // metres per tile (REQ-GW-TILE-SIZE)
|
||||
double beltSpeed_tps; // REQ-GW-BELT-SPEED (tiles/s, converted from m/s in config)
|
||||
int tunnelMaxDistance_tiles; // REQ-BLD-TUNNEL-PAIR
|
||||
double departureIntervalSeconds; // REQ-SHP-RALLY
|
||||
|
||||
WorldRegions regions;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -8,6 +9,7 @@
|
||||
|
||||
#include "BuildingType.h"
|
||||
#include "Rotation.h"
|
||||
#include "ShipLayout.h"
|
||||
|
||||
struct BlueprintBuilding
|
||||
{
|
||||
@@ -15,6 +17,7 @@ struct BlueprintBuilding
|
||||
Rotation rotation;
|
||||
QPoint offset; // tile offset from bounding-box center (floor for even sizes)
|
||||
std::string recipeId; // empty = none selected
|
||||
std::optional<ShipLayoutConfig> shipLayout;
|
||||
};
|
||||
|
||||
struct Blueprint
|
||||
|
||||
@@ -6,11 +6,11 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Blueprint.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingId.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FireEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Item.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Port.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicDropEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ SET(SRCS
|
||||
${SRCS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
|
||||
27
src/lib/core/DisplayName.cpp
Normal file
27
src/lib/core/DisplayName.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#include "DisplayName.h"
|
||||
|
||||
#include <cctype>
|
||||
|
||||
std::string toDisplayName(const std::string& id)
|
||||
{
|
||||
std::string result;
|
||||
bool nextUpper = true;
|
||||
for (char c : id)
|
||||
{
|
||||
if (c == '_')
|
||||
{
|
||||
result += ' ';
|
||||
nextUpper = true;
|
||||
}
|
||||
else if (nextUpper)
|
||||
{
|
||||
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
nextUpper = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
5
src/lib/core/DisplayName.h
Normal file
5
src/lib/core/DisplayName.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
std::string toDisplayName(const std::string& id);
|
||||
@@ -18,6 +18,11 @@ entt::entity EntityAdmin::createEntity()
|
||||
return m_registry.create();
|
||||
}
|
||||
|
||||
entt::entity EntityAdmin::createModuleEntity()
|
||||
{
|
||||
return m_registry.create();
|
||||
}
|
||||
|
||||
bool EntityAdmin::isValid(entt::entity entity) const
|
||||
{
|
||||
return m_registry.valid(entity);
|
||||
@@ -34,9 +39,9 @@ void EntityAdmin::clear()
|
||||
}
|
||||
|
||||
entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
|
||||
float maxSpeedPerTick, float mainAccelPerTick,
|
||||
float maneuveringAccelPerTick, float angularAccelPerTick,
|
||||
float maxRotationSpeedPerTick, float sensorRange,
|
||||
float maxSpeed_tpt, float mainAcceleration_tptt,
|
||||
float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt,
|
||||
float maxRotationSpeed_rpt, float sensorRange_tiles,
|
||||
int level, const std::string& schematicId, bool isEnemy)
|
||||
{
|
||||
entt::entity entity = createEntity();
|
||||
@@ -45,17 +50,17 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
|
||||
add<FactionComponent>(entity, FactionComponent{isEnemy});
|
||||
add<FacingComponent>(entity, FacingComponent{0.0f});
|
||||
add<DynamicBodyComponent>(entity, DynamicBodyComponent{
|
||||
maxSpeedPerTick,
|
||||
mainAccelPerTick,
|
||||
maneuveringAccelPerTick,
|
||||
angularAccelPerTick,
|
||||
maxRotationSpeedPerTick,
|
||||
QVector2D(0.0f, 0.0f), // velocity
|
||||
0.0f, // angularVelocity
|
||||
QVector2D(0.0f, 0.0f), // linearAcceleration
|
||||
0.0f // angularAcceleration
|
||||
maxSpeed_tpt,
|
||||
mainAcceleration_tptt,
|
||||
maneuveringAcceleration_tptt,
|
||||
maxAngularAcceleration_rptt,
|
||||
maxRotationSpeed_rpt,
|
||||
QVector2D(0.0f, 0.0f), // velocity_tpt
|
||||
0.0f, // angularVelocity_rpt
|
||||
QVector2D(0.0f, 0.0f), // linearAcceleration_tptt
|
||||
0.0f // angularAcceleration_rptt
|
||||
});
|
||||
add<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange});
|
||||
add<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange_tiles});
|
||||
add<ShipIdentityComponent>(entity, ShipIdentityComponent{level, schematicId});
|
||||
add<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)});
|
||||
return entity;
|
||||
|
||||
@@ -53,9 +53,9 @@ public:
|
||||
// -- Factory methods ----------------------------------------------------
|
||||
|
||||
entt::entity spawnShip(QVector2D position, float hp, float maxHp,
|
||||
float maxSpeedPerTick, float mainAccelPerTick,
|
||||
float maneuveringAccelPerTick, float angularAccelPerTick,
|
||||
float maxRotationSpeedPerTick, float sensorRange,
|
||||
float maxSpeed_tpt, float mainAcceleration_tptt,
|
||||
float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt,
|
||||
float maxRotationSpeed_rpt, float sensorRange_tiles,
|
||||
int level, const std::string& schematicId, bool isEnemy);
|
||||
|
||||
entt::entity spawnStation(QPoint anchor, QSize footprint,
|
||||
@@ -66,6 +66,10 @@ public:
|
||||
|
||||
entt::entity spawnHqProxy(QVector2D position, float hp, float maxHp);
|
||||
|
||||
// Creates a bare entity for module child entities (weapons, salvage, repair).
|
||||
// Caller is responsible for attaching all required components.
|
||||
entt::entity createModuleEntity();
|
||||
|
||||
private:
|
||||
entt::entity createEntity();
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Tick.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
// Transient record emitted each time a weapon fires (REQ-SHP-FIRING,
|
||||
// REQ-SHP-FIRING-BEAM). Buffered in a sim-owned queue and drained by the
|
||||
// renderer each frame to draw the 0.3-second laser beam.
|
||||
struct FireEvent
|
||||
{
|
||||
entt::entity shooter;
|
||||
entt::entity target;
|
||||
Tick emittedAt;
|
||||
};
|
||||
28
src/lib/core/SchematicChoiceOption.h
Normal file
28
src/lib/core/SchematicChoiceOption.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class SchematicType
|
||||
{
|
||||
Ship,
|
||||
Module,
|
||||
Recipe
|
||||
};
|
||||
|
||||
// One option presented to the player in the schematic choice dialog
|
||||
// (REQ-DEF-SCHEMATIC-DROP). Built by the simulation when enemy stations are
|
||||
// destroyed; the UI reads these to populate the dialog.
|
||||
struct SchematicChoiceOption
|
||||
{
|
||||
std::string schematicId;
|
||||
SchematicType type;
|
||||
std::string displayName;
|
||||
bool isNewUnlock;
|
||||
int targetLevel;
|
||||
|
||||
// Display names of items produced by recipes that would newly become
|
||||
// implicitly unlocked (REQ-LOCK-IMPLICIT) if this option is selected.
|
||||
// Deduplicated and sorted alphabetically; empty if none.
|
||||
std::vector<std::string> newlyUnlockedItemNames;
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
// Emitted in tick step 9 (Deaths & loot) when a destroyed enemy-defence-station
|
||||
// set awards a schematic (REQ-DEF-SCHEMATIC-DROP). The UI renders a toast
|
||||
// (REQ-UI-SCHEMATIC-TOAST); wasNewUnlock chooses between the "unlocked" and
|
||||
// "level -> N" wording.
|
||||
struct SchematicDropEvent
|
||||
{
|
||||
std::string schematicId; // matches ShipDef::id in the config.
|
||||
int newLevel;
|
||||
bool wasNewUnlock;
|
||||
};
|
||||
@@ -4,18 +4,18 @@
|
||||
|
||||
struct DynamicBodyComponent
|
||||
{
|
||||
// --- dynamics parameters (formerly ShipDynamics) ---
|
||||
float maxSpeedPerTick;
|
||||
float mainAccelerationPerTick;
|
||||
float maneuveringAccelerationPerTick;
|
||||
float angularAccelerationPerTick;
|
||||
float maxRotationSpeedPerTick;
|
||||
// --- dynamics parameters ---
|
||||
float maxSpeed_tpt; // tiles/tick
|
||||
float mainAcceleration_tptt; // tiles/tick²
|
||||
float maneuveringAcceleration_tptt; // tiles/tick²
|
||||
float maxAngularAcceleration_rptt; // rad/tick²
|
||||
float maxRotationSpeed_rpt; // rad/tick
|
||||
|
||||
// --- integrated state ---
|
||||
QVector2D velocity;
|
||||
float angularVelocity;
|
||||
QVector2D velocity_tpt; // tiles/tick
|
||||
float angularVelocity_rpt; // rad/tick
|
||||
|
||||
// --- written each tick by MovementIntentSystem, consumed by DynamicBodySystem ---
|
||||
QVector2D linearAcceleration;
|
||||
float angularAcceleration;
|
||||
QVector2D linearAcceleration_tptt; // tiles/tick²
|
||||
float angularAcceleration_rptt; // rad/tick²
|
||||
};
|
||||
|
||||
9
src/lib/ecs/component/ModuleOwnerComponent.h
Normal file
9
src/lib/ecs/component/ModuleOwnerComponent.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
// Links a capability module child entity back to its owner ship or station.
|
||||
struct ModuleOwnerComponent
|
||||
{
|
||||
entt::entity owner;
|
||||
};
|
||||
@@ -7,4 +7,5 @@
|
||||
struct RepairBehaviorComponent
|
||||
{
|
||||
std::optional<entt::entity> currentTarget;
|
||||
float maxRepairRange_tiles = 0.0f;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
struct RepairToolComponent
|
||||
{
|
||||
float ratePerTick;
|
||||
float range;
|
||||
float range_tiles;
|
||||
std::optional<entt::entity> currentTarget;
|
||||
};
|
||||
|
||||
@@ -10,4 +10,5 @@ struct SalvageBehaviorComponent
|
||||
{
|
||||
std::optional<QVector2D> scrapTarget;
|
||||
BuildingId deliveryBay; // kInvalidBuildingId until assigned at a salvage bay
|
||||
float maxCollectionRange_tiles = 0.0f;
|
||||
};
|
||||
|
||||
@@ -4,5 +4,7 @@ struct SalvageCargoComponent
|
||||
{
|
||||
int capacity;
|
||||
int current;
|
||||
float collectionRange;
|
||||
float collectionRange_tiles;
|
||||
int collectionIntervalTicks;
|
||||
int cooldownTicksRemaining;
|
||||
};
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
struct SensorRangeComponent
|
||||
{
|
||||
float value;
|
||||
float value_tiles;
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
struct WeaponComponent
|
||||
{
|
||||
float damage;
|
||||
float range;
|
||||
float range_tiles;
|
||||
float fireRateHz;
|
||||
float cooldownTicks;
|
||||
std::optional<entt::entity> currentTarget;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "AiSystem.h"
|
||||
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <QVector2D>
|
||||
@@ -14,6 +15,7 @@
|
||||
#include "HealthComponent.h"
|
||||
#include "HomeReturnBehaviorComponent.h"
|
||||
#include "HqProxyComponent.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "MovementIntentComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "RallyBehaviorComponent.h"
|
||||
@@ -26,6 +28,44 @@
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "StationBodyComponent.h"
|
||||
#include "ThreatResponseBehaviorComponent.h"
|
||||
#include "tracing.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers for repair targeting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct RepairableInfo
|
||||
{
|
||||
entt::entity entity;
|
||||
QVector2D position;
|
||||
bool isEnemy;
|
||||
bool isShip;
|
||||
float hp;
|
||||
float maxHp;
|
||||
};
|
||||
|
||||
static std::vector<RepairableInfo> buildRepairables(EntityAdmin& admin)
|
||||
{
|
||||
std::vector<RepairableInfo> repairables;
|
||||
|
||||
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent, HealthComponent>(
|
||||
[&repairables](entt::entity e, const ShipIdentityComponent& /*si*/,
|
||||
const PositionComponent& pos, const FactionComponent& f,
|
||||
const HealthComponent& h)
|
||||
{
|
||||
repairables.push_back({e, pos.value, f.isEnemy, true, h.hp, h.maxHp});
|
||||
});
|
||||
|
||||
admin.forEach<StationBodyComponent, PositionComponent, FactionComponent, HealthComponent>(
|
||||
[&repairables](entt::entity e, const StationBodyComponent& /*sb*/,
|
||||
const PositionComponent& pos, const FactionComponent& f,
|
||||
const HealthComponent& h)
|
||||
{
|
||||
repairables.push_back({e, pos.value, f.isEnemy, false, h.hp, h.maxHp});
|
||||
});
|
||||
|
||||
return repairables;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tickHomeReturnBehavior (priority 4)
|
||||
@@ -33,6 +73,7 @@
|
||||
|
||||
void AiSystem::tickHomeReturnBehavior(EntityAdmin& admin)
|
||||
{
|
||||
TRACE();
|
||||
admin.forEach<HomeReturnBehaviorComponent, HealthComponent, MovementIntentComponent>(
|
||||
[](entt::entity /*e*/, const HomeReturnBehaviorComponent& homeReturnBehavior,
|
||||
const HealthComponent& h, MovementIntentComponent& intent)
|
||||
@@ -53,6 +94,7 @@ void AiSystem::tickHomeReturnBehavior(EntityAdmin& admin)
|
||||
|
||||
void AiSystem::tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSystem& buildings)
|
||||
{
|
||||
TRACE();
|
||||
// Snapshot all combatant entities for target acquisition.
|
||||
struct CombatantInfo
|
||||
{
|
||||
@@ -90,7 +132,7 @@ void AiSystem::tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSyst
|
||||
PositionComponent& pos, FactionComponent& faction,
|
||||
SensorRangeComponent& sensor, MovementIntentComponent& intent)
|
||||
{
|
||||
const float range = sensor.value;
|
||||
const float range = sensor.value_tiles;
|
||||
|
||||
// Validate current target.
|
||||
bool targetValid = false;
|
||||
@@ -180,33 +222,8 @@ void AiSystem::tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSyst
|
||||
|
||||
void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
|
||||
{
|
||||
// Snapshot all entities with health for repair targeting.
|
||||
struct RepairableInfo
|
||||
{
|
||||
entt::entity entity;
|
||||
QVector2D position;
|
||||
bool isEnemy;
|
||||
bool isShip;
|
||||
float hp;
|
||||
float maxHp;
|
||||
};
|
||||
std::vector<RepairableInfo> repairables;
|
||||
|
||||
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent, HealthComponent>(
|
||||
[&repairables](entt::entity e, const ShipIdentityComponent& /*si*/,
|
||||
const PositionComponent& pos, const FactionComponent& f,
|
||||
const HealthComponent& h)
|
||||
{
|
||||
repairables.push_back({e, pos.value, f.isEnemy, true, h.hp, h.maxHp});
|
||||
});
|
||||
|
||||
admin.forEach<StationBodyComponent, PositionComponent, FactionComponent, HealthComponent>(
|
||||
[&repairables](entt::entity e, const StationBodyComponent& /*sb*/,
|
||||
const PositionComponent& pos, const FactionComponent& f,
|
||||
const HealthComponent& h)
|
||||
{
|
||||
repairables.push_back({e, pos.value, f.isEnemy, false, h.hp, h.maxHp});
|
||||
});
|
||||
TRACE();
|
||||
std::vector<RepairableInfo> repairables = buildRepairables(admin);
|
||||
|
||||
// Snapshot enemy ships for threat detection.
|
||||
struct EnemyInfo
|
||||
@@ -224,19 +241,17 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
|
||||
}
|
||||
});
|
||||
|
||||
admin.forEach<RepairBehaviorComponent, RepairToolComponent, PositionComponent,
|
||||
admin.forEach<RepairBehaviorComponent, PositionComponent,
|
||||
FactionComponent, SensorRangeComponent, MovementIntentComponent>(
|
||||
[&](entt::entity e, RepairBehaviorComponent& rb, RepairToolComponent& rt,
|
||||
[&](entt::entity e, RepairBehaviorComponent& rb,
|
||||
PositionComponent& pos, FactionComponent& /*faction*/,
|
||||
SensorRangeComponent& sensor, MovementIntentComponent& intent)
|
||||
{
|
||||
const float repairRange = rt.range;
|
||||
|
||||
// Flee if enemy nearby.
|
||||
bool enemyNearby = false;
|
||||
for (const EnemyInfo& enemy : enemies)
|
||||
{
|
||||
if ((enemy.position - pos.value).length() <= sensor.value)
|
||||
if ((enemy.position - pos.value).length() <= sensor.value_tiles)
|
||||
{
|
||||
enemyNearby = true;
|
||||
break;
|
||||
@@ -270,7 +285,7 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
|
||||
if (!targetValid)
|
||||
{
|
||||
rb.currentTarget = std::nullopt;
|
||||
float bestDist = sensor.value;
|
||||
float bestDist = sensor.value_tiles;
|
||||
|
||||
for (const RepairableInfo& r : repairables)
|
||||
{
|
||||
@@ -303,17 +318,6 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
|
||||
targetPos = admin.get<PositionComponent>(target).value;
|
||||
}
|
||||
|
||||
const float distToTarget = (targetPos - pos.value).length();
|
||||
if (distToTarget <= repairRange)
|
||||
{
|
||||
if (admin.isValid(target) && admin.hasAll<HealthComponent>(target))
|
||||
{
|
||||
HealthComponent& targetHealth = admin.get<HealthComponent>(target);
|
||||
targetHealth.hp = std::min(targetHealth.hp + rt.ratePerTick,
|
||||
targetHealth.maxHp);
|
||||
}
|
||||
}
|
||||
|
||||
if (2 > intent.priority)
|
||||
{
|
||||
intent = MovementIntentComponent{2, targetPos};
|
||||
@@ -321,6 +325,68 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tickRepairTools
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void AiSystem::tickRepairTools(EntityAdmin& admin)
|
||||
{
|
||||
TRACE();
|
||||
const std::vector<RepairableInfo> repairables = buildRepairables(admin);
|
||||
|
||||
admin.forEach<RepairToolComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity /*e*/, RepairToolComponent& rt, const ModuleOwnerComponent& owner)
|
||||
{
|
||||
if (!admin.hasAll<RepairBehaviorComponent>(owner.owner)) { return; }
|
||||
|
||||
const RepairBehaviorComponent& rb =
|
||||
admin.get<RepairBehaviorComponent>(owner.owner);
|
||||
const PositionComponent& ownerPos =
|
||||
admin.get<PositionComponent>(owner.owner);
|
||||
|
||||
// Try the ship's preferred nav target first.
|
||||
if (rb.currentTarget)
|
||||
{
|
||||
const entt::entity preferred = *rb.currentTarget;
|
||||
if (admin.isValid(preferred) && admin.hasAll<HealthComponent>(preferred)
|
||||
&& admin.hasAll<PositionComponent>(preferred))
|
||||
{
|
||||
HealthComponent& th = admin.get<HealthComponent>(preferred);
|
||||
const float dist =
|
||||
(admin.get<PositionComponent>(preferred).value
|
||||
- ownerPos.value).length();
|
||||
if (th.hp > 0.0f && th.hp < th.maxHp && dist <= rt.range_tiles)
|
||||
{
|
||||
rt.currentTarget = rb.currentTarget;
|
||||
th.hp = std::min(th.hp + rt.ratePerTick, th.maxHp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preferred target unavailable; scan for nearest damaged friendly in range.
|
||||
rt.currentTarget = std::nullopt;
|
||||
float bestDist = rt.range_tiles;
|
||||
for (const RepairableInfo& r : repairables)
|
||||
{
|
||||
if (r.isEnemy) { continue; }
|
||||
if (r.hp <= 0.0f || r.hp >= r.maxHp) { continue; }
|
||||
const float dist = (r.position - ownerPos.value).length();
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
rt.currentTarget = r.entity;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rt.currentTarget) { return; }
|
||||
|
||||
HealthComponent& targetHealth =
|
||||
admin.get<HealthComponent>(*rt.currentTarget);
|
||||
targetHealth.hp = std::min(targetHealth.hp + rt.ratePerTick, targetHealth.maxHp);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tickSalvageBehavior (priority 1)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -328,6 +394,7 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
|
||||
void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
|
||||
BuildingSystem& buildings)
|
||||
{
|
||||
TRACE();
|
||||
// Snapshot enemy ships for threat detection.
|
||||
struct EnemyShipPos
|
||||
{
|
||||
@@ -344,15 +411,38 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
|
||||
}
|
||||
});
|
||||
|
||||
// Aggregate cargo across all salvage-module children per owning ship.
|
||||
struct AggregatedCargo
|
||||
{
|
||||
int totalCurrent = 0;
|
||||
int totalCapacity = 0;
|
||||
};
|
||||
std::unordered_map<entt::entity, AggregatedCargo> cargoByShip;
|
||||
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity /*ce*/, const SalvageCargoComponent& c, const ModuleOwnerComponent& o)
|
||||
{
|
||||
AggregatedCargo& agg = cargoByShip[o.owner];
|
||||
agg.totalCurrent += c.current;
|
||||
agg.totalCapacity += c.capacity;
|
||||
});
|
||||
|
||||
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
|
||||
|
||||
admin.forEach<SalvageBehaviorComponent, SalvageCargoComponent, PositionComponent,
|
||||
// Tick down per-module collection cooldowns.
|
||||
admin.forEach<SalvageCargoComponent>(
|
||||
[](entt::entity /*e*/, SalvageCargoComponent& c)
|
||||
{
|
||||
if (c.cooldownTicksRemaining > 0) { --c.cooldownTicksRemaining; }
|
||||
});
|
||||
|
||||
admin.forEach<SalvageBehaviorComponent, PositionComponent,
|
||||
SensorRangeComponent, MovementIntentComponent>(
|
||||
[&](entt::entity /*e*/, SalvageBehaviorComponent& salvageBehavior,
|
||||
SalvageCargoComponent& cargo, PositionComponent& pos,
|
||||
[&](entt::entity e, SalvageBehaviorComponent& salvageBehavior,
|
||||
PositionComponent& pos,
|
||||
SensorRangeComponent& sensor, MovementIntentComponent& intent)
|
||||
{
|
||||
const float collectRange = cargo.collectionRange;
|
||||
const float collectRange = salvageBehavior.maxCollectionRange_tiles;
|
||||
const AggregatedCargo& cargoState = cargoByShip[e];
|
||||
|
||||
// Assign nearest SalvageBay if needed.
|
||||
if (salvageBehavior.deliveryBay == kInvalidBuildingId)
|
||||
@@ -378,7 +468,8 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
|
||||
}
|
||||
}
|
||||
|
||||
const bool cargoFull = (cargo.current >= cargo.capacity);
|
||||
const bool cargoFull = (cargoState.totalCurrent >= cargoState.totalCapacity
|
||||
&& cargoState.totalCapacity > 0);
|
||||
|
||||
if (cargoFull)
|
||||
{
|
||||
@@ -389,17 +480,26 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
|
||||
if (bayId != kInvalidBuildingId
|
||||
&& (pos.value - bayPos).length() <= 1.0f)
|
||||
{
|
||||
// Decrement first non-empty salvage child.
|
||||
bool delivered = false;
|
||||
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity /*ce*/, SalvageCargoComponent& c,
|
||||
const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (delivered || o.owner != e || c.current <= 0) { return; }
|
||||
if (buildings.deliverScrapToSalvageBay(bayId))
|
||||
{
|
||||
--cargo.current;
|
||||
--c.current;
|
||||
delivered = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Retreat if enemy near and cargo empty.
|
||||
bool retreating = false;
|
||||
if (cargo.current == 0)
|
||||
if (cargoState.totalCurrent == 0)
|
||||
{
|
||||
for (const EnemyShipPos& enemy : enemyShips)
|
||||
{
|
||||
@@ -417,19 +517,33 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
|
||||
}
|
||||
if (retreating) { return; }
|
||||
|
||||
// Collect nearby scrap.
|
||||
// Per-module independent collection: each ready module collects one scrap.
|
||||
bool anythingCollected = false;
|
||||
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity /*ce*/, SalvageCargoComponent& c,
|
||||
const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (o.owner != e || c.current >= c.capacity
|
||||
|| c.cooldownTicksRemaining > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (const ScrapInfo& si : allScrap)
|
||||
{
|
||||
if ((si.position - pos.value).length() <= collectRange)
|
||||
{
|
||||
if ((si.position - pos.value).length() > c.collectionRange_tiles) { continue; }
|
||||
if (scraps.consume(si.entity))
|
||||
{
|
||||
++cargo.current;
|
||||
salvageBehavior.scrapTarget = std::nullopt;
|
||||
}
|
||||
++c.current;
|
||||
c.cooldownTicksRemaining = c.collectionIntervalTicks;
|
||||
anythingCollected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (anythingCollected)
|
||||
{
|
||||
salvageBehavior.scrapTarget = std::nullopt;
|
||||
}
|
||||
|
||||
// Move toward scrap target or find a new one.
|
||||
if (salvageBehavior.scrapTarget)
|
||||
@@ -441,7 +555,7 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
|
||||
}
|
||||
else
|
||||
{
|
||||
float bestDist = sensor.value;
|
||||
float bestDist = sensor.value_tiles;
|
||||
std::optional<QVector2D> bestPos;
|
||||
for (const ScrapInfo& si : allScrap)
|
||||
{
|
||||
|
||||
@@ -10,5 +10,6 @@ public:
|
||||
void tickHomeReturnBehavior(EntityAdmin& admin);
|
||||
void tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSystem& buildings);
|
||||
void tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings);
|
||||
void tickRepairTools(EntityAdmin& admin);
|
||||
void tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps, BuildingSystem& buildings);
|
||||
};
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "StationBodyComponent.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "SensorRangeComponent.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "ThreatResponseBehaviorComponent.h"
|
||||
#include "tracing.h"
|
||||
#include "WeaponComponent.h"
|
||||
|
||||
static constexpr Tick kWeaponImpactDelayTicks = 5;
|
||||
@@ -20,26 +21,21 @@ CombatSystem::CombatSystem(const GameConfig& config)
|
||||
void CombatSystem::tick(Tick currentTick,
|
||||
EntityAdmin& admin,
|
||||
BuildingSystem& /*buildings*/,
|
||||
std::vector<FireEvent>& outFireEvents)
|
||||
std::vector<WeaponFiredEvent>& outWeaponFiredEvents)
|
||||
{
|
||||
// Ship weapons.
|
||||
admin.forEach<WeaponComponent, ThreatResponseBehaviorComponent,
|
||||
PositionComponent, FactionComponent>(
|
||||
[&](entt::entity e, WeaponComponent& weapon,
|
||||
ThreatResponseBehaviorComponent& threatResponseBehavior,
|
||||
PositionComponent& pos, FactionComponent& faction)
|
||||
TRACE();
|
||||
// All weapons (ships and stations) are child entities linked via ModuleOwnerComponent.
|
||||
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity /*e*/, WeaponComponent& weapon, const ModuleOwnerComponent& owner)
|
||||
{
|
||||
weapon.currentTarget = threatResponseBehavior.currentTarget;
|
||||
resolveWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents);
|
||||
});
|
||||
|
||||
// Station weapons (entities with StationBodyComponent; ships are excluded because
|
||||
// they lack that component and are already handled by the ship loop above).
|
||||
admin.forEach<WeaponComponent, PositionComponent, FactionComponent, StationBodyComponent>(
|
||||
[&](entt::entity e, WeaponComponent& weapon, PositionComponent& pos,
|
||||
FactionComponent& faction, const StationBodyComponent& /*sb*/)
|
||||
if (admin.hasAll<ThreatResponseBehaviorComponent>(owner.owner))
|
||||
{
|
||||
resolveWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents);
|
||||
weapon.currentTarget =
|
||||
admin.get<ThreatResponseBehaviorComponent>(owner.owner).currentTarget;
|
||||
}
|
||||
const PositionComponent& pos = admin.get<PositionComponent>(owner.owner);
|
||||
const FactionComponent& faction = admin.get<FactionComponent>(owner.owner);
|
||||
resolveWeapon(owner.owner, weapon, pos, faction, currentTick, admin, outWeaponFiredEvents);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,7 +46,7 @@ void CombatSystem::resolveWeapon(
|
||||
const FactionComponent& ownFaction,
|
||||
Tick currentTick,
|
||||
EntityAdmin& admin,
|
||||
std::vector<FireEvent>& out)
|
||||
std::vector<WeaponFiredEvent>& out)
|
||||
{
|
||||
if (weapon.cooldownTicks > 0.0f)
|
||||
{
|
||||
@@ -73,7 +69,7 @@ void CombatSystem::resolveWeapon(
|
||||
{
|
||||
const float distanceSquared =
|
||||
(ownPos.value - admin.get<PositionComponent>(t).value).lengthSquared();
|
||||
if (distanceSquared > weapon.range * weapon.range)
|
||||
if (distanceSquared > weapon.range_tiles * weapon.range_tiles)
|
||||
{
|
||||
weapon.currentTarget = std::nullopt;
|
||||
}
|
||||
@@ -85,8 +81,8 @@ void CombatSystem::resolveWeapon(
|
||||
if (!weapon.currentTarget)
|
||||
{
|
||||
const float acquisitionRange = admin.hasAll<SensorRangeComponent>(shipEntity)
|
||||
? admin.get<SensorRangeComponent>(shipEntity).value
|
||||
: weapon.range;
|
||||
? admin.get<SensorRangeComponent>(shipEntity).value_tiles
|
||||
: weapon.range_tiles;
|
||||
float bestDistanceSquared = acquisitionRange * acquisitionRange;
|
||||
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>(
|
||||
[&](entt::entity candidate, const ShipIdentityComponent& /*si*/,
|
||||
@@ -119,7 +115,7 @@ void CombatSystem::resolveWeapon(
|
||||
m_pendingDamage.push_back({targetEntity, weapon.damage,
|
||||
currentTick + kWeaponImpactDelayTicks});
|
||||
|
||||
FireEvent evt;
|
||||
WeaponFiredEvent evt;
|
||||
evt.shooter = shipEntity;
|
||||
evt.target = targetEntity;
|
||||
evt.emittedAt = currentTick;
|
||||
@@ -130,6 +126,7 @@ void CombatSystem::resolveWeapon(
|
||||
|
||||
void CombatSystem::applyPendingDamage(Tick currentTick, EntityAdmin& admin)
|
||||
{
|
||||
TRACE();
|
||||
std::vector<PendingDamage>::iterator it = m_pendingDamage.begin();
|
||||
while (it != m_pendingDamage.end())
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#include "Building.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "FireEvent.h"
|
||||
#include "WeaponFiredEvent.h"
|
||||
#include "GameConfig.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "Tick.h"
|
||||
@@ -26,7 +26,7 @@ public:
|
||||
void tick(Tick currentTick,
|
||||
EntityAdmin& admin,
|
||||
BuildingSystem& buildings,
|
||||
std::vector<FireEvent>& outFireEvents);
|
||||
std::vector<WeaponFiredEvent>& outWeaponFiredEvents);
|
||||
|
||||
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
|
||||
|
||||
@@ -47,7 +47,7 @@ private:
|
||||
const FactionComponent& ownFaction,
|
||||
Tick currentTick,
|
||||
EntityAdmin& admin,
|
||||
std::vector<FireEvent>& out);
|
||||
std::vector<WeaponFiredEvent>& out);
|
||||
|
||||
const GameConfig& m_config;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "EntityAdmin.h"
|
||||
#include "FacingComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "tracing.h"
|
||||
|
||||
static float wrapAngle(float a)
|
||||
{
|
||||
@@ -21,31 +22,33 @@ static float wrapAngle(float a)
|
||||
|
||||
void DynamicBodySystem::tick(EntityAdmin& admin)
|
||||
{
|
||||
TRACE();
|
||||
admin.forEach<PositionComponent, FacingComponent, DynamicBodyComponent>(
|
||||
[](entt::entity /*e*/, PositionComponent& pos, FacingComponent& facing,
|
||||
DynamicBodyComponent& body)
|
||||
{
|
||||
// Integrate angular velocity, clamp to max rotation speed, then advance facing.
|
||||
body.angularVelocity += body.angularAcceleration;
|
||||
body.angularVelocity = std::max(-body.maxRotationSpeedPerTick,
|
||||
std::min(body.angularVelocity,
|
||||
body.maxRotationSpeedPerTick));
|
||||
facing.radians = wrapAngle(facing.radians + body.angularVelocity);
|
||||
body.angularVelocity_rpt += body.angularAcceleration_rptt;
|
||||
body.angularVelocity_rpt = std::max(-body.maxRotationSpeed_rpt,
|
||||
std::min(body.angularVelocity_rpt,
|
||||
body.maxRotationSpeed_rpt));
|
||||
facing.radians = wrapAngle(facing.radians + body.angularVelocity_rpt);
|
||||
|
||||
// Integrate linear velocity and cap to max speed.
|
||||
body.velocity += body.linearAcceleration;
|
||||
const float speed = body.velocity.length();
|
||||
if (speed > body.maxSpeedPerTick)
|
||||
body.velocity_tpt += body.linearAcceleration_tptt;
|
||||
const float speed = body.velocity_tpt.length();
|
||||
if (speed > body.maxSpeed_tpt)
|
||||
{
|
||||
body.velocity = body.velocity.normalized() * body.maxSpeedPerTick;
|
||||
body.velocity_tpt = body.velocity_tpt.normalized() * body.maxSpeed_tpt;
|
||||
}
|
||||
|
||||
// Advance position.
|
||||
pos.value += body.velocity;
|
||||
pos.value += body.velocity_tpt;
|
||||
|
||||
// Reset per-tick fields so stale values don't linger if the intent
|
||||
// system is skipped for this entity in a future tick.
|
||||
body.linearAcceleration = QVector2D(0.0f, 0.0f);
|
||||
body.angularAcceleration = 0.0f;
|
||||
body.linearAcceleration_tptt = QVector2D(0.0f, 0.0f);
|
||||
body.angularAcceleration_rptt = 0.0f;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "FacingComponent.h"
|
||||
#include "MovementIntentComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "tracing.h"
|
||||
|
||||
static float wrapAngle(float a)
|
||||
{
|
||||
@@ -22,6 +23,7 @@ static float wrapAngle(float a)
|
||||
|
||||
void MovementIntentSystem::tick(EntityAdmin& admin)
|
||||
{
|
||||
TRACE();
|
||||
admin.forEach<PositionComponent, FacingComponent, DynamicBodyComponent,
|
||||
MovementIntentComponent>(
|
||||
[](entt::entity /*e*/, const PositionComponent& pos, const FacingComponent& facing,
|
||||
@@ -30,16 +32,16 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
|
||||
if (intent.priority == 0)
|
||||
{
|
||||
// No movement intent: brake using available thrust.
|
||||
const float linearBraking = std::min(body.velocity.length(),
|
||||
body.maneuveringAccelerationPerTick);
|
||||
body.linearAcceleration = (body.velocity.length() > 0.0001f)
|
||||
? -body.velocity.normalized() * linearBraking
|
||||
const float linearBraking = std::min(body.velocity_tpt.length(),
|
||||
body.maneuveringAcceleration_tptt);
|
||||
body.linearAcceleration_tptt = (body.velocity_tpt.length() > 0.0001f)
|
||||
? -body.velocity_tpt.normalized() * linearBraking
|
||||
: QVector2D(0.0f, 0.0f);
|
||||
|
||||
const float angBraking = std::min(std::abs(body.angularVelocity),
|
||||
body.angularAccelerationPerTick);
|
||||
body.angularAcceleration =
|
||||
(body.angularVelocity >= 0.0f) ? -angBraking : angBraking;
|
||||
const float angBraking = std::min(std::abs(body.angularVelocity_rpt),
|
||||
body.maxAngularAcceleration_rptt);
|
||||
body.angularAcceleration_rptt =
|
||||
(body.angularVelocity_rpt >= 0.0f) ? -angBraking : angBraking;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,8 +52,8 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
|
||||
{
|
||||
// Already at target: no new thrust. The ship drifts; it will
|
||||
// re-approach next tick once it has moved away.
|
||||
body.linearAcceleration = QVector2D(0.0f, 0.0f);
|
||||
body.angularAcceleration = 0.0f;
|
||||
body.linearAcceleration_tptt = QVector2D(0.0f, 0.0f);
|
||||
body.angularAcceleration_rptt = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,11 +62,11 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
|
||||
const float desiredAngle = std::atan2(delta.y(), delta.x());
|
||||
const float angleDiff = wrapAngle(desiredAngle - facing.radians);
|
||||
|
||||
const float rotDelta = std::max(-body.angularAccelerationPerTick,
|
||||
const float rotDelta = std::max(-body.maxAngularAcceleration_rptt,
|
||||
std::min(angleDiff,
|
||||
body.angularAccelerationPerTick));
|
||||
body.maxAngularAcceleration_rptt));
|
||||
|
||||
float newAngVel = body.angularVelocity + rotDelta;
|
||||
float newAngVel = body.angularVelocity_rpt + rotDelta;
|
||||
|
||||
// Overshoot prevention: if the accumulated angular velocity already
|
||||
// exceeds the remaining angle, snap it to exactly that angle so the
|
||||
@@ -75,8 +77,8 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
|
||||
newAngVel = angleDiff;
|
||||
}
|
||||
|
||||
body.angularAcceleration = newAngVel - body.angularVelocity;
|
||||
// DynamicBodySystem applies the clamp to maxRotationSpeedPerTick after
|
||||
body.angularAcceleration_rptt = newAngVel - body.angularVelocity_rpt;
|
||||
// DynamicBodySystem applies the clamp to maxRotationSpeed_rpt after
|
||||
// integrating, so we do not clamp here.
|
||||
|
||||
// --- Linear acceleration ---
|
||||
@@ -88,22 +90,22 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
|
||||
const QVector2D facingVec(std::cos(projectedRadians),
|
||||
std::sin(projectedRadians));
|
||||
|
||||
const float manAccel = body.maneuveringAccelerationPerTick;
|
||||
const float stoppingDist = (body.maxSpeedPerTick * body.maxSpeedPerTick)
|
||||
const float manAccel = body.maneuveringAcceleration_tptt;
|
||||
const float stoppingDist = (body.maxSpeed_tpt * body.maxSpeed_tpt)
|
||||
/ (2.0f * manAccel);
|
||||
// Cap to dist so the ship never overshoots the target in a single tick.
|
||||
const float baseDesiredSpeed = (dist <= stoppingDist)
|
||||
? std::sqrt(2.0f * manAccel * dist)
|
||||
: body.maxSpeedPerTick;
|
||||
: body.maxSpeed_tpt;
|
||||
const float desiredSpeed = std::min(dist, baseDesiredSpeed);
|
||||
|
||||
const QVector2D desiredVel = delta.normalized() * desiredSpeed;
|
||||
const QVector2D velError = desiredVel - body.velocity;
|
||||
const QVector2D velError = desiredVel - body.velocity_tpt;
|
||||
|
||||
const float mainAligned = std::max(0.0f,
|
||||
QVector2D::dotProduct(velError, facingVec));
|
||||
const float mainApplied = std::min(mainAligned,
|
||||
body.mainAccelerationPerTick);
|
||||
body.mainAcceleration_tptt);
|
||||
const QVector2D mainDelta = facingVec * mainApplied;
|
||||
|
||||
const QVector2D remaining = velError - mainDelta;
|
||||
@@ -112,6 +114,7 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
|
||||
? remaining.normalized() * manAccel
|
||||
: remaining;
|
||||
|
||||
body.linearAcceleration = mainDelta + maneuverDelta;
|
||||
body.linearAcceleration_tptt = mainDelta + maneuverDelta;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "EntityAdmin.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "ScrapDataComponent.h"
|
||||
#include "tracing.h"
|
||||
|
||||
ScrapSystem::ScrapSystem(EntityAdmin& admin)
|
||||
: m_admin(admin)
|
||||
@@ -17,6 +18,7 @@ entt::entity ScrapSystem::spawn(QVector2D position, int amount, Tick despawnAt)
|
||||
|
||||
void ScrapSystem::tickDespawn(Tick currentTick)
|
||||
{
|
||||
TRACE();
|
||||
std::vector<entt::entity> expired;
|
||||
m_admin.forEach<DespawnAtComponent>(
|
||||
[&expired, currentTick](entt::entity e, DespawnAtComponent& d)
|
||||
@@ -54,3 +56,4 @@ std::vector<ScrapInfo> ScrapSystem::allScrapInfo() const
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "ModulesConfig.h"
|
||||
#include "MovementIntentComponent.h"
|
||||
#include "RallyBehaviorComponent.h"
|
||||
@@ -18,6 +21,7 @@
|
||||
#include "SensorRangeComponent.h"
|
||||
#include "Tick.h"
|
||||
#include "ThreatResponseBehaviorComponent.h"
|
||||
#include "tracing.h"
|
||||
#include "WeaponComponent.h"
|
||||
|
||||
ShipSystem::ShipSystem(const GameConfig& config, EntityAdmin& admin)
|
||||
@@ -52,101 +56,154 @@ const ModuleDef* ShipSystem::findModuleDef(const std::string& id) const
|
||||
|
||||
entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
QVector2D position, bool isEnemy,
|
||||
const std::optional<ShipLayoutConfig>& layout)
|
||||
const std::optional<ShipLayoutConfig>& layout,
|
||||
const std::map<std::string, int>& moduleLevelOverrides)
|
||||
{
|
||||
const ShipDef* def = findShipDef(schematicId);
|
||||
assert(def != nullptr);
|
||||
|
||||
const double x = static_cast<double>(level);
|
||||
const float tickRate = static_cast<float>(kTickRateHz);
|
||||
const float tileSize = static_cast<float>(m_config.world.tileSize_m);
|
||||
|
||||
float hp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
||||
float maxHp = hp;
|
||||
float maxSpeedPerTick = static_cast<float>(def->movement.speedFormula.evaluate(x))
|
||||
/ tickRate;
|
||||
float mainAccelPerTick = static_cast<float>(
|
||||
float maxSpeed_tpt = static_cast<float>(def->movement.speedFormula.evaluate(x))
|
||||
/ tileSize / tickRate;
|
||||
float mainAcceleration_tptt = static_cast<float>(
|
||||
def->movement.mainAccelerationFormula.evaluate(x))
|
||||
/ tickRate;
|
||||
float maneuveringAccelPerTick = static_cast<float>(
|
||||
/ tileSize / tickRate;
|
||||
float maneuveringAcceleration_tptt = static_cast<float>(
|
||||
def->movement.maneuveringAccelerationFormula.evaluate(x))
|
||||
/ tickRate;
|
||||
float angularAccelPerTick = static_cast<float>(
|
||||
/ tileSize / tickRate;
|
||||
float maxAngularAcceleration_rptt = static_cast<float>(
|
||||
def->movement.angularAccelerationFormula.evaluate(x))
|
||||
/ tickRate;
|
||||
float maxRotationSpeedPerTick = static_cast<float>(
|
||||
float maxRotationSpeed_rpt = static_cast<float>(
|
||||
def->movement.maxRotationSpeedFormula.evaluate(x))
|
||||
/ tickRate;
|
||||
float sensorRange = static_cast<float>(
|
||||
def->sensor.sensorRangeFormula.evaluate(x));
|
||||
float sensorRange_tiles = static_cast<float>(
|
||||
def->sensor.sensorRangeFormula.evaluate(x))
|
||||
/ tileSize;
|
||||
|
||||
entt::entity entity = m_admin.spawnShip(
|
||||
position, hp, maxHp,
|
||||
maxSpeedPerTick, mainAccelPerTick, maneuveringAccelPerTick,
|
||||
angularAccelPerTick, maxRotationSpeedPerTick, sensorRange,
|
||||
maxSpeed_tpt, mainAcceleration_tptt, maneuveringAcceleration_tptt,
|
||||
maxAngularAcceleration_rptt, maxRotationSpeed_rpt, sensorRange_tiles,
|
||||
level, schematicId, isEnemy);
|
||||
|
||||
// Optional components based on ship role.
|
||||
if (def->combat)
|
||||
{
|
||||
WeaponComponent w;
|
||||
w.damage = static_cast<float>(def->combat->damageFormula.evaluate(x));
|
||||
w.range = static_cast<float>(def->combat->attackRangeFormula.evaluate(x));
|
||||
w.fireRateHz = static_cast<float>(def->combat->attackRateFormula.evaluate(x));
|
||||
w.cooldownTicks = 0.0f;
|
||||
w.currentTarget = std::nullopt;
|
||||
m_admin.addComponent<WeaponComponent>(entity, w);
|
||||
// Determine module list: configured layout takes precedence over default.
|
||||
const std::vector<PlacedModule>& modules =
|
||||
layout.has_value() ? layout->placedModules : def->defaultModules;
|
||||
|
||||
m_admin.addComponent<ThreatResponseBehaviorComponent>(
|
||||
entity, ThreatResponseBehaviorComponent{});
|
||||
// --- Pass 1: create capability child entities ----------------------------
|
||||
std::vector<entt::entity> weaponChildren;
|
||||
std::vector<entt::entity> salvageChildren;
|
||||
std::vector<entt::entity> repairChildren;
|
||||
|
||||
if (!isEnemy)
|
||||
{
|
||||
m_admin.addComponent<RallyBehaviorComponent>(
|
||||
entity, RallyBehaviorComponent{m_rallyPoint});
|
||||
}
|
||||
}
|
||||
|
||||
if (def->salvage)
|
||||
{
|
||||
SalvageCargoComponent cargo;
|
||||
cargo.capacity = def->salvage->cargoCapacity;
|
||||
cargo.current = 0;
|
||||
cargo.collectionRange = static_cast<float>(def->salvage->collectionRange);
|
||||
m_admin.addComponent<SalvageCargoComponent>(entity, cargo);
|
||||
|
||||
SalvageBehaviorComponent salvageBehavior;
|
||||
salvageBehavior.scrapTarget = std::nullopt;
|
||||
salvageBehavior.deliveryBay = kInvalidBuildingId;
|
||||
m_admin.addComponent<SalvageBehaviorComponent>(entity, salvageBehavior);
|
||||
}
|
||||
|
||||
if (def->repair)
|
||||
{
|
||||
RepairToolComponent rt;
|
||||
rt.ratePerTick = static_cast<float>(def->repair->repairRateFormula.evaluate(x));
|
||||
rt.range = static_cast<float>(def->repair->repairRangeFormula.evaluate(x));
|
||||
rt.currentTarget = std::nullopt;
|
||||
m_admin.addComponent<RepairToolComponent>(entity, rt);
|
||||
|
||||
m_admin.addComponent<RepairBehaviorComponent>(entity, RepairBehaviorComponent{});
|
||||
}
|
||||
|
||||
// Apply module stat modifiers (REQ-MOD-STAT-CALC).
|
||||
if (layout.has_value() && !layout->placedModules.empty())
|
||||
{
|
||||
std::map<std::string, std::pair<double, double>> mods;
|
||||
for (const PlacedModule& pm : layout->placedModules)
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
if (!modDef)
|
||||
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||
|
||||
const auto overIt = moduleLevelOverrides.find(pm.moduleId);
|
||||
const double mx = static_cast<double>(
|
||||
overIt != moduleLevelOverrides.end() ? overIt->second : modDef->playerProductionLevel);
|
||||
|
||||
if (modDef->weaponCapability)
|
||||
{
|
||||
continue;
|
||||
WeaponComponent w;
|
||||
w.damage = static_cast<float>(
|
||||
modDef->weaponCapability->damageFormula.evaluate(mx));
|
||||
w.range_tiles = static_cast<float>(
|
||||
modDef->weaponCapability->attackRangeFormula.evaluate(mx)) / tileSize;
|
||||
w.fireRateHz = static_cast<float>(
|
||||
modDef->weaponCapability->attackRateFormula.evaluate(mx));
|
||||
w.cooldownTicks = 0.0f;
|
||||
w.currentTarget = std::nullopt;
|
||||
|
||||
entt::entity child = m_admin.createModuleEntity();
|
||||
m_admin.addComponent<WeaponComponent>(child, w);
|
||||
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
|
||||
weaponChildren.push_back(child);
|
||||
}
|
||||
|
||||
if (modDef->salvageCapability)
|
||||
{
|
||||
SalvageCargoComponent cargo;
|
||||
cargo.capacity = static_cast<int>(
|
||||
modDef->salvageCapability->cargoCapacityFormula.evaluate(mx));
|
||||
cargo.current = 0;
|
||||
cargo.collectionRange_tiles = static_cast<float>(
|
||||
modDef->salvageCapability->collectionRangeFormula.evaluate(mx)) / tileSize;
|
||||
const double rate = modDef->salvageCapability->collectionRateFormula.evaluate(mx);
|
||||
cargo.collectionIntervalTicks = (rate > 0.0)
|
||||
? static_cast<int>(kTickRateHz / rate + 0.5)
|
||||
: 0;
|
||||
cargo.cooldownTicksRemaining = 0;
|
||||
|
||||
entt::entity child = m_admin.createModuleEntity();
|
||||
m_admin.addComponent<SalvageCargoComponent>(child, cargo);
|
||||
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
|
||||
salvageChildren.push_back(child);
|
||||
}
|
||||
|
||||
if (modDef->repairCapability)
|
||||
{
|
||||
RepairToolComponent rt;
|
||||
rt.ratePerTick = static_cast<float>(
|
||||
modDef->repairCapability->repairRateFormula.evaluate(mx))
|
||||
/ static_cast<float>(kTickRateHz);
|
||||
rt.range_tiles = static_cast<float>(
|
||||
modDef->repairCapability->repairRangeFormula.evaluate(mx)) / tileSize;
|
||||
rt.currentTarget = std::nullopt;
|
||||
|
||||
entt::entity child = m_admin.createModuleEntity();
|
||||
m_admin.addComponent<RepairToolComponent>(child, rt);
|
||||
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
|
||||
repairChildren.push_back(child);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Pass 2: apply passive stat modifiers --------------------------------
|
||||
|
||||
// Accumulate hull-level modifiers.
|
||||
std::map<std::string, std::pair<double, double>> hullMods;
|
||||
// Per-capability-type modifier accumulators (applied to each child).
|
||||
std::map<std::string, std::pair<double, double>> weaponMods;
|
||||
std::map<std::string, std::pair<double, double>> salvageMods;
|
||||
std::map<std::string, std::pair<double, double>> repairMods;
|
||||
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||
|
||||
const auto overIt2 = moduleLevelOverrides.find(pm.moduleId);
|
||||
const double mx = static_cast<double>(
|
||||
overIt2 != moduleLevelOverrides.end() ? overIt2->second : modDef->playerProductionLevel);
|
||||
|
||||
for (const ModuleStatModifier& sm : modDef->statModifiers)
|
||||
{
|
||||
const double val = sm.formula.evaluate(
|
||||
static_cast<double>(modDef->playerProductionLevel));
|
||||
std::pair<double, double>& acc = mods[sm.stat];
|
||||
const double val = sm.formula.evaluate(mx);
|
||||
|
||||
// Route modifier to the correct accumulator by stat category.
|
||||
// weapon/salvage/repair stats go to the corresponding child map;
|
||||
// hull stats (hp, speed, sensor_range, …) go to hullMods.
|
||||
const bool isWeaponStat = (sm.stat == "damage"
|
||||
|| sm.stat == "attack_range"
|
||||
|| sm.stat == "attack_rate");
|
||||
const bool isSalvageStat = (sm.stat == "collection_range"
|
||||
|| sm.stat == "cargo_capacity");
|
||||
const bool isRepairStat = (sm.stat == "repair_rate"
|
||||
|| sm.stat == "repair_range");
|
||||
|
||||
std::map<std::string, std::pair<double, double>>* target = &hullMods;
|
||||
if (isWeaponStat) { target = &weaponMods; }
|
||||
if (isSalvageStat) { target = &salvageMods; }
|
||||
if (isRepairStat) { target = &repairMods; }
|
||||
|
||||
std::pair<double, double>& acc = (*target)[sm.stat];
|
||||
if (sm.modifierType == "multiplicative")
|
||||
{
|
||||
acc.first += (val - 1.0);
|
||||
@@ -158,9 +215,49 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
}
|
||||
}
|
||||
|
||||
auto applyMod = [&mods](float& stat, const std::string& name) {
|
||||
const std::map<std::string, std::pair<double, double>>::const_iterator it =
|
||||
mods.find(name);
|
||||
// Range stat additive modifiers are expressed in metres in config; convert to tiles.
|
||||
const double tileSizeD = static_cast<double>(m_config.world.tileSize_m);
|
||||
const double tickRateD = static_cast<double>(kTickRateHz);
|
||||
const char* const kRangeStats[] = {
|
||||
"sensor_range", "attack_range", "collection_range", "repair_range"
|
||||
};
|
||||
std::map<std::string, std::pair<double, double>>* allModMaps[] = {
|
||||
&hullMods, &weaponMods, &salvageMods, &repairMods
|
||||
};
|
||||
for (const char* stat : kRangeStats)
|
||||
{
|
||||
for (std::map<std::string, std::pair<double, double>>* mods : allModMaps)
|
||||
{
|
||||
std::map<std::string, std::pair<double, double>>::iterator it = mods->find(stat);
|
||||
if (it != mods->end())
|
||||
{
|
||||
it->second.second /= tileSizeD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Acceleration additive modifiers are in m/s² in config; convert to tiles/tick
|
||||
// (same as the base spawn conversion: / tileSize / tickRate).
|
||||
const char* const kAccelerationStats[] = {
|
||||
"main_acceleration", "maneuvering_acceleration"
|
||||
};
|
||||
for (const char* stat : kAccelerationStats)
|
||||
{
|
||||
for (std::map<std::string, std::pair<double, double>>* mods : allModMaps)
|
||||
{
|
||||
std::map<std::string, std::pair<double, double>>::iterator it = mods->find(stat);
|
||||
if (it != mods->end())
|
||||
{
|
||||
it->second.second /= tileSizeD * tickRateD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: apply a modifier map to a float stat.
|
||||
auto applyMod = [](float& stat, const std::string& name,
|
||||
const std::map<std::string, std::pair<double, double>>& mods)
|
||||
{
|
||||
const auto it = mods.find(name);
|
||||
if (it != mods.end())
|
||||
{
|
||||
stat = static_cast<float>(
|
||||
@@ -169,32 +266,102 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
}
|
||||
};
|
||||
|
||||
// Apply hull modifiers.
|
||||
{
|
||||
HealthComponent& health = m_admin.get<HealthComponent>(entity);
|
||||
DynamicBodyComponent& dynamics = m_admin.get<DynamicBodyComponent>(entity);
|
||||
SensorRangeComponent& sensor = m_admin.get<SensorRangeComponent>(entity);
|
||||
|
||||
applyMod(health.maxHp, "hp");
|
||||
applyMod(health.maxHp, "hp", hullMods);
|
||||
health.hp = health.maxHp;
|
||||
applyMod(dynamics.maxSpeedPerTick, "speed");
|
||||
applyMod(dynamics.mainAccelerationPerTick, "main_acceleration");
|
||||
applyMod(dynamics.maneuveringAccelerationPerTick, "maneuvering_acceleration");
|
||||
applyMod(dynamics.angularAccelerationPerTick, "angular_acceleration");
|
||||
applyMod(dynamics.maxRotationSpeedPerTick, "max_rotation_speed");
|
||||
applyMod(sensor.value, "sensor_range");
|
||||
applyMod(dynamics.maxSpeed_tpt, "speed", hullMods);
|
||||
applyMod(dynamics.mainAcceleration_tptt, "main_acceleration", hullMods);
|
||||
applyMod(dynamics.maneuveringAcceleration_tptt, "maneuvering_acceleration", hullMods);
|
||||
applyMod(dynamics.maxAngularAcceleration_rptt, "angular_acceleration", hullMods);
|
||||
applyMod(dynamics.maxRotationSpeed_rpt, "max_rotation_speed", hullMods);
|
||||
applyMod(sensor.value_tiles, "sensor_range", hullMods);
|
||||
}
|
||||
|
||||
if (m_admin.hasAll<WeaponComponent>(entity))
|
||||
// Apply weapon modifiers to each weapon child.
|
||||
for (entt::entity child : weaponChildren)
|
||||
{
|
||||
WeaponComponent& weapon = m_admin.get<WeaponComponent>(entity);
|
||||
applyMod(weapon.damage, "damage");
|
||||
applyMod(weapon.range, "attack_range");
|
||||
applyMod(weapon.fireRateHz, "attack_rate");
|
||||
WeaponComponent& w = m_admin.get<WeaponComponent>(child);
|
||||
applyMod(w.damage, "damage", weaponMods);
|
||||
applyMod(w.range_tiles, "attack_range", weaponMods);
|
||||
applyMod(w.fireRateHz, "attack_rate", weaponMods);
|
||||
}
|
||||
if (m_admin.hasAll<RepairToolComponent>(entity))
|
||||
|
||||
// Apply salvage modifiers to each salvage child.
|
||||
for (entt::entity child : salvageChildren)
|
||||
{
|
||||
RepairToolComponent& repairTool = m_admin.get<RepairToolComponent>(entity);
|
||||
applyMod(repairTool.ratePerTick, "repair_rate");
|
||||
applyMod(repairTool.range, "repair_range");
|
||||
SalvageCargoComponent& c = m_admin.get<SalvageCargoComponent>(child);
|
||||
float fRange = c.collectionRange_tiles;
|
||||
float fCapacity = static_cast<float>(c.capacity);
|
||||
// Apply rate modifier: compute rate from interval, apply multiplier, convert back.
|
||||
float fRate = (c.collectionIntervalTicks > 0)
|
||||
? static_cast<float>(kTickRateHz) / static_cast<float>(c.collectionIntervalTicks)
|
||||
: 0.0f;
|
||||
applyMod(fRange, "collection_range", salvageMods);
|
||||
applyMod(fCapacity, "cargo_capacity", salvageMods);
|
||||
applyMod(fRate, "collection_rate", salvageMods);
|
||||
c.collectionRange_tiles = fRange;
|
||||
c.capacity = static_cast<int>(fCapacity + 0.5f);
|
||||
c.collectionIntervalTicks = (fRate > 0.0f)
|
||||
? static_cast<int>(static_cast<float>(kTickRateHz) / fRate + 0.5f)
|
||||
: 0;
|
||||
}
|
||||
|
||||
// Apply repair modifiers to each repair child.
|
||||
for (entt::entity child : repairChildren)
|
||||
{
|
||||
RepairToolComponent& rt = m_admin.get<RepairToolComponent>(child);
|
||||
applyMod(rt.ratePerTick, "repair_rate", repairMods);
|
||||
applyMod(rt.range_tiles, "repair_range", repairMods);
|
||||
}
|
||||
|
||||
// --- Pass 3: attach behavior components based on capability presence -----
|
||||
|
||||
if (!weaponChildren.empty())
|
||||
{
|
||||
m_admin.addComponent<ThreatResponseBehaviorComponent>(
|
||||
entity, ThreatResponseBehaviorComponent{});
|
||||
|
||||
if (!isEnemy)
|
||||
{
|
||||
m_admin.addComponent<RallyBehaviorComponent>(
|
||||
entity, RallyBehaviorComponent{m_rallyPoint});
|
||||
}
|
||||
}
|
||||
|
||||
if (!salvageChildren.empty())
|
||||
{
|
||||
float maxCollRange = 0.0f;
|
||||
for (entt::entity child : salvageChildren)
|
||||
{
|
||||
const float r = m_admin.get<SalvageCargoComponent>(child).collectionRange_tiles;
|
||||
if (r > maxCollRange) { maxCollRange = r; }
|
||||
}
|
||||
|
||||
SalvageBehaviorComponent sb;
|
||||
sb.scrapTarget = std::nullopt;
|
||||
sb.deliveryBay = kInvalidBuildingId;
|
||||
sb.maxCollectionRange_tiles = maxCollRange;
|
||||
m_admin.addComponent<SalvageBehaviorComponent>(entity, sb);
|
||||
}
|
||||
|
||||
if (!repairChildren.empty())
|
||||
{
|
||||
float maxRepairRange = 0.0f;
|
||||
for (entt::entity child : repairChildren)
|
||||
{
|
||||
const float r = m_admin.get<RepairToolComponent>(child).range_tiles;
|
||||
if (r > maxRepairRange) { maxRepairRange = r; }
|
||||
}
|
||||
|
||||
RepairBehaviorComponent rb;
|
||||
rb.currentTarget = std::nullopt;
|
||||
rb.maxRepairRange_tiles = maxRepairRange;
|
||||
m_admin.addComponent<RepairBehaviorComponent>(entity, rb);
|
||||
}
|
||||
|
||||
return entity;
|
||||
@@ -202,11 +369,19 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
|
||||
void ShipSystem::despawn(entt::entity entity)
|
||||
{
|
||||
std::vector<entt::entity> children;
|
||||
m_admin.forEach<ModuleOwnerComponent>(
|
||||
[&](entt::entity e, const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (o.owner == entity) { children.push_back(e); }
|
||||
});
|
||||
for (entt::entity child : children) { m_admin.destroy(child); }
|
||||
m_admin.destroy(entity);
|
||||
}
|
||||
|
||||
void ShipSystem::clearMovementIntents()
|
||||
{
|
||||
TRACE();
|
||||
m_admin.forEach<MovementIntentComponent>(
|
||||
[](entt::entity /*e*/, MovementIntentComponent& i)
|
||||
{
|
||||
@@ -221,6 +396,7 @@ void ShipSystem::setRallyPoint(QVector2D point)
|
||||
|
||||
void ShipSystem::triggerRallyDeparture()
|
||||
{
|
||||
TRACE();
|
||||
std::vector<entt::entity> toRemove;
|
||||
m_admin.forEach<RallyBehaviorComponent, FactionComponent>(
|
||||
[&toRemove](entt::entity e, const RallyBehaviorComponent& /*rb*/,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
@@ -19,7 +20,8 @@ public:
|
||||
|
||||
entt::entity spawn(const std::string& schematicId, int level, QVector2D position,
|
||||
bool isEnemy = false,
|
||||
const std::optional<ShipLayoutConfig>& layout = std::nullopt);
|
||||
const std::optional<ShipLayoutConfig>& layout = std::nullopt,
|
||||
const std::map<std::string, int>& moduleLevelOverrides = {});
|
||||
void despawn(entt::entity entity);
|
||||
|
||||
// Reset all movement intents to priority 0 before behavior systems run.
|
||||
|
||||
23
src/lib/eventsystem/CMakeLists.txt
Normal file
23
src/lib/eventsystem/CMakeLists.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
add_subdirectory(event)
|
||||
|
||||
SET(HDRS
|
||||
${HDRS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Event.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EventHandler.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EventHandlerBase.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EventManager.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
SET(SRCS
|
||||
${SRCS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EventHandlerBase.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EventManager.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
SET(LIB_INCLUDE_PATH
|
||||
${LIB_INCLUDE_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PARENT_SCOPE
|
||||
)
|
||||
10
src/lib/eventsystem/Event.h
Normal file
10
src/lib/eventsystem/Event.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#ifndef EVENT_H
|
||||
#define EVENT_H
|
||||
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
virtual ~Event() = default;
|
||||
};
|
||||
|
||||
#endif // EVENT_H
|
||||
80
src/lib/eventsystem/EventHandler.h
Normal file
80
src/lib/eventsystem/EventHandler.h
Normal file
@@ -0,0 +1,80 @@
|
||||
#ifndef EVENT_HANDLER_H
|
||||
#define EVENT_HANDLER_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "EventHandlerBase.h"
|
||||
#include "EventManager.h"
|
||||
|
||||
template <typename T>
|
||||
class EventHandler: public EventHandlerBase
|
||||
{
|
||||
public:
|
||||
void registerForEvent()
|
||||
{
|
||||
EventManager::getInstance()->registerEventHandler(this);
|
||||
}
|
||||
|
||||
void unregisterForEvent()
|
||||
{
|
||||
EventManager::getInstance()->unregisterEventHandler(this);
|
||||
}
|
||||
|
||||
void handleBaseEvent(std::shared_ptr<const Event> event) override
|
||||
{
|
||||
std::shared_ptr<const T> specificEvent = std::dynamic_pointer_cast<const T>(event);
|
||||
if (specificEvent)
|
||||
{
|
||||
handleEvent(specificEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
virtual void handleEvent(std::shared_ptr<const T> event) = 0;
|
||||
};
|
||||
|
||||
|
||||
template <typename... Ts> class CombinedEventHandlerHelper
|
||||
{
|
||||
protected:
|
||||
void registerForEventsHelper() {}
|
||||
void unregisterForEventsHelper() {}
|
||||
};
|
||||
|
||||
|
||||
template <typename T, typename... Ts>
|
||||
class CombinedEventHandlerHelper<T, Ts...>
|
||||
: public EventHandler<T>
|
||||
, public CombinedEventHandlerHelper<Ts...>
|
||||
{
|
||||
protected:
|
||||
void registerForEventsHelper()
|
||||
{
|
||||
EventHandler<T>::registerForEvent();
|
||||
CombinedEventHandlerHelper<Ts...>::registerForEventsHelper();
|
||||
}
|
||||
|
||||
void unregisterForEventsHelper()
|
||||
{
|
||||
EventHandler<T>::unregisterForEvent();
|
||||
CombinedEventHandlerHelper<Ts...>::unregisterForEventsHelper();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
template <typename... Ts>
|
||||
class CombinedEventHandler: public CombinedEventHandlerHelper<Ts...>
|
||||
{
|
||||
protected:
|
||||
void registerForEvents()
|
||||
{
|
||||
CombinedEventHandlerHelper<Ts...>::registerForEventsHelper();
|
||||
}
|
||||
|
||||
void unregisterForEvents()
|
||||
{
|
||||
CombinedEventHandlerHelper<Ts...>::unregisterForEventsHelper();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EVENT_HANDLER_H
|
||||
7
src/lib/eventsystem/EventHandlerBase.cpp
Normal file
7
src/lib/eventsystem/EventHandlerBase.cpp
Normal file
@@ -0,0 +1,7 @@
|
||||
#include "EventHandlerBase.h"
|
||||
|
||||
unsigned int EventHandlerBase::s_nextId = 0;
|
||||
|
||||
EventHandlerBase::EventHandlerBase(): m_id(s_nextId++)
|
||||
{
|
||||
}
|
||||
25
src/lib/eventsystem/EventHandlerBase.h
Normal file
25
src/lib/eventsystem/EventHandlerBase.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#ifndef EVENT_HANDLER_BASE_H
|
||||
#define EVENT_HANDLER_BASE_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
class Event;
|
||||
class EventManager;
|
||||
|
||||
class EventHandlerBase
|
||||
{
|
||||
private:
|
||||
static unsigned int s_nextId;
|
||||
|
||||
public:
|
||||
EventHandlerBase();
|
||||
virtual ~EventHandlerBase() = default;
|
||||
virtual void handleBaseEvent(std::shared_ptr<const Event> event) = 0;
|
||||
|
||||
private:
|
||||
const unsigned int m_id;
|
||||
|
||||
friend EventManager;
|
||||
};
|
||||
|
||||
#endif // EVENT_HANDLER_BASE_H
|
||||
129
src/lib/eventsystem/EventManager.cpp
Normal file
129
src/lib/eventsystem/EventManager.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "EventManager.h"
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "EventHandlerBase.h"
|
||||
|
||||
std::shared_ptr<EventManager> EventManager::getInstance()
|
||||
{
|
||||
if (!s_instance)
|
||||
{
|
||||
s_instance = std::shared_ptr<EventManager>(new EventManager());
|
||||
}
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
void EventManager::destroyInstance()
|
||||
{
|
||||
if (s_instance)
|
||||
{
|
||||
s_instance.reset();
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<EventManager> EventManager::s_instance = std::shared_ptr<EventManager>();
|
||||
|
||||
void EventManager::registerEventHandler(EventHandlerBase *eventHandler)
|
||||
{
|
||||
std::scoped_lock<std::mutex> lock(m_eventHandlersMutex);
|
||||
m_eventHandlers[eventHandler->m_id] = eventHandler;
|
||||
}
|
||||
|
||||
void EventManager::unregisterEventHandler(EventHandlerBase *eventHandler)
|
||||
{
|
||||
std::scoped_lock<std::mutex> lock(m_eventHandlersMutex);
|
||||
m_eventHandlers.erase(eventHandler->m_id);
|
||||
}
|
||||
|
||||
void EventManager::sendEventImmediately(std::shared_ptr<Event> event)
|
||||
{
|
||||
if (event)
|
||||
{
|
||||
std::set<unsigned int> eventHandlerIds;
|
||||
{
|
||||
std::scoped_lock<std::mutex> lock(m_eventHandlersMutex);
|
||||
for (auto it : m_eventHandlers)
|
||||
{
|
||||
eventHandlerIds.insert(it.first);
|
||||
}
|
||||
}
|
||||
|
||||
for (unsigned int id : eventHandlerIds)
|
||||
{
|
||||
// this is necessary to allow HandleBaseEvent() to remove event handlers without causing a crash
|
||||
EventHandlerBase *eventHandler = nullptr;
|
||||
{
|
||||
std::scoped_lock<std::mutex> lock(m_eventHandlersMutex);
|
||||
auto it = m_eventHandlers.find(id);
|
||||
if (it != m_eventHandlers.end())
|
||||
{
|
||||
eventHandler = it->second;
|
||||
}
|
||||
}
|
||||
if (eventHandler)
|
||||
{
|
||||
eventHandler->handleBaseEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EventManager::addEvent(std::shared_ptr<Event> event)
|
||||
{
|
||||
if (event)
|
||||
{
|
||||
std::scoped_lock<std::mutex> lock(m_eventsMutex);
|
||||
m_events.push_back(event);
|
||||
}
|
||||
}
|
||||
|
||||
void EventManager::processEvents()
|
||||
{
|
||||
std::vector<std::shared_ptr<Event>> events;
|
||||
{
|
||||
std::scoped_lock<std::mutex> lock(m_eventsMutex);
|
||||
events = m_events;
|
||||
m_events.clear();
|
||||
}
|
||||
|
||||
std::set<unsigned int> eventHandlerIds;
|
||||
{
|
||||
std::scoped_lock<std::mutex> lock(m_eventHandlersMutex);
|
||||
for (auto it : m_eventHandlers)
|
||||
{
|
||||
eventHandlerIds.insert(it.first);
|
||||
}
|
||||
}
|
||||
|
||||
for (std::shared_ptr<Event> event : events)
|
||||
{
|
||||
for (unsigned int id : eventHandlerIds)
|
||||
{
|
||||
// this is necessary to allow HandleBaseEvent() to remove event handlers without causing a crash
|
||||
EventHandlerBase *eventHandler = nullptr;
|
||||
{
|
||||
std::scoped_lock<std::mutex> lock(m_eventHandlersMutex);
|
||||
auto it = m_eventHandlers.find(id);
|
||||
if (it != m_eventHandlers.end())
|
||||
{
|
||||
eventHandler = it->second;
|
||||
}
|
||||
}
|
||||
if (eventHandler)
|
||||
{
|
||||
eventHandler->handleBaseEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EventManager::clearEvents()
|
||||
{
|
||||
std::scoped_lock<std::mutex> lock(m_eventsMutex);
|
||||
m_events.clear();
|
||||
}
|
||||
|
||||
bool EventManager::hasEvents() const
|
||||
{
|
||||
return !m_events.empty();
|
||||
}
|
||||
44
src/lib/eventsystem/EventManager.h
Normal file
44
src/lib/eventsystem/EventManager.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifndef EVENT_MANAGER_H
|
||||
#define EVENT_MANAGER_H
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class EventHandlerBase;
|
||||
|
||||
class EventManager
|
||||
{
|
||||
public:
|
||||
static std::shared_ptr<EventManager> getInstance();
|
||||
static void destroyInstance();
|
||||
|
||||
private:
|
||||
static std::shared_ptr<EventManager> s_instance;
|
||||
|
||||
public:
|
||||
EventManager(EventManager const &) = delete;
|
||||
void operator=(EventManager const &) = delete;
|
||||
|
||||
void registerEventHandler(EventHandlerBase *eventHandler);
|
||||
void unregisterEventHandler(EventHandlerBase *eventHandler);
|
||||
void sendEventImmediately(std::shared_ptr<Event> event);
|
||||
void addEvent(std::shared_ptr<Event> event);
|
||||
void processEvents();
|
||||
void clearEvents();
|
||||
|
||||
bool hasEvents() const;
|
||||
|
||||
private:
|
||||
EventManager() = default;
|
||||
|
||||
std::map<unsigned int, EventHandlerBase *> m_eventHandlers;
|
||||
std::vector<std::shared_ptr<Event>> m_events;
|
||||
std::mutex m_eventHandlersMutex;
|
||||
std::mutex m_eventsMutex;
|
||||
};
|
||||
|
||||
#endif // EVENT_MANAGER_H
|
||||
10
src/lib/eventsystem/event/ArenaInspectRequestedEvent.h
Normal file
10
src/lib/eventsystem/event/ArenaInspectRequestedEvent.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class ArenaInspectRequestedEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit ArenaInspectRequestedEvent(int arenaIndex) : arenaIndex(arenaIndex) {}
|
||||
const int arenaIndex;
|
||||
};
|
||||
10
src/lib/eventsystem/event/ArenaStartRequestedEvent.h
Normal file
10
src/lib/eventsystem/event/ArenaStartRequestedEvent.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class ArenaStartRequestedEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit ArenaStartRequestedEvent(int arenaIndex) : arenaIndex(arenaIndex) {}
|
||||
const int arenaIndex;
|
||||
};
|
||||
7
src/lib/eventsystem/event/BlueprintModeExitedEvent.h
Normal file
7
src/lib/eventsystem/event/BlueprintModeExitedEvent.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class BlueprintModeExitedEvent : public Event
|
||||
{
|
||||
};
|
||||
12
src/lib/eventsystem/event/BlueprintPlacementRequestedEvent.h
Normal file
12
src/lib/eventsystem/event/BlueprintPlacementRequestedEvent.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "Blueprint.h"
|
||||
#include "Event.h"
|
||||
|
||||
class BlueprintPlacementRequestedEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit BlueprintPlacementRequestedEvent(Blueprint blueprint)
|
||||
: blueprint(std::move(blueprint)) {}
|
||||
const Blueprint blueprint;
|
||||
};
|
||||
17
src/lib/eventsystem/event/BossWaveUpdatedEvent.h
Normal file
17
src/lib/eventsystem/event/BossWaveUpdatedEvent.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef BOSS_WAVE_UPDATED_EVENT_H
|
||||
#define BOSS_WAVE_UPDATED_EVENT_H
|
||||
|
||||
#include "Event.h"
|
||||
#include "Tick.h"
|
||||
|
||||
class BossWaveUpdatedEvent : public Event
|
||||
{
|
||||
public:
|
||||
BossWaveUpdatedEvent(int counter, Tick countdownTicks)
|
||||
: counter(counter), countdownTicks(countdownTicks) {}
|
||||
const int counter;
|
||||
const Tick countdownTicks;
|
||||
};
|
||||
|
||||
#endif // BOSS_WAVE_UPDATED_EVENT_H
|
||||
|
||||
7
src/lib/eventsystem/event/BuilderModeExitedEvent.h
Normal file
7
src/lib/eventsystem/event/BuilderModeExitedEvent.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class BuilderModeExitedEvent : public Event
|
||||
{
|
||||
};
|
||||
14
src/lib/eventsystem/event/BuildingBlocksChangedEvent.h
Normal file
14
src/lib/eventsystem/event/BuildingBlocksChangedEvent.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef BUILDING_BLOCKS_CHANGED_EVENT_H
|
||||
#define BUILDING_BLOCKS_CHANGED_EVENT_H
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class BuildingBlocksChangedEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit BuildingBlocksChangedEvent(int blocks) : blocks(blocks) {}
|
||||
const int blocks;
|
||||
};
|
||||
|
||||
#endif // BUILDING_BLOCKS_CHANGED_EVENT_H
|
||||
|
||||
11
src/lib/eventsystem/event/BuildingTypeSelectedEvent.h
Normal file
11
src/lib/eventsystem/event/BuildingTypeSelectedEvent.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "BuildingType.h"
|
||||
#include "Event.h"
|
||||
|
||||
class BuildingTypeSelectedEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit BuildingTypeSelectedEvent(BuildingType type) : type(type) {}
|
||||
const BuildingType type;
|
||||
};
|
||||
41
src/lib/eventsystem/event/CMakeLists.txt
Normal file
41
src/lib/eventsystem/event/CMakeLists.txt
Normal file
@@ -0,0 +1,41 @@
|
||||
SET(HDRS
|
||||
${HDRS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TracePrintRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TickAdvancedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBlocksChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuilderModeExitedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintModeExitedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EscapeMenuRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTypeSelectedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ExitBuilderModeRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeToggleRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPlacementRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ExitBlueprintModeRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SpeedChangeRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/LayoutDialogRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindowClosedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaStartRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaInspectRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WeaponFiredEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DebugDrawToggledEvent.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
SET(SRCS
|
||||
${SRCS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TracePrintRequestedEvent.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
set(LIB_INCLUDE_PATH
|
||||
${LIB_INCLUDE_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PARENT_SCOPE
|
||||
)
|
||||
14
src/lib/eventsystem/event/DebugDrawToggledEvent.h
Normal file
14
src/lib/eventsystem/event/DebugDrawToggledEvent.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class DebugDrawToggledEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit DebugDrawToggledEvent(bool active)
|
||||
: active(active)
|
||||
{
|
||||
}
|
||||
|
||||
const bool active;
|
||||
};
|
||||
10
src/lib/eventsystem/event/DemolishModeChangedEvent.h
Normal file
10
src/lib/eventsystem/event/DemolishModeChangedEvent.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class DemolishModeChangedEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit DemolishModeChangedEvent(bool active) : active(active) {}
|
||||
const bool active;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class DemolishModeToggleRequestedEvent : public Event
|
||||
{
|
||||
};
|
||||
21
src/lib/eventsystem/event/EntitySelectedEvent.h
Normal file
21
src/lib/eventsystem/event/EntitySelectedEvent.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef ENTITY_SELECTED_EVENT_H
|
||||
#define ENTITY_SELECTED_EVENT_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class EntitySelectedEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit EntitySelectedEvent(std::optional<entt::entity> entity)
|
||||
: entity(entity)
|
||||
{
|
||||
}
|
||||
|
||||
const std::optional<entt::entity> entity;
|
||||
};
|
||||
|
||||
#endif // ENTITY_SELECTED_EVENT_H
|
||||
7
src/lib/eventsystem/event/EscapeMenuRequestedEvent.h
Normal file
7
src/lib/eventsystem/event/EscapeMenuRequestedEvent.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class EscapeMenuRequestedEvent : public Event
|
||||
{
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class ExitBlueprintModeRequestedEvent : public Event
|
||||
{
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class ExitBuilderModeRequestedEvent : public Event
|
||||
{
|
||||
};
|
||||
7
src/lib/eventsystem/event/GameOverEvent.h
Normal file
7
src/lib/eventsystem/event/GameOverEvent.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class GameOverEvent : public Event
|
||||
{
|
||||
};
|
||||
14
src/lib/eventsystem/event/GameSpeedChangedEvent.h
Normal file
14
src/lib/eventsystem/event/GameSpeedChangedEvent.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef GAME_SPEED_CHANGED_EVENT_H
|
||||
#define GAME_SPEED_CHANGED_EVENT_H
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class GameSpeedChangedEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit GameSpeedChangedEvent(double speed) : speed(speed) {}
|
||||
const double speed;
|
||||
};
|
||||
|
||||
#endif // GAME_SPEED_CHANGED_EVENT_H
|
||||
|
||||
7
src/lib/eventsystem/event/InspectWindowClosedEvent.h
Normal file
7
src/lib/eventsystem/event/InspectWindowClosedEvent.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class InspectWindowClosedEvent : public Event
|
||||
{
|
||||
};
|
||||
12
src/lib/eventsystem/event/LayoutDialogRequestedEvent.h
Normal file
12
src/lib/eventsystem/event/LayoutDialogRequestedEvent.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "BuildingId.h"
|
||||
#include "Event.h"
|
||||
|
||||
class LayoutDialogRequestedEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit LayoutDialogRequestedEvent(BuildingId shipyardId)
|
||||
: shipyardId(shipyardId) {}
|
||||
const BuildingId shipyardId;
|
||||
};
|
||||
14
src/lib/eventsystem/event/SchematicChoicesAvailableEvent.h
Normal file
14
src/lib/eventsystem/event/SchematicChoicesAvailableEvent.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "Event.h"
|
||||
#include "SchematicChoiceOption.h"
|
||||
|
||||
class SchematicChoicesAvailableEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit SchematicChoicesAvailableEvent(std::vector<SchematicChoiceOption> choices)
|
||||
: choices(std::move(choices)) {}
|
||||
const std::vector<SchematicChoiceOption> choices;
|
||||
};
|
||||
14
src/lib/eventsystem/event/SelectionChangedEvent.h
Normal file
14
src/lib/eventsystem/event/SelectionChangedEvent.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "BuildingId.h"
|
||||
#include "Event.h"
|
||||
|
||||
class SelectionChangedEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit SelectionChangedEvent(std::vector<BuildingId> ids)
|
||||
: ids(std::move(ids)) {}
|
||||
const std::vector<BuildingId> ids;
|
||||
};
|
||||
10
src/lib/eventsystem/event/SpeedChangeRequestedEvent.h
Normal file
10
src/lib/eventsystem/event/SpeedChangeRequestedEvent.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class SpeedChangeRequestedEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit SpeedChangeRequestedEvent(double multiplier) : multiplier(multiplier) {}
|
||||
const double multiplier;
|
||||
};
|
||||
15
src/lib/eventsystem/event/TickAdvancedEvent.h
Normal file
15
src/lib/eventsystem/event/TickAdvancedEvent.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#ifndef TICK_ADVANCED_EVENT_H
|
||||
#define TICK_ADVANCED_EVENT_H
|
||||
|
||||
#include "Event.h"
|
||||
#include "Tick.h"
|
||||
|
||||
class TickAdvancedEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit TickAdvancedEvent(Tick tick) : tick(tick) {}
|
||||
const Tick tick;
|
||||
};
|
||||
|
||||
#endif // TICK_ADVANCED_EVENT_H
|
||||
|
||||
1
src/lib/eventsystem/event/TracePrintRequestedEvent.cpp
Normal file
1
src/lib/eventsystem/event/TracePrintRequestedEvent.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "TracePrintRequestedEvent.h"
|
||||
10
src/lib/eventsystem/event/TracePrintRequestedEvent.h
Normal file
10
src/lib/eventsystem/event/TracePrintRequestedEvent.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#ifndef TRACE_PRINT_REQUESTED_EVENT_H
|
||||
#define TRACE_PRINT_REQUESTED_EVENT_H
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class TracePrintRequestedEvent : public Event
|
||||
{
|
||||
};
|
||||
|
||||
#endif // TRACE_PRINT_REQUESTED_EVENT_H
|
||||
17
src/lib/eventsystem/event/WeaponFiredEvent.h
Normal file
17
src/lib/eventsystem/event/WeaponFiredEvent.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
#include "Tick.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
struct WeaponFiredEvent : public Event
|
||||
{
|
||||
WeaponFiredEvent() = default;
|
||||
WeaponFiredEvent(entt::entity shooter, entt::entity target, Tick emittedAt)
|
||||
: shooter(shooter), target(target), emittedAt(emittedAt) {}
|
||||
|
||||
entt::entity shooter = entt::null;
|
||||
entt::entity target = entt::null;
|
||||
Tick emittedAt = 0;
|
||||
};
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include "Tick.h"
|
||||
#include "tracing.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -46,8 +47,8 @@ QPointF BeltSystem::slotWorldPos(QPoint tile, Rotation dir, double progress)
|
||||
// Construction / placement
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
BeltSystem::BeltSystem(double beltSpeedTilesPerSecond)
|
||||
: m_progressPerTick(beltSpeedTilesPerSecond * kTickDurationSeconds)
|
||||
BeltSystem::BeltSystem(double beltSpeed_tps)
|
||||
: m_progressPerTick_tpt(beltSpeed_tps * kTickDurationSeconds)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -396,6 +397,7 @@ void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
|
||||
|
||||
void BeltSystem::tick()
|
||||
{
|
||||
TRACE();
|
||||
advanceProgress();
|
||||
advanceTunnelProgress();
|
||||
moveItemsToNextTile();
|
||||
@@ -412,7 +414,7 @@ void BeltSystem::advanceProgress()
|
||||
|
||||
for (std::size_t i = 0; i < bt.itemSlots.size(); ++i)
|
||||
{
|
||||
bt.itemSlots[i].progress += m_progressPerTick;
|
||||
bt.itemSlots[i].progress += m_progressPerTick_tpt;
|
||||
|
||||
// Absolute cap: slot i cannot exceed 1.0 - i * 0.25.
|
||||
const double absoluteCap = 1.0 - i * 0.25;
|
||||
@@ -440,7 +442,7 @@ void BeltSystem::advanceProgress()
|
||||
|
||||
for (std::size_t i = 0; i < st.back.size(); ++i)
|
||||
{
|
||||
st.back[i].progress += m_progressPerTick;
|
||||
st.back[i].progress += m_progressPerTick_tpt;
|
||||
const double absoluteCap = 0.5 - i * 0.25;
|
||||
if (st.back[i].progress > absoluteCap)
|
||||
{
|
||||
@@ -463,7 +465,7 @@ void BeltSystem::advanceProgress()
|
||||
|
||||
if (st.frontA)
|
||||
{
|
||||
st.frontA->progress += m_progressPerTick;
|
||||
st.frontA->progress += m_progressPerTick_tpt;
|
||||
if (st.frontA->progress > 1.0)
|
||||
{
|
||||
st.frontA->progress = 1.0;
|
||||
@@ -472,7 +474,7 @@ void BeltSystem::advanceProgress()
|
||||
|
||||
if (st.frontB)
|
||||
{
|
||||
st.frontB->progress += m_progressPerTick;
|
||||
st.frontB->progress += m_progressPerTick_tpt;
|
||||
if (st.frontB->progress > 1.0)
|
||||
{
|
||||
st.frontB->progress = 1.0;
|
||||
@@ -490,7 +492,7 @@ void BeltSystem::advanceTunnelProgress()
|
||||
|
||||
for (std::size_t i = 0; i < te.itemSlots.size(); ++i)
|
||||
{
|
||||
te.itemSlots[i].progress += m_progressPerTick;
|
||||
te.itemSlots[i].progress += m_progressPerTick_tpt;
|
||||
|
||||
const double absoluteCap = 1.0 - i * 0.25;
|
||||
if (te.itemSlots[i].progress > absoluteCap)
|
||||
@@ -516,7 +518,7 @@ void BeltSystem::advanceTunnelProgress()
|
||||
|
||||
for (std::size_t i = 0; i < tx.itemSlots.size(); ++i)
|
||||
{
|
||||
tx.itemSlots[i].progress += m_progressPerTick;
|
||||
tx.itemSlots[i].progress += m_progressPerTick_tpt;
|
||||
|
||||
const double absoluteCap = 1.0 - i * 0.25;
|
||||
if (tx.itemSlots[i].progress > absoluteCap)
|
||||
@@ -540,7 +542,7 @@ void BeltSystem::advanceTunnelProgress()
|
||||
for (std::size_t i = 0; i < link.items.size(); ++i)
|
||||
{
|
||||
TunnelTransitItem& ti = link.items[i];
|
||||
ti.progress += m_progressPerTick;
|
||||
ti.progress += m_progressPerTick_tpt;
|
||||
if (ti.progress > link.length)
|
||||
{
|
||||
ti.progress = link.length;
|
||||
@@ -754,13 +756,13 @@ void BeltSystem::routeSplitterItems()
|
||||
else if (preferA && !st.frontB)
|
||||
{
|
||||
// Preferred (A) is full — fall back to B; nextOutputIsA stays.
|
||||
st.frontB = BeltItemSlot{item, 0.0};
|
||||
st.frontB = BeltItemSlot{item, 0.75};
|
||||
routed = true;
|
||||
}
|
||||
else if (!preferA && !st.frontA)
|
||||
{
|
||||
// Preferred (B) is full — fall back to A; nextOutputIsA stays.
|
||||
st.frontA = BeltItemSlot{item, 0.0};
|
||||
st.frontA = BeltItemSlot{item, 0.75};
|
||||
routed = true;
|
||||
}
|
||||
// else both fronts occupied — back stays.
|
||||
@@ -960,3 +962,5 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ struct VisualItem
|
||||
class BeltSystem
|
||||
{
|
||||
public:
|
||||
explicit BeltSystem(double beltSpeedTilesPerSecond);
|
||||
explicit BeltSystem(double beltSpeed_tps);
|
||||
|
||||
// -- Placement -----------------------------------------------------------
|
||||
// Register a new belt tile. Any items already on this tile are cleared.
|
||||
@@ -170,7 +170,7 @@ private:
|
||||
std::vector<TunnelTransitItem> items; // front (highest progress) to back
|
||||
};
|
||||
|
||||
double m_progressPerTick; // beltSpeedTilesPerSecond / kTickRateHz
|
||||
double m_progressPerTick_tpt; // beltSpeed_tps / kTickRateHz
|
||||
|
||||
std::map<std::pair<int, int>, BeltTile> m_belts;
|
||||
std::map<std::pair<int, int>, SplitterTile> m_splitters;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <set>
|
||||
|
||||
#include "SurfaceMask.h"
|
||||
#include "tracing.h"
|
||||
|
||||
BuildingSystem::BuildingSystem(const GameConfig& config,
|
||||
BeltSystem& belts,
|
||||
@@ -13,12 +14,14 @@ BuildingSystem::BuildingSystem(const GameConfig& config,
|
||||
std::function<void(int)> addBuildingBlocks,
|
||||
std::function<void(const std::string&, QVector2D,
|
||||
const std::optional<ShipLayoutConfig>&)> spawnShip,
|
||||
std::function<bool(const std::string&)> isItemUnlocked,
|
||||
std::mt19937& rng)
|
||||
: m_config(config)
|
||||
, m_belts(belts)
|
||||
, m_allocateBuildingId(std::move(allocateBuildingId))
|
||||
, m_addBuildingBlocks(std::move(addBuildingBlocks))
|
||||
, m_spawnShip(std::move(spawnShip))
|
||||
, m_isItemUnlocked(std::move(isItemUnlocked))
|
||||
, m_rng(rng)
|
||||
{
|
||||
}
|
||||
@@ -202,17 +205,19 @@ std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
|
||||
|
||||
std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe)
|
||||
{
|
||||
std::vector<const RecipeOutput*> eligible;
|
||||
std::vector<double> weights;
|
||||
weights.reserve(recipe.outputs.size());
|
||||
for (const RecipeOutput& out : recipe.outputs)
|
||||
{
|
||||
if (!m_isItemUnlocked(out.item)) { continue; }
|
||||
eligible.push_back(&out);
|
||||
weights.push_back(out.probability.value_or(1.0));
|
||||
}
|
||||
|
||||
std::discrete_distribution<int> dist(weights.begin(), weights.end());
|
||||
const int idx = dist(m_rng);
|
||||
if (eligible.empty()) { return {}; }
|
||||
|
||||
const RecipeOutput& chosen = recipe.outputs[static_cast<std::size_t>(idx)];
|
||||
std::discrete_distribution<int> dist(weights.begin(), weights.end());
|
||||
const RecipeOutput& chosen = *eligible[static_cast<std::size_t>(dist(m_rng))];
|
||||
std::vector<Item> result;
|
||||
Item item;
|
||||
item.type.id = chosen.item;
|
||||
@@ -410,6 +415,7 @@ void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout
|
||||
|
||||
void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
{
|
||||
TRACE();
|
||||
if (m_constructionQueue.empty())
|
||||
{
|
||||
return;
|
||||
@@ -491,7 +497,7 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
}
|
||||
else if (front.type == BuildingType::TunnelEntry)
|
||||
{
|
||||
m_belts.placeTunnelEntry(front.anchor, front.rotation, m_config.world.tunnelMaxDistance);
|
||||
m_belts.placeTunnelEntry(front.anchor, front.rotation, m_config.world.tunnelMaxDistance_tiles);
|
||||
}
|
||||
else if (front.type == BuildingType::TunnelExit)
|
||||
{
|
||||
@@ -516,6 +522,7 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
|
||||
void BuildingSystem::tickBeltPull()
|
||||
{
|
||||
TRACE();
|
||||
for (Building& building : m_buildings)
|
||||
{
|
||||
// HQ: pull building_block items and add to global stock.
|
||||
@@ -591,6 +598,7 @@ void BuildingSystem::tickBeltPull()
|
||||
|
||||
void BuildingSystem::tickProduction(Tick currentTick)
|
||||
{
|
||||
TRACE();
|
||||
for (Building& building : m_buildings)
|
||||
{
|
||||
// Skip types without a recipe-based production loop.
|
||||
@@ -656,6 +664,7 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
||||
if (building.type == BuildingType::ReprocessingPlant)
|
||||
{
|
||||
chosen = rollReprocessingOutput(*recipe);
|
||||
if (chosen.empty()) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -694,6 +703,7 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
||||
|
||||
void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
||||
{
|
||||
TRACE();
|
||||
for (Building& building : m_buildings)
|
||||
{
|
||||
if (building.type != BuildingType::Shipyard)
|
||||
@@ -795,6 +805,7 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
||||
|
||||
void BuildingSystem::tickBeltPush()
|
||||
{
|
||||
TRACE();
|
||||
for (Building& building : m_buildings)
|
||||
{
|
||||
if (building.outputBuffer.items.empty())
|
||||
@@ -856,6 +867,44 @@ std::vector<ConstructionSite> BuildingSystem::allSites() const
|
||||
m_constructionQueue.end());
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
bool isProductionBuildingType(BuildingType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case BuildingType::Miner:
|
||||
case BuildingType::Smelter:
|
||||
case BuildingType::Assembler:
|
||||
case BuildingType::ReprocessingPlant:
|
||||
case BuildingType::Shipyard:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int BuildingSystem::productionBuildingCount() const
|
||||
{
|
||||
int count = 0;
|
||||
for (const Building& b : m_buildings)
|
||||
{
|
||||
if (isProductionBuildingType(b.type)) { ++count; }
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int BuildingSystem::activeProductionBuildingCount() const
|
||||
{
|
||||
int count = 0;
|
||||
for (const Building& b : m_buildings)
|
||||
{
|
||||
if (isProductionBuildingType(b.type) && b.production.has_value()) { ++count; }
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::allBeltTiles() const
|
||||
{
|
||||
std::vector<BeltTileInfo> result;
|
||||
@@ -987,7 +1036,7 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
|
||||
else if (b.type == BuildingType::TunnelEntry)
|
||||
{
|
||||
m_belts.removeTile(b.anchor);
|
||||
m_belts.placeTunnelEntry(b.anchor, newRotation, m_config.world.tunnelMaxDistance);
|
||||
m_belts.placeTunnelEntry(b.anchor, newRotation, m_config.world.tunnelMaxDistance_tiles);
|
||||
}
|
||||
else if (b.type == BuildingType::TunnelExit)
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user