// WeaponDef loader on hand-written samples. #include "game/data/weapon.h" #include "test_main.h" using namespace game::data; namespace { const char* kGun = "weapon\n{\n" "\tname\t@WEAPON_TEST_GUN\n" "\tWeaponClass\tbullet\n" // key case must not matter "\tweaponfamily\tgauss\n" "\tRequires\tWEP_A\n" "\trequires\tWEP_B\n" "\tcost\t50\n" "\tturretsize\tsmall\n" "\tturretclass\tStandard\n" "\ttrackspeed_mod\t1.0\n" "\tburst_volleys\t3\n" "\trecharge_time\t2.5\n" "\tmuzzle_speed\t300\n" "\ticon_rect\t\"96 64 32 32\"\n" "\trange\t455\n" "\trange_planet\t1075\n" "\thidden\t1\n" "\tfc_requires_los\ttrue\n" "\tfc_controllable\tFALSE\n" "\tbolt\n\t{\n" "\t\tbeam_origin\t-0.5\n" "\t\tbeam_length\t1.5\n" "\t\timpact_effect\teffects/x_impact.effect\n" "\t\trangetable { pb_range 125 pb_range_dev 4 pb_range_dam 45\n" "\t\t eff_range 320 eff_range_dev 5.5 eff_range_dam 45\n" "\t\t max_range 455 max_range_dev 6 max_range_dam 40 }\n" "\t\teffect\teffects/x_bullet.effect\n" "\t\tricochet_mod\t-.8\n" "\t\tmass 100\n" "\t\tdam_pop\t3500\n" "\t\tdam_infra\t.00005\n" "\t\tdam_terra\t0\n" "\t}\n" "\tdam_est\t12\n" "\trating_frate\t7\n" "\trating_dam\t2.5\n" "}\n"; } // namespace TEST(weapon_typed_fields) { auto r = parse_weapon(kGun, "Weapons/test_gun.weapon"); CHECK(r.ok()); CHECK(r.problems.empty()); const WeaponDef& w = *r.value; CHECK_EQ(w.file, "Weapons/test_gun.weapon"); CHECK_EQ(w.name, "@WEAPON_TEST_GUN"); CHECK_EQ(w.weapon_class, "bullet"); CHECK_EQ(w.weapon_family, "gauss"); CHECK_EQ(w.requires_tech.size(), std::size_t{2}); CHECK_EQ(w.requires_tech[0], "WEP_A"); CHECK_EQ(w.requires_tech[1], "WEP_B"); CHECK(w.cost && *w.cost == 50); CHECK_EQ(w.turret_size, "small"); CHECK_EQ(w.turret_class, "Standard"); // kept as written CHECK(w.track_speed_mod && *w.track_speed_mod == 1.0); CHECK(w.burst_volleys && *w.burst_volleys == 3); CHECK(w.recharge_time && *w.recharge_time == 2.5); CHECK(w.muzzle_speed && *w.muzzle_speed == 300); CHECK_EQ(w.icon_rect, "96 64 32 32"); CHECK(w.range && *w.range == 455); CHECK(w.range_planet && *w.range_planet == 1075); CHECK(w.hidden && *w.hidden); // `hidden 1` reads as true CHECK(w.fc.requires_los && *w.fc.requires_los); CHECK(w.fc.controllable && !*w.fc.controllable); // FALSE any case CHECK(!w.fc.holdsfire); // absent -> nullopt CHECK(w.dam_est && *w.dam_est == 12); CHECK(w.ratings.fire_rate && *w.ratings.fire_rate == 7); CHECK(w.ratings.damage && *w.ratings.damage == 2.5); CHECK(!w.ratings.accuracy); CHECK_EQ(w.scope == WeaponScope::Player, true); CHECK(!w.id); } TEST(weapon_bolt_block) { auto r = parse_weapon(kGun); CHECK(r.ok()); const WeaponDef& w = *r.value; CHECK_EQ(w.behavior_kind, "bolt"); CHECK(w.behavior() != nullptr); CHECK(w.bolt.has_value()); const BoltDef& b = *w.bolt; CHECK(b.beam_origin && *b.beam_origin == -0.5); CHECK(b.beam_length && *b.beam_length == 1.5); CHECK(b.ricochet_mod && *b.ricochet_mod == -0.8); CHECK(b.mass && *b.mass == 100); CHECK_EQ(b.effect, "effects/x_bullet.effect"); CHECK_EQ(b.impact_effect, "effects/x_impact.effect"); CHECK_EQ(b.expire_effect, ""); CHECK(b.planet.pop && *b.planet.pop == 3500); CHECK(b.planet.infra && *b.planet.infra == 0.00005); CHECK(b.planet.terra && *b.planet.terra == 0); CHECK(b.rangetable.point_blank.range && *b.rangetable.point_blank.range == 125); CHECK(b.rangetable.effective.deviation && *b.rangetable.effective.deviation == 5.5); CHECK(b.rangetable.maximum.damage && *b.rangetable.maximum.damage == 40); // the weapon-level views mirror the bolt CHECK(w.rangetable && w.rangetable->maximum.range && *w.rangetable->maximum.range == 455); CHECK(w.planet_damage.pop && *w.planet_damage.pop == 3500); } TEST(weapon_torpedo_has_rangetable_but_no_bolt) { auto r = parse_weapon( "weapon { name @W weaponclass torpedo turretsize large turretclass torpedo\n" " torpedo { tracking 1 rangetable { pb_range 200 max_range 1400 } dam_pop 100000 dam_infra 0.0001 } }"); CHECK(r.ok()); const WeaponDef& w = *r.value; CHECK_EQ(w.behavior_kind, "torpedo"); CHECK(!w.bolt); CHECK(w.rangetable && w.rangetable->maximum.range && *w.rangetable->maximum.range == 1400); CHECK(!w.rangetable->effective.range); CHECK(w.planet_damage.pop && *w.planet_damage.pop == 100000); CHECK(!w.planet_damage.terra); CHECK(w.behavior()->find("tracking") != nullptr); } TEST(weapon_beam_without_rangetable) { auto r = parse_weapon("weapon { name @W weaponclass beam turretsize large turretclass beam beam { dam 40 dam_pop 1 } }"); CHECK(r.ok()); CHECK_EQ(r.value->behavior_kind, "beam"); CHECK(!r.value->rangetable); CHECK(!r.value->bolt); CHECK(r.value->planet_damage.pop.has_value()); } TEST(weapon_missing_keys_and_bad_values_are_problems) { auto r = parse_weapon("weapon { weaponclass bullet turretsize small turretclass standard cost 1.0f trackspeed_mod \"1\" }", "x.weapon"); CHECK(r.ok()); // still loads int missing = 0, bad = 0; for (const Problem& p : r.problems) { if (p.kind == Problem::Kind::MissingKey) ++missing; if (p.kind == Problem::Kind::BadValue) ++bad; CHECK_EQ(p.file, "x.weapon"); } CHECK_EQ(missing, 1); // name CHECK_EQ(bad, 2); // cost 1.0f, quoted trackspeed_mod CHECK(!r.value->cost); CHECK(!r.value->track_speed_mod); CHECK_EQ(r.value->raw.str("cost"), "1.0f"); // raw keeps the text } TEST(weapon_no_block_is_fatal) { auto r = parse_weapon("shipsection { model x }"); CHECK(!r.ok()); CHECK_EQ(r.problems.size(), std::size_t{1}); CHECK(r.problems[0].kind == Problem::Kind::MissingBlock); } TEST(weapon_syntax_error_is_fatal_and_recovery_is_reported) { // Engine parity: an unterminated quote is NOT fatal -- the string runs to EOF (swallowing the // '}') and EOF closes the block; the recovery is reported as a Syntax-kind problem. auto bad = parse_weapon("weapon { name \"unterminated }"); CHECK(bad.ok()); bool reported = false; for (const auto& p : bad.problems) if (p.kind == Problem::Kind::Syntax) reported = true; CHECK(reported); auto rec = parse_weapon("weapon { name @W weaponclass bullet turretsize small turretclass standard\n"); // EOF closes CHECK(rec.ok()); CHECK_EQ(rec.problems.size(), std::size_t{1}); CHECK(rec.problems[0].kind == Problem::Kind::Syntax); CHECK_EQ(rec.value->name, "@W"); } TEST(weapon_repeated_scalar_last_wins_and_lists_keep_order) { auto r = parse_weapon( "weapon { name @W weaponclass rider turretsize large turretclass dronerider cost 1 cost 2\n" " compatible_section _Drone compatible_section _DroneHeavy exclusive_species zuul }"); CHECK(r.ok()); CHECK(r.value->cost && *r.value->cost == 2); CHECK_EQ(r.value->compatible_section.size(), std::size_t{2}); CHECK_EQ(r.value->compatible_section[1], "_DroneHeavy"); CHECK_EQ(r.value->exclusive_species, "zuul"); CHECK_EQ(r.value->raw.all("cost").size(), std::size_t{2}); }