# Three.js 遊戲原型集

五支不同類型的網頁遊戲原型，用來驗證「瀏覽器能做什麼樣的遊戲」。
免安裝、開連結就跑；沒有 build 步驟，Three.js 由 CDN 載入。

## 執行

Apache 開著就能跑：

```
http://localhost/etung/three/games/
```

> 必須走 http。ES Module 在 `file://` 底下會被 CORS 擋掉。

只有 **04 佔地盤**的連線模式需要額外開伺服器（見下方）。

## 五支原型

| # | 名稱 | 類型 | 狀態 |
| --- | --- | --- | --- |
| 01 | 熔爐 | 光影解謎（Sokoban + 射線遮蔽） | 原型，3 關 |
| 02 | 防線 | 塔防 | **打磨最完整**，真實素材 |
| 03 | 堆疊 | 物理高分（Rapier） | 原型 |
| 04 | 佔地盤 | 多人 .io（伺服器權威） | 原型，附單機 bot |
| 05 | 品牌展示 | 捲動驅動 3D 敘事 | 可用模板 |
| 06 | 馴獸誌 | 養成 RPG（RO + POE + 魔力寶貝） | v1，核心循環完整 |

---

## 02 防線（重點）

塔防，十波。點空地建造、點已有的塔升級（最高 Lv.3，每升一級塔身多疊一節模組）。

### 四種塔

| 塔 | 造價 | 角色 | 特性 |
| --- | --- | --- | --- |
| 脈衝塔 | 50 | 快速單體 | DPS 26、射程 3.4。每金效率最高的主力 |
| 爆裂塔 | 120 | 範圍傷害 | 一發打一片，專清成群小兵 |
| 狙擊塔 | 155 | 超遠單體 | 射程 6.4（近兩倍），單發 105 |
| 緩速塔 | 90 | 支援 | 不造成傷害，範圍內敵人減速 50% |

緩速塔是**光環**而非發射型：不打子彈，改成持續標記範圍內的敵人。
多座重疊時取最強的一個而非相乘——相乘會讓疊塔變成無敵解。

### 平衡

**是量出來的，不是憑感覺調的。** `config.js` 的 `DIFFICULTY` 與 `TOWERS`
都可以在執行期改，所以能寫程式跑完整場來掃描數值。

| 策略 | 結果 |
| --- | --- |
| 脈衝 + 爆裂 + 緩速 | 通關，剩 19 命 ← 最佳解 |
| 四種均衡 | 通關，剩 13 命 |
| 脈衝 + 爆裂（無支援） | 通關，剩 13 命 |
| 全脈衝塔 | 通關，剩 4 命 |
| 全狙擊塔 | 通關，剩 2 命 |
| 全緩速塔 | 死於第 3 波（沒有傷害，正確） |
| 最佳組合但不升級 | 死於第 10 波 |

設計上要的就是這個形狀：有明確最佳解、單一策略會被狠狠懲罰、升級是必需品。

狙擊塔的數值特別難調。它的「每金 DPS」只有脈衝塔的一半（0.28 vs 0.53），
換來的是近兩倍射程。傷害調到 140 時純狙擊流會變成通 17 命的支配解，
其他三種塔就失去意義；調到 68 時它又變成純粹的陷阱選項（帶著它會輸）。
105 是唯一同時滿足「混搭時有用」與「單壓時只能勉強撐過」的值。

**難度成長靠「數量」而不是「血量」。** 純指數血量成長會讓前八波毫無張力、
成敗全押在最後一隻王身上（這是第一版的實際狀況，掃描時抓出來的）。
數量成長同時壓迫 DPS 與範圍傷害，爆裂塔才有存在意義。

---

## 06 馴獸誌（v1）

三款遊戲各取一塊：

| 來源 | 取用的系統 |
| --- | --- |
| 魔力寶貝 | 回合制戰鬥、五行相剋、捕捉與資質 |
| POE | 隨機詞綴裝備、稀有度階級 |
| RO | 等級成長、區域推進 |

**介面刻意幾乎沒有文字。** 狀態全部用頭像、血條、五行色點與數字表達；
四個指令是圖示按鈕；裝備只顯示首字與稀有度顏色。
所有說明都收在 `title` 裡，滑鼠停留才出現。

頭像不是另外準備的圖檔，而是**用戰鬥中的同一份模型離線渲染出來的**
（`lib/icon.js`）——換模型時圖示自動跟著換。

### 平衡（跑幾千場模擬調出來的）

`systems.js` 的 `BALANCE` 是旋鈕，可在執行期修改以便掃描。

| 發現 | 修正 |
| --- | --- |
| 戰鬥只有 2–3 回合，五行與蓄氣機制來不及發揮 | `hpScale` 1 → 3.2，變成 7–8 回合 |
| 高防高血互毆拖到 24 回合 | 加入疲勞機制（第 8 回合後傷害遞增），降到 14 回合 |
| 區間 12–22 的區域，玩家 Lv13 進去會被 Lv22 野怪處決 | 收窄區間，等級改用三角分佈往中間集中 |
| 起始寵物一路硬打，敗率 76% | 野生資質平均降到約 42；捕捉換寵後敗率降到 34% |

最後的結果：只用起始寵物硬撐要打 213 場且一路慘輸，
抓到合適的物種培養則 82 場就能過絕巔——**捕捉這個核心動作真的有回報**。

### 還沒做的

- 職業轉職樹（RO）與天賦樹（POE）
- 寵物進化、合成
- 裝備重鑄
- 存檔（目前重整就重來）

---

## 04 佔地盤 · 開伺服器

