Reflected Swap Meet: the reflector chooses the trade (rev 27)
'FULL REFLECTION lets the other player decide which objects, if any, will be swapped' — the reflector's choice now rides their counteract (params.cardId, reflector's item first), resolved with the roles swapped when the reflection settles; 'none' or no choice trades nothing. The client walks the reflector through the same give-and-take picker, with a 'swap nothing' refusal. Also from room XRT7: the trade picker now honors each room's own rules revision (older rooms trade only object-typed cards), so it can no longer offer a wizardblade a rev-25 engine will quietly refuse — and a fizzled swap finally says so in the log instead of vanishing. 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
3aef6694f3
commit
2bb2ca62d8
@@ -383,6 +383,14 @@
|
||||
attachedNumber = attachedNumber?.instanceId === card.instanceId ? null : card;
|
||||
return;
|
||||
}
|
||||
// Fully reflecting a SWAP MEET: "lets the other player decide which
|
||||
// objects, if any, will be swapped" — the choice rides the counter.
|
||||
if (youMustRespond && card.cardId === "full-reflection" &&
|
||||
view.stack?.attackCard?.cardId === "swap-meet" && view.deckRev >= 27) {
|
||||
reflectSwapCard = card;
|
||||
reflectSwapMine = null;
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
type: "counteract", instanceId: card.instanceId,
|
||||
...(attachedNumber ? { numberInstanceIds: [attachedNumber.instanceId] } : {}),
|
||||
@@ -838,11 +846,55 @@
|
||||
// hand cards are not on the table).
|
||||
let swapMeetTarget = $state<string | null>(null);
|
||||
let swapMine = $state<string | null>(null);
|
||||
// Reflecting a SWAP MEET: the reflector picks the trade — or none at all.
|
||||
let reflectSwapCard = $state<CardInstance | null>(null);
|
||||
let reflectSwapMine = $state<string | null>(null);
|
||||
const reflectMyTradables = $derived.by(() => {
|
||||
if (!view || !reflectSwapCard) return [];
|
||||
const opts = new Map<string, string>();
|
||||
for (const c of view.yourHand) {
|
||||
if (tradableHere(c.cardId)) opts.set(c.cardId, cardDef(c.cardId).name);
|
||||
}
|
||||
if (me?.carriedTreasureId) {
|
||||
const t = view.treasures.find((t) => t.id === me.carriedTreasureId);
|
||||
opts.set("treasure", `${t?.owner ?? "the"}'s treasure (carried)`);
|
||||
}
|
||||
return [...opts].map(([key, label]) => ({ key, label }));
|
||||
});
|
||||
const reflectTheirTradables = $derived.by(() => {
|
||||
if (!view || !reflectSwapCard || !view.stack) return [];
|
||||
const them = view.players.find((p) => p.id === view.stack!.attackerId);
|
||||
if (!them) return [];
|
||||
const opts = new Map<string, string>();
|
||||
for (const c of them.displayed) {
|
||||
if (tradableHere(c.cardId)) opts.set(c.cardId, cardDef(c.cardId).name);
|
||||
}
|
||||
if (them.carriedTreasureId && (reflectSwapMine === "treasure" || !me?.carriedTreasureId)) {
|
||||
const t = view.treasures.find((t) => t.id === them.carriedTreasureId);
|
||||
opts.set("treasure", `${t?.owner ?? "the"}'s treasure (carried)`);
|
||||
}
|
||||
return [...opts].map(([key, label]) => ({ key, label }));
|
||||
});
|
||||
function castReflectSwap(pair: string) {
|
||||
if (!reflectSwapCard) return;
|
||||
dispatch({
|
||||
type: "counteract", instanceId: reflectSwapCard.instanceId,
|
||||
params: { cardId: pair },
|
||||
});
|
||||
reflectSwapCard = null;
|
||||
reflectSwapMine = null;
|
||||
}
|
||||
// Older rooms' rules trade only object-typed cards (rev < 26): the picker
|
||||
// must offer exactly what that room's engine will honor.
|
||||
function tradableHere(cardId: string): boolean {
|
||||
if (!view) return false;
|
||||
return view.deckRev >= 26 ? isMovableObject(cardId) : cardDef(cardId).cardType === "object";
|
||||
}
|
||||
const myTradables = $derived.by(() => {
|
||||
if (!view) return [];
|
||||
const opts = new Map<string, string>();
|
||||
for (const c of view.yourHand) {
|
||||
if (isMovableObject(c.cardId)) opts.set(c.cardId, cardDef(c.cardId).name);
|
||||
if (tradableHere(c.cardId)) opts.set(c.cardId, cardDef(c.cardId).name);
|
||||
}
|
||||
if (me?.carriedTreasureId) {
|
||||
const t = view.treasures.find((t) => t.id === me.carriedTreasureId);
|
||||
@@ -856,7 +908,7 @@
|
||||
if (!them) return [];
|
||||
const opts = new Map<string, string>();
|
||||
for (const c of them.displayed) {
|
||||
if (isMovableObject(c.cardId)) opts.set(c.cardId, cardDef(c.cardId).name);
|
||||
if (tradableHere(c.cardId)) opts.set(c.cardId, cardDef(c.cardId).name);
|
||||
}
|
||||
// Their treasure is claimable unless your arms are already full.
|
||||
if (them.carriedTreasureId && (swapMine === "treasure" || !me?.carriedTreasureId)) {
|
||||
@@ -1460,7 +1512,21 @@
|
||||
<div class="slip winner">🏆 {view.winner} wins!</div>
|
||||
{:else if youMustRespond}
|
||||
<div class="slip urgent">
|
||||
{#if view.stack?.defenderId === view.you}
|
||||
{#if reflectSwapCard}
|
||||
Your reflection turns the trade over to you —
|
||||
{#if !reflectSwapMine}
|
||||
give your:
|
||||
{#each reflectMyTradables as c (c.key)}
|
||||
<button class="stamp tiny" onclick={() => (reflectSwapMine = c.key)}>{c.label}</button>
|
||||
{/each}
|
||||
{:else}
|
||||
and take their:
|
||||
{#each reflectTheirTradables as c (c.key)}
|
||||
<button class="stamp tiny" onclick={() => castReflectSwap(`${reflectSwapMine};${c.key}`)}>{c.label}</button>
|
||||
{/each}
|
||||
{/if}
|
||||
<button class="hint-cancel" onclick={() => castReflectSwap("none")}>swap nothing</button>
|
||||
{:else if view.stack?.defenderId === view.you}
|
||||
Under attack — respond by your hand.
|
||||
{:else}
|
||||
Your spell is countered — respond by your hand.
|
||||
|
||||
@@ -136,7 +136,7 @@ class LocalGame {
|
||||
seed,
|
||||
sets: expansion ? ["basic", "expansion1"] : ["basic"],
|
||||
...(colors ? { colors } : {}),
|
||||
deckRev: 26,
|
||||
deckRev: 27,
|
||||
};
|
||||
const { state, events } = createGame(config);
|
||||
for (const e of events) {
|
||||
|
||||
@@ -137,6 +137,7 @@ export function humanize(e: GameEvent): string | null {
|
||||
case "illusionBelieved": return e.believed ? `${e.player} flinches — the illusion feels real!` : `${e.player} laughs off the illusion.`;
|
||||
case "itemStolen": return `${e.to} picks ${e.from}'s pocket.`;
|
||||
case "itemsSwapped": return `${e.a} and ${e.b} swap items.`;
|
||||
case "swapFizzled": return `${e.player}'s trade comes to nothing — the named items were not there to swap.`;
|
||||
case "wardSprung": return `${e.owner}'s treasure was WARDED — it bites ${e.victim}!`;
|
||||
case "curseRemoved": return `${e.caster} lifts a curse from ${e.target}.`;
|
||||
case "objectEnchanted": return `An object gleams with Swarthmore's enchantment.`;
|
||||
|
||||
Reference in New Issue
Block a user