载入中...
搜索中...
未找到
AttackVfxRecipe.cpp
浏览该文件的文档.
2
3#include "common/Value.h"
4
5#include <cmath>
6#include <limits>
7#include <set>
8#include <utility>
9
10namespace eve::stylize {
11namespace {
12
13Result<void> rejectUnknown(const Value::Object& object, const std::set<std::string>& allowed,
14 const std::string& path) {
15 for (const auto& [key, value] : object) {
16 (void)value;
17 if (!allowed.contains(key))
18 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("unknown attack VFX field"), std::string(path + "." + key)));
19 }
20 return Result<void>::success();
21}
22
23Result<float> number(const Value& value, const std::string& path) {
24 if (const auto* v = value.getIf<double>()) {
25 if (!std::isfinite(*v)) return Result<float>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected finite number"), std::string(path)));
26 return Result<float>::success(static_cast<float>(*v));
27 }
28 if (const auto* v = value.getIf<std::int64_t>())
29 return Result<float>::success(static_cast<float>(*v));
30 return Result<float>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected number"), std::string(path)));
31}
32
33Result<double> number64(const Value& value, const std::string& path) {
34 if (const auto* v = value.getIf<double>()) {
35 if (!std::isfinite(*v)) return Result<double>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected finite number"), std::string(path)));
37 }
38 if (const auto* v = value.getIf<std::int64_t>()) return Result<double>::success(static_cast<double>(*v));
39 return Result<double>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected number"), std::string(path)));
40}
41
42Result<LogicalId> parseLogicalId(const Value& value, const std::string& path) {
43 if (!value.isString()) return Result<LogicalId>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected logical id string"), std::string(path)));
44 auto parsed = LogicalId::parse(value.asString());
45 if (!parsed) return Result<LogicalId>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("logical id must be namespace:name"), std::string(path)));
46 return Result<LogicalId>::success(std::move(*parsed));
47}
48
49bool finiteUnit(float value) { return std::isfinite(value) && value >= 0.f && value <= 1.f; }
50
51Result<AttackVfxVec3> parseVec3(const Value& value, const std::string& path) {
52 const auto* array = value.getIf<Value::Array>();
53 if (!array || array->size() != 3)
54 return Result<AttackVfxVec3>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected three-number array"), std::string(path)));
55 AttackVfxVec3 out;
56 float* components[] = {&out.x, &out.y, &out.z};
57 for (std::size_t i = 0; i < 3; ++i) {
58 auto parsed = number((*array)[i], path + "[" + std::to_string(i) + "]");
59 if (!parsed) return Result<AttackVfxVec3>::failure(parsed.status());
60 *components[i] = std::move(parsed).takeValue();
61 }
63}
64
65Result<AttackVfxPhaseKind> parsePhaseKind(const Value& value, const std::string& path) {
66 if (!value.isString()) return Result<AttackVfxPhaseKind>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected string"), std::string(path)));
67 const auto& text = value.asString();
74 return Result<AttackVfxPhaseKind>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("unknown phase kind"), std::string(path)));
75}
76
77Result<AttackVfxLayerRole> parseLayerRole(const Value& value, const std::string& path) {
78 if (!value.isString()) return Result<AttackVfxLayerRole>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected string"), std::string(path)));
79 const auto& text = value.asString();
89 return Result<AttackVfxLayerRole>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("unknown layer role"), std::string(path)));
90}
91
92Result<AttackVfxStopBehavior> parseStopBehavior(const Value& value, const std::string& path) {
93 if (!value.isString())
94 return Result<AttackVfxStopBehavior>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected string"), std::string(path)));
95 const auto& text = value.asString();
97 if (text == "clearImmediately")
99 return Result<AttackVfxStopBehavior>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("unknown stopBehavior"), std::string(path)));
100}
101
102Result<AttackVfxSpatialAttachment> parseAttachment(const Value& value, const std::string& path) {
103 if (!value.isString())
104 return Result<AttackVfxSpatialAttachment>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected string"), std::string(path)));
105 const auto& text = value.asString();
106 if (text == "followTarget")
108 if (text == "followPositionOnly")
110 if (text == "worldTransformAtStart")
112 return Result<AttackVfxSpatialAttachment>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("unknown attachment"), std::string(path)));
113}
114
115Result<AttackVfxSpatialAnchor> parseAnchor(const Value& value, const std::string& path) {
116 if (!value.isString())
117 return Result<AttackVfxSpatialAnchor>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected string"), std::string(path)));
118 const auto& text = value.asString();
121 return Result<AttackVfxSpatialAnchor>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("unknown anchor"), std::string(path)));
122}
123
124Result<AttackVfxSpatial> parseSpatial(const Value* value, const std::string& path) {
125 AttackVfxSpatial spatial;
126 if (!value) return Result<AttackVfxSpatial>::success(spatial);
127 const auto* object = value->getIf<Value::Object>();
128 if (!object) return Result<AttackVfxSpatial>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected object"), std::string(path)));
129 auto known = rejectUnknown(*object,
130 {"attachment", "anchor", "targetIndex", "bone", "positionOffset",
131 "rotationOffsetDegrees", "scale"},
132 path);
133 if (!known) return Result<AttackVfxSpatial>::failure(known.status());
134
135 if (const auto it = object->find("attachment"); it != object->end()) {
136 auto parsed = parseAttachment(it->second, path + ".attachment");
137 if (!parsed) return Result<AttackVfxSpatial>::failure(parsed.status());
138 spatial.attachment = std::move(parsed).takeValue();
139 }
140 if (const auto it = object->find("anchor"); it != object->end()) {
141 auto parsed = parseAnchor(it->second, path + ".anchor");
142 if (!parsed) return Result<AttackVfxSpatial>::failure(parsed.status());
143 spatial.anchor = std::move(parsed).takeValue();
144 }
145 if (const auto it = object->find("targetIndex"); it != object->end()) {
146 const auto* index = it->second.getIf<std::int64_t>();
147 if (!index || *index < 0)
148 return Result<AttackVfxSpatial>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("targetIndex must be >= 0"), std::string(path + ".targetIndex")));
149 spatial.targetIndex = static_cast<std::size_t>(*index);
150 }
151 if (const auto it = object->find("bone"); it != object->end()) {
152 if (!it->second.isString())
153 return Result<AttackVfxSpatial>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("bone must be a string"), std::string(path + ".bone")));
154 spatial.bone = it->second.asString();
155 }
156 if (const auto it = object->find("positionOffset"); it != object->end()) {
157 auto parsed = parseVec3(it->second, path + ".positionOffset");
158 if (!parsed) return Result<AttackVfxSpatial>::failure(parsed.status());
159 spatial.positionOffset = std::move(parsed).takeValue();
160 }
161 if (const auto it = object->find("rotationOffsetDegrees"); it != object->end()) {
162 auto parsed = parseVec3(it->second, path + ".rotationOffsetDegrees");
163 if (!parsed) return Result<AttackVfxSpatial>::failure(parsed.status());
164 spatial.rotationOffsetDegrees = std::move(parsed).takeValue();
165 }
166 if (const auto it = object->find("scale"); it != object->end()) {
167 auto parsed = parseVec3(it->second, path + ".scale");
168 if (!parsed) return Result<AttackVfxSpatial>::failure(parsed.status());
169 spatial.scale = std::move(parsed).takeValue();
170 }
171 auto valid = spatial.validate(path);
172 if (!valid) return Result<AttackVfxSpatial>::failure(valid.status());
173 return Result<AttackVfxSpatial>::success(spatial);
174}
175
176Result<std::map<std::string, float>> parseFloatMap(const Value* value, const std::string& path) {
177 std::map<std::string, float> out;
178 if (!value) return Result<std::map<std::string, float>>::success(out);
179 const auto* object = value->getIf<Value::Object>();
180 if (!object)
181 return Result<std::map<std::string, float>>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected object"), std::string(path)));
182 for (const auto& [name, raw] : *object) {
183 auto parsed = number(raw, path + "." + name);
184 if (!parsed) return Result<std::map<std::string, float>>::failure(parsed.status());
185 out.emplace(name, std::move(parsed).takeValue());
186 }
187 return Result<std::map<std::string, float>>::success(std::move(out));
188}
189
190Result<std::map<std::string, std::string>> parseStringMap(const Value* value, const std::string& path) {
191 std::map<std::string, std::string> out;
192 if (!value) return Result<std::map<std::string, std::string>>::success(out);
193 const auto* object = value->getIf<Value::Object>();
194 if (!object)
195 return Result<std::map<std::string, std::string>>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected object"), std::string(path)));
196 for (const auto& [name, raw] : *object) {
197 if (!raw.isString())
198 return Result<std::map<std::string, std::string>>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected string"), std::string(path + "." + name)));
199 out.emplace(name, raw.asString());
200 }
201 return Result<std::map<std::string, std::string>>::success(std::move(out));
202}
203
204Result<AttackVfxLayer> parseLayer(const Value& value, const std::string& path) {
205 const auto* object = value.getIf<Value::Object>();
206 if (!object) return Result<AttackVfxLayer>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected object"), std::string(path)));
207 auto known =
208 rejectUnknown(*object, {"role", "uri", "spatial", "floatParams", "stopBehavior"}, path);
209 if (!known) return Result<AttackVfxLayer>::failure(known.status());
210
211 AttackVfxLayer layer;
212 const auto role = object->find("role");
213 if (role == object->end())
214 return Result<AttackVfxLayer>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("role is required"), std::string(path + ".role")));
215 auto parsedRole = parseLayerRole(role->second, path + ".role");
216 if (!parsedRole)
217 return Result<AttackVfxLayer>::failure(parsedRole.status());
218 layer.role = std::move(parsedRole).takeValue();
219
220 if (const auto it = object->find("uri"); it != object->end()) {
221 if (!it->second.isString())
222 return Result<AttackVfxLayer>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("uri must be a string"), std::string(path + ".uri")));
223 layer.uri = it->second.asString();
224 }
225 if (const auto it = object->find("spatial"); it != object->end()) {
226 auto parsed = parseSpatial(&it->second, path + ".spatial");
227 if (!parsed)
228 return Result<AttackVfxLayer>::failure(parsed.status());
229 layer.spatial = std::move(parsed).takeValue();
230 }
231 if (const auto it = object->find("floatParams"); it != object->end()) {
232 auto parsed = parseFloatMap(&it->second, path + ".floatParams");
233 if (!parsed)
234 return Result<AttackVfxLayer>::failure(parsed.status());
235 layer.floatParams = std::move(parsed).takeValue();
236 }
237 if (const auto it = object->find("stopBehavior"); it != object->end()) {
238 auto parsed = parseStopBehavior(it->second, path + ".stopBehavior");
239 if (!parsed)
240 return Result<AttackVfxLayer>::failure(parsed.status());
241 layer.stopBehavior = std::move(parsed).takeValue();
242 }
243 auto valid = layer.validate(path);
244 if (!valid) return Result<AttackVfxLayer>::failure(valid.status());
245 return Result<AttackVfxLayer>::success(std::move(layer));
246}
247
248Result<AttackVfxPhase> parsePhase(const Value& value, const std::string& path) {
249 const auto* object = value.getIf<Value::Object>();
250 if (!object) return Result<AttackVfxPhase>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected object"), std::string(path)));
251 auto known = rejectUnknown(
252 *object, {"kind", "startCue", "endCue", "startOffsetSeconds", "durationSeconds", "layers"}, path);
253 if (!known) return Result<AttackVfxPhase>::failure(known.status());
254
255 AttackVfxPhase phase;
256 const auto kind = object->find("kind");
257 if (kind == object->end())
258 return Result<AttackVfxPhase>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("kind is required"), std::string(path + ".kind")));
259 auto parsedKind = parsePhaseKind(kind->second, path + ".kind");
260 if (!parsedKind)
261 return Result<AttackVfxPhase>::failure(parsedKind.status());
262 phase.kind = std::move(parsedKind).takeValue();
263
264 if (const auto it = object->find("startCue"); it != object->end()) {
265 if (!it->second.isString())
266 return Result<AttackVfxPhase>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("startCue must be a string"), std::string(path + ".startCue")));
267 phase.startCue = it->second.asString();
268 }
269 if (const auto it = object->find("endCue"); it != object->end()) {
270 if (!it->second.isString())
271 return Result<AttackVfxPhase>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("endCue must be a string"), std::string(path + ".endCue")));
272 phase.endCue = it->second.asString();
273 }
274 if (const auto it = object->find("startOffsetSeconds"); it != object->end()) {
275 auto parsed = number64(it->second, path + ".startOffsetSeconds");
276 if (!parsed)
277 return Result<AttackVfxPhase>::failure(parsed.status());
278 phase.startOffsetSeconds = std::move(parsed).takeValue();
279 }
280 if (const auto it = object->find("durationSeconds"); it != object->end()) {
281 auto parsed = number64(it->second, path + ".durationSeconds");
282 if (!parsed)
283 return Result<AttackVfxPhase>::failure(parsed.status());
284 phase.durationSeconds = std::move(parsed).takeValue();
285 }
286 if (const auto it = object->find("layers"); it != object->end()) {
287 const auto* layers = it->second.getIf<Value::Array>();
288 if (!layers)
289 return Result<AttackVfxPhase>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("layers must be an array"), std::string(path + ".layers")));
290 phase.layers.reserve(layers->size());
291 for (std::size_t i = 0; i < layers->size(); ++i) {
292 auto parsed = parseLayer((*layers)[i], path + ".layers[" + std::to_string(i) + "]");
293 if (!parsed)
294 return Result<AttackVfxPhase>::failure(parsed.status());
295 phase.layers.push_back(std::move(parsed).takeValue());
296 }
297 }
298 auto valid = phase.validate(path);
299 if (!valid) return Result<AttackVfxPhase>::failure(valid.status());
300 return Result<AttackVfxPhase>::success(std::move(phase));
301}
302
303Result<AttackVfxPalette> parsePalette(const Value* value, const std::string& path) {
304 AttackVfxPalette palette;
306 const auto* object = value->getIf<Value::Object>();
307 if (!object) return Result<AttackVfxPalette>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected object"), std::string(path)));
308 auto known = rejectUnknown(*object, {"primary", "secondary", "emissive"}, path);
309 if (!known) return Result<AttackVfxPalette>::failure(known.status());
310 if (const auto it = object->find("primary"); it != object->end()) {
311 auto parsed = parseVec3(it->second, path + ".primary");
312 if (!parsed)
313 return Result<AttackVfxPalette>::failure(parsed.status());
314 palette.primary = std::move(parsed).takeValue();
315 }
316 if (const auto it = object->find("secondary"); it != object->end()) {
317 auto parsed = parseVec3(it->second, path + ".secondary");
318 if (!parsed)
319 return Result<AttackVfxPalette>::failure(parsed.status());
320 palette.secondary = std::move(parsed).takeValue();
321 }
322 if (const auto it = object->find("emissive"); it != object->end()) {
323 auto parsed = parseVec3(it->second, path + ".emissive");
324 if (!parsed)
325 return Result<AttackVfxPalette>::failure(parsed.status());
326 palette.emissive = std::move(parsed).takeValue();
327 }
329}
330
331Result<AttackVfxSkin> parseSkinObject(const Value::Object& object, const std::string& path, bool requireSchema) {
332 auto known = rejectUnknown(object,
333 {"schema", "schemaVersion", "id", "palette", "styleHints", "shakeProfile",
334 "distortionProfile", "statusOverlayUri"},
335 path);
336 if (!known) return Result<AttackVfxSkin>::failure(known.status());
337
338 if (requireSchema) {
339 const auto schema = object.find("schema");
340 if (schema == object.end() || !schema->second.isString() ||
341 schema->second.asString() != "eve.stylize.attack-vfx-skin")
342 return Result<AttackVfxSkin>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("schema must be eve.stylize.attack-vfx-skin"), std::string(path + ".schema")));
343 const auto version = object.find("schemaVersion");
344 if (version == object.end())
345 return Result<AttackVfxSkin>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("schemaVersion is required"), std::string(path + ".schemaVersion")));
346 const auto* ver = version->second.getIf<std::int64_t>();
347 if (!ver || *ver != 1)
348 return Result<AttackVfxSkin>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("unsupported skin schemaVersion"), std::string(path + ".schemaVersion")));
349 }
350
351 AttackVfxSkin skin;
352 const auto id = object.find("id");
353 if (id == object.end())
354 return Result<AttackVfxSkin>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("id is required"), std::string(path + ".id")));
355 auto parsedId = parseLogicalId(id->second, path + ".id");
356 if (!parsedId)
357 return Result<AttackVfxSkin>::failure(parsedId.status());
358 skin.id = std::move(parsedId).takeValue();
359
360 if (const auto it = object.find("palette"); it != object.end()) {
361 auto parsed = parsePalette(&it->second, path + ".palette");
362 if (!parsed)
363 return Result<AttackVfxSkin>::failure(parsed.status());
364 skin.palette = std::move(parsed).takeValue();
365 }
366 if (const auto it = object.find("styleHints"); it != object.end()) {
367 auto parsed = parseStringMap(&it->second, path + ".styleHints");
368 if (!parsed)
369 return Result<AttackVfxSkin>::failure(parsed.status());
370 skin.styleHints = std::move(parsed).takeValue();
371 }
372 if (const auto it = object.find("shakeProfile"); it != object.end()) {
373 auto parsed = parseFloatMap(&it->second, path + ".shakeProfile");
374 if (!parsed)
375 return Result<AttackVfxSkin>::failure(parsed.status());
376 skin.shakeProfile = std::move(parsed).takeValue();
377 }
378 if (const auto it = object.find("distortionProfile"); it != object.end()) {
379 auto parsed = parseFloatMap(&it->second, path + ".distortionProfile");
380 if (!parsed)
381 return Result<AttackVfxSkin>::failure(parsed.status());
382 skin.distortionProfile = std::move(parsed).takeValue();
383 }
384 if (const auto it = object.find("statusOverlayUri"); it != object.end()) {
385 if (!it->second.isString())
386 return Result<AttackVfxSkin>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("statusOverlayUri must be a string"), std::string(path + ".statusOverlayUri")));
387 skin.statusOverlayUri = it->second.asString();
388 }
389 auto valid = skin.validate();
390 if (!valid) return Result<AttackVfxSkin>::failure(valid.status());
391 return Result<AttackVfxSkin>::success(std::move(skin));
392}
393
394Result<AttackVfxBudget> parseBudget(const Value* value, const std::string& path) {
395 AttackVfxBudget budget;
396 if (!value) return Result<AttackVfxBudget>::success(budget);
397 const auto* object = value->getIf<Value::Object>();
398 if (!object) return Result<AttackVfxBudget>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("expected object"), std::string(path)));
399 auto known = rejectUnknown(*object, {"lod", "maxParticles", "allowDistortion"}, path);
400 if (!known) return Result<AttackVfxBudget>::failure(known.status());
401 if (const auto it = object->find("lod"); it != object->end()) {
402 const auto* lod = it->second.getIf<std::int64_t>();
403 if (!lod || *lod < 0)
404 return Result<AttackVfxBudget>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("lod must be >= 0"), std::string(path + ".lod")));
405 if (*lod > static_cast<std::int64_t>(std::numeric_limits<int>::max()))
406 return Result<AttackVfxBudget>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("lod exceeds int range"), std::string(path + ".lod")));
407 budget.lod = static_cast<int>(*lod);
408 }
409 if (const auto it = object->find("maxParticles"); it != object->end()) {
410 const auto* count = it->second.getIf<std::int64_t>();
411 if (!count || *count < 0)
412 return Result<AttackVfxBudget>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("maxParticles must be >= 0"), std::string(path + ".maxParticles")));
413 if (*count > static_cast<std::int64_t>(std::numeric_limits<int>::max()))
414 return Result<AttackVfxBudget>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("maxParticles exceeds int range"), std::string(path + ".maxParticles")));
415 budget.maxParticles = static_cast<int>(*count);
416 }
417 if (const auto it = object->find("allowDistortion"); it != object->end()) {
418 const auto* enabled = it->second.getIf<bool>();
419 if (!enabled)
420 return Result<AttackVfxBudget>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("allowDistortion must be boolean"), std::string(path + ".allowDistortion")));
421 budget.allowDistortion = *enabled;
422 }
424}
425
426} // namespace
427
429 switch (kind) {
430 case AttackVfxPhaseKind::Anticipate: return "anticipate";
431 case AttackVfxPhaseKind::Release: return "release";
432 case AttackVfxPhaseKind::Travel: return "travel";
433 case AttackVfxPhaseKind::Impact: return "impact";
434 case AttackVfxPhaseKind::Aftermath: return "aftermath";
435 case AttackVfxPhaseKind::Status: return "status";
436 }
437 return "release";
438}
439
440std::string_view attackVfxLayerRoleName(AttackVfxLayerRole role) noexcept {
441 switch (role) {
442 case AttackVfxLayerRole::BlockingMesh: return "blockingMesh";
443 case AttackVfxLayerRole::MeshVfx: return "meshVfx";
444 case AttackVfxLayerRole::Trail: return "trail";
445 case AttackVfxLayerRole::Particles: return "particles";
446 case AttackVfxLayerRole::Decal: return "decal";
447 case AttackVfxLayerRole::Distortion: return "distortion";
448 case AttackVfxLayerRole::Camera: return "camera";
449 case AttackVfxLayerRole::Prefab: return "prefab";
450 case AttackVfxLayerRole::Audio: return "audio";
451 }
452 return "particles";
453}
454
455std::string_view attackVfxStopBehaviorName(AttackVfxStopBehavior behavior) noexcept {
456 switch (behavior) {
457 case AttackVfxStopBehavior::StopEmitting: return "stopEmitting";
458 case AttackVfxStopBehavior::ClearImmediately: return "clearImmediately";
459 }
460 return "stopEmitting";
461}
462
464 switch (mode) {
465 case AttackVfxSpatialAttachment::FollowTarget: return "followTarget";
466 case AttackVfxSpatialAttachment::FollowPositionOnly: return "followPositionOnly";
467 case AttackVfxSpatialAttachment::WorldTransformAtStart: return "worldTransformAtStart";
468 }
469 return "followTarget";
470}
471
473 switch (anchor) {
474 case AttackVfxSpatialAnchor::Source: return "source";
475 case AttackVfxSpatialAnchor::Target: return "target";
476 }
477 return "source";
478}
479
481 const auto finite = [](float v) { return std::isfinite(v); };
483 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("positionOffset must be finite"), std::string(std::string(path) + ".positionOffset")));
485 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("rotationOffsetDegrees must be finite"), std::string(std::string(path) + ".rotationOffsetDegrees")));
486 if (!(std::isfinite(scale.x) && scale.x > 0.f) || !(std::isfinite(scale.y) && scale.y > 0.f) ||
487 !(std::isfinite(scale.z) && scale.z > 0.f))
488 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("scale components must be finite and > 0"), std::string(std::string(path) + ".scale")));
489 return Result<void>::success();
490}
491
493 auto spatialStatus = spatial.validate(std::string(path) + ".spatial");
494 if (!spatialStatus) return spatialStatus;
495 const bool uriOptional =
497 if (!uriOptional && uri.empty())
498 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("uri is required for this layer role"), std::string(std::string(path) + ".uri")));
499 for (const auto& [name, value] : floatParams) {
500 (void)name;
501 if (!std::isfinite(value))
502 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("floatParams values must be finite"), std::string(std::string(path) + ".floatParams")));
503 }
504 return Result<void>::success();
505}
506
508 if (!std::isfinite(startOffsetSeconds) || startOffsetSeconds < 0.0)
509 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("startOffsetSeconds must be finite and >= 0"), std::string(std::string(path) + ".startOffsetSeconds")));
510 if (!std::isfinite(durationSeconds) || durationSeconds < 0.0)
511 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("durationSeconds must be finite and >= 0"), std::string(std::string(path) + ".durationSeconds")));
512 for (std::size_t i = 0; i < layers.size(); ++i) {
513 auto status = layers[i].validate(std::string(path) + ".layers[" + std::to_string(i) + "]");
514 if (!status) return status;
515 }
516 return Result<void>::success();
517}
518
520 if (!id.isValid()) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("skin id is required"), std::string("id")));
521 const auto checkColor = [](const AttackVfxVec3& c, std::string_view path) -> Result<void> {
522 if (!finiteUnit(c.x) || !finiteUnit(c.y) || !finiteUnit(c.z))
523 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("palette channel must be in [0,1]"), std::string(path)));
524 return Result<void>::success();
525 };
526 if (auto s = checkColor(palette.primary, "palette.primary"); !s) return s;
527 if (auto s = checkColor(palette.secondary, "palette.secondary"); !s) return s;
528 if (auto s = checkColor(palette.emissive, "palette.emissive"); !s) return s;
529 for (const auto& [name, value] : shakeProfile) {
530 (void)name;
531 if (!std::isfinite(value))
532 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("shakeProfile values must be finite"), std::string("shakeProfile")));
533 }
534 for (const auto& [name, value] : distortionProfile) {
535 (void)name;
536 if (!std::isfinite(value))
537 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("distortionProfile values must be finite"), std::string("distortionProfile")));
538 }
539 return Result<void>::success();
540}
541
543 if (!id.isValid()) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("recipe id is required"), std::string("id")));
544 if (phases.empty()) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("recipe requires at least one phase"), std::string("phases")));
545 if (budget.lod < 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("lod must be >= 0"), std::string("budget.lod")));
546 if (budget.maxParticles < 0)
547 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("maxParticles must be >= 0"), std::string("budget.maxParticles")));
548 for (std::size_t i = 0; i < phases.size(); ++i) {
549 auto status = phases[i].validate("phases[" + std::to_string(i) + "]");
550 if (!status) return status;
551 }
552 if (skin) {
553 auto status = skin->validate();
554 if (!status) return status;
555 if (skinId && skinId->format() != skin->id.format())
556 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("inline skin.id must match skinId"), std::string("skin.id")));
557 }
558 return Result<void>::success();
559}
560
562 auto parsed = Value::fromJson(json);
563 if (!parsed) return Result<AttackVfxSkin>::failure(parsed.status());
564 const auto* object = parsed.value().getIf<Value::Object>();
565 if (!object) return Result<AttackVfxSkin>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("skin must be an object"), std::string("")));
566 return parseSkinObject(*object, "", true);
567}
568
570 auto parsed = Value::fromJson(json);
571 if (!parsed)
572 return Result<AttackVfxRecipe>::failure(parsed.status());
573 const auto* object = parsed.value().getIf<Value::Object>();
574 if (!object) return Result<AttackVfxRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("recipe must be an object"), std::string("")));
575
576 auto known = rejectUnknown(*object, {"schema", "schemaVersion", "id", "phases", "skinId", "skin", "budget"}, "");
577 if (!known)
578 return Result<AttackVfxRecipe>::failure(known.status());
579
580 const auto schema = object->find("schema");
581 if (schema == object->end() || !schema->second.isString() || schema->second.asString() != schemaId)
582 return Result<AttackVfxRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("schema must be eve.stylize.attack-vfx"), std::string("schema")));
583 const auto version = object->find("schemaVersion");
584 if (version == object->end())
585 return Result<AttackVfxRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("schemaVersion is required"), std::string("schemaVersion")));
586 const auto* ver = version->second.getIf<std::int64_t>();
587 if (!ver || *ver != static_cast<std::int64_t>(schemaVersion))
588 return Result<AttackVfxRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("unsupported schemaVersion"), std::string("schemaVersion")));
589
590 AttackVfxRecipe recipe;
591 const auto id = object->find("id");
592 if (id == object->end())
593 return Result<AttackVfxRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("id is required"), std::string("id")));
594 auto parsedId = parseLogicalId(id->second, "id");
595 if (!parsedId)
596 return Result<AttackVfxRecipe>::failure(parsedId.status());
597 recipe.id = std::move(parsedId).takeValue();
598
599 const auto phases = object->find("phases");
600 if (phases == object->end())
601 return Result<AttackVfxRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("phases is required"), std::string("phases")));
602 const auto* phaseArray = phases->second.getIf<Value::Array>();
603 if (!phaseArray)
604 return Result<AttackVfxRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("phases must be an array"), std::string("phases")));
605 recipe.phases.reserve(phaseArray->size());
606 for (std::size_t i = 0; i < phaseArray->size(); ++i) {
607 auto parsedPhase = parsePhase((*phaseArray)[i], "phases[" + std::to_string(i) + "]");
608 if (!parsedPhase)
609 return Result<AttackVfxRecipe>::failure(parsedPhase.status());
610 recipe.phases.push_back(std::move(parsedPhase).takeValue());
611 }
612
613 if (const auto it = object->find("skinId"); it != object->end()) {
614 auto parsedSkinId = parseLogicalId(it->second, "skinId");
615 if (!parsedSkinId)
616 return Result<AttackVfxRecipe>::failure(parsedSkinId.status());
617 recipe.skinId = std::move(parsedSkinId).takeValue();
618 }
619 if (const auto it = object->find("skin"); it != object->end()) {
620 const auto* skinObject = it->second.getIf<Value::Object>();
621 if (!skinObject)
622 return Result<AttackVfxRecipe>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("skin must be an object"), std::string("skin")));
623 // Inline skin omits schema fields; id is still required.
624 auto parsedSkin = parseSkinObject(*skinObject, "skin", false);
625 if (!parsedSkin)
626 return Result<AttackVfxRecipe>::failure(parsedSkin.status());
627 recipe.skin = std::move(parsedSkin).takeValue();
628 if (!recipe.skinId) recipe.skinId = recipe.skin->id;
629 }
630 if (const auto it = object->find("budget"); it != object->end()) {
631 auto parsedBudget = parseBudget(&it->second, "budget");
632 if (!parsedBudget)
633 return Result<AttackVfxRecipe>::failure(parsedBudget.status());
634 recipe.budget = std::move(parsedBudget).takeValue();
635 }
636
637 auto valid = recipe.validate();
638 if (!valid)
640 return Result<AttackVfxRecipe>::success(std::move(recipe));
641}
642
643namespace {
644
645Value makeVec3(const AttackVfxVec3& value) {
646 Value::Array array;
647 array.emplace_back(static_cast<double>(value.x));
648 array.emplace_back(static_cast<double>(value.y));
649 array.emplace_back(static_cast<double>(value.z));
650 return Value(std::move(array));
651}
652
653Value::Object encodeSpatial(const AttackVfxSpatial& spatial) {
655 object.emplace("attachment", Value(std::string(attackVfxSpatialAttachmentName(spatial.attachment))));
656 object.emplace("anchor", Value(std::string(attackVfxSpatialAnchorName(spatial.anchor))));
657 object.emplace("targetIndex", Value(static_cast<std::int64_t>(spatial.targetIndex)));
658 if (!spatial.bone.empty()) object.emplace("bone", Value(spatial.bone));
659 object.emplace("positionOffset", makeVec3(spatial.positionOffset));
660 object.emplace("rotationOffsetDegrees", makeVec3(spatial.rotationOffsetDegrees));
661 object.emplace("scale", makeVec3(spatial.scale));
662 return object;
663}
664
665Value::Object encodeFloatMap(const std::map<std::string, float>& values) {
667 for (const auto& [name, value] : values) object.emplace(name, Value(static_cast<double>(value)));
668 return object;
669}
670
671Value::Object encodeStringMap(const std::map<std::string, std::string>& values) {
673 for (const auto& [name, value] : values) object.emplace(name, Value(value));
674 return object;
675}
676
677Value::Object encodeLayer(const AttackVfxLayer& layer) {
679 object.emplace("role", Value(std::string(attackVfxLayerRoleName(layer.role))));
680 if (!layer.uri.empty()) object.emplace("uri", Value(layer.uri));
681 object.emplace("spatial", Value(encodeSpatial(layer.spatial)));
682 if (!layer.floatParams.empty()) object.emplace("floatParams", Value(encodeFloatMap(layer.floatParams)));
683 object.emplace("stopBehavior", Value(std::string(attackVfxStopBehaviorName(layer.stopBehavior))));
684 return object;
685}
686
687Value::Object encodePhase(const AttackVfxPhase& phase) {
689 object.emplace("kind", Value(std::string(attackVfxPhaseKindName(phase.kind))));
690 if (!phase.startCue.empty()) object.emplace("startCue", Value(phase.startCue));
691 if (!phase.endCue.empty()) object.emplace("endCue", Value(phase.endCue));
692 object.emplace("startOffsetSeconds", Value(phase.startOffsetSeconds));
693 object.emplace("durationSeconds", Value(phase.durationSeconds));
694 Value::Array layers;
695 layers.reserve(phase.layers.size());
696 for (const auto& layer : phase.layers) layers.emplace_back(Value(encodeLayer(layer)));
697 object.emplace("layers", Value(std::move(layers)));
698 return object;
699}
700
701Value::Object encodeSkinBody(const AttackVfxSkin& skin, bool includeSchema) {
703 if (includeSchema) {
704 object.emplace("schema", Value(std::string("eve.stylize.attack-vfx-skin")));
705 object.emplace("schemaVersion", Value(static_cast<std::int64_t>(1)));
706 }
707 object.emplace("id", Value(skin.id.format()));
709 palette.emplace("primary", makeVec3(skin.palette.primary));
710 palette.emplace("secondary", makeVec3(skin.palette.secondary));
711 palette.emplace("emissive", makeVec3(skin.palette.emissive));
712 object.emplace("palette", Value(std::move(palette)));
713 if (!skin.styleHints.empty()) object.emplace("styleHints", Value(encodeStringMap(skin.styleHints)));
714 if (!skin.shakeProfile.empty()) object.emplace("shakeProfile", Value(encodeFloatMap(skin.shakeProfile)));
715 if (!skin.distortionProfile.empty())
716 object.emplace("distortionProfile", Value(encodeFloatMap(skin.distortionProfile)));
717 if (!skin.statusOverlayUri.empty()) object.emplace("statusOverlayUri", Value(skin.statusOverlayUri));
718 return object;
719}
720
721} // namespace
722
724 auto valid = validate();
725 if (!valid) return Result<std::string>::failure(valid.status());
726 return Value(encodeSkinBody(*this, true)).toJson();
727}
728
730 auto valid = validate();
731 if (!valid) return Result<std::string>::failure(valid.status());
733 object.emplace("schema", std::string(schemaId));
734 object.emplace("schemaVersion", static_cast<std::int64_t>(schemaVersion));
735 object.emplace("id", id.format());
736 Value::Array phasesJson;
737 phasesJson.reserve(phases.size());
738 for (const auto& phase : phases) phasesJson.emplace_back(Value(encodePhase(phase)));
739 object.emplace("phases", std::move(phasesJson));
740 if (skinId) object.emplace("skinId", skinId->format());
741 if (skin) object.emplace("skin", encodeSkinBody(*skin, false));
742 Value::Object budgetJson;
743 budgetJson.emplace("lod", static_cast<std::int64_t>(budget.lod));
744 budgetJson.emplace("maxParticles", static_cast<std::int64_t>(budget.maxParticles));
745 budgetJson.emplace("allowDistortion", budget.allowDistortion);
746 object.emplace("budget", std::move(budgetJson));
747 return Value(std::move(object)).toJson();
748}
749
751
753 auto parsed = AttackVfxRecipe::fromJson(json);
754 if (!parsed)
755 return Result<std::uint64_t>::failure(parsed.status());
756 recipe_ = std::move(parsed).takeValue();
757 ++revision_;
758 return Result<std::uint64_t>::success(revision_);
759}
760
761} // namespace eve::stylize
double value
Data-driven layered attack VFX recipe and element skin (Phase 1 schema).
eve::action::ActionSpatialBinding spatial
const std::string & s
float phase
Definition CaveMesh.cpp:58
Vec3 anchor
Definition CaveMesh.cpp:90
std::map< std::string, Var > values
std::string palette
std::uint32_t key
wgpu::PopErrorScopeStatus status
float v
std::int32_t c
std::string text
TokenKind kind
std::string name
bool valid
bool finite
TileLayer * layer
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
int lod
double number
std::uint32_t count
Json object
uint32_t index
Owning, renderer-independent dynamic values.
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 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
Result< std::string > toJson() const
Serialize this value as deterministic compact JSON.
Definition Value.cpp:67
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
std::vector< Value > Array
Definition Value.h:33
Result< std::uint64_t > reload(std::string_view json)
Parse and atomically replace the recipe on success.
AttackVfxRecipeSlot(AttackVfxRecipe recipe)
Construct a slot from an already validated recipe.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
AttackVfxLayerRole
Which existing presentation backend a layer binds to.
AttackVfxSpatialAttachment
How a layer follows its resolved spatial anchor.
AttackVfxPhaseKind
Attack presentation phase along the cast → hit → linger timeline.
std::string_view attackVfxSpatialAnchorName(AttackVfxSpatialAnchor anchor) noexcept
Stable JSON spelling for spatial anchor.
AttackVfxStopBehavior
Exit policy when a phase or instance stops.
std::string_view attackVfxSpatialAttachmentName(AttackVfxSpatialAttachment mode) noexcept
Stable JSON spelling for spatial attachment.
AttackVfxSpatialAnchor
Which request endpoint supplies the spatial anchor.
std::string_view attackVfxPhaseKindName(AttackVfxPhaseKind kind) noexcept
Stable JSON spelling for a phase kind.
std::string_view attackVfxLayerRoleName(AttackVfxLayerRole role) noexcept
Stable JSON spelling for a layer role.
std::string_view attackVfxStopBehaviorName(AttackVfxStopBehavior behavior) noexcept
Stable JSON spelling for stop behavior.
bool enabled
ActionSpatialVector3 rotationOffsetDegrees
std::map< std::string, float > floatParams
Result< void > validate(std::string_view path) const
Validate role/uri/spatial/parameter invariants.
std::vector< AttackVfxLayer > layers
Result< void > validate(std::string_view path) const
Validate cues, timing, and nested layers.
Versioned attack VFX composition recipe.
Result< std::string > toJson() const
Serialize this recipe in canonical current-schema JSON form.
static Result< AttackVfxRecipe > fromJson(std::string_view json)
Parse, validate, and return a complete recipe without mutating live state.
std::optional< LogicalId > skinId
Result< void > validate() const
Validate id, phases, optional skin, and budget invariants.
static constexpr std::string_view schemaId
static constexpr std::uint32_t schemaVersion
std::optional< AttackVfxSkin > skin
std::vector< AttackVfxPhase > phases
Result< void > validate() const
Validate id, palette range, and finite profile values.
static Result< AttackVfxSkin > fromJson(std::string_view json)
Parse a standalone skin document.
std::map< std::string, float > distortionProfile
Result< std::string > toJson() const
Serialize in canonical current-schema JSON.
std::map< std::string, float > shakeProfile
Result< void > validate(std::string_view path) const
Validate finite offsets and strictly positive scale.
Owning three-component float used at the recipe boundary.