diff --git a/assets/images/playfield-fantasy.png b/assets/images/playfield-fantasy.png new file mode 100644 index 0000000..5ecfc41 Binary files /dev/null and b/assets/images/playfield-fantasy.png differ diff --git a/assets/images/playfield-starscape.png b/assets/images/playfield-starscape.png new file mode 100644 index 0000000..3e86962 Binary files /dev/null and b/assets/images/playfield-starscape.png differ diff --git a/data/playfields.json b/data/playfields.json index a218e65..7124781 100644 --- a/data/playfields.json +++ b/data/playfields.json @@ -14,10 +14,16 @@ "path": "assets/images/playfield-wood-cherry.png" }, { - "id": "light", - "name": "Bright Wooden Table", - "key": "playfield-light", - "path": "assets/images/playfield-wood-light.png" + "id": "stars", + "name": "Starscape", + "key": "playfield-starscape", + "path": "assets/images/playfield-starscape.png" + }, + { + "id": "fantasy", + "name": "Fantasy", + "key": "playfield-fantasy", + "path": "assets/images/playfield-fantasy.png" }, { "id": "dark", diff --git a/src/data/gamesRegistry.js b/src/data/gamesRegistry.js index 7370f1a..65421ba 100644 --- a/src/data/gamesRegistry.js +++ b/src/data/gamesRegistry.js @@ -12,6 +12,7 @@ export function registerGame(definition) { minOpponents: definition.minOpponents ?? 1, maxOpponents: definition.maxOpponents ?? 1, defaultOpponents: definition.defaultOpponents ?? null, + defaultPlayfield: definition.defaultPlayfield ?? null, hasTutorial: definition.hasTutorial ?? false, iconFrame: definition.iconFrame ?? null, }); @@ -104,4 +105,4 @@ registerGame({ slug: 'spireclimb', name: 'Spire Climb', category: 'cards', cardG registerGame({ slug: 'azul', name: 'Azul', category: 'tabletop', minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3, defaultOpponents: 3, hasTutorial: true, iconFrame: 75 }); registerGame({ slug: 'jumble', name: 'Jumble', category: 'word', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 76 }); registerGame({ slug: 'dungeonboss', name: 'Dungeon Boss', category: 'cards', cardGame: true, minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3, defaultOpponents: 3, hasTutorial: true, iconFrame: 77 }); -registerGame({ slug: 'swdbg', name: 'Star Wars', category: 'cards', cardGame: true, minPlayers: 2, maxPlayers: 2, minOpponents: 1, maxOpponents: 1, hasTutorial: true, iconFrame: 78 }); +registerGame({ slug: 'swdbg', name: 'Star Wars', category: 'cards', cardGame: true, minPlayers: 2, maxPlayers: 2, minOpponents: 1, maxOpponents: 1, hasTutorial: true, iconFrame: 78, defaultPlayfield: 'stars' }); diff --git a/src/games/swdbg/SWDBGGame.js b/src/games/swdbg/SWDBGGame.js index 23d9694..2c09372 100644 --- a/src/games/swdbg/SWDBGGame.js +++ b/src/games/swdbg/SWDBGGame.js @@ -135,6 +135,8 @@ export default class SWDBGGame extends Phaser.Scene { this._recorded = false; this._rowPos = new Map(); // uid → {x,y} this._playPos = new Map(); // uid → {x,y} + this._pendingCardUids = new Set(); // uids whose deal/refill animation hasn't landed yet + this._dealingInitial = false; // true only during the game-start deal sequence this._actionButtons = []; this.hoverTimer = null; this.hoverVisible = false; @@ -228,10 +230,19 @@ export default class SWDBGGame extends Phaser.Scene { startGame(faction) { this.gs = newGame({ humanFaction: faction }); - takeEvents(this.gs); - this.renderAll(); - this.showBanner(faction === 'empire' ? 'You command the Empire — your turn first' : 'The Empire moves first — hold the line'); - this.pump(); + const events = takeEvents(this.gs); + const oppSeat = 1 - this.humanSeat; + const oppDraws = events.filter((e) => e.type === 'draw' && e.seat === oppSeat); + const humanDraws = events.filter((e) => e.type === 'draw' && e.seat === this.humanSeat); + const rowRefills = events.filter((e) => e.type === 'rowRefill'); + const ordered = [...oppDraws, ...humanDraws, ...rowRefills]; + this._dealingInitial = true; + this.playEvents(ordered, { + onDone: () => { + this._dealingInitial = false; + this.showBanner(faction === 'empire' ? 'You command the Empire — your turn first' : 'The Empire moves first — hold the line'); + }, + }); } // ── art lookup ───────────────────────────────────────────────────────────── @@ -679,6 +690,11 @@ export default class SWDBGGame extends Phaser.Scene { seatName(seat) { return seat === this.humanSeat ? 'you' : (this.opponents[0]?.name || 'Opponent'); } + // final resting slot for opponent capital index `i` / in-play index `i` — + // shared by renderOpponent() and the opponent's card-play deal-in animation + oppCapitalSlot(i) { return { x: 660 + i * 122, y: 140, w: 108, h: 152 }; } + oppInPlaySlot(i) { return { x: 1210 + (i % 5) * 96, y: 100 + Math.floor(i / 5) * 88, w: 84, h: 118 }; } + renderOpponent() { const gs = this.gs; const seat = 1 - this.humanSeat; @@ -713,21 +729,23 @@ export default class SWDBGGame extends Phaser.Scene { }).setOrigin(0.5).setAngle(-6)); } this._oppBasePos = { x: 392, y: 140 }; + this.renderOpponentPiles(570, p); // capitals p.capitals.forEach((e, i) => { - const x = 620 + i * 122; - this._playPos.set(e.card.uid, { x, y: 140 }); - this.makeCard(x, 140, e.card, 108, 152, { + const slot = this.oppCapitalSlot(i); + this._playPos.set(e.card.uid, { x: slot.x, y: slot.y }); + if (this._pendingCardUids.has(e.card.uid)) return; + this.makeCard(slot.x, slot.y, e.card, slot.w, slot.h, { parent: this.boardLayer, hover: false, damage: e.damage, onClick: () => this.showInspect(e.card), }); }); // opponent cards in play (their turn) p.inPlay.forEach((e, i) => { - const x = 1210 + (i % 5) * 96; - const y = 100 + Math.floor(i / 5) * 88; - this._playPos.set(e.card.uid, { x, y }); - this.makeCard(x, y, e.card, 84, 118, { + const slot = this.oppInPlaySlot(i); + this._playPos.set(e.card.uid, { x: slot.x, y: slot.y }); + if (this._pendingCardUids.has(e.card.uid)) return; + this.makeCard(slot.x, slot.y, e.card, slot.w, slot.h, { parent: this.boardLayer, hover: false, committed: e.committed, attackNow: entryAttack(gs, seat, e), onClick: () => this.showInspect(e.card), @@ -740,11 +758,11 @@ export default class SWDBGGame extends Phaser.Scene { `Discard ${p.discard.length}`, `Resources ${p.resources}`, ]; - this.boardLayer.add(this.add.text(cx, 70, lines.join('\n'), { + this.boardLayer.add(this.add.text(cx, 95, lines.join('\n'), { fontFamily: '"Julius Sans One"', fontSize: '16px', color: C.muted, lineSpacing: 7, })); // bases remaining / lost - this.renderBasePips(cx, 168, p, info); + this.renderBasePips(cx, 193, p, info); if (gs.turnSeat === seat && !gs.over) { this.boardLayer.add(this.add.text(GAME_WIDTH / 2, 48, '— THEIR TURN —', { fontFamily: 'Righteous', fontSize: '15px', color: info.colorHex, @@ -752,6 +770,38 @@ export default class SWDBGGame extends Phaser.Scene { } } + // small face-down hand fan + draw/discard pile icons, shown just right of the + // portrait so pile sizes are visible at a glance without opening any panel + renderOpponentPiles(x, p) { + const pw = 30, ph = 42; + const pendingInHand = p.hand.filter((c) => this._pendingCardUids.has(c.uid)).length; + const visibleCount = p.hand.length - pendingInHand; + const shown = Math.min(visibleCount, 4); + const fanY = 72; + for (let i = 0; i < shown; i++) { + const off = i - (shown - 1) / 2; + this.makeCardBack(x + off * 7, fanY, pw, ph, this.boardLayer).setAngle(off * 7); + } + this.boardLayer.add(this.add.text(x, fanY + ph / 2 + 11, `HAND ${visibleCount}`, { + fontFamily: '"Julius Sans One"', fontSize: '11px', color: C.muted, + }).setOrigin(0.5)); + const pile = (y, count, label) => { + if (count > 0) { + this.makeCardBack(x, y, pw, ph, this.boardLayer); + } else { + const g = this.add.graphics(); + g.lineStyle(2, 0x2a3c66, 0.6); + g.strokeRoundedRect(x - pw / 2, y - ph / 2, pw, ph, 6); + this.boardLayer.add(g); + } + this.boardLayer.add(this.add.text(x, y + ph / 2 + 11, `${label} ${count}`, { + fontFamily: '"Julius Sans One"', fontSize: '11px', color: C.muted, + }).setOrigin(0.5)); + }; + pile(132, p.deck.length, 'DRAW'); + pile(192, p.discard.length, 'DISCARD'); + } + renderBasePips(x, y, p, info) { this.boardLayer.add(this.add.text(x, y - 22, `Bases lost ${p.lostBases}/${this.gs.meta.basesToWin}`, { fontFamily: '"Julius Sans One"', fontSize: '15px', color: C.muted, @@ -766,6 +816,14 @@ export default class SWDBGGame extends Phaser.Scene { } } + // final resting slot for row index `i` given a card's faction — shared by + // renderGalaxy() and the row-refill deal-in animation + galaxyRowSlot(i, faction, humanFaction) { + const rowX0 = 510, cw = 158, ch = 222, y = 430; + const rowNudge = faction === 'neutral' ? 0 : faction === humanFaction ? 12 : -12; + return { x: rowX0 + i * (cw + 14), y: y + rowNudge, w: cw, h: ch }; + } + // ── galaxy band ───────────────────────────────────────────────────────────── renderGalaxy() { const gs = this.gs; @@ -790,17 +848,14 @@ export default class SWDBGGame extends Phaser.Scene { const humanFaction = gs.players[this.humanSeat].faction; gs.galaxy.row.forEach((card, i) => { const def = cardDef(card); - const x = rowX0 + i * (cw + 14); - // nudge cards vertically to hint at ownership: down for the player's own - // faction (easier to buy), up for the opponent's faction, untouched if neutral - const rowNudge = def.faction === 'neutral' ? 0 : def.faction === humanFaction ? 12 : -12; - const cy = y + rowNudge; - this._rowPos.set(card.uid, { x, y: cy }); + const slot = this.galaxyRowSlot(i, def.faction, humanFaction); + this._rowPos.set(card.uid, { x: slot.x, y: slot.y }); + if (this._pendingCardUids.has(card.uid)) return; const buyable = !this.squad.size && legal && legal.buys.some((b) => b.uid === card.uid); const targetable = this.squad.size > 0 && this.rowTargetReachable(card); const freeTarget = this.mode.type === 'target' && ['discardRow', 'freePurchase', 'destroyCapital'].includes(this.mode.decision?.op) && (this.mode.candidates || []).some((c) => c.uid === card.uid); - this.makeCard(x, cy, card, cw, ch, { + this.makeCard(slot.x, slot.y, card, slot.w, slot.h, { parent: this.rowLayer, showText: true, highlightBuy: buyable, highlightTarget: targetable, highlightGold: freeTarget, onClick: () => this.onRowClicked(card), @@ -856,6 +911,17 @@ export default class SWDBGGame extends Phaser.Scene { } // ── human area ────────────────────────────────────────────────────────────── + // final resting slot for the human's own capital index `i` / in-play index + // `i` of a row with `unitsLength` cards — shared by renderHuman() and the + // human's own card-play deal-in animation + humanCapitalSlot(i) { return { x: 620 + i * 128, y: 730, w: 116, h: 162 }; } + humanInPlaySlot(i, unitsLength) { + const uw = 122, uh = 170; + const pitch = Math.min(uw + 10, 620 / Math.max(1, unitsLength)); + const x0 = 1180 - ((unitsLength - 1) * pitch) / 2; + return { x: x0 + i * pitch, y: 730, w: uw, h: uh }; + } + renderHuman() { const gs = this.gs; const seat = this.humanSeat; @@ -891,38 +957,35 @@ export default class SWDBGGame extends Phaser.Scene { // capitals p.capitals.forEach((e, i) => { - const x = 620 + i * 128; - const y = playRowY; - this._playPos.set(e.card.uid, { x, y }); + const slot = this.humanCapitalSlot(i); + this._playPos.set(e.card.uid, { x: slot.x, y: slot.y }); + if (this._pendingCardUids.has(e.card.uid)) return; const canUse = legal && legal.abilities.some((a) => a.uid === e.card.uid); const selectable = this.myTurnMode() && !e.committed && entryAttack(gs, seat, e) > 0; - this.makeCard(x, y, e.card, 116, 162, { + this.makeCard(slot.x, slot.y, e.card, slot.w, slot.h, { parent: this.boardLayer, hover: false, damage: e.damage, selected: this.squad.has(e.card.uid), committed: e.committed, attackNow: entryAttack(gs, seat, e), onClick: () => selectable ? this.toggleSquad(e.card.uid) : this.showInspect(e.card), }); - if (canUse) this.addUsePill(x, this.usePillY(y, 162), () => this.beginAbility({ zone: 'capital', uid: e.card.uid })); + if (canUse) this.addUsePill(slot.x, this.usePillY(slot.y, slot.h), () => this.beginAbility({ zone: 'capital', uid: e.card.uid })); }); // units in play const units = p.inPlay; - const uw = 122, uh = 170; - const pitch = Math.min(uw + 10, 620 / Math.max(1, units.length)); - const x0 = 1180 - ((units.length - 1) * pitch) / 2; units.forEach((e, i) => { - const x = x0 + i * pitch; - const y = playRowY; - this._playPos.set(e.card.uid, { x, y }); + const slot = this.humanInPlaySlot(i, units.length); + this._playPos.set(e.card.uid, { x: slot.x, y: slot.y }); + if (this._pendingCardUids.has(e.card.uid)) return; const canUse = legal && legal.abilities.some((a) => a.uid === e.card.uid); const selectable = this.myTurnMode() && !e.committed && entryAttack(gs, seat, e) > 0; - this.makeCard(x, y, e.card, uw, uh, { + this.makeCard(slot.x, slot.y, e.card, slot.w, slot.h, { parent: this.boardLayer, hover: false, selected: this.squad.has(e.card.uid), committed: e.committed, attackNow: entryAttack(gs, seat, e), onClick: () => selectable ? this.toggleSquad(e.card.uid) : this.showInspect(e.card), }); - if (canUse) this.addUsePill(x, this.usePillY(y, uh), () => this.beginAbility({ zone: 'play', uid: e.card.uid })); + if (canUse) this.addUsePill(slot.x, this.usePillY(slot.y, slot.h), () => this.beginAbility({ zone: 'play', uid: e.card.uid })); }); if (!units.length && !p.capitals.length) { this.boardLayer.add(this.add.text(1180, playRowY, 'cards you play land here', { @@ -974,19 +1037,31 @@ export default class SWDBGGame extends Phaser.Scene { const pitch = Math.min(cw + 8, 1250 / Math.max(1, p.hand.length)); let x = GAME_WIDTH / 2 - ((p.hand.length - 1) * pitch) / 2; for (const inst of p.hand) { + const cardX = x; + x += pitch; + if (this._pendingCardUids.has(inst.uid)) continue; const playable = this.myTurnMode(); const discardable = this.mode.type === 'oppDiscard' || (this.mode.type === 'target' && this.mode.decision?.op === 'exileCards' && (this.mode.candidates || []).some((c) => c.zone === 'hand' && c.uid === inst.uid)); - this.makeCard(x, y, inst, cw, chh, { + this.makeCard(cardX, y, inst, cw, chh, { parent: this.handLayer, showText: true, highlightGold: discardable, onClick: () => this.onHandClicked(inst), }); if (!playable && !discardable) this.handLayer.list[this.handLayer.list.length - 1].setAlpha(0.9); - x += pitch; } } + // final resting x for hand slot `index` of a hand with `handLength` cards — + // shared by renderHand() and the deal-in animation so a card's flight target + // always matches where it will actually land + humanHandSlotX(index, handLength) { + const cw = 148; + const pitch = Math.min(cw + 8, 1250 / Math.max(1, handLength)); + const x0 = GAME_WIDTH / 2 - ((handLength - 1) * pitch) / 2; + return x0 + index * pitch; + } + // small draw/discard pile shown beside the human hand: card back + count when // non-empty, dashed outline placeholder when empty renderHandPile(x, y, count, label) { @@ -1262,9 +1337,22 @@ export default class SWDBGGame extends Phaser.Scene { b.setDepth(DEPTH.overlay + 1); this._modalButtons = this._modalButtons || []; this._modalButtons.push(b); + this.guardFreshButton(b); return b; } + // Button's onClick fires on 'pointerup', while everything else in this game + // (cards, the USE pill) fires on 'pointerdown'. When a popup opens + // synchronously from a pointerdown handler (e.g. clicking "USE") while the + // mouse is still physically held down, that same click's eventual release + // would otherwise land on a freshly-created popup button and fire it + // immediately. Swallow just that one pending release before arming it. + guardFreshButton(b) { + if (!this.input.activePointer.isDown) return; + b.input.enabled = false; + this.input.once('pointerup', () => { if (b.input) b.input.enabled = true; }); + } + showBaseChoiceModal(d) { const root = this.modalRoot(); const p = this.gs.players[this.humanSeat]; @@ -1447,6 +1535,7 @@ export default class SWDBGGame extends Phaser.Scene { const b = new Button(this, 1700, 700 + i * 76, label, cb, { width: 190, fontSize: 18 }); b.setDepth(DEPTH.ui); this._actionButtons.push(b); + this.guardFreshButton(b); } clearActionButtons() { for (const b of this._actionButtons) b.destroy(); @@ -1454,9 +1543,12 @@ export default class SWDBGGame extends Phaser.Scene { } // ── event playback ────────────────────────────────────────────────────────── - playEvents(events) { + playEvents(events, opts = {}) { this.busy = true; this.setPrompt(''); + for (const e of events) { + if ((e.type === 'draw' || e.type === 'rowRefill' || e.type === 'play') && e.uid != null) this._pendingCardUids.add(e.uid); + } const queue = events.filter((e) => this.eventDelay(e) > 0); const step = () => { const e = queue.shift(); @@ -1464,6 +1556,7 @@ export default class SWDBGGame extends Phaser.Scene { this.busy = false; this.renderAll(); this.pump(); + opts.onDone?.(); return; } this.eventFx(e); @@ -1477,7 +1570,7 @@ export default class SWDBGGame extends Phaser.Scene { const aiEvent = e.seat != null && e.seat !== this.humanSeat; switch (e.type) { case 'turnStart': return 460; - case 'play': return aiEvent ? 300 : 140; + case 'play': return aiEvent ? 300 : 220; case 'buy': return 420; case 'bounty': return 640; case 'attackBase': return 380; @@ -1495,10 +1588,131 @@ export default class SWDBGGame extends Phaser.Scene { case 'repair': return 300; case 'rowDiscard': return 340; case 'galaxyReshuffle': return 300; + case 'draw': return this._dealingInitial ? 300 : 260; + case 'rowRefill': return this._dealingInitial ? 320 : 280; default: return 0; } } + // fly + resize + optional mid-flight flip, used for dealing a card from a + // pile (draw pile / galaxy deck) to its final hand/row slot. The flip runs + // inside a nested container so its scaleX collapse doesn't fight the outer + // tween's own position/resize scale, and is timed to finish with margin + // before the outer tween's onComplete destroys everything. + dealCardAnimated(opts) { + const { + fromX, fromY, fromW, fromH, + toX, toY, toW, toH, + toAngle = 0, + flipToId = null, // card def id to reveal mid-flight; null = stays a card-back + faceId = null, // card def id to show face-up for the whole flight (no back stage at all) + duration = 280, + onLand, + } = opts; + const outer = this.add.container(fromX, fromY); + outer.setScale(fromW / toW, fromH / toH); + this.fxLayer.add(outer); + const flipHost = this.add.container(0, 0); + outer.add(flipHost); + const back = faceId != null + ? this.makeCard(0, 0, { uid: -1, id: faceId }, toW, toH, { parent: flipHost, hover: false, showText: true, noHoverPreview: true }) + : this.makeCardBack(0, 0, toW, toH, flipHost); + + this.tweens.add({ + targets: outer, x: toX, y: toY, scaleX: 1, scaleY: 1, angle: toAngle, + duration, ease: 'Cubic.easeInOut', + onComplete: () => { outer.destroy(); onLand?.(); }, + }); + + if (flipToId != null) { + this.tweens.add({ + targets: flipHost, scaleX: 0, duration: duration * 0.35, delay: duration * 0.15, + ease: 'Cubic.easeIn', + onComplete: () => { + back.destroy(); + this.makeCard(0, 0, { uid: -1, id: flipToId }, toW, toH, { + parent: flipHost, hover: false, showText: true, noHoverPreview: true, + }); + this.tweens.add({ targets: flipHost, scaleX: 1, duration: duration * 0.35, ease: 'Cubic.easeOut' }); + }, + }); + } + } + + animateDraw(e) { + const p = this.gs.players[e.seat]; + const idx = p.hand.findIndex((c) => c.uid === e.uid); + if (idx < 0) { this._pendingCardUids.delete(e.uid); return; } + const dur = this._dealingInitial ? 300 : 260; + if (e.seat === this.humanSeat) { + this.dealCardAnimated({ + fromX: 392, fromY: 911, fromW: 90, fromH: 124, + toX: this.humanHandSlotX(idx, p.hand.length), toY: 985, toW: 148, toH: 206, + flipToId: e.id, duration: dur, + onLand: () => { this._pendingCardUids.delete(e.uid); this.renderAll(); }, + }); + } else { + const remainingPending = p.hand.filter((c) => this._pendingCardUids.has(c.uid) && c.uid !== e.uid).length; + const visibleAfter = p.hand.length - remainingPending; + const shown = Math.min(visibleAfter, 4); + const off = (shown - 1) / 2; // land in the rightmost fan slot — backs are interchangeable + this.dealCardAnimated({ + fromX: 570, fromY: 132, fromW: 30, fromH: 42, + toX: 570 + off * 7, toY: 72, toW: 30, toH: 42, toAngle: off * 7, + flipToId: null, duration: dur, + onLand: () => { this._pendingCardUids.delete(e.uid); this.renderAll(); }, + }); + } + } + + // opponent's hand is only ever shown as a generic face-down fan, so a played + // card animates out of that fan (reveal-flip included, since playing it is + // exactly the moment it stops being secret) into its capital/in-play slot + animatePlay(e) { + const p = this.gs.players[e.seat]; + const pool = e.capital ? p.capitals : p.inPlay; + const idx = pool.findIndex((entry) => entry.card.uid === e.uid); + if (idx < 0) { this._pendingCardUids.delete(e.uid); return; } + const slot = e.capital ? this.oppCapitalSlot(idx) : this.oppInPlaySlot(idx); + this.dealCardAnimated({ + fromX: 570, fromY: 72, fromW: 30, fromH: 42, + toX: slot.x, toY: slot.y, toW: slot.w, toH: slot.h, + flipToId: e.id, duration: 300, + onLand: () => { this._pendingCardUids.delete(e.uid); this.renderAll(); }, + }); + } + + // the human already knows their own card (they just clicked it), so it + // flies already face-up — no back stage, no flip — from the hand strip + // into its capital/in-play slot + animateHumanPlay(e) { + const p = this.gs.players[e.seat]; + const pool = e.capital ? p.capitals : p.inPlay; + const idx = pool.findIndex((entry) => entry.card.uid === e.uid); + if (idx < 0) { this._pendingCardUids.delete(e.uid); return; } + const slot = e.capital ? this.humanCapitalSlot(idx) : this.humanInPlaySlot(idx, pool.length); + this.dealCardAnimated({ + fromX: GAME_WIDTH / 2, fromY: 985, fromW: 60, fromH: 84, + toX: slot.x, toY: slot.y, toW: slot.w, toH: slot.h, + faceId: e.id, duration: 220, + onLand: () => { this._pendingCardUids.delete(e.uid); this.renderAll(); }, + }); + } + + animateRowRefill(e) { + const row = this.gs.galaxy.row; + const idx = row.findIndex((c) => c.uid === e.uid); + if (idx < 0) { this._pendingCardUids.delete(e.uid); return; } + const humanFaction = this.gs.players[this.humanSeat].faction; + const slot = this.galaxyRowSlot(idx, cardDef(e.id).faction, humanFaction); + this.dealCardAnimated({ + fromX: 330, fromY: 430, fromW: 158 * 0.9, fromH: 222 * 0.9, + toX: slot.x, toY: slot.y, toW: slot.w, toH: slot.h, + flipToId: e.id, duration: this._dealingInitial ? 320 : 280, + onLand: () => { this._pendingCardUids.delete(e.uid); this.renderAll(); }, + }); + } + eventFx(e) { switch (e.type) { case 'turnStart': @@ -1508,6 +1722,8 @@ export default class SWDBGGame extends Phaser.Scene { break; case 'play': this.sfx(SFX.CARD_PLACE); + if (e.seat !== this.humanSeat) this.animatePlay(e); + else this.animateHumanPlay(e); break; case 'buy': { this.sfx(SFX.PURCHASE); @@ -1599,6 +1815,14 @@ export default class SWDBGGame extends Phaser.Scene { case 'galaxyReshuffle': this.sfx(SFX.CARD_SHUFFLE); break; + case 'draw': + this.sfx(SFX.CARD_DEAL); + this.animateDraw(e); + break; + case 'rowRefill': + this.sfx(SFX.CARD_DEAL); + this.animateRowRefill(e); + break; default: break; } } diff --git a/src/games/swdbg/SWDBGLogic.js b/src/games/swdbg/SWDBGLogic.js index 959f364..4a66790 100644 --- a/src/games/swdbg/SWDBGLogic.js +++ b/src/games/swdbg/SWDBGLogic.js @@ -114,8 +114,9 @@ function drawCards(state, seat, n) { p.discard = []; emit(state, { type: 'reshuffle', seat }); } - p.hand.push(p.deck.pop()); - emit(state, { type: 'draw', seat }); + const card = p.deck.pop(); + p.hand.push(card); + emit(state, { type: 'draw', seat, uid: card.uid, id: card.id }); } } diff --git a/src/scenes/OpponentSelectScene.js b/src/scenes/OpponentSelectScene.js index c8d2803..1ffa67f 100644 --- a/src/scenes/OpponentSelectScene.js +++ b/src/scenes/OpponentSelectScene.js @@ -164,7 +164,7 @@ export default class OpponentSelectScene extends Phaser.Scene { // Apply playfield default; card back is chosen randomly if (!isWordGame && this.gameDef.slug !== 'battleship' && this.gameDef.slug !== 'mastermind') { const pfd = this.cache.json.get('playfields') ?? {}; - this.applyDefault('playfields', pfd.default, 'selectedPlayfield', 'playfieldTiles'); + this.applyDefault('playfields', this.gameDef.defaultPlayfield ?? pfd.default, 'selectedPlayfield', 'playfieldTiles'); if (this.selectedPlayfield?.type === 'colored' && this._colorDropdownDomEl) { this._colorDropdownDomEl.node.style.display = 'block'; }