载入中...
搜索中...
未找到
CarrierRecipeCodec.cpp
浏览该文件的文档.
2
3#include "common/Diagnostic.h"
4#include "common/Time.h"
5
6#include <cmath>
7#include <cstdint>
8#include <limits>
9#include <optional>
10#include <string>
11#include <string_view>
12#include <utility>
13
14namespace eve::weapon {
15namespace {
16
17const Value* field(const Value::Object& object, const char* name) {
18 const auto it = object.find(name);
19 return it == object.end() ? nullptr : &it->second;
20}
21
22bool readString(const Value::Object& object, const char* name, std::string& output) {
23 const auto* value = field(object, name);
24 const auto* text = value ? value->getIf<std::string>() : nullptr;
25 if (!text) return false;
26 output = *text;
27 return true;
28}
29
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.carrier.recipe"));
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.carrier.recipe"));
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.carrier.recipe"));
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.carrier.recipe"));
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.carrier.recipe"));
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.carrier.recipe"));
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.carrier.recipe"));
72}
73
74Result<void> assignOptionalDouble(const Value::Object& object, const char* name, double& dest, std::string_view path) {
75 auto value = optionalDouble(object, name, path);
76 if (!value.ok()) return Result<void>::failure(value.status());
77 if (value.value().has_value()) dest = *value.value();
78 return Result<void>::success();
79}
80
81Result<void> assignOptionalInt(const Value::Object& object, const char* name, int& dest, std::string_view path) {
82 auto value = optionalInt(object, name, path);
83 if (!value.ok()) return Result<void>::failure(value.status());
84 if (value.value().has_value()) dest = *value.value();
85 return Result<void>::success();
86}
87
88Result<Duration> readSeconds(const Value::Object& object, const char* name, std::string_view path) {
89 auto seconds = optionalDouble(object, name, path);
90 if (!seconds.ok()) return Result<Duration>::failure(seconds.status());
91 if (!seconds.value().has_value() || !(*seconds.value() > 0.0))
92 return Result<Duration>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Expected positive seconds"), std::string(path), {},
93 "weapon.carrier.recipe"));
94 auto duration = Duration::fromSeconds(*seconds.value());
95 if (!duration.ok()) return Result<Duration>::failure(duration.status());
96 return Result<Duration>::success(duration.value());
97}
98
99Result<LogicalId> readLogicalId(const Value::Object& object, const char* name, std::string_view path) {
100 std::string text;
101 if (!readString(object, name, text) || text.empty())
102 return Result<LogicalId>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Expected non-empty logical id"), std::string(path), {},
103 "weapon.carrier.recipe"));
104 auto parsed = LogicalId::parse(text);
105 if (!parsed.has_value())
106 return Result<LogicalId>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Logical id must be namespace:name"), std::string(path), {},
107 "weapon.carrier.recipe"));
108 return Result<LogicalId>::success(std::move(*parsed));
109}
110
111Result<CarrierMotionOpKind> parseMotionKind(std::string_view text, std::string_view path) {
112 if (text == "linear" || text == "IntegrateLinear") return Result<CarrierMotionOpKind>::success(CarrierMotionOpKind::IntegrateLinear);
113 if (text == "gravity" || text == "ApplyGravity") return Result<CarrierMotionOpKind>::success(CarrierMotionOpKind::ApplyGravity);
114 if (text == "homing" || text == "SteerHoming") return Result<CarrierMotionOpKind>::success(CarrierMotionOpKind::SteerHoming);
115 if (text == "accelerate" || text == "Accelerate") return Result<CarrierMotionOpKind>::success(CarrierMotionOpKind::Accelerate);
116 if (text == "sway" || text == "CurveSway") return Result<CarrierMotionOpKind>::success(CarrierMotionOpKind::CurveSway);
117 if (text == "helix" || text == "CurveHelix") return Result<CarrierMotionOpKind>::success(CarrierMotionOpKind::CurveHelix);
118 if (text == "avoidBody" || text == "SteerAvoidBody") return Result<CarrierMotionOpKind>::success(CarrierMotionOpKind::SteerAvoidBody);
119 return Result<CarrierMotionOpKind>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Unknown motion kind"), std::string(path), {},
120 "weapon.carrier.recipe"));
121}
122
123Result<CarrierTriggerKind> parseTriggerKind(std::string_view text, std::string_view path) {
124 if (text == "onExpire" || text == "OnExpire") return Result<CarrierTriggerKind>::success(CarrierTriggerKind::OnExpire);
125 if (text == "onFuse" || text == "OnFuse") return Result<CarrierTriggerKind>::success(CarrierTriggerKind::OnFuse);
126 if (text == "onInterval" || text == "OnInterval") return Result<CarrierTriggerKind>::success(CarrierTriggerKind::OnInterval);
127 if (text == "onHit" || text == "OnHit") return Result<CarrierTriggerKind>::success(CarrierTriggerKind::OnHit);
128 if (text == "onProximity" || text == "OnProximity") return Result<CarrierTriggerKind>::success(CarrierTriggerKind::OnProximity);
129 return Result<CarrierTriggerKind>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Unknown trigger kind"), std::string(path), {},
130 "weapon.carrier.recipe"));
131}
132
133Result<CarrierImpactKind> parseImpactKind(std::string_view text, std::string_view path) {
134 if (text == "emitHit" || text == "hit" || text == "EmitHit") return Result<CarrierImpactKind>::success(CarrierImpactKind::EmitHit);
135 if (text == "splash" || text == "Splash") return Result<CarrierImpactKind>::success(CarrierImpactKind::Splash);
136 if (text == "pierce" || text == "Pierce") return Result<CarrierImpactKind>::success(CarrierImpactKind::Pierce);
137 if (text == "bounce" || text == "Bounce") return Result<CarrierImpactKind>::success(CarrierImpactKind::Bounce);
138 if (text == "release" || text == "Release") return Result<CarrierImpactKind>::success(CarrierImpactKind::Release);
139 if (text == "spawnChild" || text == "SpawnChild") return Result<CarrierImpactKind>::success(CarrierImpactKind::SpawnChild);
140 return Result<CarrierImpactKind>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Unknown impact kind"), std::string(path), {},
141 "weapon.carrier.recipe"));
142}
143
144const char* motionKindName(CarrierMotionOpKind kind) {
145 switch (kind) {
146 case CarrierMotionOpKind::SteerHoming: return "homing";
147 case CarrierMotionOpKind::SteerAvoidBody: return "avoidBody";
148 case CarrierMotionOpKind::ApplyGravity: return "gravity";
149 case CarrierMotionOpKind::Accelerate: return "accelerate";
150 case CarrierMotionOpKind::CurveSway: return "sway";
151 case CarrierMotionOpKind::CurveHelix: return "helix";
152 case CarrierMotionOpKind::IntegrateLinear: return "linear";
153 }
154 return "linear";
155}
156
157const char* triggerKindName(CarrierTriggerKind kind) {
158 switch (kind) {
159 case CarrierTriggerKind::OnExpire: return "onExpire";
160 case CarrierTriggerKind::OnFuse: return "onFuse";
161 case CarrierTriggerKind::OnInterval: return "onInterval";
162 case CarrierTriggerKind::OnHit: return "onHit";
163 case CarrierTriggerKind::OnProximity: return "onProximity";
164 }
165 return "onExpire";
166}
167
168const char* impactKindName(CarrierImpactKind kind) {
169 switch (kind) {
170 case CarrierImpactKind::EmitHit: return "emitHit";
171 case CarrierImpactKind::Splash: return "splash";
172 case CarrierImpactKind::Pierce: return "pierce";
173 case CarrierImpactKind::Bounce: return "bounce";
174 case CarrierImpactKind::Release: return "release";
175 case CarrierImpactKind::SpawnChild: return "spawnChild";
176 }
177 return "release";
178}
179
180Result<CarrierMotionOp> decodeMotionOp(const Value& value, std::string path) {
181 CarrierMotionOp op;
182 if (const auto* text = value.getIf<std::string>()) {
183 auto kind = parseMotionKind(*text, path);
184 if (!kind.ok()) return Result<CarrierMotionOp>::failure(kind.status());
185 op.kind = kind.value();
187 }
188 const auto* object = value.getIf<Value::Object>();
189 if (!object) return Result<CarrierMotionOp>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Motion op must be string or object"), std::string(path), {},
190 "weapon.carrier.recipe"));
191 std::string kindText;
192 if (!readString(*object, "kind", kindText))
193 return Result<CarrierMotionOp>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Motion op requires kind"), std::string(path + ".kind"), {},
194 "weapon.carrier.recipe"));
195 auto kind = parseMotionKind(kindText, path + ".kind");
196 if (!kind.ok()) return Result<CarrierMotionOp>::failure(kind.status());
197 op.kind = kind.value();
198 if (auto assigned = assignOptionalDouble(*object, "gravity", op.gravity, path + ".gravity"); !assigned)
199 return Result<CarrierMotionOp>::failure(assigned.status());
200 if (auto assigned =
201 assignOptionalDouble(*object, "maxTurnRateDegrees", op.maxTurnRateDegrees, path + ".maxTurnRateDegrees");
202 !assigned)
203 return Result<CarrierMotionOp>::failure(assigned.status());
204 if (op.maxTurnRateDegrees == 0.0) {
205 if (auto assigned = assignOptionalDouble(*object, "turnRate", op.maxTurnRateDegrees, path + ".turnRate");
206 !assigned)
207 return Result<CarrierMotionOp>::failure(assigned.status());
208 }
209 if (auto assigned = assignOptionalDouble(*object, "acceleration", op.acceleration, path + ".acceleration");
210 !assigned)
211 return Result<CarrierMotionOp>::failure(assigned.status());
212 if (auto assigned = assignOptionalDouble(*object, "avoidLookAhead", op.avoidLookAhead, path + ".avoidLookAhead");
213 !assigned)
214 return Result<CarrierMotionOp>::failure(assigned.status());
215 if (auto assigned = assignOptionalDouble(*object, "avoidStrength", op.avoidStrength, path + ".avoidStrength");
216 !assigned)
217 return Result<CarrierMotionOp>::failure(assigned.status());
218 if (auto assigned =
219 assignOptionalDouble(*object, "avoidRadiusPadding", op.avoidRadiusPadding, path + ".avoidRadiusPadding");
220 !assigned)
221 return Result<CarrierMotionOp>::failure(assigned.status());
222 if (auto assigned = assignOptionalDouble(*object, "curveAmplitude", op.curveAmplitude, path + ".curveAmplitude");
223 !assigned)
224 return Result<CarrierMotionOp>::failure(assigned.status());
225 if (op.curveAmplitude == 0.0) {
226 if (auto assigned = assignOptionalDouble(*object, "amplitude", op.curveAmplitude, path + ".amplitude");
227 !assigned)
228 return Result<CarrierMotionOp>::failure(assigned.status());
229 }
230 if (auto assigned =
231 assignOptionalDouble(*object, "curveFrequencyHz", op.curveFrequencyHz, path + ".curveFrequencyHz");
232 !assigned)
233 return Result<CarrierMotionOp>::failure(assigned.status());
234 if (op.curveFrequencyHz == 0.0) {
235 if (auto assigned = assignOptionalDouble(*object, "frequency", op.curveFrequencyHz, path + ".frequency");
236 !assigned)
237 return Result<CarrierMotionOp>::failure(assigned.status());
238 }
240}
241
242Result<CarrierTrigger> decodeTrigger(const Value& value, std::string path) {
243 CarrierTrigger trigger;
244 if (const auto* text = value.getIf<std::string>()) {
245 auto kind = parseTriggerKind(*text, path);
246 if (!kind.ok()) return Result<CarrierTrigger>::failure(kind.status());
247 trigger.kind = kind.value();
248 return Result<CarrierTrigger>::success(trigger);
249 }
250 const auto* object = value.getIf<Value::Object>();
251 if (!object) return Result<CarrierTrigger>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Trigger must be string or object"), std::string(path), {},
252 "weapon.carrier.recipe"));
253 std::string kindText;
254 if (!readString(*object, "kind", kindText))
255 return Result<CarrierTrigger>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Trigger requires kind"), std::string(path + ".kind"), {},
256 "weapon.carrier.recipe"));
257 auto kind = parseTriggerKind(kindText, path + ".kind");
258 if (!kind.ok()) return Result<CarrierTrigger>::failure(kind.status());
259 trigger.kind = kind.value();
260
261 auto fuseField = optionalDouble(*object, "fuse", path + ".fuse");
262 if (!fuseField.ok()) return Result<CarrierTrigger>::failure(fuseField.status());
263 auto secondsField = optionalDouble(*object, "seconds", path + ".seconds");
264 if (!secondsField.ok()) return Result<CarrierTrigger>::failure(secondsField.status());
265 const auto fuseSeconds = fuseField.value().has_value() ? fuseField.value() : secondsField.value();
266 if (fuseSeconds.has_value()) {
267 auto duration = Duration::fromSeconds(*fuseSeconds);
268 if (!duration.ok()) return Result<CarrierTrigger>::failure(duration.status());
269 trigger.fuse = duration.value();
270 }
271
272 auto intervalField = optionalDouble(*object, "interval", path + ".interval");
273 if (!intervalField.ok()) return Result<CarrierTrigger>::failure(intervalField.status());
274 if (intervalField.value().has_value()) {
275 auto duration = Duration::fromSeconds(*intervalField.value());
276 if (!duration.ok()) return Result<CarrierTrigger>::failure(duration.status());
277 trigger.interval = duration.value();
278 }
279
280 if (auto assigned =
281 assignOptionalDouble(*object, "proximityRadius", trigger.proximityRadius, path + ".proximityRadius");
282 !assigned)
283 return Result<CarrierTrigger>::failure(assigned.status());
284 if (trigger.proximityRadius == 0.0) {
285 if (auto assigned = assignOptionalDouble(*object, "radius", trigger.proximityRadius, path + ".radius");
286 !assigned)
287 return Result<CarrierTrigger>::failure(assigned.status());
288 }
289 return Result<CarrierTrigger>::success(trigger);
290}
291
292Result<CarrierImpact> decodeImpact(const Value& value, std::string path) {
293 const auto* object = value.getIf<Value::Object>();
294 if (!object) return Result<CarrierImpact>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Impact must be an object"), std::string(path), {},
295 "weapon.carrier.recipe"));
296 CarrierImpact impact;
297 std::string kindText;
298 if (!readString(*object, "kind", kindText))
299 return Result<CarrierImpact>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Impact requires kind"), std::string(path + ".kind"), {},
300 "weapon.carrier.recipe"));
301 auto kind = parseImpactKind(kindText, path + ".kind");
302 if (!kind.ok()) return Result<CarrierImpact>::failure(kind.status());
303 impact.kind = kind.value();
304 std::string onText;
305 if (readString(*object, "on", onText)) {
306 auto on = parseTriggerKind(onText, path + ".on");
307 if (!on.ok()) return Result<CarrierImpact>::failure(on.status());
308 impact.on = on.value();
309 }
310 if (auto assigned = assignOptionalDouble(*object, "damage", impact.damage, path + ".damage"); !assigned)
311 return Result<CarrierImpact>::failure(assigned.status());
312 if (auto assigned = assignOptionalDouble(*object, "splashRadius", impact.splashRadius, path + ".splashRadius");
313 !assigned)
314 return Result<CarrierImpact>::failure(assigned.status());
315 if (impact.splashRadius == 0.0) {
316 if (auto assigned = assignOptionalDouble(*object, "radius", impact.splashRadius, path + ".radius"); !assigned)
317 return Result<CarrierImpact>::failure(assigned.status());
318 }
319 if (auto assigned = assignOptionalInt(*object, "pierceCount", impact.pierceCount, path + ".pierceCount"); !assigned)
320 return Result<CarrierImpact>::failure(assigned.status());
321 if (impact.pierceCount == 0) {
322 if (auto assigned = assignOptionalInt(*object, "count", impact.pierceCount, path + ".count"); !assigned)
323 return Result<CarrierImpact>::failure(assigned.status());
324 }
325 if (auto assigned = assignOptionalInt(*object, "bounceCount", impact.bounceCount, path + ".bounceCount"); !assigned)
326 return Result<CarrierImpact>::failure(assigned.status());
327 if (auto assigned = assignOptionalDouble(*object, "restitution", impact.restitution, path + ".restitution");
328 !assigned)
329 return Result<CarrierImpact>::failure(assigned.status());
330 (void)readString(*object, "damageType", impact.damageType);
331 (void)readString(*object, "element", impact.element);
332 if (const auto* child = field(*object, "childRecipeId")) {
333 if (const auto* text = child->getIf<std::string>()) {
334 auto parsed = LogicalId::parse(*text);
335 if (!parsed.has_value())
336 return Result<CarrierImpact>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Invalid childRecipeId"), std::string(path + ".childRecipeId"), {},
337 "weapon.carrier.recipe"));
338 impact.childRecipeId = std::move(*parsed);
339 } else if (!child->isNull()) {
340 return Result<CarrierImpact>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("childRecipeId must be a string"), std::string(path + ".childRecipeId"), {},
341 "weapon.carrier.recipe"));
342 }
343 }
344 return Result<CarrierImpact>::success(impact);
345}
346
347Result<CarrierRecipe> decodePreset(const Value::Object& object) {
348 CarrierRecipe recipe;
349 auto id = readLogicalId(object, "id", "id");
350 if (!id.ok()) return Result<CarrierRecipe>::failure(id.status());
351 recipe.id = id.value();
352
353 auto lifetime = readSeconds(object, "lifetime", "lifetime");
354 if (!lifetime.ok()) return Result<CarrierRecipe>::failure(lifetime.status());
355 recipe.lifetime = lifetime.value();
356
357 auto speedField = optionalDouble(object, "speed", "speed");
358 if (!speedField.ok()) return Result<CarrierRecipe>::failure(speedField.status());
359 if (!speedField.value().has_value() || !(*speedField.value() > 0.0))
360 return Result<CarrierRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Preset requires positive speed"), std::string("speed"), {},
361 "weapon.carrier.recipe"));
362 recipe.speed = *speedField.value();
363
364 double damage = 0.0;
365 if (auto assigned = assignOptionalDouble(object, "damage", damage, "damage"); !assigned)
366 return Result<CarrierRecipe>::failure(assigned.status());
367 std::string damageType;
368 std::string element;
369 (void)readString(object, "damageType", damageType);
370 (void)readString(object, "element", element);
371
372 double gravity = 0.0;
373 double homing = 0.0;
374 double accelerate = 0.0;
375 if (auto assigned = assignOptionalDouble(object, "gravity", gravity, "gravity"); !assigned)
376 return Result<CarrierRecipe>::failure(assigned.status());
377 if (auto assigned = assignOptionalDouble(object, "homing", homing, "homing"); !assigned)
378 return Result<CarrierRecipe>::failure(assigned.status());
379 if (homing == 0.0) {
380 if (auto assigned = assignOptionalDouble(object, "maxTurnRateDegrees", homing, "maxTurnRateDegrees"); !assigned)
381 return Result<CarrierRecipe>::failure(assigned.status());
382 }
383 if (auto assigned = assignOptionalDouble(object, "accelerate", accelerate, "accelerate"); !assigned)
384 return Result<CarrierRecipe>::failure(assigned.status());
385
386 int pierce = 0;
387 int bounce = 0;
388 if (auto assigned = assignOptionalInt(object, "pierce", pierce, "pierce"); !assigned)
389 return Result<CarrierRecipe>::failure(assigned.status());
390 if (auto assigned = assignOptionalInt(object, "bounce", bounce, "bounce"); !assigned)
391 return Result<CarrierRecipe>::failure(assigned.status());
392
393 double fuse = 0.0;
394 double splash = 0.0;
395 if (auto assigned = assignOptionalDouble(object, "fuse", fuse, "fuse"); !assigned)
396 return Result<CarrierRecipe>::failure(assigned.status());
397 if (auto assigned = assignOptionalDouble(object, "splash", splash, "splash"); !assigned)
398 return Result<CarrierRecipe>::failure(assigned.status());
399
400 std::string motionPreset = "linear";
401 (void)readString(object, "motion", motionPreset);
402
403 if (homing > 0.0 || motionPreset == "homing") {
404 CarrierMotionOp op;
406 op.maxTurnRateDegrees = homing > 0.0 ? homing : 90.0;
407 recipe.motionOps.push_back(op);
408 }
409 if (gravity > 0.0 || motionPreset == "ballistic") {
410 CarrierMotionOp op;
412 op.gravity = gravity > 0.0 ? gravity : 9.8;
413 recipe.motionOps.push_back(op);
414 }
415 if (accelerate > 0.0) {
416 CarrierMotionOp op;
418 op.acceleration = accelerate;
419 recipe.motionOps.push_back(op);
420 }
421 recipe.motionOps.push_back({CarrierMotionOpKind::IntegrateLinear});
422
423 recipe.triggers.push_back({CarrierTriggerKind::OnExpire});
424 recipe.triggers.push_back({CarrierTriggerKind::OnHit});
425 if (fuse > 0.0) {
426 CarrierTrigger trigger;
427 trigger.kind = CarrierTriggerKind::OnFuse;
429 if (!duration.ok()) return Result<CarrierRecipe>::failure(duration.status());
430 trigger.fuse = duration.value();
431 recipe.triggers.push_back(trigger);
432 }
433
434 if (damage > 0.0 || !element.empty() || !damageType.empty()) {
435 CarrierImpact hit;
438 hit.damage = damage;
439 hit.damageType = damageType;
440 hit.element = element;
441 recipe.impacts.push_back(hit);
442 }
443 if (pierce > 0) {
444 CarrierImpact impact;
445 impact.kind = CarrierImpactKind::Pierce;
446 impact.on = CarrierTriggerKind::OnHit;
447 impact.pierceCount = pierce;
448 recipe.impacts.push_back(impact);
449 }
450 if (bounce > 0) {
451 CarrierImpact impact;
452 impact.kind = CarrierImpactKind::Bounce;
453 impact.on = CarrierTriggerKind::OnHit;
454 impact.bounceCount = bounce;
455 recipe.impacts.push_back(impact);
456 }
457 recipe.impacts.push_back({CarrierImpactKind::Release, CarrierTriggerKind::OnHit});
458
459 if (fuse > 0.0) {
460 CarrierImpact splashImpact;
461 splashImpact.kind = splash > 0.0 ? CarrierImpactKind::Splash : CarrierImpactKind::EmitHit;
462 splashImpact.on = CarrierTriggerKind::OnFuse;
463 splashImpact.damage = damage;
464 splashImpact.splashRadius = splash;
465 splashImpact.damageType = damageType;
466 splashImpact.element = element;
467 recipe.impacts.push_back(splashImpact);
468 recipe.impacts.push_back({CarrierImpactKind::Release, CarrierTriggerKind::OnFuse});
469 }
470 if (splash > 0.0 && fuse <= 0.0) {
471 CarrierImpact splashImpact;
472 splashImpact.kind = CarrierImpactKind::Splash;
473 splashImpact.on = CarrierTriggerKind::OnHit;
474 splashImpact.damage = damage;
475 splashImpact.splashRadius = splash;
476 splashImpact.damageType = damageType;
477 splashImpact.element = element;
478 recipe.impacts.push_back(splashImpact);
479 }
480
481 recipe.impacts.push_back({CarrierImpactKind::Release, CarrierTriggerKind::OnExpire});
482
483 auto valid = recipe.validate();
484 if (!valid) return Result<CarrierRecipe>::failure(valid.status());
485 return Result<CarrierRecipe>::success(std::move(recipe));
486}
487
488Result<CarrierRecipe> decodeFull(const Value::Object& object) {
489 CarrierRecipe recipe;
490 auto id = readLogicalId(object, "id", "id");
491 if (!id.ok()) return Result<CarrierRecipe>::failure(id.status());
492 recipe.id = id.value();
493
494 auto lifetime = readSeconds(object, "lifetime", "lifetime");
495 if (!lifetime.ok()) return Result<CarrierRecipe>::failure(lifetime.status());
496 recipe.lifetime = lifetime.value();
497
498 auto speedField = optionalDouble(object, "speed", "speed");
499 if (!speedField.ok()) return Result<CarrierRecipe>::failure(speedField.status());
500 if (!speedField.value().has_value() || !(*speedField.value() > 0.0))
501 return Result<CarrierRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Recipe requires positive speed"), std::string("speed"), {},
502 "weapon.carrier.recipe"));
503 recipe.speed = *speedField.value();
504
505 const auto* motion = field(object, "motion");
506 if (!motion) motion = field(object, "motionOps");
507 if (!motion || !motion->isArray())
508 return Result<CarrierRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Full recipe requires motion array"), std::string("motion"), {},
509 "weapon.carrier.recipe"));
510 for (std::size_t i = 0; i < motion->arraySize(); ++i) {
511 auto op = decodeMotionOp(motion->at(i), "motion[" + std::to_string(i) + "]");
512 if (!op.ok()) return Result<CarrierRecipe>::failure(op.status());
513 recipe.motionOps.push_back(op.value());
514 }
515
516 const auto* triggers = field(object, "triggers");
517 if (!triggers || !triggers->isArray())
518 return Result<CarrierRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Full recipe requires triggers array"), std::string("triggers"), {},
519 "weapon.carrier.recipe"));
520 for (std::size_t i = 0; i < triggers->arraySize(); ++i) {
521 auto trigger = decodeTrigger(triggers->at(i), "triggers[" + std::to_string(i) + "]");
522 if (!trigger.ok()) return Result<CarrierRecipe>::failure(trigger.status());
523 recipe.triggers.push_back(trigger.value());
524 }
525
526 const auto* impacts = field(object, "impacts");
527 if (!impacts || !impacts->isArray())
528 return Result<CarrierRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Full recipe requires impacts array"), std::string("impacts"), {},
529 "weapon.carrier.recipe"));
530 for (std::size_t i = 0; i < impacts->arraySize(); ++i) {
531 auto impact = decodeImpact(impacts->at(i), "impacts[" + std::to_string(i) + "]");
532 if (!impact.ok()) return Result<CarrierRecipe>::failure(impact.status());
533 recipe.impacts.push_back(impact.value());
534 }
535
536 auto valid = recipe.validate();
537 if (!valid) return Result<CarrierRecipe>::failure(valid.status());
538 return Result<CarrierRecipe>::success(std::move(recipe));
539}
540
541} // namespace
542
544 const auto* object = value.getIf<Value::Object>();
545 if (!object) return Result<CarrierRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("Carrier recipe must be an object"), std::string(""), {},
546 "weapon.carrier.recipe"));
547 if (field(*object, "motionOps") || field(*object, "triggers") || field(*object, "impacts")) {
548 if (const auto* motion = field(*object, "motion"); motion && motion->isString()) {
549 // Preset with convenience motion string plus optional explicit arrays is ambiguous —
550 // prefer full form only when motion is an array or motionOps is present.
551 }
552 if (field(*object, "motionOps") || (field(*object, "motion") && field(*object, "motion")->isArray()))
553 return decodeFull(*object);
554 }
555 if (field(*object, "motion") && field(*object, "motion")->isArray() && field(*object, "triggers") &&
556 field(*object, "impacts"))
557 return decodeFull(*object);
558 return decodePreset(*object);
559}
560
562 auto valid = recipe.validate();
563 if (!valid) return Result<Value>::failure(valid.status());
564
566 object.emplace("id", Value(recipe.id.format()));
567 object.emplace("lifetime", Value(recipe.lifetime.seconds()));
568 object.emplace("speed", Value(recipe.speed));
569
570 Value::Array motion;
571 for (const auto& op : recipe.motionOps) {
572 Value::Object entry;
573 entry.emplace("kind", Value(std::string(motionKindName(op.kind))));
574 if (op.gravity != 0.0) entry.emplace("gravity", Value(op.gravity));
575 if (op.maxTurnRateDegrees != 0.0) entry.emplace("maxTurnRateDegrees", Value(op.maxTurnRateDegrees));
576 if (op.acceleration != 0.0) entry.emplace("acceleration", Value(op.acceleration));
577 if (op.avoidLookAhead != 0.0) entry.emplace("avoidLookAhead", Value(op.avoidLookAhead));
578 if (op.avoidStrength != 1.0) entry.emplace("avoidStrength", Value(op.avoidStrength));
579 if (op.avoidRadiusPadding != 0.0) entry.emplace("avoidRadiusPadding", Value(op.avoidRadiusPadding));
580 if (op.curveAmplitude != 0.0) entry.emplace("curveAmplitude", Value(op.curveAmplitude));
581 if (op.curveFrequencyHz != 0.0) entry.emplace("curveFrequencyHz", Value(op.curveFrequencyHz));
582 motion.push_back(Value(std::move(entry)));
583 }
584 object.emplace("motion", Value(std::move(motion)));
585
586 Value::Array triggers;
587 for (const auto& trigger : recipe.triggers) {
588 Value::Object entry;
589 entry.emplace("kind", Value(std::string(triggerKindName(trigger.kind))));
590 if (trigger.fuse > Duration::zero()) entry.emplace("fuse", Value(trigger.fuse.seconds()));
591 if (trigger.interval > Duration::zero()) entry.emplace("interval", Value(trigger.interval.seconds()));
592 if (trigger.proximityRadius > 0.0) entry.emplace("proximityRadius", Value(trigger.proximityRadius));
593 triggers.push_back(Value(std::move(entry)));
594 }
595 object.emplace("triggers", Value(std::move(triggers)));
596
597 Value::Array impacts;
598 for (const auto& impact : recipe.impacts) {
599 Value::Object entry;
600 entry.emplace("kind", Value(std::string(impactKindName(impact.kind))));
601 entry.emplace("on", Value(std::string(triggerKindName(impact.on))));
602 if (impact.damage != 0.0) entry.emplace("damage", Value(impact.damage));
603 if (impact.splashRadius != 0.0) entry.emplace("splashRadius", Value(impact.splashRadius));
604 if (impact.pierceCount != 0) entry.emplace("pierceCount", Value(static_cast<std::int64_t>(impact.pierceCount)));
605 if (impact.bounceCount != 0) entry.emplace("bounceCount", Value(static_cast<std::int64_t>(impact.bounceCount)));
606 if (impact.restitution != 1.0) entry.emplace("restitution", Value(impact.restitution));
607 if (!impact.damageType.empty()) entry.emplace("damageType", Value(impact.damageType));
608 if (!impact.element.empty()) entry.emplace("element", Value(impact.element));
609 if (impact.childRecipeId.isValid()) entry.emplace("childRecipeId", Value(impact.childRecipeId.format()));
610 impacts.push_back(Value(std::move(entry)));
611 }
612 object.emplace("impacts", Value(std::move(impacts)));
613 return Result<Value>::success(Value(std::move(object)));
614}
615
616} // namespace eve::weapon
double value
float duration
std::string output
Decode/encode CombatCarrier recipes from canonical eve::Value trees.
Stable, structured diagnostics shared by engine modules.
wgpu::PopErrorScopeStatus status
std::string text
TokenKind kind
std::string name
bool valid
std::weak_ptr< const void > lifetime
std::string path
Definition PlayHost.cpp:110
bool hit
double number
std::string damageType
std::string element
double splash
double gravity
double accelerate
double fuse
double homing
Json object
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
static Result< Duration > fromSeconds(double seconds)
Convert finite seconds to the nearest nanosecond.
Definition Time.cpp:16
static constexpr Duration zero() noexcept
Return zero duration.
Definition Time.h:60
double seconds() const noexcept
Return this duration as seconds for legacy/presentation APIs.
Definition Time.cpp:28
const std::string & format() const noexcept
Returns the canonical namespace:name representation.
Definition Identity.h:436
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
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
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
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)
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:635
载具模块入口(eve.Vehicle):定义注册 / 实体工厂 / RTS 命令 / 帧调度。 设计文档:docs/dev/通用载具系统设计.md
CarrierImpactKind
What happens when a matching trigger fires.
CarrierMotionOpKind
Ordered ballistic operators applied each simulation step.
Result< Value > encodeCarrierRecipe(const CarrierRecipe &recipe)
Encode one CarrierRecipe into an owning Value object (full form).
Result< CarrierRecipe > decodeCarrierRecipe(const Value &value)
Decode one CarrierRecipe from an owning Value object.
CarrierTriggerKind
When an impact may fire.
Data-driven composition recipe for one carrier family.
std::vector< CarrierTrigger > triggers
Result< void > validate() const
Validate finite parameters and composition constraints.
std::vector< CarrierMotionOp > motionOps
std::vector< CarrierImpact > impacts