Remove schematic upgrades and module and ship levels
This commit is contained in:
@@ -27,15 +27,14 @@ id = "laser_cannon_s"
|
|||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "laser_cannon_s_module", amount = 1}]
|
materials = [{item = "laser_cannon_s_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 0.5
|
production_time_seconds = 0.5
|
||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "Ls"
|
glyph = "Ls"
|
||||||
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
damage_formula = "2"
|
damage = 2
|
||||||
attack_range_m_formula = "50"
|
attack_range_m = 50
|
||||||
attack_rate_hz_formula = "2.0"
|
attack_rate_hz = 2.0
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
@@ -45,15 +44,14 @@ surface_mask = [
|
|||||||
"OO",
|
"OO",
|
||||||
"OO"]
|
"OO"]
|
||||||
materials = [{item = "laser_cannon_m_module", amount = 1}]
|
materials = [{item = "laser_cannon_m_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "Lm"
|
glyph = "Lm"
|
||||||
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
damage_formula = "10"
|
damage = 10
|
||||||
attack_range_m_formula = "70"
|
attack_range_m = 70
|
||||||
attack_rate_hz_formula = "1.5"
|
attack_rate_hz = 1.5
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
@@ -64,15 +62,14 @@ surface_mask = [
|
|||||||
"OOO",
|
"OOO",
|
||||||
"OOO"]
|
"OOO"]
|
||||||
materials = [{item = "laser_cannon_l_module", amount = 1}]
|
materials = [{item = "laser_cannon_l_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 8
|
production_time_seconds = 8
|
||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "Ll"
|
glyph = "Ll"
|
||||||
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
damage_formula = "40"
|
damage = 40
|
||||||
attack_range_m_formula = "100"
|
attack_range_m = 100
|
||||||
attack_rate_hz_formula = "0.8"
|
attack_rate_hz = 0.8
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Utility tools
|
# Utility tools
|
||||||
@@ -83,15 +80,14 @@ id = "salvager"
|
|||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "salvager_module", amount = 1}]
|
materials = [{item = "salvager_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
fill_color = "#AACC44"
|
fill_color = "#AACC44"
|
||||||
glyph = "Sv"
|
glyph = "Sv"
|
||||||
|
|
||||||
[module.salvage]
|
[module.salvage]
|
||||||
collection_range_m_formula = "500"
|
collection_range_m = 500
|
||||||
cargo_capacity_formula = "10"
|
cargo_capacity = 10
|
||||||
collection_rate_hz_formula = "0.5"
|
collection_rate_hz = 0.5
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
@@ -99,15 +95,14 @@ id = "repair_tool"
|
|||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "repair_tool_module", amount = 1}]
|
materials = [{item = "repair_tool_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
fill_color = "#66CCFF"
|
fill_color = "#66CCFF"
|
||||||
glyph = "Rp"
|
glyph = "Rp"
|
||||||
|
|
||||||
[module.repair]
|
[module.repair]
|
||||||
repair_rate_hz_formula = "1"
|
repair_rate_hz = 1
|
||||||
repair_amount_hp_formula = "5 + x"
|
repair_amount_hp = 6
|
||||||
repair_range_m_formula = "80"
|
repair_range_m = 80
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Propulsion
|
# Propulsion
|
||||||
@@ -118,14 +113,13 @@ id = "afterburner"
|
|||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["OOO"]
|
surface_mask = ["OOO"]
|
||||||
materials = [{item = "afterburner_module", amount = 1}]
|
materials = [{item = "afterburner_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "Ab"
|
glyph = "Ab"
|
||||||
|
|
||||||
[module.movement]
|
[module.movement]
|
||||||
multiplied_speed_mps_formula = "1.6"
|
multiplied_speed_mps = 1.6
|
||||||
added_main_acceleration_mpss_formula = "60"
|
added_main_acceleration_mpss = 60
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
@@ -133,14 +127,13 @@ id = "maneuvering_thrusters"
|
|||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
|
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "Mt"
|
glyph = "Mt"
|
||||||
|
|
||||||
[module.movement]
|
[module.movement]
|
||||||
multiplied_speed_mps_formula = "1.2"
|
multiplied_speed_mps = 1.2
|
||||||
added_maneuvering_acceleration_mpss_formula = "10"
|
added_maneuvering_acceleration_mpss = 10
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Defense & sensors
|
# Defense & sensors
|
||||||
@@ -151,13 +144,12 @@ id = "armor_plates"
|
|||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "armor_plates_module", amount = 1}]
|
materials = [{item = "armor_plates_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 3
|
production_time_seconds = 3
|
||||||
fill_color = "#808080"
|
fill_color = "#808080"
|
||||||
glyph = "A"
|
glyph = "A"
|
||||||
|
|
||||||
[module.health]
|
[module.health]
|
||||||
added_hp_formula = "40"
|
added_hp = 40
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
@@ -165,13 +157,12 @@ id = "sensor_booster"
|
|||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "sensor_booster_module", amount = 1}]
|
materials = [{item = "sensor_booster_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "S"
|
glyph = "S"
|
||||||
|
|
||||||
[module.sensor]
|
[module.sensor]
|
||||||
added_sensor_range_m_formula = "50"
|
added_sensor_range_m = 50
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Weapon modifiers
|
# Weapon modifiers
|
||||||
@@ -185,13 +176,12 @@ surface_mask = [
|
|||||||
"OX",
|
"OX",
|
||||||
]
|
]
|
||||||
materials = [{item = "weapon_upgrade_module", amount = 1}]
|
materials = [{item = "weapon_upgrade_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "Wu"
|
glyph = "Wu"
|
||||||
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
multiplied_damage_formula = "1.2"
|
multiplied_damage = 1.2
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
@@ -202,13 +192,12 @@ surface_mask = [
|
|||||||
"OX",
|
"OX",
|
||||||
]
|
]
|
||||||
materials = [{item = "weapon_primer_module", amount = 1}]
|
materials = [{item = "weapon_primer_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "Wp"
|
glyph = "Wp"
|
||||||
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
multiplied_attack_rate_hz_formula = "1.2"
|
multiplied_attack_rate_hz = 1.2
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
@@ -219,14 +208,13 @@ surface_mask = [
|
|||||||
"OX",
|
"OX",
|
||||||
]
|
]
|
||||||
materials = [{item = "weapon_stabilizer_module", amount = 1}]
|
materials = [{item = "weapon_stabilizer_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "Ws"
|
glyph = "Ws"
|
||||||
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
multiplied_attack_range_m_formula = "1.5"
|
multiplied_attack_range_m = 1.5
|
||||||
multiplied_attack_rate_hz_formula = "0.8"
|
multiplied_attack_rate_hz = 0.8
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Drone modules
|
# Drone modules
|
||||||
@@ -242,7 +230,6 @@ surface_mask = [
|
|||||||
"OO",
|
"OO",
|
||||||
"OO"]
|
"OO"]
|
||||||
materials = [{item = "drone_bay_module", amount = 1}]
|
materials = [{item = "drone_bay_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
fill_color = "#CC66FF"
|
fill_color = "#CC66FF"
|
||||||
glyph = "Db"
|
glyph = "Db"
|
||||||
@@ -255,7 +242,6 @@ surface_mask = [
|
|||||||
"OOOOOO",
|
"OOOOOO",
|
||||||
"OOOOOO"]
|
"OOOOOO"]
|
||||||
materials = [{item = "drone_hangar_module", amount = 1}]
|
materials = [{item = "drone_hangar_module", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 20
|
production_time_seconds = 20
|
||||||
fill_color = "#9933CC"
|
fill_color = "#9933CC"
|
||||||
glyph = "Dh"
|
glyph = "Dh"
|
||||||
|
|||||||
@@ -23,21 +23,20 @@ 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
|
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "3"
|
hp = 3
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps_formula = "40"
|
speed_mps = 40
|
||||||
main_acceleration_mpss_formula = "2"
|
main_acceleration_mpss = 2
|
||||||
maneuvering_acceleration_mpss_formula = "1"
|
maneuvering_acceleration_mpss = 1
|
||||||
angular_acceleration_radpss_formula = "12.56"
|
angular_acceleration_radpss = 12.56
|
||||||
max_rotation_speed_radps_formula = "6.28"
|
max_rotation_speed_radps = 6.28
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_m_formula = "150"
|
sensor_range_m = 150
|
||||||
|
|
||||||
|
|
||||||
# Frigate — 5 cells in a plus shape. Holds a couple of small guns plus at
|
# Frigate — 5 cells in a plus shape. Holds a couple of small guns plus at
|
||||||
@@ -54,21 +53,20 @@ layout = [
|
|||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "frigate_hull", amount = 1}]
|
materials = [{item = "frigate_hull", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 10
|
production_time_seconds = 10
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "30"
|
hp = 30
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps_formula = "30"
|
speed_mps = 30
|
||||||
main_acceleration_mpss_formula = "50"
|
main_acceleration_mpss = 50
|
||||||
maneuvering_acceleration_mpss_formula = "25"
|
maneuvering_acceleration_mpss = 25
|
||||||
angular_acceleration_radpss_formula = "8"
|
angular_acceleration_radpss = 8
|
||||||
max_rotation_speed_radps_formula = "4"
|
max_rotation_speed_radps = 4
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_m_formula = "200"
|
sensor_range_m = 200
|
||||||
|
|
||||||
|
|
||||||
# Destroyer — 8 cells: a long gun deck with three turret bumps on top.
|
# Destroyer — 8 cells: a long gun deck with three turret bumps on top.
|
||||||
@@ -84,21 +82,20 @@ layout = [
|
|||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "destroyer_hull", amount = 1}]
|
materials = [{item = "destroyer_hull", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 15
|
production_time_seconds = 15
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "50"
|
hp = 50
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps_formula = "25"
|
speed_mps = 25
|
||||||
main_acceleration_mpss_formula = "40"
|
main_acceleration_mpss = 40
|
||||||
maneuvering_acceleration_mpss_formula = "20"
|
maneuvering_acceleration_mpss = 20
|
||||||
angular_acceleration_radpss_formula = "6"
|
angular_acceleration_radpss = 6
|
||||||
max_rotation_speed_radps_formula = "3"
|
max_rotation_speed_radps = 3
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_m_formula = "220"
|
sensor_range_m = 220
|
||||||
|
|
||||||
|
|
||||||
# Cruiser — 12 cells with notched corners. Fits at most two 2x2 m guns
|
# Cruiser — 12 cells with notched corners. Fits at most two 2x2 m guns
|
||||||
@@ -116,21 +113,20 @@ layout = [
|
|||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "cruiser_hull", amount = 1}]
|
materials = [{item = "cruiser_hull", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 25
|
production_time_seconds = 25
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "120"
|
hp = 120
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps_formula = "20"
|
speed_mps = 20
|
||||||
main_acceleration_mpss_formula = "30"
|
main_acceleration_mpss = 30
|
||||||
maneuvering_acceleration_mpss_formula = "15"
|
maneuvering_acceleration_mpss = 15
|
||||||
angular_acceleration_radpss_formula = "4"
|
angular_acceleration_radpss = 4
|
||||||
max_rotation_speed_radps_formula = "2"
|
max_rotation_speed_radps = 2
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_m_formula = "250"
|
sensor_range_m = 250
|
||||||
|
|
||||||
|
|
||||||
# Battlecruiser — 16 cells: a wide bow split into two gun cheeks, tapering
|
# Battlecruiser — 16 cells: a wide bow split into two gun cheeks, tapering
|
||||||
@@ -149,21 +145,20 @@ layout = [
|
|||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "battlecruiser_hull", amount = 1}]
|
materials = [{item = "battlecruiser_hull", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 35
|
production_time_seconds = 35
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "180"
|
hp = 180
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps_formula = "18"
|
speed_mps = 18
|
||||||
main_acceleration_mpss_formula = "25"
|
main_acceleration_mpss = 25
|
||||||
maneuvering_acceleration_mpss_formula = "12"
|
maneuvering_acceleration_mpss = 12
|
||||||
angular_acceleration_radpss_formula = "3"
|
angular_acceleration_radpss = 3
|
||||||
max_rotation_speed_radps_formula = "1.5"
|
max_rotation_speed_radps = 1.5
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_m_formula = "260"
|
sensor_range_m = 260
|
||||||
|
|
||||||
|
|
||||||
# Battleship — 24 cells: a broadside hull with notched flanks on every other
|
# Battleship — 24 cells: a broadside hull with notched flanks on every other
|
||||||
@@ -185,21 +180,20 @@ layout = [
|
|||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "battleship_hull", amount = 1}]
|
materials = [{item = "battleship_hull", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 60
|
production_time_seconds = 60
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "350"
|
hp = 350
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps_formula = "14"
|
speed_mps = 14
|
||||||
main_acceleration_mpss_formula = "18"
|
main_acceleration_mpss = 18
|
||||||
maneuvering_acceleration_mpss_formula = "8"
|
maneuvering_acceleration_mpss = 8
|
||||||
angular_acceleration_radpss_formula = "2"
|
angular_acceleration_radpss = 2
|
||||||
max_rotation_speed_radps_formula = "1"
|
max_rotation_speed_radps = 1
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_m_formula = "280"
|
sensor_range_m = 280
|
||||||
|
|
||||||
|
|
||||||
# Dreadnought — 36 cells: the main battery deck is split into three 3x3 gun
|
# Dreadnought — 36 cells: the main battery deck is split into three 3x3 gun
|
||||||
@@ -220,21 +214,20 @@ layout = [
|
|||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "dreadnought_hull", amount = 1}]
|
materials = [{item = "dreadnought_hull", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 120
|
production_time_seconds = 120
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "800"
|
hp = 800
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps_formula = "8"
|
speed_mps = 8
|
||||||
main_acceleration_mpss_formula = "10"
|
main_acceleration_mpss = 10
|
||||||
maneuvering_acceleration_mpss_formula = "5"
|
maneuvering_acceleration_mpss = 5
|
||||||
angular_acceleration_radpss_formula = "1"
|
angular_acceleration_radpss = 1
|
||||||
max_rotation_speed_radps_formula = "0.5"
|
max_rotation_speed_radps = 0.5
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_m_formula = "300"
|
sensor_range_m = 300
|
||||||
|
|
||||||
|
|
||||||
# Carrier — 37 cells: the top flight deck (rows 0-1) is the only place wide
|
# Carrier — 37 cells: the top flight deck (rows 0-1) is the only place wide
|
||||||
@@ -254,18 +247,17 @@ layout = [
|
|||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "carrier_hull", amount = 1}]
|
materials = [{item = "carrier_hull", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 120
|
production_time_seconds = 120
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "700"
|
hp = 700
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps_formula = "9"
|
speed_mps = 9
|
||||||
main_acceleration_mpss_formula = "10"
|
main_acceleration_mpss = 10
|
||||||
maneuvering_acceleration_mpss_formula = "5"
|
maneuvering_acceleration_mpss = 5
|
||||||
angular_acceleration_radpss_formula = "1"
|
angular_acceleration_radpss = 1
|
||||||
max_rotation_speed_radps_formula = "0.5"
|
max_rotation_speed_radps = 0.5
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_m_formula = "350"
|
sensor_range_m = 350
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ artifact_win_count = 3
|
|||||||
|
|
||||||
[waves]
|
[waves]
|
||||||
threat_rate_formula = "x"
|
threat_rate_formula = "x"
|
||||||
ship_level_formula = "1"
|
|
||||||
gap_min_seconds = 15
|
gap_min_seconds = 15
|
||||||
gap_max_seconds = 45
|
gap_max_seconds = 45
|
||||||
spawn_duration_seconds = 10
|
spawn_duration_seconds = 10
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ enemy_buffer_width_tiles = 10
|
|||||||
name = "Alpha"
|
name = "Alpha"
|
||||||
[[arena.team.ship]]
|
[[arena.team.ship]]
|
||||||
schematic = "drone"
|
schematic = "drone"
|
||||||
level = 1
|
|
||||||
count = 5
|
count = 5
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||||
@@ -19,7 +18,6 @@ enemy_buffer_width_tiles = 10
|
|||||||
name = "Beta"
|
name = "Beta"
|
||||||
[[arena.team.ship]]
|
[[arena.team.ship]]
|
||||||
schematic = "drone"
|
schematic = "drone"
|
||||||
level = 1
|
|
||||||
count = 2
|
count = 2
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||||
@@ -41,7 +39,6 @@ enemy_buffer_width_tiles = 10
|
|||||||
name = "Fighters"
|
name = "Fighters"
|
||||||
[[arena.team.ship]]
|
[[arena.team.ship]]
|
||||||
schematic = "drone"
|
schematic = "drone"
|
||||||
level = 1
|
|
||||||
count = 5
|
count = 5
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||||
@@ -51,7 +48,6 @@ enemy_buffer_width_tiles = 10
|
|||||||
name = "Supported"
|
name = "Supported"
|
||||||
[[arena.team.ship]]
|
[[arena.team.ship]]
|
||||||
schematic = "drone"
|
schematic = "drone"
|
||||||
level = 1
|
|
||||||
count = 3
|
count = 3
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||||
@@ -59,7 +55,6 @@ enemy_buffer_width_tiles = 10
|
|||||||
|
|
||||||
[[arena.team.ship]]
|
[[arena.team.ship]]
|
||||||
schematic = "drone"
|
schematic = "drone"
|
||||||
level = 1
|
|
||||||
count = 2
|
count = 2
|
||||||
modules = [
|
modules = [
|
||||||
{type = "repair_tool", x = 1, y = 1, rotation = "east"},
|
{type = "repair_tool", x = 1, y = 1, rotation = "east"},
|
||||||
@@ -76,7 +71,6 @@ enemy_buffer_width_tiles = 15
|
|||||||
name = "Fortified"
|
name = "Fortified"
|
||||||
[[arena.team.ship]]
|
[[arena.team.ship]]
|
||||||
schematic = "drone"
|
schematic = "drone"
|
||||||
level = 1
|
|
||||||
count = 3
|
count = 3
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||||
@@ -96,7 +90,6 @@ enemy_buffer_width_tiles = 15
|
|||||||
name = "Swarm"
|
name = "Swarm"
|
||||||
[[arena.team.ship]]
|
[[arena.team.ship]]
|
||||||
schematic = "drone"
|
schematic = "drone"
|
||||||
level = 1
|
|
||||||
count = 8
|
count = 8
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
|
||||||
|
|||||||
@@ -3,136 +3,126 @@ id = "armor_plate"
|
|||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "iron_ingot", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 3
|
production_time_seconds = 3
|
||||||
fill_color = "#808080"
|
fill_color = "#808080"
|
||||||
glyph = "A"
|
glyph = "A"
|
||||||
|
|
||||||
[module.health]
|
[module.health]
|
||||||
multiplied_hp_formula = "1.5"
|
multiplied_hp = 1.5
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "sensor_booster"
|
id = "sensor_booster"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "circuit_board", amount = 1}]
|
materials = [{item = "circuit_board", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "S"
|
glyph = "S"
|
||||||
|
|
||||||
[module.sensor]
|
[module.sensor]
|
||||||
added_sensor_range_m_formula = "100"
|
added_sensor_range_m = 100
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_upgrade"
|
id = "weapon_upgrade"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "W"
|
glyph = "W"
|
||||||
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
multiplied_damage_formula = "1.2"
|
multiplied_damage = 1.2
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "laser_cannon"
|
id = "laser_cannon"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "L"
|
glyph = "L"
|
||||||
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
damage_formula = "2"
|
damage = 2
|
||||||
attack_range_m_formula = "50"
|
attack_range_m = 50
|
||||||
attack_rate_hz_formula = "2.0"
|
attack_rate_hz = 2.0
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "salvager"
|
id = "salvager"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "iron_ingot", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
fill_color = "#AACC44"
|
fill_color = "#AACC44"
|
||||||
glyph = "Sv"
|
glyph = "Sv"
|
||||||
|
|
||||||
[module.salvage]
|
[module.salvage]
|
||||||
collection_range_m_formula = "500"
|
collection_range_m = 500
|
||||||
cargo_capacity_formula = "10"
|
cargo_capacity = 10
|
||||||
collection_rate_hz_formula = "0.5"
|
collection_rate_hz = 0.5
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "repair_tool"
|
id = "repair_tool"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "circuit_board", amount = 2}]
|
materials = [{item = "circuit_board", amount = 2}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
fill_color = "#66CCFF"
|
fill_color = "#66CCFF"
|
||||||
glyph = "Rp"
|
glyph = "Rp"
|
||||||
|
|
||||||
[module.repair]
|
[module.repair]
|
||||||
repair_rate_hz_formula = "1"
|
repair_rate_hz = 1
|
||||||
repair_amount_hp_formula = "5 + x"
|
repair_amount_hp = 6
|
||||||
repair_range_m_formula = "800"
|
repair_range_m = 800
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_primer"
|
id = "weapon_primer"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "Wp"
|
glyph = "Wp"
|
||||||
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
multiplied_attack_rate_hz_formula = "1.2"
|
multiplied_attack_rate_hz = 1.2
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_stabilizer"
|
id = "weapon_stabilizer"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "Ws"
|
glyph = "Ws"
|
||||||
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
multiplied_attack_range_m_formula = "1.5"
|
multiplied_attack_range_m = 1.5
|
||||||
multiplied_attack_rate_hz_formula = "0.8"
|
multiplied_attack_rate_hz = 0.8
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "afterburner"
|
id = "afterburner"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "Ab"
|
glyph = "Ab"
|
||||||
|
|
||||||
[module.movement]
|
[module.movement]
|
||||||
multiplied_speed_mps_formula = "1.6"
|
multiplied_speed_mps = 1.6
|
||||||
added_main_acceleration_mpss_formula = "60"
|
added_main_acceleration_mpss = 60
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "maneuvering_thrusters"
|
id = "maneuvering_thrusters"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "Mt"
|
glyph = "Mt"
|
||||||
|
|
||||||
[module.movement]
|
[module.movement]
|
||||||
multiplied_speed_mps_formula = "1.2"
|
multiplied_speed_mps = 1.2
|
||||||
added_maneuvering_acceleration_mpss_formula = "10"
|
added_maneuvering_acceleration_mpss = 10
|
||||||
|
|||||||
@@ -6,21 +6,20 @@ default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
|||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||||
player_production_level = 3
|
|
||||||
production_time_seconds = 10
|
production_time_seconds = 10
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "40 + 5*x"
|
hp = 45
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps_formula = "2000 + 50*x"
|
speed_mps = 2050
|
||||||
main_acceleration_mpss_formula = "1000000"
|
main_acceleration_mpss = 1000000
|
||||||
maneuvering_acceleration_mpss_formula = "1000000"
|
maneuvering_acceleration_mpss = 1000000
|
||||||
angular_acceleration_radpss_formula = "100000"
|
angular_acceleration_radpss = 100000
|
||||||
max_rotation_speed_radps_formula = "100000"
|
max_rotation_speed_radps = 100000
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_m_formula = "2000"
|
sensor_range_m = 2000
|
||||||
|
|
||||||
|
|
||||||
[[ship]]
|
[[ship]]
|
||||||
@@ -31,21 +30,20 @@ default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
|||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 2}]
|
||||||
player_production_level = 5
|
|
||||||
production_time_seconds = 20
|
production_time_seconds = 20
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "120 + 15*x"
|
hp = 135
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps_formula = "1200"
|
speed_mps = 1200
|
||||||
main_acceleration_mpss_formula = "1000000"
|
main_acceleration_mpss = 1000000
|
||||||
maneuvering_acceleration_mpss_formula = "1000000"
|
maneuvering_acceleration_mpss = 1000000
|
||||||
angular_acceleration_radpss_formula = "100000"
|
angular_acceleration_radpss = 100000
|
||||||
max_rotation_speed_radps_formula = "100000"
|
max_rotation_speed_radps = 100000
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_m_formula = "3000"
|
sensor_range_m = 3000
|
||||||
|
|
||||||
|
|
||||||
[[ship]]
|
[[ship]]
|
||||||
@@ -55,21 +53,20 @@ layout = ["OOO", "OOO"]
|
|||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 4}]
|
materials = [{item = "iron_ingot", amount = 4}]
|
||||||
player_production_level = 3
|
|
||||||
production_time_seconds = 10
|
production_time_seconds = 10
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "40 + 4*x"
|
hp = 44
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps_formula = "1100"
|
speed_mps = 1100
|
||||||
main_acceleration_mpss_formula = "1000000"
|
main_acceleration_mpss = 1000000
|
||||||
maneuvering_acceleration_mpss_formula = "1000000"
|
maneuvering_acceleration_mpss = 1000000
|
||||||
angular_acceleration_radpss_formula = "100000"
|
angular_acceleration_radpss = 100000
|
||||||
max_rotation_speed_radps_formula = "100000"
|
max_rotation_speed_radps = 100000
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_m_formula = "2500"
|
sensor_range_m = 2500
|
||||||
|
|
||||||
|
|
||||||
[[ship]]
|
[[ship]]
|
||||||
@@ -79,18 +76,17 @@ layout = ["XOX", "OOO", "XOX"]
|
|||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount = 2}]
|
||||||
player_production_level = 3
|
|
||||||
production_time_seconds = 15
|
production_time_seconds = 15
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "60 + 5*x"
|
hp = 65
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps_formula = "1300"
|
speed_mps = 1300
|
||||||
main_acceleration_mpss_formula = "1000000"
|
main_acceleration_mpss = 1000000
|
||||||
maneuvering_acceleration_mpss_formula = "1000000"
|
maneuvering_acceleration_mpss = 1000000
|
||||||
angular_acceleration_radpss_formula = "100000"
|
angular_acceleration_radpss = 100000
|
||||||
max_rotation_speed_radps_formula = "100000"
|
max_rotation_speed_radps = 100000
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_m_formula = "2500"
|
sensor_range_m = 2500
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ artifact_win_count = 3
|
|||||||
|
|
||||||
[waves]
|
[waves]
|
||||||
threat_rate_formula = "x"
|
threat_rate_formula = "x"
|
||||||
ship_level_formula = "1 + x / 10"
|
|
||||||
gap_min_seconds = 15
|
gap_min_seconds = 15
|
||||||
gap_max_seconds = 45
|
gap_max_seconds = 45
|
||||||
spawn_duration_seconds = 10
|
spawn_duration_seconds = 10
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
Config files use the TOML format. The following config files drive game parameters:
|
Config files use the TOML format. The following config files drive game parameters:
|
||||||
|
|
||||||
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, boss wave timing, enemy ship level formula, belt speed, starting building blocks, departure interval, ship orbit factor, rally orbit radius, scrap-per-threat conversion, combat target-selection parameters (target score formula, overclaim penalty formula, target hysteresis), artifact chance formula, and artifact win count.
|
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, boss wave timing, belt speed, starting building blocks, departure interval, ship orbit factor, rally orbit radius, scrap-per-threat conversion, combat target-selection parameters (target score formula, overclaim penalty formula, target hysteresis), artifact chance formula, and artifact win count.
|
||||||
- **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, 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 plain values, required build materials, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the schematic already unlocked), a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
|
||||||
- **modules.toml** — per module type: id, surface mask, materials list, initial player production level, production time, fill color, glyph, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the module schematic already unlocked), and an optional capability section and/or stat modifier formulas. A module with a capability section (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas is a **capability module** that grants the ship a weapon, salvage bay, or repair tool per instance (see REQ-MOD-CONFIG for the full list of formulas per capability type). A module with only `added_*`/`multiplied_*` formulas is a **passive module** that modifies stats on the ship or on capability module instances (see REQ-MOD-STAT-CALC).
|
- **modules.toml** — per module type: id, surface mask, materials list, production time, fill color, glyph, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the module schematic already unlocked), 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; a distinct beam color per tool type (weapon, repair, salvage) and beam width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
|
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship schematic, and station type; a distinct beam color per tool type (weapon, repair, salvage) and beam width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
|
||||||
- **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.
|
||||||
@@ -117,7 +117,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- REQ-BLD-SMELTER: **Smelter** (2×2): Converts ore or scrap into basic materials. No recipe selection required. Inputs, outputs, and rates are defined in `recipes.toml [[recipe]]` entries with `building = "smelter"`.
|
- REQ-BLD-SMELTER: **Smelter** (2×2): Converts ore or scrap into basic materials. No recipe selection required. Inputs, outputs, and rates are defined in `recipes.toml [[recipe]]` entries with `building = "smelter"`.
|
||||||
- REQ-BLD-ASSEMBLER: **Assembler** (3×3): The player selects a recipe from the config-defined crafting tree. Produces the selected output item at the rate defined in the corresponding `recipes.toml [[recipe]]` entry with `building = "assembler"`. Only implicitly unlocked recipes are available for selection (REQ-LOCK-UI-RECIPE).
|
- REQ-BLD-ASSEMBLER: **Assembler** (3×3): The player selects a recipe from the config-defined crafting tree. Produces the selected output item at the rate defined in the corresponding `recipes.toml [[recipe]]` entry with `building = "assembler"`. Only implicitly unlocked recipes are available for selection (REQ-LOCK-UI-RECIPE).
|
||||||
- REQ-BLD-REPROCESSING: **Reprocessing Plant** (3×3): Consumes scrap per cycle and produces exactly one higher-level intermediate product per cycle via weighted random pick. The input quantity, possible output items, per-output weights, and amounts are defined in `recipes.toml [[recipe]]` entries with `building = "reprocessing_plant"` (`inputs`, `outputs[].item`, `outputs[].amount`, `outputs[].weight`). Weights are normalized at load time; their sum does not need to equal 1. The output is rolled at cycle start (see REQ-MAT-CYCLE); the pool of eligible outputs is restricted to implicitly unlocked item types (REQ-LOCK-REPROCESSING-POOL). The output buffer holds at most one cycle's output — see REQ-MAT-OUTPUT-BUFFER-REPROCESSING.
|
- REQ-BLD-REPROCESSING: **Reprocessing Plant** (3×3): Consumes scrap per cycle and produces exactly one higher-level intermediate product per cycle via weighted random pick. The input quantity, possible output items, per-output weights, and amounts are defined in `recipes.toml [[recipe]]` entries with `building = "reprocessing_plant"` (`inputs`, `outputs[].item`, `outputs[].amount`, `outputs[].weight`). Weights are normalized at load time; their sum does not need to equal 1. The output is rolled at cycle start (see REQ-MAT-CYCLE); the pool of eligible outputs is restricted to implicitly unlocked item types (REQ-LOCK-REPROCESSING-POOL). The output buffer holds at most one cycle's output — see REQ-MAT-OUTPUT-BUFFER-REPROCESSING.
|
||||||
- REQ-BLD-SHIPYARD: **Shipyard** (4×2): The player selects a schematic. When all required materials — the ship's base materials (`[ship.schematic].materials`) plus the materials of all modules in the configured layout (REQ-MOD-MATERIALS) — are present in its input buffer, the shipyard consumes them and begins a production cycle lasting the ship's base `[ship.schematic].production_time_seconds` plus the sum of production times contributed by all module instances in the configured layout (REQ-MOD-PRODUCTION-TIME). One ship of that type is spawned at `ships.toml [ship.schematic].player_production_level` (initial value 5, incremented by duplicate schematic drops per REQ-DEF-SCHEMATIC-DROP) with the configured modules when the cycle completes. The shipyard cannot start a new cycle while one is in progress. If the player confirms a layout change (REQ-MOD-UI-DIALOG) while a production cycle is in progress, the current cycle is cancelled and all consumed materials are discarded; the shipyard returns to idle with the new layout configuration.
|
- REQ-BLD-SHIPYARD: **Shipyard** (4×2): The player selects a schematic. When all required materials — the ship's base materials (`[ship.schematic].materials`) plus the materials of all modules in the configured layout (REQ-MOD-MATERIALS) — are present in its input buffer, the shipyard consumes them and begins a production cycle lasting the ship's base `[ship.schematic].production_time_seconds` plus the sum of production times contributed by all module instances in the configured layout (REQ-MOD-PRODUCTION-TIME). One ship of that type is spawned with the configured modules when the cycle completes. The shipyard cannot start a new cycle while one is in progress. If the player confirms a layout change (REQ-MOD-UI-DIALOG) while a production cycle is in progress, the current cycle is cancelled and all consumed materials are discarded; the shipyard returns to idle with the new layout configuration.
|
||||||
- REQ-BLD-SALVAGE-BAY: **Salvage Bay** (3×2): A dedicated drop-off point for salvage ships. Scrap delivered here is placed onto connected output belts.
|
- REQ-BLD-SALVAGE-BAY: **Salvage Bay** (3×2): A dedicated drop-off point for salvage ships. Scrap delivered here is placed onto connected output belts.
|
||||||
- REQ-BLD-BELT: **Belt** (1×1): Transports items. A belt tile has one direction (N, S, E, W) set at placement (modified by rotation). Curved belts are auto-derived: when a belt tile's outgoing direction leads into another belt whose direction is orthogonal, the downstream belt is rendered and behaves as a curve. Belt speed is defined in `world.toml [world].belt_speed_tiles_per_second` (REQ-GW-BELT-SPEED).
|
- REQ-BLD-BELT: **Belt** (1×1): Transports items. A belt tile has one direction (N, S, E, W) set at placement (modified by rotation). Curved belts are auto-derived: when a belt tile's outgoing direction leads into another belt whose direction is orthogonal, the downstream belt is rendered and behaves as a curve. Belt speed is defined in `world.toml [world].belt_speed_tiles_per_second` (REQ-GW-BELT-SPEED).
|
||||||
- REQ-BLD-SPLITTER: **Splitter** (1×1): Distributes incoming items between two output directions. Each output can optionally have a filter (a list of item types), configurable via the selected building panel; only implicitly unlocked item types are available as filter options (REQ-LOCK-UI-SPLITTER). Routing rules:
|
- REQ-BLD-SPLITTER: **Splitter** (1×1): Distributes incoming items between two output directions. Each output can optionally have a filter (a list of item types), configurable via the selected building panel; only implicitly unlocked item types are available as filter options (REQ-LOCK-UI-SPLITTER). Routing rules:
|
||||||
@@ -155,10 +155,10 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
## Ships
|
## Ships
|
||||||
|
|
||||||
- REQ-SHP-AUTONOMOUS: Ships are produced by shipyards and are fully autonomous once produced.
|
- REQ-SHP-AUTONOMOUS: Ships are produced by shipyards and are fully autonomous once produced.
|
||||||
- REQ-SHP-STATS: Base hull stats are defined as formulas of ship level in `ships.toml`: HP (`[ship.health].hp_formula`), max linear speed (`[ship.movement].speed_formula`), sensor range (`[ship.sensors].range_formula`), main acceleration (`[ship.movement].main_acceleration_formula`, tiles/s²), maneuvering acceleration (`[ship.movement].maneuvering_acceleration_formula`, tiles/s²), angular acceleration (`[ship.movement].angular_acceleration_formula`, rad/s²), max rotation speed (`[ship.movement].max_rotation_speed_formula`, rad/s). Required build materials (`[ship.schematic].materials`) and the station level at which the schematic becomes available for unlock (`[[ship]].unlock_at_station_level`; -1 = player starts with the schematic already unlocked) are also defined there. Combat, salvage, and repair capabilities are provided by modules (see REQ-MOD-CONFIG). Final hull stats incorporate passive module modifiers per REQ-MOD-STAT-CALC.
|
- REQ-SHP-STATS: Base hull stats are defined as plain values in `ships.toml`: HP (`[ship.health].hp`), max linear speed (`[ship.movement].speed`), sensor range (`[ship.sensors].range`), main acceleration (`[ship.movement].main_acceleration`, tiles/s²), maneuvering acceleration (`[ship.movement].maneuvering_acceleration`, tiles/s²), angular acceleration (`[ship.movement].angular_acceleration`, rad/s²), max rotation speed (`[ship.movement].max_rotation_speed`, rad/s). Required build materials (`[ship.schematic].materials`) and the station level at which the schematic becomes available for unlock (`[[ship]].unlock_at_station_level`; -1 = player starts with the schematic already unlocked) are also defined there. Combat, salvage, and repair capabilities are provided by modules (see REQ-MOD-CONFIG). Final hull stats incorporate passive module modifiers per REQ-MOD-STAT-CALC.
|
||||||
- REQ-SHP-SPAWN-PLAYER: A ship produced by a shipyard spawns centered on the shipyard's output port tile.
|
- REQ-SHP-SPAWN-PLAYER: A ship produced by a shipyard spawns centered on the shipyard's output port tile.
|
||||||
- REQ-SHP-SPAWN-ENEMY: Enemy ships spawn at a uniformly random position within the current enemy buffer zone — random X across the buffer's width and random Y across the world height.
|
- REQ-SHP-SPAWN-ENEMY: Enemy ships spawn at a uniformly random position within the current enemy buffer zone — random X across the buffer's width and random Y across the world height.
|
||||||
- REQ-SHP-MOVEMENT: Ships move using a physics-based model. Each ship has a velocity and a facing direction, both updated each tick. The main acceleration (`main_acceleration_formula`) is applied along the ship's current facing direction only. The maneuvering acceleration (`maneuvering_acceleration_formula`) can be applied in any direction independently of the facing direction, enabling lateral or braking movement without rotating. The angular acceleration (`angular_acceleration_formula`) controls how quickly the ship rotates. Linear speed is capped at the ship's `speed_formula` value; rotation rate is capped at the ship's `max_rotation_speed_formula` value. Ship position refers to the ship's center for all range, sensor, and attack checks.
|
- REQ-SHP-MOVEMENT: Ships move using a physics-based model. Each ship has a velocity and a facing direction, both updated each tick. The main acceleration (`main_acceleration`) is applied along the ship's current facing direction only. The maneuvering acceleration (`maneuvering_acceleration`) can be applied in any direction independently of the facing direction, enabling lateral or braking movement without rotating. The angular acceleration (`angular_acceleration`) controls how quickly the ship rotates. Linear speed is capped at the ship's `speed` value; rotation rate is capped at the ship's `max_rotation_speed` value. Ship position refers to the ship's center for all range, sensor, and attack checks.
|
||||||
- REQ-SHP-ORBIT: Several behaviors keep a ship circling its target at a fixed standoff distance (an **orbit**) rather than approaching a fixed point. The orbit radius depends on the behavior:
|
- REQ-SHP-ORBIT: Several behaviors keep a ship circling its target at a fixed standoff distance (an **orbit**) rather than approaching a fixed point. The orbit radius depends on the behavior:
|
||||||
- **Combat engagement** (REQ-SHP-COMBAT, REQ-SHP-ENEMY-AI): `world.toml [world].orbit_factor` multiplied by the maximum weapon `attack_range` across the ship's weapon module instances.
|
- **Combat engagement** (REQ-SHP-COMBAT, REQ-SHP-ENEMY-AI): `world.toml [world].orbit_factor` multiplied by the maximum weapon `attack_range` across the ship's weapon module instances.
|
||||||
- **Repair** (REQ-SHP-REPAIR): `orbit_factor` multiplied by the maximum `repair_range` across the ship's repair module instances.
|
- **Repair** (REQ-SHP-REPAIR): `orbit_factor` multiplied by the maximum `repair_range` across the ship's repair module instances.
|
||||||
@@ -196,16 +196,15 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- `id` — unique identifier, also used as the display name in the UI.
|
- `id` — unique identifier, also used as the display name in the UI.
|
||||||
- `surface_mask` — footprint within the ship layout grid (see Module Surface Mask Format).
|
- `surface_mask` — footprint within the ship layout grid (see Module Surface Mask Format).
|
||||||
- `materials` — list of materials required per instance (added to the ship's build cost).
|
- `materials` — list of materials required per instance (added to the ship's build cost).
|
||||||
- `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.
|
||||||
- `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). 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 values. A module with base stat values is a capability module — each placed instance grants the ship an independent weapon, salvage bay, or repair tool with its own state (cooldown, target). A ship may have multiple capability module instances of the same or different types. Base stat values per capability type:
|
||||||
- **Weapon** (`[module.weapon]`): `damage_formula`, `attack_range_formula`, `attack_rate_formula`.
|
- **Weapon** (`[module.weapon]`): `damage`, `attack_range`, `attack_rate`.
|
||||||
- **Salvage** (`[module.salvage]`): `collection_range_formula` (tiles), `cargo_capacity_formula` (integer scrap units; contributes to the ship's cargo capacity stat per REQ-MOD-CARGO-CAPACITY), `collection_rate_formula` (collection cycles per second; each cycle collects 1 scrap).
|
- **Salvage** (`[module.salvage]`): `collection_range` (tiles), `cargo_capacity` (integer scrap units; contributes to the ship's cargo capacity stat per REQ-MOD-CARGO-CAPACITY), `collection_rate` (collection cycles per second; each cycle collects 1 scrap).
|
||||||
- **Repair** (`[module.repair]`): `repair_rate_formula` (repair cycles per second), `repair_amount_hp_formula` (HP restored per repair cycle), `repair_range_formula` (tiles).
|
- **Repair** (`[module.repair]`): `repair_rate` (repair cycles per second), `repair_amount_hp` (HP restored per repair cycle), `repair_range` (tiles).
|
||||||
- Zero or more **passive stat modifier formulas** (`added_*`/`multiplied_*`) that boost stats on the ship hull or on capability module instances (see REQ-MOD-STAT-CALC). A single module may be both a capability module and provide passive modifiers.
|
- Zero or more **passive stat modifiers** (`added_*`/`multiplied_*`) that boost stats on the ship hull or on capability module instances (see REQ-MOD-STAT-CALC). A single module may be both a capability module and provide passive modifiers.
|
||||||
|
|
||||||
- REQ-MOD-LAYOUT: Each ship in `ships.toml` defines a `layout` — a list of strings representing the ship's module grid (see Ship Layout Format). All ships define a layout.
|
- REQ-MOD-LAYOUT: Each ship in `ships.toml` defines a `layout` — a list of strings representing the ship's module grid (see Ship Layout Format). All ships define a layout.
|
||||||
|
|
||||||
@@ -233,20 +232,20 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
- REQ-THREAT-SCRAP: The threat value of scrap is the constant `1 / world.toml [world].scrap_per_threat`. This is the exact inverse of the scrap-drop conversion in REQ-RES-SCRAP-DROP, so a destroyed ship drops scrap worth precisely its own threat cost. Because scrap threat is now a fixed constant, it no longer depends on any ship's threat cost, removing the potential circularity with REQ-MOD-THREAT for ships built from reprocessing-only materials.
|
- REQ-THREAT-SCRAP: The threat value of scrap is the constant `1 / world.toml [world].scrap_per_threat`. This is the exact inverse of the scrap-drop conversion in REQ-RES-SCRAP-DROP, so a destroyed ship drops scrap worth precisely its own threat cost. Because scrap threat is now a fixed constant, it no longer depends on any ship's threat cost, removing the potential circularity with REQ-MOD-THREAT for ships built from reprocessing-only materials.
|
||||||
- REQ-MOD-STAT-CALC: For each stat (on the ship hull or on a capability module instance), the final value is computed as: `final = base × total_multiplier + total_additive`, where:
|
- 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 value — the hull stat value (for hull stats) or the capability module's base stat value (for capability module stats).
|
||||||
- `total_multiplier` = 1 + sum of (m_i − 1) for each multiplicative modifier m_i from all passive module instances. Each m_i is 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 the module's multiplicative modifier value.
|
||||||
- `total_additive` = sum of all additive modifier values from all passive module instances. Each additive value is evaluated from the module's additive formula at the module's `player_production_level`.
|
- `total_additive` = sum of all additive modifier values from all passive module instances. Each additive value is the module's additive modifier value.
|
||||||
|
|
||||||
Passive modifier formulas follow the naming convention: a module may define `added_<stat>_formula` (additive) and/or `multiplied_<stat>_formula` (multiplicative) under `[module.<category>]`. The category determines what the modifier targets:
|
Passive modifiers follow the naming convention: a module may define `added_<stat>` (additive) and/or `multiplied_<stat>` (multiplicative) under `[module.<category>]`. The category determines what the modifier targets:
|
||||||
- `[module.health]`, `[module.movement]`, `[module.sensor]` — modifiers apply to the ship hull's stats.
|
- `[module.health]`, `[module.movement]`, `[module.sensor]` — modifiers apply to the ship hull's stats.
|
||||||
- `[module.weapon]` — modifiers apply to every weapon module instance on the ship.
|
- `[module.weapon]` — modifiers apply to every weapon module instance on the ship.
|
||||||
- `[module.salvage]` — modifiers apply to every salvage module instance on the ship.
|
- `[module.salvage]` — modifiers apply to every salvage module instance on the ship.
|
||||||
- `[module.repair]` — modifiers apply to every repair module instance on the ship.
|
- `[module.repair]` — modifiers apply to every repair module instance on the ship.
|
||||||
- `[module.cargo]` — modifiers (`added_cargo_capacity`/`multiplied_cargo_capacity`) apply to the ship's **cargo capacity**, a ship-level stat (see REQ-MOD-CARGO-CAPACITY).
|
- `[module.cargo]` — modifiers (`added_cargo_capacity`/`multiplied_cargo_capacity`) apply to the ship's **cargo capacity**, a ship-level stat (see REQ-MOD-CARGO-CAPACITY).
|
||||||
|
|
||||||
Example: `[module.sensor].added_sensor_range_formula` adds to the ship's sensor range. `[module.weapon].multiplied_damage_formula` multiplies the damage of every weapon module instance on the ship.
|
Example: `[module.sensor].added_sensor_range` adds to the ship's sensor range. `[module.weapon].multiplied_damage` multiplies the damage of every weapon module instance on the ship.
|
||||||
|
|
||||||
- REQ-MOD-CARGO-CAPACITY: **Cargo capacity** is a first-class ship stat — the total number of scrap units the ship can hold in the single shared cargo pool used by its salvage modules (REQ-SHP-SALVAGE). Unlike other ship stats it has no hull base formula; its `base` (per REQ-MOD-STAT-CALC) is the **sum** of the `cargo_capacity` base values of every cargo-providing capability module instance on the ship — currently each salvage module's `cargo_capacity_formula`, evaluated at that module instance's `player_production_level`. Passive modifiers targeting `cargo_capacity` are declared under the `[module.cargo]` category and apply to this ship-level sum (`final = base × total_multiplier + total_additive`); they are not salvage-category modifiers and therefore scale the whole pool rather than any single instance. A ship whose cargo capacity is 0 (no cargo-providing module) is given no cargo pool.
|
- REQ-MOD-CARGO-CAPACITY: **Cargo capacity** is a first-class ship stat — the total number of scrap units the ship can hold in the single shared cargo pool used by its salvage modules (REQ-SHP-SALVAGE). Unlike other ship stats it has no hull base formula; its `base` (per REQ-MOD-STAT-CALC) is the **sum** of the `cargo_capacity` base values of every cargo-providing capability module instance on the ship — currently each salvage module's `cargo_capacity` value. Passive modifiers targeting `cargo_capacity` are declared under the `[module.cargo]` category and apply to this ship-level sum (`final = base × total_multiplier + total_additive`); they are not salvage-category modifiers and therefore scale the whole pool rather than any single instance. A ship whose cargo capacity is 0 (no cargo-providing module) is given no cargo pool.
|
||||||
|
|
||||||
### Module UI
|
### Module UI
|
||||||
|
|
||||||
@@ -260,7 +259,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- **Right** (below the grid): The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD).
|
- **Right** (below the grid): The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD).
|
||||||
- **Bottom**: A "Confirm" button and a "Cancel" button. Cancel discards all changes made in this dialog session and closes the dialog. Confirm applies the changes: the shipyard's configured layout is updated, the required materials and cycle time displayed in the selected building panel are recalculated, and the ship layout preview is refreshed.
|
- **Bottom**: A "Confirm" button and a "Cancel" button. Cancel discards all changes made in this dialog session and closes the dialog. Confirm applies the changes: the shipyard's configured layout is updated, the required materials and cycle time displayed in the selected building panel are recalculated, and the ship layout preview is refreshed.
|
||||||
|
|
||||||
- REQ-MOD-UI-STATS-PANEL: The **ship stats panel** in the layout configuration dialog shows the stats of the currently configured ship layout as they would be computed at the schematic's `player_production_level`, incorporating all passive module modifiers per REQ-MOD-STAT-CALC. The panel updates in real time whenever modules are placed or removed in the layout grid.
|
- REQ-MOD-UI-STATS-PANEL: The **ship stats panel** in the layout configuration dialog shows the stats of the currently configured ship layout as they would be computed, incorporating all passive module modifiers per REQ-MOD-STAT-CALC. The panel updates in real time whenever modules are placed or removed in the layout grid.
|
||||||
|
|
||||||
The panel always shows all hull stats as final computed values:
|
The panel always shows all hull stats as final computed values:
|
||||||
- HP
|
- HP
|
||||||
@@ -277,7 +276,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- **Salvage** (shown only if at least one salvage module is placed): combined collection rate = Σ(collection_rate_i) across all salvage module instances; maximum range = max(collection_range_i) across all salvage module instances.
|
- **Salvage** (shown only if at least one salvage module is placed): combined collection rate = Σ(collection_rate_i) across all salvage module instances; maximum range = max(collection_range_i) across all salvage module instances.
|
||||||
- **Repair** (shown only if at least one repair module is placed): combined repair rate (HP/s) = Σ(repair_rate_i × repair_amount_hp_i) across all repair module instances; maximum range = max(repair_range_i) across all repair module instances.
|
- **Repair** (shown only if at least one repair module is placed): combined repair rate (HP/s) = Σ(repair_rate_i × repair_amount_hp_i) across all repair module instances; maximum range = max(repair_range_i) across all repair module instances.
|
||||||
|
|
||||||
All capability module stat values incorporate passive modifiers targeting the relevant capability category per REQ-MOD-STAT-CALC. Each capability module instance uses its own `player_production_level` for formula evaluation.
|
All capability module stat values incorporate passive modifiers targeting the relevant capability category per REQ-MOD-STAT-CALC.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -305,21 +304,20 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- REQ-DEF-NO-CROSSFIRE: Enemy and player defence stations are never in each other's firing range.
|
- REQ-DEF-NO-CROSSFIRE: Enemy and player defence stations are never in each other's firing range.
|
||||||
- REQ-DEF-PUSH: When both enemy defence stations in a set are destroyed, the boss countdown is advanced (REQ-WAV-BOSS-ADVANCE), the scrollable area is extended (REQ-GW-PUSH-EXPAND), a new set of enemy defence stations is placed at the new boundary, and exactly one schematic drop is awarded for the destroyed set (REQ-DEF-SCHEMATIC-DROP).
|
- REQ-DEF-PUSH: When both enemy defence stations in a set are destroyed, the boss countdown is advanced (REQ-WAV-BOSS-ADVANCE), the scrollable area is extended (REQ-GW-PUSH-EXPAND), a new set of enemy defence stations is placed at the new boundary, and exactly one schematic drop is awarded for the destroyed set (REQ-DEF-SCHEMATIC-DROP).
|
||||||
- REQ-DEF-SCHEMATIC-DROP: Each destroyed set of enemy defence stations awards exactly one schematic drop (not one per station). The drop opens a **schematic choice dialog** — a modal dialog that pauses the game (speed set to 0×; on close, speed is restored to what it was before the dialog opened). Before drawing schematic picks, an artifact roll is made: evaluate `world.toml [world].artifact_chance_formula` with `x` set to the level of the destroyed station set, clamp the result to [0, 1], then compare against a uniform random value in [0, 1). If the roll succeeds, the dialog presents one **artifact option** plus two schematic picks drawn from the eligible pool; otherwise it presents three schematic picks. Up to three (or two, if an artifact option is present) schematic options are drawn uniformly at random **without replacement** from the eligible drop pool. If the pool contains fewer than the required number of entries, only that many schematic options are shown (the artifact option is always shown if the roll succeeded). The eligible drop pool contains:
|
- REQ-DEF-SCHEMATIC-DROP: Each destroyed set of enemy defence stations awards exactly one schematic drop (not one per station). The drop opens a **schematic choice dialog** — a modal dialog that pauses the game (speed set to 0×; on close, speed is restored to what it was before the dialog opened). Before drawing schematic picks, an artifact roll is made: evaluate `world.toml [world].artifact_chance_formula` with `x` set to the level of the destroyed station set, clamp the result to [0, 1], then compare against a uniform random value in [0, 1). If the roll succeeds, the dialog presents one **artifact option** plus two schematic picks drawn from the eligible pool; otherwise it presents three schematic picks. Up to three (or two, if an artifact option is present) schematic options are drawn uniformly at random **without replacement** from the eligible drop pool. If the pool contains fewer than the required number of entries, only that many schematic options are shown (the artifact option is always shown if the roll succeeded). The eligible drop pool contains:
|
||||||
- All **ship schematics** and **module schematics** whose `unlock_at_station_level` is -1 or is ≤ the level of the destroyed station set.
|
- All **ship schematics** and **module schematics** whose `unlock_at_station_level` is ≥ 0 and ≤ the level of the destroyed station set, and which have not yet been unlocked.
|
||||||
- All **assembler recipe schematics** whose `unlock_at_station_level` is ≥ 0 and ≤ the level of the destroyed station set, whose output item is currently implicitly unlocked (REQ-LOCK-IMPLICIT), and which have not yet been awarded.
|
- All **assembler recipe schematics** whose `unlock_at_station_level` is ≥ 0 and ≤ the level of the destroyed station set, whose output item is currently implicitly unlocked (REQ-LOCK-IMPLICIT), and which have not yet been awarded.
|
||||||
|
|
||||||
Each option in the dialog displays: the schematic name (ship `display_name` from `ships.toml`, module `id` from `modules.toml`, or the output item type for assembler recipes), the schematic type (ship, module, or assembler recipe), and whether selecting it would be a **new unlock** or a **level-up** (showing the target level for level-ups). Assembler recipe schematics are always new unlocks since they are removed from the pool once awarded. The artifact option (if present) is displayed as a distinct entry with the name "Artifact" and no unlock/level-up annotation.
|
Each option in the dialog displays: the schematic name (ship `display_name` from `ships.toml`, module `id` from `modules.toml`, or the output item type for assembler recipes) and the schematic type (ship, module, or assembler recipe). The artifact option (if present) is displayed as a distinct entry with the name "Artifact".
|
||||||
|
|
||||||
Each option additionally displays a vertical list of item names labeled "Unlocks recipes for:", showing which recipes would newly become implicitly unlocked (REQ-LOCK-IMPLICIT) if this option were selected — specifically, the output items of miner recipes and assembler recipes (without `unlock_at_station_level`) that are not currently implicitly unlocked but would become so after applying this option's effect:
|
Each option additionally displays a vertical list of item names labeled "Unlocks recipes for:", showing which recipes would newly become implicitly unlocked (REQ-LOCK-IMPLICIT) if this option were selected — specifically, the output items of miner recipes and assembler recipes (without `unlock_at_station_level`) that are not currently implicitly unlocked but would become so after applying this option's effect:
|
||||||
- For a ship or module schematic that would be a **new unlock**, its `materials` are added to the base set per REQ-LOCK-IMPLICIT step 1a before recomputation.
|
- For a ship or module schematic, its `materials` are added to the base set per REQ-LOCK-IMPLICIT step 1a before recomputation.
|
||||||
- For a ship or module schematic **level-up**, the implicit unlock set is unchanged, so the list is always empty.
|
|
||||||
- For an assembler recipe schematic, its output item is added to the base set per REQ-LOCK-IMPLICIT step 1b before recomputation.
|
- For an assembler recipe schematic, its output item is added to the base set per REQ-LOCK-IMPLICIT step 1b before recomputation.
|
||||||
|
|
||||||
Item names are deduplicated and sorted alphabetically. If no recipes would be newly unlocked, the list shows "None".
|
Item names are deduplicated and sorted alphabetically. If no recipes would be newly unlocked, the list shows "None".
|
||||||
|
|
||||||
The player selects one option by clicking it. If the player selects the artifact option, the player's artifact count is incremented by 1 (REQ-WIN-ARTIFACT-COUNT) and the dialog closes; no schematic is applied. Otherwise, the selected schematic is applied and the dialog closes:
|
The player selects one option by clicking it. If the player selects the artifact option, the player's artifact count is incremented by 1 (REQ-WIN-ARTIFACT-COUNT) and the dialog closes; no schematic is applied. Otherwise, the selected schematic is applied and the dialog closes:
|
||||||
|
|
||||||
For a **ship or module schematic**: if the player does not yet have the schematic, it is unlocked (ship schematics unlock the corresponding shipyard selection; module schematics unlock the module type for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG)). If the player already has it, the schematic's `player_production_level` is incremented by 1 — for ship schematics, subsequent ships of that type are produced at a higher level; for module schematics, all instances of that module type use the higher level in their stat formulas.
|
For a **ship or module schematic**: it is unlocked (ship schematics unlock the corresponding shipyard selection; module schematics unlock the module type for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG)).
|
||||||
|
|
||||||
For an **assembler recipe schematic**: the recipe is explicitly unlocked and becomes available in the assembler recipe-selection dialog (subject to REQ-LOCK-UI-RECIPE). The schematic is removed from the drop pool permanently (REQ-LOCK-EXPLICIT). The implicit unlock set is recomputed (REQ-LOCK-IMPLICIT).
|
For an **assembler recipe schematic**: the recipe is explicitly unlocked and becomes available in the assembler recipe-selection dialog (subject to REQ-LOCK-UI-RECIPE). The schematic is removed from the drop pool permanently (REQ-LOCK-EXPLICIT). The implicit unlock set is recomputed (REQ-LOCK-IMPLICIT).
|
||||||
|
|
||||||
@@ -345,11 +343,10 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
## Threat Level & Enemy Waves
|
## Threat Level & Enemy Waves
|
||||||
|
|
||||||
- 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 formula.
|
||||||
- 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 (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-TRIGGER: When the gap timer expires outside a quiet window, a normal wave is triggered. Ships are selected one at a time: from all schematics whose threat cost (REQ-MOD-THREAT) is > 0, uniformly randomly pick one whose cost fits the remaining threat budget. For wave ship selection, the threat cost is computed using the schematic's `default_modules` layout (REQ-WAV-DEFAULT-MODULES). Repeat until no eligible schematic fits. Any remaining threat carries over to the next normal wave. A longer gap results in a larger wave.
|
||||||
- REQ-WAV-SHIP-LEVEL: Each wave's (normal and boss) enemy ships are assigned a level determined by `world.toml [waves].ship_level_formula` where `x` is the boss wave counter (REQ-WAV-BOSS-COUNTER). Per-ship stats are computed from the ship level via the formulas in `ships.toml` (see REQ-SHP-STATS). Threat cost is level-independent (REQ-MOD-THREAT).
|
|
||||||
- REQ-WAV-BOSS-COUNTDOWN: A **boss countdown** timer starts at `world.toml [waves].boss_countdown_seconds` (default 300) at game start and counts down continuously in real game-time seconds. It is not paused during quiet windows. When it reaches 0, a boss wave is triggered (REQ-WAV-BOSS-TRIGGER). Immediately after the boss wave fires, `x` increments (REQ-WAV-BOSS-COUNTER) and a fresh countdown starts at the same configured value.
|
- REQ-WAV-BOSS-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.
|
||||||
@@ -447,7 +444,7 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
- For a **ship schematic** (Shipyard): the ship's `display_name`; the name and quantity of each base required material (`[ship.schematic].materials`, excluding any module contributions); the base production time (`[ship.schematic].production_time_seconds`); and "Produces: 1 <ship display name>".
|
- For a **ship schematic** (Shipyard): the ship's `display_name`; the name and quantity of each base required material (`[ship.schematic].materials`, excluding any module contributions); the base production time (`[ship.schematic].production_time_seconds`); and "Produces: 1 <ship display name>".
|
||||||
- REQ-UI-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. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT).
|
- REQ-UI-SHIP-STATS-PANEL: When a single ship is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **ship stats panel**. The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed from its installed modules per REQ-MOD-STAT-CALC. The panel always shows all hull stats: HP (current / maximum), max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed. In addition, capability module summaries are shown conditioned on which module types are installed, using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each section appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT).
|
||||||
- REQ-UI-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
|
||||||
@@ -484,7 +481,7 @@ A separate executable target (`balancing`) that links against `lib` but contains
|
|||||||
### Config
|
### Config
|
||||||
|
|
||||||
- REQ-BAL-CONFIG: The balancing tool reads arena definitions from a `balancing.toml` file. The file is read at startup and again each time the player triggers a config reload (REQ-BAL-UI-RELOAD). If parsing fails or required fields are missing at startup, the tool aborts with a clear error message. If parsing fails during a reload, a modal error dialog is shown describing the failure and the current arena list is left unchanged.
|
- REQ-BAL-CONFIG: The balancing tool reads arena definitions from a `balancing.toml` file. The file is read at startup and again each time the player triggers a config reload (REQ-BAL-UI-RELOAD). If parsing fails or required fields are missing at startup, the tool aborts with a clear error message. If parsing fails during a reload, a modal error dialog is shown describing the failure and the current arena list is left unchanged.
|
||||||
- REQ-BAL-CONFIG-GAME: Ship stats are read from `ships.toml` and defence station stats are read from `stations.toml`, using the same config loading as the main game. Formula evaluation uses the levels specified in the arena config.
|
- REQ-BAL-CONFIG-GAME: Ship stats are read from `ships.toml` and defence station stats are read from `stations.toml`, using the same config loading as the main game. Ship stats are the plain values from `ships.toml`; defence station formula evaluation uses the station levels specified in the arena config.
|
||||||
|
|
||||||
### Arena Definition
|
### Arena Definition
|
||||||
|
|
||||||
@@ -494,7 +491,7 @@ A separate executable target (`balancing`) that links against `lib` but contains
|
|||||||
- **World height** (in tiles).
|
- **World height** (in tiles).
|
||||||
- Exactly **two teams**, each with a human-readable **team name**.
|
- Exactly **two teams**, each with a human-readable **team name**.
|
||||||
- REQ-BAL-TEAM: Each team defines:
|
- REQ-BAL-TEAM: Each team defines:
|
||||||
- A list of **ship entries**, each specifying: ship schematic (type), level, count, and an optional `modules` array defining the module layout applied to every ship of that entry. The `modules` array format is identical to that used in `ship_layouts.toml` (see Layout Blueprint TOML Format). If `modules` is omitted, ships of that entry have no modules. Invalid module instances (unknown type, position outside the grid, position on a non-buildable cell, or overlapping another module in the same entry) are silently skipped during loading.
|
- A list of **ship entries**, each specifying: ship schematic (type), count, and an optional `modules` array defining the module layout applied to every ship of that entry. The `modules` array format is identical to that used in `ship_layouts.toml` (see Layout Blueprint TOML Format). If `modules` is omitted, ships of that entry have no modules. Invalid module instances (unknown type, position outside the grid, position on a non-buildable cell, or overlapping another module in the same entry) are silently skipped during loading.
|
||||||
- An optional list of **defence station entries**, each specifying: station type (`player_station` or `enemy_station` from `stations.toml`), level, and tile position (x, y).
|
- An optional list of **defence station entries**, each specifying: station type (`player_station` or `enemy_station` from `stations.toml`), level, and tile position (x, y).
|
||||||
- REQ-BAL-HQ: Each team has an HQ placed automatically at the vertical center of the arena at the far end of that team's buffer zone. HQ stats are read from `stations.toml [hq]` at level 1. Team 1's HQ is at the left edge; team 2's HQ is at the right edge.
|
- REQ-BAL-HQ: Each team has an HQ placed automatically at the vertical center of the arena at the far end of that team's buffer zone. HQ stats are read from `stations.toml [hq]` at level 1. Team 1's HQ is at the left edge; team 2's HQ is at the right edge.
|
||||||
- REQ-BAL-SPAWN: Team 1's ships spawn in team 1's buffer zone (left side); team 2's ships spawn in team 2's buffer zone (right side). Spawn positions are uniformly random within the respective buffer zone.
|
- REQ-BAL-SPAWN: Team 1's ships spawn in team 1's buffer zone (left side); team 2's ships spawn in team 2's buffer zone (right side). Spawn positions are uniformly random within the respective buffer zone.
|
||||||
@@ -512,7 +509,7 @@ A separate executable target (`balancing`) that links against `lib` but contains
|
|||||||
- REQ-BAL-UI-WINDOW: On startup the tool displays a window containing a "Reload Config" button and a "Start All" button at the top (in that order, left to right), followed by a scrollable vertical list of arena widgets, one per arena defined in `balancing.toml`. Simulations do not start automatically on startup. All buttons and controls in the main window are disabled while an arena is being inspected (REQ-BAL-UI-INSPECT).
|
- REQ-BAL-UI-WINDOW: On startup the tool displays a window containing a "Reload Config" button and a "Start All" button at the top (in that order, left to right), followed by a scrollable vertical list of arena widgets, one per arena defined in `balancing.toml`. Simulations do not start automatically on startup. All buttons and controls in the main window are disabled while an arena is being inspected (REQ-BAL-UI-INSPECT).
|
||||||
- REQ-BAL-UI-RELOAD: The "Reload Config" button reloads all config files from disk (`balancing.toml`, `ships.toml`, `stations.toml`), stops any running simulations, and replaces the arena widget list with freshly created widgets from the reloaded config. The button is disabled while any arena simulation is currently running.
|
- REQ-BAL-UI-RELOAD: The "Reload Config" button reloads all config files from disk (`balancing.toml`, `ships.toml`, `stations.toml`), stops any running simulations, and replaces the arena widget list with freshly created widgets from the reloaded config. The button is disabled while any arena simulation is currently running.
|
||||||
- REQ-BAL-UI-START-ALL: The "Start All" button is placed above the scrollable arena list, to the right of the "Reload Config" button. Clicking it starts (or restarts) the simulation for every arena that is not currently running. The button is disabled when all arenas are currently running.
|
- REQ-BAL-UI-START-ALL: The "Start All" button is placed above the scrollable arena list, to the right of the "Reload Config" button. Clicking it starts (or restarts) the simulation for every arena that is not currently running. The button is disabled when all arenas are currently running.
|
||||||
- REQ-BAL-UI-WIDGET: Each arena widget displays the arena name, an "Inspect" button (to the right of the arena name), and two columns (one per team). Each column shows the team name as a header, then directly below the header the team's **accumulated threat level** — the sum, across the team's configured ship entries, of each entry's `count` multiplied by the threat cost (REQ-MOD-THREAT) of one ship of that entry computed from its level-independent module layout. Only ships contribute; the HQ and defence stations are excluded. This value is static: it is computed once from the full configured roster and does not change as ships are destroyed during the fight. Below the threat level, the column shows a list of entries. The HQ is always the first entry in each column. Below the HQ, ship types are listed, followed by defence stations (if any). Each entry uses the format `surviving/total TypeName Llevel` — for example `2/3 Fighter L5` or `1/1 HQ L1`. The surviving count updates live as the simulation progresses. When the fight ends, the winning team's name header is prefixed with `[WON]`.
|
- REQ-BAL-UI-WIDGET: Each arena widget displays the arena name, an "Inspect" button (to the right of the arena name), and two columns (one per team). Each column shows the team name as a header, then directly below the header the team's **accumulated threat level** — the sum, across the team's configured ship entries, of each entry's `count` multiplied by the threat cost (REQ-MOD-THREAT) of one ship of that entry computed from its level-independent module layout. Only ships contribute; the HQ and defence stations are excluded. This value is static: it is computed once from the full configured roster and does not change as ships are destroyed during the fight. Below the threat level, the column shows a list of entries. The HQ is always the first entry in each column. Below the HQ, ship types are listed, followed by defence stations (if any). Each entry uses the format `surviving/total TypeName` for ship entries and `surviving/total TypeName Llevel` for the HQ and defence station entries — for example `2/3 Fighter`, `1/1 HQ L1`, or `2/2 Enemy Station L3`. The surviving count updates live as the simulation progresses. When the fight ends, the winning team's name header is prefixed with `[WON]`.
|
||||||
- REQ-BAL-UI-WIDGET-START: Each arena widget contains a "Start" button that starts the simulation for that arena. The button is disabled while the arena's simulation is running. When a finished arena's Start button is clicked, a fresh simulation is created and started (the widget resets to initial unit counts, the border returns to blue, and the previous results are replaced).
|
- REQ-BAL-UI-WIDGET-START: Each arena widget contains a "Start" button that starts the simulation for that arena. The button is disabled while the arena's simulation is running. When a finished arena's Start button is clicked, a fresh simulation is created and started (the widget resets to initial unit counts, the border returns to blue, and the previous results are replaced).
|
||||||
- REQ-BAL-UI-WIDGET-BORDER: Each arena widget has a colored border indicating its state: grey when not yet started, blue while its simulation is running, and green when the fight has ended.
|
- REQ-BAL-UI-WIDGET-BORDER: Each arena widget has a colored border indicating its state: grey when not yet started, blue while its simulation is running, and green when the fight has ended.
|
||||||
- REQ-BAL-UI-INSPECT: Clicking an arena widget's "Inspect" button opens a new inspect window for that arena. Any previously open inspect window is closed first (its arena's simulation is aborted and its widget border returns to grey). The inspected arena is restarted with a fresh simulation that runs at controllable game speed with full rendering (REQ-BAL-SIM-SPEED). The arena widget updates live during inspection (surviving counts, border color, `[WON]` prefix) as it does for non-inspected arenas. Only one inspect window may be open at a time.
|
- REQ-BAL-UI-INSPECT: Clicking an arena widget's "Inspect" button opens a new inspect window for that arena. Any previously open inspect window is closed first (its arena's simulation is aborted and its widget border returns to grey). The inspected arena is restarted with a fresh simulation that runs at controllable game speed with full rendering (REQ-BAL-SIM-SPEED). The arena widget updates live during inspection (surviving counts, border color, `[WON]` prefix) as it does for non-inspected arenas. Only one inspect window may be open at a time.
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ void ArenaSimulation::spawnShips()
|
|||||||
for (int i = 0; i < entry.count; ++i)
|
for (int i = 0; i < entry.count; ++i)
|
||||||
{
|
{
|
||||||
const QVector2D pos(xDist(m_rng), yDist(m_rng));
|
const QVector2D pos(xDist(m_rng), yDist(m_rng));
|
||||||
m_shipSystem->spawn(entry.schematicId, entry.level, pos, false,
|
m_shipSystem->spawn(entry.schematicId, pos, false,
|
||||||
entry.layout);
|
entry.layout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,7 +248,7 @@ void ArenaSimulation::spawnShips()
|
|||||||
for (int i = 0; i < entry.count; ++i)
|
for (int i = 0; i < entry.count; ++i)
|
||||||
{
|
{
|
||||||
const QVector2D pos(xDist(m_rng), yDist(m_rng));
|
const QVector2D pos(xDist(m_rng), yDist(m_rng));
|
||||||
m_shipSystem->spawn(entry.schematicId, entry.level, pos, true,
|
m_shipSystem->spawn(entry.schematicId, pos, true,
|
||||||
entry.layout);
|
entry.layout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,7 +500,7 @@ void ArenaSimulation::updateStatus()
|
|||||||
{
|
{
|
||||||
ArenaStatus::Entry entry;
|
ArenaStatus::Entry entry;
|
||||||
entry.displayName = shipEntry.schematicId;
|
entry.displayName = shipEntry.schematicId;
|
||||||
entry.level = shipEntry.level;
|
// Ships no longer carry a level (level suffix stays empty).
|
||||||
entry.total = shipEntry.count;
|
entry.total = shipEntry.count;
|
||||||
|
|
||||||
int surviving = 0;
|
int surviving = 0;
|
||||||
@@ -512,7 +512,6 @@ void ArenaSimulation::updateStatus()
|
|||||||
{
|
{
|
||||||
if (f.isEnemy == isEnemyTeam
|
if (f.isEnemy == isEnemyTeam
|
||||||
&& si.schematicId == shipEntry.schematicId
|
&& si.schematicId == shipEntry.schematicId
|
||||||
&& si.level == shipEntry.level
|
|
||||||
&& h.hp > 0.0f)
|
&& h.hp > 0.0f)
|
||||||
{
|
{
|
||||||
++surviving;
|
++surviving;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -32,7 +33,9 @@ struct ArenaStatus
|
|||||||
struct Entry
|
struct Entry
|
||||||
{
|
{
|
||||||
std::string displayName;
|
std::string displayName;
|
||||||
int level;
|
// Level suffix shown in the widget/inspect display. Set for the HQ and
|
||||||
|
// defence stations; empty for ships, which no longer have a level.
|
||||||
|
std::optional<int> level;
|
||||||
int total;
|
int total;
|
||||||
int surviving;
|
int surviving;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -126,11 +126,14 @@ void ArenaWidget::updateStatus(const ArenaStatus& status)
|
|||||||
{
|
{
|
||||||
lines += "\n";
|
lines += "\n";
|
||||||
}
|
}
|
||||||
lines += QString("%1/%2 %3 L%4")
|
lines += QString("%1/%2 %3")
|
||||||
.arg(entry.surviving)
|
.arg(entry.surviving)
|
||||||
.arg(entry.total)
|
.arg(entry.total)
|
||||||
.arg(QString::fromStdString(entry.displayName))
|
.arg(QString::fromStdString(entry.displayName));
|
||||||
.arg(entry.level);
|
if (entry.level.has_value())
|
||||||
|
{
|
||||||
|
lines += QString(" L%1").arg(entry.level.value());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
content->setText(lines);
|
content->setText(lines);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,8 +125,6 @@ BalancingConfig loadBalancingConfig(const std::string& path)
|
|||||||
ArenaShipEntry entry;
|
ArenaShipEntry entry;
|
||||||
entry.schematicId = requireString((*shipTbl)["schematic"],
|
entry.schematicId = requireString((*shipTbl)["schematic"],
|
||||||
sPrefix + ".schematic");
|
sPrefix + ".schematic");
|
||||||
entry.level = static_cast<int>(
|
|
||||||
requireInt((*shipTbl)["level"], sPrefix + ".level"));
|
|
||||||
entry.count = static_cast<int>(
|
entry.count = static_cast<int>(
|
||||||
requireInt((*shipTbl)["count"], sPrefix + ".count"));
|
requireInt((*shipTbl)["count"], sPrefix + ".count"));
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ struct ArenaStationEntry
|
|||||||
struct ArenaShipEntry
|
struct ArenaShipEntry
|
||||||
{
|
{
|
||||||
std::string schematicId;
|
std::string schematicId;
|
||||||
int level;
|
|
||||||
int count;
|
int count;
|
||||||
std::optional<ShipLayoutConfig> layout;
|
std::optional<ShipLayoutConfig> layout;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -223,11 +223,14 @@ void InspectWindow::updateInfoPanel(const ArenaStatus& status)
|
|||||||
{
|
{
|
||||||
lines += "\n";
|
lines += "\n";
|
||||||
}
|
}
|
||||||
lines += QString("%1/%2 %3 L%4")
|
lines += QString("%1/%2 %3")
|
||||||
.arg(entry.surviving)
|
.arg(entry.surviving)
|
||||||
.arg(entry.total)
|
.arg(entry.total)
|
||||||
.arg(QString::fromStdString(entry.displayName))
|
.arg(QString::fromStdString(entry.displayName));
|
||||||
.arg(entry.level);
|
if (entry.level.has_value())
|
||||||
|
{
|
||||||
|
lines += QString(" L%1").arg(entry.level.value());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
content->setText(lines);
|
content->setText(lines);
|
||||||
}
|
}
|
||||||
@@ -256,9 +259,8 @@ void InspectWindow::handleEvent(std::shared_ptr<const EntitySelectedEvent> event
|
|||||||
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity);
|
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity);
|
||||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
||||||
|
|
||||||
m_entityTitleLabel->setText(tr("Ship: %1 (Lv %2)")
|
m_entityTitleLabel->setText(tr("Ship: %1")
|
||||||
.arg(QString::fromStdString(identity.schematicId))
|
.arg(QString::fromStdString(identity.schematicId)));
|
||||||
.arg(identity.level));
|
|
||||||
m_entityTitleLabel->show();
|
m_entityTitleLabel->show();
|
||||||
|
|
||||||
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
||||||
|
|||||||
@@ -284,7 +284,6 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
|||||||
cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds");
|
cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds");
|
||||||
|
|
||||||
cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula");
|
cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula");
|
||||||
cfg.waves.shipLevelFormula = requireFormula(tbl["waves"]["ship_level_formula"], file, "waves.ship_level_formula");
|
|
||||||
cfg.waves.gapMinSeconds = requireDouble(tbl["waves"]["gap_min_seconds"], file, "waves.gap_min_seconds");
|
cfg.waves.gapMinSeconds = requireDouble(tbl["waves"]["gap_min_seconds"], file, "waves.gap_min_seconds");
|
||||||
cfg.waves.gapMaxSeconds = requireDouble(tbl["waves"]["gap_max_seconds"], file, "waves.gap_max_seconds");
|
cfg.waves.gapMaxSeconds = requireDouble(tbl["waves"]["gap_max_seconds"], file, "waves.gap_max_seconds");
|
||||||
cfg.waves.spawnDurationSeconds = requireDouble(tbl["waves"]["spawn_duration_seconds"], file, "waves.spawn_duration_seconds");
|
cfg.waves.spawnDurationSeconds = requireDouble(tbl["waves"]["spawn_duration_seconds"], file, "waves.spawn_duration_seconds");
|
||||||
@@ -433,8 +432,6 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
|||||||
|
|
||||||
const toml::array& materials = requireArray(bpMt["materials"], file, bpPath + ".materials");
|
const toml::array& materials = requireArray(bpMt["materials"], file, bpPath + ".materials");
|
||||||
def.schematic.materials = parseIngredients(materials, file, bpPath + ".materials");
|
def.schematic.materials = parseIngredients(materials, file, bpPath + ".materials");
|
||||||
def.schematic.playerProductionLevel = static_cast<int>(requireInt(
|
|
||||||
bpMt["player_production_level"], file, bpPath + ".player_production_level"));
|
|
||||||
def.schematic.productionTimeSeconds = requireDouble(
|
def.schematic.productionTimeSeconds = requireDouble(
|
||||||
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
|
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
|
||||||
}
|
}
|
||||||
@@ -444,7 +441,7 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
|||||||
const std::string hPath = elemPath + ".health";
|
const std::string hPath = elemPath + ".health";
|
||||||
const toml::table& hTable = requireTable(mt["health"], file, hPath);
|
const toml::table& hTable = requireTable(mt["health"], file, hPath);
|
||||||
toml::table& hMt = const_cast<toml::table&>(hTable);
|
toml::table& hMt = const_cast<toml::table&>(hTable);
|
||||||
def.health.hpFormula = requireFormula(hMt["hp_formula"], file, hPath + ".hp_formula");
|
def.health.hp = static_cast<float>(requireDouble(hMt["hp"], file, hPath + ".hp"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Movement
|
// Movement
|
||||||
@@ -452,11 +449,11 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
|||||||
const std::string mPath = elemPath + ".movement";
|
const std::string mPath = elemPath + ".movement";
|
||||||
const toml::table& mTable = requireTable(mt["movement"], file, mPath);
|
const toml::table& mTable = requireTable(mt["movement"], file, mPath);
|
||||||
toml::table& mMt = const_cast<toml::table&>(mTable);
|
toml::table& mMt = const_cast<toml::table&>(mTable);
|
||||||
def.movement.speedFormula = requireFormula(mMt["speed_mps_formula"], file, mPath + ".speed_mps_formula");
|
def.movement.speed_mps = static_cast<float>(requireDouble(mMt["speed_mps"], file, mPath + ".speed_mps"));
|
||||||
def.movement.mainAccelerationFormula = requireFormula(mMt["main_acceleration_mpss_formula"], file, mPath + ".main_acceleration_mpss_formula");
|
def.movement.mainAcceleration_mpss = static_cast<float>(requireDouble(mMt["main_acceleration_mpss"], file, mPath + ".main_acceleration_mpss"));
|
||||||
def.movement.maneuveringAccelerationFormula = requireFormula(mMt["maneuvering_acceleration_mpss_formula"], file, mPath + ".maneuvering_acceleration_mpss_formula");
|
def.movement.maneuveringAcceleration_mpss = static_cast<float>(requireDouble(mMt["maneuvering_acceleration_mpss"], file, mPath + ".maneuvering_acceleration_mpss"));
|
||||||
def.movement.angularAccelerationFormula = requireFormula(mMt["angular_acceleration_radpss_formula"], file, mPath + ".angular_acceleration_radpss_formula");
|
def.movement.angularAcceleration_radpss = static_cast<float>(requireDouble(mMt["angular_acceleration_radpss"], file, mPath + ".angular_acceleration_radpss"));
|
||||||
def.movement.maxRotationSpeedFormula = requireFormula(mMt["max_rotation_speed_radps_formula"], file, mPath + ".max_rotation_speed_radps_formula");
|
def.movement.maxRotationSpeed_radps = static_cast<float>(requireDouble(mMt["max_rotation_speed_radps"], file, mPath + ".max_rotation_speed_radps"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sensor
|
// Sensor
|
||||||
@@ -464,7 +461,7 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
|||||||
const std::string snsPath = elemPath + ".sensor";
|
const std::string snsPath = elemPath + ".sensor";
|
||||||
const toml::table& snsTable = requireTable(mt["sensor"], file, snsPath);
|
const toml::table& snsTable = requireTable(mt["sensor"], file, snsPath);
|
||||||
toml::table& snsMt = const_cast<toml::table&>(snsTable);
|
toml::table& snsMt = const_cast<toml::table&>(snsTable);
|
||||||
def.sensor.sensorRangeFormula = requireFormula(snsMt["sensor_range_m_formula"], file, snsPath + ".sensor_range_m_formula");
|
def.sensor.sensorRange_m = static_cast<float>(requireDouble(snsMt["sensor_range_m"], file, snsPath + ".sensor_range_m"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: default_modules (REQ-WAV-DEFAULT-MODULES)
|
// Optional: default_modules (REQ-WAV-DEFAULT-MODULES)
|
||||||
@@ -577,8 +574,6 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|||||||
def.unlockAtStationLevel = static_cast<int>(
|
def.unlockAtStationLevel = static_cast<int>(
|
||||||
mt["unlock_at_station_level"].value_or<int64_t>(-1));
|
mt["unlock_at_station_level"].value_or<int64_t>(-1));
|
||||||
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||||
def.playerProductionLevel = static_cast<int>(requireInt(
|
|
||||||
mt["player_production_level"], file, elemPath + ".player_production_level"));
|
|
||||||
def.productionTimeSeconds = requireDouble(
|
def.productionTimeSeconds = requireDouble(
|
||||||
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
||||||
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
||||||
@@ -601,15 +596,15 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|||||||
elemPath + "." + se.category);
|
elemPath + "." + se.category);
|
||||||
toml::table& catMt = const_cast<toml::table&>(catTable);
|
toml::table& catMt = const_cast<toml::table&>(catTable);
|
||||||
|
|
||||||
const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix + "_formula";
|
const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix;
|
||||||
const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix + "_formula";
|
const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix;
|
||||||
|
|
||||||
if (catMt.contains(addedKey))
|
if (catMt.contains(addedKey))
|
||||||
{
|
{
|
||||||
ModuleStatModifier mod;
|
ModuleStatModifier mod;
|
||||||
mod.stat = se.stat;
|
mod.stat = se.stat;
|
||||||
mod.modifierType = "additive";
|
mod.modifierType = "additive";
|
||||||
mod.formula = requireFormula(catMt[addedKey], file,
|
mod.value = requireDouble(catMt[addedKey], file,
|
||||||
elemPath + "." + se.category + "." + addedKey);
|
elemPath + "." + se.category + "." + addedKey);
|
||||||
def.statModifiers.push_back(std::move(mod));
|
def.statModifiers.push_back(std::move(mod));
|
||||||
}
|
}
|
||||||
@@ -619,7 +614,7 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|||||||
ModuleStatModifier mod;
|
ModuleStatModifier mod;
|
||||||
mod.stat = se.stat;
|
mod.stat = se.stat;
|
||||||
mod.modifierType = "multiplicative";
|
mod.modifierType = "multiplicative";
|
||||||
mod.formula = requireFormula(catMt[multipliedKey], file,
|
mod.value = requireDouble(catMt[multipliedKey], file,
|
||||||
elemPath + "." + se.category + "." + multipliedKey);
|
elemPath + "." + se.category + "." + multipliedKey);
|
||||||
def.statModifiers.push_back(std::move(mod));
|
def.statModifiers.push_back(std::move(mod));
|
||||||
}
|
}
|
||||||
@@ -631,16 +626,16 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|||||||
const std::string wPath = elemPath + ".weapon";
|
const std::string wPath = elemPath + ".weapon";
|
||||||
const toml::table& wTable = requireTable(mt["weapon"], file, wPath);
|
const toml::table& wTable = requireTable(mt["weapon"], file, wPath);
|
||||||
toml::table& wMt = const_cast<toml::table&>(wTable);
|
toml::table& wMt = const_cast<toml::table&>(wTable);
|
||||||
if (wMt.contains("damage_formula") || wMt.contains("attack_range_m_formula")
|
if (wMt.contains("damage") || wMt.contains("attack_range_m")
|
||||||
|| wMt.contains("attack_rate_hz_formula"))
|
|| wMt.contains("attack_rate_hz"))
|
||||||
{
|
{
|
||||||
ModuleWeaponCapability cap;
|
ModuleWeaponCapability cap;
|
||||||
cap.damageFormula = requireFormula(wMt["damage_formula"],
|
cap.damage = static_cast<float>(requireDouble(wMt["damage"],
|
||||||
file, wPath + ".damage_formula");
|
file, wPath + ".damage"));
|
||||||
cap.attackRangeFormula = requireFormula(wMt["attack_range_m_formula"],
|
cap.attackRange_m = static_cast<float>(requireDouble(wMt["attack_range_m"],
|
||||||
file, wPath + ".attack_range_m_formula");
|
file, wPath + ".attack_range_m"));
|
||||||
cap.attackRateFormula = requireFormula(wMt["attack_rate_hz_formula"],
|
cap.attackRate_hz = static_cast<float>(requireDouble(wMt["attack_rate_hz"],
|
||||||
file, wPath + ".attack_rate_hz_formula");
|
file, wPath + ".attack_rate_hz"));
|
||||||
def.weaponCapability = std::move(cap);
|
def.weaponCapability = std::move(cap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -651,16 +646,16 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|||||||
const std::string sPath = elemPath + ".salvage";
|
const std::string sPath = elemPath + ".salvage";
|
||||||
const toml::table& sTable = requireTable(mt["salvage"], file, sPath);
|
const toml::table& sTable = requireTable(mt["salvage"], file, sPath);
|
||||||
toml::table& sMt = const_cast<toml::table&>(sTable);
|
toml::table& sMt = const_cast<toml::table&>(sTable);
|
||||||
if (sMt.contains("collection_range_m_formula") || sMt.contains("cargo_capacity_formula")
|
if (sMt.contains("collection_range_m") || sMt.contains("cargo_capacity")
|
||||||
|| sMt.contains("collection_rate_hz_formula"))
|
|| sMt.contains("collection_rate_hz"))
|
||||||
{
|
{
|
||||||
ModuleSalvageCapability cap;
|
ModuleSalvageCapability cap;
|
||||||
cap.collectionRangeFormula = requireFormula(sMt["collection_range_m_formula"],
|
cap.collectionRange_m = static_cast<float>(requireDouble(sMt["collection_range_m"],
|
||||||
file, sPath + ".collection_range_m_formula");
|
file, sPath + ".collection_range_m"));
|
||||||
cap.cargoCapacityFormula = requireFormula(sMt["cargo_capacity_formula"],
|
cap.cargoCapacity = static_cast<float>(requireDouble(sMt["cargo_capacity"],
|
||||||
file, sPath + ".cargo_capacity_formula");
|
file, sPath + ".cargo_capacity"));
|
||||||
cap.collectionRateFormula = requireFormula(sMt["collection_rate_hz_formula"],
|
cap.collectionRate_hz = static_cast<float>(requireDouble(sMt["collection_rate_hz"],
|
||||||
file, sPath + ".collection_rate_hz_formula");
|
file, sPath + ".collection_rate_hz"));
|
||||||
def.salvageCapability = std::move(cap);
|
def.salvageCapability = std::move(cap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -671,15 +666,15 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|||||||
const std::string rPath = elemPath + ".repair";
|
const std::string rPath = elemPath + ".repair";
|
||||||
const toml::table& rTable = requireTable(mt["repair"], file, rPath);
|
const toml::table& rTable = requireTable(mt["repair"], file, rPath);
|
||||||
toml::table& rMt = const_cast<toml::table&>(rTable);
|
toml::table& rMt = const_cast<toml::table&>(rTable);
|
||||||
if (rMt.contains("repair_rate_hz_formula") || rMt.contains("repair_range_m_formula"))
|
if (rMt.contains("repair_rate_hz") || rMt.contains("repair_range_m"))
|
||||||
{
|
{
|
||||||
ModuleRepairCapability cap;
|
ModuleRepairCapability cap;
|
||||||
cap.repairRateFormula = requireFormula(rMt["repair_rate_hz_formula"],
|
cap.repairRate_hz = static_cast<float>(requireDouble(rMt["repair_rate_hz"],
|
||||||
file, rPath + ".repair_rate_hz_formula");
|
file, rPath + ".repair_rate_hz"));
|
||||||
cap.repairAmountHpFormula = requireFormula(rMt["repair_amount_hp_formula"],
|
cap.repairAmountHp = static_cast<float>(requireDouble(rMt["repair_amount_hp"],
|
||||||
file, rPath + ".repair_amount_hp_formula");
|
file, rPath + ".repair_amount_hp"));
|
||||||
cap.repairRangeFormula = requireFormula(rMt["repair_range_m_formula"],
|
cap.repairRange_m = static_cast<float>(requireDouble(rMt["repair_range_m"],
|
||||||
file, rPath + ".repair_range_m_formula");
|
file, rPath + ".repair_range_m"));
|
||||||
def.repairCapability = std::move(cap);
|
def.repairCapability = std::move(cap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "Formula.h"
|
|
||||||
#include "RecipesConfig.h"
|
#include "RecipesConfig.h"
|
||||||
|
|
||||||
// A single stat modifier contributed by a module instance.
|
// A single stat modifier contributed by a module instance.
|
||||||
@@ -13,29 +12,29 @@ struct ModuleStatModifier
|
|||||||
{
|
{
|
||||||
std::string stat; // e.g. "hp", "speed", "sensor_range"
|
std::string stat; // e.g. "hp", "speed", "sensor_range"
|
||||||
std::string modifierType; // "additive" or "multiplicative"
|
std::string modifierType; // "additive" or "multiplicative"
|
||||||
Formula formula;
|
double value;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Capability sections — present when the module grants that capability.
|
// Capability sections — present when the module grants that capability.
|
||||||
struct ModuleWeaponCapability
|
struct ModuleWeaponCapability
|
||||||
{
|
{
|
||||||
Formula damageFormula;
|
float damage;
|
||||||
Formula attackRangeFormula;
|
float attackRange_m;
|
||||||
Formula attackRateFormula;
|
float attackRate_hz;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ModuleSalvageCapability
|
struct ModuleSalvageCapability
|
||||||
{
|
{
|
||||||
Formula collectionRangeFormula;
|
float collectionRange_m;
|
||||||
Formula cargoCapacityFormula;
|
float cargoCapacity;
|
||||||
Formula collectionRateFormula;
|
float collectionRate_hz;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ModuleRepairCapability
|
struct ModuleRepairCapability
|
||||||
{
|
{
|
||||||
Formula repairRateFormula; // repair cycles per second
|
float repairRate_hz; // repair cycles per second
|
||||||
Formula repairAmountHpFormula; // HP restored per cycle
|
float repairAmountHp; // HP restored per cycle
|
||||||
Formula repairRangeFormula;
|
float repairRange_m;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ModuleDef
|
struct ModuleDef
|
||||||
@@ -44,7 +43,6 @@ struct ModuleDef
|
|||||||
int unlockAtStationLevel;
|
int unlockAtStationLevel;
|
||||||
std::vector<std::string> surfaceMask;
|
std::vector<std::string> surfaceMask;
|
||||||
std::vector<RecipeIngredient> materials;
|
std::vector<RecipeIngredient> materials;
|
||||||
int playerProductionLevel;
|
|
||||||
double productionTimeSeconds;
|
double productionTimeSeconds;
|
||||||
std::string fillColor;
|
std::string fillColor;
|
||||||
std::string glyph;
|
std::string glyph;
|
||||||
|
|||||||
@@ -3,36 +3,33 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "Formula.h"
|
|
||||||
#include "RecipesConfig.h" // for RecipeIngredient
|
#include "RecipesConfig.h" // for RecipeIngredient
|
||||||
#include "ShipLayout.h" // for PlacedModule
|
#include "ShipLayout.h" // for PlacedModule
|
||||||
|
|
||||||
// Build materials and initial per-schematic production level
|
// Build materials and base production time (REQ-BLD-SHIPYARD, REQ-DEF-SCHEMATIC-DROP).
|
||||||
// (REQ-BLD-SHIPYARD, REQ-DEF-SCHEMATIC-DROP).
|
|
||||||
struct ShipSchematic
|
struct ShipSchematic
|
||||||
{
|
{
|
||||||
std::vector<RecipeIngredient> materials;
|
std::vector<RecipeIngredient> materials;
|
||||||
int playerProductionLevel;
|
|
||||||
double productionTimeSeconds;
|
double productionTimeSeconds;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ShipHealth
|
struct ShipHealth
|
||||||
{
|
{
|
||||||
Formula hpFormula; // REQ-SHP-STATS
|
float hp; // REQ-SHP-STATS
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ShipMovement
|
struct ShipMovement
|
||||||
{
|
{
|
||||||
Formula speedFormula; // max linear speed cap, tiles/s (REQ-SHP-STATS, REQ-SHP-MOVEMENT)
|
float speed_mps; // max linear speed cap, m/s (REQ-SHP-STATS, REQ-SHP-MOVEMENT)
|
||||||
Formula mainAccelerationFormula; // forward acceleration, tiles/s²
|
float mainAcceleration_mpss; // forward acceleration, m/s²
|
||||||
Formula maneuveringAccelerationFormula;// omnidirectional acceleration, tiles/s²
|
float maneuveringAcceleration_mpss;// omnidirectional acceleration, m/s²
|
||||||
Formula angularAccelerationFormula; // angular acceleration, rad/s²
|
float angularAcceleration_radpss; // angular acceleration, rad/s²
|
||||||
Formula maxRotationSpeedFormula; // angular velocity cap, rad/s
|
float maxRotationSpeed_radps; // angular velocity cap, rad/s
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ShipSensor
|
struct ShipSensor
|
||||||
{
|
{
|
||||||
Formula sensorRangeFormula; // REQ-SHP-SENSOR, REQ-SHP-STATS
|
float sensorRange_m; // REQ-SHP-SENSOR, REQ-SHP-STATS
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ShipDef
|
struct ShipDef
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ struct WorldPush
|
|||||||
struct WorldWaves
|
struct WorldWaves
|
||||||
{
|
{
|
||||||
Formula threatRateFormula; // threat/s as a function of boss wave counter x
|
Formula threatRateFormula; // threat/s as a function of boss wave counter x
|
||||||
Formula shipLevelFormula; // enemy ship level as a function of boss wave counter x
|
|
||||||
double gapMinSeconds;
|
double gapMinSeconds;
|
||||||
double gapMaxSeconds;
|
double gapMaxSeconds;
|
||||||
double spawnDurationSeconds;
|
double spawnDurationSeconds;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
|
|||||||
float maxSpeed_tpt, float mainAcceleration_tptt,
|
float maxSpeed_tpt, float mainAcceleration_tptt,
|
||||||
float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt,
|
float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt,
|
||||||
float maxRotationSpeed_rpt, float sensorRange_tiles,
|
float maxRotationSpeed_rpt, float sensorRange_tiles,
|
||||||
int level, const std::string& schematicId, bool isEnemy)
|
const std::string& schematicId, bool isEnemy)
|
||||||
{
|
{
|
||||||
entt::entity entity = createEntity();
|
entt::entity entity = createEntity();
|
||||||
add<PositionComponent>(entity, PositionComponent{position});
|
add<PositionComponent>(entity, PositionComponent{position});
|
||||||
@@ -61,7 +61,7 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
|
|||||||
0.0f // angularAcceleration_rptt
|
0.0f // angularAcceleration_rptt
|
||||||
});
|
});
|
||||||
add<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange_tiles});
|
add<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange_tiles});
|
||||||
add<ShipIdentityComponent>(entity, ShipIdentityComponent{level, schematicId});
|
add<ShipIdentityComponent>(entity, ShipIdentityComponent{schematicId});
|
||||||
add<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)});
|
add<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)});
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public:
|
|||||||
float maxSpeed_tpt, float mainAcceleration_tptt,
|
float maxSpeed_tpt, float mainAcceleration_tptt,
|
||||||
float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt,
|
float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt,
|
||||||
float maxRotationSpeed_rpt, float sensorRange_tiles,
|
float maxRotationSpeed_rpt, float sensorRange_tiles,
|
||||||
int level, const std::string& schematicId, bool isEnemy);
|
const std::string& schematicId, bool isEnemy);
|
||||||
|
|
||||||
entt::entity spawnStation(QPoint anchor, QSize footprint,
|
entt::entity spawnStation(QPoint anchor, QSize footprint,
|
||||||
const std::vector<QPoint>& bodyCells,
|
const std::vector<QPoint>& bodyCells,
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ struct SchematicChoiceOption
|
|||||||
std::string schematicId;
|
std::string schematicId;
|
||||||
SchematicType type;
|
SchematicType type;
|
||||||
std::string displayName;
|
std::string displayName;
|
||||||
bool isNewUnlock;
|
|
||||||
int targetLevel;
|
|
||||||
|
|
||||||
// Display names of items produced by recipes that would newly become
|
// Display names of items produced by recipes that would newly become
|
||||||
// implicitly unlocked (REQ-LOCK-IMPLICIT) if this option is selected.
|
// implicitly unlocked (REQ-LOCK-IMPLICIT) if this option is selected.
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
struct ShipIdentityComponent
|
struct ShipIdentityComponent
|
||||||
{
|
{
|
||||||
int level;
|
|
||||||
std::string schematicId;
|
std::string schematicId;
|
||||||
// Scrap dropped on destruction, derived from the ship's as-built threat cost
|
// Scrap dropped on destruction, derived from the ship's as-built threat cost
|
||||||
// at spawn time (REQ-RES-SCRAP-DROP).
|
// at spawn time (REQ-RES-SCRAP-DROP).
|
||||||
|
|||||||
@@ -65,43 +65,31 @@ const ModuleDef* ShipSystem::findModuleDef(const std::string& id) const
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
entt::entity ShipSystem::spawn(const std::string& schematicId,
|
||||||
QVector2D position, bool isEnemy,
|
QVector2D position, bool isEnemy,
|
||||||
const std::optional<ShipLayoutConfig>& layout,
|
const std::optional<ShipLayoutConfig>& layout)
|
||||||
const std::map<std::string, int>& moduleLevelOverrides)
|
|
||||||
{
|
{
|
||||||
const ShipDef* def = findShipDef(schematicId);
|
const ShipDef* def = findShipDef(schematicId);
|
||||||
assert(def != nullptr);
|
assert(def != nullptr);
|
||||||
|
|
||||||
const double x = static_cast<double>(level);
|
|
||||||
const float tickRate = static_cast<float>(kTickRateHz);
|
const float tickRate = static_cast<float>(kTickRateHz);
|
||||||
const float tileSize = static_cast<float>(m_config.world.tileSize_m);
|
const float tileSize = static_cast<float>(m_config.world.tileSize_m);
|
||||||
|
|
||||||
float hp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
float hp = def->health.hp;
|
||||||
float maxHp = hp;
|
float maxHp = hp;
|
||||||
float maxSpeed_tpt = static_cast<float>(def->movement.speedFormula.evaluate(x))
|
float maxSpeed_tpt = def->movement.speed_mps / tileSize / tickRate;
|
||||||
/ tileSize / tickRate;
|
float mainAcceleration_tptt = def->movement.mainAcceleration_mpss / tileSize / tickRate;
|
||||||
float mainAcceleration_tptt = static_cast<float>(
|
float maneuveringAcceleration_tptt = def->movement.maneuveringAcceleration_mpss
|
||||||
def->movement.mainAccelerationFormula.evaluate(x))
|
|
||||||
/ tileSize / tickRate;
|
|
||||||
float maneuveringAcceleration_tptt = static_cast<float>(
|
|
||||||
def->movement.maneuveringAccelerationFormula.evaluate(x))
|
|
||||||
/ tileSize / tickRate;
|
/ tileSize / tickRate;
|
||||||
float maxAngularAcceleration_rptt = static_cast<float>(
|
float maxAngularAcceleration_rptt = def->movement.angularAcceleration_radpss / tickRate;
|
||||||
def->movement.angularAccelerationFormula.evaluate(x))
|
float maxRotationSpeed_rpt = def->movement.maxRotationSpeed_radps / tickRate;
|
||||||
/ tickRate;
|
float sensorRange_tiles = def->sensor.sensorRange_m / tileSize;
|
||||||
float maxRotationSpeed_rpt = static_cast<float>(
|
|
||||||
def->movement.maxRotationSpeedFormula.evaluate(x))
|
|
||||||
/ tickRate;
|
|
||||||
float sensorRange_tiles = static_cast<float>(
|
|
||||||
def->sensor.sensorRangeFormula.evaluate(x))
|
|
||||||
/ tileSize;
|
|
||||||
|
|
||||||
entt::entity entity = m_admin.spawnShip(
|
entt::entity entity = m_admin.spawnShip(
|
||||||
position, hp, maxHp,
|
position, hp, maxHp,
|
||||||
maxSpeed_tpt, mainAcceleration_tptt, maneuveringAcceleration_tptt,
|
maxSpeed_tpt, mainAcceleration_tptt, maneuveringAcceleration_tptt,
|
||||||
maxAngularAcceleration_rptt, maxRotationSpeed_rpt, sensorRange_tiles,
|
maxAngularAcceleration_rptt, maxRotationSpeed_rpt, sensorRange_tiles,
|
||||||
level, schematicId, isEnemy);
|
schematicId, isEnemy);
|
||||||
|
|
||||||
// Determine module list: configured layout takes precedence over default.
|
// Determine module list: configured layout takes precedence over default.
|
||||||
const std::vector<PlacedModule>& modules =
|
const std::vector<PlacedModule>& modules =
|
||||||
@@ -131,19 +119,12 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||||
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||||
|
|
||||||
const auto overIt = moduleLevelOverrides.find(pm.moduleId);
|
|
||||||
const double mx = static_cast<double>(
|
|
||||||
overIt != moduleLevelOverrides.end() ? overIt->second : modDef->playerProductionLevel);
|
|
||||||
|
|
||||||
if (modDef->weaponCapability)
|
if (modDef->weaponCapability)
|
||||||
{
|
{
|
||||||
WeaponComponent w;
|
WeaponComponent w;
|
||||||
w.damage = static_cast<float>(
|
w.damage = modDef->weaponCapability->damage;
|
||||||
modDef->weaponCapability->damageFormula.evaluate(mx));
|
w.range_tiles = modDef->weaponCapability->attackRange_m / tileSize;
|
||||||
w.range_tiles = static_cast<float>(
|
w.fireRateHz = modDef->weaponCapability->attackRate_hz;
|
||||||
modDef->weaponCapability->attackRangeFormula.evaluate(mx)) / tileSize;
|
|
||||||
w.fireRateHz = static_cast<float>(
|
|
||||||
modDef->weaponCapability->attackRateFormula.evaluate(mx));
|
|
||||||
w.cooldownTicks = 0.0f;
|
w.cooldownTicks = 0.0f;
|
||||||
w.currentTarget = std::nullopt;
|
w.currentTarget = std::nullopt;
|
||||||
|
|
||||||
@@ -155,12 +136,11 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
|
|
||||||
if (modDef->salvageCapability)
|
if (modDef->salvageCapability)
|
||||||
{
|
{
|
||||||
cargoCapacityBase += modDef->salvageCapability->cargoCapacityFormula.evaluate(mx);
|
cargoCapacityBase += modDef->salvageCapability->cargoCapacity;
|
||||||
|
|
||||||
SalvagerComponent salvager;
|
SalvagerComponent salvager;
|
||||||
salvager.collectionRange_tiles = static_cast<float>(
|
salvager.collectionRange_tiles = modDef->salvageCapability->collectionRange_m / tileSize;
|
||||||
modDef->salvageCapability->collectionRangeFormula.evaluate(mx)) / tileSize;
|
const double rate = modDef->salvageCapability->collectionRate_hz;
|
||||||
const double rate = modDef->salvageCapability->collectionRateFormula.evaluate(mx);
|
|
||||||
salvager.collectionIntervalTicks = (rate > 0.0)
|
salvager.collectionIntervalTicks = (rate > 0.0)
|
||||||
? static_cast<int>(kTickRateHz / rate + 0.5)
|
? static_cast<int>(kTickRateHz / rate + 0.5)
|
||||||
: 0;
|
: 0;
|
||||||
@@ -175,16 +155,13 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
if (modDef->repairCapability)
|
if (modDef->repairCapability)
|
||||||
{
|
{
|
||||||
RepairToolComponent rt;
|
RepairToolComponent rt;
|
||||||
const double repairRateHz =
|
const double repairRateHz = modDef->repairCapability->repairRate_hz;
|
||||||
modDef->repairCapability->repairRateFormula.evaluate(mx);
|
|
||||||
rt.repairIntervalTicks = (repairRateHz > 0.0)
|
rt.repairIntervalTicks = (repairRateHz > 0.0)
|
||||||
? static_cast<int>(kTickRateHz / repairRateHz + 0.5)
|
? static_cast<int>(kTickRateHz / repairRateHz + 0.5)
|
||||||
: 0;
|
: 0;
|
||||||
rt.repairAmountHp = static_cast<float>(
|
rt.repairAmountHp = modDef->repairCapability->repairAmountHp;
|
||||||
modDef->repairCapability->repairAmountHpFormula.evaluate(mx));
|
|
||||||
rt.cooldownTicksRemaining = 0;
|
rt.cooldownTicksRemaining = 0;
|
||||||
rt.range_tiles = static_cast<float>(
|
rt.range_tiles = modDef->repairCapability->repairRange_m / tileSize;
|
||||||
modDef->repairCapability->repairRangeFormula.evaluate(mx)) / tileSize;
|
|
||||||
rt.currentTarget = std::nullopt;
|
rt.currentTarget = std::nullopt;
|
||||||
|
|
||||||
entt::entity child = m_admin.createModuleEntity();
|
entt::entity child = m_admin.createModuleEntity();
|
||||||
@@ -210,13 +187,9 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||||
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||||
|
|
||||||
const auto overIt2 = moduleLevelOverrides.find(pm.moduleId);
|
|
||||||
const double mx = static_cast<double>(
|
|
||||||
overIt2 != moduleLevelOverrides.end() ? overIt2->second : modDef->playerProductionLevel);
|
|
||||||
|
|
||||||
for (const ModuleStatModifier& sm : modDef->statModifiers)
|
for (const ModuleStatModifier& sm : modDef->statModifiers)
|
||||||
{
|
{
|
||||||
const double val = sm.formula.evaluate(mx);
|
const double val = sm.value;
|
||||||
|
|
||||||
// Route modifier to the correct accumulator by stat category.
|
// Route modifier to the correct accumulator by stat category.
|
||||||
// weapon/salvage/repair stats go to the corresponding child map;
|
// weapon/salvage/repair stats go to the corresponding child map;
|
||||||
|
|||||||
@@ -18,10 +18,9 @@ class ShipSystem
|
|||||||
public:
|
public:
|
||||||
ShipSystem(const GameConfig& config, EntityAdmin& admin);
|
ShipSystem(const GameConfig& config, EntityAdmin& admin);
|
||||||
|
|
||||||
entt::entity spawn(const std::string& schematicId, int level, QVector2D position,
|
entt::entity spawn(const std::string& schematicId, QVector2D position,
|
||||||
bool isEnemy = false,
|
bool isEnemy = false,
|
||||||
const std::optional<ShipLayoutConfig>& layout = std::nullopt,
|
const std::optional<ShipLayoutConfig>& layout = std::nullopt);
|
||||||
const std::map<std::string, int>& moduleLevelOverrides = {});
|
|
||||||
void despawn(entt::entity entity);
|
void despawn(entt::entity entity);
|
||||||
|
|
||||||
// Reset all movement intents to inactive before behavior systems run.
|
// Reset all movement intents to inactive before behavior systems run.
|
||||||
|
|||||||
@@ -17,9 +17,7 @@
|
|||||||
|
|
||||||
ShipStats calculateShipStats(const GameConfig& config,
|
ShipStats calculateShipStats(const GameConfig& config,
|
||||||
const std::string& shipId,
|
const std::string& shipId,
|
||||||
int level,
|
const std::vector<PlacedModule>& modules)
|
||||||
const std::vector<PlacedModule>& modules,
|
|
||||||
const std::map<std::string, int>& moduleLevelOverrides)
|
|
||||||
{
|
{
|
||||||
ShipStats result{};
|
ShipStats result{};
|
||||||
|
|
||||||
@@ -39,24 +37,20 @@ ShipStats calculateShipStats(const GameConfig& config,
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
const double x = static_cast<double>(level);
|
|
||||||
const double tileSize = config.world.tileSize_m;
|
const double tileSize = config.world.tileSize_m;
|
||||||
|
|
||||||
// --- Base hull stats (convert from SI to display units) ------------------
|
// --- Base hull stats (convert from SI to display units) ------------------
|
||||||
result.hp = static_cast<float>(
|
result.hp = shipDef->health.hp;
|
||||||
shipDef->health.hpFormula.evaluate(x));
|
|
||||||
result.maxSpeed_tps = static_cast<float>(
|
result.maxSpeed_tps = static_cast<float>(
|
||||||
shipDef->movement.speedFormula.evaluate(x) / tileSize);
|
shipDef->movement.speed_mps / tileSize);
|
||||||
result.sensorRange_tiles = static_cast<float>(
|
result.sensorRange_tiles = static_cast<float>(
|
||||||
shipDef->sensor.sensorRangeFormula.evaluate(x) / tileSize);
|
shipDef->sensor.sensorRange_m / tileSize);
|
||||||
result.mainAcceleration_tpss = static_cast<float>(
|
result.mainAcceleration_tpss = static_cast<float>(
|
||||||
shipDef->movement.mainAccelerationFormula.evaluate(x) / tileSize);
|
shipDef->movement.mainAcceleration_mpss / tileSize);
|
||||||
result.maneuveringAcceleration_tpss = static_cast<float>(
|
result.maneuveringAcceleration_tpss = static_cast<float>(
|
||||||
shipDef->movement.maneuveringAccelerationFormula.evaluate(x) / tileSize);
|
shipDef->movement.maneuveringAcceleration_mpss / tileSize);
|
||||||
result.angularAcceleration_radpss = static_cast<float>(
|
result.angularAcceleration_radpss = shipDef->movement.angularAcceleration_radpss;
|
||||||
shipDef->movement.angularAccelerationFormula.evaluate(x));
|
result.maxRotationSpeed_radps = shipDef->movement.maxRotationSpeed_radps;
|
||||||
result.maxRotationSpeed_radps = static_cast<float>(
|
|
||||||
shipDef->movement.maxRotationSpeedFormula.evaluate(x));
|
|
||||||
|
|
||||||
// --- Pass 1: base capability stats per module instance -------------------
|
// --- Pass 1: base capability stats per module instance -------------------
|
||||||
struct WeaponInstance { float damage; float range_tiles; float rate_hz; };
|
struct WeaponInstance { float damage; float range_tiles; float rate_hz; };
|
||||||
@@ -76,33 +70,29 @@ ShipStats calculateShipStats(const GameConfig& config,
|
|||||||
const ModuleDef* def = findModuleDef(pm.moduleId);
|
const ModuleDef* def = findModuleDef(pm.moduleId);
|
||||||
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||||
|
|
||||||
const auto overIt = moduleLevelOverrides.find(pm.moduleId);
|
|
||||||
const double mx = static_cast<double>(
|
|
||||||
overIt != moduleLevelOverrides.end() ? overIt->second : def->playerProductionLevel);
|
|
||||||
|
|
||||||
if (def->weaponCapability)
|
if (def->weaponCapability)
|
||||||
{
|
{
|
||||||
WeaponInstance wi;
|
WeaponInstance wi;
|
||||||
wi.damage = static_cast<float>(def->weaponCapability->damageFormula.evaluate(mx));
|
wi.damage = def->weaponCapability->damage;
|
||||||
wi.range_tiles = static_cast<float>(def->weaponCapability->attackRangeFormula.evaluate(mx) / tileSize);
|
wi.range_tiles = static_cast<float>(def->weaponCapability->attackRange_m / tileSize);
|
||||||
wi.rate_hz = static_cast<float>(def->weaponCapability->attackRateFormula.evaluate(mx));
|
wi.rate_hz = def->weaponCapability->attackRate_hz;
|
||||||
weaponInstances.push_back(wi);
|
weaponInstances.push_back(wi);
|
||||||
}
|
}
|
||||||
if (def->salvageCapability)
|
if (def->salvageCapability)
|
||||||
{
|
{
|
||||||
cargoCapacityBase += def->salvageCapability->cargoCapacityFormula.evaluate(mx);
|
cargoCapacityBase += def->salvageCapability->cargoCapacity;
|
||||||
|
|
||||||
SalvageInstance si;
|
SalvageInstance si;
|
||||||
si.range_tiles = static_cast<float>(def->salvageCapability->collectionRangeFormula.evaluate(mx) / tileSize);
|
si.range_tiles = static_cast<float>(def->salvageCapability->collectionRange_m / tileSize);
|
||||||
si.rate = static_cast<float>(def->salvageCapability->collectionRateFormula.evaluate(mx));
|
si.rate = def->salvageCapability->collectionRate_hz;
|
||||||
salvageInstances.push_back(si);
|
salvageInstances.push_back(si);
|
||||||
}
|
}
|
||||||
if (def->repairCapability)
|
if (def->repairCapability)
|
||||||
{
|
{
|
||||||
RepairInstance ri;
|
RepairInstance ri;
|
||||||
ri.rate_hz = static_cast<float>(def->repairCapability->repairRateFormula.evaluate(mx));
|
ri.rate_hz = def->repairCapability->repairRate_hz;
|
||||||
ri.amount_hp = static_cast<float>(def->repairCapability->repairAmountHpFormula.evaluate(mx));
|
ri.amount_hp = def->repairCapability->repairAmountHp;
|
||||||
ri.range_tiles = static_cast<float>(def->repairCapability->repairRangeFormula.evaluate(mx) / tileSize);
|
ri.range_tiles = static_cast<float>(def->repairCapability->repairRange_m / tileSize);
|
||||||
repairInstances.push_back(ri);
|
repairInstances.push_back(ri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,13 +110,9 @@ ShipStats calculateShipStats(const GameConfig& config,
|
|||||||
const ModuleDef* def = findModuleDef(pm.moduleId);
|
const ModuleDef* def = findModuleDef(pm.moduleId);
|
||||||
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||||
|
|
||||||
const auto overIt = moduleLevelOverrides.find(pm.moduleId);
|
|
||||||
const double mx = static_cast<double>(
|
|
||||||
overIt != moduleLevelOverrides.end() ? overIt->second : def->playerProductionLevel);
|
|
||||||
|
|
||||||
for (const ModuleStatModifier& sm : def->statModifiers)
|
for (const ModuleStatModifier& sm : def->statModifiers)
|
||||||
{
|
{
|
||||||
const double val = sm.formula.evaluate(mx);
|
const double val = sm.value;
|
||||||
|
|
||||||
const bool isWeaponStat = (sm.stat == "damage"
|
const bool isWeaponStat = (sm.stat == "damage"
|
||||||
|| sm.stat == "attack_range"
|
|| sm.stat == "attack_range"
|
||||||
|
|||||||
@@ -51,9 +51,7 @@ struct ShipStats
|
|||||||
|
|
||||||
ShipStats calculateShipStats(const GameConfig& config,
|
ShipStats calculateShipStats(const GameConfig& config,
|
||||||
const std::string& shipId,
|
const std::string& shipId,
|
||||||
int level,
|
const std::vector<PlacedModule>& modules);
|
||||||
const std::vector<PlacedModule>& modules,
|
|
||||||
const std::map<std::string, int>& moduleLevelOverrides = {});
|
|
||||||
|
|
||||||
ShipStats buildShipStatsFromEntity(const EntityAdmin& admin, entt::entity shipEntity);
|
ShipStats buildShipStatsFromEntity(const EntityAdmin& admin, entt::entity shipEntity);
|
||||||
|
|
||||||
|
|||||||
@@ -61,13 +61,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::map<std::string, int> moduleLevels;
|
m_shipSystem->spawn(id, pos, /*isEnemy=*/false, layout);
|
||||||
for (const auto& [mId, mState] : m_moduleSchematicLevels)
|
|
||||||
{
|
|
||||||
moduleLevels[mId] = mState.level;
|
|
||||||
}
|
|
||||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout,
|
|
||||||
moduleLevels);
|
|
||||||
},
|
},
|
||||||
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
||||||
m_rng);
|
m_rng);
|
||||||
@@ -86,7 +80,6 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
|||||||
{
|
{
|
||||||
SchematicState state;
|
SchematicState state;
|
||||||
state.unlocked = (def.unlockAtStationLevel == -1);
|
state.unlocked = (def.unlockAtStationLevel == -1);
|
||||||
state.level = (def.unlockAtStationLevel == -1) ? def.schematic.playerProductionLevel : 0;
|
|
||||||
m_schematicLevels[def.id] = state;
|
m_schematicLevels[def.id] = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +88,6 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
|||||||
{
|
{
|
||||||
SchematicState state;
|
SchematicState state;
|
||||||
state.unlocked = (def.unlockAtStationLevel == -1);
|
state.unlocked = (def.unlockAtStationLevel == -1);
|
||||||
state.level = (def.unlockAtStationLevel == -1) ? def.playerProductionLevel : 0;
|
|
||||||
m_moduleSchematicLevels[def.id] = state;
|
m_moduleSchematicLevels[def.id] = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,13 +159,7 @@ void Simulation::reset(unsigned int seed)
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::map<std::string, int> moduleLevels;
|
m_shipSystem->spawn(id, pos, /*isEnemy=*/false, layout);
|
||||||
for (const auto& [mId, mState] : m_moduleSchematicLevels)
|
|
||||||
{
|
|
||||||
moduleLevels[mId] = mState.level;
|
|
||||||
}
|
|
||||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout,
|
|
||||||
moduleLevels);
|
|
||||||
},
|
},
|
||||||
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
||||||
m_rng);
|
m_rng);
|
||||||
@@ -192,7 +178,6 @@ void Simulation::reset(unsigned int seed)
|
|||||||
{
|
{
|
||||||
SchematicState state;
|
SchematicState state;
|
||||||
state.unlocked = (def.unlockAtStationLevel == -1);
|
state.unlocked = (def.unlockAtStationLevel == -1);
|
||||||
state.level = (def.unlockAtStationLevel == -1) ? def.schematic.playerProductionLevel : 0;
|
|
||||||
m_schematicLevels[def.id] = state;
|
m_schematicLevels[def.id] = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +186,6 @@ void Simulation::reset(unsigned int seed)
|
|||||||
{
|
{
|
||||||
SchematicState state;
|
SchematicState state;
|
||||||
state.unlocked = (def.unlockAtStationLevel == -1);
|
state.unlocked = (def.unlockAtStationLevel == -1);
|
||||||
state.level = (def.unlockAtStationLevel == -1) ? def.playerProductionLevel : 0;
|
|
||||||
m_moduleSchematicLevels[def.id] = state;
|
m_moduleSchematicLevels[def.id] = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,19 +627,21 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
|||||||
struct PoolEntry { std::string id; DropType type; };
|
struct PoolEntry { std::string id; DropType type; };
|
||||||
std::vector<PoolEntry> pool;
|
std::vector<PoolEntry> pool;
|
||||||
|
|
||||||
|
// Owned schematics leave the pool (REQ-DEF-SCHEMATIC-DROP): only offer
|
||||||
|
// ship/module schematics that are gated to a station level in range and not
|
||||||
|
// yet unlocked. Schematics with unlock_at_station_level -1 start unlocked, so
|
||||||
|
// they are always owned and never eligible.
|
||||||
for (const ShipDef& def : m_config.ships.ships)
|
for (const ShipDef& def : m_config.ships.ships)
|
||||||
{
|
{
|
||||||
if (def.unlockAtStationLevel == -1 || def.unlockAtStationLevel <= destroyedStationLevel)
|
if (def.unlockAtStationLevel < 0 || def.unlockAtStationLevel > destroyedStationLevel) { continue; }
|
||||||
{
|
if (m_schematicLevels.at(def.id).unlocked) { continue; }
|
||||||
pool.push_back({def.id, DropType::Ship});
|
pool.push_back({def.id, DropType::Ship});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (const ModuleDef& def : m_config.modules.modules)
|
for (const ModuleDef& def : m_config.modules.modules)
|
||||||
{
|
{
|
||||||
if (def.unlockAtStationLevel == -1 || def.unlockAtStationLevel <= destroyedStationLevel)
|
if (def.unlockAtStationLevel < 0 || def.unlockAtStationLevel > destroyedStationLevel) { continue; }
|
||||||
{
|
if (m_moduleSchematicLevels.at(def.id).unlocked) { continue; }
|
||||||
pool.push_back({def.id, DropType::Module});
|
pool.push_back({def.id, DropType::Module});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (const RecipeDef& def : m_config.recipes.recipes)
|
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||||
{
|
{
|
||||||
@@ -704,23 +690,15 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
|||||||
{
|
{
|
||||||
option.type = SchematicType::Ship;
|
option.type = SchematicType::Ship;
|
||||||
option.displayName = toDisplayName(entry.id);
|
option.displayName = toDisplayName(entry.id);
|
||||||
const SchematicState& state = m_schematicLevels.at(entry.id);
|
|
||||||
option.isNewUnlock = !state.unlocked;
|
|
||||||
option.targetLevel = state.level + 1;
|
|
||||||
}
|
}
|
||||||
else if (entry.type == DropType::Module)
|
else if (entry.type == DropType::Module)
|
||||||
{
|
{
|
||||||
option.type = SchematicType::Module;
|
option.type = SchematicType::Module;
|
||||||
option.displayName = toDisplayName(entry.id);
|
option.displayName = toDisplayName(entry.id);
|
||||||
const SchematicState& state = m_moduleSchematicLevels.at(entry.id);
|
|
||||||
option.isNewUnlock = !state.unlocked;
|
|
||||||
option.targetLevel = state.level + 1;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
option.type = SchematicType::Recipe;
|
option.type = SchematicType::Recipe;
|
||||||
option.isNewUnlock = true;
|
|
||||||
option.targetLevel = 0;
|
|
||||||
for (const RecipeDef& def : m_config.recipes.recipes)
|
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||||
{
|
{
|
||||||
if (def.id == entry.id && !def.outputs.empty())
|
if (def.id == entry.id && !def.outputs.empty())
|
||||||
@@ -736,11 +714,11 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
|||||||
std::set<std::string> hypotheticalModuleIds = currentModuleIds;
|
std::set<std::string> hypotheticalModuleIds = currentModuleIds;
|
||||||
std::set<std::string> hypotheticalRecipeSchematicIds = m_unlockedRecipeSchematicIds;
|
std::set<std::string> hypotheticalRecipeSchematicIds = m_unlockedRecipeSchematicIds;
|
||||||
|
|
||||||
if (entry.type == DropType::Ship && option.isNewUnlock)
|
if (entry.type == DropType::Ship)
|
||||||
{
|
{
|
||||||
hypotheticalShipIds.insert(entry.id);
|
hypotheticalShipIds.insert(entry.id);
|
||||||
}
|
}
|
||||||
else if (entry.type == DropType::Module && option.isNewUnlock)
|
else if (entry.type == DropType::Module)
|
||||||
{
|
{
|
||||||
hypotheticalModuleIds.insert(entry.id);
|
hypotheticalModuleIds.insert(entry.id);
|
||||||
}
|
}
|
||||||
@@ -762,8 +740,6 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
|||||||
artifactOption.schematicId = "";
|
artifactOption.schematicId = "";
|
||||||
artifactOption.type = SchematicType::Artifact;
|
artifactOption.type = SchematicType::Artifact;
|
||||||
artifactOption.displayName = "Artifact";
|
artifactOption.displayName = "Artifact";
|
||||||
artifactOption.isNewUnlock = false;
|
|
||||||
artifactOption.targetLevel = 0;
|
|
||||||
m_pendingSchematicChoices.push_back(std::move(artifactOption));
|
m_pendingSchematicChoices.push_back(std::move(artifactOption));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -794,7 +770,6 @@ void Simulation::applySchematicChoice(int choiceIndex)
|
|||||||
? m_moduleSchematicLevels.at(chosen.schematicId)
|
? m_moduleSchematicLevels.at(chosen.schematicId)
|
||||||
: m_schematicLevels.at(chosen.schematicId);
|
: m_schematicLevels.at(chosen.schematicId);
|
||||||
state.unlocked = true;
|
state.unlocked = true;
|
||||||
state.level += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
recomputeUnlocked();
|
recomputeUnlocked();
|
||||||
@@ -957,7 +932,6 @@ void Simulation::appendSchematicMap(Hasher& hasher,
|
|||||||
{
|
{
|
||||||
hasher.append(entry.first);
|
hasher.append(entry.first);
|
||||||
hasher.append(entry.second.unlocked);
|
hasher.append(entry.second.unlocked);
|
||||||
hasher.append(entry.second.level);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1050,7 +1024,6 @@ unsigned long long Simulation::computeStateChecksum() const
|
|||||||
[&hasher](entt::entity entity, const ShipIdentityComponent& c)
|
[&hasher](entt::entity entity, const ShipIdentityComponent& c)
|
||||||
{
|
{
|
||||||
hasher.append(static_cast<std::uint32_t>(entity));
|
hasher.append(static_cast<std::uint32_t>(entity));
|
||||||
hasher.append(c.level);
|
|
||||||
hasher.append(c.schematicId);
|
hasher.append(c.schematicId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1148,17 +1121,6 @@ Tick Simulation::normalGapRemainingTicks() const
|
|||||||
return m_waveSystem->normalGapRemainingTicks();
|
return m_waveSystem->normalGapRemainingTicks();
|
||||||
}
|
}
|
||||||
|
|
||||||
int Simulation::schematicLevel(const std::string& shipId) const
|
|
||||||
{
|
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
|
||||||
m_schematicLevels.find(shipId);
|
|
||||||
if (it == m_schematicLevels.end())
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return it->second.level;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Simulation::isSchematicUnlocked(const std::string& shipId) const
|
bool Simulation::isSchematicUnlocked(const std::string& shipId) const
|
||||||
{
|
{
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
const std::map<std::string, SchematicState>::const_iterator it =
|
||||||
@@ -1170,17 +1132,6 @@ bool Simulation::isSchematicUnlocked(const std::string& shipId) const
|
|||||||
return it->second.unlocked;
|
return it->second.unlocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Simulation::moduleSchematicLevel(const std::string& moduleId) const
|
|
||||||
{
|
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
|
||||||
m_moduleSchematicLevels.find(moduleId);
|
|
||||||
if (it == m_moduleSchematicLevels.end())
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return it->second.level;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
|
bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
|
||||||
{
|
{
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
const std::map<std::string, SchematicState>::const_iterator it =
|
||||||
|
|||||||
@@ -83,12 +83,10 @@ public:
|
|||||||
Tick bossCountdownTicks() const;
|
Tick bossCountdownTicks() const;
|
||||||
Tick normalGapRemainingTicks() const;
|
Tick normalGapRemainingTicks() const;
|
||||||
|
|
||||||
// Ship schematic state queries.
|
// Ship schematic state query.
|
||||||
int schematicLevel(const std::string& shipId) const;
|
|
||||||
bool isSchematicUnlocked(const std::string& shipId) const;
|
bool isSchematicUnlocked(const std::string& shipId) const;
|
||||||
|
|
||||||
// Module schematic state queries.
|
// Module schematic state query.
|
||||||
int moduleSchematicLevel(const std::string& moduleId) const;
|
|
||||||
bool isModuleSchematicUnlocked(const std::string& moduleId) const;
|
bool isModuleSchematicUnlocked(const std::string& moduleId) const;
|
||||||
|
|
||||||
// Implicit recipe/item unlock queries (REQ-LOCK-IMPLICIT).
|
// Implicit recipe/item unlock queries (REQ-LOCK-IMPLICIT).
|
||||||
@@ -182,7 +180,6 @@ private:
|
|||||||
struct SchematicState
|
struct SchematicState
|
||||||
{
|
{
|
||||||
bool unlocked;
|
bool unlocked;
|
||||||
int level;
|
|
||||||
};
|
};
|
||||||
std::map<std::string, SchematicState> m_schematicLevels;
|
std::map<std::string, SchematicState> m_schematicLevels;
|
||||||
std::map<std::string, SchematicState> m_moduleSchematicLevels;
|
std::map<std::string, SchematicState> m_moduleSchematicLevels;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ void WaveSystem::tickWaveScheduler(Tick currentTick, ShipSystem& ships,
|
|||||||
{
|
{
|
||||||
if (currentTick >= entry.spawnAt)
|
if (currentTick >= entry.spawnAt)
|
||||||
{
|
{
|
||||||
ships.spawn(entry.schematicId, entry.level, entry.position,
|
ships.spawn(entry.schematicId, entry.position,
|
||||||
/*isEnemy=*/true, entry.layout);
|
/*isEnemy=*/true, entry.layout);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -174,11 +174,7 @@ std::vector<WaveSystem::SpawnEntry> WaveSystem::selectWaveShips(double& budget,
|
|||||||
Tick currentTick,
|
Tick currentTick,
|
||||||
int worldHeightTiles)
|
int worldHeightTiles)
|
||||||
{
|
{
|
||||||
const int shipLevel = std::max(1, static_cast<int>(
|
// Build eligible ship list with their (level-independent) threat costs.
|
||||||
m_config.world.waves.shipLevelFormula.evaluate(
|
|
||||||
static_cast<double>(m_bossWaveCounter))));
|
|
||||||
|
|
||||||
// Build eligible ship list with their costs at the current level.
|
|
||||||
struct EligibleShip
|
struct EligibleShip
|
||||||
{
|
{
|
||||||
std::string schematicId;
|
std::string schematicId;
|
||||||
@@ -242,7 +238,6 @@ std::vector<WaveSystem::SpawnEntry> WaveSystem::selectWaveShips(double& budget,
|
|||||||
|
|
||||||
SpawnEntry entry;
|
SpawnEntry entry;
|
||||||
entry.schematicId = chosen.schematicId;
|
entry.schematicId = chosen.schematicId;
|
||||||
entry.level = shipLevel;
|
|
||||||
entry.spawnAt = 0; // set below after all picks are done
|
entry.spawnAt = 0; // set below after all picks are done
|
||||||
entry.position = QVector2D(xDist(m_rng),
|
entry.position = QVector2D(xDist(m_rng),
|
||||||
static_cast<float>(yDist(m_rng)) + 0.5f);
|
static_cast<float>(yDist(m_rng)) + 0.5f);
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ private:
|
|||||||
struct SpawnEntry
|
struct SpawnEntry
|
||||||
{
|
{
|
||||||
std::string schematicId;
|
std::string schematicId;
|
||||||
int level;
|
|
||||||
Tick spawnAt;
|
Tick spawnAt;
|
||||||
QVector2D position;
|
QVector2D position;
|
||||||
ShipLayoutConfig layout;
|
ShipLayoutConfig layout;
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ TEST_CASE("BehaviorSystem: clearMovementIntents resets all ships to inactive",
|
|||||||
"[behavior]")
|
"[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
f.admin.get<MovementIntentComponent>(e) = MovementIntentComponent{true, QVector2D(10.0f, 0.0f)};
|
f.admin.get<MovementIntentComponent>(e) = MovementIntentComponent{true, QVector2D(10.0f, 0.0f)};
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
@@ -254,7 +254,7 @@ TEST_CASE("BehaviorSystem: tickMovement advances ship by maxSpeed_tpt toward tar
|
|||||||
"[behavior]")
|
"[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeed_tpt;
|
const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeed_tpt;
|
||||||
f.admin.get<MovementIntentComponent>(e) = MovementIntentComponent{true, QVector2D(100.0f, 0.0f)};
|
f.admin.get<MovementIntentComponent>(e) = MovementIntentComponent{true, QVector2D(100.0f, 0.0f)};
|
||||||
@@ -269,7 +269,7 @@ TEST_CASE("BehaviorSystem: tickMovement stops exactly at target without overshoo
|
|||||||
"[behavior]")
|
"[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeed_tpt;
|
const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeed_tpt;
|
||||||
const QVector2D target(speed * 0.5f, 0.0f);
|
const QVector2D target(speed * 0.5f, 0.0f);
|
||||||
@@ -288,7 +288,7 @@ TEST_CASE("BehaviorSystem: tickMovement stops exactly at target without overshoo
|
|||||||
TEST_CASE("BehaviorSystem: healthy player ship does not retreat", "[behavior]")
|
TEST_CASE("BehaviorSystem: healthy player ship does not retreat", "[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
f.admin.get<HealthComponent>(e).hp = f.admin.get<HealthComponent>(e).maxHp; // full HP
|
f.admin.get<HealthComponent>(e).hp = f.admin.get<HealthComponent>(e).maxHp; // full HP
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
@@ -301,7 +301,7 @@ TEST_CASE("BehaviorSystem: low-HP player ship retreats toward the rally point",
|
|||||||
Fixture f;
|
Fixture f;
|
||||||
const QVector2D rallyPoint(-50.0f, 0.0f);
|
const QVector2D rallyPoint(-50.0f, 0.0f);
|
||||||
f.ships.setRallyPoint(rallyPoint);
|
f.ships.setRallyPoint(rallyPoint);
|
||||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
f.admin.get<HealthComponent>(e).hp = f.admin.get<HealthComponent>(e).maxHp * 0.2f; // below threshold
|
f.admin.get<HealthComponent>(e).hp = f.admin.get<HealthComponent>(e).maxHp * 0.2f; // below threshold
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
@@ -316,8 +316,8 @@ TEST_CASE("BehaviorSystem: low-HP retreat outranks attacking a nearby enemy", "[
|
|||||||
Fixture f;
|
Fixture f;
|
||||||
const QVector2D rallyPoint(-50.0f, 0.0f);
|
const QVector2D rallyPoint(-50.0f, 0.0f);
|
||||||
f.ships.setRallyPoint(rallyPoint);
|
f.ships.setRallyPoint(rallyPoint);
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f), /*isEnemy=*/true);
|
f.ships.spawn("interceptor", QVector2D(5.0f, 0.0f), /*isEnemy=*/true);
|
||||||
f.admin.get<HealthComponent>(player).hp = f.admin.get<HealthComponent>(player).maxHp * 0.1f;
|
f.admin.get<HealthComponent>(player).hp = f.admin.get<HealthComponent>(player).maxHp * 0.1f;
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
@@ -329,7 +329,7 @@ TEST_CASE("BehaviorSystem: low-HP retreat outranks attacking a nearby enemy", "[
|
|||||||
TEST_CASE("BehaviorSystem: enemy ships never retreat even at low HP", "[behavior]")
|
TEST_CASE("BehaviorSystem: enemy ships never retreat even at low HP", "[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
f.admin.get<HealthComponent>(enemy).hp = f.admin.get<HealthComponent>(enemy).maxHp * 0.05f;
|
f.admin.get<HealthComponent>(enemy).hp = f.admin.get<HealthComponent>(enemy).maxHp * 0.05f;
|
||||||
|
|
||||||
@@ -347,8 +347,8 @@ TEST_CASE("BehaviorSystem: player combat ship acquires nearest enemy ship in ran
|
|||||||
"[behavior]")
|
"[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f),
|
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
@@ -364,8 +364,8 @@ TEST_CASE("BehaviorSystem: player combat ship does not target friendly ships",
|
|||||||
"[behavior]")
|
"[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity e1 = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e1 = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f)); // also player
|
f.ships.spawn("interceptor", QVector2D(5.0f, 0.0f)); // also player
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
|
|
||||||
@@ -378,8 +378,8 @@ TEST_CASE("BehaviorSystem: player combat ship ignores enemy beyond engagement ra
|
|||||||
"[behavior]")
|
"[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
f.ships.spawn("interceptor", 1, QVector2D(500.0f, 0.0f), /*isEnemy=*/true);
|
f.ships.spawn("interceptor", QVector2D(500.0f, 0.0f), /*isEnemy=*/true);
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
|
|
||||||
@@ -398,10 +398,10 @@ TEST_CASE("BehaviorSystem: overclaim penalty steers a ship off a claimed target"
|
|||||||
SECTION("no claim: the nearer enemy is chosen")
|
SECTION("no claim: the nearer enemy is chosen")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
const entt::entity nearEnemy = f.ships.spawn("interceptor", 1, QVector2D(8.0f, 0.0f),
|
const entt::entity nearEnemy = f.ships.spawn("interceptor", QVector2D(8.0f, 0.0f),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
f.ships.spawn("interceptor", 1, QVector2D(0.0f, 12.0f), /*isEnemy=*/true);
|
f.ships.spawn("interceptor", QVector2D(0.0f, 12.0f), /*isEnemy=*/true);
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
|
|
||||||
@@ -412,15 +412,15 @@ TEST_CASE("BehaviorSystem: overclaim penalty steers a ship off a claimed target"
|
|||||||
{
|
{
|
||||||
const float d = 10.0f;
|
const float d = 10.0f;
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
const entt::entity enemyA = f.ships.spawn("interceptor", 1, QVector2D(d, 0.0f),
|
const entt::entity enemyA = f.ships.spawn("interceptor", QVector2D(d, 0.0f),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
const entt::entity enemyB = f.ships.spawn("interceptor", 1, QVector2D(0.0f, d),
|
const entt::entity enemyB = f.ships.spawn("interceptor", QVector2D(0.0f, d),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
// A second player ship already commits to enemyA, registering a claim, so
|
// A second player ship already commits to enemyA, registering a claim, so
|
||||||
// the penalised score of enemyA falls below the unclaimed equidistant enemyB.
|
// the penalised score of enemyA falls below the unclaimed equidistant enemyB.
|
||||||
const entt::entity claimant = f.ships.spawn("interceptor", 1, QVector2D(d, 1.0f));
|
const entt::entity claimant = f.ships.spawn("interceptor", QVector2D(d, 1.0f));
|
||||||
f.admin.get<AttackBehavior>(claimant).currentTarget = enemyA;
|
f.admin.get<AttackBehavior>(claimant).currentTarget = enemyA;
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
@@ -437,10 +437,10 @@ TEST_CASE("BehaviorSystem: hysteresis keeps a ship on its own claimed target",
|
|||||||
{
|
{
|
||||||
const float d = 10.0f;
|
const float d = 10.0f;
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
const entt::entity enemyA = f.ships.spawn("interceptor", 1, QVector2D(d, 0.0f),
|
const entt::entity enemyA = f.ships.spawn("interceptor", QVector2D(d, 0.0f),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
f.ships.spawn("interceptor", 1, QVector2D(0.0f, d), /*isEnemy=*/true);
|
f.ships.spawn("interceptor", QVector2D(0.0f, d), /*isEnemy=*/true);
|
||||||
|
|
||||||
// The ship already holds enemyA; enemyB is equidistant. Without self-exclusion
|
// The ship already holds enemyA; enemyB is equidistant. Without self-exclusion
|
||||||
// the ship's own claim would penalise enemyA and flip the choice.
|
// the ship's own claim would penalise enemyA and flip the choice.
|
||||||
@@ -459,10 +459,10 @@ TEST_CASE("BehaviorSystem: a ship switches off a heavily overclaimed target",
|
|||||||
{
|
{
|
||||||
const float d = 10.0f;
|
const float d = 10.0f;
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
const entt::entity enemyA = f.ships.spawn("interceptor", 1, QVector2D(d, 0.0f),
|
const entt::entity enemyA = f.ships.spawn("interceptor", QVector2D(d, 0.0f),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
const entt::entity enemyB = f.ships.spawn("interceptor", 1, QVector2D(0.0f, d),
|
const entt::entity enemyB = f.ships.spawn("interceptor", QVector2D(0.0f, d),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
f.admin.get<AttackBehavior>(player).currentTarget = enemyA;
|
f.admin.get<AttackBehavior>(player).currentTarget = enemyA;
|
||||||
@@ -471,7 +471,7 @@ TEST_CASE("BehaviorSystem: a ship switches off a heavily overclaimed target",
|
|||||||
for (int i = 0; i < 5; ++i)
|
for (int i = 0; i < 5; ++i)
|
||||||
{
|
{
|
||||||
const entt::entity other =
|
const entt::entity other =
|
||||||
f.ships.spawn("interceptor", 1, QVector2D(d, static_cast<float>(2 + i)));
|
f.ships.spawn("interceptor", QVector2D(d, static_cast<float>(2 + i)));
|
||||||
f.admin.get<AttackBehavior>(other).currentTarget = enemyA;
|
f.admin.get<AttackBehavior>(other).currentTarget = enemyA;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,8 +488,8 @@ TEST_CASE("BehaviorSystem: enemy ship acquires nearest player ship in range",
|
|||||||
"[behavior]")
|
"[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f),
|
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
@@ -505,7 +505,7 @@ TEST_CASE("BehaviorSystem: enemy ship with no target advances leftward",
|
|||||||
"[behavior]")
|
"[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(100.0f, 0.0f),
|
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(100.0f, 0.0f),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
@@ -526,7 +526,7 @@ TEST_CASE("BehaviorSystem: advancing ship targets center between enemy defence s
|
|||||||
f.admin.spawnStation(QPoint(1000, 10), QSize(1, 1), body, 100.0f, 100.0f, /*isEnemy=*/true);
|
f.admin.spawnStation(QPoint(1000, 10), QSize(1, 1), body, 100.0f, 100.0f, /*isEnemy=*/true);
|
||||||
f.admin.spawnStation(QPoint(1000, 30), QSize(1, 1), body, 100.0f, 100.0f, /*isEnemy=*/true);
|
f.admin.spawnStation(QPoint(1000, 30), QSize(1, 1), body, 100.0f, 100.0f, /*isEnemy=*/true);
|
||||||
|
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f),
|
||||||
/*isEnemy=*/false);
|
/*isEnemy=*/false);
|
||||||
// Player ships rally until departure; drop Rally so Advance is the fallback.
|
// Player ships rally until departure; drop Rally so Advance is the fallback.
|
||||||
f.ships.triggerRallyDeparture();
|
f.ships.triggerRallyDeparture();
|
||||||
@@ -549,7 +549,7 @@ TEST_CASE("BehaviorSystem: advancing ship falls back to enemy HQ, then off-world
|
|||||||
const QVector2D hqPos(5.0f, 7.0f);
|
const QVector2D hqPos(5.0f, 7.0f);
|
||||||
const entt::entity hq = f.admin.spawnHqProxy(hqPos, 100.0f, 100.0f);
|
const entt::entity hq = f.admin.spawnHqProxy(hqPos, 100.0f, 100.0f);
|
||||||
|
|
||||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(1000.0f, 0.0f),
|
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(1000.0f, 0.0f),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
@@ -576,9 +576,9 @@ TEST_CASE("BehaviorSystem: repair ship orbits damaged friendly ship",
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f));
|
const entt::entity friendly = f.ships.spawn("interceptor", QVector2D(5.0f, 0.0f));
|
||||||
|
|
||||||
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||||
|
|
||||||
@@ -605,10 +605,10 @@ TEST_CASE("BehaviorSystem: idle repair ship stands by with the fleet instead of
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
// A healthy ally (nothing to repair) and a far enemy station (no threat in range).
|
// A healthy ally (nothing to repair) and a far enemy station (no threat in range).
|
||||||
const entt::entity ally = f.ships.spawn("interceptor", 1, QVector2D(50.0f, 0.0f));
|
const entt::entity ally = f.ships.spawn("interceptor", QVector2D(50.0f, 0.0f));
|
||||||
const std::vector<QPoint> body{QPoint(0, 0)};
|
const std::vector<QPoint> body{QPoint(0, 0)};
|
||||||
f.admin.spawnStation(QPoint(1000, 0), QSize(1, 1), body, 100.0f, 100.0f, /*isEnemy=*/true);
|
f.admin.spawnStation(QPoint(1000, 0), QSize(1, 1), body, 100.0f, 100.0f, /*isEnemy=*/true);
|
||||||
|
|
||||||
@@ -628,8 +628,8 @@ TEST_CASE("BehaviorSystem: repair ship heals damaged ally within repair range",
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, repairLayout);
|
f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f), false, repairLayout);
|
||||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f));
|
const entt::entity friendly = f.ships.spawn("interceptor", QVector2D(1.0f, 0.0f));
|
||||||
|
|
||||||
const float initialHp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
const float initialHp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||||
f.admin.get<HealthComponent>(friendly).hp = initialHp;
|
f.admin.get<HealthComponent>(friendly).hp = initialHp;
|
||||||
@@ -644,8 +644,8 @@ TEST_CASE("BehaviorSystem: repair ship does not heal above maxHp", "[behavior]")
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, repairLayout);
|
f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f), false, repairLayout);
|
||||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f));
|
const entt::entity friendly = f.ships.spawn("interceptor", QVector2D(1.0f, 0.0f));
|
||||||
|
|
||||||
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp - 0.001f;
|
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp - 0.001f;
|
||||||
|
|
||||||
@@ -666,9 +666,9 @@ TEST_CASE("RepairSystem: tool heals the in-range damaged target chosen by the ex
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f));
|
const entt::entity friendly = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f));
|
||||||
|
|
||||||
const float initHp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
const float initHp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||||
f.admin.get<HealthComponent>(friendly).hp = initHp;
|
f.admin.get<HealthComponent>(friendly).hp = initHp;
|
||||||
@@ -688,12 +688,12 @@ TEST_CASE("RepairSystem: tool falls back to in-range target when its target is o
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
// out of repair range (80) but in sensor range (200)
|
// out of repair range (80) but in sensor range (200)
|
||||||
const entt::entity outOfRange = f.ships.spawn("interceptor", 1, QVector2D(90.0f, 0.0f));
|
const entt::entity outOfRange = f.ships.spawn("interceptor", QVector2D(90.0f, 0.0f));
|
||||||
// within repair range
|
// within repair range
|
||||||
const entt::entity fallback = f.ships.spawn("interceptor", 1, QVector2D(20.0f, 0.0f));
|
const entt::entity fallback = f.ships.spawn("interceptor", QVector2D(20.0f, 0.0f));
|
||||||
|
|
||||||
const float outInitHp = f.admin.get<HealthComponent>(outOfRange).maxHp * 0.5f;
|
const float outInitHp = f.admin.get<HealthComponent>(outOfRange).maxHp * 0.5f;
|
||||||
const float fallbackInitHp = f.admin.get<HealthComponent>(fallback).maxHp * 0.5f;
|
const float fallbackInitHp = f.admin.get<HealthComponent>(fallback).maxHp * 0.5f;
|
||||||
@@ -717,10 +717,10 @@ TEST_CASE("RepairSystem: tool falls back when its target is fully healed",
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
const entt::entity healed = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f));
|
const entt::entity healed = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f));
|
||||||
const entt::entity fallback = f.ships.spawn("interceptor", 1, QVector2D(15.0f, 0.0f));
|
const entt::entity fallback = f.ships.spawn("interceptor", QVector2D(15.0f, 0.0f));
|
||||||
|
|
||||||
f.admin.get<HealthComponent>(healed).hp = f.admin.get<HealthComponent>(healed).maxHp;
|
f.admin.get<HealthComponent>(healed).hp = f.admin.get<HealthComponent>(healed).maxHp;
|
||||||
const float fallbackInitHp = f.admin.get<HealthComponent>(fallback).maxHp * 0.5f;
|
const float fallbackInitHp = f.admin.get<HealthComponent>(fallback).maxHp * 0.5f;
|
||||||
@@ -740,10 +740,10 @@ TEST_CASE("RepairSystem: tool falls back when its target is destroyed",
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
const entt::entity gone = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f));
|
const entt::entity gone = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f));
|
||||||
const entt::entity fallback = f.ships.spawn("interceptor", 1, QVector2D(15.0f, 0.0f));
|
const entt::entity fallback = f.ships.spawn("interceptor", QVector2D(15.0f, 0.0f));
|
||||||
|
|
||||||
const float fallbackInitHp = f.admin.get<HealthComponent>(fallback).maxHp * 0.5f;
|
const float fallbackInitHp = f.admin.get<HealthComponent>(fallback).maxHp * 0.5f;
|
||||||
f.admin.get<HealthComponent>(fallback).hp = fallbackInitHp;
|
f.admin.get<HealthComponent>(fallback).hp = fallbackInitHp;
|
||||||
@@ -763,10 +763,10 @@ TEST_CASE("RepairSystem: tool target is cleared when no repairable target is in
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
// damaged but beyond repair range (80)
|
// damaged but beyond repair range (80)
|
||||||
const entt::entity outOfRange = f.ships.spawn("interceptor", 1, QVector2D(150.0f, 0.0f));
|
const entt::entity outOfRange = f.ships.spawn("interceptor", QVector2D(150.0f, 0.0f));
|
||||||
|
|
||||||
const float initHp = f.admin.get<HealthComponent>(outOfRange).maxHp * 0.5f;
|
const float initHp = f.admin.get<HealthComponent>(outOfRange).maxHp * 0.5f;
|
||||||
f.admin.get<HealthComponent>(outOfRange).hp = initHp;
|
f.admin.get<HealthComponent>(outOfRange).hp = initHp;
|
||||||
@@ -785,9 +785,9 @@ TEST_CASE("RepairSystem: two repair modules both heal the chosen target additive
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeTwoModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeTwoModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
const entt::entity targetA = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f));
|
const entt::entity targetA = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f));
|
||||||
|
|
||||||
const float initHp = f.admin.get<HealthComponent>(targetA).maxHp * 0.5f;
|
const float initHp = f.admin.get<HealthComponent>(targetA).maxHp * 0.5f;
|
||||||
f.admin.get<HealthComponent>(targetA).hp = initHp;
|
f.admin.get<HealthComponent>(targetA).hp = initHp;
|
||||||
@@ -813,10 +813,10 @@ TEST_CASE("RepairSystem: two modules both fall back and heal the same target",
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeTwoModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeTwoModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
const entt::entity healed = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f));
|
const entt::entity healed = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f));
|
||||||
const entt::entity targetB = f.ships.spawn("interceptor", 1, QVector2D(20.0f, 0.0f));
|
const entt::entity targetB = f.ships.spawn("interceptor", QVector2D(20.0f, 0.0f));
|
||||||
|
|
||||||
f.admin.get<HealthComponent>(healed).hp = f.admin.get<HealthComponent>(healed).maxHp;
|
f.admin.get<HealthComponent>(healed).hp = f.admin.get<HealthComponent>(healed).maxHp;
|
||||||
const float initHp = f.admin.get<HealthComponent>(targetB).maxHp * 0.5f;
|
const float initHp = f.admin.get<HealthComponent>(targetB).maxHp * 0.5f;
|
||||||
@@ -847,7 +847,7 @@ TEST_CASE("RepairSystem: does not crash when a tool's owner is not a repair ship
|
|||||||
Fixture f;
|
Fixture f;
|
||||||
|
|
||||||
// Bare child entity: RepairToolComponent + ModuleOwnerComponent, owner is a combat ship.
|
// Bare child entity: RepairToolComponent + ModuleOwnerComponent, owner is a combat ship.
|
||||||
const entt::entity ownerShip = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity ownerShip = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
const entt::entity moduleEntity = f.admin.createModuleEntity();
|
const entt::entity moduleEntity = f.admin.createModuleEntity();
|
||||||
RepairToolComponent rt;
|
RepairToolComponent rt;
|
||||||
rt.repairAmountHp = 1.0f;
|
rt.repairAmountHp = 1.0f;
|
||||||
@@ -868,7 +868,7 @@ TEST_CASE("RepairSystem: repair tool does not repair an HQ", "[behavior]")
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
|
|
||||||
// A damaged, same-faction HQ in repair range — spawned as a station and tagged
|
// A damaged, same-faction HQ in repair range — spawned as a station and tagged
|
||||||
@@ -890,7 +890,7 @@ TEST_CASE("RepairSystem: repair tool still repairs a damaged defence station", "
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
|
|
||||||
// A damaged, same-faction defence station (no HQ tag) in repair range.
|
// A damaged, same-faction defence station (no HQ tag) in repair range.
|
||||||
@@ -914,7 +914,7 @@ TEST_CASE("BehaviorSystem: salvage ship orbits nearest scrap", "[behavior]")
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, salvageLayout);
|
false, salvageLayout);
|
||||||
|
|
||||||
const QVector2D scrapPos(100.0f, 0.0f);
|
const QVector2D scrapPos(100.0f, 0.0f);
|
||||||
@@ -937,7 +937,7 @@ TEST_CASE("BehaviorSystem: salvage ship collects scrap on arrival", "[behavior]"
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, salvageLayout);
|
false, salvageLayout);
|
||||||
const entt::entity scrapEntity = f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
const entt::entity scrapEntity = f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
||||||
|
|
||||||
@@ -967,7 +967,7 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
|
|||||||
REQUIRE(f.buildings.findBuilding(bayId) != nullptr);
|
REQUIRE(f.buildings.findBuilding(bayId) != nullptr);
|
||||||
|
|
||||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(5.0f, 0.0f),
|
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(5.0f, 0.0f),
|
||||||
false, salvageLayout);
|
false, salvageLayout);
|
||||||
{
|
{
|
||||||
REQUIRE(f.admin.isValid(firstSalvageChild(f.admin, ship)));
|
REQUIRE(f.admin.isValid(firstSalvageChild(f.admin, ship)));
|
||||||
@@ -1001,7 +1001,7 @@ TEST_CASE("SalvagerSystem: module does not collect scrap beyond its collection r
|
|||||||
// collection_range_m_formula = "50"; scrap at distance 55 must not be collected.
|
// collection_range_m_formula = "50"; scrap at distance 55 must not be collected.
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, salvageLayout);
|
false, salvageLayout);
|
||||||
f.scraps.spawn(QVector2D(55.0f, 0.0f), 1, 100000);
|
f.scraps.spawn(QVector2D(55.0f, 0.0f), 1, 100000);
|
||||||
|
|
||||||
@@ -1016,7 +1016,7 @@ TEST_CASE("SalvagerSystem: module collects scrap within its collection range",
|
|||||||
// collection_range_m_formula = "50"; scrap at distance 45 must be collected.
|
// collection_range_m_formula = "50"; scrap at distance 45 must be collected.
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, salvageLayout);
|
false, salvageLayout);
|
||||||
f.scraps.spawn(QVector2D(45.0f, 0.0f), 1, 100000);
|
f.scraps.spawn(QVector2D(45.0f, 0.0f), 1, 100000);
|
||||||
|
|
||||||
@@ -1033,7 +1033,7 @@ TEST_CASE("SalvagerSystem: collection sets cooldown on module", "[behavior]")
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, salvageLayout);
|
false, salvageLayout);
|
||||||
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
||||||
|
|
||||||
@@ -1051,7 +1051,7 @@ TEST_CASE("SalvagerSystem: module on cooldown does not collect scrap", "[behavio
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, salvageLayout);
|
false, salvageLayout);
|
||||||
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
||||||
|
|
||||||
@@ -1066,7 +1066,7 @@ TEST_CASE("SalvagerSystem: module collects again after cooldown expires", "[beha
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, salvageLayout);
|
false, salvageLayout);
|
||||||
const entt::entity sc = firstSalvageChild(f.admin, ship);
|
const entt::entity sc = firstSalvageChild(f.admin, ship);
|
||||||
|
|
||||||
@@ -1093,7 +1093,7 @@ TEST_CASE("SalvagerSystem: two salvage modules collect independently in same tic
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig salvageLayout = makeTwoModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeTwoModuleLayout("salvager");
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, salvageLayout);
|
false, salvageLayout);
|
||||||
|
|
||||||
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
||||||
@@ -1110,7 +1110,7 @@ TEST_CASE("SalvagerSystem: second salvage module does not collect when first is
|
|||||||
// One module on cooldown, one ready: only the ready module collects.
|
// One module on cooldown, one ready: only the ready module collects.
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig salvageLayout = makeTwoModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeTwoModuleLayout("salvager");
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, salvageLayout);
|
false, salvageLayout);
|
||||||
|
|
||||||
// Put the first salvage child on cooldown.
|
// Put the first salvage child on cooldown.
|
||||||
@@ -1141,7 +1141,7 @@ TEST_CASE("SalvagerSystem: second salvage module does not collect when first is
|
|||||||
TEST_CASE("SensorRange: sensorRange is populated from config formula at spawn", "[sensor]")
|
TEST_CASE("SensorRange: sensorRange is populated from config formula at spawn", "[sensor]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
REQUIRE(f.admin.get<SensorRangeComponent>(e).value_tiles == Approx(200.0f));
|
REQUIRE(f.admin.get<SensorRangeComponent>(e).value_tiles == Approx(200.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1152,8 +1152,8 @@ TEST_CASE("SensorRange: sensorRange is populated from config formula at spawn",
|
|||||||
TEST_CASE("SensorRange: player combat ship acquires enemy just inside sensor range", "[sensor]")
|
TEST_CASE("SensorRange: player combat ship acquires enemy just inside sensor range", "[sensor]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(190.0f, 0.0f),
|
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(190.0f, 0.0f),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
@@ -1164,8 +1164,8 @@ TEST_CASE("SensorRange: player combat ship acquires enemy just inside sensor ran
|
|||||||
TEST_CASE("SensorRange: player combat ship ignores enemy just outside sensor range", "[sensor]")
|
TEST_CASE("SensorRange: player combat ship ignores enemy just outside sensor range", "[sensor]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
f.ships.spawn("interceptor", 1, QVector2D(210.0f, 0.0f), /*isEnemy=*/true);
|
f.ships.spawn("interceptor", QVector2D(210.0f, 0.0f), /*isEnemy=*/true);
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
|
|
||||||
@@ -1175,8 +1175,8 @@ TEST_CASE("SensorRange: player combat ship ignores enemy just outside sensor ran
|
|||||||
TEST_CASE("SensorRange: enemy ship ignores player just outside sensor range", "[sensor]")
|
TEST_CASE("SensorRange: enemy ship ignores player just outside sensor range", "[sensor]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(210.0f, 0.0f),
|
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(210.0f, 0.0f),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
@@ -1194,9 +1194,9 @@ TEST_CASE("SensorRange: repair ship retreats from enemy within sensor range", "[
|
|||||||
const QVector2D rallyPoint(-100.0f, 0.0f);
|
const QVector2D rallyPoint(-100.0f, 0.0f);
|
||||||
f.ships.setRallyPoint(rallyPoint);
|
f.ships.setRallyPoint(rallyPoint);
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
f.ships.spawn("interceptor", 1, QVector2D(200.0f, 0.0f), /*isEnemy=*/true);
|
f.ships.spawn("interceptor", QVector2D(200.0f, 0.0f), /*isEnemy=*/true);
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
|
|
||||||
@@ -1208,9 +1208,9 @@ TEST_CASE("SensorRange: repair ship does not retreat from enemy beyond sensor ra
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f), /*isEnemy=*/true);
|
f.ships.spawn("interceptor", QVector2D(300.0f, 0.0f), /*isEnemy=*/true);
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
|
|
||||||
@@ -1225,9 +1225,9 @@ TEST_CASE("SensorRange: repair ship does not acquire damaged ally beyond sensor
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, repairLayout);
|
false, repairLayout);
|
||||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f));
|
const entt::entity friendly = f.ships.spawn("interceptor", QVector2D(300.0f, 0.0f));
|
||||||
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
@@ -1243,7 +1243,7 @@ TEST_CASE("SensorRange: salvage ship ignores scrap beyond sensor range", "[senso
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||||
false, salvageLayout);
|
false, salvageLayout);
|
||||||
f.scraps.spawn(QVector2D(300.0f, 0.0f), 1, 100000);
|
f.scraps.spawn(QVector2D(300.0f, 0.0f), 1, 100000);
|
||||||
|
|
||||||
@@ -1261,8 +1261,8 @@ TEST_CASE("Orbit: combat ship's intent carries the target center and orbit radiu
|
|||||||
"[orbit]")
|
"[orbit]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f),
|
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
@@ -1282,7 +1282,7 @@ TEST_CASE("Orbit: rally ship orbits the rally point at the configured rally radi
|
|||||||
Fixture f;
|
Fixture f;
|
||||||
const QVector2D rallyPoint(-50.0f, 0.0f);
|
const QVector2D rallyPoint(-50.0f, 0.0f);
|
||||||
f.ships.setRallyPoint(rallyPoint);
|
f.ships.setRallyPoint(rallyPoint);
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
f.decide();
|
f.decide();
|
||||||
|
|
||||||
@@ -1298,8 +1298,8 @@ TEST_CASE("Orbit: combat ship settles near the orbit radius and circles a statio
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const QVector2D enemyPos(80.0f, 0.0f);
|
const QVector2D enemyPos(80.0f, 0.0f);
|
||||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, enemyPos, /*isEnemy=*/true);
|
const entt::entity enemy = f.ships.spawn("interceptor", enemyPos, /*isEnemy=*/true);
|
||||||
|
|
||||||
const float orbitRadius = f.admin.get<AttackBehavior>(player).orbitRadius_tiles;
|
const float orbitRadius = f.admin.get<AttackBehavior>(player).orbitRadius_tiles;
|
||||||
REQUIRE(orbitRadius > 0.0f);
|
REQUIRE(orbitRadius > 0.0f);
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ TEST_CASE("CombatSystem: ship fires when cooldown=0 and target in range", "[comb
|
|||||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
|
||||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
|
||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
|
|
||||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||||
@@ -127,8 +127,8 @@ TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]"
|
|||||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
|
||||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
|
||||||
|
|
||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
{
|
{
|
||||||
@@ -163,8 +163,8 @@ TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
|
|||||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(0.0f, 0.0f), true);
|
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(0.0f, 0.0f), true);
|
||||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(500.0f, 0.0f), false);
|
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(500.0f, 0.0f), false);
|
||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
@@ -200,7 +200,7 @@ TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]
|
|||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
const entt::entity enemyShip = sim.ships().spawn(
|
const entt::entity enemyShip = sim.ships().spawn(
|
||||||
combatDef->id, 1,
|
combatDef->id,
|
||||||
QVector2D(stationCenter.x() + 1.0f, stationCenter.y()),
|
QVector2D(stationCenter.x() + 1.0f, stationCenter.y()),
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
|
|||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
sim.ships().spawn(
|
sim.ships().spawn(
|
||||||
combatDef->id, 1,
|
combatDef->id,
|
||||||
QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
|
QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
|
||||||
/*isEnemy=*/false);
|
/*isEnemy=*/false);
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]
|
|||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
const entt::entity playerShip = sim.ships().spawn(
|
const entt::entity playerShip = sim.ships().spawn(
|
||||||
combatDef->id, 1,
|
combatDef->id,
|
||||||
QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
|
QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
|
||||||
/*isEnemy=*/false);
|
/*isEnemy=*/false);
|
||||||
|
|
||||||
@@ -304,8 +304,8 @@ TEST_CASE("CombatSystem: damage not applied before impact tick", "[combat]")
|
|||||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
|
||||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
|
||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
|
|
||||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||||
@@ -326,8 +326,8 @@ TEST_CASE("CombatSystem: damage applied exactly at impact tick", "[combat]")
|
|||||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
|
||||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
|
||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
|
|
||||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||||
@@ -345,8 +345,8 @@ TEST_CASE("CombatSystem: damage silently dropped if target already dead", "[comb
|
|||||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
|
||||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
|
||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
@@ -366,8 +366,8 @@ TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat
|
|||||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
|
||||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
|
||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
|
|
||||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||||
@@ -393,7 +393,7 @@ TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
|
|||||||
const ShipDef* combatDef = findCombatShip(sim.config());
|
const ShipDef* combatDef = findCombatShip(sim.config());
|
||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
const entt::entity ship = sim.ships().spawn(combatDef->id, 1,
|
const entt::entity ship = sim.ships().spawn(combatDef->id,
|
||||||
QVector2D(10.0f, 10.0f));
|
QVector2D(10.0f, 10.0f));
|
||||||
|
|
||||||
sim.admin().get<HealthComponent>(ship).hp = -1.0f;
|
sim.admin().get<HealthComponent>(ship).hp = -1.0f;
|
||||||
@@ -411,7 +411,7 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
|||||||
// (REQ-RES-SCRAP-DROP): round(threat * scrap_per_threat). The interceptor's
|
// (REQ-RES-SCRAP-DROP): round(threat * scrap_per_threat). The interceptor's
|
||||||
// threat is 59.0 and the test config sets scrap_per_threat = 1.0, so it drops
|
// threat is 59.0 and the test config sets scrap_per_threat = 1.0, so it drops
|
||||||
// round(59.0 * 1.0) = 59 scrap.
|
// round(59.0 * 1.0) = 59 scrap.
|
||||||
const entt::entity ship = sim.ships().spawn("interceptor", 1,
|
const entt::entity ship = sim.ships().spawn("interceptor",
|
||||||
QVector2D(10.0f, 10.0f));
|
QVector2D(10.0f, 10.0f));
|
||||||
sim.admin().get<HealthComponent>(ship).hp = -1.0f;
|
sim.admin().get<HealthComponent>(ship).hp = -1.0f;
|
||||||
|
|
||||||
|
|||||||
@@ -38,14 +38,13 @@ TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
|
|||||||
CHECK(armor.materials.size() == 1);
|
CHECK(armor.materials.size() == 1);
|
||||||
CHECK(armor.materials[0].item == "iron_ingot");
|
CHECK(armor.materials[0].item == "iron_ingot");
|
||||||
CHECK(armor.materials[0].amount == 2);
|
CHECK(armor.materials[0].amount == 2);
|
||||||
CHECK(armor.playerProductionLevel == 1);
|
|
||||||
CHECK(armor.productionTimeSeconds == Approx(3.0));
|
CHECK(armor.productionTimeSeconds == Approx(3.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);
|
||||||
CHECK(armor.statModifiers[0].stat == "hp");
|
CHECK(armor.statModifiers[0].stat == "hp");
|
||||||
CHECK(armor.statModifiers[0].modifierType == "multiplicative");
|
CHECK(armor.statModifiers[0].modifierType == "multiplicative");
|
||||||
CHECK(armor.statModifiers[0].formula.evaluate(1.0) == Approx(1.5));
|
CHECK(armor.statModifiers[0].value == Approx(1.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modules]")
|
TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modules]")
|
||||||
@@ -58,7 +57,7 @@ TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modul
|
|||||||
REQUIRE(sensor.statModifiers.size() == 1);
|
REQUIRE(sensor.statModifiers.size() == 1);
|
||||||
CHECK(sensor.statModifiers[0].stat == "sensor_range");
|
CHECK(sensor.statModifiers[0].stat == "sensor_range");
|
||||||
CHECK(sensor.statModifiers[0].modifierType == "additive");
|
CHECK(sensor.statModifiers[0].modifierType == "additive");
|
||||||
CHECK(sensor.statModifiers[0].formula.evaluate(1.0) == Approx(100.0));
|
CHECK(sensor.statModifiers[0].value == Approx(100.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (weapon_primer)", "[config][modules]")
|
TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (weapon_primer)", "[config][modules]")
|
||||||
@@ -70,7 +69,7 @@ TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (wea
|
|||||||
const ModuleStatModifier* sm = findModifier(*primer, "attack_rate");
|
const ModuleStatModifier* sm = findModifier(*primer, "attack_rate");
|
||||||
REQUIRE(sm != nullptr);
|
REQUIRE(sm != nullptr);
|
||||||
CHECK(sm->modifierType == "multiplicative");
|
CHECK(sm->modifierType == "multiplicative");
|
||||||
CHECK(sm->formula.evaluate(1.0) == Approx(1.2));
|
CHECK(sm->value == Approx(1.2));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modifiers", "[config][modules]")
|
TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modifiers", "[config][modules]")
|
||||||
@@ -83,12 +82,12 @@ TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modi
|
|||||||
const ModuleStatModifier* rangeMod = findModifier(*stab, "attack_range");
|
const ModuleStatModifier* rangeMod = findModifier(*stab, "attack_range");
|
||||||
REQUIRE(rangeMod != nullptr);
|
REQUIRE(rangeMod != nullptr);
|
||||||
CHECK(rangeMod->modifierType == "multiplicative");
|
CHECK(rangeMod->modifierType == "multiplicative");
|
||||||
CHECK(rangeMod->formula.evaluate(1.0) == Approx(1.5));
|
CHECK(rangeMod->value == Approx(1.5));
|
||||||
|
|
||||||
const ModuleStatModifier* rateMod = findModifier(*stab, "attack_rate");
|
const ModuleStatModifier* rateMod = findModifier(*stab, "attack_rate");
|
||||||
REQUIRE(rateMod != nullptr);
|
REQUIRE(rateMod != nullptr);
|
||||||
CHECK(rateMod->modifierType == "multiplicative");
|
CHECK(rateMod->modifierType == "multiplicative");
|
||||||
CHECK(rateMod->formula.evaluate(1.0) == Approx(0.8));
|
CHECK(rateMod->value == Approx(0.8));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive main_acceleration", "[config][modules]")
|
TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive main_acceleration", "[config][modules]")
|
||||||
@@ -101,12 +100,12 @@ TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive ma
|
|||||||
const ModuleStatModifier* speedMod = findModifier(*ab, "speed");
|
const ModuleStatModifier* speedMod = findModifier(*ab, "speed");
|
||||||
REQUIRE(speedMod != nullptr);
|
REQUIRE(speedMod != nullptr);
|
||||||
CHECK(speedMod->modifierType == "multiplicative");
|
CHECK(speedMod->modifierType == "multiplicative");
|
||||||
CHECK(speedMod->formula.evaluate(1.0) == Approx(1.6));
|
CHECK(speedMod->value == Approx(1.6));
|
||||||
|
|
||||||
const ModuleStatModifier* accelMod = findModifier(*ab, "main_acceleration");
|
const ModuleStatModifier* accelMod = findModifier(*ab, "main_acceleration");
|
||||||
REQUIRE(accelMod != nullptr);
|
REQUIRE(accelMod != nullptr);
|
||||||
CHECK(accelMod->modifierType == "additive");
|
CHECK(accelMod->modifierType == "additive");
|
||||||
CHECK(accelMod->formula.evaluate(1.0) == Approx(60.0));
|
CHECK(accelMod->value == Approx(60.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and additive maneuvering_acceleration", "[config][modules]")
|
TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and additive maneuvering_acceleration", "[config][modules]")
|
||||||
@@ -119,12 +118,12 @@ TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and a
|
|||||||
const ModuleStatModifier* speedMod = findModifier(*mt, "speed");
|
const ModuleStatModifier* speedMod = findModifier(*mt, "speed");
|
||||||
REQUIRE(speedMod != nullptr);
|
REQUIRE(speedMod != nullptr);
|
||||||
CHECK(speedMod->modifierType == "multiplicative");
|
CHECK(speedMod->modifierType == "multiplicative");
|
||||||
CHECK(speedMod->formula.evaluate(1.0) == Approx(1.2));
|
CHECK(speedMod->value == Approx(1.2));
|
||||||
|
|
||||||
const ModuleStatModifier* accelMod = findModifier(*mt, "maneuvering_acceleration");
|
const ModuleStatModifier* accelMod = findModifier(*mt, "maneuvering_acceleration");
|
||||||
REQUIRE(accelMod != nullptr);
|
REQUIRE(accelMod != nullptr);
|
||||||
CHECK(accelMod->modifierType == "additive");
|
CHECK(accelMod->modifierType == "additive");
|
||||||
CHECK(accelMod->formula.evaluate(1.0) == Approx(10.0));
|
CHECK(accelMod->value == Approx(10.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]")
|
TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]")
|
||||||
|
|||||||
@@ -300,13 +300,6 @@ TEST_CASE("RecipeSchematic: newlyUnlockedItemNames is sorted, deduplicated, and
|
|||||||
{
|
{
|
||||||
CHECK(opt.newlyUnlockedItemNames[j - 1] < opt.newlyUnlockedItemNames[j]);
|
CHECK(opt.newlyUnlockedItemNames[j - 1] < opt.newlyUnlockedItemNames[j]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A level-up doesn't change the explicit unlock state, so the
|
|
||||||
// implicit unlock set - and thus this preview - must be empty.
|
|
||||||
if (!opt.isNewUnlock)
|
|
||||||
{
|
|
||||||
CHECK(opt.newlyUnlockedItemNames.empty());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SimulationTestAccess::applySchematicChoice(sim, 0);
|
SimulationTestAccess::applySchematicChoice(sim, 0);
|
||||||
@@ -361,3 +354,54 @@ TEST_CASE("RecipeSchematic: newlyUnlockedItemNames matches recipes that actually
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Owned ship/module schematics leave the drop pool (REQ-DEF-SCHEMATIC-DROP)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("SchematicDrop: an owned ship schematic is never offered again",
|
||||||
|
"[recipe_schematic]")
|
||||||
|
{
|
||||||
|
// repair_ship has unlock_at_station_level = 0 in the test config, so it is
|
||||||
|
// locked at start and becomes eligible once a station set is destroyed.
|
||||||
|
Simulation sim(loadConfig());
|
||||||
|
REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship"));
|
||||||
|
|
||||||
|
bool wasUnlocked = false;
|
||||||
|
for (int i = 0; i < 200; ++i)
|
||||||
|
{
|
||||||
|
killEnemyStations(sim);
|
||||||
|
if (!sim.hasSchematicChoicesPending()) { continue; }
|
||||||
|
|
||||||
|
const std::vector<SchematicChoiceOption>& choices =
|
||||||
|
sim.getPendingSchematicChoices();
|
||||||
|
|
||||||
|
// Locate the repair_ship option, if present in this drop.
|
||||||
|
int repairShipIndex = -1;
|
||||||
|
for (int j = 0; j < static_cast<int>(choices.size()); ++j)
|
||||||
|
{
|
||||||
|
if (choices[j].schematicId == "repair_ship") { repairShipIndex = j; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once owned, it must never appear in the pool again.
|
||||||
|
if (sim.isSchematicUnlocked("repair_ship"))
|
||||||
|
{
|
||||||
|
CHECK(repairShipIndex == -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer picking repair_ship the first time it shows up so we exercise
|
||||||
|
// the transition from unowned to owned; otherwise just take choice 0.
|
||||||
|
int pick = 0;
|
||||||
|
if (repairShipIndex >= 0 && !sim.isSchematicUnlocked("repair_ship"))
|
||||||
|
{
|
||||||
|
pick = repairShipIndex;
|
||||||
|
wasUnlocked = true;
|
||||||
|
}
|
||||||
|
SimulationTestAccess::applySchematicChoice(sim, pick);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity: we actually unlocked it at some point, so the exclusion CHECK above
|
||||||
|
// was meaningfully exercised.
|
||||||
|
CHECK(wasUnlocked);
|
||||||
|
CHECK(sim.isSchematicUnlocked("repair_ship"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,11 +110,9 @@ TEST_CASE("Ship spawn: no modules leaves base stats unchanged", "[modules]")
|
|||||||
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
REQUIRE(def != nullptr);
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
const float expectedHp = static_cast<float>(def->health.hp);
|
||||||
const float expectedHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
|
||||||
|
|
||||||
const entt::entity e = sim.ships().spawn("interceptor",
|
const entt::entity e = sim.ships().spawn("interceptor",
|
||||||
def->schematic.playerProductionLevel,
|
|
||||||
QVector2D(5.0f, 5.0f), false, std::nullopt);
|
QVector2D(5.0f, 5.0f), false, std::nullopt);
|
||||||
|
|
||||||
REQUIRE(sim.admin().isValid(e));
|
REQUIRE(sim.admin().isValid(e));
|
||||||
@@ -127,8 +125,7 @@ TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
|
|||||||
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
REQUIRE(def != nullptr);
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
const float baseHp = static_cast<float>(def->health.hp);
|
||||||
const float baseHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
|
||||||
|
|
||||||
ShipLayoutConfig layout;
|
ShipLayoutConfig layout;
|
||||||
PlacedModule pm;
|
PlacedModule pm;
|
||||||
@@ -138,7 +135,6 @@ TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
|
|||||||
layout.placedModules.push_back(pm);
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
const entt::entity e = sim.ships().spawn("interceptor",
|
const entt::entity e = sim.ships().spawn("interceptor",
|
||||||
def->schematic.playerProductionLevel,
|
|
||||||
QVector2D(5.0f, 5.0f), false, layout);
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
REQUIRE(sim.admin().isValid(e));
|
REQUIRE(sim.admin().isValid(e));
|
||||||
@@ -154,9 +150,8 @@ TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
|
|||||||
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
REQUIRE(def != nullptr);
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
|
||||||
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
||||||
const float baseRange_tiles = static_cast<float>(def->sensor.sensorRangeFormula.evaluate(x)) / tileSize;
|
const float baseRange_tiles = static_cast<float>(def->sensor.sensorRange_m) / tileSize;
|
||||||
|
|
||||||
ShipLayoutConfig layout;
|
ShipLayoutConfig layout;
|
||||||
PlacedModule pm;
|
PlacedModule pm;
|
||||||
@@ -166,7 +161,6 @@ TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
|
|||||||
layout.placedModules.push_back(pm);
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
const entt::entity e = sim.ships().spawn("interceptor",
|
const entt::entity e = sim.ships().spawn("interceptor",
|
||||||
def->schematic.playerProductionLevel,
|
|
||||||
QVector2D(5.0f, 5.0f), false, layout);
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
REQUIRE(sim.admin().isValid(e));
|
REQUIRE(sim.admin().isValid(e));
|
||||||
@@ -181,8 +175,7 @@ TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
|
|||||||
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
REQUIRE(def != nullptr);
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
const float baseHp = static_cast<float>(def->health.hp);
|
||||||
const float baseHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
|
||||||
|
|
||||||
ShipLayoutConfig layout;
|
ShipLayoutConfig layout;
|
||||||
for (int i = 0; i < 2; ++i)
|
for (int i = 0; i < 2; ++i)
|
||||||
@@ -195,7 +188,6 @@ TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
|
|||||||
}
|
}
|
||||||
|
|
||||||
const entt::entity e = sim.ships().spawn("interceptor",
|
const entt::entity e = sim.ships().spawn("interceptor",
|
||||||
def->schematic.playerProductionLevel,
|
|
||||||
QVector2D(5.0f, 5.0f), false, layout);
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
REQUIRE(sim.admin().isValid(e));
|
REQUIRE(sim.admin().isValid(e));
|
||||||
@@ -321,7 +313,6 @@ TEST_CASE("Ship spawn: weapon_primer multiplies attack rate in simulation", "[mo
|
|||||||
}
|
}
|
||||||
|
|
||||||
const entt::entity ship = sim.ships().spawn("interceptor",
|
const entt::entity ship = sim.ships().spawn("interceptor",
|
||||||
def->schematic.playerProductionLevel,
|
|
||||||
QVector2D(5.0f, 5.0f), false, layout);
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship);
|
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship);
|
||||||
@@ -349,7 +340,6 @@ TEST_CASE("Ship spawn: weapon_stabilizer multiplies attack range in simulation",
|
|||||||
}
|
}
|
||||||
|
|
||||||
const entt::entity ship = sim.ships().spawn("interceptor",
|
const entt::entity ship = sim.ships().spawn("interceptor",
|
||||||
def->schematic.playerProductionLevel,
|
|
||||||
QVector2D(5.0f, 5.0f), false, layout);
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship);
|
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship);
|
||||||
@@ -364,10 +354,9 @@ TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s²
|
|||||||
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
REQUIRE(def != nullptr);
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
|
||||||
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
||||||
const float tickRate = static_cast<float>(kTickRateHz);
|
const float tickRate = static_cast<float>(kTickRateHz);
|
||||||
const float base_mpss = static_cast<float>(def->movement.mainAccelerationFormula.evaluate(x));
|
const float base_mpss = static_cast<float>(def->movement.mainAcceleration_mpss);
|
||||||
|
|
||||||
ShipLayoutConfig layout;
|
ShipLayoutConfig layout;
|
||||||
PlacedModule pm;
|
PlacedModule pm;
|
||||||
@@ -377,7 +366,6 @@ TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s²
|
|||||||
layout.placedModules.push_back(pm);
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
const entt::entity e = sim.ships().spawn("interceptor",
|
const entt::entity e = sim.ships().spawn("interceptor",
|
||||||
def->schematic.playerProductionLevel,
|
|
||||||
QVector2D(5.0f, 5.0f), false, layout);
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
// added_main_acceleration_mpss = 60; same conversion as base: / tileSize / tickRate
|
// added_main_acceleration_mpss = 60; same conversion as base: / tileSize / tickRate
|
||||||
@@ -391,10 +379,9 @@ TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration i
|
|||||||
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
REQUIRE(def != nullptr);
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
|
||||||
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
||||||
const float tickRate = static_cast<float>(kTickRateHz);
|
const float tickRate = static_cast<float>(kTickRateHz);
|
||||||
const float base_mpss = static_cast<float>(def->movement.maneuveringAccelerationFormula.evaluate(x));
|
const float base_mpss = static_cast<float>(def->movement.maneuveringAcceleration_mpss);
|
||||||
|
|
||||||
ShipLayoutConfig layout;
|
ShipLayoutConfig layout;
|
||||||
PlacedModule pm;
|
PlacedModule pm;
|
||||||
@@ -404,7 +391,6 @@ TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration i
|
|||||||
layout.placedModules.push_back(pm);
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
const entt::entity e = sim.ships().spawn("interceptor",
|
const entt::entity e = sim.ships().spawn("interceptor",
|
||||||
def->schematic.playerProductionLevel,
|
|
||||||
QVector2D(5.0f, 5.0f), false, layout);
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
// added_maneuvering_acceleration_mpss = 10; same conversion as base: / tileSize / tickRate
|
// added_maneuvering_acceleration_mpss = 10; same conversion as base: / tileSize / tickRate
|
||||||
@@ -433,7 +419,7 @@ TEST_CASE("calculateShipStats: weapon_primer multiplies attack rate in stats vie
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
||||||
def->schematic.playerProductionLevel, modules);
|
modules);
|
||||||
|
|
||||||
REQUIRE(stats.weapons.has_value());
|
REQUIRE(stats.weapons.has_value());
|
||||||
// base: damage = 2, rate = 2.0 hz; weapon_primer multiplies rate by 1.2 → DPS = 2 * 2.4 = 4.8
|
// base: damage = 2, rate = 2.0 hz; weapon_primer multiplies rate by 1.2 → DPS = 2 * 2.4 = 4.8
|
||||||
@@ -459,7 +445,7 @@ TEST_CASE("calculateShipStats: weapon_stabilizer multiplies attack range in stat
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
||||||
def->schematic.playerProductionLevel, modules);
|
modules);
|
||||||
|
|
||||||
REQUIRE(stats.weapons.has_value());
|
REQUIRE(stats.weapons.has_value());
|
||||||
// base range = 50 m / tileSize; weapon_stabilizer multiplier = 1.5
|
// base range = 50 m / tileSize; weapon_stabilizer multiplier = 1.5
|
||||||
@@ -472,9 +458,8 @@ TEST_CASE("calculateShipStats: afterburner additive main_acceleration is convert
|
|||||||
const ShipDef* def = findSchematic(cfg, "interceptor");
|
const ShipDef* def = findSchematic(cfg, "interceptor");
|
||||||
REQUIRE(def != nullptr);
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
|
||||||
const float tileSize = static_cast<float>(cfg.world.tileSize_m);
|
const float tileSize = static_cast<float>(cfg.world.tileSize_m);
|
||||||
const float base_mpss = static_cast<float>(def->movement.mainAccelerationFormula.evaluate(x));
|
const float base_mpss = static_cast<float>(def->movement.mainAcceleration_mpss);
|
||||||
|
|
||||||
std::vector<PlacedModule> modules;
|
std::vector<PlacedModule> modules;
|
||||||
PlacedModule pm;
|
PlacedModule pm;
|
||||||
@@ -484,7 +469,7 @@ TEST_CASE("calculateShipStats: afterburner additive main_acceleration is convert
|
|||||||
modules.push_back(pm);
|
modules.push_back(pm);
|
||||||
|
|
||||||
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
||||||
def->schematic.playerProductionLevel, modules);
|
modules);
|
||||||
|
|
||||||
// added_main_acceleration_mpss = 60; converted to tiles/s²: / tileSize
|
// added_main_acceleration_mpss = 60; converted to tiles/s²: / tileSize
|
||||||
const float expected = (base_mpss + 60.0f) / tileSize;
|
const float expected = (base_mpss + 60.0f) / tileSize;
|
||||||
@@ -497,9 +482,8 @@ TEST_CASE("calculateShipStats: maneuvering_thrusters additive maneuvering_accele
|
|||||||
const ShipDef* def = findSchematic(cfg, "interceptor");
|
const ShipDef* def = findSchematic(cfg, "interceptor");
|
||||||
REQUIRE(def != nullptr);
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
|
||||||
const float tileSize = static_cast<float>(cfg.world.tileSize_m);
|
const float tileSize = static_cast<float>(cfg.world.tileSize_m);
|
||||||
const float base_mpss = static_cast<float>(def->movement.maneuveringAccelerationFormula.evaluate(x));
|
const float base_mpss = static_cast<float>(def->movement.maneuveringAcceleration_mpss);
|
||||||
|
|
||||||
std::vector<PlacedModule> modules;
|
std::vector<PlacedModule> modules;
|
||||||
PlacedModule pm;
|
PlacedModule pm;
|
||||||
@@ -509,7 +493,7 @@ TEST_CASE("calculateShipStats: maneuvering_thrusters additive maneuvering_accele
|
|||||||
modules.push_back(pm);
|
modules.push_back(pm);
|
||||||
|
|
||||||
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
||||||
def->schematic.playerProductionLevel, modules);
|
modules);
|
||||||
|
|
||||||
// added_maneuvering_acceleration_mpss = 10; converted to tiles/s²: / tileSize
|
// added_maneuvering_acceleration_mpss = 10; converted to tiles/s²: / tileSize
|
||||||
const float expected = (base_mpss + 10.0f) / tileSize;
|
const float expected = (base_mpss + 10.0f) / tileSize;
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ TEST_CASE("ShipSystem: interceptor spawn has weapon child and attack behavior, n
|
|||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
REQUIRE(admin.isValid(e));
|
REQUIRE(admin.isValid(e));
|
||||||
REQUIRE(admin.isValid(firstWeaponChild(admin, e)));
|
REQUIRE(admin.isValid(firstWeaponChild(admin, e)));
|
||||||
@@ -118,7 +118,7 @@ TEST_CASE("ShipSystem: enemy combat ship has no rally or retreat behavior", "[sh
|
|||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f), /*isEnemy=*/true);
|
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f), /*isEnemy=*/true);
|
||||||
|
|
||||||
REQUIRE(admin.hasAll<AttackBehavior>(e));
|
REQUIRE(admin.hasAll<AttackBehavior>(e));
|
||||||
REQUIRE(admin.hasAll<AdvanceBehavior>(e));
|
REQUIRE(admin.hasAll<AdvanceBehavior>(e));
|
||||||
@@ -133,7 +133,7 @@ TEST_CASE("ShipSystem: setRetreatEnabled(false) suppresses player retreat behavi
|
|||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
ss.setRetreatEnabled(false);
|
ss.setRetreatEnabled(false);
|
||||||
|
|
||||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
// Other player behaviors are unaffected; only retreat is suppressed.
|
// Other player behaviors are unaffected; only retreat is suppressed.
|
||||||
REQUIRE(admin.hasAll<AttackBehavior>(e));
|
REQUIRE(admin.hasAll<AttackBehavior>(e));
|
||||||
@@ -147,7 +147,7 @@ TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]
|
|||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
// hp_formula = "40 + 5*x" at x=1 → 45
|
// hp_formula = "40 + 5*x" at x=1 → 45
|
||||||
REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(45.0f));
|
REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(45.0f));
|
||||||
@@ -163,28 +163,28 @@ TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]
|
|||||||
REQUIRE(admin.get<WeaponComponent>(wc).cooldownTicks == Approx(0.0f));
|
REQUIRE(admin.get<WeaponComponent>(wc).cooldownTicks == Approx(0.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]")
|
TEST_CASE("ShipSystem: interceptor hp matches config value", "[ship]")
|
||||||
{
|
{
|
||||||
EntityAdmin admin;
|
EntityAdmin admin;
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const entt::entity e = ss.spawn("interceptor", 5, QVector2D(0.0f, 0.0f));
|
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
// hp_formula = "40 + 5*x" at x=5 → 65
|
// interceptor hp = 45 (plain value) in the test config
|
||||||
REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(65.0f));
|
REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(45.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ShipSystem: interceptor level 0 maxSpeed_tpt matches formula / tileSize / kTickRateHz", "[ship]")
|
TEST_CASE("ShipSystem: interceptor maxSpeed_tpt matches config value / tileSize / kTickRateHz", "[ship]")
|
||||||
{
|
{
|
||||||
EntityAdmin admin;
|
EntityAdmin admin;
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const entt::entity e = ss.spawn("interceptor", 0, QVector2D(0.0f, 0.0f));
|
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
// speed_mps_formula = "2000 + 50*x" m/s at x=0 → 2000 m/s; maxSpeed_tpt = 2000/(10*30)
|
// speed_mps = 2050 m/s in the test config; maxSpeed_tpt = 2050/(10*30)
|
||||||
const float expected = 2000.0f / 10.0f / static_cast<float>(kTickRateHz);
|
const float expected = 2050.0f / 10.0f / static_cast<float>(kTickRateHz);
|
||||||
REQUIRE(admin.get<DynamicBodyComponent>(e).maxSpeed_tpt == Approx(expected));
|
REQUIRE(admin.get<DynamicBodyComponent>(e).maxSpeed_tpt == Approx(expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ TEST_CASE("ShipSystem: salvage_ship spawn with salvage module has cargo child an
|
|||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const ShipLayoutConfig layout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig layout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
const entt::entity e = ss.spawn("salvage_ship", QVector2D(0.0f, 0.0f), false, layout);
|
||||||
|
|
||||||
REQUIRE(admin.isValid(firstSalvageChild(admin, e)));
|
REQUIRE(admin.isValid(firstSalvageChild(admin, e)));
|
||||||
REQUIRE(admin.hasAll<SalvageScrapBehavior>(e));
|
REQUIRE(admin.hasAll<SalvageScrapBehavior>(e));
|
||||||
@@ -216,7 +216,7 @@ TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
|
|||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const ShipLayoutConfig layout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig layout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
const entt::entity e = ss.spawn("salvage_ship", QVector2D(0.0f, 0.0f), false, layout);
|
||||||
|
|
||||||
// salvager: cargo_capacity_formula = "10", collection_range_m_formula = "500" m → 500/10 = 50 tiles
|
// salvager: cargo_capacity_formula = "10", collection_range_m_formula = "500" m → 500/10 = 50 tiles
|
||||||
const entt::entity sc = firstSalvageChild(admin, e);
|
const entt::entity sc = firstSalvageChild(admin, e);
|
||||||
@@ -241,7 +241,7 @@ TEST_CASE("ShipSystem: repair_ship spawn with repair module has repair child and
|
|||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
const entt::entity e = ss.spawn("repair_ship", QVector2D(0.0f, 0.0f), false, layout);
|
||||||
|
|
||||||
REQUIRE(admin.isValid(firstRepairChild(admin, e)));
|
REQUIRE(admin.isValid(firstRepairChild(admin, e)));
|
||||||
REQUIRE(admin.hasAll<RepairBehavior>(e));
|
REQUIRE(admin.hasAll<RepairBehavior>(e));
|
||||||
@@ -256,7 +256,7 @@ TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas",
|
|||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool");
|
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool");
|
||||||
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
const entt::entity e = ss.spawn("repair_ship", QVector2D(0.0f, 0.0f), false, layout);
|
||||||
|
|
||||||
const entt::entity rc = firstRepairChild(admin, e);
|
const entt::entity rc = firstRepairChild(admin, e);
|
||||||
REQUIRE(admin.isValid(rc));
|
REQUIRE(admin.isValid(rc));
|
||||||
@@ -280,9 +280,9 @@ TEST_CASE("ShipSystem: spawned ships are valid entities", "[ship]")
|
|||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const entt::entity e1 = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e1 = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity e2 = ss.spawn("salvage_ship", 1, QVector2D(1.0f, 0.0f), false, salvageLayout);
|
const entt::entity e2 = ss.spawn("salvage_ship", QVector2D(1.0f, 0.0f), false, salvageLayout);
|
||||||
|
|
||||||
REQUIRE(admin.isValid(e1));
|
REQUIRE(admin.isValid(e1));
|
||||||
REQUIRE(admin.isValid(e2));
|
REQUIRE(admin.isValid(e2));
|
||||||
@@ -295,7 +295,7 @@ TEST_CASE("ShipSystem: despawn removes the ship and its weapon children", "[ship
|
|||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||||
const entt::entity wc = firstWeaponChild(admin, e);
|
const entt::entity wc = firstWeaponChild(admin, e);
|
||||||
REQUIRE(admin.isValid(e));
|
REQUIRE(admin.isValid(e));
|
||||||
REQUIRE(admin.isValid(wc));
|
REQUIRE(admin.isValid(wc));
|
||||||
|
|||||||
@@ -234,20 +234,17 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::set<std::string> unlockedModuleIds;
|
std::set<std::string> unlockedModuleIds;
|
||||||
std::map<std::string, int> moduleLevels;
|
|
||||||
for (const ModuleDef& def : m_sim->config().modules.modules)
|
for (const ModuleDef& def : m_sim->config().modules.modules)
|
||||||
{
|
{
|
||||||
if (m_sim->isModuleSchematicUnlocked(def.id))
|
if (m_sim->isModuleSchematicUnlocked(def.id))
|
||||||
{
|
{
|
||||||
unlockedModuleIds.insert(def.id);
|
unlockedModuleIds.insert(def.id);
|
||||||
}
|
}
|
||||||
moduleLevels[def.id] = m_sim->moduleSchematicLevel(def.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ShipLayoutDialog dialog(&m_sim->config(), schematicId, currentLayout,
|
ShipLayoutDialog dialog(&m_sim->config(), schematicId, currentLayout,
|
||||||
m_layoutBlueprints,
|
m_layoutBlueprints,
|
||||||
std::move(unlockedModuleIds),
|
std::move(unlockedModuleIds),
|
||||||
std::move(moduleLevels),
|
|
||||||
m_gameWorldView->isDebugDrawEnabled(),
|
m_gameWorldView->isDebugDrawEnabled(),
|
||||||
this);
|
this);
|
||||||
if (dialog.exec() == QDialog::Accepted && dialog.result().has_value())
|
if (dialog.exec() == QDialog::Accepted && dialog.result().has_value())
|
||||||
|
|||||||
@@ -70,19 +70,6 @@ SchematicChoiceDialog::SchematicChoiceDialog(
|
|||||||
typeLabel->setAlignment(Qt::AlignCenter);
|
typeLabel->setAlignment(Qt::AlignCenter);
|
||||||
cardLayout->addWidget(typeLabel);
|
cardLayout->addWidget(typeLabel);
|
||||||
|
|
||||||
QString statusText;
|
|
||||||
if (option.isNewUnlock)
|
|
||||||
{
|
|
||||||
statusText = tr("New unlock");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
statusText = tr("Level up -> %1").arg(option.targetLevel);
|
|
||||||
}
|
|
||||||
QLabel* statusLabel = new QLabel(statusText, card);
|
|
||||||
statusLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
cardLayout->addWidget(statusLabel);
|
|
||||||
|
|
||||||
QLabel* unlocksHeaderLabel = new QLabel(tr("Unlocks recipes for:"), card);
|
QLabel* unlocksHeaderLabel = new QLabel(tr("Unlocks recipes for:"), card);
|
||||||
QFont unlocksHeaderFont = unlocksHeaderLabel->font();
|
QFont unlocksHeaderFont = unlocksHeaderLabel->font();
|
||||||
unlocksHeaderFont.setBold(true);
|
unlocksHeaderFont.setBold(true);
|
||||||
|
|||||||
@@ -821,9 +821,8 @@ void SelectedBuildingPanel::buildEntityShip(entt::entity entity)
|
|||||||
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity);
|
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity);
|
||||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
||||||
|
|
||||||
m_entityTitleLabel->setText(tr("Ship: %1 (Lv %2)")
|
m_entityTitleLabel->setText(tr("Ship: %1")
|
||||||
.arg(QString::fromStdString(identity.schematicId))
|
.arg(QString::fromStdString(identity.schematicId)));
|
||||||
.arg(identity.level));
|
|
||||||
m_entityTitleLabel->show();
|
m_entityTitleLabel->show();
|
||||||
|
|
||||||
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
||||||
|
|||||||
@@ -365,14 +365,12 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
|||||||
const ShipLayoutConfig& currentLayout,
|
const ShipLayoutConfig& currentLayout,
|
||||||
std::vector<ShipLayoutBlueprint>& allBlueprints,
|
std::vector<ShipLayoutBlueprint>& allBlueprints,
|
||||||
std::set<std::string> unlockedModuleIds,
|
std::set<std::string> unlockedModuleIds,
|
||||||
std::map<std::string, int> moduleLevels,
|
|
||||||
bool debugDraw,
|
bool debugDraw,
|
||||||
QWidget* parent)
|
QWidget* parent)
|
||||||
: QDialog(parent)
|
: QDialog(parent)
|
||||||
, m_config(config)
|
, m_config(config)
|
||||||
, m_shipId(shipId)
|
, m_shipId(shipId)
|
||||||
, m_unlockedModuleIds(std::move(unlockedModuleIds))
|
, m_unlockedModuleIds(std::move(unlockedModuleIds))
|
||||||
, m_moduleLevels(std::move(moduleLevels))
|
|
||||||
, m_rows(0)
|
, m_rows(0)
|
||||||
, m_cols(0)
|
, m_cols(0)
|
||||||
, m_placedModules(currentLayout.placedModules)
|
, m_placedModules(currentLayout.placedModules)
|
||||||
@@ -699,16 +697,7 @@ void ShipLayoutDialog::updateGridWidget()
|
|||||||
|
|
||||||
void ShipLayoutDialog::updateStats()
|
void ShipLayoutDialog::updateStats()
|
||||||
{
|
{
|
||||||
int level = 1;
|
m_statsPanel->refresh(m_shipId, m_placedModules);
|
||||||
for (const ShipDef& def : m_config->ships.ships)
|
|
||||||
{
|
|
||||||
if (def.id == m_shipId)
|
|
||||||
{
|
|
||||||
level = def.schematic.playerProductionLevel;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_statsPanel->refresh(m_shipId, level, m_placedModules, m_moduleLevels);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShipLayoutDialog::canPlaceModule(const ModuleDef& def, QPoint position,
|
bool ShipLayoutDialog::canPlaceModule(const ModuleDef& def, QPoint position,
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ public:
|
|||||||
const ShipLayoutConfig& currentLayout,
|
const ShipLayoutConfig& currentLayout,
|
||||||
std::vector<ShipLayoutBlueprint>& allBlueprints,
|
std::vector<ShipLayoutBlueprint>& allBlueprints,
|
||||||
std::set<std::string> unlockedModuleIds,
|
std::set<std::string> unlockedModuleIds,
|
||||||
std::map<std::string, int> moduleLevels,
|
|
||||||
bool debugDraw,
|
bool debugDraw,
|
||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
@@ -63,7 +62,6 @@ private:
|
|||||||
const GameConfig* m_config;
|
const GameConfig* m_config;
|
||||||
std::string m_shipId;
|
std::string m_shipId;
|
||||||
std::set<std::string> m_unlockedModuleIds;
|
std::set<std::string> m_unlockedModuleIds;
|
||||||
std::map<std::string, int> m_moduleLevels;
|
|
||||||
std::vector<std::string> m_shipLayout;
|
std::vector<std::string> m_shipLayout;
|
||||||
int m_rows;
|
int m_rows;
|
||||||
int m_cols;
|
int m_cols;
|
||||||
|
|||||||
@@ -116,12 +116,9 @@ ShipStatsPanel::ShipStatsPanel(const GameConfig* config, QWidget* parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ShipStatsPanel::refresh(const std::string& shipId,
|
void ShipStatsPanel::refresh(const std::string& shipId,
|
||||||
int level,
|
const std::vector<PlacedModule>& modules)
|
||||||
const std::vector<PlacedModule>& modules,
|
|
||||||
const std::map<std::string, int>& moduleLevelOverrides)
|
|
||||||
{
|
{
|
||||||
const ShipStats stats = calculateShipStats(*m_config, shipId, level, modules,
|
const ShipStats stats = calculateShipStats(*m_config, shipId, modules);
|
||||||
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);
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ public:
|
|||||||
explicit ShipStatsPanel(const GameConfig* config, QWidget* parent = nullptr);
|
explicit ShipStatsPanel(const GameConfig* config, QWidget* parent = nullptr);
|
||||||
|
|
||||||
void refresh(const std::string& shipId,
|
void refresh(const std::string& shipId,
|
||||||
int level,
|
const std::vector<PlacedModule>& modules);
|
||||||
const std::vector<PlacedModule>& modules,
|
|
||||||
const std::map<std::string, int>& moduleLevelOverrides = {});
|
|
||||||
|
|
||||||
void refreshFromLive(const ShipStats& stats, float currentHp);
|
void refreshFromLive(const ShipStats& stats, float currentHp);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user