Stone soaks show their work in the damage line
'Takes 2 damage' from a 3-point bolt read as broken arithmetic unless you remembered the bloodstone. The damaged event now carries its soaks (bloodstone's flat point, soulstone's spell-damage clamp) and the log prints them: 'takes 2 damage (lightning-blast (reflected) — bloodstone soaks 1)'. A blow the bloodstone drinks entirely — previously silent — announces itself too. Events are derived, so replays gain the notes retroactively. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
co-authored by
Claude Fable 5
parent
1d7407cb20
commit
5243a173a8
@@ -501,8 +501,9 @@ export type GameEvent =
|
|||||||
| { type: "attackAbsorbedIntoHand"; player: PlayerId; attackCard: CardInstance }
|
| { type: "attackAbsorbedIntoHand"; player: PlayerId; attackCard: CardInstance }
|
||||||
| { type: "attackMissed"; attacker: PlayerId; defender: PlayerId; attackCardId: string | null; because: "invisible" | "shrink" }
|
| { type: "attackMissed"; attacker: PlayerId; defender: PlayerId; attackCardId: string | null; because: "invisible" | "shrink" }
|
||||||
| { type: "attackResolved"; attacker: PlayerId; defender: PlayerId; attackCardId: string | null; damageDealt: number; reflectedDamage: number; fullyStopped: boolean; redirected: boolean }
|
| { type: "attackResolved"; attacker: PlayerId; defender: PlayerId; attackCardId: string | null; damageDealt: number; reflectedDamage: number; fullyStopped: boolean; redirected: boolean }
|
||||||
| { type: "damaged"; player: PlayerId; amount: number; source: string; lifeAfter: number }
|
| { type: "damaged"; player: PlayerId; amount: number; source: string; lifeAfter: number;
|
||||||
| { type: "damageImmune"; player: PlayerId; source: string; because: "medusa" }
|
soaks?: { what: "bloodstone" | "soulstone"; amount: number }[] }
|
||||||
|
| { type: "damageImmune"; player: PlayerId; source: string; because: "medusa" | "bloodstone" }
|
||||||
| { type: "lifeGained"; player: PlayerId; amount: number; source: string; lifeAfter: number }
|
| { type: "lifeGained"; player: PlayerId; amount: number; source: string; lifeAfter: number }
|
||||||
| { type: "stunned"; player: PlayerId; turnsLost: number }
|
| { type: "stunned"; player: PlayerId; turnsLost: number }
|
||||||
| { type: "knockedBack"; player: PlayerId; from: Cell; to: Cell; squares: number }
|
| { type: "knockedBack"; player: PlayerId; from: Cell; to: Cell; squares: number }
|
||||||
@@ -5569,17 +5570,27 @@ function applyDamage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BLOODSTONE: "Lowers all damage done you by one point per attack."
|
// BLOODSTONE: "Lowers all damage done you by one point per attack."
|
||||||
|
const soaks: { what: "bloodstone" | "soulstone"; amount: number }[] = [];
|
||||||
if (displays(target, "bloodstone")) {
|
if (displays(target, "bloodstone")) {
|
||||||
amount = Math.max(0, amount - 1);
|
amount = Math.max(0, amount - 1);
|
||||||
if (amount === 0) return;
|
if (amount === 0) {
|
||||||
|
events.push({ type: "damageImmune", player: target.id, source, because: "bloodstone" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
soaks.push({ what: "bloodstone", amount: 1 });
|
||||||
}
|
}
|
||||||
// SOULSTONE: "Last three points ... can only be lost to physical damage."
|
// SOULSTONE: "Last three points ... can only be lost to physical damage."
|
||||||
if (damageKind === "spell" && displays(target, "soulstone") && target.life > 3) {
|
if (damageKind === "spell" && displays(target, "soulstone") && target.life > 3) {
|
||||||
amount = Math.min(amount, target.life - 3);
|
const clamped = Math.min(amount, target.life - 3);
|
||||||
|
if (clamped < amount) soaks.push({ what: "soulstone", amount: amount - clamped });
|
||||||
|
amount = clamped;
|
||||||
}
|
}
|
||||||
|
|
||||||
target.life -= amount;
|
target.life -= amount;
|
||||||
events.push({ type: "damaged", player: target.id, amount, source, lifeAfter: target.life });
|
events.push({
|
||||||
|
type: "damaged", player: target.id, amount, source, lifeAfter: target.life,
|
||||||
|
...(soaks.length > 0 ? { soaks } : {}),
|
||||||
|
});
|
||||||
if (target.life > 0) return;
|
if (target.life > 0) return;
|
||||||
|
|
||||||
target.alive = false;
|
target.alive = false;
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ export function humanize(e: GameEvent): string | null {
|
|||||||
if (e.redirected) return `The spell is reflected back at ${e.attacker}!`;
|
if (e.redirected) return `The spell is reflected back at ${e.attacker}!`;
|
||||||
if (e.fullyStopped) return `The attack is completely stopped.`;
|
if (e.fullyStopped) return `The attack is completely stopped.`;
|
||||||
return null; // the damaged event tells the story
|
return null; // the damaged event tells the story
|
||||||
case "damaged": return `${e.player} takes ${e.amount} damage (${e.source}) — ${e.lifeAfter} life left.`;
|
case "damaged": {
|
||||||
|
const soak = e.soaks?.map((s) => `${s.what} soaks ${s.amount}`).join(", ");
|
||||||
|
return `${e.player} takes ${e.amount} damage (${e.source}${soak ? ` — ${soak}` : ""}) — ${e.lifeAfter} life left.`;
|
||||||
|
}
|
||||||
case "stunned": return `${e.player} is stunned and loses a turn!`;
|
case "stunned": return `${e.player} is stunned and loses a turn!`;
|
||||||
case "knockedBack": return `${e.player} is knocked back ${e.squares} square(s)!`;
|
case "knockedBack": return `${e.player} is knocked back ${e.squares} square(s)!`;
|
||||||
case "stonesDestroyed": return `${e.player}'s magic stones are destroyed!`;
|
case "stonesDestroyed": return `${e.player}'s magic stones are destroyed!`;
|
||||||
@@ -64,7 +67,9 @@ export function humanize(e: GameEvent): string | null {
|
|||||||
case "attackMissed": return e.because === "invisible"
|
case "attackMissed": return e.because === "invisible"
|
||||||
? `The attack passes through empty air — ${e.defender} is invisible!`
|
? `The attack passes through empty air — ${e.defender} is invisible!`
|
||||||
: `${e.defender} is too small to hit — the attack misses!`;
|
: `${e.defender} is too small to hit — the attack misses!`;
|
||||||
case "damageImmune": return `${e.player} is stone — the damage has no effect.`;
|
case "damageImmune": return e.because === "bloodstone"
|
||||||
|
? `${e.player}'s bloodstone drinks the whole blow — no damage.`
|
||||||
|
: `${e.player} is stone — the damage has no effect.`;
|
||||||
case "lifeGained": return `${e.player} gains ${e.amount} life (${e.source}) — now ${e.lifeAfter}.`;
|
case "lifeGained": return `${e.player} gains ${e.amount} life (${e.source}) — now ${e.lifeAfter}.`;
|
||||||
case "spellSustained": return `${spellName(e.cardId)} settles over ${e.target} (${e.turns} turn${e.turns === 1 ? "" : "s"}).`;
|
case "spellSustained": return `${spellName(e.cardId)} settles over ${e.target} (${e.turns} turn${e.turns === 1 ? "" : "s"}).`;
|
||||||
case "spellExpired": return `${spellName(e.cardId)} wears off ${e.target}.`;
|
case "spellExpired": return `${spellName(e.cardId)} wears off ${e.target}.`;
|
||||||
|
|||||||
Reference in New Issue
Block a user