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.
581 lines
20 KiB
C++
581 lines
20 KiB
C++
#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());
|
||
} |