Card wave 2: durations, doors, teleports, modifiers; harden room auth
Duration system: sustained effects expire at the start of the caster's turns; SLOW (movement 1, no number cards, attack every other turn), NO SPELL, MEDUSA (paralysis + damage immunity), INVISIBLE (1-in-4 hit roll), SHRINK (50% miss, movement 2). Doors: PICK LOCK and MASTER KEY (displayed, reusable) unlock adjacent doors until end of turn, REMOVE LOCK is permanent, JAM LOCK seals a door for everyone. Movement: TELEPORT (4 spaces through walls, ends movement), PASS THROUGH WALL charges, POWER RUN (life for spaces), SWAP (consumes movement), GO AWAY (knockback + lost turn), TELEPORT OPPONENT. Card warfare: CARD ERASURE (named), THOUGHT-STEAL (2 random via seeded RNG), TELEPATH (private hand reveal), POWER DRAIN (damage feeds the caster), SUDDEN DEATH, STONE DEAD, WIZARDBLADE (same-square, number-powered, stays displayed). Cast modifiers: AMPLIFY doubles power/duration (stackable x2), ADD permits two number cards, EXTEND doubles duration; REVERSE heals instead of harms but keeps secondary effects. Counters now also halve durations (BLUNT) and split them (REFLECTION). Security (from review findings): room codes and game seeds now come from node:crypto, and every seat gets a secret token — reclaiming a name in a room requires its token, closing the impersonation hole. 29 cards implemented; 51 tests passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
36b3ffe9a6
commit
67743dd17e
@@ -34,6 +34,28 @@ function humanize(e: GameEvent): string | null {
|
||||
case "wallDestroyed": return e.wasDoor ? `A door is blasted to rubble!` : `A wall crumbles!`;
|
||||
case "extraTurnGranted": return `${e.player} speeds up — extra turn banked.`;
|
||||
case "trapSprung": return `${e.player} walked into an old TRAP! Lose a turn.`;
|
||||
case "attackMissed": return e.because === "invisible"
|
||||
? `The attack passes through empty air — ${e.defender} is invisible!`
|
||||
: `${e.defender} is too small to hit — the attack misses!`;
|
||||
case "damageImmune": return `${e.player} is stone — the damage has no effect.`;
|
||||
case "lifeGained": return `${e.player} gains ${e.amount} life (${e.source}) — now ${e.lifeAfter}.`;
|
||||
case "spellSustained": return `${cardDef(e.cardId).name} settles over ${e.target} (${e.turns} turn${e.turns === 1 ? "" : "s"}).`;
|
||||
case "spellExpired": return `${cardDef(e.cardId).name} wears off ${e.target}.`;
|
||||
case "teleported": return e.by === e.player
|
||||
? `${e.player} teleports across the maze!`
|
||||
: `${e.player} is teleported away by ${e.by}!`;
|
||||
case "positionsSwapped": return `${e.a} and ${e.b} swap places!`;
|
||||
case "cardErased": return e.found
|
||||
? `${e.player}'s ${e.cardId ? cardDef(e.cardId).name : "card"} is erased from their mind!`
|
||||
: `${e.player} wasn't holding that card — the erasure fizzles.`;
|
||||
case "cardsStolen": return `${e.to} steals ${e.count} card(s) from ${e.from}'s thoughts!`;
|
||||
case "handRevealed": return `${e.to} reads ${e.player}'s mind — their hand is revealed.`;
|
||||
case "doorUnlocked": return `${e.player} unlocks a door.`;
|
||||
case "doorsRelocked": return `The door swings shut and relocks.`;
|
||||
case "doorJammed": return `${e.player} jams a door's lock solid.`;
|
||||
case "lockRemoved": return `${e.player} removes a door's lock for good.`;
|
||||
case "cardDisplayed": return `${e.player} displays ${cardDef(e.card.cardId).name}.`;
|
||||
case "lifeTraded": return `${e.player} burns ${e.points} life for speed!`;
|
||||
case "trapRedrawnDuringDeal": return null;
|
||||
case "died": return `☠ ${e.player} is dead${e.killedBy ? ` — killed by ${e.killedBy}` : ""}.`;
|
||||
case "handTaken": return `${e.to} takes ${e.count} cards from ${e.from}'s body.`;
|
||||
@@ -61,6 +83,7 @@ class Net {
|
||||
error = $state<string | null>(null);
|
||||
|
||||
private ws: WebSocket | null = null;
|
||||
private token: string | null = null;
|
||||
|
||||
connect(): void {
|
||||
if (this.ws) return;
|
||||
@@ -75,6 +98,9 @@ class Net {
|
||||
ws.onmessage = (raw) => {
|
||||
const msg = JSON.parse(raw.data as string);
|
||||
switch (msg.type) {
|
||||
case "seat":
|
||||
this.token = msg.token;
|
||||
break;
|
||||
case "room":
|
||||
this.roomId = msg.roomId;
|
||||
this.players = msg.players;
|
||||
@@ -109,7 +135,7 @@ class Net {
|
||||
|
||||
join(roomId: string, name: string): void {
|
||||
this.you = name;
|
||||
this.send({ type: "join", roomId, name });
|
||||
this.send({ type: "join", roomId, name, token: this.token });
|
||||
}
|
||||
|
||||
start(): void {
|
||||
|
||||
Reference in New Issue
Block a user