Add toggleable rules panel to Tents game
- Introduce a slide-in "How to Play" panel listing the four Tents rules, kept in sync with the engine's own rule comment so the UI can't drift. - Add a Rules button in the HUD parked where the panel's Close button will appear, so opening reads as one control morphing rather than two controls. - Animate the panel open/close and swap visibility/interactivity between the Rules and Close buttons to avoid stacking both in the same spot.
This commit is contained in:
parent
9d82073e5a
commit
e3c764a981
|
|
@ -13,6 +13,16 @@ import {
|
||||||
|
|
||||||
const FONT_D = 'Righteous';
|
const FONT_D = 'Righteous';
|
||||||
const FONT_B = '"Julius Sans One"';
|
const FONT_B = '"Julius Sans One"';
|
||||||
|
const RULES_W = 420;
|
||||||
|
|
||||||
|
// The same four rules the engine enforces (TentsLogic.js rule comment) —
|
||||||
|
// kept here as plain strings so the panel can never drift from the model.
|
||||||
|
const RULES_TEXT = [
|
||||||
|
'Every tent must sit next to a tree — up, down, left, or right (no diagonals).',
|
||||||
|
"It's a one-to-one pairing: each tree gets exactly one tent, and each tent belongs to exactly one tree.",
|
||||||
|
'Tents can never touch another tent, not even corner to corner.',
|
||||||
|
'The numbers along the top and side are the exact tent count for that column and row — no more, no less.',
|
||||||
|
];
|
||||||
|
|
||||||
// ── Color helpers (day → night lerps drive the whole scene) ─────────────────
|
// ── Color helpers (day → night lerps drive the whole scene) ─────────────────
|
||||||
|
|
||||||
|
|
@ -75,6 +85,7 @@ export default class TentsGame extends Phaser.Scene {
|
||||||
this.sky = { t: 0 }; // 0 = midday, 1 = starry night
|
this.sky = { t: 0 }; // 0 = midday, 1 = starry night
|
||||||
this.won = false;
|
this.won = false;
|
||||||
this.winPanelShown = false;
|
this.winPanelShown = false;
|
||||||
|
this.rulesOpen = false;
|
||||||
this.difficulty = 'porch';
|
this.difficulty = 'porch';
|
||||||
this.hover = null; // {c, r}
|
this.hover = null; // {c, r}
|
||||||
this.embers = [];
|
this.embers = [];
|
||||||
|
|
@ -107,6 +118,7 @@ export default class TentsGame extends Phaser.Scene {
|
||||||
|
|
||||||
this.buildHud();
|
this.buildHud();
|
||||||
this.hudLayer.setVisible(false);
|
this.hudLayer.setVisible(false);
|
||||||
|
this.buildRulesPanel();
|
||||||
this.buildSelectScreen();
|
this.buildSelectScreen();
|
||||||
|
|
||||||
this.events.on('shutdown', () => {
|
this.events.on('shutdown', () => {
|
||||||
|
|
@ -209,7 +221,14 @@ export default class TentsGame extends Phaser.Scene {
|
||||||
this.btnReset = mk('Reset Tents', 700, () => this.resetTents(), 'ghost');
|
this.btnReset = mk('Reset Tents', 700, () => this.resetTents(), 'ghost');
|
||||||
this.btnMenu = mk('Menu', 780, () => this.scene.start('GameMenu'), 'ghost');
|
this.btnMenu = mk('Menu', 780, () => this.scene.start('GameMenu'), 'ghost');
|
||||||
|
|
||||||
this.hudLayer.add([this.btnNew, this.btnAnswer, this.btnReset, this.btnMenu]);
|
// Parked at the same screen position the rules panel's Close button sits
|
||||||
|
// at once open (GAME_WIDTH - RULES_W/2, GAME_HEIGHT - 80), so opening the
|
||||||
|
// panel reads as this button "becoming" Close rather than two separate
|
||||||
|
// controls.
|
||||||
|
this.btnRules = new Button(this, GAME_WIDTH - RULES_W / 2, GAME_HEIGHT - 80, 'Rules',
|
||||||
|
() => this.toggleRules(), { width: 200, height: 56, variant: 'ghost' }).setDepth(31);
|
||||||
|
|
||||||
|
this.hudLayer.add([this.btnNew, this.btnAnswer, this.btnReset, this.btnMenu, this.btnRules]);
|
||||||
}
|
}
|
||||||
|
|
||||||
makeText(str, x, y, font, size, color) {
|
makeText(str, x, y, font, size, color) {
|
||||||
|
|
@ -219,6 +238,68 @@ export default class TentsGame extends Phaser.Scene {
|
||||||
}).setOrigin(0, 0).setDepth(30);
|
}).setOrigin(0, 0).setDepth(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Rules panel (toggled in-play; parked off-screen until opened) ──────────
|
||||||
|
|
||||||
|
buildRulesPanel() {
|
||||||
|
const panel = this.add.container(GAME_WIDTH, 0).setDepth(35);
|
||||||
|
|
||||||
|
const bg = this.add.graphics();
|
||||||
|
bg.fillStyle(0x101024, 0.95);
|
||||||
|
bg.fillRect(0, 0, RULES_W, GAME_HEIGHT);
|
||||||
|
bg.lineStyle(3, 0xffc46a, 0.9);
|
||||||
|
bg.lineBetween(0, 0, 0, GAME_HEIGHT);
|
||||||
|
panel.add(bg);
|
||||||
|
|
||||||
|
// Pushed down ~100px so the title doesn't sit under the soundtrack
|
||||||
|
// controls (MusicPlayer's skip/mute buttons + track name) in the
|
||||||
|
// top-right corner, which render above this panel's depth.
|
||||||
|
panel.add(this.add.text(RULES_W / 2, 150, 'How to Play', {
|
||||||
|
fontFamily: FONT_D, fontSize: '36px', color: '#ffd76a',
|
||||||
|
stroke: '#3a2a14', strokeThickness: 6,
|
||||||
|
}).setOrigin(0.5));
|
||||||
|
|
||||||
|
let y = 218;
|
||||||
|
RULES_TEXT.forEach((line, i) => {
|
||||||
|
panel.add(this.add.text(24, y, `${i + 1}.`, {
|
||||||
|
fontFamily: FONT_D, fontSize: '22px', color: '#ffd76a',
|
||||||
|
}).setOrigin(0, 0));
|
||||||
|
const body = this.add.text(58, y, line, {
|
||||||
|
fontFamily: FONT_B, fontSize: '20px', color: '#fdf6e3',
|
||||||
|
wordWrap: { width: RULES_W - 82 }, lineSpacing: 4,
|
||||||
|
}).setOrigin(0, 0);
|
||||||
|
panel.add(body);
|
||||||
|
y += body.height + 34;
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeBtn = new Button(this, RULES_W / 2, GAME_HEIGHT - 80, 'Close',
|
||||||
|
() => this.toggleRules(), { width: 200, height: 56, variant: 'ghost' });
|
||||||
|
panel.add(closeBtn);
|
||||||
|
|
||||||
|
this.rulesPanel = panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleRules() {
|
||||||
|
this.rulesOpen = !this.rulesOpen;
|
||||||
|
this.tweens.add({
|
||||||
|
targets: this.rulesPanel,
|
||||||
|
x: this.rulesOpen ? GAME_WIDTH - RULES_W : GAME_WIDTH,
|
||||||
|
duration: 220,
|
||||||
|
ease: 'Quad.Out',
|
||||||
|
});
|
||||||
|
// The panel's own Close button sits in this same screen spot once open,
|
||||||
|
// so hide (and un-click) the Rules button rather than stacking both.
|
||||||
|
this.btnRules.setVisible(!this.rulesOpen);
|
||||||
|
if (this.rulesOpen) {
|
||||||
|
this.btnRules.disableInteractive();
|
||||||
|
} else {
|
||||||
|
this.btnRules.setInteractive({
|
||||||
|
useHandCursor: true,
|
||||||
|
hitArea: new Phaser.Geom.Rectangle(0, 0, 200, 56),
|
||||||
|
hitAreaCallback: Phaser.Geom.Rectangle.Contains,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Select screen ──────────────────────────────────────────────────────────
|
// ── Select screen ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
buildSelectScreen() {
|
buildSelectScreen() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue