Files
dota_factory/src/balancing/ArenaSimulation.cpp

623 lines
21 KiB
C++

#include "ArenaSimulation.h"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <string>
#include <QVector2D>
#include "AiSystem.h"
#include "Building.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "CombatSystem.h"
#include "DynamicBodySystem.h"
#include "EntityAdmin.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "HqProxyComponent.h"
#include "ModuleOwnerComponent.h"
#include "MovementIntentSystem.h"
#include "PositionComponent.h"
#include "RepairSystem.h"
#include "SalvagerSystem.h"
#include "ScrapSystem.h"
#include "ShipIdentityComponent.h"
#include "ShipSystem.h"
#include "ShipsConfig.h"
#include "StationBodyComponent.h"
#include "StationsConfig.h"
#include "SurfaceMask.h"
#include "ThreatCostCalculator.h"
#include "WeaponComponent.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_nextBuildingId(1)
, m_beltSystem(1.0)
, m_team1HqEntity(entt::null)
, m_team2HqEntity(entt::null)
, m_finished(false)
, m_winnerTeam(-1)
, m_stopRequested(false)
{
m_buildingSystem = std::make_unique<BuildingSystem>(
m_gameConfig,
m_beltSystem,
[this]() { return allocateBuildingId(); },
[](int) {},
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
[](const std::string&) -> bool { return true; },
m_rng);
m_shipSystem = std::make_unique<ShipSystem>(m_gameConfig, m_admin);
// Arena fights are symmetric and aggressive: player-faction ships must not
// retreat (REQ-BAL-SIM-AI). Only one faction would otherwise get retreat.
m_shipSystem->setRetreatEnabled(false);
m_aiSystem = std::make_unique<AiSystem>(m_gameConfig);
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin);
m_repairSystem = std::make_unique<RepairSystem>(m_admin);
// Static accumulated threat per team: sum of count * per-ship threat cost
// (REQ-MOD-THREAT) over the configured ship roster. Ships only; HQ and
// defence stations are excluded. Level-independent, so computed once here.
for (int ti = 0; ti < 2; ++ti)
{
double teamThreat = 0.0;
for (const ArenaShipEntry& shipEntry : m_arenaConfig.teams[ti].ships)
{
const std::vector<PlacedModule>& modules = shipEntry.layout
? shipEntry.layout->placedModules
: std::vector<PlacedModule>{};
const double shipThreat = calculateShipThreatCost(
m_gameConfig.threatCosts, m_gameConfig, shipEntry.schematicId, modules);
teamThreat += shipThreat * shipEntry.count;
}
m_teamThreat[ti] = teamThreat;
}
placeStructures();
spawnShips();
computeTeamMaxEhp();
m_shipSystem->triggerRallyDeparture();
updateStatus();
}
std::string ArenaStatus::TeamStatus::ehpPercentText() const
{
if (maxEhp <= 0.0)
{
return "n/a";
}
const int percent = static_cast<int>(std::lround(100.0 * currentEhp / maxEhp));
return std::to_string(percent) + "%";
}
void ArenaSimulation::computeTeamMaxEhp()
{
m_teamMaxEhp[0] = 0.0;
m_teamMaxEhp[1] = 0.0;
// Ships contribute their full max HP.
m_admin.forEach<ShipIdentityComponent, FactionComponent, HealthComponent>(
[this](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const FactionComponent& f, const HealthComponent& h)
{
m_teamMaxEhp[f.isEnemy ? 1 : 0] += static_cast<double>(h.maxHp);
});
// Defence stations contribute their full max HP; the HQ is excluded.
m_admin.forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[this](entt::entity e, const StationBodyComponent& /*sb*/,
const FactionComponent& f, const HealthComponent& h)
{
if (m_admin.hasAll<HqProxyComponent>(e))
{
return;
}
m_teamMaxEhp[f.isEnemy ? 1 : 0] += static_cast<double>(h.maxHp);
});
}
ArenaSimulation::~ArenaSimulation() = default;
BuildingId ArenaSimulation::allocateBuildingId()
{
return m_nextBuildingId++;
}
void ArenaSimulation::placeStructures()
{
const int totalWidth = m_arenaConfig.playerBufferWidth_tiles
+ m_arenaConfig.contestZoneWidth_tiles
+ m_arenaConfig.enemyBufferWidth_tiles;
const int midY = m_arenaConfig.heightTiles / 2;
// Team 1 HQ — ECS proxy entity, player faction (isEnemy=false).
{
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));
const QPoint anchor(anchorX, anchorY);
std::vector<QPoint> absCells;
for (const QPoint& rel : hqParsed.bodyCells)
{
absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
}
m_team1HqEntity = m_admin.spawnStation(anchor, hqParsed.footprint, absCells,
hp, hp, false);
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
m_admin.addComponent<HqProxyComponent>(m_team1HqEntity);
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
}
// Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon.
{
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));
const QPoint anchor(anchorX, anchorY);
std::vector<QPoint> absCells;
for (const QPoint& rel : hqParsed.bodyCells)
{
absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
}
m_team2HqEntity = m_admin.spawnStation(anchor, hqParsed.footprint, absCells,
hp, hp, true);
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
m_admin.addComponent<HqProxyComponent>(m_team2HqEntity);
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
}
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
{
float hp = 0.0f;
WeaponComponent weapon;
weapon.cooldownTicks = 0.0f;
weapon.currentTarget = std::nullopt;
const double lv = static_cast<double>(entry.level);
const float tileSize = static_cast<float>(m_gameConfig.world.tileSize_m);
const std::vector<std::string>& mask = isEnemy
? m_gameConfig.stations.enemyStation.surfaceMask
: m_gameConfig.stations.playerStation.surfaceMask;
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_tiles = static_cast<float>(
m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv)) / tileSize;
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_tiles = static_cast<float>(
m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv)) / tileSize;
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.enemyStation.fireRateFormula.evaluate(lv));
}
const ParsedSurfaceMask parsed = parseSurfaceMask(mask, Rotation::East);
const QPoint& anchor = entry.position;
std::vector<QPoint> absCells;
for (const QPoint& rel : parsed.bodyCells)
{
absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
}
const entt::entity stationEntity = m_admin.spawnStation(
anchor, parsed.footprint, absCells, hp, hp, isEnemy);
{
entt::entity wChild = m_admin.createModuleEntity();
m_admin.addComponent<WeaponComponent>(wChild, weapon);
m_admin.addComponent<ModuleOwnerComponent>(wChild,
ModuleOwnerComponent{stationEntity});
}
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
};
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)
{
placeArenaStation(entry, false);
}
for (const ArenaStationEntry& entry : m_arenaConfig.teams[1].stations)
{
placeArenaStation(entry, true);
}
}
void ArenaSimulation::spawnShips()
{
const int contestStart = m_arenaConfig.playerBufferWidth_tiles;
const int team2Start = contestStart + m_arenaConfig.contestZoneWidth_tiles;
const int totalWidth = team2Start + m_arenaConfig.enemyBufferWidth_tiles;
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_tiles));
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, pos, false,
entry.layout);
}
}
}
// 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, pos, true,
entry.layout);
}
}
}
}
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): evaluate, select winner, execute.
// Module + combat systems emit their tool beams into a shared buffer.
m_shipSystem->clearMovementIntents();
m_aiSystem->tick(m_admin, *m_buildingSystem, *m_scrapSystem);
std::vector<BeamFiredEvent> beamFiredEvents;
m_salvagerSystem->tick(m_currentTick, *m_scrapSystem, *m_buildingSystem, beamFiredEvents);
m_repairSystem->tick(m_currentTick, beamFiredEvents);
// Combat resolution (tick step 8).
m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, beamFiredEvents);
m_beamFiredEvents.insert(m_beamFiredEvents.end(), beamFiredEvents.begin(), beamFiredEvents.end());
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
// Deaths (tick step 9, simplified).
tickDeaths();
// Movement (tick step 10).
m_movementIntentSystem->tick(m_admin);
m_dynamicBodySystem->tick(m_admin);
// 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<entt::entity> deadShips;
m_admin.forEach<ShipIdentityComponent, HealthComponent>(
[&deadShips](entt::entity e, const ShipIdentityComponent& /*si*/,
const HealthComponent& h)
{
if (h.hp <= 0.0f)
{
deadShips.push_back(e);
}
});
for (entt::entity deadEntity : deadShips)
{
const ShipIdentityComponent& si = m_admin.get<ShipIdentityComponent>(deadEntity);
const PositionComponent& pos = m_admin.get<PositionComponent>(deadEntity);
if (si.scrapDrop > 0)
{
const Tick despawnAt = m_currentTick
+ secondsToTicks(m_gameConfig.world.scrapDespawnSeconds);
m_scrapSystem->spawn(pos.value, si.scrapDrop, despawnAt);
}
m_shipSystem->despawn(deadEntity);
}
// Dead stations.
std::vector<entt::entity> deadStations;
m_admin.forEach<StationBodyComponent, HealthComponent>(
[&deadStations](entt::entity e, const StationBodyComponent& /*sb*/,
const HealthComponent& h)
{
if (h.hp <= 0.0f)
{
deadStations.push_back(e);
}
});
for (entt::entity deadEntity : deadStations)
{
const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
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);
}
// Check end conditions — HQ proxy entities.
const bool team1HqGone = !m_admin.isValid(m_team1HqEntity)
|| m_admin.get<HealthComponent>(m_team1HqEntity).hp <= 0.0f;
const bool team2HqGone = !m_admin.isValid(m_team2HqEntity)
|| m_admin.get<HealthComponent>(m_team2HqEntity).hp <= 0.0f;
if (team1HqGone || team2HqGone)
{
m_finished = true;
m_winnerTeam = team1HqGone ? 1 : 0;
updateStatus();
return;
}
// Check if all ships and defence stations of one team are destroyed.
bool team1HasUnits = false;
bool team2HasUnits = false;
m_admin.forEach<ShipIdentityComponent, FactionComponent>(
[&team1HasUnits, &team2HasUnits](entt::entity /*e*/,
const ShipIdentityComponent& /*si*/,
const FactionComponent& f)
{
if (f.isEnemy) { team2HasUnits = true; }
else { team1HasUnits = true; }
});
m_admin.forEach<StationBodyComponent, FactionComponent>(
[&team1HasUnits, &team2HasUnits](entt::entity /*e*/,
const StationBodyComponent& /*sb*/,
const FactionComponent& f)
{
if (f.isEnemy) { team2HasUnits = true; }
else { team1HasUnits = true; }
});
if (!team1HasUnits || !team2HasUnits)
{
m_finished = true;
m_winnerTeam = team1HasUnits ? 0 : 1;
updateStatus();
}
}
void ArenaSimulation::tickOnce()
{
if (!m_finished)
{
tick();
updateStatus();
}
}
std::vector<BeamFiredEvent> ArenaSimulation::drainBeamFiredEvents()
{
std::vector<BeamFiredEvent> result;
result.swap(m_beamFiredEvents);
return result;
}
bool ArenaSimulation::isFinished() const
{
return m_finished;
}
int ArenaSimulation::winnerTeam() const
{
return m_winnerTeam;
}
Tick ArenaSimulation::currentTick() const
{
return m_currentTick;
}
const ArenaConfig& ArenaSimulation::arenaConfig() const
{
return m_arenaConfig;
}
const BuildingSystem& ArenaSimulation::buildings() const
{
return *m_buildingSystem;
}
const ShipSystem& ArenaSimulation::ships() const
{
return *m_shipSystem;
}
const ScrapSystem& ArenaSimulation::scraps() const
{
return *m_scrapSystem;
}
EntityAdmin& ArenaSimulation::admin()
{
return m_admin;
}
const EntityAdmin& ArenaSimulation::admin() const
{
return m_admin;
}
void ArenaSimulation::updateStatus()
{
ArenaStatus newStatus;
newStatus.finished = m_finished;
newStatus.winnerTeam = m_winnerTeam;
// Live remaining HP of each team's ships and defence stations (HQ excluded);
// the EHP-percentage numerator (denominator is the fixed m_teamMaxEhp).
double currentEhp[2] = {0.0, 0.0};
m_admin.forEach<ShipIdentityComponent, FactionComponent, HealthComponent>(
[&currentEhp](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const FactionComponent& f, const HealthComponent& h)
{
if (h.hp > 0.0f)
{
currentEhp[f.isEnemy ? 1 : 0] += static_cast<double>(h.hp);
}
});
m_admin.forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[this, &currentEhp](entt::entity e, const StationBodyComponent& /*sb*/,
const FactionComponent& f, const HealthComponent& h)
{
if (m_admin.hasAll<HqProxyComponent>(e))
{
return;
}
if (h.hp > 0.0f)
{
currentEhp[f.isEnemy ? 1 : 0] += static_cast<double>(h.hp);
}
});
for (int ti = 0; ti < 2; ++ti)
{
ArenaStatus::TeamStatus& teamStatus = newStatus.teams[ti];
teamStatus.name = m_arenaConfig.teams[ti].name;
teamStatus.threatLevel = m_teamThreat[ti];
teamStatus.currentEhp = currentEhp[ti];
teamStatus.maxEhp = m_teamMaxEhp[ti];
// HQ entry (always first).
{
ArenaStatus::Entry hqEntry;
hqEntry.displayName = "HQ";
hqEntry.level = 1;
hqEntry.total = 1;
const entt::entity hqEntity = (ti == 0) ? m_team1HqEntity : m_team2HqEntity;
hqEntry.surviving = (m_admin.isValid(hqEntity)
&& m_admin.get<HealthComponent>(hqEntity).hp > 0.0f) ? 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;
// Ships no longer carry a level (level suffix stays empty).
entry.total = shipEntry.count;
int surviving = 0;
const bool isEnemyTeam = (ti == 1);
m_admin.forEach<ShipIdentityComponent, FactionComponent, HealthComponent>(
[&surviving, &shipEntry, isEnemyTeam](entt::entity /*e*/,
const ShipIdentityComponent& si, const FactionComponent& f,
const HealthComponent& h)
{
if (f.isEnemy == isEnemyTeam
&& si.schematicId == shipEntry.schematicId
&& h.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;
int surviving = 0;
const bool isEnemyTeam = (ti == 1);
m_admin.forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[&surviving, &stationEntry, isEnemyTeam](entt::entity /*e*/,
const StationBodyComponent& sb, const FactionComponent& f,
const HealthComponent& h)
{
if (f.isEnemy == isEnemyTeam
&& sb.anchor == stationEntry.position
&& h.hp > 0.0f)
{
surviving = 1;
}
});
entry.surviving = surviving;
teamStatus.entries.push_back(entry);
}
}
std::lock_guard<std::mutex> lock(m_statusMutex);
m_status = newStatus;
}