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",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
"debug": "vite --host 0.0.0.0 --debug",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"generate:audio": "node scripts/generate-bgm.mjs",
|
"generate:audio": "node scripts/generate-bgm.mjs",
|
||||||
"preview": "vite preview --host 0.0.0.0",
|
"preview": "vite preview --host 0.0.0.0",
|
||||||
|
|||||||
@@ -1,58 +1,128 @@
|
|||||||
|
import { spawn } from 'node:child_process';
|
||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
|
|
||||||
const targetUrl = process.env.VERIFY_URL ?? 'http://localhost:5173/';
|
const targetUrl = process.env.VERIFY_URL ?? 'http://localhost:5173/';
|
||||||
|
|
||||||
const browser = await chromium.launch({ headless: true });
|
let serverProcess;
|
||||||
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
let browser;
|
||||||
|
|
||||||
await page.goto(targetUrl, { waitUntil: 'networkidle' });
|
try {
|
||||||
await page.waitForSelector('canvas');
|
serverProcess = await ensureLocalServer(targetUrl);
|
||||||
await page.waitForFunction(() => window.__HEROS_GAME__ !== undefined);
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
await page.mouse.click(962, 240);
|
browser = await chromium.launch({ headless: true });
|
||||||
await page.waitForTimeout(250);
|
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||||
|
|
||||||
for (let i = 0; i < 20; i += 1) {
|
await page.goto(targetUrl, { waitUntil: 'networkidle' });
|
||||||
const isBattleActive = await page.evaluate(() => {
|
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) ?? [];
|
const activeScenes = window.__HEROS_GAME__?.scene.getScenes(true) ?? [];
|
||||||
return activeScenes.some((scene) => scene.scene.key === 'BattleScene');
|
return activeScenes.some((scene) => scene.scene.key === 'BattleScene');
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isBattleActive) {
|
await page.keyboard.press('F9');
|
||||||
break;
|
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');
|
if (!result.activeScenes.includes('BattleScene')) {
|
||||||
await page.waitForTimeout(220);
|
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(() => {
|
async function ensureLocalServer(url) {
|
||||||
const activeScenes = window.__HEROS_GAME__?.scene.getScenes(true) ?? [];
|
if (await canReach(url)) {
|
||||||
return activeScenes.some((scene) => scene.scene.key === 'BattleScene');
|
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 stderr = [];
|
||||||
const canvas = document.querySelector('canvas');
|
const child = spawn(process.execPath, ['node_modules/vite/bin/vite.js', '--host', '127.0.0.1', '--port', parsed.port || '5173'], {
|
||||||
const activeScenes = window.__HEROS_GAME__?.scene.getScenes(true).map((scene) => scene.scene.key) ?? [];
|
cwd: process.cwd(),
|
||||||
|
env: process.env,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
child.stderr.on('data', (chunk) => stderr.push(chunk.toString()));
|
||||||
activeScenes,
|
child.stdout.on('data', () => {});
|
||||||
canvasWidth: canvas?.width ?? 0,
|
|
||||||
canvasHeight: canvas?.height ?? 0
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
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) {
|
child.kill();
|
||||||
throw new Error(`Unexpected canvas size: ${result.canvasWidth}x${result.canvasHeight}`);
|
throw new Error(`Vite server did not start at ${url}\n${stderr.join('')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.activeScenes.includes('BattleScene')) {
|
async function canReach(url) {
|
||||||
throw new Error(`BattleScene was not active. Active scenes: ${result.activeScenes.join(', ')}`);
|
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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private bondStates = new Map<string, BondState>();
|
private bondStates = new Map<string, BondState>();
|
||||||
private attackIntents: AttackIntent[] = [];
|
private attackIntents: AttackIntent[] = [];
|
||||||
private pendingMove?: PendingMove;
|
private pendingMove?: PendingMove;
|
||||||
|
private debugOverlay?: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('BattleScene');
|
super('BattleScene');
|
||||||
@@ -168,6 +169,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.handleRightClick(pointer);
|
this.handleRightClick(pointer);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.installDebugHotkeys();
|
||||||
|
|
||||||
this.add.rectangle(0, 0, width, height, 0x080b0d).setOrigin(0);
|
this.add.rectangle(0, 0, width, height, 0x080b0d).setOrigin(0);
|
||||||
this.drawMap();
|
this.drawMap();
|
||||||
@@ -1473,4 +1475,99 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
this.renderRosterPanel(this.rosterTab, text);
|
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 {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__HEROS_GAME__?: Phaser.Game;
|
__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 = {
|
const config: Phaser.Types.Core.GameConfig = {
|
||||||
type: Phaser.AUTO,
|
type: Phaser.AUTO,
|
||||||
parent: 'game',
|
parent: 'game',
|
||||||
@@ -32,4 +47,24 @@ const game = new Phaser.Game(config);
|
|||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
window.__HEROS_GAME__ = game;
|
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