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>
66 lines
3.1 KiB
TypeScript
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);
|
|
}
|