Files
西风逍遥游 fa18e77b75 weapon: multi-form weapon system (kind/resource/stage) + P0 combat features
Generalize the weapon module from gun-only into a data-driven, cuttable
multi-form system supporting hot / melee / magic / missile weapons for
RPG / RTS / ARPG.

Core (WeaponTypes.h / WeaponSystem.cpp):
- WeaponKind (melee/ranged/magic/missile/custom) selects the attack form.
- AttackResource gates triggers by ammo/mana/charges/stamina/none.
- AttackStage machine (Idle->Windup->Active->Recover) replaces the hard-coded
  reload/burst path for non-ranged forms; hot weapons keep the mature pipeline
  so ranged behavior is byte-identical (regression baseline).
- IWeaponLogic gains begin/channel/end hooks (default no-op, backward compat).
- AttackRequest generalizes FireRequest (target handle, arc, aoe).
- WeaponRigEntity for hand-held weapons; WeaponMountEntity (turret) unchanged.

P0 combat features (data fields + event payloads, no arch change):
- Multi-pellet (shotgun), spread bloom, recoil model, runtime fire selector
  (safe/semi/auto), shared ammo pool (AmmoPoolEntity), damage type/element,
  ADS/zoom with aim_in/aim_out events.

Docs: 通用武器系统设计.md, 武器系统-对比Unity插件-差距分析.md.
Tests: test/weapon.cpp grows to 25 cases / 210 assertions, all passing.
2026-08-25 22:15:01 +08:00

