载入中...
搜索中...
未找到
MeleeHit.cpp
浏览该文件的文档.
1#include "combat/MeleeHit.h"
2
3#include <algorithm>
4#include <cmath>
5#include <limits>
6#include <tuple>
7#include <utility>
8
9namespace eve::combat {
10namespace {
11
12bool finitePoint(MeleePoint3 value) {
13 return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z);
14}
15
16MeleePoint3 add(MeleePoint3 a, MeleePoint3 b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
17
18MeleePoint3 sub(MeleePoint3 a, MeleePoint3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
19
20MeleePoint3 scale(MeleePoint3 a, double s) { return {a.x * s, a.y * s, a.z * s}; }
21
22double dot(MeleePoint3 a, MeleePoint3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
23
24double lengthSq(MeleePoint3 a) { return dot(a, a); }
25
26double length(MeleePoint3 a) { return std::sqrt(lengthSq(a)); }
27
28MeleePoint3 normalizeOrZero(MeleePoint3 a) {
29 const double mag = length(a);
30 return mag > 0.0 ? scale(a, 1.0 / mag) : MeleePoint3{};
31}
32
33MeleePoint3 rotateYaw(MeleePoint3 local, double yaw) {
34 const double c = std::cos(yaw);
35 const double s = std::sin(yaw);
36 return {local.x * c - local.z * s, local.y, local.x * s + local.z * c};
37}
38
39struct CapsuleWorld {
40 MeleePoint3 a;
41 MeleePoint3 b;
42 double radius = 0.0;
43};
44
45CapsuleWorld capsuleAt(const MeleeShape& shape, const MeleePose& pose, MeleePoint3 localOffset) {
46 const MeleePoint3 center = add(pose.position, rotateYaw(localOffset, pose.yawRadians));
47 const MeleePoint3 axis = rotateYaw({0.0, shape.halfHeight, 0.0}, pose.yawRadians);
48 return {sub(center, axis), add(center, axis), shape.radius};
49}
50
51MeleePoint3 closestOnSegment(MeleePoint3 p, MeleePoint3 a, MeleePoint3 b) {
52 const MeleePoint3 ab = sub(b, a);
53 const double denom = lengthSq(ab);
54 if (denom <= 0.0) return a;
55 const double t = std::clamp(dot(sub(p, a), ab) / denom, 0.0, 1.0);
56 return add(a, scale(ab, t));
57}
58
59bool spheresOverlap(MeleePoint3 a, double ra, MeleePoint3 b, double rb, MeleePoint3& contact, MeleePoint3& normal) {
60 const MeleePoint3 delta = sub(b, a);
61 const double dist = length(delta);
62 const double limit = ra + rb;
63 if (dist > limit) return false;
64 normal = dist > 0.0 ? scale(delta, 1.0 / dist) : MeleePoint3{1.0, 0.0, 0.0};
65 contact = add(a, scale(normal, ra));
66 return true;
67}
68
69bool capsuleSphereOverlap(const CapsuleWorld& capsule, MeleePoint3 center, double radius, MeleePoint3& contact,
70 MeleePoint3& normal) {
71 const MeleePoint3 closest = closestOnSegment(center, capsule.a, capsule.b);
72 return spheresOverlap(closest, capsule.radius, center, radius, contact, normal);
73}
74
75bool capsuleCapsuleOverlap(const CapsuleWorld& a, const CapsuleWorld& b, MeleePoint3& contact, MeleePoint3& normal) {
76 // Sample closest approach via discrete segment samples; deterministic and adequate for arena scale.
77 constexpr int kSamples = 8;
78 double best = std::numeric_limits<double>::infinity();
79 MeleePoint3 bestA{};
80 MeleePoint3 bestB{};
81 for (int i = 0; i <= kSamples; ++i) {
82 const double t = static_cast<double>(i) / static_cast<double>(kSamples);
83 const MeleePoint3 pa = add(a.a, scale(sub(a.b, a.a), t));
84 const MeleePoint3 pb = closestOnSegment(pa, b.a, b.b);
85 const double d = lengthSq(sub(pb, pa));
86 if (d < best) {
87 best = d;
88 bestA = pa;
89 bestB = pb;
90 }
91 }
92 return spheresOverlap(bestA, a.radius, bestB, b.radius, contact, normal);
93}
94
95bool shapesOverlap(const MeleeShape& attackShape, const MeleePose& attackPose, MeleePoint3 attackOffset,
96 const MeleeShape& hurtShape, const MeleePose& hurtPose, MeleePoint3& contact,
97 MeleePoint3& normal) {
98 if (attackShape.kind == MeleeShapeKind::Sphere && hurtShape.kind == MeleeShapeKind::Sphere) {
99 const MeleePoint3 a = add(attackPose.position, rotateYaw(attackOffset, attackPose.yawRadians));
100 const MeleePoint3 b = hurtPose.position;
101 return spheresOverlap(a, attackShape.radius, b, hurtShape.radius, contact, normal);
102 }
103 if (attackShape.kind == MeleeShapeKind::Capsule && hurtShape.kind == MeleeShapeKind::Sphere) {
104 return capsuleSphereOverlap(capsuleAt(attackShape, attackPose, attackOffset), hurtPose.position,
105 hurtShape.radius, contact, normal);
106 }
107 if (attackShape.kind == MeleeShapeKind::Sphere && hurtShape.kind == MeleeShapeKind::Capsule) {
108 const bool hit =
109 capsuleSphereOverlap(capsuleAt(hurtShape, hurtPose, {}),
110 add(attackPose.position, rotateYaw(attackOffset, attackPose.yawRadians)),
111 attackShape.radius, contact, normal);
112 if (hit) normal = scale(normal, -1.0);
113 return hit;
114 }
115 return capsuleCapsuleOverlap(capsuleAt(attackShape, attackPose, attackOffset),
116 capsuleAt(hurtShape, hurtPose, {}), contact, normal);
117}
118
119std::string hurtKey(SubjectRef subject, std::string_view hurtboxId) {
120 return subject.format() + "|" + std::string(hurtboxId);
121}
122
123} // namespace
124
126 if (!std::isfinite(radius) || radius <= 0.0) return Result<void>::failure(
127 Diagnostic::error(DiagnosticCode::InvalidArgument, "melee shape radius is invalid", "radius"));
128 if (kind == MeleeShapeKind::Capsule && (!std::isfinite(halfHeight) || halfHeight < 0.0))
130 Diagnostic::error(DiagnosticCode::InvalidArgument, "melee capsule halfHeight is invalid", "halfHeight"));
133 Diagnostic::error(DiagnosticCode::InvalidArgument, "melee shape kind is invalid", "kind"));
134 return Result<void>::success();
135}
136
138 if (!finitePoint(position) || !std::isfinite(yawRadians)) return Result<void>::failure(
139 Diagnostic::error(DiagnosticCode::InvalidArgument, "melee pose is invalid", "pose"));
140 return Result<void>::success();
141}
142
145 Diagnostic::error(DiagnosticCode::InvalidArgument, "hurtbox subject is nil", "subject"));
146 if (hurtboxId.empty()) return Result<void>::failure(
147 Diagnostic::error(DiagnosticCode::InvalidArgument, "hurtbox id is empty", "hurtboxId"));
148 if (bodyPart.empty()) return Result<void>::failure(
149 Diagnostic::error(DiagnosticCode::InvalidArgument, "hurtbox body part is empty", "bodyPart"));
150 return shape.validate();
151}
152
154 if (hitboxId.empty()) return Result<void>::failure(
155 Diagnostic::error(DiagnosticCode::InvalidArgument, "hitbox id is empty", "hitboxId"));
156 if (!finitePoint(localOffset)) return Result<void>::failure(
157 Diagnostic::error(DiagnosticCode::InvalidArgument, "hitbox local offset is invalid", "localOffset"));
158 if (!std::isfinite(healthDamage) || healthDamage < 0.0) return Result<void>::failure(
159 Diagnostic::error(DiagnosticCode::InvalidArgument, "hitbox health damage is invalid", "healthDamage"));
160 if (!std::isfinite(poiseDamage) || poiseDamage < 0.0) return Result<void>::failure(
161 Diagnostic::error(DiagnosticCode::InvalidArgument, "hitbox poise damage is invalid", "poiseDamage"));
162 if (damageType.empty()) return Result<void>::failure(
163 Diagnostic::error(DiagnosticCode::InvalidArgument, "hitbox damage type is empty", "damageType"));
164 if (!std::isfinite(knockbackSpeed) || knockbackSpeed < 0.0)
166 Diagnostic::error(DiagnosticCode::InvalidArgument, "hitbox knockback speed is invalid", "knockbackSpeed"));
167 if (!std::isfinite(knockbackLift) || knockbackLift < 0.0)
169 Diagnostic::error(DiagnosticCode::InvalidArgument, "hitbox knockback lift is invalid", "knockbackLift"));
170 return shape.validate();
171}
172
174 auto valid = definition.validate();
175 if (!valid) return valid;
176 hitboxes_[definition.hitboxId] = std::move(definition);
178}
179
181 auto valid = definition.validate();
182 if (!valid) return valid;
183 const std::string key = hurtKey(definition.subject, definition.hurtboxId);
184 HurtboxState state;
185 state.definition = std::move(definition);
186 hurtboxes_[key] = std::move(state);
188}
189
191 if (!subject.isValid()) return Result<void>::failure(
192 Diagnostic::error(DiagnosticCode::InvalidArgument, "subject is nil", "subject"));
193 const std::string prefix = subject.format() + "|";
194 for (auto it = hurtboxes_.begin(); it != hurtboxes_.end();) {
195 if (it->first.rfind(prefix, 0) == 0)
196 it = hurtboxes_.erase(it);
197 else
198 ++it;
199 }
201}
202
204 auto valid = pose.validate();
205 if (!valid) return valid;
206 const auto found = hurtboxes_.find(hurtKey(subject, hurtboxId));
207 if (found == hurtboxes_.end())
208 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "hurtbox was not found", "hurtboxId"));
209 found->second.pose = pose;
210 found->second.hasPose = true;
212}
213
215 action::ActionExecutionId execution) {
216 if (!subject.isValid()) return Result<void>::failure(
217 Diagnostic::error(DiagnosticCode::InvalidArgument, "subject is nil", "subject"));
218 if (hitboxId.empty()) return Result<void>::failure(
219 Diagnostic::error(DiagnosticCode::InvalidArgument, "hitbox id is empty", "hitboxId"));
220 if (!hitboxes_.contains(std::string(hitboxId)))
221 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "hitbox catalog entry missing",
222 std::string(hitboxId)));
223 if (!poses_)
225 Diagnostic::error(DiagnosticCode::NotFound, "melee pose source is unavailable", "poseSource"));
226 auto pose = poses_->pose(subject, hitboxId);
227 if (!pose) return Result<void>::failure(pose.status());
228 auto valid = pose.value().validate();
229 if (!valid) return valid;
230 ArmedKey key{subject.format(), std::string(hitboxId), execution.value()};
231 ArmedHitbox armed;
232 armed.subject = subject;
233 armed.hitboxId = std::string(hitboxId);
234 armed.execution = execution;
235 armed.previous = pose.value();
236 armed.current = pose.value();
237 armed.hasPose = true;
238 armed_[std::move(key)] = std::move(armed);
240}
241
243 action::ActionExecutionId execution) {
244 ArmedKey key{subject.format(), std::string(hitboxId), execution.value()};
245 const auto found = armed_.find(key);
246 if (found == armed_.end()) return Result<void>::success(Status::success(StatusCode::NoOp));
247 for (auto it = hitMemory_.begin(); it != hitMemory_.end();) {
248 if (std::get<0>(it->first) == execution.value() && std::get<1>(it->first) == hitboxId)
249 it = hitMemory_.erase(it);
250 else
251 ++it;
252 }
253 armed_.erase(found);
255}
256
258 if (tick.value() < lastTick_.value())
260 Diagnostic::error(DiagnosticCode::InvalidArgument, "melee tick must be non-decreasing", "tick"));
261 if (!poses_ && !armed_.empty())
263 Diagnostic::error(DiagnosticCode::NotFound, "melee pose source is unavailable", "poseSource"));
264
265 MeleeHitFrame frame;
266 frame.tick = tick;
267 for (auto& [key, armed] : armed_) {
268 (void)key;
269 auto pose = poses_->pose(armed.subject, armed.hitboxId);
270 if (!pose) return Result<MeleeHitFrame>::failure(pose.status());
271 auto valid = pose.value().validate();
272 if (!valid) return Result<MeleeHitFrame>::failure(valid.status());
273 armed.previous = armed.current;
274 armed.current = pose.value();
275 const auto& definition = hitboxes_.at(armed.hitboxId);
276 for (const auto& [hurtKeyName, hurt] : hurtboxes_) {
277 (void)hurtKeyName;
278 if (!hurt.hasPose) continue;
279 if (hurt.definition.subject == armed.subject) continue;
280 HitMemoryKey memory{armed.execution.value(), armed.hitboxId, hurt.definition.subject.format(),
281 hurt.definition.hurtboxId};
282 if (hitMemory_.contains(memory)) continue;
283
284 MeleePoint3 contact{};
286 const bool previousHit =
287 shapesOverlap(definition.shape, armed.previous, definition.localOffset, hurt.definition.shape,
288 hurt.pose, contact, normal);
289 const bool currentHit =
290 shapesOverlap(definition.shape, armed.current, definition.localOffset, hurt.definition.shape,
291 hurt.pose, contact, normal);
292 if (!previousHit && !currentHit) continue;
293
294 MeleeHitEvent event;
295 event.source = armed.subject;
296 event.target = hurt.definition.subject;
297 event.hitboxId = armed.hitboxId;
298 event.hurtboxId = hurt.definition.hurtboxId;
299 event.bodyPart = hurt.definition.bodyPart;
300 event.contact = contact;
301 event.normal = normal;
302 event.healthDamage = definition.healthDamage;
303 event.poiseDamage = definition.poiseDamage;
304 event.damageType = definition.damageType;
305 event.knockback = {normal.x * definition.knockbackSpeed, definition.knockbackLift,
306 normal.z * definition.knockbackSpeed};
307 event.actionExecution = armed.execution;
308 hitMemory_[memory] = true;
309 frame.hits.push_back(std::move(event));
310 }
311 }
312 lastTick_ = tick;
314}
315
317 DamageRuntime& damage, std::map<std::string, CombatState, std::less<>>& states,
318 const std::vector<MeleeHitEvent>& hits,
319 const std::map<std::string, double, std::less<>>& bodyPartMultipliers) const {
320 std::vector<DamageOutcome> outcomes;
321 outcomes.reserve(hits.size());
322 for (const auto& hit : hits) {
323 const auto found = states.find(hit.target.format());
324 if (found == states.end())
325 return Result<std::vector<DamageOutcome>>::failure(
326 Diagnostic::error(DiagnosticCode::NotFound, "damage target was not found", hit.target.format()));
328 request.source = hit.source;
329 request.target = hit.target;
330 request.actionExecution = hit.actionExecution;
331 request.damageType = hit.damageType;
332 request.healthDamage = hit.healthDamage;
333 request.poiseDamage = hit.poiseDamage;
334 request.knockback = hit.knockback;
335 const auto mult = bodyPartMultipliers.find(hit.bodyPart);
336 if (mult != bodyPartMultipliers.end()) request.incomingDamageMultiplier = mult->second;
337 auto outcome = damage.apply(found->second, request);
338 if (!outcome) return Result<std::vector<DamageOutcome>>::failure(outcome.status());
339 outcomes.push_back(std::move(outcome).takeValue());
340 }
341 return Result<std::vector<DamageOutcome>>::success(std::move(outcomes),
343}
344
345} // namespace eve::combat
double value
int subject
Definition AnimSmr.cpp:163
eve::EntitySpatialPose pose
const std::string & s
float length
Definition CaveMesh.cpp:94
glm::vec4 p[6]
std::uint64_t localOffset
const GltfImportRequest & request
std::uint32_t key
ShaderImageInput shape
vk::UniqueDeviceMemory memory
std::uint32_t ab
std::int32_t c
std::string local
std::array< float, 3 > scale
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
Deterministic bone-agnostic melee hitbox/hurtbox sweep.
Texture * normal
float radius
bool hit
float d
float t
bool found
std::map< Cell, int > best
SimulationTick tick
int limit
Definition TreeMesh.cpp:164
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
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
Owner-thread deterministic damage coordinator.
Definition Damage.h:125
Result< DamageOutcome > apply(CombatState &target, const DamageRequest &request) const
Resolve and atomically commit one damage request.
Definition Damage.cpp:200
virtual Result< MeleePose > pose(SubjectRef subject, std::string_view hitboxId) const =0
Resolve the current world pose for one subject's active hitbox.
Result< void > registerHitbox(MeleeHitboxDefinition definition)
Register or replace one hitbox catalog entry after full validation.
Definition MeleeHit.cpp:173
Result< std::vector< DamageOutcome > > applyHits(DamageRuntime &damage, std::map< std::string, CombatState, std::less<> > &states, const std::vector< MeleeHitEvent > &hits, const std::map< std::string, double, std::less<> > &bodyPartMultipliers={}) const
Apply owning hit events through DamageRuntime using a mutable state map.
Definition MeleeHit.cpp:316
Result< void > setHurtboxPose(SubjectRef subject, std::string_view hurtboxId, MeleePose pose)
Update the current world pose of one registered hurtbox.
Definition MeleeHit.cpp:203
Result< void > armHitbox(SubjectRef subject, std::string_view hitboxId, action::ActionExecutionId execution)
Arm one hitbox while a Montage window owns it.
Definition MeleeHit.cpp:214
Result< MeleeHitFrame > advance(SimulationTick tick)
Sample poses for armed hitboxes, sweep against hurtboxes and emit unique hits.
Definition MeleeHit.cpp:257
Result< void > clearHurtboxes(SubjectRef subject)
Remove every hurtbox owned by a subject.
Definition MeleeHit.cpp:190
Result< void > disarmHitbox(SubjectRef subject, std::string_view hitboxId, action::ActionExecutionId execution)
Disarm one hitbox window and forget its per-target hit memory.
Definition MeleeHit.cpp:242
Result< void > registerHurtbox(MeleeHurtboxDefinition definition)
Register or replace one hurtbox after full validation.
Definition MeleeHit.cpp:180
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
double dot(const Vec2 &a, const Vec2 &b)
Dot.
Definition UrbanTypes.h:38
double lengthSq(const Vec2 &a)
Length sq.
Definition UrbanTypes.h:42
int axis(int64_t a, size_t rank)
Axis.
Mutable combat state owned by the gameplay domain for one subject.
Definition Damage.h:37
Owning input for one damage transaction.
Definition Damage.h:49
One confirmed contact produced by a sweep frame.
Definition MeleeHit.h:85
Complete owning result of one melee advance.
Definition MeleeHit.h:101
std::vector< MeleeHitEvent > hits
Definition MeleeHit.h:103
Catalog entry that maps a timeline hitbox resource id onto geometry.
Definition MeleeHit.h:67
double knockbackLift
Upward impulse added on hit; independent of the contact normal.
Definition MeleeHit.h:78
MeleePoint3 localOffset
Local offset from the pose provided by the pose source.
Definition MeleeHit.h:71
Result< void > validate() const
Validate id, shape and finite damage.
Definition MeleeHit.cpp:153
double knockbackSpeed
Horizontal impulse magnitude along the contact normal (attacker → target).
Definition MeleeHit.h:76
Body-part tag used for multipliers and debugging.
Definition MeleeHit.h:56
Result< void > validate() const
Validate identity and shape.
Definition MeleeHit.cpp:143
Finite world-space point used by melee geometry.
Definition MeleeHit.h:21
World pose for one shape sample.
Definition MeleeHit.h:46
double yawRadians
Yaw radians around world +Y; pitch/roll are unused for v1 capsules.
Definition MeleeHit.h:49
Result< void > validate() const
Reject non-finite pose values.
Definition MeleeHit.cpp:137
MeleePoint3 position
Definition MeleeHit.h:47
Result< void > validate() const
Reject non-finite or non-positive dimensions.
Definition MeleeHit.cpp:125
MeleeShapeKind kind
Definition MeleeHit.h:37