零相依，不用 `npm install`：

```bash
cd three/games/04-territory
node server/server.mjs
```

然後在遊戲畫面左下角切到「連線伺服器」（預設 `ws://localhost:8787`）。
網址帶 `?server=ws://…` 可以直接連線，方便分享。

沒開伺服器也能玩——預設是**單機 bot 模式**，跑的是同一份規則模擬
（`shared/sim.js` 被 Node 伺服器與瀏覽器共用），所以單機不是另外寫的簡化版。

---

## 素材

| 來源 | 內容 | 授權 |
| --- | --- | --- |
| [Kenney · Tower Defense Kit](https://kenney.nl/assets/tower-defense-kit) | 30 個 `.glb` 模型 + 色票貼圖 | CC0 |
| [Kenney · Sci-fi Sounds](https://kenney.nl/assets/sci-fi-sounds) | 雷射、撞擊、爆炸 | CC0 |
| [Kenney · Interface Sounds](https://kenney.nl/assets/interface-sounds) | 確認、錯誤、點擊 | CC0 |
| [Kenney · Music Jingles](https://kenney.nl/assets/music-jingles) | 勝負短曲 | CC0 |

全部 CC0（公眾領域，可商用、不需標註）。授權原文在 `assets/KENNEY-LICENSE.txt`。
`assets/` 總共 1.4 MB。

音效檔名是語意化的（`shoot-pulse`、`kill-a`、`leak`…），要換聲音直接覆蓋
`assets/audio/` 底下的同名 `.ogg` 就好，不用改程式。

### 還缺的：循環背景音樂

目前只有勝負短曲，沒有持續播放的 BGM。原本要用 Replicate 生成，
但帳號額度不足（`402 Insufficient credit`）。補上的方式：

1. 儲值後用 `meta/musicgen` 生成，或
2. 找一首 CC0 的循環曲丟進 `assets/audio/bgm.ogg`，
   在 `game.js` 的 `startWave()` 裡用 `sound` 播放並設 `loop`

---

## 檔案結構

```
index.html              入口頁
assets/
  models/               .glb 模型 + Textures/colormap.png
  audio/                .ogg 音效
lib/
  stage.js              renderer/scene/camera 啟動（支援正交相機）
  loop.js               主迴圈與固定時間步
  input.js              鍵盤與指標
  sound.js              取樣式音效播放（WebAudio）
  models.js             GLB 載入、尺寸正規化、抽出 InstancedMesh 來源
  juice.js              粒子、畫面震動、跳出的數字、頓幀
  embers.js             飄浮餘燼粒子
  hud.css               共用介面樣式
01-forge/               levels.js（關卡資料）· game.js · main.js
02-defense/             config.js（難度與砲塔）· game.js · main.js
03-stack/               game.js · main.js
04-territory/           shared/sim.js（規則）· server/server.mjs · net.js · game.js
05-showcase/            scene.js（鏡位與熱點）· main.js
```

---

## 實作筆記（踩過的坑）

**1. 粒子大小不能用魔術常數**

第一版寫 `gl_PointSize = aSize * (260.0 / -mv.z)`，那個 `260` 是憑感覺湊的。
換一個鏡頭距離之後，每顆粒子算出來三百多像素，加法混合疊起來整個畫面全白。
正確做法是用投影矩陣換算：`projectionMatrix[1][1]` 就是 `1/tan(fov/2)`，
配合繪圖緩衝區高度可以對任何視角、任何解析度算出正確的像素大小。

**2. 光源太低的話，調亮度是沒有用的**

熔爐那支一開始整個地板是黑的。原因不是光不夠亮，而是火盆的光只離地 0.5 單位，
等於用極斜的角度掠過地面，漫反射項幾乎是零。抬到 1.15 才照得到地。
**先確認幾何關係，再調數值。**

**3. `readPixels` 讀不到東西，不代表畫面是黑的**

WebGLRenderer 沒開 `preserveDrawingBuffer` 的話，畫面合成後緩衝區就失效，
讀回來是垃圾值。我一度以為場景過曝，其實是讀取方式錯了。

**4. 光線遮蔽判定不要用格子 DDA**

格子演算法在光線擦過格線時會把兩側格子都算成遮蔽，導致關卡一開始就已經解開。
改用解析解（線段 vs 圓、線段 vs AABB），而且判定用的半徑跟畫出來的柱子一樣粗——
**看起來擋住就是真的擋住**。

**5. `ctx.filter` 是逐一繪圖指令套用的**

開著 blur 直接畫幾百個字符，等於對整張畫布做上千次全畫布模糊，分頁會凍住數十秒。
要先把筆畫清晰地畫進暫存 canvas，再整層 `drawImage` 一次性模糊。

**6. Kenney 的 GLB 不是自包含的**

它們引用外部的 `Textures/colormap.png`，只複製 `.glb` 會全部變成無貼圖的白模型。
貼圖必須放在模型目錄底下的 `Textures/`。

**7. 物理一定要固定時間步**

`lib/loop.js` 的 `fixedStepper`。不脫鉤的話 144Hz 與 60Hz 螢幕會疊出完全不同的結果，
掉幀的瞬間物體還會直接穿過地板。

**8. 測試寫錯比程式寫錯更常發生**

佔地盤的「自撞」與「互砍」測試各失敗一次，兩次都是測試本身的問題：
第一次的路徑根本沒跨過自己的軌跡；第二次是兩個玩家在測試期間都持續移動，
路徑早就不受控了。**測規則就要直接構造狀態、只跑一個 tick**，
不要用「讓角色走過去」的方式，因為那還牽扯到其他所有規則。
