// 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 banks_of(const std::vector& v, const char* rule) { std::vector 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 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 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 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 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)), ""); }