18std::vector<AnimStateMachine*>& machines() {
19 static std::vector<AnimStateMachine*>
instances;
23double stateNumber(
const StateValue&
v) {
return v.isInt() ?
static_cast<double>(
v.asInt()) :
v.asDouble(); }
28 const char* stateKind()
const override {
return "anim"; }
30 bool captureState(StateValue& out)
override {
32 for (AnimStateMachine*
m : machines()) {
34 if (
m->captureState(sv)) arr.pushBack(std::move(sv));
40 bool restoreState(
const StateValue& in, std::string* err)
override {
42 if (err) *err =
"anim: expected array of state machines";
45 const size_t n = std::min(in.arraySize(), machines().
size());
46 for (
size_t i = 0; i <
n; ++i) {
47 if (!machines()[i]->restoreState(in.at(i), err))
return false;
52 bool resetToDefaults()
override {
53 for (AnimStateMachine*
m : machines())
m->resetToDefaults();
60 static AnimStateProvider provider;
61 eve::cap::addListener<eve::caps::IStateProvider>(&provider);
68 if (!skeleton_)
throw Exception(
"AnimStateMachine: skeleton is null");
73 machines().push_back(
this);
77 machines().erase(std::remove(machines().
begin(), machines().
end(),
this), machines().
end());
80void AnimStateMachine::requireState(
const std::string &
name)
const {
81 if (states_.find(
name) == states_.end()) {
82 throw Exception(
"AnimStateMachine: unknown state '%s'",
name.c_str());
86AnimStateMachine::Transition &AnimStateMachine::requireTransition(
int id) {
87 if (id < 0 || id >=
static_cast<int>(transitions_.size())) {
88 throw Exception(
"AnimStateMachine: invalid transition id %d",
id);
90 return transitions_[
static_cast<size_t>(
id)];
93AnimStateMachine::FloatOp AnimStateMachine::parseFloatOp(
const std::string &op) {
94 if (op ==
">")
return FloatOp::Gt;
95 if (op ==
">=")
return FloatOp::Ge;
96 if (op ==
"<")
return FloatOp::Lt;
97 if (op ==
"<=")
return FloatOp::Le;
98 if (op ==
"==")
return FloatOp::Eq;
99 if (op ==
"!=")
return FloatOp::Ne;
100 throw Exception(
"AnimStateMachine: unknown float op '%s'", op.c_str());
104 if (
name.empty())
throw Exception(
"AnimStateMachine.addState: name empty");
105 if (!
clip)
throw Exception(
"AnimStateMachine.addState: clip is null");
106 if (states_.count(
name)) {
107 throw Exception(
"AnimStateMachine.addState: duplicate state '%s'",
name.c_str());
110 stateOrder_.push_back(
name);
115 currentState_ =
name;
119 states_[
name].clip->sample(0.f, &pose_, skeleton_);
123 return states_.count(
name) > 0;
127 float blendSeconds) {
130 if (blendSeconds < 0.f) {
131 throw Exception(
"AnimStateMachine.addTransition: blendSeconds must be >= 0");
136 tr.blendSeconds = blendSeconds;
137 transitions_.push_back(
tr);
138 return static_cast<int>(transitions_.size()) - 1;
142 const std::string &op,
float threshold) {
143 if (param.empty())
throw Exception(
"AnimStateMachine.addFloatCondition: param empty");
144 Transition &
tr = requireTransition(transitionId);
146 c.kind = CondKind::Float;
148 c.op = parseFloatOp(op);
149 c.threshold = threshold;
150 tr.conditions.push_back(
c);
151 floats_.emplace(param, 0.f);
155 if (param.empty())
throw Exception(
"AnimStateMachine.addBoolCondition: param empty");
156 Transition &
tr = requireTransition(transitionId);
158 c.kind = CondKind::Bool;
161 tr.conditions.push_back(
c);
162 bools_.emplace(param,
false);
166 if (param.empty())
throw Exception(
"AnimStateMachine.addTriggerCondition: param empty");
167 Transition &
tr = requireTransition(transitionId);
169 c.kind = CondKind::Trigger;
171 tr.conditions.push_back(
c);
172 triggers_.emplace(param,
false);
176 Transition &
tr = requireTransition(transitionId);
177 tr.exitTime =
clampf(normalizedTime, 0.f, 1.f);
178 tr.hasExitTime =
true;
182 requireTransition(transitionId).hasExitTime =
enabled;
188 auto it = floats_.find(
name);
189 return it == floats_.end() ? 0.f : it->second;
195 auto it = bools_.find(
name);
196 return it == bools_.end() ? false : it->second;
205bool AnimStateMachine::conditionsMet(
const Transition &
tr)
const {
206 for (
const Condition &
c :
tr.conditions) {
208 case CondKind::Float: {
212 case FloatOp::Gt:
ok =
v >
c.threshold;
break;
213 case FloatOp::Ge:
ok =
v >=
c.threshold;
break;
214 case FloatOp::Lt:
ok =
v <
c.threshold;
break;
215 case FloatOp::Le:
ok =
v <=
c.threshold;
break;
216 case FloatOp::Eq:
ok = std::fabs(
v -
c.threshold) < 1e-5f;
break;
217 case FloatOp::Ne:
ok = std::fabs(
v -
c.threshold) >= 1e-5f;
break;
219 if (!
ok)
return false;
222 case CondKind::Bool: {
223 if (
getBool(
c.param) !=
c.boolValue)
return false;
226 case CondKind::Trigger: {
227 auto it = triggers_.find(
c.param);
228 if (it == triggers_.end() || !it->second)
return false;
236bool AnimStateMachine::tryTransition() {
237 if (currentState_.empty() || blending_)
return false;
238 const State &cur = states_.at(currentState_);
239 const float dur = cur.clip ? cur.clip->getDuration() : 0.f;
240 const float norm = dur > 1e-8f ?
clampf(stateTime_ / dur, 0.f, 1.f) : 1.f;
242 for (
const Transition &
tr : transitions_) {
243 if (
tr.from != currentState_)
continue;
244 if (
tr.hasExitTime && norm <
tr.exitTime)
continue;
245 if (!conditionsMet(
tr))
continue;
248 for (
const Condition &
c :
tr.conditions) {
249 if (
c.kind == CondKind::Trigger) triggers_[
c.param] =
false;
253 blendDuration_ =
tr.blendSeconds;
256 if (blendDuration_ <= 1e-6f) {
257 currentState_ = nextState_;
259 states_.at(currentState_).clip->sample(0.f, &pose_, skeleton_);
263 currentState_ = nextState_;
265 states_.at(currentState_).clip->sample(0.f, &toPose_, skeleton_);
266 pose_.
blendFrom(&fromPose_, &toPose_, 0.f);
273void AnimStateMachine::updateUnchecked(
float dt) {
274 if (dt < 0.f)
throw Exception(
"AnimStateMachine.update: dt must be >= 0");
276 if (!stateOrder_.empty())
setEntry(stateOrder_.front());
279 if (currentState_.empty())
return;
284 const State &dst = states_.at(currentState_);
285 dst.clip->sample(stateTime_, &toPose_, skeleton_);
286 float t = blendDuration_ > 1e-8f ? blendElapsed_ / blendDuration_ : 1.f;
295 if (!blending_) tryTransition();
300 const State &st = states_.at(currentState_);
301 st.clip->sample(stateTime_, &pose_, skeleton_);
306 if (
step.delta.nanoseconds() < 0)
308 "animation state-machine delta must be non-negative"));
309 if (hasLastTick_ &&
step.tick <= lastTick_)
313 const float dt =
static_cast<float>(
step.delta.seconds());
314 if (!std::isfinite(dt))
318 lastTick_ =
step.tick;
326 duration.ignore(
"legacy animation state-machine update received an invalid duration");
332 "animation state-machine simulation tick overflow"))
333 .
ignore(
"legacy animation state-machine update tick overflow");
336 advance({*nextTick, std::move(
duration).takeValue()}).ignore(
"legacy animation state-machine update facade");
353 out.
set(
"floats", std::move(floats));
357 out.
set(
"bools", std::move(bools));
361 out.
set(
"triggers", std::move(triggers));
367 if (err) *err =
"anim: state is not an object";
373 if (err) *err =
"anim: missing currentState/started";
376 const std::string stateName =
current->asString();
377 const bool wasStarted =
started->asBool();
378 if (wasStarted && !stateName.empty() && !
hasState(stateName)) {
379 if (err) *err =
"anim: unknown state '" + stateName +
"'";
384 bool restoredHasTick =
false;
385 if (
const StateValue* hasTick = in.
find(
"hasSimulationTick"); hasTick && hasTick->isBool())
386 restoredHasTick = hasTick->asBool();
387 if (restoredHasTick) {
390 if (err) *err =
"anim: missing simulationTick";
394 std::size_t parsed = 0;
395 const std::uint64_t
value = std::stoull(
tick->asString(), &parsed);
396 if (parsed !=
tick->asString().size())
throw std::invalid_argument(
"trailing characters");
399 if (err) *err =
"anim: invalid simulationTick";
408 if (
const StateValue* floats = in.
find(
"floats"); floats && floats->isObject()) {
409 for (
const auto&
key : floats->keys()) {
411 if (
v && (
v->isInt() ||
v->isFloat())) floats_[
key] =
static_cast<float>(stateNumber(*
v));
415 for (
const auto&
key : bools->keys()) {
417 if (
v &&
v->isBool()) bools_[
key] =
v->asBool();
421 for (
const auto&
key : triggers->keys()) {
423 if (
v &&
v->isBool()) triggers_[
key] =
v->asBool();
427 currentState_ = stateName;
429 if (
const StateValue* st = in.
find(
"stateTime"); st && (st->isInt() || st->isFloat()))
430 stateTime_ =
static_cast<float>(stateNumber(*st));
431 if (
const StateValue* bd = in.
find(
"blendDuration"); bd && (bd->isInt() || bd->isFloat()))
432 blendDuration_ =
static_cast<float>(stateNumber(*bd));
433 if (
const StateValue* be = in.
find(
"blendElapsed"); be && (be->isInt() || be->isFloat()))
434 blendElapsed_ =
static_cast<float>(stateNumber(*be));
436 started_ = wasStarted;
437 lastTick_ = restoredTick;
438 hasLastTick_ = restoredHasTick;
440 if (started_ && !currentState_.empty()) {
441 states_.at(currentState_).clip->sample(stateTime_, &pose_, skeleton_);
453 blendDuration_ = 0.f;
456 hasLastTick_ =
false;
457 if (!stateOrder_.empty()) {
462 currentState_.clear();
std::vector< BuildingInstanceSnapshot > instances
HexCoordinates to
Cell the unit walks towards on this segment.
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.
static Result< Duration > fromSeconds(double seconds)
Convert finite seconds to the nearest nanosecond.
EVENGINE_API_FOUNDATION public API.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
static Result failure(Status status)
Construct a failed result from a structured status.
JSON-compatible state value tree used by state hot reload.
static StateValue object()
Empty object.
static StateValue boolean(bool v)
Boolean value.
static StateValue array()
Empty array.
static StateValue number(double v)
Floating-point value.
const StateValue * find(const std::string &key) const
Look up key; nullptr when absent.
static StateValue string(std::string v)
String value.
void set(const std::string &key, StateValue v)
Insert or replace key; only valid on objects.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Keyframed skeletal animation clip (local TRS tracks per bone). Script type: AnimClip.
Evaluated local (and optional world) pose for an AnimSkeleton. Script type: AnimPose.
void blendFrom(const AnimPose *a, const AnimPose *b, float t)
Blend from.
void copyFrom(const AnimPose *other)
Copies from.
void resize(int boneCount)
Resize.
3D bone hierarchy + bind-pose local TRS for skeletal animation. Independent of ik::Skeleton3D (FABRIK...
int getBoneCount() const
Returns the bone count.
void applyBindPose(class AnimPose *pose) const
Fill pose locals with bind pose.
void setHasExitTime(int transitionId, bool enabled)
Sets the has exit time.
float getFloat(const std::string &name) const
Returns the float.
int addTransition(const std::string &from, const std::string &to, float blendSeconds)
Add transition from → to with blend duration.
void addTriggerCondition(int transitionId, const std::string ¶m)
Adds trigger condition.
bool captureState(StateValue &out) const
Serialize runtime state (params, current/next state, blend).
void update(float dt)
Legacy seconds facade retained for scripts and old callers.
bool hasState(const std::string &name) const
True when state.
AnimStateMachine(AnimSkeleton *skeleton)
Anim state machine.
void resetTrigger(const std::string &name)
Resets trigger.
void addBoolCondition(int transitionId, const std::string ¶m, bool value)
Adds bool condition.
void setTrigger(const std::string &name)
Sets the trigger.
void addFloatCondition(int transitionId, const std::string ¶m, const std::string &op, float threshold)
Comparison op: ">", ">=", "<", "<=", "==", "!=".
void setBool(const std::string &name, bool value)
Sets the bool.
void setFloat(const std::string &name, float value)
Sets the float.
bool restoreState(const StateValue &in, std::string *err=nullptr)
Restore runtime state captured by captureState().
void addState(const std::string &name, AnimClip *clip)
Adds state.
bool getBool(const std::string &name) const
Returns the bool.
~AnimStateMachine() override
Anim state machine.
AnimPose * getPose() override
Borrowed pointer accessor. @ownership Borrowed @lifetime Valid while the owning animation object rema...
eve::Result< void > advance(const eve::SimulationStep &step) override
Advance state and pose using one scheduler-owned deterministic step.
void setEntry(const std::string &name)
Sets the entry.
void setExitTime(int transitionId, float normalizedTime)
Optional: require normalized local time in [0,1] >= exitTime before transition.
bool resetToDefaults()
Clear params and re-seat the entry state (restore fallback).
Runtime-state serialization for state hot reload.
constexpr std::optional< StrongUint64 > incremented() const noexcept
Returns the next value, or empty instead of unsigned wraparound.
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
static constexpr StrongUint64 zero() noexcept
Returns the zero value for this strong type.
float clampf(float v, float lo, float hi)
Clampf.
WGPUStringView sv(const char *s)
Build a WGPUStringView from a C string (null-safe, length auto-computed).
State
Lifecycle state of a generic transaction plan.
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
One deterministic fixed-step emitted by SimulationClock.