Files
西风逍遥游 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

117 lines
4.0 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "rpg/RPGActor.h"
#include "rpg/Trait.h"
#include "rpg/TraitSystem.h"
#include "rpg/RPG.h"
#include <cmath>
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.trait.registryJsonAndCRUD") {
TraitRegistry::clear();
int n = TraitRegistry::loadFromJson(R"([
{"id":"fire_resist",
"traits":[
{"kind":"elementRate","target":"fire","value":0.5},
{"kind":"elementRate","target":"ice","value":1.5}
],
"tags":["defense"]},
{"id":"mighty",
"traits":[
{"kind":"paramRate","target":"attack","value":1.5},
{"kind":"exParam","target":"critRate","value":0.3}
]}
])");
CHECK_EQ(n, 2);
CHECK_EQ(TraitRegistry::count(), 2);
const TraitDefinition *fr = TraitRegistry::find("fire_resist");
REQUIRE(fr != nullptr);
CHECK(fr->hasTag("defense"));
REQUIRE(fr->traits.size() == 2);
CHECK_EQ(fr->traits[0].target, std::string("fire"));
CHECK(approxEq(fr->traits[0].value, 0.5));
TraitRegistry::clear();
CHECK_EQ(TraitRegistry::count(), 0);
}
TEST_CASE("rpg.trait.applyQueriesAndModifiers") {
auto *rpg = RPG::create();
rpg->clearTraitDefinitions();
int registered = rpg->registerTraitsFromJson(R"([
{"id":"mighty",
"traits":[
{"kind":"paramRate","target":"attack","value":1.5},
{"kind":"exParam","target":"critRate","value":0.3}
]},
{"id":"fire_resist","traits":[{"kind":"elementRate","target":"fire","value":0.5}]},
{"id":"stone_skin","traits":[{"kind":"stateResist","target":"poison"}]},
{"id":"brutal",
"traits":[{"kind":"attackTimes","value":1},{"kind":"attackElement","target":"fire"}]}
])");
CHECK_EQ(registered, 4);
RPGActor *actor = rpg->newActor();
actor->setBaseAttribute("attack", 100.0);
int id = actor->applyTrait("mighty", "class");
CHECK(id > 0);
// paramRate 1.5 → mulAdd +0.5 → attack = 150
CHECK(approxEq(actor->getFinalAttribute("attack"), 150.0));
CHECK(approxEq(actor->getParamRate("attack"), 1.5));
CHECK(approxEq(actor->getExParam("critRate"), 0.3));
CHECK(actor->hasTrait("mighty"));
CHECK_EQ(actor->getTraitCount(), 1);
actor->applyTrait("fire_resist", "equipment");
CHECK(approxEq(actor->getElementRate("fire"), 0.5));
CHECK(approxEq(actor->getElementRate("ice"), 1.0)); // 未命中 → 1
CHECK_EQ(actor->getTraitCount(), 2);
actor->applyTrait("stone_skin", "race");
CHECK(actor->isStateResist("poison"));
CHECK(!actor->isStateResist("sleep"));
actor->applyTrait("brutal", "passive");
CHECK_EQ(actor->getAttackTimesAdd(), 1);
const auto elems = actor->getAttackElements();
REQUIRE(elems.size() == 1);
CHECK_EQ(elems[0], std::string("fire"));
// 移除按 source
int removedEquip = actor->removeTraitsBySource("equipment");
CHECK_EQ(removedEquip, 1);
CHECK(approxEq(actor->getElementRate("fire"), 1.0));
// 移除 paramRate 特征后属性加成随之撤销
int removedMighty = actor->removeTraitsByTrait("mighty");
CHECK_EQ(removedMighty, 1);
CHECK(approxEq(actor->getFinalAttribute("attack"), 100.0));
CHECK(!actor->hasTrait("mighty"));
rpg->clearTraitDefinitions();
actor->release();
}
TEST_CASE("rpg.trait.facadeEventsIndependent") {
auto *rpg = RPG::create();
rpg->clearTraitDefinitions();
int registered = rpg->registerTraitsFromJson(
R"([{"id":"t1","traits":[{"kind":"exParam","target":"evasion","value":0.2}]}])");
CHECK_EQ(registered, 1);
RPGActor *a = rpg->newActor();
int id = a->applyTrait("t1", "src");
CHECK(id > 0);
CHECK(approxEq(a->getExParam("evasion"), 0.2));
// 两 actor 特征互不影响
RPGActor *b = rpg->newActor();
CHECK(approxEq(b->getExParam("evasion"), 0.0));
rpg->clearTraitDefinitions();
a->release();
b->release();
}