From ab01489ea14e11620c075e4d6de54d140b7f0562 Mon Sep 17 00:00:00 2001 From: CAtlas0784 Date: Sun, 18 Oct 2026 22:16:18 +0700 Subject: [PATCH] feat: add material inventory editor --- README.md | 13 ++++- index.html | 43 +++++++++++++++ js/app.js | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++++- js/i18n.js | 46 ++++++++++++++++ 4 files changed, 254 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5645509..bc73192 100644 --- a/README.md +++ b/README.md @@ -51,17 +51,23 @@ - **ปรับแต่งกรวยแสง:** สวมใส่, ปรับเลเวล 1-80, เลื่อนขั้น 0-6 และระดับการขัดเกลา Superimposition (S1-S5) - **สลับกรวยแสงประจำตัวอัตโนมัติ:** ตรวจจับและสลับใส่ Signature Lightcone ของแต่ละตัวละครได้ในคลิกเดียว -### 💾 6. จัดการไฟล์เซฟ GaC (Persistent & FreeSR Data) +### 🎒 6. คลังไอเทม (Material Inventory) +- ค้นหารายการจาก Item ID ใน catalog ของ Client ที่เซิร์ฟเวอร์กำลังใช้ พร้อมแก้จำนวนได้หลายรายการก่อนบันทึกครั้งเดียว +- ค่าในช่องที่ว่างใช้ค่าที่เซิร์ฟเวอร์สร้างตามปกติ; ล้างค่าที่เคยกำหนดเองเพื่อกลับไปใช้ค่าเริ่มต้นได้ +- การบันทึกแก้เฉพาะจำนวนไอเทมใน `freesr-data.json` ไม่เขียนทับตัวละคร, LC, Relic หรือ Battle Config +- รองรับเฉพาะ GaC-hkrpg-go ผ่าน `/api/inventory/materials`; บันทึกลงไฟล์ทันทีและต้องเข้าเกมใหม่เพื่อโหลด session พร้อมจำนวนที่แก้แล้ว + +### 💾 7. จัดการไฟล์เซฟ GaC (Persistent & FreeSR Data) - **ตั้งค่าตัวเอก (ผู้บุกเบิก):** สลับเพศ (Caelus / Stelle) และเปลี่ยนธาตุ (กายภาพ, ไฟ, จินตภาพ, ความจำ) - **สลับพาธ March 7th:** เลือกระหว่างพาธการอนุรักษ์ (น้ำแข็ง) และพาธล่าสังหาร (จินตภาพ) - **สวิตช์ระบบพิเศษ Ex-Rank:** จัดการเปิด/ปิดความสามารถพิเศษ Ex-Rank สำหรับตัวละครเฉพาะ (เช่น Jing Yuan, Dan Heng • IL, Acheron) - **ตัวแก้ไข JSON ดิบ:** ตรวจสอบ แก้ไข จัดรูปแบบ และดาวน์โหลดสำรองไฟล์ `freesr-data.json` และ `persistent` -### 💻 7. คอนโซลคำสั่ง GM (GM & Server Console) +### 💻 8. คอนโซลคำสั่ง GM (GM & Server Console) - ส่งคำสั่ง GM ไปยังเซิร์ฟเวอร์ได้โดยตรงผ่านหน้าเว็บ - ดูผลตอบกลับและตรวจสอบสถานะการทำงานของเซิร์ฟเวอร์แบบ Real-time -### 🌐 8. รองรับ 13 ภาษาทั่วโลก (Multi-Language Support) +### 🌐 9. รองรับ 13 ภาษาทั่วโลก (Multi-Language Support) - สลับเปลี่ยนภาษาได้ทันทีโดยไม่ต้องรีเฟรชหน้าเว็บ: - 🇹🇭 ไทย (Thai) - 🇺🇸 English @@ -103,6 +109,7 @@ GaC-Srtool สื่อสารกับเซิร์ฟเวอร์ GaC-h | --- | --- | --- | | `/api/status` | GET | ข้อมูลสถานะเซิร์ฟเวอร์, ทีม, ตัวละคร และพิกัด | | `/api/freesr` | GET, POST | ดึงหรืออัปเดตข้อมูล `freesr-data.json` | +| `/api/inventory/materials` | GET, POST | อ่าน Client item catalog และบันทึกจำนวนไอเทมแบบเจาะจงโดยไม่เขียนทับข้อมูลอื่น | | `/api/freesr/avatars` | GET, POST | ดึงหรืออัปเดตข้อมูลตัวละครและสถิติ | | `/api/persistent` | GET, POST | ดึงหรืออัปเดตข้อมูล `persistent` | | `/api/battle_report` | GET, POST | ดึงสถิติดาเมจและผลการต่อสู้ล่าสุด | diff --git a/index.html b/index.html index f2aa4f2..1b7718a 100644 --- a/index.html +++ b/index.html @@ -55,6 +55,7 @@ + @@ -398,6 +399,48 @@ + +
+
+
+
+
🎒 จัดการจำนวนไอเทม
+

