Improve trade modal layout and card draw animations
- Make trade modal panel height dynamic; wrap hand cards to second row if >4 - Refactor card fly animations to use Containers for correct border scaling - Animate AI partner card placement to HUD thumbnails - Add Waters Rise animation: card flies to meter, triggers water rise with segment fade and marker slide
This commit is contained in:
parent
49761bf264
commit
9d8366ac5b
|
|
@ -911,14 +911,20 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
if (this.tradeModalObjs.length) return; // already open
|
||||
const reg = (o) => { this.tradeModalObjs.push(o); return o; };
|
||||
|
||||
const panelX = 160, panelY = 330, panelW = 1600, panelH = 400;
|
||||
const panelX = 160, panelY = 310, panelW = 1600;
|
||||
const cx = GAME_WIDTH / 2;
|
||||
const portR = 36;
|
||||
const portCY = panelY + 60;
|
||||
// Push portrait below the title/instruction block (~panelY+75) with a clear gap
|
||||
const portCY = panelY + 130;
|
||||
const nameY = portCY + portR + 14;
|
||||
const roleY = nameY + 22;
|
||||
const cardsTop = roleY + 30;
|
||||
const cardW = 70, cardH = 95, cardGap = 6;
|
||||
const CARDS_PER_ROW = 4;
|
||||
const hasSecondRow = this.gs.players.some((p) => p.hand.length > CARDS_PER_ROW);
|
||||
// Panel height: one row of cards, or two if any hand overflows
|
||||
const cardsAreaH = hasSecondRow ? cardH * 2 + cardGap : cardH;
|
||||
const panelH = (cardsTop - panelY) + cardsAreaH + 24;
|
||||
const hasArt = this.textures.exists('forbiddenisland-cards');
|
||||
|
||||
const N = this.gs.players.length;
|
||||
|
|
@ -1003,15 +1009,17 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
fontFamily: '"Julius Sans One"', fontSize: '12px', color: role.colorHex,
|
||||
}).setDepth(DEPTH.popup + 3));
|
||||
|
||||
// Hand cards
|
||||
// Hand cards (wrap to second row after CARDS_PER_ROW)
|
||||
if (!player.hand.length) {
|
||||
reg(this.add.text(textX, cardsTop + cardH / 2, 'No cards', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '11px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0, 0.5).setDepth(DEPTH.popup + 3));
|
||||
} else {
|
||||
player.hand.forEach((card, ci) => {
|
||||
const wx = textX + ci * (cardW + cardGap) + cardW / 2;
|
||||
const wy = cardsTop + cardH / 2;
|
||||
const row = ci < CARDS_PER_ROW ? 0 : 1;
|
||||
const colIdx = ci < CARDS_PER_ROW ? ci : ci - CARDS_PER_ROW;
|
||||
const wx = textX + colIdx * (cardW + cardGap) + cardW / 2;
|
||||
const wy = cardsTop + row * (cardH + cardGap) + cardH / 2;
|
||||
const frame = hasArt ? cardFrame(card) : null;
|
||||
|
||||
let cardObj;
|
||||
|
|
@ -1436,72 +1444,103 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
onComplete: () => {
|
||||
back.destroy();
|
||||
|
||||
// Create face-up card at center
|
||||
let faceObj;
|
||||
// Create face-up card Container (image/vector + border) at center.
|
||||
// Using a Container means the border travels and scales with the card
|
||||
// for all three fly destinations (human hand, AI HUD, chat fade).
|
||||
let faceContent;
|
||||
if (frame != null) {
|
||||
faceObj = this.add.image(cx, cy, 'forbiddenisland-cards', frame)
|
||||
.setScale(SX_DRAW, SY_DRAW)
|
||||
.setDepth(depth);
|
||||
const img = this.add.image(0, 0, 'forbiddenisland-cards', frame).setScale(SX_DRAW, SY_DRAW);
|
||||
const borderGfx = this.add.graphics();
|
||||
borderGfx.lineStyle(3, 0xd4c08a, 1);
|
||||
borderGfx.strokeRoundedRect(-DRAW_W / 2, -DRAW_H / 2, DRAW_W, DRAW_H, 12);
|
||||
faceContent = [img, borderGfx];
|
||||
} else {
|
||||
// Vector fallback
|
||||
const info = cardInfo(card);
|
||||
const g = this.add.graphics().setDepth(depth);
|
||||
const g = this.add.graphics();
|
||||
g.fillStyle(info.color, 1);
|
||||
g.fillRoundedRect(-DRAW_W / 2, -DRAW_H / 2, DRAW_W, DRAW_H, 16);
|
||||
g.lineStyle(3, 0xffffff, 0.6);
|
||||
g.lineStyle(3, 0xd4c08a, 1);
|
||||
g.strokeRoundedRect(-DRAW_W / 2, -DRAW_H / 2, DRAW_W, DRAW_H, 16);
|
||||
this.add.text(cx, cy, info.label, {
|
||||
const lbl = this.add.text(0, 0, info.label, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '28px', color: info.text, align: 'center',
|
||||
}).setOrigin(0.5).setDepth(depth + 1);
|
||||
faceObj = g;
|
||||
}).setOrigin(0.5);
|
||||
faceContent = [g, lbl];
|
||||
}
|
||||
const faceObj = this.add.container(cx, cy, faceContent).setDepth(depth);
|
||||
|
||||
faceObj.scaleX = 0;
|
||||
if (this.cache.audio.exists('sfx-card-show')) this.sound.play('sfx-card-show', { volume: 0.35 });
|
||||
|
||||
// Flip open
|
||||
// Flip open — container collapses to scaleX=0 then reopens to 1
|
||||
this.tweens.add({
|
||||
targets: faceObj,
|
||||
scaleX: frame != null ? SX_DRAW : 1,
|
||||
scaleX: 1,
|
||||
duration: 150,
|
||||
ease: 'Linear',
|
||||
onComplete: () => {
|
||||
// 4. Pause 1.2 seconds
|
||||
this.time.delayedCall(1200, () => {
|
||||
if (isHuman && handSlot >= 0) {
|
||||
// 5a. Human player — fly to hand slot, keep image visible until renderHand replaces it
|
||||
// 5a. Human player — fly to hand slot; park container so renderHand can destroy it
|
||||
const tx = RAIL_X + 8 + handSlot * (HAND_W + 10) + HAND_W / 2;
|
||||
const ty = 884 + HAND_H / 2;
|
||||
// Container scale needed to render at HAND_W × HAND_H from DRAW_W × DRAW_H content
|
||||
const csX = HAND_W / DRAW_W, csY = HAND_H / DRAW_H;
|
||||
this.tweens.add({
|
||||
targets: faceObj,
|
||||
x: tx, y: ty,
|
||||
scaleX: SX_HAND,
|
||||
scaleY: SY_HAND,
|
||||
scaleX: csX, scaleY: csY,
|
||||
duration: 350,
|
||||
ease: 'Cubic.easeIn',
|
||||
onComplete: () => {
|
||||
if (this.cache.audio.exists('sfx-card-place')) this.sound.play('sfx-card-place', { volume: 0.3 });
|
||||
// Park the image at the hand slot; renderHand will destroy it when re-rendering
|
||||
this.tempHandImages.push(faceObj);
|
||||
onDone();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 5b. AI player or Waters Rise — shrink and fade toward chat panel
|
||||
const chatCX = RAIL_X + RAIL_W / 2;
|
||||
const chatCY = 248 + 235;
|
||||
} else if (!isHuman && handSlot >= 0 && this.partnerCardSlots[seat]) {
|
||||
// 5b. AI player drawing a hand card — fly to partner HUD thumbnail slot
|
||||
const slot = this.partnerCardSlots[seat];
|
||||
const HUD_W = 24, HUD_H = 33, HUD_GAP = 3;
|
||||
const tx = slot.cardX + handSlot * (HUD_W + HUD_GAP) + HUD_W / 2;
|
||||
const ty = slot.cardY + HUD_H / 2;
|
||||
// Container scale to render at HUD thumbnail size from DRAW content size
|
||||
const csX = HUD_W / DRAW_W, csY = HUD_H / DRAW_H;
|
||||
this.tweens.add({
|
||||
targets: faceObj,
|
||||
x: chatCX, y: chatCY,
|
||||
scaleX: 0.05, scaleY: 0.05,
|
||||
alpha: 0,
|
||||
x: tx, y: ty,
|
||||
scaleX: csX, scaleY: csY,
|
||||
duration: 380,
|
||||
ease: 'Cubic.easeIn',
|
||||
onComplete: () => {
|
||||
if (this.cache.audio.exists('sfx-card-place')) this.sound.play('sfx-card-place', { volume: 0.2 });
|
||||
faceObj.destroy();
|
||||
onDone();
|
||||
},
|
||||
});
|
||||
} else if (card === SPECIAL.WATERS_RISE && this.waterX != null) {
|
||||
// 5c. Waters Rise — card flies to the next unfilled meter segment, then level rises
|
||||
const oldLevel = this.gs.waterLevel;
|
||||
const newLevel = Math.min(oldLevel + 1, MAX_WATER);
|
||||
const mSegH = (this.waterBottom - this.waterTop) / MAX_WATER;
|
||||
const targetX = this.waterX;
|
||||
const targetY = this.waterTop + (MAX_WATER - newLevel) * mSegH + mSegH / 2;
|
||||
this.tweens.add({
|
||||
targets: faceObj,
|
||||
x: targetX, y: targetY,
|
||||
scaleX: 0.12, scaleY: 0.08,
|
||||
duration: 500,
|
||||
ease: 'Cubic.easeIn',
|
||||
onComplete: () => {
|
||||
faceObj.destroy();
|
||||
this.animateWaterRise(oldLevel, onDone);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Fallback — destroy and continue
|
||||
faceObj.destroy();
|
||||
onDone();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -1512,6 +1551,53 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
});
|
||||
}
|
||||
|
||||
// Animate the water level rising: fade in the new segment, slide the triangle marker up.
|
||||
animateWaterRise(oldLevel, onDone) {
|
||||
const D = DEPTH.ui + 2;
|
||||
const newLevel = Math.min(oldLevel + 1, MAX_WATER);
|
||||
const segH = (this.waterBottom - this.waterTop) / MAX_WATER;
|
||||
const w = 40, barX = this.waterX - w / 2;
|
||||
|
||||
const segColor = newLevel >= MAX_WATER ? 0xb22a2a
|
||||
: newLevel >= 8 ? 0xd1632f
|
||||
: 0x2f8fd0;
|
||||
|
||||
// New-level segment overlay fades in from transparent
|
||||
const newSegY = this.waterTop + (MAX_WATER - newLevel) * segH;
|
||||
const segG = this.add.graphics().setDepth(D);
|
||||
segG.fillStyle(segColor, 1);
|
||||
segG.fillRoundedRect(barX, newSegY + 2, w, segH - 4, 6);
|
||||
segG.lineStyle(1, 0x000000, 0.4);
|
||||
segG.strokeRoundedRect(barX, newSegY + 2, w, segH - 4, 6);
|
||||
segG.alpha = 0;
|
||||
|
||||
// Gold triangle marker slides from old-level center to new-level center
|
||||
const oldMarkerY = this.waterTop + (MAX_WATER - oldLevel) * segH + segH / 2;
|
||||
const newMarkerY = this.waterTop + (MAX_WATER - newLevel) * segH + segH / 2;
|
||||
const markG = this.add.graphics().setDepth(D + 1);
|
||||
markG.fillStyle(COLORS.gold, 1);
|
||||
markG.fillTriangle(-12, -8, -12, 8, -2, 0);
|
||||
markG.x = barX;
|
||||
markG.y = oldMarkerY;
|
||||
|
||||
if (this.cache.audio.exists('sfx-card-place')) this.sound.play('sfx-card-place', { volume: 0.5 });
|
||||
|
||||
// Marker slides up quickly, segment fades in slowly
|
||||
this.tweens.add({
|
||||
targets: markG, y: newMarkerY, duration: 600, ease: 'Cubic.easeOut',
|
||||
onComplete: () => markG.destroy(),
|
||||
});
|
||||
this.tweens.add({
|
||||
targets: segG, alpha: 1, duration: 900, ease: 'Cubic.easeOut',
|
||||
onComplete: () => {
|
||||
// Call onDone first (triggers endActions + render → drawWaterMeter redraws the
|
||||
// filled segment on waterG), then destroy the overlay so there's no flash.
|
||||
onDone();
|
||||
segG.destroy();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
aiTurn(seat) {
|
||||
this.busy = true;
|
||||
// Announce the plan.
|
||||
|
|
|
|||
Loading…
Reference in New Issue