载入中...
搜索中...
未找到
CombatCharacter.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <utility>
6
7namespace eve::combat {
8namespace {
9
10bool finite2(double x, double z) { return std::isfinite(x) && std::isfinite(z); }
11
12bool finite3(CombatVector3 value) { return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z); }
13
14double lengthXZ(CombatVector3 value) { return std::hypot(value.x, value.z); }
15
16CombatVector3 normalizedXZ(CombatVector3 value) {
17 const double magnitude = lengthXZ(value);
18 if (magnitude <= 0.0) return {};
19 return {value.x / magnitude, 0.0, value.z / magnitude};
20}
21
22CombatVector3 scaleXZ(CombatVector3 value, double scale) { return {value.x * scale, 0.0, value.z * scale}; }
23
24} // namespace
25
28 Diagnostic::error(DiagnosticCode::InvalidArgument, "subject is nil", "subject"));
29 if (ownerId.empty()) return Result<void>::failure(
30 Diagnostic::error(DiagnosticCode::InvalidArgument, "owner id is empty", "ownerId"));
31 if (!finite3(initialPosition) || !std::isfinite(maximumSpeed) || maximumSpeed <= 0.0 ||
32 !std::isfinite(acceleration) || acceleration <= 0.0 || !std::isfinite(jumpSpeed) || jumpSpeed < 0.0 ||
33 !std::isfinite(gravity) || gravity < 0.0 || !std::isfinite(dodgeSpeed) || dodgeSpeed <= 0.0 ||
34 !std::isfinite(dodgeDuration) || dodgeDuration <= 0.0 || !std::isfinite(capsuleRadius) || capsuleRadius <= 0.0)
36 Diagnostic::error(DiagnosticCode::InvalidArgument, "character movement limits are invalid", "movement"));
37 return Result<void>::success();
38}
39
41 auto valid = definition.validate();
42 if (!valid) return valid;
43 const std::string key = definition.subject.format();
44 if (states_.contains(key))
46 Diagnostic::error(DiagnosticCode::AlreadyExists, "character subject already exists", key));
48 state.subject = definition.subject;
49 state.ownerId = std::move(definition.ownerId);
50 state.position = definition.initialPosition;
51 state.facing = {1.0, 0.0, 0.0};
52 state.maximumSpeed = definition.maximumSpeed;
53 state.acceleration = definition.acceleration;
54 state.jumpSpeed = definition.jumpSpeed;
55 state.gravity = definition.gravity;
56 state.dodgeSpeed = definition.dodgeSpeed;
57 state.dodgeDuration = definition.dodgeDuration;
58 state.capsuleRadius = definition.capsuleRadius;
59 state.maxAirJumps = definition.maxAirJumps;
61 states_.emplace(key, std::move(state));
63}
64
66 if (!subject.isValid()) return Result<void>::failure(
67 Diagnostic::error(DiagnosticCode::InvalidArgument, "subject is nil", "subject"));
68 if (!states_.erase(subject.format()))
70 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
72}
73
75 const auto found = states_.find(subject.format());
76 if (found == states_.end())
78 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
80}
81
83 auto found = states_.find(subject.format());
84 if (found == states_.end())
86 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
87 if (found->second.mode == CombatCharacterMode::Dead || found->second.mode == CombatCharacterMode::Stunned)
89 if (!finite2(direction.x, direction.z) || !std::isfinite(speedFraction) || speedFraction < 0.0 ||
90 speedFraction > 1.0)
92 Diagnostic::error(DiagnosticCode::InvalidArgument, "move intent is invalid", "moveIntent"));
93 found->second.moveDirection = normalizedXZ(direction);
94 found->second.moveSpeedFraction = speedFraction;
95 if (lengthXZ(found->second.moveDirection) > 0.0) found->second.facing = found->second.moveDirection;
97}
98
100 auto found = states_.find(subject.format());
101 if (found == states_.end())
103 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
104 auto& state = found->second;
109 state.velocity.y = state.jumpSpeed;
111 state.airJumpsUsed = 0;
113 }
115 if (state.airJumpsUsed >= state.maxAirJumps) return Result<void>::success(Status::success(StatusCode::NoOp));
116 state.velocity.y = state.jumpSpeed;
117 state.airJumpsUsed += 1;
120 }
122}
123
125 auto found = states_.find(subject.format());
126 if (found == states_.end())
128 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
129 auto& state = found->second;
133 CombatVector3 dodgeDir = state.facing;
134 if (direction) {
135 if (!finite2(direction->x, direction->z)) return Result<void>::failure(
136 Diagnostic::error(DiagnosticCode::InvalidArgument, "dodge direction is invalid", "direction"));
137 dodgeDir = normalizedXZ(*direction);
138 if (lengthXZ(dodgeDir) <= 0.0) dodgeDir = state.facing;
139 }
140 state.facing = dodgeDir;
141 state.velocity = {dodgeDir.x * state.dodgeSpeed, 0.0, dodgeDir.z * state.dodgeSpeed};
143 state.modeTimeRemaining = state.dodgeDuration;
144 state.invulnerable = true;
145 state.rootMotionDelta.reset();
147}
148
151 auto found = states_.find(subject.format());
152 if (found == states_.end())
154 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
155 if (!finite3(lockTarget)) return Result<void>::failure(
156 Diagnostic::error(DiagnosticCode::InvalidArgument, "lock target is invalid", "lockTarget"));
157 CombatVector3 toTarget =
158 normalizedXZ({lockTarget.x - found->second.position.x, 0.0, lockTarget.z - found->second.position.z});
159 if (lengthXZ(toTarget) <= 0.0) toTarget = found->second.facing;
160 CombatVector3 direction = toTarget;
161 switch (relative) {
162 case CombatDodgeRelative::Forward: direction = toTarget; break;
163 case CombatDodgeRelative::Back: direction = {-toTarget.x, 0.0, -toTarget.z}; break;
164 case CombatDodgeRelative::Left: direction = {toTarget.z, 0.0, -toTarget.x}; break;
165 case CombatDodgeRelative::Right: direction = {-toTarget.z, 0.0, toTarget.x}; break;
166 }
167 return dodge(subject, direction);
168}
169
171 auto found = states_.find(subject.format());
172 if (found == states_.end())
174 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
175 if (found->second.mode == CombatCharacterMode::Dead || found->second.mode == CombatCharacterMode::Stunned ||
176 found->second.mode == CombatCharacterMode::Dodging)
179 found->second.modeTimeRemaining = 0.0;
181}
182
184 auto found = states_.find(subject.format());
185 if (found == states_.end())
187 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
188 if (found->second.mode != CombatCharacterMode::Attacking)
190 found->second.mode = found->second.position.y > 0.0 ? CombatCharacterMode::Airborne : CombatCharacterMode::Grounded;
191 found->second.rootMotionDelta.reset();
193}
194
196 auto found = states_.find(subject.format());
197 if (found == states_.end())
199 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
201 Diagnostic::error(DiagnosticCode::InvalidArgument, "stun duration is invalid", "duration"));
202 if (found->second.mode == CombatCharacterMode::Dead)
204 found->second.mode = CombatCharacterMode::Stunned;
205 found->second.modeTimeRemaining = duration.seconds();
206 found->second.invulnerable = false;
207 found->second.moveSpeedFraction = 0.0;
208 found->second.moveDirection = {};
209 found->second.rootMotionDelta.reset();
211}
212
214 auto found = states_.find(subject.format());
215 if (found == states_.end())
217 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
218 if (!finite3(impulse)) return Result<void>::failure(
219 Diagnostic::error(DiagnosticCode::InvalidArgument, "impulse is invalid", "impulse"));
220 if (found->second.mode == CombatCharacterMode::Dead)
222 found->second.velocity.x += impulse.x;
223 found->second.velocity.y += impulse.y;
224 found->second.velocity.z += impulse.z;
225 if (impulse.y > 0.0 && found->second.mode == CombatCharacterMode::Grounded)
228}
229
231 Duration stunDuration, Impulse3 knockback) {
232 if (reaction == HitReaction::Death) return kill(subject);
233 if (reaction == HitReaction::Flinch || reaction == HitReaction::Stagger || reaction == HitReaction::Knockdown) {
234 auto stunned = applyStun(subject, stunDuration);
235 if (!stunned) return stunned;
236 }
237 if (knockback.x != 0.0 || knockback.y != 0.0 || knockback.z != 0.0)
238 return applyImpulse(subject, {knockback.x, knockback.y, knockback.z});
240}
241
243 auto found = states_.find(subject.format());
244 if (found == states_.end())
246 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
247 found->second.mode = CombatCharacterMode::Dead;
248 found->second.modeTimeRemaining = 0.0;
249 found->second.invulnerable = false;
250 found->second.velocity = {};
251 found->second.moveSpeedFraction = 0.0;
252 found->second.rootMotionDelta.reset();
254}
255
257 auto found = states_.find(subject.format());
258 if (found == states_.end())
260 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
261 if (!finite3(delta)) return Result<void>::failure(
262 Diagnostic::error(DiagnosticCode::InvalidArgument, "root motion delta is invalid", "delta"));
263 found->second.rootMotionDelta = delta;
265}
266
268 auto found = states_.find(subject.format());
269 if (found == states_.end())
271 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
272 if (!finite2(facing.x, facing.z)) return Result<void>::failure(
273 Diagnostic::error(DiagnosticCode::InvalidArgument, "facing is invalid", "facing"));
274 CombatVector3 planar = normalizedXZ(facing);
275 if (lengthXZ(planar) <= 0.0) return Result<void>::failure(
276 Diagnostic::error(DiagnosticCode::InvalidArgument, "facing is degenerate", "facing"));
277 found->second.facing = planar;
279}
280
282 auto found = states_.find(subject.format());
283 if (found == states_.end())
285 Diagnostic::error(DiagnosticCode::NotFound, "character subject was not found", subject.format()));
286 found->second.timeFrozen = frozen;
288}
289
291 if (step.tick.value() < lastTick_.value())
293 Diagnostic::error(DiagnosticCode::InvalidArgument, "character tick must be non-decreasing", "tick"));
294 if (step.delta < Duration::zero())
296 Diagnostic::error(DiagnosticCode::InvalidArgument, "character delta is invalid", "delta"));
297
298 const double dt = step.delta.seconds();
300 advance.tick = step.tick;
301 std::map<std::string, CombatCharacterState, std::less<>> candidate = states_;
302
303 for (auto& [key, state] : candidate) {
304 (void)key;
305 const CombatCharacterMode previousMode = state.mode;
306 const CombatVector3 previousPosition = state.position;
307
308 if (state.mode == CombatCharacterMode::Dead || state.timeFrozen) continue;
309
310 double ground = 0.0;
311 if (ground_) {
312 auto sampled = ground_->sampleHeight(state.position.x, state.position.z);
313 if (!sampled) return Result<CombatCharacterAdvance>::failure(sampled.status());
314 ground = sampled.value();
315 if (!std::isfinite(ground))
317 Diagnostic::error(DiagnosticCode::InvalidArgument, "ground height is invalid", "ground"));
318 }
319
321 state.modeTimeRemaining = std::max(0.0, state.modeTimeRemaining - dt);
322 if (state.modeTimeRemaining <= 0.0) {
324 advance.events.push_back(
325 {state.subject, CombatCharacterEventKind::DodgeEnded, state.mode, state.position, step.tick});
326 state.invulnerable = false;
327 }
329 }
330 }
331
332 CombatVector3 unconstrained = state.position;
334 unconstrained.x += state.velocity.x * dt;
335 unconstrained.z += state.velocity.z * dt;
336 } else if (state.mode == CombatCharacterMode::Attacking && state.rootMotionDelta) {
337 unconstrained = {state.position.x + state.rootMotionDelta->x, state.position.y + state.rootMotionDelta->y,
338 state.position.z + state.rootMotionDelta->z};
339 state.rootMotionDelta.reset();
340 } else if (state.mode == CombatCharacterMode::Stunned) {
341 unconstrained.x += state.velocity.x * dt;
342 unconstrained.z += state.velocity.z * dt;
343 } else {
344 const double targetSpeed = state.maximumSpeed * state.moveSpeedFraction;
345 CombatVector3 desired = {state.moveDirection.x * targetSpeed, 0.0, state.moveDirection.z * targetSpeed};
346 CombatVector3 planar = {state.velocity.x, 0.0, state.velocity.z};
347 CombatVector3 deltaV = {desired.x - planar.x, 0.0, desired.z - planar.z};
348 const double deltaMag = lengthXZ(deltaV);
349 const double maxDelta = state.acceleration * dt;
350 if (deltaMag > maxDelta && deltaMag > 0.0) deltaV = scaleXZ(deltaV, maxDelta / deltaMag);
351 state.velocity.x += deltaV.x;
352 state.velocity.z += deltaV.z;
353 unconstrained.x += state.velocity.x * dt;
354 unconstrained.z += state.velocity.z * dt;
355 }
356
357 if (probe_) {
358 auto resolved = probe_->resolve(previousPosition, unconstrained, state.capsuleRadius);
359 if (!resolved) return Result<CombatCharacterAdvance>::failure(resolved.status());
360 unconstrained = resolved.value();
361 }
362 state.position = unconstrained;
363
364 if (ground_) {
365 auto sampled = ground_->sampleHeight(state.position.x, state.position.z);
366 if (!sampled) return Result<CombatCharacterAdvance>::failure(sampled.status());
367 ground = sampled.value();
368 if (!std::isfinite(ground))
370 Diagnostic::error(DiagnosticCode::InvalidArgument, "ground height is invalid", "ground"));
371 }
372
375 (state.mode == CombatCharacterMode::Grounded && state.position.y > ground)) {
376 state.velocity.y -= state.gravity * dt;
377 state.position.y += state.velocity.y * dt;
378 if (state.position.y <= ground) {
379 state.position.y = ground;
380 state.velocity.y = 0.0;
381 state.airJumpsUsed = 0;
384 advance.events.push_back(
385 {state.subject, CombatCharacterEventKind::Landed, state.mode, state.position, step.tick});
386 }
387 } else if (state.mode == CombatCharacterMode::Grounded) {
389 }
390 }
391
392 if (previousMode != state.mode)
393 advance.events.push_back(
394 {state.subject, CombatCharacterEventKind::ModeChanged, state.mode, state.position, step.tick});
395 if (previousPosition.x != state.position.x || previousPosition.y != state.position.y ||
396 previousPosition.z != state.position.z)
397 advance.events.push_back(
398 {state.subject, CombatCharacterEventKind::Moved, state.mode, state.position, step.tick});
399 }
400
401 states_ = std::move(candidate);
402 lastTick_ = step.tick;
404}
405
406std::vector<CombatCharacterState> CombatCharacterRuntime::states() const {
407 std::vector<CombatCharacterState> result;
408 result.reserve(states_.size());
409 for (const auto& [key, state] : states_) {
410 (void)key;
411 result.push_back(state);
412 }
413 return result;
414}
415
416} // namespace eve::combat
double value
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
float duration
Vec3 relative
Definition AnimSmr.cpp:164
int subject
Definition AnimSmr.cpp:163
3D action-combat character controller.
std::uint32_t key
std::array< float, 3 > scale
bool valid
ModuleFacing facing
RoadLaneDirection direction
bool found
float step
Definition TreeMesh.cpp:314
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
Signed, fixed-resolution simulation duration in nanoseconds.
Definition Time.h:43
static constexpr Duration zero() noexcept
Return zero duration.
Definition Time.h:60
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
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
Strong, domain-neutral reference to a runtime subject.
Definition SubjectRef.h:26
bool isValid() const noexcept
Return whether this reference is valid and non-nil.
Definition SubjectRef.h:38
Result< void > applyStun(SubjectRef subject, Duration duration)
Apply a stun that blocks jump/dodge/attack for the supplied duration.
Result< void > applyDamageReaction(SubjectRef subject, HitReaction reaction, Duration stunDuration, Impulse3 knockback)
Apply death/stun and knockback from one damage outcome.
Result< void > setFacing(SubjectRef subject, CombatVector3 facing)
Replace planar facing from a finite XZ vector; Y is ignored.
Result< void > setTimeFrozen(SubjectRef subject, bool frozen)
Pause integration for hitstop while retaining the committed pose.
Result< void > kill(SubjectRef subject)
Mark the character dead; further locomotion intents become NoOp.
Result< void > setRootMotionDelta(SubjectRef subject, CombatVector3 delta)
Queue one frame of authored root-motion delta consumed by the next advance.
Result< CombatCharacterAdvance > advance(const SimulationStep &step)
Atomically advance every character using only the supplied deterministic step.
std::vector< CombatCharacterState > states() const
Return owning states in stable subject-id order.
Result< void > endAttack(SubjectRef subject)
Leave attacking mode when recover completes.
Result< CombatCharacterState > state(SubjectRef subject) const
Return an owning state snapshot, or NotFound for a stale subject.
Result< void > setMoveIntent(SubjectRef subject, CombatVector3 direction, double speedFraction)
Set normalized planar movement intent.
Result< void > beginAttack(SubjectRef subject)
Enter attacking mode; root motion may replace horizontal intent.
Result< void > dodge(SubjectRef subject, std::optional< CombatVector3 > direction=std::nullopt)
Begin a dodge that grants invulnerability for dodgeDuration.
Result< void > unregisterSubject(SubjectRef subject)
Remove one character; missing subjects are rejected.
Result< void > registerSubject(CombatCharacterDefinition definition)
Register one unique character from validated owning definition data.
Result< void > applyImpulse(SubjectRef subject, CombatVector3 impulse)
Add an instantaneous velocity impulse (knockback / launch).
Result< void > jump(SubjectRef subject)
Request a jump or air jump when the current mode allows it.
Result< void > dodgeRelative(SubjectRef subject, CombatVector3 lockTarget, CombatDodgeRelative relative)
Dodge relative to a lock-on target on the XZ plane.
virtual Result< double > sampleHeight(double x, double z) const =0
Return finite ground height at the supplied world XZ.
virtual Result< CombatVector3 > resolve(const CombatVector3 &from, const CombatVector3 &to, double radius) const =0
Resolve a candidate translation for one capsule.
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
CombatCharacterMode
High-level locomotion mode owned by the character controller.
CombatDodgeRelative
Lock-relative dodge direction on the XZ plane.
HitReaction
Typed reaction selected after health and poise are resolved.
Definition Damage.h:26
One deterministic fixed-step emitted by SimulationClock.
Definition Time.h:158
Complete owning result of one character advance.
Immutable registration for one action-combat character.
Result< void > validate() const
Validate identity and finite positive movement limits.
Owning read-only projection of one character.
Finite 3D combat vector.
Renderer/physics-neutral knockback impulse authored in world units.
Definition Damage.h:19