Files
EVEngine/test/rpg_equipment.cpp
西风逍遥游 6cc4e8b602 feat(rpg): RPG-Maker-style main model + complete RPG interface
Extend the RPG module into a data-driven RPG-Maker-style main model and add
a complete turn-based RPG example demonstrating the whole surface.

New RPG subsystems (all string-keyed, JSON-driven, Squirrel-bound):
- Quest tracker: QuestRegistry + Tracker + QuestSystem (states, notify,
  prerequisite chain, events); eve.RPG.newTracker().
- Progression: LevelSystem + RPGActor::Progression (XP/level/level-up events).
- Vitals: VitalsSystem + RPGActor::Vitals (current resource, takeDamage/heal/
  death/revive).
- Traits: Trait/TraitSystem + RPGActor::Traits (paramRate, elementRate,
  stateRate/resist, exParam, attack elements/states/times/speed).
- Turn-based battle: BattleSystem (damage-formula DSL "a.atk*4-b.def*2",
  hit/crit/element) + Battle (arbitrary sides, initiative order, target
  resolution, winner detection, events).
- Classes: Class/ClassSystem + RPGActor::ClassInfo (class traits + learn
  skills by level).
- GameState: global switches/variables/self-variables.
- Equipment + loot: EquipmentSystem (equip -> attribute modifiers) and
  LootSystem (loot table -> inventory Bag), via the inventory module.

Bindings: bind RPGActor::release for script-side destruction; wrap
registerSkillDamage double as float (SQUSEDOUBLE).

Example: examples/rpg-classic - complete RPG interface (bag/shop/stat
allocation/equipment/adjust every 3 waves/status screen anytime).

Tests: rpg_quest, rpg_progression, rpg_trait, rpg_battle, rpg_class,
rpg_gamestate, rpg_equipment (loot). Full rpg.* regression green.
2026-08-28 15:02:13 +08:00

122 lines
3.7 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "rpg/RPGActor.h"
#include "rpg/EquipmentSystem.h"
#include "rpg/LootSystem.h"
#include "rpg/RPG.h"
#include "inventory/Bag.h"
#include "inventory/Equipment.h"
#include "inventory/Item.h"
#include <cmath>
#include <random>
using namespace eve::rpg;
namespace {
bool approxEq(double a, double b, double eps = 1e-9) { return std::abs(a - b) < eps; }
} // namespace
TEST_CASE("rpg.equipment.statSyncAppliesAndRemoves") {
auto *rpg = RPG::create();
rpg->clearAllItemStats();
int registered = rpg->registerItemStatsFromJson(
"sword",
R"([
{"attribute":"attack","op":"add","value":15},
{"attribute":"attack","op":"mulMul","value":0.1}
])");
CHECK_EQ(registered, 2);
CHECK_EQ(rpg->getItemStatCount(), 1);
RPGActor *actor = rpg->newActor();
actor->setBaseAttribute("attack", 10.0);
eve::inventory::EquipmentSet equip;
equip.setId("hero");
equip.defineSlot("weapon");
equip.defineSlot("armor");
// 未装备:sync 不应产生加成
CHECK_EQ(rpg->syncEquipModifiers(actor, &equip), 0);
CHECK(approxEq(actor->getFinalAttribute("attack"), 10.0));
// 直接往武器槽塞物品(模拟已装备 sword)
equip.stackAt("weapon")->itemId = "sword";
equip.stackAt("weapon")->quantity = 1;
int applied = rpg->syncEquipModifiers(actor, &equip);
CHECK_EQ(applied, 2);
// 10 + 15 = 25,再 * (1 + 0.1) = 27.5
CHECK(approxEq(actor->getFinalAttribute("attack"), 27.5));
// 换装/卸下后重新 sync 应移除旧加成
equip.stackAt("weapon")->clear();
CHECK_EQ(rpg->syncEquipModifiers(actor, &equip), 0);
CHECK(approxEq(actor->getFinalAttribute("attack"), 10.0));
rpg->clearAllItemStats();
actor->release();
}
TEST_CASE("rpg.loot.rollGuaranteedAndSeeded") {
auto *rpg = RPG::create();
rpg->clearLootTables();
eve::inventory::ItemRegistry::clear();
eve::inventory::ItemDefinition gold;
gold.id = "gold";
gold.displayName = "Gold";
gold.maxStack = 999;
eve::inventory::ItemDefinition potion;
potion.id = "potion";
potion.displayName = "Potion";
potion.maxStack = 20;
eve::inventory::ItemRegistry::registerItem(gold);
eve::inventory::ItemRegistry::registerItem(potion);
int tables = rpg->registerLootTablesFromJson(R"([
{"id":"goblin","entries":[
{"itemId":"gold","chance":1.0,"minQty":5,"maxQty":5},
{"itemId":"potion","chance":0.0,"minQty":1,"maxQty":1}
]}
])");
CHECK_EQ(tables, 1);
CHECK_EQ(rpg->getLootTableCount(), 1);
eve::inventory::Bag bag(20);
bag.setId("player");
int drops = rpg->rollLoot("goblin", &bag, 42);
CHECK_EQ(drops, 1); // 只有 gold 必掉
CHECK_EQ(bag.countItem("gold"), 5);
CHECK_EQ(bag.countItem("potion"), 0);
// 未知表返回 0
eve::inventory::Bag bag2(20);
int missing = rpg->rollLoot("missing", &bag2, 42);
CHECK_EQ(missing, 0);
rpg->clearLootTables();
eve::inventory::ItemRegistry::clear();
}
TEST_CASE("rpg.loot.rollDirectWithRng") {
LootSystem::clear();
eve::inventory::ItemRegistry::clear();
eve::inventory::ItemDefinition a;
a.id = "a";
a.displayName = "A";
a.maxStack = 999;
eve::inventory::ItemRegistry::registerItem(a);
int tables = LootSystem::registerTablesFromJson(R"([
{"id":"t","entries":[{"itemId":"a","chance":1.0,"minQty":2,"maxQty":2}]}
])");
CHECK_EQ(tables, 1);
eve::inventory::Bag bag(20);
std::mt19937 rng(7);
int rolls = LootSystem::roll("t", &bag, rng);
CHECK_EQ(rolls, 1);
CHECK_EQ(bag.countItem("a"), 2);
LootSystem::clear();
eve::inventory::ItemRegistry::clear();
}