feat: module 05 assemble

Cards become JSON data in course/cards (one file per card, validated
against card.schema.json and the catalog); the M5 references load them
via registerCards and interpret effects, replacing content closures.
Adds assemble(base, donor, half), the assemble/buffHand effects with an
injected chooser, Fleshshape as the data-only Expansion Drop, and stages
course/cards in verify-references and start-module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-29 03:17:21 +01:00
1 parent 55fe5a71d7
commit e04dc1a11f
73 files changed
+6010 -9

No files matched your search

@@ -0,0 +1,53 @@
//! Auras, and the one routine that applies trigger multipliers.
//!
//! 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.
use super::card::{Aura, Stacking, TriggerOn};
use super::world::{ActiveAura, PlayerId, World};
/// Aura Update (Other): replace the snapshot with the auras of every minion now in play.
pub fn aura_update_other(world: &mut World) {
let mut active = Vec::new();
for id in world.minions_in_play() {
for &aura in &world.card(world.card_name(id)).auras {
active.push(ActiveAura {
owner: world.owner_of(id),
aura,
});
}
}
world.set_active_auras(active);
}
/// 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 `Stacking::Additive` one adds `times - 1` extra activations.
pub fn activations(world: &World, owner: PlayerId, on: TriggerOn) -> u32 {
let mut max = 1;
let mut extra = 0;
for active in world.active_auras() {
let Aura::TriggerMultiplier {
trigger,
times,
stacking,
} = active.aura;
if active.owner != owner || trigger != on {
continue;
}
match stacking {
Stacking::Max => max = max.max(times),
Stacking::Additive => extra += times - 1,
}
}
max + extra
}