载入中...
搜索中...
未找到
TacticsBattle.cpp
浏览该文件的文档.
2
4
5#include <algorithm>
6#include <cstdint>
7#include <limits>
8#include <utility>
9
10namespace eve::tactics {
11namespace {
12
13bool sameHandle(const ecs::EntityHandle& left, const ecs::EntityHandle& right) noexcept {
14 return left.table == right.table && left.type == right.type && left.id == right.id &&
15 left.generation == right.generation;
16}
17
18template <typename T>
19T* resolve(const ecs::EntityHandle& handle) noexcept {
20 return dynamic_cast<T*>(ecs::try_get(handle));
21}
22
23bool containsHandle(const std::vector<ecs::EntityHandle>& values, const ecs::EntityHandle& handle) {
24 return std::any_of(values.begin(), values.end(), [&](const auto& value) { return sameHandle(value, handle); });
25}
26
27void emit(Battle& battle, BattlePhase from, BattlePhase to, SimulationTick tick, std::string type,
28 SubjectRef subject = {}) {
29 auto events = battle.events();
30 events->values.push_back(
31 BattleEvent{events->nextSequence++, 0, 0, from, to, tick, std::move(type), subject});
32}
33
34Result<Revision> nextRevision(Battle& battle) {
35 const auto next = battle.turn()->revision.incremented();
36 if (!next)
38 "tactics battle revision is exhausted", "battle.revision"));
39 return Result<Revision>::success(*next);
40}
41
42void record(Battle& battle, BattleCommand command) {
43 auto commands = battle.commands();
44 command.sequence = commands->nextSequence++;
45 command.resultingRevision = battle.turn()->revision;
46 std::uint64_t correlation = command.sequence;
47 if (command.triggerSequence != 0) {
48 const auto trigger = std::find_if(battle.events()->values.begin(), battle.events()->values.end(),
49 [&](const auto& event) { return event.sequence == command.triggerSequence; });
50 if (trigger != battle.events()->values.end())
51 correlation = trigger->correlationCommand != 0 ? trigger->correlationCommand
52 : trigger->causationCommand;
53 if (correlation == 0) correlation = command.sequence;
54 }
55 for (auto& event : battle.events()->values) {
56 if (event.causationCommand != 0) continue;
57 event.causationCommand = command.sequence;
58 event.correlationCommand = correlation;
59 }
60 commands->values.push_back(std::move(command));
61}
62
63SubjectRef sideSubject(const ecs::EntityHandle& handle) {
64 auto* side = resolve<TacticalSide>(handle);
65 return side ? side->identity()->subject : SubjectRef::nil();
66}
67
68bool objectiveSatisfied(Battle& battle, const ObjectiveSpec& objective) {
69 switch (objective.kind) {
71 for (const auto& handle : battle.turn()->units) {
72 auto* unit = resolve<TacticalUnit>(handle);
73 if (unit && unit->turn()->alive && sideSubject(unit->membership()->side) == objective.targetSide)
74 return false;
75 }
76 return true;
77 case ObjectiveKind::SurviveRounds: return battle.turn()->round >= objective.requiredRound;
79 for (const Cell cell : objective.requiredCells) {
80 const auto occupant = battle.board()->value.occupant(cell);
81 if (!occupant) return false;
82 TacticalUnit* occupyingUnit = nullptr;
83 for (const auto& handle : battle.turn()->units) {
84 auto* unit = resolve<TacticalUnit>(handle);
85 if (unit && unit->identity()->subject == *occupant) {
86 occupyingUnit = unit;
87 break;
88 }
89 }
90 if (!occupyingUnit || !occupyingUnit->turn()->alive ||
91 sideSubject(occupyingUnit->membership()->side) != objective.beneficiarySide)
92 return false;
93 }
94 return true;
95 }
96 return false;
97}
98
99bool evaluateObjectives(Battle& battle, Revision revision) {
100 bool endsBattle = false;
101 for (auto& objective : battle.objectives()->values) {
102 if (objective.status != ObjectiveStatus::Pending || !objectiveSatisfied(battle, objective.spec)) continue;
103 objective.status = ObjectiveStatus::Completed;
104 objective.completedRevision = revision;
105 emit(battle, battle.turn()->phase, battle.turn()->phase, battle.turn()->tick, "objective.completed",
106 objective.spec.beneficiarySide);
107 endsBattle = endsBattle || objective.spec.endsBattle;
108 }
109 return endsBattle;
110}
111
112std::uint64_t initialStreamState(std::uint64_t seed, std::string_view stream) noexcept {
113 std::uint64_t hash = 14695981039346656037ull ^ seed;
114 for (const unsigned char byte : stream) {
115 hash ^= byte;
116 hash *= 1099511628211ull;
117 }
118 return hash;
119}
120
121std::uint64_t splitMix64(std::uint64_t& state) noexcept {
122 std::uint64_t value = (state += 0x9e3779b97f4a7c15ull);
123 value = (value ^ (value >> 30u)) * 0xbf58476d1ce4e5b9ull;
124 value = (value ^ (value >> 27u)) * 0x94d049bb133111ebull;
125 return value ^ (value >> 31u);
126}
127
128Result<BattlePhase> transition(Battle& battle, BattlePhase next, SimulationTick tick, std::string type,
129 Revision revision, BattleCommand command, SubjectRef subject = {}) {
130 auto turn = battle.turn();
131 const auto from = turn->phase;
132 turn->phase = next;
133 turn->tick = tick;
134 turn->revision = revision;
135 emit(battle, from, next, tick, std::move(type), subject);
136 record(battle, std::move(command));
138}
139
141Result<const ITurnPolicy*> schedulingPolicy(Battle& battle) {
142 return TurnPolicyRegistry::builtins().find(battle.turn()->policyId);
143}
144
154Result<void> validateChargeProgress(Battle& battle, ChargeModel model) {
156 if (!model.isChargeBased()) return Result<void>::success(usable);
157 for (const auto& handle : battle.turn()->units) {
158 TacticalUnit* unit = resolve<TacticalUnit>(handle);
159 if (unit == nullptr || !unit->turn()->alive) continue;
160 const std::int64_t gain = static_cast<std::int64_t>(model.initiativeGain) *
161 static_cast<std::int64_t>(unit->turn()->initiative);
162 if (gain > 0) return Result<void>::success(usable);
163 }
166 "charge-time turn policy requires a living unit that can gain charge", "battle.units"));
167}
168
176void applyChargeGain(Battle& battle, ChargeModel model) {
177 if (model.initiativeGain == 0) return;
178 constexpr int kMaxCharge = std::numeric_limits<int>::max();
179 for (const auto& handle : battle.turn()->units) {
180 TacticalUnit* unit = resolve<TacticalUnit>(handle);
181 if (unit == nullptr || !unit->turn()->alive) continue;
182 const std::int64_t gain = static_cast<std::int64_t>(model.initiativeGain) *
183 static_cast<std::int64_t>(unit->turn()->initiative);
184 const std::int64_t next = static_cast<std::int64_t>(unit->turn()->charge) + gain;
185 unit->turn()->charge = next < 0 ? 0 : (next > kMaxCharge ? kMaxCharge : static_cast<int>(next));
186 }
187}
188
195std::vector<ecs::EntityHandle> readySchedule(Battle& battle, ChargeModel model,
196 const std::vector<ecs::EntityHandle>& ordered) {
197 if (!model.isChargeBased()) return ordered;
198 std::vector<ecs::EntityHandle> result;
199 result.reserve(ordered.size());
200 for (const auto& handle : ordered) {
201 TacticalUnit* unit = resolve<TacticalUnit>(handle);
202 if (unit != nullptr && unit->turn()->charge >= model.threshold) result.push_back(handle);
203 }
204 return result;
205}
206
208void consumeActivationCharge(Battle& battle, ChargeModel model, const std::vector<ecs::EntityHandle>& activated) {
209 if (model.cost == 0) return;
210 for (const auto& handle : activated) {
211 TacticalUnit* unit = resolve<TacticalUnit>(handle);
212 if (unit == nullptr) continue;
213 unit->turn()->charge = std::max(0, unit->turn()->charge - model.cost);
214 }
215}
216
218TacticalUnit* findUnit(Battle& battle, SubjectRef subject) {
219 for (const auto& handle : battle.turn()->units) {
220 TacticalUnit* unit = resolve<TacticalUnit>(handle);
221 if (unit != nullptr && unit->identity()->subject == subject) return unit;
222 }
223 return nullptr;
224}
225
227bool scheduleEntryAlive(const ecs::EntityHandle& handle) {
228 TacticalUnit* unit = resolve<TacticalUnit>(handle);
229 return unit != nullptr && unit->turn()->alive;
230}
231
232} // namespace
233
234Result<void> BattleSystem::addSide(Battle& battle, ecs::EntityHandle sideHandle) {
235 if (battle.turn()->status != BattleStatus::Setup)
236 return Result<void>::failure(Diagnostic::error(DiagnosticCode::PreconditionViolation, "tactics sides can only be added during setup", "battle.status"));
237 auto* side = resolve<TacticalSide>(sideHandle);
238 if (side == nullptr)
239 return Result<void>::failure(Diagnostic::error(DiagnosticCode::StaleHandle, "tactics side handle is stale or has the wrong type", "side"));
240 auto& sides = battle.turn()->sides;
241 if (containsHandle(sides, sideHandle))
242 return Result<void>::failure(Diagnostic::error(DiagnosticCode::Conflict, "tactics side is already registered", "side"));
243 for (const auto& existingHandle : sides) {
244 auto* existing = resolve<TacticalSide>(existingHandle);
245 if (!existing)
246 return Result<void>::failure(Diagnostic::error(DiagnosticCode::StaleHandle, "tactics battle contains a stale side", "battle.sides"));
247 if (existing->identity()->subject == side->identity()->subject)
248 return Result<void>::failure(Diagnostic::error(DiagnosticCode::Conflict, "tactics side subject is already registered", "side.subject"));
249 }
250 auto revision = nextRevision(battle);
251 if (!revision) return Result<void>::failure(revision.status());
252 sides.push_back(sideHandle);
253 battle.turn()->revision = std::move(revision).takeValue();
255}
256
257Result<void> BattleSystem::addUnit(Battle& battle, ecs::EntityHandle unitHandle, ecs::EntityHandle sideHandle,
258 Cell cellValue) {
259 if (battle.turn()->status != BattleStatus::Setup)
260 return Result<void>::failure(Diagnostic::error(DiagnosticCode::PreconditionViolation, "tactics units can only be added during setup", "battle.status"));
261 TacticalUnit* unit = resolve<TacticalUnit>(unitHandle);
262 if (unit == nullptr)
263 return Result<void>::failure(Diagnostic::error(DiagnosticCode::StaleHandle, "tactics unit handle is stale or has the wrong type", "unit"));
264 if (resolve<TacticalSide>(sideHandle) == nullptr || !containsHandle(battle.turn()->sides, sideHandle))
265 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "tactics unit side is not registered with the battle", "side"));
266 if (containsHandle(battle.turn()->units, unitHandle))
267 return Result<void>::failure(Diagnostic::error(DiagnosticCode::Conflict, "tactics unit is already registered", "unit"));
268 if (!unit->identity()->subject.isValid())
269 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "tactics unit requires a valid SubjectRef", "unit.subject"));
270 for (const auto& existingHandle : battle.turn()->units) {
271 auto* existing = resolve<TacticalUnit>(existingHandle);
272 if (!existing)
273 return Result<void>::failure(Diagnostic::error(DiagnosticCode::StaleHandle, "tactics battle contains a stale unit", "battle.units"));
274 if (existing->identity()->subject == unit->identity()->subject)
275 return Result<void>::failure(Diagnostic::error(DiagnosticCode::Conflict, "tactics unit subject is already registered", "unit.subject"));
276 }
277
278 auto placed = battle.board()->value.place(unit->identity()->subject, cellValue);
279 if (!placed) return Result<void>::failure(placed.status());
280
281 auto revision = nextRevision(battle);
282 if (!revision) {
283 battle.board()->value.remove(unit->identity()->subject).ignore("rollback tactics unit placement");
284 return Result<void>::failure(revision.status());
285 }
286
287 unit->membership()->battle = battle.identity()->self;
288 unit->membership()->side = sideHandle;
289 unit->position()->cell = cellValue;
290 unit->position()->placed = true;
291 battle.turn()->units.push_back(unitHandle);
292 battle.turn()->revision = std::move(revision).takeValue();
294}
295
297 auto turn = battle.turn();
298 const Revision expectedRevision = turn->revision;
299 if (turn->status != BattleStatus::Setup)
300 return Result<void>::failure(Diagnostic::error(DiagnosticCode::Conflict, "tactics battle has already started", "battle.status"));
301 if (turn->sides.empty() || turn->units.empty())
302 return Result<void>::failure(Diagnostic::error(DiagnosticCode::PreconditionViolation, "tactics battle requires sides and units", "battle.setup"));
303 // Resolve the policy before mutating anything: an unregistered id must be a
304 // refusal that leaves the battle in Setup, not a battle that starts and then
305 // cannot schedule.
307 if (!policy) return Result<void>::failure(policy.status());
308 // A charge rule that can never make progress is refused here, before any
309 // mutation, so an unschedulable battle stays in Setup instead of advancing
310 // forever without an activation.
311 auto chargeUsable = validateChargeProgress(battle, policy.value()->chargeModel(battle));
312 if (!chargeUsable) return Result<void>::failure(chargeUsable.status());
313 auto boardValid = battle.board()->value.validateInvariants();
314 if (!boardValid) return Result<void>::failure(boardValid.status());
315 auto units = orderedUnits(battle);
316 if (!units) return Result<void>::failure(units.status());
317 auto revision = nextRevision(battle);
318 if (!revision) return Result<void>::failure(revision.status());
319
320 turn->policyId = std::move(policyId);
321 turn->status = BattleStatus::Running;
323 turn->tick = SimulationTick::zero();
324 turn->round = 0;
325 turn->cursor = 0;
326 turn->revision = std::move(revision).takeValue();
327 emit(battle, BattlePhase::Setup, BattlePhase::BattleStart, turn->tick, "battle.started",
328 battle.identity()->subject);
329 BattleCommand command;
331 command.expectedRevision = expectedRevision;
332 command.policyId = turn->policyId;
333 record(battle, std::move(command));
335}
336
344Result<AbilityReceipt> validateAbility(Battle& battle, SubjectRef actor, const LogicalId& action, Cell targetCell,
345 SubjectRef targetUnit, std::string_view payload) {
346 auto turn = battle.turn();
347 if (turn->status != BattleStatus::Running || turn->phase != BattlePhase::Acting || !turn->activeUnit)
349 DiagnosticCode::PreconditionViolation, "tactics battle is not accepting an ability", "battle.phase"));
350 if (!action.isValid())
352 Diagnostic::error(DiagnosticCode::InvalidArgument, "tactics ability requires a valid action id", "action"));
353 TacticalUnit* unit = resolve<TacticalUnit>(*turn->activeUnit);
354 if (unit == nullptr)
356 Diagnostic::error(DiagnosticCode::StaleHandle, "tactics active unit is stale", "battle.activeUnit"));
357 if (unit->identity()->subject != actor)
359 DiagnosticCode::PreconditionViolation, "tactics actor does not own the active turn", "actor"));
360 // The action economy is enforced here: an activation costs one action point,
361 // and a unit with none left cannot act.
362 if (unit->turn()->actionPoints <= 0)
364 DiagnosticCode::PreconditionViolation, "tactics unit has no action points left", "unit.actionPoints"));
365 if (targetCell.layer != unit->position()->cell.layer)
367 "tactics ability target must share the actor's layer",
368 "targetCell.layer"));
369 auto targetState = battle.board()->value.cell(targetCell);
370 if (!targetState) return Result<AbilityReceipt>::failure(targetState.status());
371 if (targetUnit.isValid()) {
372 // A named target must be a living unit of this battle; a stale or foreign subject
373 // is a refusal, never a silently dropped target.
374 TacticalUnit* targeted = findUnit(battle, targetUnit);
375 if (targeted == nullptr)
377 DiagnosticCode::NotFound, "tactics ability target is not a unit of this battle", "targetUnit"));
378 if (!targeted->turn()->alive)
380 DiagnosticCode::PreconditionViolation, "tactics ability target is already defeated", "targetUnit"));
381 }
382 if (payload.size() > kMaxAbilityPayloadBytes)
384 DiagnosticCode::InvalidArgument, "tactics ability payload exceeds the persisted size bound", "payload"));
385
386 AbilityReceipt receipt;
387 receipt.actor = actor;
388 receipt.action = action;
389 receipt.targetUnit = targetUnit;
390 receipt.targetCell = targetCell;
391 receipt.remainingActionPoints = unit->turn()->actionPoints - 1;
392 return Result<AbilityReceipt>::success(std::move(receipt));
393}
394
396 Cell targetCell, SubjectRef targetUnit,
397 std::string_view payload) {
398 return validateAbility(battle, actor, action, targetCell, targetUnit, payload);
399}
400
402 Cell targetCell, SubjectRef targetUnit, std::string payload) {
403 auto validated = validateAbility(battle, actor, action, targetCell, targetUnit, payload);
404 if (!validated) return Result<AbilityReceipt>::failure(validated.status());
405
406 auto turn = battle.turn();
407 const Revision expectedRevision = turn->revision;
408 auto revision = nextRevision(battle);
410 TacticalUnit* unit = resolve<TacticalUnit>(*turn->activeUnit);
411 if (unit == nullptr)
413 Diagnostic::error(DiagnosticCode::StaleHandle, "tactics active unit is stale", "battle.activeUnit"));
414 --unit->turn()->actionPoints;
415 turn->revision = std::move(revision).takeValue();
416 // The event carries the actor; the action id and both targets are read back from the
417 // command log entry this declaration records, joined by its sequence. That keeps one
418 // authority for the declaration instead of copying it into a second shape.
419 emit(battle, turn->phase, turn->phase, turn->tick, "action.declared", actor);
420 BattleCommand command;
422 command.expectedRevision = expectedRevision;
423 command.actor = actor;
424 command.action = action;
425 command.cell = targetCell;
426 command.targetUnit = targetUnit;
427 command.payload = std::move(payload);
428 record(battle, std::move(command));
429 return Result<AbilityReceipt>::success(std::move(validated).takeValue(), Status::success(StatusCode::Applied));
430}
431
433 auto turn = battle.turn();
434 if (turn->status != BattleStatus::Running)
436 Diagnostic::error(DiagnosticCode::PreconditionViolation, "tactics battle is not running", "battle.status"));
437 if (step.delta.nanoseconds() < 0 || step.tick <= turn->tick)
439 DiagnosticCode::InvalidArgument, "tactics step requires a non-negative delta and increasing tick", "step"));
440 auto revision = nextRevision(battle);
441 if (!revision) return Result<BattlePhase>::failure(revision.status());
442 const Revision committedRevision = std::move(revision).takeValue();
443 BattleCommand command;
445 command.expectedRevision = turn->revision;
446 command.step = step;
447
448 switch (turn->phase) {
450 return transition(battle, BattlePhase::RoundStart, step.tick, "round.pending", committedRevision,
451 std::move(command));
453 auto policy = schedulingPolicy(battle);
454 if (!policy) return Result<BattlePhase>::failure(policy.status());
455 const ChargeModel charge = policy.value()->chargeModel(battle);
456 applyChargeGain(battle, charge);
457 auto ordered = orderedUnits(battle);
458 if (!ordered) return Result<BattlePhase>::failure(ordered.status());
459 turn->units = std::move(ordered).takeValue();
460 turn->cursor = 0;
461 ++turn->round;
462 for (const auto& handle : turn->units) {
463 TacticalUnit* unit = resolve<TacticalUnit>(handle);
464 if (unit != nullptr) {
465 unit->turn()->actionPoints = unit->turn()->roundActionPoints;
466 unit->turn()->movePoints = unit->turn()->roundMovePoints;
467 unit->turn()->reactionPoints = unit->turn()->roundReactionPoints;
468 unit->turn()->acted = false;
469 }
470 }
471 if (turn->units.empty())
472 return transition(battle, BattlePhase::BattleEnd, step.tick, "battle.empty", committedRevision,
473 std::move(command));
474 // A charge rule may leave units out of this round's queue. That is a real
475 // scheduling outcome, so it is reported as an unchanged phase with NoOp
476 // rather than as a new phase only one policy can reach: the caller can
477 // tell "time passed, nobody acted" apart from an activation, and the
478 // round counter and resource refresh above still describe a real round.
479 turn->schedule = readySchedule(battle, charge, turn->units);
480 if (turn->schedule.empty()) {
481 auto progress = validateChargeProgress(battle, charge);
482 if (!progress) return Result<BattlePhase>::failure(progress.status());
483 turn->tick = step.tick;
484 turn->revision = committedRevision;
485 record(battle, std::move(command));
487 }
488 consumeActivationCharge(battle, charge, turn->schedule);
489 TacticalUnit* unit = resolve<TacticalUnit>(turn->schedule.front());
490 if (unit == nullptr)
492 DiagnosticCode::StaleHandle, "tactics schedule contains a stale unit", "battle.schedule"));
493 turn->activeUnit = turn->schedule.front();
494 turn->activeSide = unit->membership()->side;
495 return transition(battle, BattlePhase::TurnStart, step.tick, "turn.pending", committedRevision,
496 std::move(command),
497 unit->identity()->subject);
498 }
500 TacticalUnit* unit = turn->activeUnit ? resolve<TacticalUnit>(*turn->activeUnit) : nullptr;
501 if (unit == nullptr)
503 DiagnosticCode::StaleHandle, "tactics active unit is stale", "battle.activeUnit"));
504 return transition(battle, BattlePhase::Acting, step.tick, "turn.started", committedRevision,
505 std::move(command),
506 unit->identity()->subject);
507 }
509 // The round's queue is fixed when the round starts, so the remaining
510 // order cannot drift. A unit that died before its activation is skipped
511 // rather than shifting the cursor onto somebody else's turn.
512 do {
513 ++turn->cursor;
514 } while (turn->cursor < turn->schedule.size() && !scheduleEntryAlive(turn->schedule[turn->cursor]));
515 if (turn->cursor >= turn->schedule.size()) {
516 turn->activeUnit.reset();
517 turn->activeSide.reset();
518 return transition(battle, BattlePhase::RoundEnd, step.tick, "round.pending", committedRevision,
519 std::move(command));
520 }
521 TacticalUnit* unit = resolve<TacticalUnit>(turn->schedule[turn->cursor]);
522 if (unit == nullptr)
524 DiagnosticCode::StaleHandle, "tactics schedule contains a stale unit", "battle.schedule"));
525 turn->activeUnit = turn->schedule[turn->cursor];
526 turn->activeSide = unit->membership()->side;
527 return transition(battle, BattlePhase::TurnStart, step.tick, "turn.pending", committedRevision,
528 std::move(command),
529 unit->identity()->subject);
530 }
532 turn->tick = step.tick;
533 if (evaluateObjectives(battle, committedRevision)) {
534 turn->status = BattleStatus::Ended;
535 return transition(battle, BattlePhase::BattleEnd, step.tick, "battle.objective_completed",
536 committedRevision, std::move(command));
537 }
538 return transition(battle, BattlePhase::RoundStart, step.tick, "round.completed", committedRevision,
539 std::move(command));
541 turn->status = BattleStatus::Ended;
542 turn->tick = step.tick;
543 turn->revision = committedRevision;
544 record(battle, std::move(command));
550 "tactics phase requires an explicit command before advancing", "battle.phase"));
553 "running tactics battle cannot remain in setup phase",
554 "battle.phase"));
555 }
557 Diagnostic::error(DiagnosticCode::InvariantViolation, "tactics battle phase is invalid", "battle.phase"));
558}
559
561 auto turn = battle.turn();
562 const Revision expectedRevision = turn->revision;
563 if (turn->status != BattleStatus::Running || turn->phase != BattlePhase::Acting || !turn->activeUnit)
564 return Result<void>::failure(Diagnostic::error(DiagnosticCode::PreconditionViolation, "tactics battle is not accepting end-turn", "battle.phase"));
565 TacticalUnit* unit = resolve<TacticalUnit>(*turn->activeUnit);
566 if (unit == nullptr)
567 return Result<void>::failure(Diagnostic::error(DiagnosticCode::StaleHandle, "tactics active unit is stale", "battle.activeUnit"));
568 if (unit->identity()->subject != actor)
569 return Result<void>::failure(Diagnostic::error(DiagnosticCode::PreconditionViolation, "tactics actor does not own the active turn", "actor"));
570 auto revision = nextRevision(battle);
571 if (!revision) return Result<void>::failure(revision.status());
572 unit->turn()->acted = true;
573 const auto from = turn->phase;
574 turn->phase = BattlePhase::TurnEnd;
575 turn->revision = std::move(revision).takeValue();
576 emit(battle, from, BattlePhase::TurnEnd, turn->tick, "turn.completed", actor);
577 BattleCommand command;
579 command.expectedRevision = expectedRevision;
580 command.actor = actor;
581 record(battle, std::move(command));
583}
584
586 return moveUnit(battle, actor, destination, battle.turn()->tick);
587}
588
590 SimulationTick commandTick) {
591 if (commandTick < battle.turn()->tick)
593 DiagnosticCode::InvalidArgument, "tactics movement command tick cannot move backward", "commandTick"));
594 const Revision expectedRevision = battle.turn()->revision;
595 auto preview = previewMove(battle, actor, destination);
596 if (!preview) return Result<MoveReceipt>::failure(preview.status());
597 MoveReceipt receipt = std::move(preview).takeValue();
598 auto turn = battle.turn();
599 TacticalUnit* unit = resolve<TacticalUnit>(*turn->activeUnit);
600 auto revision = nextRevision(battle);
601 if (!revision) return Result<MoveReceipt>::failure(revision.status());
602
603 auto moved = battle.board()->value.move(actor, destination);
604 if (!moved) return Result<MoveReceipt>::failure(moved.status());
605 unit->position()->cell = destination;
606 unit->turn()->movePoints -= receipt.cost;
607 receipt.remainingMovePoints = unit->turn()->movePoints;
608 turn->tick = commandTick;
609 turn->revision = std::move(revision).takeValue();
610 emit(battle, BattlePhase::Acting, BattlePhase::Acting, turn->tick, "unit.moved", actor);
611 if (evaluateObjectives(battle, turn->revision)) {
612 turn->status = BattleStatus::Ended;
614 emit(battle, BattlePhase::Acting, BattlePhase::BattleEnd, turn->tick, "battle.objective_completed",
615 battle.identity()->subject);
616 }
617 BattleCommand command;
619 command.expectedRevision = expectedRevision;
620 command.actor = actor;
621 command.cell = destination;
622 command.step.tick = commandTick;
623 record(battle, std::move(command));
625}
626
628 return faceUnit(battle, actor, facing, battle.turn()->tick);
629}
630
632 if (commandTick < battle.turn()->tick)
633 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "tactics facing command tick cannot move backward", "commandTick"));
634 auto preview = previewFace(battle, actor, facing);
635 if (!preview) return preview;
636 auto turn = battle.turn();
637 const Revision expectedRevision = turn->revision;
638 TacticalUnit* unit = resolve<TacticalUnit>(*turn->activeUnit);
639 if (unit->position()->facing == facing) return Result<void>::success(Status::success(StatusCode::NoOp));
640 auto revision = nextRevision(battle);
641 if (!revision) return Result<void>::failure(revision.status());
642 unit->position()->facing = facing;
643 turn->tick = commandTick;
644 turn->revision = std::move(revision).takeValue();
645 emit(battle, BattlePhase::Acting, BattlePhase::Acting, turn->tick, "unit.facing_changed", actor);
646 BattleCommand command;
648 command.expectedRevision = expectedRevision;
649 command.actor = actor;
650 command.facing = facing;
651 command.step.tick = commandTick;
652 record(battle, std::move(command));
654}
655
657 auto turn = battle.turn();
658 if (turn->status != BattleStatus::Running || turn->phase != BattlePhase::Acting || !turn->activeUnit)
659 return Result<void>::failure(Diagnostic::error(DiagnosticCode::PreconditionViolation, "tactics battle is not accepting facing changes", "battle.phase"));
660 TacticalUnit* unit = resolve<TacticalUnit>(*turn->activeUnit);
661 if (!unit)
662 return Result<void>::failure(Diagnostic::error(DiagnosticCode::StaleHandle, "tactics active unit is stale", "battle.activeUnit"));
663 if (unit->identity()->subject != actor)
665 "tactics actor does not own the active turn", "actor"));
666 if (battle.board()->value.topology() == BoardTopology::ExplicitGraph)
668 Diagnostic::error(DiagnosticCode::Unsupported, "graph boards have no coordinate facing", "facing"));
669 const int facingCount = battle.board()->value.topology() == BoardTopology::Square4 ? 4
670 : battle.board()->value.topology() == BoardTopology::Square8 ? 8
671 : 6;
672 if (facing < 0 || facing >= facingCount)
673 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "tactics facing is invalid for board topology", "facing"));
675 Status::success(unit->position()->facing == facing ? StatusCode::NoOp : StatusCode::Ok));
676}
677
679 return waitUnit(battle, actor, battle.turn()->tick);
680}
681
683 if (commandTick < battle.turn()->tick)
684 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "tactics wait command tick cannot move backward", "commandTick"));
685 auto preview = previewWait(battle, actor);
686 if (!preview) return preview;
687 auto turn = battle.turn();
688 const Revision expectedRevision = turn->revision;
689 TacticalUnit* unit = resolve<TacticalUnit>(*turn->activeUnit);
690 auto revision = nextRevision(battle);
691 if (!revision) return Result<void>::failure(revision.status());
692 unit->turn()->acted = true;
693 turn->tick = commandTick;
694 turn->phase = BattlePhase::TurnEnd;
695 turn->revision = std::move(revision).takeValue();
696 emit(battle, BattlePhase::Acting, BattlePhase::TurnEnd, turn->tick, "unit.waited", actor);
697 BattleCommand command;
699 command.expectedRevision = expectedRevision;
700 command.actor = actor;
701 command.step.tick = commandTick;
702 record(battle, std::move(command));
704}
705
707 auto turn = battle.turn();
708 if (turn->status != BattleStatus::Running || turn->phase != BattlePhase::Acting || !turn->activeUnit)
709 return Result<void>::failure(Diagnostic::error(DiagnosticCode::PreconditionViolation, "tactics battle is not accepting wait", "battle.phase"));
710 TacticalUnit* unit = resolve<TacticalUnit>(*turn->activeUnit);
711 if (!unit)
712 return Result<void>::failure(Diagnostic::error(DiagnosticCode::StaleHandle, "tactics active unit is stale", "battle.activeUnit"));
713 if (unit->identity()->subject != actor)
714 return Result<void>::failure(Diagnostic::error(DiagnosticCode::PreconditionViolation, "tactics actor does not own the active turn", "actor"));
715 return Result<void>::success();
716}
717
719 auto turn = battle.turn();
720 if (turn->status != BattleStatus::Running || turn->phase != BattlePhase::Acting || !turn->activeUnit)
722 DiagnosticCode::PreconditionViolation, "tactics battle is not accepting movement", "battle.phase"));
723 TacticalUnit* unit = resolve<TacticalUnit>(*turn->activeUnit);
724 if (unit == nullptr)
726 Diagnostic::error(DiagnosticCode::StaleHandle, "tactics active unit is stale", "battle.activeUnit"));
727 if (unit->identity()->subject != actor)
729 "tactics actor does not own the active turn", "actor"));
730
731 auto path = PathQuery::path(battle.board()->value, actor, destination, unit->turn()->movePoints);
732 if (!path) return Result<MoveReceipt>::failure(path.status());
733 std::vector<Cell> cells = std::move(path).takeValue();
734 auto reachable = PathQuery::reachable(battle.board()->value, actor, unit->turn()->movePoints);
735 if (!reachable) return Result<MoveReceipt>::failure(reachable.status());
736 auto cost = reachable.value().cost(destination);
737 if (!cost) return Result<MoveReceipt>::failure(cost.status());
738 const int consumed = std::move(cost).takeValue();
739
740 return Result<MoveReceipt>::success(MoveReceipt{actor, std::move(cells), consumed,
741 unit->turn()->movePoints - consumed},
743}
744
746 auto turn = battle.turn();
747 const Revision expectedRevision = turn->revision;
748 if (turn->status != BattleStatus::Running)
749 return Result<void>::failure(Diagnostic::error(DiagnosticCode::PreconditionViolation, "only a running tactics battle can finish", "battle.status"));
750 auto revision = nextRevision(battle);
751 if (!revision) return Result<void>::failure(revision.status());
752 const auto from = turn->phase;
753 turn->status = BattleStatus::Ended;
755 turn->revision = std::move(revision).takeValue();
756 emit(battle, from, BattlePhase::BattleEnd, turn->tick, "battle.ended", battle.identity()->subject);
757 BattleCommand command;
759 command.expectedRevision = expectedRevision;
760 record(battle, std::move(command));
762}
763
764Result<std::size_t> BattleSystem::openReaction(Battle& battle, std::uint64_t triggerSequence,
765 std::vector<ReactionCandidate> candidates) {
766 auto turn = battle.turn();
767 const Revision expectedRevision = turn->revision;
768 auto reactions = battle.reactions();
769 if (turn->status != BattleStatus::Running ||
770 (turn->phase != BattlePhase::Acting && turn->phase != BattlePhase::Reaction))
772 "tactics reactions require an acting or reaction phase",
773 "battle.phase"));
774 if (triggerSequence == 0 || candidates.empty())
776 DiagnosticCode::InvalidArgument, "tactics reaction window requires a trigger and candidates", "reaction"));
777 const auto triggerEvent = std::find_if(battle.events()->values.begin(), battle.events()->values.end(),
778 [&](const auto& event) { return event.sequence == triggerSequence; });
779 if (triggerEvent == battle.events()->values.end())
781 DiagnosticCode::NotFound, "tactics reaction trigger event does not exist", "reaction.triggerSequence"));
782 const std::size_t depth = reactions->stack.size() + 1;
783 if (depth > reactions->maxDepth) {
785 DiagnosticCode::PreconditionViolation, "tactics reaction stack depth exceeded", "reaction.depth"));
786 }
787 for (const auto& candidate : candidates) {
788 if (!candidate.reactor.isValid() || !candidate.action.isValid())
791 "tactics reaction candidate requires valid identities", "reaction.candidate"));
792 }
793 std::sort(candidates.begin(), candidates.end(), [](const auto& left, const auto& right) {
794 if (left.priority != right.priority) return left.priority > right.priority;
795 if (left.initiative != right.initiative) return left.initiative > right.initiative;
796 if (left.reactor.format() != right.reactor.format()) return left.reactor.format() < right.reactor.format();
797 return left.action.format() < right.action.format();
798 });
799 candidates.erase(std::unique(candidates.begin(), candidates.end(), [](const auto& left, const auto& right) {
800 return left.reactor == right.reactor && left.action == right.action;
801 }),
802 candidates.end());
803 auto revision = nextRevision(battle);
804 if (!revision) return Result<std::size_t>::failure(revision.status());
805 const auto recordedCandidates = candidates;
806 reactions->stack.push_back({triggerSequence, depth, std::move(candidates)});
807 const auto from = turn->phase;
809 turn->revision = std::move(revision).takeValue();
810 emit(battle, from, BattlePhase::Reaction, turn->tick, "reaction.opened");
811 BattleCommand command;
813 command.expectedRevision = expectedRevision;
814 command.triggerSequence = triggerSequence;
815 command.candidates = recordedCandidates;
816 record(battle, std::move(command));
818}
819
821 auto turn = battle.turn();
822 const Revision expectedRevision = turn->revision;
823 auto reactions = battle.reactions();
824 if (turn->status != BattleStatus::Running || turn->phase != BattlePhase::Reaction || reactions->stack.empty())
826 DiagnosticCode::PreconditionViolation, "tactics battle has no open reaction window", "reaction"));
827 ReactionWindow& window = reactions->stack.back();
828 const auto candidate = std::find_if(window.candidates.begin(), window.candidates.end(), [&](const auto& value) {
829 return value.reactor == reactor && value.action == action;
830 });
831 if (candidate == window.candidates.end())
833 DiagnosticCode::NotFound, "tactics reaction is not eligible in the top window", "reaction.candidate"));
834 const std::string key = std::to_string(window.triggerSequence) + ":" + reactor.format() + ":" + action.format();
835 if (std::find(reactions->seen.begin(), reactions->seen.end(), key) != reactions->seen.end())
837 DiagnosticCode::Conflict, "tactics reaction already fired for this trigger chain", "reaction.cycle"));
838
839 TacticalUnit* reactingUnit = nullptr;
840 for (const auto& handle : turn->units) {
841 TacticalUnit* unit = resolve<TacticalUnit>(handle);
842 if (unit != nullptr && unit->identity()->subject == reactor) {
843 reactingUnit = unit;
844 break;
845 }
846 }
847 if (reactingUnit == nullptr)
849 DiagnosticCode::NotFound, "tactics reacting unit is not part of the battle", "reaction.reactor"));
850 if (!reactingUnit->turn()->alive || reactingUnit->turn()->reactionPoints <= 0)
853 "tactics reacting unit has no available reaction point", "reaction.resource"));
854 auto revision = nextRevision(battle);
856
857 --reactingUnit->turn()->reactionPoints;
858 reactions->seen.push_back(key);
859 const std::uint64_t trigger = window.triggerSequence;
860 reactions->stack.pop_back();
861 turn->phase = reactions->stack.empty() ? BattlePhase::Acting : BattlePhase::Reaction;
862 turn->revision = std::move(revision).takeValue();
863 emit(battle, BattlePhase::Reaction, turn->phase, turn->tick, "reaction.accepted", reactor);
864 BattleCommand command;
866 command.expectedRevision = expectedRevision;
867 command.actor = reactor;
868 command.action = action;
869 command.triggerSequence = trigger;
870 record(battle, std::move(command));
872 {trigger, reactor, action, reactingUnit->turn()->reactionPoints}, Status::success(StatusCode::Applied));
873}
874
876 auto turn = battle.turn();
877 const Revision expectedRevision = turn->revision;
878 auto reactions = battle.reactions();
879 if (turn->status != BattleStatus::Running || turn->phase != BattlePhase::Reaction || reactions->stack.empty())
880 return Result<void>::failure(Diagnostic::error(DiagnosticCode::PreconditionViolation, "tactics battle has no open reaction window", "reaction"));
881 auto revision = nextRevision(battle);
882 if (!revision) return Result<void>::failure(revision.status());
883 const std::uint64_t trigger = reactions->stack.back().triggerSequence;
884 reactions->stack.pop_back();
885 turn->phase = reactions->stack.empty() ? BattlePhase::Acting : BattlePhase::Reaction;
886 turn->revision = std::move(revision).takeValue();
887 emit(battle, BattlePhase::Reaction, turn->phase, turn->tick, "reaction.closed");
888 BattleCommand command;
890 command.expectedRevision = expectedRevision;
891 command.triggerSequence = trigger;
892 record(battle, std::move(command));
894}
895
897 auto turn = battle.turn();
898 if (turn->status != BattleStatus::Setup)
899 return Result<void>::failure(Diagnostic::error(DiagnosticCode::PreconditionViolation, "tactics objectives can only be added during setup", "battle.status"));
900 if (!objective.id.isValid() || !objective.beneficiarySide.isValid())
901 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "tactics objective requires valid identities", "objective"));
902 const auto sideExists = [&](SubjectRef subject) {
903 return std::any_of(turn->sides.begin(), turn->sides.end(), [&](const auto& handle) {
904 return sideSubject(handle) == subject;
905 });
906 };
907 if (!sideExists(objective.beneficiarySide))
908 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "tactics objective beneficiary side is not registered", "objective.beneficiarySide"));
909 if (objective.kind == ObjectiveKind::EliminateSide &&
910 (!objective.targetSide.isValid() || !sideExists(objective.targetSide)))
911 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "tactics elimination target side is not registered", "objective.targetSide"));
912 if (objective.kind == ObjectiveKind::SurviveRounds && objective.requiredRound == 0)
913 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "survive-rounds objective requires a positive round", "objective.requiredRound"));
914 if (objective.kind == ObjectiveKind::OccupyCells) {
915 if (objective.requiredCells.empty())
916 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "occupy-cells objective requires cells", "objective.requiredCells"));
917 std::sort(objective.requiredCells.begin(), objective.requiredCells.end());
918 objective.requiredCells.erase(std::unique(objective.requiredCells.begin(), objective.requiredCells.end()),
919 objective.requiredCells.end());
920 for (const Cell cell : objective.requiredCells)
921 if (!battle.board()->value.contains(cell))
922 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "objective cell does not exist on the board", "objective.requiredCells"));
923 }
924 const auto duplicate = std::find_if(battle.objectives()->values.begin(), battle.objectives()->values.end(),
925 [&](const auto& value) { return value.spec.id == objective.id; });
926 if (duplicate != battle.objectives()->values.end())
927 return Result<void>::failure(Diagnostic::error(DiagnosticCode::Conflict, "tactics objective ID already exists", "objective.id"));
928 auto revision = nextRevision(battle);
929 if (!revision) return Result<void>::failure(revision.status());
930 battle.objectives()->values.push_back({std::move(objective), ObjectiveStatus::Pending, {}});
931 turn->revision = std::move(revision).takeValue();
933}
934
936 auto turn = battle.turn();
937 const Revision expectedRevision = turn->revision;
938 if (turn->status != BattleStatus::Running)
939 return Result<void>::failure(Diagnostic::error(DiagnosticCode::PreconditionViolation, "tactics battle is not accepting outcomes", "battle.status"));
940 TacticalUnit* target = findUnit(battle, subject);
941 if (!target) return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "tactics defeat target is not in the battle", "unit"));
942 if (!target->turn()->alive)
944 auto revision = nextRevision(battle);
945 if (!revision) return Result<void>::failure(revision.status());
946 auto removed = battle.board()->value.remove(subject);
947 if (!removed) return Result<void>::failure(removed.status());
948 target->turn()->alive = false;
949 target->position()->placed = false;
950 turn->revision = std::move(revision).takeValue();
951 emit(battle, turn->phase, turn->phase, turn->tick, "unit.defeated", subject);
952 if (evaluateObjectives(battle, turn->revision)) {
953 const BattlePhase from = turn->phase;
954 turn->status = BattleStatus::Ended;
956 emit(battle, from, BattlePhase::BattleEnd, turn->tick, "battle.objective_completed",
957 battle.identity()->subject);
958 }
959 BattleCommand command;
961 command.expectedRevision = expectedRevision;
962 command.actor = subject;
963 record(battle, std::move(command));
965}
966
968 auto turn = battle.turn();
969 const Revision expectedRevision = turn->revision;
970 if (turn->status != BattleStatus::Running)
972 DiagnosticCode::PreconditionViolation, "tactics random streams require a running battle", "battle.status"));
973 if (!stream.isValid())
975 DiagnosticCode::InvalidArgument, "tactics random stream requires a logical ID", "stream"));
976 auto revision = nextRevision(battle);
977 if (!revision) return Result<std::uint64_t>::failure(revision.status());
978 auto [iterator, inserted] = battle.random()->streams.try_emplace(
979 stream.format(), Battle::RandomStreamState{initialStreamState(battle.identity()->seed, stream.format()), 0});
980 (void)inserted;
981 const std::uint64_t value = splitMix64(iterator->second.state);
982 ++iterator->second.rollIndex;
983 turn->revision = std::move(revision).takeValue();
984 emit(battle, turn->phase, turn->phase, turn->tick, "random.rolled", battle.identity()->subject);
985 BattleCommand command;
987 command.expectedRevision = expectedRevision;
988 command.action = stream;
989 record(battle, std::move(command));
991}
992
993Result<std::vector<ecs::EntityHandle>> BattleSystem::orderedUnits(Battle& battle) {
994 // Resolve the scheduling policy once: an unregistered id cannot be scheduled.
995 auto policy = TurnPolicyRegistry::builtins().find(battle.turn()->policyId);
996 if (!policy) return Result<std::vector<ecs::EntityHandle>>::failure(policy.status());
997
998 const auto sideIndexOf = [&](const ecs::EntityHandle& side) {
999 const auto& sides = battle.turn()->sides;
1000 const auto found =
1001 std::find_if(sides.begin(), sides.end(), [&](const auto& value) { return sameHandle(value, side); });
1002 return static_cast<std::size_t>(std::distance(sides.begin(), found));
1003 };
1004
1005 // Project once, sort the projection, then map back, so the comparator never
1006 // resolves a unit and the policy sees values only.
1007 struct Entry {
1008 ecs::EntityHandle handle;
1009 UnitOrder order;
1010 std::string canonical;
1011 };
1012 std::vector<Entry> entries;
1013 std::vector<ecs::EntityHandle> result;
1014 for (const auto& handle : battle.turn()->units) {
1015 TacticalUnit* unit = resolve<TacticalUnit>(handle);
1016 if (unit == nullptr)
1017 return Result<std::vector<ecs::EntityHandle>>::failure(
1018 Diagnostic::error(DiagnosticCode::StaleHandle, "tactics battle contains a stale unit", "battle.units"));
1019 if (!unit->turn()->alive) continue;
1020 result.push_back(handle);
1021 entries.push_back({handle,
1022 UnitOrder{unit->turn()->initiative, sideIndexOf(unit->membership()->side),
1023 unit->turn()->charge},
1024 unit->identity()->subject.format()});
1025 }
1026 std::sort(entries.begin(), entries.end(), [&](const Entry& left, const Entry& right) {
1027 const ITurnPolicy* resolved = policy.value();
1028 switch (resolved->order(battle, left.order, right.order)) {
1029 case TurnOrder::LeftFirst: return true;
1030 case TurnOrder::RightFirst: return false;
1031 case TurnOrder::Equivalent: break;
1032 }
1033 // The caller owns the tie-break, so every policy is deterministic on equal
1034 // keys without having to restate this rule.
1035 return left.canonical < right.canonical;
1036 });
1037 result.clear();
1038 result.reserve(entries.size());
1039 for (const Entry& entry : entries) result.push_back(entry.handle);
1040 return Result<std::vector<ecs::EntityHandle>>::success(std::move(result));
1041}
1042
1043} // namespace eve::tactics
LogicalId target
double value
Value::Object payload
int subject
Definition AnimSmr.cpp:163
std::string from
std::unordered_map< std::string, QuestRuntime > entries
eve::resource::CostSpec cost
std::map< std::string, Var > values
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
std::uint32_t key
HexVec3 left
HexVec3 right
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
std::vector< std::int32_t > order
ModuleFacing facing
std::uint64_t revision
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
std::string action
Definition PlayHost.cpp:117
std::string path
Definition PlayHost.cpp:110
std::uint32_t seed
Definition PointSet.cpp:807
PrimitiveHandle handle
glm::mat4 model
bool found
int removed
Checked battle setup and deterministic phase systems.
Cell cell
TacticalUnit * unit
bool placed
Battle::Events events
Battle::Reactions reactions
std::vector< UnitCandidate > units
TacticalUnit::TurnResources turn
SimulationTick tick
std::string policyId
Battle::Objectives objectives
ecs::EntityHandle side
float step
Definition TreeMesh.cpp:314
int sides
Definition TreeMesh.cpp:322
Pluggable deterministic turn-scheduling policies.
std::uint32_t depth
std::vector< VegetationPresetCommand > commands
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Human-readable, scoped identifier in the form namespace:name.
Definition Identity.h:411
const std::string & format() const noexcept
Returns the canonical namespace:name representation.
Definition Identity.h:436
bool isValid() const noexcept
Returns whether this value contains a valid namespace and name.
Definition Identity.h:433
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
static SubjectRef nil() noexcept
Return the invalid nil reference.
Definition SubjectRef.h:35
std::string format() const
Return the canonical lower-case UUID spelling.
Definition SubjectRef.h:44
bool isValid() const noexcept
Return whether this reference is valid and non-nil.
Definition SubjectRef.h:38
static constexpr StrongUint64 zero() noexcept
Returns the zero value for this strong type.
static Result< AbilityReceipt > previewAbility(Battle &battle, SubjectRef actor, const LogicalId &action, Cell targetCell, SubjectRef targetUnit={}, std::string_view payload={})
Preview an ability declaration with the exact validator used by commit.
static Result< void > endTurn(Battle &battle, SubjectRef actor)
End the currently acting unit's turn and enter TurnEnd.
static Result< void > defeatUnit(Battle &battle, SubjectRef unit)
Apply a tactical defeat outcome and evaluate affected objectives atomically.
static Result< std::size_t > openReaction(Battle &battle, std::uint64_t triggerSequence, std::vector< ReactionCandidate > candidates)
Open a sorted reaction window for an existing battle event sequence.
static Result< AbilityReceipt > useAbility(Battle &battle, SubjectRef actor, const LogicalId &action, Cell targetCell, SubjectRef targetUnit={}, std::string payload={})
Declare an ability activation by the active unit.
static Result< void > addObjective(Battle &battle, ObjectiveSpec objective)
Register a validated built-in objective during setup.
static Result< std::uint64_t > roll(Battle &battle, const LogicalId &stream)
Consume one value from a battle-owned named deterministic stream.
static Result< void > declineReaction(Battle &battle)
Decline and close the top reaction window without consuming resources.
static Result< void > finish(Battle &battle)
End a running battle and enter BattleEnd.
static Result< MoveReceipt > previewMove(Battle &battle, SubjectRef actor, Cell destination)
Preview movement with the exact validator used by commit, without mutation.
static Result< void > addSide(Battle &battle, ecs::EntityHandle side)
Register a side during setup using a generation-checked handle.
static Result< void > faceUnit(Battle &battle, SubjectRef actor, int facing)
Change the active unit's logical facing without consuming movement points.
static Result< ReactionReceipt > acceptReaction(Battle &battle, SubjectRef reactor, const LogicalId &action)
Accept one eligible reaction, consume one reaction point and close the top window.
static Result< void > previewFace(Battle &battle, SubjectRef actor, int facing)
Preview a facing action with the exact validator used by commit.
static Result< BattlePhase > advance(Battle &battle, const SimulationStep &step)
Advance exactly one observable automatic phase transition.
static Result< void > waitUnit(Battle &battle, SubjectRef actor)
Commit a wait action and enter TurnEnd for the active unit.
static Result< MoveReceipt > moveUnit(Battle &battle, SubjectRef actor, Cell destination)
Validate and atomically commit movement for the active unit.
static Result< void > addUnit(Battle &battle, ecs::EntityHandle unit, ecs::EntityHandle side, Cell cell)
Register and place a unit during setup atomically.
static Result< void > previewWait(Battle &battle, SubjectRef actor)
Preview a wait action with the exact validator used by commit.
static Result< void > start(Battle &battle, std::string policyId)
Start a configured battle and emit battle.started.
Battle short-root and authoritative owner of board and turn state.
static Result< std::vector< Cell > > path(const BoardState &board, SubjectRef subject, Cell target, int budget)
Compute a minimum-cost origin-to-target path within budget.
static Result< Reachability > reachable(const BoardState &board, SubjectRef subject, int budget)
Compute all unoccupied reachable cells for a placed subject.
Tactical unit short-root entity.
static TurnPolicyRegistry & builtins()
Return the process-wide registry carrying the built-in policies.
Result< const ITurnPolicy * > find(std::string_view id) const
Resolve a registered policy by id, or NotFound.
eve::StatusCode Status
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
::eve::SubjectRef SubjectRef
Strong reference to a runtime subject.
Definition Targeting.h:76
Result< AbilityReceipt > validateAbility(Battle &battle, SubjectRef actor, const LogicalId &action, Cell targetCell, SubjectRef targetUnit, std::string_view payload)
Validate one ability declaration without mutating anything.
@ UseAbility
Declare a unit ability activation against a target cell.
BattlePhase
Explicit, externally observable battle phase.
constexpr std::size_t kMaxAbilityPayloadBytes
Upper bound on the opaque ability payload a declaration may carry.
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
Definition Time.h:31
detail::StrongUint64< detail::RevisionTag > Revision
Monotonic content/state revision used for optimistic-concurrency checks.
Definition Revision.h:13
One deterministic fixed-step emitted by SimulationClock.
Definition Time.h:158
SimulationTick tick
Tick reached after this step is applied.
Definition Time.h:160
One declared ability activation and the resource state it left behind.
int remainingActionPoints
Remaining action points after the activation was paid for.
SubjectRef targetUnit
The targeted unit, or an invalid subject when the ability targets a cell only.
One accepted replay command using only persistent identities and values.
SubjectRef targetUnit
The unit this command targeted, when the command targets one.
std::string payload
Opaque caller-owned effect parameters, as declared by the caller.
std::vector< ReactionCandidate > candidates
std::vector< BattleEvent > values
One named deterministic random stream's serializable state.
Logical board coordinate independent of rendering projection.
The charge rule a policy declares for round scheduling.
Definition TurnPolicy.h:63
Committed tactical movement and remaining resource state.
Persistent objective definition using stable side identities.
std::vector< Cell > requiredCells
One open reaction window on the battle-owned LIFO stack.
bool consumed
Definition Graphics.cpp:184