From e3c764a98145bf33e50db6661c0847cff3a62737 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Thu, 1 Oct 2026 19:42:47 -0600 Subject: [PATCH] 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. --- src/games/tents/TentsGame.js | 83 +++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/src/games/tents/TentsGame.js b/src/games/tents/TentsGame.js index b431bef..26540f9 100644 --- a/src/games/tents/TentsGame.js +++ b/src/games/tents/TentsGame.js @@ -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() {