add balancing tool target

This commit is contained in:
2026-05-03 11:17:54 +02:00
parent 5153129909
commit a4427f7f67
12 changed files with 1164 additions and 0 deletions

View File

@@ -0,0 +1,439 @@
#include "ArenaSimulation.h"
#include <algorithm>
#include <cassert>
#include <QVector2D>
#include "Building.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "CombatSystem.h"
#include "ScrapSystem.h"
#include "Ship.h"
#include "ShipSystem.h"
#include "ShipsConfig.h"
#include "StationsConfig.h"
#include "SurfaceMask.h"
ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
ArenaConfig arenaConfig,
unsigned int seed)
: m_gameConfig(gameConfig)
, m_arenaConfig(std::move(arenaConfig))
, m_rng(seed)
, m_currentTick(0)
, m_nextId(1)
, m_beltSystem(1.0)
, m_team1HqId(kInvalidEntityId)
, m_team2HqId(kInvalidEntityId)
, m_finished(false)
, m_stopRequested(false)
{
m_buildingSystem = std::make_unique<BuildingSystem>(
m_gameConfig,
m_beltSystem,
[this]() { return allocateId(); },
[](int) {},
[](const std::string&, QVector2D) {},
m_rng);
m_shipSystem = std::make_unique<ShipSystem>(
m_gameConfig, [this]() { return allocateId(); });
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
placeStructures();
spawnShips();
m_shipSystem->triggerRallyDeparture();
updateStatus();
}
ArenaSimulation::~ArenaSimulation() = default;
EntityId ArenaSimulation::allocateId()
{
return m_nextId++;
}
void ArenaSimulation::placeStructures()
{
const int totalWidth = m_arenaConfig.playerBufferWidth
+ m_arenaConfig.contestZoneWidth
+ m_arenaConfig.enemyBufferWidth;
const int midY = m_arenaConfig.heightTiles / 2;
// Team 1 HQ at left edge, placed as Hq (enemy ships target Hq).
{
const ParsedSurfaceMask hqParsed =
parseSurfaceMask(m_gameConfig.stations.hq.surfaceMask, Rotation::East);
const int anchorX = 0;
const int anchorY = midY - hqParsed.footprint.height() / 2;
const float hp = static_cast<float>(
m_gameConfig.stations.hq.hpFormula.evaluate(1.0));
m_team1HqId = m_buildingSystem->placeImmediate(
BuildingType::Hq,
m_gameConfig.stations.hq.surfaceMask,
QPoint(anchorX, anchorY),
Rotation::East, hp, hp);
}
// Team 2 HQ at right edge, placed as EnemyDefenceStation (player ships target these).
// No weapon — it's just a destructible target.
{
const ParsedSurfaceMask hqParsed =
parseSurfaceMask(m_gameConfig.stations.hq.surfaceMask, Rotation::West);
const int anchorX = totalWidth - hqParsed.footprint.width();
const int anchorY = midY - hqParsed.footprint.height() / 2;
const float hp = static_cast<float>(
m_gameConfig.stations.hq.hpFormula.evaluate(1.0));
m_team2HqId = m_buildingSystem->placeImmediate(
BuildingType::EnemyDefenceStation,
m_gameConfig.stations.hq.surfaceMask,
QPoint(anchorX, anchorY),
Rotation::West, hp, hp);
}
// Team 1 defence stations (PlayerDefenceStation — targeted by team 2).
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)
{
float hp = 0.0f;
StationWeapon weapon;
weapon.cooldownTicks = 0.0f;
const double lv = static_cast<double>(entry.level);
if (entry.stationType == "player_station")
{
hp = static_cast<float>(
m_gameConfig.stations.playerStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.playerStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.playerStation.fireRateFormula.evaluate(lv));
}
else
{
hp = static_cast<float>(
m_gameConfig.stations.enemyStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.enemyStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.enemyStation.fireRateFormula.evaluate(lv));
}
const EntityId stationId = m_buildingSystem->placeImmediate(
BuildingType::PlayerDefenceStation,
m_gameConfig.stations.playerStation.surfaceMask,
entry.position,
Rotation::East, hp, hp);
m_buildingSystem->initStationWeapon(stationId, weapon);
}
// Team 2 defence stations (EnemyDefenceStation — targeted by team 1).
for (const ArenaStationEntry& entry : m_arenaConfig.teams[1].stations)
{
float hp = 0.0f;
StationWeapon weapon;
weapon.cooldownTicks = 0.0f;
const double lv = static_cast<double>(entry.level);
if (entry.stationType == "player_station")
{
hp = static_cast<float>(
m_gameConfig.stations.playerStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.playerStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.playerStation.fireRateFormula.evaluate(lv));
}
else
{
hp = static_cast<float>(
m_gameConfig.stations.enemyStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.enemyStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.enemyStation.fireRateFormula.evaluate(lv));
}
const EntityId stationId = m_buildingSystem->placeImmediate(
BuildingType::EnemyDefenceStation,
m_gameConfig.stations.enemyStation.surfaceMask,
entry.position,
Rotation::East, hp, hp);
m_buildingSystem->initStationWeapon(stationId, weapon);
}
}
void ArenaSimulation::spawnShips()
{
const int contestStart = m_arenaConfig.playerBufferWidth;
const int team2Start = contestStart + m_arenaConfig.contestZoneWidth;
const int totalWidth = team2Start + m_arenaConfig.enemyBufferWidth;
std::uniform_real_distribution<float> yDist(0.0f,
static_cast<float>(m_arenaConfig.heightTiles));
// Team 1: isEnemy=false, spawn in player buffer zone.
{
std::uniform_real_distribution<float> xDist(0.0f,
static_cast<float>(m_arenaConfig.playerBufferWidth));
for (const ArenaShipEntry& entry : m_arenaConfig.teams[0].ships)
{
for (int i = 0; i < entry.count; ++i)
{
const QVector2D pos(xDist(m_rng), yDist(m_rng));
m_shipSystem->spawn(entry.schematicId, entry.level, pos, false);
}
}
}
// Team 2: isEnemy=true, spawn in enemy buffer zone.
{
std::uniform_real_distribution<float> xDist(
static_cast<float>(team2Start),
static_cast<float>(totalWidth));
for (const ArenaShipEntry& entry : m_arenaConfig.teams[1].ships)
{
for (int i = 0; i < entry.count; ++i)
{
const QVector2D pos(xDist(m_rng), yDist(m_rng));
m_shipSystem->spawn(entry.schematicId, entry.level, pos, true);
}
}
}
}
void ArenaSimulation::run()
{
while (!m_finished && !m_stopRequested.load(std::memory_order_relaxed))
{
tick();
}
{
std::lock_guard<std::mutex> lock(m_statusMutex);
m_status.finished = true;
}
}
void ArenaSimulation::requestStop()
{
m_stopRequested.store(true, std::memory_order_relaxed);
}
ArenaStatus ArenaSimulation::status() const
{
std::lock_guard<std::mutex> lock(m_statusMutex);
return m_status;
}
void ArenaSimulation::tick()
{
// Ship behavior systems (tick step 7).
m_shipSystem->clearMovementIntents();
m_shipSystem->tickHomeReturn();
m_shipSystem->tickThreatResponse(*m_buildingSystem);
m_shipSystem->tickRepairBehavior(*m_buildingSystem);
m_shipSystem->tickScrapCollector(*m_scrapSystem, *m_buildingSystem);
// Combat resolution (tick step 8).
std::vector<FireEvent> fireEvents;
m_combatSystem->tick(m_currentTick, *m_shipSystem, *m_buildingSystem, fireEvents);
m_combatSystem->applyPendingDamage(m_currentTick, *m_shipSystem, *m_buildingSystem);
// Deaths (tick step 9, simplified).
tickDeaths();
// Movement (tick step 10).
m_shipSystem->tickMovement();
// Scrap despawn (tick step 11).
m_scrapSystem->tickDespawn(m_currentTick);
++m_currentTick;
if (m_currentTick % 30 == 0)
{
updateStatus();
}
}
void ArenaSimulation::tickDeaths()
{
// Dead ships.
std::vector<EntityId> deadShipIds;
m_shipSystem->forEach([&deadShipIds](Ship& s)
{
if (s.hp <= 0.0f)
{
deadShipIds.push_back(s.id);
}
});
for (EntityId deadId : deadShipIds)
{
const Ship* s = m_shipSystem->findShip(deadId);
if (!s)
{
continue;
}
for (const ShipDef& def : m_gameConfig.ships.ships)
{
if (def.id == s->schematicId && def.loot.scrapDrop > 0)
{
const Tick despawnAt = m_currentTick
+ secondsToTicks(m_gameConfig.world.scrapDespawnSeconds);
m_scrapSystem->spawn(s->position, def.loot.scrapDrop, despawnAt);
break;
}
}
m_shipSystem->despawn(deadId);
}
// Dead buildings (HQ and defence stations).
std::vector<EntityId> deadBuildingIds;
for (const Building& b : m_buildingSystem->allBuildings())
{
if (b.hp <= 0.0f
&& (b.type == BuildingType::Hq
|| b.type == BuildingType::PlayerDefenceStation
|| b.type == BuildingType::EnemyDefenceStation))
{
deadBuildingIds.push_back(b.id);
}
}
for (EntityId deadId : deadBuildingIds)
{
m_buildingSystem->removeBuilding(deadId);
}
// Check end conditions.
const bool team1HqGone =
(m_buildingSystem->findBuilding(m_team1HqId) == nullptr);
const bool team2HqGone =
(m_buildingSystem->findBuilding(m_team2HqId) == nullptr);
if (team1HqGone || team2HqGone)
{
m_finished = true;
updateStatus();
return;
}
// Check if all ships of one team are destroyed.
bool team1HasShips = false;
bool team2HasShips = false;
m_shipSystem->forEach([&team1HasShips, &team2HasShips](Ship& s)
{
if (s.isEnemy)
{
team2HasShips = true;
}
else
{
team1HasShips = true;
}
});
if (!team1HasShips || !team2HasShips)
{
m_finished = true;
updateStatus();
}
}
void ArenaSimulation::updateStatus()
{
ArenaStatus newStatus;
newStatus.finished = m_finished;
for (int ti = 0; ti < 2; ++ti)
{
ArenaStatus::TeamStatus& teamStatus = newStatus.teams[ti];
teamStatus.name = m_arenaConfig.teams[ti].name;
// HQ entry (always first).
{
ArenaStatus::Entry hqEntry;
hqEntry.displayName = "HQ";
hqEntry.level = 1;
hqEntry.total = 1;
const EntityId hqId = (ti == 0) ? m_team1HqId : m_team2HqId;
hqEntry.surviving = (m_buildingSystem->findBuilding(hqId) != nullptr) ? 1 : 0;
teamStatus.entries.push_back(hqEntry);
}
// Ship entries.
for (const ArenaShipEntry& shipEntry : m_arenaConfig.teams[ti].ships)
{
ArenaStatus::Entry entry;
entry.displayName = shipEntry.schematicId;
entry.level = shipEntry.level;
entry.total = shipEntry.count;
int surviving = 0;
const bool isEnemyTeam = (ti == 1);
m_shipSystem->forEach(
[&surviving, &shipEntry, isEnemyTeam](Ship& s)
{
if (s.isEnemy == isEnemyTeam
&& s.schematicId == shipEntry.schematicId
&& s.level == shipEntry.level
&& s.hp > 0.0f)
{
++surviving;
}
});
entry.surviving = surviving;
teamStatus.entries.push_back(entry);
}
// Station entries.
for (std::size_t si = 0; si < m_arenaConfig.teams[ti].stations.size(); ++si)
{
const ArenaStationEntry& stationEntry = m_arenaConfig.teams[ti].stations[si];
ArenaStatus::Entry entry;
entry.displayName = "Station";
entry.level = stationEntry.level;
entry.total = 1;
// Count surviving stations of this team at this position.
const BuildingType expectedType = (ti == 0)
? BuildingType::PlayerDefenceStation
: BuildingType::EnemyDefenceStation;
int surviving = 0;
for (const Building& b : m_buildingSystem->allBuildings())
{
if (b.type == expectedType && b.anchor == stationEntry.position)
{
surviving = 1;
break;
}
}
entry.surviving = surviving;
teamStatus.entries.push_back(entry);
}
}
std::lock_guard<std::mutex> lock(m_statusMutex);
m_status = newStatus;
}