载入中...
搜索中...
未找到
SpellFragmentCompiler.cpp
浏览该文件的文档.
2
3#include "common/Diagnostic.h"
4#include "common/Time.h"
6
7#include <cmath>
8#include <cstdint>
9#include <limits>
10#include <string>
11#include <string_view>
12#include <utility>
13#include <vector>
14
15namespace eve::weapon {
16namespace {
17
18const Value* field(const Value::Object& object, const char* name) {
19 const auto it = object.find(name);
20 return it == object.end() ? nullptr : &it->second;
21}
22
23bool readString(const Value::Object& object, const char* name, std::string& output) {
24 const auto* value = field(object, name);
25 const auto* text = value ? value->getIf<std::string>() : nullptr;
26 if (!text) return false;
27 output = *text;
28 return true;
29}
30
31Result<std::optional<double>> optionalDouble(const Value::Object& object, const char* name, std::string_view path) {
32 const auto* value = field(object, name);
33 if (!value) return Result<std::optional<double>>::success(std::nullopt);
34 if (const auto* number = value->getIf<double>()) {
35 if (!std::isfinite(*number))
36 return Result<std::optional<double>>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Expected finite number"), std::string(path), {},
37 "weapon.spell.fragments"));
38 return Result<std::optional<double>>::success(*number);
39 }
40 if (const auto* integer = value->getIf<std::int64_t>()) {
41 const double converted = static_cast<double>(*integer);
42 if (!std::isfinite(converted))
43 return Result<std::optional<double>>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Expected finite number"), std::string(path), {},
44 "weapon.spell.fragments"));
45 return Result<std::optional<double>>::success(converted);
46 }
47 return Result<std::optional<double>>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Expected number"), std::string(path), {},
48 "weapon.spell.fragments"));
49}
50
51Result<std::optional<int>> optionalInt(const Value::Object& object, const char* name, std::string_view path) {
52 const auto* value = field(object, name);
53 if (!value) return Result<std::optional<int>>::success(std::nullopt);
54 if (const auto* integer = value->getIf<std::int64_t>()) {
55 if (*integer < std::numeric_limits<int>::min() || *integer > std::numeric_limits<int>::max())
56 return Result<std::optional<int>>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Integer out of range"), std::string(path), {},
57 "weapon.spell.fragments"));
58 return Result<std::optional<int>>::success(static_cast<int>(*integer));
59 }
60 if (const auto* number = value->getIf<double>()) {
61 if (!std::isfinite(*number) || *number != std::floor(*number))
62 return Result<std::optional<int>>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Expected integer"), std::string(path), {},
63 "weapon.spell.fragments"));
64 if (*number < static_cast<double>(std::numeric_limits<int>::min()) ||
65 *number > static_cast<double>(std::numeric_limits<int>::max()))
66 return Result<std::optional<int>>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Integer out of range"), std::string(path), {},
67 "weapon.spell.fragments"));
68 return Result<std::optional<int>>::success(static_cast<int>(*number));
69 }
70 return Result<std::optional<int>>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Expected integer"), std::string(path), {},
71 "weapon.spell.fragments"));
72}
73
74enum class ArithKind : std::uint8_t { Add, Multiply };
75
76struct ArithOp {
77 ArithKind kind = ArithKind::Add;
78 double value = 0.0;
79};
80
81[[nodiscard]] double applyArith(double base, const std::vector<ArithOp>& ops) {
82 double result = base;
83 for (const auto& op : ops) {
84 if (op.kind == ArithKind::Add)
85 result += op.value;
86 else
87 result *= op.value;
88 }
89 return result;
90}
91
92struct ModifierStack {
93 std::vector<ArithOp> speedOps;
94 std::vector<ArithOp> damageOps;
95 std::vector<ArithOp> lifetimeOps;
96 double gravity = 0.0;
97 double homing = 0.0;
98 double accelerate = 0.0;
99 double swayAmplitude = 0.0;
100 double swayFrequency = 0.0;
101 double helixAmplitude = 0.0;
102 double helixFrequency = 0.0;
103 int pierce = 0;
104 int bounce = 0;
105 double restitution = 1.0;
106 double splash = 0.0;
107 double fuse = 0.0;
108 std::string element;
109 std::string damageType;
110 std::optional<CarrierVolleySpec> volley;
111};
112
113Result<void> applyModifier(ModifierStack& stack, const Value::Object& object, std::string_view kind,
114 std::string path) {
115 if (kind == "homing") {
116 auto turnRate = optionalDouble(object, "turnRate", path + ".turnRate");
117 if (!turnRate.ok()) return Result<void>::failure(turnRate.status());
118 auto alt = optionalDouble(object, "maxTurnRateDegrees", path + ".maxTurnRateDegrees");
119 if (!alt.ok()) return Result<void>::failure(alt.status());
120 const double turn = turnRate.value().value_or(alt.value().value_or(90.0));
121 if (!(turn > 0.0)) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("homing turnRate must be positive"), std::string(path), {},
122 "weapon.spell.fragments"));
123 stack.homing = turn;
124 return Result<void>::success();
125 }
126 if (kind == "gravity") {
127 auto gravity = optionalDouble(object, "gravity", path + ".gravity");
128 if (!gravity.ok()) return Result<void>::failure(gravity.status());
129 const double value = gravity.value().value_or(9.8);
130 if (value < 0.0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("gravity must be non-negative"), std::string(path), {},
131 "weapon.spell.fragments"));
132 stack.gravity = value;
133 return Result<void>::success();
134 }
135 if (kind == "accelerate") {
136 auto acceleration = optionalDouble(object, "acceleration", path + ".acceleration");
137 if (!acceleration.ok()) return Result<void>::failure(acceleration.status());
138 if (!acceleration.value().has_value() || *acceleration.value() < 0.0)
139 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("accelerate requires non-negative acceleration"), std::string(path), {},
140 "weapon.spell.fragments"));
141 stack.accelerate = *acceleration.value();
142 return Result<void>::success();
143 }
144 if (kind == "sway" || kind == "helix") {
145 auto amplitude = optionalDouble(object, "amplitude", path + ".amplitude");
146 if (!amplitude.ok()) return Result<void>::failure(amplitude.status());
147 auto altAmp = optionalDouble(object, "curveAmplitude", path + ".curveAmplitude");
148 if (!altAmp.ok()) return Result<void>::failure(altAmp.status());
149 const auto amp = amplitude.value().has_value() ? amplitude.value() : altAmp.value();
150 if (!amp.has_value() || *amp < 0.0)
151 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("curve fragment requires non-negative amplitude"), std::string(path), {},
152 "weapon.spell.fragments"));
153 auto frequency = optionalDouble(object, "frequency", path + ".frequency");
154 if (!frequency.ok()) return Result<void>::failure(frequency.status());
155 auto altFreq = optionalDouble(object, "curveFrequencyHz", path + ".curveFrequencyHz");
156 if (!altFreq.ok()) return Result<void>::failure(altFreq.status());
157 const double freq = frequency.value().value_or(altFreq.value().value_or(2.0));
158 if (!(freq > 0.0)) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("curve frequency must be positive"), std::string(path), {},
159 "weapon.spell.fragments"));
160 if (kind == "sway") {
161 stack.swayAmplitude = *amp;
162 stack.swayFrequency = freq;
163 } else {
164 stack.helixAmplitude = *amp;
165 stack.helixFrequency = freq;
166 }
167 return Result<void>::success();
168 }
169 if (kind == "pierce" || kind == "bounce") {
170 auto count = optionalInt(object, "count", path + ".count");
171 if (!count.ok()) return Result<void>::failure(count.status());
172 const int value = count.value().value_or(1);
173 if (value <= 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("count must be positive"), std::string(path), {},
174 "weapon.spell.fragments"));
175 if (kind == "pierce") {
176 stack.pierce = value;
177 } else {
178 stack.bounce = value;
179 auto restitution = optionalDouble(object, "restitution", path + ".restitution");
180 if (!restitution.ok()) return Result<void>::failure(restitution.status());
181 if (restitution.value().has_value()) {
182 if (*restitution.value() < 0.0)
183 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("restitution must be non-negative"), std::string(path), {},
184 "weapon.spell.fragments"));
185 stack.restitution = *restitution.value();
186 }
187 }
188 return Result<void>::success();
189 }
190 if (kind == "damage" || kind == "speed" || kind == "lifetime") {
191 auto add = optionalDouble(object, "add", path + ".add");
192 if (!add.ok()) return Result<void>::failure(add.status());
193 auto multiply = optionalDouble(object, "multiply", path + ".multiply");
194 if (!multiply.ok()) return Result<void>::failure(multiply.status());
195 if (!add.value().has_value() && !multiply.value().has_value())
196 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("modifier requires add and/or multiply"), std::string(path), {},
197 "weapon.spell.fragments"));
198 auto& ops = (kind == "damage") ? stack.damageOps : (kind == "speed") ? stack.speedOps : stack.lifetimeOps;
199 // Preserve fragment order: add keys then multiply keys as written is ambiguous in objects,
200 // so apply add before multiply when both are present in one fragment; separate fragments
201 // retain left-to-right order across the sequence.
202 if (add.value().has_value()) ops.push_back({ArithKind::Add, *add.value()});
203 if (multiply.value().has_value()) ops.push_back({ArithKind::Multiply, *multiply.value()});
204 return Result<void>::success();
205 }
206 if (kind == "element") {
207 if (!readString(object, "value", stack.element) && !readString(object, "element", stack.element))
208 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("element requires value"), std::string(path), {},
209 "weapon.spell.fragments"));
210 return Result<void>::success();
211 }
212 if (kind == "damageType") {
213 if (!readString(object, "value", stack.damageType) && !readString(object, "damageType", stack.damageType))
214 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("damageType requires value"), std::string(path), {},
215 "weapon.spell.fragments"));
216 return Result<void>::success();
217 }
218 if (kind == "splash") {
219 auto radius = optionalDouble(object, "radius", path + ".radius");
220 if (!radius.ok()) return Result<void>::failure(radius.status());
221 auto alt = optionalDouble(object, "splash", path + ".splash");
222 if (!alt.ok()) return Result<void>::failure(alt.status());
223 const auto value = radius.value().has_value() ? radius.value() : alt.value();
224 if (!value.has_value() || !(*value > 0.0))
225 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("splash requires positive radius"), std::string(path), {},
226 "weapon.spell.fragments"));
227 stack.splash = *value;
228 return Result<void>::success();
229 }
230 if (kind == "fuse") {
231 auto seconds = optionalDouble(object, "seconds", path + ".seconds");
232 if (!seconds.ok()) return Result<void>::failure(seconds.status());
233 auto alt = optionalDouble(object, "fuse", path + ".fuse");
234 if (!alt.ok()) return Result<void>::failure(alt.status());
235 const auto value = seconds.value().has_value() ? seconds.value() : alt.value();
236 if (!value.has_value() || !(*value > 0.0))
237 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("fuse requires positive seconds"), std::string(path), {},
238 "weapon.spell.fragments"));
239 stack.fuse = *value;
240 auto splash = optionalDouble(object, "splash", path + ".splash");
241 if (!splash.ok()) return Result<void>::failure(splash.status());
242 auto splashAlt = optionalDouble(object, "radius", path + ".radius");
243 if (!splashAlt.ok()) return Result<void>::failure(splashAlt.status());
244 if (splash.value().has_value())
245 stack.splash = *splash.value();
246 else if (splashAlt.value().has_value())
247 stack.splash = *splashAlt.value();
248 return Result<void>::success();
249 }
250 if (kind == "fan" || kind == "multicast" || kind == "ring") {
251 CarrierVolleySpec volley;
253 auto count = optionalInt(object, "count", path + ".count");
254 if (!count.ok()) return Result<void>::failure(count.status());
255 if (!count.value().has_value() || *count.value() < 1)
256 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("volley requires positive count"), std::string(path), {},
257 "weapon.spell.fragments"));
258 volley.count = *count.value();
259 auto spread = optionalDouble(object, "spread", path + ".spread");
260 if (!spread.ok()) return Result<void>::failure(spread.status());
261 auto spreadAlt = optionalDouble(object, "spreadDegrees", path + ".spreadDegrees");
262 if (!spreadAlt.ok()) return Result<void>::failure(spreadAlt.status());
263 volley.spreadDegrees = spread.value().value_or(spreadAlt.value().value_or(0.0));
264 stack.volley = volley;
265 return Result<void>::success();
266 }
267 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Unknown fragment kind"), std::string(path + ".kind"), {},
268 "weapon.spell.fragments"));
269}
270
271Result<CarrierRecipe> buildProjectile(const Value::Object& object, const ModifierStack& stack, std::string path) {
272 if (field(object, "triggers") || field(object, "impacts") || field(object, "motionOps") ||
273 (field(object, "motion") && field(object, "motion")->isArray())) {
274 return Result<CarrierRecipe>::failure(Diagnostic::error(DiagnosticCode::Unsupported, std::string("Projectile fragments must use preset fields, not full-form motion/triggers/impacts"), std::string(path), {},
275 "weapon.spell.fragments"));
276 }
277
278 std::string id;
279 if (!readString(object, "id", id) || id.empty())
280 return Result<CarrierRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("projectile requires id"), std::string(path + ".id"), {},
281 "weapon.spell.fragments"));
282
283 auto speedField = optionalDouble(object, "speed", path + ".speed");
284 if (!speedField.ok()) return Result<CarrierRecipe>::failure(speedField.status());
285 double speed = applyArith(speedField.value().value_or(10.0), stack.speedOps);
286 if (!(speed > 0.0) || !std::isfinite(speed))
287 return Result<CarrierRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("projectile speed must be positive"), std::string(path + ".speed"), {},
288 "weapon.spell.fragments"));
289
290 auto lifetimeField = optionalDouble(object, "lifetime", path + ".lifetime");
291 if (!lifetimeField.ok()) return Result<CarrierRecipe>::failure(lifetimeField.status());
292 double lifetime = applyArith(lifetimeField.value().value_or(2.0), stack.lifetimeOps);
293 if (!(lifetime > 0.0) || !std::isfinite(lifetime))
294 return Result<CarrierRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("projectile lifetime must be positive"), std::string(path + ".lifetime"), {},
295 "weapon.spell.fragments"));
296
297 auto damageField = optionalDouble(object, "damage", path + ".damage");
298 if (!damageField.ok()) return Result<CarrierRecipe>::failure(damageField.status());
299 double damage = applyArith(damageField.value().value_or(0.0), stack.damageOps);
300
301 ModifierStack merged = stack;
302 auto seedDouble = [&](const char* name, double& dest) -> Result<void> {
303 auto value = optionalDouble(object, name, std::string(path) + "." + name);
304 if (!value.ok()) return Result<void>::failure(value.status());
305 if (dest <= 0.0 && value.value().has_value()) dest = *value.value();
306 return Result<void>::success();
307 };
308 if (auto seeded = seedDouble("homing", merged.homing); !seeded)
309 return Result<CarrierRecipe>::failure(seeded.status());
310 if (auto seeded = seedDouble("maxTurnRateDegrees", merged.homing); !seeded)
311 return Result<CarrierRecipe>::failure(seeded.status());
312 if (auto seeded = seedDouble("gravity", merged.gravity); !seeded)
313 return Result<CarrierRecipe>::failure(seeded.status());
314 if (auto seeded = seedDouble("accelerate", merged.accelerate); !seeded)
315 return Result<CarrierRecipe>::failure(seeded.status());
316
317 std::string motionPreset;
318 (void)readString(object, "motion", motionPreset);
319 if (motionPreset == "homing" && merged.homing <= 0.0) merged.homing = 90.0;
320 if (motionPreset == "ballistic" && merged.gravity <= 0.0) merged.gravity = 9.8;
321
322 auto pierceField = optionalInt(object, "pierce", path + ".pierce");
323 if (!pierceField.ok()) return Result<CarrierRecipe>::failure(pierceField.status());
324 if (merged.pierce <= 0 && pierceField.value().has_value()) merged.pierce = *pierceField.value();
325 auto bounceField = optionalInt(object, "bounce", path + ".bounce");
326 if (!bounceField.ok()) return Result<CarrierRecipe>::failure(bounceField.status());
327 if (merged.bounce <= 0 && bounceField.value().has_value()) merged.bounce = *bounceField.value();
328
329 Value::Object preset;
330 preset["id"] = Value(id);
331 preset["speed"] = Value(speed);
332 preset["lifetime"] = Value(lifetime);
333 preset["damage"] = Value(damage);
334 if (merged.gravity > 0.0) preset["gravity"] = Value(merged.gravity);
335 if (merged.homing > 0.0) preset["homing"] = Value(merged.homing);
336 if (merged.accelerate > 0.0) preset["accelerate"] = Value(merged.accelerate);
337 if (merged.pierce > 0) preset["pierce"] = Value(static_cast<std::int64_t>(merged.pierce));
338 if (merged.bounce > 0) preset["bounce"] = Value(static_cast<std::int64_t>(merged.bounce));
339 if (merged.fuse > 0.0) preset["fuse"] = Value(merged.fuse);
340 if (merged.splash > 0.0) preset["splash"] = Value(merged.splash);
341
342 std::string element;
343 std::string damageType;
344 (void)readString(object, "element", element);
345 (void)readString(object, "damageType", damageType);
346 if (!merged.element.empty()) element = merged.element;
347 if (!merged.damageType.empty()) damageType = merged.damageType;
348 if (!element.empty()) preset["element"] = Value(element);
349 if (!damageType.empty()) preset["damageType"] = Value(damageType);
350
351 auto recipe = decodeCarrierRecipe(Value(std::move(preset)));
352 if (!recipe.ok()) return recipe;
353 CarrierRecipe built = std::move(recipe).takeValue();
354
355 // Preserve preset motion from decode; only inject curve ops that presets cannot express.
356 if (merged.swayAmplitude > 0.0 || merged.helixAmplitude > 0.0) {
357 std::vector<CarrierMotionOp> motion = built.motionOps;
358 // Insert curve ops before the final IntegrateLinear.
359 if (!motion.empty() && motion.back().kind == CarrierMotionOpKind::IntegrateLinear) motion.pop_back();
360 if (merged.swayAmplitude > 0.0) {
361 CarrierMotionOp op;
363 op.curveAmplitude = merged.swayAmplitude;
364 op.curveFrequencyHz = merged.swayFrequency > 0.0 ? merged.swayFrequency : 2.0;
365 motion.push_back(op);
366 }
367 if (merged.helixAmplitude > 0.0) {
368 CarrierMotionOp op;
370 op.curveAmplitude = merged.helixAmplitude;
371 op.curveFrequencyHz = merged.helixFrequency > 0.0 ? merged.helixFrequency : 2.0;
372 motion.push_back(op);
373 }
374 motion.push_back({CarrierMotionOpKind::IntegrateLinear});
375 built.motionOps = std::move(motion);
376 }
377
378 if (merged.bounce > 0) {
379 for (auto& impact : built.impacts) {
380 if (impact.kind == CarrierImpactKind::Bounce) impact.restitution = merged.restitution;
381 }
382 }
383
384 auto valid = built.validate();
385 if (!valid) return Result<CarrierRecipe>::failure(valid.status());
386 return Result<CarrierRecipe>::success(std::move(built));
387}
388
389} // namespace
390
392 if (!fragments.isArray())
393 return Result<SpellCastPlan>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Fragments must be an array"), std::string(""), {},
394 "weapon.spell.fragments"));
395 if (fragments.arraySize() == 0)
396 return Result<SpellCastPlan>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Fragments must not be empty"), std::string(""), {},
397 "weapon.spell.fragments"));
398
399 ModifierStack stack;
400 std::optional<CarrierRecipe> recipe;
401 for (std::size_t i = 0; i < fragments.arraySize(); ++i) {
402 const std::string path = "[" + std::to_string(i) + "]";
403 const auto* object = fragments.at(i).getIf<Value::Object>();
404 if (!object)
405 return Result<SpellCastPlan>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Fragment must be an object"), std::string(path), {},
406 "weapon.spell.fragments"));
407 std::string kind;
408 if (!readString(*object, "kind", kind) || kind.empty())
409 return Result<SpellCastPlan>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Fragment requires kind"), std::string(path + ".kind"), {},
410 "weapon.spell.fragments"));
411
412 if (kind == "projectile" || kind == "bolt" || kind == "grenade" || kind == "missile") {
413 if (recipe.has_value())
414 return Result<SpellCastPlan>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Only one projectile fragment is supported per cast"), std::string(path), {},
415 "weapon.spell.fragments"));
416 auto built = buildProjectile(*object, stack, path);
417 if (!built.ok()) return Result<SpellCastPlan>::failure(built.status());
418 recipe = std::move(built).takeValue();
419 continue;
420 }
421
422 if (recipe.has_value())
423 return Result<SpellCastPlan>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Modifiers must appear before the projectile fragment"), std::string(path), {},
424 "weapon.spell.fragments"));
425
426 auto applied = applyModifier(stack, *object, kind, path);
427 if (!applied) return Result<SpellCastPlan>::failure(applied.status());
428 }
429
430 if (!recipe.has_value())
431 return Result<SpellCastPlan>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Fragment sequence requires one projectile fragment"), std::string(""), {},
432 "weapon.spell.fragments"));
433
434 SpellCastPlan plan;
435 plan.recipe = std::move(*recipe);
436 plan.volley = stack.volley;
437 return Result<SpellCastPlan>::success(std::move(plan));
438}
439
441 auto parsed = Value::fromJson(json);
442 if (!parsed.ok()) return Result<SpellCastPlan>::failure(parsed.status());
443 return compileSpellFragments(parsed.value());
444}
445
446} // namespace eve::weapon
double value
std::string output
Decode/encode CombatCarrier recipes from canonical eve::Value trees.
Stable, structured diagnostics shared by engine modules.
std::string text
TokenKind kind
std::string name
bool valid
std::weak_ptr< const void > lifetime
float radius
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
double number
std::uint32_t count
std::vector< ArithOp > speedOps
std::string damageType
double helixAmplitude
std::vector< ArithOp > lifetimeOps
std::string element
double splash
double restitution
double swayFrequency
double gravity
double helixFrequency
std::optional< CarrierVolleySpec > volley
double accelerate
double fuse
double swayAmplitude
double homing
std::vector< ArithOp > damageOps
Compile Noita-style spell/modifier fragment sequences into carrier recipes.
TacticalUnit::TurnResources turn
Common time values and an injectable deterministic simulation clock.
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
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
bool isArray() const noexcept
Return true when this value is an array.
Definition Value.h:97
std::size_t arraySize() const
Return the number of array elements.
Definition Value.cpp:100
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
Definition Value.h:180
Value & at(std::size_t index)
Return a bounds-checked array element.
Definition Value.cpp:106
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
bool readString(const eve::Value::Object &object, const char *name, std::string &output)
const EditorValue * field(const EditorValue &value, const char *name)
载具模块入口(eve.Vehicle):定义注册 / 实体工厂 / RTS 命令 / 帧调度。 设计文档:docs/dev/通用载具系统设计.md
Result< CarrierRecipe > decodeCarrierRecipe(const Value &value)
Decode one CarrierRecipe from an owning Value object.
Result< SpellCastPlan > compileSpellFragmentsJson(const std::string &json)
Compile a JSON array string into one cast plan.
Result< SpellCastPlan > compileSpellFragments(const Value &fragments)
Compile a fragment array Value into one cast plan.
CarrierVolleyPattern
Multi-projectile aim pattern for one salvo.
One compiled cast unit ready to register and spawn.
std::optional< CarrierVolleySpec > volley