From 48a1a9ccbb989e1fcba6c4e5c78a15b388cd667e Mon Sep 17 00:00:00 2001 From: Wickedness Date: Mon, 22 Jun 2026 10:50:03 +0900 Subject: [PATCH] Add browser debugging support --- .vscode/launch.json | 15 ++++ .vscode/tasks.json | 27 +++++++ docs/debugging.md | 33 ++++++++ package.json | 1 + scripts/verify-flow.mjs | 138 +++++++++++++++++++++++++-------- src/game/scenes/BattleScene.ts | 97 +++++++++++++++++++++++ src/main.ts | 35 +++++++++ vite.config.ts | 11 +++ 8 files changed, 323 insertions(+), 34 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 docs/debugging.md create mode 100644 vite.config.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8b2a9eb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Heros Web (Chrome)", + "type": "pwa-chrome", + "request": "launch", + "url": "http://localhost:5173", + "webRoot": "${workspaceFolder}/src", + "preLaunchTask": "dev server", + "sourceMaps": true, + "skipFiles": ["/**", "${workspaceFolder}/node_modules/**"] + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..29cb5e2 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,27 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "dev server", + "type": "shell", + "command": "pnpm dev", + "isBackground": true, + "problemMatcher": { + "owner": "vite", + "pattern": [ + { + "regexp": ".", + "file": 1, + "location": 2, + "message": 3 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Local:" + } + } + } + ] +} diff --git a/docs/debugging.md b/docs/debugging.md new file mode 100644 index 0000000..59f7adb --- /dev/null +++ b/docs/debugging.md @@ -0,0 +1,33 @@ +# Debugging + +## VS Code + +1. Open this folder in VS Code. +2. Run `Debug Heros Web (Chrome)` from Run and Debug. +3. VS Code starts the Vite dev server and opens Chrome at `http://localhost:5173`. +4. Set breakpoints in `src/game/scenes/*.ts`. + +## Browser Console + +Development builds expose: + +```js +window.__HEROS_GAME__ +window.__HEROS_DEBUG__ +``` + +Useful commands: + +```js +__HEROS_DEBUG__.activeScenes() +__HEROS_DEBUG__.battle() +__HEROS_DEBUG__.goToBattle() +__HEROS_DEBUG__.toggleBattleOverlay() +``` + +## Battle Hotkeys + +- `F9`: Toggle battle debug overlay. +- `F10`: Log battle state to the browser console. + +The battle debug state includes turn, phase, selected unit, pending move, acted units, unit coordinates, attack intents, and bond progress. diff --git a/package.json b/package.json index 7a2105d..55c3ed7 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", + "debug": "vite --host 0.0.0.0 --debug", "build": "tsc && vite build", "generate:audio": "node scripts/generate-bgm.mjs", "preview": "vite preview --host 0.0.0.0", diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index f93df1b..2d36efc 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -1,58 +1,128 @@ +import { spawn } from 'node:child_process'; import { chromium } from 'playwright'; const targetUrl = process.env.VERIFY_URL ?? 'http://localhost:5173/'; -const browser = await chromium.launch({ headless: true }); -const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); +let serverProcess; +let browser; -await page.goto(targetUrl, { waitUntil: 'networkidle' }); -await page.waitForSelector('canvas'); -await page.waitForFunction(() => window.__HEROS_GAME__ !== undefined); -await page.waitForTimeout(500); +try { + serverProcess = await ensureLocalServer(targetUrl); -await page.mouse.click(962, 240); -await page.waitForTimeout(250); + browser = await chromium.launch({ headless: true }); + const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); -for (let i = 0; i < 20; i += 1) { - const isBattleActive = await page.evaluate(() => { + await page.goto(targetUrl, { waitUntil: 'networkidle' }); + await page.waitForSelector('canvas'); + await page.waitForFunction(() => window.__HEROS_GAME__ !== undefined); + await page.waitForFunction(() => window.__HEROS_DEBUG__ !== undefined); + await page.waitForTimeout(500); + + const debugBeforeBattle = await page.evaluate(() => window.__HEROS_DEBUG__?.activeScenes() ?? []); + if (!debugBeforeBattle.includes('TitleScene')) { + throw new Error(`Expected TitleScene before starting. Active scenes: ${debugBeforeBattle.join(', ')}`); + } + + await page.mouse.click(962, 240); + await page.waitForTimeout(250); + + for (let i = 0; i < 20; i += 1) { + const isBattleActive = await page.evaluate(() => { + const activeScenes = window.__HEROS_GAME__?.scene.getScenes(true) ?? []; + return activeScenes.some((scene) => scene.scene.key === 'BattleScene'); + }); + + if (isBattleActive) { + break; + } + + await page.keyboard.press('Space'); + await page.waitForTimeout(220); + } + + await page.waitForFunction(() => { const activeScenes = window.__HEROS_GAME__?.scene.getScenes(true) ?? []; return activeScenes.some((scene) => scene.scene.key === 'BattleScene'); }); - if (isBattleActive) { - break; + await page.keyboard.press('F9'); + await page.waitForTimeout(100); + await page.screenshot({ path: 'dist/verification-battle.png', fullPage: true }); + + const result = await page.evaluate(() => { + const canvas = document.querySelector('canvas'); + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + const battleState = window.__HEROS_DEBUG__?.battle(); + + return { + activeScenes, + battleState, + canvasWidth: canvas?.width ?? 0, + canvasHeight: canvas?.height ?? 0 + }; + }); + + if (result.canvasWidth !== 1280 || result.canvasHeight !== 720) { + throw new Error(`Unexpected canvas size: ${result.canvasWidth}x${result.canvasHeight}`); } - await page.keyboard.press('Space'); - await page.waitForTimeout(220); + if (!result.activeScenes.includes('BattleScene')) { + throw new Error(`BattleScene was not active. Active scenes: ${result.activeScenes.join(', ')}`); + } + + if (!result.battleState || result.battleState.scene !== 'BattleScene') { + throw new Error(`Debug battle state was not available: ${JSON.stringify(result.battleState)}`); + } + + console.log(`Verified title-to-battle flow and debug API at ${targetUrl}`); +} finally { + await browser?.close(); + if (serverProcess && !serverProcess.killed) { + serverProcess.kill(); + } } -await page.waitForFunction(() => { - const activeScenes = window.__HEROS_GAME__?.scene.getScenes(true) ?? []; - return activeScenes.some((scene) => scene.scene.key === 'BattleScene'); -}); +async function ensureLocalServer(url) { + if (await canReach(url)) { + return undefined; + } -await page.screenshot({ path: 'dist/verification-battle.png', fullPage: true }); + const parsed = new URL(url); + const isLocal = ['localhost', '127.0.0.1', '0.0.0.0'].includes(parsed.hostname); + if (!isLocal) { + throw new Error(`No server responded at ${url}`); + } -const result = await page.evaluate(() => { - const canvas = document.querySelector('canvas'); - const activeScenes = window.__HEROS_GAME__?.scene.getScenes(true).map((scene) => scene.scene.key) ?? []; + const stderr = []; + const child = spawn(process.execPath, ['node_modules/vite/bin/vite.js', '--host', '127.0.0.1', '--port', parsed.port || '5173'], { + cwd: process.cwd(), + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'] + }); - return { - activeScenes, - canvasWidth: canvas?.width ?? 0, - canvasHeight: canvas?.height ?? 0 - }; -}); + child.stderr.on('data', (chunk) => stderr.push(chunk.toString())); + child.stdout.on('data', () => {}); -await browser.close(); + for (let i = 0; i < 80; i += 1) { + if (await canReach(url)) { + return child; + } + await delay(250); + } -if (result.canvasWidth !== 1280 || result.canvasHeight !== 720) { - throw new Error(`Unexpected canvas size: ${result.canvasWidth}x${result.canvasHeight}`); + child.kill(); + throw new Error(`Vite server did not start at ${url}\n${stderr.join('')}`); } -if (!result.activeScenes.includes('BattleScene')) { - throw new Error(`BattleScene was not active. Active scenes: ${result.activeScenes.join(', ')}`); +async function canReach(url) { + try { + const response = await fetch(url, { signal: AbortSignal.timeout(1000) }); + return response.ok; + } catch { + return false; + } } -console.log(`Verified title-to-battle flow at ${targetUrl}`); +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index bb289d9..2dfccd6 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -152,6 +152,7 @@ export class BattleScene extends Phaser.Scene { private bondStates = new Map(); private attackIntents: AttackIntent[] = []; private pendingMove?: PendingMove; + private debugOverlay?: Phaser.GameObjects.Text; constructor() { super('BattleScene'); @@ -168,6 +169,7 @@ export class BattleScene extends Phaser.Scene { this.handleRightClick(pointer); } }); + this.installDebugHotkeys(); this.add.rectangle(0, 0, width, height, 0x080b0d).setOrigin(0); this.drawMap(); @@ -1473,4 +1475,99 @@ export class BattleScene extends Phaser.Scene { this.renderRosterPanel(this.rosterTab, text); } + + getDebugState() { + return { + scene: this.scene.key, + turnNumber: this.turnNumber, + activeFaction: this.activeFaction, + phase: this.phase, + selectedUnitId: this.selectedUnit?.id ?? null, + pendingMove: this.pendingMove + ? { + unitId: this.pendingMove.unit.id, + from: { x: this.pendingMove.fromX, y: this.pendingMove.fromY }, + to: { x: this.pendingMove.toX, y: this.pendingMove.toY } + } + : null, + actedUnitIds: Array.from(this.actedUnitIds), + attackIntents: this.attackIntents.map((intent) => ({ ...intent })), + units: firstBattleUnits.map((unit) => ({ + id: unit.id, + name: unit.name, + faction: unit.faction, + classKey: unit.classKey, + hp: unit.hp, + maxHp: unit.maxHp, + x: unit.x, + y: unit.y, + acted: this.actedUnitIds.has(unit.id) + })), + bonds: Array.from(this.bondStates.values()).map((bond) => ({ + id: bond.id, + unitIds: bond.unitIds, + level: bond.level, + exp: bond.exp, + battleExp: bond.battleExp + })) + }; + } + + toggleDebugOverlay() { + if (this.debugOverlay) { + this.debugOverlay.destroy(); + this.debugOverlay = undefined; + return; + } + + this.debugOverlay = this.add.text(12, 12, '', { + fontFamily: 'Consolas, monospace', + fontSize: '13px', + color: '#dff7ff', + backgroundColor: 'rgba(0, 0, 0, 0.72)', + padding: { x: 8, y: 6 } + }); + this.debugOverlay.setDepth(1000); + this.updateDebugOverlay(); + } + + private installDebugHotkeys() { + if (!import.meta.env.DEV) { + return; + } + + this.input.keyboard?.on('keydown-F9', () => this.toggleDebugOverlay()); + this.input.keyboard?.on('keydown-F10', () => { + console.info('[Battle Debug]', this.getDebugState()); + this.updateDebugOverlay(); + }); + this.input.on('pointermove', () => this.updateDebugOverlay()); + this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { + this.debugOverlay?.destroy(); + this.debugOverlay = undefined; + }); + } + + private updateDebugOverlay() { + if (!this.debugOverlay) { + return; + } + + const pointerTile = this.pointerToTile(this.input.activePointer); + const selected = this.selectedUnit ? `${this.selectedUnit.name} (${this.selectedUnit.x},${this.selectedUnit.y})` : 'none'; + const pending = this.pendingMove + ? `${this.pendingMove.unit.name}: (${this.pendingMove.fromX},${this.pendingMove.fromY}) -> (${this.pendingMove.toX},${this.pendingMove.toY})` + : 'none'; + + this.debugOverlay.setText([ + 'Heros Web Debug', + `scene=${this.scene.key}`, + `turn=${this.turnNumber} faction=${this.activeFaction} phase=${this.phase}`, + `selected=${selected}`, + `pendingMove=${pending}`, + `acted=${Array.from(this.actedUnitIds).join(',') || 'none'}`, + `pointerTile=${pointerTile ? `${pointerTile.x},${pointerTile.y}` : 'outside'}`, + 'F9 toggle overlay / F10 log state' + ]); + } } diff --git a/src/main.ts b/src/main.ts index e5b1809..2249672 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,9 +10,24 @@ import './styles/global.css'; declare global { interface Window { __HEROS_GAME__?: Phaser.Game; + __HEROS_DEBUG__?: HerosDebugApi; } } +type DebugBattleScene = Phaser.Scene & { + getDebugState?: () => unknown; + toggleDebugOverlay?: () => void; +}; + +type HerosDebugApi = { + game: Phaser.Game; + activeScenes: () => string[]; + battle: () => unknown; + goToBattle: () => void; + scene: (key: string) => Phaser.Scene | undefined; + toggleBattleOverlay: () => void; +}; + const config: Phaser.Types.Core.GameConfig = { type: Phaser.AUTO, parent: 'game', @@ -32,4 +47,24 @@ const game = new Phaser.Game(config); if (import.meta.env.DEV) { window.__HEROS_GAME__ = game; + window.__HEROS_DEBUG__ = createDebugApi(game); + console.info('[Heros Debug] Use window.__HEROS_DEBUG__.battle() or press F9 in battle.'); +} + +function createDebugApi(game: Phaser.Game): HerosDebugApi { + const scene = (key: string) => game.scene.getScene(key); + const battleScene = () => scene('BattleScene') as DebugBattleScene | undefined; + + return { + game, + activeScenes: () => game.scene.getScenes(true).map((activeScene) => activeScene.scene.key), + battle: () => battleScene()?.getDebugState?.() ?? { active: false, reason: 'BattleScene is not active yet.' }, + goToBattle: () => { + game.scene.start('BattleScene'); + }, + scene, + toggleBattleOverlay: () => { + battleScene()?.toggleDebugOverlay?.(); + } + }; } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5cb7214 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { + host: '0.0.0.0', + port: 5173 + }, + build: { + sourcemap: true + } +});