diff --git a/plans/260410-0913-phaser-web-client/phase-01-project-scaffold.md b/plans/260410-0913-phaser-web-client/phase-01-project-scaffold.md new file mode 100644 index 0000000..b6b7ea3 --- /dev/null +++ b/plans/260410-0913-phaser-web-client/phase-01-project-scaffold.md @@ -0,0 +1,100 @@ +# Phase 1: Project Scaffold + +## Context Links +- [Plan overview](plan.md) +- Server WS handler: `landlords-server/.../handler/WebsocketTransferHandler.java` +- Server WS proxy: `landlords-server/.../proxy/WebsocketProxy.java` + +## Overview +- **Priority:** P1 (blocker for all other phases) +- **Status:** Pending +- **Description:** Initialize Vite + Phaser 3 project in `web-client/`, verify Phaser boots a blank canvas. + +## Requirements +- `npm create vite` with vanilla JS template (or manual init) +- Phaser 3 latest stable as dependency +- Vite dev server on port 5173 (default) +- `index.html` with a `#game-container` div for Phaser canvas + a `#ui-overlay` div for DOM menus +- `src/main.js` creates Phaser.Game with config from `src/config/game-config.js` +- BootScene placeholder that shows "Loading..." text + +## Architecture + +``` +web-client/ + package.json + vite.config.js + index.html <-- #game-container + #ui-overlay + src/ + main.js <-- Phaser.Game instantiation + config/ + game-config.js <-- Phaser config: 800x800, Scale.FIT, scenes list + scenes/ + boot-scene.js <-- placeholder "Loading..." text + public/ + (empty, for future assets) +``` + +## Related Code Files + +### Files to Create +- `web-client/package.json` +- `web-client/vite.config.js` +- `web-client/index.html` +- `web-client/src/main.js` +- `web-client/src/config/game-config.js` +- `web-client/src/scenes/boot-scene.js` + +### Files to Modify +- None + +## Implementation Steps + +1. Create `web-client/` directory at repo root +2. Create `package.json` with: + - `name: "caro-web-client"` + - `type: "module"` + - `scripts: { "dev": "vite", "build": "vite build", "preview": "vite preview" }` + - `dependencies: { "phaser": "^3.80.0" }` + - `devDependencies: { "vite": "^6.0.0" }` +3. Create `vite.config.js`: + ```js + import { defineConfig } from 'vite'; + export default defineConfig({ + server: { port: 5173 } + }); + ``` +4. Create `index.html`: + - Minimal HTML5 boilerplate + - `
` -- Phaser mounts here + - `` -- DOM menus render here (hidden by default) + - `` + - Basic CSS: body margin 0, background #1a1a2e, flex-center the container, overlay absolute positioned over canvas +5. Create `src/config/game-config.js`: + - Export Phaser config object: `type: Phaser.AUTO`, `width: 800`, `height: 800` + - `scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }` + - `parent: 'game-container'` + - `backgroundColor: '#2d2d44'` + - `scene: [BootScene]` (import from scenes) +6. Create `src/scenes/boot-scene.js`: + - Extends `Phaser.Scene`, key: `'BootScene'` + - `create()`: display centered "Loading..." text + - Will be expanded in Phase 3 to transition to MenuScene +7. Create `src/main.js`: + - Import config from `game-config.js` + - `new Phaser.Game(config)` + - Export game instance for potential service access +8. Run `npm install` and `npm run dev` to verify Phaser boots + +## Success Criteria +- [ ] `npm run dev` starts Vite on port 5173 +- [ ] Browser shows Phaser canvas with "Loading..." text +- [ ] No console errors +- [ ] `npm run build` produces working static build in `dist/` + +## Risk Assessment +- **Node.js not installed:** User must have Node.js. Document in README. +- **Phaser version mismatch:** Pin to `^3.80.0` for stability. + +## Next Steps +- Phase 2: Services layer (can start immediately after scaffold) diff --git a/plans/260410-0913-phaser-web-client/phase-02-services-layer.md b/plans/260410-0913-phaser-web-client/phase-02-services-layer.md new file mode 100644 index 0000000..34f4e91 --- /dev/null +++ b/plans/260410-0913-phaser-web-client/phase-02-services-layer.md @@ -0,0 +1,190 @@ +# Phase 2: Services Layer + +## Context Links +- [Plan overview](plan.md) +- [Phase 1: Scaffold](phase-01-project-scaffold.md) +- Server `Msg` entity: `landlords-common/.../entity/Msg.java` -- `{code, data, info}` +- Server event codes: `landlords-common/.../enums/ServerEventCode.java`, `ClientEventCode.java` +- Server WS handler: `landlords-server/.../handler/WebsocketTransferHandler.java` + +## Overview +- **Priority:** P1 (all scenes depend on these services) +- **Status:** Pending +- **Blocked by:** Phase 1 +- **Description:** Build three decoupled service modules: WebSocket connection, event bus, game state. Plus protocol constants extracted from server source. + +## Key Insights +- Server WS message format: `{"code": "CODE_...", "data": "json_string_or_plain_string", "info": ""}` +- `data` field is a JSON **string** (not nested object) -- must `JSON.parse(data)` when data is structured +- Server sends `CODE_CLIENT_CONNECT` + `CODE_CLIENT_NICKNAME_SET` ~2s after WS handshake (server has `Thread.sleep(2000L)`) +- Heartbeat: server reads idle timeout; client must send `CODE_CLIENT_HEAD_BEAT` every ~50s +- Server ignores heartbeat messages (no handler called, just keeps connection alive) + +## Architecture + +``` +connection-service.js + |-- wraps browser WebSocket + |-- auto-reconnect with backoff + |-- heartbeat timer (50s interval) + |-- on message: parse JSON -> event-bus.emit(code, parsedData) + |-- send(code, data): serialize to Msg format -> ws.send() + +event-bus.js + |-- on(event, callback): subscribe + |-- off(event, callback): unsubscribe + |-- emit(event, data): notify all subscribers + +game-state-service.js + |-- stores: clientId, nickname, roomId, isBlack, isMyTurn, boardState[][], moves[] + |-- reset methods for new game / exit room + |-- no logic, pure state container + +protocol-constants.js + |-- SERVER_EVENTS: all CODE_CLIENT_* codes (server -> client) + |-- CLIENT_EVENTS: all CODE_* codes (client -> server) + |-- string constants, no enums needed +``` + +### Data Flow: Sending a Move +``` +GameScene.onBoardClick(row, col) + -> connectionService.send('CODE_GAME_MOVE', JSON.stringify({row, col})) + -> ws.send('{"code":"CODE_GAME_MOVE","data":"{\"row\":7,\"col\":7}","info":""}') +``` + +### Data Flow: Receiving a Move +``` +ws.onmessage(frame) + -> JSON.parse(frame.data) => {code: "CODE_GAME_MOVE_SUCCESS", data: "{\"row\":7,...}", info: ""} + -> eventBus.emit('CODE_GAME_MOVE_SUCCESS', {row:7, col:7, piece:"BLACK", playerNickname:"p1", playerId:1}) + (data string auto-parsed to object by connection-service) +``` + +## Related Code Files + +### Files to Create +- `web-client/src/services/connection-service.js` +- `web-client/src/services/event-bus.js` +- `web-client/src/services/game-state-service.js` +- `web-client/src/config/protocol-constants.js` + +### Files to Modify +- None + +## Implementation Steps + +### 1. `protocol-constants.js` + +Export two frozen objects with all event code strings: + +```js +/** @enum {string} Codes the client sends TO the server */ +export const ServerEventCode = Object.freeze({ + NICKNAME_SET: 'CODE_CLIENT_NICKNAME_SET', + INFO_SET: 'CODE_CLIENT_INFO_SET', + ROOM_CREATE: 'CODE_ROOM_CREATE', + ROOM_CREATE_PVE: 'CODE_ROOM_CREATE_PVE', + ROOM_JOIN: 'CODE_ROOM_JOIN', + GET_ROOMS: 'CODE_GET_ROOMS', + GAME_MOVE: 'CODE_GAME_MOVE', + GAME_READY: 'CODE_GAME_READY', + CLIENT_EXIT: 'CODE_CLIENT_EXIT', + GAME_WATCH: 'CODE_GAME_WATCH', + GAME_WATCH_EXIT: 'CODE_GAME_WATCH_EXIT', + HEARTBEAT: 'CODE_CLIENT_HEAD_BEAT', +}); + +/** @enum {string} Codes the server sends TO the client */ +export const ClientEventCode = Object.freeze({ + CLIENT_CONNECT: 'CODE_CLIENT_CONNECT', + NICKNAME_SET: 'CODE_CLIENT_NICKNAME_SET', + SHOW_OPTIONS: 'CODE_SHOW_OPTIONS', + SHOW_ROOMS: 'CODE_SHOW_ROOMS', + ROOM_CREATE_SUCCESS: 'CODE_ROOM_CREATE_SUCCESS', + ROOM_JOIN_SUCCESS: 'CODE_ROOM_JOIN_SUCCESS', + ROOM_JOIN_FAIL_FULL: 'CODE_ROOM_JOIN_FAIL_BY_FULL', + ROOM_JOIN_FAIL_INEXIST: 'CODE_ROOM_JOIN_FAIL_BY_INEXIST', + GAME_STARTING: 'CODE_GAME_STARTING', + GAME_MOVE_SUCCESS: 'CODE_GAME_MOVE_SUCCESS', + GAME_MOVE_INVALID: 'CODE_GAME_MOVE_INVALID', + GAME_MOVE_OCCUPIED: 'CODE_GAME_MOVE_OCCUPIED', + GAME_MOVE_OUT_OF_BOUNDS: 'CODE_GAME_MOVE_OUT_OF_BOUNDS', + GAME_MOVE_NOT_YOUR_TURN: 'CODE_GAME_MOVE_NOT_YOUR_TURN', + GAME_OVER: 'CODE_GAME_OVER', + GAME_READY: 'CODE_GAME_READY', + CLIENT_EXIT: 'CODE_CLIENT_EXIT', + CLIENT_KICK: 'CODE_CLIENT_KICK', + GAME_WATCH: 'CODE_GAME_WATCH', + GAME_WATCH_SUCCESSFUL: 'CODE_GAME_WATCH_SUCCESSFUL', + PVE_DIFFICULTY_NOT_SUPPORT: 'CODE_PVE_DIFFICULTY_NOT_SUPPORT', +}); +``` + +### 2. `event-bus.js` + +Simple pub/sub: +- `_listeners` Map of `event -> Set