载入中...
搜索中...
未找到
CombatCarrier.cpp
浏览该文件的文档.
2
3#include "common/Diagnostic.h"
4
5#include <algorithm>
6#include <cmath>
7#include <limits>
8#include <utility>
9
10namespace eve::weapon {
11namespace {
12
13constexpr std::uint32_t kDefaultCapacity = 128;
14constexpr std::uint32_t kMaximumCapacity = 1024 * 1024;
15constexpr double kPi = 3.14159265358979323846;
16
17bool finite(const ProjectilePoint& value) {
18 return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z);
19}
20
21bool finite(const ProjectileVector& value) {
22 return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z);
23}
24
25double length(const ProjectileVector& value) {
26 return std::sqrt(value.x * value.x + value.y * value.y + value.z * value.z);
27}
28
29ProjectileVector normalized(const ProjectileVector& value) {
30 const double magnitude = length(value);
31 return {value.x / magnitude, value.y / magnitude, value.z / magnitude};
32}
33
34ProjectileVector directionTo(const ProjectilePoint& from, const ProjectilePoint& to) {
35 return {to.x - from.x, to.y - from.y, to.z - from.z};
36}
37
38ProjectileVector steer(ProjectileVector velocity, ProjectileVector desired, double maxRadians) {
39 const double speed = length(velocity);
40 if (speed <= 0.0) return velocity;
41 ProjectileVector current = normalized(velocity);
42 desired = normalized(desired);
43 const double dot = std::clamp(current.x * desired.x + current.y * desired.y + current.z * desired.z, -1.0, 1.0);
44 const double angle = std::acos(dot);
45 if (angle <= maxRadians || angle <= 1e-12) return {desired.x * speed, desired.y * speed, desired.z * speed};
46 const double alpha = maxRadians / angle;
47 ProjectileVector blended{current.x + (desired.x - current.x) * alpha, current.y + (desired.y - current.y) * alpha,
48 current.z + (desired.z - current.z) * alpha};
49 blended = normalized(blended);
50 return {blended.x * speed, blended.y * speed, blended.z * speed};
51}
52
53ProjectileVector reflect(ProjectileVector velocity, ProjectileVector normal) {
54 const double nLen = length(normal);
55 if (nLen <= 1e-12) return velocity;
56 normal = {normal.x / nLen, normal.y / nLen, normal.z / nLen};
57 const double into = velocity.x * normal.x + velocity.y * normal.y + velocity.z * normal.z;
58 return {velocity.x - 2.0 * into * normal.x, velocity.y - 2.0 * into * normal.y,
59 velocity.z - 2.0 * into * normal.z};
60}
61
62ProjectileVector cross(const ProjectileVector& a, const ProjectileVector& b) {
63 return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
64}
65
66ProjectileVector rotateAroundAxis(ProjectileVector vector, ProjectileVector axis, double radians) {
67 axis = normalized(axis);
68 const double cosA = std::cos(radians);
69 const double sinA = std::sin(radians);
70 const auto axisCrossV = cross(axis, vector);
71 const double axisDotV = axis.x * vector.x + axis.y * vector.y + axis.z * vector.z;
72 return {vector.x * cosA + axisCrossV.x * sinA + axis.x * axisDotV * (1.0 - cosA),
73 vector.y * cosA + axisCrossV.y * sinA + axis.y * axisDotV * (1.0 - cosA),
74 vector.z * cosA + axisCrossV.z * sinA + axis.z * axisDotV * (1.0 - cosA)};
75}
76
77ProjectileVector lateralBasis(const ProjectileVector& forward) {
78 ProjectileVector up{0.0, 1.0, 0.0};
79 auto side = cross(forward, up);
80 if (length(side) <= 1e-12) {
81 up = {1.0, 0.0, 0.0};
82 side = cross(forward, up);
83 }
84 return normalized(side);
85}
86
87bool crossed(Duration previousAge, Duration nextAge, Duration mark) {
88 return previousAge < mark && nextAge >= mark;
89}
90
91int intervalIndex(Duration age, Duration interval) {
92 if (interval <= Duration::zero()) return 0;
93 return static_cast<int>(age.nanoseconds() / interval.nanoseconds());
94}
95
96Result<void> validateVolley(const CarrierVolleySpec& volley) {
97 if (volley.count <= 0)
98 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Volley count must be positive", "count"));
99 if (volley.count > 256)
100 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Volley count must be <= 256", "count"));
101 if (volley.pattern == CarrierVolleyPattern::Fan) {
102 if (!std::isfinite(volley.spreadDegrees) || volley.spreadDegrees < 0.0)
103 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Fan volley spreadDegrees must be finite and non-negative", "spreadDegrees"));
104 }
105 return Result<void>::success();
106}
107
108Result<std::vector<ProjectileVector>> volleyDirections(const ProjectileVector& base,
109 const CarrierVolleySpec& volley) {
110 auto valid = validateVolley(volley);
111 if (!valid) return Result<std::vector<ProjectileVector>>::failure(valid.status());
112 if (length(base) <= 1e-12)
113 return Result<std::vector<ProjectileVector>>::failure(
114 Diagnostic::error(DiagnosticCode::InvalidArgument, "Volley base direction must be non-zero", "direction"));
115
116 const auto forward = normalized(base);
117 ProjectileVector yawAxis{0.0, 1.0, 0.0};
118 if (std::fabs(forward.x * yawAxis.x + forward.y * yawAxis.y + forward.z * yawAxis.z) > 0.999)
119 yawAxis = lateralBasis(forward);
120
121 std::vector<ProjectileVector> directions;
122 directions.reserve(static_cast<std::size_t>(volley.count));
123 if (volley.count == 1) {
124 directions.push_back(forward);
125 return Result<std::vector<ProjectileVector>>::success(std::move(directions));
126 }
127 if (volley.pattern == CarrierVolleyPattern::Ring) {
128 for (int i = 0; i < volley.count; ++i) {
129 const double radians = (2.0 * kPi * static_cast<double>(i)) / static_cast<double>(volley.count);
130 directions.push_back(normalized(rotateAroundAxis(forward, yawAxis, radians)));
131 }
132 return Result<std::vector<ProjectileVector>>::success(std::move(directions));
133 }
134 const double total = volley.spreadDegrees * kPi / 180.0;
135 for (int i = 0; i < volley.count; ++i) {
136 const double t = static_cast<double>(i) / static_cast<double>(volley.count - 1);
137 const double radians = -0.5 * total + t * total;
138 directions.push_back(normalized(rotateAroundAxis(forward, yawAxis, radians)));
139 }
140 return Result<std::vector<ProjectileVector>>::success(std::move(directions));
141}
142
143} // namespace
144
146 if (!id.isValid()) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Carrier recipe id must not be empty", "id"));
147 if (lifetime <= Duration::zero())
148 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Carrier lifetime must be positive", "lifetime"));
149 if (!std::isfinite(speed) || speed <= 0.0)
150 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Carrier speed must be finite and positive", "speed"));
151 if (motionOps.empty())
152 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Carrier recipe requires at least one motion op", "motionOps"));
153
154 bool hasIntegrate = false;
155 for (std::size_t i = 0; i < motionOps.size(); ++i) {
156 const auto& op = motionOps[i];
157 const auto path = "motionOps[" + std::to_string(i) + "]";
158 switch (op.kind) {
160 if (!std::isfinite(op.maxTurnRateDegrees) || op.maxTurnRateDegrees <= 0.0)
161 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Homing turn rate must be finite and positive", path + ".maxTurnRateDegrees"));
162 break;
164 if (!std::isfinite(op.avoidLookAhead) || op.avoidLookAhead <= 0.0)
165 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Body-avoid look-ahead must be finite and positive", path + ".avoidLookAhead"));
166 if (!std::isfinite(op.avoidStrength) || op.avoidStrength < 0.0)
167 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Body-avoid strength must be finite and non-negative", path + ".avoidStrength"));
168 if (!std::isfinite(op.avoidRadiusPadding) || op.avoidRadiusPadding < 0.0)
169 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Body-avoid radius padding must be finite and non-negative", path + ".avoidRadiusPadding"));
170 if (!std::isfinite(op.maxTurnRateDegrees) || op.maxTurnRateDegrees <= 0.0)
171 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Body-avoid turn rate must be finite and positive", path + ".maxTurnRateDegrees"));
172 break;
174 if (!std::isfinite(op.gravity) || op.gravity < 0.0)
175 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Gravity must be finite and non-negative", path + ".gravity"));
176 break;
178 if (!std::isfinite(op.acceleration) || op.acceleration < 0.0)
179 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Acceleration must be finite and non-negative", path + ".acceleration"));
180 break;
183 if (!std::isfinite(op.curveAmplitude) || op.curveAmplitude < 0.0)
184 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Curve amplitude must be finite and non-negative", path + ".curveAmplitude"));
185 if (!std::isfinite(op.curveFrequencyHz) || op.curveFrequencyHz <= 0.0)
186 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Curve frequency must be finite and positive", path + ".curveFrequencyHz"));
187 break;
189 hasIntegrate = true;
190 break;
191 }
192 }
193 if (!hasIntegrate)
194 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Carrier recipe must include IntegrateLinear so position advances", "motionOps"));
195
196 if (triggers.empty())
197 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Carrier recipe requires at least one trigger", "triggers"));
198
199 for (std::size_t i = 0; i < triggers.size(); ++i) {
200 const auto& trigger = triggers[i];
201 const auto path = "triggers[" + std::to_string(i) + "]";
202 switch (trigger.kind) {
204 if (trigger.fuse <= Duration::zero())
205 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Fuse trigger requires positive fuse", path + ".fuse"));
206 break;
208 if (trigger.interval <= Duration::zero())
209 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Interval trigger requires positive interval", path + ".interval"));
210 break;
212 if (!std::isfinite(trigger.proximityRadius) || trigger.proximityRadius <= 0.0)
213 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Proximity trigger requires positive radius", path + ".proximityRadius"));
214 break;
217 break;
218 }
219 }
220
221 for (std::size_t i = 0; i < impacts.size(); ++i) {
222 const auto& impact = impacts[i];
223 const auto path = "impacts[" + std::to_string(i) + "]";
224 if (!std::isfinite(impact.damage) || impact.damage < 0.0)
225 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Impact damage must be finite and non-negative", path + ".damage"));
226 if (!std::isfinite(impact.splashRadius) || impact.splashRadius < 0.0)
227 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Splash radius must be finite and non-negative", path + ".splashRadius"));
228 if (!std::isfinite(impact.restitution) || impact.restitution < 0.0)
229 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Bounce restitution must be finite and non-negative", path + ".restitution"));
230 if (impact.pierceCount < 0 || impact.bounceCount < 0)
231 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Pierce/bounce budgets must be non-negative", path));
232 if (impact.kind == CarrierImpactKind::SpawnChild && !impact.childRecipeId.isValid())
233 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "SpawnChild requires childRecipeId", path + ".childRecipeId"));
234 if (impact.kind == CarrierImpactKind::Splash && impact.splashRadius <= 0.0)
235 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Splash impact requires positive splashRadius", path + ".splashRadius"));
236 }
237 return Result<void>::success();
238}
239
241 auto valid = definition.validate();
242 if (!valid) return Result<CarrierRecipe>::failure(valid.status());
243 if (!std::isfinite(damage) || damage < 0.0)
245 DiagnosticCode::InvalidArgument, "Bridge damage must be finite and non-negative", "damage"));
246
247 CarrierRecipe recipe;
248 recipe.id = definition.id;
249 recipe.lifetime = definition.lifetime;
250 recipe.speed = definition.speed;
251
252 switch (definition.mode) {
255 break;
257 recipe.motionOps.push_back({CarrierMotionOpKind::ApplyGravity, definition.gravity});
259 break;
261 recipe.motionOps.push_back(
262 {CarrierMotionOpKind::SteerHoming, 0.0, definition.maxTurnRateDegrees});
264 break;
265 }
266
267 recipe.triggers.push_back({CarrierTriggerKind::OnExpire});
268 recipe.triggers.push_back({CarrierTriggerKind::OnHit});
272
273 auto recipeValid = recipe.validate();
274 if (!recipeValid) return Result<CarrierRecipe>::failure(recipeValid.status());
275 return Result<CarrierRecipe>::success(std::move(recipe));
276}
277
278CombatCarrierRuntime::CombatCarrierRuntime() : slots_(kDefaultCapacity) {}
279
281 if (activeCount_ != 0)
282 return Result<void>::failure(Diagnostic::error(DiagnosticCode::Conflict, "Carrier pool cannot be resized while active", {}));
283 if (capacity == 0 || capacity > kMaximumCapacity)
284 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Carrier pool capacity must be in 1..1048576", "capacity"));
285 slots_.assign(capacity, Slot{});
286 return Result<void>::success();
287}
288
289Result<CombatCarrierRuntime::LiveRecipe> CombatCarrierRuntime::prepareRecipe(const CarrierRecipe& recipe) {
290 auto valid = recipe.validate();
291 if (!valid) return Result<LiveRecipe>::failure(valid.status());
292
293 LiveRecipe live;
294 live.recipe = recipe;
295 for (const auto& op : recipe.motionOps) {
296 if (op.kind == CarrierMotionOpKind::SteerHoming) live.needsHoming = true;
297 if (op.kind == CarrierMotionOpKind::SteerAvoidBody) live.needsBodyAvoid = true;
298 }
299 for (const auto& trigger : recipe.triggers) {
300 if (trigger.kind == CarrierTriggerKind::OnHit) live.needsHit = true;
301 if (trigger.kind == CarrierTriggerKind::OnProximity) live.needsProximity = true;
302 }
303 for (const auto& impact : recipe.impacts) {
304 if (impact.kind == CarrierImpactKind::Pierce)
305 live.initialPierce = std::max(live.initialPierce, impact.pierceCount);
306 if (impact.kind == CarrierImpactKind::Bounce)
307 live.initialBounce = std::max(live.initialBounce, impact.bounceCount);
308 }
309 return Result<LiveRecipe>::success(std::move(live));
310}
311
313 auto prepared = prepareRecipe(recipe);
314 if (!prepared) return Result<void>::failure(prepared.status());
315 LiveRecipe live = std::move(prepared).takeValue();
316 for (auto& existing : recipes_) {
317 if (existing.recipe.id == recipe.id) {
318 existing = std::move(live);
319 return Result<void>::success();
320 }
321 }
322 recipes_.push_back(std::move(live));
323 return Result<void>::success();
324}
325
326std::optional<CarrierRecipe> CombatCarrierRuntime::findRecipe(const LogicalId& id) const {
327 for (const auto& live : recipes_) {
328 if (live.recipe.id == id) return live.recipe;
329 }
330 return std::nullopt;
331}
332
333Result<void> CombatCarrierRuntime::validateSpawn(const CarrierRecipe& recipe,
335 auto valid = recipe.validate();
336 if (!valid) return valid;
337 if (!finite(request.position) || !finite(request.direction))
338 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Carrier transform must contain finite values", "request"));
339 if (length(request.direction) <= 0.0)
340 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Carrier direction must be non-zero", "direction"));
341
342 bool needsTarget = false;
343 for (const auto& op : recipe.motionOps) {
344 if (op.kind == CarrierMotionOpKind::SteerHoming) needsTarget = true;
345 }
346 for (const auto& trigger : recipe.triggers) {
347 if (trigger.kind == CarrierTriggerKind::OnProximity) needsTarget = true;
348 }
349 if (needsTarget && !request.target.has_value())
350 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Homing/proximity carrier requires a target handle", "target"));
351 return Result<void>::success();
352}
353
354Result<CarrierHandle> CombatCarrierRuntime::spawnPrepared(const LiveRecipe& live,
355 const CarrierSpawnRequest& request) {
356 auto valid = validateSpawn(live.recipe, request);
357 if (!valid) return Result<CarrierHandle>::failure(valid.status());
358
359 for (std::uint32_t slot = 0; slot < slots_.size(); ++slot) {
360 if (slots_[slot].state.has_value()) continue;
361 Slot& cell = slots_[slot];
362 ++cell.generation;
363 if (cell.generation == 0) ++cell.generation;
364
365 CarrierState state;
366 state.handle = {slot, cell.generation};
367 state.recipeId = live.recipe.id;
368 state.lifetime = live.recipe.lifetime;
369 state.age = Duration::zero();
370 state.pierceRemaining = live.initialPierce;
371 state.bounceRemaining = live.initialBounce;
372 state.target = request.target;
373 state.source = request.source;
374 const auto dir = normalized(request.direction);
375 state.motion.position = request.position;
376 state.motion.velocity = {dir.x * live.recipe.speed, dir.y * live.recipe.speed, dir.z * live.recipe.speed};
377 cell.state = std::move(state);
378 ++activeCount_;
379 return Result<CarrierHandle>::success(cell.state->handle);
380 }
382 Diagnostic::error(DiagnosticCode::Conflict, "Carrier pool is full", "capacity"));
383}
384
386 for (const auto& live : recipes_) {
387 if (live.recipe.id == recipeId) return spawnPrepared(live, request);
388 }
390 Diagnostic::error(DiagnosticCode::NotFound, "Carrier recipe is not registered", "recipeId"));
391}
392
394 auto registered = registerRecipe(recipe);
395 if (!registered) return Result<CarrierHandle>::failure(registered.status());
396 return spawn(recipe.id, request);
397}
398
401 const CarrierVolleySpec& volley) {
402 auto dirs = volleyDirections(request.direction, volley);
403 if (!dirs) return Result<std::vector<CarrierHandle>>::failure(dirs.status());
404 if (activeCount_ + dirs.value().size() > slots_.size())
405 return Result<std::vector<CarrierHandle>>::failure(
406 Diagnostic::error(DiagnosticCode::Conflict, "Carrier pool cannot fit the full volley", "capacity"));
407
408 std::vector<CarrierHandle> handles;
409 handles.reserve(dirs.value().size());
410 const std::size_t activeBefore = activeCount_;
411 const auto slotsBefore = slots_;
412 for (const auto& direction : dirs.value()) {
414 one.direction = direction;
415 auto spawned = spawn(recipeId, one);
416 if (!spawned) {
417 slots_ = slotsBefore;
418 activeCount_ = activeBefore;
419 return Result<std::vector<CarrierHandle>>::failure(spawned.status());
420 }
421 handles.push_back(spawned.value());
422 }
423 return Result<std::vector<CarrierHandle>>::success(std::move(handles));
424}
425
428 const CarrierVolleySpec& volley) {
429 auto registered = registerRecipe(recipe);
430 if (!registered) return Result<std::vector<CarrierHandle>>::failure(registered.status());
431 return spawnVolley(recipe.id, request, volley);
432}
433
435 const IProjectileTargetProvider* targets,
436 const ICarrierHitProbe* hits,
437 const ICarrierBodyDefenseProvider* bodies) {
438 if (delta < Duration::zero())
440 Diagnostic::error(DiagnosticCode::InvalidArgument, "Carrier update delta must be >= 0", "delta"));
442
443 struct Candidate {
445 bool release = false;
446 };
447 struct TriggerFire {
448 CarrierTriggerKind kind = CarrierTriggerKind::OnExpire;
449 std::optional<ICarrierHitProbe::Contact> contact;
450 };
451
452 std::vector<Candidate> candidates;
453 std::vector<CarrierEvent> events;
454 std::vector<CarrierChildSpawn> childSpawns;
455 candidates.reserve(activeCount_);
456
457 auto lookupLive = [&](const LogicalId& id) -> const LiveRecipe* {
458 for (const auto& live : recipes_) {
459 if (live.recipe.id == id) return &live;
460 }
461 return nullptr;
462 };
463
464 for (const auto& slot : slots_) {
465 if (!slot.state.has_value()) continue;
466 const LiveRecipe* live = lookupLive(slot.state->recipeId);
467 if (live == nullptr)
469 DiagnosticCode::NotFound, "Live carrier references an unregistered recipe", "recipeId"));
470 if ((live->needsHoming || live->needsProximity) && targets == nullptr)
472 DiagnosticCode::Unsupported, "Homing/proximity carrier update requires a target provider", "targets"));
473 if (live->needsHit && hits == nullptr)
475 Diagnostic::error(DiagnosticCode::Unsupported, "OnHit carrier update requires a hit probe", "hits"));
476 if (live->needsBodyAvoid && bodies == nullptr)
479 "SteerAvoidBody carrier update requires a body-defense provider", "bodies"));
480 candidates.push_back({*slot.state, false});
481 }
482
483 for (auto& candidate : candidates) {
484 const LiveRecipe* live = lookupLive(candidate.state.recipeId);
485 if (live == nullptr)
487 DiagnosticCode::NotFound, "Live carrier references an unregistered recipe", "recipeId"));
488
489 const Duration previousAge = candidate.state.age;
490 CarrierMotion previous = candidate.state.motion;
491 CarrierMotion motion = previous;
492
493 std::optional<ProjectilePoint> targetPos;
494 if (live->needsHoming || live->needsProximity) {
495 if (!candidate.state.target.has_value())
497 DiagnosticCode::InvalidArgument, "Homing/proximity carrier lost its target handle", "target"));
498 auto resolved = targets->position(*candidate.state.target);
499 if (!resolved) return Result<CarrierFrame>::failure(resolved.status());
500 targetPos = resolved.value();
501 }
502
503 const double dtSeconds = delta.seconds();
504 const double ageSeconds = candidate.state.age.seconds();
505 for (const auto& op : live->recipe.motionOps) {
506 switch (op.kind) {
508 const auto desired = directionTo(motion.position, *targetPos);
509 if (length(desired) <= 1e-12) break;
510 const double maxRadians = op.maxTurnRateDegrees * kPi / 180.0 * dtSeconds;
511 motion.velocity = steer(motion.velocity, desired, maxRadians);
512 break;
513 }
515 ecs::EntityHandle targetHandle{};
516 if (candidate.state.target.has_value()) targetHandle = *candidate.state.target;
517 auto samples = bodies->sample(candidate.state.handle, targetHandle, motion);
518 if (!samples) return Result<CarrierFrame>::failure(samples.status());
519 ProjectileVector avoid{0.0, 0.0, 0.0};
520 bool any = false;
521 const auto fwd = length(motion.velocity) > 1e-12 ? normalized(motion.velocity)
522 : ProjectileVector{1.0, 0.0, 0.0};
523 for (const auto& body : samples.value()) {
524 if (!std::isfinite(body.radius) || body.radius < 0.0) {
527 "Body defense radius must be finite and non-negative", "bodies"));
528 }
529 const auto toCenter = directionTo(motion.position, body.center);
530 const double dist = length(toCenter);
531 const double blockAt = body.radius + op.avoidRadiusPadding + op.avoidLookAhead;
532 if (dist <= 1e-12 || dist > blockAt) continue;
533 if (length(body.facing) > 1e-12 && body.frontConeDegrees < 180.0) {
534 const auto approach =
535 normalized(ProjectileVector{-toCenter.x, -toCenter.y, -toCenter.z});
536 const auto face = normalized(body.facing);
537 const double dot = std::clamp(
538 approach.x * face.x + approach.y * face.y + approach.z * face.z, -1.0, 1.0);
539 const double angleDeg = std::acos(dot) * 180.0 / kPi;
540 if (angleDeg > body.frontConeDegrees) continue;
541 }
542 auto lateral = cross(fwd, toCenter);
543 if (length(lateral) <= 1e-12) lateral = lateralBasis(fwd);
544 lateral = normalized(lateral);
545 if (targetPos.has_value()) {
546 const auto around = directionTo(body.center, *targetPos);
547 const auto left = lateral;
548 const auto right = ProjectileVector{-lateral.x, -lateral.y, -lateral.z};
549 const double leftScore = left.x * around.x + left.y * around.y + left.z * around.z;
550 const double rightScore = right.x * around.x + right.y * around.y + right.z * around.z;
551 if (rightScore > leftScore) lateral = right;
552 }
553 const double weight = op.avoidStrength * (1.0 - dist / blockAt);
554 avoid.x += lateral.x * weight;
555 avoid.y += lateral.y * weight;
556 avoid.z += lateral.z * weight;
557 any = true;
558 }
559 if (any && length(avoid) > 1e-12) {
560 const auto desired = ProjectileVector{fwd.x + avoid.x, fwd.y + avoid.y, fwd.z + avoid.z};
561 const double maxRadians = op.maxTurnRateDegrees * kPi / 180.0 * dtSeconds;
562 motion.velocity = steer(motion.velocity, desired, maxRadians);
563 }
564 break;
565 }
567 motion.velocity.y -= op.gravity * dtSeconds;
568 break;
570 const double speed = length(motion.velocity);
571 if (speed <= 1e-12) break;
572 const auto dir = normalized(motion.velocity);
573 const double next = speed + op.acceleration * dtSeconds;
574 motion.velocity = {dir.x * next, dir.y * next, dir.z * next};
575 break;
576 }
578 const double speed = length(motion.velocity);
579 if (speed <= 1e-12) break;
580 const auto fwd = normalized(motion.velocity);
581 const auto side = lateralBasis(fwd);
582 const double omega = 2.0 * kPi * op.curveFrequencyHz;
583 const double vLat = op.curveAmplitude * omega * std::cos(omega * ageSeconds);
584 motion.velocity = {fwd.x * speed + side.x * vLat, fwd.y * speed + side.y * vLat,
585 fwd.z * speed + side.z * vLat};
586 break;
587 }
589 const double speed = length(motion.velocity);
590 if (speed <= 1e-12) break;
591 const auto fwd = normalized(motion.velocity);
592 const auto side0 = lateralBasis(fwd);
593 const auto up = normalized(cross(side0, fwd));
594 const double omega = 2.0 * kPi * op.curveFrequencyHz;
595 const double phase = omega * ageSeconds;
596 const auto side = ProjectileVector{side0.x * std::cos(phase) + up.x * std::sin(phase),
597 side0.y * std::cos(phase) + up.y * std::sin(phase),
598 side0.z * std::cos(phase) + up.z * std::sin(phase)};
599 const double vLat = op.curveAmplitude * omega;
600 motion.velocity = {fwd.x * speed + side.x * vLat, fwd.y * speed + side.y * vLat,
601 fwd.z * speed + side.z * vLat};
602 break;
603 }
605 motion.position.x += motion.velocity.x * dtSeconds;
606 motion.position.y += motion.velocity.y * dtSeconds;
607 motion.position.z += motion.velocity.z * dtSeconds;
608 break;
609 }
610 }
611
612 if (!finite(motion.position) || !finite(motion.velocity))
614 Diagnostic::error(DiagnosticCode::Failed, "Carrier motion produced non-finite values", "motion"));
615
616 auto nextAge = previousAge.tryAdd(delta);
617 if (!nextAge) return Result<CarrierFrame>::failure(nextAge.status());
618
619 candidate.state.motion = motion;
620 candidate.state.age = std::move(nextAge).takeValue();
621
622 std::vector<TriggerFire> fires;
623 for (const auto& trigger : live->recipe.triggers) {
624 switch (trigger.kind) {
626 if (crossed(previousAge, candidate.state.age, candidate.state.lifetime) ||
627 candidate.state.age >= candidate.state.lifetime)
628 fires.push_back({CarrierTriggerKind::OnExpire, std::nullopt});
629 break;
631 if (crossed(previousAge, candidate.state.age, trigger.fuse))
632 fires.push_back({CarrierTriggerKind::OnFuse, std::nullopt});
633 break;
635 const int before = intervalIndex(previousAge, trigger.interval);
636 const int after = intervalIndex(candidate.state.age, trigger.interval);
637 for (int i = before + 1; i <= after; ++i)
638 fires.push_back({CarrierTriggerKind::OnInterval, std::nullopt});
639 break;
640 }
642 auto contacts = hits->query(candidate.state.handle, previous, motion);
643 if (!contacts) return Result<CarrierFrame>::failure(contacts.status());
644 for (auto& contact : contacts.value())
645 fires.push_back({CarrierTriggerKind::OnHit, std::move(contact)});
646 break;
647 }
649 const auto deltaPos = directionTo(motion.position, *targetPos);
650 const double dist = length(deltaPos);
651 if (dist <= trigger.proximityRadius)
652 fires.push_back({CarrierTriggerKind::OnProximity, std::nullopt});
653 break;
654 }
655 }
656 }
657
658 for (const auto& fire : fires) {
659 bool suppressRelease = false;
660 bool requestRelease = false;
661
662 for (const auto& impact : live->recipe.impacts) {
663 if (impact.on != fire.kind) continue;
664
665 CarrierEvent event;
666 event.carrier = candidate.state.handle;
667 event.recipeId = candidate.state.recipeId;
668 event.trigger = fire.kind;
669 event.impact = impact.kind;
670 event.position = candidate.state.motion.position;
671 event.source = candidate.state.source;
672 if (fire.contact.has_value()) {
673 event.target = fire.contact->target;
674 event.normal = fire.contact->normal;
675 event.position = fire.contact->point;
676 } else if (candidate.state.target.has_value()) {
677 event.target = *candidate.state.target;
678 }
679
680 switch (impact.kind) {
681 case CarrierImpactKind::EmitHit:
682 event.damage = impact.damage;
683 event.damageType = impact.damageType;
684 event.element = impact.element;
685 events.push_back(std::move(event));
686 break;
687 case CarrierImpactKind::Splash:
688 event.damage = impact.damage;
689 event.splashRadius = impact.splashRadius;
690 event.damageType = impact.damageType;
691 event.element = impact.element;
692 event.position = candidate.state.motion.position;
693 events.push_back(std::move(event));
694 break;
695 case CarrierImpactKind::Pierce:
696 if (candidate.state.pierceRemaining > 0) {
697 --candidate.state.pierceRemaining;
698 suppressRelease = true;
699 }
700 break;
701 case CarrierImpactKind::Bounce:
702 if (candidate.state.bounceRemaining > 0 && fire.contact.has_value()) {
703 --candidate.state.bounceRemaining;
704 auto reflected = reflect(candidate.state.motion.velocity, fire.contact->normal);
705 reflected.x *= impact.restitution;
706 reflected.y *= impact.restitution;
707 reflected.z *= impact.restitution;
708 candidate.state.motion.velocity = reflected;
709 suppressRelease = true;
710 }
711 break;
712 case CarrierImpactKind::Release:
713 requestRelease = true;
714 break;
715 case CarrierImpactKind::SpawnChild: {
716 CarrierChildSpawn child;
717 child.recipeId = impact.childRecipeId;
718 child.parent = candidate.state.handle;
719 child.request.position = candidate.state.motion.position;
720 child.request.direction = candidate.state.motion.velocity;
721 if (length(child.request.direction) <= 1e-12) child.request.direction = {1.0, 0.0, 0.0};
722 child.request.target = candidate.state.target;
723 child.request.source = candidate.state.source;
724 childSpawns.push_back(std::move(child));
725 break;
726 }
727 }
728 }
729
730 if (requestRelease && !suppressRelease) candidate.release = true;
731 }
732
733 if (candidate.state.age >= candidate.state.lifetime) {
734 bool hasExpireRelease = false;
735 for (const auto& impact : live->recipe.impacts) {
736 if (impact.kind == CarrierImpactKind::Release && impact.on == CarrierTriggerKind::OnExpire)
737 hasExpireRelease = true;
738 }
739 if (!hasExpireRelease &&
740 std::any_of(fires.begin(), fires.end(),
741 [](const TriggerFire& f) { return f.kind == CarrierTriggerKind::OnExpire; }))
742 candidate.release = true;
743 }
744 }
745
746 std::vector<Slot> staged = slots_;
747 std::size_t stagedActive = activeCount_;
748 for (const auto& candidate : candidates) {
749 auto& cell = staged[candidate.state.handle.slot];
750 if (cell.generation != candidate.state.handle.generation || !cell.state.has_value())
752 Diagnostic::error(DiagnosticCode::StaleHandle, "Carrier handle became stale", "handle"));
753 if (candidate.release) {
754 cell.state.reset();
755 --stagedActive;
756 } else {
757 cell.state = candidate.state;
758 }
759 }
760
761 std::vector<CarrierHandle> spawnedChildren;
762 spawnedChildren.reserve(childSpawns.size());
763 for (const auto& child : childSpawns) {
764 const LiveRecipe* childLive = lookupLive(child.recipeId);
765 if (childLive == nullptr)
767 DiagnosticCode::NotFound, "Child spawn references an unregistered recipe", "childRecipeId"));
768 auto valid = validateSpawn(childLive->recipe, child.request);
769 if (!valid) return Result<CarrierFrame>::failure(valid.status());
770
771 bool placed = false;
772 for (std::uint32_t slot = 0; slot < staged.size(); ++slot) {
773 if (staged[slot].state.has_value()) continue;
774 Slot& cell = staged[slot];
775 ++cell.generation;
776 if (cell.generation == 0) ++cell.generation;
777 CarrierState state;
778 state.handle = {slot, cell.generation};
779 state.recipeId = childLive->recipe.id;
780 state.lifetime = childLive->recipe.lifetime;
781 state.age = Duration::zero();
782 state.pierceRemaining = childLive->initialPierce;
783 state.bounceRemaining = childLive->initialBounce;
784 state.target = child.request.target;
785 state.source = child.request.source;
786 const auto dir = normalized(child.request.direction);
787 state.motion.position = child.request.position;
788 state.motion.velocity = {dir.x * childLive->recipe.speed, dir.y * childLive->recipe.speed,
789 dir.z * childLive->recipe.speed};
790 cell.state = std::move(state);
791 ++stagedActive;
792 spawnedChildren.push_back(cell.state->handle);
793 placed = true;
794 break;
795 }
796 if (!placed)
798 DiagnosticCode::Conflict, "Carrier pool cannot fit deferred child spawns", "capacity"));
799 }
800
801 slots_ = std::move(staged);
802 activeCount_ = stagedActive;
803
804 CarrierFrame frame;
805 for (const auto& candidate : candidates) {
806 frame.advanced.push_back(candidate.state.handle);
807 if (candidate.release) frame.released.push_back(candidate.state.handle);
808 }
809 for (const auto& child : spawnedChildren) frame.advanced.push_back(child);
810 frame.events = std::move(events);
811 frame.childSpawns = std::move(childSpawns);
812 return Result<CarrierFrame>::success(std::move(frame));
813}
814
815Result<void> CombatCarrierRuntime::release(CarrierHandle handle) {
816 if (handle.slot >= slots_.size())
817 return Result<void>::failure(Diagnostic::error(DiagnosticCode::StaleHandle, "Carrier handle slot is out of range", "handle"));
818 Slot& cell = slots_[handle.slot];
819 if (!cell.state.has_value() || cell.generation != handle.generation)
820 return Result<void>::failure(Diagnostic::error(DiagnosticCode::StaleHandle, "Carrier handle is stale", "handle"));
821 cell.state.reset();
822 --activeCount_;
823 return Result<void>::success();
824}
825
826std::optional<CarrierState> CombatCarrierRuntime::find(CarrierHandle handle) const {
827 if (handle.slot >= slots_.size()) return std::nullopt;
828 const Slot& cell = slots_[handle.slot];
829 if (!cell.state.has_value() || cell.generation != handle.generation) return std::nullopt;
830 return cell.state;
831}
832
833std::vector<CarrierState> CombatCarrierRuntime::states() const {
834 std::vector<CarrierState> result;
835 result.reserve(activeCount_);
836 for (const auto& slot : slots_) {
837 if (slot.state.has_value()) result.push_back(*slot.state);
838 }
839 return result;
840}
841
842CarrierRuntimeSnapshot CombatCarrierRuntime::snapshot() const {
844 snap.slots.reserve(slots_.size());
845 for (const auto& slot : slots_) snap.slots.push_back({slot.generation, slot.state});
846 return snap;
847}
848
849Result<void> CombatCarrierRuntime::restore(const CarrierRuntimeSnapshot& snapshot) {
850 if (snapshot.slots.empty() || snapshot.slots.size() > kMaximumCapacity)
851 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Carrier snapshot capacity must be in 1..1048576", "slots"));
852
853 std::vector<Slot> staged;
854 staged.reserve(snapshot.slots.size());
855 std::size_t active = 0;
856 for (const auto& slot : snapshot.slots) {
857 if (slot.state.has_value()) {
858 if (slot.state->handle.slot >= snapshot.slots.size() || slot.state->handle.generation != slot.generation)
859 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Carrier snapshot state handle does not match slot metadata", "slots"));
860 if (!findRecipe(slot.state->recipeId).has_value())
861 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "Carrier snapshot references an unregistered recipe", "recipeId"));
862 ++active;
863 }
864 staged.push_back({slot.generation, slot.state});
865 }
866 slots_ = std::move(staged);
867 activeCount_ = active;
868 return Result<void>::success();
869}
870
871} // namespace eve::weapon
double value
bool & active
std::string from
float phase
Definition CaveMesh.cpp:58
float length
Definition CaveMesh.cpp:94
Composable combat-carrier runtime: motion ops, triggers, and impacts.
Stable, structured diagnostics shared by engine modules.
const GltfImportRequest & request
std::uint32_t spawned
std::uint32_t capacity
HexVec3 up
HexVec3 left
HexVec3 right
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
TokenKind kind
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
Texture * normal
graphics::Canvas * previous
size_t directions
Definition OnnxLstm.cpp:29
bool finite
float f
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
PrimitiveHandle handle
std::function< eve::Result< void >()> release
Definition Procgen.cpp:86
float t
RoadLaneDirection direction
double current
std::optional< CarrierVolleySpec > volley
Cell cell
bool placed
Battle::Events events
ecs::EntityHandle side
V3 dir
Definition TreeMesh.cpp:150
std::string body
float angle
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
Result< Duration > tryAdd(Duration other) const
Add two durations with overflow validation.
Definition Time.cpp:30
static constexpr Duration zero() noexcept
Return zero duration.
Definition Time.h:60
double seconds() const noexcept
Return this duration as seconds for legacy/presentation APIs.
Definition Time.cpp:28
constexpr bool isZero() const noexcept
Whether this duration is exactly zero.
Definition Time.h:75
Human-readable, scoped identifier in the form namespace:name.
Definition Identity.h:411
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
CombatCarrierRuntime()
Combat carrier runtime.
Result< void > configurePool(std::uint32_t capacity)
Resize the pool while empty; capacity must be in 1..1048576.
std::size_t capacity() const noexcept
Current fixed pool capacity.
Result< std::vector< CarrierHandle > > spawnVolley(const LogicalId &recipeId, const CarrierSpawnRequest &request, const CarrierVolleySpec &volley)
Spawn multiple carriers in deterministic Fan/Ring directions.
std::optional< CarrierRecipe > findRecipe(const LogicalId &id) const
Look up a previously registered recipe by id.
Result< CarrierFrame > update(Duration delta, const IProjectileTargetProvider *targets=nullptr, const ICarrierHitProbe *hits=nullptr, const ICarrierBodyDefenseProvider *bodies=nullptr)
Atomically advance all live carriers by supplied deterministic time.
Result< CarrierHandle > spawn(const LogicalId &recipeId, const CarrierSpawnRequest &request)
Spawn one carrier from a registered recipe id.
Result< void > registerRecipe(const CarrierRecipe &recipe)
Register or replace a recipe by id. @ownership Runtime copies the recipe; callers may discard their i...
Optional body/shield boundary for SteerAvoidBody.
virtual Result< std::vector< CarrierBodyDefenseSample > > sample(CarrierHandle handle, ecs::EntityHandle target, const CarrierMotion &motion) const =0
Sample body/shield volumes relevant to one carrier step.
Optional swept-hit boundary used only by OnHit triggers.
virtual Result< std::vector< Contact > > query(CarrierHandle handle, const CarrierMotion &previous, const CarrierMotion &current) const =0
Query contacts for one candidate step.
Optional target-position boundary used only by homing projectiles.
virtual Result< ProjectilePoint > position(ecs::EntityHandle target) const =0
Resolve one generation-checked target to an owning simulation-space position.
std::vector< double > forward(const Policy &p, const Observation &o)
Forward.
Definition Learning.h:65
double dot(const Vec2 &a, const Vec2 &b)
Dot.
Definition UrbanTypes.h:38
double cross(const Vec2 &a, const Vec2 &b)
Cross.
Definition UrbanTypes.h:36
int axis(int64_t a, size_t rank)
Axis.
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:635
载具模块入口(eve.Vehicle):定义注册 / 实体工厂 / RTS 命令 / 帧调度。 设计文档:docs/dev/通用载具系统设计.md
Result< CarrierRecipe > carrierRecipeFromProjectile(const ProjectileDefinition &definition, double damage)
Build a default recipe matching a classic ProjectileDefinition mode.
CarrierTriggerKind
When an impact may fire.
Generation-qualified reference to one pooled carrier slot.
Live motion snapshot for one carrier.
Data-driven composition recipe for one carrier family.
std::vector< CarrierTrigger > triggers
Result< void > validate() const
Validate finite parameters and composition constraints.
std::vector< CarrierMotionOp > motionOps
std::vector< CarrierImpact > impacts
Complete deterministic carrier-pool state for rollback/save.
std::vector< CarrierSlotSnapshot > slots
Owning observable state for one live carrier.
Deterministic multi-direction spawn descriptor.
Data-driven trajectory and lifetime definition.
Owning velocity or direction vector in projectile simulation space.