Compare commits
9 Commits
3716c2b734
...
config
| Author | SHA1 | Date | |
|---|---|---|---|
| c44936d1fe | |||
| 68c1345660 | |||
| dbf334c829 | |||
| f225c1330e | |||
| fba98c928f | |||
| 282ace4c11 | |||
| 1ea1cc59fb | |||
| 123c544423 | |||
| 10c5ad678f |
@@ -1,130 +1,36 @@
|
|||||||
[[module]]
|
# modules.toml
|
||||||
id = "armor_plate"
|
#
|
||||||
unlock_at_station_level = -1
|
# First real-content iteration: module ids and surface masks are the designed
|
||||||
surface_mask = ["OO"]
|
# content; stats, materials, and threat costs are placeholders until the
|
||||||
materials = [{item = "armor_plate_module", amount = 1}]
|
# recipe and balancing passes.
|
||||||
player_production_level = 1
|
#
|
||||||
production_time_seconds = 3
|
# Surface mask footprint ladder — footprints gate which hulls can mount a
|
||||||
threat_cost = 20.0
|
# module, purely through geometry (see ships.toml for the matching hull
|
||||||
fill_color = "#808080"
|
# grids):
|
||||||
glyph = "A"
|
#
|
||||||
|
# 1x1 laser_cannon_s, salvager, repair_tool fits every hull, incl. drones
|
||||||
[module.health]
|
# 1x2 maneuvering_thrusters, sensor_booster,
|
||||||
added_hp_formula = "40"
|
# 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]]
|
[[module]]
|
||||||
id = "sensor_booster"
|
id = "laser_cannon_s"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "sensor_booster_module", amount = 1}]
|
materials = [{item = "laser_cannon_s_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_m_formula = "50"
|
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
|
||||||
id = "manuvering_thrusters"
|
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = ["O"]
|
|
||||||
materials = [{item = "manuvering_thrusters_module", amount = 1}]
|
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 2
|
|
||||||
threat_cost = 1.0
|
|
||||||
fill_color = "#40A0FF"
|
|
||||||
glyph = "Mt"
|
|
||||||
|
|
||||||
[module.movement]
|
|
||||||
multiplied_speed_mps_formula = "1.2"
|
|
||||||
added_maneuvering_acceleration_mpss_formula = "10"
|
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
|
||||||
id = "afterburner"
|
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = ["O"]
|
|
||||||
materials = [{item = "afterburner_module", amount = 1}]
|
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 2
|
|
||||||
threat_cost = 1.0
|
|
||||||
fill_color = "#40A0FF"
|
|
||||||
glyph = "Ab"
|
|
||||||
|
|
||||||
[module.movement]
|
|
||||||
multiplied_speed_mps_formula = "1.6"
|
|
||||||
added_main_acceleration_mpss_formula = "60"
|
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
|
||||||
id = "weapon_upgrade"
|
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = [
|
|
||||||
"OO",
|
|
||||||
"O ",
|
|
||||||
]
|
|
||||||
materials = [{item = "weapon_upgrade_module", amount = 1}]
|
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 4
|
|
||||||
threat_cost = 10.0
|
|
||||||
fill_color = "#FF4040"
|
|
||||||
glyph = "Wu"
|
|
||||||
|
|
||||||
[module.weapon]
|
|
||||||
multiplied_damage_formula = "1.2"
|
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
|
||||||
id = "weapon_primer"
|
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = [
|
|
||||||
"OO",
|
|
||||||
"O ",
|
|
||||||
]
|
|
||||||
materials = [{item = "weapon_primer_module", amount = 1}]
|
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 4
|
|
||||||
threat_cost = 10.0
|
|
||||||
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 = [
|
|
||||||
"OO",
|
|
||||||
"O ",
|
|
||||||
]
|
|
||||||
materials = [{item = "weapon_stabilizer_module", amount = 1}]
|
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 4
|
|
||||||
threat_cost = 10.0
|
|
||||||
fill_color = "#FF4040"
|
|
||||||
glyph = "Ws"
|
|
||||||
|
|
||||||
[module.weapon]
|
|
||||||
multiplied_attack_range_m_formula = "1.5"
|
|
||||||
multiplied_attack_rate_hz_formula = "0.8"
|
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
|
||||||
id = "laser_cannon_xs"
|
|
||||||
unlock_at_station_level = -1
|
|
||||||
surface_mask = ["O"]
|
|
||||||
materials = [{item = "iron_ore", amount = 1}]
|
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 0.5
|
production_time_seconds = 0.5
|
||||||
threat_cost = 5.0
|
|
||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "L"
|
glyph = "Ls"
|
||||||
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
damage_formula = "2"
|
damage_formula = "2"
|
||||||
@@ -133,17 +39,16 @@ attack_rate_hz_formula = "2.0"
|
|||||||
|
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "laser_cannon_s"
|
id = "laser_cannon_m"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
"OO"]
|
"OO"]
|
||||||
materials = [{item = "laser_cannon_s_module", amount = 1}]
|
materials = [{item = "laser_cannon_m_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 0.5
|
production_time_seconds = 2
|
||||||
threat_cost = 30.0
|
|
||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "L"
|
glyph = "Lm"
|
||||||
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
damage_formula = "10"
|
damage_formula = "10"
|
||||||
@@ -151,6 +56,28 @@ attack_range_m_formula = "70"
|
|||||||
attack_rate_hz_formula = "1.5"
|
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]]
|
[[module]]
|
||||||
id = "salvager"
|
id = "salvager"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
@@ -158,7 +85,6 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "salvager_module", amount = 1}]
|
materials = [{item = "salvager_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
threat_cost = 0.0
|
|
||||||
fill_color = "#AACC44"
|
fill_color = "#AACC44"
|
||||||
glyph = "Sv"
|
glyph = "Sv"
|
||||||
|
|
||||||
@@ -175,10 +101,160 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "repair_tool_module", amount = 1}]
|
materials = [{item = "repair_tool_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
threat_cost = 0.0
|
|
||||||
fill_color = "#66CCFF"
|
fill_color = "#66CCFF"
|
||||||
glyph = "Rp"
|
glyph = "Rp"
|
||||||
|
|
||||||
[module.repair]
|
[module.repair]
|
||||||
repair_rate_hz_formula = "5 + x"
|
repair_rate_hz_formula = "5 + x"
|
||||||
repair_range_m_formula = "800"
|
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
|
||||||
|
fill_color = "#808080"
|
||||||
|
glyph = "A"
|
||||||
|
|
||||||
|
[module.health]
|
||||||
|
added_hp_formula = "40"
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "sensor_booster"
|
||||||
|
unlock_at_station_level = -1
|
||||||
|
surface_mask = ["OO"]
|
||||||
|
materials = [{item = "sensor_booster_module", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 2
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "S"
|
||||||
|
|
||||||
|
[module.sensor]
|
||||||
|
added_sensor_range_m_formula = "50"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Weapon modifiers
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_upgrade"
|
||||||
|
unlock_at_station_level = -1
|
||||||
|
surface_mask = [
|
||||||
|
"OO",
|
||||||
|
"OX",
|
||||||
|
]
|
||||||
|
materials = [{item = "weapon_upgrade_module", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 4
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "Wu"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
multiplied_damage_formula = "1.2"
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
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 = 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 = [
|
||||||
|
"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]]
|
[[recipe]]
|
||||||
id = "mine_iron_ore"
|
id = "mine_iron_ore"
|
||||||
building = "miner"
|
building = "miner"
|
||||||
@@ -12,6 +38,18 @@ inputs = []
|
|||||||
outputs = [{item = "copper_ore", amount = 1}]
|
outputs = [{item = "copper_ore", amount = 1}]
|
||||||
duration_seconds = 1.5
|
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]]
|
[[recipe]]
|
||||||
id = "iron_ingot"
|
id = "iron_ingot"
|
||||||
building = "smelter"
|
building = "smelter"
|
||||||
@@ -27,54 +65,17 @@ outputs = [{item = "copper_ingot", amount = 1}]
|
|||||||
duration_seconds = 2.5
|
duration_seconds = 2.5
|
||||||
|
|
||||||
[[recipe]]
|
[[recipe]]
|
||||||
id = "circuit_board"
|
id = "titanium_ingot"
|
||||||
building = "assembler"
|
building = "smelter"
|
||||||
inputs = [{item = "iron_ingot", amount = 1}, {item = "copper_ingot", amount = 2}]
|
inputs = [{item = "titanium_ore", amount = 3}]
|
||||||
outputs = [{item = "circuit_board", amount = 1}]
|
outputs = [{item = "titanium_ingot", amount = 1}]
|
||||||
duration_seconds = 2.0
|
|
||||||
|
|
||||||
[[recipe]]
|
|
||||||
id = "drone_hull"
|
|
||||||
building = "assembler"
|
|
||||||
inputs = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 1}]
|
|
||||||
outputs = [{item = "drone_hull", amount = 1}]
|
|
||||||
duration_seconds = 4.0
|
duration_seconds = 4.0
|
||||||
|
|
||||||
[[recipe]]
|
# -----------------------------------------------------------------------------
|
||||||
id = "laser_cannon_xs_module"
|
# Reprocessing
|
||||||
building = "assembler"
|
#
|
||||||
inputs = [{item = "iron_ingot", amount = 2}, {item = "circuit_board", amount = 1}]
|
# The only source of advanced_alloy: salvaged scrap from destroyed ships.
|
||||||
outputs = [{item = "laser_cannon_xs_module", amount = 1}]
|
# -----------------------------------------------------------------------------
|
||||||
duration_seconds = 3.0
|
|
||||||
|
|
||||||
[[recipe]]
|
|
||||||
id = "laser_cannon_s_module"
|
|
||||||
building = "assembler"
|
|
||||||
inputs = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount = 2}]
|
|
||||||
outputs = [{item = "laser_cannon_s_module", amount = 1}]
|
|
||||||
duration_seconds = 6.0
|
|
||||||
|
|
||||||
[[recipe]]
|
|
||||||
id = "salvager_module"
|
|
||||||
building = "assembler"
|
|
||||||
inputs = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
|
||||||
outputs = [{item = "salvager_module", amount = 1}]
|
|
||||||
duration_seconds = 6.0
|
|
||||||
|
|
||||||
[[recipe]]
|
|
||||||
id = "repair_tool_module"
|
|
||||||
building = "assembler"
|
|
||||||
inputs = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 2}]
|
|
||||||
outputs = [{item = "repair_tool_module", amount = 1}]
|
|
||||||
duration_seconds = 6.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
|
|
||||||
|
|
||||||
[[recipe]]
|
[[recipe]]
|
||||||
id = "reprocessing_cycle"
|
id = "reprocessing_cycle"
|
||||||
@@ -85,15 +86,354 @@ duration_seconds = 3.0
|
|||||||
[[recipe.outputs]]
|
[[recipe.outputs]]
|
||||||
item = "iron_ingot"
|
item = "iron_ingot"
|
||||||
amount = 2
|
amount = 2
|
||||||
probability = 0.6
|
probability = 0.45
|
||||||
|
|
||||||
[[recipe.outputs]]
|
[[recipe.outputs]]
|
||||||
item = "circuit_board"
|
item = "copper_ingot"
|
||||||
amount = 1
|
amount = 1
|
||||||
probability = 0.3
|
probability = 0.25
|
||||||
|
|
||||||
|
[[recipe.outputs]]
|
||||||
|
item = "titanium_ingot"
|
||||||
|
amount = 1
|
||||||
|
probability = 0.15
|
||||||
|
|
||||||
[[recipe.outputs]]
|
[[recipe.outputs]]
|
||||||
item = "advanced_alloy"
|
item = "advanced_alloy"
|
||||||
amount = 1
|
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,17 +1,31 @@
|
|||||||
|
# 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]]
|
[[ship]]
|
||||||
id = "drone"
|
id = "drone"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
layout = ["O"]
|
layout = ["O"]
|
||||||
default_modules = [{type = "laser_cannon_xs", x = 0, y = 0, rotation = "east"}]
|
default_modules = [{type = "laser_cannon_s", x = 0, y = 0, rotation = "east"}]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ore", amount = 1}]
|
materials = [{item = "iron_ore", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
|
|
||||||
[ship.threat]
|
|
||||||
cost_formula = "10"
|
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "3"
|
hp_formula = "3"
|
||||||
|
|
||||||
@@ -27,3 +41,255 @@ sensor_range_m_formula = "150"
|
|||||||
|
|
||||||
[ship.loot]
|
[ship.loot]
|
||||||
scrap_drop = 2
|
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 = "frigate"
|
||||||
|
unlock_at_station_level = -1
|
||||||
|
layout = [
|
||||||
|
"XOX",
|
||||||
|
"OOO",
|
||||||
|
"XOX",
|
||||||
|
]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "frigate_hull", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 10
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp_formula = "30"
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
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_m_formula = "200"
|
||||||
|
|
||||||
|
[ship.loot]
|
||||||
|
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 = "destroyer"
|
||||||
|
unlock_at_station_level = -1
|
||||||
|
layout = [
|
||||||
|
"OXOXO",
|
||||||
|
"OOOOO",
|
||||||
|
]
|
||||||
|
|
||||||
|
[ship.schematic]
|
||||||
|
materials = [{item = "destroyer_hull", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 15
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp_formula = "50"
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
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_m_formula = "220"
|
||||||
|
|
||||||
|
[ship.loot]
|
||||||
|
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
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ glyph = "E"
|
|||||||
# drawn around it. One section per ItemType.
|
# drawn around it. One section per ItemType.
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# --- ores ---
|
||||||
|
|
||||||
[items.iron_ore]
|
[items.iron_ore]
|
||||||
fill = "#8a5a4a"
|
fill = "#8a5a4a"
|
||||||
outline = "#201010"
|
outline = "#201010"
|
||||||
@@ -114,6 +116,12 @@ outline = "#201010"
|
|||||||
fill = "#c47a3a"
|
fill = "#c47a3a"
|
||||||
outline = "#3a1a0a"
|
outline = "#3a1a0a"
|
||||||
|
|
||||||
|
[items.titanium_ore]
|
||||||
|
fill = "#9aa3ad"
|
||||||
|
outline = "#2a2e33"
|
||||||
|
|
||||||
|
# --- ingots ---
|
||||||
|
|
||||||
[items.iron_ingot]
|
[items.iron_ingot]
|
||||||
fill = "#b0b0b8"
|
fill = "#b0b0b8"
|
||||||
outline = "#202028"
|
outline = "#202028"
|
||||||
@@ -122,30 +130,80 @@ outline = "#202028"
|
|||||||
fill = "#d48a4a"
|
fill = "#d48a4a"
|
||||||
outline = "#402010"
|
outline = "#402010"
|
||||||
|
|
||||||
[items.circuit_board]
|
[items.titanium_ingot]
|
||||||
fill = "#2ea35a"
|
fill = "#c8d2dc"
|
||||||
outline = "#0a2a14"
|
outline = "#3a4048"
|
||||||
|
|
||||||
[items.advanced_alloy]
|
# --- salvage loop ---
|
||||||
fill = "#a06acc"
|
|
||||||
outline = "#201030"
|
|
||||||
|
|
||||||
[items.building_block]
|
|
||||||
fill = "#c8b070"
|
|
||||||
outline = "#302810"
|
|
||||||
|
|
||||||
[items.scrap]
|
[items.scrap]
|
||||||
fill = "#7a7268"
|
fill = "#7a7268"
|
||||||
outline = "#201a14"
|
outline = "#201a14"
|
||||||
|
|
||||||
[items.drone_hull]
|
[items.advanced_alloy]
|
||||||
fill = "#1b1b1b"
|
fill = "#a06acc"
|
||||||
outline = "#1402b3"
|
outline = "#201030"
|
||||||
|
|
||||||
[items.laser_cannon_xs_module]
|
# --- 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"
|
fill = "#691313"
|
||||||
outline = "#f3ff4f"
|
outline = "#f3ff4f"
|
||||||
|
|
||||||
|
[items.laser_cannon_m_module]
|
||||||
|
fill = "#892020"
|
||||||
|
outline = "#f3ff4f"
|
||||||
|
|
||||||
|
[items.laser_cannon_l_module]
|
||||||
|
fill = "#a92d2d"
|
||||||
|
outline = "#f3ff4f"
|
||||||
|
|
||||||
[items.salvager_module]
|
[items.salvager_module]
|
||||||
fill = "#b2cfdd"
|
fill = "#b2cfdd"
|
||||||
outline = "#236137"
|
outline = "#236137"
|
||||||
@@ -154,6 +212,76 @@ outline = "#236137"
|
|||||||
fill = "#2e9ba3"
|
fill = "#2e9ba3"
|
||||||
outline = "#689275"
|
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
|
||||||
#
|
#
|
||||||
@@ -164,6 +292,34 @@ outline = "#689275"
|
|||||||
fill = "#3366ff"
|
fill = "#3366ff"
|
||||||
outline = "#ffffff"
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.frigate]
|
||||||
|
fill = "#44aaff"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.destroyer]
|
||||||
|
fill = "#33ccaa"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.cruiser]
|
||||||
|
fill = "#66cc33"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.battlecruiser]
|
||||||
|
fill = "#cccc33"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.battleship]
|
||||||
|
fill = "#ff9933"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.dreadnought]
|
||||||
|
fill = "#ff5533"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.carrier]
|
||||||
|
fill = "#cc66ff"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Laser beams (REQ-SHP-FIRING-BEAM)
|
# Laser beams (REQ-SHP-FIRING-BEAM)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
level = 1
|
level = 1
|
||||||
count = 5
|
count = 5
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[arena.team]]
|
[[arena.team]]
|
||||||
@@ -22,7 +22,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
level = 1
|
level = 1
|
||||||
count = 2
|
count = 2
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon_xs", x = 1, y = 1, 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_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 = "weapon_upgrade", x = 1, y = 1, rotation = "east"},
|
||||||
@@ -44,7 +44,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
level = 1
|
level = 1
|
||||||
count = 5
|
count = 5
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[arena.team]]
|
[[arena.team]]
|
||||||
@@ -54,7 +54,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
level = 1
|
level = 1
|
||||||
count = 3
|
count = 3
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[arena.team.ship]]
|
[[arena.team.ship]]
|
||||||
@@ -79,7 +79,7 @@ enemy_buffer_width_tiles = 15
|
|||||||
level = 1
|
level = 1
|
||||||
count = 3
|
count = 3
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||||
]
|
]
|
||||||
[[arena.team.station]]
|
[[arena.team.station]]
|
||||||
type = "player_station"
|
type = "player_station"
|
||||||
@@ -99,5 +99,5 @@ enemy_buffer_width_tiles = 15
|
|||||||
level = 1
|
level = 1
|
||||||
count = 8
|
count = 8
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ surface_mask = ["OO"]
|
|||||||
materials = [{item = "iron_ingot", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 3
|
production_time_seconds = 3
|
||||||
threat_cost = 2.0
|
|
||||||
fill_color = "#808080"
|
fill_color = "#808080"
|
||||||
glyph = "A"
|
glyph = "A"
|
||||||
|
|
||||||
@@ -19,7 +18,6 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "circuit_board", amount = 1}]
|
materials = [{item = "circuit_board", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
threat_cost = 1.0
|
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "S"
|
glyph = "S"
|
||||||
|
|
||||||
@@ -33,7 +31,6 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
threat_cost = 3.0
|
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "W"
|
glyph = "W"
|
||||||
|
|
||||||
@@ -47,7 +44,6 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
threat_cost = 5.0
|
|
||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "L"
|
glyph = "L"
|
||||||
|
|
||||||
@@ -63,7 +59,6 @@ surface_mask = ["OO"]
|
|||||||
materials = [{item = "iron_ingot", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
threat_cost = 0.0
|
|
||||||
fill_color = "#AACC44"
|
fill_color = "#AACC44"
|
||||||
glyph = "Sv"
|
glyph = "Sv"
|
||||||
|
|
||||||
@@ -79,7 +74,6 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "circuit_board", amount = 2}]
|
materials = [{item = "circuit_board", amount = 2}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
threat_cost = 0.0
|
|
||||||
fill_color = "#66CCFF"
|
fill_color = "#66CCFF"
|
||||||
glyph = "Rp"
|
glyph = "Rp"
|
||||||
|
|
||||||
@@ -94,7 +88,6 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
threat_cost = 1.0
|
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "Wp"
|
glyph = "Wp"
|
||||||
|
|
||||||
@@ -108,7 +101,6 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
threat_cost = 1.0
|
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "Ws"
|
glyph = "Ws"
|
||||||
|
|
||||||
@@ -123,7 +115,6 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
threat_cost = 1.0
|
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "Ab"
|
glyph = "Ab"
|
||||||
|
|
||||||
@@ -138,7 +129,6 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
threat_cost = 1.0
|
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "Mt"
|
glyph = "Mt"
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount
|
|||||||
player_production_level = 3
|
player_production_level = 3
|
||||||
production_time_seconds = 10
|
production_time_seconds = 10
|
||||||
|
|
||||||
[ship.threat]
|
|
||||||
cost_formula = "5 + 1*x"
|
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "40 + 5*x"
|
hp_formula = "40 + 5*x"
|
||||||
|
|
||||||
@@ -40,9 +37,6 @@ materials = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount
|
|||||||
player_production_level = 5
|
player_production_level = 5
|
||||||
production_time_seconds = 20
|
production_time_seconds = 20
|
||||||
|
|
||||||
[ship.threat]
|
|
||||||
cost_formula = "10 + 2*x"
|
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "120 + 15*x"
|
hp_formula = "120 + 15*x"
|
||||||
|
|
||||||
@@ -70,9 +64,6 @@ materials = [{item = "iron_ingot", amount = 4}]
|
|||||||
player_production_level = 3
|
player_production_level = 3
|
||||||
production_time_seconds = 10
|
production_time_seconds = 10
|
||||||
|
|
||||||
[ship.threat]
|
|
||||||
cost_formula = "0"
|
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "40 + 4*x"
|
hp_formula = "40 + 4*x"
|
||||||
|
|
||||||
@@ -100,9 +91,6 @@ materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount
|
|||||||
player_production_level = 3
|
player_production_level = 3
|
||||||
production_time_seconds = 15
|
production_time_seconds = 15
|
||||||
|
|
||||||
[ship.threat]
|
|
||||||
cost_formula = "0"
|
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "60 + 5*x"
|
hp_formula = "60 + 5*x"
|
||||||
|
|
||||||
|
|||||||
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.
|
||||||
@@ -7,8 +7,8 @@ Config files use the TOML format. The following config files drive game paramete
|
|||||||
- **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.
|
- **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.
|
- **buildings.toml** — building block cost and construction time per building type.
|
||||||
- **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).
|
- **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, threat cost formula, 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, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
|
- **ships.toml** — per schematic: a human-readable display name (used in the UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as 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, threat cost, 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).
|
- **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.
|
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
|
||||||
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship schematic, and station type; 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.
|
- **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.
|
||||||
@@ -182,7 +182,6 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- `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).
|
- `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.
|
- `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.
|
- `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.
|
- `fill_color` — fill color used to render this module's cells in the layout grid.
|
||||||
- `glyph` — single character rendered on this module's cells in the layout grid and preview widget.
|
- `glyph` — single character rendered on this module's cells in the layout grid and preview widget.
|
||||||
- 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:
|
- 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:
|
||||||
@@ -203,7 +202,19 @@ 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-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-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-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).
|
||||||
|
|
||||||
|
- 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:
|
- 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).
|
- `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_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`.
|
||||||
@@ -247,6 +258,8 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
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.
|
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).
|
- 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
|
### Layout Blueprints
|
||||||
@@ -314,8 +327,8 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- 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-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-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-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_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 normal wave. A longer gap results in a larger wave. Because enemy ship level increases with the boss wave counter (REQ-WAV-SHIP-LEVEL), threat cost per ship rises as the game progresses.
|
- 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 and threat cost are computed from the ship level via the formulas in `ships.toml` (see REQ-SHP-STATS).
|
- 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-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-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-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.
|
||||||
@@ -336,27 +349,30 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
### Layout
|
### 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 |
|
| Header Bar | |
|
||||||
+--------------------------------------------------+
|
+--------------------------------------+ Selected |
|
||||||
| |
|
| | Building |
|
||||||
| Game World (70%) |
|
| | Panel |
|
||||||
| |
|
| +--------------+
|
||||||
+-----------------+-----------------+--------------+
|
| Game World | Build |
|
||||||
| Selected | Build Button | Blueprint |
|
| | Button |
|
||||||
| Building Panel | Grid | Panel |
|
| | Grid |
|
||||||
| (left) | (center) | (right) |
|
| +--------------+
|
||||||
+-----------------+-----------------+--------------+
|
| | 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, 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-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-BOSS-STATUS: The header bar displays, to the left of the speed buttons, the current boss wave counter (REQ-WAV-BOSS-COUNTER) and the time remaining on the boss countdown (REQ-WAV-BOSS-COUNTDOWN). The boss wave counter is shown as `Boss Wave #<x>` and the countdown as `Next boss: <M:SS>`, where `<M:SS>` is the remaining seconds formatted as whole minutes and two-digit seconds. Both values update continuously as the simulation runs.
|
||||||
- REQ-UI-SPEED: The game speed controls in the header bar are buttons for 0×, 0.5×, 1×, 2×, and 4× speed. The currently active speed is shown as selected. All game simulation (production, movement, threat accumulation, wave timing) scales with the selected speed. 0× pauses the game.
|
- REQ-UI-SPEED: The game speed controls in the header bar are buttons for 0×, 0.5×, 1×, 2×, and 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-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-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-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
|
### Game World
|
||||||
|
|
||||||
@@ -381,6 +397,9 @@ The screen is divided into three vertical sections:
|
|||||||
- 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:
|
- 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).
|
- `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.
|
- `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
|
### Escape Menu
|
||||||
|
|
||||||
@@ -400,7 +419,7 @@ The screen is divided into three vertical sections:
|
|||||||
- 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-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-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel shows a "Clear" button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels.
|
||||||
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. Clicking a ship or defence station clears any existing selection and establishes a single-entity selection containing only that entity. Ships and defence stations cannot participate in multi-select together with buildings. Clicking empty world space (no building, ship, or defence station) clears the selection.
|
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. 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.
|
- 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.
|
- 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
|
### Build Button Grid
|
||||||
|
|||||||
@@ -429,14 +429,6 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
|||||||
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
|
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
|
// Health
|
||||||
{
|
{
|
||||||
const std::string hPath = elemPath + ".health";
|
const std::string hPath = elemPath + ".health";
|
||||||
@@ -587,7 +579,6 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|||||||
mt["player_production_level"], file, elemPath + ".player_production_level"));
|
mt["player_production_level"], file, elemPath + ".player_production_level"));
|
||||||
def.productionTimeSeconds = requireDouble(
|
def.productionTimeSeconds = requireDouble(
|
||||||
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
||||||
def.threatCost = requireDouble(mt["threat_cost"], file, elemPath + ".threat_cost");
|
|
||||||
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
||||||
def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph");
|
def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph");
|
||||||
|
|
||||||
@@ -704,5 +695,6 @@ GameConfig ConfigLoader::loadFromDirectory(const std::string& configDir)
|
|||||||
cfg.ships = loadShips(configDir + "/ships.toml");
|
cfg.ships = loadShips(configDir + "/ships.toml");
|
||||||
cfg.stations = loadStations(configDir + "/stations.toml");
|
cfg.stations = loadStations(configDir + "/stations.toml");
|
||||||
cfg.modules = loadModules(configDir + "/modules.toml");
|
cfg.modules = loadModules(configDir + "/modules.toml");
|
||||||
|
cfg.threatCosts = computeThreatCostTable(cfg);
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
#include "StationsConfig.h"
|
#include "StationsConfig.h"
|
||||||
#include "ModulesConfig.h"
|
#include "ModulesConfig.h"
|
||||||
|
#include "ThreatCostCalculator.h"
|
||||||
|
|
||||||
// Aggregate of all simulation config files. Loaded at startup and reloaded
|
// Aggregate of all simulation config files. Loaded at startup and reloaded
|
||||||
// from disk on each game restart (REQ-CFG-RELOAD). See architecture.md "Config Loading".
|
// from disk on each game restart (REQ-CFG-RELOAD). See architecture.md "Config Loading".
|
||||||
@@ -17,4 +18,5 @@ struct GameConfig
|
|||||||
ShipsConfig ships;
|
ShipsConfig ships;
|
||||||
StationsConfig stations;
|
StationsConfig stations;
|
||||||
ModulesConfig modules;
|
ModulesConfig modules;
|
||||||
|
ThreatCostTable threatCosts;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ struct ModuleDef
|
|||||||
std::vector<RecipeIngredient> materials;
|
std::vector<RecipeIngredient> materials;
|
||||||
int playerProductionLevel;
|
int playerProductionLevel;
|
||||||
double productionTimeSeconds;
|
double productionTimeSeconds;
|
||||||
double threatCost;
|
|
||||||
std::string fillColor;
|
std::string fillColor;
|
||||||
std::string glyph;
|
std::string glyph;
|
||||||
std::vector<ModuleStatModifier> statModifiers;
|
std::vector<ModuleStatModifier> statModifiers;
|
||||||
|
|||||||
@@ -16,13 +16,6 @@ struct ShipSchematic
|
|||||||
double productionTimeSeconds;
|
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
|
struct ShipHealth
|
||||||
{
|
{
|
||||||
Formula hpFormula; // REQ-SHP-STATS
|
Formula hpFormula; // REQ-SHP-STATS
|
||||||
@@ -55,7 +48,6 @@ struct ShipDef
|
|||||||
std::vector<std::string> layout;
|
std::vector<std::string> layout;
|
||||||
|
|
||||||
ShipSchematic schematic;
|
ShipSchematic schematic;
|
||||||
ShipThreat threat;
|
|
||||||
ShipHealth health;
|
ShipHealth health;
|
||||||
ShipMovement movement;
|
ShipMovement movement;
|
||||||
ShipSensor sensor;
|
ShipSensor sensor;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaStartRequestedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ArenaStartRequestedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaInspectRequestedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ArenaInspectRequestedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WeaponFiredEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WeaponFiredEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DebugDrawToggledEvent.h
|
||||||
PARENT_SCOPE
|
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;
|
||||||
|
};
|
||||||
@@ -756,13 +756,13 @@ void BeltSystem::routeSplitterItems()
|
|||||||
else if (preferA && !st.frontB)
|
else if (preferA && !st.frontB)
|
||||||
{
|
{
|
||||||
// Preferred (A) is full — fall back to B; nextOutputIsA stays.
|
// Preferred (A) is full — fall back to B; nextOutputIsA stays.
|
||||||
st.frontB = BeltItemSlot{item, 0.0};
|
st.frontB = BeltItemSlot{item, 0.75};
|
||||||
routed = true;
|
routed = true;
|
||||||
}
|
}
|
||||||
else if (!preferA && !st.frontA)
|
else if (!preferA && !st.frontA)
|
||||||
{
|
{
|
||||||
// Preferred (B) is full — fall back to A; nextOutputIsA stays.
|
// Preferred (B) is full — fall back to A; nextOutputIsA stays.
|
||||||
st.frontA = BeltItemSlot{item, 0.0};
|
st.frontA = BeltItemSlot{item, 0.75};
|
||||||
routed = true;
|
routed = true;
|
||||||
}
|
}
|
||||||
// else both fronts occupied — back stays.
|
// else both fronts occupied — back stays.
|
||||||
@@ -963,3 +963,4 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -867,6 +867,44 @@ std::vector<ConstructionSite> BuildingSystem::allSites() const
|
|||||||
m_constructionQueue.end());
|
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<BuildingSystem::BeltTileInfo> BuildingSystem::allBeltTiles() const
|
||||||
{
|
{
|
||||||
std::vector<BeltTileInfo> result;
|
std::vector<BeltTileInfo> result;
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ public:
|
|||||||
const ConstructionSite* findSite(BuildingId id) const;
|
const ConstructionSite* findSite(BuildingId id) const;
|
||||||
std::vector<Building> allBuildings() const;
|
std::vector<Building> allBuildings() const;
|
||||||
std::vector<ConstructionSite> allSites() const;
|
std::vector<ConstructionSite> allSites() const;
|
||||||
|
|
||||||
|
// REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed
|
||||||
|
// (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings.
|
||||||
|
int productionBuildingCount() const;
|
||||||
|
|
||||||
|
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above
|
||||||
|
// that currently has an active production cycle.
|
||||||
|
int activeProductionBuildingCount() const;
|
||||||
std::vector<BeltTileInfo> allBeltTiles() const;
|
std::vector<BeltTileInfo> allBeltTiles() const;
|
||||||
bool isTileOccupied(QPoint tile) const;
|
bool isTileOccupied(QPoint tile) const;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
@@ -21,6 +22,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -863,6 +863,21 @@ double Simulation::threatLevel() const
|
|||||||
return m_waveSystem->threatLevel();
|
return m_waveSystem->threatLevel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double Simulation::threatAccumulationRate() const
|
||||||
|
{
|
||||||
|
return m_waveSystem->threatAccumulationRate();
|
||||||
|
}
|
||||||
|
|
||||||
|
double Simulation::maxFactoryProductionThreatRate() const
|
||||||
|
{
|
||||||
|
return static_cast<double>(m_buildingSystem->productionBuildingCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
double Simulation::currentFactoryProductionThreatRate() const
|
||||||
|
{
|
||||||
|
return static_cast<double>(m_buildingSystem->activeProductionBuildingCount());
|
||||||
|
}
|
||||||
|
|
||||||
int Simulation::bossWaveCounter() const
|
int Simulation::bossWaveCounter() const
|
||||||
{
|
{
|
||||||
return m_waveSystem->bossWaveCounter();
|
return m_waveSystem->bossWaveCounter();
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ public:
|
|||||||
int buildingBlocksStock() const;
|
int buildingBlocksStock() const;
|
||||||
bool isGameOver() const;
|
bool isGameOver() const;
|
||||||
double threatLevel() const;
|
double threatLevel() const;
|
||||||
|
double threatAccumulationRate() const;
|
||||||
|
double maxFactoryProductionThreatRate() const;
|
||||||
|
double currentFactoryProductionThreatRate() const;
|
||||||
int bossWaveCounter() const;
|
int bossWaveCounter() const;
|
||||||
Tick bossCountdownTicks() const;
|
Tick bossCountdownTicks() const;
|
||||||
Tick normalGapRemainingTicks() const;
|
Tick normalGapRemainingTicks() const;
|
||||||
|
|||||||
247
src/lib/sim/ThreatCostCalculator.cpp
Normal file
247
src/lib/sim/ThreatCostCalculator.cpp
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
#include "ThreatCostCalculator.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
#include "GameConfig.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
struct RecipeRef
|
||||||
|
{
|
||||||
|
const RecipeDef* recipe;
|
||||||
|
std::string outputItem;
|
||||||
|
int outputAmount;
|
||||||
|
double probability;
|
||||||
|
};
|
||||||
|
|
||||||
|
double computeMaterialThreat(const ThreatCostTable& table,
|
||||||
|
const std::vector<RecipeIngredient>& materials)
|
||||||
|
{
|
||||||
|
double total = 0.0;
|
||||||
|
for (const RecipeIngredient& mat : materials)
|
||||||
|
{
|
||||||
|
std::map<std::string, double>::const_iterator it = table.itemThreat.find(mat.item);
|
||||||
|
if (it != table.itemThreat.end())
|
||||||
|
{
|
||||||
|
total += it->second * mat.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool allInputsResolved(const RecipeDef& recipe,
|
||||||
|
const std::map<std::string, double>& resolved)
|
||||||
|
{
|
||||||
|
for (const RecipeIngredient& input : recipe.inputs)
|
||||||
|
{
|
||||||
|
if (resolved.find(input.item) == resolved.end())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
double computeRecipeThreat(const RecipeDef& recipe,
|
||||||
|
const std::map<std::string, double>& resolved)
|
||||||
|
{
|
||||||
|
double threat = recipe.durationSeconds;
|
||||||
|
for (const RecipeIngredient& input : recipe.inputs)
|
||||||
|
{
|
||||||
|
threat += resolved.at(input.item) * input.amount;
|
||||||
|
}
|
||||||
|
return threat;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
ThreatCostTable computeThreatCostTable(const GameConfig& config)
|
||||||
|
{
|
||||||
|
ThreatCostTable table;
|
||||||
|
|
||||||
|
// Build lookup: output item → non-reprocessing recipes and reprocessing recipes.
|
||||||
|
std::map<std::string, std::vector<RecipeRef>> nonReprocessingRecipes;
|
||||||
|
std::map<std::string, std::vector<RecipeRef>> reprocessingRecipes;
|
||||||
|
|
||||||
|
for (const RecipeDef& recipe : config.recipes.recipes)
|
||||||
|
{
|
||||||
|
if (recipe.building == BuildingType::ReprocessingPlant)
|
||||||
|
{
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
RecipeRef ref;
|
||||||
|
ref.recipe = &recipe;
|
||||||
|
ref.outputItem = out.item;
|
||||||
|
ref.outputAmount = out.amount;
|
||||||
|
ref.probability = out.probability.value_or(1.0);
|
||||||
|
reprocessingRecipes[out.item].push_back(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
RecipeRef ref;
|
||||||
|
ref.recipe = &recipe;
|
||||||
|
ref.outputItem = out.item;
|
||||||
|
ref.outputAmount = out.amount;
|
||||||
|
ref.probability = 1.0;
|
||||||
|
nonReprocessingRecipes[out.item].push_back(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all item names that need resolving.
|
||||||
|
std::set<std::string> unresolved;
|
||||||
|
for (const std::pair<const std::string, std::vector<RecipeRef>>& entry : nonReprocessingRecipes)
|
||||||
|
{
|
||||||
|
unresolved.insert(entry.first);
|
||||||
|
}
|
||||||
|
for (const std::pair<const std::string, std::vector<RecipeRef>>& entry : reprocessingRecipes)
|
||||||
|
{
|
||||||
|
unresolved.insert(entry.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iteratively resolve non-reprocessing items.
|
||||||
|
bool progress = true;
|
||||||
|
while (progress)
|
||||||
|
{
|
||||||
|
progress = false;
|
||||||
|
std::set<std::string> newlyResolved;
|
||||||
|
for (const std::string& item : unresolved)
|
||||||
|
{
|
||||||
|
std::map<std::string, std::vector<RecipeRef>>::const_iterator it =
|
||||||
|
nonReprocessingRecipes.find(item);
|
||||||
|
if (it == nonReprocessingRecipes.end())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double maxThreat = -1.0;
|
||||||
|
for (const RecipeRef& ref : it->second)
|
||||||
|
{
|
||||||
|
if (allInputsResolved(*ref.recipe, table.itemThreat))
|
||||||
|
{
|
||||||
|
double threat = computeRecipeThreat(*ref.recipe, table.itemThreat);
|
||||||
|
if (threat > maxThreat)
|
||||||
|
{
|
||||||
|
maxThreat = threat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxThreat >= 0.0)
|
||||||
|
{
|
||||||
|
table.itemThreat[item] = maxThreat;
|
||||||
|
newlyResolved.insert(item);
|
||||||
|
progress = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const std::string& item : newlyResolved)
|
||||||
|
{
|
||||||
|
unresolved.erase(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute scrap threat (REQ-THREAT-SCRAP): find the ship with the smallest
|
||||||
|
// scrap_drop and use its threat cost.
|
||||||
|
int minScrapDrop = std::numeric_limits<int>::max();
|
||||||
|
const ShipDef* cheapestScrapShip = nullptr;
|
||||||
|
for (const ShipDef& def : config.ships.ships)
|
||||||
|
{
|
||||||
|
if (def.loot.scrapDrop > 0 && def.loot.scrapDrop < minScrapDrop)
|
||||||
|
{
|
||||||
|
minScrapDrop = def.loot.scrapDrop;
|
||||||
|
cheapestScrapShip = &def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cheapestScrapShip != nullptr)
|
||||||
|
{
|
||||||
|
double shipThreat = calculateShipThreatCost(table, config,
|
||||||
|
cheapestScrapShip->id, cheapestScrapShip->defaultModules);
|
||||||
|
table.scrapThreat = shipThreat / minScrapDrop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve reprocessing-only items.
|
||||||
|
for (const std::string& item : unresolved)
|
||||||
|
{
|
||||||
|
std::map<std::string, std::vector<RecipeRef>>::const_iterator it =
|
||||||
|
reprocessingRecipes.find(item);
|
||||||
|
if (it == reprocessingRecipes.end())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const RecipeRef& ref : it->second)
|
||||||
|
{
|
||||||
|
int scrapPerCycle = 0;
|
||||||
|
for (const RecipeIngredient& input : ref.recipe->inputs)
|
||||||
|
{
|
||||||
|
scrapPerCycle += input.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
double threat = (table.scrapThreat * scrapPerCycle
|
||||||
|
+ ref.recipe->durationSeconds) / ref.probability;
|
||||||
|
std::map<std::string, double>::iterator existing = table.itemThreat.find(item);
|
||||||
|
if (existing == table.itemThreat.end() || threat > existing->second)
|
||||||
|
{
|
||||||
|
table.itemThreat[item] = threat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
double calculateShipThreatCost(const ThreatCostTable& table,
|
||||||
|
const GameConfig& config,
|
||||||
|
const std::string& shipId,
|
||||||
|
const std::vector<PlacedModule>& modules)
|
||||||
|
{
|
||||||
|
const ShipDef* shipDef = nullptr;
|
||||||
|
for (const ShipDef& d : config.ships.ships)
|
||||||
|
{
|
||||||
|
if (d.id == shipId)
|
||||||
|
{
|
||||||
|
shipDef = &d;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shipDef == nullptr)
|
||||||
|
{
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double threat = shipDef->schematic.productionTimeSeconds;
|
||||||
|
|
||||||
|
// Add material threat for ship base materials.
|
||||||
|
threat += computeMaterialThreat(table, shipDef->schematic.materials);
|
||||||
|
|
||||||
|
// Add module production times and material threats.
|
||||||
|
for (const PlacedModule& pm : modules)
|
||||||
|
{
|
||||||
|
const ModuleDef* moduleDef = nullptr;
|
||||||
|
for (const ModuleDef& d : config.modules.modules)
|
||||||
|
{
|
||||||
|
if (d.id == pm.moduleId)
|
||||||
|
{
|
||||||
|
moduleDef = &d;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (moduleDef == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
threat += moduleDef->productionTimeSeconds;
|
||||||
|
threat += computeMaterialThreat(table, moduleDef->materials);
|
||||||
|
}
|
||||||
|
|
||||||
|
return threat;
|
||||||
|
}
|
||||||
22
src/lib/sim/ThreatCostCalculator.h
Normal file
22
src/lib/sim/ThreatCostCalculator.h
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "ShipLayout.h"
|
||||||
|
|
||||||
|
struct GameConfig;
|
||||||
|
|
||||||
|
struct ThreatCostTable
|
||||||
|
{
|
||||||
|
std::map<std::string, double> itemThreat;
|
||||||
|
double scrapThreat = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
ThreatCostTable computeThreatCostTable(const GameConfig& config);
|
||||||
|
|
||||||
|
double calculateShipThreatCost(const ThreatCostTable& table,
|
||||||
|
const GameConfig& config,
|
||||||
|
const std::string& shipId,
|
||||||
|
const std::vector<PlacedModule>& modules);
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
|
#include "ThreatCostCalculator.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
|
|
||||||
WaveSystem::WaveSystem(const GameConfig& config, std::mt19937& rng)
|
WaveSystem::WaveSystem(const GameConfig& config, std::mt19937& rng)
|
||||||
@@ -102,6 +103,16 @@ double WaveSystem::threatLevel() const
|
|||||||
return m_threatLevel;
|
return m_threatLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double WaveSystem::threatAccumulationRate() const
|
||||||
|
{
|
||||||
|
if (isInQuietWindow())
|
||||||
|
{
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
const double x = static_cast<double>(m_bossWaveCounter);
|
||||||
|
return std::max(0.0, m_config.world.waves.threatRateFormula.evaluate(x));
|
||||||
|
}
|
||||||
|
|
||||||
int WaveSystem::generation() const
|
int WaveSystem::generation() const
|
||||||
{
|
{
|
||||||
return m_generation;
|
return m_generation;
|
||||||
@@ -177,7 +188,8 @@ std::vector<WaveSystem::SpawnEntry> WaveSystem::selectWaveShips(double& budget,
|
|||||||
std::vector<EligibleShip> eligible;
|
std::vector<EligibleShip> eligible;
|
||||||
for (const ShipDef& def : m_config.ships.ships)
|
for (const ShipDef& def : m_config.ships.ships)
|
||||||
{
|
{
|
||||||
const double cost = def.threat.costFormula.evaluate(static_cast<double>(shipLevel));
|
const double cost = calculateShipThreatCost(m_config.threatCosts, m_config,
|
||||||
|
def.id, def.defaultModules);
|
||||||
if (cost > 0.0)
|
if (cost > 0.0)
|
||||||
{
|
{
|
||||||
EligibleShip es;
|
EligibleShip es;
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ public:
|
|||||||
|
|
||||||
double threatLevel() const;
|
double threatLevel() const;
|
||||||
|
|
||||||
|
// Current rate at which threatLevel() is increasing, in threat/second
|
||||||
|
// (REQ-WAV-THREAT-RATE). 0 during a quiet window (REQ-WAV-QUIET) or when
|
||||||
|
// the rate formula evaluates to a negative value.
|
||||||
|
double threatAccumulationRate() const;
|
||||||
|
|
||||||
// Current enemy-station generation level (0 for initial set,
|
// Current enemy-station generation level (0 for initial set,
|
||||||
// incremented by 1 after each push — REQ-PSH-STATION-STATS).
|
// incremented by 1 after each push — REQ-PSH-STATION-STATS).
|
||||||
int generation() const;
|
int generation() const;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
@@ -553,6 +555,64 @@ TEST_CASE("BeltSystem: splitter falls back to other output when preferred is blo
|
|||||||
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: splitter fallback enters the open output at progress 0.75", "[belt]")
|
||||||
|
{
|
||||||
|
// When the preferred output is blocked, the diverted item is dropped onto the
|
||||||
|
// open output near its edge (progress 0.75) instead of at progress 0.0. This
|
||||||
|
// closes the large gap that would otherwise appear between items leaving the
|
||||||
|
// open side of a half-blocked splitter.
|
||||||
|
//
|
||||||
|
// Progress/tick = 0.25 so the 0.0-vs-0.75 entry position is observable: a
|
||||||
|
// normally-routed item starts at 0.0, a fallback item starts at 0.75.
|
||||||
|
const double quarterSpeed = 0.25 * static_cast<double>(kTickRateHz);
|
||||||
|
BeltSystem bs(quarterSpeed);
|
||||||
|
|
||||||
|
const QPoint tileSpl(1, 0);
|
||||||
|
const QPoint tileB(1, 1); // South output belt; North output has no belt (blocked).
|
||||||
|
|
||||||
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
||||||
|
bs.placeBelt(tileB, Rotation::South);
|
||||||
|
|
||||||
|
// Reads a named item's progress along the South output via the rendering contract.
|
||||||
|
// slotWorldPos maps a South-bound slot on tileSpl (y = 0) to worldPos.y == progress.
|
||||||
|
// Matching by id avoids the blocked North item, which also renders at worldPos.y 0.
|
||||||
|
auto southProgressOf = [&bs](const std::string& id) -> std::optional<double>
|
||||||
|
{
|
||||||
|
std::optional<double> progress;
|
||||||
|
bs.forEachVisualItem(QRect(-5, -5, 20, 20), [&](VisualItem vi)
|
||||||
|
{
|
||||||
|
if (vi.type.id == id)
|
||||||
|
{
|
||||||
|
progress = vi.worldPos.y();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return progress;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Permanently block output A: route one item to frontA where it sticks at 1.0
|
||||||
|
// (North has no downstream tile, so it can never move out).
|
||||||
|
bs.tryPutItem(tileSpl, makeItem("blockA"));
|
||||||
|
bs.tick(); // back: 0.25
|
||||||
|
bs.tick(); // back: 0.5 -> frontA at 0.0 (preferred A), nextOutputIsA = false
|
||||||
|
bs.tick(); bs.tick(); bs.tick(); bs.tick(); // frontA: 0.25 -> 0.5 -> 0.75 -> 1.0 (stuck)
|
||||||
|
|
||||||
|
// Item routed to B as the *preferred* output enters at progress 0.0.
|
||||||
|
bs.tryPutItem(tileSpl, makeItem("toB_pref"));
|
||||||
|
bs.tick(); // back: 0.25
|
||||||
|
bs.tick(); // back: 0.5 -> frontB at 0.0 (preferred B), nextOutputIsA = true
|
||||||
|
REQUIRE(southProgressOf("toB_pref") == Approx(0.0));
|
||||||
|
|
||||||
|
// Let it traverse and hand off to the downstream belt, freeing frontB.
|
||||||
|
bs.tick(); bs.tick(); bs.tick(); bs.tick(); // frontB: 0.25 -> 0.5 -> 0.75 -> 1.0 -> tileB
|
||||||
|
|
||||||
|
// Next item prefers A again (nextOutputIsA == true), but A is still blocked,
|
||||||
|
// so it falls back to B — and must enter near the edge at progress 0.75.
|
||||||
|
bs.tryPutItem(tileSpl, makeItem("toB_fallback"));
|
||||||
|
bs.tick(); // back: 0.25
|
||||||
|
bs.tick(); // back: 0.5 -> fallback routes to frontB at 0.75
|
||||||
|
REQUIRE(southProgressOf("toB_fallback") == Approx(0.75));
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Splitter — direct building input (no output belts)
|
// Splitter — direct building input (no output belts)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -337,6 +337,85 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
|
|||||||
REQUIRE_FALSE(b->production.has_value());
|
REQUIRE_FALSE(b->production.has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// REQ-UI-DEBUG-OVERLAY production counts
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites", "[building]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||||
|
int stock = 0;
|
||||||
|
std::mt19937 rng(0);
|
||||||
|
BuildingId nextBuildingId = 1;
|
||||||
|
BuildingSystem bs(cfg, belts,
|
||||||
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
|
[&stock](int n) { stock += n; },
|
||||||
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
|
rng);
|
||||||
|
|
||||||
|
const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
|
const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(10, 0), Rotation::East, 0);
|
||||||
|
(void)smelterId;
|
||||||
|
|
||||||
|
Tick tick = 0;
|
||||||
|
// Both still under construction.
|
||||||
|
REQUIRE(bs.productionBuildingCount() == 0);
|
||||||
|
|
||||||
|
// The queue builds one at a time: miner (10s) completes at tick 300, then
|
||||||
|
// the smelter (15s) starts and completes at tick 300 + 450 = 750.
|
||||||
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
||||||
|
REQUIRE(bs.productionBuildingCount() == 1);
|
||||||
|
|
||||||
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)), tick);
|
||||||
|
REQUIRE(bs.productionBuildingCount() == 2);
|
||||||
|
|
||||||
|
// Neither has a recipe selected, so neither has an active cycle.
|
||||||
|
REQUIRE(bs.activeProductionBuildingCount() == 0);
|
||||||
|
|
||||||
|
bs.setRecipe(minerId, "mine_iron_ore");
|
||||||
|
runTicks(bs, belts, 1, tick);
|
||||||
|
REQUIRE(bs.activeProductionBuildingCount() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle state",
|
||||||
|
"[building]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||||
|
int stock = 0;
|
||||||
|
std::mt19937 rng(0);
|
||||||
|
BuildingId nextBuildingId = 1;
|
||||||
|
BuildingSystem bs(cfg, belts,
|
||||||
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
|
[&stock](int n) { stock += n; },
|
||||||
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
|
rng);
|
||||||
|
|
||||||
|
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
|
bs.setRecipe(id, "mine_iron_ore");
|
||||||
|
|
||||||
|
Tick tick = 0;
|
||||||
|
// Not yet operational while under construction.
|
||||||
|
REQUIRE(bs.activeProductionBuildingCount() == 0);
|
||||||
|
|
||||||
|
// Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330).
|
||||||
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
||||||
|
REQUIRE(bs.activeProductionBuildingCount() == 1);
|
||||||
|
|
||||||
|
// Run cycles 1 and 2 to completion (1s each); cycle 3 stalls once the
|
||||||
|
// output buffer (capacity 2) is full (REQ-MAT-OUTPUT-BUFFER).
|
||||||
|
runTicks(bs, belts, 2 * static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
||||||
|
|
||||||
|
const Building* b = bs.findBuilding(id);
|
||||||
|
REQUIRE(b != nullptr);
|
||||||
|
REQUIRE(static_cast<int>(b->outputBuffer.items.size()) == 2);
|
||||||
|
REQUIRE_FALSE(b->production.has_value());
|
||||||
|
REQUIRE(bs.activeProductionBuildingCount() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Belt pull → input buffer
|
// Belt pull → input buffer
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ add_files(
|
|||||||
BlueprintSerializerTest.cpp
|
BlueprintSerializerTest.cpp
|
||||||
ModuleConfigTest.cpp
|
ModuleConfigTest.cpp
|
||||||
ShipModuleTest.cpp
|
ShipModuleTest.cpp
|
||||||
|
ThreatCostCalculatorTest.cpp
|
||||||
RecipeSchematicTest.cpp
|
RecipeSchematicTest.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
|
|||||||
CHECK(armor.materials[0].amount == 2);
|
CHECK(armor.materials[0].amount == 2);
|
||||||
CHECK(armor.playerProductionLevel == 1);
|
CHECK(armor.playerProductionLevel == 1);
|
||||||
CHECK(armor.productionTimeSeconds == Approx(3.0));
|
CHECK(armor.productionTimeSeconds == Approx(3.0));
|
||||||
CHECK(armor.threatCost == Approx(2.0));
|
|
||||||
CHECK(armor.fillColor == "#808080");
|
CHECK(armor.fillColor == "#808080");
|
||||||
CHECK(armor.glyph == "A");
|
CHECK(armor.glyph == "A");
|
||||||
REQUIRE(armor.statModifiers.size() == 1);
|
REQUIRE(armor.statModifiers.size() == 1);
|
||||||
|
|||||||
110
src/test/ThreatCostCalculatorTest.cpp
Normal file
110
src/test/ThreatCostCalculatorTest.cpp
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include "ConfigLoader.h"
|
||||||
|
#include "ThreatCostCalculator.h"
|
||||||
|
|
||||||
|
static GameConfig loadConfig()
|
||||||
|
{
|
||||||
|
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ThreatCostCalculator: miner item threat equals duration", "[threat]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ThreatCostTable& table = cfg.threatCosts;
|
||||||
|
|
||||||
|
CHECK(table.itemThreat.at("iron_ore") == Approx(1.0));
|
||||||
|
CHECK(table.itemThreat.at("copper_ore") == Approx(1.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ThreatCostCalculator: smelter item threat includes input costs", "[threat]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ThreatCostTable& table = cfg.threatCosts;
|
||||||
|
|
||||||
|
// iron_ingot: duration 2.0 + iron_ore(1.0) * 2 = 4.0
|
||||||
|
CHECK(table.itemThreat.at("iron_ingot") == Approx(4.0));
|
||||||
|
// copper_ingot: duration 2.5 + copper_ore(1.5) * 2 = 5.5
|
||||||
|
CHECK(table.itemThreat.at("copper_ingot") == Approx(5.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ThreatCostCalculator: assembler takes max across recipes", "[threat]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ThreatCostTable& table = cfg.threatCosts;
|
||||||
|
|
||||||
|
// circuit_board has three non-reprocessing recipes:
|
||||||
|
// 5.0 + iron_ingot(4.0)*3 + copper_ingot(5.5)*2 = 28.0
|
||||||
|
// 3.0 + copper_ingot(5.5)*3 = 19.5
|
||||||
|
// 6.0 + iron_ingot(4.0)*5 = 26.0
|
||||||
|
// max = 28.0
|
||||||
|
CHECK(table.itemThreat.at("circuit_board") == Approx(28.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ThreatCostCalculator: scrap threat from cheapest ship", "[threat]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ThreatCostTable& table = cfg.threatCosts;
|
||||||
|
|
||||||
|
// Cheapest ship by scrap_drop is interceptor (scrap_drop=2).
|
||||||
|
// Interceptor threat: 10 + iron_ingot(4)*3 + circuit_board(28)*1
|
||||||
|
// + laser_cannon(5 + iron_ingot(4)*1) = 10 + 12 + 28 + 9 = 59.0
|
||||||
|
// scrapThreat = 59.0 / 2 = 29.5
|
||||||
|
CHECK(table.scrapThreat == Approx(29.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ThreatCostCalculator: reprocessing-only item threat", "[threat]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ThreatCostTable& table = cfg.threatCosts;
|
||||||
|
|
||||||
|
// advanced_alloy: reprocessing recipe with scrap*5, duration 3.0, probability 0.1
|
||||||
|
// (29.5 * 5 + 3.0) / 0.1 = 1505.0
|
||||||
|
CHECK(table.itemThreat.at("advanced_alloy") == Approx(1505.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ThreatCostCalculator: ship threat with default modules", "[threat]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ThreatCostTable& table = cfg.threatCosts;
|
||||||
|
|
||||||
|
// interceptor: 10 + iron_ingot(4)*3 + circuit_board(28)*1 + laser_cannon(5 + 4*1) = 59.0
|
||||||
|
double interceptorThreat = calculateShipThreatCost(
|
||||||
|
table, cfg, "interceptor", cfg.ships.ships[0].defaultModules);
|
||||||
|
CHECK(interceptorThreat == Approx(59.0));
|
||||||
|
|
||||||
|
// salvage_ship (no default modules): 10 + iron_ingot(4)*4 = 26.0
|
||||||
|
double salvageThreat = calculateShipThreatCost(
|
||||||
|
table, cfg, "salvage_ship", cfg.ships.ships[2].defaultModules);
|
||||||
|
CHECK(salvageThreat == Approx(26.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ThreatCostCalculator: ship threat with custom modules", "[threat]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ThreatCostTable& table = cfg.threatCosts;
|
||||||
|
|
||||||
|
// interceptor base: 10 + iron_ingot(4)*3 + circuit_board(28)*1 = 50.0
|
||||||
|
// + armor_plate: 3 + iron_ingot(4)*2 = 11.0
|
||||||
|
// + sensor_booster: 2 + circuit_board(28)*1 = 30.0
|
||||||
|
// total = 50.0 + 11.0 + 30.0 = 91.0
|
||||||
|
std::vector<PlacedModule> modules;
|
||||||
|
PlacedModule armor;
|
||||||
|
armor.moduleId = "armor_plate";
|
||||||
|
modules.push_back(armor);
|
||||||
|
PlacedModule sensor;
|
||||||
|
sensor.moduleId = "sensor_booster";
|
||||||
|
modules.push_back(sensor);
|
||||||
|
|
||||||
|
double threat = calculateShipThreatCost(table, cfg, "interceptor", modules);
|
||||||
|
CHECK(threat == Approx(91.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ThreatCostCalculator: unknown ship returns zero", "[threat]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ThreatCostTable& table = cfg.threatCosts;
|
||||||
|
|
||||||
|
double threat = calculateShipThreatCost(table, cfg, "nonexistent_ship", {});
|
||||||
|
CHECK(threat == Approx(0.0));
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
#include "ThreatCostCalculator.h"
|
||||||
#include "WaveSystem.h"
|
#include "WaveSystem.h"
|
||||||
|
|
||||||
static GameConfig loadConfig()
|
static GameConfig loadConfig()
|
||||||
@@ -50,6 +51,35 @@ TEST_CASE("WaveSystem: threat accumulates at boss wave counter rate", "[wave]")
|
|||||||
REQUIRE(ws.threatLevel() == Approx(1.0));
|
REQUIRE(ws.threatLevel() == Approx(1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("WaveSystem: threatAccumulationRate matches the rate formula outside quiet windows",
|
||||||
|
"[wave]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
std::mt19937 rng(42);
|
||||||
|
WaveSystem ws(cfg, rng);
|
||||||
|
|
||||||
|
// threat_rate_formula = "x", boss wave counter starts at 1 → rate = 1 threat/s.
|
||||||
|
REQUIRE(ws.threatAccumulationRate() == Approx(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("WaveSystem: threatAccumulationRate is 0 during a quiet window", "[wave]")
|
||||||
|
{
|
||||||
|
GameConfig cfg = loadConfig();
|
||||||
|
// Start with the boss countdown already at the pre-boss quiet threshold.
|
||||||
|
cfg.world.waves.bossCountdownSeconds = cfg.world.waves.bossQuietBeforeSeconds;
|
||||||
|
std::mt19937 rng(42);
|
||||||
|
WaveSystem ws(cfg, rng);
|
||||||
|
|
||||||
|
REQUIRE(ws.threatAccumulationRate() == Approx(0.0));
|
||||||
|
|
||||||
|
const double before = ws.threatLevel();
|
||||||
|
for (int i = 0; i < static_cast<int>(secondsToTicks(1.0)); ++i)
|
||||||
|
{
|
||||||
|
ws.tickThreatAccumulation();
|
||||||
|
}
|
||||||
|
REQUIRE(ws.threatLevel() == Approx(before));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruction", "[wave]")
|
TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruction", "[wave]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
@@ -201,25 +231,16 @@ TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]
|
|||||||
REQUIRE(foundEnemyShip);
|
REQUIRE(foundEnemyShip);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("WaveSystem: only eligible ships (cost > 0) appear in waves", "[wave]")
|
TEST_CASE("WaveSystem: all ships have positive dynamic threat cost", "[wave]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig(), 42);
|
const GameConfig cfg = loadConfig();
|
||||||
|
|
||||||
// Run long enough for several waves.
|
for (const ShipDef& def : cfg.ships.ships)
|
||||||
const int limit = static_cast<int>(secondsToTicks(120.0));
|
|
||||||
for (int i = 0; i < limit; ++i)
|
|
||||||
{
|
{
|
||||||
sim.tick();
|
const double cost = calculateShipThreatCost(cfg.threatCosts, cfg,
|
||||||
|
def.id, def.defaultModules);
|
||||||
|
CHECK(cost > 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
sim.admin().forEach<ShipIdentityComponent, FactionComponent>(
|
|
||||||
[&](entt::entity /*e*/, const ShipIdentityComponent& si, const FactionComponent& f)
|
|
||||||
{
|
|
||||||
if (!f.isEnemy) { return; }
|
|
||||||
// salvage_ship and repair_ship have cost_formula = "0" and must not spawn.
|
|
||||||
REQUIRE(si.schematicId != "salvage_ship");
|
|
||||||
REQUIRE(si.schematicId != "repair_ship");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPen>
|
#include <QPen>
|
||||||
#include <QPolygonF>
|
#include <QPolygonF>
|
||||||
|
#include <QStringList>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
@@ -924,10 +925,18 @@ void GameWorldView::drawDebugOverlay(QPainter& painter)
|
|||||||
{
|
{
|
||||||
painter.resetTransform();
|
painter.resetTransform();
|
||||||
|
|
||||||
const QString line1 = tr("Accumulated Threat Level: %1")
|
const QStringList lines = {
|
||||||
.arg(m_sim->threatLevel(), 0, 'f', 1);
|
tr("Accumulated Threat Level: %1")
|
||||||
const QString line2 = tr("Time until Wave: %1s")
|
.arg(m_sim->threatLevel(), 0, 'f', 1),
|
||||||
.arg(ticksToSeconds(m_sim->normalGapRemainingTicks()), 0, 'f', 1);
|
tr("Time until Wave: %1s")
|
||||||
|
.arg(ticksToSeconds(m_sim->normalGapRemainingTicks()), 0, 'f', 1),
|
||||||
|
tr("Threat Accumulation Rate: %1 threat/s")
|
||||||
|
.arg(m_sim->threatAccumulationRate(), 0, 'f', 1),
|
||||||
|
tr("Max Factory Production: %1 threat/s")
|
||||||
|
.arg(m_sim->maxFactoryProductionThreatRate(), 0, 'f', 1),
|
||||||
|
tr("Current Factory Production: %1 threat/s")
|
||||||
|
.arg(m_sim->currentFactoryProductionThreatRate(), 0, 'f', 1),
|
||||||
|
};
|
||||||
|
|
||||||
QFont font = painter.font();
|
QFont font = painter.font();
|
||||||
font.setPointSize(m_visuals->toast.fontSize);
|
font.setPointSize(m_visuals->toast.fontSize);
|
||||||
@@ -937,19 +946,26 @@ void GameWorldView::drawDebugOverlay(QPainter& painter)
|
|||||||
const int lineH = fm.height();
|
const int lineH = fm.height();
|
||||||
const int padding = 8;
|
const int padding = 8;
|
||||||
const int spacing = 4;
|
const int spacing = 4;
|
||||||
const int textW = std::max(fm.horizontalAdvance(line1),
|
|
||||||
fm.horizontalAdvance(line2));
|
int textW = 0;
|
||||||
|
for (const QString& line : lines)
|
||||||
|
{
|
||||||
|
textW = std::max(textW, fm.horizontalAdvance(line));
|
||||||
|
}
|
||||||
const int bgW = textW + padding * 2;
|
const int bgW = textW + padding * 2;
|
||||||
const int bgH = lineH * 2 + spacing + padding * 2;
|
const int bgH = lineH * lines.size() + spacing * (lines.size() - 1) + padding * 2;
|
||||||
|
|
||||||
const QRect bgRect(padding, padding, bgW, bgH);
|
const QRect bgRect(padding, padding, bgW, bgH);
|
||||||
painter.fillRect(bgRect, QColor(0, 0, 0, 160));
|
painter.fillRect(bgRect, QColor(0, 0, 0, 160));
|
||||||
|
|
||||||
painter.setPen(Qt::white);
|
painter.setPen(Qt::white);
|
||||||
const QRect textRect1(padding * 2, padding + padding, textW, lineH);
|
int y = padding * 2;
|
||||||
const QRect textRect2(padding * 2, textRect1.bottom() + spacing, textW, lineH);
|
for (const QString& line : lines)
|
||||||
painter.drawText(textRect1, Qt::AlignLeft | Qt::AlignVCenter, line1);
|
{
|
||||||
painter.drawText(textRect2, Qt::AlignLeft | Qt::AlignVCenter, line2);
|
const QRect textRect(padding * 2, y, textW, lineH);
|
||||||
|
painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, line);
|
||||||
|
y += lineH + spacing;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::drawBeams(QPainter& painter)
|
void GameWorldView::drawBeams(QPainter& painter)
|
||||||
@@ -1144,6 +1160,8 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
|
|||||||
break;
|
break;
|
||||||
case Qt::Key_M:
|
case Qt::Key_M:
|
||||||
m_debugDraw = !m_debugDraw;
|
m_debugDraw = !m_debugDraw;
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<DebugDrawToggledEvent>(m_debugDraw));
|
||||||
break;
|
break;
|
||||||
case Qt::Key_L:
|
case Qt::Key_L:
|
||||||
EventManager::getInstance()->addEvent(std::make_shared<TracePrintRequestedEvent>());
|
EventManager::getInstance()->addEvent(std::make_shared<TracePrintRequestedEvent>());
|
||||||
@@ -1438,6 +1456,11 @@ double GameWorldView::gameSpeed() const
|
|||||||
return m_gameSpeedMultiplier;
|
return m_gameSpeedMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GameWorldView::isDebugDrawEnabled() const
|
||||||
|
{
|
||||||
|
return m_debugDraw;
|
||||||
|
}
|
||||||
|
|
||||||
void GameWorldView::resetFrameTimer()
|
void GameWorldView::resetFrameTimer()
|
||||||
{
|
{
|
||||||
m_frameTimer.restart();
|
m_frameTimer.restart();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
#include "ExitBlueprintModeRequestedEvent.h"
|
#include "ExitBlueprintModeRequestedEvent.h"
|
||||||
#include "ExitBuilderModeRequestedEvent.h"
|
#include "ExitBuilderModeRequestedEvent.h"
|
||||||
|
#include "DebugDrawToggledEvent.h"
|
||||||
#include "WeaponFiredEvent.h"
|
#include "WeaponFiredEvent.h"
|
||||||
#include "SchematicChoiceOption.h"
|
#include "SchematicChoiceOption.h"
|
||||||
#include "SpeedChangeRequestedEvent.h"
|
#include "SpeedChangeRequestedEvent.h"
|
||||||
@@ -65,6 +66,7 @@ public:
|
|||||||
~GameWorldView() override;
|
~GameWorldView() override;
|
||||||
|
|
||||||
double gameSpeed() const;
|
double gameSpeed() const;
|
||||||
|
bool isDebugDrawEnabled() const;
|
||||||
void resetFrameTimer();
|
void resetFrameTimer();
|
||||||
void setGameSpeed(double multiplier);
|
void setGameSpeed(double multiplier);
|
||||||
void resetForNewGame();
|
void resetForNewGame();
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCloseEvent>
|
#include <QCloseEvent>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QResizeEvent>
|
#include <QResizeEvent>
|
||||||
@@ -40,18 +39,18 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, QWidget* p
|
|||||||
|
|
||||||
m_gameWorldView = new GameWorldView(sim, &sim->config(), &m_visuals, this);
|
m_gameWorldView = new GameWorldView(sim, &sim->config(), &m_visuals, this);
|
||||||
|
|
||||||
m_bottomPanel = new QWidget(this);
|
m_sidePanel = new QWidget(this);
|
||||||
QHBoxLayout* bottomLayout = new QHBoxLayout(m_bottomPanel);
|
QVBoxLayout* sideLayout = new QVBoxLayout(m_sidePanel);
|
||||||
bottomLayout->setContentsMargins(0, 0, 0, 0);
|
sideLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
bottomLayout->setSpacing(0);
|
sideLayout->setSpacing(0);
|
||||||
|
|
||||||
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->config(), m_bottomPanel);
|
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->config(), m_sidePanel);
|
||||||
m_buildButtonGrid = new BuildButtonGrid(&sim->config(), m_bottomPanel);
|
m_buildButtonGrid = new BuildButtonGrid(&sim->config(), m_sidePanel);
|
||||||
m_blueprintPanel = new BlueprintPanel(sim, &sim->config(), m_bottomPanel);
|
m_blueprintPanel = new BlueprintPanel(sim, &sim->config(), m_sidePanel);
|
||||||
|
|
||||||
bottomLayout->addWidget(m_selectedBuildingPanel, 1);
|
sideLayout->addWidget(m_selectedBuildingPanel, 1);
|
||||||
bottomLayout->addWidget(m_buildButtonGrid, 1);
|
sideLayout->addWidget(m_buildButtonGrid, 1);
|
||||||
bottomLayout->addWidget(m_blueprintPanel, 1);
|
sideLayout->addWidget(m_blueprintPanel, 1);
|
||||||
|
|
||||||
m_gameWorldView->setFocus();
|
m_gameWorldView->setFocus();
|
||||||
|
|
||||||
@@ -117,13 +116,12 @@ void MainWindow::layoutPanels()
|
|||||||
const int totalH = height();
|
const int totalH = height();
|
||||||
const int headerH = m_headerBar->sizeHint().height();
|
const int headerH = m_headerBar->sizeHint().height();
|
||||||
if (headerH <= 0) { return; }
|
if (headerH <= 0) { return; }
|
||||||
const int remaining = totalH - headerH;
|
const int mainW = totalW * 75 / 100;
|
||||||
const int gameH = remaining * 70 / 100;
|
const int sideW = totalW - mainW;
|
||||||
const int panelH = remaining - gameH;
|
|
||||||
|
|
||||||
m_headerBar->setGeometry(0, 0, totalW, headerH);
|
m_headerBar->setGeometry(0, 0, mainW, headerH);
|
||||||
m_gameWorldView->setGeometry(0, headerH, totalW, gameH);
|
m_gameWorldView->setGeometry(0, headerH, mainW, totalH - headerH);
|
||||||
m_bottomPanel->setGeometry(0, headerH + gameH, totalW, panelH);
|
m_sidePanel->setGeometry(mainW, 0, sideW, totalH);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
|
void MainWindow::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
|
||||||
@@ -223,6 +221,7 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
|
|||||||
m_layoutBlueprints,
|
m_layoutBlueprints,
|
||||||
std::move(unlockedModuleIds),
|
std::move(unlockedModuleIds),
|
||||||
std::move(moduleLevels),
|
std::move(moduleLevels),
|
||||||
|
m_gameWorldView->isDebugDrawEnabled(),
|
||||||
this);
|
this);
|
||||||
if (dialog.exec() == QDialog::Accepted && dialog.result().has_value())
|
if (dialog.exec() == QDialog::Accepted && dialog.result().has_value())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ private:
|
|||||||
SelectedBuildingPanel* m_selectedBuildingPanel;
|
SelectedBuildingPanel* m_selectedBuildingPanel;
|
||||||
BuildButtonGrid* m_buildButtonGrid;
|
BuildButtonGrid* m_buildButtonGrid;
|
||||||
BlueprintPanel* m_blueprintPanel;
|
BlueprintPanel* m_blueprintPanel;
|
||||||
QWidget* m_bottomPanel;
|
QWidget* m_sidePanel;
|
||||||
|
|
||||||
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
|
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipStatsCalculator.h"
|
#include "ShipStatsCalculator.h"
|
||||||
#include "ShipStatsPanel.h"
|
#include "ShipStatsPanel.h"
|
||||||
|
#include "ThreatCostCalculator.h"
|
||||||
#include "StationBodyComponent.h"
|
#include "StationBodyComponent.h"
|
||||||
#include "TickAdvancedEvent.h"
|
#include "TickAdvancedEvent.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
@@ -784,6 +785,19 @@ void SelectedBuildingPanel::buildEntityShip(entt::entity entity)
|
|||||||
|
|
||||||
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
||||||
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
||||||
|
m_entityStatsPanel->setDebugDrawEnabled(m_debugDraw);
|
||||||
|
|
||||||
|
for (const ShipDef& def : m_config->ships.ships)
|
||||||
|
{
|
||||||
|
if (def.id == identity.schematicId)
|
||||||
|
{
|
||||||
|
double threat = calculateShipThreatCost(
|
||||||
|
m_config->threatCosts, *m_config, def.id, def.defaultModules);
|
||||||
|
m_entityStatsPanel->setThreatCost(threat);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_entityStatsPanel->show();
|
m_entityStatsPanel->show();
|
||||||
|
|
||||||
m_stationStatsLabel->hide();
|
m_stationStatsLabel->hide();
|
||||||
@@ -869,3 +883,9 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr<const SelectionChangedEv
|
|||||||
{
|
{
|
||||||
onSelectionChanged(event->ids);
|
onSelectionChanged(event->ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event)
|
||||||
|
{
|
||||||
|
m_debugDraw = event->active;
|
||||||
|
m_entityStatsPanel->setDebugDrawEnabled(event->active);
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
|
#include "DebugDrawToggledEvent.h"
|
||||||
#include "EntitySelectedEvent.h"
|
#include "EntitySelectedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
@@ -33,7 +34,8 @@ class QVBoxLayout;
|
|||||||
class SelectedBuildingPanel : public QWidget,
|
class SelectedBuildingPanel : public QWidget,
|
||||||
public CombinedEventHandler<TickAdvancedEvent,
|
public CombinedEventHandler<TickAdvancedEvent,
|
||||||
EntitySelectedEvent,
|
EntitySelectedEvent,
|
||||||
SelectionChangedEvent>
|
SelectionChangedEvent,
|
||||||
|
DebugDrawToggledEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ private:
|
|||||||
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
|
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onRecipeChanged(int comboIndex);
|
void onRecipeChanged(int comboIndex);
|
||||||
@@ -86,6 +89,7 @@ private:
|
|||||||
QPoint m_splitterTile;
|
QPoint m_splitterTile;
|
||||||
std::string m_currentRecipeId;
|
std::string m_currentRecipeId;
|
||||||
|
|
||||||
|
bool m_debugDraw = false;
|
||||||
std::optional<entt::entity> m_selectedEntity;
|
std::optional<entt::entity> m_selectedEntity;
|
||||||
ShipStatsPanel* m_entityStatsPanel;
|
ShipStatsPanel* m_entityStatsPanel;
|
||||||
QLabel* m_entityTitleLabel;
|
QLabel* m_entityTitleLabel;
|
||||||
|
|||||||
@@ -366,6 +366,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
|||||||
std::vector<ShipLayoutBlueprint>& allBlueprints,
|
std::vector<ShipLayoutBlueprint>& allBlueprints,
|
||||||
std::set<std::string> unlockedModuleIds,
|
std::set<std::string> unlockedModuleIds,
|
||||||
std::map<std::string, int> moduleLevels,
|
std::map<std::string, int> moduleLevels,
|
||||||
|
bool debugDraw,
|
||||||
QWidget* parent)
|
QWidget* parent)
|
||||||
: QDialog(parent)
|
: QDialog(parent)
|
||||||
, m_config(config)
|
, m_config(config)
|
||||||
@@ -380,6 +381,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
|||||||
, m_removeButton(nullptr)
|
, m_removeButton(nullptr)
|
||||||
, m_gridWidget(nullptr)
|
, m_gridWidget(nullptr)
|
||||||
, m_statsPanel(nullptr)
|
, m_statsPanel(nullptr)
|
||||||
|
, m_debugDraw(debugDraw)
|
||||||
{
|
{
|
||||||
setWindowTitle(tr("Configure Ship Layout"));
|
setWindowTitle(tr("Configure Ship Layout"));
|
||||||
setModal(true);
|
setModal(true);
|
||||||
@@ -434,6 +436,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
|||||||
|
|
||||||
// Left column: ship stats panel.
|
// Left column: ship stats panel.
|
||||||
m_statsPanel = new ShipStatsPanel(config, this);
|
m_statsPanel = new ShipStatsPanel(config, this);
|
||||||
|
m_statsPanel->setDebugDrawEnabled(m_debugDraw);
|
||||||
columnsLayout->addWidget(m_statsPanel);
|
columnsLayout->addWidget(m_statsPanel);
|
||||||
|
|
||||||
// Center column: module selection buttons.
|
// Center column: module selection buttons.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public:
|
|||||||
std::vector<ShipLayoutBlueprint>& allBlueprints,
|
std::vector<ShipLayoutBlueprint>& allBlueprints,
|
||||||
std::set<std::string> unlockedModuleIds,
|
std::set<std::string> unlockedModuleIds,
|
||||||
std::map<std::string, int> moduleLevels,
|
std::map<std::string, int> moduleLevels,
|
||||||
|
bool debugDraw,
|
||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
std::optional<ShipLayoutConfig> result() const;
|
std::optional<ShipLayoutConfig> result() const;
|
||||||
@@ -77,6 +78,7 @@ private:
|
|||||||
QPushButton* m_removeButton;
|
QPushButton* m_removeButton;
|
||||||
QWidget* m_gridWidget;
|
QWidget* m_gridWidget;
|
||||||
ShipStatsPanel* m_statsPanel;
|
ShipStatsPanel* m_statsPanel;
|
||||||
|
bool m_debugDraw;
|
||||||
|
|
||||||
std::optional<ShipLayoutConfig> m_result;
|
std::optional<ShipLayoutConfig> m_result;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "ShipStatsCalculator.h"
|
#include "ShipStatsCalculator.h"
|
||||||
|
#include "ThreatCostCalculator.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -103,6 +104,11 @@ ShipStatsPanel::ShipStatsPanel(const GameConfig* config, QWidget* parent)
|
|||||||
m_repairSection->setVisible(false);
|
m_repairSection->setVisible(false);
|
||||||
layout->addWidget(m_repairSection);
|
layout->addWidget(m_repairSection);
|
||||||
|
|
||||||
|
// Threat cost — debug-only, initially hidden.
|
||||||
|
m_threatCostLabel = makeStatLabel(this);
|
||||||
|
m_threatCostLabel->setVisible(false);
|
||||||
|
layout->addWidget(m_threatCostLabel);
|
||||||
|
|
||||||
layout->addStretch();
|
layout->addStretch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +121,10 @@ void ShipStatsPanel::refresh(const std::string& shipId,
|
|||||||
moduleLevelOverrides);
|
moduleLevelOverrides);
|
||||||
const QString hpText = tr("HP: %1").arg(static_cast<int>(stats.hp + 0.5f));
|
const QString hpText = tr("HP: %1").arg(static_cast<int>(stats.hp + 0.5f));
|
||||||
applyStats(stats, hpText);
|
applyStats(stats, hpText);
|
||||||
|
|
||||||
|
const double threat = calculateShipThreatCost(m_config->threatCosts, *m_config,
|
||||||
|
shipId, modules);
|
||||||
|
setThreatCost(threat);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShipStatsPanel::refreshFromLive(const ShipStats& stats, float currentHp)
|
void ShipStatsPanel::refreshFromLive(const ShipStats& stats, float currentHp)
|
||||||
@@ -180,3 +190,15 @@ void ShipStatsPanel::applyStats(const ShipStats& stats, const QString& hpText)
|
|||||||
m_repairSection->setVisible(false);
|
m_repairSection->setVisible(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ShipStatsPanel::setThreatCost(double cost)
|
||||||
|
{
|
||||||
|
m_threatCostLabel->setText(tr("Threat Cost: %1").arg(cost, 0, 'f', 1));
|
||||||
|
m_threatCostLabel->setVisible(m_debugDraw);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipStatsPanel::setDebugDrawEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
m_debugDraw = enabled;
|
||||||
|
m_threatCostLabel->setVisible(m_debugDraw);
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,10 +26,14 @@ public:
|
|||||||
|
|
||||||
void refreshFromLive(const ShipStats& stats, float currentHp);
|
void refreshFromLive(const ShipStats& stats, float currentHp);
|
||||||
|
|
||||||
|
void setThreatCost(double cost);
|
||||||
|
void setDebugDrawEnabled(bool enabled);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void applyStats(const ShipStats& stats, const QString& hpText);
|
void applyStats(const ShipStats& stats, const QString& hpText);
|
||||||
|
|
||||||
const GameConfig* m_config;
|
const GameConfig* m_config;
|
||||||
|
bool m_debugDraw = false;
|
||||||
|
|
||||||
QLabel* m_hpLabel;
|
QLabel* m_hpLabel;
|
||||||
QLabel* m_speedLabel;
|
QLabel* m_speedLabel;
|
||||||
@@ -50,4 +54,6 @@ private:
|
|||||||
QWidget* m_repairSection;
|
QWidget* m_repairSection;
|
||||||
QLabel* m_repairRateLabel;
|
QLabel* m_repairRateLabel;
|
||||||
QLabel* m_repairRangeLabel;
|
QLabel* m_repairRangeLabel;
|
||||||
|
|
||||||
|
QLabel* m_threatCostLabel;
|
||||||
};
|
};
|
||||||
|
|||||||
170
tools/verify_layouts.py
Normal file
170
tools/verify_layouts.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify which module surface masks fit which ship layout grids.
|
||||||
|
|
||||||
|
Reads ships.toml and modules.toml, then checks every module footprint
|
||||||
|
against every ship layout the same way the game does (all four mask
|
||||||
|
rotations, every placement position). Prints three reports:
|
||||||
|
|
||||||
|
1. Fit matrix — can the footprint be placed on the hull at all?
|
||||||
|
2. Max simultaneous — how many disjoint copies of a footprint fit at
|
||||||
|
once (only computed for footprints of 4+ cells;
|
||||||
|
smaller ones would be slow and uninteresting).
|
||||||
|
3. Cell counts — buildable cells per hull.
|
||||||
|
|
||||||
|
Use it after editing layout grids or surface masks to confirm the
|
||||||
|
footprint-gating rules in docs/content_design.md still hold, e.g. that
|
||||||
|
no 2x2 area exists on s-class hulls or that the drone hangar fits the
|
||||||
|
carrier only.
|
||||||
|
|
||||||
|
Usage (from the repository root or anywhere else):
|
||||||
|
|
||||||
|
python dota_factory/tools/verify_layouts.py
|
||||||
|
python dota_factory/tools/verify_layouts.py --config-dir path/to/config
|
||||||
|
|
||||||
|
By default the config directory is resolved relative to this script
|
||||||
|
(../bin/app/data/config). Requires the 'toml' package on Python < 3.11
|
||||||
|
(pip install --user toml); on 3.11+ the standard tomllib is used.
|
||||||
|
|
||||||
|
The script is informational only — it always exits 0. Read the matrix
|
||||||
|
and compare it against the intended gating in docs/content_design.md.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def load_toml(path):
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
with open(path, "rb") as fh:
|
||||||
|
return tomllib.load(fh)
|
||||||
|
except ImportError:
|
||||||
|
import toml
|
||||||
|
return toml.load(path)
|
||||||
|
|
||||||
|
|
||||||
|
def grid_cells(rows):
|
||||||
|
"""Set of (x, y) for every 'O' cell in a list of layout/mask strings."""
|
||||||
|
return {(x, y)
|
||||||
|
for y, row in enumerate(rows)
|
||||||
|
for x, ch in enumerate(row)
|
||||||
|
if ch == "O"}
|
||||||
|
|
||||||
|
|
||||||
|
def rotate_cw(shape):
|
||||||
|
height = max(y for x, y in shape) + 1
|
||||||
|
return {(height - 1 - y, x) for x, y in shape}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(shape):
|
||||||
|
min_x = min(x for x, y in shape)
|
||||||
|
min_y = min(y for x, y in shape)
|
||||||
|
return frozenset((x - min_x, y - min_y) for x, y in shape)
|
||||||
|
|
||||||
|
|
||||||
|
def orientations(shape):
|
||||||
|
"""All distinct 90-degree rotations of a shape, normalized to (0, 0)."""
|
||||||
|
result = []
|
||||||
|
current = shape
|
||||||
|
for _ in range(4):
|
||||||
|
norm = normalize(current)
|
||||||
|
if norm not in result:
|
||||||
|
result.append(norm)
|
||||||
|
current = rotate_cw(current)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def placements(layout, shape):
|
||||||
|
"""Every position (in any rotation) where shape fits fully on layout."""
|
||||||
|
found = []
|
||||||
|
layout_w = max(x for x, y in layout) + 1
|
||||||
|
layout_h = max(y for x, y in layout) + 1
|
||||||
|
for orient in orientations(shape):
|
||||||
|
shape_w = max(x for x, y in orient) + 1
|
||||||
|
shape_h = max(y for x, y in orient) + 1
|
||||||
|
for off_x in range(layout_w - shape_w + 1):
|
||||||
|
for off_y in range(layout_h - shape_h + 1):
|
||||||
|
cells = frozenset((x + off_x, y + off_y) for x, y in orient)
|
||||||
|
if cells <= layout and cells not in found:
|
||||||
|
found.append(cells)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def max_disjoint(layout, shape):
|
||||||
|
"""Maximum number of non-overlapping placements of shape on layout."""
|
||||||
|
options = placements(layout, shape)
|
||||||
|
best = [0]
|
||||||
|
|
||||||
|
def recurse(start_index, used, count):
|
||||||
|
if count > best[0]:
|
||||||
|
best[0] = count
|
||||||
|
for i in range(start_index, len(options)):
|
||||||
|
if not (options[i] & used):
|
||||||
|
recurse(i + 1, used | options[i], count + 1)
|
||||||
|
|
||||||
|
recurse(0, frozenset(), 0)
|
||||||
|
return best[0]
|
||||||
|
|
||||||
|
|
||||||
|
def shape_label(shape):
|
||||||
|
width = max(x for x, y in shape) + 1
|
||||||
|
height = max(y for x, y in shape) + 1
|
||||||
|
if len(shape) == width * height:
|
||||||
|
return "{}x{}".format(width, height)
|
||||||
|
return "{}x{}-{}c".format(width, height, len(shape))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
default_dir = os.path.normpath(os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
"..", "bin", "app", "data", "config"))
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Check module surface masks against ship layout grids.")
|
||||||
|
parser.add_argument("--config-dir", default=default_dir,
|
||||||
|
help="directory containing ships.toml and modules.toml"
|
||||||
|
" (default: %(default)s)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
ships = load_toml(os.path.join(args.config_dir, "ships.toml"))["ship"]
|
||||||
|
modules = load_toml(os.path.join(args.config_dir, "modules.toml"))["module"]
|
||||||
|
|
||||||
|
layouts = [(s["id"], grid_cells(s["layout"])) for s in ships]
|
||||||
|
|
||||||
|
# Group modules that share the same footprint (up to rotation).
|
||||||
|
footprint_modules = {} # canonical shape -> [module ids]
|
||||||
|
for module in modules:
|
||||||
|
shape = grid_cells(module["surface_mask"])
|
||||||
|
canonical = min(orientations(shape), key=sorted)
|
||||||
|
footprint_modules.setdefault(canonical, []).append(module["id"])
|
||||||
|
footprints = sorted(footprint_modules.items(), key=lambda e: len(e[0]))
|
||||||
|
|
||||||
|
column_header = "".join("{:>8}".format(ship_id[:7])
|
||||||
|
for ship_id, _ in layouts)
|
||||||
|
|
||||||
|
print("Fit matrix (YES = at least one placement exists)")
|
||||||
|
print("{:48}{}".format("footprint (modules)", column_header))
|
||||||
|
for shape, module_ids in footprints:
|
||||||
|
name = "{:8}{}".format(shape_label(shape), ", ".join(module_ids))
|
||||||
|
row = "".join("{:>8}".format("YES" if placements(layout, shape) else "-")
|
||||||
|
for _, layout in layouts)
|
||||||
|
print("{:48}{}".format(name[:47], row))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Max simultaneous (disjoint) placements, footprints of 4+ cells")
|
||||||
|
print("{:48}{}".format("footprint (modules)", column_header))
|
||||||
|
for shape, module_ids in footprints:
|
||||||
|
if len(shape) < 4:
|
||||||
|
continue
|
||||||
|
name = "{:8}{}".format(shape_label(shape), ", ".join(module_ids))
|
||||||
|
row = "".join("{:>8}".format(max_disjoint(layout, shape))
|
||||||
|
for _, layout in layouts)
|
||||||
|
print("{:48}{}".format(name[:47], row))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Buildable cells per hull")
|
||||||
|
for ship_id, layout in layouts:
|
||||||
|
print("{:16}{}".format(ship_id, len(layout)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
132
tools/verify_recipes.py
Normal file
132
tools/verify_recipes.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify the recipe tree is closed and consistent.
|
||||||
|
|
||||||
|
Reads recipes.toml, ships.toml, modules.toml, and visuals.toml, then checks:
|
||||||
|
|
||||||
|
1. Producers — every item consumed anywhere (recipe inputs, ship hull
|
||||||
|
materials, module materials) is produced by some recipe. 'scrap' is
|
||||||
|
exempt: it drops from destroyed ships.
|
||||||
|
2. Visuals — every item that exists in the economy has an [items.*]
|
||||||
|
entry in visuals.toml, and visuals.toml has no entries for items
|
||||||
|
that no longer exist.
|
||||||
|
3. Orphans — items that are produced but never consumed (warning only;
|
||||||
|
'building_block' is exempt: the HQ consumes it).
|
||||||
|
|
||||||
|
It also prints which items are obtainable ONLY through reprocessing —
|
||||||
|
the combat-gated materials — so changes to that gate are visible.
|
||||||
|
|
||||||
|
Usage (from the repository root or anywhere else):
|
||||||
|
|
||||||
|
python dota_factory/tools/verify_recipes.py
|
||||||
|
python dota_factory/tools/verify_recipes.py --config-dir path/to/config
|
||||||
|
|
||||||
|
By default the config directory is resolved relative to this script
|
||||||
|
(../bin/app/data/config). Requires the 'toml' package on Python < 3.11
|
||||||
|
(pip install --user toml); on 3.11+ the standard tomllib is used.
|
||||||
|
|
||||||
|
Exits 1 if a producer or visuals check fails, 0 otherwise (warnings do
|
||||||
|
not affect the exit code).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
WORLD_SOURCED_ITEMS = {"scrap"} # dropped by destroyed ships
|
||||||
|
IMPLICITLY_CONSUMED_ITEMS = {"building_block"} # consumed by the HQ
|
||||||
|
|
||||||
|
|
||||||
|
def load_toml(path):
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
with open(path, "rb") as fh:
|
||||||
|
return tomllib.load(fh)
|
||||||
|
except ImportError:
|
||||||
|
import toml
|
||||||
|
return toml.load(path)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
default_dir = os.path.normpath(os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
"..", "bin", "app", "data", "config"))
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Check recipe tree consistency across the config files.")
|
||||||
|
parser.add_argument("--config-dir", default=default_dir,
|
||||||
|
help="directory containing the config toml files"
|
||||||
|
" (default: %(default)s)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
recipes = load_toml(os.path.join(args.config_dir, "recipes.toml"))["recipe"]
|
||||||
|
ships = load_toml(os.path.join(args.config_dir, "ships.toml"))["ship"]
|
||||||
|
modules = load_toml(os.path.join(args.config_dir, "modules.toml"))["module"]
|
||||||
|
visuals = load_toml(os.path.join(args.config_dir, "visuals.toml"))
|
||||||
|
|
||||||
|
produced = {} # item id -> [producer descriptions]
|
||||||
|
consumed = {} # item id -> [consumer descriptions]
|
||||||
|
|
||||||
|
for recipe in recipes:
|
||||||
|
for output in recipe.get("outputs", []):
|
||||||
|
produced.setdefault(output["item"], []).append(
|
||||||
|
"recipe '{}'".format(recipe["id"]))
|
||||||
|
for inp in recipe.get("inputs", []):
|
||||||
|
consumed.setdefault(inp["item"], []).append(
|
||||||
|
"recipe '{}'".format(recipe["id"]))
|
||||||
|
|
||||||
|
for ship in ships:
|
||||||
|
for material in ship["schematic"]["materials"]:
|
||||||
|
consumed.setdefault(material["item"], []).append(
|
||||||
|
"ship '{}'".format(ship["id"]))
|
||||||
|
|
||||||
|
for module in modules:
|
||||||
|
for material in module["materials"]:
|
||||||
|
consumed.setdefault(material["item"], []).append(
|
||||||
|
"module '{}'".format(module["id"]))
|
||||||
|
|
||||||
|
all_items = set(produced) | set(consumed) | WORLD_SOURCED_ITEMS
|
||||||
|
visual_items = set(visuals.get("items", {}))
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
for item in sorted(consumed):
|
||||||
|
if item not in produced and item not in WORLD_SOURCED_ITEMS:
|
||||||
|
errors.append("no producer for '{}' (consumed by {})".format(
|
||||||
|
item, ", ".join(sorted(set(consumed[item])))))
|
||||||
|
|
||||||
|
for item in sorted(all_items - visual_items):
|
||||||
|
errors.append("no [items.{}] entry in visuals.toml".format(item))
|
||||||
|
for item in sorted(visual_items - all_items):
|
||||||
|
warnings.append("visuals.toml entry [items.{}] matches no known item"
|
||||||
|
.format(item))
|
||||||
|
|
||||||
|
for item in sorted(produced):
|
||||||
|
if item not in consumed and item not in IMPLICITLY_CONSUMED_ITEMS:
|
||||||
|
warnings.append("'{}' is produced but never consumed (by {})"
|
||||||
|
.format(item, ", ".join(sorted(set(produced[item])))))
|
||||||
|
|
||||||
|
reprocessing_only = sorted(
|
||||||
|
item for item, producers in produced.items()
|
||||||
|
if all("reprocessing" in p for p in producers))
|
||||||
|
|
||||||
|
print("{} items, {} recipes, {} ships, {} modules".format(
|
||||||
|
len(all_items), len(recipes), len(ships), len(modules)))
|
||||||
|
print("obtainable only via reprocessing: {}".format(
|
||||||
|
", ".join(reprocessing_only) if reprocessing_only else "(none)"))
|
||||||
|
print()
|
||||||
|
|
||||||
|
for warning in warnings:
|
||||||
|
print("WARNING: {}".format(warning))
|
||||||
|
for error in errors:
|
||||||
|
print("ERROR: {}".format(error))
|
||||||
|
if not errors and not warnings:
|
||||||
|
print("all checks passed")
|
||||||
|
elif not errors:
|
||||||
|
print("no errors")
|
||||||
|
|
||||||
|
return 1 if errors else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user