ค้นหาด้วย Item ID แล้วแก้จำนวนหลายรายการก่อนบันทึกครั้งเดียว รายการมาจาก catalog ของ Client ที่เซิร์ฟเวอร์ใช้ ช่องว่างหมายถึงใช้ค่าเริ่มต้นของเซิร์ฟเวอร์

+
+
+ — + +
+
+
+ + + +
+
กำลังโหลดรายการจากเซิร์ฟเวอร์...
+
+
+
+ + + + + + + + + + +
Item IDจำนวนในกระเป๋า
กำลังโหลดรายการจากเซิร์ฟเวอร์...
+
+
+ + + +
+
+
+
diff --git a/js/app.js b/js/app.js index 356f11a..00d1fe3 100644 --- a/js/app.js +++ b/js/app.js @@ -43,7 +43,11 @@ const state = { defaultCreative: localStorage.getItem('gac_srtool_default_creative') === 'true', showcaseRolls: localStorage.getItem('gac_srtool_showcase_rolls') !== 'false', showcaseAura: localStorage.getItem('gac_srtool_showcase_aura') !== 'false', - eqActiveSubtab: 'lc' + eqActiveSubtab: 'lc', + inventoryItems: [], + inventorySaved: {}, + inventoryDraft: {}, + inventoryPage: 0 }; // UI Elements Map @@ -183,6 +187,18 @@ const el = { toggleAhaGlobal: document.getElementById('toggle-aha-global'), btnSavePersistentToggles: document.getElementById('btn-save-persistent-toggles'), + // Material Inventory + inventorySearch: document.getElementById('inventory-search'), + inventoryBody: document.getElementById('inventory-materials-body'), + inventoryCount: document.getElementById('inventory-count'), + inventoryDirtyCount: document.getElementById('inventory-dirty-count'), + inventoryStatus: document.getElementById('inventory-status'), + inventoryPageLabel: document.getElementById('inventory-page-label'), + btnInventorySave: document.getElementById('btn-inventory-save'), + btnInventoryDiscard: document.getElementById('btn-inventory-discard'), + btnInventoryPrev: document.getElementById('btn-inventory-prev'), + btnInventoryNext: document.getElementById('btn-inventory-next'), + // Ex-Rank tabBtnExrank: document.getElementById('tab-btn-exrank'), exrankList: document.getElementById('exrank-list'), @@ -1070,7 +1086,18 @@ async function refreshStatus() { try { let data; if (state.serverMode === 'gac') { - data = await apiCall('/api/status'); + const [status, freesr] = await Promise.all([ + apiCall('/api/status'), + apiCall('/api/freesr').catch(() => null) + ]); + data = status; + // GAC status contains persistent settings and lineup only. Refresh the + // FreeSR snapshot separately so the avatar editor shows the server's + // latest skills/Trace values instead of a stale browser cache. + if (freesr?.data) { + state.fullFreesrData = freesr.data; + await saveFreesrCache(freesr.data); + } } else if (state.serverMode === 'firefly') { // FireflyGo uses data-in-game.json for live state and freesr-data.json for inventory const datagame = await apiCall('/api/data-in-game').catch(() => null); @@ -6820,6 +6847,8 @@ function setupTabs() { renderAvatarsGrid(); } else if (targetId === 'tab-equipment') { loadEquipment(); + } else if (targetId === 'tab-inventory') { + loadMaterialsInventory(); } else if (targetId === 'tab-persistent') { loadPersistentSettings(); } else if (targetId === 'tab-json') { @@ -6832,6 +6861,129 @@ function setupTabs() { }); } +const INVENTORY_PAGE_SIZE = 100; + +function inventoryFilteredIds() { + const query = (el.inventorySearch?.value || '').trim(); + if (!query) return state.inventoryItems; + return state.inventoryItems.filter(id => String(id).includes(query)); +} + +function inventoryDirtyIds() { + return state.inventoryItems.filter(id => { + const key = String(id); + if (!Object.prototype.hasOwnProperty.call(state.inventoryDraft, key)) return false; + const hasSaved = Object.prototype.hasOwnProperty.call(state.inventorySaved, key); + const draft = state.inventoryDraft[key]; + return draft === null ? hasSaved : (!hasSaved || draft !== state.inventorySaved[key]); + }); +} + +function renderMaterialsInventory() { + if (!el.inventoryBody) return; + const ids = inventoryFilteredIds(); + const dirty = inventoryDirtyIds(); + const pageCount = Math.max(1, Math.ceil(ids.length / INVENTORY_PAGE_SIZE)); + state.inventoryPage = Math.min(state.inventoryPage, pageCount - 1); + const start = state.inventoryPage * INVENTORY_PAGE_SIZE; + const pageIds = ids.slice(start, start + INVENTORY_PAGE_SIZE); + + el.inventoryCount.textContent = t('inventoryItemTotal', { count: state.inventoryItems.length }); + el.inventoryDirtyCount.textContent = t('inventoryPending', { count: dirty.length }); + el.inventoryDirtyCount.style.display = dirty.length ? '' : 'none'; + el.btnInventorySave.disabled = dirty.length === 0; + el.btnInventoryDiscard.disabled = dirty.length === 0; + el.btnInventoryPrev.disabled = state.inventoryPage === 0; + el.btnInventoryNext.disabled = state.inventoryPage >= pageCount - 1; + el.inventoryPageLabel.textContent = t('inventoryPage', { page: state.inventoryPage + 1, pages: pageCount, count: ids.length }); + + if (!pageIds.length) { + el.inventoryBody.innerHTML = `${t('inventoryNoMatch')}`; + return; + } + el.inventoryBody.innerHTML = pageIds.map(id => { + const key = String(id); + const hasSaved = Object.prototype.hasOwnProperty.call(state.inventorySaved, key); + const value = Object.prototype.hasOwnProperty.call(state.inventoryDraft, key) + ? state.inventoryDraft[key] + : (hasSaved ? state.inventorySaved[key] : ''); + const changed = Object.prototype.hasOwnProperty.call(state.inventoryDraft, key) + && (value === null ? hasSaved : (!hasSaved || value !== state.inventorySaved[key])); + const displayValue = value === null ? '' : value; + return ` + ${id} + + `; + }).join(''); +} + +async function loadMaterialsInventory() { + if (!el.inventoryStatus) return; + if (state.serverMode !== 'gac') { + el.inventoryStatus.textContent = t('inventoryGaCOnly'); + return; + } + el.inventoryStatus.textContent = t('inventoryLoadHint'); + try { + const result = await apiCall('/api/inventory/materials'); + if (!Array.isArray(result.item_ids)) throw new Error('Server returned an invalid item catalog'); + state.inventoryItems = [...new Set([...result.item_ids, 1, 2])].filter(Number.isInteger).sort((a, b) => a - b); + state.inventorySaved = result.materials && typeof result.materials === 'object' ? result.materials : {}; + state.inventoryDraft = {}; + state.inventoryPage = 0; + el.inventoryStatus.textContent = t('inventoryLoadSuccess', { count: state.inventoryItems.length }); + renderMaterialsInventory(); + } catch (error) { + el.inventoryStatus.textContent = `${t('inventoryLoadFailed')}: ${error.message}`; + el.inventoryBody.innerHTML = `${t('inventoryLoadFailed')}`; + } +} + +async function saveMaterialsInventory() { + const updates = inventoryDirtyIds().map(id => { + const count = state.inventoryDraft[String(id)]; + return count === null ? { item_id: id, reset: true } : { item_id: id, count }; + }); + if (!updates.length) return; + el.btnInventorySave.disabled = true; + el.inventoryStatus.textContent = t('inventorySaving', { count: updates.length }); + try { + const result = await apiCall('/api/inventory/materials', 'POST', { updates }); + state.inventorySaved = result.materials || state.inventorySaved; + state.inventoryDraft = {}; + if (state.fullFreesrData) { + state.fullFreesrData.materials = { ...state.inventorySaved }; + await saveFreesrCache(state.fullFreesrData); + } + el.inventoryStatus.textContent = t('inventorySaveSuccess', { count: updates.length }); + renderMaterialsInventory(); + } catch (error) { + el.inventoryStatus.textContent = `${t('inventorySaveFailed')}: ${error.message}`; + renderMaterialsInventory(); + } +} + +function setupInventoryEvents() { + el.inventorySearch?.addEventListener('input', () => { state.inventoryPage = 0; renderMaterialsInventory(); }); + el.inventoryBody?.addEventListener('change', event => { + const input = event.target.closest('.inventory-count-input'); + if (!input) return; + const raw = input.value.trim(); + if (raw === '') { + state.inventoryDraft[input.dataset.itemId] = null; + } else { + const value = Number(raw); + if (!Number.isSafeInteger(value) || value < 0 || value > 4294967295) return; + state.inventoryDraft[input.dataset.itemId] = value; + } + renderMaterialsInventory(); + }); + el.btnInventorySave?.addEventListener('click', saveMaterialsInventory); + el.btnInventoryDiscard?.addEventListener('click', () => { state.inventoryDraft = {}; renderMaterialsInventory(); }); + el.btnInventoryPrev?.addEventListener('click', () => { state.inventoryPage = Math.max(0, state.inventoryPage - 1); renderMaterialsInventory(); }); + el.btnInventoryNext?.addEventListener('click', () => { state.inventoryPage++; renderMaterialsInventory(); }); +} + // Setup Event Listeners function setupEvents() { // Quick Ping & Refresh @@ -8382,6 +8534,7 @@ async function init() { setupTabs(); setupModalSubtabs(); setupEvents(); + setupInventoryEvents(); setupDamageAnalystEvents(); setupGMConsoleEvents(); applyTranslations(); diff --git a/js/i18n.js b/js/i18n.js index 2cc5117..341b433 100644 --- a/js/i18n.js +++ b/js/i18n.js @@ -133,6 +133,29 @@ export const TRANSLATIONS = { "tabDashboard": "📊 แดชบอร์ด & จัดทีม", "tabAvatars": "👥 ตัวละคร", "tabEquipment": "🗡️ กรวยแสง & รีลิกส์", + "tabInventory": "🎒 ไอเทมในกระเป๋า", + "inventoryTitle": "🎒 จัดการจำนวนไอเทม", + "inventoryDescription": "ค้นหาด้วย Item ID แล้วแก้จำนวนหลายรายการก่อนบันทึกครั้งเดียว รายการมาจาก catalog ของ Client ที่เซิร์ฟเวอร์ใช้ ช่องว่างหมายถึงใช้ค่าเริ่มต้นของเซิร์ฟเวอร์", + "inventorySearchPlaceholder": "ค้นหา Item ID...", + "inventoryDiscard": "ทิ้งการแก้ไข", + "inventorySave": "บันทึกการเปลี่ยนแปลง", + "inventoryLoadHint": "กำลังโหลดรายการจากเซิร์ฟเวอร์...", + "inventoryItemTotal": "{count} รายการ", + "inventoryPending": "รอบันทึก {count}", + "inventoryPage": "หน้า {page}/{pages} • {count} รายการ", + "inventoryNoMatch": "ไม่พบ Item ID ที่ค้นหา", + "inventoryCountFor": "จำนวนไอเทม {id}", + "inventoryDefaultPlaceholder": "ค่าเริ่มต้นเซิร์ฟเวอร์", + "inventoryGaCOnly": "ตัวจัดการนี้ใช้ได้กับ GaC-hkrpg-go เท่านั้น", + "inventoryLoadSuccess": "โหลด catalog แล้ว {count} รายการ • ค่าเริ่มต้นจะใช้จำนวนของเซิร์ฟเวอร์", + "inventoryLoadFailed": "โหลด catalog ไม่สำเร็จ", + "inventorySaving": "กำลังบันทึก {count} รายการ...", + "inventorySaveSuccess": "บันทึก {count} รายการแล้ว • เข้าเกมใหม่เพื่อโหลดค่าเข้ากระเป๋า", + "inventorySaveFailed": "บันทึกไม่สำเร็จ", + "inventoryItemId": "Item ID", + "inventoryDefaultCount": "จำนวนในกระเป๋า", + "inventoryPrevious": "ก่อนหน้า", + "inventoryNext": "ถัดไป", "tabPersistent": "💾 จัดการเซฟ GaC", "tabExrank": "⚡ สกิลพิเศษ Ex-Rank", "tabJson": "📝 จัดการไฟล์ JSON", @@ -580,6 +603,29 @@ export const TRANSLATIONS = { "tabDashboard": "📊 Dashboard & Party", "tabAvatars": "👥 Characters", "tabEquipment": "🗡️ Lightcones & Relics", + "tabInventory": "🎒 Inventory", + "inventoryTitle": "🎒 Item quantities", + "inventoryDescription": "Search by item ID, edit several quantities, then save once. Items come from the Client catalog used by the server. Leave a field empty to use the server default.", + "inventorySearchPlaceholder": "Search item ID...", + "inventoryDiscard": "Discard edits", + "inventorySave": "Save changes", + "inventoryLoadHint": "Loading the server catalog...", + "inventoryItemTotal": "{count} items", + "inventoryPending": "{count} pending", + "inventoryPage": "Page {page}/{pages} • {count} items", + "inventoryNoMatch": "No matching item IDs", + "inventoryCountFor": "Quantity for item {id}", + "inventoryDefaultPlaceholder": "Server default", + "inventoryGaCOnly": "This editor is available with GaC-hkrpg-go only.", + "inventoryLoadSuccess": "Loaded {count} catalog items • unchanged items use the server default", + "inventoryLoadFailed": "Could not load the catalog", + "inventorySaving": "Saving {count} items...", + "inventorySaveSuccess": "Saved {count} items • log in again to load the updated bag", + "inventorySaveFailed": "Could not save inventory", + "inventoryItemId": "Item ID", + "inventoryDefaultCount": "Bag quantity", + "inventoryPrevious": "Previous", + "inventoryNext": "Next", "tabPersistent": "💾 GaC Save Manager", "tabExrank": "⚡ Ex-Rank Skills", "tabJson": "📝 JSON Management",