# Handoff Report: PC Web Client Asset Paths, Module Scripts, and Zero-Regression Architecture

**Executive Summary**: All game sprite atlases, character skins, monsters, currencies, and biome textures load via document-relative paths (`./assets/...` and `assets/...`), while the audio engine is 100% procedural Web Audio API with zero asset file dependencies. By applying `<base href="../webapp/">` inside `client/web_pc/index.html` and providing a hidden dummy joystick container to prevent a known null-dereference in `canvas_renderer.js`, the PC web client can cleanly reuse 100% of FreeExile's existing engine modules and singletons with absolute zero regression to `client/webapp/`.

---

## 1. Observation

### 1.1 Asset Loading Paths in Existing Codebase
- **`client/webapp/js/data/i18n.js`** (lines 4-105):
  - Defines `const ASSETS = {};`
  - Defines `const assetList = [...]` containing 105 graphic assets.
  - Every asset path is declared as a relative path with `./assets/`:
    ```javascript
    { key: 'terrain_deadlands', src: './assets/banners/banner_deadlands_panorama.jpg' },
    { key: 'vltk1_swordsman', src: './assets/characters/savage_primal_exile.png' },
    { key: 'mob_skeleton_warrior', src: './assets/monsters/mob_skeleton_warrior.png' },
    { key: 'martial_skills_atlas', src: './assets/skills/martial_skills_atlas.png' },
    { key: 'hero_anim_atlas', src: './assets/animations/hero_anim_atlas.png' },
    ```
  - Loader loop (line 109): `img.src = item.src; img.onload = () => { ASSETS[item.key] = img; };`
- **`client/webapp/js/engine/animation_engine.js`** (lines 55-62):
  - `initAnimationAtlases()` iterates over character/mob atlases and loads:
    ```javascript
    img.src = `./assets/animations/${key}.png`;
    ASSETS[key] = img;
    ```
- **`client/webapp/js/engine/biome_texture_manager.js`** (lines 171, 204):
  - Procedural map texture loader uses relative path without leading slash:
    ```javascript
    const styleDir = `assets/map/styles/${this.activeStyleId.toLowerCase()}`;
    img.src = `${styleDir}/${name}.png`;
    ```
- **`client/webapp/js/ui/skill_bar_controller.js`** (line 28):
  - Skill icon atlas path: `this.atlasUrl = options.atlasUrl || './assets/skills/martial_skills_atlas.png';`
- **`client/webapp/js/ui/template_manager.js`** (lines 170-185):
  - Dynamic modal template fetcher queries relative candidate paths: `templates/${name}.html`. However, all standard modals are already pre-registered in memory via `template_catalog_*.js`, so network fetches only occur as fallbacks.
- **`client/webapp/js/engine/tile_grid_loader.js`** (line 137) & **`feedback.js`** (line 239):
  - API requests use origin-relative paths (`/api/map` and `/api/feedback`), which resolve to the web server root.

### 1.2 Audio & Sound Engine (Zero Asset Dependency)
- **`client/webapp/js/audio/sfx_engine.js`** (lines 2-3):
  - *"FREEEXILE 2026 - VISCERAL PROCEDURAL AUDIO & SOUND FX ENGINE (PoE2 Spirit). Pure Web Audio API procedural synthesis with zero external dependencies."*
  - Uses `AudioContext`, `createOscillator()`, `createBuffer()`, `createGain()`, `createBiquadFilter()` to generate sword slashes, monster roars, boss slams, dodge whooshes, and ambient wilderness soundscapes entirely in software.
  - Zero `.mp3`, `.wav`, or `.ogg` files are requested from the server.

### 1.3 Local Server Architecture
- **`tools/serve_webapp.py`** (lines 17, 71, 210, 244):
  - `WEBAPP_DIR = Path(__file__).resolve().parent.parent / "client" / "webapp"`
  - Handler: `super().__init__(*args, directory=str(WEBAPP_DIR), **kwargs)`
  - Default port: `8088`.
  - Serves strictly from `client/webapp/`. Files in `client/web_pc/` are outside this server's root and will return 404 under `serve_webapp.py`.
