载入中...
搜索中...
未找到
BattleTactics.cpp
浏览该文件的文档.
1#include "rpg/BattleTactics.h"
2
3#include "common/Json.h"
4#include "rpg/RPGActor.h"
5#include "rpg/Skill.h"
6
7#include <cmath>
8#include <unordered_map>
9#include <unordered_set>
10#include <utility>
11
12namespace eve::rpg {
13namespace {
14
15std::unordered_map<std::string, BattleTacticsDefinition> &definitions() {
16 static std::unordered_map<std::string, BattleTacticsDefinition> value;
17 return value;
18}
19
20bool validId(const std::string &value, bool allowEmpty = false) {
21 if (value.empty()) return allowEmpty;
22 if (value.size() > 256) return false;
23 for (unsigned char ch : value)
24 if (ch < 0x20 || ch == 0x7f) return false;
25 return true;
26}
27
28bool parsePolicy(const std::string &value, BattleTargetPolicy &policy) {
29 if (value == "auto") policy = BattleTargetPolicy::Auto;
30 else if (value == "self") policy = BattleTargetPolicy::Self;
31 else if (value == "lowestHealthAlly") policy = BattleTargetPolicy::LowestHealthAlly;
32 else if (value == "lowestHealthEnemy") policy = BattleTargetPolicy::LowestHealthEnemy;
33 else return false;
34 return true;
35}
36
37bool policyMatchesTargetType(BattleTargetPolicy policy, const std::string &targetType) {
38 if (policy == BattleTargetPolicy::Auto) return true;
39 if (targetType == "self") return policy == BattleTargetPolicy::Self;
40 if (targetType == "allySingle") return policy == BattleTargetPolicy::LowestHealthAlly;
42}
43
44} // namespace
45
47 std::string parseError;
48 const auto document = eve::json::Document::parse(json, &parseError);
49 if (!document.valid())
51 parseError.empty() ? "invalid JSON" : parseError, "$",
52 {}, "rpg.battle-tactics"));
53 const auto root = document.root();
54 if (!root.isArray() || root.size() == 0)
56 "battle tactics catalogue must be a non-empty array",
57 "$", {}, "rpg.battle-tactics"));
58 const std::unordered_set<std::string> definitionFields = {"id", "rules"};
59 const std::unordered_set<std::string> ruleFields = {
60 "skillId", "targetPolicy", "conditionResource", "belowRatio"};
61 std::unordered_map<std::string, BattleTacticsDefinition> proposed;
62 for (std::size_t index = 0; index < root.size(); ++index) {
63 const auto object = root.at(index);
64 const std::string path = "$[" + std::to_string(index) + "]";
65 if (!object.isObject())
67 "battle tactics definition must be an object", path,
68 {}, "rpg.battle-tactics"));
69 for (const auto &key : object.keys())
70 if (!definitionFields.contains(key))
72 eve::DiagnosticCode::InvalidArgument, "battle tactics definition contains an unknown field",
73 path + "." + key, {}, "rpg.battle-tactics"));
74 const auto id = object.get("id");
75 const auto rules = object.get("rules");
76 if (!id.isString() || !validId(id.asString()))
78 "battle tactics id must be a stable non-empty id",
79 path + ".id", {}, "rpg.battle-tactics"));
80 if (proposed.contains(id.asString()))
82 "duplicate battle tactics id", path + ".id", {},
83 "rpg.battle-tactics"));
84 if (!rules.isArray() || rules.size() == 0 || rules.size() > 16)
86 "rules must contain between 1 and 16 entries",
87 path + ".rules", {}, "rpg.battle-tactics"));
88 BattleTacticsDefinition definition;
89 definition.id = id.asString();
90 for (std::size_t ruleIndex = 0; ruleIndex < rules.size(); ++ruleIndex) {
91 const auto rule = rules.at(ruleIndex);
92 const std::string rulePath = path + ".rules[" + std::to_string(ruleIndex) + "]";
93 if (!rule.isObject())
95 "battle tactic rule must be an object",
96 rulePath, {}, "rpg.battle-tactics"));
97 for (const auto &key : rule.keys())
98 if (!ruleFields.contains(key))
100 eve::DiagnosticCode::InvalidArgument, "battle tactic rule contains an unknown field",
101 rulePath + "." + key, {}, "rpg.battle-tactics"));
102 const auto skillId = rule.get("skillId");
103 const auto targetPolicy = rule.get("targetPolicy");
104 const auto conditionResource = rule.get("conditionResource");
105 const auto belowRatio = rule.get("belowRatio");
106 if (!skillId.isString() || !validId(skillId.asString(), true) ||
107 (!skillId.asString().empty() && !SkillRegistry::find(skillId.asString())))
109 eve::DiagnosticCode::NotFound, "skillId must be empty or reference a registered skill",
110 rulePath + ".skillId", {}, "rpg.battle-tactics"));
112 if (!targetPolicy.isString() || !parsePolicy(targetPolicy.asString(), parsedPolicy))
115 rulePath + ".targetPolicy", {}, "rpg.battle-tactics"));
116 const std::string targetType = skillId.asString().empty()
117 ? "enemySingle"
118 : SkillRegistry::find(skillId.asString())->targetType;
119 if (!policyMatchesTargetType(parsedPolicy, targetType))
121 eve::DiagnosticCode::InvalidArgument, "targetPolicy does not match the skill target type",
122 rulePath + ".targetPolicy", {}, "rpg.battle-tactics"));
123 if (!conditionResource.isString() || !validId(conditionResource.asString(), true) ||
124 !belowRatio.isNumber() || !std::isfinite(belowRatio.asDouble()) ||
125 belowRatio.asDouble() < 0.0 || belowRatio.asDouble() > 1.0)
127 "conditionResource and belowRatio are invalid",
128 rulePath, {}, "rpg.battle-tactics"));
129 if (parsedPolicy == BattleTargetPolicy::Auto && !conditionResource.asString().empty())
131 eve::DiagnosticCode::InvalidArgument, "conditional rules require an explicit target policy",
132 rulePath, {}, "rpg.battle-tactics"));
133 BattleTacticRule parsed;
134 parsed.skillId = skillId.asString();
135 parsed.targetPolicy = parsedPolicy;
136 parsed.conditionResource = conditionResource.asString();
137 parsed.belowRatio = belowRatio.asDouble();
138 definition.rules.push_back(std::move(parsed));
139 }
140 if (!definition.rules.back().conditionResource.empty())
142 eve::DiagnosticCode::InvalidArgument, "the final battle tactic rule must be unconditional",
143 path + ".rules", {}, "rpg.battle-tactics"));
144 proposed.emplace(definition.id, std::move(definition));
145 }
146 definitions() = std::move(proposed);
147 return eve::Result<int>::success(static_cast<int>(definitions().size()));
148}
149
151int BattleTacticsCatalogue::count() { return static_cast<int>(definitions().size()); }
152
154 const std::string &tacticsId) {
155 if (!battle || !actor)
157 "battle and actor must not be null", "battle",
158 {}, "rpg.battle-tactics"));
159 const auto found = definitions().find(tacticsId);
160 if (found == definitions().end())
162 "battle tactics id is not registered",
163 "tacticsId", {}, "rpg.battle-tactics"));
164 for (const auto &rule : found->second.rules) {
165 if (!rule.conditionResource.empty()) {
166 const int side = battle->sideOf(actor);
167 RPGActor *target = rule.targetPolicy == BattleTargetPolicy::Self
168 ? actor
169 : battle->lowestHealthTarget(
170 side, rule.targetPolicy == BattleTargetPolicy::LowestHealthAlly);
171 if (!target) continue;
172 const double maximum = target->getMax(rule.conditionResource);
173 if (maximum <= 0.0 || target->getCurrent(rule.conditionResource) / maximum >= rule.belowRatio)
174 continue;
175 }
176 auto queued = battle->setActionByPolicyChecked(actor, rule.skillId, rule.targetPolicy);
177 if (!queued.ok()) return eve::Result<std::string>::failure(queued.status());
178 return eve::Result<std::string>::success(rule.skillId);
179 }
181 "battle tactics has no applicable default rule",
182 "rules", {}, "rpg.battle-tactics"));
183}
184
185} // namespace eve::rpg
LogicalId target
double value
int root
Definition AnimSmr.cpp:119
Strict ordered battle-action policies for companions and AI.
float maximum[3]
std::uint32_t key
size_t queued
Definition OnnxGpgpu.cpp:60
std::string path
Definition PlayHost.cpp:110
bool found
ecs::EntityHandle side
float size
Definition TreeMesh.cpp:156
uint32_t index
const std::map< std::string, Value > & definitions
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Document parse(const std::string &text, std::string *error=nullptr)
Parse.
Definition Json.cpp:581
static int count()
Returns the number of .
static eve::Result< std::string > queueAction(Battle *battle, RPGActor *actor, const std::string &tacticsId)
Select the first matching rule and queue it through Battle's canonical checked API.
static eve::Result< int > replaceFromJsonStrict(const std::string &json)
Validate and atomically replace all tactic definitions.
一场回合制战斗(任意阵营数)。 @ownership 由调用方创建/销毁;Battle 不持有参与者的所有权(参与者为借用 ECS actor)。 @thread 在单一模拟线程上驱动整场战斗。
Definition Battle.h:54
eve::Result< void > setActionByPolicyChecked(RPGActor *actor, const std::string &skillId, BattleTargetPolicy policy)
Resolve a deterministic target policy and queue one checked action atomically.
Definition Battle.cpp:243
属性 / 状态 / 技能三表合一的 ECS 实体。
Definition RPGActor.h:34
double getMax(const std::string &resource)
Definition RPGActor.cpp:251
static const SkillDefinition * find(const std::string &id)
Find a registered skill definition by id.
Definition Skill.cpp:90
bool parsePolicy(const std::string &name, StackPolicy &policy)
Parses a lowercase legacy stack policy name.
RPG 模块的基础实体:把属性 / 状态 / 技能三张表挂到一个 ECS 实体上。
BattleTargetPolicy
Deterministic target-selection policies for queued battle actions.
Definition Battle.h:36
One ordered tactic rule; an empty condition resource is an unconditional default.
BattleTargetPolicy targetPolicy
One immutable ordered tactic definition.
std::string targetType
Definition Skill.h:32