add basic types and fix cmake
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
SET(HDRS)
|
||||
SET(SRCS)
|
||||
SET(LIB_INCLUDE_PATH)
|
||||
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(config)
|
||||
add_subdirectory(utility)
|
||||
|
||||
SET(HDRS
|
||||
|
||||
26
src/lib/config/BuildingsConfig.h
Normal file
26
src/lib/config/BuildingsConfig.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "BuildingType.h"
|
||||
|
||||
// A single entry from buildings.toml [[building]].
|
||||
struct BuildingDef
|
||||
{
|
||||
std::string id; // Raw id string from TOML, e.g. "miner".
|
||||
BuildingType type; // Parsed from id at load time.
|
||||
int cost; // REQ-BLD-COST
|
||||
bool playerPlaceable; // Shown in the build menu if true.
|
||||
double constructionTimeSeconds; // REQ-BLD-QUEUE
|
||||
|
||||
// Rows of the surface_mask (REQ-BLD requirements, "Surface Mask Format").
|
||||
// Stored as raw strings here; parsing into per-cell tiles + output ports
|
||||
// happens when buildings are placed, not at load time.
|
||||
std::vector<std::string> surfaceMask;
|
||||
};
|
||||
|
||||
struct BuildingsConfig
|
||||
{
|
||||
std::vector<BuildingDef> buildings;
|
||||
};
|
||||
25
src/lib/config/CMakeLists.txt
Normal file
25
src/lib/config/CMakeLists.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
SET(HDRS
|
||||
${HDRS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Formula.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingsConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipesConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipsConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/StationsConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
SET(SRCS
|
||||
${SRCS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Formula.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
set(LIB_INCLUDE_PATH
|
||||
${LIB_INCLUDE_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PARENT_SCOPE
|
||||
)
|
||||
496
src/lib/config/ConfigLoader.cpp
Normal file
496
src/lib/config/ConfigLoader.cpp
Normal file
@@ -0,0 +1,496 @@
|
||||
#include "ConfigLoader.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "toml.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// --- Error helpers --------------------------------------------------------
|
||||
|
||||
std::runtime_error makeError(const std::string& file,
|
||||
const std::string& path,
|
||||
const std::string& why)
|
||||
{
|
||||
return std::runtime_error("Config: " + file + ": '" + path + "' " + why);
|
||||
}
|
||||
|
||||
// --- Typed accessors (throw on missing or wrong type) ---------------------
|
||||
|
||||
int64_t requireInt(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const std::optional<int64_t> value = node.value<int64_t>();
|
||||
if (!value)
|
||||
{
|
||||
throw makeError(file, path, "missing or not an integer");
|
||||
}
|
||||
return *value;
|
||||
}
|
||||
|
||||
double requireDouble(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
if (const std::optional<double> v = node.value<double>())
|
||||
{
|
||||
return *v;
|
||||
}
|
||||
if (const std::optional<int64_t> v = node.value<int64_t>())
|
||||
{
|
||||
return static_cast<double>(*v);
|
||||
}
|
||||
throw makeError(file, path, "missing or not a number");
|
||||
}
|
||||
|
||||
std::string requireString(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const std::optional<std::string> value = node.value<std::string>();
|
||||
if (!value)
|
||||
{
|
||||
throw makeError(file, path, "missing or not a string");
|
||||
}
|
||||
return *value;
|
||||
}
|
||||
|
||||
bool requireBool(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const std::optional<bool> value = node.value<bool>();
|
||||
if (!value)
|
||||
{
|
||||
throw makeError(file, path, "missing or not a boolean");
|
||||
}
|
||||
return *value;
|
||||
}
|
||||
|
||||
const toml::array& requireArray(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const toml::array* arr = node.as_array();
|
||||
if (arr == nullptr)
|
||||
{
|
||||
throw makeError(file, path, "missing or not an array");
|
||||
}
|
||||
return *arr;
|
||||
}
|
||||
|
||||
const toml::table& requireTable(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const toml::table* tbl = node.as_table();
|
||||
if (tbl == nullptr)
|
||||
{
|
||||
throw makeError(file, path, "missing or not a table");
|
||||
}
|
||||
return *tbl;
|
||||
}
|
||||
|
||||
Formula requireFormula(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const std::string source = requireString(node, file, path);
|
||||
try
|
||||
{
|
||||
return Formula::compile(source);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
throw makeError(file, path, std::string("formula error: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> requireStringArray(const toml::node_view<toml::node>& node,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
const toml::array& arr = requireArray(node, file, path);
|
||||
std::vector<std::string> 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 std::optional<std::string> s = arr[i].value<std::string>();
|
||||
if (!s)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a string");
|
||||
}
|
||||
result.push_back(*s);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<RecipeIngredient> parseIngredients(const toml::array& arr,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
std::vector<RecipeIngredient> 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)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a table");
|
||||
}
|
||||
|
||||
// We need a mutable node_view to reuse our helpers, which is fine
|
||||
// because the helpers never mutate.
|
||||
toml::table& mt = const_cast<toml::table&>(*t);
|
||||
|
||||
RecipeIngredient ing;
|
||||
ing.item = requireString(mt["item"], file, elemPath + ".item");
|
||||
ing.amount = static_cast<int>(requireInt(mt["amount"], file, elemPath + ".amount"));
|
||||
result.push_back(std::move(ing));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<RecipeOutput> parseRecipeOutputs(const toml::array& arr,
|
||||
const std::string& file,
|
||||
const std::string& path)
|
||||
{
|
||||
std::vector<RecipeOutput> 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)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*t);
|
||||
|
||||
RecipeOutput out;
|
||||
out.item = requireString(mt["item"], file, elemPath + ".item");
|
||||
out.amount = static_cast<int>(requireInt(mt["amount"], file, elemPath + ".amount"));
|
||||
if (const std::optional<double> p = mt["probability"].value<double>())
|
||||
{
|
||||
out.probability = *p;
|
||||
}
|
||||
else if (const std::optional<int64_t> p = mt["probability"].value<int64_t>())
|
||||
{
|
||||
out.probability = static_cast<double>(*p);
|
||||
}
|
||||
result.push_back(std::move(out));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
toml::table parseFile(const std::string& path, const std::string& file)
|
||||
{
|
||||
try
|
||||
{
|
||||
return toml::parse_file(path);
|
||||
}
|
||||
catch (const toml::parse_error& e)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "Config: " << file << ": TOML parse error: " << e.description()
|
||||
<< " at " << e.source().begin;
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
// --- Per-file loaders -----------------------------------------------------
|
||||
|
||||
WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
||||
{
|
||||
const std::string file = "world.toml";
|
||||
toml::table tbl = parseFile(path, file);
|
||||
|
||||
WorldConfig cfg;
|
||||
|
||||
cfg.heightTiles = static_cast<int>(requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles"));
|
||||
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
|
||||
cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds");
|
||||
|
||||
cfg.regions.asteroidWidth = static_cast<int>(requireInt(tbl["regions"]["asteroid_width"], file, "regions.asteroid_width"));
|
||||
cfg.regions.playerBufferWidth = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width"], file, "regions.player_buffer_width"));
|
||||
cfg.regions.contestZoneWidth = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width"], file, "regions.contest_zone_width"));
|
||||
cfg.regions.enemyBufferWidth = static_cast<int>(requireInt(tbl["regions"]["enemy_buffer_width"], file, "regions.enemy_buffer_width"));
|
||||
|
||||
cfg.expansion.columnsPerExpansion = static_cast<int>(requireInt(tbl["expansion"]["columns_per_expansion"], file, "expansion.columns_per_expansion"));
|
||||
cfg.expansion.costBuildingBlocks = static_cast<int>(requireInt(tbl["expansion"]["cost_building_blocks"], file, "expansion.cost_building_blocks"));
|
||||
|
||||
cfg.push.pushExpandColumns = static_cast<int>(requireInt(tbl["push"]["push_expand_columns"], file, "push.push_expand_columns"));
|
||||
cfg.push.scalingFactor = requireDouble(tbl["push"]["scaling_factor"], file, "push.scaling_factor");
|
||||
|
||||
cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula");
|
||||
cfg.waves.shipLevelFormula = requireFormula(tbl["waves"]["ship_level_formula"], file, "waves.ship_level_formula");
|
||||
cfg.waves.gapMinSeconds = requireDouble(tbl["waves"]["gap_min_seconds"], file, "waves.gap_min_seconds");
|
||||
cfg.waves.gapMaxSeconds = requireDouble(tbl["waves"]["gap_max_seconds"], file, "waves.gap_max_seconds");
|
||||
cfg.waves.spawnDurationSeconds = requireDouble(tbl["waves"]["spawn_duration_seconds"], file, "waves.spawn_duration_seconds");
|
||||
|
||||
if (cfg.waves.gapMinSeconds > cfg.waves.gapMaxSeconds)
|
||||
{
|
||||
throw makeError(file, "waves", "gap_min_seconds > gap_max_seconds");
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
|
||||
{
|
||||
const std::string file = "buildings.toml";
|
||||
toml::table tbl = parseFile(path, file);
|
||||
|
||||
BuildingsConfig cfg;
|
||||
const toml::array& arr = requireArray(tbl["building"], file, "building");
|
||||
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = "building[" + std::to_string(i) + "]";
|
||||
const toml::table* bt = arr[i].as_table();
|
||||
if (bt == nullptr)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*bt);
|
||||
|
||||
BuildingDef def;
|
||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||
def.cost = static_cast<int>(requireInt(mt["cost"], file, elemPath + ".cost"));
|
||||
def.playerPlaceable = requireBool(mt["player_placeable"], file, elemPath + ".player_placeable");
|
||||
def.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
|
||||
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||
|
||||
const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
|
||||
if (!parsedType)
|
||||
{
|
||||
throw makeError(file, elemPath + ".id", "unknown building id '" + def.id + "'");
|
||||
}
|
||||
def.type = *parsedType;
|
||||
|
||||
cfg.buildings.push_back(std::move(def));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
|
||||
{
|
||||
const std::string file = "recipes.toml";
|
||||
toml::table tbl = parseFile(path, file);
|
||||
|
||||
RecipesConfig cfg;
|
||||
const toml::array& arr = requireArray(tbl["recipe"], file, "recipe");
|
||||
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = "recipe[" + std::to_string(i) + "]";
|
||||
const toml::table* rt = arr[i].as_table();
|
||||
if (rt == nullptr)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*rt);
|
||||
|
||||
RecipeDef def;
|
||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||
def.durationSeconds = requireDouble(mt["duration_seconds"], file, elemPath + ".duration_seconds");
|
||||
|
||||
const std::string buildingId = requireString(mt["building"], file, elemPath + ".building");
|
||||
const std::optional<BuildingType> parsedType = parseBuildingType(buildingId);
|
||||
if (!parsedType)
|
||||
{
|
||||
throw makeError(file, elemPath + ".building",
|
||||
"unknown building id '" + buildingId + "'");
|
||||
}
|
||||
def.building = *parsedType;
|
||||
|
||||
// inputs may be omitted (e.g. miner recipes). An empty array is fine.
|
||||
if (mt.contains("inputs"))
|
||||
{
|
||||
const toml::array& inputs = requireArray(mt["inputs"], file, elemPath + ".inputs");
|
||||
def.inputs = parseIngredients(inputs, file, elemPath + ".inputs");
|
||||
}
|
||||
|
||||
const toml::array& outputs = requireArray(mt["outputs"], file, elemPath + ".outputs");
|
||||
def.outputs = parseRecipeOutputs(outputs, file, elemPath + ".outputs");
|
||||
|
||||
cfg.recipes.push_back(std::move(def));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
{
|
||||
const std::string file = "ships.toml";
|
||||
toml::table tbl = parseFile(path, file);
|
||||
|
||||
ShipsConfig cfg;
|
||||
const toml::array& arr = requireArray(tbl["ship"], file, "ship");
|
||||
|
||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
const std::string elemPath = "ship[" + std::to_string(i) + "]";
|
||||
const toml::table* st = arr[i].as_table();
|
||||
if (st == nullptr)
|
||||
{
|
||||
throw makeError(file, elemPath, "not a table");
|
||||
}
|
||||
toml::table& mt = const_cast<toml::table&>(*st);
|
||||
|
||||
ShipDef def;
|
||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||
def.availableFromStart = requireBool(mt["available_from_start"], file, elemPath + ".available_from_start");
|
||||
|
||||
// Blueprint
|
||||
{
|
||||
const std::string bpPath = elemPath + ".blueprint";
|
||||
const toml::table& bpTable = requireTable(mt["blueprint"], file, bpPath);
|
||||
toml::table& bpMt = const_cast<toml::table&>(bpTable);
|
||||
|
||||
const toml::array& materials = requireArray(bpMt["materials"], file, bpPath + ".materials");
|
||||
def.blueprint.materials = parseIngredients(materials, file, bpPath + ".materials");
|
||||
def.blueprint.playerProductionLevel = static_cast<int>(requireInt(
|
||||
bpMt["player_production_level"], file, bpPath + ".player_production_level"));
|
||||
}
|
||||
|
||||
// Threat
|
||||
{
|
||||
const std::string tPath = elemPath + ".threat";
|
||||
const toml::table& tTable = requireTable(mt["threat"], file, tPath);
|
||||
toml::table& tMt = const_cast<toml::table&>(tTable);
|
||||
def.threat.costFormula = requireFormula(tMt["cost_formula"], file, tPath + ".cost_formula");
|
||||
}
|
||||
|
||||
// Health
|
||||
{
|
||||
const std::string hPath = elemPath + ".health";
|
||||
const toml::table& hTable = requireTable(mt["health"], file, hPath);
|
||||
toml::table& hMt = const_cast<toml::table&>(hTable);
|
||||
def.health.hpFormula = requireFormula(hMt["hp_formula"], file, hPath + ".hp_formula");
|
||||
}
|
||||
|
||||
// Movement
|
||||
{
|
||||
const std::string mPath = elemPath + ".movement";
|
||||
const toml::table& mTable = requireTable(mt["movement"], file, mPath);
|
||||
toml::table& mMt = const_cast<toml::table&>(mTable);
|
||||
def.movement.speedFormula = requireFormula(mMt["speed_formula"], file, mPath + ".speed_formula");
|
||||
}
|
||||
|
||||
// Loot
|
||||
{
|
||||
const std::string lPath = elemPath + ".loot";
|
||||
const toml::table& lTable = requireTable(mt["loot"], file, lPath);
|
||||
toml::table& lMt = const_cast<toml::table&>(lTable);
|
||||
def.loot.scrapDrop = static_cast<int>(requireInt(lMt["scrap_drop"], file, lPath + ".scrap_drop"));
|
||||
}
|
||||
|
||||
// Optional: combat
|
||||
if (mt.contains("combat"))
|
||||
{
|
||||
const std::string cPath = elemPath + ".combat";
|
||||
const toml::table& cTable = requireTable(mt["combat"], file, cPath);
|
||||
toml::table& cMt = const_cast<toml::table&>(cTable);
|
||||
ShipCombat combat {
|
||||
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));
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
StationsConfig ConfigLoader::loadStations(const std::string& path)
|
||||
{
|
||||
const std::string file = "stations.toml";
|
||||
toml::table tbl = parseFile(path, file);
|
||||
|
||||
StationsConfig cfg;
|
||||
|
||||
// HQ
|
||||
{
|
||||
const std::string p = "hq";
|
||||
cfg.hq.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
||||
cfg.hq.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||
}
|
||||
|
||||
// Player station
|
||||
{
|
||||
const std::string p = "player_station";
|
||||
cfg.playerStation.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
||||
cfg.playerStation.level = static_cast<int>(requireInt(tbl[p]["level"], file, p + ".level"));
|
||||
cfg.playerStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||
cfg.playerStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
||||
cfg.playerStation.rangeFormula = requireFormula(tbl[p]["range_formula"], file, p + ".range_formula");
|
||||
cfg.playerStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_formula"], file, p + ".fire_rate_formula");
|
||||
cfg.playerStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
||||
}
|
||||
|
||||
// Enemy station
|
||||
{
|
||||
const std::string p = "enemy_station";
|
||||
cfg.enemyStation.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
||||
cfg.enemyStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||
cfg.enemyStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
||||
cfg.enemyStation.rangeFormula = requireFormula(tbl[p]["range_formula"], file, p + ".range_formula");
|
||||
cfg.enemyStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_formula"], file, p + ".fire_rate_formula");
|
||||
cfg.enemyStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
GameConfig ConfigLoader::loadFromDirectory(const std::string& configDir)
|
||||
{
|
||||
GameConfig cfg;
|
||||
cfg.world = loadWorld(configDir + "/world.toml");
|
||||
cfg.buildings = loadBuildings(configDir + "/buildings.toml");
|
||||
cfg.recipes = loadRecipes(configDir + "/recipes.toml");
|
||||
cfg.ships = loadShips(configDir + "/ships.toml");
|
||||
cfg.stations = loadStations(configDir + "/stations.toml");
|
||||
return cfg;
|
||||
}
|
||||
24
src/lib/config/ConfigLoader.h
Normal file
24
src/lib/config/ConfigLoader.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "GameConfig.h"
|
||||
|
||||
// Parses the five simulation TOML files from a directory and returns a fully
|
||||
// populated, immutable GameConfig. Throws std::runtime_error on any parse or
|
||||
// validation failure; the exception message identifies the offending file,
|
||||
// field, or formula (see architecture.md "Config Loading").
|
||||
//
|
||||
// Per-file helpers are exposed so tests can exercise individual loaders in
|
||||
// isolation.
|
||||
class ConfigLoader
|
||||
{
|
||||
public:
|
||||
static GameConfig loadFromDirectory(const std::string& configDir);
|
||||
|
||||
static WorldConfig loadWorld(const std::string& path);
|
||||
static BuildingsConfig loadBuildings(const std::string& path);
|
||||
static RecipesConfig loadRecipes(const std::string& path);
|
||||
static ShipsConfig loadShips(const std::string& path);
|
||||
static StationsConfig loadStations(const std::string& path);
|
||||
};
|
||||
68
src/lib/config/Formula.cpp
Normal file
68
src/lib/config/Formula.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include "Formula.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include "tinyexpr.h"
|
||||
|
||||
Formula::Formula(Formula&& other) noexcept
|
||||
: m_source(std::move(other.m_source))
|
||||
, m_x(std::move(other.m_x))
|
||||
, m_expr(other.m_expr)
|
||||
{
|
||||
other.m_expr = nullptr;
|
||||
}
|
||||
|
||||
Formula& Formula::operator=(Formula&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
te_free(m_expr);
|
||||
m_source = std::move(other.m_source);
|
||||
m_x = std::move(other.m_x);
|
||||
m_expr = other.m_expr;
|
||||
other.m_expr = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
Formula::~Formula()
|
||||
{
|
||||
te_free(m_expr);
|
||||
}
|
||||
|
||||
Formula Formula::compile(const std::string& source)
|
||||
{
|
||||
Formula result;
|
||||
result.m_source = source;
|
||||
result.m_x = std::make_unique<double>(0.0);
|
||||
|
||||
const te_variable variables[] = {
|
||||
{ "x", result.m_x.get(), 0, nullptr },
|
||||
};
|
||||
|
||||
int errorPos = 0;
|
||||
result.m_expr = te_compile(result.m_source.c_str(), variables, 1, &errorPos);
|
||||
|
||||
if (result.m_expr == nullptr)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Formula parse error at position " + std::to_string(errorPos)
|
||||
+ " in \"" + source + "\"");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
double Formula::evaluate(double x) const
|
||||
{
|
||||
if (m_expr == nullptr)
|
||||
{
|
||||
throw std::runtime_error("Formula::evaluate called on uninitialized formula");
|
||||
}
|
||||
|
||||
// The variable at *m_x is what the compiled tree dereferences during
|
||||
// te_eval. Mutating it here does not affect logical const-ness of the
|
||||
// formula — the formula itself is unchanged.
|
||||
*m_x = x;
|
||||
return te_eval(m_expr);
|
||||
}
|
||||
40
src/lib/config/Formula.h
Normal file
40
src/lib/config/Formula.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// Forward declaration so tinyexpr.h stays out of this header.
|
||||
struct te_expr;
|
||||
|
||||
// Compiled single-variable expression. The bound variable is named x.
|
||||
// Compile once at config load; evaluate many times at simulation time.
|
||||
// tinyexpr bakes the address of the bound variable into the compiled tree, so
|
||||
// we keep x on the heap to preserve that pointer across moves.
|
||||
class Formula
|
||||
{
|
||||
public:
|
||||
Formula() = default;
|
||||
|
||||
Formula(const Formula&) = delete;
|
||||
Formula& operator=(const Formula&) = delete;
|
||||
|
||||
Formula(Formula&& other) noexcept;
|
||||
Formula& operator=(Formula&& other) noexcept;
|
||||
|
||||
~Formula();
|
||||
|
||||
// Parses source and returns a ready-to-evaluate Formula.
|
||||
// Throws std::runtime_error with the source and error position on failure.
|
||||
static Formula compile(const std::string& source);
|
||||
|
||||
// Evaluates the expression at the given x. Requires a compiled formula.
|
||||
double evaluate(double x) const;
|
||||
|
||||
const std::string& source() const { return m_source; }
|
||||
bool isValid() const { return m_expr != nullptr; }
|
||||
|
||||
private:
|
||||
std::string m_source;
|
||||
std::unique_ptr<double> m_x;
|
||||
te_expr* m_expr = nullptr;
|
||||
};
|
||||
18
src/lib/config/GameConfig.h
Normal file
18
src/lib/config/GameConfig.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "WorldConfig.h"
|
||||
#include "BuildingsConfig.h"
|
||||
#include "RecipesConfig.h"
|
||||
#include "ShipsConfig.h"
|
||||
#include "StationsConfig.h"
|
||||
|
||||
// Aggregate of all five simulation config files, loaded once at startup and
|
||||
// immutable for the rest of the game. See architecture.md "Config Loading".
|
||||
struct GameConfig
|
||||
{
|
||||
WorldConfig world;
|
||||
BuildingsConfig buildings;
|
||||
RecipesConfig recipes;
|
||||
ShipsConfig ships;
|
||||
StationsConfig stations;
|
||||
};
|
||||
40
src/lib/config/RecipesConfig.h
Normal file
40
src/lib/config/RecipesConfig.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "BuildingType.h"
|
||||
|
||||
// One entry in [[recipe]].inputs — amount units of a named item consumed per
|
||||
// production cycle (REQ-MAT-CYCLE).
|
||||
struct RecipeIngredient
|
||||
{
|
||||
std::string item;
|
||||
int amount;
|
||||
};
|
||||
|
||||
// One entry in [[recipe]].outputs. For reprocessing_plant recipes, probability
|
||||
// is populated and outputs are rolled with weighted pick at cycle start
|
||||
// (REQ-BLD-REPROCESSING, REQ-MAT-CYCLE). For other buildings, probability is
|
||||
// std::nullopt and all outputs are produced on every cycle.
|
||||
struct RecipeOutput
|
||||
{
|
||||
std::string item;
|
||||
int amount;
|
||||
std::optional<double> probability;
|
||||
};
|
||||
|
||||
struct RecipeDef
|
||||
{
|
||||
std::string id; // Unique recipe id; used by UI for selection.
|
||||
BuildingType building; // Which BuildingType can run this recipe.
|
||||
std::vector<RecipeIngredient> inputs;
|
||||
std::vector<RecipeOutput> outputs;
|
||||
double durationSeconds;
|
||||
};
|
||||
|
||||
struct RecipesConfig
|
||||
{
|
||||
std::vector<RecipeDef> recipes;
|
||||
};
|
||||
84
src/lib/config/ShipsConfig.h
Normal file
84
src/lib/config/ShipsConfig.h
Normal file
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Formula.h"
|
||||
#include "RecipesConfig.h" // for RecipeIngredient
|
||||
|
||||
// Build materials and initial per-blueprint production level
|
||||
// (REQ-BLD-SHIPYARD, REQ-DEF-BLUEPRINT-DROP).
|
||||
struct ShipBlueprint
|
||||
{
|
||||
std::vector<RecipeIngredient> materials;
|
||||
int playerProductionLevel;
|
||||
};
|
||||
|
||||
// Wave scheduling cost (REQ-WAV-THREAT-COST). Ships with cost_formula that
|
||||
// always evaluates to 0 are ineligible as wave picks.
|
||||
struct ShipThreat
|
||||
{
|
||||
Formula costFormula;
|
||||
};
|
||||
|
||||
struct ShipHealth
|
||||
{
|
||||
Formula hpFormula; // REQ-SHP-STATS
|
||||
};
|
||||
|
||||
struct ShipMovement
|
||||
{
|
||||
Formula speedFormula; // REQ-SHP-STATS, REQ-SHP-MOVEMENT
|
||||
};
|
||||
|
||||
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).
|
||||
struct ShipLoot
|
||||
{
|
||||
int scrapDrop;
|
||||
};
|
||||
|
||||
struct ShipDef
|
||||
{
|
||||
std::string id;
|
||||
bool availableFromStart;
|
||||
|
||||
ShipBlueprint blueprint;
|
||||
ShipThreat threat;
|
||||
ShipHealth health;
|
||||
ShipMovement movement;
|
||||
ShipLoot loot;
|
||||
|
||||
// Role-specific sections. A ship is a combat ship if combat is present,
|
||||
// a salvage ship if salvage is present, etc. A ship may have multiple
|
||||
// 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
|
||||
{
|
||||
std::vector<ShipDef> ships;
|
||||
};
|
||||
45
src/lib/config/StationsConfig.h
Normal file
45
src/lib/config/StationsConfig.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Formula.h"
|
||||
|
||||
// REQ-HQ-STATS. HP may become a formula of player level later; for now the
|
||||
// requirement only lists hp so we store a formula that callers evaluate at 0.
|
||||
struct HqConfig
|
||||
{
|
||||
std::vector<std::string> surfaceMask;
|
||||
Formula hpFormula;
|
||||
};
|
||||
|
||||
// REQ-DEF-PLAYER-FIRE. Stats are formulas of a fixed station level.
|
||||
struct PlayerStationConfig
|
||||
{
|
||||
std::vector<std::string> surfaceMask;
|
||||
int level;
|
||||
Formula hpFormula;
|
||||
Formula damageFormula;
|
||||
Formula rangeFormula;
|
||||
Formula fireRateFormula; // shots per second
|
||||
Formula scrapDropFormula;
|
||||
};
|
||||
|
||||
// REQ-PSH-STATION-STATS. Stats are formulas of the station generation level,
|
||||
// which increments each time a new set is placed.
|
||||
struct EnemyStationConfig
|
||||
{
|
||||
std::vector<std::string> surfaceMask;
|
||||
Formula hpFormula;
|
||||
Formula damageFormula;
|
||||
Formula rangeFormula;
|
||||
Formula fireRateFormula;
|
||||
Formula scrapDropFormula;
|
||||
};
|
||||
|
||||
struct StationsConfig
|
||||
{
|
||||
HqConfig hq;
|
||||
PlayerStationConfig playerStation;
|
||||
EnemyStationConfig enemyStation;
|
||||
};
|
||||
48
src/lib/config/WorldConfig.h
Normal file
48
src/lib/config/WorldConfig.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "Formula.h"
|
||||
|
||||
// Region widths are in tiles (REQ-GW-REGIONS).
|
||||
struct WorldRegions
|
||||
{
|
||||
int asteroidWidth;
|
||||
int playerBufferWidth;
|
||||
int contestZoneWidth;
|
||||
int enemyBufferWidth;
|
||||
};
|
||||
|
||||
// Asteroid expansion (REQ-EXP-UNLOCK, REQ-EXP-COST).
|
||||
struct WorldExpansion
|
||||
{
|
||||
int columnsPerExpansion;
|
||||
int costBuildingBlocks;
|
||||
};
|
||||
|
||||
// Push scaling (REQ-PSH-*).
|
||||
struct WorldPush
|
||||
{
|
||||
int pushExpandColumns;
|
||||
double scalingFactor;
|
||||
};
|
||||
|
||||
// Wave scheduling (REQ-WAV-*).
|
||||
struct WorldWaves
|
||||
{
|
||||
Formula threatRateFormula; // threat/s as a function of elapsed game-time seconds
|
||||
Formula shipLevelFormula; // enemy ship level as a function of elapsed game-time seconds
|
||||
double gapMinSeconds;
|
||||
double gapMaxSeconds;
|
||||
double spawnDurationSeconds;
|
||||
};
|
||||
|
||||
struct WorldConfig
|
||||
{
|
||||
int heightTiles; // REQ-GW-HEIGHT
|
||||
int refundPercentage; // REQ-BLD-DEMOLISH
|
||||
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP
|
||||
|
||||
WorldRegions regions;
|
||||
WorldExpansion expansion;
|
||||
WorldPush push;
|
||||
WorldWaves waves;
|
||||
};
|
||||
14
src/lib/core/BlueprintDropEvent.h
Normal file
14
src/lib/core/BlueprintDropEvent.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
// Emitted in tick step 9 (Deaths & loot) when a destroyed enemy-defence-station
|
||||
// set awards a blueprint (REQ-DEF-BLUEPRINT-DROP). The UI renders a toast
|
||||
// (REQ-UI-BLUEPRINT-TOAST); wasNewUnlock chooses between the "unlocked" and
|
||||
// "level -> N" wording.
|
||||
struct BlueprintDropEvent
|
||||
{
|
||||
std::string blueprintId; // matches ShipDef::id in the config.
|
||||
int newLevel;
|
||||
bool wasNewUnlock;
|
||||
};
|
||||
36
src/lib/core/BuildingType.cpp
Normal file
36
src/lib/core/BuildingType.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#include "BuildingType.h"
|
||||
|
||||
std::optional<BuildingType> parseBuildingType(const std::string& id)
|
||||
{
|
||||
if (id == "miner") { return BuildingType::Miner; }
|
||||
if (id == "smelter") { return BuildingType::Smelter; }
|
||||
if (id == "assembler") { return BuildingType::Assembler; }
|
||||
if (id == "reprocessing_plant") { return BuildingType::ReprocessingPlant; }
|
||||
if (id == "shipyard") { return BuildingType::Shipyard; }
|
||||
if (id == "salvage_bay") { return BuildingType::SalvageBay; }
|
||||
if (id == "belt") { return BuildingType::Belt; }
|
||||
if (id == "splitter") { return BuildingType::Splitter; }
|
||||
if (id == "hq") { return BuildingType::Hq; }
|
||||
if (id == "player_defence_station") { return BuildingType::PlayerDefenceStation; }
|
||||
if (id == "enemy_defence_station") { return BuildingType::EnemyDefenceStation; }
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string buildingTypeId(BuildingType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case BuildingType::Miner: return "miner";
|
||||
case BuildingType::Smelter: return "smelter";
|
||||
case BuildingType::Assembler: return "assembler";
|
||||
case BuildingType::ReprocessingPlant: return "reprocessing_plant";
|
||||
case BuildingType::Shipyard: return "shipyard";
|
||||
case BuildingType::SalvageBay: return "salvage_bay";
|
||||
case BuildingType::Belt: return "belt";
|
||||
case BuildingType::Splitter: return "splitter";
|
||||
case BuildingType::Hq: return "hq";
|
||||
case BuildingType::PlayerDefenceStation: return "player_defence_station";
|
||||
case BuildingType::EnemyDefenceStation: return "enemy_defence_station";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
29
src/lib/core/BuildingType.h
Normal file
29
src/lib/core/BuildingType.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
// All building types defined in requirements.md. Belts and splitters share the
|
||||
// enum for cost/construction/placement/visuals lookup, but their runtime data
|
||||
// lives in the belt subsystem rather than in Building instances.
|
||||
enum class BuildingType
|
||||
{
|
||||
Miner,
|
||||
Smelter,
|
||||
Assembler,
|
||||
ReprocessingPlant,
|
||||
Shipyard,
|
||||
SalvageBay,
|
||||
Belt,
|
||||
Splitter,
|
||||
Hq,
|
||||
PlayerDefenceStation,
|
||||
EnemyDefenceStation,
|
||||
};
|
||||
|
||||
// Maps a config id string (e.g. "miner", "reprocessing_plant") to a
|
||||
// BuildingType. Returns std::nullopt for unknown ids.
|
||||
std::optional<BuildingType> parseBuildingType(const std::string& id);
|
||||
|
||||
// Canonical id string for a BuildingType. The inverse of parseBuildingType.
|
||||
std::string buildingTypeId(BuildingType type);
|
||||
26
src/lib/core/CMakeLists.txt
Normal file
26
src/lib/core/CMakeLists.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
SET(HDRS
|
||||
${HDRS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Tick.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityId.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Rotation.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Item.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Port.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FireEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintDropEvent.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
SET(SRCS
|
||||
${SRCS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
set(LIB_INCLUDE_PATH
|
||||
${LIB_INCLUDE_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PARENT_SCOPE
|
||||
)
|
||||
9
src/lib/core/EntityId.h
Normal file
9
src/lib/core/EntityId.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
// Canonical reference to every targetable entity in the simulation: ships,
|
||||
// scrap drops, and buildings (including HQ and defence stations).
|
||||
// Ids are allocated centrally by the Simulation, strictly increasing, never
|
||||
// reused. 0 is reserved as the invalid id.
|
||||
using EntityId = long long;
|
||||
|
||||
constexpr EntityId kInvalidEntityId = 0;
|
||||
14
src/lib/core/FireEvent.h
Normal file
14
src/lib/core/FireEvent.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "EntityId.h"
|
||||
#include "Tick.h"
|
||||
|
||||
// Transient record emitted each time a weapon fires (REQ-SHP-FIRING,
|
||||
// REQ-SHP-FIRING-BEAM). Buffered in a sim-owned queue and drained by the
|
||||
// renderer each frame to draw the 0.3-second laser beam.
|
||||
struct FireEvent
|
||||
{
|
||||
EntityId shooter;
|
||||
EntityId target;
|
||||
Tick emittedAt;
|
||||
};
|
||||
10
src/lib/core/Item.h
Normal file
10
src/lib/core/Item.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "ItemType.h"
|
||||
|
||||
// Items on belts have no persistent identity across ticks; see
|
||||
// architecture.md "Belt Subsystem".
|
||||
struct Item
|
||||
{
|
||||
ItemType type;
|
||||
};
|
||||
29
src/lib/core/ItemType.h
Normal file
29
src/lib/core/ItemType.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
// Opaque tagged id of every transportable material (ores, ingots,
|
||||
// intermediates, building_blocks, scrap). Defined in config; the simulation
|
||||
// does not enumerate item types.
|
||||
//
|
||||
// Wrapped in a struct (rather than a bare std::string typedef) so that function
|
||||
// signatures make the semantic intent explicit.
|
||||
struct ItemType
|
||||
{
|
||||
std::string id;
|
||||
};
|
||||
|
||||
inline bool operator==(const ItemType& a, const ItemType& b)
|
||||
{
|
||||
return a.id == b.id;
|
||||
}
|
||||
|
||||
inline bool operator!=(const ItemType& a, const ItemType& b)
|
||||
{
|
||||
return a.id != b.id;
|
||||
}
|
||||
|
||||
inline bool operator<(const ItemType& a, const ItemType& b)
|
||||
{
|
||||
return a.id < b.id;
|
||||
}
|
||||
12
src/lib/core/MovementIntent.h
Normal file
12
src/lib/core/MovementIntent.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
// A ship-behavior system writes this each tick before movement runs; the
|
||||
// highest-priority write wins. Priority order is fixed globally — see
|
||||
// architecture.md "Movement Arbitration".
|
||||
struct MovementIntent
|
||||
{
|
||||
int priority;
|
||||
QVector2D target;
|
||||
};
|
||||
13
src/lib/core/Port.h
Normal file
13
src/lib/core/Port.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPoint>
|
||||
|
||||
#include "Rotation.h"
|
||||
|
||||
// Identifies a belt-adjacent cell and the direction of flow across it. Used by
|
||||
// the belt subsystem's push/pull interface.
|
||||
struct Port
|
||||
{
|
||||
QPoint tile;
|
||||
Rotation direction;
|
||||
};
|
||||
12
src/lib/core/Rotation.h
Normal file
12
src/lib/core/Rotation.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
// Rotation applied to a building's surface_mask when placed. Also the direction
|
||||
// of an output port or belt flow.
|
||||
// North = -Y (up), East = +X (right), South = +Y (down), West = -X (left).
|
||||
enum class Rotation
|
||||
{
|
||||
North,
|
||||
East,
|
||||
South,
|
||||
West,
|
||||
};
|
||||
23
src/lib/core/Tick.h
Normal file
23
src/lib/core/Tick.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
// Discrete simulation time, measured in ticks since t=0.
|
||||
using Tick = long long;
|
||||
|
||||
// Fixed simulation tick rate. See architecture.md "Fixed-Timestep Tick-Based Simulation".
|
||||
constexpr int kTickRateHz = 30;
|
||||
constexpr double kTickDurationMs = 1000.0 / kTickRateHz;
|
||||
constexpr double kTickDurationSeconds = 1.0 / kTickRateHz;
|
||||
|
||||
// Converts a wall-clock duration (in seconds, as it appears in config TOML) to
|
||||
// an integer tick count. Rounds to nearest to avoid systematic drift from
|
||||
// repeated conversions.
|
||||
constexpr Tick secondsToTicks(double seconds)
|
||||
{
|
||||
return static_cast<Tick>(seconds * kTickRateHz + 0.5);
|
||||
}
|
||||
|
||||
// Inverse of secondsToTicks; useful for logging and UI display.
|
||||
constexpr double ticksToSeconds(Tick ticks)
|
||||
{
|
||||
return static_cast<double>(ticks) / kTickRateHz;
|
||||
}
|
||||
Reference in New Issue
Block a user