Compare commits
4 Commits
master
...
16c76487c0
| Author | SHA1 | Date | |
|---|---|---|---|
| 16c76487c0 | |||
| 3f746ba0f9 | |||
| ff55b4947a | |||
| 3dc503606c |
@@ -29,6 +29,7 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "laser_cannon_s_module", amount = 1}]
|
materials = [{item = "laser_cannon_s_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 0.5
|
production_time_seconds = 0.5
|
||||||
|
threat_cost = 5.0
|
||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "Ls"
|
glyph = "Ls"
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ surface_mask = [
|
|||||||
materials = [{item = "laser_cannon_m_module", amount = 1}]
|
materials = [{item = "laser_cannon_m_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
|
threat_cost = 30.0
|
||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "Lm"
|
glyph = "Lm"
|
||||||
|
|
||||||
@@ -66,6 +68,7 @@ surface_mask = [
|
|||||||
materials = [{item = "laser_cannon_l_module", amount = 1}]
|
materials = [{item = "laser_cannon_l_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 8
|
production_time_seconds = 8
|
||||||
|
threat_cost = 150.0
|
||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "Ll"
|
glyph = "Ll"
|
||||||
|
|
||||||
@@ -85,6 +88,7 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "salvager_module", amount = 1}]
|
materials = [{item = "salvager_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
|
threat_cost = 0.0
|
||||||
fill_color = "#AACC44"
|
fill_color = "#AACC44"
|
||||||
glyph = "Sv"
|
glyph = "Sv"
|
||||||
|
|
||||||
@@ -101,6 +105,7 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "repair_tool_module", amount = 1}]
|
materials = [{item = "repair_tool_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
|
threat_cost = 0.0
|
||||||
fill_color = "#66CCFF"
|
fill_color = "#66CCFF"
|
||||||
glyph = "Rp"
|
glyph = "Rp"
|
||||||
|
|
||||||
@@ -119,6 +124,7 @@ surface_mask = ["OOO"]
|
|||||||
materials = [{item = "afterburner_module", amount = 1}]
|
materials = [{item = "afterburner_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
|
threat_cost = 1.0
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "Ab"
|
glyph = "Ab"
|
||||||
|
|
||||||
@@ -134,6 +140,7 @@ surface_mask = ["OO"]
|
|||||||
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
|
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
|
threat_cost = 1.0
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "Mt"
|
glyph = "Mt"
|
||||||
|
|
||||||
@@ -152,6 +159,7 @@ surface_mask = ["OO"]
|
|||||||
materials = [{item = "armor_plates_module", amount = 1}]
|
materials = [{item = "armor_plates_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 3
|
production_time_seconds = 3
|
||||||
|
threat_cost = 20.0
|
||||||
fill_color = "#808080"
|
fill_color = "#808080"
|
||||||
glyph = "A"
|
glyph = "A"
|
||||||
|
|
||||||
@@ -166,6 +174,7 @@ surface_mask = ["OO"]
|
|||||||
materials = [{item = "sensor_booster_module", amount = 1}]
|
materials = [{item = "sensor_booster_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
|
threat_cost = 1.0
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "S"
|
glyph = "S"
|
||||||
|
|
||||||
@@ -186,6 +195,7 @@ surface_mask = [
|
|||||||
materials = [{item = "weapon_upgrade_module", amount = 1}]
|
materials = [{item = "weapon_upgrade_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
|
threat_cost = 10.0
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "Wu"
|
glyph = "Wu"
|
||||||
|
|
||||||
@@ -203,6 +213,7 @@ surface_mask = [
|
|||||||
materials = [{item = "weapon_primer_module", amount = 1}]
|
materials = [{item = "weapon_primer_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
|
threat_cost = 10.0
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "Wp"
|
glyph = "Wp"
|
||||||
|
|
||||||
@@ -220,6 +231,7 @@ surface_mask = [
|
|||||||
materials = [{item = "weapon_stabilizer_module", amount = 1}]
|
materials = [{item = "weapon_stabilizer_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
|
threat_cost = 10.0
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "Ws"
|
glyph = "Ws"
|
||||||
|
|
||||||
@@ -243,6 +255,7 @@ surface_mask = [
|
|||||||
materials = [{item = "drone_bay_module", amount = 1}]
|
materials = [{item = "drone_bay_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
|
threat_cost = 15.0
|
||||||
fill_color = "#CC66FF"
|
fill_color = "#CC66FF"
|
||||||
glyph = "Db"
|
glyph = "Db"
|
||||||
|
|
||||||
@@ -256,5 +269,6 @@ surface_mask = [
|
|||||||
materials = [{item = "drone_hangar_module", amount = 1}]
|
materials = [{item = "drone_hangar_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 20
|
production_time_seconds = 20
|
||||||
|
threat_cost = 100.0
|
||||||
fill_color = "#9933CC"
|
fill_color = "#9933CC"
|
||||||
glyph = "Dh"
|
glyph = "Dh"
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
# hangar; carrier fits one drone hangar (2x6)
|
# hangar; carrier fits one drone hangar (2x6)
|
||||||
# but no l gun (its deck rows are broken up
|
# but no l gun (its deck rows are broken up
|
||||||
# by elevator shafts)
|
# by elevator shafts)
|
||||||
|
#
|
||||||
|
# All new hulls have threat cost_formula = "0" so enemy waves do not spawn
|
||||||
|
# them until the balancing pass gives them real stats and default loadouts.
|
||||||
|
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "drone"
|
id = "drone"
|
||||||
@@ -22,10 +25,13 @@ layout = ["O"]
|
|||||||
default_modules = [{type = "laser_cannon_s", x = 0, y = 0, rotation = "east"}]
|
default_modules = [{type = "laser_cannon_s", x = 0, y = 0, rotation = "east"}]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ore", amount = 1}]
|
materials = [{item = "drone_hull", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "10"
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "3"
|
hp_formula = "3"
|
||||||
|
|
||||||
@@ -60,6 +66,9 @@ materials = [{item = "frigate_hull", amount = 1}]
|
|||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 10
|
production_time_seconds = 10
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "0"
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "30"
|
hp_formula = "30"
|
||||||
|
|
||||||
@@ -93,6 +102,9 @@ materials = [{item = "destroyer_hull", amount = 1}]
|
|||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 15
|
production_time_seconds = 15
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "0"
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "50"
|
hp_formula = "50"
|
||||||
|
|
||||||
@@ -128,6 +140,9 @@ materials = [{item = "cruiser_hull", amount = 1}]
|
|||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 25
|
production_time_seconds = 25
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "0"
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "120"
|
hp_formula = "120"
|
||||||
|
|
||||||
@@ -164,6 +179,9 @@ materials = [{item = "battlecruiser_hull", amount = 1}]
|
|||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 35
|
production_time_seconds = 35
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "0"
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "180"
|
hp_formula = "180"
|
||||||
|
|
||||||
@@ -203,6 +221,9 @@ materials = [{item = "battleship_hull", amount = 1}]
|
|||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 60
|
production_time_seconds = 60
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "0"
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "350"
|
hp_formula = "350"
|
||||||
|
|
||||||
@@ -241,6 +262,9 @@ materials = [{item = "dreadnought_hull", amount = 1}]
|
|||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 120
|
production_time_seconds = 120
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "0"
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "800"
|
hp_formula = "800"
|
||||||
|
|
||||||
@@ -278,6 +302,9 @@ materials = [{item = "carrier_hull", amount = 1}]
|
|||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 120
|
production_time_seconds = 120
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "0"
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "700"
|
hp_formula = "700"
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ tile_size_m = 10
|
|||||||
belt_speed_mps = 20
|
belt_speed_mps = 20
|
||||||
tunnel_max_distance_tiles = 10
|
tunnel_max_distance_tiles = 10
|
||||||
departure_interval_seconds = 20
|
departure_interval_seconds = 20
|
||||||
orbit_factor = 0.8
|
|
||||||
rally_orbit_radius_tiles = 5.0
|
|
||||||
|
|
||||||
[regions]
|
[regions]
|
||||||
asteroid_width_tiles = 40
|
asteroid_width_tiles = 40
|
||||||
@@ -24,11 +22,6 @@ cost_building_blocks = 200
|
|||||||
push_expand_columns_tiles = 10
|
push_expand_columns_tiles = 10
|
||||||
boss_advance_seconds = 60
|
boss_advance_seconds = 60
|
||||||
|
|
||||||
[targeting]
|
|
||||||
target_score_formula = "1 / (1 + x)" # x = distance / max weapon range; higher = better, clamped to >=0
|
|
||||||
overclaim_penalty_formula = "max(0.5, 1 - 0.1*x)" # x = competing claim count; multiplies score, clamped to [0,1]
|
|
||||||
target_hysteresis = 0.10 # keep current target unless a challenger beats it by >10%
|
|
||||||
|
|
||||||
[waves]
|
[waves]
|
||||||
threat_rate_formula = "x"
|
threat_rate_formula = "x"
|
||||||
ship_level_formula = "1"
|
ship_level_formula = "1"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ surface_mask = ["OO"]
|
|||||||
materials = [{item = "iron_ingot", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 3
|
production_time_seconds = 3
|
||||||
|
threat_cost = 2.0
|
||||||
fill_color = "#808080"
|
fill_color = "#808080"
|
||||||
glyph = "A"
|
glyph = "A"
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "circuit_board", amount = 1}]
|
materials = [{item = "circuit_board", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
|
threat_cost = 1.0
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "S"
|
glyph = "S"
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
|
threat_cost = 3.0
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "W"
|
glyph = "W"
|
||||||
|
|
||||||
@@ -44,6 +47,7 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
|
threat_cost = 5.0
|
||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "L"
|
glyph = "L"
|
||||||
|
|
||||||
@@ -59,6 +63,7 @@ surface_mask = ["OO"]
|
|||||||
materials = [{item = "iron_ingot", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
|
threat_cost = 0.0
|
||||||
fill_color = "#AACC44"
|
fill_color = "#AACC44"
|
||||||
glyph = "Sv"
|
glyph = "Sv"
|
||||||
|
|
||||||
@@ -74,6 +79,7 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "circuit_board", amount = 2}]
|
materials = [{item = "circuit_board", amount = 2}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
|
threat_cost = 0.0
|
||||||
fill_color = "#66CCFF"
|
fill_color = "#66CCFF"
|
||||||
glyph = "Rp"
|
glyph = "Rp"
|
||||||
|
|
||||||
@@ -88,6 +94,7 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
|
threat_cost = 1.0
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "Wp"
|
glyph = "Wp"
|
||||||
|
|
||||||
@@ -101,6 +108,7 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 4
|
production_time_seconds = 4
|
||||||
|
threat_cost = 1.0
|
||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "Ws"
|
glyph = "Ws"
|
||||||
|
|
||||||
@@ -115,6 +123,7 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
|
threat_cost = 1.0
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "Ab"
|
glyph = "Ab"
|
||||||
|
|
||||||
@@ -129,6 +138,7 @@ surface_mask = ["O"]
|
|||||||
materials = [{item = "iron_ingot", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
|
threat_cost = 1.0
|
||||||
fill_color = "#40A0FF"
|
fill_color = "#40A0FF"
|
||||||
glyph = "Mt"
|
glyph = "Mt"
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount
|
|||||||
player_production_level = 3
|
player_production_level = 3
|
||||||
production_time_seconds = 10
|
production_time_seconds = 10
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "5 + 1*x"
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "40 + 5*x"
|
hp_formula = "40 + 5*x"
|
||||||
|
|
||||||
@@ -37,6 +40,9 @@ materials = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount
|
|||||||
player_production_level = 5
|
player_production_level = 5
|
||||||
production_time_seconds = 20
|
production_time_seconds = 20
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "10 + 2*x"
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "120 + 15*x"
|
hp_formula = "120 + 15*x"
|
||||||
|
|
||||||
@@ -64,6 +70,9 @@ materials = [{item = "iron_ingot", amount = 4}]
|
|||||||
player_production_level = 3
|
player_production_level = 3
|
||||||
production_time_seconds = 10
|
production_time_seconds = 10
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "0"
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "40 + 4*x"
|
hp_formula = "40 + 4*x"
|
||||||
|
|
||||||
@@ -91,6 +100,9 @@ materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount
|
|||||||
player_production_level = 3
|
player_production_level = 3
|
||||||
production_time_seconds = 15
|
production_time_seconds = 15
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "0"
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp_formula = "60 + 5*x"
|
hp_formula = "60 + 5*x"
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ tile_size_m = 10
|
|||||||
belt_speed_mps = 20
|
belt_speed_mps = 20
|
||||||
tunnel_max_distance_tiles = 10
|
tunnel_max_distance_tiles = 10
|
||||||
departure_interval_seconds = 20
|
departure_interval_seconds = 20
|
||||||
orbit_factor = 0.8
|
|
||||||
rally_orbit_radius_tiles = 5.0
|
|
||||||
|
|
||||||
[regions]
|
[regions]
|
||||||
asteroid_width_tiles = 40
|
asteroid_width_tiles = 40
|
||||||
@@ -24,11 +22,6 @@ cost_building_blocks = 200
|
|||||||
push_expand_columns_tiles = 20
|
push_expand_columns_tiles = 20
|
||||||
boss_advance_seconds = 60
|
boss_advance_seconds = 60
|
||||||
|
|
||||||
[targeting]
|
|
||||||
target_score_formula = "1 / (1 + x)" # x = distance / max weapon range; higher = better, clamped to >=0
|
|
||||||
overclaim_penalty_formula = "max(0.5, 1 - 0.1*x)" # x = competing claim count; multiplies score, clamped to [0,1]
|
|
||||||
target_hysteresis = 0.10 # keep current target unless a challenger beats it by >10%
|
|
||||||
|
|
||||||
[waves]
|
[waves]
|
||||||
threat_rate_formula = "x"
|
threat_rate_formula = "x"
|
||||||
ship_level_formula = "1 + x / 10"
|
ship_level_formula = "1 + x / 10"
|
||||||
|
|||||||
@@ -52,50 +52,30 @@ See REQ-GW-COORDS for the authoritative tile-coordinate convention. This section
|
|||||||
|
|
||||||
Simulation types shared across subsystems:
|
Simulation types shared across subsystems:
|
||||||
|
|
||||||
- `EntityId` — strictly increasing integer handle, allocated centrally by the simulation. Assigned to every targetable entity: ships, scrap drops, **and** buildings (including HQ and defence stations). Buildings additionally retain their anchor tile for spatial lookups and placement; the `EntityId` is the canonical reference used by ship-component target fields (`Weapon.currentTarget`, `RepairTool.currentTarget`, `AttackBehavior.currentTarget`, etc.), so a combat ship can target either another ship or a defence station uniformly.
|
- `EntityId` — strictly increasing integer handle, allocated centrally by the simulation. Assigned to every targetable entity: ships, scrap drops, **and** buildings (including HQ and defence stations). Buildings additionally retain their anchor tile for spatial lookups and placement; the `EntityId` is the canonical reference used by ship-component target fields (`Weapon.currentTarget`, `RepairTool.currentTarget`, `ThreatResponse.currentTarget`, etc.), so a combat ship can target either another ship or a defence station uniformly.
|
||||||
- `Rotation` — enum `{ North, East, South, West }`. The rotation applied to a building's surface_mask when placed.
|
- `Rotation` — enum `{ North, East, South, West }`. The rotation applied to a building's surface_mask when placed.
|
||||||
- `BuildingType` — enum covering every building type in requirements.md (Miner, Smelter, Assembler, ReprocessingPlant, Shipyard, SalvageBay, Belt, Splitter, Hq, PlayerDefenceStation, EnemyDefenceStation). `Belt` and `Splitter` share the enum for cost, construction, placement, and `visuals.toml` lookup, but their runtime data lives inside the belt subsystem rather than in `Building` instances (see Belt Subsystem).
|
- `BuildingType` — enum covering every building type in requirements.md (Miner, Smelter, Assembler, ReprocessingPlant, Shipyard, SalvageBay, Belt, Splitter, Hq, PlayerDefenceStation, EnemyDefenceStation). `Belt` and `Splitter` share the enum for cost, construction, placement, and `visuals.toml` lookup, but their runtime data lives inside the belt subsystem rather than in `Building` instances (see Belt Subsystem).
|
||||||
- `ItemType` — tagged id of every transportable material (ores, ingots, intermediates, building_blocks, scrap).
|
- `ItemType` — tagged id of every transportable material (ores, ingots, intermediates, building_blocks, scrap).
|
||||||
- `Item` — `struct Item { ItemType type; }`. Items on belts have no persistent identity across ticks.
|
- `Item` — `struct Item { ItemType type; }`. Items on belts have no persistent identity across ticks.
|
||||||
- `Port` — `struct Port { QPoint tile; Rotation direction; }`. Identifies a belt-adjacent cell and the direction of flow across that cell.
|
- `Port` — `struct Port { QPoint tile; Rotation direction; }`. Identifies a belt-adjacent cell and the direction of flow across that cell.
|
||||||
- `MovementIntent` — `struct MovementIntent { bool active; QVector2D target; }`. Written by the winning behavior's executor (see Movement Arbitration). Cleared (`active = false`) at the start of each tick; `tickMovement` brakes when inactive, otherwise drives toward `target`.
|
- `MovementIntent` — `struct MovementIntent { int priority; QVector2D target; }`. Priority follows the order declared under Movement Arbitration. Cleared at the start of each tick; the highest-priority write wins; `tickMovement` reads the winner.
|
||||||
- `WeaponFiredEvent` — `struct WeaponFiredEvent : public Event { entt::entity shooter; entt::entity target; Tick emittedAt; }`. Transient record emitted each time a weapon fires (REQ-SHP-FIRING, REQ-SHP-FIRING-BEAM). Buffered in a sim-owned vector during the tick, then drained and re-emitted via EventManager by the UI frame handler; see Sim → UI Events.
|
- `FireEvent` — `struct FireEvent { EntityId shooter; EntityId target; Tick emittedAt; }`. Transient record emitted each time a weapon fires (REQ-SHP-FIRING, REQ-SHP-FIRING-BEAM). Buffered in a sim-owned queue and drained by the renderer; see Sim → UI Events.
|
||||||
- `SchematicChoiceOption` — `struct SchematicChoiceOption { string schematicId; SchematicType type; string displayName; bool isNewUnlock; int targetLevel; }`. Describes one option in the schematic choice dialog (REQ-DEF-SCHEMATIC-DROP). Up to three are generated when an enemy station set is destroyed. `SchematicType` is `Ship`, `Module`, or `Recipe`.
|
- `SchematicDropEvent` — `struct SchematicDropEvent { ShipSchematicId schematic; int newLevel; bool wasNewUnlock; }`. Emitted when a destroyed enemy-defence-station set awards a schematic (REQ-DEF-SCHEMATIC-DROP). The UI renders a toast (REQ-UI-SCHEMATIC-TOAST); `wasNewUnlock` chooses between the "unlocked" and "level → N" wording.
|
||||||
- `SchematicChoicesAvailableEvent` — EventManager event carrying a `vector<SchematicChoiceOption>`. Sent by the UI each frame when pending choices are detected; handled by `MainWindow` which opens the schematic choice dialog.
|
|
||||||
|
|
||||||
## Event System
|
## Sim → UI Events
|
||||||
|
|
||||||
All inter-component communication — both sim→UI and UI→UI — uses a unified `EventManager`/`EventHandler` system. No custom Qt signals/slots are used for inter-widget communication.
|
The sim owns a small set of per-frame event queues that the UI drains on each render. These carry one-shot signals that are not derivable from persistent state — currently weapon fires (REQ-SHP-FIRING-BEAM) and schematic drops (REQ-UI-SCHEMATIC-TOAST). Additional event types can be added here later (e.g., building-complete, unit-death flashes) without changing the pattern.
|
||||||
|
|
||||||
### EventManager
|
Implementation: a plain `std::vector<FireEvent>` owned by `Simulation`, one vector per event type. Combat resolution (tick-order step 8) appends to it. The UI calls `simulation.drainFireEvents()` once per rendered frame, which returns the accumulated vector by move and clears the internal one. Beams are tracked by the renderer for 0.3 s of wall time (9 ticks at 30 Hz) using the events' `emittedAt` tick, then discarded. If either the shooter or target entity is gone when the renderer looks them up, the beam is dropped early.
|
||||||
|
|
||||||
`EventManager` is a singleton (`EventManager::getInstance()`) that routes events to registered handlers.
|
We deliberately do **not** use `QObject` signals/slots or `QEvent`:
|
||||||
|
|
||||||
- `sendEventImmediately(shared_ptr<Event>)` — synchronous dispatch to all handlers of the event's type.
|
- **Determinism.** A plain ordered vector preserves tick-order exactly; the queue is part of per-tick state, inspectable in tests.
|
||||||
- `addEvent(shared_ptr<Event>)` — queues the event for later batch processing.
|
- **Sim/UI seam.** The sim exposes pull-style access only; the UI never subscribes into the sim, keeping the simulation/presentation split clean.
|
||||||
- `processEvents()` — drains the queue, dispatching each event to its handlers.
|
- **Headless testability.** Catch2 tests read the queue directly after `tick()`; no event loop, no `QApplication`.
|
||||||
|
- **Zero overhead.** Sim types remain plain structs — no `QObject`, no moc, no signal dispatch machinery.
|
||||||
|
|
||||||
The EventManager is thread-safe (mutex-guarded).
|
If the number of event types grows past a handful, we can wrap them in a small `EventQueue<T>` template, still owned by the sim. Signals/slots would only be warranted if we needed multiple independent subscribers or cross-thread dispatch, and we need neither.
|
||||||
|
|
||||||
### EventHandler
|
|
||||||
|
|
||||||
`EventHandler<T>` is a CRTP-style template that a class inherits to receive events of type `T`. It provides `registerForEvent()` / `unregisterForEvent()` and requires an override of `handleEvent(shared_ptr<const T>)`.
|
|
||||||
|
|
||||||
`CombinedEventHandler<Ts...>` is a variadic template for classes that handle multiple event types. It provides `registerForEvents()` / `unregisterForEvents()` and requires one `handleEvent` override per type.
|
|
||||||
|
|
||||||
### Sim → UI Events
|
|
||||||
|
|
||||||
The simulation layer stays free of EventManager — it uses a plain `std::vector<WeaponFiredEvent>` internally (owned by `CombatSystem`). This preserves determinism, tick-order fidelity, and headless testability (Catch2 tests read the queue directly via `drainWeaponFiredEvents()` after `tick()`).
|
|
||||||
|
|
||||||
The UI frame handler (`GameWorldView::onFrame` / `ArenaView::onFrame`) bridges the gap: each frame it calls `simulation.drainWeaponFiredEvents()`, then re-emits each `WeaponFiredEvent` via `EventManager::sendEventImmediately()`. Subscribers (the same view's `handleEvent(WeaponFiredEvent)`) create `ActiveBeam` records tracked for 0.3 s of wall time, then discarded. If either the shooter or target entity is gone when the renderer looks them up, the beam is dropped early.
|
|
||||||
|
|
||||||
Schematic drops: when an enemy station set is destroyed, the simulation generates up to 3 `SchematicChoiceOption` entries and stores them as pending state. The UI polls `hasSchematicChoicesPending()` each frame and, when true, sends a `SchematicChoicesAvailableEvent` via EventManager. `MainWindow` handles this event by pausing the game and opening a modal `SchematicChoiceDialog`. The player's selection is fed back via `applySchematicChoice(index)`.
|
|
||||||
|
|
||||||
### UI Events
|
|
||||||
|
|
||||||
All UI interactions — building selection, builder/blueprint mode transitions, speed changes, demolish mode, escape menu, layout dialog requests — are communicated via EventManager events rather than Qt signals/slots. Each event is a small struct inheriting `Event` (e.g., `SelectionChangedEvent`, `BuildingTypeSelectedEvent`, `SpeedChangeRequestedEvent`). Widgets register as `CombinedEventHandler` for the events they care about and emit events via `EventManager::sendEventImmediately()`.
|
|
||||||
|
|
||||||
Bidirectional interactions use separate request/notification event types to avoid infinite recursion (e.g., `ExitBuilderModeRequestedEvent` from `BuildButtonGrid` → `GameWorldView`, vs. `BuilderModeExitedEvent` from `GameWorldView` → `BuildButtonGrid`).
|
|
||||||
|
|
||||||
## Tick Order
|
## Tick Order
|
||||||
|
|
||||||
@@ -107,9 +87,9 @@ Within a single simulation tick, subsystems run in this fixed order. The order i
|
|||||||
4. **Building production** — advance production timers; start new cycles when inputs and output-buffer space permit (REQ-MAT-CYCLE); on completion, deposit output.
|
4. **Building production** — advance production timers; start new cycles when inputs and output-buffer space permit (REQ-MAT-CYCLE); on completion, deposit output.
|
||||||
5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT).
|
5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT).
|
||||||
6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER).
|
6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER).
|
||||||
7. **Ship behavior systems** — clear `MovementIntent` on each ship, then the `AiSystem` runs three batched phases: every behavior **evaluator** scores its behavior and sets its target data; a **selection** pass records the highest-scoring behavior per ship in `SelectedBehaviorComponent`; each behavior **executor** runs for the winner, writing `MovementIntent` and preferred module targets. The module systems then perform world mutation: `SalvagerSystem` (scrap collection/delivery) and `RepairSystem` (healing). See Movement Arbitration.
|
7. **Ship behavior systems** — clear `MovementIntent` on each ship, then run `tickThreatResponse`, `tickScrapCollector`, `tickRepairBehavior`, `tickHomeReturn` in any order (arbitration is via intent priority).
|
||||||
8. **Combat resolution** — ships and defence stations validate/acquire targets, fire, apply damage; queue deaths. Each fire appends a `WeaponFiredEvent` to the sim's weapon-fired-event queue (REQ-SHP-FIRING-BEAM).
|
8. **Combat resolution** — ships and defence stations acquire targets, fire, apply damage; queue deaths. Each fire appends a `FireEvent` to the sim's fire-event queue (REQ-SHP-FIRING-BEAM).
|
||||||
9. **Deaths & loot** — process queued deaths: drop scrap (REQ-RES-SCRAP-DROP); if a full enemy-defence-station set was destroyed this tick, generate up to 3 schematic choice options (REQ-DEF-SCHEMATIC-DROP) stored as pending state for the UI to present; remove entities.
|
9. **Deaths & loot** — process queued deaths: drop scrap (REQ-RES-SCRAP-DROP); if a full enemy-defence-station set was destroyed this tick, award one schematic (REQ-DEF-SCHEMATIC-DROP) and append a `SchematicDropEvent`; remove entities.
|
||||||
10. **`tickMovement`** — advance ship positions based on final `MovementIntent`.
|
10. **`tickMovement`** — advance ship positions based on final `MovementIntent`.
|
||||||
11. **Scrap despawn** — decrement scrap timers; remove expired scrap (REQ-RES-SCRAP-DROP).
|
11. **Scrap despawn** — decrement scrap timers; remove expired scrap (REQ-RES-SCRAP-DROP).
|
||||||
|
|
||||||
@@ -217,20 +197,16 @@ struct RepairTool { float ratePerTick; std::optional<EntityId> currentTarget;
|
|||||||
|
|
||||||
### Behavior Components
|
### Behavior Components
|
||||||
|
|
||||||
Behaviors are decomposed, not bundled into per-role monolithic AIs. This is the critical modeling choice: adding a capability (e.g., putting a `Weapon` on a repair ship) must not require rewriting AI code. Each behavior is a small component carrying its own target data plus a `float score` written by its evaluator each tick.
|
Behaviors are decomposed, not bundled into per-role monolithic AIs. This is the critical modeling choice: adding a capability (e.g., putting a `Weapon` on a repair ship) must not require rewriting AI code.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
struct AdvanceBehavior { float score; }; // baseline fallback, all ships
|
struct ThreatResponse { float engagementRange; CombatStance stance;
|
||||||
struct RallyBehavior { QVector2D rallyPoint; float score; }; // player combat ships
|
CombatTargetPriority priority;
|
||||||
struct RetreatBehavior { float retreatHpFraction; QVector2D retreatPoint; // player ships
|
std::optional<EntityId> currentTarget; };
|
||||||
float score; };
|
struct ScrapCollector { std::optional<QVector2D> scrapTarget; EntityId deliveryBay; };
|
||||||
struct AttackBehavior { std::optional<EntityId> currentTarget; float score; };
|
struct RepairBehavior { RepairTargetPriority priority;
|
||||||
struct RepairBehavior { std::optional<EntityId> currentTarget;
|
std::optional<EntityId> currentTarget; };
|
||||||
float maxRepairRange_tiles; float score; };
|
struct HomeReturn { float retreatHpFraction; QVector2D homePos; };
|
||||||
struct SalvageScrapBehavior { std::optional<QVector2D> scrapTarget;
|
|
||||||
float maxCollectionRange_tiles; float score; };
|
|
||||||
struct DeliverScrapBehavior { BuildingId deliveryBay; float score; };
|
|
||||||
struct SelectedBehaviorComponent { BehaviorKind winner; float bestScore; }; // selection result
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ship
|
### Ship
|
||||||
@@ -250,42 +226,38 @@ struct Ship {
|
|||||||
std::optional<SalvageCargo> cargo;
|
std::optional<SalvageCargo> cargo;
|
||||||
std::optional<RepairTool> repairTool;
|
std::optional<RepairTool> repairTool;
|
||||||
|
|
||||||
// Behaviors (attached per capability; AdvanceBehavior + SelectedBehaviorComponent
|
// Behaviors
|
||||||
// on every ship, RetreatBehavior on player ships, etc.)
|
std::optional<ThreatResponse> threatResponse;
|
||||||
std::optional<AttackBehavior> attackBehavior;
|
std::optional<ScrapCollector> scrapCollector;
|
||||||
std::optional<SalvageScrapBehavior> salvageScrapBehavior;
|
|
||||||
std::optional<DeliverScrapBehavior> deliverScrapBehavior;
|
|
||||||
std::optional<RepairBehavior> repairBehavior;
|
std::optional<RepairBehavior> repairBehavior;
|
||||||
|
std::optional<HomeReturn> homeReturn;
|
||||||
|
|
||||||
// Written by the winning behavior's executor, read by movement.
|
// Written by behavior systems, read by movement.
|
||||||
MovementIntent intent;
|
MovementIntent intent;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Systems
|
### Systems
|
||||||
|
|
||||||
Each behavior is split into a stateless **evaluator** and **executor** class (one per behavior, e.g. `AttackEvaluator`/`AttackExecutor`), orchestrated by `AiSystem`. Evaluators and executors only read/write behavior components and module target fields — they never mutate the game world. World mutation lives in dedicated module systems that run every tick, independent of which behavior won:
|
Each behavior has its own tick system. A system iterates a flat `std::vector<Ship>` and skips ships that do not have the relevant components.
|
||||||
|
|
||||||
- `CombatSystem` — validates each weapon's executor-set target, falls back to nearest-target acquisition, fires, applies damage.
|
- `tickThreatResponse` — requires `threatResponse` + `weapon`. Acquires target, fires, manages cooldown.
|
||||||
- `SalvagerSystem` — collects scrap into cargo and delivers full cargo at a `SalvageBay`.
|
- `tickScrapCollector` — requires `scrapCollector` + `cargo`. Flies to scrap, picks up, returns to delivery bay.
|
||||||
- `RepairSystem` — validates each repair tool's target, falls back to nearest damaged friendly, applies healing.
|
- `tickRepairBehavior` — requires `repairBehavior` + `repairTool`. Finds damaged target, moves to range, repairs.
|
||||||
- `MovementIntentSystem` (`tickMovement`) — reads `MovementIntent`, advances `position`; brakes when inactive.
|
- `tickHomeReturn` — requires `homeReturn`. Overrides movement if hp drops below threshold.
|
||||||
|
- `tickMovement` — reads `intent`, advances `position`.
|
||||||
|
|
||||||
### Movement Arbitration
|
### Movement Arbitration
|
||||||
|
|
||||||
Arbitration is **score-based**, not fixed-priority. In a single tick `AiSystem` runs three phases:
|
When multiple behaviors want to drive movement, a fixed global priority resolves the conflict. Each behavior system writes a `MovementIntent` carrying its priority; a higher-priority write overwrites a lower-priority one. `tickMovement` reads the final winner.
|
||||||
|
|
||||||
1. **Evaluate** — every behavior's evaluator iterates the ships that have its component, sets its target data, and writes a `float score` (see `BehaviorScores.h`). An evaluator returns an inactive score when its behavior does not apply.
|
Initial priority order (subject to tuning):
|
||||||
2. **Select** — `selectWinningBehaviors` resets each `SelectedBehaviorComponent`, then compares every behavior's score per ship, recording the highest as `winner`. Behaviors are considered highest-band first so a strict `>` breaks ties toward the more urgent behavior.
|
|
||||||
3. **Execute** — each behavior's executor runs only for ships where it is the `winner`, writing the single `MovementIntent` and any preferred module targets.
|
|
||||||
|
|
||||||
`AdvanceBehavior` is present on every ship with the lowest score, guaranteeing a winner. The resulting band order:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Retreat > Attack / Repair / SalvageScrap / DeliverScrap > Rally > Advance
|
HomeReturn > ThreatResponse > RepairBehavior > ScrapCollector
|
||||||
```
|
```
|
||||||
|
|
||||||
`MovementIntent` is cleared (inactive) at the start of each tick; `tickMovement` runs last.
|
`tickMovement` runs last. Intents are cleared at the start of each tick.
|
||||||
|
|
||||||
### Why Not ECS
|
### Why Not ECS
|
||||||
|
|
||||||
@@ -308,7 +280,7 @@ The game world is rendered by a single `GameWorldView` widget that inherits `QOp
|
|||||||
|
|
||||||
### Threading
|
### Threading
|
||||||
|
|
||||||
Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly without locks. If profiling later justifies moving the sim to a worker thread, the pull-style `drainWeaponFiredEvents()` / `getPendingSchematicChoices()` / `applySchematicChoice()` / `forEachVisualItem()` APIs already support a clean snapshot-and-render split; a single mutex at the sim boundary would suffice. The `ArenaSimulation` used by the balancing tool runs headlessly on a worker thread; fire events accumulate in its internal vector and are only drained when `ArenaView` drives `tickOnce()` on the main thread during interactive inspection.
|
Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly without locks. If profiling later justifies moving the sim to a worker thread, the pull-style `drainFireEvents()` / `drainSchematicDropEvents()` / `forEachVisualItem()` APIs already support a clean snapshot-and-render split; a single mutex at the sim boundary would suffice.
|
||||||
|
|
||||||
### Layer Order (back to front)
|
### Layer Order (back to front)
|
||||||
|
|
||||||
@@ -317,9 +289,9 @@ Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly
|
|||||||
3. **Belt items** — 10×10 colored squares emitted by `BeltSystem::forEachVisualItem`.
|
3. **Belt items** — 10×10 colored squares emitted by `BeltSystem::forEachVisualItem`.
|
||||||
4. **Scrap** — glyphs at world positions.
|
4. **Scrap** — glyphs at world positions.
|
||||||
5. **Ships** — colored arrows oriented by velocity; color keyed to role (player combat / salvage / repair / enemy).
|
5. **Ships** — colored arrows oriented by velocity; color keyed to role (player combat / salvage / repair / enemy).
|
||||||
6. **Laser beams** — lines derived from live `WeaponFiredEvent`s kept by the renderer for 0.3 s (REQ-SHP-FIRING-BEAM).
|
6. **Laser beams** — lines derived from live `FireEvent`s kept by the renderer for 0.3 s (REQ-SHP-FIRING-BEAM).
|
||||||
7. **Build overlays** — ghost in builder mode (REQ-BLD-GHOST), demolish-mode tint, tile highlight under cursor, box-drag selection rectangle.
|
7. **Build overlays** — ghost in builder mode (REQ-BLD-GHOST), demolish-mode tint, tile highlight under cursor, box-drag selection rectangle.
|
||||||
8. **Screen-space UI** — screen-anchored elements, drawn after resetting the world-space transform.
|
8. **Screen-space UI** — schematic toasts (REQ-UI-SCHEMATIC-TOAST) and any other screen-anchored elements, drawn after resetting the world-space transform.
|
||||||
|
|
||||||
### Coordinates and Scrolling
|
### Coordinates and Scrolling
|
||||||
|
|
||||||
|
|||||||
@@ -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, and combat target-selection parameters (target score formula, overclaim penalty formula, target hysteresis).
|
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, boss wave timing, enemy ship level formula, belt speed, starting building blocks, departure interval.
|
||||||
- **buildings.toml** — building block cost and construction time per building type.
|
- **buildings.toml** — building block cost and construction time per building type.
|
||||||
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. Assembler recipe entries may optionally define `unlock_at_station_level` (integer): -1 means the recipe is explicitly unlocked at game start; a value ≥ 0 means the recipe starts locked and a schematic for it can be awarded via defence station destruction (see REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP).
|
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. Assembler recipe entries may optionally define `unlock_at_station_level` (integer): -1 means the recipe is explicitly unlocked at game start; a value ≥ 0 means the recipe starts locked and a schematic for it can be awarded via defence station destruction (see REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP).
|
||||||
- **ships.toml** — per schematic: a human-readable display name (used in the UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as formulas of ship level, required build materials, player production level, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the schematic already unlocked), a layout grid defining the ship's module slots, a `scrap_drop` loot value, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
|
- **ships.toml** — per schematic: a human-readable display name (used in toasts and UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as formulas of ship level, required build materials, threat cost formula, player production level, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the schematic already unlocked), a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
|
||||||
- **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, initial player production level, production time, threat cost, fill color, glyph, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the module schematic already unlocked), and an optional capability section and/or stat modifier formulas. A module with a capability section (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas is a **capability module** that grants the ship a weapon, salvage bay, or repair tool per instance (see REQ-MOD-CONFIG for the full list of formulas per capability type). A module with only `added_*`/`multiplied_*` formulas is a **passive module** that modifies stats on the ship or on capability module instances (see REQ-MOD-STAT-CALC).
|
||||||
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
|
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
|
||||||
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship schematic, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
|
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship schematic, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
|
||||||
- **ship_layouts.toml** — named layout blueprints per ship type; written and read by the application to persist the layout blueprint panel (REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD). Not a game parameter file; the simulation does not read it.
|
- **ship_layouts.toml** — named layout blueprints per ship type; written and read by the application to persist the layout blueprint panel (REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD). Not a game parameter file; the simulation does not read it.
|
||||||
@@ -153,36 +153,23 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- 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_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-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.
|
|
||||||
- **Repair** (REQ-SHP-REPAIR): `orbit_factor` multiplied by the maximum `repair_range` across the ship's repair module instances.
|
|
||||||
- **Salvage** (REQ-SHP-SALVAGE): `orbit_factor` multiplied by the maximum `collection_range` across the ship's salvage module instances.
|
|
||||||
- **Rally** (REQ-SHP-RALLY): `world.toml [world].rally_orbit_radius_tiles` — a fixed radius in tiles, independent of any tool range (the rally point is a position, not a tool-bearing target).
|
|
||||||
|
|
||||||
All tool ranges incorporate passive module modifiers (REQ-MOD-STAT-CALC). While orbiting, the ship navigates to maintain the orbit radius from the target's current center (REQ-SHP-MOVEMENT) while moving tangentially around it: if it is farther than the orbit radius it closes in, if it is nearer it backs off, and at the radius it circles. The orbit direction (clockwise or counter-clockwise) is fixed for the duration of orbiting a given target. Orbiting uses the standard physics movement model (REQ-SHP-MOVEMENT) and introduces no new movement constraints. Orbiting does not by itself trigger tool use — weapons, repair tools, and salvage bays still fire/heal/collect strictly per their own range and rate checks (REQ-SHP-FIRING, REQ-SHP-REPAIR, REQ-SHP-SALVAGE). With `orbit_factor` ≤ 1 the orbit lies within the maximum tool range, so the longest-range tool of that type remains in range while the ship orbits.
|
|
||||||
- REQ-SHP-NO-COLLISION: Ships do not collide with each other or with defence stations; they may visually overlap.
|
- REQ-SHP-NO-COLLISION: Ships do not collide with each other or with defence stations; they may visually overlap.
|
||||||
- REQ-SHP-SENSOR: A ship perceives only entities within its sensor range. Behavior is driven by what is in sensor range; entities outside sensor range are ignored.
|
- REQ-SHP-SENSOR: A ship perceives only entities within its sensor range. Behavior is driven by what is in sensor range; entities outside sensor range are ignored.
|
||||||
- REQ-SHP-FIRING: All weapons — on ships and on defence stations — fire when off cooldown and the target is within attack range. Firing emits a fire event and starts a 0.15-second damage delay (half the beam duration). When that delay expires, damage is applied to the target — unless the target has already been destroyed, in which case the damage is silently dropped. If the shooter is destroyed before the delay expires, damage is still applied when the delay expires. There is no projectile entity and no intervening collision. The weapon's cooldown begins at the moment of firing, not at damage application.
|
- REQ-SHP-FIRING: All weapons — on ships and on defence stations — fire when off cooldown and the target is within attack range. Firing emits a fire event and starts a 0.15-second damage delay (half the beam duration). When that delay expires, damage is applied to the target — unless the target has already been destroyed, in which case the damage is silently dropped. If the shooter is destroyed before the delay expires, damage is still applied when the delay expires. There is no projectile entity and no intervening collision. The weapon's cooldown begins at the moment of firing, not at damage application.
|
||||||
- REQ-SHP-FIRING-BEAM: Each fire event produces a visual laser beam drawn from the shooter's position to the target for 0.3 seconds. The beam endpoint is not the target's center but a point randomly offset from it: the offset direction is uniformly random and the offset magnitude is uniformly random up to half the target's visual size (for ships: half their rendered radius; for buildings/stations: half the shorter side of their tile footprint, in world units). The offset is chosen once per fire event and held fixed for the beam's lifetime. The beam is a pure rendering effect and has no simulation state (does not block movement, does not re-apply damage over its lifetime). Beams follow the shooter and target positions if either moves during the 0.3-second window. The beam is rendered for its full 0.3-second duration even if the shooter or target is destroyed before it expires.
|
- REQ-SHP-FIRING-BEAM: Each fire event produces a visual laser beam drawn from the shooter's position to the target for 0.3 seconds. The beam endpoint is not the target's center but a point randomly offset from it: the offset direction is uniformly random and the offset magnitude is uniformly random up to half the target's visual size (for ships: half their rendered radius; for buildings/stations: half the shorter side of their tile footprint, in world units). The offset is chosen once per fire event and held fixed for the beam's lifetime. The beam is a pure rendering effect and has no simulation state (does not block movement, does not re-apply damage over its lifetime). Beams follow the shooter and target positions if either moves during the 0.3-second window. The beam is rendered for its full 0.3-second duration even if the shooter or target is destroyed before it expires.
|
||||||
- REQ-SHP-COMBAT: Ships with at least one **weapon module** (player) — engage enemy ships within sensor range. When engaging an enemy, the ship orbits it at the combat orbit radius (REQ-SHP-ORBIT) rather than approaching its center. The player can configure the following per shipyard (applied to all ships produced by that shipyard):
|
- REQ-SHP-COMBAT: Ships with at least one **weapon module** (player) — engage enemy ships within sensor range. The player can configure the following per shipyard (applied to all ships produced by that shipyard):
|
||||||
- Stance: aggressive (advance toward enemies) / defensive (hold position near asteroid).
|
- Stance: aggressive (advance toward enemies) / defensive (hold position near asteroid).
|
||||||
- Target priority: closest / highest HP / structures first.
|
- Target priority: closest / highest HP / structures first.
|
||||||
- REQ-SHP-RALLY: After spawning, aggressive-stance ships with weapon modules move to and orbit the **rally point** — the midpoint between the two player defence stations (center of their Y-span, at the player defence stations' X position) — at the rally orbit radius (REQ-SHP-ORBIT). While orbiting the rally point, ships still engage any enemy that enters sensor range (switching to the combat orbit per REQ-SHP-COMBAT). Every `world.toml [world].departure_interval_seconds` seconds (default 20), all ships with weapon modules currently at the rally point depart simultaneously and begin their normal aggressive advance toward the enemy. The departure timer is global and shared across all shipyards; it is not reset by individual ship arrivals at the rally point.
|
- REQ-SHP-RALLY: After spawning, aggressive-stance ships with weapon modules move to and loiter at the **rally point** — the midpoint between the two player defence stations (center of their Y-span, at the player defence stations' X position). While at the rally point, ships still engage any enemy that enters sensor range. Every `world.toml [world].departure_interval_seconds` seconds (default 20), all ships with weapon modules currently at the rally point depart simultaneously and begin their normal aggressive advance toward the enemy. The departure timer is global and shared across all shipyards; it is not reset by individual ship arrivals at the rally point.
|
||||||
- REQ-SHP-SALVAGE: Ships with at least one **salvage module** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If scrap enters sensor range, navigate toward it by orbiting it at the salvage orbit radius (REQ-SHP-ORBIT); when it is within a module's `collection_range`, that module collects it (consuming the scrap entity). Once all cargo is full, fly to a Salvage Bay and deliver (a direct approach, not an orbit — the ship must reach the bay); after delivery, resume patrol. If an enemy ship enters sensor range, the ship retreats (REQ-SHP-RETREAT) until no enemy is in sensor range, then resumes patrol — this applies regardless of whether the ship is targeting or carrying scrap. Ships with salvage modules are vulnerable to enemy ships while operating.
|
- REQ-SHP-SALVAGE: Ships with at least one **salvage module** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If scrap enters sensor range, move to it; when it is within a module's `collection_range`, that module collects it (consuming the scrap entity). Once all cargo is full, fly to a Salvage Bay and deliver; after delivery, resume patrol. If an enemy ship enters sensor range while not currently targeting or carrying scrap, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. Ships with salvage modules are vulnerable to enemy ships while operating.
|
||||||
|
|
||||||
Each salvage module instance operates independently: it has its own cargo hold (`cargo_capacity`), collection range (`collection_range`), and collection rate (`collection_rate`, in collections per second). After collecting a piece of scrap, the module cannot collect again until `1 / collection_rate` seconds have elapsed. A ship with multiple salvage modules can therefore collect multiple pieces of scrap per tick (one per ready module), and installs of different module types may have different ranges and rates. The ship navigates based on the maximum collection range across all installed salvage modules.
|
Each salvage module instance operates independently: it has its own cargo hold (`cargo_capacity`), collection range (`collection_range`), and collection rate (`collection_rate`, in collections per second). After collecting a piece of scrap, the module cannot collect again until `1 / collection_rate` seconds have elapsed. A ship with multiple salvage modules can therefore collect multiple pieces of scrap per tick (one per ready module), and installs of different module types may have different ranges and rates. The ship navigates based on the maximum collection range across all installed salvage modules.
|
||||||
|
- REQ-SHP-REPAIR: Ships with at least one **repair module** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If a damaged player defence station or player ship enters sensor range, move to it and repair. If an enemy ship enters sensor range while not currently repairing, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. The player can configure the target priority per shipyard:
|
||||||
Salvage collection and delivery are world-state changes performed every tick regardless of which behavior the ship is currently executing; the salvage behavior only governs where the ship navigates (toward scrap, toward a Salvage Bay, or — when retreating — toward the rally point).
|
|
||||||
- REQ-SHP-REPAIR: Ships with at least one **repair module** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If a damaged player defence station or player ship enters sensor range, navigate toward it by orbiting it at the repair orbit radius (REQ-SHP-ORBIT) and repair. If an enemy ship enters sensor range, the ship retreats (REQ-SHP-RETREAT) until no enemy is in sensor range, then resumes patrol. The player can configure the target priority per shipyard:
|
|
||||||
- Defence stations first / ships first / nearest target.
|
- Defence stations first / ships first / nearest target.
|
||||||
|
|
||||||
Each repair module instance operates independently: it has its own repair rate (`repair_rate`) and repair range (`repair_range`). On each tick, a module first attempts to heal the ship's current behavior-level navigation target if that target is within the module's `repair_range` and is damaged (HP above zero and below maximum HP). If those conditions are not met — because the target is out of the module's `repair_range`, already at full health, or destroyed — the module independently searches for the nearest damaged friendly (player ship or player defence station) within its own `repair_range` and heals that instead. If no valid target is found within range, the module idles. A ship with multiple repair modules can therefore heal different targets simultaneously. Navigation is driven solely by the behavior-level target; individual module fallback targets do not affect which direction the ship moves. Repair healing is a world-state change applied every tick regardless of which behavior the ship is currently executing.
|
Each repair module instance operates independently: it has its own repair rate (`repair_rate`) and repair range (`repair_range`). On each tick, a module first attempts to heal the ship's current behavior-level navigation target if that target is within the module's `repair_range` and is damaged (HP above zero and below maximum HP). If those conditions are not met — because the target is out of the module's `repair_range`, already at full health, or destroyed — the module independently searches for the nearest damaged friendly (player ship or player defence station) within its own `repair_range` and heals that instead. If no valid target is found within range, the module idles. A ship with multiple repair modules can therefore heal different targets simultaneously. Navigation is driven solely by the behavior-level target; individual module fallback targets do not affect which direction the ship moves.
|
||||||
- REQ-SHP-RETREAT: **Player ships retreat to the rally point (REQ-SHP-RALLY) when threatened.** A ship retreats while either condition holds: (a) its HP is below a low-HP threshold (currently 30% of its maximum HP); or (b) it has no weapon modules and an enemy ship is within its sensor range. Retreating takes priority over the ship's other behaviors and moves it toward the rally point; the ship resumes its normal behavior once neither condition holds. Enemy ships never retreat (REQ-SHP-ENEMY-AI).
|
- REQ-SHP-ENEMY-AI: **Enemy ships** — engage the closest valid target (player defence station, HQ, or player ship) within their sensor range. If no target is in sensor range, they move toward the asteroid (leftward in world coordinates).
|
||||||
- REQ-SHP-ENEMY-AI: **Enemy ships** — engage the closest valid target (player defence station, HQ, or player ship) within their sensor range, orbiting the engaged target at the combat orbit radius (REQ-SHP-ORBIT). If no target is in sensor range, they move toward the asteroid (leftward in world coordinates).
|
- REQ-SHP-SCHEMATICS: The player selects a schematic per shipyard by clicking it. New schematics are unlocked automatically when an enemy defence station set is destroyed (REQ-DEF-SCHEMATIC-DROP) — there is no physical loot to collect.
|
||||||
- REQ-SHP-TARGET-SELECT: **Combat target selection.** Both player combat ships (REQ-SHP-COMBAT) and enemy ships (REQ-SHP-ENEMY-AI) pick which hostile to engage by scoring every valid target (an opposing-faction ship, defence station, or HQ) within sensor range and engaging the highest-scoring one. A target's score is the product of a **base desirability** and an **overclaim penalty** (REQ-SHP-TARGET-CLAIM). The base desirability is `world.toml [targeting].target_score_formula` evaluated with `x` set to the target's distance from the ship divided by the ship's maximum weapon `attack_range` (falling back to sensor range for a ship with no weapon), clamped to a minimum of 0. The default formula `1 / (1 + x)` decreases with distance, so — absent any claims — the nearest target is chosen, realizing the closest-target priority referenced by REQ-SHP-COMBAT and REQ-SHP-ENEMY-AI. A ship engages at most one target at a time; all of its weapons fire on that target subject to their own range and rate checks (REQ-SHP-FIRING).
|
|
||||||
- REQ-SHP-TARGET-CLAIM: **Overclaim penalty.** To stop every ship from dogpiling the same hostile, each target a ship is currently engaging counts as a **claim** on that target. When scoring a candidate, its base desirability (REQ-SHP-TARGET-SELECT) is multiplied by `world.toml [targeting].overclaim_penalty_formula` evaluated with `x` set to the number of ships currently claiming that candidate — a ship never counts its own claim against the target it already holds — clamped to the range [0, 1]. The penalty is 1 (no reduction) at zero claims and decreases as claims accumulate, so heavily-claimed targets become less attractive and ships spread across the available hostiles. The default formula `max(0.5, 1 - 0.1*x)` reduces desirability by 0.1 per claim down to a floor of 0.5. Because claims reflect the previous tick's engagements, target distribution converges over successive ticks rather than instantaneously.
|
|
||||||
- REQ-SHP-TARGET-HYSTERESIS: **Target stickiness.** A ship keeps engaging its current target as long as that target remains valid and within sensor range, switching to a different target only when the best alternative's score exceeds the current target's score by more than the fractional margin `world.toml [targeting].target_hysteresis` (default 0.10). This prevents ships from rapidly oscillating between targets of near-equal score and preserves focus fire.
|
|
||||||
- REQ-SHP-SCHEMATICS: The player selects a schematic per shipyard by clicking it. New schematics are unlocked by destroying enemy defence station sets (REQ-DEF-SCHEMATIC-DROP) — there is no physical loot to collect.
|
|
||||||
|
|
||||||
## Ship Modules
|
## Ship Modules
|
||||||
|
|
||||||
@@ -195,6 +182,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- `player_production_level` — initial level for this module type; used as `x` in its stat formulas. Incremented by 1 on each duplicate schematic drop (REQ-DEF-SCHEMATIC-DROP).
|
- `player_production_level` — initial level for this module type; used as `x` in its stat formulas. Incremented by 1 on each duplicate schematic drop (REQ-DEF-SCHEMATIC-DROP).
|
||||||
- `unlock_at_station_level` — the enemy defence station level at which this module's schematic becomes available for unlock; -1 means the player starts with the module schematic already unlocked.
|
- `unlock_at_station_level` — the enemy defence station level at which this module's schematic becomes available for unlock; -1 means the player starts with the module schematic already unlocked.
|
||||||
- `production_time_seconds` — time added to the ship's production cycle per instance.
|
- `production_time_seconds` — time added to the ship's production cycle per instance.
|
||||||
|
- `threat_cost` — threat cost added to the ship's threat cost per instance.
|
||||||
- `fill_color` — fill color used to render this module's cells in the layout grid.
|
- `fill_color` — fill color used to render this module's cells in the layout grid.
|
||||||
- `glyph` — single character rendered on this module's cells in the layout grid and preview widget.
|
- `glyph` — single character rendered on this module's cells in the layout grid and preview widget.
|
||||||
- An optional **capability section** (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas. A module with base stat formulas is a capability module — each placed instance grants the ship an independent weapon, salvage bay, or repair tool with its own state (cooldown, target, cargo). A ship may have multiple capability module instances of the same or different types. Base stat formulas per capability type:
|
- An optional **capability section** (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas. A module with base stat formulas is a capability module — each placed instance grants the ship an independent weapon, salvage bay, or repair tool with its own state (cooldown, target, cargo). A ship may have multiple capability module instances of the same or different types. Base stat formulas per capability type:
|
||||||
@@ -215,19 +203,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
- REQ-MOD-MATERIALS: The total materials required to build a ship are the union of the ship's base `[ship.schematic].materials` and the `materials` of every module instance in the configured layout. Quantities of the same item type are summed.
|
- REQ-MOD-MATERIALS: The total materials required to build a ship are the union of the ship's base `[ship.schematic].materials` and the `materials` of every module instance in the configured layout. Quantities of the same item type are summed.
|
||||||
- REQ-MOD-PRODUCTION-TIME: The total production time is the ship's base `[ship.schematic].production_time_seconds` plus the sum of `production_time_seconds` for every module instance in the configured layout.
|
- REQ-MOD-PRODUCTION-TIME: The total production time is the ship's base `[ship.schematic].production_time_seconds` plus the sum of `production_time_seconds` for every module instance in the configured layout.
|
||||||
- REQ-MOD-THREAT: The threat cost of a ship is dynamically derived from the accumulated total production time required to produce that ship from scratch. One second of production time equals one threat. The total production time is the sum of:
|
- REQ-MOD-THREAT: The total threat cost of a ship is the ship's base `[ship.threat].cost_formula` evaluated at the ship's level, plus the sum of `threat_cost` for every module instance in the configured layout.
|
||||||
1. The ship's base `production_time_seconds`.
|
|
||||||
2. The `production_time_seconds` of every module instance in the configured layout.
|
|
||||||
3. For every material required (the union of the ship's base materials and all module instance materials, with quantities summed per item type): the recursive production time of that material multiplied by the required quantity (see REQ-THREAT-ITEM).
|
|
||||||
|
|
||||||
- REQ-THREAT-ITEM: The threat value of an item type (in seconds) is determined by the recipe that produces it:
|
|
||||||
- **Miner recipe**: the recipe's `duration_seconds`.
|
|
||||||
- **Smelter recipe**: the recipe's `duration_seconds` plus the sum of each input's threat value multiplied by that input's required quantity.
|
|
||||||
- **Assembler recipe**: the recipe's `duration_seconds` plus the sum of each input's threat value multiplied by that input's required quantity.
|
|
||||||
- **Reprocessing-only item** (an item type that has no miner, smelter, or assembler recipe producing it, and is only obtainable via reprocessing): `(scrap_threat × scrap_per_cycle + duration_seconds) / probability`, where `scrap_threat` is the threat value of scrap (see REQ-THREAT-SCRAP), `scrap_per_cycle` is the number of scrap consumed per reprocessing cycle, `duration_seconds` is the reprocessing cycle time, and `probability` is the normalized weight of that item in the reprocessing output pool.
|
|
||||||
- **Multiple recipes**: if an item type can be produced by more than one non-reprocessing recipe (miner, smelter, or assembler), its threat value is the **maximum** across all such recipes. The reprocessing path is only used when no other recipe exists.
|
|
||||||
|
|
||||||
- REQ-THREAT-SCRAP: The threat value of scrap is derived from the ship schematic with the smallest configured `scrap_drop` value (from `ships.toml [ship.loot].scrap_drop`). Scrap threat = that ship's threat cost (REQ-MOD-THREAT) / that ship's `scrap_drop` value. If multiple schematics share the same smallest `scrap_drop`, any one of them may be used.
|
|
||||||
- REQ-MOD-STAT-CALC: For each stat (on the ship hull or on a capability module instance), the final value is computed as: `final = base × total_multiplier + total_additive`, where:
|
- REQ-MOD-STAT-CALC: For each stat (on the ship hull or on a capability module instance), the final value is computed as: `final = base × total_multiplier + total_additive`, where:
|
||||||
- `base` is the stat's base formula evaluated at the ship's production level (for hull stats) or at the capability module's `player_production_level` (for capability module stats).
|
- `base` is the stat's base formula evaluated at the ship's production level (for hull stats) or at the capability module's `player_production_level` (for capability module stats).
|
||||||
- `total_multiplier` = 1 + sum of (m_i − 1) for each multiplicative modifier m_i from all passive module instances. Each m_i is evaluated from the module's multiplicative formula at the module's `player_production_level`.
|
- `total_multiplier` = 1 + sum of (m_i − 1) for each multiplicative modifier m_i from all passive module instances. Each m_i is evaluated from the module's multiplicative formula at the module's `player_production_level`.
|
||||||
@@ -271,8 +247,6 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
All capability module stat values incorporate passive modifiers targeting the relevant capability category per REQ-MOD-STAT-CALC. Each capability module instance uses its own `player_production_level` for formula evaluation.
|
All capability module stat values incorporate passive modifiers targeting the relevant capability category per REQ-MOD-STAT-CALC. Each capability module instance uses its own `player_production_level` for formula evaluation.
|
||||||
|
|
||||||
While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT) for the current layout configuration. This value updates in real time as modules are placed or removed.
|
|
||||||
|
|
||||||
- REQ-MOD-UI-LAYOUT-SIZE: Ship layouts are small enough to display in the layout configuration dialog without scrolling (maximum grid size fits within the dialog).
|
- REQ-MOD-UI-LAYOUT-SIZE: Ship layouts are small enough to display in the layout configuration dialog without scrolling (maximum grid size fits within the dialog).
|
||||||
|
|
||||||
### Layout Blueprints
|
### Layout Blueprints
|
||||||
@@ -296,24 +270,13 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- REQ-DEF-ENEMY-FIRE: Enemy defence stations automatically fire at player ships within range.
|
- REQ-DEF-ENEMY-FIRE: Enemy defence stations automatically fire at player ships within range.
|
||||||
- REQ-DEF-NO-CROSSFIRE: Enemy and player defence stations are never in each other's firing range.
|
- REQ-DEF-NO-CROSSFIRE: Enemy and player defence stations are never in each other's firing range.
|
||||||
- REQ-DEF-PUSH: When both enemy defence stations in a set are destroyed, the boss countdown is advanced (REQ-WAV-BOSS-ADVANCE), the scrollable area is extended (REQ-GW-PUSH-EXPAND), a new set of enemy defence stations is placed at the new boundary, and exactly one schematic drop is awarded for the destroyed set (REQ-DEF-SCHEMATIC-DROP).
|
- REQ-DEF-PUSH: When both enemy defence stations in a set are destroyed, the boss countdown is advanced (REQ-WAV-BOSS-ADVANCE), the scrollable area is extended (REQ-GW-PUSH-EXPAND), a new set of enemy defence stations is placed at the new boundary, and exactly one schematic drop is awarded for the destroyed set (REQ-DEF-SCHEMATIC-DROP).
|
||||||
- REQ-DEF-SCHEMATIC-DROP: Each destroyed set of enemy defence stations awards exactly one schematic drop (not one per station). The drop opens a **schematic choice dialog** — a modal dialog that pauses the game (speed set to 0×; on close, speed is restored to what it was before the dialog opened). Up to three schematic options are drawn uniformly at random **without replacement** from the eligible drop pool. If the pool contains fewer than three entries, only that many options are shown. The eligible drop pool contains:
|
- REQ-DEF-SCHEMATIC-DROP: Each destroyed set of enemy defence stations awards exactly one schematic drop (not one per station). The drop is automatic — no physical item to collect. A schematic is chosen uniformly at random from the eligible drop pool, which 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 -1 or is ≤ the level of the destroyed station set.
|
||||||
- All **assembler recipe schematics** whose `unlock_at_station_level` is ≥ 0 and ≤ the level of the destroyed station set, whose output item is currently implicitly unlocked (REQ-LOCK-IMPLICIT), and which have not yet been awarded.
|
- 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.
|
For a **ship or module schematic** drop: 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. The player is notified via a toast (REQ-UI-SCHEMATIC-TOAST).
|
||||||
|
|
||||||
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 an **assembler recipe schematic** drop: the recipe is explicitly unlocked and becomes available in the assembler recipe-selection dropdown (subject to REQ-LOCK-UI-RECIPE). The schematic is removed from the drop pool permanently (REQ-LOCK-EXPLICIT). The implicit unlock set is recomputed (REQ-LOCK-IMPLICIT). No toast is shown.
|
||||||
- For a ship or module schematic that would be a **new unlock**, its `materials` are added to the base set per REQ-LOCK-IMPLICIT step 1a before recomputation.
|
|
||||||
- For a ship or module schematic **level-up**, the implicit unlock set is unchanged, so the list is always empty.
|
|
||||||
- For an assembler recipe schematic, its output item is added to the base set per REQ-LOCK-IMPLICIT step 1b before recomputation.
|
|
||||||
|
|
||||||
Item names are deduplicated and sorted alphabetically. If no recipes would be newly unlocked, the list shows "None".
|
|
||||||
|
|
||||||
The player selects one option by clicking it. The selected schematic is applied and the dialog closes:
|
|
||||||
|
|
||||||
For a **ship or module schematic**: if the player does not yet have the schematic, it is unlocked (ship schematics unlock the corresponding shipyard selection; module schematics unlock the module type for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG)). If the player already has it, the schematic's `player_production_level` is incremented by 1 — for ship schematics, subsequent ships of that type are produced at a higher level; for module schematics, all instances of that module type use the higher level in their stat formulas.
|
|
||||||
|
|
||||||
For an **assembler recipe schematic**: the recipe is explicitly unlocked and becomes available in the assembler recipe-selection dropdown (subject to REQ-LOCK-UI-RECIPE). The schematic is removed from the drop pool permanently (REQ-LOCK-EXPLICIT). The implicit unlock set is recomputed (REQ-LOCK-IMPLICIT).
|
|
||||||
|
|
||||||
## Progression & Locking
|
## Progression & Locking
|
||||||
|
|
||||||
@@ -340,8 +303,8 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- REQ-WAV-BOSS-COUNTER: A global **boss wave counter** `x` starts at 1 at game start and increments by 1 immediately after each boss wave fires. It represents the current boss wave cycle number and is used as the variable in the threat rate and ship level formulas.
|
- REQ-WAV-BOSS-COUNTER: A global **boss wave counter** `x` starts at 1 at game start and increments by 1 immediately after each boss wave fires. It represents the current boss wave cycle number and is used as the variable in the threat rate and ship level formulas.
|
||||||
- REQ-WAV-THREAT-RATE: A global **threat level** accumulates continuously over real game time. The rate of increase per second is determined by `world.toml [waves].threat_rate_formula` where `x` is the boss wave counter (REQ-WAV-BOSS-COUNTER), clamped to a minimum of 0 (negative formula values are treated as 0). The rate is constant within each boss wave cycle and steps up each time `x` increments. Threat accumulation is paused during quiet windows (REQ-WAV-QUIET). Example: `1*x - 30` yields 0 threat/s when x ≤ 30 and increases linearly beyond that.
|
- REQ-WAV-THREAT-RATE: A global **threat level** accumulates continuously over real game time. The rate of increase per second is determined by `world.toml [waves].threat_rate_formula` where `x` is the boss wave counter (REQ-WAV-BOSS-COUNTER), clamped to a minimum of 0 (negative formula values are treated as 0). The rate is constant within each boss wave cycle and steps up each time `x` increments. Threat accumulation is paused during quiet windows (REQ-WAV-QUIET). Example: `1*x - 30` yields 0 threat/s when x ≤ 30 and increases linearly beyond that.
|
||||||
- REQ-WAV-GAP: At game start and immediately after each normal wave is triggered, a random inter-wave gap is drawn uniformly from [`world.toml [waves].gap_min_seconds`, `gap_max_seconds`]. The gap timer does not advance while inside a quiet window (REQ-WAV-QUIET); if a gap would expire inside a quiet window, its expiry is deferred until the quiet window ends.
|
- REQ-WAV-GAP: At game start and immediately after each normal wave is triggered, a random inter-wave gap is drawn uniformly from [`world.toml [waves].gap_min_seconds`, `gap_max_seconds`]. The gap timer does not advance while inside a quiet window (REQ-WAV-QUIET); if a gap would expire inside a quiet window, its expiry is deferred until the quiet window ends.
|
||||||
- REQ-WAV-TRIGGER: When the gap timer expires outside a quiet window, a normal wave is triggered. Ships are selected one at a time: from all schematics whose threat cost (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_formula` evaluates to > 0 at the current enemy ship level, uniformly randomly pick one whose cost fits the remaining threat budget. Repeat until no eligible schematic fits. Any remaining threat carries over to the next normal wave. A longer gap results in a larger wave. Because enemy ship level increases with the boss wave counter (REQ-WAV-SHIP-LEVEL), threat cost per ship rises as the game progresses.
|
||||||
- REQ-WAV-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-SHIP-LEVEL: Each wave's (normal and boss) enemy ships are assigned a level determined by `world.toml [waves].ship_level_formula` where `x` is the boss wave counter (REQ-WAV-BOSS-COUNTER). Per-ship stats and threat cost are computed from the ship level via the formulas in `ships.toml` (see REQ-SHP-STATS).
|
||||||
- REQ-WAV-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.
|
||||||
@@ -362,30 +325,27 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
### Layout
|
### Layout
|
||||||
|
|
||||||
The screen is divided into two columns: a main column (75% width) containing the header bar and game world, and a side panel column (25% width) containing the three UI panels stacked vertically:
|
The screen is divided into three vertical sections:
|
||||||
|
|
||||||
```
|
```
|
||||||
+--------------------------------------+--------------+
|
+--------------------------------------------------+
|
||||||
| Header Bar | |
|
| Header Bar |
|
||||||
+--------------------------------------+ Selected |
|
+--------------------------------------------------+
|
||||||
| | Building |
|
| |
|
||||||
| | Panel |
|
| Game World (70%) |
|
||||||
| +--------------+
|
| |
|
||||||
| Game World | Build |
|
+-----------------+-----------------+--------------+
|
||||||
| | Button |
|
| Selected | Build Button | Blueprint |
|
||||||
| | Grid |
|
| Building Panel | Grid | Panel |
|
||||||
| +--------------+
|
| (left) | (center) | (right) |
|
||||||
| | Blueprint |
|
+-----------------+-----------------+--------------+
|
||||||
| | Panel |
|
|
||||||
+--------------------------------------+--------------+
|
|
||||||
(75% width) (25% width)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- REQ-UI-HEADER: The header bar spans the width of the game world column (75% of the screen width) and always shows the elapsed survival time and the current global building blocks stock on the left, the boss wave counter and boss countdown (REQ-UI-BOSS-STATUS) to the left of the speed buttons, and game speed controls on the right.
|
- REQ-UI-HEADER: The header bar spans the full width above the game world and always shows the elapsed survival time and the current global building blocks stock on the left, the boss wave counter and boss countdown (REQ-UI-BOSS-STATUS) to the left of the speed buttons, and game speed controls on the right.
|
||||||
- REQ-UI-BOSS-STATUS: The header bar displays, to the left of the speed buttons, the current boss wave counter (REQ-WAV-BOSS-COUNTER) and the time remaining on the boss countdown (REQ-WAV-BOSS-COUNTDOWN). The boss wave counter is shown as `Boss Wave #<x>` and the countdown as `Next boss: <M:SS>`, where `<M:SS>` is the remaining seconds formatted as whole minutes and two-digit seconds. Both values update continuously as the simulation runs.
|
- REQ-UI-BOSS-STATUS: The header bar displays, to the left of the speed buttons, the current boss wave counter (REQ-WAV-BOSS-COUNTER) and the time remaining on the boss countdown (REQ-WAV-BOSS-COUNTDOWN). The boss wave counter is shown as `Boss Wave #<x>` and the countdown as `Next boss: <M:SS>`, where `<M:SS>` is the remaining seconds formatted as whole minutes and two-digit seconds. Both values update continuously as the simulation runs.
|
||||||
- REQ-UI-SPEED: The game speed controls in the header bar are buttons for 0×, 0.5×, 1×, 2×, and 4× speed. The currently active speed is shown as selected. All game simulation (production, movement, threat accumulation, wave timing) scales with the selected speed. 0× pauses the game.
|
- REQ-UI-SPEED: The game speed controls in the header bar are buttons for 0×, 0.5×, 1×, 2×, and 4× speed. The currently active speed is shown as selected. All game simulation (production, movement, threat accumulation, wave timing) scales with the selected speed. 0× pauses the game.
|
||||||
- REQ-UI-WORLD-SIZE: The game world view occupies the full height below the header bar in the main column (75% of the screen width).
|
- REQ-UI-WORLD-HEIGHT: The game world view occupies 70% of the remaining screen height below the header bar.
|
||||||
- REQ-UI-PANEL-COLUMN: The side panel column occupies 25% of the screen width and the full screen height. It is divided into three equal-height panels stacked top to bottom: selected building panel (top), build button grid (middle), and blueprint panel (bottom).
|
- REQ-UI-PANEL-HEIGHT: The UI panel occupies the remaining 30% of the screen height, split horizontally into a selected building panel (left), a build button grid (center), and a blueprint panel (right).
|
||||||
|
|
||||||
### Game World
|
### Game World
|
||||||
|
|
||||||
@@ -394,6 +354,13 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
- REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port.
|
- REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port.
|
||||||
- REQ-UI-HP-BARS: All entities with HP — the HQ, player and enemy defence stations, and player and enemy ships — render an HP bar below them. The bar is always visible regardless of current HP. The bar's filled portion represents the fraction of current HP to maximum HP.
|
- REQ-UI-HP-BARS: All entities with HP — the HQ, player and enemy defence stations, and player and enemy ships — render an HP bar below them. The bar is always visible regardless of current HP. The bar's filled portion represents the fraction of current HP to maximum HP.
|
||||||
- REQ-UI-NO-ZOOM: The view has a fixed zoom level; the player cannot zoom in or out.
|
- REQ-UI-NO-ZOOM: The view has a fixed zoom level; the player cannot zoom in or out.
|
||||||
|
- REQ-UI-SCHEMATIC-TOAST: When a schematic is unlocked or leveled up (REQ-DEF-SCHEMATIC-DROP), a transient notification toast appears in the top-right corner of the game world view for 4 seconds and then fades out. Toast text:
|
||||||
|
- **Ship schematic — new unlock**: `Schematic unlocked: <Ship Name>` (where `<Ship Name>` is `ships.toml [ship.schematic].display_name`).
|
||||||
|
- **Ship schematic — level-up (duplicate drop)**: `<Ship Name> production level → N` (where N is the new level).
|
||||||
|
- **Module schematic — new unlock**: `Module unlocked: <Module Id>` (where `<Module Id>` is the module's `id` from `modules.toml`).
|
||||||
|
- **Module schematic — level-up (duplicate drop)**: `<Module Id> production level → N` (where N is the new level).
|
||||||
|
|
||||||
|
If multiple toasts arrive in close succession, they stack vertically in a queue (most recent at the top) and each fades out independently after its own 4-second lifetime.
|
||||||
- REQ-UI-HOTKEYS: Global keyboard shortcuts:
|
- REQ-UI-HOTKEYS: Global keyboard shortcuts:
|
||||||
- **Space** — toggles pause. Pressing Space pauses (sets speed to 0×) and stores the previously selected non-zero speed; pressing Space again restores that speed.
|
- **Space** — toggles pause. Pressing Space pauses (sets speed to 0×) and stores the previously selected non-zero speed; pressing Space again restores that speed.
|
||||||
- **W** — increases game speed by one step in the sequence 0×, 0.5×, 1×, 2×, 4× (no wrap-around past 4×).
|
- **W** — increases game speed by one step in the sequence 0×, 0.5×, 1×, 2×, 4× (no wrap-around past 4×).
|
||||||
@@ -410,9 +377,6 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
- REQ-UI-DEBUG-OVERLAY: While debug draw mode is active (REQ-UI-DEBUG-DRAW), a text overlay is drawn in the upper left corner of the game world view. The overlay has a semi-transparent black background sized to fit its content. It displays the following lines of text:
|
- REQ-UI-DEBUG-OVERLAY: While debug draw mode is active (REQ-UI-DEBUG-DRAW), a text overlay is drawn in the upper left corner of the game world view. The overlay has a semi-transparent black background sized to fit its content. It displays the following lines of text:
|
||||||
- `Accumulated Threat Level: <level>` — where `<level>` is the current accumulated threat level (see REQ-WAV-THREAT-RATE).
|
- `Accumulated Threat Level: <level>` — where `<level>` is the current accumulated threat level (see REQ-WAV-THREAT-RATE).
|
||||||
- `Time until Wave: <time_s>` — where `<time_s>` is the remaining time in seconds on the normal-wave inter-wave gap timer (see REQ-WAV-GAP). During a quiet window the gap timer is frozen; the displayed value reflects that frozen state.
|
- `Time until Wave: <time_s>` — where `<time_s>` is the remaining time in seconds on the normal-wave inter-wave gap timer (see REQ-WAV-GAP). During a quiet window the gap timer is frozen; the displayed value reflects that frozen state.
|
||||||
- `Threat Accumulation Rate: <rate> threat/s` — the rate at which the accumulated threat level is currently increasing (see REQ-WAV-THREAT-RATE). During a quiet window (REQ-WAV-QUIET), this is 0, reflecting that accumulation is currently paused.
|
|
||||||
- `Max Factory Production: <rate> threat/s` — the threat-equivalent of the factory's total possible production: 1 threat/second for each completed (operational, not under construction) miner, smelter, assembler, reprocessing plant, and shipyard. One second of production equals one threat (see REQ-MOD-THREAT).
|
|
||||||
- `Current Factory Production: <rate> threat/s` — the threat-equivalent of the factory's current production: 1 threat/second for each completed miner, smelter, assembler, reprocessing plant, or shipyard that currently has an active production cycle (see REQ-MAT-CYCLE; for shipyards, an in-progress production cycle per REQ-BLD-SHIPYARD).
|
|
||||||
|
|
||||||
### Escape Menu
|
### Escape Menu
|
||||||
|
|
||||||
@@ -432,7 +396,7 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
- REQ-UI-CONFIG-INLINE: Recipe, schematic, ship stance, and target priority configuration for a selected building is shown and changed inline within this panel. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic dropdown (REQ-MOD-UI-PREVIEW).
|
- REQ-UI-CONFIG-INLINE: Recipe, schematic, ship stance, and target priority configuration for a selected building is shown and changed inline within this panel. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic dropdown (REQ-MOD-UI-PREVIEW).
|
||||||
- REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel shows a "Clear" button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels.
|
- REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel shows a "Clear" button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels.
|
||||||
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. Clicking a ship or defence station clears any existing selection and establishes a single-entity selection containing only that entity. Ships and defence stations cannot participate in multi-select together with buildings. Clicking empty world space (no building, ship, or defence station) clears the selection.
|
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. Clicking a ship or defence station clears any existing selection and establishes a single-entity selection containing only that entity. Ships and defence stations cannot participate in multi-select together with buildings. Clicking empty world space (no building, ship, or defence station) clears the selection.
|
||||||
- REQ-UI-SHIP-STATS-PANEL: When a single ship is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **ship stats panel**. The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed at the ship's actual level with its installed modules per REQ-MOD-STAT-CALC. The panel always shows all hull stats: HP (current / maximum), max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed. In addition, capability module summaries are shown conditioned on which module types are installed, using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each section appearing only if at least one instance of that module type is installed. 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 at the ship's actual level with its installed modules per REQ-MOD-STAT-CALC. The panel always shows all hull stats: HP (current / maximum), max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed. In addition, capability module summaries are shown conditioned on which module types are installed, using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each section appearing only if at least one instance of that module type is installed.
|
||||||
- REQ-UI-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
|
||||||
|
|||||||
@@ -17,8 +17,6 @@
|
|||||||
#include "ModuleOwnerComponent.h"
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "MovementIntentSystem.h"
|
#include "MovementIntentSystem.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "RepairSystem.h"
|
|
||||||
#include "SalvagerSystem.h"
|
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
@@ -53,16 +51,11 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
|||||||
m_rng);
|
m_rng);
|
||||||
|
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_gameConfig, m_admin);
|
m_shipSystem = std::make_unique<ShipSystem>(m_gameConfig, m_admin);
|
||||||
// Arena fights are symmetric and aggressive: player-faction ships must not
|
m_aiSystem = std::make_unique<AiSystem>();
|
||||||
// retreat (REQ-BAL-SIM-AI). Only one faction would otherwise get retreat.
|
|
||||||
m_shipSystem->setRetreatEnabled(false);
|
|
||||||
m_aiSystem = std::make_unique<AiSystem>(m_gameConfig);
|
|
||||||
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
||||||
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
|
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
|
||||||
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
|
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
|
||||||
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
|
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
|
||||||
m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin);
|
|
||||||
m_repairSystem = std::make_unique<RepairSystem>(m_admin);
|
|
||||||
|
|
||||||
placeStructures();
|
placeStructures();
|
||||||
spawnShips();
|
spawnShips();
|
||||||
@@ -257,16 +250,18 @@ ArenaStatus ArenaSimulation::status() const
|
|||||||
|
|
||||||
void ArenaSimulation::tick()
|
void ArenaSimulation::tick()
|
||||||
{
|
{
|
||||||
// Ship behavior systems (tick step 7): evaluate, select winner, execute.
|
// Ship behavior systems (tick step 7).
|
||||||
m_shipSystem->clearMovementIntents();
|
m_shipSystem->clearMovementIntents();
|
||||||
m_aiSystem->tick(m_admin, *m_buildingSystem, *m_scrapSystem);
|
m_aiSystem->tickHomeReturnBehavior(m_admin);
|
||||||
m_salvagerSystem->tick(*m_scrapSystem, *m_buildingSystem);
|
m_aiSystem->tickThreatResponseBehavior(m_admin, *m_buildingSystem);
|
||||||
m_repairSystem->tick();
|
m_aiSystem->tickRepairBehavior(m_admin, *m_buildingSystem);
|
||||||
|
m_aiSystem->tickRepairTools(m_admin);
|
||||||
|
m_aiSystem->tickSalvageBehavior(m_admin, *m_scrapSystem, *m_buildingSystem);
|
||||||
|
|
||||||
// Combat resolution (tick step 8).
|
// Combat resolution (tick step 8).
|
||||||
std::vector<WeaponFiredEvent> weaponFiredEvents;
|
std::vector<FireEvent> fireEvents;
|
||||||
m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, weaponFiredEvents);
|
m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, fireEvents);
|
||||||
m_weaponFiredEvents.insert(m_weaponFiredEvents.end(), weaponFiredEvents.begin(), weaponFiredEvents.end());
|
m_fireEvents.insert(m_fireEvents.end(), fireEvents.begin(), fireEvents.end());
|
||||||
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
||||||
|
|
||||||
// Deaths (tick step 9, simplified).
|
// Deaths (tick step 9, simplified).
|
||||||
@@ -398,10 +393,10 @@ void ArenaSimulation::tickOnce()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<WeaponFiredEvent> ArenaSimulation::drainWeaponFiredEvents()
|
std::vector<FireEvent> ArenaSimulation::drainFireEvents()
|
||||||
{
|
{
|
||||||
std::vector<WeaponFiredEvent> result;
|
std::vector<FireEvent> result;
|
||||||
result.swap(m_weaponFiredEvents);
|
result.swap(m_fireEvents);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
#include "WeaponFiredEvent.h"
|
#include "FireEvent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
@@ -22,8 +22,6 @@ class BuildingSystem;
|
|||||||
class CombatSystem;
|
class CombatSystem;
|
||||||
class DynamicBodySystem;
|
class DynamicBodySystem;
|
||||||
class MovementIntentSystem;
|
class MovementIntentSystem;
|
||||||
class RepairSystem;
|
|
||||||
class SalvagerSystem;
|
|
||||||
class ShipSystem;
|
class ShipSystem;
|
||||||
class ScrapSystem;
|
class ScrapSystem;
|
||||||
|
|
||||||
@@ -60,7 +58,7 @@ public:
|
|||||||
void requestStop();
|
void requestStop();
|
||||||
|
|
||||||
void tickOnce();
|
void tickOnce();
|
||||||
std::vector<WeaponFiredEvent> drainWeaponFiredEvents();
|
std::vector<FireEvent> drainFireEvents();
|
||||||
|
|
||||||
ArenaStatus status() const;
|
ArenaStatus status() const;
|
||||||
bool isFinished() const;
|
bool isFinished() const;
|
||||||
@@ -98,8 +96,6 @@ private:
|
|||||||
std::unique_ptr<DynamicBodySystem> m_dynamicBodySystem;
|
std::unique_ptr<DynamicBodySystem> m_dynamicBodySystem;
|
||||||
std::unique_ptr<CombatSystem> m_combatSystem;
|
std::unique_ptr<CombatSystem> m_combatSystem;
|
||||||
std::unique_ptr<ScrapSystem> m_scrapSystem;
|
std::unique_ptr<ScrapSystem> m_scrapSystem;
|
||||||
std::unique_ptr<SalvagerSystem> m_salvagerSystem;
|
|
||||||
std::unique_ptr<RepairSystem> m_repairSystem;
|
|
||||||
|
|
||||||
entt::entity m_team1HqEntity;
|
entt::entity m_team1HqEntity;
|
||||||
entt::entity m_team2HqEntity;
|
entt::entity m_team2HqEntity;
|
||||||
@@ -108,7 +104,7 @@ private:
|
|||||||
int m_winnerTeam;
|
int m_winnerTeam;
|
||||||
std::atomic<bool> m_stopRequested;
|
std::atomic<bool> m_stopRequested;
|
||||||
|
|
||||||
std::vector<WeaponFiredEvent> m_weaponFiredEvents;
|
std::vector<FireEvent> m_fireEvents;
|
||||||
|
|
||||||
mutable std::mutex m_statusMutex;
|
mutable std::mutex m_statusMutex;
|
||||||
ArenaStatus m_status;
|
ArenaStatus m_status;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
#include "EventManager.h"
|
#include "EventManager.h"
|
||||||
#include "FacingComponent.h"
|
#include "FacingComponent.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "GameSpeedChangedEvent.h"
|
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
@@ -46,13 +45,6 @@ ArenaView::ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals,
|
|||||||
connect(m_renderTimer, &QTimer::timeout, this, &ArenaView::onFrame);
|
connect(m_renderTimer, &QTimer::timeout, this, &ArenaView::onFrame);
|
||||||
m_renderTimer->start();
|
m_renderTimer->start();
|
||||||
m_frameTimer.start();
|
m_frameTimer.start();
|
||||||
|
|
||||||
registerForEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
ArenaView::~ArenaView()
|
|
||||||
{
|
|
||||||
unregisterForEvent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArenaView::setGameSpeed(double multiplier)
|
void ArenaView::setGameSpeed(double multiplier)
|
||||||
@@ -62,8 +54,7 @@ void ArenaView::setGameSpeed(double multiplier)
|
|||||||
m_prevNonZeroSpeed = multiplier;
|
m_prevNonZeroSpeed = multiplier;
|
||||||
}
|
}
|
||||||
m_gameSpeedMultiplier = multiplier;
|
m_gameSpeedMultiplier = multiplier;
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
emit speedChanged(multiplier);
|
||||||
std::make_shared<GameSpeedChangedEvent>(multiplier));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double ArenaView::gameSpeed() const
|
double ArenaView::gameSpeed() const
|
||||||
@@ -102,17 +93,34 @@ void ArenaView::onFrame()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit fire events via EventManager
|
|
||||||
{
|
{
|
||||||
const std::vector<WeaponFiredEvent> fires = m_sim->drainWeaponFiredEvents();
|
const std::vector<FireEvent> fires = m_sim->drainFireEvents();
|
||||||
for (const WeaponFiredEvent& fe : fires)
|
for (const FireEvent& fe : fires)
|
||||||
{
|
{
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
float maxRadius = 0.125f;
|
||||||
std::make_shared<WeaponFiredEvent>(fe));
|
if (m_sim->admin().isValid(fe.target)
|
||||||
|
&& m_sim->admin().hasAll<StationBodyComponent>(fe.target))
|
||||||
|
{
|
||||||
|
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(fe.target);
|
||||||
|
const int shorter = std::min(sb.footprint.width(),
|
||||||
|
sb.footprint.height());
|
||||||
|
maxRadius = shorter / 2.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uniform_real_distribution<float> angleDist(0.0f, 6.28318530f);
|
||||||
|
std::uniform_real_distribution<float> radiusDist(0.0f, maxRadius);
|
||||||
|
const float angle = angleDist(m_rng);
|
||||||
|
const float radius = radiusDist(m_rng);
|
||||||
|
|
||||||
|
ActiveBeam beam;
|
||||||
|
beam.event = fe;
|
||||||
|
beam.emittedWallMs = m_wallMs;
|
||||||
|
beam.targetOffset = QVector2D(radius * std::cos(angle),
|
||||||
|
radius * std::sin(angle));
|
||||||
|
m_activeBeams.push_back(beam);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expire old beams
|
|
||||||
{
|
{
|
||||||
std::vector<ActiveBeam> live;
|
std::vector<ActiveBeam> live;
|
||||||
for (const ActiveBeam& b : m_activeBeams)
|
for (const ActiveBeam& b : m_activeBeams)
|
||||||
@@ -128,36 +136,12 @@ void ArenaView::onFrame()
|
|||||||
if (m_sim->isFinished() && !m_finishedEmitted)
|
if (m_sim->isFinished() && !m_finishedEmitted)
|
||||||
{
|
{
|
||||||
m_finishedEmitted = true;
|
m_finishedEmitted = true;
|
||||||
|
emit finished();
|
||||||
}
|
}
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArenaView::handleEvent(std::shared_ptr<const WeaponFiredEvent> event)
|
|
||||||
{
|
|
||||||
float maxRadius = 0.125f;
|
|
||||||
if (m_sim->admin().isValid(event->target)
|
|
||||||
&& m_sim->admin().hasAll<StationBodyComponent>(event->target))
|
|
||||||
{
|
|
||||||
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(event->target);
|
|
||||||
const int shorter = std::min(sb.footprint.width(),
|
|
||||||
sb.footprint.height());
|
|
||||||
maxRadius = shorter / 2.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::uniform_real_distribution<float> angleDist(0.0f, 6.28318530f);
|
|
||||||
std::uniform_real_distribution<float> radiusDist(0.0f, maxRadius);
|
|
||||||
const float angle = angleDist(m_rng);
|
|
||||||
const float radius = radiusDist(m_rng);
|
|
||||||
|
|
||||||
ActiveBeam beam;
|
|
||||||
beam.event = *event;
|
|
||||||
beam.emittedWallMs = m_wallMs;
|
|
||||||
beam.targetOffset = QVector2D(radius * std::cos(angle),
|
|
||||||
radius * std::sin(angle));
|
|
||||||
m_activeBeams.push_back(beam);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ArenaView::paintGL()
|
void ArenaView::paintGL()
|
||||||
{
|
{
|
||||||
QPainter painter(this);
|
QPainter painter(this);
|
||||||
@@ -430,3 +414,4 @@ void ArenaView::drawBeams(QPainter& painter)
|
|||||||
worldToWidget(*targetPos + beam.targetOffset));
|
worldToWidget(*targetPos + beam.targetOffset));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
#include "EventHandler.h"
|
#include "FireEvent.h"
|
||||||
#include "WeaponFiredEvent.h"
|
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
#include "EntitySelectedEvent.h"
|
#include "EntitySelectedEvent.h"
|
||||||
@@ -21,21 +20,23 @@
|
|||||||
class ArenaSimulation;
|
class ArenaSimulation;
|
||||||
class QPainter;
|
class QPainter;
|
||||||
|
|
||||||
class ArenaView : public QOpenGLWidget,
|
class ArenaView : public QOpenGLWidget
|
||||||
public EventHandler<WeaponFiredEvent>
|
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals,
|
ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals,
|
||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
~ArenaView() override;
|
|
||||||
|
|
||||||
void setGameSpeed(double multiplier);
|
void setGameSpeed(double multiplier);
|
||||||
double gameSpeed() const;
|
double gameSpeed() const;
|
||||||
void togglePause();
|
void togglePause();
|
||||||
void stopRendering();
|
void stopRendering();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void speedChanged(double multiplier);
|
||||||
|
void finished();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintGL() override;
|
void paintGL() override;
|
||||||
void mousePressEvent(QMouseEvent* event) override;
|
void mousePressEvent(QMouseEvent* event) override;
|
||||||
@@ -44,8 +45,6 @@ private slots:
|
|||||||
void onFrame();
|
void onFrame();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleEvent(std::shared_ptr<const WeaponFiredEvent> event) override;
|
|
||||||
|
|
||||||
void drawTiles(QPainter& painter);
|
void drawTiles(QPainter& painter);
|
||||||
void drawBuildings(QPainter& painter);
|
void drawBuildings(QPainter& painter);
|
||||||
void drawStations(QPainter& painter);
|
void drawStations(QPainter& painter);
|
||||||
@@ -63,7 +62,7 @@ private:
|
|||||||
|
|
||||||
struct ActiveBeam
|
struct ActiveBeam
|
||||||
{
|
{
|
||||||
WeaponFiredEvent event;
|
FireEvent event;
|
||||||
qint64 emittedWallMs;
|
qint64 emittedWallMs;
|
||||||
QVector2D targetOffset;
|
QVector2D targetOffset;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,13 +3,8 @@
|
|||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "ArenaInspectRequestedEvent.h"
|
ArenaWidget::ArenaWidget(const std::string& arenaName, QWidget* parent)
|
||||||
#include "ArenaStartRequestedEvent.h"
|
|
||||||
#include "EventManager.h"
|
|
||||||
|
|
||||||
ArenaWidget::ArenaWidget(int arenaIndex, const std::string& arenaName, QWidget* parent)
|
|
||||||
: QFrame(parent)
|
: QFrame(parent)
|
||||||
, m_arenaIndex(arenaIndex)
|
|
||||||
, m_running(false)
|
, m_running(false)
|
||||||
, m_wasFinished(false)
|
, m_wasFinished(false)
|
||||||
{
|
{
|
||||||
@@ -36,17 +31,11 @@ void ArenaWidget::buildLayout(const std::string& arenaName)
|
|||||||
titleRow->addStretch();
|
titleRow->addStretch();
|
||||||
|
|
||||||
m_inspectButton = new QPushButton(tr("Inspect"), this);
|
m_inspectButton = new QPushButton(tr("Inspect"), this);
|
||||||
connect(m_inspectButton, &QPushButton::clicked, this, [this]() {
|
connect(m_inspectButton, &QPushButton::clicked, this, &ArenaWidget::inspectRequested);
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<ArenaInspectRequestedEvent>(m_arenaIndex));
|
|
||||||
});
|
|
||||||
titleRow->addWidget(m_inspectButton);
|
titleRow->addWidget(m_inspectButton);
|
||||||
|
|
||||||
m_startButton = new QPushButton(tr("Start"), this);
|
m_startButton = new QPushButton(tr("Start"), this);
|
||||||
connect(m_startButton, &QPushButton::clicked, this, [this]() {
|
connect(m_startButton, &QPushButton::clicked, this, &ArenaWidget::startRequested);
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<ArenaStartRequestedEvent>(m_arenaIndex));
|
|
||||||
});
|
|
||||||
titleRow->addWidget(m_startButton);
|
titleRow->addWidget(m_startButton);
|
||||||
|
|
||||||
outerLayout->addLayout(titleRow);
|
outerLayout->addLayout(titleRow);
|
||||||
|
|||||||
@@ -14,16 +14,19 @@ class ArenaWidget : public QFrame
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ArenaWidget(int arenaIndex, const std::string& arenaName, QWidget* parent = nullptr);
|
explicit ArenaWidget(const std::string& arenaName, QWidget* parent = nullptr);
|
||||||
|
|
||||||
void updateStatus(const ArenaStatus& status);
|
void updateStatus(const ArenaStatus& status);
|
||||||
void startSimulation();
|
void startSimulation();
|
||||||
void resetToGrey();
|
void resetToGrey();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void startRequested();
|
||||||
|
void inspectRequested();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void buildLayout(const std::string& arenaName);
|
void buildLayout(const std::string& arenaName);
|
||||||
|
|
||||||
int m_arenaIndex;
|
|
||||||
QLabel* m_titleLabel;
|
QLabel* m_titleLabel;
|
||||||
QLabel* m_team1Header;
|
QLabel* m_team1Header;
|
||||||
QLabel* m_team2Header;
|
QLabel* m_team2Header;
|
||||||
|
|||||||
@@ -48,17 +48,14 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
|||||||
m_pollTimer = new QTimer(this);
|
m_pollTimer = new QTimer(this);
|
||||||
connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses);
|
connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses);
|
||||||
m_pollTimer->start(100);
|
m_pollTimer->start(100);
|
||||||
|
|
||||||
registerForEvents();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BalancingWindow::~BalancingWindow()
|
BalancingWindow::~BalancingWindow()
|
||||||
{
|
{
|
||||||
unregisterForEvents();
|
|
||||||
|
|
||||||
m_pollTimer->stop();
|
m_pollTimer->stop();
|
||||||
if (m_inspectWindow)
|
if (m_inspectWindow)
|
||||||
{
|
{
|
||||||
|
m_inspectWindow->disconnect(this);
|
||||||
delete m_inspectWindow;
|
delete m_inspectWindow;
|
||||||
m_inspectWindow = nullptr;
|
m_inspectWindow = nullptr;
|
||||||
}
|
}
|
||||||
@@ -84,11 +81,16 @@ void BalancingWindow::populateArenas(const BalancingConfig& balancingConfig)
|
|||||||
entry.config = arenaConfig;
|
entry.config = arenaConfig;
|
||||||
entry.simulation = std::make_unique<ArenaSimulation>(
|
entry.simulation = std::make_unique<ArenaSimulation>(
|
||||||
m_gameConfig, arenaConfig, m_nextSeed++);
|
m_gameConfig, arenaConfig, m_nextSeed++);
|
||||||
entry.widget = new ArenaWidget(index, arenaConfig.name, scrollContent);
|
entry.widget = new ArenaWidget(arenaConfig.name, scrollContent);
|
||||||
contentLayout->addWidget(entry.widget);
|
contentLayout->addWidget(entry.widget);
|
||||||
|
|
||||||
entry.widget->updateStatus(entry.simulation->status());
|
entry.widget->updateStatus(entry.simulation->status());
|
||||||
|
|
||||||
|
connect(entry.widget, &ArenaWidget::startRequested,
|
||||||
|
this, [this, index]() { startArena(index); });
|
||||||
|
connect(entry.widget, &ArenaWidget::inspectRequested,
|
||||||
|
this, [this, index]() { inspectArena(index); });
|
||||||
|
|
||||||
m_arenas.push_back(std::move(entry));
|
m_arenas.push_back(std::move(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,21 +158,6 @@ void BalancingWindow::startAll()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BalancingWindow::handleEvent(std::shared_ptr<const ArenaStartRequestedEvent> event)
|
|
||||||
{
|
|
||||||
startArena(event->arenaIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BalancingWindow::handleEvent(std::shared_ptr<const ArenaInspectRequestedEvent> event)
|
|
||||||
{
|
|
||||||
inspectArena(event->arenaIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BalancingWindow::handleEvent(std::shared_ptr<const InspectWindowClosedEvent> /*event*/)
|
|
||||||
{
|
|
||||||
closeInspectWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BalancingWindow::startArena(int index)
|
void BalancingWindow::startArena(int index)
|
||||||
{
|
{
|
||||||
ArenaEntry& entry = m_arenas[index];
|
ArenaEntry& entry = m_arenas[index];
|
||||||
@@ -192,6 +179,7 @@ void BalancingWindow::inspectArena(int index)
|
|||||||
{
|
{
|
||||||
if (m_inspectWindow)
|
if (m_inspectWindow)
|
||||||
{
|
{
|
||||||
|
m_inspectWindow->disconnect(this);
|
||||||
delete m_inspectWindow;
|
delete m_inspectWindow;
|
||||||
m_inspectWindow = nullptr;
|
m_inspectWindow = nullptr;
|
||||||
|
|
||||||
@@ -222,6 +210,8 @@ void BalancingWindow::inspectArena(int index)
|
|||||||
|
|
||||||
m_inspectWindow = new InspectWindow(
|
m_inspectWindow = new InspectWindow(
|
||||||
m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr);
|
m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr);
|
||||||
|
connect(m_inspectWindow, &InspectWindow::closed,
|
||||||
|
this, &BalancingWindow::closeInspectWindow);
|
||||||
|
|
||||||
setMainControlsEnabled(false);
|
setMainControlsEnabled(false);
|
||||||
m_inspectWindow->show();
|
m_inspectWindow->show();
|
||||||
@@ -234,6 +224,7 @@ void BalancingWindow::closeInspectWindow()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_inspectWindow->disconnect(this);
|
||||||
m_inspectWindow->deleteLater();
|
m_inspectWindow->deleteLater();
|
||||||
m_inspectWindow = nullptr;
|
m_inspectWindow = nullptr;
|
||||||
|
|
||||||
|
|||||||
@@ -10,22 +10,15 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "ArenaInspectRequestedEvent.h"
|
|
||||||
#include "ArenaStartRequestedEvent.h"
|
|
||||||
#include "ArenaWidget.h"
|
#include "ArenaWidget.h"
|
||||||
#include "ArenaSimulation.h"
|
#include "ArenaSimulation.h"
|
||||||
#include "BalancingConfig.h"
|
#include "BalancingConfig.h"
|
||||||
#include "EventHandler.h"
|
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "InspectWindowClosedEvent.h"
|
|
||||||
#include "VisualsConfig.h"
|
#include "VisualsConfig.h"
|
||||||
|
|
||||||
class InspectWindow;
|
class InspectWindow;
|
||||||
|
|
||||||
class BalancingWindow : public QWidget,
|
class BalancingWindow : public QWidget
|
||||||
public CombinedEventHandler<ArenaStartRequestedEvent,
|
|
||||||
ArenaInspectRequestedEvent,
|
|
||||||
InspectWindowClosedEvent>
|
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -37,20 +30,15 @@ public:
|
|||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
~BalancingWindow() override;
|
~BalancingWindow() override;
|
||||||
|
|
||||||
private:
|
|
||||||
void handleEvent(std::shared_ptr<const ArenaStartRequestedEvent> event) override;
|
|
||||||
void handleEvent(std::shared_ptr<const ArenaInspectRequestedEvent> event) override;
|
|
||||||
void handleEvent(std::shared_ptr<const InspectWindowClosedEvent> event) override;
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void pollStatuses();
|
void pollStatuses();
|
||||||
void reloadConfig();
|
void reloadConfig();
|
||||||
void startAll();
|
void startAll();
|
||||||
|
|
||||||
private:
|
|
||||||
void startArena(int index);
|
void startArena(int index);
|
||||||
void inspectArena(int index);
|
void inspectArena(int index);
|
||||||
void closeInspectWindow();
|
void closeInspectWindow();
|
||||||
|
|
||||||
|
private:
|
||||||
void populateArenas(const BalancingConfig& balancingConfig);
|
void populateArenas(const BalancingConfig& balancingConfig);
|
||||||
void stopAllArenas();
|
void stopAllArenas();
|
||||||
void updateButtons();
|
void updateButtons();
|
||||||
|
|||||||
@@ -10,9 +10,7 @@
|
|||||||
|
|
||||||
#include "ArenaView.h"
|
#include "ArenaView.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "EventManager.h"
|
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "InspectWindowClosedEvent.h"
|
|
||||||
#include "ModuleOwnerComponent.h"
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipStatsCalculator.h"
|
#include "ShipStatsCalculator.h"
|
||||||
@@ -78,6 +76,9 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const GameConfig* config,
|
|||||||
m_arenaView = new ArenaView(sim, visuals, this);
|
m_arenaView = new ArenaView(sim, visuals, this);
|
||||||
mainLayout->addWidget(m_arenaView, 1);
|
mainLayout->addWidget(m_arenaView, 1);
|
||||||
|
|
||||||
|
connect(m_arenaView, &ArenaView::speedChanged,
|
||||||
|
this, &InspectWindow::onSpeedChanged);
|
||||||
|
|
||||||
// Info panel (bottom)
|
// Info panel (bottom)
|
||||||
{
|
{
|
||||||
QWidget* infoPanel = new QWidget(this);
|
QWidget* infoPanel = new QWidget(this);
|
||||||
@@ -139,20 +140,19 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const GameConfig* config,
|
|||||||
|
|
||||||
setFocusPolicy(Qt::StrongFocus);
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
|
|
||||||
registerForEvents();
|
registerForEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
InspectWindow::~InspectWindow()
|
InspectWindow::~InspectWindow()
|
||||||
{
|
{
|
||||||
unregisterForEvents();
|
unregisterForEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InspectWindow::closeEvent(QCloseEvent* event)
|
void InspectWindow::closeEvent(QCloseEvent* event)
|
||||||
{
|
{
|
||||||
m_arenaView->stopRendering();
|
m_arenaView->stopRendering();
|
||||||
m_pollTimer->stop();
|
m_pollTimer->stop();
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
emit closed();
|
||||||
std::make_shared<InspectWindowClosedEvent>());
|
|
||||||
event->accept();
|
event->accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,11 +176,11 @@ void InspectWindow::onSpeedButton(int index)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InspectWindow::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event)
|
void InspectWindow::onSpeedChanged(double multiplier)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < kSpeedCount; ++i)
|
for (int i = 0; i < kSpeedCount; ++i)
|
||||||
{
|
{
|
||||||
const bool active = (std::abs(kSpeeds[i] - event->speed) < 0.001);
|
const bool active = (std::abs(kSpeeds[i] - multiplier) < 0.001);
|
||||||
m_speedButtons[static_cast<std::size_t>(i)]->setChecked(active);
|
m_speedButtons[static_cast<std::size_t>(i)]->setChecked(active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,13 @@
|
|||||||
#include "EntitySelectedEvent.h"
|
#include "EntitySelectedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "GameSpeedChangedEvent.h"
|
|
||||||
#include "VisualsConfig.h"
|
#include "VisualsConfig.h"
|
||||||
|
|
||||||
class ArenaView;
|
class ArenaView;
|
||||||
class ShipStatsPanel;
|
class ShipStatsPanel;
|
||||||
|
|
||||||
class InspectWindow : public QWidget,
|
class InspectWindow : public QWidget,
|
||||||
public CombinedEventHandler<EntitySelectedEvent,
|
public EventHandler<EntitySelectedEvent>
|
||||||
GameSpeedChangedEvent>
|
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -33,16 +31,19 @@ public:
|
|||||||
const std::string& arenaName, QWidget* parent = nullptr);
|
const std::string& arenaName, QWidget* parent = nullptr);
|
||||||
~InspectWindow() override;
|
~InspectWindow() override;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void closed();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void closeEvent(QCloseEvent* event) override;
|
void closeEvent(QCloseEvent* event) override;
|
||||||
void keyPressEvent(QKeyEvent* event) override;
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
|
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event) override;
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onSpeedButton(int index);
|
void onSpeedButton(int index);
|
||||||
|
void onSpeedChanged(double multiplier);
|
||||||
void pollStatus();
|
void pollStatus();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -268,8 +268,6 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
|||||||
cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m;
|
cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m;
|
||||||
cfg.tunnelMaxDistance_tiles = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles"));
|
cfg.tunnelMaxDistance_tiles = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles"));
|
||||||
cfg.departureIntervalSeconds = requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds");
|
cfg.departureIntervalSeconds = requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds");
|
||||||
cfg.orbitFactor = requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor");
|
|
||||||
cfg.rallyOrbitRadius_tiles = requireDouble(tbl["world"]["rally_orbit_radius_tiles"], file, "world.rally_orbit_radius_tiles");
|
|
||||||
|
|
||||||
cfg.regions.asteroidWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles"));
|
cfg.regions.asteroidWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles"));
|
||||||
cfg.regions.playerBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles"));
|
cfg.regions.playerBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles"));
|
||||||
@@ -297,10 +295,6 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
|||||||
throw makeError(file, "waves", "gap_min_seconds > gap_max_seconds");
|
throw makeError(file, "waves", "gap_min_seconds > gap_max_seconds");
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.targeting.targetScoreFormula = requireFormula(tbl["targeting"]["target_score_formula"], file, "targeting.target_score_formula");
|
|
||||||
cfg.targeting.overclaimPenaltyFormula = requireFormula(tbl["targeting"]["overclaim_penalty_formula"], file, "targeting.overclaim_penalty_formula");
|
|
||||||
cfg.targeting.hysteresis = requireDouble(tbl["targeting"]["target_hysteresis"], file, "targeting.target_hysteresis");
|
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,6 +429,14 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
|||||||
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
|
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Threat
|
||||||
|
{
|
||||||
|
const std::string tPath = elemPath + ".threat";
|
||||||
|
const toml::table& tTable = requireTable(mt["threat"], file, tPath);
|
||||||
|
toml::table& tMt = const_cast<toml::table&>(tTable);
|
||||||
|
def.threat.costFormula = requireFormula(tMt["cost_formula"], file, tPath + ".cost_formula");
|
||||||
|
}
|
||||||
|
|
||||||
// Health
|
// Health
|
||||||
{
|
{
|
||||||
const std::string hPath = elemPath + ".health";
|
const std::string hPath = elemPath + ".health";
|
||||||
@@ -585,6 +587,7 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|||||||
mt["player_production_level"], file, elemPath + ".player_production_level"));
|
mt["player_production_level"], file, elemPath + ".player_production_level"));
|
||||||
def.productionTimeSeconds = requireDouble(
|
def.productionTimeSeconds = requireDouble(
|
||||||
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
||||||
|
def.threatCost = requireDouble(mt["threat_cost"], file, elemPath + ".threat_cost");
|
||||||
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
||||||
def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph");
|
def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph");
|
||||||
|
|
||||||
@@ -701,6 +704,5 @@ GameConfig ConfigLoader::loadFromDirectory(const std::string& configDir)
|
|||||||
cfg.ships = loadShips(configDir + "/ships.toml");
|
cfg.ships = loadShips(configDir + "/ships.toml");
|
||||||
cfg.stations = loadStations(configDir + "/stations.toml");
|
cfg.stations = loadStations(configDir + "/stations.toml");
|
||||||
cfg.modules = loadModules(configDir + "/modules.toml");
|
cfg.modules = loadModules(configDir + "/modules.toml");
|
||||||
cfg.threatCosts = computeThreatCostTable(cfg);
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,6 @@
|
|||||||
|
|
||||||
#include "tinyexpr.h"
|
#include "tinyexpr.h"
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
// tinyexpr has no built-in min/max; expose them so config formulas can
|
|
||||||
// clamp (e.g. a floored overclaim penalty "max(0.5, 1 - 0.1*x)").
|
|
||||||
double formulaMin(double a, double b) { return a < b ? a : b; }
|
|
||||||
double formulaMax(double a, double b) { return a > b ? a : b; }
|
|
||||||
}
|
|
||||||
|
|
||||||
Formula::Formula(Formula&& other) noexcept
|
Formula::Formula(Formula&& other) noexcept
|
||||||
: m_source(std::move(other.m_source))
|
: m_source(std::move(other.m_source))
|
||||||
, m_x(std::move(other.m_x))
|
, m_x(std::move(other.m_x))
|
||||||
@@ -45,14 +37,11 @@ Formula Formula::compile(const std::string& source)
|
|||||||
result.m_x = std::make_unique<double>(0.0);
|
result.m_x = std::make_unique<double>(0.0);
|
||||||
|
|
||||||
const te_variable variables[] = {
|
const te_variable variables[] = {
|
||||||
{ "x", result.m_x.get(), TE_VARIABLE, nullptr },
|
{ "x", result.m_x.get(), 0, nullptr },
|
||||||
{ "min", reinterpret_cast<const void*>(&formulaMin), TE_FUNCTION2 | TE_FLAG_PURE, nullptr },
|
|
||||||
{ "max", reinterpret_cast<const void*>(&formulaMax), TE_FUNCTION2 | TE_FLAG_PURE, nullptr },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
int errorPos = 0;
|
int errorPos = 0;
|
||||||
const int variableCount = static_cast<int>(sizeof(variables) / sizeof(variables[0]));
|
result.m_expr = te_compile(result.m_source.c_str(), variables, 1, &errorPos);
|
||||||
result.m_expr = te_compile(result.m_source.c_str(), variables, variableCount, &errorPos);
|
|
||||||
|
|
||||||
if (result.m_expr == nullptr)
|
if (result.m_expr == nullptr)
|
||||||
{
|
{
|
||||||
@@ -77,4 +66,3 @@ double Formula::evaluate(double x) const
|
|||||||
*m_x = x;
|
*m_x = x;
|
||||||
return te_eval(m_expr);
|
return te_eval(m_expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
#include "StationsConfig.h"
|
#include "StationsConfig.h"
|
||||||
#include "ModulesConfig.h"
|
#include "ModulesConfig.h"
|
||||||
#include "ThreatCostCalculator.h"
|
|
||||||
|
|
||||||
// Aggregate of all simulation config files. Loaded at startup and reloaded
|
// Aggregate of all simulation config files. Loaded at startup and reloaded
|
||||||
// from disk on each game restart (REQ-CFG-RELOAD). See architecture.md "Config Loading".
|
// from disk on each game restart (REQ-CFG-RELOAD). See architecture.md "Config Loading".
|
||||||
@@ -18,5 +17,4 @@ struct GameConfig
|
|||||||
ShipsConfig ships;
|
ShipsConfig ships;
|
||||||
StationsConfig stations;
|
StationsConfig stations;
|
||||||
ModulesConfig modules;
|
ModulesConfig modules;
|
||||||
ThreatCostTable threatCosts;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ struct ModuleDef
|
|||||||
std::vector<RecipeIngredient> materials;
|
std::vector<RecipeIngredient> materials;
|
||||||
int playerProductionLevel;
|
int playerProductionLevel;
|
||||||
double productionTimeSeconds;
|
double productionTimeSeconds;
|
||||||
|
double threatCost;
|
||||||
std::string fillColor;
|
std::string fillColor;
|
||||||
std::string glyph;
|
std::string glyph;
|
||||||
std::vector<ModuleStatModifier> statModifiers;
|
std::vector<ModuleStatModifier> statModifiers;
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ struct ShipSchematic
|
|||||||
double productionTimeSeconds;
|
double productionTimeSeconds;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Wave scheduling cost (REQ-WAV-THREAT-COST). Ships with cost_formula that
|
||||||
|
// always evaluates to 0 are ineligible as wave picks.
|
||||||
|
struct ShipThreat
|
||||||
|
{
|
||||||
|
Formula costFormula;
|
||||||
|
};
|
||||||
|
|
||||||
struct ShipHealth
|
struct ShipHealth
|
||||||
{
|
{
|
||||||
Formula hpFormula; // REQ-SHP-STATS
|
Formula hpFormula; // REQ-SHP-STATS
|
||||||
@@ -48,6 +55,7 @@ struct ShipDef
|
|||||||
std::vector<std::string> layout;
|
std::vector<std::string> layout;
|
||||||
|
|
||||||
ShipSchematic schematic;
|
ShipSchematic schematic;
|
||||||
|
ShipThreat threat;
|
||||||
ShipHealth health;
|
ShipHealth health;
|
||||||
ShipMovement movement;
|
ShipMovement movement;
|
||||||
ShipSensor sensor;
|
ShipSensor sensor;
|
||||||
|
|||||||
@@ -39,14 +39,6 @@ struct WorldWaves
|
|||||||
double bossQuietAfterSeconds; // suppress normal waves this long after boss (REQ-WAV-QUIET)
|
double bossQuietAfterSeconds; // suppress normal waves this long after boss (REQ-WAV-QUIET)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ship target selection (claim-aware scoring).
|
|
||||||
struct WorldTargeting
|
|
||||||
{
|
|
||||||
Formula targetScoreFormula; // x = distance / max weapon range; higher = better
|
|
||||||
Formula overclaimPenaltyFormula; // x = competing claim count; factor in [0,1]
|
|
||||||
double hysteresis; // fractional margin a challenger must beat the current target by
|
|
||||||
};
|
|
||||||
|
|
||||||
struct WorldConfig
|
struct WorldConfig
|
||||||
{
|
{
|
||||||
int heightTiles; // REQ-GW-HEIGHT
|
int heightTiles; // REQ-GW-HEIGHT
|
||||||
@@ -57,12 +49,9 @@ struct WorldConfig
|
|||||||
double beltSpeed_tps; // REQ-GW-BELT-SPEED (tiles/s, converted from m/s in config)
|
double beltSpeed_tps; // REQ-GW-BELT-SPEED (tiles/s, converted from m/s in config)
|
||||||
int tunnelMaxDistance_tiles; // REQ-BLD-TUNNEL-PAIR
|
int tunnelMaxDistance_tiles; // REQ-BLD-TUNNEL-PAIR
|
||||||
double departureIntervalSeconds; // REQ-SHP-RALLY
|
double departureIntervalSeconds; // REQ-SHP-RALLY
|
||||||
double orbitFactor; // REQ-SHP-ORBIT (multiplies tool range for orbit radius)
|
|
||||||
double rallyOrbitRadius_tiles; // REQ-SHP-ORBIT (fixed orbit radius around the rally point)
|
|
||||||
|
|
||||||
WorldRegions regions;
|
WorldRegions regions;
|
||||||
WorldExpansion expansion;
|
WorldExpansion expansion;
|
||||||
WorldPush push;
|
WorldPush push;
|
||||||
WorldWaves waves;
|
WorldWaves waves;
|
||||||
WorldTargeting targeting;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.h
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Blueprint.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Blueprint.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingId.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingId.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FireEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Item.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Item.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Port.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Port.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicDropEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
|
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,7 +18,6 @@ SET(SRCS
|
|||||||
${SRCS}
|
${SRCS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
#include "DisplayName.h"
|
|
||||||
|
|
||||||
#include <cctype>
|
|
||||||
|
|
||||||
std::string toDisplayName(const std::string& id)
|
|
||||||
{
|
|
||||||
std::string result;
|
|
||||||
bool nextUpper = true;
|
|
||||||
for (char c : id)
|
|
||||||
{
|
|
||||||
if (c == '_')
|
|
||||||
{
|
|
||||||
result += ' ';
|
|
||||||
nextUpper = true;
|
|
||||||
}
|
|
||||||
else if (nextUpper)
|
|
||||||
{
|
|
||||||
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
|
||||||
nextUpper = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
std::string toDisplayName(const std::string& id);
|
|
||||||
15
src/lib/core/FireEvent.h
Normal file
15
src/lib/core/FireEvent.h
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
// Transient record emitted each time a weapon fires (REQ-SHP-FIRING,
|
||||||
|
// REQ-SHP-FIRING-BEAM). Buffered in a sim-owned queue and drained by the
|
||||||
|
// renderer each frame to draw the 0.3-second laser beam.
|
||||||
|
struct FireEvent
|
||||||
|
{
|
||||||
|
entt::entity shooter;
|
||||||
|
entt::entity target;
|
||||||
|
Tick emittedAt;
|
||||||
|
};
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
enum class SchematicType
|
|
||||||
{
|
|
||||||
Ship,
|
|
||||||
Module,
|
|
||||||
Recipe
|
|
||||||
};
|
|
||||||
|
|
||||||
// One option presented to the player in the schematic choice dialog
|
|
||||||
// (REQ-DEF-SCHEMATIC-DROP). Built by the simulation when enemy stations are
|
|
||||||
// destroyed; the UI reads these to populate the dialog.
|
|
||||||
struct SchematicChoiceOption
|
|
||||||
{
|
|
||||||
std::string schematicId;
|
|
||||||
SchematicType type;
|
|
||||||
std::string displayName;
|
|
||||||
bool isNewUnlock;
|
|
||||||
int targetLevel;
|
|
||||||
|
|
||||||
// Display names of items produced by recipes that would newly become
|
|
||||||
// implicitly unlocked (REQ-LOCK-IMPLICIT) if this option is selected.
|
|
||||||
// Deduplicated and sorted alphabetically; empty if none.
|
|
||||||
std::vector<std::string> newlyUnlockedItemNames;
|
|
||||||
};
|
|
||||||
15
src/lib/core/SchematicDropEvent.h
Normal file
15
src/lib/core/SchematicDropEvent.h
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Emitted in tick step 9 (Deaths & loot) when a destroyed enemy-defence-station
|
||||||
|
// set awards a schematic (REQ-DEF-SCHEMATIC-DROP). The UI renders a toast
|
||||||
|
// (REQ-UI-SCHEMATIC-TOAST); wasNewUnlock chooses between the "unlocked" and
|
||||||
|
// "level -> N" wording. isModuleSchematic selects ship vs. module toast text.
|
||||||
|
struct SchematicDropEvent
|
||||||
|
{
|
||||||
|
std::string schematicId; // matches ShipDef::id or ModuleDef::id in the config.
|
||||||
|
int newLevel;
|
||||||
|
bool wasNewUnlock;
|
||||||
|
bool isModuleSchematic;
|
||||||
|
};
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
// Baseline fallback behavior, present on every ship. The executor moves the ship
|
|
||||||
// toward the opposing side (direction derived from FactionComponent), so a ship
|
|
||||||
// with no better behavior keeps advancing.
|
|
||||||
struct AdvanceBehavior
|
|
||||||
{
|
|
||||||
float score = 0.0f;
|
|
||||||
};
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
|
||||||
|
|
||||||
// Combat behavior for ships with weapons (was ThreatResponseBehaviorComponent).
|
|
||||||
// The evaluator sets currentTarget; the executor pushes it to in-range weapons.
|
|
||||||
struct AttackBehavior
|
|
||||||
{
|
|
||||||
std::optional<entt::entity> currentTarget;
|
|
||||||
float orbitRadius_tiles = 0.0f; // REQ-SHP-ORBIT
|
|
||||||
float score = 0.0f;
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
// Identifies a ship behavior. Written into SelectedBehaviorComponent by the
|
|
||||||
// AiSystem selection pass so each behavior's executor can tell whether it won.
|
|
||||||
enum class BehaviorKind
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
Advance,
|
|
||||||
Rally,
|
|
||||||
Retreat,
|
|
||||||
Attack,
|
|
||||||
Repair,
|
|
||||||
SalvageScrap,
|
|
||||||
DeliverScrap
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
// Score bands for ship-behavior evaluation. The AiSystem selection pass picks
|
|
||||||
// the behavior with the highest score per ship; these constants define a single
|
|
||||||
// comparable scale so the desired priority falls out:
|
|
||||||
// Retreat > Attack > Repair / Salvage / Deliver > Rally > Advance.
|
|
||||||
// Evaluators may return kInactive when their behavior does not apply this tick.
|
|
||||||
namespace BehaviorScores
|
|
||||||
{
|
|
||||||
constexpr float kInactive = 0.0f;
|
|
||||||
constexpr float kAdvance = 0.05f; // baseline fallback; always present
|
|
||||||
constexpr float kRally = 0.20f;
|
|
||||||
constexpr float kDeliver = 0.50f; // cargo full
|
|
||||||
constexpr float kRepair = 0.55f;
|
|
||||||
constexpr float kSalvage = 0.55f; // cargo not full and scrap in range
|
|
||||||
constexpr float kAttack = 0.60f; // healthy and target in sensor range
|
|
||||||
constexpr float kRetreat = 0.90f;
|
|
||||||
|
|
||||||
// Health fraction at/below which a ship is considered "low HP" — used by the
|
|
||||||
// Attack evaluator (do not attack when low) and the Retreat evaluator.
|
|
||||||
constexpr float kLowHpFraction = 0.3f;
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,24 @@
|
|||||||
SET(HDRS
|
SET(HDRS
|
||||||
${HDRS}
|
${HDRS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/AdvanceBehavior.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/AttackBehavior.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BehaviorKind.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BehaviorScores.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DeliverScrapBehavior.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DespawnAtComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/DespawnAtComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodyComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodyComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FacingComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/FacingComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FactionComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/FactionComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/HealthComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/HealthComponent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/HomeReturnBehaviorComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/HqProxyComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/HqProxyComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/PositionComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/PositionComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RallyBehavior.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RallyBehaviorComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RepairBehavior.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RepairBehaviorComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RepairToolComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RepairToolComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RetreatBehavior.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SalvageBehaviorComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SalvageCargoComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SalvageCargoComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SalvageScrapBehavior.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ScrapDataComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ScrapDataComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBehaviorComponent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SensorRangeComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SensorRangeComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipIdentityComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipIdentityComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/StationBodyComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/StationBodyComponent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ThreatResponseBehaviorComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WeaponComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WeaponComponent.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "BuildingId.h"
|
|
||||||
|
|
||||||
// Deliver-scrap behavior (one half of the old SalvageBehaviorComponent). Scored
|
|
||||||
// high only when cargo is full. The evaluator assigns the nearest SalvageBay;
|
|
||||||
// SalvagerSystem performs the actual delivery.
|
|
||||||
struct DeliverScrapBehavior
|
|
||||||
{
|
|
||||||
BuildingId deliveryBay = kInvalidBuildingId;
|
|
||||||
float score = 0.0f;
|
|
||||||
};
|
|
||||||
9
src/lib/ecs/component/HomeReturnBehaviorComponent.h
Normal file
9
src/lib/ecs/component/HomeReturnBehaviorComponent.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QVector2D>
|
||||||
|
|
||||||
|
struct HomeReturnBehaviorComponent
|
||||||
|
{
|
||||||
|
float retreatHpFraction;
|
||||||
|
QVector2D homePos;
|
||||||
|
};
|
||||||
@@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
// The winning behavior's executor writes this each tick before movement runs.
|
// A ship-behavior system writes this each tick before movement runs; the
|
||||||
// `active` is false when no behavior set a destination (the ship brakes); the
|
// highest-priority write wins. Priority order is fixed globally — see
|
||||||
// score-based selection (see architecture.md "Movement Arbitration") decides
|
// architecture.md "Movement Arbitration".
|
||||||
// which single executor writes here.
|
|
||||||
struct MovementIntentComponent
|
struct MovementIntentComponent
|
||||||
{
|
{
|
||||||
bool active = false;
|
int priority;
|
||||||
QVector2D target;
|
QVector2D target;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QVector2D>
|
|
||||||
|
|
||||||
// Player combat ships loiter at the rally point until the departure timer
|
|
||||||
// removes this component (ShipSystem::triggerRallyDeparture).
|
|
||||||
struct RallyBehavior
|
|
||||||
{
|
|
||||||
QVector2D rallyPoint;
|
|
||||||
float orbitRadius_tiles = 0.0f; // REQ-SHP-ORBIT
|
|
||||||
float score = 0.0f;
|
|
||||||
};
|
|
||||||
8
src/lib/ecs/component/RallyBehaviorComponent.h
Normal file
8
src/lib/ecs/component/RallyBehaviorComponent.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QVector2D>
|
||||||
|
|
||||||
|
struct RallyBehaviorComponent
|
||||||
|
{
|
||||||
|
QVector2D rallyPoint;
|
||||||
|
};
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
|
||||||
|
|
||||||
// Repair behavior for ships with repair modules. The evaluator picks the nearest
|
|
||||||
// damaged friendly as currentTarget; the executor moves toward it and assigns
|
|
||||||
// in-range repair tools. RepairSystem applies the actual healing.
|
|
||||||
struct RepairBehavior
|
|
||||||
{
|
|
||||||
std::optional<entt::entity> currentTarget;
|
|
||||||
float maxRepairRange_tiles = 0.0f;
|
|
||||||
float orbitRadius_tiles = 0.0f; // REQ-SHP-ORBIT
|
|
||||||
float score = 0.0f;
|
|
||||||
};
|
|
||||||
11
src/lib/ecs/component/RepairBehaviorComponent.h
Normal file
11
src/lib/ecs/component/RepairBehaviorComponent.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
struct RepairBehaviorComponent
|
||||||
|
{
|
||||||
|
std::optional<entt::entity> currentTarget;
|
||||||
|
float maxRepairRange_tiles = 0.0f;
|
||||||
|
};
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QVector2D>
|
|
||||||
|
|
||||||
// Player-only retreat behavior (replaces HomeReturnBehaviorComponent). Scored
|
|
||||||
// high when HP is low, or when an enemy is in sensor range and the ship cannot
|
|
||||||
// fight back. The executor moves the ship to retreatPoint (the rally point).
|
|
||||||
struct RetreatBehavior
|
|
||||||
{
|
|
||||||
float retreatHpFraction = 0.0f;
|
|
||||||
QVector2D retreatPoint;
|
|
||||||
float score = 0.0f;
|
|
||||||
};
|
|
||||||
14
src/lib/ecs/component/SalvageBehaviorComponent.h
Normal file
14
src/lib/ecs/component/SalvageBehaviorComponent.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <QVector2D>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
|
||||||
|
struct SalvageBehaviorComponent
|
||||||
|
{
|
||||||
|
std::optional<QVector2D> scrapTarget;
|
||||||
|
BuildingId deliveryBay; // kInvalidBuildingId until assigned at a salvage bay
|
||||||
|
float maxCollectionRange_tiles = 0.0f;
|
||||||
|
};
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
#include <QVector2D>
|
|
||||||
|
|
||||||
// Collect-scrap behavior (one half of the old SalvageBehaviorComponent). The
|
|
||||||
// evaluator finds the nearest scrap and sets scrapTarget when cargo is not full.
|
|
||||||
struct SalvageScrapBehavior
|
|
||||||
{
|
|
||||||
std::optional<QVector2D> scrapTarget;
|
|
||||||
float maxCollectionRange_tiles = 0.0f;
|
|
||||||
float orbitRadius_tiles = 0.0f; // REQ-SHP-ORBIT
|
|
||||||
float score = 0.0f;
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "BehaviorKind.h"
|
|
||||||
|
|
||||||
// Result of the AiSystem selection pass: the highest-scoring behavior for a
|
|
||||||
// ship this tick. Each behavior's executor acts only when it is the winner.
|
|
||||||
struct SelectedBehaviorComponent
|
|
||||||
{
|
|
||||||
BehaviorKind winner = BehaviorKind::None;
|
|
||||||
float bestScore = 0.0f;
|
|
||||||
};
|
|
||||||
10
src/lib/ecs/component/ThreatResponseBehaviorComponent.h
Normal file
10
src/lib/ecs/component/ThreatResponseBehaviorComponent.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
struct ThreatResponseBehaviorComponent
|
||||||
|
{
|
||||||
|
std::optional<entt::entity> currentTarget;
|
||||||
|
};
|
||||||
@@ -1,89 +1,587 @@
|
|||||||
#include "AiSystem.h"
|
#include "AiSystem.h"
|
||||||
|
|
||||||
#include <limits>
|
#include <optional>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "GameConfig.h"
|
#include <QVector2D>
|
||||||
|
|
||||||
#include "AdvanceBehavior.h"
|
#include "Building.h"
|
||||||
#include "AttackBehavior.h"
|
#include "BuildingSystem.h"
|
||||||
#include "BehaviorKind.h"
|
#include "BuildingType.h"
|
||||||
#include "DeliverScrapBehavior.h"
|
#include "BuildingId.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "RallyBehavior.h"
|
#include "FactionComponent.h"
|
||||||
#include "RepairBehavior.h"
|
#include "HealthComponent.h"
|
||||||
#include "RetreatBehavior.h"
|
#include "HomeReturnBehaviorComponent.h"
|
||||||
#include "SalvageScrapBehavior.h"
|
#include "HqProxyComponent.h"
|
||||||
#include "SelectedBehaviorComponent.h"
|
#include "ModuleOwnerComponent.h"
|
||||||
|
#include "MovementIntentComponent.h"
|
||||||
|
#include "PositionComponent.h"
|
||||||
|
#include "RallyBehaviorComponent.h"
|
||||||
|
#include "RepairBehaviorComponent.h"
|
||||||
|
#include "RepairToolComponent.h"
|
||||||
|
#include "SalvageBehaviorComponent.h"
|
||||||
|
#include "SalvageCargoComponent.h"
|
||||||
|
#include "ScrapSystem.h"
|
||||||
|
#include "SensorRangeComponent.h"
|
||||||
|
#include "ShipIdentityComponent.h"
|
||||||
|
#include "StationBodyComponent.h"
|
||||||
|
#include "ThreatResponseBehaviorComponent.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
|
|
||||||
namespace
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shared helpers for repair targeting
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct RepairableInfo
|
||||||
{
|
{
|
||||||
// Records a behavior's score for its owning ship, keeping the highest seen.
|
entt::entity entity;
|
||||||
// Considered high-priority first, so strict '>' breaks ties toward priority.
|
QVector2D position;
|
||||||
template <typename Behavior>
|
bool isEnemy;
|
||||||
void consider(EntityAdmin& admin, BehaviorKind kind)
|
bool isShip;
|
||||||
|
float hp;
|
||||||
|
float maxHp;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::vector<RepairableInfo> buildRepairables(EntityAdmin& admin)
|
||||||
|
{
|
||||||
|
std::vector<RepairableInfo> repairables;
|
||||||
|
|
||||||
|
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent, HealthComponent>(
|
||||||
|
[&repairables](entt::entity e, const ShipIdentityComponent& /*si*/,
|
||||||
|
const PositionComponent& pos, const FactionComponent& f,
|
||||||
|
const HealthComponent& h)
|
||||||
{
|
{
|
||||||
admin.forEach<Behavior, SelectedBehaviorComponent>(
|
repairables.push_back({e, pos.value, f.isEnemy, true, h.hp, h.maxHp});
|
||||||
[kind](entt::entity /*e*/, const Behavior& behavior,
|
});
|
||||||
SelectedBehaviorComponent& selected)
|
|
||||||
|
admin.forEach<StationBodyComponent, PositionComponent, FactionComponent, HealthComponent>(
|
||||||
|
[&repairables](entt::entity e, const StationBodyComponent& /*sb*/,
|
||||||
|
const PositionComponent& pos, const FactionComponent& f,
|
||||||
|
const HealthComponent& h)
|
||||||
{
|
{
|
||||||
if (behavior.score > selected.bestScore)
|
repairables.push_back({e, pos.value, f.isEnemy, false, h.hp, h.maxHp});
|
||||||
|
});
|
||||||
|
|
||||||
|
return repairables;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tickHomeReturnBehavior (priority 4)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void AiSystem::tickHomeReturnBehavior(EntityAdmin& admin)
|
||||||
|
{
|
||||||
|
TRACE();
|
||||||
|
admin.forEach<HomeReturnBehaviorComponent, HealthComponent, MovementIntentComponent>(
|
||||||
|
[](entt::entity /*e*/, const HomeReturnBehaviorComponent& homeReturnBehavior,
|
||||||
|
const HealthComponent& h, MovementIntentComponent& intent)
|
||||||
{
|
{
|
||||||
selected.bestScore = behavior.score;
|
if (h.hp / h.maxHp < homeReturnBehavior.retreatHpFraction)
|
||||||
selected.winner = kind;
|
{
|
||||||
|
if (4 > intent.priority)
|
||||||
|
{
|
||||||
|
intent = MovementIntentComponent{4, homeReturnBehavior.homePos};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tickThreatResponseBehavior (priority 3)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void AiSystem::tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSystem& buildings)
|
||||||
|
{
|
||||||
|
TRACE();
|
||||||
|
// Snapshot all combatant entities for target acquisition.
|
||||||
|
struct CombatantInfo
|
||||||
|
{
|
||||||
|
entt::entity entity;
|
||||||
|
QVector2D position;
|
||||||
|
bool isEnemy;
|
||||||
|
bool isStation;
|
||||||
|
};
|
||||||
|
std::vector<CombatantInfo> combatants;
|
||||||
|
|
||||||
|
admin.forEach<PositionComponent, FactionComponent, ShipIdentityComponent>(
|
||||||
|
[&combatants](entt::entity e, const PositionComponent& pos,
|
||||||
|
const FactionComponent& f, const ShipIdentityComponent& /*si*/)
|
||||||
|
{
|
||||||
|
combatants.push_back({e, pos.value, f.isEnemy, false});
|
||||||
|
});
|
||||||
|
|
||||||
|
admin.forEach<PositionComponent, FactionComponent, StationBodyComponent>(
|
||||||
|
[&combatants](entt::entity e, const PositionComponent& pos,
|
||||||
|
const FactionComponent& f, const StationBodyComponent& /*sb*/)
|
||||||
|
{
|
||||||
|
combatants.push_back({e, pos.value, f.isEnemy, true});
|
||||||
|
});
|
||||||
|
|
||||||
|
admin.forEach<PositionComponent, FactionComponent, HqProxyComponent>(
|
||||||
|
[&combatants](entt::entity e, const PositionComponent& pos,
|
||||||
|
const FactionComponent& f, const HqProxyComponent& /*hq*/)
|
||||||
|
{
|
||||||
|
combatants.push_back({e, pos.value, f.isEnemy, true});
|
||||||
|
});
|
||||||
|
|
||||||
|
admin.forEach<ThreatResponseBehaviorComponent, PositionComponent, FactionComponent,
|
||||||
|
SensorRangeComponent, MovementIntentComponent>(
|
||||||
|
[&](entt::entity e, ThreatResponseBehaviorComponent& threatResponseBehavior,
|
||||||
|
PositionComponent& pos, FactionComponent& faction,
|
||||||
|
SensorRangeComponent& sensor, MovementIntentComponent& intent)
|
||||||
|
{
|
||||||
|
const float range = sensor.value_tiles;
|
||||||
|
|
||||||
|
// Validate current target.
|
||||||
|
bool targetValid = false;
|
||||||
|
if (threatResponseBehavior.currentTarget)
|
||||||
|
{
|
||||||
|
const entt::entity t = *threatResponseBehavior.currentTarget;
|
||||||
|
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
||||||
|
{
|
||||||
|
const float dist =
|
||||||
|
(admin.get<PositionComponent>(t).value - pos.value).length();
|
||||||
|
if (dist <= range)
|
||||||
|
{
|
||||||
|
targetValid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetValid)
|
||||||
|
{
|
||||||
|
threatResponseBehavior.currentTarget = std::nullopt;
|
||||||
|
float bestDist = range;
|
||||||
|
|
||||||
|
for (const CombatantInfo& c : combatants)
|
||||||
|
{
|
||||||
|
if (c.entity == e) { continue; }
|
||||||
|
|
||||||
|
bool isValidTarget = false;
|
||||||
|
if (!faction.isEnemy)
|
||||||
|
{
|
||||||
|
isValidTarget = c.isEnemy;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
isValidTarget = !c.isEnemy;
|
||||||
|
}
|
||||||
|
if (!isValidTarget) { continue; }
|
||||||
|
|
||||||
|
const float dist = (c.position - pos.value).length();
|
||||||
|
if (dist < bestDist)
|
||||||
|
{
|
||||||
|
bestDist = dist;
|
||||||
|
threatResponseBehavior.currentTarget = c.entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threatResponseBehavior.currentTarget)
|
||||||
|
{
|
||||||
|
const entt::entity t = *threatResponseBehavior.currentTarget;
|
||||||
|
QVector2D dest = pos.value;
|
||||||
|
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
||||||
|
{
|
||||||
|
dest = admin.get<PositionComponent>(t).value;
|
||||||
|
}
|
||||||
|
if (3 > intent.priority)
|
||||||
|
{
|
||||||
|
intent = MovementIntentComponent{3, dest};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (3 > intent.priority)
|
||||||
|
{
|
||||||
|
if (admin.hasAll<RallyBehaviorComponent>(e))
|
||||||
|
{
|
||||||
|
intent = MovementIntentComponent{
|
||||||
|
3, admin.get<RallyBehaviorComponent>(e).rallyPoint};
|
||||||
|
}
|
||||||
|
else if (!faction.isEnemy)
|
||||||
|
{
|
||||||
|
intent = MovementIntentComponent{
|
||||||
|
3, QVector2D(pos.value.x() + 1000.0f, pos.value.y())};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
intent = MovementIntentComponent{
|
||||||
|
3, QVector2D(-10000.0f, pos.value.y())};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tickRepairBehavior (priority 2)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
|
||||||
|
{
|
||||||
|
TRACE();
|
||||||
|
std::vector<RepairableInfo> repairables = buildRepairables(admin);
|
||||||
|
|
||||||
|
// Snapshot enemy ships for threat detection.
|
||||||
|
struct EnemyInfo
|
||||||
|
{
|
||||||
|
QVector2D position;
|
||||||
|
};
|
||||||
|
std::vector<EnemyInfo> enemies;
|
||||||
|
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>(
|
||||||
|
[&enemies](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
||||||
|
const PositionComponent& pos, const FactionComponent& f)
|
||||||
|
{
|
||||||
|
if (f.isEnemy)
|
||||||
|
{
|
||||||
|
enemies.push_back({pos.value});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
admin.forEach<RepairBehaviorComponent, PositionComponent,
|
||||||
|
FactionComponent, SensorRangeComponent, MovementIntentComponent>(
|
||||||
|
[&](entt::entity e, RepairBehaviorComponent& rb,
|
||||||
|
PositionComponent& pos, FactionComponent& /*faction*/,
|
||||||
|
SensorRangeComponent& sensor, MovementIntentComponent& intent)
|
||||||
|
{
|
||||||
|
// Flee if enemy nearby.
|
||||||
|
bool enemyNearby = false;
|
||||||
|
for (const EnemyInfo& enemy : enemies)
|
||||||
|
{
|
||||||
|
if ((enemy.position - pos.value).length() <= sensor.value_tiles)
|
||||||
|
{
|
||||||
|
enemyNearby = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (enemyNearby)
|
||||||
|
{
|
||||||
|
if (2 > intent.priority)
|
||||||
|
{
|
||||||
|
intent = MovementIntentComponent{
|
||||||
|
2, QVector2D(-10000.0f, pos.value.y())};
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate current target.
|
||||||
|
bool targetValid = false;
|
||||||
|
if (rb.currentTarget)
|
||||||
|
{
|
||||||
|
const entt::entity t = *rb.currentTarget;
|
||||||
|
if (admin.isValid(t) && admin.hasAll<HealthComponent>(t))
|
||||||
|
{
|
||||||
|
const HealthComponent& th = admin.get<HealthComponent>(t);
|
||||||
|
if (th.hp > 0.0f && th.hp < th.maxHp)
|
||||||
|
{
|
||||||
|
targetValid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetValid)
|
||||||
|
{
|
||||||
|
rb.currentTarget = std::nullopt;
|
||||||
|
float bestDist = sensor.value_tiles;
|
||||||
|
|
||||||
|
for (const RepairableInfo& r : repairables)
|
||||||
|
{
|
||||||
|
if (r.entity == e) { continue; }
|
||||||
|
if (r.isEnemy) { continue; }
|
||||||
|
if (r.hp >= r.maxHp) { continue; }
|
||||||
|
const float dist = (r.position - pos.value).length();
|
||||||
|
if (dist < bestDist)
|
||||||
|
{
|
||||||
|
bestDist = dist;
|
||||||
|
rb.currentTarget = r.entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rb.currentTarget)
|
||||||
|
{
|
||||||
|
if (2 > intent.priority)
|
||||||
|
{
|
||||||
|
intent = MovementIntentComponent{
|
||||||
|
2, QVector2D(pos.value.x() + 1000.0f, pos.value.y())};
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entt::entity target = *rb.currentTarget;
|
||||||
|
QVector2D targetPos = pos.value;
|
||||||
|
if (admin.isValid(target) && admin.hasAll<PositionComponent>(target))
|
||||||
|
{
|
||||||
|
targetPos = admin.get<PositionComponent>(target).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (2 > intent.priority)
|
||||||
|
{
|
||||||
|
intent = MovementIntentComponent{2, targetPos};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tickRepairTools
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void AiSystem::tickRepairTools(EntityAdmin& admin)
|
||||||
|
{
|
||||||
|
TRACE();
|
||||||
|
const std::vector<RepairableInfo> repairables = buildRepairables(admin);
|
||||||
|
|
||||||
|
admin.forEach<RepairToolComponent, ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity /*e*/, RepairToolComponent& rt, const ModuleOwnerComponent& owner)
|
||||||
|
{
|
||||||
|
if (!admin.hasAll<RepairBehaviorComponent>(owner.owner)) { return; }
|
||||||
|
|
||||||
|
const RepairBehaviorComponent& rb =
|
||||||
|
admin.get<RepairBehaviorComponent>(owner.owner);
|
||||||
|
const PositionComponent& ownerPos =
|
||||||
|
admin.get<PositionComponent>(owner.owner);
|
||||||
|
|
||||||
|
// Try the ship's preferred nav target first.
|
||||||
|
if (rb.currentTarget)
|
||||||
|
{
|
||||||
|
const entt::entity preferred = *rb.currentTarget;
|
||||||
|
if (admin.isValid(preferred) && admin.hasAll<HealthComponent>(preferred)
|
||||||
|
&& admin.hasAll<PositionComponent>(preferred))
|
||||||
|
{
|
||||||
|
HealthComponent& th = admin.get<HealthComponent>(preferred);
|
||||||
|
const float dist =
|
||||||
|
(admin.get<PositionComponent>(preferred).value
|
||||||
|
- ownerPos.value).length();
|
||||||
|
if (th.hp > 0.0f && th.hp < th.maxHp && dist <= rt.range_tiles)
|
||||||
|
{
|
||||||
|
rt.currentTarget = rb.currentTarget;
|
||||||
|
th.hp = std::min(th.hp + rt.ratePerTick, th.maxHp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preferred target unavailable; scan for nearest damaged friendly in range.
|
||||||
|
rt.currentTarget = std::nullopt;
|
||||||
|
float bestDist = rt.range_tiles;
|
||||||
|
for (const RepairableInfo& r : repairables)
|
||||||
|
{
|
||||||
|
if (r.isEnemy) { continue; }
|
||||||
|
if (r.hp <= 0.0f || r.hp >= r.maxHp) { continue; }
|
||||||
|
const float dist = (r.position - ownerPos.value).length();
|
||||||
|
if (dist < bestDist)
|
||||||
|
{
|
||||||
|
bestDist = dist;
|
||||||
|
rt.currentTarget = r.entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rt.currentTarget) { return; }
|
||||||
|
|
||||||
|
HealthComponent& targetHealth =
|
||||||
|
admin.get<HealthComponent>(*rt.currentTarget);
|
||||||
|
targetHealth.hp = std::min(targetHealth.hp + rt.ratePerTick, targetHealth.maxHp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tickSalvageBehavior (priority 1)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
|
||||||
|
BuildingSystem& buildings)
|
||||||
|
{
|
||||||
|
TRACE();
|
||||||
|
// Snapshot enemy ships for threat detection.
|
||||||
|
struct EnemyShipPos
|
||||||
|
{
|
||||||
|
QVector2D position;
|
||||||
|
};
|
||||||
|
std::vector<EnemyShipPos> enemyShips;
|
||||||
|
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>(
|
||||||
|
[&enemyShips](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
||||||
|
const PositionComponent& pos, const FactionComponent& f)
|
||||||
|
{
|
||||||
|
if (f.isEnemy)
|
||||||
|
{
|
||||||
|
enemyShips.push_back({pos.value});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aggregate cargo across all salvage-module children per owning ship.
|
||||||
|
struct AggregatedCargo
|
||||||
|
{
|
||||||
|
int totalCurrent = 0;
|
||||||
|
int totalCapacity = 0;
|
||||||
|
};
|
||||||
|
std::unordered_map<entt::entity, AggregatedCargo> cargoByShip;
|
||||||
|
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity /*ce*/, const SalvageCargoComponent& c, const ModuleOwnerComponent& o)
|
||||||
|
{
|
||||||
|
AggregatedCargo& agg = cargoByShip[o.owner];
|
||||||
|
agg.totalCurrent += c.current;
|
||||||
|
agg.totalCapacity += c.capacity;
|
||||||
|
});
|
||||||
|
|
||||||
|
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
|
||||||
|
|
||||||
|
// Tick down per-module collection cooldowns.
|
||||||
|
admin.forEach<SalvageCargoComponent>(
|
||||||
|
[](entt::entity /*e*/, SalvageCargoComponent& c)
|
||||||
|
{
|
||||||
|
if (c.cooldownTicksRemaining > 0) { --c.cooldownTicksRemaining; }
|
||||||
|
});
|
||||||
|
|
||||||
|
admin.forEach<SalvageBehaviorComponent, PositionComponent,
|
||||||
|
SensorRangeComponent, MovementIntentComponent>(
|
||||||
|
[&](entt::entity e, SalvageBehaviorComponent& salvageBehavior,
|
||||||
|
PositionComponent& pos,
|
||||||
|
SensorRangeComponent& sensor, MovementIntentComponent& intent)
|
||||||
|
{
|
||||||
|
const float collectRange = salvageBehavior.maxCollectionRange_tiles;
|
||||||
|
const AggregatedCargo& cargoState = cargoByShip[e];
|
||||||
|
|
||||||
|
// Assign nearest SalvageBay if needed.
|
||||||
|
if (salvageBehavior.deliveryBay == kInvalidBuildingId)
|
||||||
|
{
|
||||||
|
const Building* bay = buildings.findNearestBuilding(pos.value,
|
||||||
|
BuildingType::SalvageBay);
|
||||||
|
if (bay)
|
||||||
|
{
|
||||||
|
salvageBehavior.deliveryBay = bay->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BuildingId bayId = salvageBehavior.deliveryBay;
|
||||||
|
|
||||||
|
QVector2D bayPos = pos.value;
|
||||||
|
if (bayId != kInvalidBuildingId)
|
||||||
|
{
|
||||||
|
const Building* bay = buildings.findBuilding(bayId);
|
||||||
|
if (bay)
|
||||||
|
{
|
||||||
|
bayPos = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f,
|
||||||
|
bay->anchor.y() + bay->footprint.height() / 2.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool cargoFull = (cargoState.totalCurrent >= cargoState.totalCapacity
|
||||||
|
&& cargoState.totalCapacity > 0);
|
||||||
|
|
||||||
|
if (cargoFull)
|
||||||
|
{
|
||||||
|
if (1 > intent.priority)
|
||||||
|
{
|
||||||
|
intent = MovementIntentComponent{1, bayPos};
|
||||||
|
}
|
||||||
|
if (bayId != kInvalidBuildingId
|
||||||
|
&& (pos.value - bayPos).length() <= 1.0f)
|
||||||
|
{
|
||||||
|
// Decrement first non-empty salvage child.
|
||||||
|
bool delivered = false;
|
||||||
|
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity /*ce*/, SalvageCargoComponent& c,
|
||||||
|
const ModuleOwnerComponent& o)
|
||||||
|
{
|
||||||
|
if (delivered || o.owner != e || c.current <= 0) { return; }
|
||||||
|
if (buildings.deliverScrapToSalvageBay(bayId))
|
||||||
|
{
|
||||||
|
--c.current;
|
||||||
|
delivered = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AiSystem::AiSystem(const GameConfig& config)
|
// Retreat if enemy near and cargo empty.
|
||||||
: m_attackEvaluator(config.world.targeting)
|
bool retreating = false;
|
||||||
{
|
if (cargoState.totalCurrent == 0)
|
||||||
}
|
|
||||||
|
|
||||||
void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
|
|
||||||
const ScrapSystem& scraps)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
|
|
||||||
// Phase 1: evaluators score behaviors and set their target data.
|
|
||||||
m_advanceEvaluator.evaluate(admin);
|
|
||||||
m_rallyEvaluator.evaluate(admin);
|
|
||||||
m_retreatEvaluator.evaluate(admin);
|
|
||||||
m_attackEvaluator.evaluate(admin);
|
|
||||||
m_repairEvaluator.evaluate(admin);
|
|
||||||
m_salvageScrapEvaluator.evaluate(admin, scraps);
|
|
||||||
m_deliverScrapEvaluator.evaluate(admin, buildings);
|
|
||||||
|
|
||||||
// Phase 2: pick the highest-scoring behavior per ship.
|
|
||||||
selectWinningBehaviors(admin);
|
|
||||||
|
|
||||||
// Phase 3: executors run for the winning behavior.
|
|
||||||
m_advanceExecutor.execute(admin);
|
|
||||||
m_rallyExecutor.execute(admin);
|
|
||||||
m_retreatExecutor.execute(admin);
|
|
||||||
m_attackExecutor.execute(admin);
|
|
||||||
m_repairExecutor.execute(admin);
|
|
||||||
m_salvageScrapExecutor.execute(admin);
|
|
||||||
m_deliverScrapExecutor.execute(admin, buildings);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AiSystem::selectWinningBehaviors(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
admin.forEach<SelectedBehaviorComponent>(
|
|
||||||
[](entt::entity /*e*/, SelectedBehaviorComponent& selected)
|
|
||||||
{
|
{
|
||||||
selected.winner = BehaviorKind::None;
|
for (const EnemyShipPos& enemy : enemyShips)
|
||||||
selected.bestScore = std::numeric_limits<float>::lowest();
|
{
|
||||||
});
|
if ((enemy.position - pos.value).length() <= collectRange)
|
||||||
|
{
|
||||||
|
if (1 > intent.priority)
|
||||||
|
{
|
||||||
|
intent = MovementIntentComponent{
|
||||||
|
1, QVector2D(-10000.0f, pos.value.y())};
|
||||||
|
}
|
||||||
|
retreating = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (retreating) { return; }
|
||||||
|
|
||||||
// Highest priority first so ties resolve toward the more urgent behavior.
|
// Per-module independent collection: each ready module collects one scrap.
|
||||||
consider<RetreatBehavior>(admin, BehaviorKind::Retreat);
|
bool anythingCollected = false;
|
||||||
consider<AttackBehavior>(admin, BehaviorKind::Attack);
|
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||||
consider<RepairBehavior>(admin, BehaviorKind::Repair);
|
[&](entt::entity /*ce*/, SalvageCargoComponent& c,
|
||||||
consider<SalvageScrapBehavior>(admin, BehaviorKind::SalvageScrap);
|
const ModuleOwnerComponent& o)
|
||||||
consider<DeliverScrapBehavior>(admin, BehaviorKind::DeliverScrap);
|
{
|
||||||
consider<RallyBehavior>(admin, BehaviorKind::Rally);
|
if (o.owner != e || c.current >= c.capacity
|
||||||
consider<AdvanceBehavior>(admin, BehaviorKind::Advance);
|
|| c.cooldownTicksRemaining > 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const ScrapInfo& si : allScrap)
|
||||||
|
{
|
||||||
|
if ((si.position - pos.value).length() > c.collectionRange_tiles) { continue; }
|
||||||
|
if (scraps.consume(si.entity))
|
||||||
|
{
|
||||||
|
++c.current;
|
||||||
|
c.cooldownTicksRemaining = c.collectionIntervalTicks;
|
||||||
|
anythingCollected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (anythingCollected)
|
||||||
|
{
|
||||||
|
salvageBehavior.scrapTarget = std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move toward scrap target or find a new one.
|
||||||
|
if (salvageBehavior.scrapTarget)
|
||||||
|
{
|
||||||
|
if (1 > intent.priority)
|
||||||
|
{
|
||||||
|
intent = MovementIntentComponent{1, *salvageBehavior.scrapTarget};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float bestDist = sensor.value_tiles;
|
||||||
|
std::optional<QVector2D> bestPos;
|
||||||
|
for (const ScrapInfo& si : allScrap)
|
||||||
|
{
|
||||||
|
const float dist = (si.position - pos.value).length();
|
||||||
|
if (dist < bestDist)
|
||||||
|
{
|
||||||
|
bestDist = dist;
|
||||||
|
bestPos = si.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestPos)
|
||||||
|
{
|
||||||
|
salvageBehavior.scrapTarget = bestPos;
|
||||||
|
if (1 > intent.priority)
|
||||||
|
{
|
||||||
|
intent = MovementIntentComponent{1, *bestPos};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (1 > intent.priority)
|
||||||
|
{
|
||||||
|
intent = MovementIntentComponent{
|
||||||
|
1, QVector2D(pos.value.x() + 1000.0f, pos.value.y())};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,15 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "AdvanceEvaluator.h"
|
|
||||||
#include "AdvanceExecutor.h"
|
|
||||||
#include "AttackEvaluator.h"
|
|
||||||
#include "AttackExecutor.h"
|
|
||||||
#include "DeliverScrapEvaluator.h"
|
|
||||||
#include "DeliverScrapExecutor.h"
|
|
||||||
#include "RallyEvaluator.h"
|
|
||||||
#include "RallyExecutor.h"
|
|
||||||
#include "RepairEvaluator.h"
|
|
||||||
#include "RepairExecutor.h"
|
|
||||||
#include "RetreatEvaluator.h"
|
|
||||||
#include "RetreatExecutor.h"
|
|
||||||
#include "SalvageScrapEvaluator.h"
|
|
||||||
#include "SalvageScrapExecutor.h"
|
|
||||||
|
|
||||||
class BuildingSystem;
|
class BuildingSystem;
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
class ScrapSystem;
|
class ScrapSystem;
|
||||||
struct GameConfig;
|
|
||||||
|
|
||||||
// Orchestrates ship-behavior decision-making in three batched phases:
|
|
||||||
// 1. evaluators score each behavior and set its target data,
|
|
||||||
// 2. selectWinningBehaviors picks the highest-scoring behavior per ship,
|
|
||||||
// 3. executors run for the winning behavior, setting movement intent and
|
|
||||||
// preferred module targets.
|
|
||||||
// All world mutation (collection, healing, damage) is left to the module
|
|
||||||
// systems (SalvagerSystem, RepairSystem, CombatSystem).
|
|
||||||
class AiSystem
|
class AiSystem
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit AiSystem(const GameConfig& config);
|
void tickHomeReturnBehavior(EntityAdmin& admin);
|
||||||
|
void tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSystem& buildings);
|
||||||
void tick(EntityAdmin& admin, const BuildingSystem& buildings, const ScrapSystem& scraps);
|
void tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings);
|
||||||
|
void tickRepairTools(EntityAdmin& admin);
|
||||||
private:
|
void tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps, BuildingSystem& buildings);
|
||||||
void selectWinningBehaviors(EntityAdmin& admin);
|
|
||||||
|
|
||||||
AdvanceEvaluator m_advanceEvaluator;
|
|
||||||
RallyEvaluator m_rallyEvaluator;
|
|
||||||
RetreatEvaluator m_retreatEvaluator;
|
|
||||||
AttackEvaluator m_attackEvaluator;
|
|
||||||
RepairEvaluator m_repairEvaluator;
|
|
||||||
SalvageScrapEvaluator m_salvageScrapEvaluator;
|
|
||||||
DeliverScrapEvaluator m_deliverScrapEvaluator;
|
|
||||||
|
|
||||||
AdvanceExecutor m_advanceExecutor;
|
|
||||||
RallyExecutor m_rallyExecutor;
|
|
||||||
RetreatExecutor m_retreatExecutor;
|
|
||||||
AttackExecutor m_attackExecutor;
|
|
||||||
RepairExecutor m_repairExecutor;
|
|
||||||
SalvageScrapExecutor m_salvageScrapExecutor;
|
|
||||||
DeliverScrapExecutor m_deliverScrapExecutor;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,9 @@
|
|||||||
SET(HDRS
|
SET(HDRS
|
||||||
${HDRS}
|
${HDRS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AdvanceEvaluator.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AdvanceExecutor.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AttackEvaluator.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AttackExecutor.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/BehaviorTargeting.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapEvaluator.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapExecutor.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyEvaluator.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyExecutor.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RepairEvaluator.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RepairExecutor.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RetreatEvaluator.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RetreatExecutor.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/SalvageScrapEvaluator.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/SalvageScrapExecutor.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RepairSystem.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SalvagerSystem.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
@@ -28,27 +11,10 @@ SET(HDRS
|
|||||||
|
|
||||||
SET(SRCS
|
SET(SRCS
|
||||||
${SRCS}
|
${SRCS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AdvanceEvaluator.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AdvanceExecutor.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AttackEvaluator.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AttackExecutor.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/BehaviorTargeting.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapEvaluator.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapExecutor.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyEvaluator.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyExecutor.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RepairEvaluator.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RepairExecutor.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RetreatEvaluator.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RetreatExecutor.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/SalvageScrapEvaluator.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/SalvageScrapExecutor.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RepairSystem.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SalvagerSystem.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
@@ -57,6 +23,5 @@ SET(SRCS
|
|||||||
set(LIB_INCLUDE_PATH
|
set(LIB_INCLUDE_PATH
|
||||||
${LIB_INCLUDE_PATH}
|
${LIB_INCLUDE_PATH}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai
|
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
|
#include "ThreatResponseBehaviorComponent.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
#include "WeaponComponent.h"
|
#include "WeaponComponent.h"
|
||||||
|
|
||||||
@@ -20,18 +21,21 @@ CombatSystem::CombatSystem(const GameConfig& config)
|
|||||||
void CombatSystem::tick(Tick currentTick,
|
void CombatSystem::tick(Tick currentTick,
|
||||||
EntityAdmin& admin,
|
EntityAdmin& admin,
|
||||||
BuildingSystem& /*buildings*/,
|
BuildingSystem& /*buildings*/,
|
||||||
std::vector<WeaponFiredEvent>& outWeaponFiredEvents)
|
std::vector<FireEvent>& outFireEvents)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
// All weapons (ships and stations) are child entities linked via ModuleOwnerComponent.
|
// All weapons (ships and stations) are child entities linked via ModuleOwnerComponent.
|
||||||
// AttackExecutor has already set each weapon's preferred (in-range) target; here we
|
|
||||||
// validate it, fall back to nearest-target acquisition, and fire.
|
|
||||||
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||||
[&](entt::entity /*e*/, WeaponComponent& weapon, const ModuleOwnerComponent& owner)
|
[&](entt::entity /*e*/, WeaponComponent& weapon, const ModuleOwnerComponent& owner)
|
||||||
{
|
{
|
||||||
|
if (admin.hasAll<ThreatResponseBehaviorComponent>(owner.owner))
|
||||||
|
{
|
||||||
|
weapon.currentTarget =
|
||||||
|
admin.get<ThreatResponseBehaviorComponent>(owner.owner).currentTarget;
|
||||||
|
}
|
||||||
const PositionComponent& pos = admin.get<PositionComponent>(owner.owner);
|
const PositionComponent& pos = admin.get<PositionComponent>(owner.owner);
|
||||||
const FactionComponent& faction = admin.get<FactionComponent>(owner.owner);
|
const FactionComponent& faction = admin.get<FactionComponent>(owner.owner);
|
||||||
resolveWeapon(owner.owner, weapon, pos, faction, currentTick, admin, outWeaponFiredEvents);
|
resolveWeapon(owner.owner, weapon, pos, faction, currentTick, admin, outFireEvents);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +46,7 @@ void CombatSystem::resolveWeapon(
|
|||||||
const FactionComponent& ownFaction,
|
const FactionComponent& ownFaction,
|
||||||
Tick currentTick,
|
Tick currentTick,
|
||||||
EntityAdmin& admin,
|
EntityAdmin& admin,
|
||||||
std::vector<WeaponFiredEvent>& out)
|
std::vector<FireEvent>& out)
|
||||||
{
|
{
|
||||||
if (weapon.cooldownTicks > 0.0f)
|
if (weapon.cooldownTicks > 0.0f)
|
||||||
{
|
{
|
||||||
@@ -111,7 +115,7 @@ void CombatSystem::resolveWeapon(
|
|||||||
m_pendingDamage.push_back({targetEntity, weapon.damage,
|
m_pendingDamage.push_back({targetEntity, weapon.damage,
|
||||||
currentTick + kWeaponImpactDelayTicks});
|
currentTick + kWeaponImpactDelayTicks});
|
||||||
|
|
||||||
WeaponFiredEvent evt;
|
FireEvent evt;
|
||||||
evt.shooter = shipEntity;
|
evt.shooter = shipEntity;
|
||||||
evt.target = targetEntity;
|
evt.target = targetEntity;
|
||||||
evt.emittedAt = currentTick;
|
evt.emittedAt = currentTick;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "WeaponFiredEvent.h"
|
#include "FireEvent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
@@ -26,7 +26,7 @@ public:
|
|||||||
void tick(Tick currentTick,
|
void tick(Tick currentTick,
|
||||||
EntityAdmin& admin,
|
EntityAdmin& admin,
|
||||||
BuildingSystem& buildings,
|
BuildingSystem& buildings,
|
||||||
std::vector<WeaponFiredEvent>& outWeaponFiredEvents);
|
std::vector<FireEvent>& outFireEvents);
|
||||||
|
|
||||||
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
|
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ private:
|
|||||||
const FactionComponent& ownFaction,
|
const FactionComponent& ownFaction,
|
||||||
Tick currentTick,
|
Tick currentTick,
|
||||||
EntityAdmin& admin,
|
EntityAdmin& admin,
|
||||||
std::vector<WeaponFiredEvent>& out);
|
std::vector<FireEvent>& out);
|
||||||
|
|
||||||
const GameConfig& m_config;
|
const GameConfig& m_config;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
|
|||||||
[](entt::entity /*e*/, const PositionComponent& pos, const FacingComponent& facing,
|
[](entt::entity /*e*/, const PositionComponent& pos, const FacingComponent& facing,
|
||||||
DynamicBodyComponent& body, const MovementIntentComponent& intent)
|
DynamicBodyComponent& body, const MovementIntentComponent& intent)
|
||||||
{
|
{
|
||||||
if (!intent.active)
|
if (intent.priority == 0)
|
||||||
{
|
{
|
||||||
// No movement intent: brake using available thrust.
|
// No movement intent: brake using available thrust.
|
||||||
const float linearBraking = std::min(body.velocity_tpt.length(),
|
const float linearBraking = std::min(body.velocity_tpt.length(),
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
#include "RepairSystem.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <optional>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QVector2D>
|
|
||||||
|
|
||||||
#include "BehaviorTargeting.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "HealthComponent.h"
|
|
||||||
#include "ModuleOwnerComponent.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "RepairToolComponent.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
RepairSystem::RepairSystem(EntityAdmin& admin)
|
|
||||||
: m_admin(admin)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void RepairSystem::tick()
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
const std::vector<RepairableInfo> repairables = buildRepairables(m_admin);
|
|
||||||
|
|
||||||
m_admin.forEach<RepairToolComponent, ModuleOwnerComponent>(
|
|
||||||
[&](entt::entity /*re*/, RepairToolComponent& tool, const ModuleOwnerComponent& owner)
|
|
||||||
{
|
|
||||||
if (!m_admin.hasAll<PositionComponent>(owner.owner)) { return; }
|
|
||||||
const QVector2D ownerPos = m_admin.get<PositionComponent>(owner.owner).value;
|
|
||||||
|
|
||||||
// Honour the executor-set target if it is still valid and in range.
|
|
||||||
if (tool.currentTarget)
|
|
||||||
{
|
|
||||||
const entt::entity t = *tool.currentTarget;
|
|
||||||
if (m_admin.isValid(t) && m_admin.hasAll<HealthComponent, PositionComponent>(t))
|
|
||||||
{
|
|
||||||
HealthComponent& th = m_admin.get<HealthComponent>(t);
|
|
||||||
const float dist =
|
|
||||||
(m_admin.get<PositionComponent>(t).value - ownerPos).length();
|
|
||||||
if (th.hp > 0.0f && th.hp < th.maxHp && dist <= tool.range_tiles)
|
|
||||||
{
|
|
||||||
th.hp = std::min(th.hp + tool.ratePerTick, th.maxHp);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: heal the nearest damaged friendly within tool range.
|
|
||||||
tool.currentTarget = std::nullopt;
|
|
||||||
float bestDist = tool.range_tiles;
|
|
||||||
for (const RepairableInfo& r : repairables)
|
|
||||||
{
|
|
||||||
if (r.isEnemy) { continue; }
|
|
||||||
if (r.hp <= 0.0f || r.hp >= r.maxHp) { continue; }
|
|
||||||
const float dist = (r.position - ownerPos).length();
|
|
||||||
if (dist < bestDist)
|
|
||||||
{
|
|
||||||
bestDist = dist;
|
|
||||||
tool.currentTarget = r.entity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tool.currentTarget) { return; }
|
|
||||||
|
|
||||||
HealthComponent& targetHealth = m_admin.get<HealthComponent>(*tool.currentTarget);
|
|
||||||
targetHealth.hp = std::min(targetHealth.hp + tool.ratePerTick, targetHealth.maxHp);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
|
|
||||||
// World-mutation system for repair modules: validates each tool's target (set by
|
|
||||||
// RepairExecutor), falls back to the nearest damaged friendly in range, and
|
|
||||||
// applies healing. Runs every tick, independent of behavior selection.
|
|
||||||
class RepairSystem
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit RepairSystem(EntityAdmin& admin);
|
|
||||||
|
|
||||||
void tick();
|
|
||||||
|
|
||||||
private:
|
|
||||||
EntityAdmin& m_admin;
|
|
||||||
};
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
#include "SalvagerSystem.h"
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QVector2D>
|
|
||||||
|
|
||||||
#include "Building.h"
|
|
||||||
#include "BuildingSystem.h"
|
|
||||||
#include "DeliverScrapBehavior.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "ModuleOwnerComponent.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "SalvageCargoComponent.h"
|
|
||||||
#include "ScrapSystem.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
SalvagerSystem::SalvagerSystem(EntityAdmin& admin)
|
|
||||||
: m_admin(admin)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void SalvagerSystem::tick(ScrapSystem& scraps, BuildingSystem& buildings)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
|
|
||||||
|
|
||||||
// Tick down per-module collection cooldowns.
|
|
||||||
m_admin.forEach<SalvageCargoComponent>(
|
|
||||||
[](entt::entity /*e*/, SalvageCargoComponent& c)
|
|
||||||
{
|
|
||||||
if (c.cooldownTicksRemaining > 0) { --c.cooldownTicksRemaining; }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Collection: each ready, in-range module collects one scrap.
|
|
||||||
m_admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
|
||||||
[&](entt::entity /*ce*/, SalvageCargoComponent& c, const ModuleOwnerComponent& o)
|
|
||||||
{
|
|
||||||
if (c.current >= c.capacity || c.cooldownTicksRemaining > 0) { return; }
|
|
||||||
if (!m_admin.hasAll<PositionComponent>(o.owner)) { return; }
|
|
||||||
|
|
||||||
const QVector2D ownerPos = m_admin.get<PositionComponent>(o.owner).value;
|
|
||||||
for (const ScrapInfo& si : allScrap)
|
|
||||||
{
|
|
||||||
if ((si.position - ownerPos).length() > c.collectionRange_tiles) { continue; }
|
|
||||||
if (scraps.consume(si.entity))
|
|
||||||
{
|
|
||||||
++c.current;
|
|
||||||
c.cooldownTicksRemaining = c.collectionIntervalTicks;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delivery: a ship at its assigned bay hands over one unit of cargo per tick.
|
|
||||||
m_admin.forEach<DeliverScrapBehavior, PositionComponent>(
|
|
||||||
[&](entt::entity ship, const DeliverScrapBehavior& deliver, const PositionComponent& pos)
|
|
||||||
{
|
|
||||||
if (deliver.deliveryBay == kInvalidBuildingId) { return; }
|
|
||||||
const Building* bay = buildings.findBuilding(deliver.deliveryBay);
|
|
||||||
if (!bay) { return; }
|
|
||||||
|
|
||||||
const QVector2D bayCenter(bay->anchor.x() + bay->footprint.width() / 2.0f,
|
|
||||||
bay->anchor.y() + bay->footprint.height() / 2.0f);
|
|
||||||
if ((pos.value - bayCenter).length() > 1.0f) { return; }
|
|
||||||
|
|
||||||
// Decrement the first non-empty salvage child belonging to this ship.
|
|
||||||
bool delivered = false;
|
|
||||||
m_admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
|
||||||
[&](entt::entity /*ce*/, SalvageCargoComponent& c, const ModuleOwnerComponent& o)
|
|
||||||
{
|
|
||||||
if (delivered || o.owner != ship || c.current <= 0) { return; }
|
|
||||||
if (buildings.deliverScrapToSalvageBay(deliver.deliveryBay))
|
|
||||||
{
|
|
||||||
--c.current;
|
|
||||||
delivered = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class BuildingSystem;
|
|
||||||
class EntityAdmin;
|
|
||||||
class ScrapSystem;
|
|
||||||
|
|
||||||
// World-mutation system for salvage modules: collects scrap into cargo and
|
|
||||||
// delivers full cargo at a SalvageBay. Runs every tick, independent of which
|
|
||||||
// behavior the AiSystem selected.
|
|
||||||
class SalvagerSystem
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit SalvagerSystem(EntityAdmin& admin);
|
|
||||||
|
|
||||||
void tick(ScrapSystem& scraps, BuildingSystem& buildings);
|
|
||||||
|
|
||||||
private:
|
|
||||||
EntityAdmin& m_admin;
|
|
||||||
};
|
|
||||||
@@ -6,10 +6,6 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "AdvanceBehavior.h"
|
|
||||||
#include "AttackBehavior.h"
|
|
||||||
#include "BehaviorScores.h"
|
|
||||||
#include "DeliverScrapBehavior.h"
|
|
||||||
#include "DynamicBodyComponent.h"
|
#include "DynamicBodyComponent.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
@@ -17,15 +13,14 @@
|
|||||||
#include "ModuleOwnerComponent.h"
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "ModulesConfig.h"
|
#include "ModulesConfig.h"
|
||||||
#include "MovementIntentComponent.h"
|
#include "MovementIntentComponent.h"
|
||||||
#include "RallyBehavior.h"
|
#include "RallyBehaviorComponent.h"
|
||||||
#include "RepairBehavior.h"
|
#include "RepairBehaviorComponent.h"
|
||||||
#include "RepairToolComponent.h"
|
#include "RepairToolComponent.h"
|
||||||
#include "RetreatBehavior.h"
|
#include "SalvageBehaviorComponent.h"
|
||||||
#include "SalvageCargoComponent.h"
|
#include "SalvageCargoComponent.h"
|
||||||
#include "SalvageScrapBehavior.h"
|
|
||||||
#include "SelectedBehaviorComponent.h"
|
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
#include "ThreatResponseBehaviorComponent.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
#include "WeaponComponent.h"
|
#include "WeaponComponent.h"
|
||||||
|
|
||||||
@@ -326,42 +321,15 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
|
|
||||||
// --- Pass 3: attach behavior components based on capability presence -----
|
// --- Pass 3: attach behavior components based on capability presence -----
|
||||||
|
|
||||||
// Baseline: every ship can always fall back to advancing, and needs a slot
|
|
||||||
// for the per-tick behavior selection result.
|
|
||||||
m_admin.addComponent<AdvanceBehavior>(entity, AdvanceBehavior{});
|
|
||||||
m_admin.addComponent<SelectedBehaviorComponent>(entity, SelectedBehaviorComponent{});
|
|
||||||
|
|
||||||
// Player ships retreat to the rally point when threatened or badly damaged
|
|
||||||
// (disabled by the balancing tool to keep arena fights symmetric).
|
|
||||||
if (!isEnemy && m_retreatEnabled)
|
|
||||||
{
|
|
||||||
RetreatBehavior retreat;
|
|
||||||
retreat.retreatHpFraction = BehaviorScores::kLowHpFraction;
|
|
||||||
retreat.retreatPoint = m_rallyPoint;
|
|
||||||
m_admin.addComponent<RetreatBehavior>(entity, retreat);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!weaponChildren.empty())
|
if (!weaponChildren.empty())
|
||||||
{
|
{
|
||||||
float maxWeaponRange = 0.0f;
|
m_admin.addComponent<ThreatResponseBehaviorComponent>(
|
||||||
for (entt::entity child : weaponChildren)
|
entity, ThreatResponseBehaviorComponent{});
|
||||||
{
|
|
||||||
const float r = m_admin.get<WeaponComponent>(child).range_tiles;
|
|
||||||
if (r > maxWeaponRange) { maxWeaponRange = r; }
|
|
||||||
}
|
|
||||||
|
|
||||||
AttackBehavior attack;
|
|
||||||
attack.orbitRadius_tiles =
|
|
||||||
maxWeaponRange * static_cast<float>(m_config.world.orbitFactor);
|
|
||||||
m_admin.addComponent<AttackBehavior>(entity, attack);
|
|
||||||
|
|
||||||
if (!isEnemy)
|
if (!isEnemy)
|
||||||
{
|
{
|
||||||
RallyBehavior rally;
|
m_admin.addComponent<RallyBehaviorComponent>(
|
||||||
rally.rallyPoint = m_rallyPoint;
|
entity, RallyBehaviorComponent{m_rallyPoint});
|
||||||
rally.orbitRadius_tiles =
|
|
||||||
static_cast<float>(m_config.world.rallyOrbitRadius_tiles);
|
|
||||||
m_admin.addComponent<RallyBehavior>(entity, rally);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,16 +342,11 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
if (r > maxCollRange) { maxCollRange = r; }
|
if (r > maxCollRange) { maxCollRange = r; }
|
||||||
}
|
}
|
||||||
|
|
||||||
SalvageScrapBehavior salvage;
|
SalvageBehaviorComponent sb;
|
||||||
salvage.scrapTarget = std::nullopt;
|
sb.scrapTarget = std::nullopt;
|
||||||
salvage.maxCollectionRange_tiles = maxCollRange;
|
sb.deliveryBay = kInvalidBuildingId;
|
||||||
salvage.orbitRadius_tiles =
|
sb.maxCollectionRange_tiles = maxCollRange;
|
||||||
maxCollRange * static_cast<float>(m_config.world.orbitFactor);
|
m_admin.addComponent<SalvageBehaviorComponent>(entity, sb);
|
||||||
m_admin.addComponent<SalvageScrapBehavior>(entity, salvage);
|
|
||||||
|
|
||||||
DeliverScrapBehavior deliver;
|
|
||||||
deliver.deliveryBay = kInvalidBuildingId;
|
|
||||||
m_admin.addComponent<DeliverScrapBehavior>(entity, deliver);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!repairChildren.empty())
|
if (!repairChildren.empty())
|
||||||
@@ -395,12 +358,10 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
if (r > maxRepairRange) { maxRepairRange = r; }
|
if (r > maxRepairRange) { maxRepairRange = r; }
|
||||||
}
|
}
|
||||||
|
|
||||||
RepairBehavior repair;
|
RepairBehaviorComponent rb;
|
||||||
repair.currentTarget = std::nullopt;
|
rb.currentTarget = std::nullopt;
|
||||||
repair.maxRepairRange_tiles = maxRepairRange;
|
rb.maxRepairRange_tiles = maxRepairRange;
|
||||||
repair.orbitRadius_tiles =
|
m_admin.addComponent<RepairBehaviorComponent>(entity, rb);
|
||||||
maxRepairRange * static_cast<float>(m_config.world.orbitFactor);
|
|
||||||
m_admin.addComponent<RepairBehavior>(entity, repair);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return entity;
|
return entity;
|
||||||
@@ -424,7 +385,7 @@ void ShipSystem::clearMovementIntents()
|
|||||||
m_admin.forEach<MovementIntentComponent>(
|
m_admin.forEach<MovementIntentComponent>(
|
||||||
[](entt::entity /*e*/, MovementIntentComponent& i)
|
[](entt::entity /*e*/, MovementIntentComponent& i)
|
||||||
{
|
{
|
||||||
i = MovementIntentComponent{false, QVector2D(0.0f, 0.0f)};
|
i = MovementIntentComponent{0, QVector2D(0.0f, 0.0f)};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,17 +394,12 @@ void ShipSystem::setRallyPoint(QVector2D point)
|
|||||||
m_rallyPoint = point;
|
m_rallyPoint = point;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShipSystem::setRetreatEnabled(bool enabled)
|
|
||||||
{
|
|
||||||
m_retreatEnabled = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShipSystem::triggerRallyDeparture()
|
void ShipSystem::triggerRallyDeparture()
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
std::vector<entt::entity> toRemove;
|
std::vector<entt::entity> toRemove;
|
||||||
m_admin.forEach<RallyBehavior, FactionComponent>(
|
m_admin.forEach<RallyBehaviorComponent, FactionComponent>(
|
||||||
[&toRemove](entt::entity e, const RallyBehavior& /*rb*/,
|
[&toRemove](entt::entity e, const RallyBehaviorComponent& /*rb*/,
|
||||||
const FactionComponent& f)
|
const FactionComponent& f)
|
||||||
{
|
{
|
||||||
if (!f.isEnemy)
|
if (!f.isEnemy)
|
||||||
@@ -453,6 +409,6 @@ void ShipSystem::triggerRallyDeparture()
|
|||||||
});
|
});
|
||||||
for (entt::entity e : toRemove)
|
for (entt::entity e : toRemove)
|
||||||
{
|
{
|
||||||
m_admin.removeComponent<RallyBehavior>(e);
|
m_admin.removeComponent<RallyBehaviorComponent>(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public:
|
|||||||
const std::map<std::string, int>& moduleLevelOverrides = {});
|
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 priority 0 before behavior systems run.
|
||||||
void clearMovementIntents();
|
void clearMovementIntents();
|
||||||
|
|
||||||
// Set the rally point that newly spawned player combat ships will loiter at.
|
// Set the rally point that newly spawned player combat ships will loiter at.
|
||||||
@@ -33,11 +33,6 @@ public:
|
|||||||
// Release all gathered player combat ships to advance toward the enemy.
|
// Release all gathered player combat ships to advance toward the enemy.
|
||||||
void triggerRallyDeparture();
|
void triggerRallyDeparture();
|
||||||
|
|
||||||
// Controls whether newly spawned player ships receive a RetreatBehavior. The
|
|
||||||
// balancing tool disables this so arena fights stay symmetric and aggressive
|
|
||||||
// (REQ-BAL-SIM-AI); the main game keeps it enabled (REQ-SHP-RETREAT).
|
|
||||||
void setRetreatEnabled(bool enabled);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const ShipDef* findShipDef(const std::string& schematicId) const;
|
const ShipDef* findShipDef(const std::string& schematicId) const;
|
||||||
const ModuleDef* findModuleDef(const std::string& id) const;
|
const ModuleDef* findModuleDef(const std::string& id) const;
|
||||||
@@ -45,5 +40,4 @@ private:
|
|||||||
const GameConfig& m_config;
|
const GameConfig& m_config;
|
||||||
EntityAdmin& m_admin;
|
EntityAdmin& m_admin;
|
||||||
QVector2D m_rallyPoint;
|
QVector2D m_rallyPoint;
|
||||||
bool m_retreatEnabled = true;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
#include "AdvanceEvaluator.h"
|
|
||||||
|
|
||||||
#include "AdvanceBehavior.h"
|
|
||||||
#include "BehaviorScores.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
void AdvanceEvaluator::evaluate(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
admin.forEach<AdvanceBehavior>(
|
|
||||||
[](entt::entity /*e*/, AdvanceBehavior& advance)
|
|
||||||
{
|
|
||||||
advance.score = BehaviorScores::kAdvance;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
|
|
||||||
// Baseline fallback: gives every ship a constant low score so there is always a
|
|
||||||
// winning behavior. The actual movement direction is decided by AdvanceExecutor.
|
|
||||||
class AdvanceEvaluator
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void evaluate(EntityAdmin& admin);
|
|
||||||
};
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
#include "AdvanceExecutor.h"
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
#include <QVector2D>
|
|
||||||
|
|
||||||
#include "AdvanceBehavior.h"
|
|
||||||
#include "BehaviorKind.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "FactionComponent.h"
|
|
||||||
#include "HealthComponent.h"
|
|
||||||
#include "HqProxyComponent.h"
|
|
||||||
#include "MovementIntentComponent.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "SelectedBehaviorComponent.h"
|
|
||||||
#include "StationBodyComponent.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
// Accumulates positions to produce their centroid (the center between them).
|
|
||||||
struct Centroid
|
|
||||||
{
|
|
||||||
QVector2D sum;
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
void add(const QVector2D& point)
|
|
||||||
{
|
|
||||||
sum += point;
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<QVector2D> value() const
|
|
||||||
{
|
|
||||||
if (count == 0) { return std::nullopt; }
|
|
||||||
return sum / static_cast<float>(count);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void AdvanceExecutor::execute(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
|
|
||||||
// Centroid of each faction's alive defence stations. In the arena the HQ is
|
|
||||||
// spawned as a station, so it is part of this centroid; in the main game the
|
|
||||||
// enemy side has only its defence stations.
|
|
||||||
Centroid enemyStations;
|
|
||||||
Centroid playerStations;
|
|
||||||
admin.forEach<StationBodyComponent, PositionComponent, FactionComponent, HealthComponent>(
|
|
||||||
[&enemyStations, &playerStations](entt::entity /*e*/,
|
|
||||||
const StationBodyComponent& /*sb*/, const PositionComponent& pos,
|
|
||||||
const FactionComponent& faction, const HealthComponent& health)
|
|
||||||
{
|
|
||||||
if (health.hp <= 0.0f) { return; }
|
|
||||||
Centroid& centroid = faction.isEnemy ? enemyStations : playerStations;
|
|
||||||
centroid.add(pos.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fallback target per faction: the HQ proxy (main game only), used when a side
|
|
||||||
// has lost all of its defence stations.
|
|
||||||
Centroid enemyHq;
|
|
||||||
Centroid playerHq;
|
|
||||||
admin.forEach<HqProxyComponent, PositionComponent, FactionComponent, HealthComponent>(
|
|
||||||
[&enemyHq, &playerHq](entt::entity /*e*/, const HqProxyComponent& /*hq*/,
|
|
||||||
const PositionComponent& pos, const FactionComponent& faction,
|
|
||||||
const HealthComponent& health)
|
|
||||||
{
|
|
||||||
if (health.hp <= 0.0f) { return; }
|
|
||||||
Centroid& centroid = faction.isEnemy ? enemyHq : playerHq;
|
|
||||||
centroid.add(pos.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const std::optional<QVector2D> enemyStationCenter = enemyStations.value();
|
|
||||||
const std::optional<QVector2D> playerStationCenter = playerStations.value();
|
|
||||||
const std::optional<QVector2D> enemyHqCenter = enemyHq.value();
|
|
||||||
const std::optional<QVector2D> playerHqCenter = playerHq.value();
|
|
||||||
|
|
||||||
admin.forEach<AdvanceBehavior, SelectedBehaviorComponent, PositionComponent,
|
|
||||||
FactionComponent, MovementIntentComponent>(
|
|
||||||
[&](entt::entity /*e*/, const AdvanceBehavior& /*advance*/,
|
|
||||||
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
|
|
||||||
const FactionComponent& faction, MovementIntentComponent& intent)
|
|
||||||
{
|
|
||||||
if (selected.winner != BehaviorKind::Advance) { return; }
|
|
||||||
|
|
||||||
// Aim at the center between the opposing side's defence stations; fall
|
|
||||||
// back to the opposing HQ, then to an off-world point in the advance
|
|
||||||
// direction so the ship keeps moving when no target structure exists.
|
|
||||||
const std::optional<QVector2D>& stationCenter =
|
|
||||||
faction.isEnemy ? playerStationCenter : enemyStationCenter;
|
|
||||||
const std::optional<QVector2D>& hqCenter =
|
|
||||||
faction.isEnemy ? playerHqCenter : enemyHqCenter;
|
|
||||||
|
|
||||||
QVector2D target;
|
|
||||||
if (stationCenter)
|
|
||||||
{
|
|
||||||
target = *stationCenter;
|
|
||||||
}
|
|
||||||
else if (hqCenter)
|
|
||||||
{
|
|
||||||
target = *hqCenter;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
target = faction.isEnemy
|
|
||||||
? QVector2D(-10000.0f, pos.value.y())
|
|
||||||
: QVector2D(pos.value.x() + 1000.0f, pos.value.y());
|
|
||||||
}
|
|
||||||
intent = MovementIntentComponent{true, target};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
|
|
||||||
// Moves a ship toward the opposing side when Advance is the winning behavior:
|
|
||||||
// player ships advance toward +x (the enemy), enemy ships toward -x (the base).
|
|
||||||
class AdvanceExecutor
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void execute(EntityAdmin& admin);
|
|
||||||
};
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
#include "AttackEvaluator.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QVector2D>
|
|
||||||
|
|
||||||
#include "AttackBehavior.h"
|
|
||||||
#include "BehaviorScores.h"
|
|
||||||
#include "BehaviorTargeting.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "FactionComponent.h"
|
|
||||||
#include "HealthComponent.h"
|
|
||||||
#include "ModuleOwnerComponent.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "SensorRangeComponent.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
#include "WeaponComponent.h"
|
|
||||||
#include "WorldConfig.h"
|
|
||||||
|
|
||||||
AttackEvaluator::AttackEvaluator(const WorldTargeting& targeting)
|
|
||||||
: m_targeting(&targeting)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void AttackEvaluator::evaluate(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
const std::vector<CombatantInfo> combatants = buildCombatants(admin);
|
|
||||||
|
|
||||||
// Pass A: the maximum weapon range per ship, used to normalise target
|
|
||||||
// distance. Ships without a weapon fall back to their sensor range below.
|
|
||||||
std::unordered_map<entt::entity, float> maxWeaponRange_tiles;
|
|
||||||
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
|
||||||
[&maxWeaponRange_tiles](entt::entity /*we*/, const WeaponComponent& weapon,
|
|
||||||
const ModuleOwnerComponent& owner)
|
|
||||||
{
|
|
||||||
float& best = maxWeaponRange_tiles[owner.owner];
|
|
||||||
best = std::max(best, weapon.range_tiles);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pass B: claim counts, taken from every ship's current target before any
|
|
||||||
// target is reassigned this tick. Each ship reads the previous tick's claim
|
|
||||||
// state and excludes its own contribution when scoring its current target.
|
|
||||||
std::unordered_map<entt::entity, int> claimsByTarget;
|
|
||||||
admin.forEach<AttackBehavior>(
|
|
||||||
[&claimsByTarget, &admin](entt::entity /*e*/, const AttackBehavior& attack)
|
|
||||||
{
|
|
||||||
if (attack.currentTarget && admin.isValid(*attack.currentTarget))
|
|
||||||
{
|
|
||||||
++claimsByTarget[*attack.currentTarget];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pass C: per-ship target selection.
|
|
||||||
admin.forEach<AttackBehavior, PositionComponent, FactionComponent,
|
|
||||||
SensorRangeComponent, HealthComponent>(
|
|
||||||
[&](entt::entity e, AttackBehavior& attack, const PositionComponent& pos,
|
|
||||||
const FactionComponent& faction, const SensorRangeComponent& sensor,
|
|
||||||
const HealthComponent& health)
|
|
||||||
{
|
|
||||||
const float sensorRange_tiles = sensor.value_tiles;
|
|
||||||
|
|
||||||
// Distance normaliser: max weapon range, or sensor range if unarmed.
|
|
||||||
float weaponRange_tiles = sensorRange_tiles;
|
|
||||||
const auto weaponRangeIt = maxWeaponRange_tiles.find(e);
|
|
||||||
if (weaponRangeIt != maxWeaponRange_tiles.end() && weaponRangeIt->second > 0.0f)
|
|
||||||
{
|
|
||||||
weaponRange_tiles = weaponRangeIt->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scores a single candidate: base desirability from distance, reduced
|
|
||||||
// by the overclaim penalty. selfClaimed subtracts this ship's own claim
|
|
||||||
// so it does not penalise the target it already holds.
|
|
||||||
const auto scoreOf =
|
|
||||||
[&](const QVector2D& candidatePos, entt::entity candidate) -> float
|
|
||||||
{
|
|
||||||
const float dist = (candidatePos - pos.value).length();
|
|
||||||
const float x = dist / weaponRange_tiles;
|
|
||||||
float base = static_cast<float>(m_targeting->targetScoreFormula.evaluate(x));
|
|
||||||
base = std::max(base, 0.0f);
|
|
||||||
|
|
||||||
int claims = 0;
|
|
||||||
const auto claimIt = claimsByTarget.find(candidate);
|
|
||||||
if (claimIt != claimsByTarget.end()) { claims = claimIt->second; }
|
|
||||||
if (attack.currentTarget && candidate == *attack.currentTarget) { --claims; }
|
|
||||||
|
|
||||||
float penalty = static_cast<float>(
|
|
||||||
m_targeting->overclaimPenaltyFormula.evaluate(claims));
|
|
||||||
penalty = std::clamp(penalty, 0.0f, 1.0f);
|
|
||||||
|
|
||||||
return base * penalty;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find the best candidate among in-range enemies.
|
|
||||||
std::optional<entt::entity> bestTarget;
|
|
||||||
float bestScore = 0.0f;
|
|
||||||
for (const CombatantInfo& c : combatants)
|
|
||||||
{
|
|
||||||
if (c.entity == e) { continue; }
|
|
||||||
const bool isValidTarget = faction.isEnemy ? !c.isEnemy : c.isEnemy;
|
|
||||||
if (!isValidTarget) { continue; }
|
|
||||||
|
|
||||||
const float dist = (c.position - pos.value).length();
|
|
||||||
if (dist > sensorRange_tiles) { continue; }
|
|
||||||
|
|
||||||
const float score = scoreOf(c.position, c.entity);
|
|
||||||
if (!bestTarget || score > bestScore)
|
|
||||||
{
|
|
||||||
bestScore = score;
|
|
||||||
bestTarget = c.entity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hysteresis: keep the current target if it is still valid and in
|
|
||||||
// range, unless a challenger beats its score by more than the margin.
|
|
||||||
bool keptCurrent = false;
|
|
||||||
if (attack.currentTarget)
|
|
||||||
{
|
|
||||||
const entt::entity t = *attack.currentTarget;
|
|
||||||
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
|
||||||
{
|
|
||||||
const QVector2D targetPos = admin.get<PositionComponent>(t).value;
|
|
||||||
const float dist = (targetPos - pos.value).length();
|
|
||||||
if (dist <= sensorRange_tiles)
|
|
||||||
{
|
|
||||||
const float currentScore = scoreOf(targetPos, t);
|
|
||||||
const float margin = 1.0f + static_cast<float>(m_targeting->hysteresis);
|
|
||||||
if (!bestTarget || bestScore <= currentScore * margin)
|
|
||||||
{
|
|
||||||
keptCurrent = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!keptCurrent) { attack.currentTarget = bestTarget; }
|
|
||||||
|
|
||||||
const bool healthy =
|
|
||||||
(health.maxHp > 0.0f)
|
|
||||||
&& (health.hp / health.maxHp >= BehaviorScores::kLowHpFraction);
|
|
||||||
attack.score = (healthy && attack.currentTarget)
|
|
||||||
? BehaviorScores::kAttack
|
|
||||||
: BehaviorScores::kInactive;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
struct WorldTargeting;
|
|
||||||
|
|
||||||
// Acquires/validates a combat target for ships with weapons. Scores high only
|
|
||||||
// when the ship's health is not low and a valid target is within sensor range.
|
|
||||||
//
|
|
||||||
// Target choice is claim-aware: each tick the desirability of every candidate is
|
|
||||||
// scored from a configurable distance formula and reduced by a soft overclaim
|
|
||||||
// penalty that scales with how many other ships already target it, spreading
|
|
||||||
// ships across enemies instead of dogpiling the nearest one.
|
|
||||||
class AttackEvaluator
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit AttackEvaluator(const WorldTargeting& targeting);
|
|
||||||
|
|
||||||
void evaluate(EntityAdmin& admin);
|
|
||||||
|
|
||||||
private:
|
|
||||||
const WorldTargeting* m_targeting;
|
|
||||||
};
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
#include "AttackExecutor.h"
|
|
||||||
|
|
||||||
#include "AttackBehavior.h"
|
|
||||||
#include "BehaviorKind.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "ModuleOwnerComponent.h"
|
|
||||||
#include "MovementIntentComponent.h"
|
|
||||||
#include "OrbitMath.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "SelectedBehaviorComponent.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
#include "WeaponComponent.h"
|
|
||||||
|
|
||||||
void AttackExecutor::execute(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
|
|
||||||
// Ships: move toward the behavior target.
|
|
||||||
admin.forEach<AttackBehavior, SelectedBehaviorComponent, PositionComponent,
|
|
||||||
MovementIntentComponent>(
|
|
||||||
[&](entt::entity /*e*/, const AttackBehavior& attack,
|
|
||||||
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
|
|
||||||
MovementIntentComponent& intent)
|
|
||||||
{
|
|
||||||
if (selected.winner != BehaviorKind::Attack) { return; }
|
|
||||||
if (!attack.currentTarget) { return; }
|
|
||||||
|
|
||||||
const entt::entity t = *attack.currentTarget;
|
|
||||||
QVector2D dest = pos.value;
|
|
||||||
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
|
||||||
{
|
|
||||||
const QVector2D targetPos = admin.get<PositionComponent>(t).value;
|
|
||||||
dest = OrbitMath::computeOrbitDestination(pos.value, targetPos,
|
|
||||||
attack.orbitRadius_tiles);
|
|
||||||
}
|
|
||||||
intent = MovementIntentComponent{true, dest};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Weapons: assign the behavior target only if it is within this weapon's range.
|
|
||||||
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
|
||||||
[&](entt::entity /*we*/, WeaponComponent& weapon, const ModuleOwnerComponent& owner)
|
|
||||||
{
|
|
||||||
if (!admin.hasAll<AttackBehavior, SelectedBehaviorComponent>(owner.owner))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const SelectedBehaviorComponent& selected =
|
|
||||||
admin.get<SelectedBehaviorComponent>(owner.owner);
|
|
||||||
if (selected.winner != BehaviorKind::Attack) { return; }
|
|
||||||
|
|
||||||
const AttackBehavior& attack = admin.get<AttackBehavior>(owner.owner);
|
|
||||||
if (!attack.currentTarget) { return; }
|
|
||||||
|
|
||||||
const entt::entity t = *attack.currentTarget;
|
|
||||||
if (!admin.isValid(t) || !admin.hasAll<PositionComponent>(t)) { return; }
|
|
||||||
|
|
||||||
const QVector2D ownerPos = admin.get<PositionComponent>(owner.owner).value;
|
|
||||||
const float dist = (admin.get<PositionComponent>(t).value - ownerPos).length();
|
|
||||||
if (dist <= weapon.range_tiles)
|
|
||||||
{
|
|
||||||
weapon.currentTarget = t;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
|
|
||||||
// When Attack wins, moves the ship toward its target and assigns that target to
|
|
||||||
// each weapon that has it in range. Weapons whose range excludes the target are
|
|
||||||
// left untouched so CombatSystem can keep/acquire a closer target (no thrash).
|
|
||||||
class AttackExecutor
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void execute(EntityAdmin& admin);
|
|
||||||
};
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#include "BehaviorTargeting.h"
|
|
||||||
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "FactionComponent.h"
|
|
||||||
#include "HealthComponent.h"
|
|
||||||
#include "HqProxyComponent.h"
|
|
||||||
#include "ModuleOwnerComponent.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "SalvageCargoComponent.h"
|
|
||||||
#include "ShipIdentityComponent.h"
|
|
||||||
#include "StationBodyComponent.h"
|
|
||||||
|
|
||||||
std::vector<RepairableInfo> buildRepairables(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
std::vector<RepairableInfo> repairables;
|
|
||||||
|
|
||||||
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent, HealthComponent>(
|
|
||||||
[&repairables](entt::entity e, const ShipIdentityComponent& /*si*/,
|
|
||||||
const PositionComponent& pos, const FactionComponent& f,
|
|
||||||
const HealthComponent& h)
|
|
||||||
{
|
|
||||||
repairables.push_back({e, pos.value, f.isEnemy, true, h.hp, h.maxHp});
|
|
||||||
});
|
|
||||||
|
|
||||||
admin.forEach<StationBodyComponent, PositionComponent, FactionComponent, HealthComponent>(
|
|
||||||
[&repairables](entt::entity e, const StationBodyComponent& /*sb*/,
|
|
||||||
const PositionComponent& pos, const FactionComponent& f,
|
|
||||||
const HealthComponent& h)
|
|
||||||
{
|
|
||||||
repairables.push_back({e, pos.value, f.isEnemy, false, h.hp, h.maxHp});
|
|
||||||
});
|
|
||||||
|
|
||||||
return repairables;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<CombatantInfo> buildCombatants(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
std::vector<CombatantInfo> combatants;
|
|
||||||
|
|
||||||
admin.forEach<PositionComponent, FactionComponent, ShipIdentityComponent>(
|
|
||||||
[&combatants](entt::entity e, const PositionComponent& pos,
|
|
||||||
const FactionComponent& f, const ShipIdentityComponent& /*si*/)
|
|
||||||
{
|
|
||||||
combatants.push_back({e, pos.value, f.isEnemy, false});
|
|
||||||
});
|
|
||||||
|
|
||||||
admin.forEach<PositionComponent, FactionComponent, StationBodyComponent>(
|
|
||||||
[&combatants](entt::entity e, const PositionComponent& pos,
|
|
||||||
const FactionComponent& f, const StationBodyComponent& /*sb*/)
|
|
||||||
{
|
|
||||||
combatants.push_back({e, pos.value, f.isEnemy, true});
|
|
||||||
});
|
|
||||||
|
|
||||||
admin.forEach<PositionComponent, FactionComponent, HqProxyComponent>(
|
|
||||||
[&combatants](entt::entity e, const PositionComponent& pos,
|
|
||||||
const FactionComponent& f, const HqProxyComponent& /*hq*/)
|
|
||||||
{
|
|
||||||
combatants.push_back({e, pos.value, f.isEnemy, true});
|
|
||||||
});
|
|
||||||
|
|
||||||
return combatants;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unordered_map<entt::entity, CargoState> buildCargoByShip(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
std::unordered_map<entt::entity, CargoState> cargoByShip;
|
|
||||||
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
|
||||||
[&cargoByShip](entt::entity /*ce*/, const SalvageCargoComponent& c,
|
|
||||||
const ModuleOwnerComponent& o)
|
|
||||||
{
|
|
||||||
CargoState& agg = cargoByShip[o.owner];
|
|
||||||
agg.current += c.current;
|
|
||||||
agg.capacity += c.capacity;
|
|
||||||
});
|
|
||||||
return cargoByShip;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isCargoFull(const CargoState& cargo)
|
|
||||||
{
|
|
||||||
return cargo.capacity > 0 && cargo.current >= cargo.capacity;
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QVector2D>
|
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
|
|
||||||
// Shared, per-call target snapshots used by behavior evaluators and the repair
|
|
||||||
// system. Each caller builds its own snapshot (no cross-system caching).
|
|
||||||
|
|
||||||
struct RepairableInfo
|
|
||||||
{
|
|
||||||
entt::entity entity;
|
|
||||||
QVector2D position;
|
|
||||||
bool isEnemy;
|
|
||||||
bool isShip;
|
|
||||||
float hp;
|
|
||||||
float maxHp;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CombatantInfo
|
|
||||||
{
|
|
||||||
entt::entity entity;
|
|
||||||
QVector2D position;
|
|
||||||
bool isEnemy;
|
|
||||||
bool isStation;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CargoState
|
|
||||||
{
|
|
||||||
int current = 0;
|
|
||||||
int capacity = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// All ships and stations with health — candidates for repair targeting.
|
|
||||||
std::vector<RepairableInfo> buildRepairables(EntityAdmin& admin);
|
|
||||||
|
|
||||||
// All ships, stations, and the HQ proxy — candidates for attack targeting.
|
|
||||||
std::vector<CombatantInfo> buildCombatants(EntityAdmin& admin);
|
|
||||||
|
|
||||||
// Aggregated salvage cargo per owning ship, summed across its salvage modules.
|
|
||||||
std::unordered_map<entt::entity, CargoState> buildCargoByShip(EntityAdmin& admin);
|
|
||||||
|
|
||||||
// True when the ship's aggregated cargo is at capacity (and it has any capacity).
|
|
||||||
bool isCargoFull(const CargoState& cargo);
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#include "DeliverScrapEvaluator.h"
|
|
||||||
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
#include "BehaviorScores.h"
|
|
||||||
#include "BehaviorTargeting.h"
|
|
||||||
#include "Building.h"
|
|
||||||
#include "BuildingSystem.h"
|
|
||||||
#include "BuildingType.h"
|
|
||||||
#include "DeliverScrapBehavior.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
void DeliverScrapEvaluator::evaluate(EntityAdmin& admin, const BuildingSystem& buildings)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
|
|
||||||
|
|
||||||
admin.forEach<DeliverScrapBehavior, PositionComponent>(
|
|
||||||
[&](entt::entity e, DeliverScrapBehavior& deliver, const PositionComponent& pos)
|
|
||||||
{
|
|
||||||
const std::unordered_map<entt::entity, CargoState>::const_iterator it =
|
|
||||||
cargoByShip.find(e);
|
|
||||||
const bool cargoFull = (it != cargoByShip.end()) && isCargoFull(it->second);
|
|
||||||
|
|
||||||
if (!cargoFull)
|
|
||||||
{
|
|
||||||
deliver.score = BehaviorScores::kInactive;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign nearest SalvageBay if not yet assigned.
|
|
||||||
if (deliver.deliveryBay == kInvalidBuildingId)
|
|
||||||
{
|
|
||||||
const Building* bay =
|
|
||||||
buildings.findNearestBuilding(pos.value, BuildingType::SalvageBay);
|
|
||||||
if (bay) { deliver.deliveryBay = bay->id; }
|
|
||||||
}
|
|
||||||
|
|
||||||
deliver.score = BehaviorScores::kDeliver;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
class BuildingSystem;
|
|
||||||
|
|
||||||
// Scores high only when the ship's cargo is full, and assigns the nearest
|
|
||||||
// SalvageBay as the delivery destination.
|
|
||||||
class DeliverScrapEvaluator
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void evaluate(EntityAdmin& admin, const BuildingSystem& buildings);
|
|
||||||
};
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#include "DeliverScrapExecutor.h"
|
|
||||||
|
|
||||||
#include <QVector2D>
|
|
||||||
|
|
||||||
#include "BehaviorKind.h"
|
|
||||||
#include "Building.h"
|
|
||||||
#include "BuildingSystem.h"
|
|
||||||
#include "DeliverScrapBehavior.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "MovementIntentComponent.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "SelectedBehaviorComponent.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
void DeliverScrapExecutor::execute(EntityAdmin& admin, const BuildingSystem& buildings)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
admin.forEach<DeliverScrapBehavior, SelectedBehaviorComponent, PositionComponent,
|
|
||||||
MovementIntentComponent>(
|
|
||||||
[&](entt::entity /*e*/, const DeliverScrapBehavior& deliver,
|
|
||||||
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
|
|
||||||
MovementIntentComponent& intent)
|
|
||||||
{
|
|
||||||
if (selected.winner != BehaviorKind::DeliverScrap) { return; }
|
|
||||||
|
|
||||||
QVector2D dest = pos.value;
|
|
||||||
if (deliver.deliveryBay != kInvalidBuildingId)
|
|
||||||
{
|
|
||||||
const Building* bay = buildings.findBuilding(deliver.deliveryBay);
|
|
||||||
if (bay)
|
|
||||||
{
|
|
||||||
dest = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f,
|
|
||||||
bay->anchor.y() + bay->footprint.height() / 2.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
intent = MovementIntentComponent{true, dest};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
class BuildingSystem;
|
|
||||||
|
|
||||||
// Moves a ship toward its delivery bay when DeliverScrap is the winning
|
|
||||||
// behavior. Never decrements cargo — SalvagerSystem performs the delivery.
|
|
||||||
class DeliverScrapExecutor
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void execute(EntityAdmin& admin, const BuildingSystem& buildings);
|
|
||||||
};
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
#include <QVector2D>
|
|
||||||
|
|
||||||
// Orbit movement helper (REQ-SHP-ORBIT). Behaviors that keep a ship circling a
|
|
||||||
// target (attack, repair, salvage, rally) feed the result of this function as the
|
|
||||||
// movement intent destination instead of the target's center.
|
|
||||||
namespace OrbitMath
|
|
||||||
{
|
|
||||||
// Lead angle (radians) by which the radial direction is rotated to produce
|
|
||||||
// tangential motion. The fixed positive (counter-clockwise) sense makes the
|
|
||||||
// orbit direction stable for the duration of orbiting a given target.
|
|
||||||
constexpr float kOrbitLeadAngle_rad = 0.6f;
|
|
||||||
|
|
||||||
// Returns a destination on the orbit circle of `radius` around `target`. The
|
|
||||||
// result always lies exactly `radius` from `target`, so steering toward it
|
|
||||||
// both corrects the standoff distance and advances the ship tangentially.
|
|
||||||
// A radius of zero or less falls back to the target center (legacy "approach
|
|
||||||
// the target" behavior), e.g. when the ship has no tool range to orbit at.
|
|
||||||
inline QVector2D computeOrbitDestination(const QVector2D& shipPos,
|
|
||||||
const QVector2D& target, float radius)
|
|
||||||
{
|
|
||||||
if (radius <= 0.0f) { return target; }
|
|
||||||
|
|
||||||
QVector2D radial = shipPos - target;
|
|
||||||
float length = radial.length();
|
|
||||||
if (length < 1.0e-4f)
|
|
||||||
{
|
|
||||||
// Ship sits on the target; pick an arbitrary radial direction.
|
|
||||||
radial = QVector2D(1.0f, 0.0f);
|
|
||||||
length = 1.0f;
|
|
||||||
}
|
|
||||||
const QVector2D radialDirection = radial / length;
|
|
||||||
|
|
||||||
const float cosLead = std::cos(kOrbitLeadAngle_rad);
|
|
||||||
const float sinLead = std::sin(kOrbitLeadAngle_rad);
|
|
||||||
const QVector2D leadDirection(
|
|
||||||
radialDirection.x() * cosLead - radialDirection.y() * sinLead,
|
|
||||||
radialDirection.x() * sinLead + radialDirection.y() * cosLead);
|
|
||||||
|
|
||||||
return target + radius * leadDirection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#include "RallyEvaluator.h"
|
|
||||||
|
|
||||||
#include "BehaviorScores.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "RallyBehavior.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
void RallyEvaluator::evaluate(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
admin.forEach<RallyBehavior>(
|
|
||||||
[](entt::entity /*e*/, RallyBehavior& rally)
|
|
||||||
{
|
|
||||||
rally.score = BehaviorScores::kRally;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
|
|
||||||
// Scores the rally behavior so player combat ships gather at the rally point
|
|
||||||
// until an enemy appears (Attack outscores it) or the departure timer removes
|
|
||||||
// the RallyBehavior component.
|
|
||||||
class RallyEvaluator
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void evaluate(EntityAdmin& admin);
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#include "RallyExecutor.h"
|
|
||||||
|
|
||||||
#include "BehaviorKind.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "MovementIntentComponent.h"
|
|
||||||
#include "OrbitMath.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "RallyBehavior.h"
|
|
||||||
#include "SelectedBehaviorComponent.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
void RallyExecutor::execute(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
admin.forEach<RallyBehavior, SelectedBehaviorComponent, PositionComponent,
|
|
||||||
MovementIntentComponent>(
|
|
||||||
[](entt::entity /*e*/, const RallyBehavior& rally,
|
|
||||||
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
|
|
||||||
MovementIntentComponent& intent)
|
|
||||||
{
|
|
||||||
if (selected.winner != BehaviorKind::Rally) { return; }
|
|
||||||
const QVector2D dest = OrbitMath::computeOrbitDestination(
|
|
||||||
pos.value, rally.rallyPoint, rally.orbitRadius_tiles);
|
|
||||||
intent = MovementIntentComponent{true, dest};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
|
|
||||||
// Moves a ship to its rally point when Rally is the winning behavior.
|
|
||||||
class RallyExecutor
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void execute(EntityAdmin& admin);
|
|
||||||
};
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
#include "RepairEvaluator.h"
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "BehaviorScores.h"
|
|
||||||
#include "BehaviorTargeting.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "HealthComponent.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "RepairBehavior.h"
|
|
||||||
#include "SensorRangeComponent.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
void RepairEvaluator::evaluate(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
const std::vector<RepairableInfo> repairables = buildRepairables(admin);
|
|
||||||
|
|
||||||
admin.forEach<RepairBehavior, PositionComponent, SensorRangeComponent>(
|
|
||||||
[&](entt::entity e, RepairBehavior& repair, const PositionComponent& pos,
|
|
||||||
const SensorRangeComponent& sensor)
|
|
||||||
{
|
|
||||||
// Validate current target: alive and still damaged.
|
|
||||||
bool targetValid = false;
|
|
||||||
if (repair.currentTarget)
|
|
||||||
{
|
|
||||||
const entt::entity t = *repair.currentTarget;
|
|
||||||
if (admin.isValid(t) && admin.hasAll<HealthComponent>(t))
|
|
||||||
{
|
|
||||||
const HealthComponent& th = admin.get<HealthComponent>(t);
|
|
||||||
if (th.hp > 0.0f && th.hp < th.maxHp) { targetValid = true; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Acquire nearest damaged friendly within sensor range.
|
|
||||||
if (!targetValid)
|
|
||||||
{
|
|
||||||
repair.currentTarget = std::nullopt;
|
|
||||||
float bestDist = sensor.value_tiles;
|
|
||||||
for (const RepairableInfo& r : repairables)
|
|
||||||
{
|
|
||||||
if (r.entity == e) { continue; }
|
|
||||||
if (r.isEnemy) { continue; }
|
|
||||||
if (r.hp <= 0.0f || r.hp >= r.maxHp) { continue; }
|
|
||||||
const float dist = (r.position - pos.value).length();
|
|
||||||
if (dist < bestDist)
|
|
||||||
{
|
|
||||||
bestDist = dist;
|
|
||||||
repair.currentTarget = r.entity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repair.score = repair.currentTarget
|
|
||||||
? BehaviorScores::kRepair
|
|
||||||
: BehaviorScores::kInactive;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
|
|
||||||
// Picks the nearest damaged friendly within sensor range as the repair target.
|
|
||||||
// Scores high when such a target exists.
|
|
||||||
class RepairEvaluator
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void evaluate(EntityAdmin& admin);
|
|
||||||
};
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
#include "RepairExecutor.h"
|
|
||||||
|
|
||||||
#include "BehaviorKind.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "ModuleOwnerComponent.h"
|
|
||||||
#include "MovementIntentComponent.h"
|
|
||||||
#include "OrbitMath.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "RepairBehavior.h"
|
|
||||||
#include "RepairToolComponent.h"
|
|
||||||
#include "SelectedBehaviorComponent.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
void RepairExecutor::execute(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
|
|
||||||
// Ships: move toward the repair target.
|
|
||||||
admin.forEach<RepairBehavior, SelectedBehaviorComponent, PositionComponent,
|
|
||||||
MovementIntentComponent>(
|
|
||||||
[&](entt::entity /*e*/, const RepairBehavior& repair,
|
|
||||||
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
|
|
||||||
MovementIntentComponent& intent)
|
|
||||||
{
|
|
||||||
if (selected.winner != BehaviorKind::Repair) { return; }
|
|
||||||
if (!repair.currentTarget) { return; }
|
|
||||||
|
|
||||||
const entt::entity t = *repair.currentTarget;
|
|
||||||
QVector2D dest = pos.value;
|
|
||||||
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
|
||||||
{
|
|
||||||
const QVector2D targetPos = admin.get<PositionComponent>(t).value;
|
|
||||||
dest = OrbitMath::computeOrbitDestination(pos.value, targetPos,
|
|
||||||
repair.orbitRadius_tiles);
|
|
||||||
}
|
|
||||||
intent = MovementIntentComponent{true, dest};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Repair tools: prefer the behavior target if it is within tool range.
|
|
||||||
admin.forEach<RepairToolComponent, ModuleOwnerComponent>(
|
|
||||||
[&](entt::entity /*re*/, RepairToolComponent& tool, const ModuleOwnerComponent& owner)
|
|
||||||
{
|
|
||||||
if (!admin.hasAll<RepairBehavior, SelectedBehaviorComponent>(owner.owner))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const SelectedBehaviorComponent& selected =
|
|
||||||
admin.get<SelectedBehaviorComponent>(owner.owner);
|
|
||||||
if (selected.winner != BehaviorKind::Repair) { return; }
|
|
||||||
|
|
||||||
const RepairBehavior& repair = admin.get<RepairBehavior>(owner.owner);
|
|
||||||
if (!repair.currentTarget) { return; }
|
|
||||||
|
|
||||||
const entt::entity t = *repair.currentTarget;
|
|
||||||
if (!admin.isValid(t) || !admin.hasAll<PositionComponent>(t)) { return; }
|
|
||||||
|
|
||||||
const QVector2D ownerPos = admin.get<PositionComponent>(owner.owner).value;
|
|
||||||
const float dist = (admin.get<PositionComponent>(t).value - ownerPos).length();
|
|
||||||
if (dist <= tool.range_tiles)
|
|
||||||
{
|
|
||||||
tool.currentTarget = t;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
|
|
||||||
// When Repair wins, moves the ship toward its target and assigns that target to
|
|
||||||
// each repair tool that has it in range. RepairSystem applies the healing and
|
|
||||||
// does fallback acquisition for tools whose preferred target is out of range.
|
|
||||||
class RepairExecutor
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void execute(EntityAdmin& admin);
|
|
||||||
};
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#include "RetreatEvaluator.h"
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QVector2D>
|
|
||||||
|
|
||||||
#include "AttackBehavior.h"
|
|
||||||
#include "BehaviorScores.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "FactionComponent.h"
|
|
||||||
#include "HealthComponent.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "RetreatBehavior.h"
|
|
||||||
#include "SensorRangeComponent.h"
|
|
||||||
#include "ShipIdentityComponent.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
void RetreatEvaluator::evaluate(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
|
|
||||||
// Snapshot enemy ship positions for threat detection.
|
|
||||||
std::vector<QVector2D> enemyShips;
|
|
||||||
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>(
|
|
||||||
[&enemyShips](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
|
||||||
const PositionComponent& pos, const FactionComponent& f)
|
|
||||||
{
|
|
||||||
if (f.isEnemy) { enemyShips.push_back(pos.value); }
|
|
||||||
});
|
|
||||||
|
|
||||||
admin.forEach<RetreatBehavior, PositionComponent, HealthComponent, SensorRangeComponent>(
|
|
||||||
[&](entt::entity e, RetreatBehavior& retreat, const PositionComponent& pos,
|
|
||||||
const HealthComponent& health, const SensorRangeComponent& sensor)
|
|
||||||
{
|
|
||||||
const bool lowHp = (health.maxHp > 0.0f)
|
|
||||||
&& (health.hp / health.maxHp < retreat.retreatHpFraction);
|
|
||||||
|
|
||||||
bool threatened = false;
|
|
||||||
const bool hasWeapons = admin.hasAll<AttackBehavior>(e);
|
|
||||||
if (!hasWeapons)
|
|
||||||
{
|
|
||||||
for (const QVector2D& enemy : enemyShips)
|
|
||||||
{
|
|
||||||
if ((enemy - pos.value).length() <= sensor.value_tiles)
|
|
||||||
{
|
|
||||||
threatened = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
retreat.score = (lowHp || threatened)
|
|
||||||
? BehaviorScores::kRetreat
|
|
||||||
: BehaviorScores::kInactive;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
|
|
||||||
// Scores high (above all task behaviors) when the ship's health is below its
|
|
||||||
// retreat threshold, or when an enemy ship is within sensor range and the ship
|
|
||||||
// has no weapons to fight back with.
|
|
||||||
class RetreatEvaluator
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void evaluate(EntityAdmin& admin);
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#include "RetreatExecutor.h"
|
|
||||||
|
|
||||||
#include "BehaviorKind.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "MovementIntentComponent.h"
|
|
||||||
#include "RetreatBehavior.h"
|
|
||||||
#include "SelectedBehaviorComponent.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
void RetreatExecutor::execute(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
admin.forEach<RetreatBehavior, SelectedBehaviorComponent, MovementIntentComponent>(
|
|
||||||
[](entt::entity /*e*/, const RetreatBehavior& retreat,
|
|
||||||
const SelectedBehaviorComponent& selected, MovementIntentComponent& intent)
|
|
||||||
{
|
|
||||||
if (selected.winner != BehaviorKind::Retreat) { return; }
|
|
||||||
intent = MovementIntentComponent{true, retreat.retreatPoint};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
|
|
||||||
// Moves a ship to its retreat point (the rally point) when Retreat wins.
|
|
||||||
class RetreatExecutor
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void execute(EntityAdmin& admin);
|
|
||||||
};
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
#include "SalvageScrapEvaluator.h"
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QVector2D>
|
|
||||||
|
|
||||||
#include "BehaviorScores.h"
|
|
||||||
#include "BehaviorTargeting.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "SalvageScrapBehavior.h"
|
|
||||||
#include "ScrapSystem.h"
|
|
||||||
#include "SensorRangeComponent.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
void SalvageScrapEvaluator::evaluate(EntityAdmin& admin, const ScrapSystem& scraps)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
|
|
||||||
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
|
|
||||||
|
|
||||||
admin.forEach<SalvageScrapBehavior, PositionComponent, SensorRangeComponent>(
|
|
||||||
[&](entt::entity e, SalvageScrapBehavior& salvage, const PositionComponent& pos,
|
|
||||||
const SensorRangeComponent& sensor)
|
|
||||||
{
|
|
||||||
const std::unordered_map<entt::entity, CargoState>::const_iterator it =
|
|
||||||
cargoByShip.find(e);
|
|
||||||
const bool cargoFull = (it != cargoByShip.end()) && isCargoFull(it->second);
|
|
||||||
|
|
||||||
if (cargoFull)
|
|
||||||
{
|
|
||||||
salvage.scrapTarget = std::nullopt;
|
|
||||||
salvage.score = BehaviorScores::kInactive;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find nearest scrap within sensor range.
|
|
||||||
float bestDist = sensor.value_tiles;
|
|
||||||
std::optional<QVector2D> bestPos;
|
|
||||||
for (const ScrapInfo& si : allScrap)
|
|
||||||
{
|
|
||||||
const float dist = (si.position - pos.value).length();
|
|
||||||
if (dist < bestDist)
|
|
||||||
{
|
|
||||||
bestDist = dist;
|
|
||||||
bestPos = si.position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
salvage.scrapTarget = bestPos;
|
|
||||||
salvage.score = bestPos ? BehaviorScores::kSalvage : BehaviorScores::kInactive;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
class ScrapSystem;
|
|
||||||
|
|
||||||
// When cargo is not full, finds the nearest scrap within sensor range and sets
|
|
||||||
// it as the target, scoring high. Scores inactive when cargo is full or no scrap
|
|
||||||
// is in range (Advance then handles roaming).
|
|
||||||
class SalvageScrapEvaluator
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void evaluate(EntityAdmin& admin, const ScrapSystem& scraps);
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#include "SalvageScrapExecutor.h"
|
|
||||||
|
|
||||||
#include "BehaviorKind.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "MovementIntentComponent.h"
|
|
||||||
#include "OrbitMath.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "SalvageScrapBehavior.h"
|
|
||||||
#include "SelectedBehaviorComponent.h"
|
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
void SalvageScrapExecutor::execute(EntityAdmin& admin)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
admin.forEach<SalvageScrapBehavior, SelectedBehaviorComponent, PositionComponent,
|
|
||||||
MovementIntentComponent>(
|
|
||||||
[](entt::entity /*e*/, const SalvageScrapBehavior& salvage,
|
|
||||||
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
|
|
||||||
MovementIntentComponent& intent)
|
|
||||||
{
|
|
||||||
if (selected.winner != BehaviorKind::SalvageScrap) { return; }
|
|
||||||
if (!salvage.scrapTarget) { return; }
|
|
||||||
const QVector2D dest = OrbitMath::computeOrbitDestination(
|
|
||||||
pos.value, *salvage.scrapTarget, salvage.orbitRadius_tiles);
|
|
||||||
intent = MovementIntentComponent{true, dest};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class EntityAdmin;
|
|
||||||
|
|
||||||
// Moves a ship toward its scrap target when SalvageScrap is the winning behavior.
|
|
||||||
class SalvageScrapExecutor
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void execute(EntityAdmin& admin);
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Event.h"
|
|
||||||
|
|
||||||
class ArenaInspectRequestedEvent : public Event
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit ArenaInspectRequestedEvent(int arenaIndex) : arenaIndex(arenaIndex) {}
|
|
||||||
const int arenaIndex;
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Event.h"
|
|
||||||
|
|
||||||
class ArenaStartRequestedEvent : public Event
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit ArenaStartRequestedEvent(int arenaIndex) : arenaIndex(arenaIndex) {}
|
|
||||||
const int arenaIndex;
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Event.h"
|
|
||||||
|
|
||||||
class BlueprintModeExitedEvent : public Event
|
|
||||||
{
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Blueprint.h"
|
|
||||||
#include "Event.h"
|
|
||||||
|
|
||||||
class BlueprintPlacementRequestedEvent : public Event
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit BlueprintPlacementRequestedEvent(Blueprint blueprint)
|
|
||||||
: blueprint(std::move(blueprint)) {}
|
|
||||||
const Blueprint blueprint;
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Event.h"
|
|
||||||
|
|
||||||
class BuilderModeExitedEvent : public Event
|
|
||||||
{
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "BuildingType.h"
|
|
||||||
#include "Event.h"
|
|
||||||
|
|
||||||
class BuildingTypeSelectedEvent : public Event
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit BuildingTypeSelectedEvent(BuildingType type) : type(type) {}
|
|
||||||
const BuildingType type;
|
|
||||||
};
|
|
||||||
@@ -6,25 +6,6 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuilderModeExitedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintModeExitedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EscapeMenuRequestedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeChangedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTypeSelectedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ExitBuilderModeRequestedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeToggleRequestedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPlacementRequestedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ExitBlueprintModeRequestedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SpeedChangeRequestedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/LayoutDialogRequestedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindowClosedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaStartRequestedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaInspectRequestedEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WeaponFiredEvent.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DebugDrawToggledEvent.h
|
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Event.h"
|
|
||||||
|
|
||||||
class DebugDrawToggledEvent : public Event
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit DebugDrawToggledEvent(bool active)
|
|
||||||
: active(active)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool active;
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Event.h"
|
|
||||||
|
|
||||||
class DemolishModeChangedEvent : public Event
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit DemolishModeChangedEvent(bool active) : active(active) {}
|
|
||||||
const bool active;
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Event.h"
|
|
||||||
|
|
||||||
class DemolishModeToggleRequestedEvent : public Event
|
|
||||||
{
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user