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.tradeModalObjs = []; // all objects in the trade modal
|
||||||
this.tradeHighlightObjs = []; // highlight borders for selected card
|
this.tradeHighlightObjs = []; // highlight borders for selected card
|
||||||
this.tradeSelection = null; // { seat, cardIdx, card, frame, worldX, worldY }
|
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() {
|
create() {
|
||||||
|
|
@ -225,7 +230,7 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Chat panel
|
// Chat panel
|
||||||
const chatY = 248, chatH = 470;
|
const chatY = 248, chatH = 548;
|
||||||
const cp = this.add.graphics().setDepth(DEPTH.ui);
|
const cp = this.add.graphics().setDepth(DEPTH.ui);
|
||||||
cp.fillStyle(0x000000, 0.4); cp.fillRoundedRect(RAIL_X, chatY, RAIL_W, chatH, 12);
|
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);
|
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,
|
wordWrap: { width: this.chatBox.w }, lineSpacing: 4,
|
||||||
}).setDepth(DEPTH.ui);
|
}).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
|
// Hand
|
||||||
this.add.text(RAIL_X, 856, 'YOUR HAND', { fontFamily: 'Righteous', fontSize: '14px', color: COLORS.accentHex }).setDepth(DEPTH.ui);
|
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);
|
this.handLayer = this.add.container(0, 0).setDepth(DEPTH.ui);
|
||||||
|
|
@ -415,19 +402,11 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
||||||
this.drawWaterMeter();
|
this.drawWaterMeter();
|
||||||
this.renderBanner();
|
this.renderBanner();
|
||||||
this.renderButtons();
|
this.renderButtons();
|
||||||
this.renderPriorities();
|
|
||||||
this.renderChat();
|
this.renderChat();
|
||||||
this.highlightTargets();
|
this.highlightTargets();
|
||||||
this.updateDeckCounts();
|
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() {
|
renderPawns() {
|
||||||
this.pawnLayer.removeAll(true);
|
this.pawnLayer.removeAll(true);
|
||||||
this.pawnObjects = {};
|
this.pawnObjects = {};
|
||||||
|
|
@ -584,7 +563,7 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
||||||
hitZone.on('pointerover', () => { this.input.setDefaultCursor('pointer'); });
|
hitZone.on('pointerover', () => { this.input.setDefaultCursor('pointer'); });
|
||||||
hitZone.on('pointerout', () => { this.input.setDefaultCursor('default'); });
|
hitZone.on('pointerout', () => { this.input.setDefaultCursor('default'); });
|
||||||
hitZone.on('pointerup', () => {
|
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();
|
this.openTradeModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -643,6 +622,28 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '11px', color: role.colorHex,
|
fontFamily: '"Julius Sans One"', fontSize: '11px', color: role.colorHex,
|
||||||
}).setDepth(DEPTH.ui + 1));
|
}).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
|
// Store card slot origin for renderPartnerHUD
|
||||||
this.partnerCardSlots[player.seat] = { cardX: textX, cardY: cardsY };
|
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,
|
fontFamily: '"Julius Sans One"', fontSize: '12px', color: role.colorHex,
|
||||||
}).setDepth(DEPTH.popup + 3));
|
}).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)
|
// Hand cards (wrap to second row after CARDS_PER_ROW)
|
||||||
if (!player.hand.length) {
|
if (!player.hand.length) {
|
||||||
reg(this.add.text(textX, cardsTop + cardH / 2, 'No cards', {
|
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();
|
if (this.gs.phase === 'won') this.endGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Priorities ──────────────────────────────────────────────────────────────
|
// ── Per-player directions ──────────────────────────────────────────────────
|
||||||
toggleFocus(k) {
|
directionLabel(dir) {
|
||||||
const cur = this.gs.priorities.focusTreasure;
|
const MAP = { earth: 'Earth', wind: 'Wind', fire: 'Fire', ocean: 'Ocean', regroup: 'Regroup', defend: 'Defend' };
|
||||||
this.gs = setPriority(this.gs, { focusTreasure: cur === k ? null : k });
|
return MAP[dir] ?? 'Auto';
|
||||||
if (cur !== k) this.ackPriority(`focus on ${TREASURES[k].name}`);
|
}
|
||||||
|
|
||||||
|
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();
|
this.render();
|
||||||
}
|
}
|
||||||
toggleRegroup() {
|
|
||||||
const v = !this.gs.priorities.regroup;
|
openDirectionMenu(seat, x, y, baseDepth = DEPTH.popup) {
|
||||||
this.gs = setPriority(this.gs, { regroup: v });
|
this.closeDirectionMenu();
|
||||||
if (v) this.ackPriority('regroup at Fools\' Landing');
|
|
||||||
this.render();
|
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;
|
closeDirectionMenu() {
|
||||||
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) : [];
|
if (this.directionDropdown) { try { this.directionDropdown.destroy(); } catch {} this.directionDropdown = null; }
|
||||||
this.gs = setPriority(this.gs, { saveTiles: tiles });
|
if (this.directionDismiss) { try { this.directionDismiss.destroy(); } catch {} this.directionDismiss = null; }
|
||||||
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); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Initial flood animation ──────────────────────────────────────────────────
|
// ── Initial flood animation ──────────────────────────────────────────────────
|
||||||
|
|
@ -1601,6 +1696,7 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
||||||
|
|
||||||
aiTurn(seat) {
|
aiTurn(seat) {
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
|
this.gs = setPriority(this.gs, this.directionToPriority(this.gs, this.seatDirections[seat] ?? null));
|
||||||
// Announce the plan.
|
// Announce the plan.
|
||||||
const intent = describeIntent(this.gs, seat);
|
const intent = describeIntent(this.gs, seat);
|
||||||
const line = lineForIntent(intent);
|
const line = lineForIntent(intent);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue