implement ui

This commit is contained in:
2026-04-20 20:33:37 +02:00
parent 498b97db20
commit 94123e93d6
19 changed files with 2312 additions and 19 deletions

231
src/ui/VisualsLoader.cpp Normal file
View File

@@ -0,0 +1,231 @@
#include "VisualsLoader.h"
#include <sstream>
#include <stdexcept>
#include <string>
#include <QColor>
#include "toml.hpp"
#include "BuildingType.h"
namespace
{
std::runtime_error makeError(const std::string& section, const std::string& why)
{
return std::runtime_error("visuals.toml: [" + section + "] " + why);
}
QColor parseColor(const std::string& s, const std::string& ctx)
{
if (s.empty() || s[0] != '#')
{
throw std::runtime_error("visuals.toml: invalid color '" + s + "' in " + ctx);
}
if (s.size() == 9)
{
bool ok1 = true, ok2 = true, ok3 = true, ok4 = true;
int r = QString::fromStdString(s.substr(1, 2)).toInt(&ok1, 16);
int g = QString::fromStdString(s.substr(3, 2)).toInt(&ok2, 16);
int b = QString::fromStdString(s.substr(5, 2)).toInt(&ok3, 16);
int a = QString::fromStdString(s.substr(7, 2)).toInt(&ok4, 16);
if (!ok1 || !ok2 || !ok3 || !ok4)
{
throw std::runtime_error("visuals.toml: malformed color '" + s + "' in " + ctx);
}
return QColor(r, g, b, a);
}
QColor c(QString::fromStdString(s));
if (!c.isValid())
{
throw std::runtime_error("visuals.toml: invalid color '" + s + "' in " + ctx);
}
return c;
}
std::string requireString(toml::table& tbl, const std::string& key,
const std::string& ctx)
{
const std::optional<std::string> v = tbl[key].value<std::string>();
if (!v)
{
throw makeError(ctx, "missing or invalid string '" + key + "'");
}
return *v;
}
int requireInt(toml::table& tbl, const std::string& key, const std::string& ctx)
{
const std::optional<int64_t> v = tbl[key].value<int64_t>();
if (!v)
{
throw makeError(ctx, "missing or invalid integer '" + key + "'");
}
return static_cast<int>(*v);
}
toml::table& requireSubtable(toml::table& tbl, const std::string& key,
const std::string& ctx)
{
toml::table* sub = tbl[key].as_table();
if (sub == nullptr)
{
throw makeError(ctx, "missing section '" + key + "'");
}
return *sub;
}
TileVisuals parseTile(toml::table& tbl, const std::string& ctx)
{
TileVisuals v;
v.fill = parseColor(requireString(tbl, "fill", ctx), ctx + ".fill");
return v;
}
BuildingVisuals parseBuilding(toml::table& tbl, const std::string& ctx)
{
BuildingVisuals v;
v.fill = parseColor(requireString(tbl, "fill", ctx), ctx + ".fill");
v.outline = parseColor(requireString(tbl, "outline", ctx), ctx + ".outline");
v.glyph = QString::fromStdString(requireString(tbl, "glyph", ctx));
return v;
}
ItemVisuals parseItem(toml::table& tbl, const std::string& ctx)
{
ItemVisuals v;
v.fill = parseColor(requireString(tbl, "fill", ctx), ctx + ".fill");
v.outline = parseColor(requireString(tbl, "outline", ctx), ctx + ".outline");
return v;
}
ShipVisuals parseShip(toml::table& tbl, const std::string& ctx)
{
ShipVisuals v;
v.fill = parseColor(requireString(tbl, "fill", ctx), ctx + ".fill");
v.outline = parseColor(requireString(tbl, "outline", ctx), ctx + ".outline");
return v;
}
struct BuildingEntry
{
const char* key;
BuildingType type;
};
const BuildingEntry kBuildingEntries[] = {
{ "hq", BuildingType::Hq },
{ "miner", BuildingType::Miner },
{ "smelter", BuildingType::Smelter },
{ "assembler", BuildingType::Assembler },
{ "reprocessing_plant",BuildingType::ReprocessingPlant },
{ "shipyard", BuildingType::Shipyard },
{ "salvage_bay", BuildingType::SalvageBay },
{ "belt", BuildingType::Belt },
{ "splitter", BuildingType::Splitter },
};
} // namespace
VisualsConfig VisualsLoader::load(const std::string& path)
{
toml::table tbl;
try
{
tbl = toml::parse_file(path);
}
catch (const toml::parse_error& e)
{
std::ostringstream oss;
oss << "visuals.toml: parse error: " << e.description()
<< " at " << e.source().begin;
throw std::runtime_error(oss.str());
}
VisualsConfig cfg;
// Tiles
{
toml::table& tiles = requireSubtable(tbl, "tiles", "root");
cfg.asteroid = parseTile(requireSubtable(tiles, "asteroid", "tiles"), "tiles.asteroid");
cfg.space = parseTile(requireSubtable(tiles, "space", "tiles"), "tiles.space");
}
// Buildings ([buildings.*] sections)
{
toml::table& bldgs = requireSubtable(tbl, "buildings", "root");
for (const BuildingEntry& entry : kBuildingEntries)
{
std::string ctx = std::string("buildings.") + entry.key;
cfg.buildings[entry.type] = parseBuilding(
requireSubtable(bldgs, entry.key, "buildings"), ctx);
}
}
// Stations ([stations.*] → mapped as PlayerDefenceStation / EnemyDefenceStation)
{
toml::table& stns = requireSubtable(tbl, "stations", "root");
cfg.buildings[BuildingType::PlayerDefenceStation] = parseBuilding(
requireSubtable(stns, "player", "stations"), "stations.player");
cfg.buildings[BuildingType::EnemyDefenceStation] = parseBuilding(
requireSubtable(stns, "enemy", "stations"), "stations.enemy");
}
// Items (iterate all keys in [items])
{
toml::table& items = requireSubtable(tbl, "items", "root");
for (toml::table::iterator it = items.begin(); it != items.end(); ++it)
{
std::string itemId = std::string(it->first.str());
toml::table* sub = it->second.as_table();
if (sub == nullptr)
{
throw std::runtime_error("visuals.toml: items." + itemId + " is not a table");
}
cfg.items[itemId] = parseItem(*sub, "items." + itemId);
}
}
// Ships
{
toml::table& ships = requireSubtable(tbl, "ships", "root");
cfg.ships[ShipRole::PlayerCombat] = parseShip(
requireSubtable(ships, "player_combat", "ships"), "ships.player_combat");
cfg.ships[ShipRole::Salvage] = parseShip(
requireSubtable(ships, "salvage", "ships"), "ships.salvage");
cfg.ships[ShipRole::Repair] = parseShip(
requireSubtable(ships, "repair", "ships"), "ships.repair");
cfg.ships[ShipRole::Enemy] = parseShip(
requireSubtable(ships, "enemy", "ships"), "ships.enemy");
}
// Beams
{
toml::table& beams = requireSubtable(tbl, "beams", "root");
cfg.beams.color = parseColor(requireString(beams, "color", "beams"), "beams.color");
cfg.beams.widthPx = requireInt(beams, "width_px", "beams");
}
// Overlays
{
toml::table& ov = requireSubtable(tbl, "overlays", "root");
cfg.overlays.ghostValid = parseColor(requireString(ov, "ghost_valid", "overlays"), "overlays.ghost_valid");
cfg.overlays.ghostInvalid = parseColor(requireString(ov, "ghost_invalid", "overlays"), "overlays.ghost_invalid");
cfg.overlays.demolishTint = parseColor(requireString(ov, "demolish_tint", "overlays"), "overlays.demolish_tint");
cfg.overlays.selectionRect = parseColor(requireString(ov, "selection_rect", "overlays"), "overlays.selection_rect");
cfg.overlays.tileHighlight = parseColor(requireString(ov, "tile_highlight", "overlays"), "overlays.tile_highlight");
cfg.overlays.selectedOutline = parseColor(requireString(ov, "selected_outline", "overlays"), "overlays.selected_outline");
}
// Toast
{
toml::table& t = requireSubtable(tbl, "toast", "root");
cfg.toast.bg = parseColor(requireString(t, "bg", "toast"), "toast.bg");
cfg.toast.fg = parseColor(requireString(t, "fg", "toast"), "toast.fg");
cfg.toast.fontSize = requireInt(t, "font_size", "toast");
}
return cfg;
}