The die rolls in the open
Every player-facing D4 roll now lands in the chronicle as pips with its purpose: "🎲 alice rolls a 3 — aiming at the unseen — only a 1 finds them." A shared rollD4 helper threads the generic dieRolled event through blind staggers and swings, pit leaps, ooze and pit struggles, and the invisible/shrink miss rolls — the table sees the die land, exactly as it would in person. Rolls whose numbers already showed (the troll's swing, the misdirection direction, the opening roll-off) keep their lines. And the physical die's other job — "any 50-50 call" — gets its button: 🎲 beside the say-box in online games (server-rolled, published through the chat ledger so it persists, replays, and counts toward unread badges) and under the chronicle in hotseat, rolled on the device. Chronicle only; no game state touched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d6d34887e3
commit
ef661c79ad
+19
-17
@@ -500,6 +500,7 @@ export type GameEvent =
|
||||
| { type: "castAroundCorner"; caster: PlayerId }
|
||||
| { type: "moveBumped"; player: PlayerId; direction: Side }
|
||||
| { type: "attackMisdirected"; attacker: PlayerId; intended: PlayerId; rolledDirection: Side; newTarget: PlayerId | null }
|
||||
| { type: "dieRolled"; player: PlayerId | null; roll: number; purpose: string }
|
||||
| { type: "retreatedInHorror"; player: PlayerId; from: Cell; to: Cell }
|
||||
| { type: "illusionWallCreated"; caster: PlayerId; edge: { cell: Cell; side: Side } }
|
||||
| { type: "illusionTested"; player: PlayerId; edge: string; result: "believes" | "seesThrough" }
|
||||
@@ -3322,9 +3323,7 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult {
|
||||
// BLIND (or a DUST CLOUD): "must roll direction on D4 if attempting to
|
||||
// move ... bumping into a wall counts as one space of movement."
|
||||
if (isBlinded(state, p)) {
|
||||
const [roll, rngNext] = rollDie(state.rng);
|
||||
state.rng = rngNext;
|
||||
direction = SIDES[roll - 1]!;
|
||||
direction = SIDES[rollD4(state, events, p.id, "a blind stagger picks the direction") - 1]!;
|
||||
}
|
||||
|
||||
// A believed (or untested, when bumped) ILLUSION WALL blocks the believer.
|
||||
@@ -3460,8 +3459,7 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult {
|
||||
// fall in (2 damage, movement over); otherwise you sail across to the far
|
||||
// side (if there is open floor there).
|
||||
if (content?.kind === "pit" && !misted && !p.inPit) {
|
||||
const [roll, rngNext] = rollDie(state.rng);
|
||||
state.rng = rngNext;
|
||||
const roll = rollD4(state, events, p.id, "leaping the pit — a 1 falls in");
|
||||
if (roll === 1) {
|
||||
p.inPit = true;
|
||||
events.push({ type: "fellInPit", player: p.id, at: p.position });
|
||||
@@ -3845,15 +3843,15 @@ function doPunch(prev: GameState, targetId: PlayerId): CommandResult {
|
||||
|
||||
// BLIND: "...engage in combat..." — a blinded brawler flails on a die
|
||||
// roll; the swing connects only on a 1 (same convention as INVISIBLE).
|
||||
const preRoll: GameEvent[] = [];
|
||||
if (isBlinded(state, attacker)) {
|
||||
const [roll, rngNext] = rollDie(state.rng);
|
||||
state.rng = rngNext;
|
||||
const roll = rollD4(state, preRoll, attacker.id, "a blind swing — only a 1 connects");
|
||||
if (roll !== 1) {
|
||||
state.turn.attackUsed = true;
|
||||
return {
|
||||
ok: true,
|
||||
state,
|
||||
events: [{
|
||||
events: [...preRoll, {
|
||||
type: "attackMisdirected", attacker: attacker.id, intended: target.id,
|
||||
rolledDirection: SIDES[roll - 1]!, newTarget: null,
|
||||
}],
|
||||
@@ -3879,7 +3877,7 @@ function doPunch(prev: GameState, targetId: PlayerId): CommandResult {
|
||||
return {
|
||||
ok: true,
|
||||
state,
|
||||
events: [{ type: "punched", attacker: attacker.id, target: target.id, at: attacker.position }],
|
||||
events: [...preRoll, { type: "punched", attacker: attacker.id, target: target.id, at: attacker.position }],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4706,8 +4704,7 @@ function resolveStack(state: GameState, events: GameEvent[]): void {
|
||||
// SHRINK (50% miss). "If a spell misses, it dissipates harmlessly." A
|
||||
// creature sharing the square has nothing to aim: no miss rolls.
|
||||
if (!stack.creatureId && sustainedOn(state, defender.id, "invisible").length > 0) {
|
||||
const [roll, rngNext] = rollDie(state.rng);
|
||||
state.rng = rngNext;
|
||||
const roll = rollD4(state, events, attacker.id, "aiming at the unseen — only a 1 finds them");
|
||||
if (roll !== 1) {
|
||||
events.push({ type: "attackMissed", attacker: attacker.id, defender: defender.id, attackCardId: attackId, because: "invisible" });
|
||||
events.push({ type: "attackResolved", attacker: attacker.id, defender: defender.id, attackCardId: attackId, damageDealt: 0, reflectedDamage: 0, fullyStopped: true, redirected: false });
|
||||
@@ -4715,8 +4712,7 @@ function resolveStack(state: GameState, events: GameEvent[]): void {
|
||||
}
|
||||
}
|
||||
if (!stack.creatureId && sustainedOn(state, defender.id, "shrink").length > 0) {
|
||||
const [roll, rngNext] = rollDie(state.rng);
|
||||
state.rng = rngNext;
|
||||
const roll = rollD4(state, events, attacker.id, "aiming at the tiny — 1-2 hits");
|
||||
if (roll > 2) {
|
||||
events.push({ type: "attackMissed", attacker: attacker.id, defender: defender.id, attackCardId: attackId, because: "shrink" });
|
||||
events.push({ type: "attackResolved", attacker: attacker.id, defender: defender.id, attackCardId: attackId, damageDealt: 0, reflectedDamage: 0, fullyStopped: true, redirected: false });
|
||||
@@ -5283,16 +5279,14 @@ function beginTurnFor(state: GameState, events: GameEvent[], index: number): voi
|
||||
// KILLER OOZE / PIT: struggle rolls before you may move this turn.
|
||||
let struggleImmobilized = false;
|
||||
if (player.fallenInOoze) {
|
||||
const [roll, rngNext] = rollDie(state.rng);
|
||||
state.rng = rngNext;
|
||||
const roll = rollD4(state, events, player.id, "struggling in the ooze — 1-2 breaks free");
|
||||
const stood = roll <= 2;
|
||||
events.push({ type: "struggledInOoze", player: player.id, stood });
|
||||
if (stood) player.fallenInOoze = false;
|
||||
else struggleImmobilized = true;
|
||||
}
|
||||
if (player.inPit) {
|
||||
const [roll, rngNext] = rollDie(state.rng);
|
||||
state.rng = rngNext;
|
||||
const roll = rollD4(state, events, player.id, "climbing from the pit — 1-2 gets out");
|
||||
const out = roll <= 2;
|
||||
events.push({ type: "climbedFromPit", player: player.id, success: out });
|
||||
if (out) player.inPit = false;
|
||||
@@ -5335,6 +5329,14 @@ function beginTurnFor(state: GameState, events: GameEvent[], index: number): voi
|
||||
};
|
||||
}
|
||||
|
||||
/** Roll the D4 in the open: the pips land in the chronicle. */
|
||||
function rollD4(state: GameState, events: GameEvent[], player: PlayerId | null, purpose: string): number {
|
||||
const [roll, next] = rollDie(state.rng);
|
||||
state.rng = next;
|
||||
events.push({ type: "dieRolled", player, roll, purpose });
|
||||
return roll;
|
||||
}
|
||||
|
||||
/** An effect's turns ran out: announce it and clean up what it built. */
|
||||
function expireEffect(state: GameState, events: GameEvent[], s: SustainedEffect): void {
|
||||
events.push({ type: "spellExpired", effectId: s.id, cardId: s.cardId, target: s.targetId });
|
||||
|
||||
Reference in New Issue
Block a user