Files
systemic-game-design-course/course/11-conditions/reference/ts/src/engine/aura.ts
T
Lucas WintherandClaude Opus 5.5 34c655c1a4 feat: Module 11, Conditions (NOUN ON NOUN IS ..., checked per object at use; NEAR as the Expansion Drop)
Adds course/11-conditions: lesson, scenarios, nine test_11 tests in both
tracks, the cumulative reference (Module 10's plus conditions) and the NEAR
drop. The engine learns a condition role, rules carry conditions, and
Level.hasProperty and conversion evaluate content's condition checks against
the board whenever a rule is used. Resolution pairs only rules with the same
conditions. Catalog, harness contract, research notes and the diff-tracks
generator follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:00:26 +01:00

66 lines
3.1 KiB
TypeScript

// Auras, the one routine that applies trigger multipliers, and the lookup for
// damage redirects.
//
// An aura is data on a card: a continuous effect its minion has while in play.
// The engine never asks "is that card on the board?" in the middle of resolving
// something. It takes a snapshot of the auras in force at defined points (an Aura
// Update) and every rule reads the snapshot.
//
// Hearthstone runs two kinds of Aura Update when the outermost Phase ends: one for
// Health/Attack auras before the Death Creation Step, and one for every other aura
// after it. Trigger multipliers are "Other" auras, so a minion that dies in a Death
// Creation Step no longer multiplies the Deathrattles of that step's Death Phase.
// Summoning a minion also runs an Aura Update.
//
// Module 10: auras can come from outside the cards. SpellGrid compiles rules on its
// grid into auras (`World.setRuleAuras`); they join the snapshot at the next Aura
// Update, beside the minions' own, and go through the same routine. A rule's
// multiplier stacks with a card's by the same `stacking` field. A cap (`IS NOT`)
// limits the total, whatever multiplies it.
import type { TriggerOn } from "./card";
import type { EntityId, PlayerId, World } from "./world";
/**
* Aura Update (Other): replace the snapshot with the auras of every minion now in
* play, and the auras from outside the cards, which apply to both players.
*/
export function auraUpdateOther(world: World): void {
const players: PlayerId[] = [0, 1];
world.setActiveAuras([
...world.minionsInPlay().flatMap((id) =>
world.card(world.cardName(id)).auras.map((aura) => ({ owner: world.ownerOf(id), source: id, aura })),
),
...world.ruleAuras().flatMap((aura) => players.map((owner) => ({ owner, aura }))),
]);
}
/**
* The minion that takes damage meant for `player`'s hero, if a redirect is in force:
* the oldest (order of play) of `player`'s minions with a `damageRedirect` aura in the
* snapshot. The snapshot lists minions oldest first, so that is the first one found.
*/
export function redirectFor(world: World, player: PlayerId): EntityId | undefined {
return world.activeAuras().find((a) => a.aura.type === "damageRedirect" && a.owner === player)?.source;
}
/**
* How many times a trigger listening for `on`, on a minion `owner` controls, activates
* when it resolves. Reads the snapshot from the last Aura Update. With no multiplier in
* force the answer is 1. Multipliers with `stacking: "max"` contribute their largest
* `times`; each `"additive"` one adds `times - 1` extra activations. A cap
* (`triggerCap`) then limits the total to its `times`.
*/
export function activations(world: World, owner: PlayerId, on: TriggerOn): number {
let max = 1;
let extra = 0;
let cap = Infinity;
for (const { owner: auraOwner, aura } of world.activeAuras()) {
if (aura.type === "damageRedirect" || auraOwner !== owner || aura.trigger !== on) continue;
if (aura.type === "triggerCap") cap = Math.min(cap, aura.times);
else if (aura.stacking === "max") max = Math.max(max, aura.times);
else extra += aura.times - 1;
}
return Math.min(cap, max + extra);
}