Rev 20: a wave names its victims before it pushes (LK97)
Kestrel stood against a wall Mad Hywel turned to water, was washed one square back into stone, and was crushed twice. The sweep walked the way it pushed: having shoved him onto the next square, its next probe found him there and shoved him again with the force left. From rev 20 a sweep names every victim along its line first, then pushes each once; older games replay the double shove. A test washes a wizard one square into a wall under both readings. With it, a client fix: a tap on a bare square while holding a card no longer walks there. Kestrel tapped a slime with LIGHTNING BLAST in hand and strode into it. The tap now says the card is aimed at a wizard or creature and leaves the feet alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
e5d82a94af
commit
3f71b04c3b
+46
-42
@@ -238,7 +238,7 @@ export interface CastParams {
|
||||
}
|
||||
|
||||
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
|
||||
export const CURRENT_RULES_REV = 19;
|
||||
export const CURRENT_RULES_REV = 20;
|
||||
|
||||
/** Every rulings revision since the baseline, newest last — the entries a
|
||||
* game's deckRev freezes it before or after. Shown to players as the house
|
||||
@@ -262,6 +262,7 @@ export const RULES_REVISIONS: { rev: number; note: string }[] = [
|
||||
{ rev: 17, note: "A warp mouth on a pit's rim is a way off it like any square: a bare step beside one square and one mouth is a fork the walker names. In every game the mouth may be named, and is taken when no square offers; only an older game's bare step still lands on the square unasked." },
|
||||
{ rev: 18, note: "REFLECTION's returning half is an attack on the caster in its own right, with the caster's own counteraction window — an ABSORB or a BLUNT meets it as it would any blow. Before, the half landed the instant the spell resolved." },
|
||||
{ rev: 19, note: "DUST CLOUD blinds whoever stands in it: no LOS spell may be cast from inside a cloud, nor at anyone standing in one, and VISIONSTONE does not see through it. Spells cast on oneself still work. Before, the cloud blocked only sight lines passing through it." },
|
||||
{ rev: 20, note: "A waterwall's wave names its victims before it pushes any of them. Before, a wave walking the way it pushed could catch a wizard it had just shoved and shove them again with the force left — one square into a wall cost two points instead of one." },
|
||||
];
|
||||
|
||||
export interface GameConfig {
|
||||
@@ -2876,6 +2877,48 @@ function waveForce(range: number, dist: number): number {
|
||||
return range - dist;
|
||||
}
|
||||
|
||||
/** One line of a wave's sweep: the squares it reaches, nearest first, with
|
||||
* the force left at each. The victims are named before any is pushed
|
||||
* (rev 20). Before it the sweep, walking the way it pushed, could find a
|
||||
* victim it had just shoved and shove them again with the force left: a
|
||||
* wizard washed one square into a wall was crushed twice. Older games
|
||||
* replay that sweep. */
|
||||
function sweepWave(
|
||||
state: GameState, events: GameEvent[], line: { cell: Cell; force: number }[], dir: Side, reason: string,
|
||||
): void {
|
||||
const gather = ({ cell, force }: { cell: Cell; force: number }) => ({
|
||||
force,
|
||||
players: state.players.filter((p) => p.alive && cellKey(p.position) === cellKey(cell)),
|
||||
creatures: state.creatures.filter((c) => cellKey(c.position) === cellKey(cell)),
|
||||
slime: state.squareContents[cellKey(cell)]?.kind === "slime" ? cell : null,
|
||||
});
|
||||
const apply = (hit: ReturnType<typeof gather>) => {
|
||||
for (const p of hit.players) washBackN(state, events, p, dir, hit.force);
|
||||
for (const c of hit.creatures) {
|
||||
if (!state.creatures.includes(c)) continue;
|
||||
if (c.kind === "fire-imp") destroyCreature(state, events, c, reason);
|
||||
else washBackCreature(state, events, c, dir, hit.force);
|
||||
}
|
||||
if (hit.slime) {
|
||||
delete state.squareContents[cellKey(hit.slime)];
|
||||
delete state.slimeTraps[cellKey(hit.slime)];
|
||||
events.push({ type: "slimeWashed", cell: hit.slime });
|
||||
}
|
||||
};
|
||||
if ((state.config.deckRev ?? 1) >= 20) line.map(gather).forEach(apply);
|
||||
else for (const step of line) apply(gather(step));
|
||||
}
|
||||
|
||||
function waveLine(start: Cell, dir: Side, range: number): { cell: Cell; force: number }[] {
|
||||
const line: { cell: Cell; force: number }[] = [];
|
||||
let probe = start;
|
||||
for (let dist = 0; dist < range; dist++) {
|
||||
line.push({ cell: probe, force: waveForce(range, dist) });
|
||||
probe = neighbor(probe, dir);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
/** A collapsing waterwall wave from an edge: wash players back `range`. */
|
||||
function waveFromEdge(state: GameState, events: GameEvent[], cell: Cell, side: Side, range: number, reason = "rushing water"): void {
|
||||
// A warp mouth's "far side" is the opposite rim: the collapse washes both
|
||||
@@ -2890,51 +2933,12 @@ function waveFromEdge(state: GameState, events: GameEvent[], cell: Cell, side: S
|
||||
{ start: cell, dir: opposite(side) },
|
||||
{ start: neighbor(cell, side), dir: side },
|
||||
];
|
||||
for (const { start, dir } of pushes) {
|
||||
let probe = start;
|
||||
for (let dist = 0; dist < range; dist++) {
|
||||
const force = waveForce(range, dist);
|
||||
for (const p of state.players) {
|
||||
if (p.alive && cellKey(p.position) === cellKey(probe)) washBackN(state, events, p, dir, force);
|
||||
}
|
||||
for (const c of [...state.creatures]) {
|
||||
if (cellKey(c.position) !== cellKey(probe)) continue;
|
||||
if (c.kind === "fire-imp") {
|
||||
destroyCreature(state, events, c, reason);
|
||||
} else {
|
||||
washBackCreature(state, events, c, dir, force);
|
||||
}
|
||||
}
|
||||
if (state.squareContents[cellKey(probe)]?.kind === "slime") {
|
||||
delete state.squareContents[cellKey(probe)];
|
||||
delete state.slimeTraps[cellKey(probe)];
|
||||
events.push({ type: "slimeWashed", cell: probe });
|
||||
}
|
||||
probe = neighbor(probe, dir);
|
||||
}
|
||||
}
|
||||
for (const { start, dir } of pushes) sweepWave(state, events, waveLine(start, dir, range), dir, reason);
|
||||
}
|
||||
|
||||
/** A wave bursting outward from a cell in all four directions. */
|
||||
function waveFromCell(state: GameState, events: GameEvent[], center: Cell, range: number): void {
|
||||
for (const dir of SIDES) {
|
||||
let probe = center;
|
||||
for (let dist = 0; dist < range; dist++) {
|
||||
probe = neighbor(probe, dir);
|
||||
const force = waveForce(range, dist);
|
||||
for (const p of state.players) {
|
||||
if (p.alive && cellKey(p.position) === cellKey(probe)) washBackN(state, events, p, dir, force);
|
||||
}
|
||||
for (const c of [...state.creatures]) {
|
||||
if (cellKey(c.position) !== cellKey(probe)) continue;
|
||||
if (c.kind === "fire-imp") {
|
||||
destroyCreature(state, events, c, "rushing water");
|
||||
} else {
|
||||
washBackCreature(state, events, c, dir, force);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const dir of SIDES) sweepWave(state, events, waveLine(neighbor(center, dir), dir, range), dir, "rushing water");
|
||||
}
|
||||
|
||||
/** A monster summon: ATTACK-typed, uses your attack, appears in your LOS. */
|
||||
|
||||
Reference in New Issue
Block a user