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:
Brian Fertig 2026-10-01 19:42:47 -06:00
parent 9d82073e5a
commit e3c764a981
1 changed files with 82 additions and 1 deletions

View File

@ -13,6 +13,16 @@ import {
const FONT_D = 'Righteous';
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) ─────────────────
@ -75,6 +85,7 @@ export default class TentsGame extends Phaser.Scene {
this.sky = { t: 0 }; // 0 = midday, 1 = starry night
this.won = false;
this.winPanelShown = false;
this.rulesOpen = false;
this.difficulty = 'porch';
this.hover = null; // {c, r}
this.embers = [];
@ -107,6 +118,7 @@ export default class TentsGame extends Phaser.Scene {
this.buildHud();
this.hudLayer.setVisible(false);
this.buildRulesPanel();
this.buildSelectScreen();
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.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) {
@ -219,6 +238,68 @@ export default class TentsGame extends Phaser.Scene {
}).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 ──────────────────────────────────────────────────────────
buildSelectScreen() {