define ship roles via added modules and allow multiple weapons
This commit is contained in:
@@ -34,7 +34,7 @@ threat_cost = 3.0
|
|||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "W"
|
glyph = "W"
|
||||||
|
|
||||||
[module.combat]
|
[module.weapon]
|
||||||
multiplied_damage_formula = "1.0 + 0.15 * x"
|
multiplied_damage_formula = "1.0 + 0.15 * x"
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
@@ -49,3 +49,46 @@ glyph = "E"
|
|||||||
|
|
||||||
[module.movement]
|
[module.movement]
|
||||||
added_speed_formula = "0.5 * x"
|
added_speed_formula = "0.5 * x"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "laser_cannon"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 5
|
||||||
|
threat_cost = 5.0
|
||||||
|
fill_color = "#FF8040"
|
||||||
|
glyph = "L"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
damage_formula = "2"
|
||||||
|
attack_range_formula = "5"
|
||||||
|
attack_rate_formula = "2.0"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "salvage_bay_module"
|
||||||
|
surface_mask = ["OO"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 5
|
||||||
|
threat_cost = 0.0
|
||||||
|
fill_color = "#AACC44"
|
||||||
|
glyph = "Sv"
|
||||||
|
|
||||||
|
[module.salvage]
|
||||||
|
collection_range_formula = "50"
|
||||||
|
cargo_capacity_formula = "10"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "repair_tool_module"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "circuit_board", amount = 2}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 5
|
||||||
|
threat_cost = 0.0
|
||||||
|
fill_color = "#66CCFF"
|
||||||
|
glyph = "Rp"
|
||||||
|
|
||||||
|
[module.repair]
|
||||||
|
repair_rate_formula = "5 + x"
|
||||||
|
repair_range_formula = "80"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
id = "fighter"
|
id = "fighter"
|
||||||
available_from_start = true
|
available_from_start = true
|
||||||
layout = ["XOX", "OOO", "XOX"]
|
layout = ["XOX", "OOO", "XOX"]
|
||||||
|
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||||
@@ -24,11 +25,6 @@ max_rotation_speed_formula = "6.28"
|
|||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "15"
|
sensor_range_formula = "15"
|
||||||
|
|
||||||
[ship.combat]
|
|
||||||
damage_formula = "2"
|
|
||||||
attack_range_formula = "5"
|
|
||||||
attack_rate_formula = "2.0"
|
|
||||||
|
|
||||||
[ship.loot]
|
[ship.loot]
|
||||||
scrap_drop = 2
|
scrap_drop = 2
|
||||||
|
|
||||||
@@ -38,6 +34,7 @@ scrap_drop = 2
|
|||||||
id = "sniper"
|
id = "sniper"
|
||||||
available_from_start = true
|
available_from_start = true
|
||||||
layout = ["XOOX", "OOOO", "XOOX"]
|
layout = ["XOOX", "OOOO", "XOOX"]
|
||||||
|
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||||
@@ -60,11 +57,6 @@ max_rotation_speed_formula = "3.14"
|
|||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "25"
|
sensor_range_formula = "25"
|
||||||
|
|
||||||
[ship.combat]
|
|
||||||
damage_formula = "10"
|
|
||||||
attack_range_formula = "20"
|
|
||||||
attack_rate_formula = "0.5"
|
|
||||||
|
|
||||||
[ship.loot]
|
[ship.loot]
|
||||||
scrap_drop = 2
|
scrap_drop = 2
|
||||||
|
|
||||||
@@ -74,6 +66,7 @@ scrap_drop = 2
|
|||||||
id = "gunship"
|
id = "gunship"
|
||||||
available_from_start = true
|
available_from_start = true
|
||||||
layout = ["XOOOX", "OOOOO", "OOOOO", "XOOOX"]
|
layout = ["XOOOX", "OOOOO", "OOOOO", "XOOOX"]
|
||||||
|
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||||
@@ -96,11 +89,6 @@ max_rotation_speed_formula = "3.14"
|
|||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "20"
|
sensor_range_formula = "20"
|
||||||
|
|
||||||
[ship.combat]
|
|
||||||
damage_formula = "1"
|
|
||||||
attack_range_formula = "10"
|
|
||||||
attack_rate_formula = "5"
|
|
||||||
|
|
||||||
[ship.loot]
|
[ship.loot]
|
||||||
scrap_drop = 2
|
scrap_drop = 2
|
||||||
|
|
||||||
@@ -132,10 +120,6 @@ max_rotation_speed_formula = "6.28"
|
|||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "250"
|
sensor_range_formula = "250"
|
||||||
|
|
||||||
[ship.salvage]
|
|
||||||
collection_range = 50
|
|
||||||
cargo_capacity = 10
|
|
||||||
|
|
||||||
[ship.loot]
|
[ship.loot]
|
||||||
scrap_drop = 2
|
scrap_drop = 2
|
||||||
|
|
||||||
@@ -166,9 +150,5 @@ max_rotation_speed_formula = "6.28"
|
|||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "250"
|
sensor_range_formula = "250"
|
||||||
|
|
||||||
[ship.repair]
|
|
||||||
repair_rate_formula = "5 + x"
|
|
||||||
repair_range_formula = "80"
|
|
||||||
|
|
||||||
[ship.loot]
|
[ship.loot]
|
||||||
scrap_drop = 2
|
scrap_drop = 2
|
||||||
|
|||||||
@@ -141,26 +141,29 @@ outline = "#201a14"
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Ships
|
# Ships
|
||||||
#
|
#
|
||||||
# Ships are drawn as oriented triangles/arrows. Color is keyed to role, not
|
# Ships are drawn as oriented triangles/arrows. Color is keyed to schematic id.
|
||||||
# schematic (architecture.md, "Layer Order").
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
[ships.player_combat]
|
[ships.fighter]
|
||||||
fill = "#3366ff"
|
fill = "#3366ff"
|
||||||
outline = "#ffffff"
|
outline = "#ffffff"
|
||||||
|
|
||||||
[ships.salvage]
|
[ships.sniper]
|
||||||
|
fill = "#3366ff"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.gunship]
|
||||||
|
fill = "#3366ff"
|
||||||
|
outline = "#ffffff"
|
||||||
|
|
||||||
|
[ships.salvage_ship]
|
||||||
fill = "#33cc66"
|
fill = "#33cc66"
|
||||||
outline = "#ffffff"
|
outline = "#ffffff"
|
||||||
|
|
||||||
[ships.repair]
|
[ships.repair_ship]
|
||||||
fill = "#66ccff"
|
fill = "#66ccff"
|
||||||
outline = "#ffffff"
|
outline = "#ffffff"
|
||||||
|
|
||||||
[ships.enemy]
|
|
||||||
fill = "#cc3333"
|
|
||||||
outline = "#ffffff"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Laser beams (REQ-SHP-FIRING-BEAM)
|
# Laser beams (REQ-SHP-FIRING-BEAM)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ enemy_buffer_width = 10
|
|||||||
level = 1
|
level = 1
|
||||||
count = 5
|
count = 5
|
||||||
modules = [
|
modules = [
|
||||||
|
{type = "laser_cannon", x = 1, y = 1, rotation = "east"},
|
||||||
{type = "weapon_upgrade", x = 0, y = 1, rotation = "east"},
|
{type = "weapon_upgrade", x = 0, y = 1, rotation = "east"},
|
||||||
{type = "sensor_booster", x = 1, y = 0, rotation = "east"},
|
{type = "sensor_booster", x = 2, y = 1, rotation = "east"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[arena.team]]
|
[[arena.team]]
|
||||||
@@ -23,6 +24,7 @@ enemy_buffer_width = 10
|
|||||||
level = 1
|
level = 1
|
||||||
count = 1
|
count = 1
|
||||||
modules = [
|
modules = [
|
||||||
|
{type = "laser_cannon", x = 1, y = 1, rotation = "east"},
|
||||||
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
|
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
|
||||||
{type = "weapon_upgrade", x = 1, y = 2, rotation = "east"},
|
{type = "weapon_upgrade", x = 1, y = 2, rotation = "east"},
|
||||||
]
|
]
|
||||||
@@ -42,6 +44,7 @@ enemy_buffer_width = 10
|
|||||||
level = 1
|
level = 1
|
||||||
count = 1
|
count = 1
|
||||||
modules = [
|
modules = [
|
||||||
|
{type = "laser_cannon", x = 1, y = 1, rotation = "east"},
|
||||||
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
|
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
|
||||||
{type = "sensor_booster", x = 0, y = 1, rotation = "east"},
|
{type = "sensor_booster", x = 0, y = 1, rotation = "east"},
|
||||||
]
|
]
|
||||||
@@ -53,6 +56,7 @@ enemy_buffer_width = 10
|
|||||||
level = 1
|
level = 1
|
||||||
count = 1
|
count = 1
|
||||||
modules = [
|
modules = [
|
||||||
|
{type = "laser_cannon", x = 2, y = 1, rotation = "east"},
|
||||||
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
|
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
|
||||||
{type = "weapon_upgrade", x = 3, y = 1, rotation = "east"},
|
{type = "weapon_upgrade", x = 3, y = 1, rotation = "east"},
|
||||||
{type = "engine_booster", x = 0, y = 1, rotation = "east"},
|
{type = "engine_booster", x = 0, y = 1, rotation = "east"},
|
||||||
@@ -73,6 +77,7 @@ enemy_buffer_width = 10
|
|||||||
level = 1
|
level = 1
|
||||||
count = 1
|
count = 1
|
||||||
modules = [
|
modules = [
|
||||||
|
{type = "laser_cannon", x = 2, y = 2, rotation = "east"},
|
||||||
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
|
{type = "armor_plate", x = 1, y = 0, rotation = "east"},
|
||||||
{type = "weapon_upgrade", x = 3, y = 2, rotation = "east"},
|
{type = "weapon_upgrade", x = 3, y = 2, rotation = "east"},
|
||||||
{type = "engine_booster", x = 0, y = 1, rotation = "east"},
|
{type = "engine_booster", x = 0, y = 1, rotation = "east"},
|
||||||
@@ -85,6 +90,7 @@ enemy_buffer_width = 10
|
|||||||
level = 1
|
level = 1
|
||||||
count = 5
|
count = 5
|
||||||
modules = [
|
modules = [
|
||||||
|
{type = "laser_cannon", x = 1, y = 1, rotation = "east"},
|
||||||
{type = "engine_booster", x = 1, y = 0, rotation = "east"},
|
{type = "engine_booster", x = 1, y = 0, rotation = "east"},
|
||||||
{type = "sensor_booster", x = 2, y = 1, rotation = "east"},
|
{type = "sensor_booster", x = 2, y = 1, rotation = "east"},
|
||||||
]
|
]
|
||||||
@@ -104,7 +110,8 @@ enemy_buffer_width = 15
|
|||||||
level = 1
|
level = 1
|
||||||
count = 3
|
count = 3
|
||||||
modules = [
|
modules = [
|
||||||
{type = "weapon_upgrade", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon", x = 1, y = 1, rotation = "east"},
|
||||||
|
{type = "weapon_upgrade", x = 2, y = 1, rotation = "east"},
|
||||||
{type = "sensor_booster", x = 1, y = 0, rotation = "east"},
|
{type = "sensor_booster", x = 1, y = 0, rotation = "east"},
|
||||||
]
|
]
|
||||||
[[arena.team.station]]
|
[[arena.team.station]]
|
||||||
@@ -125,5 +132,6 @@ enemy_buffer_width = 15
|
|||||||
level = 1
|
level = 1
|
||||||
count = 8
|
count = 8
|
||||||
modules = [
|
modules = [
|
||||||
|
{type = "laser_cannon", x = 1, y = 1, rotation = "east"},
|
||||||
{type = "engine_booster", x = 1, y = 0, rotation = "east"},
|
{type = "engine_booster", x = 1, y = 0, rotation = "east"},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -34,5 +34,48 @@ threat_cost = 3.0
|
|||||||
fill_color = "#FF4040"
|
fill_color = "#FF4040"
|
||||||
glyph = "W"
|
glyph = "W"
|
||||||
|
|
||||||
[module.combat]
|
[module.weapon]
|
||||||
multiplied_damage_formula = "1.2"
|
multiplied_damage_formula = "1.2"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "laser_cannon"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 5
|
||||||
|
threat_cost = 5.0
|
||||||
|
fill_color = "#FF8040"
|
||||||
|
glyph = "L"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
damage_formula = "2"
|
||||||
|
attack_range_formula = "5"
|
||||||
|
attack_rate_formula = "2.0"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "salvage_bay_module"
|
||||||
|
surface_mask = ["OO"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 5
|
||||||
|
threat_cost = 0.0
|
||||||
|
fill_color = "#AACC44"
|
||||||
|
glyph = "Sv"
|
||||||
|
|
||||||
|
[module.salvage]
|
||||||
|
collection_range_formula = "50"
|
||||||
|
cargo_capacity_formula = "10"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "repair_tool_module"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "circuit_board", amount = 2}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 5
|
||||||
|
threat_cost = 0.0
|
||||||
|
fill_color = "#66CCFF"
|
||||||
|
glyph = "Rp"
|
||||||
|
|
||||||
|
[module.repair]
|
||||||
|
repair_rate_formula = "5 + x"
|
||||||
|
repair_range_formula = "80"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
id = "interceptor"
|
id = "interceptor"
|
||||||
available_from_start = true
|
available_from_start = true
|
||||||
layout = ["XOX", "OOO", "XOX"]
|
layout = ["XOX", "OOO", "XOX"]
|
||||||
|
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||||
@@ -24,11 +25,6 @@ max_rotation_speed_formula = "100000"
|
|||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "200"
|
sensor_range_formula = "200"
|
||||||
|
|
||||||
[ship.combat]
|
|
||||||
damage_formula = "10 + 2*x"
|
|
||||||
attack_range_formula = "150"
|
|
||||||
attack_rate_formula = "2.0"
|
|
||||||
|
|
||||||
[ship.loot]
|
[ship.loot]
|
||||||
scrap_drop = 2
|
scrap_drop = 2
|
||||||
|
|
||||||
@@ -37,6 +33,7 @@ scrap_drop = 2
|
|||||||
id = "destroyer"
|
id = "destroyer"
|
||||||
available_from_start = true
|
available_from_start = true
|
||||||
layout = ["XOOX", "OOOO", "XOOX"]
|
layout = ["XOOX", "OOOO", "XOOX"]
|
||||||
|
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 2}]
|
||||||
@@ -59,11 +56,6 @@ max_rotation_speed_formula = "100000"
|
|||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "300"
|
sensor_range_formula = "300"
|
||||||
|
|
||||||
[ship.combat]
|
|
||||||
damage_formula = "12 + 2*x"
|
|
||||||
attack_range_formula = "250"
|
|
||||||
attack_rate_formula = "1.0"
|
|
||||||
|
|
||||||
[ship.loot]
|
[ship.loot]
|
||||||
scrap_drop = 4
|
scrap_drop = 4
|
||||||
|
|
||||||
@@ -94,10 +86,6 @@ max_rotation_speed_formula = "100000"
|
|||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "250"
|
sensor_range_formula = "250"
|
||||||
|
|
||||||
[ship.salvage]
|
|
||||||
collection_range = 50
|
|
||||||
cargo_capacity = 10
|
|
||||||
|
|
||||||
[ship.loot]
|
[ship.loot]
|
||||||
scrap_drop = 2
|
scrap_drop = 2
|
||||||
|
|
||||||
@@ -128,9 +116,5 @@ max_rotation_speed_formula = "100000"
|
|||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "250"
|
sensor_range_formula = "250"
|
||||||
|
|
||||||
[ship.repair]
|
|
||||||
repair_rate_formula = "5 + x"
|
|
||||||
repair_range_formula = "80"
|
|
||||||
|
|
||||||
[ship.loot]
|
[ship.loot]
|
||||||
scrap_drop = 2
|
scrap_drop = 2
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "MovementIntentSystem.h"
|
#include "MovementIntentSystem.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
@@ -159,7 +160,12 @@ void ArenaSimulation::placeStructures()
|
|||||||
}
|
}
|
||||||
const entt::entity stationEntity = m_admin.spawnStation(
|
const entt::entity stationEntity = m_admin.spawnStation(
|
||||||
anchor, parsed.footprint, absCells, hp, hp, isEnemy);
|
anchor, parsed.footprint, absCells, hp, hp, isEnemy);
|
||||||
m_admin.addComponent<WeaponComponent>(stationEntity, weapon);
|
{
|
||||||
|
entt::entity wChild = m_admin.createModuleEntity();
|
||||||
|
m_admin.addComponent<WeaponComponent>(wChild, weapon);
|
||||||
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
|
ModuleOwnerComponent{stationEntity});
|
||||||
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -247,6 +253,7 @@ void ArenaSimulation::tick()
|
|||||||
m_aiSystem->tickHomeReturnBehavior(m_admin);
|
m_aiSystem->tickHomeReturnBehavior(m_admin);
|
||||||
m_aiSystem->tickThreatResponseBehavior(m_admin, *m_buildingSystem);
|
m_aiSystem->tickThreatResponseBehavior(m_admin, *m_buildingSystem);
|
||||||
m_aiSystem->tickRepairBehavior(m_admin, *m_buildingSystem);
|
m_aiSystem->tickRepairBehavior(m_admin, *m_buildingSystem);
|
||||||
|
m_aiSystem->tickRepairTools(m_admin);
|
||||||
m_aiSystem->tickSalvageBehavior(m_admin, *m_scrapSystem, *m_buildingSystem);
|
m_aiSystem->tickSalvageBehavior(m_admin, *m_scrapSystem, *m_buildingSystem);
|
||||||
|
|
||||||
// Combat resolution (tick step 8).
|
// Combat resolution (tick step 8).
|
||||||
@@ -320,6 +327,15 @@ void ArenaSimulation::tickDeaths()
|
|||||||
{
|
{
|
||||||
const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
|
const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
|
||||||
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
|
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
|
||||||
|
{
|
||||||
|
std::vector<entt::entity> stationChildren;
|
||||||
|
m_admin.forEach<ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity ce, const ModuleOwnerComponent& o)
|
||||||
|
{
|
||||||
|
if (o.owner == deadEntity) { stationChildren.push_back(ce); }
|
||||||
|
});
|
||||||
|
for (entt::entity ce : stationChildren) { m_admin.destroy(ce); }
|
||||||
|
}
|
||||||
m_admin.destroy(deadEntity);
|
m_admin.destroy(deadEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,23 +14,12 @@
|
|||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "RepairToolComponent.h"
|
|
||||||
#include "SalvageCargoComponent.h"
|
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "StationBodyComponent.h"
|
#include "StationBodyComponent.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
ShipRole shipRoleFromComponents(bool isEnemy, bool hasCargo, bool hasRepairTool)
|
|
||||||
{
|
|
||||||
if (isEnemy) { return ShipRole::Enemy; }
|
|
||||||
if (hasCargo) { return ShipRole::Salvage; }
|
|
||||||
if (hasRepairTool) { return ShipRole::Repair; }
|
|
||||||
return ShipRole::PlayerCombat;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
@@ -322,15 +311,12 @@ void ArenaView::drawShips(QPainter& painter)
|
|||||||
{
|
{
|
||||||
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
|
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
|
||||||
FactionComponent>(
|
FactionComponent>(
|
||||||
[&](entt::entity e, const ShipIdentityComponent& /*si*/,
|
[&](entt::entity /*e*/, const ShipIdentityComponent& si,
|
||||||
const PositionComponent& pos, const FacingComponent& facing,
|
const PositionComponent& pos, const FacingComponent& facing,
|
||||||
const FactionComponent& fac)
|
const FactionComponent& /*fac*/)
|
||||||
{
|
{
|
||||||
const bool hasCargo = m_sim->admin().hasAll<SalvageCargoComponent>(e);
|
const std::map<std::string, ShipVisuals>::const_iterator it =
|
||||||
const bool hasRepair = m_sim->admin().hasAll<RepairToolComponent>(e);
|
m_visuals->ships.find(si.schematicId);
|
||||||
const ShipRole role = shipRoleFromComponents(fac.isEnemy, hasCargo, hasRepair);
|
|
||||||
const std::map<ShipRole, ShipVisuals>::const_iterator it =
|
|
||||||
m_visuals->ships.find(role);
|
|
||||||
if (it == m_visuals->ships.end()) { return; }
|
if (it == m_visuals->ships.end()) { return; }
|
||||||
|
|
||||||
const QPointF center = worldToWidget(pos.value);
|
const QPointF center = worldToWidget(pos.value);
|
||||||
|
|||||||
@@ -7,8 +7,13 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
#include "toml.hpp"
|
#include "toml.hpp"
|
||||||
|
|
||||||
|
#include "Rotation.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -207,6 +212,42 @@ toml::table parseFile(const std::string& path, const std::string& file)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rotation parseRotationString(const std::string& s)
|
||||||
|
{
|
||||||
|
if (s == "east") { return Rotation::East; }
|
||||||
|
if (s == "south") { return Rotation::South; }
|
||||||
|
if (s == "west") { return Rotation::West; }
|
||||||
|
return Rotation::North;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<PlacedModule> parsePlacedModules(const toml::array& arr,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
std::vector<PlacedModule> result;
|
||||||
|
result.reserve(arr.size());
|
||||||
|
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||||
|
const toml::table* t = arr[i].as_table();
|
||||||
|
if (t == nullptr) { continue; }
|
||||||
|
toml::table& mt = const_cast<toml::table&>(*t);
|
||||||
|
|
||||||
|
const std::optional<std::string> type = mt["type"].value<std::string>();
|
||||||
|
const std::optional<int64_t> x = mt["x"].value<int64_t>();
|
||||||
|
const std::optional<int64_t> y = mt["y"].value<int64_t>();
|
||||||
|
const std::optional<std::string> rot = mt["rotation"].value<std::string>();
|
||||||
|
if (!type || !x || !y || !rot) { continue; }
|
||||||
|
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = *type;
|
||||||
|
pm.position = QPoint(static_cast<int>(*x), static_cast<int>(*y));
|
||||||
|
pm.rotation = parseRotationString(*rot);
|
||||||
|
result.push_back(std::move(pm));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
@@ -418,43 +459,13 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
|||||||
def.loot.scrapDrop = static_cast<int>(requireInt(lMt["scrap_drop"], file, lPath + ".scrap_drop"));
|
def.loot.scrapDrop = static_cast<int>(requireInt(lMt["scrap_drop"], file, lPath + ".scrap_drop"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: combat
|
// Optional: default_modules (REQ-WAV-DEFAULT-MODULES)
|
||||||
if (mt.contains("combat"))
|
if (mt.contains("default_modules"))
|
||||||
{
|
{
|
||||||
const std::string cPath = elemPath + ".combat";
|
const toml::array& modArr = requireArray(mt["default_modules"], file,
|
||||||
const toml::table& cTable = requireTable(mt["combat"], file, cPath);
|
elemPath + ".default_modules");
|
||||||
toml::table& cMt = const_cast<toml::table&>(cTable);
|
def.defaultModules = parsePlacedModules(modArr, file,
|
||||||
ShipCombat combat {
|
elemPath + ".default_modules");
|
||||||
requireFormula(cMt["damage_formula"], file, cPath + ".damage_formula"),
|
|
||||||
requireFormula(cMt["attack_range_formula"], file, cPath + ".attack_range_formula"),
|
|
||||||
requireFormula(cMt["attack_rate_formula"], file, cPath + ".attack_rate_formula"),
|
|
||||||
};
|
|
||||||
def.combat = std::move(combat);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional: salvage
|
|
||||||
if (mt.contains("salvage"))
|
|
||||||
{
|
|
||||||
const std::string sPath = elemPath + ".salvage";
|
|
||||||
const toml::table& sTable = requireTable(mt["salvage"], file, sPath);
|
|
||||||
toml::table& sMt = const_cast<toml::table&>(sTable);
|
|
||||||
ShipSalvage salvage;
|
|
||||||
salvage.collectionRange = requireDouble(sMt["collection_range"], file, sPath + ".collection_range");
|
|
||||||
salvage.cargoCapacity = static_cast<int>(requireInt(sMt["cargo_capacity"], file, sPath + ".cargo_capacity"));
|
|
||||||
def.salvage = salvage;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional: repair
|
|
||||||
if (mt.contains("repair"))
|
|
||||||
{
|
|
||||||
const std::string rPath = elemPath + ".repair";
|
|
||||||
const toml::table& rTable = requireTable(mt["repair"], file, rPath);
|
|
||||||
toml::table& rMt = const_cast<toml::table&>(rTable);
|
|
||||||
ShipRepair repair {
|
|
||||||
requireFormula(rMt["repair_rate_formula"], file, rPath + ".repair_rate_formula"),
|
|
||||||
requireFormula(rMt["repair_range_formula"], file, rPath + ".repair_range_formula"),
|
|
||||||
};
|
|
||||||
def.repair = std::move(repair);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.ships.push_back(std::move(def));
|
cfg.ships.push_back(std::move(def));
|
||||||
@@ -514,9 +525,11 @@ static const StatEntry kKnownStats[] = {
|
|||||||
{"health", "hp"},
|
{"health", "hp"},
|
||||||
{"movement", "speed"},
|
{"movement", "speed"},
|
||||||
{"sensor", "sensor_range"},
|
{"sensor", "sensor_range"},
|
||||||
{"combat", "damage"},
|
{"weapon", "damage"},
|
||||||
{"combat", "attack_range"},
|
{"weapon", "attack_range"},
|
||||||
{"combat", "attack_rate"},
|
{"weapon", "attack_rate"},
|
||||||
|
{"salvage", "collection_range"},
|
||||||
|
{"salvage", "cargo_capacity"},
|
||||||
{"repair", "repair_rate"},
|
{"repair", "repair_rate"},
|
||||||
{"repair", "repair_range"},
|
{"repair", "repair_range"},
|
||||||
};
|
};
|
||||||
@@ -597,6 +610,60 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Weapon capability section: [module.weapon] with base stat formulas
|
||||||
|
if (mt.contains("weapon"))
|
||||||
|
{
|
||||||
|
const std::string wPath = elemPath + ".weapon";
|
||||||
|
const toml::table& wTable = requireTable(mt["weapon"], file, wPath);
|
||||||
|
toml::table& wMt = const_cast<toml::table&>(wTable);
|
||||||
|
if (wMt.contains("damage_formula") || wMt.contains("attack_range_formula")
|
||||||
|
|| wMt.contains("attack_rate_formula"))
|
||||||
|
{
|
||||||
|
ModuleWeaponCapability cap;
|
||||||
|
cap.damageFormula = requireFormula(wMt["damage_formula"],
|
||||||
|
file, wPath + ".damage_formula");
|
||||||
|
cap.attackRangeFormula = requireFormula(wMt["attack_range_formula"],
|
||||||
|
file, wPath + ".attack_range_formula");
|
||||||
|
cap.attackRateFormula = requireFormula(wMt["attack_rate_formula"],
|
||||||
|
file, wPath + ".attack_rate_formula");
|
||||||
|
def.weaponCapability = std::move(cap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salvage capability section: [module.salvage] with base stat formulas
|
||||||
|
if (mt.contains("salvage"))
|
||||||
|
{
|
||||||
|
const std::string sPath = elemPath + ".salvage";
|
||||||
|
const toml::table& sTable = requireTable(mt["salvage"], file, sPath);
|
||||||
|
toml::table& sMt = const_cast<toml::table&>(sTable);
|
||||||
|
if (sMt.contains("collection_range_formula") || sMt.contains("cargo_capacity_formula"))
|
||||||
|
{
|
||||||
|
ModuleSalvageCapability cap;
|
||||||
|
cap.collectionRangeFormula = requireFormula(sMt["collection_range_formula"],
|
||||||
|
file, sPath + ".collection_range_formula");
|
||||||
|
cap.cargoCapacityFormula = requireFormula(sMt["cargo_capacity_formula"],
|
||||||
|
file, sPath + ".cargo_capacity_formula");
|
||||||
|
def.salvageCapability = std::move(cap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repair capability section: [module.repair] with base stat formulas
|
||||||
|
if (mt.contains("repair"))
|
||||||
|
{
|
||||||
|
const std::string rPath = elemPath + ".repair";
|
||||||
|
const toml::table& rTable = requireTable(mt["repair"], file, rPath);
|
||||||
|
toml::table& rMt = const_cast<toml::table&>(rTable);
|
||||||
|
if (rMt.contains("repair_rate_formula") || rMt.contains("repair_range_formula"))
|
||||||
|
{
|
||||||
|
ModuleRepairCapability cap;
|
||||||
|
cap.repairRateFormula = requireFormula(rMt["repair_rate_formula"],
|
||||||
|
file, rPath + ".repair_rate_formula");
|
||||||
|
cap.repairRangeFormula = requireFormula(rMt["repair_range_formula"],
|
||||||
|
file, rPath + ".repair_range_formula");
|
||||||
|
def.repairCapability = std::move(cap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cfg.modules.push_back(std::move(def));
|
cfg.modules.push_back(std::move(def));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -15,6 +16,26 @@ struct ModuleStatModifier
|
|||||||
Formula formula;
|
Formula formula;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Capability sections — present when the module grants that capability.
|
||||||
|
struct ModuleWeaponCapability
|
||||||
|
{
|
||||||
|
Formula damageFormula;
|
||||||
|
Formula attackRangeFormula;
|
||||||
|
Formula attackRateFormula;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ModuleSalvageCapability
|
||||||
|
{
|
||||||
|
Formula collectionRangeFormula;
|
||||||
|
Formula cargoCapacityFormula;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ModuleRepairCapability
|
||||||
|
{
|
||||||
|
Formula repairRateFormula;
|
||||||
|
Formula repairRangeFormula;
|
||||||
|
};
|
||||||
|
|
||||||
struct ModuleDef
|
struct ModuleDef
|
||||||
{
|
{
|
||||||
std::string id;
|
std::string id;
|
||||||
@@ -26,6 +47,10 @@ struct ModuleDef
|
|||||||
std::string fillColor;
|
std::string fillColor;
|
||||||
std::string glyph;
|
std::string glyph;
|
||||||
std::vector<ModuleStatModifier> statModifiers;
|
std::vector<ModuleStatModifier> statModifiers;
|
||||||
|
|
||||||
|
std::optional<ModuleWeaponCapability> weaponCapability;
|
||||||
|
std::optional<ModuleSalvageCapability> salvageCapability;
|
||||||
|
std::optional<ModuleRepairCapability> repairCapability;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ModulesConfig
|
struct ModulesConfig
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "Formula.h"
|
#include "Formula.h"
|
||||||
#include "RecipesConfig.h" // for RecipeIngredient
|
#include "RecipesConfig.h" // for RecipeIngredient
|
||||||
|
#include "ShipLayout.h" // for PlacedModule
|
||||||
|
|
||||||
// Build materials and initial per-schematic production level
|
// Build materials and initial per-schematic production level
|
||||||
// (REQ-BLD-SHIPYARD, REQ-DEF-SCHEMATIC-DROP).
|
// (REQ-BLD-SHIPYARD, REQ-DEF-SCHEMATIC-DROP).
|
||||||
@@ -42,27 +42,6 @@ struct ShipSensor
|
|||||||
Formula sensorRangeFormula; // REQ-SHP-SENSOR, REQ-SHP-STATS
|
Formula sensorRangeFormula; // REQ-SHP-SENSOR, REQ-SHP-STATS
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ShipCombat
|
|
||||||
{
|
|
||||||
Formula damageFormula;
|
|
||||||
Formula attackRangeFormula;
|
|
||||||
Formula attackRateFormula; // shots per second
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optional; present only on salvage ships (REQ-SHP-SALVAGE).
|
|
||||||
struct ShipSalvage
|
|
||||||
{
|
|
||||||
double collectionRange;
|
|
||||||
int cargoCapacity;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optional; present only on repair ships (REQ-SHP-REPAIR).
|
|
||||||
struct ShipRepair
|
|
||||||
{
|
|
||||||
Formula repairRateFormula;
|
|
||||||
Formula repairRangeFormula;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Scrap dropped on destruction (REQ-RES-SCRAP-DROP).
|
// Scrap dropped on destruction (REQ-RES-SCRAP-DROP).
|
||||||
struct ShipLoot
|
struct ShipLoot
|
||||||
{
|
{
|
||||||
@@ -82,12 +61,8 @@ struct ShipDef
|
|||||||
ShipSensor sensor;
|
ShipSensor sensor;
|
||||||
ShipLoot loot;
|
ShipLoot loot;
|
||||||
|
|
||||||
// Role-specific sections. A ship is a combat ship if combat is present,
|
// Module layout used for enemy wave ships (REQ-WAV-DEFAULT-MODULES).
|
||||||
// a salvage ship if salvage is present, etc. A ship may have multiple
|
std::vector<PlacedModule> defaultModules;
|
||||||
// of these set (hybrid ships) once the behavior systems support it.
|
|
||||||
std::optional<ShipCombat> combat;
|
|
||||||
std::optional<ShipSalvage> salvage;
|
|
||||||
std::optional<ShipRepair> repair;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ShipsConfig
|
struct ShipsConfig
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ entt::entity EntityAdmin::createEntity()
|
|||||||
return m_registry.create();
|
return m_registry.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entt::entity EntityAdmin::createModuleEntity()
|
||||||
|
{
|
||||||
|
return m_registry.create();
|
||||||
|
}
|
||||||
|
|
||||||
bool EntityAdmin::isValid(entt::entity entity) const
|
bool EntityAdmin::isValid(entt::entity entity) const
|
||||||
{
|
{
|
||||||
return m_registry.valid(entity);
|
return m_registry.valid(entity);
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ public:
|
|||||||
|
|
||||||
entt::entity spawnHqProxy(QVector2D position, float hp, float maxHp);
|
entt::entity spawnHqProxy(QVector2D position, float hp, float maxHp);
|
||||||
|
|
||||||
|
// Creates a bare entity for module child entities (weapons, salvage, repair).
|
||||||
|
// Caller is responsible for attaching all required components.
|
||||||
|
entt::entity createModuleEntity();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
entt::entity createEntity();
|
entt::entity createEntity();
|
||||||
|
|
||||||
|
|||||||
9
src/lib/ecs/component/ModuleOwnerComponent.h
Normal file
9
src/lib/ecs/component/ModuleOwnerComponent.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
// Links a capability module child entity back to its owner ship or station.
|
||||||
|
struct ModuleOwnerComponent
|
||||||
|
{
|
||||||
|
entt::entity owner;
|
||||||
|
};
|
||||||
@@ -7,4 +7,5 @@
|
|||||||
struct RepairBehaviorComponent
|
struct RepairBehaviorComponent
|
||||||
{
|
{
|
||||||
std::optional<entt::entity> currentTarget;
|
std::optional<entt::entity> currentTarget;
|
||||||
|
float maxRepairRange = 0.0f;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,5 +9,6 @@
|
|||||||
struct SalvageBehaviorComponent
|
struct SalvageBehaviorComponent
|
||||||
{
|
{
|
||||||
std::optional<QVector2D> scrapTarget;
|
std::optional<QVector2D> scrapTarget;
|
||||||
BuildingId deliveryBay; // kInvalidBuildingId until assigned at a salvage bay
|
BuildingId deliveryBay; // kInvalidBuildingId until assigned at a salvage bay
|
||||||
|
float maxCollectionRange = 0.0f;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "AiSystem.h"
|
#include "AiSystem.h"
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "HomeReturnBehaviorComponent.h"
|
#include "HomeReturnBehaviorComponent.h"
|
||||||
#include "HqProxyComponent.h"
|
#include "HqProxyComponent.h"
|
||||||
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "MovementIntentComponent.h"
|
#include "MovementIntentComponent.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "RallyBehaviorComponent.h"
|
#include "RallyBehaviorComponent.h"
|
||||||
@@ -224,13 +226,13 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
admin.forEach<RepairBehaviorComponent, RepairToolComponent, PositionComponent,
|
admin.forEach<RepairBehaviorComponent, PositionComponent,
|
||||||
FactionComponent, SensorRangeComponent, MovementIntentComponent>(
|
FactionComponent, SensorRangeComponent, MovementIntentComponent>(
|
||||||
[&](entt::entity e, RepairBehaviorComponent& rb, RepairToolComponent& rt,
|
[&](entt::entity e, RepairBehaviorComponent& rb,
|
||||||
PositionComponent& pos, FactionComponent& /*faction*/,
|
PositionComponent& pos, FactionComponent& /*faction*/,
|
||||||
SensorRangeComponent& sensor, MovementIntentComponent& intent)
|
SensorRangeComponent& sensor, MovementIntentComponent& intent)
|
||||||
{
|
{
|
||||||
const float repairRange = rt.range;
|
const float repairRange = rb.maxRepairRange;
|
||||||
|
|
||||||
// Flee if enemy nearby.
|
// Flee if enemy nearby.
|
||||||
bool enemyNearby = false;
|
bool enemyNearby = false;
|
||||||
@@ -303,17 +305,6 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
|
|||||||
targetPos = admin.get<PositionComponent>(target).value;
|
targetPos = admin.get<PositionComponent>(target).value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float distToTarget = (targetPos - pos.value).length();
|
|
||||||
if (distToTarget <= repairRange)
|
|
||||||
{
|
|
||||||
if (admin.isValid(target) && admin.hasAll<HealthComponent>(target))
|
|
||||||
{
|
|
||||||
HealthComponent& targetHealth = admin.get<HealthComponent>(target);
|
|
||||||
targetHealth.hp = std::min(targetHealth.hp + rt.ratePerTick,
|
|
||||||
targetHealth.maxHp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (2 > intent.priority)
|
if (2 > intent.priority)
|
||||||
{
|
{
|
||||||
intent = MovementIntentComponent{2, targetPos};
|
intent = MovementIntentComponent{2, targetPos};
|
||||||
@@ -321,6 +312,33 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tickRepairTools
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void AiSystem::tickRepairTools(EntityAdmin& 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);
|
||||||
|
if (!rb.currentTarget) { return; }
|
||||||
|
|
||||||
|
const entt::entity target = *rb.currentTarget;
|
||||||
|
if (!admin.isValid(target) || !admin.hasAll<HealthComponent>(target)) { return; }
|
||||||
|
|
||||||
|
const PositionComponent& ownerPos = admin.get<PositionComponent>(owner.owner);
|
||||||
|
const PositionComponent& targetPos = admin.get<PositionComponent>(target);
|
||||||
|
const float dist = (targetPos.value - ownerPos.value).length();
|
||||||
|
if (dist > rt.range) { return; }
|
||||||
|
|
||||||
|
HealthComponent& targetHealth = admin.get<HealthComponent>(target);
|
||||||
|
targetHealth.hp = std::min(targetHealth.hp + rt.ratePerTick, targetHealth.maxHp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// tickSalvageBehavior (priority 1)
|
// tickSalvageBehavior (priority 1)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -344,15 +362,31 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Aggregate cargo across all salvage-module children per owning ship.
|
||||||
|
struct AggregatedCargo
|
||||||
|
{
|
||||||
|
int totalCurrent = 0;
|
||||||
|
int totalCapacity = 0;
|
||||||
|
};
|
||||||
|
std::unordered_map<entt::entity, AggregatedCargo> cargoByShip;
|
||||||
|
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity /*ce*/, const SalvageCargoComponent& c, const ModuleOwnerComponent& o)
|
||||||
|
{
|
||||||
|
AggregatedCargo& agg = cargoByShip[o.owner];
|
||||||
|
agg.totalCurrent += c.current;
|
||||||
|
agg.totalCapacity += c.capacity;
|
||||||
|
});
|
||||||
|
|
||||||
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
|
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
|
||||||
|
|
||||||
admin.forEach<SalvageBehaviorComponent, SalvageCargoComponent, PositionComponent,
|
admin.forEach<SalvageBehaviorComponent, PositionComponent,
|
||||||
SensorRangeComponent, MovementIntentComponent>(
|
SensorRangeComponent, MovementIntentComponent>(
|
||||||
[&](entt::entity /*e*/, SalvageBehaviorComponent& salvageBehavior,
|
[&](entt::entity e, SalvageBehaviorComponent& salvageBehavior,
|
||||||
SalvageCargoComponent& cargo, PositionComponent& pos,
|
PositionComponent& pos,
|
||||||
SensorRangeComponent& sensor, MovementIntentComponent& intent)
|
SensorRangeComponent& sensor, MovementIntentComponent& intent)
|
||||||
{
|
{
|
||||||
const float collectRange = cargo.collectionRange;
|
const float collectRange = salvageBehavior.maxCollectionRange;
|
||||||
|
const AggregatedCargo& cargoState = cargoByShip[e];
|
||||||
|
|
||||||
// Assign nearest SalvageBay if needed.
|
// Assign nearest SalvageBay if needed.
|
||||||
if (salvageBehavior.deliveryBay == kInvalidBuildingId)
|
if (salvageBehavior.deliveryBay == kInvalidBuildingId)
|
||||||
@@ -378,7 +412,8 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool cargoFull = (cargo.current >= cargo.capacity);
|
const bool cargoFull = (cargoState.totalCurrent >= cargoState.totalCapacity
|
||||||
|
&& cargoState.totalCapacity > 0);
|
||||||
|
|
||||||
if (cargoFull)
|
if (cargoFull)
|
||||||
{
|
{
|
||||||
@@ -389,17 +424,26 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
|
|||||||
if (bayId != kInvalidBuildingId
|
if (bayId != kInvalidBuildingId
|
||||||
&& (pos.value - bayPos).length() <= 1.0f)
|
&& (pos.value - bayPos).length() <= 1.0f)
|
||||||
{
|
{
|
||||||
if (buildings.deliverScrapToSalvageBay(bayId))
|
// Decrement first non-empty salvage child.
|
||||||
{
|
bool delivered = false;
|
||||||
--cargo.current;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retreat if enemy near and cargo empty.
|
// Retreat if enemy near and cargo empty.
|
||||||
bool retreating = false;
|
bool retreating = false;
|
||||||
if (cargo.current == 0)
|
if (cargoState.totalCurrent == 0)
|
||||||
{
|
{
|
||||||
for (const EnemyShipPos& enemy : enemyShips)
|
for (const EnemyShipPos& enemy : enemyShips)
|
||||||
{
|
{
|
||||||
@@ -417,16 +461,24 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
|
|||||||
}
|
}
|
||||||
if (retreating) { return; }
|
if (retreating) { return; }
|
||||||
|
|
||||||
// Collect nearby scrap.
|
// Collect nearby scrap — increment first non-full salvage child.
|
||||||
for (const ScrapInfo& si : allScrap)
|
for (const ScrapInfo& si : allScrap)
|
||||||
{
|
{
|
||||||
if ((si.position - pos.value).length() <= collectRange)
|
if ((si.position - pos.value).length() <= collectRange)
|
||||||
{
|
{
|
||||||
if (scraps.consume(si.entity))
|
bool collected = false;
|
||||||
{
|
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||||
++cargo.current;
|
[&](entt::entity /*ce*/, SalvageCargoComponent& c,
|
||||||
salvageBehavior.scrapTarget = std::nullopt;
|
const ModuleOwnerComponent& o)
|
||||||
}
|
{
|
||||||
|
if (collected || o.owner != e || c.current >= c.capacity) { return; }
|
||||||
|
if (scraps.consume(si.entity))
|
||||||
|
{
|
||||||
|
++c.current;
|
||||||
|
salvageBehavior.scrapTarget = std::nullopt;
|
||||||
|
collected = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ public:
|
|||||||
void tickHomeReturnBehavior(EntityAdmin& admin);
|
void tickHomeReturnBehavior(EntityAdmin& admin);
|
||||||
void tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSystem& buildings);
|
void tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSystem& buildings);
|
||||||
void tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings);
|
void tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings);
|
||||||
|
void tickRepairTools(EntityAdmin& admin);
|
||||||
void tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps, BuildingSystem& buildings);
|
void tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps, BuildingSystem& buildings);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "StationBodyComponent.h"
|
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
@@ -22,24 +22,18 @@ void CombatSystem::tick(Tick currentTick,
|
|||||||
BuildingSystem& /*buildings*/,
|
BuildingSystem& /*buildings*/,
|
||||||
std::vector<FireEvent>& outFireEvents)
|
std::vector<FireEvent>& outFireEvents)
|
||||||
{
|
{
|
||||||
// Ship weapons.
|
// All weapons (ships and stations) are child entities linked via ModuleOwnerComponent.
|
||||||
admin.forEach<WeaponComponent, ThreatResponseBehaviorComponent,
|
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||||
PositionComponent, FactionComponent>(
|
[&](entt::entity /*e*/, WeaponComponent& weapon, const ModuleOwnerComponent& owner)
|
||||||
[&](entt::entity e, WeaponComponent& weapon,
|
|
||||||
ThreatResponseBehaviorComponent& threatResponseBehavior,
|
|
||||||
PositionComponent& pos, FactionComponent& faction)
|
|
||||||
{
|
{
|
||||||
weapon.currentTarget = threatResponseBehavior.currentTarget;
|
if (admin.hasAll<ThreatResponseBehaviorComponent>(owner.owner))
|
||||||
resolveWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents);
|
{
|
||||||
});
|
weapon.currentTarget =
|
||||||
|
admin.get<ThreatResponseBehaviorComponent>(owner.owner).currentTarget;
|
||||||
// Station weapons (entities with StationBodyComponent; ships are excluded because
|
}
|
||||||
// they lack that component and are already handled by the ship loop above).
|
const PositionComponent& pos = admin.get<PositionComponent>(owner.owner);
|
||||||
admin.forEach<WeaponComponent, PositionComponent, FactionComponent, StationBodyComponent>(
|
const FactionComponent& faction = admin.get<FactionComponent>(owner.owner);
|
||||||
[&](entt::entity e, WeaponComponent& weapon, PositionComponent& pos,
|
resolveWeapon(owner.owner, weapon, pos, faction, currentTick, admin, outFireEvents);
|
||||||
FactionComponent& faction, const StationBodyComponent& /*sb*/)
|
|
||||||
{
|
|
||||||
resolveWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "DynamicBodyComponent.h"
|
#include "DynamicBodyComponent.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "ModulesConfig.h"
|
#include "ModulesConfig.h"
|
||||||
#include "MovementIntentComponent.h"
|
#include "MovementIntentComponent.h"
|
||||||
#include "RallyBehaviorComponent.h"
|
#include "RallyBehaviorComponent.h"
|
||||||
@@ -85,17 +87,182 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
angularAccelPerTick, maxRotationSpeedPerTick, sensorRange,
|
angularAccelPerTick, maxRotationSpeedPerTick, sensorRange,
|
||||||
level, schematicId, isEnemy);
|
level, schematicId, isEnemy);
|
||||||
|
|
||||||
// Optional components based on ship role.
|
// Determine module list: configured layout takes precedence over default.
|
||||||
if (def->combat)
|
const std::vector<PlacedModule>& modules =
|
||||||
{
|
layout.has_value() ? layout->placedModules : def->defaultModules;
|
||||||
WeaponComponent w;
|
|
||||||
w.damage = static_cast<float>(def->combat->damageFormula.evaluate(x));
|
|
||||||
w.range = static_cast<float>(def->combat->attackRangeFormula.evaluate(x));
|
|
||||||
w.fireRateHz = static_cast<float>(def->combat->attackRateFormula.evaluate(x));
|
|
||||||
w.cooldownTicks = 0.0f;
|
|
||||||
w.currentTarget = std::nullopt;
|
|
||||||
m_admin.addComponent<WeaponComponent>(entity, w);
|
|
||||||
|
|
||||||
|
// --- Pass 1: create capability child entities ----------------------------
|
||||||
|
std::vector<entt::entity> weaponChildren;
|
||||||
|
std::vector<entt::entity> salvageChildren;
|
||||||
|
std::vector<entt::entity> repairChildren;
|
||||||
|
|
||||||
|
for (const PlacedModule& pm : modules)
|
||||||
|
{
|
||||||
|
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||||
|
if (!modDef) { continue; }
|
||||||
|
|
||||||
|
const double mx = static_cast<double>(modDef->playerProductionLevel);
|
||||||
|
|
||||||
|
if (modDef->weaponCapability)
|
||||||
|
{
|
||||||
|
WeaponComponent w;
|
||||||
|
w.damage = static_cast<float>(
|
||||||
|
modDef->weaponCapability->damageFormula.evaluate(mx));
|
||||||
|
w.range = static_cast<float>(
|
||||||
|
modDef->weaponCapability->attackRangeFormula.evaluate(mx));
|
||||||
|
w.fireRateHz = static_cast<float>(
|
||||||
|
modDef->weaponCapability->attackRateFormula.evaluate(mx));
|
||||||
|
w.cooldownTicks = 0.0f;
|
||||||
|
w.currentTarget = std::nullopt;
|
||||||
|
|
||||||
|
entt::entity child = m_admin.createModuleEntity();
|
||||||
|
m_admin.addComponent<WeaponComponent>(child, w);
|
||||||
|
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
|
||||||
|
weaponChildren.push_back(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modDef->salvageCapability)
|
||||||
|
{
|
||||||
|
SalvageCargoComponent cargo;
|
||||||
|
cargo.capacity = static_cast<int>(
|
||||||
|
modDef->salvageCapability->cargoCapacityFormula.evaluate(mx));
|
||||||
|
cargo.current = 0;
|
||||||
|
cargo.collectionRange = static_cast<float>(
|
||||||
|
modDef->salvageCapability->collectionRangeFormula.evaluate(mx));
|
||||||
|
|
||||||
|
entt::entity child = m_admin.createModuleEntity();
|
||||||
|
m_admin.addComponent<SalvageCargoComponent>(child, cargo);
|
||||||
|
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
|
||||||
|
salvageChildren.push_back(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modDef->repairCapability)
|
||||||
|
{
|
||||||
|
RepairToolComponent rt;
|
||||||
|
rt.ratePerTick = static_cast<float>(
|
||||||
|
modDef->repairCapability->repairRateFormula.evaluate(mx))
|
||||||
|
/ static_cast<float>(kTickRateHz);
|
||||||
|
rt.range = static_cast<float>(
|
||||||
|
modDef->repairCapability->repairRangeFormula.evaluate(mx));
|
||||||
|
rt.currentTarget = std::nullopt;
|
||||||
|
|
||||||
|
entt::entity child = m_admin.createModuleEntity();
|
||||||
|
m_admin.addComponent<RepairToolComponent>(child, rt);
|
||||||
|
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
|
||||||
|
repairChildren.push_back(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Pass 2: apply passive stat modifiers --------------------------------
|
||||||
|
|
||||||
|
// Accumulate hull-level modifiers.
|
||||||
|
std::map<std::string, std::pair<double, double>> hullMods;
|
||||||
|
// Per-capability-type modifier accumulators (applied to each child).
|
||||||
|
std::map<std::string, std::pair<double, double>> weaponMods;
|
||||||
|
std::map<std::string, std::pair<double, double>> salvageMods;
|
||||||
|
std::map<std::string, std::pair<double, double>> repairMods;
|
||||||
|
|
||||||
|
for (const PlacedModule& pm : modules)
|
||||||
|
{
|
||||||
|
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||||
|
if (!modDef) { continue; }
|
||||||
|
|
||||||
|
const double mx = static_cast<double>(modDef->playerProductionLevel);
|
||||||
|
|
||||||
|
for (const ModuleStatModifier& sm : modDef->statModifiers)
|
||||||
|
{
|
||||||
|
const double val = sm.formula.evaluate(mx);
|
||||||
|
|
||||||
|
// Route modifier to the correct accumulator by stat category.
|
||||||
|
// weapon/salvage/repair stats go to the corresponding child map;
|
||||||
|
// hull stats (hp, speed, sensor_range, …) go to hullMods.
|
||||||
|
const bool isWeaponStat = (sm.stat == "damage"
|
||||||
|
|| sm.stat == "attack_range"
|
||||||
|
|| sm.stat == "attack_rate");
|
||||||
|
const bool isSalvageStat = (sm.stat == "collection_range"
|
||||||
|
|| sm.stat == "cargo_capacity");
|
||||||
|
const bool isRepairStat = (sm.stat == "repair_rate"
|
||||||
|
|| sm.stat == "repair_range");
|
||||||
|
|
||||||
|
std::map<std::string, std::pair<double, double>>* target = &hullMods;
|
||||||
|
if (isWeaponStat) { target = &weaponMods; }
|
||||||
|
if (isSalvageStat) { target = &salvageMods; }
|
||||||
|
if (isRepairStat) { target = &repairMods; }
|
||||||
|
|
||||||
|
std::pair<double, double>& acc = (*target)[sm.stat];
|
||||||
|
if (sm.modifierType == "multiplicative")
|
||||||
|
{
|
||||||
|
acc.first += (val - 1.0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
acc.second += val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: apply a modifier map to a float stat.
|
||||||
|
auto applyMod = [](float& stat, const std::string& name,
|
||||||
|
const std::map<std::string, std::pair<double, double>>& mods)
|
||||||
|
{
|
||||||
|
const auto it = mods.find(name);
|
||||||
|
if (it != mods.end())
|
||||||
|
{
|
||||||
|
stat = static_cast<float>(
|
||||||
|
static_cast<double>(stat) * (1.0 + it->second.first)
|
||||||
|
+ it->second.second);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply hull modifiers.
|
||||||
|
{
|
||||||
|
HealthComponent& health = m_admin.get<HealthComponent>(entity);
|
||||||
|
DynamicBodyComponent& dynamics = m_admin.get<DynamicBodyComponent>(entity);
|
||||||
|
SensorRangeComponent& sensor = m_admin.get<SensorRangeComponent>(entity);
|
||||||
|
|
||||||
|
applyMod(health.maxHp, "hp", hullMods);
|
||||||
|
health.hp = health.maxHp;
|
||||||
|
applyMod(dynamics.maxSpeedPerTick, "speed", hullMods);
|
||||||
|
applyMod(dynamics.mainAccelerationPerTick, "main_acceleration", hullMods);
|
||||||
|
applyMod(dynamics.maneuveringAccelerationPerTick, "maneuvering_acceleration", hullMods);
|
||||||
|
applyMod(dynamics.angularAccelerationPerTick, "angular_acceleration", hullMods);
|
||||||
|
applyMod(dynamics.maxRotationSpeedPerTick, "max_rotation_speed", hullMods);
|
||||||
|
applyMod(sensor.value, "sensor_range", hullMods);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply weapon modifiers to each weapon child.
|
||||||
|
for (entt::entity child : weaponChildren)
|
||||||
|
{
|
||||||
|
WeaponComponent& w = m_admin.get<WeaponComponent>(child);
|
||||||
|
applyMod(w.damage, "damage", weaponMods);
|
||||||
|
applyMod(w.range, "attack_range", weaponMods);
|
||||||
|
applyMod(w.fireRateHz, "attack_rate", weaponMods);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply salvage modifiers to each salvage child.
|
||||||
|
for (entt::entity child : salvageChildren)
|
||||||
|
{
|
||||||
|
SalvageCargoComponent& c = m_admin.get<SalvageCargoComponent>(child);
|
||||||
|
float fRange = c.collectionRange;
|
||||||
|
float fCapacity = static_cast<float>(c.capacity);
|
||||||
|
applyMod(fRange, "collection_range", salvageMods);
|
||||||
|
applyMod(fCapacity, "cargo_capacity", salvageMods);
|
||||||
|
c.collectionRange = fRange;
|
||||||
|
c.capacity = static_cast<int>(fCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply repair modifiers to each repair child.
|
||||||
|
for (entt::entity child : repairChildren)
|
||||||
|
{
|
||||||
|
RepairToolComponent& rt = m_admin.get<RepairToolComponent>(child);
|
||||||
|
applyMod(rt.ratePerTick, "repair_rate", repairMods);
|
||||||
|
applyMod(rt.range, "repair_range", repairMods);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Pass 3: attach behavior components based on capability presence -----
|
||||||
|
|
||||||
|
if (!weaponChildren.empty())
|
||||||
|
{
|
||||||
m_admin.addComponent<ThreatResponseBehaviorComponent>(
|
m_admin.addComponent<ThreatResponseBehaviorComponent>(
|
||||||
entity, ThreatResponseBehaviorComponent{});
|
entity, ThreatResponseBehaviorComponent{});
|
||||||
|
|
||||||
@@ -106,95 +273,35 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (def->salvage)
|
if (!salvageChildren.empty())
|
||||||
{
|
{
|
||||||
SalvageCargoComponent cargo;
|
float maxCollRange = 0.0f;
|
||||||
cargo.capacity = def->salvage->cargoCapacity;
|
for (entt::entity child : salvageChildren)
|
||||||
cargo.current = 0;
|
{
|
||||||
cargo.collectionRange = static_cast<float>(def->salvage->collectionRange);
|
const float r = m_admin.get<SalvageCargoComponent>(child).collectionRange;
|
||||||
m_admin.addComponent<SalvageCargoComponent>(entity, cargo);
|
if (r > maxCollRange) { maxCollRange = r; }
|
||||||
|
}
|
||||||
|
|
||||||
SalvageBehaviorComponent salvageBehavior;
|
SalvageBehaviorComponent sb;
|
||||||
salvageBehavior.scrapTarget = std::nullopt;
|
sb.scrapTarget = std::nullopt;
|
||||||
salvageBehavior.deliveryBay = kInvalidBuildingId;
|
sb.deliveryBay = kInvalidBuildingId;
|
||||||
m_admin.addComponent<SalvageBehaviorComponent>(entity, salvageBehavior);
|
sb.maxCollectionRange = maxCollRange;
|
||||||
|
m_admin.addComponent<SalvageBehaviorComponent>(entity, sb);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (def->repair)
|
if (!repairChildren.empty())
|
||||||
{
|
{
|
||||||
RepairToolComponent rt;
|
float maxRepairRange = 0.0f;
|
||||||
rt.ratePerTick = static_cast<float>(def->repair->repairRateFormula.evaluate(x));
|
for (entt::entity child : repairChildren)
|
||||||
rt.range = static_cast<float>(def->repair->repairRangeFormula.evaluate(x));
|
|
||||||
rt.currentTarget = std::nullopt;
|
|
||||||
m_admin.addComponent<RepairToolComponent>(entity, rt);
|
|
||||||
|
|
||||||
m_admin.addComponent<RepairBehaviorComponent>(entity, RepairBehaviorComponent{});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply module stat modifiers (REQ-MOD-STAT-CALC).
|
|
||||||
if (layout.has_value() && !layout->placedModules.empty())
|
|
||||||
{
|
|
||||||
std::map<std::string, std::pair<double, double>> mods;
|
|
||||||
for (const PlacedModule& pm : layout->placedModules)
|
|
||||||
{
|
{
|
||||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
const float r = m_admin.get<RepairToolComponent>(child).range;
|
||||||
if (!modDef)
|
if (r > maxRepairRange) { maxRepairRange = r; }
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (const ModuleStatModifier& sm : modDef->statModifiers)
|
|
||||||
{
|
|
||||||
const double val = sm.formula.evaluate(
|
|
||||||
static_cast<double>(modDef->playerProductionLevel));
|
|
||||||
std::pair<double, double>& acc = mods[sm.stat];
|
|
||||||
if (sm.modifierType == "multiplicative")
|
|
||||||
{
|
|
||||||
acc.first += (val - 1.0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
acc.second += val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto applyMod = [&mods](float& stat, const std::string& name) {
|
RepairBehaviorComponent rb;
|
||||||
const std::map<std::string, std::pair<double, double>>::const_iterator it =
|
rb.currentTarget = std::nullopt;
|
||||||
mods.find(name);
|
rb.maxRepairRange = maxRepairRange;
|
||||||
if (it != mods.end())
|
m_admin.addComponent<RepairBehaviorComponent>(entity, rb);
|
||||||
{
|
|
||||||
stat = static_cast<float>(
|
|
||||||
static_cast<double>(stat) * (1.0 + it->second.first)
|
|
||||||
+ it->second.second);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
HealthComponent& health = m_admin.get<HealthComponent>(entity);
|
|
||||||
DynamicBodyComponent& dynamics = m_admin.get<DynamicBodyComponent>(entity);
|
|
||||||
SensorRangeComponent& sensor = m_admin.get<SensorRangeComponent>(entity);
|
|
||||||
|
|
||||||
applyMod(health.maxHp, "hp");
|
|
||||||
health.hp = health.maxHp;
|
|
||||||
applyMod(dynamics.maxSpeedPerTick, "speed");
|
|
||||||
applyMod(dynamics.mainAccelerationPerTick, "main_acceleration");
|
|
||||||
applyMod(dynamics.maneuveringAccelerationPerTick, "maneuvering_acceleration");
|
|
||||||
applyMod(dynamics.angularAccelerationPerTick, "angular_acceleration");
|
|
||||||
applyMod(dynamics.maxRotationSpeedPerTick, "max_rotation_speed");
|
|
||||||
applyMod(sensor.value, "sensor_range");
|
|
||||||
|
|
||||||
if (m_admin.hasAll<WeaponComponent>(entity))
|
|
||||||
{
|
|
||||||
WeaponComponent& weapon = m_admin.get<WeaponComponent>(entity);
|
|
||||||
applyMod(weapon.damage, "damage");
|
|
||||||
applyMod(weapon.range, "attack_range");
|
|
||||||
applyMod(weapon.fireRateHz, "attack_rate");
|
|
||||||
}
|
|
||||||
if (m_admin.hasAll<RepairToolComponent>(entity))
|
|
||||||
{
|
|
||||||
RepairToolComponent& repairTool = m_admin.get<RepairToolComponent>(entity);
|
|
||||||
applyMod(repairTool.ratePerTick, "repair_rate");
|
|
||||||
applyMod(repairTool.range, "repair_range");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return entity;
|
return entity;
|
||||||
@@ -202,6 +309,13 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
|
|
||||||
void ShipSystem::despawn(entt::entity entity)
|
void ShipSystem::despawn(entt::entity entity)
|
||||||
{
|
{
|
||||||
|
std::vector<entt::entity> children;
|
||||||
|
m_admin.forEach<ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity e, const ModuleOwnerComponent& o)
|
||||||
|
{
|
||||||
|
if (o.owner == entity) { children.push_back(e); }
|
||||||
|
});
|
||||||
|
for (entt::entity child : children) { m_admin.destroy(child); }
|
||||||
m_admin.destroy(entity);
|
m_admin.destroy(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "DynamicBodySystem.h"
|
#include "DynamicBodySystem.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "MovementIntentSystem.h"
|
#include "MovementIntentSystem.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
@@ -173,6 +174,7 @@ void Simulation::tick()
|
|||||||
m_aiSystem->tickHomeReturnBehavior(m_admin); // priority 4
|
m_aiSystem->tickHomeReturnBehavior(m_admin); // priority 4
|
||||||
m_aiSystem->tickThreatResponseBehavior(m_admin, *m_buildingSystem); // priority 3
|
m_aiSystem->tickThreatResponseBehavior(m_admin, *m_buildingSystem); // priority 3
|
||||||
m_aiSystem->tickRepairBehavior(m_admin, *m_buildingSystem); // priority 2
|
m_aiSystem->tickRepairBehavior(m_admin, *m_buildingSystem); // priority 2
|
||||||
|
m_aiSystem->tickRepairTools(m_admin);
|
||||||
m_aiSystem->tickSalvageBehavior(m_admin, *m_scrapSystem, *m_buildingSystem); // priority 1
|
m_aiSystem->tickSalvageBehavior(m_admin, *m_scrapSystem, *m_buildingSystem); // priority 1
|
||||||
|
|
||||||
// Step 8: combat resolution
|
// Step 8: combat resolution
|
||||||
@@ -255,7 +257,12 @@ void Simulation::placeInitialStructures()
|
|||||||
}
|
}
|
||||||
m_playerStation1Entity = m_admin.spawnStation(
|
m_playerStation1Entity = m_admin.spawnStation(
|
||||||
anchor, psParsed.footprint, absCells, psHp, psHp, false);
|
anchor, psParsed.footprint, absCells, psHp, psHp, false);
|
||||||
m_admin.addComponent<WeaponComponent>(m_playerStation1Entity, psWeapon);
|
{
|
||||||
|
entt::entity wChild = m_admin.createModuleEntity();
|
||||||
|
m_admin.addComponent<WeaponComponent>(wChild, psWeapon);
|
||||||
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
|
ModuleOwnerComponent{m_playerStation1Entity});
|
||||||
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -267,7 +274,12 @@ void Simulation::placeInitialStructures()
|
|||||||
}
|
}
|
||||||
m_playerStation2Entity = m_admin.spawnStation(
|
m_playerStation2Entity = m_admin.spawnStation(
|
||||||
anchor, psParsed.footprint, absCells, psHp, psHp, false);
|
anchor, psParsed.footprint, absCells, psHp, psHp, false);
|
||||||
m_admin.addComponent<WeaponComponent>(m_playerStation2Entity, psWeapon);
|
{
|
||||||
|
entt::entity wChild = m_admin.createModuleEntity();
|
||||||
|
m_admin.addComponent<WeaponComponent>(wChild, psWeapon);
|
||||||
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
|
ModuleOwnerComponent{m_playerStation2Entity});
|
||||||
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +328,12 @@ void Simulation::placeEnemyStationSet(int generation)
|
|||||||
}
|
}
|
||||||
m_currentEnemyStationEntities[0] = m_admin.spawnStation(
|
m_currentEnemyStationEntities[0] = m_admin.spawnStation(
|
||||||
anchor, esParsed.footprint, absCells, esHp, esHp, true);
|
anchor, esParsed.footprint, absCells, esHp, esHp, true);
|
||||||
m_admin.addComponent<WeaponComponent>(m_currentEnemyStationEntities[0], esWeapon);
|
{
|
||||||
|
entt::entity wChild = m_admin.createModuleEntity();
|
||||||
|
m_admin.addComponent<WeaponComponent>(wChild, esWeapon);
|
||||||
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
|
ModuleOwnerComponent{m_currentEnemyStationEntities[0]});
|
||||||
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -328,7 +345,12 @@ void Simulation::placeEnemyStationSet(int generation)
|
|||||||
}
|
}
|
||||||
m_currentEnemyStationEntities[1] = m_admin.spawnStation(
|
m_currentEnemyStationEntities[1] = m_admin.spawnStation(
|
||||||
anchor, esParsed.footprint, absCells, esHp, esHp, true);
|
anchor, esParsed.footprint, absCells, esHp, esHp, true);
|
||||||
m_admin.addComponent<WeaponComponent>(m_currentEnemyStationEntities[1], esWeapon);
|
{
|
||||||
|
entt::entity wChild = m_admin.createModuleEntity();
|
||||||
|
m_admin.addComponent<WeaponComponent>(wChild, esWeapon);
|
||||||
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
|
ModuleOwnerComponent{m_currentEnemyStationEntities[1]});
|
||||||
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,6 +429,15 @@ void Simulation::tickDeathsAndLoot()
|
|||||||
m_scrapSystem->spawn(pos.value, scrap, despawnAt);
|
m_scrapSystem->spawn(pos.value, scrap, despawnAt);
|
||||||
}
|
}
|
||||||
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
|
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
|
||||||
|
{
|
||||||
|
std::vector<entt::entity> stationChildren;
|
||||||
|
m_admin.forEach<ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity ce, const ModuleOwnerComponent& o)
|
||||||
|
{
|
||||||
|
if (o.owner == deadEntity) { stationChildren.push_back(ce); }
|
||||||
|
});
|
||||||
|
for (entt::entity ce : stationChildren) { m_admin.destroy(ce); }
|
||||||
|
}
|
||||||
m_admin.destroy(deadEntity);
|
m_admin.destroy(deadEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ void WaveSystem::tickWaveScheduler(Tick currentTick, ShipSystem& ships,
|
|||||||
if (currentTick >= entry.spawnAt)
|
if (currentTick >= entry.spawnAt)
|
||||||
{
|
{
|
||||||
ships.spawn(entry.schematicId, entry.level, entry.position,
|
ships.spawn(entry.schematicId, entry.level, entry.position,
|
||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true, entry.layout);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -90,8 +90,9 @@ std::vector<WaveSystem::SpawnEntry> WaveSystem::composeWave(Tick currentTick,
|
|||||||
// Build eligible ship list with their costs at the current level.
|
// Build eligible ship list with their costs at the current level.
|
||||||
struct EligibleShip
|
struct EligibleShip
|
||||||
{
|
{
|
||||||
std::string schematicId;
|
std::string schematicId;
|
||||||
double cost;
|
double cost;
|
||||||
|
std::vector<PlacedModule> defaultModules;
|
||||||
};
|
};
|
||||||
std::vector<EligibleShip> eligible;
|
std::vector<EligibleShip> eligible;
|
||||||
for (const ShipDef& def : m_config.ships.ships)
|
for (const ShipDef& def : m_config.ships.ships)
|
||||||
@@ -100,8 +101,9 @@ std::vector<WaveSystem::SpawnEntry> WaveSystem::composeWave(Tick currentTick,
|
|||||||
if (cost > 0.0)
|
if (cost > 0.0)
|
||||||
{
|
{
|
||||||
EligibleShip es;
|
EligibleShip es;
|
||||||
es.schematicId = def.id;
|
es.schematicId = def.id;
|
||||||
es.cost = cost;
|
es.cost = cost;
|
||||||
|
es.defaultModules = def.defaultModules;
|
||||||
eligible.push_back(es);
|
eligible.push_back(es);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,11 +153,12 @@ std::vector<WaveSystem::SpawnEntry> WaveSystem::composeWave(Tick currentTick,
|
|||||||
budget -= chosen.cost;
|
budget -= chosen.cost;
|
||||||
|
|
||||||
SpawnEntry entry;
|
SpawnEntry entry;
|
||||||
entry.schematicId = chosen.schematicId;
|
entry.schematicId = chosen.schematicId;
|
||||||
entry.level = shipLevel;
|
entry.level = shipLevel;
|
||||||
entry.spawnAt = 0; // set below after all picks are done
|
entry.spawnAt = 0; // set below after all picks are done
|
||||||
entry.position = QVector2D(xDist(m_rng),
|
entry.position = QVector2D(xDist(m_rng),
|
||||||
static_cast<float>(yDist(m_rng)) + 0.5f);
|
static_cast<float>(yDist(m_rng)) + 0.5f);
|
||||||
|
entry.layout.placedModules = chosen.defaultModules;
|
||||||
picked.push_back(entry);
|
picked.push_back(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
class ShipSystem;
|
class ShipSystem;
|
||||||
@@ -40,10 +41,11 @@ public:
|
|||||||
private:
|
private:
|
||||||
struct SpawnEntry
|
struct SpawnEntry
|
||||||
{
|
{
|
||||||
std::string schematicId;
|
std::string schematicId;
|
||||||
int level;
|
int level;
|
||||||
Tick spawnAt;
|
Tick spawnAt;
|
||||||
QVector2D position;
|
QVector2D position;
|
||||||
|
ShipLayoutConfig layout;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Compose the next wave from the current threat budget, returning timed
|
// Compose the next wave from the current threat budget, returning timed
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
#include "AiSystem.h"
|
#include "AiSystem.h"
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
|
#include "ModuleOwnerComponent.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
@@ -81,6 +84,7 @@ struct Fixture
|
|||||||
ai.tickHomeReturnBehavior(admin);
|
ai.tickHomeReturnBehavior(admin);
|
||||||
ai.tickThreatResponseBehavior(admin, buildings);
|
ai.tickThreatResponseBehavior(admin, buildings);
|
||||||
ai.tickRepairBehavior(admin, buildings);
|
ai.tickRepairBehavior(admin, buildings);
|
||||||
|
ai.tickRepairTools(admin);
|
||||||
ai.tickSalvageBehavior(admin, scraps, buildings);
|
ai.tickSalvageBehavior(admin, scraps, buildings);
|
||||||
movementIntent.tick(admin);
|
movementIntent.tick(admin);
|
||||||
dynamicBody.tick(admin);
|
dynamicBody.tick(admin);
|
||||||
@@ -88,6 +92,28 @@ struct Fixture
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static ShipLayoutConfig makeSingleModuleLayout(const std::string& moduleId)
|
||||||
|
{
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = moduleId;
|
||||||
|
pm.position = QPoint(1, 1);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
static entt::entity firstSalvageChild(EntityAdmin& admin, entt::entity ship)
|
||||||
|
{
|
||||||
|
entt::entity result = entt::null;
|
||||||
|
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity ce, const SalvageCargoComponent&, const ModuleOwnerComponent& o)
|
||||||
|
{
|
||||||
|
if (o.owner == ship && result == entt::null) { result = ce; }
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// Helpers to read ECS data for a ship entity.
|
// Helpers to read ECS data for a ship entity.
|
||||||
static const MovementIntentComponent& intent(EntityAdmin& a, entt::entity e)
|
static const MovementIntentComponent& intent(EntityAdmin& a, entt::entity e)
|
||||||
{
|
{
|
||||||
@@ -299,7 +325,9 @@ TEST_CASE("BehaviorSystem: repair ship writes intent toward damaged friendly shi
|
|||||||
"[behavior]")
|
"[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool_module");
|
||||||
|
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||||
|
false, repairLayout);
|
||||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f));
|
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f));
|
||||||
|
|
||||||
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||||
@@ -315,7 +343,9 @@ TEST_CASE("BehaviorSystem: repair ship heals damaged ally within repair range",
|
|||||||
"[behavior]")
|
"[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool_module");
|
||||||
|
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||||
|
false, repairLayout);
|
||||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f));
|
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f));
|
||||||
|
|
||||||
const float initialHp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
const float initialHp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||||
@@ -323,6 +353,7 @@ TEST_CASE("BehaviorSystem: repair ship heals damaged ally within repair range",
|
|||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ai.tickRepairBehavior(f.admin, f.buildings);
|
f.ai.tickRepairBehavior(f.admin, f.buildings);
|
||||||
|
f.ai.tickRepairTools(f.admin);
|
||||||
|
|
||||||
REQUIRE(health(f.admin, friendly).hp > initialHp);
|
REQUIRE(health(f.admin, friendly).hp > initialHp);
|
||||||
}
|
}
|
||||||
@@ -330,7 +361,8 @@ TEST_CASE("BehaviorSystem: repair ship heals damaged ally within repair range",
|
|||||||
TEST_CASE("BehaviorSystem: repair ship does not heal above maxHp", "[behavior]")
|
TEST_CASE("BehaviorSystem: repair ship does not heal above maxHp", "[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool_module");
|
||||||
|
f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, repairLayout);
|
||||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f));
|
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f));
|
||||||
|
|
||||||
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp - 0.001f;
|
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp - 0.001f;
|
||||||
@@ -339,6 +371,7 @@ TEST_CASE("BehaviorSystem: repair ship does not heal above maxHp", "[behavior]")
|
|||||||
{
|
{
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ai.tickRepairBehavior(f.admin, f.buildings);
|
f.ai.tickRepairBehavior(f.admin, f.buildings);
|
||||||
|
f.ai.tickRepairTools(f.admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
const HealthComponent& h = health(f.admin, friendly);
|
const HealthComponent& h = health(f.admin, friendly);
|
||||||
@@ -353,7 +386,9 @@ TEST_CASE("BehaviorSystem: repair ship does not heal above maxHp", "[behavior]")
|
|||||||
TEST_CASE("BehaviorSystem: salvage ship writes intent toward nearest scrap", "[behavior]")
|
TEST_CASE("BehaviorSystem: salvage ship writes intent toward nearest scrap", "[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module");
|
||||||
|
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||||
|
false, salvageLayout);
|
||||||
|
|
||||||
const QVector2D scrapPos(100.0f, 0.0f);
|
const QVector2D scrapPos(100.0f, 0.0f);
|
||||||
f.scraps.spawn(scrapPos, 1, 100000);
|
f.scraps.spawn(scrapPos, 1, 100000);
|
||||||
@@ -368,13 +403,17 @@ TEST_CASE("BehaviorSystem: salvage ship writes intent toward nearest scrap", "[b
|
|||||||
TEST_CASE("BehaviorSystem: salvage ship collects scrap on arrival", "[behavior]")
|
TEST_CASE("BehaviorSystem: salvage ship collects scrap on arrival", "[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module");
|
||||||
|
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||||
|
false, salvageLayout);
|
||||||
const entt::entity scrapEntity = f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
const entt::entity scrapEntity = f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings);
|
f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings);
|
||||||
|
|
||||||
REQUIRE(f.admin.get<SalvageCargoComponent>(ship).current == 1);
|
const entt::entity sc = firstSalvageChild(f.admin, ship);
|
||||||
|
REQUIRE(f.admin.isValid(sc));
|
||||||
|
REQUIRE(f.admin.get<SalvageCargoComponent>(sc).current == 1);
|
||||||
REQUIRE_FALSE(f.admin.isValid(scrapEntity));
|
REQUIRE_FALSE(f.admin.isValid(scrapEntity));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,9 +434,15 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
|
|||||||
}
|
}
|
||||||
REQUIRE(f.buildings.findBuilding(bayId) != nullptr);
|
REQUIRE(f.buildings.findBuilding(bayId) != nullptr);
|
||||||
|
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(5.0f, 0.0f));
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module");
|
||||||
SalvageCargoComponent& cargo = f.admin.get<SalvageCargoComponent>(ship);
|
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(5.0f, 0.0f),
|
||||||
cargo.current = cargo.capacity; // full cargo
|
false, salvageLayout);
|
||||||
|
{
|
||||||
|
const entt::entity sc = firstSalvageChild(f.admin, ship);
|
||||||
|
REQUIRE(f.admin.isValid(sc));
|
||||||
|
SalvageCargoComponent& cargo = f.admin.get<SalvageCargoComponent>(sc);
|
||||||
|
cargo.current = cargo.capacity; // full cargo
|
||||||
|
}
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings);
|
f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings);
|
||||||
@@ -467,7 +512,9 @@ TEST_CASE("SensorRange: enemy ship ignores player just outside sensor range", "[
|
|||||||
TEST_CASE("SensorRange: repair ship retreats from enemy within sensor range", "[sensor]")
|
TEST_CASE("SensorRange: repair ship retreats from enemy within sensor range", "[sensor]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool_module");
|
||||||
|
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||||
|
false, repairLayout);
|
||||||
f.ships.spawn("interceptor", 1, QVector2D(200.0f, 0.0f), /*isEnemy=*/true);
|
f.ships.spawn("interceptor", 1, QVector2D(200.0f, 0.0f), /*isEnemy=*/true);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
@@ -480,7 +527,9 @@ TEST_CASE("SensorRange: repair ship retreats from enemy within sensor range", "[
|
|||||||
TEST_CASE("SensorRange: repair ship does not retreat from enemy beyond sensor range", "[sensor]")
|
TEST_CASE("SensorRange: repair ship does not retreat from enemy beyond sensor range", "[sensor]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool_module");
|
||||||
|
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||||
|
false, repairLayout);
|
||||||
f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f), /*isEnemy=*/true);
|
f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f), /*isEnemy=*/true);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
@@ -492,7 +541,9 @@ TEST_CASE("SensorRange: repair ship does not retreat from enemy beyond sensor ra
|
|||||||
TEST_CASE("SensorRange: repair ship does not acquire damaged ally beyond sensor range", "[sensor]")
|
TEST_CASE("SensorRange: repair ship does not acquire damaged ally beyond sensor range", "[sensor]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool_module");
|
||||||
|
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||||
|
false, repairLayout);
|
||||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f));
|
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f));
|
||||||
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||||
|
|
||||||
@@ -509,7 +560,9 @@ TEST_CASE("SensorRange: repair ship does not acquire damaged ally beyond sensor
|
|||||||
TEST_CASE("SensorRange: salvage ship ignores scrap beyond sensor range", "[sensor]")
|
TEST_CASE("SensorRange: salvage ship ignores scrap beyond sensor range", "[sensor]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module");
|
||||||
|
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||||
|
false, salvageLayout);
|
||||||
f.scraps.spawn(QVector2D(300.0f, 0.0f), 1, 100000);
|
f.scraps.spawn(QVector2D(300.0f, 0.0f), 1, 100000);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "FireEvent.h"
|
#include "FireEvent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "HqProxyComponent.h"
|
#include "HqProxyComponent.h"
|
||||||
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
@@ -30,7 +31,7 @@ static const ShipDef* findCombatShip(const GameConfig& cfg)
|
|||||||
{
|
{
|
||||||
for (const ShipDef& def : cfg.ships.ships)
|
for (const ShipDef& def : cfg.ships.ships)
|
||||||
{
|
{
|
||||||
if (def.combat)
|
if (!def.defaultModules.empty())
|
||||||
{
|
{
|
||||||
return &def;
|
return &def;
|
||||||
}
|
}
|
||||||
@@ -38,6 +39,17 @@ static const ShipDef* findCombatShip(const GameConfig& cfg)
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static entt::entity findWeaponChild(EntityAdmin& admin, entt::entity ship)
|
||||||
|
{
|
||||||
|
entt::entity result = entt::null;
|
||||||
|
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity ce, const WeaponComponent&, const ModuleOwnerComponent& o)
|
||||||
|
{
|
||||||
|
if (o.owner == ship && result == entt::null) { result = ce; }
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper fixture for unit tests that need ships + combat but not a full Simulation.
|
// Helper fixture for unit tests that need ships + combat but not a full Simulation.
|
||||||
struct CombatFixture
|
struct CombatFixture
|
||||||
{
|
{
|
||||||
@@ -67,10 +79,13 @@ struct CombatFixture
|
|||||||
|
|
||||||
void wireEnemyTarget(entt::entity enemy, entt::entity playerTarget)
|
void wireEnemyTarget(entt::entity enemy, entt::entity playerTarget)
|
||||||
{
|
{
|
||||||
if (admin.hasAll<WeaponComponent>(enemy))
|
// Set target on weapon child entity (CombatSystem syncs from ThreatResponse each tick,
|
||||||
|
// but also setting directly ensures the first tick fires without waiting for sync).
|
||||||
|
const entt::entity wc = findWeaponChild(admin, enemy);
|
||||||
|
if (wc != entt::null)
|
||||||
{
|
{
|
||||||
admin.get<WeaponComponent>(enemy).currentTarget = playerTarget;
|
admin.get<WeaponComponent>(wc).currentTarget = playerTarget;
|
||||||
admin.get<WeaponComponent>(enemy).cooldownTicks = 0.0f;
|
admin.get<WeaponComponent>(wc).cooldownTicks = 0.0f;
|
||||||
}
|
}
|
||||||
if (admin.hasAll<ThreatResponseBehaviorComponent>(enemy))
|
if (admin.hasAll<ThreatResponseBehaviorComponent>(enemy))
|
||||||
{
|
{
|
||||||
@@ -113,7 +128,11 @@ TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]"
|
|||||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||||
|
|
||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
f.admin.get<WeaponComponent>(enemy).cooldownTicks = 3.0f; // override to 3
|
{
|
||||||
|
const entt::entity wc = findWeaponChild(f.admin, enemy);
|
||||||
|
REQUIRE(f.admin.isValid(wc));
|
||||||
|
f.admin.get<WeaponComponent>(wc).cooldownTicks = 3.0f; // override to 3
|
||||||
|
}
|
||||||
|
|
||||||
auto enemyFiredIn = [&enemy](const std::vector<FireEvent>& evts)
|
auto enemyFiredIn = [&enemy](const std::vector<FireEvent>& evts)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -108,23 +108,19 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c
|
|||||||
REQUIRE(ironIngotIt->outputs.size() == 1);
|
REQUIRE(ironIngotIt->outputs.size() == 1);
|
||||||
REQUIRE_FALSE(ironIngotIt->outputs[0].probability.has_value());
|
REQUIRE_FALSE(ironIngotIt->outputs[0].probability.has_value());
|
||||||
|
|
||||||
// ships.toml — combat ships have a combat section; salvage ships don't.
|
// ships.toml — combat ships have default_modules with a weapon; salvage ships don't.
|
||||||
const auto interceptorIt = std::find_if(
|
const auto interceptorIt = std::find_if(
|
||||||
cfg.ships.ships.begin(), cfg.ships.ships.end(),
|
cfg.ships.ships.begin(), cfg.ships.ships.end(),
|
||||||
[](const ShipDef& s) { return s.id == "interceptor"; });
|
[](const ShipDef& s) { return s.id == "interceptor"; });
|
||||||
REQUIRE(interceptorIt != cfg.ships.ships.end());
|
REQUIRE(interceptorIt != cfg.ships.ships.end());
|
||||||
REQUIRE(interceptorIt->combat.has_value());
|
REQUIRE_FALSE(interceptorIt->defaultModules.empty());
|
||||||
REQUIRE_FALSE(interceptorIt->salvage.has_value());
|
REQUIRE(interceptorIt->defaultModules[0].moduleId == "laser_cannon");
|
||||||
REQUIRE_FALSE(interceptorIt->repair.has_value());
|
|
||||||
REQUIRE(interceptorIt->combat->damageFormula.evaluate(5.0) == Approx(20.0)); // "10 + 2*x"
|
|
||||||
|
|
||||||
const auto salvageShipIt = std::find_if(
|
const auto salvageShipIt = std::find_if(
|
||||||
cfg.ships.ships.begin(), cfg.ships.ships.end(),
|
cfg.ships.ships.begin(), cfg.ships.ships.end(),
|
||||||
[](const ShipDef& s) { return s.id == "salvage_ship"; });
|
[](const ShipDef& s) { return s.id == "salvage_ship"; });
|
||||||
REQUIRE(salvageShipIt != cfg.ships.ships.end());
|
REQUIRE(salvageShipIt != cfg.ships.ships.end());
|
||||||
REQUIRE_FALSE(salvageShipIt->combat.has_value());
|
REQUIRE(salvageShipIt->defaultModules.empty());
|
||||||
REQUIRE(salvageShipIt->salvage.has_value());
|
|
||||||
REQUIRE(salvageShipIt->salvage->cargoCapacity == 10);
|
|
||||||
|
|
||||||
// stations.toml
|
// stations.toml
|
||||||
REQUIRE(cfg.stations.playerStation.level == 5);
|
REQUIRE(cfg.stations.playerStation.level == 5);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
@@ -10,11 +11,14 @@
|
|||||||
#include "DynamicBodyComponent.h"
|
#include "DynamicBodyComponent.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "RepairBehaviorComponent.h"
|
#include "RepairBehaviorComponent.h"
|
||||||
#include "RepairToolComponent.h"
|
#include "RepairToolComponent.h"
|
||||||
|
#include "Rotation.h"
|
||||||
#include "SalvageBehaviorComponent.h"
|
#include "SalvageBehaviorComponent.h"
|
||||||
#include "SalvageCargoComponent.h"
|
#include "SalvageCargoComponent.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "ThreatResponseBehaviorComponent.h"
|
#include "ThreatResponseBehaviorComponent.h"
|
||||||
@@ -26,10 +30,58 @@ static GameConfig loadConfig()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Combat ship
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
TEST_CASE("ShipSystem: interceptor spawn has weapon and threatResponse, no cargo or repair",
|
static entt::entity firstWeaponChild(EntityAdmin& admin, entt::entity ship)
|
||||||
|
{
|
||||||
|
entt::entity result = entt::null;
|
||||||
|
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity ce, const WeaponComponent&, const ModuleOwnerComponent& o)
|
||||||
|
{
|
||||||
|
if (o.owner == ship && result == entt::null) { result = ce; }
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static entt::entity firstSalvageChild(EntityAdmin& admin, entt::entity ship)
|
||||||
|
{
|
||||||
|
entt::entity result = entt::null;
|
||||||
|
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity ce, const SalvageCargoComponent&, const ModuleOwnerComponent& o)
|
||||||
|
{
|
||||||
|
if (o.owner == ship && result == entt::null) { result = ce; }
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static entt::entity firstRepairChild(EntityAdmin& admin, entt::entity ship)
|
||||||
|
{
|
||||||
|
entt::entity result = entt::null;
|
||||||
|
admin.forEach<RepairToolComponent, ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity ce, const RepairToolComponent&, const ModuleOwnerComponent& o)
|
||||||
|
{
|
||||||
|
if (o.owner == ship && result == entt::null) { result = ce; }
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ShipLayoutConfig makeSingleModuleLayout(const std::string& moduleId)
|
||||||
|
{
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = moduleId;
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Combat ship (interceptor has default_modules = [laser_cannon])
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("ShipSystem: interceptor spawn has weapon child and threatResponse, no cargo or repair",
|
||||||
"[ship]")
|
"[ship]")
|
||||||
{
|
{
|
||||||
EntityAdmin admin;
|
EntityAdmin admin;
|
||||||
@@ -39,10 +91,10 @@ TEST_CASE("ShipSystem: interceptor spawn has weapon and threatResponse, no cargo
|
|||||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
REQUIRE(admin.isValid(e));
|
REQUIRE(admin.isValid(e));
|
||||||
REQUIRE(admin.hasAll<WeaponComponent>(e));
|
REQUIRE(admin.isValid(firstWeaponChild(admin, e)));
|
||||||
REQUIRE(admin.hasAll<ThreatResponseBehaviorComponent>(e));
|
REQUIRE(admin.hasAll<ThreatResponseBehaviorComponent>(e));
|
||||||
REQUIRE_FALSE(admin.hasAll<SalvageCargoComponent>(e));
|
REQUIRE_FALSE(admin.isValid(firstSalvageChild(admin, e)));
|
||||||
REQUIRE_FALSE(admin.hasAll<RepairToolComponent>(e));
|
REQUIRE_FALSE(admin.isValid(firstRepairChild(admin, e)));
|
||||||
REQUIRE_FALSE(admin.hasAll<RepairBehaviorComponent>(e));
|
REQUIRE_FALSE(admin.hasAll<RepairBehaviorComponent>(e));
|
||||||
REQUIRE_FALSE(admin.hasAll<SalvageBehaviorComponent>(e));
|
REQUIRE_FALSE(admin.hasAll<SalvageBehaviorComponent>(e));
|
||||||
}
|
}
|
||||||
@@ -58,14 +110,15 @@ TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]
|
|||||||
// hp_formula = "40 + 5*x" at x=1 → 45
|
// hp_formula = "40 + 5*x" at x=1 → 45
|
||||||
REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(45.0f));
|
REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(45.0f));
|
||||||
REQUIRE(admin.get<HealthComponent>(e).hp == Approx(45.0f));
|
REQUIRE(admin.get<HealthComponent>(e).hp == Approx(45.0f));
|
||||||
// damage_formula = "10 + 2*x" at x=1 → 12
|
|
||||||
REQUIRE(admin.get<WeaponComponent>(e).damage == Approx(12.0f));
|
|
||||||
// attack_range_formula = "150"
|
|
||||||
REQUIRE(admin.get<WeaponComponent>(e).range == Approx(150.0f));
|
|
||||||
// sensor_range_formula = "200"
|
// sensor_range_formula = "200"
|
||||||
REQUIRE(admin.get<SensorRangeComponent>(e).value == Approx(200.0f));
|
REQUIRE(admin.get<SensorRangeComponent>(e).value == Approx(200.0f));
|
||||||
// cooldownTicks starts at 0
|
|
||||||
REQUIRE(admin.get<WeaponComponent>(e).cooldownTicks == Approx(0.0f));
|
// laser_cannon: damage_formula = "2", attack_range_formula = "5"
|
||||||
|
const entt::entity wc = firstWeaponChild(admin, e);
|
||||||
|
REQUIRE(admin.isValid(wc));
|
||||||
|
REQUIRE(admin.get<WeaponComponent>(wc).damage == Approx(2.0f));
|
||||||
|
REQUIRE(admin.get<WeaponComponent>(wc).range == Approx(5.0f));
|
||||||
|
REQUIRE(admin.get<WeaponComponent>(wc).cooldownTicks == Approx(0.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]")
|
TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]")
|
||||||
@@ -94,22 +147,23 @@ TEST_CASE("ShipSystem: interceptor level 0 maxSpeedPerTick matches formula / kTi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Salvage ship
|
// Salvage ship (spawned with salvage_bay_module layout)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
TEST_CASE("ShipSystem: salvage_ship spawn has cargo and scrapCollector, no weapon",
|
TEST_CASE("ShipSystem: salvage_ship spawn with salvage module has cargo child and behavior, no weapon",
|
||||||
"[ship]")
|
"[ship]")
|
||||||
{
|
{
|
||||||
EntityAdmin admin;
|
EntityAdmin admin;
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
|
const ShipLayoutConfig layout = makeSingleModuleLayout("salvage_bay_module");
|
||||||
|
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
||||||
|
|
||||||
REQUIRE(admin.hasAll<SalvageCargoComponent>(e));
|
REQUIRE(admin.isValid(firstSalvageChild(admin, e)));
|
||||||
REQUIRE(admin.hasAll<SalvageBehaviorComponent>(e));
|
REQUIRE(admin.hasAll<SalvageBehaviorComponent>(e));
|
||||||
REQUIRE_FALSE(admin.hasAll<WeaponComponent>(e));
|
REQUIRE_FALSE(admin.isValid(firstWeaponChild(admin, e)));
|
||||||
REQUIRE_FALSE(admin.hasAll<RepairToolComponent>(e));
|
REQUIRE_FALSE(admin.isValid(firstRepairChild(admin, e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
|
TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
|
||||||
@@ -118,32 +172,37 @@ TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
|
|||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
|
const ShipLayoutConfig layout = makeSingleModuleLayout("salvage_bay_module");
|
||||||
|
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
||||||
|
|
||||||
// cargo_capacity = 10
|
// salvage_bay_module: cargo_capacity_formula = "10", collection_range_formula = "50"
|
||||||
REQUIRE(admin.get<SalvageCargoComponent>(e).capacity == 10);
|
const entt::entity sc = firstSalvageChild(admin, e);
|
||||||
REQUIRE(admin.get<SalvageCargoComponent>(e).current == 0);
|
REQUIRE(admin.isValid(sc));
|
||||||
|
REQUIRE(admin.get<SalvageCargoComponent>(sc).capacity == 10);
|
||||||
|
REQUIRE(admin.get<SalvageCargoComponent>(sc).current == 0);
|
||||||
REQUIRE(admin.get<SalvageBehaviorComponent>(e).deliveryBay == kInvalidBuildingId);
|
REQUIRE(admin.get<SalvageBehaviorComponent>(e).deliveryBay == kInvalidBuildingId);
|
||||||
REQUIRE_FALSE(admin.get<SalvageBehaviorComponent>(e).scrapTarget.has_value());
|
REQUIRE_FALSE(admin.get<SalvageBehaviorComponent>(e).scrapTarget.has_value());
|
||||||
|
REQUIRE(admin.get<SalvageBehaviorComponent>(e).maxCollectionRange == Approx(50.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Repair ship
|
// Repair ship (spawned with repair_tool_module layout)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
TEST_CASE("ShipSystem: repair_ship spawn has repairTool and repairBehavior, no weapon",
|
TEST_CASE("ShipSystem: repair_ship spawn with repair module has repair child and behavior, no weapon",
|
||||||
"[ship]")
|
"[ship]")
|
||||||
{
|
{
|
||||||
EntityAdmin admin;
|
EntityAdmin admin;
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool_module");
|
||||||
|
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
||||||
|
|
||||||
REQUIRE(admin.hasAll<RepairToolComponent>(e));
|
REQUIRE(admin.isValid(firstRepairChild(admin, e)));
|
||||||
REQUIRE(admin.hasAll<RepairBehaviorComponent>(e));
|
REQUIRE(admin.hasAll<RepairBehaviorComponent>(e));
|
||||||
REQUIRE_FALSE(admin.hasAll<WeaponComponent>(e));
|
REQUIRE_FALSE(admin.isValid(firstWeaponChild(admin, e)));
|
||||||
REQUIRE_FALSE(admin.hasAll<SalvageCargoComponent>(e));
|
REQUIRE_FALSE(admin.isValid(firstSalvageChild(admin, e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas", "[ship]")
|
TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas", "[ship]")
|
||||||
@@ -152,12 +211,17 @@ TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas",
|
|||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool_module");
|
||||||
|
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
||||||
|
|
||||||
// repair_rate_formula = "5 + x" at x=1 → 6
|
// repair_tool_module: repair_rate_formula = "5 + x" at x=1 → 6 / kTickRateHz
|
||||||
REQUIRE(admin.get<RepairToolComponent>(e).ratePerTick == Approx(6.0f));
|
const float expectedRate = 6.0f / static_cast<float>(kTickRateHz);
|
||||||
|
const entt::entity rc = firstRepairChild(admin, e);
|
||||||
|
REQUIRE(admin.isValid(rc));
|
||||||
|
REQUIRE(admin.get<RepairToolComponent>(rc).ratePerTick == Approx(expectedRate));
|
||||||
// repair_range_formula = "80"
|
// repair_range_formula = "80"
|
||||||
REQUIRE(admin.get<RepairToolComponent>(e).range == Approx(80.0f));
|
REQUIRE(admin.get<RepairToolComponent>(rc).range == Approx(80.0f));
|
||||||
|
REQUIRE(admin.get<RepairBehaviorComponent>(e).maxRepairRange == Approx(80.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -171,22 +235,26 @@ TEST_CASE("ShipSystem: spawned ships are valid entities", "[ship]")
|
|||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const entt::entity e1 = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e1 = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||||
const entt::entity e2 = ss.spawn("salvage_ship", 1, QVector2D(1.0f, 0.0f));
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module");
|
||||||
|
const entt::entity e2 = ss.spawn("salvage_ship", 1, QVector2D(1.0f, 0.0f), false, salvageLayout);
|
||||||
|
|
||||||
REQUIRE(admin.isValid(e1));
|
REQUIRE(admin.isValid(e1));
|
||||||
REQUIRE(admin.isValid(e2));
|
REQUIRE(admin.isValid(e2));
|
||||||
REQUIRE(e1 != e2);
|
REQUIRE(e1 != e2);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ShipSystem: despawn removes the ship", "[ship]")
|
TEST_CASE("ShipSystem: despawn removes the ship and its weapon children", "[ship]")
|
||||||
{
|
{
|
||||||
EntityAdmin admin;
|
EntityAdmin admin;
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
ShipSystem ss(cfg, admin);
|
ShipSystem ss(cfg, admin);
|
||||||
|
|
||||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||||
|
const entt::entity wc = firstWeaponChild(admin, e);
|
||||||
REQUIRE(admin.isValid(e));
|
REQUIRE(admin.isValid(e));
|
||||||
|
REQUIRE(admin.isValid(wc));
|
||||||
|
|
||||||
ss.despawn(e);
|
ss.despawn(e);
|
||||||
REQUIRE_FALSE(admin.isValid(e));
|
REQUIRE_FALSE(admin.isValid(e));
|
||||||
|
REQUIRE_FALSE(admin.isValid(wc));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "HqProxyComponent.h"
|
#include "HqProxyComponent.h"
|
||||||
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
@@ -166,13 +167,14 @@ TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
|
|||||||
|
|
||||||
TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
|
TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
|
||||||
{
|
{
|
||||||
const Simulation sim(loadConfig(), 42);
|
Simulation sim(loadConfig(), 42);
|
||||||
|
|
||||||
int armedPlayerStations = 0;
|
int armedPlayerStations = 0;
|
||||||
sim.admin().forEach<StationBodyComponent, FactionComponent, WeaponComponent>(
|
sim.admin().forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||||
[&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f,
|
[&](entt::entity /*e*/, const WeaponComponent& w, const ModuleOwnerComponent& mo)
|
||||||
const WeaponComponent& w)
|
|
||||||
{
|
{
|
||||||
|
if (!sim.admin().hasAll<StationBodyComponent>(mo.owner)) { return; }
|
||||||
|
const FactionComponent& f = sim.admin().get<FactionComponent>(mo.owner);
|
||||||
if (!f.isEnemy)
|
if (!f.isEnemy)
|
||||||
{
|
{
|
||||||
++armedPlayerStations;
|
++armedPlayerStations;
|
||||||
@@ -186,13 +188,14 @@ TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
|
|||||||
|
|
||||||
TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
|
TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
|
||||||
{
|
{
|
||||||
const Simulation sim(loadConfig(), 42);
|
Simulation sim(loadConfig(), 42);
|
||||||
|
|
||||||
int armedEnemyStations = 0;
|
int armedEnemyStations = 0;
|
||||||
sim.admin().forEach<StationBodyComponent, FactionComponent, WeaponComponent>(
|
sim.admin().forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||||
[&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f,
|
[&](entt::entity /*e*/, const WeaponComponent& w, const ModuleOwnerComponent& mo)
|
||||||
const WeaponComponent& w)
|
|
||||||
{
|
{
|
||||||
|
if (!sim.admin().hasAll<StationBodyComponent>(mo.owner)) { return; }
|
||||||
|
const FactionComponent& f = sim.admin().get<FactionComponent>(mo.owner);
|
||||||
if (f.isEnemy)
|
if (f.isEnemy)
|
||||||
{
|
{
|
||||||
++armedEnemyStations;
|
++armedEnemyStations;
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "RepairToolComponent.h"
|
|
||||||
#include "SalvageCargoComponent.h"
|
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
@@ -62,13 +60,6 @@ Rotation rotateCounterClockwise(Rotation r)
|
|||||||
return Rotation::East;
|
return Rotation::East;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShipRole shipRoleFromComponents(bool isEnemy, bool hasCargo, bool hasRepairTool)
|
|
||||||
{
|
|
||||||
if (isEnemy) { return ShipRole::Enemy; }
|
|
||||||
if (hasCargo) { return ShipRole::Salvage; }
|
|
||||||
if (hasRepairTool){ return ShipRole::Repair; }
|
|
||||||
return ShipRole::PlayerCombat;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString toDisplayName(const std::string& id)
|
QString toDisplayName(const std::string& id)
|
||||||
{
|
{
|
||||||
@@ -847,15 +838,12 @@ void GameWorldView::drawShips(QPainter& painter)
|
|||||||
{
|
{
|
||||||
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
|
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
|
||||||
FactionComponent>(
|
FactionComponent>(
|
||||||
[&](entt::entity e, const ShipIdentityComponent& /*si*/,
|
[&](entt::entity /*e*/, const ShipIdentityComponent& si,
|
||||||
const PositionComponent& pos, const FacingComponent& facing,
|
const PositionComponent& pos, const FacingComponent& facing,
|
||||||
const FactionComponent& fac)
|
const FactionComponent& /*fac*/)
|
||||||
{
|
{
|
||||||
const bool hasCargo = m_sim->admin().hasAll<SalvageCargoComponent>(e);
|
const std::map<std::string, ShipVisuals>::const_iterator it =
|
||||||
const bool hasRepair = m_sim->admin().hasAll<RepairToolComponent>(e);
|
m_visuals->ships.find(si.schematicId);
|
||||||
const ShipRole role = shipRoleFromComponents(fac.isEnemy, hasCargo, hasRepair);
|
|
||||||
const std::map<ShipRole, ShipVisuals>::const_iterator it =
|
|
||||||
m_visuals->ships.find(role);
|
|
||||||
if (it == m_visuals->ships.end()) { return; }
|
if (it == m_visuals->ships.end()) { return; }
|
||||||
|
|
||||||
const QPointF center = worldToWidget(pos.value);
|
const QPointF center = worldToWidget(pos.value);
|
||||||
@@ -884,15 +872,12 @@ void GameWorldView::drawDebugSensorRanges(QPainter& painter)
|
|||||||
painter.setBrush(Qt::NoBrush);
|
painter.setBrush(Qt::NoBrush);
|
||||||
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
|
m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
|
||||||
FactionComponent, SensorRangeComponent>(
|
FactionComponent, SensorRangeComponent>(
|
||||||
[&](entt::entity e, const ShipIdentityComponent& /*si*/,
|
[&](entt::entity /*e*/, const ShipIdentityComponent& si,
|
||||||
const PositionComponent& pos, const FacingComponent& /*facing*/,
|
const PositionComponent& pos, const FacingComponent& /*facing*/,
|
||||||
const FactionComponent& fac, const SensorRangeComponent& sensor)
|
const FactionComponent& /*fac*/, const SensorRangeComponent& sensor)
|
||||||
{
|
{
|
||||||
const bool hasCargo = m_sim->admin().hasAll<SalvageCargoComponent>(e);
|
const std::map<std::string, ShipVisuals>::const_iterator it =
|
||||||
const bool hasRepair = m_sim->admin().hasAll<RepairToolComponent>(e);
|
m_visuals->ships.find(si.schematicId);
|
||||||
const ShipRole role = shipRoleFromComponents(fac.isEnemy, hasCargo, hasRepair);
|
|
||||||
const std::map<ShipRole, ShipVisuals>::const_iterator it =
|
|
||||||
m_visuals->ships.find(role);
|
|
||||||
if (it == m_visuals->ships.end()) { return; }
|
if (it == m_visuals->ships.end()) { return; }
|
||||||
|
|
||||||
const QPointF center = worldToWidget(pos.value);
|
const QPointF center = worldToWidget(pos.value);
|
||||||
|
|||||||
@@ -55,14 +55,6 @@ struct ToastVisuals
|
|||||||
int fontSize;
|
int fontSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class ShipRole
|
|
||||||
{
|
|
||||||
PlayerCombat,
|
|
||||||
Salvage,
|
|
||||||
Repair,
|
|
||||||
Enemy,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct VisualsConfig
|
struct VisualsConfig
|
||||||
{
|
{
|
||||||
TileVisuals asteroid;
|
TileVisuals asteroid;
|
||||||
@@ -70,7 +62,7 @@ struct VisualsConfig
|
|||||||
|
|
||||||
std::map<BuildingType, BuildingVisuals> buildings;
|
std::map<BuildingType, BuildingVisuals> buildings;
|
||||||
std::map<std::string, ItemVisuals> items;
|
std::map<std::string, ItemVisuals> items;
|
||||||
std::map<ShipRole, ShipVisuals> ships;
|
std::map<std::string, ShipVisuals> ships;
|
||||||
|
|
||||||
BeamVisuals beams;
|
BeamVisuals beams;
|
||||||
OverlayVisuals overlays;
|
OverlayVisuals overlays;
|
||||||
|
|||||||
@@ -190,17 +190,20 @@ VisualsConfig VisualsLoader::load(const std::string& path)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ships
|
// Ships (dynamic keys: each key is a schematic id)
|
||||||
{
|
{
|
||||||
toml::table& ships = requireSubtable(tbl, "ships", "root");
|
toml::table& ships = requireSubtable(tbl, "ships", "root");
|
||||||
cfg.ships[ShipRole::PlayerCombat] = parseShip(
|
for (toml::table::iterator it = ships.begin(); it != ships.end(); ++it)
|
||||||
requireSubtable(ships, "player_combat", "ships"), "ships.player_combat");
|
{
|
||||||
cfg.ships[ShipRole::Salvage] = parseShip(
|
std::string schematicId = std::string(it->first.str());
|
||||||
requireSubtable(ships, "salvage", "ships"), "ships.salvage");
|
toml::table* sub = it->second.as_table();
|
||||||
cfg.ships[ShipRole::Repair] = parseShip(
|
if (sub == nullptr)
|
||||||
requireSubtable(ships, "repair", "ships"), "ships.repair");
|
{
|
||||||
cfg.ships[ShipRole::Enemy] = parseShip(
|
throw std::runtime_error("visuals.toml: ships." + schematicId
|
||||||
requireSubtable(ships, "enemy", "ships"), "ships.enemy");
|
+ " is not a table");
|
||||||
|
}
|
||||||
|
cfg.ships[schematicId] = parseShip(*sub, "ships." + schematicId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Beams
|
// Beams
|
||||||
|
|||||||
Reference in New Issue
Block a user