add balancing tool target
This commit is contained in:
439
src/balancing/ArenaSimulation.cpp
Normal file
439
src/balancing/ArenaSimulation.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user