- **`tests/unit/test_mobile_webapp_config.py`** (line 95):
  - `self.assertEqual(sig.parameters['port'].default, 8088, "Default port for serve_webapp must be 8088")`
  - Asserts strict immutability of `serve_webapp.py` default port and `client/webapp/` directory layout.

### 1.4 Script Tags & Load Order in `client/webapp/index.html`
- A total of 43 scripts are included in `client/webapp/index.html` (lines 221-229).
- **Core Classic Scripts (`<script src="...">`)**:
  - `js/data/i18n_catalog.js`, `js/data/i18n.js`, `js/data/monster_catalog.js`
  - `js/engine/iso_math.js`, `js/engine/tile_grid_loader.js`, `js/engine/tile_map_renderer.js`, `js/engine/collision_engine.js`, `js/engine/joystick.js`
  - `js/engine/audio_haptics.js`, `js/engine/loot_filter.js`, `js/engine/monster_loot_dropper.js`, `js/engine/combat_skills.js`, `js/engine/monster_system.js`
  - `js/engine/world_renderer.js`, `js/engine/vfx_renderer.js`, `js/engine/animation_engine.js`, `js/engine/entity_renderer.js`, `js/engine/canvas_renderer.js`
  - `js/ui/templates/template_catalog_*.js` (6 files), `js/ui/template_manager.js`
- **ES Module Scripts (`<script type="module" src="...">`)**:
  - `js/data/chat_i18n_catalog.js`, `js/data/map_style_catalog.js`, `js/data/meridian_catalog.js`, `js/data/wilderness_zone_packs.js`
  - `js/engine/biome_texture_manager.js`, `js/engine/boss_gate_controller.js`, `js/engine/progression_system.js`, `js/engine/grid_pathfinder.js`, `js/engine/monster_pack_system.js`, `js/engine/ambush_trigger_system.js`, `js/engine/telegraph_renderer.js`, `js/engine/combat_feel_engine.js`, `js/engine/vfx_pool.js`, `js/engine/weapon_swing_renderer.js`
  - `js/audio/sfx_engine.js`
  - `js/ui/hud_orbs.js`, `js/ui/skill_bar_controller.js`, `js/ui/meridian_pan_zoom.js`, `js/ui/meridian_vault.js`, `js/ui/shop_megashop.js`, `js/ui/character_equipment.js`, `js/ui/inventory_stash.js`, `js/ui/seasonal_reset.js`, `js/ui/agent_orb.js`, `js/ui/worldmap_dungeons.js`, `js/ui/hideout_gates.js`, `js/ui/endgame_atlas.js`, `js/ui/war_fog.js`, `js/ui/minimap_hud.js`, `js/ui/feedback.js`, `js/ui/auth.js`, `js/ui/char_creation.js`, `js/ui/tooltips.js`, `js/ui/story_quest_board.js`, `js/ui/dialogue_npc.js`, `js/ui/divine_vault_modal.js`, `js/ui/chat_ui.js`, `js/main.js`