581 lines
20 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "ScriptTest.h"
#include "common/Capability.h"
#include "common/ECS.h"
#include "weapon/Weapon.h"
#include "weapon/WeaponLogic.h"
using namespace eve::weapon;
namespace {
const char* kDefs = R"JSON(
[{"id":"rifle","logic":"hitscan","damage":25,"range":500,"spread":0.5,
"fireMode":"single","cooldown":0.2,
"ammo":{"mag":30,"reserve":90,"reload":2.0}},
{"id":"cannon","logic":"projectile","damage":320,"penetration":260,"range":1200,
"fireMode":"single","cooldown":4.0,
"ammo":{"mag":1,"reserve":40,"reload":6.0},
"projectile":{"type":"shell","speed":900,"gravity":1.0,"aoe":3.0}},
{"id":"smg","logic":"hitscan","fireMode":"burst","cooldown":0.05,
"burst":{"size":3,"interval":0.06},
"ammo":{"mag":30,"reserve":-1,"reload":1.0}},
{"id":"noreload","logic":"hitscan","ammo":{"mag":5,"reserve":0,"reload":1.0}}]
)JSON";
// 近战 / 阶段机测试定义。
const char* kMeleeDefs = R"JSON(
[{"id":"sword","kind":"melee","logic":"melee","damage":40,"range":1.6,
"stages":{"windup":0.25,"active":0.10,"recover":0.35},"arc":90,
"resource":{"kind":"none"}},
{"id":"cleaver","kind":"melee","logic":"melee","damage":15,"range":1.2,
"stages":{"windup":0.1,"active":0.05,"recover":0.1},"arc":60,
"resource":{"kind":"stamina","max":10,"cost":3}}]
)JSON";
// P0 功能测试定义(多弹丸 / 散布 bloom / 后坐 / 射击模式切换 / 开镜 / 伤害类型 / 弹药池)。
const char* kP0Defs = R"JSON(
[{"id":"shotgun","logic":"projectile","damage":10,"cooldown":0.2,
"ammo":{"mag":2,"reserve":0,"reload":1},
"projectile":{"type":"pellet","speed":500,"pelletCount":4,"pelletSpread":3}},
{"id":"bloom","logic":"hitscan","cooldown":0,
"spreadMin":1,"spreadMax":10,"spreadPerShot":3,"spreadRecover":5,
"ammo":{"mag":50,"reserve":-1}},
{"id":"recoil","logic":"hitscan","cooldown":0,"recoilPitch":2,"recoilYaw":-1,"recoilRecover":1,
"ammo":{"mag":50,"reserve":-1}},
{"id":"selector","logic":"hitscan","cooldown":0,"fireMode":"auto","fireModes":["single","burst"],
"ammo":{"mag":50,"reserve":-1}},
{"id":"fixedsel","logic":"hitscan","cooldown":0,"fireMode":"single","ammo":{"mag":50,"reserve":-1}},
{"id":"aim","logic":"hitscan","cooldown":0,"zoomFov":40,"ammo":{"mag":50,"reserve":-1}},
{"id":"typed","logic":"hitscan","cooldown":0,"damageType":"pierce","element":"fire",
"ammo":{"mag":50,"reserve":-1}},
{"id":"poolgun","logic":"hitscan","cooldown":0,"ammo":{"mag":2,"reserve":0,"reload":0.5}}]
)JSON";
int countWeaponView() {
int n = 0;
auto view = ecs::View<WeaponEntity, WeaponEntity::Identity>();
for (auto it = view.begin(); it != view.end(); ++it) ++n;
return n;
}
class StubProjectileService : public IProjectileService {
public:
int spawns = 0;
void spawnProjectile(const WeaponEntity&, const FireRequest&) override { ++spawns; }
};
class CountingLogic : public IWeaponLogic {
public:
int fires = 0;
const char* name() const override { return "test.counting"; }
bool canFire(const WeaponEntity&) const override { return true; }
void fire(WeaponEntity&, const AttackRequest&) override { ++fires; }
void update(WeaponEntity&, float) override {}
};
class HookLogic : public IWeaponLogic {
public:
int begins = 0, channels = 0, fires = 0, ends = 0;
const char* name() const override { return "test.hook"; }
bool canFire(const WeaponEntity&) const override { return true; }
void fire(WeaponEntity&, const AttackRequest&) override { ++fires; }
void update(WeaponEntity&, float) override {}
void begin(WeaponEntity&, const AttackRequest&) override { ++begins; }
void channel(WeaponEntity&, const AttackRequest&, float) override { ++channels; }
void end(WeaponEntity&, const AttackRequest&) override { ++ends; }
};
} // namespace
TEST_CASE("weapon.defs.registerJson") {
Weapon mod;
CHECK_EQ(mod.registerWeaponsFromJson(kDefs), 4);
CHECK(mod.hasWeaponDefinition("cannon"));
CHECK_EQ(mod.getWeaponDefinitionLogic("cannon"), std::string("projectile"));
CHECK_EQ(mod.getWeaponDefinitionDamage("cannon"), 320.f);
CHECK_EQ(mod.getWeaponDefinitionRange("cannon"), 1200.f);
CHECK_EQ(mod.registerWeaponsFromJson("[{\"name\":\"no-id\"}]"), 0);
CHECK_EQ(mod.registerWeaponsFromJson("not json"), 0);
CHECK_EQ(mod.getWeaponDefinitionLogic("missing"), std::string{});
}
TEST_CASE("weapon.kind.parse") {
CHECK_EQ(weaponKindName(WeaponKind::Melee), std::string("melee"));
CHECK_EQ(weaponKindName(WeaponKind::Ranged), std::string("ranged"));
CHECK_EQ(weaponKindName(WeaponKind::Magic), std::string("magic"));
CHECK_EQ(weaponKindName(WeaponKind::Missile), std::string("missile"));
CHECK((weaponKindFromName("melee") == WeaponKind::Melee));
CHECK((weaponKindFromName("ranged") == WeaponKind::Ranged));
CHECK((weaponKindFromName("nonsense") == WeaponKind::Ranged));
Weapon mod;
mod.registerWeaponsFromJson(R"JSON([{"id":"sw","kind":"melee","logic":"melee"}])JSON");
WeaponEntity* w = mod.newWeapon("sw");
REQUIRE(w != nullptr);
CHECK((w->definition()->def->kind == WeaponKind::Melee));
CHECK((w->state()->resource.kind == ResourceKind::None));
CHECK(w->state()->resource.infinite);
}
TEST_CASE("weapon.factory.newWeapon") {
Weapon mod;
mod.registerWeaponsFromJson(kDefs);
const int before = countWeaponView();
WeaponEntity* w = mod.newWeapon("cannon");
REQUIRE(w != nullptr);
CHECK_EQ(w->state()->resource.value, 1.f);
CHECK_EQ(w->state()->resource.reserve, 40);
CHECK_EQ(w->identity()->defId, std::string("cannon"));
CHECK_EQ(mod.newWeapon("missing"), nullptr);
CHECK_EQ(countWeaponView(), before + 1);
}
TEST_CASE("weapon.fire.cooldownAndAmmo") {
Weapon mod;
mod.registerWeaponsFromJson(kDefs);
WeaponEntity* w = mod.newWeapon("rifle");
REQUIRE(w != nullptr);
mod.clearEvents();
CHECK(mod.fireAt(w, 100.f, 0.f, 0.f));
CHECK_EQ(w->state()->resource.value, 29.f);
CHECK_EQ(mod.getEventCount(), 1);
CHECK_EQ(mod.getEventType(0), std::string("fire"));
CHECK(!mod.fireAt(w, 100.f, 0.f, 0.f)); // 冷却中
mod.update(0.1f);
CHECK(!mod.fireAt(w, 100.f, 0.f, 0.f));
mod.update(0.15f); // 累计 0.25s > cooldown 0.2s
CHECK(mod.fireAt(w, 100.f, 0.f, 0.f));
CHECK_EQ(w->state()->resource.value, 28.f);
}
TEST_CASE("weapon.fire.emptyAndReload") {
Weapon mod;
mod.registerWeaponsFromJson(kDefs);
WeaponEntity* w = mod.newWeapon("cannon");
REQUIRE(w != nullptr);
mod.clearEvents();
CHECK(mod.fireAt(w, 0.f, 0.f, 0.f));
CHECK_EQ(w->state()->resource.value, 0.f);
CHECK_EQ(mod.getEventCount(), 2);
CHECK_EQ(mod.getEventType(0), std::string("fire"));
CHECK_EQ(mod.getEventType(1), std::string("empty"));
mod.clearEvents();
mod.startReload(w);
CHECK(w->state()->resource.reloading);
CHECK_EQ(mod.getEventCount(), 1);
CHECK_EQ(mod.getEventType(0), std::string("reload_start"));
mod.update(3.0f);
CHECK(w->state()->resource.reloading);
mod.update(3.0f); // 累计 6s = reloadTime
CHECK(!w->state()->resource.reloading);
CHECK_EQ(w->state()->resource.value, 1.f);
CHECK_EQ(w->state()->resource.reserve, 39);
CHECK_EQ(mod.getEventCount(), 2);
CHECK_EQ(mod.getEventType(1), std::string("reload_end"));
}
TEST_CASE("weapon.fire.noReserveCannotReload") {
Weapon mod;
mod.registerWeaponsFromJson(kDefs);
WeaponEntity* w = mod.newWeapon("noreload");
REQUIRE(w != nullptr);
for (int i = 0; i < 5; ++i) CHECK(mod.fireAt(w, 0.f, 0.f, 0.f));
CHECK_EQ(w->state()->resource.value, 0.f);
mod.clearEvents();
mod.startReload(w);
CHECK(!w->state()->resource.reloading);
CHECK_EQ(mod.getEventCount(), 0);
}
TEST_CASE("weapon.fire.autoReloadOnEmpty") {
Weapon mod;
mod.registerWeaponsFromJson(kDefs);
WeaponEntity* w = mod.newWeapon("cannon");
REQUIRE(w != nullptr);
mod.clearEvents();
// 打空弹匣:冷却结束 + 有备弹时,update 应自动开始装填(无需游戏侧手动调用)。
CHECK(mod.fireAt(w, 0.f, 0.f, 0.f));
CHECK_EQ(w->state()->resource.value, 0.f);
mod.update(4.0f); // cooldown 4.0s 结束 → 自动装填开始
CHECK(w->state()->resource.reloading);
// fireAt 已推 fire+empty 两条,自动装填再推 reload_start。
CHECK_EQ(mod.getEventType(2), std::string("reload_start"));
mod.update(1.0f);
CHECK(w->state()->resource.reloading);
mod.update(1.0f); // 累计 4+1+1 = 6s = reloadTime
CHECK(!w->state()->resource.reloading);
CHECK_EQ(w->state()->resource.value, 1.f);
CHECK_EQ(w->state()->resource.reserve, 39);
CHECK_EQ(mod.getEventType(3), std::string("reload_end"));
// 无备弹的武器不会被自动装填反复触发事件。
mod.clearEvents();
WeaponEntity* nr = mod.newWeapon("noreload");
REQUIRE(nr != nullptr);
for (int i = 0; i < 5; ++i) CHECK(mod.fireAt(nr, 0.f, 0.f, 0.f));
mod.update(2.0f);
CHECK(!nr->state()->resource.reloading);
CHECK_EQ(mod.getEventCount(), 6); // 5 次 fire + 1 次 empty(打空时),无 reload 事件
}
TEST_CASE("weapon.fire.burst") {
Weapon mod;
mod.registerWeaponsFromJson(kDefs);
WeaponEntity* w = mod.newWeapon("smg");
REQUIRE(w != nullptr);
mod.clearEvents();
CHECK(mod.fireAt(w, 0.f, 0.f, 0.f));
CHECK_EQ(w->state()->resource.value, 29.f);
mod.update(0.07f); // 续发 2
mod.update(0.07f); // 续发 3
CHECK_EQ(w->state()->resource.value, 27.f);
CHECK_EQ(mod.getEventCount(), 3);
for (int i = 0; i < 3; ++i) CHECK_EQ(mod.getEventType(i), std::string("fire"));
}
TEST_CASE("weapon.logic.customRegistration") {
CountingLogic logic;
Weapon::registerLogic(&logic);
CHECK(Weapon::getLogicCount() >= 3);
Weapon mod;
CHECK_EQ(mod.registerWeaponsFromJson("[{\"id\":\"count\",\"logic\":\"test.counting\"}]"), 1);
WeaponEntity* w = mod.newWeapon("count");
REQUIRE(w != nullptr);
CHECK(mod.fireAt(w, 0.f, 0.f, 0.f));
CHECK_EQ(logic.fires, 1);
}
TEST_CASE("weapon.projectile.usesService") {
StubProjectileService svc;
eve::cap::provide<IProjectileService>(&svc);
Weapon mod;
mod.registerWeaponsFromJson(kDefs);
WeaponEntity* cannon = mod.newWeapon("cannon");
WeaponEntity* rifle = mod.newWeapon("rifle");
REQUIRE(cannon != nullptr);
REQUIRE(rifle != nullptr);
CHECK(mod.fireAt(cannon, 0.f, 0.f, 0.f));
CHECK_EQ(svc.spawns, 1);
CHECK(mod.fireAt(rifle, 0.f, 0.f, 0.f));
CHECK_EQ(svc.spawns, 1); // 直射不生成投射物
eve::cap::revoke<IProjectileService>(&svc);
}
// ---------------------------------------------------------------------------
// v2:近战 / 阶段机 / 资源 / 手持位
// ---------------------------------------------------------------------------
TEST_CASE("weapon.melee.stageMachine") {
Weapon mod;
mod.registerWeaponsFromJson(kMeleeDefs);
WeaponEntity* w = mod.newWeapon("sword");
REQUIRE(w != nullptr);
mod.clearEvents();
CHECK(mod.attack(w, 0.f, 0.f));
CHECK_EQ(mod.getStage(w), std::string("windup"));
CHECK_EQ(mod.getEventCount(), 1);
CHECK_EQ(mod.getEventType(0), std::string("windup_start"));
CHECK(!mod.canFire(w)); // 阶段中不可再触发
mod.update(0.3f); // windup 0.25s 结束 → Active,出 fire 事件
CHECK_EQ(mod.getStage(w), std::string("active"));
CHECK_EQ(mod.getEventCount(), 2);
CHECK_EQ(mod.getEventType(1), std::string("fire"));
CHECK_EQ(mod.getEventArc(1), 90.f);
mod.update(0.2f); // active 0.10s 结束 → Recover
CHECK_EQ(mod.getStage(w), std::string("recover"));
CHECK_EQ(mod.getEventCount(), 3);
CHECK_EQ(mod.getEventType(2), std::string("attack_end"));
mod.update(0.4f); // recover 0.35s 结束 → Idle
CHECK_EQ(mod.getStage(w), std::string("idle"));
CHECK(mod.canFire(w));
}
TEST_CASE("weapon.melee.infiniteNoCost") {
Weapon mod;
mod.registerWeaponsFromJson(kMeleeDefs);
WeaponEntity* w = mod.newWeapon("sword");
REQUIRE(w != nullptr);
CHECK(w->state()->resource.infinite);
CHECK(mod.attack(w, 0.f, 0.f));
mod.update(0.3f); // windup→active
mod.update(0.2f); // active→recover
mod.update(0.4f); // recover→idle
CHECK(mod.attack(w, 0.f, 0.f));
mod.update(0.3f);
mod.update(0.2f);
mod.update(0.4f);
CHECK(mod.attack(w, 0.f, 0.f)); // 无消耗,第三次仍可攻击
}
TEST_CASE("weapon.melee.staminaCost") {
Weapon mod;
mod.registerWeaponsFromJson(kMeleeDefs);
WeaponEntity* w = mod.newWeapon("cleaver");
REQUIRE(w != nullptr);
CHECK_EQ(w->state()->resource.value, 10.f);
// cleaver 阶段:windup 0.1 / active 0.05 / recover 0.1
CHECK(mod.attack(w, 0.f, 0.f));
CHECK_EQ(w->state()->resource.value, 7.f);
mod.update(0.15f);
mod.update(0.1f);
mod.update(0.15f);
CHECK(mod.attack(w, 0.f, 0.f));
CHECK_EQ(w->state()->resource.value, 4.f);
mod.update(0.15f);
mod.update(0.1f);
mod.update(0.15f);
CHECK(mod.attack(w, 0.f, 0.f));
CHECK_EQ(w->state()->resource.value, 1.f);
mod.update(0.15f);
mod.update(0.1f);
mod.update(0.15f);
CHECK(!mod.attack(w, 0.f, 0.f)); // 体力不足(1 < cost 3)
CHECK_EQ(w->state()->resource.value, 1.f);
CHECK(!mod.canFire(w));
}
TEST_CASE("weapon.stage.hooks") {
HookLogic logic;
Weapon::registerLogic(&logic);
Weapon mod;
mod.registerWeaponsFromJson(R"JSON(
[{"id":"dagger","kind":"melee","logic":"test.hook","damage":8,
"stages":{"windup":0.2,"active":0.3,"recover":0.1}}])JSON");
WeaponEntity* w = mod.newWeapon("dagger");
REQUIRE(w != nullptr);
CHECK(mod.attack(w, 0.f, 0.f));
CHECK_EQ(logic.begins, 1);
mod.update(0.2f); // windup→Active:fire 一次
CHECK_EQ(logic.fires, 1);
mod.update(0.3f); // Active→Recover:channel ≥1,end 一次
CHECK(logic.channels >= 1);
CHECK_EQ(logic.ends, 1);
}
TEST_CASE("weapon.rig.attachAndPose") {
Weapon mod;
mod.registerWeaponsFromJson(kMeleeDefs);
WeaponEntity* w = mod.newWeapon("sword");
REQUIRE(w != nullptr);
WeaponRigEntity* rig = mod.newRig("right_hand");
REQUIRE(rig != nullptr);
CHECK_EQ(rig->identity()->wield, std::string("right_hand"));
CHECK(mod.rigAttachWeapon(rig, w));
CHECK(mod.rigGetWeapon(rig) == w);
mod.rigSetPose(rig, 0.1f, 0.2f, 0.3f, 0.f, 0.f, 0.f);
CHECK_EQ(rig->held()->posX, 0.1f);
CHECK_EQ(rig->held()->posZ, 0.3f);
}
// ---------------------------------------------------------------------------
// P0:多弹丸 / 散布 bloom / 后坐 / 射击模式 / 开镜 / 伤害类型 / 弹药池
// ---------------------------------------------------------------------------
TEST_CASE("weapon.p0.multipellet") {
StubProjectileService svc;
eve::cap::provide<IProjectileService>(&svc);
Weapon mod;
mod.registerWeaponsFromJson(kP0Defs);
WeaponEntity* w = mod.newWeapon("shotgun");
REQUIRE(w != nullptr);
mod.clearEvents();
CHECK(mod.fireAt(w, 0.f, 0.f, 0.f));
CHECK_EQ(svc.spawns, 4); // 一次触发 4 粒弹丸
CHECK_EQ(mod.getEventPellets(0), 4); // 事件记录弹丸数
CHECK_EQ(mod.getEventCount(), 1);
eve::cap::revoke<IProjectileService>(&svc);
}
TEST_CASE("weapon.p0.spreadBloom") {
Weapon mod;
mod.registerWeaponsFromJson(kP0Defs);
WeaponEntity* w = mod.newWeapon("bloom");
REQUIRE(w != nullptr);
mod.clearEvents();
CHECK_EQ(w->state()->currentSpread, 1.f); // 起始 = spreadMin
CHECK(mod.fireAt(w, 0.f, 0.f, 0.f));
CHECK_EQ(mod.getSpread(w), 4.f); // 1 + 3
CHECK_EQ(mod.getEventSpread(0), 4.f);
CHECK(mod.fireAt(w, 0.f, 0.f, 0.f));
CHECK_EQ(mod.getSpread(w), 7.f);
CHECK(mod.fireAt(w, 0.f, 0.f, 0.f));
CHECK_EQ(mod.getSpread(w), 10.f); // 上限 spreadMax
mod.update(1.0f); // recover 5 → 10-5=5
CHECK_EQ(mod.getSpread(w), 5.f);
mod.update(1.0f);
CHECK_EQ(mod.getSpread(w), 1.f); // 回稳到 spreadMin
}
TEST_CASE("weapon.p0.recoilModel") {
Weapon mod;
mod.registerWeaponsFromJson(kP0Defs);
WeaponEntity* w = mod.newWeapon("recoil");
REQUIRE(w != nullptr);
mod.clearEvents();
CHECK(mod.fireAt(w, 0.f, 0.f, 0.f));
CHECK_EQ(mod.getRecoilPitch(w), 2.f);
CHECK_EQ(mod.getRecoilYaw(w), -1.f);
CHECK_EQ(mod.getEventRecoilPitch(0), 2.f); // 本发后坐
CHECK_EQ(mod.getEventRecoilYaw(0), -1.f);
mod.update(1.0f); // recover 1 → pitch 1, yaw 0
CHECK_EQ(mod.getRecoilPitch(w), 1.f);
CHECK_EQ(mod.getRecoilYaw(w), 0.f);
}
TEST_CASE("weapon.p0.fireSelector") {
Weapon mod;
mod.registerWeaponsFromJson(kP0Defs);
WeaponEntity* sel = mod.newWeapon("selector");
REQUIRE(sel != nullptr);
CHECK_EQ(mod.getFireMode(sel), std::string("auto"));
CHECK_EQ(mod.getSelectableModeCount(sel), 2);
CHECK_EQ(mod.getSelectableMode(sel, 0), std::string("single"));
CHECK(mod.setFireMode(sel, "burst"));
CHECK_EQ(mod.getFireMode(sel), std::string("burst"));
CHECK(!mod.setFireMode(sel, "auto")); // 不在可选列表
WeaponEntity* fx = mod.newWeapon("fixedsel");
REQUIRE(fx != nullptr);
CHECK(!mod.setFireMode(fx, "burst")); // 固定 single
CHECK_EQ(mod.getFireMode(fx), std::string("single"));
}
TEST_CASE("weapon.p0.aiming") {
Weapon mod;
mod.registerWeaponsFromJson(kP0Defs);
WeaponEntity* w = mod.newWeapon("aim");
REQUIRE(w != nullptr);
mod.clearEvents();
CHECK_EQ(mod.getZoomFov(w), 40.f);
CHECK(!mod.isAiming(w));
CHECK(mod.setAiming(w, true));
CHECK(mod.isAiming(w));
CHECK_EQ(mod.getEventCount(), 1);
CHECK_EQ(mod.getEventType(0), std::string("aim_in"));
CHECK(mod.setAiming(w, false));
CHECK(!mod.isAiming(w));
CHECK_EQ(mod.getEventType(1), std::string("aim_out"));
CHECK(!mod.setAiming(w, false)); // 已是 false,无变化
}
TEST_CASE("weapon.p0.damageTypeAndElement") {
Weapon mod;
mod.registerWeaponsFromJson(kP0Defs);
WeaponEntity* w = mod.newWeapon("typed");
REQUIRE(w != nullptr);
mod.clearEvents();
CHECK(mod.fireAt(w, 0.f, 0.f, 0.f));
CHECK_EQ(mod.getEventDamageType(0), std::string("pierce"));
CHECK_EQ(mod.getEventElement(0), std::string("fire"));
}
TEST_CASE("weapon.p0.sharedAmmoPool") {
Weapon mod;
mod.registerWeaponsFromJson(kP0Defs);
AmmoPoolEntity* pool = mod.newAmmoPool("pistol_ammo", "pistol", 100);
REQUIRE(pool != nullptr);
mod.ammoPoolAdd(pool, 20);
CHECK_EQ(mod.ammoPoolGetCount(pool), 20);
WeaponEntity* a = mod.newWeapon("poolgun");
REQUIRE(a != nullptr);
CHECK(mod.bindAmmoPool(a, pool));
CHECK(mod.getAmmoPool(a) == pool);
mod.clearEvents();
CHECK(mod.fireAt(a, 0.f, 0.f, 0.f));
CHECK(mod.fireAt(a, 0.f, 0.f, 0.f));
CHECK_EQ(a->state()->resource.value, 0.f);
mod.update(0.5f); // 自动装填,从池取 2 发
CHECK_EQ(a->state()->resource.value, 2.f);
CHECK_EQ(mod.ammoPoolGetCount(pool), 18);
}
TEST_CASE("weapon.mount.limitsAndAim") {
Weapon mod;
mod.registerWeaponsFromJson(kDefs);
WeaponMountEntity* m = mod.newMount("turret");
REQUIRE(m != nullptr);
mod.mountSetLimits(m, -90.f, 90.f, -10.f, 20.f, 45.f, 0.f);
WeaponEntity* w = mod.newWeapon("cannon");
CHECK(mod.mountAttachWeapon(m, w));
mod.mountAimAt(m, 200.f, 50.f);
CHECK_EQ(w->aim()->desiredYaw, 90.f); // 夹到限位
CHECK_EQ(w->aim()->desiredPitch, 20.f);
mod.update(1.0f); // turnSpeed 45°/s
CHECK_EQ(w->aim()->yaw, 45.f);
mod.update(1.0f);
CHECK_EQ(w->aim()->yaw, 90.f);
CHECK_EQ(m->state()->yaw, 90.f); // 挂点跟随武器
mod.mountDestroy(m);
mod.mountAimAt(m, 0.f, 0.f);
CHECK_EQ(w->aim()->desiredYaw, 90.f); // 已毁挂点不再瞄准
}
TEST_CASE("weapon.mount.aimsWithoutWeapon") {
Weapon mod;
WeaponMountEntity* m = mod.newMount("pintle");
REQUIRE(m != nullptr);
mod.mountSetLimits(m, -45.f, 45.f, -5.f, 5.f, 0.f, 0.f);
mod.mountAimAt(m, 100.f, 20.f);
CHECK_EQ(m->state()->yaw, 45.f);
CHECK_EQ(m->state()->pitch, 5.f);
}
static const char* kViewScript = R"SQ(
function testWeaponCppView() {
local weapon = eve.Weapon()
weapon.registerWeaponsFromJson("[{\"id\":\"pistol\",\"ammo\":{\"mag\":12}}]")
local w = weapon.newWeapon("pistol")
if (w == null) return false
local all = eve.view(eve.WeaponEntity)
foreach (e in all) {
if (e.getId() == w.getId()) return true
}
return false
}
)SQ";
UnitSciptTest(WeaponViewScriptTest, kViewScript);
TEST_CASE_FIXTURE(WeaponViewScriptTest, "weapon.script.viewSeesNewWeapon") {
CHECK(vm.callFunc(vm.findFunc("testWeaponCppView"), vm).toBool());
}