add balancing tool target
This commit is contained in:
74
bin/balancing/data/balancing.toml
Normal file
74
bin/balancing/data/balancing.toml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
[[arena]]
|
||||||
|
name = "Interceptors vs Interceptors"
|
||||||
|
height_tiles = 60
|
||||||
|
player_buffer_width = 10
|
||||||
|
contest_zone_width = 40
|
||||||
|
enemy_buffer_width = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Alpha"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "interceptor"
|
||||||
|
level = 1
|
||||||
|
count = 5
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Beta"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "interceptor"
|
||||||
|
level = 1
|
||||||
|
count = 5
|
||||||
|
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Few Destroyers vs Many Interceptors"
|
||||||
|
height_tiles = 60
|
||||||
|
player_buffer_width = 10
|
||||||
|
contest_zone_width = 60
|
||||||
|
enemy_buffer_width = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Destroyers"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "destroyer"
|
||||||
|
level = 5
|
||||||
|
count = 2
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Interceptors"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "interceptor"
|
||||||
|
level = 1
|
||||||
|
count = 10
|
||||||
|
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Stations and Ships"
|
||||||
|
height_tiles = 60
|
||||||
|
player_buffer_width = 15
|
||||||
|
contest_zone_width = 40
|
||||||
|
enemy_buffer_width = 15
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Fortified"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "interceptor"
|
||||||
|
level = 1
|
||||||
|
count = 3
|
||||||
|
[[arena.team.station]]
|
||||||
|
type = "player_station"
|
||||||
|
x = 8
|
||||||
|
y = 15
|
||||||
|
level = 1
|
||||||
|
[[arena.team.station]]
|
||||||
|
type = "player_station"
|
||||||
|
x = 8
|
||||||
|
y = 45
|
||||||
|
level = 1
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Swarm"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "interceptor"
|
||||||
|
level = 1
|
||||||
|
count = 8
|
||||||
@@ -4,6 +4,7 @@ set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
|||||||
set(TARGET_LIB_NAME "${TARGET_BASE_NAME}_lib")
|
set(TARGET_LIB_NAME "${TARGET_BASE_NAME}_lib")
|
||||||
set(TARGET_UI_NAME "${TARGET_BASE_NAME}_ui")
|
set(TARGET_UI_NAME "${TARGET_BASE_NAME}_ui")
|
||||||
set(TARGET_TEST_NAME "${TARGET_BASE_NAME}_test")
|
set(TARGET_TEST_NAME "${TARGET_BASE_NAME}_test")
|
||||||
|
set(TARGET_BALANCING_NAME "${TARGET_BASE_NAME}_balancing")
|
||||||
|
|
||||||
set(TARGET_LIB_INCLUDE_DIRS
|
set(TARGET_LIB_INCLUDE_DIRS
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/lib"
|
"${CMAKE_CURRENT_SOURCE_DIR}/lib"
|
||||||
@@ -11,6 +12,7 @@ set(TARGET_LIB_INCLUDE_DIRS
|
|||||||
)
|
)
|
||||||
set(TARGET_UI_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/ui")
|
set(TARGET_UI_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/ui")
|
||||||
set(TARGET_TEST_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/test")
|
set(TARGET_TEST_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/test")
|
||||||
|
set(TARGET_BALANCING_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/balancing")
|
||||||
|
|
||||||
set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
|
set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
|
|
||||||
@@ -188,6 +190,73 @@ unset(HDRS)
|
|||||||
unset(SRCS)
|
unset(SRCS)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# balancing — ship balancing tool; depends on lib + QtWidgets
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
COPY_QT_BINARIES("${OUTPUT_ROOT_PATH}/Debug/balancing/" True)
|
||||||
|
COPY_QT_BINARIES("${OUTPUT_ROOT_PATH}/Release/balancing/" False)
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND "cmd.exe" "/k" "rmdir" "${BACKSLASHED_OUTPUT_ROOT_PATH}\\Debug\\balancing\\data" & "mklink" "/d" "/j" "${BACKSLASHED_OUTPUT_ROOT_PATH}\\Debug\\balancing\\data" "${BACKSLASHED_CMAKE_SOURCE_DIR}\\bin\\balancing\\data" & exit
|
||||||
|
COMMAND "cmd.exe" "/k" "rmdir" "${BACKSLASHED_OUTPUT_ROOT_PATH}\\Release\\balancing\\data" & "mklink" "/d" "/j" "${BACKSLASHED_OUTPUT_ROOT_PATH}\\Release\\balancing\\data" "${BACKSLASHED_CMAKE_SOURCE_DIR}\\bin\\balancing\\data" & exit
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(HDRS)
|
||||||
|
set(SRCS)
|
||||||
|
|
||||||
|
add_subdirectory(balancing)
|
||||||
|
|
||||||
|
set(RELATIVE_HDRS)
|
||||||
|
foreach (_file ${HDRS})
|
||||||
|
file(RELATIVE_PATH _relPath "${SRC_DIR}" "${_file}")
|
||||||
|
list(APPEND RELATIVE_HDRS "${_relPath}")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
set(RELATIVE_SRCS)
|
||||||
|
foreach (_file ${SRCS})
|
||||||
|
file(RELATIVE_PATH _relPath "${SRC_DIR}" "${_file}")
|
||||||
|
list(APPEND RELATIVE_SRCS "${_relPath}")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
add_files(BALANCING_FILES ${RELATIVE_HDRS} ${RELATIVE_SRCS})
|
||||||
|
|
||||||
|
add_executable(${TARGET_BALANCING_NAME} ${BALANCING_FILES})
|
||||||
|
create_source_groups(${BALANCING_FILES})
|
||||||
|
|
||||||
|
foreach (OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES})
|
||||||
|
string(TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG)
|
||||||
|
set_target_properties(${TARGET_BALANCING_NAME} PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "${OUTPUT_ROOT_PATH}/${OUTPUTCONFIG}/balancing/"
|
||||||
|
)
|
||||||
|
endforeach ()
|
||||||
|
|
||||||
|
set_target_properties(${TARGET_BALANCING_NAME} PROPERTIES
|
||||||
|
AUTOMOC ON
|
||||||
|
CXX_STANDARD 17
|
||||||
|
VS_DEBUGGER_WORKING_DIRECTORY "${OUTPUT_ROOT_PATH}/$(Configuration)/balancing/"
|
||||||
|
)
|
||||||
|
target_include_directories(${TARGET_BALANCING_NAME} PRIVATE
|
||||||
|
"${TARGET_BALANCING_INCLUDE_DIRS}"
|
||||||
|
"${TARGET_LIB_INCLUDE_DIRS}"
|
||||||
|
"${LIB_INCLUDE_PATH}"
|
||||||
|
)
|
||||||
|
target_compile_definitions(${TARGET_BALANCING_NAME} PRIVATE
|
||||||
|
CONFIG_DIR="${CMAKE_SOURCE_DIR}/bin/app/data/config"
|
||||||
|
BALANCING_CONFIG="${CMAKE_SOURCE_DIR}/bin/balancing/data/balancing.toml"
|
||||||
|
TOML_FLOAT_CHARCONV=0
|
||||||
|
)
|
||||||
|
target_link_libraries(${TARGET_BALANCING_NAME} ${TARGET_LIB_NAME} Qt5::Widgets)
|
||||||
|
|
||||||
|
unset(BALANCING_FILES)
|
||||||
|
unset(RELATIVE_HDRS)
|
||||||
|
unset(RELATIVE_SRCS)
|
||||||
|
unset(HDRS)
|
||||||
|
unset(SRCS)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# tests — Catch2 tests; links against lib only (no QtWidgets)
|
# tests — Catch2 tests; links against lib only (no QtWidgets)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -218,3 +287,4 @@ target_link_libraries(${TARGET_TEST_NAME} ${TARGET_LIB_NAME})
|
|||||||
set(TARGET_LIB_NAME "${TARGET_LIB_NAME}" PARENT_SCOPE)
|
set(TARGET_LIB_NAME "${TARGET_LIB_NAME}" PARENT_SCOPE)
|
||||||
set(TARGET_UI_NAME "${TARGET_UI_NAME}" PARENT_SCOPE)
|
set(TARGET_UI_NAME "${TARGET_UI_NAME}" PARENT_SCOPE)
|
||||||
set(TARGET_APP_NAME "${TARGET_APP_NAME}" PARENT_SCOPE)
|
set(TARGET_APP_NAME "${TARGET_APP_NAME}" PARENT_SCOPE)
|
||||||
|
set(TARGET_BALANCING_NAME "${TARGET_BALANCING_NAME}" PARENT_SCOPE)
|
||||||
|
|||||||
88
src/balancing/ArenaButton.cpp
Normal file
88
src/balancing/ArenaButton.cpp
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#include "ArenaButton.h"
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
ArenaButton::ArenaButton(const std::string& arenaName, QWidget* parent)
|
||||||
|
: QFrame(parent)
|
||||||
|
, m_wasFinished(false)
|
||||||
|
{
|
||||||
|
buildLayout(arenaName);
|
||||||
|
setFrameStyle(QFrame::Box | QFrame::Plain);
|
||||||
|
setLineWidth(2);
|
||||||
|
setStyleSheet("ArenaButton { border: 2px solid #3366ff; padding: 8px; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArenaButton::buildLayout(const std::string& arenaName)
|
||||||
|
{
|
||||||
|
QVBoxLayout* outerLayout = new QVBoxLayout(this);
|
||||||
|
outerLayout->setContentsMargins(8, 8, 8, 8);
|
||||||
|
outerLayout->setSpacing(4);
|
||||||
|
|
||||||
|
m_titleLabel = new QLabel(QString::fromStdString(arenaName), this);
|
||||||
|
QFont titleFont = m_titleLabel->font();
|
||||||
|
titleFont.setBold(true);
|
||||||
|
titleFont.setPointSize(titleFont.pointSize() + 2);
|
||||||
|
m_titleLabel->setFont(titleFont);
|
||||||
|
outerLayout->addWidget(m_titleLabel);
|
||||||
|
|
||||||
|
QHBoxLayout* teamsLayout = new QHBoxLayout();
|
||||||
|
teamsLayout->setSpacing(16);
|
||||||
|
|
||||||
|
// Team 1 column.
|
||||||
|
QVBoxLayout* team1Layout = new QVBoxLayout();
|
||||||
|
m_team1Header = new QLabel(this);
|
||||||
|
QFont headerFont = m_team1Header->font();
|
||||||
|
headerFont.setBold(true);
|
||||||
|
m_team1Header->setFont(headerFont);
|
||||||
|
team1Layout->addWidget(m_team1Header);
|
||||||
|
m_team1Content = new QLabel(this);
|
||||||
|
team1Layout->addWidget(m_team1Content);
|
||||||
|
team1Layout->addStretch();
|
||||||
|
teamsLayout->addLayout(team1Layout);
|
||||||
|
|
||||||
|
// Team 2 column.
|
||||||
|
QVBoxLayout* team2Layout = new QVBoxLayout();
|
||||||
|
m_team2Header = new QLabel(this);
|
||||||
|
m_team2Header->setFont(headerFont);
|
||||||
|
team2Layout->addWidget(m_team2Header);
|
||||||
|
m_team2Content = new QLabel(this);
|
||||||
|
team2Layout->addWidget(m_team2Content);
|
||||||
|
team2Layout->addStretch();
|
||||||
|
teamsLayout->addLayout(team2Layout);
|
||||||
|
|
||||||
|
outerLayout->addLayout(teamsLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArenaButton::updateStatus(const ArenaStatus& status)
|
||||||
|
{
|
||||||
|
for (int ti = 0; ti < 2; ++ti)
|
||||||
|
{
|
||||||
|
const ArenaStatus::TeamStatus& team = status.teams[ti];
|
||||||
|
QLabel* header = (ti == 0) ? m_team1Header : m_team2Header;
|
||||||
|
QLabel* content = (ti == 0) ? m_team1Content : m_team2Content;
|
||||||
|
|
||||||
|
header->setText(QString::fromStdString(team.name));
|
||||||
|
|
||||||
|
QString lines;
|
||||||
|
for (const ArenaStatus::Entry& entry : team.entries)
|
||||||
|
{
|
||||||
|
if (!lines.isEmpty())
|
||||||
|
{
|
||||||
|
lines += "\n";
|
||||||
|
}
|
||||||
|
lines += QString("%1/%2 %3 L%4")
|
||||||
|
.arg(entry.surviving)
|
||||||
|
.arg(entry.total)
|
||||||
|
.arg(QString::fromStdString(entry.displayName))
|
||||||
|
.arg(entry.level);
|
||||||
|
}
|
||||||
|
content->setText(lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.finished && !m_wasFinished)
|
||||||
|
{
|
||||||
|
m_wasFinished = true;
|
||||||
|
setStyleSheet("ArenaButton { border: 2px solid #33cc33; padding: 8px; }");
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/balancing/ArenaButton.h
Normal file
29
src/balancing/ArenaButton.h
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QLabel>
|
||||||
|
|
||||||
|
#include "ArenaSimulation.h"
|
||||||
|
|
||||||
|
class ArenaButton : public QFrame
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ArenaButton(const std::string& arenaName, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
void updateStatus(const ArenaStatus& status);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void buildLayout(const std::string& arenaName);
|
||||||
|
|
||||||
|
QLabel* m_titleLabel;
|
||||||
|
QLabel* m_team1Header;
|
||||||
|
QLabel* m_team2Header;
|
||||||
|
QLabel* m_team1Content;
|
||||||
|
QLabel* m_team2Content;
|
||||||
|
bool m_wasFinished;
|
||||||
|
};
|
||||||
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;
|
||||||
|
}
|
||||||
84
src/balancing/ArenaSimulation.h
Normal file
84
src/balancing/ArenaSimulation.h
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <random>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "BalancingConfig.h"
|
||||||
|
#include "BeltSystem.h"
|
||||||
|
#include "EntityId.h"
|
||||||
|
#include "FireEvent.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
class BuildingSystem;
|
||||||
|
class CombatSystem;
|
||||||
|
class ShipSystem;
|
||||||
|
class ScrapSystem;
|
||||||
|
|
||||||
|
struct ArenaStatus
|
||||||
|
{
|
||||||
|
struct Entry
|
||||||
|
{
|
||||||
|
std::string displayName;
|
||||||
|
int level;
|
||||||
|
int total;
|
||||||
|
int surviving;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TeamStatus
|
||||||
|
{
|
||||||
|
std::string name;
|
||||||
|
std::vector<Entry> entries; // HQ first, then ships, then stations
|
||||||
|
};
|
||||||
|
|
||||||
|
TeamStatus teams[2];
|
||||||
|
bool finished = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ArenaSimulation
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ArenaSimulation(const GameConfig& gameConfig,
|
||||||
|
ArenaConfig arenaConfig,
|
||||||
|
unsigned int seed = 0);
|
||||||
|
~ArenaSimulation();
|
||||||
|
|
||||||
|
void run();
|
||||||
|
void requestStop();
|
||||||
|
|
||||||
|
ArenaStatus status() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
EntityId allocateId();
|
||||||
|
void placeStructures();
|
||||||
|
void spawnShips();
|
||||||
|
void tick();
|
||||||
|
void tickDeaths();
|
||||||
|
void updateStatus();
|
||||||
|
|
||||||
|
const GameConfig& m_gameConfig;
|
||||||
|
ArenaConfig m_arenaConfig;
|
||||||
|
std::mt19937 m_rng;
|
||||||
|
|
||||||
|
Tick m_currentTick;
|
||||||
|
EntityId m_nextId;
|
||||||
|
|
||||||
|
BeltSystem m_beltSystem;
|
||||||
|
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||||
|
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||||
|
std::unique_ptr<CombatSystem> m_combatSystem;
|
||||||
|
std::unique_ptr<ScrapSystem> m_scrapSystem;
|
||||||
|
|
||||||
|
EntityId m_team1HqId;
|
||||||
|
EntityId m_team2HqId;
|
||||||
|
|
||||||
|
bool m_finished;
|
||||||
|
std::atomic<bool> m_stopRequested;
|
||||||
|
|
||||||
|
mutable std::mutex m_statusMutex;
|
||||||
|
ArenaStatus m_status;
|
||||||
|
};
|
||||||
166
src/balancing/BalancingConfig.cpp
Normal file
166
src/balancing/BalancingConfig.cpp
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
#include "BalancingConfig.h"
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "toml.hpp"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
std::runtime_error makeError(const std::string& path, const std::string& why)
|
||||||
|
{
|
||||||
|
return std::runtime_error("balancing.toml: '" + path + "' " + why);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename NodeView>
|
||||||
|
int64_t requireInt(NodeView node, const std::string& path)
|
||||||
|
{
|
||||||
|
const std::optional<int64_t> value = node.template value<int64_t>();
|
||||||
|
if (!value)
|
||||||
|
{
|
||||||
|
throw makeError(path, "missing or not an integer");
|
||||||
|
}
|
||||||
|
return *value;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename NodeView>
|
||||||
|
std::string requireString(NodeView node, const std::string& path)
|
||||||
|
{
|
||||||
|
const std::optional<std::string> value = node.template value<std::string>();
|
||||||
|
if (!value)
|
||||||
|
{
|
||||||
|
throw makeError(path, "missing or not a string");
|
||||||
|
}
|
||||||
|
return *value;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
BalancingConfig loadBalancingConfig(const std::string& path)
|
||||||
|
{
|
||||||
|
toml::table root;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
root = toml::parse_file(path);
|
||||||
|
}
|
||||||
|
catch (const toml::parse_error& e)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(std::string("balancing.toml: parse error: ") + e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
const toml::array* arenaArray = root["arena"].as_array();
|
||||||
|
if (!arenaArray)
|
||||||
|
{
|
||||||
|
throw makeError("arena", "missing or not an array of tables");
|
||||||
|
}
|
||||||
|
|
||||||
|
BalancingConfig config;
|
||||||
|
|
||||||
|
for (std::size_t ai = 0; ai < arenaArray->size(); ++ai)
|
||||||
|
{
|
||||||
|
const toml::table* arenaTbl = (*arenaArray)[ai].as_table();
|
||||||
|
if (!arenaTbl)
|
||||||
|
{
|
||||||
|
throw makeError("arena[" + std::to_string(ai) + "]", "not a table");
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string prefix = "arena[" + std::to_string(ai) + "]";
|
||||||
|
|
||||||
|
ArenaConfig arena;
|
||||||
|
arena.name = requireString((*arenaTbl)["name"], prefix + ".name");
|
||||||
|
arena.heightTiles = static_cast<int>(
|
||||||
|
requireInt((*arenaTbl)["height_tiles"], prefix + ".height_tiles"));
|
||||||
|
arena.playerBufferWidth = static_cast<int>(
|
||||||
|
requireInt((*arenaTbl)["player_buffer_width"], prefix + ".player_buffer_width"));
|
||||||
|
arena.contestZoneWidth = static_cast<int>(
|
||||||
|
requireInt((*arenaTbl)["contest_zone_width"], prefix + ".contest_zone_width"));
|
||||||
|
arena.enemyBufferWidth = static_cast<int>(
|
||||||
|
requireInt((*arenaTbl)["enemy_buffer_width"], prefix + ".enemy_buffer_width"));
|
||||||
|
|
||||||
|
const toml::array* teamArray = (*arenaTbl)["team"].as_array();
|
||||||
|
if (!teamArray || teamArray->size() != 2)
|
||||||
|
{
|
||||||
|
throw makeError(prefix + ".team", "must contain exactly 2 teams");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int ti = 0; ti < 2; ++ti)
|
||||||
|
{
|
||||||
|
const toml::table* teamTbl = (*teamArray)[static_cast<std::size_t>(ti)].as_table();
|
||||||
|
if (!teamTbl)
|
||||||
|
{
|
||||||
|
throw makeError(prefix + ".team[" + std::to_string(ti) + "]", "not a table");
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string tPrefix = prefix + ".team[" + std::to_string(ti) + "]";
|
||||||
|
|
||||||
|
ArenaTeamConfig& team = arena.teams[ti];
|
||||||
|
team.name = requireString((*teamTbl)["name"], tPrefix + ".name");
|
||||||
|
|
||||||
|
const toml::array* shipArray = (*teamTbl)["ship"].as_array();
|
||||||
|
if (shipArray)
|
||||||
|
{
|
||||||
|
for (std::size_t si = 0; si < shipArray->size(); ++si)
|
||||||
|
{
|
||||||
|
const toml::table* shipTbl = (*shipArray)[si].as_table();
|
||||||
|
if (!shipTbl)
|
||||||
|
{
|
||||||
|
throw makeError(tPrefix + ".ship[" + std::to_string(si) + "]",
|
||||||
|
"not a table");
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string sPrefix =
|
||||||
|
tPrefix + ".ship[" + std::to_string(si) + "]";
|
||||||
|
|
||||||
|
ArenaShipEntry entry;
|
||||||
|
entry.schematicId = requireString((*shipTbl)["schematic"],
|
||||||
|
sPrefix + ".schematic");
|
||||||
|
entry.level = static_cast<int>(
|
||||||
|
requireInt((*shipTbl)["level"], sPrefix + ".level"));
|
||||||
|
entry.count = static_cast<int>(
|
||||||
|
requireInt((*shipTbl)["count"], sPrefix + ".count"));
|
||||||
|
team.ships.push_back(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toml::array* stationArray = (*teamTbl)["station"].as_array();
|
||||||
|
if (stationArray)
|
||||||
|
{
|
||||||
|
for (std::size_t si = 0; si < stationArray->size(); ++si)
|
||||||
|
{
|
||||||
|
const toml::table* stationTbl = (*stationArray)[si].as_table();
|
||||||
|
if (!stationTbl)
|
||||||
|
{
|
||||||
|
throw makeError(tPrefix + ".station[" + std::to_string(si) + "]",
|
||||||
|
"not a table");
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string sPrefix =
|
||||||
|
tPrefix + ".station[" + std::to_string(si) + "]";
|
||||||
|
|
||||||
|
ArenaStationEntry entry;
|
||||||
|
entry.stationType = requireString((*stationTbl)["type"],
|
||||||
|
sPrefix + ".type");
|
||||||
|
entry.level = static_cast<int>(
|
||||||
|
requireInt((*stationTbl)["level"], sPrefix + ".level"));
|
||||||
|
entry.position = QPoint(
|
||||||
|
static_cast<int>(requireInt((*stationTbl)["x"], sPrefix + ".x")),
|
||||||
|
static_cast<int>(requireInt((*stationTbl)["y"], sPrefix + ".y")));
|
||||||
|
|
||||||
|
if (entry.stationType != "player_station"
|
||||||
|
&& entry.stationType != "enemy_station")
|
||||||
|
{
|
||||||
|
throw makeError(sPrefix + ".type",
|
||||||
|
"must be 'player_station' or 'enemy_station'");
|
||||||
|
}
|
||||||
|
|
||||||
|
team.stations.push_back(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.arenas.push_back(arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
44
src/balancing/BalancingConfig.h
Normal file
44
src/balancing/BalancingConfig.h
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
struct ArenaStationEntry
|
||||||
|
{
|
||||||
|
std::string stationType; // "player_station" or "enemy_station"
|
||||||
|
int level;
|
||||||
|
QPoint position;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ArenaShipEntry
|
||||||
|
{
|
||||||
|
std::string schematicId;
|
||||||
|
int level;
|
||||||
|
int count;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ArenaTeamConfig
|
||||||
|
{
|
||||||
|
std::string name;
|
||||||
|
std::vector<ArenaShipEntry> ships;
|
||||||
|
std::vector<ArenaStationEntry> stations;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ArenaConfig
|
||||||
|
{
|
||||||
|
std::string name;
|
||||||
|
int heightTiles;
|
||||||
|
int playerBufferWidth;
|
||||||
|
int contestZoneWidth;
|
||||||
|
int enemyBufferWidth;
|
||||||
|
ArenaTeamConfig teams[2];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BalancingConfig
|
||||||
|
{
|
||||||
|
std::vector<ArenaConfig> arenas;
|
||||||
|
};
|
||||||
|
|
||||||
|
BalancingConfig loadBalancingConfig(const std::string& path);
|
||||||
80
src/balancing/BalancingWindow.cpp
Normal file
80
src/balancing/BalancingWindow.cpp
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#include "BalancingWindow.h"
|
||||||
|
|
||||||
|
#include <QScrollArea>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
||||||
|
const GameConfig& gameConfig,
|
||||||
|
QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
setWindowTitle("DotaFactory — Balancing Tool");
|
||||||
|
resize(800, 600);
|
||||||
|
|
||||||
|
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||||
|
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|
||||||
|
QScrollArea* scrollArea = new QScrollArea(this);
|
||||||
|
scrollArea->setWidgetResizable(true);
|
||||||
|
|
||||||
|
QWidget* scrollContent = new QWidget(scrollArea);
|
||||||
|
QVBoxLayout* contentLayout = new QVBoxLayout(scrollContent);
|
||||||
|
contentLayout->setSpacing(8);
|
||||||
|
contentLayout->setContentsMargins(8, 8, 8, 8);
|
||||||
|
|
||||||
|
unsigned int seed = 0;
|
||||||
|
for (const ArenaConfig& arenaConfig : balancingConfig.arenas)
|
||||||
|
{
|
||||||
|
ArenaEntry entry;
|
||||||
|
entry.simulation = std::make_unique<ArenaSimulation>(
|
||||||
|
gameConfig, arenaConfig, seed++);
|
||||||
|
entry.button = new ArenaButton(arenaConfig.name, scrollContent);
|
||||||
|
contentLayout->addWidget(entry.button);
|
||||||
|
|
||||||
|
entry.button->updateStatus(entry.simulation->status());
|
||||||
|
|
||||||
|
m_arenas.push_back(std::move(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
contentLayout->addStretch();
|
||||||
|
scrollArea->setWidget(scrollContent);
|
||||||
|
mainLayout->addWidget(scrollArea);
|
||||||
|
|
||||||
|
// Start worker threads.
|
||||||
|
for (ArenaEntry& entry : m_arenas)
|
||||||
|
{
|
||||||
|
ArenaSimulation* sim = entry.simulation.get();
|
||||||
|
entry.worker = std::thread([sim]() { sim->run(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll timer at ~10 Hz to update button statuses.
|
||||||
|
m_pollTimer = new QTimer(this);
|
||||||
|
connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses);
|
||||||
|
m_pollTimer->start(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
BalancingWindow::~BalancingWindow()
|
||||||
|
{
|
||||||
|
m_pollTimer->stop();
|
||||||
|
|
||||||
|
for (ArenaEntry& entry : m_arenas)
|
||||||
|
{
|
||||||
|
entry.simulation->requestStop();
|
||||||
|
}
|
||||||
|
for (ArenaEntry& entry : m_arenas)
|
||||||
|
{
|
||||||
|
if (entry.worker.joinable())
|
||||||
|
{
|
||||||
|
entry.worker.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BalancingWindow::pollStatuses()
|
||||||
|
{
|
||||||
|
for (ArenaEntry& entry : m_arenas)
|
||||||
|
{
|
||||||
|
const ArenaStatus status = entry.simulation->status();
|
||||||
|
entry.button->updateStatus(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/balancing/BalancingWindow.h
Normal file
38
src/balancing/BalancingWindow.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "ArenaButton.h"
|
||||||
|
#include "ArenaSimulation.h"
|
||||||
|
#include "BalancingConfig.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
|
||||||
|
class BalancingWindow : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
BalancingWindow(const BalancingConfig& balancingConfig,
|
||||||
|
const GameConfig& gameConfig,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
~BalancingWindow() override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void pollStatuses();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct ArenaEntry
|
||||||
|
{
|
||||||
|
std::unique_ptr<ArenaSimulation> simulation;
|
||||||
|
std::thread worker;
|
||||||
|
ArenaButton* button;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<ArenaEntry> m_arenas;
|
||||||
|
QTimer* m_pollTimer;
|
||||||
|
};
|
||||||
18
src/balancing/CMakeLists.txt
Normal file
18
src/balancing/CMakeLists.txt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
SET(HDRS
|
||||||
|
${HDRS}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ArenaButton.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ArenaSimulation.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.h
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
|
|
||||||
|
SET(SRCS
|
||||||
|
${SRCS}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ArenaButton.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ArenaSimulation.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.cpp
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
34
src/balancing/main.cpp
Normal file
34
src/balancing/main.cpp
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
|
||||||
|
#include "BalancingConfig.h"
|
||||||
|
#include "BalancingWindow.h"
|
||||||
|
#include "ConfigLoader.h"
|
||||||
|
#include "ConsoleLogger.h"
|
||||||
|
#include "logging.h"
|
||||||
|
#include "LogManager.h"
|
||||||
|
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
LogManager::getInstance()->addLogger(std::make_shared<ConsoleLogger>());
|
||||||
|
LogManager::getInstance()->setLoggingEnabled(true);
|
||||||
|
LOG_INFO("Balancing tool starting");
|
||||||
|
|
||||||
|
QApplication::setApplicationName("DotaFactory Balancing");
|
||||||
|
|
||||||
|
if (QSysInfo::windowsVersion() != QSysInfo::WV_None)
|
||||||
|
{
|
||||||
|
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
QApplication application(argc, argv);
|
||||||
|
|
||||||
|
GameConfig gameConfig = ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||||
|
BalancingConfig balancingConfig = loadBalancingConfig(BALANCING_CONFIG);
|
||||||
|
|
||||||
|
BalancingWindow window(balancingConfig, gameConfig);
|
||||||
|
window.show();
|
||||||
|
|
||||||
|
return application.exec();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user