sots-engine/tests/game_design/mini_catalog.cpp

305 lines
17 KiB
C++

#include "mini_catalog.h"
#include <chrono>
#include <filesystem>
#include <fstream>
using namespace game::data;
using namespace game::design;
namespace fs = std::filesystem;
namespace {
void put(const fs::path& p, const std::string& text) {
fs::create_directories(p.parent_path());
std::ofstream(p, std::ios::binary) << text;
}
// --- sections ---------------------------------------------------------------
std::string bank(const char* cls, const char* size, int mounts, const char* fixed = nullptr) {
std::string s = " bank {";
if (fixed) s += std::string(" weapon \"") + fixed + "\"";
if (cls) s += std::string(" turretclass ") + cls;
if (size) s += std::string(" turretsize ") + size;
for (int i = 0; i < mounts; ++i) s += " mount { node N" + std::to_string(i) + " min_azimuth -90 max_azimuth 90 }";
return s + " }\n";
}
const char* kArmorOptions = " option { option IND_PlyAlloy option IND_MagLat option IND_QrkRes } option { option IND_RefCoat option IND_ImpRfCt }\n";
std::string de_command(const char* extra = "") {
return "shipsection { model a.X section_type command section_class destroyer socket_aft CommandNode\n"
" health 300 mass 800 cost 2000 cpoints 500 command_cost 1\n" + std::string(kArmorOptions) + extra +
bank("standard", "small", 1) + "}\n";
}
std::string de_armor(const char* extra = "") {
return "shipsection { model b.X section_type mission section_class destroyer socket_fore MissionNode01 socket_aft MissionNode\n"
" health 600 mass 2000 cost 4000 cpoints 1000\n" + std::string(kArmorOptions) +
" optiondef { option SLD_MkOne option SLD_MkTwo }\n" + extra +
bank("standard", "small", 3) + bank("standard", "small", 3) + bank("standard", "medium", 1) + "}\n";
}
std::string de_fission(const char* drive) {
return "shipsection { model c.X Section_Type Engine section_class Destroyer socket_fore EngineNode\n"
" requires DRV_Fissn requires " + std::string(drive) +
"\n health 450 mass 2500 cost 5000 cpoints 1380 engine_techera fission ftlspeed .2 nodespeed 4 range 9\n" +
std::string(kArmorOptions) + " option DRV_RecFiss\n"
" netforcelimits { force_forward 23000 force_right 23000 force_up 23000 torque_yaw 5 torque_pitch 5 torque_roll 5 speed 40 rotspeed 10 }\n" +
bank("standard", "small", 1) + "}\n";
}
std::string de_platform() {
return "shipsection { model d.X section_type mission section_class destroyer\n"
" health 1500 mass 5000 cost 6000 cpoints 900 defence_platform true scanrange 12 command_quota 20\n"
" ftlspeed 0 nodespeed 0\n" + std::string(kArmorOptions) +
bank("standard", "small", 2) + bank("standard", "small", 2) + bank("standard", "medium", 1) + "}\n";
}
const char* kRider = "shipsection { model r.X section_class destroyer design_class rider entity_class rider health 50 mass 100 cost 100 }\n";
const char* kRiderGated =
"shipsection { model r.X section_class destroyer design_class rider entity_class rider requires DRN_AdvFrm health 50 mass 100 cost 100 }\n";
void write_human(const fs::path& r) {
fs::path s = r / "Species/Human/sections";
put(s / "DECommand.shipsection", de_command());
put(s / "DEArmor.shipsection", de_armor());
put(s / "DEFission.shipsection", de_fission("DRV_Node"));
put(s / "DEHyperTest.shipsection", de_fission("DRV_Hyper")); // C5: Human can never research DRV_Hyper
put(s / "DEOddTech.shipsection", de_fission("XXX_Nowhere")); // C1 warn: unknown tech
put(s / "DEDefencePlatform.shipsection", de_platform());
put(s / "DERamScoop.shipsection", de_command(" exclude \"DEFission\" ramscoop true\n"));
put(s / "DEStrafe.shipsection",
"shipsection { model e.X section_type mission section_class destroyer socket_fore F socket_aft A health 1 mass 1 cost 1\n" +
bank("strafe", "small", 4) + bank("standard", "large", 1) + "}\n");
put(s / "DETorpedo.shipsection",
"shipsection { model e.X section_type mission section_class destroyer socket_fore F socket_aft A health 1 mass 1 cost 1\n"
" requires GRP_Torps\n" + bank("torpedo", "large", 1) + bank("missile", "medium", 1) + "}\n");
put(s / "DEAssaultShuttle.shipsection",
"shipsection { model e.X section_type mission section_class destroyer socket_fore F socket_aft A health 1 mass 1 cost 1\n"
" requires DRN_AdvFrm\n" + bank("assaultshuttlerider", "large", 1) + bank("tarkahunter", "large", 1) + "}\n");
put(s / "DEPlanetMissile.shipsection",
"shipsection { model e.X section_type mission section_class destroyer socket_fore F socket_aft A health 1 mass 1 cost 1\n" +
bank("planetmissile", "large", 1) + "}\n");
put(s / "CRCommand.shipsection",
"shipsection { model f.X section_type command section_class cruiser socket_aft CommandNode health 1 mass 10 cost 100 crew 5\n" +
bank("standard", "medium", 1) + "}\n");
put(s / "CRArmor.shipsection",
"shipsection { model f.X section_type mission section_class cruiser socket_fore F socket_aft A health 1 mass 20 cost 200 crew 10\n" +
bank("standard", "large", 2) + "}\n");
put(s / "CRFission.shipsection",
"shipsection { model f.X section_type engine section_class cruiser socket_fore EngineNode health 1 mass 30 cost 300 crew 15\n"
" requires DRV_Fissn requires DRV_Node engine_techera fission ftlspeed .2 nodespeed 4\n" +
bank("standard", "medium", 1) + "}\n");
// the station triple (design_class station: no hull-class tech; partners pinned)
put(s / "DNStation.shipsection",
"shipsection { model g.X section_type mission section_class dreadnought socket_fore Fore socket_aft Aft\n"
" requires IND_OrbCom health 9000 mass 100000 cost 700000 crew 100 entity_class \"Station\" design_class station command_quota 46\n"
" explicit_command_section \"DNStation_Fore\" explicit_engine_section \"DNStation_Aft\"\n" +
bank("standard", "large", 2) + "}\n");
put(s / "DNStation_Fore.shipsection",
"shipsection { model g.X section_type command section_class dreadnought socket_aft Aft explicit_section true\n"
" design_class station health 1 mass 1 cost 1\n" + bank("standard", "medium", 1) + "}\n");
put(s / "DNStation_Aft.shipsection",
"shipsection { model g.X section_type engine section_class dreadnought socket_fore Fore explicit_section true\n"
" design_class station health 1 mass 1 cost 1\n" + bank("standard", "medium", 1) + "}\n");
put(s / "DNCommand.shipsection",
"shipsection { model g.X section_type command section_class dreadnought socket_aft CommandNode health 1 mass 1 cost 1\n" +
bank("standard", "medium", 1) + "}\n");
put(s / "_AssaultShuttle.shipsection", kRider);
put(s / "_shipsections.txt",
"1 _AssaultShuttle.shipsection\n37 DEArmor.shipsection\n42 DECommand.shipsection\n47 DEFission.shipsection\n"
"50 DEDefencePlatform.shipsection\n51 DERamScoop.shipsection\n52 DEStrafe.shipsection\n53 DETorpedo.shipsection\n"
"54 DEAssaultShuttle.shipsection\n55 DEPlanetMissile.shipsection\n56 DEHyperTest.shipsection\n57 DEOddTech.shipsection\n"
"60 CRCommand.shipsection\n61 CRArmor.shipsection\n62 CRFission.shipsection\n"
"70 DNStation.shipsection\n71 DNStation_Fore.shipsection\n72 DNStation_Aft.shipsection\n73 DNCommand.shipsection\n");
}
void write_tarkas(const fs::path& r) {
fs::path s = r / "Species/Tarkas/sections";
put(s / "DECommand.shipsection", de_command());
put(s / "DEArmor.shipsection", de_armor());
put(s / "DEFission.shipsection", de_fission("DRV_Hyper"));
put(s / "DEDefencePlatform.shipsection", de_platform());
put(s / "_AssaultShuttle.shipsection", kRiderGated);
put(s / "_HunterShip.shipsection", kRider);
put(s / "_shipsections.txt",
"1 _AssaultShuttle.shipsection\n2 _HunterShip.shipsection\n37 DEArmor.shipsection\n42 DECommand.shipsection\n"
"47 DEFission.shipsection\n50 DEDefencePlatform.shipsection\n");
}
void write_zuul(const fs::path& r) {
fs::path s = r / "Species/Zuul/sections";
put(s / "DECommand.shipsection", de_command());
put(s / "DEArmor.shipsection", de_armor());
put(s / "DEFission.shipsection", de_fission("DRV_Node"));
put(s / "_shipsections.txt", "37 DEArmor.shipsection\n42 DECommand.shipsection\n47 DEFission.shipsection\n");
}
void write_npc(const fs::path& r) {
fs::path s = r / "Species/_NPC/sections";
// an NPC hull: typeless, standalone, fixed weapons in every bank, one bank without a turret spec
put(s / "_Herald.shipsection",
"shipsection { model h.X section_class cruiser health 5000 mass 20000 cost 1 crew false nodesign 1\n" +
bank("standard", "medium", 2, "Species/_NPC/weapons/Herald_gun.weapon") +
bank("strafe", "small", 1, "Species/_NPC/weapons/Herald_beam.weapon") +
bank(nullptr, nullptr, 1, "Species/_NPC/weapons/Herald_gun.weapon") + "}\n");
// an NPC destroyer triple (nodesign, typed, sockets present, player-style banks)
put(s / "_DECommand.shipsection", de_command(" nodesign 1\n"));
put(s / "_DEMission.shipsection", de_armor(" nodesign true\n"));
put(s / "_DEEngine.shipsection", de_fission("DRV_Node"));
put(s / "_shipsections.txt", "1 _Herald.shipsection\n2 _DECommand.shipsection\n3 _DEMission.shipsection\n4 _DEEngine.shipsection\n");
put(r / "Species/_NPC/weapons/Herald_gun.weapon",
"weapon { name @WEAPON_HERALD_GUN weaponclass bullet turretsize medium turretclass standard hidden 1 cost 7 }\n");
put(r / "Species/_NPC/weapons/Herald_beam.weapon",
"weapon { name @WEAPON_HERALD_BEAM weaponclass beam turretsize small turretclass standard cost 9 }\n");
}
// --- weapons -----------------------------------------------------------------
std::string weapon(const char* cls, const char* size, const char* tcls, const std::string& extra, int cost) {
return std::string("weapon { name @W weaponclass ") + cls + " turretsize " + size + " turretclass " + tcls + " cost " +
std::to_string(cost) + " " + extra + " }\n";
}
void write_weapons(const fs::path& r) {
fs::path w = r / "Weapons";
put(w / "bal_gauss.weapon", weapon("bullet", "small", "standard", "requires WEP_GsDrvr", 50));
put(w / "las_red.weapon", weapon("beam", "small", "standard", "requires WEP_RedLas", 60));
put(w / "las_pd.weapon", weapon("beam", "tiny", "standard", "requires WEP_PD", 40));
put(w / "can_plasma.weapon", weapon("bullet", "large", "standard", "requires WEP_Plasma", 400));
put(w / "mis.weapon", weapon("missile", "medium", "Missile", "", 3000));
put(w / "mis_defplat.weapon", weapon("missile", "medium", "PlanetMissile", "", 3500));
put(w / "mis_planet.weapon", weapon("missile", "medium", "PlanetMissile", "hidden 1", 1));
put(w / "trp_photon.weapon", weapon("torpedo", "large", "torpedo", "requires WEP_PhotTrp", 900));
put(w / "bal_grapple.weapon", weapon("grapple", "medium", "grapple", "requires WEP_Grapple exclusive_species zuul", 500));
put(w / "brd_shuttle.weapon", weapon("rider", "large", "assaultshuttlerider", "requires DRN_AdvFrm compatible_section _AssaultShuttle", 800));
put(w / "brd_tarkahunter.weapon",
weapon("rider", "large", "tarkahunter", "requires DRN_AdvFrm compatible_section _HunterShip compatible_section _HunterShip_B", 800));
put(w / "hvy_bem_lancer.weapon", weapon("beam", "large", "beam", "requires GRP_HvyBeam", 1200));
put(w / "_weapons.txt",
"8 bal_gauss.weapon\n9 las_red.weapon\n10 las_pd.weapon\n11 can_plasma.weapon\n21 mis.weapon\n22 mis_defplat.weapon\n"
"23 mis_planet.weapon\n30 trp_photon.weapon\n31 bal_grapple.weapon\n32 brd_shuttle.weapon\n33 brd_tarkahunter.weapon\n"
"34 hvy_bem_lancer.weapon\n");
put(w / "_turrets.txt",
"// size weapon-size class health track az inc model\n"
"small tiny standard 100 400 1 1 \"t_s_tiny.x\"\n"
"small small standard 120 320 1 1 \"t_s.x\"\n"
"medium small standard 300 200 1 1 \"t_m_s.x\"\n"
"medium medium standard 500 140 1 1 \"t_m.x\"\n"
"large small standard 600 200 1 1 \"t_l_s.x\"\n"
"large medium standard 700 140 1 1 \"t_l_m.x\"\n"
"Large large standard 900 80 1 1 \"t_l.x\"\n"
"medium medium Missile 400 100 1 0.25 \"turret_m1missile.x\"\n"
"large medium missile 450 100 1 0.25 \"turret_l1missile.x\"\n"
"large large missile 500 100 1 0.25 \"turret_l2missile.x\"\n"
"large medium planetmissile 500 100 1 0.25 \"turret_pm.x\"\n"
"large large torpedo 800 60 1 1 \"t_trp.x\"\n"
"large large beam 1500 80 1 1 \"\"\n"
"medium medium grapple 500 140 1 1 \"t_grap.x\"\n"
"large medium grapple 700 140 1 1 \"t_grap_l.x\"\n"
"small small strafe 120 320 1 1 \"t_s.x\"\n"
"medium medium strafe 500 140 1 1 \"t_m.x\"\n"
"large large assaultshuttlerider 900 80 1 1 \"t_rider.x\"\n"
"large large tarkahunter 900 80 1 1 \"t_rider.x\"\n");
put(w / "_defaultweapons.txt",
"// <mount-class> <mount-size> <weapon-file>\n"
"standard tiny \"las_pd.weapon\"\nstandard small \"las_red.weapon\"\nstandard medium \"las_red.weapon\"\n"
"standard large \"can_plasma.weapon\"\nmissile medium \"mis.weapon\"\nstrafe small \"las_red.weapon\"\n"
"torpedo large \"trp_photon.weapon\"\n");
}
// --- tech tree --------------------------------------------------------------
void write_tech(const fs::path& r) {
put(r / "TechTree/MasterTechList.tech",
"tech { name \"IND_ROOT\" allows \"IND_PlyAlloy RP:1\" allows \"IND_CruisCon RP:1\" allows \"IND_OrbCom RP:1\" allows \"IND_RefCoat RP:1\" }\n"
"tech { name \"IND_PlyAlloy\" option_cost 1.2 allows \"IND_MagLat RP:1\" }\n"
"tech { name \"IND_MagLat\" option_cost 1.4 allows \"IND_QrkRes RP:1\" }\n"
"tech { name \"IND_QrkRes\" option_cost 1.6 }\n"
"tech { name \"IND_RefCoat\" option_cost 1.2 allows \"IND_ImpRfCt RP:1\" }\n"
"tech { name \"IND_ImpRfCt\" option_cost 1.5 }\n"
"tech { name \"IND_CruisCon\" allows \"IND_DreadCon RP:1\" }\n"
"tech { name \"IND_DreadCon\" }\n"
"tech { name \"IND_OrbCom\" }\n"
"tech { name \"DRV_ROOT\" allows \"DRV_Fissn RP:1\" }\n"
"tech { name \"DRV_Fissn\" allows \"DRV_Node RP:1 Human:100 Hiver:0 Tarkas:0 Liir:0 Zuul:100 Morrigi:0\"\n"
" allows \"DRV_Hyper RP:1 Human:0 Hiver:0 Tarkas:100 Liir:0 Zuul:0 Morrigi:0\" allows \"DRV_RecFiss RP:1\" }\n"
"tech { name \"DRV_Node\" }\n"
"tech { name \"DRV_Hyper\" }\n"
"tech { name \"DRV_RecFiss\" option_cost 1.2 }\n"
"tech { name \"WEP_ROOT\" allows \"WEP_GsDrvr RP:1\" allows \"WEP_RedLas RP:1\" allows \"WEP_Plasma RP:1\" allows \"WEP_Grapple RP:1\" }\n"
"tech { name \"WEP_GsDrvr\" }\n"
"tech { name \"WEP_RedLas\" allows \"WEP_PD RP:1\" allows \"WEP_HvyLancer RP:1\" }\n"
"tech { name \"WEP_PD\" group PD }\n"
"tech { name \"WEP_Plasma\" }\n"
"tech { name \"WEP_Grapple\" }\n"
"tech { name \"WEP_HvyLancer\" group HvyBeam }\n"
"tech { name \"TRP_ROOT\" allows \"WEP_PhotTrp RP:1\" allows \"WEP_PlasTrp RP:1\" }\n"
"tech { name \"WEP_PhotTrp\" group TORPS }\n"
"tech { name \"WEP_PlasTrp\" group TORPS }\n"
"tech { name \"DRN_ROOT\" allows \"DRN_AdvFrm RP:1\" }\n"
"tech { name \"DRN_AdvFrm\" }\n"
"tech { name \"SLD_Root\" allows \"SLD_MkOne RP:1\" }\n"
"tech { name \"SLD_MkOne\" option_cost 1.4 allows \"SLD_MkTwo RP:1\" }\n"
"tech { name \"SLD_MkTwo\" option_cost 1.8 }\n");
}
fs::path make_root() {
auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
fs::path root = fs::temp_directory_path() / ("game_design_test_" + std::to_string(static_cast<long long>(stamp)));
fs::remove_all(root);
write_weapons(root);
write_human(root);
write_tarkas(root);
write_zuul(root);
write_npc(root);
write_tech(root);
return root;
}
} // namespace
const Catalog& mini_catalog() {
static const Catalog cat = [] {
fs::path root = make_root();
Catalog c = load_catalog(root);
fs::remove_all(root);
return c;
}();
return cat;
}
const Ruleset& mini_rules() {
static const Ruleset rules(mini_catalog());
return rules;
}
SectionUse use(std::string stem, std::vector<std::string> weapons, std::vector<std::string> options) {
SectionUse u;
u.section = std::move(stem);
for (std::string& w : weapons) u.weapons.push_back(w.empty() ? WeaponRef::none() : WeaponRef::by_stem(std::move(w)));
u.options = std::move(options);
return u;
}
Design armor(Species race) {
Design d;
d.name = "Armor";
d.race = race;
d.command() = use("DECommand", {"bal_gauss"});
d.mission() = use("DEArmor", {"bal_gauss", "bal_gauss", "mis"});
d.engine() = use("DEFission", {"bal_gauss"});
d.known_techs = std::vector<std::string>{"DRV_Fissn", race == Species::Tarkas ? "DRV_Hyper" : "DRV_Node", "WEP_GsDrvr"};
return d;
}
std::vector<std::string> rules_of(const Design& d, Level level) { return rules_at(mini_rules().validate(d), level); }
std::vector<std::string> error_rules(const Design& d) { return rules_of(d, Level::Error); }
std::string join(const std::vector<std::string>& v) {
std::string s;
for (const std::string& x : v) s += (s.empty() ? "" : ",") + x;
return s;
}