feat: add material inventory editor

This commit is contained in:
CAtlas0784 committed 2026-10-18 22:16:18 +07:00
1 parent fa31704a56
commit ab01489ea1
4 files changed
+254 -5

No files matched your search

+10 -3
View File
@@ -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 | ดึงสถิติดาเมจและผลการต่อสู้ล่าสุด |
+43
View File
@@ -55,6 +55,7 @@
<button class="tab-btn active" data-tab="tab-dashboard" data-i18n="tabDashboard">📊 แดชบอร์ด & จัดทีม</button>
<button class="tab-btn" data-tab="tab-avatars" data-i18n="tabAvatars">👥 ตัวละคร</button>
<button class="tab-btn" data-tab="tab-equipment" data-i18n="tabEquipment">🗡️ กรวยแสง & รีลิกส์</button>
<button class="tab-btn" data-tab="tab-inventory" data-i18n="tabInventory">🎒 ไอเทมในกระเป๋า</button>
<button class="tab-btn" data-tab="tab-damage-analyst" id="tab-btn-damage-analyst" data-i18n="tabDamageAnalyst">💥 Damage Analyst</button>
<button class="tab-btn" data-tab="tab-persistent" id="tab-btn-persistent" data-i18n="tabPersistent">💾 จัดการเซฟ GaC</button>
<button class="tab-btn" data-tab="tab-exrank" id="tab-btn-exrank" data-i18n="tabExrank">⚡ สกิลพิเศษ Ex-Rank</button>
@@ -398,6 +399,48 @@
</div>
</section>
<!-- Material inventory editor: edits only Client-catalog item quantities. -->
<section id="tab-inventory" class="tab-content">
<div class="card" style="margin-bottom: 1rem;">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;flex-wrap:wrap;">
<div>
<div class="card-title" data-i18n="inventoryTitle">🎒 จัดการจำนวนไอเทม</div>
<p class="card-subtitle" data-i18n="inventoryDescription">ค้นหาด้วย Item ID แล้วแก้จำนวนหลายรายการก่อนบันทึกครั้งเดียว รายการมาจาก catalog ของ Client ที่เซิร์ฟเวอร์ใช้ ช่องว่างหมายถึงใช้ค่าเริ่มต้นของเซิร์ฟเวอร์</p>
</div>
<div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;">
<span id="inventory-count" class="badge-super-tag">—</span>
<span id="inventory-dirty-count" class="badge-super-tag" style="display:none;">0</span>
</div>
</div>
<div style="display:flex;gap:.65rem;align-items:center;flex-wrap:wrap;margin-top:1rem;">
<input type="search" id="inventory-search" class="form-input" style="flex:1;min-width:220px;" data-i18n-placeholder="inventorySearchPlaceholder" placeholder="ค้นหา Item ID...">
<button type="button" class="btn btn-secondary" id="btn-inventory-discard" disabled data-i18n="inventoryDiscard">ทิ้งการแก้ไข</button>
<button type="button" class="btn btn-success" id="btn-inventory-save" disabled data-i18n="inventorySave">บันทึกการเปลี่ยนแปลง</button>
</div>
<div id="inventory-status" role="status" aria-live="polite" style="font-size:.8rem;color:var(--text-muted);margin-top:.7rem;" data-i18n="inventoryLoadHint">กำลังโหลดรายการจากเซิร์ฟเวอร์...</div>
</div>
<div class="card" style="padding:0;overflow:hidden;">
<div style="overflow:auto;max-height:65vh;">
<table style="width:100%;border-collapse:collapse;min-width:420px;">
<thead style="position:sticky;top:0;background:var(--bg-card-hover);z-index:1;">
<tr>
<th style="text-align:left;padding:.8rem 1rem;" data-i18n="inventoryItemId">Item ID</th>
<th style="text-align:left;padding:.8rem 1rem;" data-i18n="inventoryDefaultCount">จำนวนในกระเป๋า</th>
</tr>
</thead>
<tbody id="inventory-materials-body">
<tr><td colspan="2" style="padding:1.5rem;text-align:center;color:var(--text-muted);" data-i18n="inventoryLoadHint">กำลังโหลดรายการจากเซิร์ฟเวอร์...</td></tr>
</tbody>
</table>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;gap:.75rem;padding:.75rem 1rem;border-top:1px solid var(--border-color);">
<button type="button" class="btn btn-sm btn-secondary" id="btn-inventory-prev" disabled data-i18n="inventoryPrevious">ก่อนหน้า</button>
<span id="inventory-page-label" style="font-size:.8rem;color:var(--text-muted);"></span>
<button type="button" class="btn btn-sm btn-secondary" id="btn-inventory-next" disabled data-i18n="inventoryNext">ถัดไป</button>
</div>
</div>
</section>
<!-- 3.5. Damage Analyst (In-Game Battle Telemetry & Damage Analysis) -->
<section id="tab-damage-analyst" class="tab-content">
<!-- Toolbar & Control Card -->
+155 -2
View File
@@ -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 = `<tr><td colspan="2" style="padding:1.5rem;text-align:center;color:var(--text-muted);">${t('inventoryNoMatch')}</td></tr>`;
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 `<tr style="border-top:1px solid var(--border-color);${changed ? 'background:rgba(245,158,11,.08);' : ''}">
<td style="padding:.55rem 1rem;font-family:monospace;">${id}</td>
<td style="padding:.4rem 1rem;"><input type="number" class="form-input inventory-count-input" data-item-id="${id}" min="0" max="4294967295" step="1" value="${displayValue}" placeholder="${t('inventoryDefaultPlaceholder')}" aria-label="${t('inventoryCountFor', { id })}" style="max-width:220px;"></td>
</tr>`;
}).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 = `<tr><td colspan="2" style="padding:1.5rem;text-align:center;color:var(--text-muted);">${t('inventoryLoadFailed')}</td></tr>`;
}
}
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();
+46
View File
@@ -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",