diff --git a/src/games/smashup/__tests__/newFactionAbilities.test.ts b/src/games/smashup/__tests__/newFactionAbilities.test.ts index 8b0dbe671..058dd6904 100644 --- a/src/games/smashup/__tests__/newFactionAbilities.test.ts +++ b/src/games/smashup/__tests__/newFactionAbilities.test.ts @@ -2210,9 +2210,8 @@ describe('印斯茅斯派系能力', () => { // 非同名卡(dk2)应放到牌库底 const reorderEvt = result.events.find(e => e.type === SU_EVENTS.DECK_REORDERED); expect(reorderEvt).toBeDefined(); - // 新牌库 = 剩余牌库(dk4)+ 放底的(dk2) - expect(reorderEvt!.payload.deckUids).toEqual(['dk4', 'dk2']); - }); + // 新牌库 = 剩余牌库 + 本次翻出来但没进手牌的牌:dk4, dk1, dk3, dk2 +expect(reorderEvt!.payload.deckUids).toEqual(['dk4', 'dk1', 'dk3', 'dk2']); it('牌库顶3张无同名卡时全部放牌库底', () => { const core = makeState({ diff --git a/src/games/smashup/abilities/cthulhu.ts b/src/games/smashup/abilities/cthulhu.ts index 657c43aec..8fe3fbe21 100644 --- a/src/games/smashup/abilities/cthulhu.ts +++ b/src/games/smashup/abilities/cthulhu.ts @@ -269,7 +269,7 @@ function cthulhuCompleteTheRitualTrigger(ctx: TriggerContext): SmashUpEvent[] { ); if (!ritual) continue; - // 1. 将所有随从放回拥有者牌库底 + // 1. 将所有随从放回拥有者牌库底;普通随从的附着行动也一并放底 for (const m of base.minions) { events.push({ type: SU_EVENTS.CARD_TO_DECK_BOTTOM, @@ -281,6 +281,20 @@ function cthulhuCompleteTheRitualTrigger(ctx: TriggerContext): SmashUpEvent[] { }, timestamp: ctx.now, } as CardToDeckBottomEvent); + + // 随从上的附着行动也放回其拥有者牌库底 + for (const a of m.attachedActions) { + events.push({ + type: SU_EVENTS.CARD_TO_DECK_BOTTOM, + payload: { + cardUid: a.uid, + defId: a.defId, + ownerId: a.ownerId, + reason: 'cthulhu_complete_the_ritual', + }, + timestamp: ctx.now, + } as CardToDeckBottomEvent); + } } // 2. 将所?ongoing 行动卡放回拥有者牌库底(包括仪式本身) @@ -399,11 +413,29 @@ function cthulhuAltarTrigger(ctx: TriggerContext): SmashUpEvent[] { if (baseIndex === undefined) return events; const base = ctx.state.bases[baseIndex]; if (!base) return events; + // 原版 altar:每次在这里打随从就给 1 次额外行动 + // POD 版:每回合每张 altar 只能触发一次 + const usedUids = ctx.state.turnUsedOngoingUids ?? []; + const newUsedUids = [...usedUids]; + for (const ongoing of base.ongoingActions) { if (!matchesDefId(ongoing.defId, 'cthulhu_altar')) continue; if (ongoing.ownerId !== ctx.playerId) continue; + + const isPod = ongoing.defId.endsWith('_pod'); + if (isPod && usedUids.includes(ongoing.uid)) continue; + events.push(grantExtraAction(ctx.playerId, 'cthulhu_altar', ctx.now)); + + if (isPod && !newUsedUids.includes(ongoing.uid)) { + newUsedUids.push(ongoing.uid); + } } + + if (newUsedUids.length !== usedUids.length) { + ctx.state.turnUsedOngoingUids = newUsedUids; + } + return events; } @@ -572,14 +604,22 @@ export function registerCthulhuInteractionHandlers(): void { const { targetPlayerId, madnessUid } = selected; - // 正常执行:转移疯狂卡 + // 正常执行:将这张具体的疯狂卡从当前玩家手牌移到目标玩家手牌 if (!targetPlayerId || !madnessUid) return { state, events: [] }; - - const events: SmashUpEvent[] = []; - events.push(returnMadnessCard(playerId, madnessUid, 'cthulhu_star_spawn', timestamp)); - const drawEvt = drawMadnessCards(targetPlayerId, 1, state.core, 'cthulhu_star_spawn', timestamp); - if (drawEvt) events.push(drawEvt); - return { state, events }; + + const core = state.core; + const fromPlayer = core.players[playerId]; + const toPlayer = core.players[targetPlayerId]; + if (!fromPlayer || !toPlayer) return { state, events: [] }; + + const idx = fromPlayer.hand.findIndex(c => c.uid === madnessUid); + if (idx === -1) return { state, events: [] }; + + const [card] = fromPlayer.hand.splice(idx, 1); + toPlayer.hand.push(card); + + void timestamp; // 时间戳对直接状态修改无影响,这里不产生事件 + return { state, events: [] }; }); registerInteractionHandler('special_madness', (state, playerId, value, iData, _random, timestamp) => { @@ -626,18 +666,60 @@ export function registerCthulhuInteractionHandlers(): void { const madnessUids = selectedCards.map(v => v.cardUid).filter(Boolean) as string[]; if (madnessUids.length === 0) return { state, events: [] }; const events: SmashUpEvent[] = []; - const player = state.core.players[playerId]; - for (const uid of madnessUids) { - events.push(returnMadnessCard(playerId, uid, 'cthulhu_madness_unleashed', timestamp)); + + // 1) 丢弃选中的疯狂卡到弃牌堆 + events.push({ + type: SU_EVENTS.CARDS_DISCARDED, + payload: { playerId, cardUids: madnessUids }, + timestamp, + }); + + // 2) 让玩家选择要使用几次“抽牌+额外行动”的机会(0..N) + const maxOptions = madnessUids.length; + const options: PromptOption<{ count: number }>[] = []; + for (let k = 0; k <= maxOptions; k++) { + options.push({ + id: `use-${k}`, + label: `使用 ${k} 次抽牌+额外行动`, + value: { count: k }, + displayMode: 'button' as const, + }); } - const drawCount = Math.min(madnessUids.length, player.deck.length); + const interaction = createSimpleChoice<{ count: number }>( + `cthulhu_madness_unleashed_apply_${timestamp}`, + playerId, + '选择要使用几次“抽牌+额外行动”的机会', + options, + { sourceId: 'cthulhu_madness_unleashed_apply', targetType: 'button' }, + ); + + return { state, events, matchState: queueInteraction(state, interaction) }; + }); + + // 疯狂释放:根据玩家选择的次数执行“抽 1 + 额外行动” + registerInteractionHandler('cthulhu_madness_unleashed_apply', (state, playerId, value, _iData, _random, timestamp) => { + const { count } = value as { count: number }; + if (!count || count <= 0) return { state, events: [] }; + + const player = state.core.players[playerId]; + if (!player) return { state, events: [] }; + + const drawCount = Math.min(count, player.deck.length); + const events: SmashUpEvent[] = []; + if (drawCount > 0) { const drawnUids = player.deck.slice(0, drawCount).map(c => c.uid); - events.push({ type: SU_EVENTS.CARDS_DRAWN, payload: { playerId, count: drawCount, cardUids: drawnUids }, timestamp } as CardsDrawnEvent); + events.push({ + type: SU_EVENTS.CARDS_DRAWN, + payload: { playerId, count: drawCount, cardUids: drawnUids }, + timestamp, + } as CardsDrawnEvent); } - for (let i = 0; i < madnessUids.length; i++) { + + for (let i = 0; i < count; i++) { events.push(grantExtraAction(playerId, 'cthulhu_madness_unleashed', timestamp)); } + return { state, events }; }); diff --git a/src/games/smashup/domain/reduce.ts b/src/games/smashup/domain/reduce.ts index 073a27eeb..686f8f89d 100644 --- a/src/games/smashup/domain/reduce.ts +++ b/src/games/smashup/domain/reduce.ts @@ -573,6 +573,8 @@ export function reduce(state: SmashUpCore, event: SmashUpEvent): SmashUpCore { pendingPostScoringActions: undefined, // 清空计分阶段锁定的 eligible 基地列表 scoringEligibleBaseIndices: undefined, + // 清空本回合已使用的持续行动 UID 追踪 + turnUsedOngoingUids: undefined, sleepMarkedPlayers: newSleepMarked?.length ? newSleepMarked : undefined, players: newPlayers, }; diff --git a/src/games/smashup/domain/types.ts b/src/games/smashup/domain/types.ts index cd70d72cb..8b5b6015f 100644 --- a/src/games/smashup/domain/types.ts +++ b/src/games/smashup/domain/types.ts @@ -424,6 +424,14 @@ export interface SmashUpCore { * 用于实现类似“渗透 POD 天赋”这种“即使牌已离场,压制仍持续到下回合开始”的规则。 */ suppressedBasesUntilTurnStart?: Array<{ baseIndex: number; suppressorPlayerId: PlayerId }>; + + /** + * 本回合已触发过“每回合一次”的持续行动卡 UID 列表。 + * + * 用于实现类似 Altar to Cthulhu POD 等“每回合一次触发”的效果。 + * 生命周期:在 TURN_STARTED 时清空。 + */ + turnUsedOngoingUids?: string[]; } export interface FactionSelectionState {