Add browser debugging support
This commit is contained in:
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@@ -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": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"]
|
||||
}
|
||||
]
|
||||
}
|
||||
27
.vscode/tasks.json
vendored
Normal file
27
.vscode/tasks.json
vendored
Normal file
@@ -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:"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
33
docs/debugging.md
Normal file
33
docs/debugging.md
Normal file
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
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 });
|
||||
let serverProcess;
|
||||
let browser;
|
||||
|
||||
try {
|
||||
serverProcess = await ensureLocalServer(targetUrl);
|
||||
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||
|
||||
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);
|
||||
|
||||
@@ -32,21 +45,23 @@ await page.waitForFunction(() => {
|
||||
return activeScenes.some((scene) => scene.scene.key === 'BattleScene');
|
||||
});
|
||||
|
||||
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_GAME__?.scene.getScenes(true).map((scene) => scene.scene.key) ?? [];
|
||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||
const battleState = window.__HEROS_DEBUG__?.battle();
|
||||
|
||||
return {
|
||||
activeScenes,
|
||||
battleState,
|
||||
canvasWidth: canvas?.width ?? 0,
|
||||
canvasHeight: canvas?.height ?? 0
|
||||
};
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
if (result.canvasWidth !== 1280 || result.canvasHeight !== 720) {
|
||||
throw new Error(`Unexpected canvas size: ${result.canvasWidth}x${result.canvasHeight}`);
|
||||
}
|
||||
@@ -55,4 +70,59 @@ if (!result.activeScenes.includes('BattleScene')) {
|
||||
throw new Error(`BattleScene was not active. Active scenes: ${result.activeScenes.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(`Verified title-to-battle flow at ${targetUrl}`);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureLocalServer(url) {
|
||||
if (await canReach(url)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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 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']
|
||||
});
|
||||
|
||||
child.stderr.on('data', (chunk) => stderr.push(chunk.toString()));
|
||||
child.stdout.on('data', () => {});
|
||||
|
||||
for (let i = 0; i < 80; i += 1) {
|
||||
if (await canReach(url)) {
|
||||
return child;
|
||||
}
|
||||
await delay(250);
|
||||
}
|
||||
|
||||
child.kill();
|
||||
throw new Error(`Vite server did not start at ${url}\n${stderr.join('')}`);
|
||||
}
|
||||
|
||||
async function canReach(url) {
|
||||
try {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(1000) });
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -152,6 +152,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
private bondStates = new Map<string, BondState>();
|
||||
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'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
35
src/main.ts
35
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?.();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
11
vite.config.ts
Normal file
11
vite.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173
|
||||
},
|
||||
build: {
|
||||
sourcemap: true
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user