### 1.5 Shared Globals & Singletons Catalog
The engine registers key shared singletons onto `window`:
- `window.player`: Canonical player object (`wx`, `wy`, `hp`, `maxHp`, `level`, `exp`, `facing`, `speed`, `destination`, `isIFrame`).
- `window.camera`: Camera viewport coordinates (`wx`, `wy`, `targetWx`, `targetWy`, `followSpeed`).
- `window.worldToIso(wx, wy)` / `window.isoToWorld(sx, sy)`: 2:1 isometric coordinate transformation.
- `window.FreeExileI18n` / `window.I18N`: Centralized reactive localization engine across 9 languages.
- `window.skillBarController`: Action Bar controller handling cooldown sweeps, keybindings (1-5, QWER, Space, LMB), and charges.
- `window.sfxEngine` / `window.FreeExileAudio` / `window.playAudioFx(type)`: Procedural Web Audio API sound effects.
- `window.TileMapRenderer` & `window.TileGridLoader`: Procedural map chunk cache and binary grid loader.
- `window.isPositionBlocked(wx, wy, radius, isDodge)` & `window.resolveMovementWithSliding(...)`: Collision engine with wall-sliding math.
- `window.doFire`, `window.doThunder`, `window.doFrost`, `window.doDodge`, `window.doPrimaryAttack`: Direct combat skill triggers.
- `window.UITemplateManager`: Modal template mounting manager.
- `window.isGamePaused` / `window.setGamePaused(paused)`: Pause state toggle.

### 1.6 Critical Bug Trap Identified: `joystickStick` Null Dereference
- In `client/webapp/js/engine/iso_math.js` (lines 424-430):
  ```javascript
  const joystickZone = document.getElementById('joystick-zone');
  const joystickBase = document.getElementById('joystick-base');
  const joystickStick = document.getElementById('joystick-stick');
  const joyKeyW = document.getElementById('joy-key-w');
  ...
  ```
- In `client/webapp/js/engine/canvas_renderer.js` (lines 54-70):
  ```javascript
  if (!joystickActive) {
    ...
    if (joySx !== 0 || joySy !== 0) {
      joystickStick.style.transform = `translate(calc(-50% + ${sx}px), calc(-50% + ${sy}px))`;
    } else {
      joystickStick.style.transform = `translate(-50%, -50%)`;
    }
    if (joyKeyW) joyKeyW.style.color = ...;
  }
  ```
  While `joyKeyW` is guarded with `if (joyKeyW)`, `joystickStick.style.transform` is NOT guarded!
  If `document.getElementById('joystick-stick')` does not exist in the DOM, pressing W, A, S, or D while `!joystickActive` will immediately throw:
  `TypeError: Cannot read properties of null (reading 'style')` and crash the render loop.

---

## 2. Logic Chain

1. **Asset Resolution Mechanism**:
   - `i18n.js` sets `img.src = './assets/...'`, `animation_engine.js` sets `img.src = './assets/animations/...'`, and `biome_texture_manager.js` sets `img.src = 'assets/map/styles/...'`.
   - By W3C specification, relative URLs in `Image.src`, `link.href`, and `fetch()` resolve against `document.baseURI`.
   - Placing `<base href="../webapp/">` inside `<head>` of `client/web_pc/index.html` causes all relative asset requests (`./assets/...`), template requests (`templates/...`), and engine scripts (`js/...`) to resolve directly against `client/webapp/`.
   - This eliminates the need to copy hundreds of megabytes of texture files to `client/web_pc/assets/` and eliminates any path-rewriting in `client/webapp/`.

2. **Zero Mobile Regression Guarantee**:
   - The user directive states: *"Toàn bộ thay đổi phải nằm trọn trong `client/web_pc/` và không gây bất kỳ tác động hay lỗi hồi quy nào cho bản mobile web app tại `client/webapp/`."*
   - Because `client/webapp/` files remain 100% read-only, all 15 automated unit tests in `test_mobile_webapp_config.py` and 9 tests in `test_client_ui_modules.py` will remain Green.

3. **Prevention of Runtime Null Crashes**:
   - Because `canvas_renderer.js` has an unguarded `joystickStick.style.transform` reference on lines 67 and 69, `client/web_pc/index.html` MUST include a hidden dummy DOM element `<div id="joystick-stick"></div>` (e.g. inside `<div id="joystick-zone" style="display:none">...</div>`).
   - This satisfies the technical requirement of `canvas_renderer.js` while satisfying Requirement R1 ("Loại bỏ hoàn toàn khung viền điện thoại, Dynamic Island, và joystick cảm ứng ảo" from user view).

