182 lines
7.2 KiB
C++
182 lines
7.2 KiB
C++
// 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.size(), std::size_t{2});
|
|
CHECK_EQ(w.requires[0], "WEP_A");
|
|
CHECK_EQ(w.requires[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});
|
|
}
|