载入中...
搜索中...
未找到
AttackVfxRuntime.cpp
浏览该文件的文档.
2
3#include "common/Capability.h"
5
6#include <cmath>
7#include <optional>
8#include <unordered_map>
9#include <utility>
10
11namespace eve::stylize {
12namespace {
13
14constexpr std::uint32_t kDefaultCapacity = 32;
15
16IAttackVfxLayerExecutor* findExecutor(AttackVfxLayerRole role) {
17 IAttackVfxLayerExecutor* found = nullptr;
18 cap::forEach<IAttackVfxLayerExecutor>([&](IAttackVfxLayerExecutor* executor) {
19 if (!found && executor && executor->role() == role) found = executor;
20 });
21 return found;
22}
23
24} // namespace
25
35
36 struct LivePhase {
37 std::vector<LiveLayer> layers;
38 };
39
40 struct Slot {
41 std::uint32_t generation = 1;
42 std::optional<AttackVfxInstanceState> state;
43 std::vector<LivePhase> phaseLayers;
44 std::vector<LiveLayer> draining;
45 };
46
47 std::vector<Slot> slots{kDefaultCapacity};
48 std::unordered_map<std::string, AttackVfxRecipe> recipes;
49 std::unordered_map<std::string, AttackVfxSkin> skins;
50 std::uint64_t tickSerial = 0;
51
53 if (handle.slot >= slots.size())
54 return Result<AttackVfxInstanceState*>::failure(Diagnostic::error(DiagnosticCode::NotFound, std::string("attack VFX handle slot out of range"), std::string("handle")));
55 auto& slot = slots[handle.slot];
56 if (!slot.state || slot.generation != handle.generation)
57 return Result<AttackVfxInstanceState*>::failure(Diagnostic::error(DiagnosticCode::StaleHandle, std::string("stale attack VFX handle"), std::string("handle")));
59 }
60
62 if (handle.slot >= slots.size()) return nullptr;
63 const auto& slot = slots[handle.slot];
64 if (!slot.state || slot.generation != handle.generation) return nullptr;
65 return &*slot.state;
66 }
67
69 if (request.skinOverride) {
70 if (!skins.contains(request.skinOverride->format()))
71 return Result<LogicalId>::failure(Diagnostic::error(DiagnosticCode::NotFound, std::string("skin override is not registered"), std::string("skinOverride")));
72 return Result<LogicalId>::success(*request.skinOverride);
73 }
74 if (recipe.skinId) {
75 if (!skins.contains(recipe.skinId->format()) && !recipe.skin)
76 return Result<LogicalId>::failure(Diagnostic::error(DiagnosticCode::NotFound, std::string("recipe skinId is not registered"), std::string("skinId")));
77 return Result<LogicalId>::success(*recipe.skinId);
78 }
80 }
81
82 const AttackVfxSkin* skinFor(const AttackVfxInstanceState& state) const {
83 if (!state.activeSkinId) return nullptr;
84 const auto it = skins.find(state.activeSkinId->format());
85 return it == skins.end() ? nullptr : &it->second;
86 }
87
89 std::size_t phaseIndex, std::size_t layerIndex,
90 const AttackVfxLayer* layer, const AttackVfxSkin* skin) const {
92 request.instance = state.handle;
93 request.phase = phase;
94 request.phaseIndex = phaseIndex;
95 request.layerIndex = layerIndex;
96 request.layer = layer;
97 request.skin = skin;
98 request.playRequest = &state.request;
99 request.tickSerial = tickSerial;
100 return request;
101 }
102
103 Result<void> startOneLayer(Slot& slot, std::size_t phaseIndex, const AttackVfxPhase& authored,
104 const AttackVfxLayer& layer, std::size_t layerIndex, const AttackVfxSkin* skin,
105 AttackVfxFrame& frame) {
106 auto& state = *slot.state;
108 event.handle = state.handle;
109 event.phase = authored.kind;
110 event.role = layer.role;
111 event.layerIndex = layerIndex;
112 event.layerCount = authored.layers.size();
113
114 auto* executor = findExecutor(layer.role);
115 if (!executor) {
117 frame.events.push_back(std::move(event));
118 return Result<void>::success();
119 }
120 auto request = makeLayerRequest(state, authored.kind, phaseIndex, layerIndex, &layer, skin);
121 auto started = executor->start(request);
122 if (!started) {
123 const auto code = started.code();
126 frame.events.push_back(std::move(event));
127 return Result<void>::success();
128 }
129 return Result<void>::failure(started.status());
130 }
131 LiveLayer live;
132 live.role = layer.role;
133 live.handle = std::move(started).takeValue();
134 live.stopBehavior = layer.stopBehavior;
135 live.layerIndex = layerIndex;
136 live.phase = authored.kind;
137 live.phaseIndex = phaseIndex;
138 slot.phaseLayers.at(phaseIndex).layers.push_back(std::move(live));
140 frame.events.push_back(std::move(event));
141 return Result<void>::success();
142 }
143
144 Result<void> startPhaseLayers(Slot& slot, std::size_t phaseIndex, AttackVfxFrame& frame) {
145 auto& state = *slot.state;
146 const auto recipeIt = recipes.find(state.recipeId.format());
147 if (recipeIt == recipes.end())
148 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, std::string("recipe missing while starting layers"), std::string("recipeId")));
149 const auto& recipe = recipeIt->second;
150 const auto& authored = recipe.phases.at(phaseIndex);
151 auto& livePhase = slot.phaseLayers.at(phaseIndex);
152 livePhase.layers.clear();
153 const AttackVfxSkin* skin = skinFor(state);
154
155 for (std::size_t layerIndex = 0; layerIndex < authored.layers.size(); ++layerIndex) {
156 auto started = startOneLayer(slot, phaseIndex, authored, authored.layers[layerIndex], layerIndex, skin,
157 frame);
158 if (!started) return started;
159 }
160
161 // Skin statusOverlayUri synthesizes a MeshVfx layer when Status phases omit it.
162 if (authored.kind == AttackVfxPhaseKind::Status && skin && !skin->statusOverlayUri.empty()) {
163 bool hasOverlay = false;
164 for (const auto& layer : authored.layers) {
165 if (layer.uri == skin->statusOverlayUri) {
166 hasOverlay = true;
167 break;
168 }
169 }
170 if (!hasOverlay) {
171 AttackVfxLayer overlay;
173 overlay.uri = skin->statusOverlayUri;
174 auto started =
175 startOneLayer(slot, phaseIndex, authored, overlay, authored.layers.size(), skin, frame);
176 if (!started) return started;
177 }
178 }
179 return Result<void>::success();
180 }
181
183 if (!live.handle.valid()) return Result<void>::success();
184 auto* executor = findExecutor(live.role);
185 if (!executor) {
186 live.handle = {};
187 return Result<void>::success();
188 }
189 auto stopped = executor->stop(live.handle, behavior);
190 if (behavior == AttackVfxStopBehavior::ClearImmediately) live.handle = {};
191 return stopped;
192 }
193
194 Result<void> stopPhaseLayers(Slot& slot, std::size_t phaseIndex,
195 std::optional<AttackVfxStopBehavior> behaviorOverride = std::nullopt) {
196 auto& livePhase = slot.phaseLayers.at(phaseIndex);
198 for (auto& live : livePhase.layers) {
199 if (!live.handle.valid()) continue;
200 const auto behavior = behaviorOverride.value_or(live.stopBehavior);
201 auto stopped = stopLiveLayer(live, behavior);
202 if (!stopped && status) status = Result<void>::failure(stopped.status());
203 if (behavior == AttackVfxStopBehavior::StopEmitting && live.handle.valid()) {
204 slot.draining.push_back(live);
205 }
206 live.handle = {};
207 }
208 livePhase.layers.clear();
209 return status;
210 }
211
214 for (auto& live : slot.draining) {
215 if (!live.handle.valid()) continue;
216 auto stopped = stopLiveLayer(live, behavior);
217 if (!stopped && status) status = Result<void>::failure(stopped.status());
218 live.handle = {};
219 }
220 if (behavior == AttackVfxStopBehavior::ClearImmediately) slot.draining.clear();
221 return status;
222 }
223
224 Result<void> updateLiveLayer(Slot& slot, LiveLayer& live, double dtSeconds) {
225 if (!live.handle.valid() || !slot.state) return Result<void>::success();
226 auto* executor = findExecutor(live.role);
227 if (!executor) return Result<void>::success();
228
229 const auto recipeIt = recipes.find(slot.state->recipeId.format());
230 const AttackVfxLayer* layer = nullptr;
231 if (recipeIt != recipes.end() && live.phaseIndex < recipeIt->second.phases.size()) {
232 const auto& authored = recipeIt->second.phases[live.phaseIndex];
233 if (live.layerIndex < authored.layers.size() &&
234 authored.layers[live.layerIndex].role == live.role) {
235 layer = &authored.layers[live.layerIndex];
236 }
237 }
238 const AttackVfxSkin* skin = skinFor(*slot.state);
239 auto request =
240 makeLayerRequest(*slot.state, live.phase, live.phaseIndex, live.layerIndex, layer, skin);
241 return executor->update(live.handle, dtSeconds, request);
242 }
243
244 Result<void> updatePhaseLayers(Slot& slot, std::size_t phaseIndex, double dtSeconds) {
245 auto& livePhase = slot.phaseLayers.at(phaseIndex);
246 for (auto& live : livePhase.layers) {
247 if (!live.handle.valid()) continue;
248 auto updated = updateLiveLayer(slot, live, dtSeconds);
249 if (!updated) return updated;
250 }
251 return Result<void>::success();
252 }
253
254 Result<void> updateDraining(Slot& slot, double dtSeconds) {
256 for (auto it = slot.draining.begin(); it != slot.draining.end();) {
257 if (!it->handle.valid()) {
258 it = slot.draining.erase(it);
259 continue;
260 }
261 auto* executor = findExecutor(it->role);
262 if (!executor) {
263 it = slot.draining.erase(it);
264 continue;
265 }
267 request.instance = slot.state ? slot.state->handle : AttackVfxHandle{};
268 request.phase = it->phase;
269 request.phaseIndex = it->phaseIndex;
270 request.layerIndex = it->layerIndex;
271 request.tickSerial = tickSerial;
272 if (slot.state) {
273 request.skin = skinFor(*slot.state);
274 request.playRequest = &slot.state->request;
275 }
276 auto updated = executor->update(it->handle, dtSeconds, request);
277 if (!updated) {
278 // Rejected/NotFound means the executor finished residual lifetime.
279 if (updated.code() == StatusCode::Rejected || updated.code() == StatusCode::NotFound ||
280 updated.code() == StatusCode::NoOp) {
281 it = slot.draining.erase(it);
282 continue;
283 }
285 ++it;
286 continue;
287 }
288 if (updated.code() == StatusCode::NoOp) {
289 it = slot.draining.erase(it);
290 continue;
291 }
292 ++it;
293 }
294 return status;
295 }
296
297 Result<void> enterPhase(Slot& slot, std::size_t phaseIndex, AttackVfxFrame& frame) {
298 auto& phase = slot.state->phases.at(phaseIndex);
299 if (phase.active || phase.completed) return Result<void>::success();
300 phase.armed = false;
301 phase.active = true;
302 phase.localTime = 0.0;
305 event.handle = slot.state->handle;
306 event.phase = phase.kind;
307 event.layerCount = phase.layerCount;
308 frame.events.push_back(std::move(event));
309 return startPhaseLayers(slot, phaseIndex, frame);
310 }
311
312 Result<void> exitPhase(Slot& slot, std::size_t phaseIndex, AttackVfxFrame& frame, std::string_view cue,
313 std::optional<AttackVfxStopBehavior> behaviorOverride = std::nullopt) {
314 auto& phase = slot.state->phases.at(phaseIndex);
315 if (!phase.active) return Result<void>::success();
316 auto stopped = stopPhaseLayers(slot, phaseIndex, behaviorOverride);
317 phase.active = false;
318 phase.completed = true;
321 event.handle = slot.state->handle;
322 event.phase = phase.kind;
323 event.cue = std::string(cue);
324 event.layerCount = phase.layerCount;
325 frame.events.push_back(std::move(event));
326 return stopped;
327 }
328
330 const auto recipeIt = recipes.find(state.recipeId.format());
331 if (recipeIt == recipes.end()) return;
332 const auto& recipe = recipeIt->second;
333 for (std::size_t i = 0; i < recipe.phases.size(); ++i) {
334 const auto& authored = recipe.phases[i];
335 auto& live = state.phases[i];
336 if (live.active || live.completed || live.armed) continue;
337 if (authored.startCue.empty()) live.armed = true;
338 }
339 }
340
341 Result<void> applyCue(Slot& slot, std::string_view cue, AttackVfxFrame& frame) {
342 auto& state = *slot.state;
343 const auto recipeIt = recipes.find(state.recipeId.format());
344 if (recipeIt == recipes.end()) return Result<void>::success();
345 const auto& recipe = recipeIt->second;
346 bool consumed = false;
347 for (std::size_t i = 0; i < recipe.phases.size(); ++i) {
348 const auto& authored = recipe.phases[i];
349 auto& live = state.phases[i];
350 if (!live.completed && !live.active && authored.startCue == cue) {
351 live.armed = true;
352 if (authored.startOffsetSeconds <= 0.0) {
353 auto entered = enterPhase(slot, i, frame);
354 if (!entered) return entered;
355 }
356 consumed = true;
357 }
358 if (live.active && !authored.endCue.empty() && authored.endCue == cue) {
359 auto exited = exitPhase(slot, i, frame, cue);
360 if (!exited) return exited;
361 consumed = true;
362 }
363 }
366 event.handle = state.handle;
367 event.cue = std::string(cue);
368 frame.events.push_back(std::move(event));
369 return Result<void>::success();
370 }
371
372 Result<void> finishInstance(std::size_t slotIndex, AttackVfxFrame& frame, std::string_view cue,
373 bool clearImmediately) {
374 auto& slot = slots.at(slotIndex);
375 if (!slot.state && slot.draining.empty()) return Result<void>::success();
377 const auto overrideBehavior =
378 clearImmediately ? std::optional<AttackVfxStopBehavior>(AttackVfxStopBehavior::ClearImmediately)
379 : std::nullopt;
380 if (slot.state) {
381 for (std::size_t i = 0; i < slot.state->phases.size(); ++i) {
382 if (slot.state->phases[i].active) {
383 auto exited = exitPhase(slot, i, frame, cue, overrideBehavior);
384 if (!exited && status) status = Result<void>::failure(exited.status());
385 }
386 }
387 }
388 if (clearImmediately) {
390 if (!cleared && status) status = Result<void>::failure(cleared.status());
391 slot.draining.clear();
392 }
393 // Free the playable identity immediately. StopEmitting residuals remain in
394 // slot.draining and continue to receive updateDraining ticks without a live state.
395 if (slot.state) {
398 event.handle = slot.state->handle;
399 event.cue = std::string(cue);
400 frame.events.push_back(std::move(event));
401 frame.stopped.push_back(slot.state->handle);
402 slot.state.reset();
403 slot.phaseLayers.clear();
404 ++slot.generation;
405 if (slot.generation == 0) slot.generation = 1;
406 }
407 return status;
408 }
409
411 AttackVfxFrame frame;
412 for (std::size_t i = 0; i < slots.size(); ++i) {
413 if (slots[i].state || !slots[i].draining.empty())
414 (void)finishInstance(i, frame, "shutdown", true);
415 }
416 }
417};
418
419AttackVfxRuntime::AttackVfxRuntime() : impl_(std::make_unique<Impl>()) {}
421 // Best-effort teardown: never throw from a destructor.
422 try {
423 impl_->stopAllOccupied();
424 } catch (...) {
425 }
426}
427
428std::size_t AttackVfxRuntime::capacity() const noexcept { return impl_->slots.size(); }
429
431 if (capacity == 0) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("pool capacity must be > 0"), std::string("capacity")));
432 for (const auto& slot : impl_->slots) {
433 if (slot.state)
434 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvariantViolation, std::string("cannot resize pool while instances are live"), std::string("capacity")));
435 }
436 AttackVfxFrame discard;
437 for (std::size_t i = 0; i < impl_->slots.size(); ++i) {
438 if (!impl_->slots[i].draining.empty())
439 (void)impl_->finishInstance(i, discard, "resize", true);
440 }
441 // Bump generations so stale handles cannot revive after resize into a default slot.
442 std::vector<Impl::Slot> next(capacity);
443 for (std::size_t i = 0; i < capacity; ++i) {
444 if (i < impl_->slots.size()) {
445 next[i].generation = impl_->slots[i].generation + 1;
446 if (next[i].generation == 0) next[i].generation = 1;
447 } else {
448 next[i].generation = 1;
449 }
450 }
451 impl_->slots = std::move(next);
452 return Result<void>::success();
453}
454
456 auto valid = recipe.validate();
457 if (!valid) return valid;
458 const std::string key = recipe.id.format();
459 for (const auto& slot : impl_->slots) {
460 if (slot.state && slot.state->recipeId.format() == key)
461 return Result<void>::failure(Diagnostic::error(DiagnosticCode::Conflict, std::string("cannot replace recipe while instances are live"), std::string("recipeId")));
462 }
463 impl_->recipes[key] = recipe;
464 if (recipe.skin) {
465 auto skinStatus = registerSkin(*recipe.skin);
466 if (!skinStatus) return skinStatus;
467 }
468 return Result<void>::success();
469}
470
472 auto valid = skin.validate();
473 if (!valid) return valid;
474 impl_->skins[skin.id.format()] = skin;
475 return Result<void>::success();
476}
477
478std::optional<AttackVfxRecipe> AttackVfxRuntime::findRecipe(const LogicalId& id) const {
479 const auto it = impl_->recipes.find(id.format());
480 if (it == impl_->recipes.end()) return std::nullopt;
481 return it->second;
482}
483
484std::optional<AttackVfxSkin> AttackVfxRuntime::findSkin(const LogicalId& id) const {
485 const auto it = impl_->skins.find(id.format());
486 if (it == impl_->skins.end()) return std::nullopt;
487 return it->second;
488}
489
491 const auto recipeIt = impl_->recipes.find(recipeId.format());
492 if (recipeIt == impl_->recipes.end())
493 return Result<AttackVfxHandle>::failure(Diagnostic::error(DiagnosticCode::NotFound, std::string("recipe is not registered"), std::string("recipeId")));
494 const auto& recipe = recipeIt->second;
495
496 auto skinId = impl_->resolveSkinId(recipe, request);
497 if (!skinId) return Result<AttackVfxHandle>::failure(skinId.status());
498
499 // Prefer a slot with no StopEmitting residuals so a new play does not share
500 // draining ParticleEffect/Decal GPU resources with the previous cycle.
501 std::size_t freeSlot = impl_->slots.size();
502 std::size_t freeWithDrain = impl_->slots.size();
503 for (std::size_t i = 0; i < impl_->slots.size(); ++i) {
504 if (impl_->slots[i].state) continue;
505 if (impl_->slots[i].draining.empty()) {
506 freeSlot = i;
507 break;
508 }
509 if (freeWithDrain == impl_->slots.size()) freeWithDrain = i;
510 }
511 if (freeSlot == impl_->slots.size()) freeSlot = freeWithDrain;
512 if (freeSlot == impl_->slots.size())
513 return Result<AttackVfxHandle>::failure(Diagnostic::error(DiagnosticCode::Failed, std::string("attack VFX pool is full"), std::string("pool")));
514
516 state.handle.slot = static_cast<std::uint32_t>(freeSlot);
517 state.handle.generation = impl_->slots[freeSlot].generation;
518 state.recipeId = recipe.id;
519 if (skinId.value().isValid()) state.activeSkinId = skinId.value();
520 state.request = request;
521 state.phases.reserve(recipe.phases.size());
522 for (const auto& phase : recipe.phases) {
524 live.kind = phase.kind;
525 live.layerCount = phase.layers.size();
526 state.phases.push_back(live);
527 }
528
529 auto& slot = impl_->slots[freeSlot];
530 // Reclaiming a slot that still drains must drop residuals immediately so the
531 // new play does not share layer handles / GPU resources with the prior cycle.
532 if (!slot.draining.empty()) {
533 AttackVfxFrame discard;
534 (void)impl_->finishInstance(freeSlot, discard, "reuse", true);
535 }
536 slot.state = std::move(state);
537 slot.phaseLayers.assign(recipe.phases.size(), Impl::LivePhase{});
538 impl_->armTimedPhases(*slot.state);
539
540 AttackVfxFrame bootstrap;
541 for (std::size_t i = 0; i < recipe.phases.size(); ++i) {
542 if (slot.state->phases[i].armed && recipe.phases[i].startOffsetSeconds <= 0.0) {
543 auto entered = impl_->enterPhase(slot, i, bootstrap);
544 if (!entered) {
545 AttackVfxFrame cleanup;
546 (void)impl_->finishInstance(freeSlot, cleanup, "play-failed", true);
547 return Result<AttackVfxHandle>::failure(entered.status());
548 }
549 }
550 }
551 (void)bootstrap;
552 return Result<AttackVfxHandle>::success(slot.state->handle);
553}
554
556 if (cue.empty())
557 return Result<AttackVfxFrame>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("cue must be non-empty"), std::string("cue")));
558 auto resolved = impl_->resolve(handle);
559 if (!resolved) return Result<AttackVfxFrame>::failure(resolved.status());
560 (void)std::move(resolved).takeValue();
561
562 AttackVfxFrame frame;
563 if (cue == "cancel") {
564 auto finished = impl_->finishInstance(handle.slot, frame, cue, true);
565 if (!finished) return Result<AttackVfxFrame>::failure(finished.status());
566 return Result<AttackVfxFrame>::success(std::move(frame));
567 }
568 auto applied = impl_->applyCue(impl_->slots[handle.slot], cue, frame);
569 if (!applied) return Result<AttackVfxFrame>::failure(applied.status());
570 return Result<AttackVfxFrame>::success(std::move(frame));
571}
572
574 if (!std::isfinite(dtSeconds) || dtSeconds < 0.0)
575 return Result<AttackVfxFrame>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, std::string("dtSeconds must be finite and >= 0"), std::string("dt")));
576
577 ++impl_->tickSerial;
578 AttackVfxFrame frame;
579 std::vector<std::size_t> toFinish;
580 Result<void> firstError = Result<void>::success();
581
582 for (std::size_t slotIndex = 0; slotIndex < impl_->slots.size(); ++slotIndex) {
583 auto& slot = impl_->slots[slotIndex];
584 if (!slot.state) {
585 if (!slot.draining.empty()) {
586 auto drained = impl_->updateDraining(slot, dtSeconds);
587 if (!drained && firstError) firstError = Result<void>::failure(drained.status());
588 }
589 continue;
590 }
591 auto& state = *slot.state;
592 state.age += dtSeconds;
593 frame.advanced.push_back(state.handle);
594
595 const auto recipeIt = impl_->recipes.find(state.recipeId.format());
596 if (recipeIt == impl_->recipes.end()) {
597 toFinish.push_back(slotIndex);
598 continue;
599 }
600 const auto& recipe = recipeIt->second;
601
602 for (std::size_t i = 0; i < recipe.phases.size(); ++i) {
603 const auto& authored = recipe.phases[i];
604 auto& live = state.phases[i];
605 if (live.armed && !live.active && !live.completed) {
606 bool shouldEnter = false;
607 double phaseDt = dtSeconds;
608 if (authored.startCue.empty()) {
609 if (state.age + 1e-12 >= authored.startOffsetSeconds) {
610 shouldEnter = true;
611 phaseDt = std::max(0.0, state.age - authored.startOffsetSeconds);
612 if (phaseDt > dtSeconds) phaseDt = dtSeconds;
613 }
614 } else {
615 live.localTime += dtSeconds;
616 if (live.localTime + 1e-12 >= authored.startOffsetSeconds) {
617 shouldEnter = true;
618 phaseDt = std::max(0.0, live.localTime - authored.startOffsetSeconds);
619 if (phaseDt > dtSeconds) phaseDt = dtSeconds;
620 }
621 }
622 if (shouldEnter) {
623 auto entered = impl_->enterPhase(slot, i, frame);
624 if (!entered) {
625 if (firstError) firstError = Result<void>::failure(entered.status());
626 continue;
627 }
628 // Apply only the post-start remainder so delayed phases do not expire early.
629 live.localTime = phaseDt;
630 auto updated = impl_->updatePhaseLayers(slot, i, phaseDt);
631 if (!updated && firstError) firstError = Result<void>::failure(updated.status());
632 if (authored.durationSeconds > 0.0 && live.localTime + 1e-12 >= authored.durationSeconds) {
633 auto exited = impl_->exitPhase(slot, i, frame, "duration");
634 if (!exited && firstError) firstError = Result<void>::failure(exited.status());
635 }
636 continue;
637 }
638 }
639 if (live.active) {
640 live.localTime += dtSeconds;
641 auto updated = impl_->updatePhaseLayers(slot, i, dtSeconds);
642 if (!updated && firstError) firstError = Result<void>::failure(updated.status());
643 if (authored.durationSeconds > 0.0 && live.localTime + 1e-12 >= authored.durationSeconds) {
644 auto exited = impl_->exitPhase(slot, i, frame, "duration");
645 if (!exited && firstError) firstError = Result<void>::failure(exited.status());
646 }
647 }
648 }
649
650 // Pump StopEmitting residuals (including layers that exited this tick) once.
651 auto drained = impl_->updateDraining(slot, dtSeconds);
652 if (!drained && firstError) firstError = Result<void>::failure(drained.status());
653
654 bool anyPending = false;
655 for (const auto& live : state.phases) {
656 if (!live.completed) {
657 anyPending = true;
658 break;
659 }
660 }
661 if (!anyPending || state.stopping) toFinish.push_back(slotIndex);
662 }
663
664 for (std::size_t slotIndex : toFinish) {
665 if (impl_->slots[slotIndex].state) {
666 const bool stopped = impl_->slots[slotIndex].state->stopping;
667 // Natural completion and StopEmitting honor authored layer stopBehavior (may drain).
668 auto finished =
669 impl_->finishInstance(slotIndex, frame, stopped ? "stop" : "complete", false);
670 if (!finished && firstError) firstError = Result<void>::failure(finished.status());
671 }
672 }
673
674 if (!firstError) return Result<AttackVfxFrame>::failure(firstError.status());
675 return Result<AttackVfxFrame>::success(std::move(frame));
676}
677
679 auto resolved = impl_->resolve(handle);
680 if (!resolved) return Result<AttackVfxFrame>::failure(resolved.status());
681 auto* state = std::move(resolved).takeValue();
682 AttackVfxFrame frame;
684 state->stopping = true;
685 return Result<AttackVfxFrame>::success(std::move(frame));
686 }
687 auto finished = impl_->finishInstance(handle.slot, frame, mode == AttackVfxStopMode::Cancel ? "cancel" : "clear",
688 true);
689 if (!finished) return Result<AttackVfxFrame>::failure(finished.status());
690 return Result<AttackVfxFrame>::success(std::move(frame));
691}
692
693std::optional<AttackVfxInstanceState> AttackVfxRuntime::inspect(AttackVfxHandle handle) const {
694 const auto* state = impl_->peek(handle);
695 if (!state) return std::nullopt;
696 return *state;
697}
698
699std::size_t AttackVfxRuntime::activeCount() const noexcept {
700 std::size_t count = 0;
701 for (const auto& slot : impl_->slots) {
702 if (slot.state) ++count;
703 }
704 return count;
705}
706
707} // namespace eve::stylize
Pluggable AttackVfx layer backends registered via capability listeners.
Pooled AttackVfx orchestration with optional layer executors.
float phase
Definition CaveMesh.cpp:58
bool draining
DiagnosticCode code
const GltfImportRequest & request
std::uint32_t capacity
std::uint32_t key
wgpu::PopErrorScopeStatus status
bool valid
TileLayer * layer
std::array< std::uint64_t, kPixelChunkSize *kPixelChunkSize > updated
std::uint32_t generation
PrimitiveHandle handle
bool found
LocalPageCacheEntry slots[ShadowConfig::kLocalSlots]
std::uint32_t count
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
Human-readable, scoped identifier in the form namespace:name.
Definition Identity.h:411
const std::string & format() const noexcept
Returns the canonical namespace:name representation.
Definition Identity.h:436
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
const Status & status() const noexcept
Inspect the structured operation status.
Definition Result.h:269
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Result< void > configurePool(std::uint32_t capacity)
Resize the instance pool. Fails when any slot is occupied.
Result< void > registerSkin(const AttackVfxSkin &skin)
Validate and store a reusable skin keyed by skin.id.
std::optional< AttackVfxInstanceState > inspect(AttackVfxHandle handle) const
Snapshot one live instance; empty when the handle is stale.
Result< AttackVfxFrame > stop(AttackVfxHandle handle, AttackVfxStopMode mode)
Stop one instance using the requested teardown mode.
Result< void > registerRecipe(const AttackVfxRecipe &recipe)
Validate and store an immutable recipe copy keyed by recipe.id.
std::size_t capacity() const noexcept
Configured pool capacity.
std::size_t activeCount() const noexcept
Number of occupied slots.
Result< AttackVfxHandle > play(const LogicalId &recipeId, const AttackVfxRequest &request)
Spawn one instance from a registered recipe.
AttackVfxRuntime()
Construct with a default pool capacity of 32.
Result< AttackVfxFrame > signal(AttackVfxHandle handle, std::string_view cue)
Deliver a named cue to one live instance (e.g. "impact", "cancel").
std::optional< AttackVfxRecipe > findRecipe(const LogicalId &id) const
Return a copy of a registered recipe when present.
std::optional< AttackVfxSkin > findSkin(const LogicalId &id) const
Return a copy of a registered skin when present.
~AttackVfxRuntime()
Attack vfx runtime.
Result< AttackVfxFrame > advance(double dtSeconds)
Advance all live instances by injected simulation seconds.
AttackVfxLayerRole
Which existing presentation backend a layer binds to.
AttackVfxPhaseKind
Attack presentation phase along the cast → hit → linger timeline.
AttackVfxStopBehavior
Exit policy when a phase or instance stops.
AttackVfxStopMode
How stop() tears down an instance.
Observable frame events emitted by advance/signal/stop.
Owning result of one atomic orchestration step.
std::vector< AttackVfxHandle > advanced
std::vector< AttackVfxFrameEvent > events
std::vector< AttackVfxHandle > stopped
Generation-qualified reference to one pooled attack VFX instance.
Owning observable state for one live instance.
Opaque executor-owned layer instance identity.
Borrowed start context for one layer inside an entering phase.
One presentation layer slot inside a phase.
Live observation of one phase inside an instance.
One timeline phase that owns zero or more layers.
std::vector< AttackVfxLayer > layers
Versioned attack VFX composition recipe.
std::optional< LogicalId > skinId
Result< void > validate() const
Validate id, phases, optional skin, and budget invariants.
std::optional< AttackVfxSkin > skin
std::optional< AttackVfxInstanceState > state
Result< AttackVfxInstanceState * > resolve(AttackVfxHandle handle)
Result< void > stopPhaseLayers(Slot &slot, std::size_t phaseIndex, std::optional< AttackVfxStopBehavior > behaviorOverride=std::nullopt)
void armTimedPhases(AttackVfxInstanceState &state)
Result< void > clearDraining(Slot &slot, AttackVfxStopBehavior behavior)
Result< void > startPhaseLayers(Slot &slot, std::size_t phaseIndex, AttackVfxFrame &frame)
Result< void > exitPhase(Slot &slot, std::size_t phaseIndex, AttackVfxFrame &frame, std::string_view cue, std::optional< AttackVfxStopBehavior > behaviorOverride=std::nullopt)
Result< LogicalId > resolveSkinId(const AttackVfxRecipe &recipe, const AttackVfxRequest &request) const
const AttackVfxSkin * skinFor(const AttackVfxInstanceState &state) const
std::unordered_map< std::string, AttackVfxRecipe > recipes
Result< void > stopLiveLayer(LiveLayer &live, AttackVfxStopBehavior behavior)
std::unordered_map< std::string, AttackVfxSkin > skins
const AttackVfxInstanceState * peek(AttackVfxHandle handle) const noexcept
Result< void > startOneLayer(Slot &slot, std::size_t phaseIndex, const AttackVfxPhase &authored, const AttackVfxLayer &layer, std::size_t layerIndex, const AttackVfxSkin *skin, AttackVfxFrame &frame)
Result< void > applyCue(Slot &slot, std::string_view cue, AttackVfxFrame &frame)
AttackVfxLayerStartRequest makeLayerRequest(const AttackVfxInstanceState &state, AttackVfxPhaseKind phase, std::size_t phaseIndex, std::size_t layerIndex, const AttackVfxLayer *layer, const AttackVfxSkin *skin) const
Result< void > finishInstance(std::size_t slotIndex, AttackVfxFrame &frame, std::string_view cue, bool clearImmediately)
Result< void > enterPhase(Slot &slot, std::size_t phaseIndex, AttackVfxFrame &frame)
Result< void > updateDraining(Slot &slot, double dtSeconds)
Result< void > updateLiveLayer(Slot &slot, LiveLayer &live, double dtSeconds)
Result< void > updatePhaseLayers(Slot &slot, std::size_t phaseIndex, double dtSeconds)
Reusable elemental / style skin applied at play time.
Result< void > validate() const
Validate id, palette range, and finite profile values.
bool started
Definition Graphics.cpp:183
bool consumed
Definition Graphics.cpp:184