... ของเดิมทั้งหมด ...
... คำอธิบาย ...
```
การแสดงผล:
- `applyModeDisplay()` ตั้ง `hidden` ให้ `#monster-settings-gac` / `#monster-settings-firefly` ตามโหมด
- แท็บ Monster Setting แสดงเมื่อ `caps.monsterSettings || caps.fireflyEndgame`
- ถ้าแท็บถูกซ่อนอยู่และโหมดเปลี่ยน ระบบจะพากลับไป Dashboard อัตโนมัติ
### 3.8 ฟังก์ชันใน `js/app.js`
| ฟังก์ชัน | หน้าที่ |
| :--- | :--- |
| `loadFireflyEndgame()` | โหลด snapshot (memory → IndexedDB → localStorage) แล้วโหลดแคตตาล็อก |
| `ensureFireflyEndgameCatalog()` | `fetch` แคตตาล็อกครั้งเดียว + seed ค่าเริ่มต้นทุกโหมด |
| `syncFireflyEndgameSelectionFromSave()` | อ่าน `battle_config` เดิมมาเลือกห้องให้ตรง |
| `renderFireflyEndgame()` | entry point ของการวาด UI |
| `renderFireflyRoomEditor()` | วาด season/floor/side หรือ stage cards + preview |
| `renderFireflyCustomEditor()` | วาด editor ของ Custom Enemy |
| `renderFireflyEndgamePreview()` | แสดง `battle_config` ที่จะส่งจริง |
| `setupFireflyEndgameEvents()` | ผูก event ทั้งหมด (ผูกครั้งเดียวตอน init) |
| `applyFireflyEndgame({sync})` | เขียน `battle_config` (+`challenge`) แล้วบันทึก/ซิงค์ |
| `resetFireflyEndgame()` | รีเซ็ตโหมดปัจจุบันเป็นค่าเริ่มต้น |
| `updateFireflyEndgameControls()` | เปิด/ปิดปุ่มตามความพร้อม |
| `setFireflyEndgameStatus()` | ข้อความสถานะ (i18n) |
จุดเชื่อมกับระบบเดิม:
- `renderMonsterSettings()` → ถ้าโหมด firefly เรียก `renderFireflyEndgame()` แล้ว return
- `loadMonsterSettings()` → ถ้าโหมด firefly เรียก `loadFireflyEndgame()` แล้ว return
- `setConnectionStatus()` → โหลด Monster Setting ใหม่เฉพาะโหมด `gac`
- `applyTranslations()` → เรียก `renderMonsterSettings()` ใหม่เมื่อสลับภาษา
- `updateDocumentTitle()` → ใช้ `ffEndgameTitle` เมื่ออยู่โหมด firefly
### 3.9 Apply ทำอะไรบ้าง
`applyFireflyEndgame({ sync })`:
1. `buildFireflyBattleConfig()` จาก selection ปัจจุบัน
2. ถ้าไม่มีข้อมูลหรือเลือกไม่ครบ → แจ้ง error และไม่เขียนอะไร
3. เขียน `state.fullFreesrData.battle_config = config`
4. ถ้าโหมดเป็น `peak`:
- `extra.challenge = buildFireflyChallenge(selection, extra.challenge)`
- เก็บ `state.fireflyExtraData` + `persistFireflyExtra()`
- รีเฟรชการ์ด extra_data
5. ถ้า `sync = false` → `saveFreesrCache()` (IndexedDB + localStorage)
6. ถ้า `sync = true` → `saveFreesrChangesToServer()` ซึ่งในโหมด firefly จะไปเรียก `pushFireflySync()` → `POST /sync`
7. อัปเดตสถานะ + toast
> `pushFireflySync()` จะบังคับ `extra_data.theory_craft.mode` จาก `state.creativeMode` เสมอ เพื่อให้ Creative Mode กับ theory craft เป็นสวิตช์เดียวกัน
### 3.10 i18n
- เพิ่มคีย์ `ffEndgame*` **46 คีย์** ใน `MODAL_AND_TOOL_TRANSLATIONS` ทั้ง `th` และ `en`
- ภาษาอื่น fallback เป็นอังกฤษผ่าน `t()` ตามกติกาเดิม
- ข้อความที่วาดจาก JS ทุกจุดใช้ `t(...)` ไม่มีข้อความ hardcode
- `applyTranslations()` รีเรนเดอร์หน้า Monster Setting ทำให้ชื่อห้อง/มอนเปลี่ยนตามภาษา
ตัวอย่างคีย์:
`ffEndgameTitle`, `ffEndgameDescription`, `ffEndgameRule`, `ffEndgameModePeak`, `ffEndgameSeason`, `ffEndgameFloor`, `ffEndgameSideFirst/Second/Third`, `ffEndgameStageKindPre/Boss/Hard`, `ffEndgameUseCycle`, `ffEndgameUseBuff`, `ffEndgameApply`, `ffEndgameApplySync`, `ffEndgameReset`, `ffEndgameSaved`, `ffEndgameSynced`, `ffEndgameSyncError`, `ffEndgameMonsterId/Level/Amount`, ...
### 3.11 CSS
เพิ่มบล็อก `.ff-endgame-*` ใน `css/styles.css`:
- `.ff-endgame-picker` — grid 2 คอลัมน์อัตโนมัติ
- `.ff-endgame-sides` / `.ff-endgame-side` — ปุ่มเลือกฝั่ง
- `.ff-endgame-toggles` — สวิตช์ TurnLimit / Buff
- `.ff-endgame-preview` — preview การ์ด
- `.ff-endgame-custom-row` — แถวกรอกมอน (ยุบเป็น 2 คอลัมน์ที่ ≤640px)
- `.ff-endgame-actions` — ปุ่มล่าง (เต็มความกว้างบนมือถือ)
ค่าเป้าหมายมือถือ: ปุ่มสูง ≥44–46px, ไม่มี horizontal scroll ระดับหน้า
### 3.12 Cache busting (สำคัญ)
เปลี่ยนไฟล์แล้วต้อง bump `?v=`:
| ไฟล์ | เวอร์ชันใหม่ |
| :--- | :--- |
| `css/styles.css` | `2.36` |
| `js/app.js` | `3.32` |
| `js/i18n.js` | `2.26` (ทุก importer ต้องตรงกัน) |
| `js/endgame.js` | `1` (ไฟล์ใหม่) |
Importer ของ `i18n.js` ที่ถูกแก้: `index.html` (ทาง `app.js`), `js/app.js`, `js/avatars-data.js`, `js/equipment-data.js`, `js/item-catalog.js`, `js/unstuck.js`
---
## 4. ไฟล์ที่เพิ่ม / แก้ในรอบนี้
### เพิ่มใหม่
| ไฟล์ | รายละเอียด |
| :--- | :--- |
| `js/endgame.js` | pure helpers สำหรับเลือกห้อง + สร้าง `battle_config` |
| `scripts/build-firefly-endgame-data.cjs` | สร้างแคตตาล็อกจาก API ต้นฉบับ |
| `assets/firefly-endgame.json` | แคตตาล็อกพร้อมใช้ (~604 KB) |
| `tests/endgame.test.cjs` | unit tests ของ endgame helpers (8 tests) |
### แก้ไข
| ไฟล์ | สิ่งที่แก้ |
| :--- | :--- |
| `index.html` | แบ่ง `#monster-settings-gac` / `#monster-settings-firefly`, เพิ่ม UI picker, bump `?v=` |
| `js/app.js` | import endgame, state ใหม่, ฟังก์ชัน picker ทั้งหมด, branch ตามโหมด, เปลี่ยน title |
| `js/modes.js` | เพิ่ม capability `fireflyEndgame` |
| `js/firefly.js` | normalize `blessings` / `monsters` ให้ตรง zod |
| `js/i18n.js` | เพิ่ม 46 คีย์ th/en, bump version |
| `css/styles.css` | สไตล์ `.ff-endgame-*` + responsive |
| `js/avatars-data.js`, `js/equipment-data.js`, `js/item-catalog.js`, `js/unstuck.js` | bump `i18n.js?v=2.26` (และ bump version ของ module ปลายทางใน `app.js`) |
| `tests/firefly-payload.test.cjs` | อัปเดต expected shape + เพิ่ม test normalize |
| `tests/mode-capabilities.test.cjs` | เพิ่ม `fireflyEndgame`, คีย์ i18n, DOM hooks |
---
## 5. สิ่งที่เสร็จแล้ว (ละเอียด)
### 5.1 โหมดเชื่อมต่อ + transport
- [x] `js/modes.js` capability matrix ครบ 3 โหมด
- [x] `fireflySyncUrl()` แปลง base URL → `/sync`
- [x] `fireflyRequest()` ไม่ส่ง custom header, มี timeout, parse error body
- [x] `probeFirefly()` = connect probe (ไม่ส่ง `data`)
- [x] `pushFireflySync()` = sync เต็ม
- [x] `refreshStatus()` แยกพฤติกรรม firefly (local-only)
- [x] กัน `tool-session`, `pagehide`, `fetchLatestBattleReport` ในโหมดที่ไม่รองรับ
### 5.2 extra_data
- [x] `normalizeExtraData()` ตรวจ/เติมทุกบล็อก
- [x] ฟอร์มใน `#card-firefly-extras` (multi_path, setting, challenge, theory_craft, lua)
- [x] `readFireflyExtrasForm()` / `renderFireflyExtras()`
- [x] Creative Mode = `theory_craft.mode` ใช้ state เดียว (`state.creativeMode`)
- [x] ทุกจุดที่เปลี่ยน Creative Mode วิ่งผ่าน `setCreativeMode()`
- [x] factory reset ล้าง `state.fireflyExtraData`
### 5.3 Enka import
- [x] `js/enka-import.js` แปลง Enka → FreeSR (avatars, lightcones, relics)
- [x] UI ในแท็บ JSON ใช้ได้ทุกโหมด
- [x] proxy `/api/enka/:uid` (dev server + `vercel.json` rewrite)
- [x] paste JSON สำรองเมื่อ proxy ใช้ไม่ได้
### 5.4 UI ตามโหมด
- [x] ซ่อน tab/card ที่ backend ไม่รองรับ
- [x] ไม่ค้างอยู่แท็บที่ถูกซ่อน
- [x] JSON editor จำกัด mode ตาม backend
- [x] badge persistence / API URL hint เปลี่ยนตามโหมด
- [x] `setupServerModes()` ผูก event ครั้งเดียว
### 5.5 Firefly Endgame Picker
- [x] ดึง logic แปลงข้อมูลจาก bundle ต้นฉบับครบทุกโหมด
- [x] สร้างแคตตาล็อกแบบ static (ไม่ต้องพึ่ง API ต้นทางตอนใช้งาน)
- [x] UI เลือก season / floor / side / stage
- [x] UI Custom Enemy (wave + monster + level + amount)
- [x] preview `battle_config` ก่อนใช้
- [x] เปิด picker ที่ห้องเดิมจาก `battle_config`
- [x] Apply → เขียน `battle_config` (ทดสอบครบ MOC/PF/AS/PEAK/CE)
- [x] Peak → อัปเดต `extra_data.challenge`
- [x] Apply & sync → `POST /sync` (error path ทดสอบแล้ว, happy path ติดที่ยังไม่มีเซิร์ฟเวอร์)
- [x] i18n th/en + รีเรนเดอร์เมื่อเปลี่ยนภาษา (แก้บั๊กสถานะค้างเป็น loading แล้ว)
- [x] responsive CSS (ทดสอบจริง 360px / 768px)
- [x] เรียง season/floor ใหม่สุด–สูงสุดขึ้นก่อน
### 5.6 Tests
```
node --test tests/*.test.cjs
ℹ tests 44
ℹ pass 44
ℹ fail 0
```
ครอบคลุม:
- โครงสร้าง payload `/sync` (avatars/lightcones/relics/battle_config)
- การ normalize `blessings` / `monsters` (รวม `dynamic_key`)
- capability matrix + i18n keys + DOM hooks
- endgame helpers: default/resolve/build/challenge/find-by-stage/custom
---
## 6. สิ่งที่ยังเหลือ (ละเอียด + วิธีทำ)
### 6.1 ✅ ทดสอบ Apply / Apply & sync ในเบราว์เซอร์
**สถานะ:** ทดสอบครบทุกโหมดแล้วเมื่อ 2026-11-04 ที่ `http://localhost:5173` โดยโหลด FreeSR JSON ขั้นต่ำผ่านแท็บ JSON ก่อน
**ผลทดสอบจริง:**
- MoC → `battle_type: "MOC"`, `stage_id: 30127121`, `cycle_count: 30`, `monsters` เป็น array ของ array ของ `{monster_id, level, amount}`
- PF → `battle_type: "PF"`, `stage_id: 30326041`, `cycle_count: 4`, `blessings: 1`, 3 waves
- AS → `battle_type: "AS"`, `stage_id: 420564`, `blessings: 2`, 1 wave
- Anomaly Arbitration → `battle_type: "PEAK"`, `stage_id: 30511021`, `blessings: 3`, และ `extra_data.challenge.challenge_peak_group_id = 11` ถูกเขียนลง `localStorage['gac_srtool_firefly_extra']`
- Custom Enemy → เพิ่ม wave/มอนแล้ว Apply ได้ `battle_type: "CE"`, `stage_id: 0`, `cycle_count` ตามที่กรอก
- รีโหลดหน้า → picker กลับมาที่ห้องเดิมจาก `syncFireflyEndgameSelectionFromSave` (ทดสอบกับ Custom: กลับมาเป็น CE และ wave ตรงกับที่บันทึก)
- Apply & sync ตอนไม่มี FireflyGo → สถานะเป็น `Sync failed: Failed to fetch` (data-state = error) ไม่มี crash และ cache ถูกบันทึกก่อน sync
- เปลี่ยนภาษาไทย→อังกฤษขณะอยู่หน้า picker → สถานะเปลี่ยนเป็น `battle_config saved locally` (ไม่ค้างที่ข้อความ loading) หลังแก้บั๊กในข้อ 6.1.1
### 6.1.1 🐞 บั๊กที่พบและแก้แล้วระหว่าง audit
`#ff-endgame-status` มี `data-i18n="ffEndgameLoading"` ทำให้ `applyTranslations()` เขียนทับข้อความสถานะเป็น "กำลังโหลด…" ทุกครั้งที่เปลี่ยนภาษา แต่ `renderFireflyEndgame()` ไม่คืนค่าสถานะที่เก็บไว้
**แก้โดย:** เพิ่ม `applyFireflyEndgameStatus()` ที่อ่าน `state.fireflyEndgame.statusKey/statusState/statusVars` แล้วเรียกที่ท้าย `renderFireflyEndgame()` ทั้งสอง branch (ทั้งตอน catalog ยังไม่โหลดและตอนเรนเดอร์ปกติ) — ตอนนี้สถานะถูกต้องหลังสลับภาษา
### 6.2 ✅ ทดสอบมือถือ 360–768px
**สถานะ:** ทดสอบด้วย iframe ขนาด 360×800 และ 768×1024 (media query ภายใน iframe ทำงานเหมือน viewport จริง)
**ผลทดสอบจริง:**
| ตัวชี้วัด | 360px | 768px |
| :--- | :--- | :--- |
| `documentElement.scrollWidth <= clientWidth` | 345 = 345 (ไม่มี horizontal scroll) | 753 = 753 |
| ความสูง tab `#ff-endgame-tabs button` | 46px | 46px |
| ความสูงปุ่ม Apply ทั้ง 3 | 46px | 46px |
| ความสูง `.ff-endgame-side` | 56px | 56–71px |
| ความสูง select | 44px | — |
| `.ff-endgame-picker` | 1 คอลัมน์ (319px) | 2 คอลัมน์ (340px + 340px) |
| `.ff-endgame-custom-row` | 2 คอลัมน์ (138px + 138px) | — |
โหมด GaCy เดิมยังทำงาน: `#monster-settings-gac` แสดง, `#monster-settings-firefly` ถูกซ่อน, แท็บ save manager กลับมา, `#card-firefly-extras` ถูกซ่อน
### 6.3 🚫 ทดสอบ `POST /sync` กับ FireflyGO จริง
**ติดเพราะ:** ไม่มีเซิร์ฟเวอร์ Firefly_Srtools รันที่ `localhost:21000`
**เมื่อมีเซิร์ฟเวอร์:**
1. ตั้ง API URL เป็น `http://localhost:21000`
2. กด Connect → ต้องได้ `extra_data` กลับมา
3. Sync → ตรวจไฟล์/สถานะฝั่งเซิร์ฟเวอร์
4. ตรวจว่า `battle_config` ที่ส่งผ่าน zod (ไม่มี warning เรื่อง schema)
### 6.4 ✅ เรียงลำดับ season และ floor ให้ใหม่สุด/สูงสุดอยู่บน
**สถานะ:** ดำเนินการเรียบร้อยแล้ว ใน `renderFireflyRoomEditor()` ทั้ง `groups` และ `levels` ถูกเรียงลำดับจากมาก→น้อย (descending) ทำให้ซีซันใหม่สุดและชั้น 12 อยู่ด้านบนสุดของ Dropdown สะดวกต่อการเลือกใช้งานทันที
### 6.5 ⏳ ตรวจ `docs/` และ commit
- `docs/` เป็น untracked ตาม `AGENTS.md` — ไฟล์นี้ถูกสร้างตามคำขอของผู้ใช้
- ยังไม่ commit งานรอบนี้
- ต้องตัดสินใจว่าจะ push ขึ้น `origin/test-gay` หรือไม่ (ต้องถามก่อน)
### 6.6 ⏳ (อนาคต) อัปเดตแคตตาล็อกเมื่อเกมออก season ใหม่
```bash
node scripts/build-firefly-endgame-data.cjs
```
แล้ว bump `?v=` ของ `assets/firefly-endgame.json` ถ้ามีการอ้างอิงในอนาคต (ปัจจุบัน fetch ตรงด้วย `cache: 'force-cache'`)
---
## 7. ข้อจำกัด / ข้อควรระวัง
1. **แคตตาล็อกเป็น snapshot**: ถ้า Firefly SrTools อัปเดตข้อมูล ต้องรันสคริปต์ใหม่
2. **ไอคอนมอนเป็น CDN ภายนอก** (`cdn.punklorde.org`) — ถ้า CDN ล่มจะเห็นอิโมจิ fallback
3. **Simulated Universe ไม่อยู่ในขอบเขต** — ต้นฉบับมี แต่รอบนี้ยังไม่ทำ
4. **`amount` ถูกเซ็ตเป็น 1 เสมอ** สำหรับห้องปกติ (custom แก้ได้)
5. **`extra_data.challenge` อัปเดตเฉพาะโหมด peak** — โหมดอื่นไม่แตะค่าเดิม
6. **เขียนทับ `battle_config` ใน local cache**: ถ้าสลับกลับไปโหมด GaCy แล้วใช้ custom wave editor ค่า `amount` จะไม่ถูกอ่าน (GaCy ใช้ `max_hp`) — แต่ GaCy จะโหลด catalog จากเซิร์ฟเวอร์ใหม่เมื่อเชื่อมต่อ
7. **`normalizeBattleConfig()` ตัด key ที่ไม่รู้จัก** รวมถึง `custom_battle_lineup`
8. **ห้ามใส่ header เพิ่มใน `/sync`** เพราะจะทำให้ preflight fail
---
## 8. คู่มือรัน/ตรวจสอบ
### Unit tests
```bash
node --test tests/*.test.cjs
```
### Dev server (มี Enka proxy)
```bash
npm run dev:proxy
# หรือ
node scripts/dev-server.cjs
```
เปิด `http://localhost:5173`
> ถ้าเจอ `EADDRINUSE` ให้ปิด process ที่ถือพอร์ต 5173 ก่อน (`Get-NetTCPConnection -LocalPort 5173`)
### ตรวจ payload ก่อนซิงค์
- เปิด DevTools → Network → กรอง `/sync`
- ดู Request Payload: ต้องมี `data.battle_config.blessings` เป็น object และ `monsters` เป็น array ของ array ของ object
### ตรวจ i18n
- สลับภาษาไทย/อังกฤษ ขณะอยู่หน้า Monster Setting
- ชื่อ season, ชื่อมอน, สถานะ, ปุ่ม ต้องเปลี่ยนตามภาษา
- ภาษาที่ไม่มีการแปลจะ fallback เป็นอังกฤษ
---
## 9. Checklist ก่อน commit
- [x] `node --test tests/*.test.cjs` ผ่านทั้งหมด
- [x] เปิดเว็บโหมด firefly → Monster Setting แสดง picker
- [x] Apply เขียน `battle_config` ถูกต้อง (MOC/PF/AS/PEAK/CE)
- [x] Apply & sync แสดง error ที่แปลแล้วเมื่อไม่มีเซิร์ฟเวอร์
- [x] เปลี่ยนภาษาแล้วหน้า picker รีเรนเดอร์ และสถานะไม่ค้างที่ loading
- [x] มือถือ 360px ไม่มี horizontal scroll และปุ่ม ≥44px
- [x] โหมด GaCy เดิมยังทำงาน (Monster Setting เดิม, Aha's Help, custom waves)
- [x] `?v=` ถูก bump ครบ (`styles.css 2.36`, `app.js 3.32`, `i18n.js 2.26` ทุก importer)
- [x] ไม่มีข้อความ hardcode ใหม่ใน HTML/JS
- [x] ไม่แก้ `README.md`
- [ ] ทดสอบ `POST /sync` happy path กับ FireflyGO จริง (ต้องมีเซิร์ฟเวอร์)