sots-engine/tests/game_design/test_banks.cpp

193 lines
8 KiB
C++

// B* bank and weapon-fit rules on the mini catalog.
#include "game/design/rules.h"
#include "mini_catalog.h"
#include "test_main.h"
using namespace game::data;
using namespace game::design;
namespace {
Fit fit(const char* section, std::size_t bank, const char* weapon) {
const Catalog& c = mini_catalog();
return weapon_fits_bank(c.turrets, c.section("Human", section)->banks[bank], *c.weapon(weapon));
}
std::vector<int> banks_of(const std::vector<Violation>& v, const char* rule) {
std::vector<int> out;
for (const Violation& x : v)
if (x.rule == rule && x.bank) out.push_back(*x.bank);
return out;
}
} // namespace
TEST(B1_one_weapon_per_bank) {
Design d = armor();
d.mission() = use("DEArmor", {"bal_gauss", "bal_gauss"});
CHECK_EQ(join(error_rules(d)), "B1,B2");
d.mission() = use("DEArmor", {"bal_gauss", "bal_gauss", "mis", "mis"});
CHECK_EQ(join(error_rules(d)), "B1");
}
TEST(B2_no_empty_bank) {
Design d = armor();
d.mission() = use("DEArmor", {"bal_gauss", "", "mis"});
std::vector<Violation> v = mini_rules().validate(d);
CHECK_EQ(join(rules_at(v, Level::Error)), "B2");
CHECK_EQ(banks_of(v, "B2").size(), std::size_t(1));
CHECK_EQ(banks_of(v, "B2")[0], 1);
}
TEST(B3_fixed_npc_banks) {
Design n;
n.race = Species::NPC;
n.hidden = true;
n.mission() = use("_Herald", {"", "", ""});
CHECK_EQ(join(error_rules(n)), ""); // empty entries take the bank's fixed weapon
n.mission().weapons = {WeaponRef::by_file("Species/_NPC/weapons/Herald_gun.weapon"),
WeaponRef::by_file("Species/_NPC/weapons/Herald_beam.weapon"),
WeaponRef::by_file("Species/_NPC/weapons/Herald_gun.weapon")};
CHECK_EQ(join(error_rules(n)), ""); // the same weapon spelled out, as NPC designs store it
n.mission().weapons[0] = WeaponRef::by_stem("bal_gauss");
std::vector<Violation> v = mini_rules().validate(n);
CHECK_EQ(join(rules_at(v, Level::Error)), "B3");
CHECK_EQ(banks_of(v, "B3")[0], 0);
// the bank without a turret spec takes only its fixed weapon
n.mission().weapons[0] = WeaponRef::none();
n.mission().weapons[2] = WeaponRef::by_file("Species/_NPC/weapons/Herald_beam.weapon");
CHECK_EQ(join(error_rules(n)), "B3");
}
TEST(B4_class_accept_table) {
CHECK(bank_accepts_class("standard", "standard"));
CHECK(bank_accepts_class("Standard", "Missile"));
CHECK(bank_accepts_class("standard", "grapple"));
CHECK(!bank_accepts_class("standard", "torpedo"));
CHECK(bank_accepts_class("strafe", "standard"));
CHECK(!bank_accepts_class("strafe", "missile"));
CHECK(bank_accepts_class("torpedo", "Torpedo"));
CHECK(!bank_accepts_class("missile", "standard"));
CHECK(!bank_accepts_class("planetmissile", "missile"));
}
TEST(B4_turret_rows) {
// small standard bank: tiny and small weapons only
CHECK(fit("DECommand", 0, "las_pd").ok);
CHECK(fit("DECommand", 0, "bal_gauss").ok);
CHECK(!fit("DECommand", 0, "mis").ok); // medium missile in a small bank: no row
CHECK(!fit("DECommand", 0, "trp_photon").ok); // class
// medium standard bank: small + medium standard, medium missile (missile row supplies the model), grapple
CHECK(fit("DEArmor", 2, "bal_gauss").ok);
CHECK(!fit("DEArmor", 2, "las_pd").ok);
Fit m = fit("DEArmor", 2, "mis");
CHECK(m.ok);
CHECK(m.row != nullptr);
if (m.row) CHECK_EQ(m.row->model, "turret_m1missile.x");
CHECK(fit("DEArmor", 2, "bal_grapple").ok);
// strafe bank: standard weapons; the row is looked up by the weapon's class
// first, so the standard row is used and the strafe rows are the fallback
Fit s = fit("DEStrafe", 0, "bal_gauss");
CHECK(s.ok);
if (s.row) CHECK_EQ(s.row->turret_class, "standard");
CHECK(!fit("DEStrafe", 0, "mis").ok);
{
// fallback to the bank's class when the weapon-class row is missing
const Catalog& c = mini_catalog();
BankDef strafe_medium;
strafe_medium.turret_class = "strafe";
strafe_medium.turret_size = "medium";
WeaponDef odd = *c.weapon("bal_gauss");
odd.turret_size = "medium";
odd.turret_class = "standard";
const TurretRow* row = turret_row(c.turrets, strafe_medium, odd);
CHECK(row != nullptr);
if (row) CHECK_EQ(row->turret_class, "standard"); // (medium, medium, standard) exists
odd.turret_class = "nothing";
CHECK(turret_row(c.turrets, strafe_medium, odd) != nullptr); // (medium, medium, strafe) row
CHECK(!weapon_fits_bank(c.turrets, strafe_medium, odd).ok); // but the class table rejects it
}
// large standard: small/medium/large standard + missiles
CHECK(fit("DEStrafe", 1, "can_plasma").ok);
CHECK(fit("DEStrafe", 1, "mis").ok);
CHECK(!fit("DEStrafe", 1, "las_pd").ok);
CHECK(!fit("DEStrafe", 1, "trp_photon").ok);
// large torpedo: large torpedoes only
CHECK(fit("DETorpedo", 0, "trp_photon").ok);
CHECK(!fit("DETorpedo", 0, "can_plasma").ok);
// medium missile bank: missiles only
CHECK(fit("DETorpedo", 1, "mis").ok);
CHECK(!fit("DETorpedo", 1, "bal_gauss").ok);
// planetmissile: the planet missiles (medium PlanetMissile in a large bank)
CHECK(fit("DEPlanetMissile", 0, "mis_defplat").ok);
CHECK(!fit("DEPlanetMissile", 0, "mis").ok);
}
TEST(B4_in_a_design) {
Design d = armor();
d.known_techs.reset();
d.command() = use("DECommand", {"trp_photon"});
CHECK_EQ(join(error_rules(d)), "B4");
d = armor();
d.known_techs.reset();
d.mission() = use("DEArmor", {"las_pd", "las_pd", "las_pd"});
std::vector<Violation> v = mini_rules().validate(d);
CHECK_EQ(join(rules_at(v, Level::Error)), "B4");
CHECK_EQ(banks_of(v, "B4").size(), std::size_t(1));
CHECK_EQ(banks_of(v, "B4")[0], 2);
d.mission() = use("DEArmor", {"mis", "bal_gauss", "mis"});
CHECK_EQ(join(error_rules(d)), "B4");
}
TEST(B5_weapon_must_exist_and_be_in_scope) {
Design d = armor();
d.command() = use("DECommand", {"bal_nothing"});
CHECK_EQ(join(error_rules(d)), "B5");
d.command().weapons = {WeaponRef::by_id(4242)};
CHECK_EQ(join(error_rules(d)), "B5");
d.command().weapons = {WeaponRef::by_file("Species/_NPC/weapons/Herald_beam.weapon")};
CHECK_EQ(join(error_rules(d)), "B5"); // NPC-only weapon on a player hull
Design n;
n.race = Species::NPC;
n.command() = use("_DECommand", {"Species/_NPC/weapons/Herald_beam.weapon"});
n.mission() = use("_DEMission", {"bal_gauss", "bal_gauss", "mis"});
n.engine() = use("_DEEngine", {"bal_gauss"});
CHECK_EQ(join(error_rules(n)), ""); // fine on an NPC hull
}
TEST(B6_exclusive_species) {
Design d = armor();
d.known_techs.reset();
d.mission() = use("DEArmor", {"bal_gauss", "bal_gauss", "bal_grapple"});
CHECK_EQ(join(error_rules(d)), "B6");
Design z = armor(Species::Zuul);
z.known_techs.reset();
z.mission() = use("DEArmor", {"bal_gauss", "bal_gauss", "bal_grapple"});
CHECK_EQ(join(error_rules(z)), "");
}
TEST(B7_rider_weapons_need_their_rider_section) {
Design d = armor();
d.known_techs.reset();
d.mission() = use("DEAssaultShuttle", {"brd_shuttle", "brd_tarkahunter"});
std::vector<Violation> v = mini_rules().validate(d);
CHECK_EQ(join(rules_at(v, Level::Error)), "B7"); // Human has _AssaultShuttle but no _HunterShip
CHECK_EQ(banks_of(v, "B7")[0], 1);
Design t = armor(Species::Tarkas);
t.known_techs.reset();
t.mission() = use("DEArmor", {"bal_gauss", "bal_gauss", "mis"});
// Tarkas mini catalog has both riders but no DEAssaultShuttle section; check the weapon rule directly
CHECK(mini_catalog().section("Tarkas", "_HunterShip") != nullptr);
}
TEST(B8_hidden_weapons_are_engine_only) {
Design d = armor();
d.known_techs.reset();
d.mission() = use("DEPlanetMissile", {"mis_planet"});
CHECK_EQ(join(error_rules(d)), "B8");
d.hidden = true;
CHECK_EQ(join(error_rules(d)), "");
d.hidden = false;
d.mission() = use("DEPlanetMissile", {"mis_defplat"});
CHECK_EQ(join(error_rules(d)), "");
}