# FireflyGO Endgame Room Picker — สถานะงานและคู่มือเทคนิค > อัปเดต: 2026-11-04 > ขอบเขต: งานเพิ่มโหมดเชื่อมต่อ FireflyGO (Firefly_Srtools) ใน GaCy-Srtool และหน้าตั้งค่าห้อง Endgame ให้เหมือนต้นฉบับ `https://srtools.punklorde.org` --- ## 1. สรุปสั้น ### โจทย์ เว็บ GaCy-Srtool เดิมออกแบบมาสำหรับเซิร์ฟเวอร์ **GaCy GO** ซึ่งมี REST API ครบ (`/api/status`, `/api/freesr`, `/api/persistent`, `/api/monster-settings`, ...) แต่ผู้ใช้ต้องการเชื่อมกับ **FireflyGO (Firefly_Srtools)** ซึ่งเป็นแบบ **push-only** คือมีแค่ `POST /sync` ปัญหาที่ตามมา: 1. ต้องเพิ่มโหมด FireflyGO โดยไม่ทำให้โหมด GaCy เดิมพัง 2. ต้องซ่อน UI ที่ต้องใช้ endpoint ที่ FireflyGO ไม่มี (save manager, inventory, exrank, damage analyst, ...) 3. ต้องแก้ `extra_data` ของ FireflyGO ได้ (multi-path, setting, challenge, theory_craft, lua) 4. ต้องนำเข้าข้อมูลจาก UID เกมจริง (Enka) ได้ทุกโหมด 5. **ต้องมีหน้า Monster Setting / เลือกห้อง Endgame แบบต้นฉบับ Firefly** (MoC, PF, AS, Anomaly Arbitration, Custom Enemy) ### สถานะปัจจุบัน (สรุป) | ส่วน | สถานะ | | :--- | :--- | | โหมด FireflyGO + `POST /sync` | ✅ เสร็จและ commit แล้ว (`8eb5190`) | | `extra_data` form | ✅ เสร็จ | | Enka UID → FreeSR import | ✅ เสร็จ | | ซ่อน/แสดง UI ตามโหมด | ✅ เสร็จ | | แคตตาล็อกห้อง Endgame (moc/pf/as/peak) | ✅ สร้างเสร็จ (`assets/firefly-endgame.json`) | | ตัวช่วยคำนวณ `battle_config` (`js/endgame.js`) | ✅ เสร็จ | | UI หน้า Monster Setting โหมด Firefly | ✅ เขียนเสร็จ (เรียง season/floor ใหม่สุดก่อนแล้ว) | | ปรับปรุง Auto-init FreeSR เมื่อ Apply ห้อง | ✅ เสร็จ (ไม่ติดบล็อก `ffEndgameNoData`) | | ดักจับ `player data is not loaded yet` & Mixed Content | ✅ เสร็จ (มีข้อความแจ้งเตือนชัดเจน) | | Unit tests | ✅ ผ่าน **44/44** | | ทดสอบมือถือ 360–768px ของหน้าใหม่ | ✅ ผ่าน (responsive styles พร้อม) | | ทดสอบ `POST /sync` กับ FireflyGO จริง | ⏳ รอเปิดเซิร์ฟเวอร์และตัวละครล็อกอินในเกม | | Commit งานรอบนี้ | ⏳ พร้อม commit | --- ## 2. สถาปัตยกรรมโหมดเชื่อมต่อ ระบบมี 3 โหมด เก็บใน `localStorage['gac_srtool_mode']` และนิยามความสามารถไว้ที่ `js/modes.js` | ความสามารถ | GaCy GO (`gac`) | FireflyGO (`firefly`) | RobinSR (`robin`) | | :--- | :---: | :---: | :---: | | `saveManager` | ✅ | — | — | | `exranks` | ✅ | — | — | | `inventory` | ✅ | — | — | | `monsterSettings` (อ่าน catalog จาก Client) | ✅ | — | — | | `fireflyEndgame` (picker จากแคตตาล็อกในเครื่อง) | — | ✅ | — | | `damageAnalyst` | ✅ | — | — | | `sceneCard` / `gmConsole` / `battleSummary` | ✅ | — | — | | `toolSession` | ✅ | — | — | | `extraData` | — | ✅ | — | | `ownedAvatarsOnly` | — | ✅ | — | | `jsonModes` | `freesr`, `persistent` | `freesr` | `freesr` | ไฟล์ที่เกี่ยวข้อง: - `js/modes.js` — capability matrix, `normalizeMode`, `modeSupports` - `js/app.js` → `applyModeDisplay()` — ซ่อน/แสดง tab และ card ตาม capability - `js/app.js` → `setupServerModes()` — ฟัง event เปลี่ยนโหมด (ผูกครั้งเดียว) ### กฎสำคัญ - **FireflyGO เป็น push-only**: การ "เชื่อมต่อ" คือ `POST /sync` ด้วย `{ username:'', password:'' }` แล้วอ่าน `{ status, message, extra_data, warnings, stats }` - การซิงค์คือ `POST /sync` ด้วย `{ username, password, data: FreeSRJson, extra_data }` - **ห้ามใส่ custom header** เพราะ FireflyGO ไม่รองรับ CORS preflight - สถานะการเชื่อมต่อของ FireflyGO เก็บใน `state.fireflyConnected` (ไม่ใช้ `state.isConnected` ร่วมกับ GaCy) - `refreshStatus()` ในโหมด Firefly เป็น local-only (ไม่อ่าน `/api/status`) --- ## 3. FireflyGO Endgame Room Picker ### 3.1 ภาพรวมการทำงาน ```mermaid sequenceDiagram autonumber actor User as ผู้ใช้ participant UI as app.js (Monster Setting) participant Cat as assets/firefly-endgame.json participant Save as state.fullFreesrData participant Srv as FireflyGO /sync User->>UI: เปิดแท็บ Monster Setting (โหมด FireflyGO) UI->>Cat: fetch แคตตาล็อก (ครั้งเดียว) Cat-->>UI: modes.{moc,pf,as,peak} + monsters UI->>Save: อ่าน battle_config เดิม UI->>UI: syncFireflyEndgameSelectionFromSave() UI-->>User: แสดง season / floor / side / stage / preview User->>UI: เลือกห้อง + กด "ใช้กับ battle_config" UI->>UI: buildFireflyBattleConfig() UI->>Save: เขียน battle_config opt Anomaly Arbitration UI->>UI: buildFireflyChallenge() → extra_data.challenge end UI->>UI: saveFreesrCache() User->>UI: กด "ใช้และซิงค์" UI->>Srv: POST /sync { data, extra_data } Srv-->>UI: { status, warnings } ``` ### 3.2 แหล่งข้อมูลแคตตาล็อก ต้นฉบับ Firefly SrTools เสิร์ฟตารางดิบของ Client ที่: ``` https://srtools.punklorde.org/api/data/moc https://srtools.punklorde.org/api/data/pf https://srtools.punklorde.org/api/data/as https://srtools.punklorde.org/api/data/peak https://srtools.punklorde.org/api/data/monster ``` ไฟล์ดิบมีขนาด 2–14 MB จึงทำสคริปต์ย่อข้อมูล: ```bash node scripts/build-firefly-endgame-data.cjs [cacheDir] ``` - ถ้ามี `cacheDir` และมีไฟล์อยู่แล้วจะใช้แคช (ไม่ยิงเน็ตซ้ำ) - ผลลัพธ์: `assets/firefly-endgame.json` (~604 KB) - ข้อมูล ณ วันที่สร้าง: | โหมด | จำนวนกลุ่ม (season/round) | | :--- | ---: | | `moc` (Memory of Chaos) | 58 | | `pf` (Pure Fiction) | 28 | | `as` (Apocalyptic Shadow) | 23 | | `peak` (Anomaly Arbitration) | 11 | | มอนสเตอร์ (unique) | 355 | โครงสร้างไฟล์: ```jsonc { "version": "2026-11-03", "source": "https://srtools.punklorde.org/api/data", "modes": { "moc": { "groups": [ { "id": 1037, "name": {"en":"..."}, "begin": "...", "end": "...", "levels": [ { "id": 5612, "floor": 12, "turnLimit": 30, "buffs": [ { "id": 1001, "name": {"en":"..."} } ], "sides": [ { "side": "firstNode", "stageId": 30127111, "level": 92, "name": {"en":"..."}, "waves": [[4033010, 4033020],[4044010]], "invasionBuffId": 3034002 } ] } ] } ] }, "pf": { "groups": [] }, "as": { "groups": [] }, "peak": { "groups": [ { "id": 9, "name": {"en":"..."}, "stages": [ { "kind": "pre|boss|hard", "stageId": 4001, "level": 80, "name": {"en":"..."}, "waves": [[...]], "buffs": [...], "turnLimit": 30 } ] } ] } }, "monsters": { "4033010": { "name": {"en":"...","th":"..."}, "rank": 1, "stance": 1, "weak": ["Fire"], "icon": "https://cdn.punklorde.org/asbres/spriteoutput/mostericon/monster_4033010.webp" } } } ``` > หมายเหตุ: path ไอคอนของข้อมูลต้นทางสะกดว่า `mostericon` (ตก `n`) จึงต้องใช้ตามนั้น ### 3.3 กติกาการแปลงข้อมูลจากต้นฉบับ | โหมด | แหล่งห้อง | กติกา | | :--- | :--- | :--- | | MoC | `Level[].EventList1[0]` (ครึ่งแรก), `EventList2[0]` (ครึ่งหลัง), `Tierce.EventList[0]` (โหนดที่สาม) | `stage_id` = `stage.ID`, `cycle_count` = `Level.TurnLimit`, blessings = `Level.MazeBuff` + `Invasion.MazeBuff.ID` | | PF | เหมือน MoC | เหมือน MoC | | AS | เหมือน MoC | เหมือน MoC | | Anomaly Arbitration (`peak`) | กลุ่ม `peak[event_id]` → เลือก stage จาก `[...PreLevel, BossLevel]` ตาม index | ถ้า `boss_mode === 'Hard'` ใช้ `BossConfig`; `stage_id = EventList[0].ID`; ต้องส่ง `extra_data.challenge.challenge_peak_group_id` = group id | | Custom Enemy (`custom`/`CE`) | ผู้ใช้กรอกเอง | `stage_id = 0`, ใช้ `monsters` ที่กรอก | ### 3.4 Schema ที่ FireflyGO ตรวจ (zod) จากการแกะ bundle ต้นฉบับ: ```ts battle_config = { battle_type: string, // MOC | PF | AS | CE | PEAK | DEFAULT blessings: { id: number, level: number, dynamic_key?: { key: any, value: number } }[], custom_stats: any[], cycle_count: number, stage_id: number, path_resonance_id?: number, // default 0 monsters: { monster_id: number, level: number, amount?: number }[][] } ``` จุดที่พลาดง่าย: - `blessings` **ต้องเป็น object** `{id, level}` ไม่ใช่ array ของ id - `monsters` เป็น **array ของ wave** และแต่ละตัวเป็น object - `amount` ไม่บังคับ แต่เราเติม `1` ให้เสมอเพื่อความชัดเจน `js/firefly.js → normalizeBattleConfig()` จึงแปลงค่าดิบให้เป็น shape นี้ก่อนส่งทุกครั้ง (รองรับข้อมูลเก่าที่เป็น id ล้วนด้วย) ### 3.5 ตัวช่วยคำนวณ (`js/endgame.js`) ฟังก์ชันทั้งหมดเป็น pure function (ทดสอบด้วย `node:test` ได้): | ฟังก์ชัน | หน้าที่ | | :--- | :--- | | `battleTypeForMode(mode)` | แปลง `moc/pf/as/peak/custom` → `MOC/PF/AS/PEAK/CE` | | `groupList(catalog, mode)` | คืนกลุ่มของโหมด | | `findGroup` / `findLevel` | ค้นหากลุ่ม/ชั้น | | `defaultEndgameSelection(catalog, mode)` | ค่าเริ่มต้น: ซีซันใหม่สุด + ชั้นสูงสุด + ฝั่งแรก; peak = stage บอส | | `resolveEndgameSelection(catalog, selection)` | คืน `{group, level, sides, stage}` หรือ `null` | | `findSelectionByStageId(catalog, mode, stageId, groupId)` | เปิด picker ที่ห้องเดียวกับ `battle_config` เดิม | | `buildFireflyBattleConfig(catalog, selection)` | สร้าง `battle_config` พร้อมส่ง | | `buildFireflyChallenge(selection, current)` | อัปเดต `extra_data.challenge` (เฉพาะ peak) | | `summarizeEndgameSelection` | สรุปจำนวน wave/มอน/บัฟ/รอบ ให้ UI | | `monsterInfo(catalog, id)` | ข้อมูลมอน (ชื่อ/ไอคอน) | ### 3.6 State ของ UI เพิ่มใน `state.fireflyEndgame` (`js/app.js`): ```js { activeMode: 'moc', // moc | pf | as | peak | custom catalog: null, // แคตตาล็อกที่โหลดแล้ว catalogState: 'idle', // idle | loading | ready | error catalogError: '', catalogPromise: null, // กัน fetch ซ้ำ selections: {}, // แยก selection ตามโหมด statusKey: '', statusVars: null, statusState: 'info' } ``` `selections` เก็บ object ที่ผูกกับ UI โดยตรง เช่น ```js // moc/pf/as { mode:'moc', groupId:1037, levelId:5612, side:'firstNode', useCycle:true, useBuff:true } // peak { mode:'peak', groupId:9, stageIndex:1, useCycle:true, useBuff:true } // custom { mode:'custom', customWaves:[[{monster_id:'4033010', level:'90', amount:'1'}]], customCycle:0 } ``` ### 3.7 UI ที่เพิ่มใน `index.html` `#tab-monster-settings` ถูกแบ่งเป็น 2 ส่วน: ```html
... ของเดิมทั้งหมด ...
``` การแสดงผล: - `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 จริง (ต้องมีเซิร์ฟเวอร์)