refactor(forbidden-island): replace global priorities with per-player direction system
Replace the single global priority buttons (focus treasure, regroup, defend) with per-player direction assignments accessible via clickable pills on each player's HUD. Each player can now be directed to: Auto, focus a specific treasure (Earth/Wind/Fire/Ocean), Regroup, or Defend Temples. - Remove old priority toggle buttons and renderPriorities() - Add direction pills on partner HUDs and portrait panels - Implement dropdown menu for selecting directions - Convert direction choices to priority objects for AI execution - Increase chat panel height to accommodate new layout - Add click consumption guard on trade modal trigger
This commit is contained in:
parent
41818f7a4a
commit
ea135b35ed
|
|
@ -65,6 +65,11 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
this.tradeModalObjs = []; // all objects in the trade modal
|
||||
this.tradeHighlightObjs = []; // highlight borders for selected card
|
||||
this.tradeSelection = null; // { seat, cardIdx, card, frame, worldX, worldY }
|
||||
this.seatDirections = {}; // seat → null|'earth'|'wind'|'fire'|'ocean'|'regroup'|'defend'
|
||||
this.directionDropdown = null; // shared popup container (one at a time)
|
||||
this.directionDismiss = null; // full-screen dismiss zone behind dropdown
|
||||
this.hudPillTexts = {}; // seat → HUD pill Phaser.Text for live label updates
|
||||
this._dirClickConsumed = false;
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
@ -225,7 +230,7 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
});
|
||||
|
||||
// Chat panel
|
||||
const chatY = 248, chatH = 470;
|
||||
const chatY = 248, chatH = 548;
|
||||
const cp = this.add.graphics().setDepth(DEPTH.ui);
|
||||
cp.fillStyle(0x000000, 0.4); cp.fillRoundedRect(RAIL_X, chatY, RAIL_W, chatH, 12);
|
||||
cp.lineStyle(2, COLORS.accent, 0.5); cp.strokeRoundedRect(RAIL_X, chatY, RAIL_W, chatH, 12);
|
||||
|
|
@ -236,24 +241,6 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
wordWrap: { width: this.chatBox.w }, lineSpacing: 4,
|
||||
}).setDepth(DEPTH.ui);
|
||||
|
||||
// Priority buttons
|
||||
const py = 740;
|
||||
this.add.text(RAIL_X, py - 24, 'DIRECT THE TEAM', { fontFamily: 'Righteous', fontSize: '14px', color: COLORS.accentHex }).setDepth(DEPTH.ui);
|
||||
this.priorityButtons = [];
|
||||
const mkChip = (x, y, w, label, onClick) => {
|
||||
const b = new Button(this, x + w / 2, y, label, onClick, { width: w, height: 40, fontSize: 16, variant: 'ghost' });
|
||||
b.setDepth(DEPTH.ui); return b;
|
||||
};
|
||||
const fw = (RAIL_W - 18) / 4;
|
||||
TREASURE_KEYS.forEach((k, i) => {
|
||||
const b = mkChip(RAIL_X + i * (fw + 6), py + 16, fw, k[0].toUpperCase() + k.slice(1), () => this.toggleFocus(k));
|
||||
b._key = k; this.priorityButtons.push(b);
|
||||
});
|
||||
const hw = (RAIL_W - 12) / 3;
|
||||
this.regroupBtn = mkChip(RAIL_X, py + 64, hw, 'Regroup', () => this.toggleRegroup());
|
||||
this.defendBtn = mkChip(RAIL_X + hw + 6, py + 64, hw, 'Defend Temples', () => this.toggleDefend());
|
||||
mkChip(RAIL_X + 2 * (hw + 6), py + 64, hw, 'Clear', () => this.clearPriorities());
|
||||
|
||||
// Hand
|
||||
this.add.text(RAIL_X, 856, 'YOUR HAND', { fontFamily: 'Righteous', fontSize: '14px', color: COLORS.accentHex }).setDepth(DEPTH.ui);
|
||||
this.handLayer = this.add.container(0, 0).setDepth(DEPTH.ui);
|
||||
|
|
@ -415,19 +402,11 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
this.drawWaterMeter();
|
||||
this.renderBanner();
|
||||
this.renderButtons();
|
||||
this.renderPriorities();
|
||||
this.renderChat();
|
||||
this.highlightTargets();
|
||||
this.updateDeckCounts();
|
||||
}
|
||||
|
||||
renderPriorities() {
|
||||
const pr = this.gs.priorities;
|
||||
for (const b of this.priorityButtons) b.setActive(pr.focusTreasure === b._key);
|
||||
this.regroupBtn.setActive(!!pr.regroup);
|
||||
this.defendBtn.setActive((pr.saveTiles ?? []).length > 0);
|
||||
}
|
||||
|
||||
renderPawns() {
|
||||
this.pawnLayer.removeAll(true);
|
||||
this.pawnObjects = {};
|
||||
|
|
@ -584,7 +563,7 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
hitZone.on('pointerover', () => { this.input.setDefaultCursor('pointer'); });
|
||||
hitZone.on('pointerout', () => { this.input.setDefaultCursor('default'); });
|
||||
hitZone.on('pointerup', () => {
|
||||
if (this.busy || !this.introComplete || isGameOver(this.gs)) return;
|
||||
if (this._dirClickConsumed || this.busy || !this.introComplete || isGameOver(this.gs)) return;
|
||||
this.openTradeModal();
|
||||
});
|
||||
|
||||
|
|
@ -643,6 +622,28 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
fontFamily: '"Julius Sans One"', fontSize: '11px', color: role.colorHex,
|
||||
}).setDepth(DEPTH.ui + 1));
|
||||
|
||||
// Direction pill (right-aligned in slot)
|
||||
const pillW = 74, pillH = 18;
|
||||
const pillX = slotLeft + slotW - (idx < N - 1 ? 6 : 4);
|
||||
const pillY = nameY - 1;
|
||||
const pillBg = reg(this.add.graphics().setDepth(DEPTH.ui + 4));
|
||||
pillBg.fillStyle(0x0a2538, 1);
|
||||
pillBg.fillRoundedRect(pillX - pillW, pillY, pillW, pillH, 4);
|
||||
pillBg.lineStyle(1, 0x2a5070, 1);
|
||||
pillBg.strokeRoundedRect(pillX - pillW, pillY, pillW, pillH, 4);
|
||||
const pillLabel = this.directionLabel(this.seatDirections[player.seat] ?? null) + ' ▼';
|
||||
const pillText = reg(this.add.text(pillX - pillW / 2, pillY + pillH / 2, pillLabel, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '10px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0.5).setDepth(DEPTH.ui + 5));
|
||||
this.hudPillTexts[player.seat] = pillText;
|
||||
const pillZone = reg(this.add.zone(pillX - pillW, pillY, pillW, pillH)
|
||||
.setOrigin(0, 0).setInteractive({ useHandCursor: true }).setDepth(DEPTH.ui + 5));
|
||||
pillZone.on('pointerup', () => {
|
||||
this._dirClickConsumed = true;
|
||||
this.time.delayedCall(0, () => { this._dirClickConsumed = false; });
|
||||
this.openDirectionMenu(player.seat, pillX - pillW, pillY + pillH + 4);
|
||||
});
|
||||
|
||||
// Store card slot origin for renderPartnerHUD
|
||||
this.partnerCardSlots[player.seat] = { cardX: textX, cardY: cardsY };
|
||||
});
|
||||
|
|
@ -1011,6 +1012,27 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
fontFamily: '"Julius Sans One"', fontSize: '12px', color: role.colorHex,
|
||||
}).setDepth(DEPTH.popup + 3));
|
||||
|
||||
// Direction pill (AI players only, right-aligned in column)
|
||||
if (player.seat !== this.humanSeat) {
|
||||
const pillW = 80, pillH = 20;
|
||||
const pillRight = panelX + (i + 1) * colW - 12;
|
||||
const pillTop = portCY - portR; // top of portrait area
|
||||
const pillBg = reg(this.add.graphics().setDepth(DEPTH.popup + 4));
|
||||
pillBg.fillStyle(0x0a2538, 1);
|
||||
pillBg.fillRoundedRect(pillRight - pillW, pillTop, pillW, pillH, 4);
|
||||
pillBg.lineStyle(1, 0x2a5070, 1);
|
||||
pillBg.strokeRoundedRect(pillRight - pillW, pillTop, pillW, pillH, 4);
|
||||
reg(this.add.text(pillRight - pillW / 2, pillTop + pillH / 2,
|
||||
this.directionLabel(this.seatDirections[player.seat] ?? null) + ' ▼', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '11px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0.5).setDepth(DEPTH.popup + 5));
|
||||
const pillZone = reg(this.add.zone(pillRight - pillW, pillTop, pillW, pillH)
|
||||
.setOrigin(0, 0).setInteractive({ useHandCursor: true }).setDepth(DEPTH.popup + 5));
|
||||
pillZone.on('pointerup', () => {
|
||||
this.openDirectionMenu(player.seat, pillRight - pillW, pillTop + pillH + 4, DEPTH.popup + 8);
|
||||
});
|
||||
}
|
||||
|
||||
// Hand cards (wrap to second row after CARDS_PER_ROW)
|
||||
if (!player.hand.length) {
|
||||
reg(this.add.text(textX, cardsTop + cardH / 2, 'No cards', {
|
||||
|
|
@ -1180,33 +1202,106 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
if (this.gs.phase === 'won') this.endGame();
|
||||
}
|
||||
|
||||
// ── Priorities ──────────────────────────────────────────────────────────────
|
||||
toggleFocus(k) {
|
||||
const cur = this.gs.priorities.focusTreasure;
|
||||
this.gs = setPriority(this.gs, { focusTreasure: cur === k ? null : k });
|
||||
if (cur !== k) this.ackPriority(`focus on ${TREASURES[k].name}`);
|
||||
// ── Per-player directions ──────────────────────────────────────────────────
|
||||
directionLabel(dir) {
|
||||
const MAP = { earth: 'Earth', wind: 'Wind', fire: 'Fire', ocean: 'Ocean', regroup: 'Regroup', defend: 'Defend' };
|
||||
return MAP[dir] ?? 'Auto';
|
||||
}
|
||||
|
||||
directionToPriority(gs, dir) {
|
||||
if (!dir || dir === 'auto') return { focusTreasure: null, regroup: false, saveTiles: [], hold: false };
|
||||
if (dir === 'regroup') return { regroup: true, focusTreasure: null, saveTiles: [] };
|
||||
if (dir === 'defend') {
|
||||
const tiles = Object.values(gs.tiles)
|
||||
.filter((t) => t.treasure && t.state !== 'sunk' && !gs.players.some((p) => p.captured[t.treasure]))
|
||||
.map((t) => t.id);
|
||||
return { saveTiles: tiles, focusTreasure: null, regroup: false };
|
||||
}
|
||||
return { focusTreasure: dir, regroup: false, saveTiles: [] };
|
||||
}
|
||||
|
||||
setDirection(seat, dir) {
|
||||
this.seatDirections[seat] = dir || null;
|
||||
this.closeDirectionMenu();
|
||||
const label = this.directionLabel(dir);
|
||||
if (this.hudPillTexts[seat]) this.hudPillTexts[seat].setText(label + ' ▼');
|
||||
// Rebuild trade modal in place so its pill reflects the new direction
|
||||
if (this.tradeModalObjs.length) { this.closeTradeModal(); this.openTradeModal(); }
|
||||
if (dir && dir !== 'auto') {
|
||||
const player = this.gs.players[seat];
|
||||
if (player) { const l = lineForAck(player.role, label); this.post(player.role, l.text, seat); }
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
toggleRegroup() {
|
||||
const v = !this.gs.priorities.regroup;
|
||||
this.gs = setPriority(this.gs, { regroup: v });
|
||||
if (v) this.ackPriority('regroup at Fools\' Landing');
|
||||
this.render();
|
||||
|
||||
openDirectionMenu(seat, x, y, baseDepth = DEPTH.popup) {
|
||||
this.closeDirectionMenu();
|
||||
|
||||
const OPTIONS = [
|
||||
{ dir: null, label: 'Auto' },
|
||||
{ dir: 'earth', label: 'Focus Earth' },
|
||||
{ dir: 'wind', label: 'Focus Wind' },
|
||||
{ dir: 'fire', label: 'Focus Fire' },
|
||||
{ dir: 'ocean', label: 'Focus Ocean' },
|
||||
{ dir: 'regroup', label: 'Regroup' },
|
||||
{ dir: 'defend', label: 'Defend Temples'},
|
||||
];
|
||||
|
||||
const rowH = 28, padX = 12, padY = 8, menuW = 140;
|
||||
const menuH = OPTIONS.length * rowH + padY * 2;
|
||||
const clampedX = Math.min(x, GAME_WIDTH - menuW - 8);
|
||||
const clampedY = Math.min(y, GAME_HEIGHT - menuH - 8);
|
||||
const curDir = this.seatDirections[seat] ?? null;
|
||||
|
||||
// Full-screen dismiss zone
|
||||
const dismiss = this.add.zone(0, 0, GAME_WIDTH, GAME_HEIGHT)
|
||||
.setOrigin(0, 0).setInteractive().setDepth(baseDepth - 1);
|
||||
dismiss.on('pointerup', () => this.closeDirectionMenu());
|
||||
this.directionDismiss = dismiss;
|
||||
|
||||
// Popup container
|
||||
const ctr = this.add.container(clampedX, clampedY).setDepth(baseDepth);
|
||||
const bg = this.add.graphics();
|
||||
bg.fillStyle(0x061a2a, 1);
|
||||
bg.fillRoundedRect(0, 0, menuW, menuH, 6);
|
||||
bg.lineStyle(1, 0x2a5070, 1);
|
||||
bg.strokeRoundedRect(0, 0, menuW, menuH, 6);
|
||||
ctr.add(bg);
|
||||
|
||||
OPTIONS.forEach((opt, i) => {
|
||||
const ry = padY + i * rowH;
|
||||
const isCurrent = opt.dir === curDir;
|
||||
const rowBg = this.add.graphics();
|
||||
rowBg.fillStyle(0xffffff, 0);
|
||||
rowBg.fillRect(1, ry, menuW - 2, rowH);
|
||||
ctr.add(rowBg);
|
||||
|
||||
const bullet = this.add.text(padX, ry + rowH / 2, isCurrent ? '●' : '○', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '10px',
|
||||
color: isCurrent ? COLORS.accentHex : COLORS.mutedHex,
|
||||
}).setOrigin(0, 0.5);
|
||||
ctr.add(bullet);
|
||||
|
||||
const lbl = this.add.text(padX + 14, ry + rowH / 2, opt.label, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '12px',
|
||||
color: isCurrent ? COLORS.textHex : COLORS.mutedHex,
|
||||
}).setOrigin(0, 0.5);
|
||||
ctr.add(lbl);
|
||||
|
||||
const row = this.add.zone(1, ry, menuW - 2, rowH)
|
||||
.setOrigin(0, 0).setInteractive({ useHandCursor: true });
|
||||
row.on('pointerover', () => { rowBg.clear(); rowBg.fillStyle(0xffffff, 0.07); rowBg.fillRect(1, ry, menuW - 2, rowH); });
|
||||
row.on('pointerout', () => { rowBg.clear(); rowBg.fillStyle(0xffffff, 0); rowBg.fillRect(1, ry, menuW - 2, rowH); });
|
||||
row.on('pointerup', () => this.setDirection(seat, opt.dir));
|
||||
ctr.add(row);
|
||||
});
|
||||
|
||||
this.directionDropdown = ctr;
|
||||
}
|
||||
toggleDefend() {
|
||||
const on = (this.gs.priorities.saveTiles ?? []).length === 0;
|
||||
const tiles = on ? Object.values(this.gs.tiles).filter((t) => t.treasure && t.state !== 'sunk' && !this.gs.players.some((p) => p.captured[t.treasure])).map((t) => t.id) : [];
|
||||
this.gs = setPriority(this.gs, { saveTiles: tiles });
|
||||
if (on) this.ackPriority('defend the temples');
|
||||
this.render();
|
||||
}
|
||||
clearPriorities() {
|
||||
this.gs = setPriority(this.gs, { focusTreasure: null, regroup: false, hold: false, saveTiles: [] });
|
||||
this.render();
|
||||
}
|
||||
ackPriority(label) {
|
||||
const ai = this.gs.players.find((p) => p.seat !== this.humanSeat);
|
||||
if (ai) { const l = lineForAck(ai.role, label); this.post(ai.role, l.text, ai.seat); }
|
||||
|
||||
closeDirectionMenu() {
|
||||
if (this.directionDropdown) { try { this.directionDropdown.destroy(); } catch {} this.directionDropdown = null; }
|
||||
if (this.directionDismiss) { try { this.directionDismiss.destroy(); } catch {} this.directionDismiss = null; }
|
||||
}
|
||||
|
||||
// ── Initial flood animation ──────────────────────────────────────────────────
|
||||
|
|
@ -1601,6 +1696,7 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
|
||||
aiTurn(seat) {
|
||||
this.busy = true;
|
||||
this.gs = setPriority(this.gs, this.directionToPriority(this.gs, this.seatDirections[seat] ?? null));
|
||||
// Announce the plan.
|
||||
const intent = describeIntent(this.gs, seat);
|
||||
const line = lineForIntent(intent);
|
||||
|
|
|
|||
Loading…
Reference in New Issue