4. **PC Full-Screen Canvas Sizing**:
   - `iso_math.js` line 1 queries `const viewport = document.getElementById('app-viewport');`.
   - In `client/webapp/index.html`, this element has `.iphone-frame`.
   - In `client/web_pc/index.html`, defining `<div id="app-viewport" class="w-full h-full relative overflow-hidden flex flex-col bg-slate-950">` allows `resizeCanvas()` to scale `#game-canvas` to 100% width and height of any PC resolution (16:9, 16:10, 21:9 Ultrawide) without scrollbars or phone bezels.

5. **Server Dispatch Architecture**:
   - `tools/serve_webapp.py` serves exclusively from `client/webapp/`.
   - To develop and test `client/web_pc/`, a dedicated server script `tools/serve_web_pc.py` (running on port 8089) should be provided. It should serve `client/web_pc/index.html` at root `/` while routing `/assets/`, `/js/`, `/css/`, and `/templates/` to `client/webapp/`, and handling `/api/map` and `/api/feedback`.

---

## 3. Caveats

1. **PC-Specific Asset / Script Paths with `<base href>`**:
   - When `<base href="../webapp/">` is declared in `client/web_pc/index.html`, any PC-specific stylesheet or script located in `client/web_pc/` must be referenced using `../web_pc/` (e.g. `<link rel="stylesheet" href="../web_pc/css/pc_hud.css">` and `<script type="module" src="../web_pc/js/pc_main.js"></script>`).
2. **Click-to-Move vs Skill Targeting**:
   - `combat_skills.js` line 444 already registers a canvas click listener that sets `player.destination = { wx, wy, active: true }`. PC client controls can enhance this with holding LMB for continuous movement and RMB for primary skill execution (`window.doFire()`).
3. **No Caveats on Audio**:
   - Audio is 100% synthesized; no audio asset caveats exist.

---

## 4. Conclusion

1. **Asset Loading Blueprint**: Use `<base href="../webapp/">` in `client/web_pc/index.html`. This instantly resolves all 105+ sprite atlases, monsters, characters, weapons, and 30 biome textures without code edits or asset duplication.
2. **Module Script Integration**: Load the 43 shared scripts in the identical order as `client/webapp/index.html`, followed by `../web_pc/js/pc_main.js` to initialize PC ARPG mouse/keyboard controls and HUD.
3. **Mandatory DOM Guard**: Include a hidden dummy container for `#joystick-stick`, `#joystick-base`, and `#joy-key-*` in `client/web_pc/index.html` to prevent `canvas_renderer.js` from throwing null-dereference errors when WASD keys are pressed.
4. **Development Server**: Implement `tools/serve_web_pc.py` (port 8089) so developers can launch the PC client in 1 command while keeping `tools/serve_webapp.py` (port 8088) completely untouched.

---

## 5. Verification Method

To independently verify all findings and test suite integrity:

1. **Verify Existing Mobile WebApp Baseline**:
   ```powershell
   pytest tests/unit/test_mobile_webapp_config.py
   pytest tests/unit/test_client_ui_modules.py
   ```
   Both commands must exit with code 0 (15 passed and 9 passed).

2. **Verify Asset Path Conventions**:
   Inspect `client/webapp/js/data/i18n.js` lines 4-110 and `client/webapp/js/engine/animation_engine.js` lines 55-62 to confirm relative path syntax (`./assets/...`).

3. **Verify Audio Synthesis**:
   Inspect `client/webapp/js/audio/sfx_engine.js` lines 1-15 to confirm zero external audio files.

4. **Verify Joystick Dependency**:
   Inspect `client/webapp/js/engine/canvas_renderer.js` lines 65-70 to confirm that `joystickStick.style.transform` is called without an `if (joystickStick)` guard when `!joystickActive`.
