Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1719781711 | ||
|
|
4108cd04b3 | ||
|
|
62a529ef3f | ||
|
|
feb18074fe | ||
|
|
ffd6b2e853 | ||
|
|
ca5831c928 | ||
|
|
e03e5b1f14 |
No files matched your search
@@ -16,6 +16,7 @@ Thumbs.db
|
|||||||
*.swo
|
*.swo
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
# Local custom configs (environment / machine specific)
|
# Local custom configs (environment / machine specific)
|
||||||
config.local.json
|
config.local.json
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
.vercel
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
tools
|
||||||
|
.env*
|
||||||
|
config.local.*
|
||||||
|
*.log
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
# GaC-Srtool 🌌
|
# GaC-Srtool 🌌
|
||||||
|
|
||||||
|
ใช้งานเว็บได้ที่ [GaCy Srtool บน Vercel](https://gacy-srtool.vercel.app/) เปิดหน้าตั้งค่าเพื่อเชื่อมต่อ SDK ของเซิร์ฟเวอร์ที่ใช้งานอยู่
|
||||||
|
|
||||||
|
ในโหมด GaC หน้า **ตั้งค่าตัวละคร / สวิตช์ระบบพิเศษ** เลือก Freecam หรือ FaceCam และตั้งระยะเริ่มต้นของ FaceCam ได้ หลังบันทึกให้กด Tab ในเกมเพื่อเปิดหรือปิดกล้อง; ลูกกลิ้งปรับระยะชั่วคราวและกลับเป็นค่าที่ตั้งไว้เมื่อเปิดใหม่
|
||||||
|
|
||||||
|
## Unstuck / แก้ตัวหมุนค้าง
|
||||||
|
|
||||||
|
เมื่อเชื่อมต่อ GaC server ใช้ปุ่ม **Unstuck** ใน GM Command Console หรือพิมพ์ `/unstuck` ได้ เว็บจะรอผลตอบรับจากเกมก่อนแสดงว่าสำเร็จ ต้องเปิดเกมและเชื่อมต่อเซิร์ฟเวอร์อยู่ คำสั่งนี้ปลดการบล็อก UI ระหว่างรอแพ็กเก็ตผ่าน heartbeat โดยไม่ลบเซฟ CW หรือเปลี่ยนแมพ และไม่แทนการแก้ handler ที่ขาด ต้องใช้ SDK server ที่รองรับ `/api/client/unstuck`
|
||||||
|
|
||||||
เว็บแอปพลิเคชันแดชบอร์ด (Web Dashboard & Management Tool) สำหรับจัดการข้อมูลตัวละคร, จัดทีม, ปรับแต่งรีลิกส์/กรวยแสง, วิเคราะห์ดาเมจจากการต่อสู้จริง และจัดการไฟล์เซฟ (`freesr-data.json`, `persistent`) ของเซิร์ฟเวอร์ **GaC-hkrpg-go** (และเซิร์ฟเวอร์มาตรฐาน FreeSR)
|
เว็บแอปพลิเคชันแดชบอร์ด (Web Dashboard & Management Tool) สำหรับจัดการข้อมูลตัวละคร, จัดทีม, ปรับแต่งรีลิกส์/กรวยแสง, วิเคราะห์ดาเมจจากการต่อสู้จริง และจัดการไฟล์เซฟ (`freesr-data.json`, `persistent`) ของเซิร์ฟเวอร์ **GaC-hkrpg-go** (และเซิร์ฟเวอร์มาตรฐาน FreeSR)
|
||||||
|
|
||||||
ทำงานแบบ Client-side 100% สามารถเปิดใช้งานผ่านเบราว์เซอร์ได้ทันทีโดยไม่ต้องติดตั้งโปรแกรมเพิ่มเติม
|
ทำงานแบบ Client-side 100% สามารถเปิดใช้งานผ่านเบราว์เซอร์ได้ทันทีโดยไม่ต้องติดตั้งโปรแกรมเพิ่มเติม
|
||||||
@@ -58,6 +66,13 @@
|
|||||||
- การบันทึกแก้เฉพาะจำนวนไอเทมใน `freesr-data.json` ไม่เขียนทับตัวละคร, LC, Relic หรือ Battle Config
|
- การบันทึกแก้เฉพาะจำนวนไอเทมใน `freesr-data.json` ไม่เขียนทับตัวละคร, LC, Relic หรือ Battle Config
|
||||||
- รองรับเฉพาะ GaC-hkrpg-go ผ่าน `/api/inventory/materials`; บันทึกลงไฟล์ทันทีและต้องเข้าเกมใหม่เพื่อโหลด session พร้อมจำนวนที่แก้แล้ว
|
- รองรับเฉพาะ GaC-hkrpg-go ผ่าน `/api/inventory/materials`; บันทึกลงไฟล์ทันทีและต้องเข้าเกมใหม่เพื่อโหลด session พร้อมจำนวนที่แก้แล้ว
|
||||||
|
|
||||||
|
### 👾 6.1 Monster Setting
|
||||||
|
- แยก Memory of Chaos, Pure Fiction, Apocalyptic Shadow และ Anomaly Arbitration เพื่อดูชื่อด่าน/มอนสเตอร์ รูป, level และ wave จาก metadata Nanoka ที่ตรงกับ `client_version` ของเซิร์ฟเวอร์และ resource `challenge_data.json` / `resources/StageConfig.json` (อ่านอย่างเดียว); เมื่อ Client เปลี่ยนเวอร์ชัน API จะดึง metadata ของเวอร์ชันนั้นให้อัตโนมัติ โดยไม่ใช้ข้อมูลจากเวอร์ชันอื่น
|
||||||
|
- เปลี่ยนการเลือกกลุ่ม/ด่านเป็นการ์ด และ Custom Enemy ค้นมอนด้วยชื่อหรือ ID พร้อมรูป/ชื่อจาก Nanoka; พิมพ์ ID เองได้ และแสดงรูปตัวอย่างขนาดใหญ่ขึ้น
|
||||||
|
- Anomaly Arbitration เป็นโหมด Endgame ที่อ่าน stage จาก resource เช่นเดียวกับโหมดอื่น ไม่ใช่ Custom Enemy; Aha's Help มีแท็บแยกจาก AA
|
||||||
|
- แก้ Custom Enemy ได้เฉพาะ Golden / Crimson Calyx และต้องเชื่อมต่อ GaC-Srtool; การกดใช้ stage จาก Client จะล้าง custom waves เพื่อให้กลับไปใช้ resource
|
||||||
|
- Weekly Boss, Stagnant Shadow, Cavern of Corrosion, Endgame และ Simulated Universe ไม่รับ custom waves จากหน้านี้
|
||||||
|
|
||||||
### 💾 7. จัดการไฟล์เซฟ GaC (Persistent & FreeSR Data)
|
### 💾 7. จัดการไฟล์เซฟ GaC (Persistent & FreeSR Data)
|
||||||
- **ตั้งค่าตัวเอก (ผู้บุกเบิก):** สลับเพศ (Caelus / Stelle) และเปลี่ยนธาตุ (กายภาพ, ไฟ, จินตภาพ, ความจำ)
|
- **ตั้งค่าตัวเอก (ผู้บุกเบิก):** สลับเพศ (Caelus / Stelle) และเปลี่ยนธาตุ (กายภาพ, ไฟ, จินตภาพ, ความจำ)
|
||||||
- **สลับพาธ March 7th:** เลือกระหว่างพาธการอนุรักษ์ (น้ำแข็ง) และพาธล่าสังหาร (จินตภาพ)
|
- **สลับพาธ March 7th:** เลือกระหว่างพาธการอนุรักษ์ (น้ำแข็ง) และพาธล่าสังหาร (จินตภาพ)
|
||||||
@@ -122,6 +137,8 @@ GaC-Srtool สื่อสารกับเซิร์ฟเวอร์ GaC-h
|
|||||||
|
|
||||||
### บันทึกตัวละครและ Traces
|
### บันทึกตัวละครและ Traces
|
||||||
|
|
||||||
|
Damage Analyst เรียงการ์ดตาม `report.avatars[].slot` จาก Battle จริง ไม่เปลี่ยนตามทีมหน้าโลก รายงานเก่าที่ไม่มี slot คงลำดับเดิม และแสดงประเภทดาเมจเพิ่มเติมที่ API ส่งมา เช่น Elation Damage / True Damage ทดสอบลำดับด้วย `node --test tests/damage-order.test.cjs` ข้อมูลสดต้องใช้ SDK และ `damage_analyst_live.lua` ฉบับที่อ่าน StatisticsComponent ของ Battle ปัจจุบัน
|
||||||
|
|
||||||
การบันทึกจากหน้ารายละเอียดส่งข้อมูลตัวละครทั้งชุดผ่าน `/api/freesr` ในคำขอเดียว รวมถึง `data.skills` และ `data.skills_by_anchor_type` สำหรับ Traces เพื่อไม่ให้ server sync ข้อมูลระดับ/Eidolon ระหว่างทางก่อนข้อมูล Trace จะถูกบันทึก `saveAvatarDetails(closeModal = true)` ยังรองรับการเรียกจากปุ่ม build ที่ไม่ต้องปิดหน้าต่าง ถ้า server ตอบผิดพลาดจะแสดงข้อความบันทึกไม่สำเร็จแทนการแจ้งว่าสำเร็จ
|
การบันทึกจากหน้ารายละเอียดส่งข้อมูลตัวละครทั้งชุดผ่าน `/api/freesr` ในคำขอเดียว รวมถึง `data.skills` และ `data.skills_by_anchor_type` สำหรับ Traces เพื่อไม่ให้ server sync ข้อมูลระดับ/Eidolon ระหว่างทางก่อนข้อมูล Trace จะถูกบันทึก `saveAvatarDetails(closeModal = true)` ยังรองรับการเรียกจากปุ่ม build ที่ไม่ต้องปิดหน้าต่าง ถ้า server ตอบผิดพลาดจะแสดงข้อความบันทึกไม่สำเร็จแทนการแจ้งว่าสำเร็จ
|
||||||
|
|
||||||
### ข้อมูล Build และแหล่งที่มา
|
### ข้อมูล Build และแหล่งที่มา
|
||||||
@@ -129,3 +146,21 @@ GaC-Srtool สื่อสารกับเซิร์ฟเวอร์ GaC-h
|
|||||||
หน้า **รายละเอียดตัวละคร → แนวทางบิลด์** แสดงอุปกรณ์และออฟหลักที่พบบ่อยในตัวอย่าง Endgame จากข้อมูลที่แสดงบน Prydwen โดยใส่โหมด/จำนวนผู้เล่นและวันที่เก็บ snapshot ไว้กำกับ ตัวเลขเป็นสัดส่วนการใช้งานในกลุ่มตัวอย่าง ไม่ใช่คะแนนจัดอันดับหรือคำแนะนำทางการ ตัวละครที่ snapshot ไม่มีตัวอย่างจะแสดงลิงก์ไปคู่มือของ Prydwen แทนการแสดงข้อมูลสำรองที่อาจทำให้เข้าใจผิด
|
หน้า **รายละเอียดตัวละคร → แนวทางบิลด์** แสดงอุปกรณ์และออฟหลักที่พบบ่อยในตัวอย่าง Endgame จากข้อมูลที่แสดงบน Prydwen โดยใส่โหมด/จำนวนผู้เล่นและวันที่เก็บ snapshot ไว้กำกับ ตัวเลขเป็นสัดส่วนการใช้งานในกลุ่มตัวอย่าง ไม่ใช่คะแนนจัดอันดับหรือคำแนะนำทางการ ตัวละครที่ snapshot ไม่มีตัวอย่างจะแสดงลิงก์ไปคู่มือของ Prydwen แทนการแสดงข้อมูลสำรองที่อาจทำให้เข้าใจผิด
|
||||||
|
|
||||||
ข้อมูลที่สร้างไว้ใน `js/build-guides-data.js` ใช้ชุดข้อมูล MIT จาก [LvlUrArti/MocDataProcessed](https://huggingface.co/datasets/LvlUrArti/MocDataProcessed) ซึ่งระบุว่าเป็นข้อมูลที่ประมวลผลจากเว็บไซต์ Prydwen โปรดให้เครดิต LvlUrArti เมื่อเผยแพร่หรือวิเคราะห์ข้อมูลต่อ สามารถอัปเดต snapshot ด้วย `npm run sync-build-guides` ได้ โดยสคริปต์จะเลือกเวอร์ชันล่าสุดที่มีใน dataset และจับคู่ตัวละครตาม ID จาก `js/avatars-data.js`.
|
ข้อมูลที่สร้างไว้ใน `js/build-guides-data.js` ใช้ชุดข้อมูล MIT จาก [LvlUrArti/MocDataProcessed](https://huggingface.co/datasets/LvlUrArti/MocDataProcessed) ซึ่งระบุว่าเป็นข้อมูลที่ประมวลผลจากเว็บไซต์ Prydwen โปรดให้เครดิต LvlUrArti เมื่อเผยแพร่หรือวิเคราะห์ข้อมูลต่อ สามารถอัปเดต snapshot ด้วย `npm run sync-build-guides` ได้ โดยสคริปต์จะเลือกเวอร์ชันล่าสุดที่มีใน dataset และจับคู่ตัวละครตาม ID จาก `js/avatars-data.js`.
|
||||||
|
|
||||||
|
### Item Book
|
||||||
|
|
||||||
|
The **Item Book** scans the pinned game-data revision for every `ItemConfig`-named table that contains item-definition rows. In this revision, 26 candidate tables were checked and 18 supplied item rows: 5,472 rows resolve to 5,414 unique item IDs. The **GaC supported** badge is derived from the Go server's `InventoryItemConfig.json`; other entries are browse-only.
|
||||||
|
|
||||||
|
Click an item's image to open its details, including its ID, source table, type, game description, and a quantity selector. The selected quantity is local to the dialog and does not call the server or add an item. Where the game table defines `PileLimit`, the selector uses it as the maximum.
|
||||||
|
|
||||||
|
Names and descriptions come from the pinned game TextMap and StarRailRes indexes for all 13 Srtool languages. At this revision, 5,150 of 5,414 item names have source-localized text; missing source names fall back to English or the item ID. Icons load from the pinned StarRailRes revision: 5,261 entries have a verified image file, while the remaining 153 retain their original game icon paths and show the missing-image state. The installed client stores these missing images inside encrypted `.block` bundles, so they are not extracted by this catalog builder.
|
||||||
|
|
||||||
|
Regenerate `assets/item-catalog.json` from the inventory catalog revision used by the Go server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/build-item-catalog.py --inventory-config <path-to-GaC-hkrpg-go>/resources/InventoryItemConfig.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## CW GM commands
|
||||||
|
|
||||||
|
In the connected GM Console: `/cw status`, `/cw gold <total>`, `/cw hp <total>`. Values affect only the active CW run. Changes are queued and applied during Preparation; they wait while fighting. Gold sets the total, rather than adding repeatedly. HP must fit the run limit. Check the server's `CW_GM applied` log or query status to confirm application. Commands are also included in console suggestions and `/help`.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"source":{"gameRepository":"https://gitlab.com/Dimbreath/turnbasedgamedata","gameRevision":"312b4597691e0a29cf9828a15b592757dba86949","candidateTables":26,"gameTables":["ExcelOutput/ActivityItemConfigAvatar.json","ExcelOutput/ActivityItemConfigAvatarLD.json","ExcelOutput/ItemConfig.json","ExcelOutput/ItemConfigAvatar.json","ExcelOutput/ItemConfigAvatarLD.json","ExcelOutput/ItemConfigAvatarPlayerIcLD.json","ExcelOutput/ItemConfigAvatarPlayerIcon.json","ExcelOutput/ItemConfigAvatarRank.json","ExcelOutput/ItemConfigAvatarRankLD.json","ExcelOutput/ItemConfigAvatarSkin.json","ExcelOutput/ItemConfigBadge.json","ExcelOutput/ItemConfigBook.json","ExcelOutput/ItemConfigDisk.json","ExcelOutput/ItemConfigEquipment.json","ExcelOutput/ItemConfigLD.json","ExcelOutput/ItemConfigPlayerRoomDynamic.json","ExcelOutput/ItemConfigRelic.json","ExcelOutput/ItemConfigTrainDynamic.json"],"tableScan":[{"table":"ExcelOutput/ActivityItemConfigAvatar.json","rows":19,"itemRows":19},{"table":"ExcelOutput/ActivityItemConfigAvatarLD.json","rows":6,"itemRows":6},{"table":"ExcelOutput/AvatarExpItemConfig.json","rows":3,"itemRows":0},{"table":"ExcelOutput/EquipmentExpItemConfig.json","rows":3,"itemRows":0},{"table":"ExcelOutput/EventMuseumItemConfig.json","rows":18,"itemRows":0},{"table":"ExcelOutput/GMAccountItemConfig.json","rows":0,"itemRows":0},{"table":"ExcelOutput/ItemConfig.json","rows":2941,"itemRows":2941},{"table":"ExcelOutput/ItemConfigAvatar.json","rows":99,"itemRows":99},{"table":"ExcelOutput/ItemConfigAvatarLD.json","rows":4,"itemRows":4},{"table":"ExcelOutput/ItemConfigAvatarPlayerIcLD.json","rows":4,"itemRows":4},{"table":"ExcelOutput/ItemConfigAvatarPlayerIcon.json","rows":94,"itemRows":94},{"table":"ExcelOutput/ItemConfigAvatarRank.json","rows":94,"itemRows":94},{"table":"ExcelOutput/ItemConfigAvatarRankLD.json","rows":4,"itemRows":4},{"table":"ExcelOutput/ItemConfigAvatarSkin.json","rows":8,"itemRows":8},{"table":"ExcelOutput/ItemConfigAvatarTest.json","rows":70,"itemRows":0},{"table":"ExcelOutput/ItemConfigAvatarTestRank.json","rows":65,"itemRows":0},{"table":"ExcelOutput/ItemConfigBadge.json","rows":21,"itemRows":21},{"table":"ExcelOutput/ItemConfigBook.json","rows":762,"itemRows":762},{"table":"ExcelOutput/ItemConfigDisk.json","rows":272,"itemRows":272},{"table":"ExcelOutput/ItemConfigEquipment.json","rows":170,"itemRows":170},{"table":"ExcelOutput/ItemConfigLD.json","rows":69,"itemRows":69},{"table":"ExcelOutput/ItemConfigPlayerRoomDynamic.json","rows":79,"itemRows":79},{"table":"ExcelOutput/ItemConfigRelic.json","rows":774,"itemRows":774},{"table":"ExcelOutput/ItemConfigTrainDynamic.json","rows":52,"itemRows":52},{"table":"ExcelOutput/MessageItemConfig.json","rows":13779,"itemRows":0},{"table":"ExcelOutput/RestaurantShopItemConfig.json","rows":6,"itemRows":0}],"assetRepository":"Mar-7th/StarRailRes","assetRevision":"dbe8cdfcfb0bf657b9fe1cf92d5537f118ccb487","assetBaseUrl":"https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@dbe8cdfcfb0bf657b9fe1cf92d5537f118ccb487/","assetFallbackBaseUrl":"https://raw.githubusercontent.com/Mar-7th/StarRailRes/dbe8cdfcfb0bf657b9fe1cf92d5537f118ccb487/","languages":["th","en","zh-CN","zh-TW","ja","ko","vi","id","es","fr","de","ru","pt"],"notes":"Includes every ItemConfig table with item-definition rows at the pinned game-data revision. Items absent from InventoryItemConfig are browse-only; the catalog does not add or summon items. Icons use only files present in the pinned public StarRailRes tree. gameIconPaths retain the source paths for items without a mapped public icon."},"counts":{"total":5414,"definitionRows":5472,"uniqueIds":5414,"supported":3550,"withImages":5261,"withoutConfiguredImage":153,"withDescriptions":4647,"nameCoverageByLanguage":{"th":5150,"en":5150,"zh-CN":5150,"zh-TW":5150,"ja":5150,"ko":5150,"vi":5150,"id":5150,"es":5150,"fr":5150,"de":5150,"ru":5150,"pt":5150},"descriptionCoverageByLanguage":{"th":3080,"en":3080,"zh-CN":3080,"zh-TW":3080,"ja":3080,"ko":3080,"vi":3080,"id":3080,"es":3080,"fr":3080,"de":3080,"ru":3080,"pt":3080},"backgroundDescriptionCoverageByLanguage":{"th":4328,"en":4328,"zh-CN":4328,"zh-TW":4328,"ja":4328,"ko":4328,"vi":4328,"id":4328,"es":4328,"fr":4328,"de":4328,"ru":4328,"pt":4328},"candidateTables":26,"definitionTables":18},"items":[{"id":1,"category":"item","type":"Virtual","subType":"Virtual","rarity":5,"names":{"th":"Stellar Jade","en":"Stellar Jade","zh-CN":"星琼","zh-TW":"星瓊","ja":"星玉","ko":"성옥","vi":"Ngọc Ánh Sao","id":"Stellar Jade","es":"Jade estelar","fr":"Jade stellaire","de":"Stellare Jade","ru":"Звёздный нефрит","pt":"Jade Estelar"},"descriptions":{},"backgroundDescriptions":{"th":"ประกายแสงอันล้ำค่าพุ่งผ่านท้องฟ้ายามราตรีราวกับดาวตก คว้าเอาไว้ก่อนที่มันจะดับสูญ บางทีอาจจะใช้แลกเป็นบางสิ่งไLine truncated
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
.item-catalog-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .65rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.item-catalog-toolbar .form-input { flex: 1 1 260px; min-width: 200px; }
|
||||||
|
.item-catalog-toolbar .form-select { flex: 0 1 230px; min-width: 190px; }
|
||||||
|
.item-catalog-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||||
|
gap: .75rem;
|
||||||
|
}
|
||||||
|
.item-catalog-card {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 68px minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: .8rem;
|
||||||
|
padding: .8rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition: border-color .15s ease, background .15s ease;
|
||||||
|
}
|
||||||
|
.item-catalog-card:hover {
|
||||||
|
border-color: rgba(56, 189, 248, .45);
|
||||||
|
background: var(--bg-card-hover);
|
||||||
|
}
|
||||||
|
.item-catalog-art {
|
||||||
|
width: 68px;
|
||||||
|
height: 68px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: .15rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: rgba(255, 255, 255, .035);
|
||||||
|
}
|
||||||
|
.item-catalog-art-button {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.item-catalog-art-button:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 3px; }
|
||||||
|
.item-catalog-art-button:hover .item-catalog-art { border-color: var(--accent-gold); transform: translateY(-1px); }
|
||||||
|
.item-catalog-image {
|
||||||
|
width: 62px;
|
||||||
|
height: 62px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.item-catalog-art-missing {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
padding: .2rem;
|
||||||
|
}
|
||||||
|
.item-catalog-art-missing span { font-size: 1.25rem; line-height: 1; color: var(--accent-cyan); }
|
||||||
|
.item-catalog-art-missing small { font-size: .55rem; line-height: 1.1; }
|
||||||
|
.item-catalog-info { min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: .2rem; }
|
||||||
|
.item-catalog-name {
|
||||||
|
max-width: 100%;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: .84rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
font-weight: 750;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.item-catalog-id { color: var(--accent-cyan); font: 700 .75rem/1.2 monospace; }
|
||||||
|
.item-catalog-details {
|
||||||
|
max-width: 100%;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: .65rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.item-catalog-support {
|
||||||
|
margin-top: .12rem;
|
||||||
|
padding: .14rem .4rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: .59rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
.item-catalog-support.is-supported { color: #34d399; background: rgba(16, 185, 129, .12); }
|
||||||
|
.item-catalog-support.is-browse-only { color: #fbbf24; background: rgba(245, 158, 11, .11); }
|
||||||
|
.item-catalog-empty { grid-column: 1 / -1; padding: 2rem 1rem; text-align: center; color: var(--text-muted); }
|
||||||
|
.item-catalog-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: .75rem;
|
||||||
|
padding: .75rem 1rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
@media (max-width: 540px) {
|
||||||
|
.item-catalog-grid { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
|
||||||
|
.item-catalog-toolbar .form-select { flex: 1 1 100%; }
|
||||||
|
}
|
||||||
|
.inventory-mode-switch {
|
||||||
|
display: flex;
|
||||||
|
gap: .5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: .35rem;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
.inventory-mode-btn {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: .55rem .85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
font-size: .8rem;
|
||||||
|
font-weight: 750;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.inventory-mode-btn:hover { color: var(--text-primary); }
|
||||||
|
.inventory-mode-btn.active {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
background: rgba(56, 189, 248, .1);
|
||||||
|
border-color: rgba(56, 189, 248, .25);
|
||||||
|
}
|
||||||
|
.inventory-mode-panel[hidden] { display: none !important; }
|
||||||
|
|
||||||
|
.item-catalog-dialog {
|
||||||
|
width: min(660px, calc(100vw - 2rem));
|
||||||
|
max-width: none;
|
||||||
|
max-height: min(86vh, 760px);
|
||||||
|
padding: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid rgba(244, 190, 86, .45);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(145deg, #202333, #11141f 68%);
|
||||||
|
box-shadow: 0 24px 90px rgba(0, 0, 0, .72), 0 0 36px rgba(244, 190, 86, .09);
|
||||||
|
}
|
||||||
|
.item-catalog-dialog::backdrop { background: rgba(4, 6, 14, .78); backdrop-filter: blur(5px); }
|
||||||
|
.item-catalog-detail-shell { overflow: auto; max-height: min(86vh, 760px); }
|
||||||
|
.item-catalog-detail-header,
|
||||||
|
.item-catalog-detail-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: .8rem;
|
||||||
|
padding: .85rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, .035);
|
||||||
|
}
|
||||||
|
.item-catalog-detail-header { color: var(--accent-gold); font-weight: 800; border-bottom: 1px solid var(--border-color); }
|
||||||
|
.item-catalog-dialog-close {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: rgba(255, 255, 255, .06);
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.item-catalog-detail-body { display: grid; grid-template-columns: 144px minmax(0, 1fr); gap: 1rem; padding: 1.15rem; }
|
||||||
|
.item-catalog-detail-art {
|
||||||
|
width: 144px;
|
||||||
|
height: 144px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
align-self: start;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(244, 190, 86, .38);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: radial-gradient(circle, rgba(244, 190, 86, .13), rgba(255, 255, 255, .03) 70%);
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
font-size: .8rem;
|
||||||
|
}
|
||||||
|
.item-catalog-detail-art img { width: 90%; height: 90%; object-fit: contain; }
|
||||||
|
.item-catalog-detail-copy { min-width: 0; }
|
||||||
|
.item-catalog-detail-copy h2 { margin: .05rem 0 .35rem; font-size: 1.15rem; line-height: 1.3; overflow-wrap: anywhere; }
|
||||||
|
.item-catalog-detail-id { color: var(--accent-cyan); font: 700 .78rem/1.3 monospace; }
|
||||||
|
.item-catalog-detail-source { margin-top: .25rem; color: var(--text-muted); font-size: .66rem; line-height: 1.35; overflow-wrap: anywhere; }
|
||||||
|
.item-catalog-detail-meta { margin-top: .5rem; color: var(--accent-gold); font-size: .78rem; line-height: 1.5; }
|
||||||
|
.item-catalog-detail-section { margin-top: .85rem; }
|
||||||
|
.item-catalog-detail-section h3 { margin: 0 0 .3rem; color: var(--text-muted); font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; }
|
||||||
|
.item-catalog-detail-description,
|
||||||
|
.item-catalog-detail-lore { color: #e1e4ee; font-size: .83rem; line-height: 1.55; white-space: pre-line; overflow-wrap: anywhere; }
|
||||||
|
.item-catalog-detail-quantity { display: flex; align-items: center; gap: .75rem; padding: .9rem 1.15rem; border-top: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color); }
|
||||||
|
.item-catalog-detail-quantity > label { color: var(--text-primary); font-size: .82rem; font-weight: 750; }
|
||||||
|
.item-catalog-amount-stepper { display: inline-flex; align-items: center; overflow: hidden; border: 1px solid var(--border-color); border-radius: 8px; background: rgba(0, 0, 0, .22); }
|
||||||
|
.item-catalog-amount-stepper button { width: 2.2rem; height: 2.2rem; border: 0; color: var(--accent-gold); background: transparent; font-size: 1.25rem; cursor: pointer; }
|
||||||
|
.item-catalog-amount-stepper button:hover { background: rgba(244, 190, 86, .12); }
|
||||||
|
.item-catalog-amount-stepper input { width: 5.2rem; height: 2.2rem; border: 0; border-right: 1px solid var(--border-color); border-left: 1px solid var(--border-color); outline: 0; color: var(--text-primary); background: transparent; text-align: center; font: 700 .95rem/1 monospace; }
|
||||||
|
.item-catalog-amount-limit { color: var(--text-muted); font-size: .72rem; }
|
||||||
|
.item-catalog-detail-footer { justify-content: flex-end; }
|
||||||
|
.item-catalog-detail-lore[hidden] { display: none !important; }
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.inventory-mode-switch { width: 100%; }
|
||||||
|
.inventory-mode-btn { flex: 1; padding-inline: .45rem; }
|
||||||
|
}
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.item-catalog-detail-body { grid-template-columns: 88px minmax(0, 1fr); gap: .75rem; padding: .85rem; }
|
||||||
|
.item-catalog-detail-art { width: 88px; height: 88px; }
|
||||||
|
.item-catalog-detail-quantity { flex-wrap: wrap; gap: .55rem; }
|
||||||
|
}
|
||||||
+355
@@ -233,6 +233,11 @@ body {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Give the stage browser the full available width when it is active. */
|
||||||
|
.main-container:has(#tab-monster-settings.active) {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
display: none;
|
display: none;
|
||||||
animation: fadeIn 0.25s ease-out;
|
animation: fadeIn 0.25s ease-out;
|
||||||
@@ -845,6 +850,202 @@ body {
|
|||||||
border-color: rgba(99, 120, 160, 0.45);
|
border-color: rgba(99, 120, 160, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Aha's Help level picker */
|
||||||
|
.aha-help-card {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.1rem;
|
||||||
|
margin: 0.25rem 0 1rem;
|
||||||
|
padding: clamp(1rem, 2.2vw, 1.5rem);
|
||||||
|
border: 1px solid rgba(245, 180, 75, 0.28);
|
||||||
|
border-radius: 18px;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at 100% 0, rgba(245, 158, 11, 0.09), transparent 42%),
|
||||||
|
linear-gradient(145deg, rgba(25, 29, 42, 0.96), rgba(15, 19, 30, 0.96));
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-emblem {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
flex: 0 0 48px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 1px solid rgba(251, 191, 36, 0.4);
|
||||||
|
border-radius: 15px;
|
||||||
|
background: linear-gradient(145deg, rgba(251, 191, 36, 0.2), rgba(245, 158, 11, 0.06));
|
||||||
|
color: #fcd34d;
|
||||||
|
font-size: 1.65rem;
|
||||||
|
text-shadow: 0 0 16px rgba(251, 191, 36, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-heading-copy {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-eyebrow {
|
||||||
|
margin: 0 0 0.3rem;
|
||||||
|
color: #fbbf24;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-heading h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-highlight);
|
||||||
|
font-size: clamp(1.1rem, 2vw, 1.35rem);
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-heading p {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 0.4rem 0 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 205px), 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-option {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 112px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
padding: 1rem 2.4rem 1rem 1rem;
|
||||||
|
border: 1px solid rgba(110, 123, 148, 0.28);
|
||||||
|
border-radius: 13px;
|
||||||
|
background: rgba(9, 13, 22, 0.62);
|
||||||
|
color: var(--text-main);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 150ms ease, background 150ms ease, box-shadow 150ms ease, transform 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-option:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: rgba(251, 191, 36, 0.55);
|
||||||
|
background: rgba(32, 34, 42, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-option:focus-visible {
|
||||||
|
outline: 3px solid rgba(56, 189, 248, 0.8);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-option.is-selected {
|
||||||
|
border-color: rgba(251, 191, 36, 0.88);
|
||||||
|
background: linear-gradient(145deg, rgba(120, 82, 22, 0.34), rgba(37, 31, 27, 0.82));
|
||||||
|
box-shadow: 0 0 0 1px rgba(251, 191, 36, 0.18), 0 8px 24px rgba(0, 0, 0, 0.17);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-option[data-kind="off"].is-selected {
|
||||||
|
border-color: rgba(148, 163, 184, 0.7);
|
||||||
|
background: linear-gradient(145deg, rgba(71, 85, 105, 0.3), rgba(24, 29, 40, 0.88));
|
||||||
|
box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-option-title {
|
||||||
|
color: var(--text-highlight);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-option-detail {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-option-check {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.8rem;
|
||||||
|
right: 0.85rem;
|
||||||
|
display: grid;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: transparent;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-option.is-selected .aha-help-option-check {
|
||||||
|
border-color: #fbbf24;
|
||||||
|
background: #fbbf24;
|
||||||
|
color: #171717;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-top: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-save-state {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-state-dot {
|
||||||
|
flex: 0 0 9px;
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #94a3b8;
|
||||||
|
box-shadow: 0 0 9px currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-save-state[data-state="saved"] .aha-help-state-dot { background: #34d399; }
|
||||||
|
.aha-help-save-state[data-state="unsaved"] .aha-help-state-dot { background: #fbbf24; }
|
||||||
|
.aha-help-save-state[data-state="error"] .aha-help-state-dot { background: #fb7185; }
|
||||||
|
.aha-help-save-state[data-state="unavailable"] .aha-help-state-dot { background: #94a3b8; }
|
||||||
|
|
||||||
|
.aha-help-save-button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 46px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
padding: 0.7rem 1.15rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-native-select {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip-path: inset(50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.switch {
|
.switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -903,6 +1104,11 @@ input:checked + .slider:before {
|
|||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#btn-save-aha-help:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.form-input, .form-select {
|
.form-input, .form-select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.65rem 0.85rem;
|
padding: 0.65rem 0.85rem;
|
||||||
@@ -3390,4 +3596,153 @@ input:checked + .slider:before {
|
|||||||
.build-guide-stat-grid {
|
.build-guide-stat-grid {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aha-help-actions {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-save-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 390px) {
|
||||||
|
.aha-help-options {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-heading {
|
||||||
|
gap: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aha-help-emblem {
|
||||||
|
flex-basis: 40px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anomaly Arbitration monster wave editor */
|
||||||
|
.monster-settings-intro { margin-bottom: 1rem; }
|
||||||
|
.monster-settings-rule { display: flex; gap: .65rem; align-items: flex-start; margin-top: .8rem; padding: .8rem 1rem; border: 1px solid rgba(251, 191, 36, .28); border-radius: 10px; background: rgba(245, 158, 11, .08); color: var(--text-main); line-height: 1.55; }
|
||||||
|
.monster-settings-tabs { display: flex; gap: .5rem; margin: 1rem 0; padding: .45rem; overflow-x: auto; border: 1px solid var(--border-color); border-radius: 12px; background: var(--bg-secondary); }
|
||||||
|
.monster-settings-tab { flex: 1 0 auto; min-height: 44px; padding: .65rem .9rem; border: 1px solid transparent; border-radius: 9px; background: transparent; color: var(--text-muted); font: inherit; font-weight: 700; white-space: nowrap; cursor: pointer; }
|
||||||
|
.monster-settings-tab:hover { color: var(--text-main); background: rgba(148, 163, 184, .08); }
|
||||||
|
.monster-settings-tab.active { color: var(--text-highlight); border-color: rgba(56, 189, 248, .36); background: rgba(14, 165, 233, .13); }
|
||||||
|
.monster-settings-tab-aha.active { color: #fcd34d; border-color: rgba(251, 191, 36, .45); background: rgba(245, 158, 11, .12); }
|
||||||
|
.monster-settings-status { margin: .75rem 0; padding: .8rem 1rem; border: 1px solid var(--border-color); border-radius: 10px; color: var(--text-muted); background: var(--bg-card); line-height: 1.5; }
|
||||||
|
.monster-settings-status[data-state="saved"] { color: #6ee7b7; }
|
||||||
|
.monster-settings-status[data-state="error"] { color: #fda4af; }
|
||||||
|
.monster-settings-status[data-state="loading"] { color: #7dd3fc; }
|
||||||
|
.monster-settings-view { display: grid; gap: 1rem; }
|
||||||
|
.monster-settings-picker-section { display: grid; gap: .65rem; }
|
||||||
|
.monster-settings-picker-title { margin: 0; color: var(--text-muted); font-size: .88rem; font-weight: 800; letter-spacing: .02em; }
|
||||||
|
.monster-settings-group-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 290px), 1fr)); gap: .75rem; }
|
||||||
|
.monster-settings-group-card { display: flex; min-width: 0; min-height: 78px; align-items: center; justify-content: space-between; gap: .75rem; padding: .75rem .85rem; border: 1px solid var(--border-color); border-radius: 12px; background: var(--bg-card); color: var(--text-main); text-align: left; cursor: pointer; transition: border-color 140ms ease, background 140ms ease, transform 140ms ease; }
|
||||||
|
.monster-settings-group-card:hover, .monster-settings-stage-card:hover { transform: translateY(-1px); border-color: rgba(56, 189, 248, .45); background: rgba(25, 35, 51, .95); }
|
||||||
|
.monster-settings-group-card.active { border-color: rgba(56, 189, 248, .72); background: linear-gradient(125deg, rgba(14, 165, 233, .16), rgba(15, 23, 42, .9)); box-shadow: inset 0 0 0 1px rgba(56, 189, 248, .1); }
|
||||||
|
.monster-settings-group-copy, .monster-settings-stage-copy { display: grid; gap: .3rem; min-width: 0; }
|
||||||
|
.monster-settings-group-name { color: var(--text-main); font-size: .91rem; line-height: 1.3; overflow-wrap: anywhere; }
|
||||||
|
.monster-settings-group-meta, .monster-settings-stage-meta { color: var(--text-muted); font-size: .75rem; line-height: 1.3; }
|
||||||
|
.monster-settings-monster.group-feature { flex: 0 0 56px; width: 56px; padding: 0; border: 0; background: transparent; }
|
||||||
|
.monster-settings-monster.group-feature .monster-settings-monster-portrait { width: 56px; height: 56px; flex-basis: 56px; }
|
||||||
|
.monster-settings-monster.group-feature .monster-settings-monster-copy { display: none; }
|
||||||
|
.monster-settings-stage-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 310px), 1fr)); gap: .75rem; }
|
||||||
|
.monster-settings-stage-card { display: flex; min-width: 0; min-height: 144px; align-items: center; gap: .9rem; padding: .85rem; border: 1px solid var(--border-color); border-radius: 14px; background: var(--bg-card); color: var(--text-main); text-align: left; cursor: pointer; transition: border-color 140ms ease, background 140ms ease, transform 140ms ease; }
|
||||||
|
.monster-settings-stage-card.active { border-color: rgba(56, 189, 248, .76); background: linear-gradient(125deg, rgba(14, 165, 233, .16), rgba(15, 23, 42, .94)); box-shadow: inset 0 0 0 1px rgba(56, 189, 248, .12); }
|
||||||
|
.monster-settings-stage-card.is-boss { border-color: rgba(251, 191, 36, .3); }
|
||||||
|
.monster-settings-stage-card.is-boss.active { border-color: rgba(251, 191, 36, .78); background: linear-gradient(125deg, rgba(245, 158, 11, .16), rgba(15, 23, 42, .94)); }
|
||||||
|
.monster-settings-stage-card:focus-visible, .monster-settings-group-card:focus-visible { outline: 2px solid rgba(56, 189, 248, .75); outline-offset: 2px; }
|
||||||
|
.monster-settings-monster.stage-feature { flex: 0 0 92px; width: 92px; flex-direction: column; align-items: center; padding: 0; border: 0; background: transparent; }
|
||||||
|
.monster-settings-monster.stage-feature .monster-settings-monster-portrait { width: 88px; height: 88px; flex-basis: 88px; border-radius: 12px; }
|
||||||
|
.monster-settings-monster.stage-feature .monster-settings-monster-copy { display: none; }
|
||||||
|
.monster-settings-stage-copy { flex: 1; }
|
||||||
|
.monster-settings-stage-copy strong { color: var(--text-main); font-size: .92rem; line-height: 1.28; overflow-wrap: anywhere; }
|
||||||
|
.monster-settings-stage-card.is-boss .monster-settings-stage-meta:first-of-type { color: #fcd34d; }
|
||||||
|
.monster-settings-view[hidden] { display: none; }
|
||||||
|
.monster-aa-round-control { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, .9fr); align-items: center; gap: 1.25rem; padding: 1rem 1.1rem; border: 1px solid rgba(56, 189, 248, .28); border-radius: 14px; background: linear-gradient(120deg, rgba(14, 165, 233, .1), rgba(15, 23, 42, .72)); }
|
||||||
|
.monster-aa-round-header { display: grid; gap: .6rem; min-width: 0; }
|
||||||
|
.monster-aa-round-copy h3 { margin: 0; color: var(--text-highlight); font-size: 1rem; }
|
||||||
|
.monster-aa-round-copy p { margin: .35rem 0 0; color: var(--text-muted); line-height: 1.5; }
|
||||||
|
.monster-aa-round-status { color: var(--text-muted); font-size: .88rem; line-height: 1.45; overflow-wrap: anywhere; }
|
||||||
|
.monster-aa-round-status.is-pending { color: #86efac; }
|
||||||
|
.monster-aa-round-status.is-error { color: #fda4af; }
|
||||||
|
.monster-aa-round-picker-wrap { display: grid; gap: .5rem; min-width: 0; }
|
||||||
|
.monster-aa-round-picker-label { color: var(--text-muted); font-size: .82rem; font-weight: 800; }
|
||||||
|
.monster-aa-round-picker { min-height: 46px; width: 100%; }
|
||||||
|
.monster-aa-round-picker:disabled { opacity: .55; cursor: not-allowed; }
|
||||||
|
.monster-settings-stage-heading { display: flex; align-items: center; gap: .65rem; flex-wrap: wrap; }
|
||||||
|
.monster-settings-stage-heading h3 { margin: 0; color: var(--text-highlight); font-size: 1.1rem; }
|
||||||
|
.monster-settings-badge { padding: .25rem .55rem; border: 1px solid rgba(192, 132, 252, .3); border-radius: 999px; color: #d8b4fe; background: rgba(168, 85, 247, .1); font-size: .75rem; font-weight: 700; }
|
||||||
|
.monster-settings-battle { display: grid; gap: .7rem; padding: 1rem; border: 1px solid var(--border-color); border-radius: 12px; background: var(--bg-card); }
|
||||||
|
.monster-settings-battle h4 { margin: 0; color: var(--text-main); }
|
||||||
|
.monster-settings-encounter-heading { display: flex; align-items: center; gap: .55rem; flex-wrap: wrap; }
|
||||||
|
.monster-settings-id-badge { color: var(--text-muted); font-size: .76rem; }
|
||||||
|
.monster-settings-meta { display: flex; gap: .55rem; flex-wrap: wrap; color: var(--text-muted); font-size: .8rem; }
|
||||||
|
.monster-settings-wave { display: flex; align-items: flex-start; gap: .8rem; padding: .7rem; border: 1px solid rgba(148, 163, 184, .13); border-radius: 9px; background: rgba(148, 163, 184, .035); }
|
||||||
|
.monster-settings-wave strong { flex: 0 0 85px; color: var(--text-muted); font-size: .82rem; }
|
||||||
|
.monster-settings-monsters { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 230px), 1fr)); gap: .65rem; flex: 1; min-width: 0; }
|
||||||
|
.monster-settings-monster { display: flex; align-items: center; gap: .75rem; min-width: 0; padding: .5rem .65rem; border: 1px solid rgba(56, 189, 248, .16); border-radius: 10px; background: rgba(14, 165, 233, .055); }
|
||||||
|
.monster-settings-monster-portrait { position: relative; display: grid; flex: 0 0 72px; width: 72px; height: 72px; place-items: center; overflow: hidden; border-radius: 10px; background: rgba(15, 23, 42, .8); font-size: 1.55rem; }
|
||||||
|
.monster-settings-monster-portrait img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
.monster-settings-monster-copy { display: grid; gap: .18rem; min-width: 0; }
|
||||||
|
.monster-settings-monster-copy strong { overflow-wrap: anywhere; color: var(--text-main); font-size: .86rem; line-height: 1.25; }
|
||||||
|
.monster-settings-monster-copy code { width: fit-content; color: var(--text-muted); font-size: .72rem; }
|
||||||
|
.monster-settings-monster.compact { padding: .25rem; }
|
||||||
|
.monster-settings-empty { padding: 1.4rem; border: 1px dashed rgba(148, 163, 184, .3); border-radius: 12px; color: var(--text-muted); background: var(--bg-card); text-align: center; line-height: 1.6; }
|
||||||
|
.monster-settings-aha-help[hidden] { display: none; }
|
||||||
|
.monster-settings-aha-help { margin-top: 1rem; }
|
||||||
|
.monster-custom-card { display: grid; gap: .85rem; }
|
||||||
|
.monster-custom-card h3 { margin: 0; color: var(--text-highlight); }
|
||||||
|
.monster-custom-card > .card-subtitle { margin: 0; }
|
||||||
|
.monster-settings-source { color: var(--text-muted); font-size: .82rem; }
|
||||||
|
.monster-custom-wave-list { display: grid; gap: .8rem; }
|
||||||
|
.monster-custom-wave { display: grid; gap: .75rem; padding: .9rem; border: 1px solid rgba(110, 123, 148, .23); border-radius: 12px; background: rgba(9, 13, 22, .5); }
|
||||||
|
.monster-custom-wave-heading, .monster-custom-row, .monster-custom-toolbar, .monster-custom-actions, .monster-custom-save-actions { display: flex; align-items: center; gap: .65rem; }
|
||||||
|
.monster-custom-wave-heading, .monster-custom-toolbar { justify-content: space-between; }
|
||||||
|
.monster-custom-wave-heading strong { color: var(--text-highlight); }
|
||||||
|
.monster-custom-rows { display: grid; gap: .55rem; }
|
||||||
|
.monster-custom-row { align-items: flex-end; padding: .7rem; border: 1px solid rgba(110, 123, 148, .16); border-radius: 10px; background: rgba(255, 255, 255, .018); }
|
||||||
|
.monster-custom-field { display: flex; min-width: 0; flex: 1 1 0; flex-direction: column; gap: .35rem; color: var(--text-muted); font-size: .76rem; font-weight: 700; }
|
||||||
|
.monster-custom-field input { width: 100%; min-height: 42px; padding: .55rem .65rem; border: 1px solid rgba(110, 123, 148, .32); border-radius: 8px; background: rgba(7, 10, 17, .76); color: var(--text-main); }
|
||||||
|
.monster-custom-field input:focus { outline: 2px solid rgba(56, 189, 248, .55); outline-offset: 1px; }
|
||||||
|
.monster-custom-monster-preview { min-height: 62px; }
|
||||||
|
.monster-custom-monster-field { min-width: 250px; }
|
||||||
|
.monster-custom-picker { position: relative; width: 100%; }
|
||||||
|
.monster-custom-picker-results { position: absolute; z-index: 30; top: calc(100% + .35rem); left: 0; right: 0; display: grid; max-height: 340px; overflow-y: auto; padding: .35rem; border: 1px solid rgba(92, 115, 148, .55); border-radius: 12px; background: #111827; box-shadow: 0 18px 42px rgba(0, 0, 0, .5); }
|
||||||
|
.monster-custom-picker-results[hidden] { display: none; }
|
||||||
|
.monster-custom-picker-option { display: flex; width: 100%; min-width: 0; padding: .25rem; border: 0; border-radius: 8px; background: transparent; color: var(--text-main); text-align: left; cursor: pointer; }
|
||||||
|
.monster-custom-picker-option:hover, .monster-custom-picker-option:focus-visible { outline: none; background: rgba(56, 189, 248, .14); }
|
||||||
|
.monster-custom-picker-option .monster-settings-monster { width: 100%; padding: .25rem .35rem; border: 0; background: transparent; }
|
||||||
|
.monster-custom-picker-option .monster-settings-monster-portrait { flex-basis: 52px; width: 52px; height: 52px; }
|
||||||
|
.monster-custom-picker-option .monster-settings-monster-copy strong { font-size: .84rem; }
|
||||||
|
.monster-custom-picker-option .monster-settings-monster-copy code { font-size: .7rem; }
|
||||||
|
.monster-custom-monster-preview .monster-settings-monster { width: fit-content; max-width: 100%; padding: .2rem .35rem; }
|
||||||
|
.monster-custom-monster-preview .monster-settings-monster-portrait { flex-basis: 80px; width: 80px; height: 80px; }
|
||||||
|
.monster-custom-monster-preview .monster-settings-monster-copy strong { font-size: .76rem; }
|
||||||
|
.monster-custom-monster-preview .monster-settings-monster-copy code { font-size: .66rem; }
|
||||||
|
.monster-custom-remove { min-width: 42px; min-height: 42px; }
|
||||||
|
.monster-custom-add-enemy { justify-self: start; }
|
||||||
|
.monster-custom-save-actions { justify-content: flex-end; }
|
||||||
|
.monster-custom-save-actions .monster-settings-status { margin: 0; }
|
||||||
|
.monster-custom-card button:disabled, .monster-custom-card input:disabled { cursor: not-allowed; opacity: .55; }
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.monster-settings-wave { flex-direction: column; }
|
||||||
|
.monster-settings-monsters { width: 100%; }
|
||||||
|
.monster-settings-stage-grid { grid-template-columns: 1fr; }
|
||||||
|
.monster-aa-round-control { grid-template-columns: 1fr; align-items: stretch; }
|
||||||
|
.monster-custom-toolbar, .monster-custom-save-actions { align-items: stretch; flex-direction: column; }
|
||||||
|
.monster-custom-actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(145px, 1fr)); }
|
||||||
|
.monster-custom-save-actions .btn { width: 100%; }
|
||||||
|
.monster-custom-row { align-items: stretch; flex-wrap: wrap; }
|
||||||
|
.monster-custom-field { flex-basis: calc(50% - .65rem); }
|
||||||
|
.monster-custom-monster-field { flex-basis: 100%; }
|
||||||
|
.monster-custom-remove { margin-left: auto; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 390px) {
|
||||||
|
.monster-settings-wave { flex-direction: column; }
|
||||||
|
.monster-custom-field { flex-basis: 100%; }
|
||||||
}
|
}
|
||||||
+151
-6
@@ -4,7 +4,8 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>GaC-Srtool</title>
|
<title>GaC-Srtool</title>
|
||||||
<link rel="stylesheet" href="css/styles.css?v=2.3">
|
<link rel="stylesheet" href="css/styles.css?v=2.16">
|
||||||
|
<link rel="stylesheet" href="css/item-catalog.css?v=2">
|
||||||
<link rel="icon" type="image/png" href="assets/logo.png">
|
<link rel="icon" type="image/png" href="assets/logo.png">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -29,8 +30,8 @@
|
|||||||
<span>🌐</span>
|
<span>🌐</span>
|
||||||
<select id="server-mode-select" class="server-mode-select">
|
<select id="server-mode-select" class="server-mode-select">
|
||||||
<option value="gac" selected>GaC-hkrpg-go</option>
|
<option value="gac" selected>GaC-hkrpg-go</option>
|
||||||
<option value="firefly">FireflyGo</option>
|
<option value="firefly" disabled data-i18n="serverModeFireflyPaused">FireflyGo (กำลังพัฒนา)</option>
|
||||||
<option value="robin">RobinSR</option>
|
<option value="robin" disabled data-i18n="serverModeRobinPaused">RobinSR (กำลังพัฒนา)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
<button class="tab-btn" data-tab="tab-avatars" data-i18n="tabAvatars">👥 ตัวละคร</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-equipment" data-i18n="tabEquipment">🗡️ กรวยแสง & รีลิกส์</button>
|
||||||
<button class="tab-btn" data-tab="tab-inventory" data-i18n="tabInventory">🎒 ไอเทมในกระเป๋า</button>
|
<button class="tab-btn" data-tab="tab-inventory" data-i18n="tabInventory">🎒 ไอเทมในกระเป๋า</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-monster-settings" id="tab-btn-monster-settings" data-i18n="tabMonsterSettings">👾 Monster Setting</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-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-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>
|
<button class="tab-btn" data-tab="tab-exrank" id="tab-btn-exrank" data-i18n="tabExrank">⚡ สกิลพิเศษ Ex-Rank</button>
|
||||||
@@ -240,6 +242,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-subtitle" style="margin-bottom: 0.25rem;">Send GM and server commands, with live status and packet logs.</p>
|
<p class="card-subtitle" style="margin-bottom: 0.25rem;">Send GM and server commands, with live status and packet logs.</p>
|
||||||
|
<div style="display:flex; align-items:center; gap:0.65rem; flex-wrap:wrap;">
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" id="btn-client-unstuck">Unstuck</button>
|
||||||
|
<span id="client-unstuck-description" style="font-size:0.75rem; color:var(--text-muted);">Clear a stuck loading spinner without restarting the game. Keeps saved CW progress.</span>
|
||||||
|
</div>
|
||||||
|
<div id="client-unstuck-status" role="status" aria-live="polite" style="font-size:0.75rem; color:var(--text-muted);"></div>
|
||||||
|
|
||||||
<!-- Interactive Terminal Screen -->
|
<!-- Interactive Terminal Screen -->
|
||||||
<div class="console-terminal-wrapper">
|
<div class="console-terminal-wrapper">
|
||||||
@@ -392,6 +399,43 @@
|
|||||||
|
|
||||||
<!-- Material inventory editor: edits only Client-catalog item quantities. -->
|
<!-- Material inventory editor: edits only Client-catalog item quantities. -->
|
||||||
<section id="tab-inventory" class="tab-content">
|
<section id="tab-inventory" class="tab-content">
|
||||||
|
<div class="inventory-mode-switch" role="tablist" aria-label="Inventory views">
|
||||||
|
<button type="button" id="btn-inventory-catalog" class="inventory-mode-btn active" role="tab" aria-selected="true" aria-controls="item-catalog-panel" data-i18n="itemCatalogTitle">📚 สมุดรวมไอเทม</button>
|
||||||
|
<button type="button" id="btn-inventory-manage" class="inventory-mode-btn" role="tab" aria-selected="false" aria-controls="inventory-manage-panel" data-i18n="inventoryTitle">🎒 จัดการจำนวนไอเทม</button>
|
||||||
|
</div>
|
||||||
|
<div id="item-catalog-panel" class="inventory-mode-panel active" role="tabpanel">
|
||||||
|
<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="itemCatalogTitle">📚 สมุดรวมไอเทม</div>
|
||||||
|
<p class="card-subtitle" data-i18n="itemCatalogDescription">ค้นชื่อหรือ ID เพื่อดูรูปและรหัสจากตารางเกม</p>
|
||||||
|
</div>
|
||||||
|
<span id="item-catalog-count" class="badge-super-tag">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="item-catalog-toolbar">
|
||||||
|
<input type="search" id="item-catalog-search" class="form-input" data-i18n-placeholder="itemCatalogSearchPlaceholder" placeholder="ค้นหาชื่อหรือ Item ID...">
|
||||||
|
<select id="item-catalog-filter" class="form-select" aria-label="Filter item catalog">
|
||||||
|
<option value="all" data-i18n="itemCatalogFilterAll">ทั้งหมด</option>
|
||||||
|
<option value="supported" data-i18n="itemCatalogFilterSupported">GaC รองรับ</option>
|
||||||
|
<option value="item" data-i18n="itemCatalogFilterItems">ไอเทม</option>
|
||||||
|
<option value="lightcone" data-i18n="itemCatalogFilterLightCones">กรวยแสง</option>
|
||||||
|
<option value="relic" data-i18n="itemCatalogFilterRelics">รีลิกส์</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="item-catalog-status" role="status" aria-live="polite" style="font-size:.8rem;color:var(--text-muted);margin-top:.7rem;" data-i18n="itemCatalogLoading">กำลังโหลดสมุดไอเทม...</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="padding:1rem 1rem 0;overflow:hidden;">
|
||||||
|
<div id="item-catalog-grid" class="item-catalog-grid">
|
||||||
|
<div class="item-catalog-empty" data-i18n="itemCatalogLoading">กำลังโหลดสมุดไอเทม...</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-catalog-pagination">
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" id="item-catalog-prev" disabled data-i18n="inventoryPrevious">ก่อนหน้า</button>
|
||||||
|
<span id="item-catalog-page-label" style="font-size:.8rem;color:var(--text-muted);"></span>
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" id="item-catalog-next" disabled data-i18n="inventoryNext">ถัดไป</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="inventory-manage-panel" class="inventory-mode-panel" role="tabpanel" hidden>
|
||||||
<div class="card" style="margin-bottom: 1rem;">
|
<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 style="display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;flex-wrap:wrap;">
|
||||||
<div>
|
<div>
|
||||||
@@ -430,6 +474,57 @@
|
|||||||
<button type="button" class="btn btn-sm btn-secondary" id="btn-inventory-next" disabled data-i18n="inventoryNext">ถัดไป</button>
|
<button type="button" class="btn btn-sm btn-secondary" id="btn-inventory-next" disabled data-i18n="inventoryNext">ถัดไป</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Monster settings: resource-backed stage inspection plus Calyx-only custom waves. -->
|
||||||
|
<section id="tab-monster-settings" class="tab-content">
|
||||||
|
<div class="card monster-settings-intro">
|
||||||
|
<div class="card-title"><span data-i18n="monsterSettingsTitle">👾 Monster Setting</span></div>
|
||||||
|
<p class="card-subtitle" data-i18n="monsterSettingsDescription">ตรวจสอบข้อมูลด่านและคลื่นมอนจาก resource ของ Client ตามเวอร์ชัน ปรับ custom monster ได้เฉพาะ Golden / Crimson Calyx เท่านั้น</p>
|
||||||
|
<div class="monster-settings-rule" role="note">
|
||||||
|
<span aria-hidden="true">🔒</span>
|
||||||
|
<span data-i18n="monsterSettingsRule">Weekly Boss, Stagnant Shadow, Cavern of Corrosion และ Endgame ใช้ข้อมูลด่านจาก Client และแก้คลื่นมอนผ่าน Srtool ไม่ได้</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="monster-settings-tabs" role="tablist" aria-label="Monster settings modes">
|
||||||
|
<button type="button" class="monster-settings-tab active" role="tab" aria-selected="true" data-monster-mode="moc" data-i18n="monsterModeMoc">Memory of Chaos</button>
|
||||||
|
<button type="button" class="monster-settings-tab" role="tab" aria-selected="false" data-monster-mode="pf" data-i18n="monsterModePf">Pure Fiction</button>
|
||||||
|
<button type="button" class="monster-settings-tab" role="tab" aria-selected="false" data-monster-mode="as" data-i18n="monsterModeAs">Apocalyptic Shadow</button>
|
||||||
|
<button type="button" class="monster-settings-tab" role="tab" aria-selected="false" data-monster-mode="aa" data-i18n="monsterModeAa">Anomaly Arbitration</button>
|
||||||
|
<button type="button" class="monster-settings-tab monster-settings-tab-aha" id="monster-mode-aha" role="tab" aria-selected="false" aria-controls="monster-settings-aha-help" data-monster-mode="aha" data-i18n="ahaHelpTitle">Aha's Help</button>
|
||||||
|
<button type="button" class="monster-settings-tab" role="tab" aria-selected="false" data-monster-mode="custom" data-i18n="monsterModeCustom">Custom Enemy</button>
|
||||||
|
<button type="button" class="monster-settings-tab" role="tab" aria-selected="false" data-monster-mode="su" data-i18n="monsterModeSu">Simulated Universe</button>
|
||||||
|
</div>
|
||||||
|
<div id="monster-settings-status" class="monster-settings-status" role="status" aria-live="polite" data-i18n="monsterSettingsLoading">กำลังอ่าน stage จาก resource ของ Client...</div>
|
||||||
|
<div id="monster-settings-view" class="monster-settings-view" role="tabpanel" aria-live="polite"></div>
|
||||||
|
|
||||||
|
<div id="monster-settings-aha-help" class="monster-settings-aha-help" role="tabpanel" aria-labelledby="monster-mode-aha" hidden>
|
||||||
|
<div class="aha-help-card" id="aha-help-card">
|
||||||
|
<div class="aha-help-heading">
|
||||||
|
<div class="aha-help-emblem" aria-hidden="true">✦</div>
|
||||||
|
<div class="aha-help-heading-copy">
|
||||||
|
<h3 data-i18n="ahaHelpTitle">Aha's Help</h3>
|
||||||
|
<p data-i18n="ahaHelpDescription">Choose the support level for the next battle that includes Voracity Corruption. This setting is separate from Aha Global.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="aha-help-options" id="aha-help-options" role="group" aria-label="Aha's Help level" aria-describedby="aha-help-status"></div>
|
||||||
|
<select id="aha-help-level" class="aha-help-native-select" aria-hidden="true" tabindex="-1" disabled>
|
||||||
|
<option value="" data-i18n="ahaHelpLoading">Loading levels…</option>
|
||||||
|
</select>
|
||||||
|
<div class="aha-help-actions">
|
||||||
|
<div class="aha-help-save-state" id="aha-help-status" role="status" aria-live="polite" data-state="loading">
|
||||||
|
<span class="aha-help-state-dot" aria-hidden="true"></span>
|
||||||
|
<span id="aha-help-status-text" data-i18n="ahaHelpLoading">Loading levels…</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-success aha-help-save-button" id="btn-save-aha-help" disabled>
|
||||||
|
<span aria-hidden="true">💾</span>
|
||||||
|
<span data-i18n="ahaHelpSave">Save Aha's Help</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 3.5. Damage Analyst (In-Game Battle Telemetry & Damage Analysis) -->
|
<!-- 3.5. Damage Analyst (In-Game Battle Telemetry & Damage Analysis) -->
|
||||||
@@ -576,6 +671,17 @@
|
|||||||
<div class="card" id="card-persistent-global">
|
<div class="card" id="card-persistent-global">
|
||||||
<div class="card-title" data-i18n="persistentGlobalTitle">⚙️ สวิตช์ระบบพิเศษของ GaC</div>
|
<div class="card-title" data-i18n="persistentGlobalTitle">⚙️ สวิตช์ระบบพิเศษของ GaC</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-top: 1rem;">
|
<div style="display: flex; flex-direction: column; gap: 1rem; margin-top: 1rem;">
|
||||||
|
<div class="ex-switch-card">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 700;">Freecam / FaceCam</div>
|
||||||
|
<div style="font-size: 0.8rem; color: var(--text-muted);">เลือกโหมด แล้วกด Tab ในเกมเพื่อเปิดหรือปิดกล้อง</div>
|
||||||
|
<label for="camera-mode">โหมดกล้อง</label>
|
||||||
|
<select id="camera-mode"><option value="freecam">Freecam</option><option value="facecam">FaceCam (ทดลอง)</option></select>
|
||||||
|
<label for="camera-distance">ระยะ FaceCam</label>
|
||||||
|
<input id="camera-distance" type="number" min="0.5" max="5" step="0.1" value="2">
|
||||||
|
<button type="button" class="btn btn-success" id="btn-save-camera">บันทึกกล้อง</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="ex-switch-card">
|
<div class="ex-switch-card">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight: 700;" data-i18n="globalFeatureSilverWolf">Silver Wolf Global Feature</div>
|
<div style="font-weight: 700;" data-i18n="globalFeatureSilverWolf">Silver Wolf Global Feature</div>
|
||||||
@@ -615,6 +721,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 5. Ex-Rank Tab -->
|
<!-- 5. Ex-Rank Tab -->
|
||||||
@@ -681,8 +788,8 @@
|
|||||||
<label class="form-label" data-i18n="lblServerMode">โหมดเซิร์ฟเวอร์</label>
|
<label class="form-label" data-i18n="lblServerMode">โหมดเซิร์ฟเวอร์</label>
|
||||||
<select id="setting-server-mode" class="form-select">
|
<select id="setting-server-mode" class="form-select">
|
||||||
<option value="gac">GaC-hkrpg-go</option>
|
<option value="gac">GaC-hkrpg-go</option>
|
||||||
<option value="firefly">FireflyGo</option>
|
<option value="firefly" disabled data-i18n="serverModeFireflyPaused">FireflyGo (กำลังพัฒนา)</option>
|
||||||
<option value="robin">RobinSR</option>
|
<option value="robin" disabled data-i18n="serverModeRobinPaused">RobinSR (กำลังพัฒนา)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1668,10 +1775,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<dialog id="item-catalog-detail" class="item-catalog-dialog" aria-labelledby="item-catalog-detail-name">
|
||||||
|
<div class="item-catalog-detail-shell">
|
||||||
|
<header class="item-catalog-detail-header">
|
||||||
|
<span data-i18n="itemCatalogDetailTitle">Item Details</span>
|
||||||
|
<button type="button" class="item-catalog-dialog-close" id="item-catalog-detail-close" data-i18n-aria-label="itemCatalogClose" aria-label="Close">×</button>
|
||||||
|
</header>
|
||||||
|
<div class="item-catalog-detail-body">
|
||||||
|
<div class="item-catalog-detail-art" id="item-catalog-detail-art"></div>
|
||||||
|
<div class="item-catalog-detail-copy">
|
||||||
|
<h2 id="item-catalog-detail-name"></h2>
|
||||||
|
<div class="item-catalog-detail-id" id="item-catalog-detail-id"></div>
|
||||||
|
<div class="item-catalog-detail-source"><span data-i18n="itemCatalogSourceTable">Source tables</span>: <span id="item-catalog-detail-source"></span></div>
|
||||||
|
<div class="item-catalog-detail-meta" id="item-catalog-detail-meta"></div>
|
||||||
|
<section class="item-catalog-detail-section">
|
||||||
|
<h3 data-i18n="itemCatalogItemDescription">Description</h3>
|
||||||
|
<div class="item-catalog-detail-description" id="item-catalog-detail-description"></div>
|
||||||
|
</section>
|
||||||
|
<section class="item-catalog-detail-section item-catalog-detail-lore" id="item-catalog-detail-lore-section" hidden>
|
||||||
|
<h3 data-i18n="itemCatalogLore">Background</h3>
|
||||||
|
<div id="item-catalog-detail-lore"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-catalog-detail-quantity">
|
||||||
|
<label for="item-catalog-detail-amount" data-i18n="itemCatalogQuantity">Quantity</label>
|
||||||
|
<div class="item-catalog-amount-stepper">
|
||||||
|
<button type="button" id="item-catalog-amount-minus" data-i18n-aria-label="itemCatalogDecrease" aria-label="Decrease quantity">−</button>
|
||||||
|
<input type="number" id="item-catalog-detail-amount" inputmode="numeric" min="1" max="1" value="1">
|
||||||
|
<button type="button" id="item-catalog-amount-plus" data-i18n-aria-label="itemCatalogIncrease" aria-label="Increase quantity">+</button>
|
||||||
|
</div>
|
||||||
|
<span id="item-catalog-amount-limit" class="item-catalog-amount-limit"></span>
|
||||||
|
</div>
|
||||||
|
<footer class="item-catalog-detail-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" id="item-catalog-detail-done" data-i18n="itemCatalogClose">Close</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<!-- Toast Notification Container -->
|
<!-- Toast Notification Container -->
|
||||||
<div class="toast-container" id="toast-container"></div>
|
<div class="toast-container" id="toast-container"></div>
|
||||||
|
|
||||||
<!-- Script Modules -->
|
<!-- Script Modules -->
|
||||||
<script type="module" src="js/app.js?v=2.9"></script>
|
<script type="module" src="js/app.js?v=3.3"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+409
@@ -6613,6 +6613,234 @@ const LAST_MILE_TRANSLATIONS = {
|
|||||||
pt: { offline: 'Desconectado', exrankToggleSuccess: 'Ex-Rank de {name}: {status}' }
|
pt: { offline: 'Desconectado', exrankToggleSuccess: 'Ex-Rank de {name}: {status}' }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AHA_HELP_TRANSLATIONS = {
|
||||||
|
th: {
|
||||||
|
ahaHelpMode: 'โหมด Anomaly Arbitration', ahaHelpTitle: 'ความช่วยเหลือจาก Aha',
|
||||||
|
ahaHelpDescription: 'เลือกระดับความช่วยเหลือสำหรับการต่อสู้ครั้งถัดไปที่มี Voracity Corruption ค่านี้แยกจาก Aha Global',
|
||||||
|
ahaHelpLegacyTitle: 'ใช้ค่าจาก FreeSR', ahaHelpLegacyDescription: 'ให้ FreeSR ควบคุมการตั้งค่านี้ (ตอนนี้: {status})',
|
||||||
|
ahaHelpOffTitle: 'ปิด', ahaHelpOffDescription: 'ไม่เพิ่มระดับ Aha’s Help', ahaHelpProgress: 'เกณฑ์ความคืบหน้า {percent}%',
|
||||||
|
ahaHelpConnect: 'เชื่อมต่อเซิร์ฟเวอร์ GaC เพื่อเลือกระดับ', ahaHelpLoading: 'กำลังโหลดระดับ…',
|
||||||
|
ahaHelpNoLevels: 'ไม่พบข้อมูลระดับ Aha’s Help', ahaHelpLoadError: 'โหลดระดับไม่สำเร็จ: {error}',
|
||||||
|
ahaHelpStatusSaved: 'บันทึกแล้ว · มีผลเมื่อเริ่มการต่อสู้ครั้งถัดไป', ahaHelpStatusUnsaved: 'มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก',
|
||||||
|
ahaHelpStatusSaveError: 'บันทึกไม่สำเร็จ: {error}', ahaHelpSave: 'บันทึกการตั้งค่า', ahaHelpSavedToast: 'บันทึก Aha’s Help แล้ว มีผลเมื่อเริ่มการต่อสู้ครั้งถัดไป'
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
ahaHelpMode: 'ANOMALY ARBITRATION', ahaHelpTitle: 'Aha’s Help',
|
||||||
|
ahaHelpDescription: 'Choose the support level for the next battle with Voracity Corruption. This is separate from Aha Global.',
|
||||||
|
ahaHelpLegacyTitle: 'Use FreeSR setting', ahaHelpLegacyDescription: 'Let FreeSR control this setting (currently: {status})',
|
||||||
|
ahaHelpOffTitle: 'Off', ahaHelpOffDescription: 'Do not add an Aha’s Help level', ahaHelpProgress: '{percent}% progress threshold',
|
||||||
|
ahaHelpConnect: 'Connect to a GaC server to choose a level', ahaHelpLoading: 'Loading levels…',
|
||||||
|
ahaHelpNoLevels: 'No Aha’s Help levels were returned', ahaHelpLoadError: 'Could not load levels: {error}',
|
||||||
|
ahaHelpStatusSaved: 'Saved · applies when the next battle starts', ahaHelpStatusUnsaved: 'You have unsaved changes',
|
||||||
|
ahaHelpStatusSaveError: 'Could not save: {error}', ahaHelpSave: 'Save settings', ahaHelpSavedToast: 'Aha’s Help saved. It applies when the next battle starts.'
|
||||||
|
},
|
||||||
|
'zh-CN': {
|
||||||
|
ahaHelpMode: '异常仲裁模式', ahaHelpTitle: '阿哈的帮助',
|
||||||
|
ahaHelpDescription: '选择下一场含有贪欲腐蚀的战斗所用的帮助等级。此设置独立于阿哈全局功能。',
|
||||||
|
ahaHelpLegacyTitle: '使用 FreeSR 设置', ahaHelpLegacyDescription: '由 FreeSR 控制此设置(当前:{status})',
|
||||||
|
ahaHelpOffTitle: '关闭', ahaHelpOffDescription: '不添加阿哈的帮助等级', ahaHelpProgress: '进度阈值 {percent}%',
|
||||||
|
ahaHelpConnect: '连接 GaC 服务器后才能选择等级', ahaHelpLoading: '正在加载等级…',
|
||||||
|
ahaHelpNoLevels: '未找到阿哈的帮助等级数据', ahaHelpLoadError: '无法加载等级:{error}',
|
||||||
|
ahaHelpStatusSaved: '已保存 · 下次战斗开始时生效', ahaHelpStatusUnsaved: '有尚未保存的更改',
|
||||||
|
ahaHelpStatusSaveError: '保存失败:{error}', ahaHelpSave: '保存设置', ahaHelpSavedToast: '阿哈的帮助已保存,将在下次战斗开始时生效。'
|
||||||
|
},
|
||||||
|
'zh-TW': {
|
||||||
|
ahaHelpMode: '異常仲裁模式', ahaHelpTitle: '阿哈的幫助',
|
||||||
|
ahaHelpDescription: '選擇下一場含有貪欲腐蝕的戰鬥所用的幫助等級。此設定獨立於阿哈全域功能。',
|
||||||
|
ahaHelpLegacyTitle: '使用 FreeSR 設定', ahaHelpLegacyDescription: '由 FreeSR 控制此設定(目前:{status})',
|
||||||
|
ahaHelpOffTitle: '關閉', ahaHelpOffDescription: '不新增阿哈的幫助等級', ahaHelpProgress: '進度門檻 {percent}%',
|
||||||
|
ahaHelpConnect: '連線至 GaC 伺服器後才能選擇等級', ahaHelpLoading: '正在載入等級…',
|
||||||
|
ahaHelpNoLevels: '找不到阿哈的幫助等級資料', ahaHelpLoadError: '無法載入等級:{error}',
|
||||||
|
ahaHelpStatusSaved: '已儲存 · 下次戰鬥開始時生效', ahaHelpStatusUnsaved: '有尚未儲存的變更',
|
||||||
|
ahaHelpStatusSaveError: '儲存失敗:{error}', ahaHelpSave: '儲存設定', ahaHelpSavedToast: '阿哈的幫助已儲存,將於下次戰鬥開始時生效。'
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
ahaHelpMode: '異常アービトレーション', ahaHelpTitle: 'アハの助け',
|
||||||
|
ahaHelpDescription: '貪欲の侵食が発生する次の戦闘で使うサポートレベルを選択します。アハのグローバル設定とは別の設定です。',
|
||||||
|
ahaHelpLegacyTitle: 'FreeSRの設定を使用', ahaHelpLegacyDescription: 'この設定をFreeSRに任せます(現在:{status})',
|
||||||
|
ahaHelpOffTitle: 'オフ', ahaHelpOffDescription: 'アハの助けのレベルを追加しません', ahaHelpProgress: '進行度のしきい値 {percent}%',
|
||||||
|
ahaHelpConnect: 'レベルを選択するにはGaCサーバーに接続してください', ahaHelpLoading: 'レベルを読み込み中…',
|
||||||
|
ahaHelpNoLevels: 'アハの助けのレベル情報がありません', ahaHelpLoadError: 'レベルを読み込めませんでした:{error}',
|
||||||
|
ahaHelpStatusSaved: '保存済み · 次の戦闘開始時に反映', ahaHelpStatusUnsaved: '未保存の変更があります',
|
||||||
|
ahaHelpStatusSaveError: '保存できませんでした:{error}', ahaHelpSave: '設定を保存', ahaHelpSavedToast: 'アハの助けを保存しました。次の戦闘開始時に反映されます。'
|
||||||
|
},
|
||||||
|
ko: {
|
||||||
|
ahaHelpMode: '이상 중재 모드', ahaHelpTitle: '아하의 도움',
|
||||||
|
ahaHelpDescription: '탐식의 침식이 포함된 다음 전투에서 사용할 지원 레벨을 선택합니다. 아하 전역 설정과는 별개입니다.',
|
||||||
|
ahaHelpLegacyTitle: 'FreeSR 설정 사용', ahaHelpLegacyDescription: '이 설정을 FreeSR에서 제어합니다 (현재: {status})',
|
||||||
|
ahaHelpOffTitle: '끔', ahaHelpOffDescription: '아하의 도움 레벨을 추가하지 않습니다', ahaHelpProgress: '진행도 기준 {percent}%',
|
||||||
|
ahaHelpConnect: '레벨을 선택하려면 GaC 서버에 연결하세요', ahaHelpLoading: '레벨 불러오는 중…',
|
||||||
|
ahaHelpNoLevels: '아하의 도움 레벨 데이터가 없습니다', ahaHelpLoadError: '레벨을 불러오지 못했습니다: {error}',
|
||||||
|
ahaHelpStatusSaved: '저장됨 · 다음 전투 시작 시 적용', ahaHelpStatusUnsaved: '저장하지 않은 변경 사항이 있습니다',
|
||||||
|
ahaHelpStatusSaveError: '저장하지 못했습니다: {error}', ahaHelpSave: '설정 저장', ahaHelpSavedToast: '아하의 도움을 저장했습니다. 다음 전투 시작 시 적용됩니다.'
|
||||||
|
},
|
||||||
|
vi: {
|
||||||
|
ahaHelpMode: 'CHẾ ĐỘ ANOMALY ARBITRATION', ahaHelpTitle: 'Sự trợ giúp của Aha',
|
||||||
|
ahaHelpDescription: 'Chọn cấp hỗ trợ cho trận chiến tiếp theo có Voracity Corruption. Thiết lập này độc lập với Aha Global.',
|
||||||
|
ahaHelpLegacyTitle: 'Dùng thiết lập FreeSR', ahaHelpLegacyDescription: 'Để FreeSR điều khiển thiết lập này (hiện tại: {status})',
|
||||||
|
ahaHelpOffTitle: 'Tắt', ahaHelpOffDescription: 'Không thêm cấp Sự trợ giúp của Aha', ahaHelpProgress: 'Ngưỡng tiến độ {percent}%',
|
||||||
|
ahaHelpConnect: 'Kết nối máy chủ GaC để chọn cấp', ahaHelpLoading: 'Đang tải các cấp…',
|
||||||
|
ahaHelpNoLevels: 'Không tìm thấy dữ liệu cấp Sự trợ giúp của Aha', ahaHelpLoadError: 'Không tải được cấp: {error}',
|
||||||
|
ahaHelpStatusSaved: 'Đã lưu · áp dụng khi bắt đầu trận chiến tiếp theo', ahaHelpStatusUnsaved: 'Bạn có thay đổi chưa lưu',
|
||||||
|
ahaHelpStatusSaveError: 'Không thể lưu: {error}', ahaHelpSave: 'Lưu thiết lập', ahaHelpSavedToast: 'Đã lưu Sự trợ giúp của Aha. Thiết lập có hiệu lực từ trận chiến tiếp theo.'
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
ahaHelpMode: 'MODE ANOMALY ARBITRATION', ahaHelpTitle: 'Bantuan Aha',
|
||||||
|
ahaHelpDescription: 'Pilih level bantuan untuk pertempuran berikutnya yang memiliki Voracity Corruption. Pengaturan ini terpisah dari Aha Global.',
|
||||||
|
ahaHelpLegacyTitle: 'Gunakan pengaturan FreeSR', ahaHelpLegacyDescription: 'Biarkan FreeSR mengatur opsi ini (saat ini: {status})',
|
||||||
|
ahaHelpOffTitle: 'Mati', ahaHelpOffDescription: 'Jangan tambahkan level Bantuan Aha', ahaHelpProgress: 'Ambang progres {percent}%',
|
||||||
|
ahaHelpConnect: 'Hubungkan ke server GaC untuk memilih level', ahaHelpLoading: 'Memuat level…',
|
||||||
|
ahaHelpNoLevels: 'Data level Bantuan Aha tidak ditemukan', ahaHelpLoadError: 'Gagal memuat level: {error}',
|
||||||
|
ahaHelpStatusSaved: 'Tersimpan · berlaku saat pertempuran berikutnya dimulai', ahaHelpStatusUnsaved: 'Ada perubahan yang belum disimpan',
|
||||||
|
ahaHelpStatusSaveError: 'Gagal menyimpan: {error}', ahaHelpSave: 'Simpan pengaturan', ahaHelpSavedToast: 'Bantuan Aha tersimpan. Pengaturan berlaku pada pertempuran berikutnya.'
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
ahaHelpMode: 'MODO ANOMALY ARBITRATION', ahaHelpTitle: 'Ayuda de Aha',
|
||||||
|
ahaHelpDescription: 'Elige el nivel de apoyo para el próximo combate con Voracity Corruption. Este ajuste es independiente de Aha Global.',
|
||||||
|
ahaHelpLegacyTitle: 'Usar ajuste de FreeSR', ahaHelpLegacyDescription: 'Dejar que FreeSR controle este ajuste (ahora: {status})',
|
||||||
|
ahaHelpOffTitle: 'Desactivado', ahaHelpOffDescription: 'No añadir un nivel de Ayuda de Aha', ahaHelpProgress: 'Umbral de progreso: {percent}%',
|
||||||
|
ahaHelpConnect: 'Conéctate a un servidor GaC para elegir un nivel', ahaHelpLoading: 'Cargando niveles…',
|
||||||
|
ahaHelpNoLevels: 'No se encontraron niveles de Ayuda de Aha', ahaHelpLoadError: 'No se pudieron cargar los niveles: {error}',
|
||||||
|
ahaHelpStatusSaved: 'Guardado · se aplica al comenzar el próximo combate', ahaHelpStatusUnsaved: 'Hay cambios sin guardar',
|
||||||
|
ahaHelpStatusSaveError: 'No se pudo guardar: {error}', ahaHelpSave: 'Guardar ajustes', ahaHelpSavedToast: 'Ayuda de Aha guardada. Se aplicará al comenzar el próximo combate.'
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
ahaHelpMode: 'MODE ANOMALY ARBITRATION', ahaHelpTitle: 'Aide d’Aha',
|
||||||
|
ahaHelpDescription: 'Choisissez le niveau de soutien pour le prochain combat avec Voracity Corruption. Ce réglage est indépendant d’Aha Global.',
|
||||||
|
ahaHelpLegacyTitle: 'Utiliser le réglage FreeSR', ahaHelpLegacyDescription: 'Laisser FreeSR gérer ce réglage (actuellement : {status})',
|
||||||
|
ahaHelpOffTitle: 'Désactivé', ahaHelpOffDescription: 'Ne pas ajouter de niveau d’aide d’Aha', ahaHelpProgress: 'Seuil de progression : {percent} %',
|
||||||
|
ahaHelpConnect: 'Connectez-vous à un serveur GaC pour choisir un niveau', ahaHelpLoading: 'Chargement des niveaux…',
|
||||||
|
ahaHelpNoLevels: 'Aucun niveau d’aide d’Aha trouvé', ahaHelpLoadError: 'Impossible de charger les niveaux : {error}',
|
||||||
|
ahaHelpStatusSaved: 'Enregistré · appliqué au prochain combat', ahaHelpStatusUnsaved: 'Modifications non enregistrées',
|
||||||
|
ahaHelpStatusSaveError: 'Échec de l’enregistrement : {error}', ahaHelpSave: 'Enregistrer', ahaHelpSavedToast: 'Aide d’Aha enregistrée. Elle sera appliquée au prochain combat.'
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
ahaHelpMode: 'ANOMALIE-ARBITRATION', ahaHelpTitle: 'Ahas Hilfe',
|
||||||
|
ahaHelpDescription: 'Wähle die Unterstützungsstufe für den nächsten Kampf mit Voracity Corruption. Diese Einstellung ist unabhängig von Aha Global.',
|
||||||
|
ahaHelpLegacyTitle: 'FreeSR-Einstellung verwenden', ahaHelpLegacyDescription: 'FreeSR steuert diese Einstellung (aktuell: {status})',
|
||||||
|
ahaHelpOffTitle: 'Aus', ahaHelpOffDescription: 'Keine Stufe von Ahas Hilfe hinzufügen', ahaHelpProgress: 'Fortschrittsschwelle {percent}%',
|
||||||
|
ahaHelpConnect: 'Verbinde dich mit einem GaC-Server, um eine Stufe auszuwählen', ahaHelpLoading: 'Stufen werden geladen…',
|
||||||
|
ahaHelpNoLevels: 'Keine Stufen für Ahas Hilfe gefunden', ahaHelpLoadError: 'Stufen konnten nicht geladen werden: {error}',
|
||||||
|
ahaHelpStatusSaved: 'Gespeichert · gilt ab dem nächsten Kampf', ahaHelpStatusUnsaved: 'Ungespeicherte Änderungen vorhanden',
|
||||||
|
ahaHelpStatusSaveError: 'Speichern fehlgeschlagen: {error}', ahaHelpSave: 'Einstellungen speichern', ahaHelpSavedToast: 'Ahas Hilfe gespeichert. Die Einstellung gilt ab dem nächsten Kampf.'
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
ahaHelpMode: 'РЕЖИМ АНОМАЛЬНОГО ПРОТИВОСТОЯНИЯ', ahaHelpTitle: 'Помощь Ахи',
|
||||||
|
ahaHelpDescription: 'Выберите уровень поддержки для следующего боя с «Порчей алчности». Эта настройка не зависит от глобальной функции Ахи.',
|
||||||
|
ahaHelpLegacyTitle: 'Использовать настройку FreeSR', ahaHelpLegacyDescription: 'Передать управление этой настройкой FreeSR (сейчас: {status})',
|
||||||
|
ahaHelpOffTitle: 'Выкл.', ahaHelpOffDescription: 'Не добавлять уровень помощи Ахи', ahaHelpProgress: 'Порог прогресса: {percent}%',
|
||||||
|
ahaHelpConnect: 'Подключитесь к серверу GaC, чтобы выбрать уровень', ahaHelpLoading: 'Загрузка уровней…',
|
||||||
|
ahaHelpNoLevels: 'Уровни помощи Ахи не найдены', ahaHelpLoadError: 'Не удалось загрузить уровни: {error}',
|
||||||
|
ahaHelpStatusSaved: 'Сохранено · вступит в силу в следующем бою', ahaHelpStatusUnsaved: 'Есть несохранённые изменения',
|
||||||
|
ahaHelpStatusSaveError: 'Не удалось сохранить: {error}', ahaHelpSave: 'Сохранить настройки', ahaHelpSavedToast: 'Помощь Ахи сохранена и вступит в силу в следующем бою.'
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
ahaHelpMode: 'MODO ANOMALY ARBITRATION', ahaHelpTitle: 'Ajuda de Aha',
|
||||||
|
ahaHelpDescription: 'Escolha o nível de apoio para a próxima batalha com Voracity Corruption. Esta opção é independente de Aha Global.',
|
||||||
|
ahaHelpLegacyTitle: 'Usar configuração do FreeSR', ahaHelpLegacyDescription: 'Deixar o FreeSR controlar esta opção (atual: {status})',
|
||||||
|
ahaHelpOffTitle: 'Desativado', ahaHelpOffDescription: 'Não adicionar um nível de Ajuda de Aha', ahaHelpProgress: 'Limite de progresso: {percent}%',
|
||||||
|
ahaHelpConnect: 'Conecte-se a um servidor GaC para escolher um nível', ahaHelpLoading: 'Carregando níveis…',
|
||||||
|
ahaHelpNoLevels: 'Nenhum nível de Ajuda de Aha encontrado', ahaHelpLoadError: 'Não foi possível carregar os níveis: {error}',
|
||||||
|
ahaHelpStatusSaved: 'Salvo · aplicado ao iniciar a próxima batalha', ahaHelpStatusUnsaved: 'Há alterações não salvas',
|
||||||
|
ahaHelpStatusSaveError: 'Falha ao salvar: {error}', ahaHelpSave: 'Salvar configuração', ahaHelpSavedToast: 'Ajuda de Aha salva. Será aplicada na próxima batalha.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const MONSTER_SETTINGS_TRANSLATIONS = {
|
||||||
|
en: { tabMonsterSettings:'👾 Monster Setting', monsterSettingsTitle:'👾 Monster Setting', monsterSettingsDescription:'Inspect stage and wave data from the installed Client resources. Custom enemy waves are limited to Golden / Crimson Calyx.', monsterSettingsRule:'Weekly Boss, Stagnant Shadow, Cavern of Corrosion, and Endgame stages use Client data and cannot be changed here.', monsterModeMoc:'Memory of Chaos', monsterModePf:'Pure Fiction', monsterModeAs:'Apocalyptic Shadow', monsterModeAa:'Anomaly Arbitration', monsterModeCustom:'Custom Enemy', monsterModeSu:'Simulated Universe', monsterSettingsLoading:'Reading stages from Client resources…', monsterSettingsNoStages:'No stages were found in the connected Client resources.', monsterSettingsGroup:'Stage group', monsterSettingsGroupOption:'Group {id} · {count} entries', monsterSettingsChallenge:'Challenge entry', monsterSettingsStageOption:'Entry {id}{boss}', monsterSettingsBoss:'Boss', monsterSettingsStageTitle:'Entry {id}', monsterSettingsDefaultBuff:'Default buff ID {id}', monsterSettingsEncounterTitle:'Encounter stage {id}', monsterSettingsLevel:'Level {level}', monsterSettingsWaveCount:'{count} waves', monsterSettingsWave:'Wave {number}', monsterSettingsSuReadOnly:'Simulated Universe monster editing is not supported by this server. No verified SU monster resources are available here.', monsterSettingsCatalogError:'Could not read Client stage resources: {error}', monsterSettingsReadOnly:'Read-only: connect to the GaC server to load its Client resource catalog.', monsterSettingsLoaded:'Read-only stage data · source: {source}', monsterSettingsClientResources:'Client resources', monsterSettingsDataUnavailable:'FreeSR player data is unavailable', monsterCustomTitle:'Custom Enemy Waves', monsterCustomDescription:'These waves are used only when entering Golden or Crimson Calyx with the GaC-Srtool session active.', monsterCustomCalyxOnly:'This setting never replaces Weekly Boss, Stagnant Shadow, Cavern of Corrosion, MoC, Pure Fiction, Apocalyptic Shadow, or AA enemies.', monsterCustomConnected:'Connected. Changes are saved to the server and apply on a later eligible Calyx entry.', monsterCustomConnect:'Connect to GaC to edit and save custom Calyx waves.', monsterCustomEmpty:'No custom waves are set. “Use Client stages” restores the installed Calyx waves.', monsterCustomAddWave:'Add wave', monsterCustomUseClient:'Use Client stages', monsterCustomSave:'Save custom waves', monsterCustomSaved:'Saved · applies to Golden / Crimson Calyx only', monsterCustomUnsaved:'Unsaved changes', monsterCustomAddEnemy:'Add enemy', monsterCustomRemoveEnemy:'Remove enemy', monsterCustomRemoveWave:'Remove wave', monsterCustomId:'Monster ID', monsterCustomLevel:'Level', monsterCustomMaxHP:'Max HP (optional)', monsterCustomNaturalHP:'Blank = use the monster’s normal HP', monsterCustomEmptyWave:'Wave {number} has no enemies.', monsterCustomInvalidEntry:'Enter a valid Monster ID, Level, and Max HP.', monsterCustomSaveSuccess:'Custom Calyx waves saved.', monsterCustomSaveError:'Could not save custom waves: {error}' },
|
||||||
|
th: { tabMonsterSettings:'👾 ตั้งค่ามอนสเตอร์', monsterSettingsTitle:'👾 ตั้งค่ามอนสเตอร์', monsterSettingsDescription:'ตรวจดู stage และ wave จาก resource ของ Client ที่ติดตั้งอยู่ Custom Enemy ใช้ได้เฉพาะ Golden / Crimson Calyx', monsterSettingsRule:'Weekly Boss, Stagnant Shadow, Cavern of Corrosion และ Endgame ใช้ข้อมูลด่านจาก Client แก้คลื่นมอนที่นี่ไม่ได้', monsterModeMoc:'ความทรงจำแห่งความโกลาหล', monsterModePf:'โลกมายาคติ', monsterModeAs:'เงาจุติ', monsterModeAa:'Anomaly Arbitration', monsterModeCustom:'Custom Enemy', monsterModeSu:'จักรวาลจำลอง', monsterSettingsLoading:'กำลังอ่านด่านจาก resource ของ Client…', monsterSettingsNoStages:'ไม่พบ stage ใน resource ของ Client ที่เชื่อมต่อ', monsterSettingsGroup:'กลุ่มด่าน', monsterSettingsGroupOption:'กลุ่ม {id} · {count} รายการ', monsterSettingsChallenge:'รายการท้าทาย', monsterSettingsStageOption:'รายการ {id}{boss}', monsterSettingsBoss:'บอส', monsterSettingsStageTitle:'รายการ {id}', monsterSettingsDefaultBuff:'Buff เริ่มต้น ID {id}', monsterSettingsEncounterTitle:'Stage ต่อสู้ {id}', monsterSettingsLevel:'เลเวล {level}', monsterSettingsWaveCount:'{count} wave', monsterSettingsWave:'Wave {number}', monsterSettingsSuReadOnly:'เซิร์ฟเวอร์นี้ยังไม่รองรับการแก้ monster ใน Simulated Universe และไม่มี resource ที่ยืนยันแล้ว', monsterSettingsCatalogError:'อ่าน resource stage ของ Client ไม่ได้: {error}', monsterSettingsReadOnly:'อ่านได้อย่างเดียว: เชื่อมต่อ GaC เพื่อโหลด catalog จาก resource Client', monsterSettingsLoaded:'ข้อมูลด่านอ่านอย่างเดียว · แหล่งที่มา: {source}', monsterSettingsClientResources:'resource ของ Client', monsterSettingsDataUnavailable:'ไม่มีข้อมูลผู้เล่น FreeSR', monsterCustomTitle:'คลื่นมอน Custom Enemy', monsterCustomDescription:'wave นี้ใช้เฉพาะเมื่อเข้าต่อสู้ Golden หรือ Crimson Calyx ขณะ Srtool เชื่อมต่อกับ GaC', monsterCustomCalyxOnly:'ค่านี้ไม่แทนมอนของ Weekly Boss, Stagnant Shadow, Cavern of Corrosion, MoC, Pure Fiction, Apocalyptic Shadow หรือ AA', monsterCustomConnected:'เชื่อมต่อแล้ว บันทึกไปเซิร์ฟเวอร์และใช้เมื่อเข้า Calyx ที่รองรับครั้งถัดไป', monsterCustomConnect:'เชื่อมต่อ GaC เพื่อแก้และบันทึก wave ของ Calyx', monsterCustomEmpty:'ยังไม่มี custom wave กด “ใช้ stage จาก Client” เพื่อกลับไปใช้ wave ของ Calyx ที่ติดตั้ง', monsterCustomAddWave:'เพิ่ม wave', monsterCustomUseClient:'ใช้ stage จาก Client', monsterCustomSave:'บันทึก custom wave', monsterCustomSaved:'บันทึกแล้ว · ใช้เฉพาะ Golden / Crimson Calyx', monsterCustomUnsaved:'มีการแก้ไขที่ยังไม่บันทึก', monsterCustomAddEnemy:'เพิ่มมอน', monsterCustomRemoveEnemy:'ลบมอน', monsterCustomRemoveWave:'ลบ wave', monsterCustomId:'Monster ID', monsterCustomLevel:'Level', monsterCustomMaxHP:'Max HP (ไม่บังคับ)', monsterCustomNaturalHP:'เว้นว่างเพื่อใช้ HP ปกติของมอน', monsterCustomEmptyWave:'Wave {number} ยังไม่มีมอน', monsterCustomInvalidEntry:'กรอก Monster ID, Level และ Max HP ให้ถูกต้อง', monsterCustomSaveSuccess:'บันทึก wave ของ Calyx แล้ว', monsterCustomSaveError:'บันทึก wave ไม่สำเร็จ: {error}' },
|
||||||
|
'zh-CN': { tabMonsterSettings:'👾 怪物设置', monsterSettingsTitle:'👾 怪物设置', monsterSettingsDescription:'查看当前客户端资源中的关卡与波次。自定义敌人波次仅适用于 Golden / Crimson Calyx。', monsterSettingsRule:'历战余响、凝滞虚影、侵蚀隧洞和末日幻影等终局关卡使用客户端数据,不能在此修改。', monsterModeMoc:'混沌回忆', monsterModePf:'虚构叙事', monsterModeAs:'末日幻影', monsterModeAa:'异相仲裁', monsterModeCustom:'自定义敌人', monsterModeSu:'模拟宇宙', monsterSettingsLoading:'正在读取客户端关卡资源…', monsterSettingsNoStages:'连接的客户端资源中没有找到关卡。', monsterSettingsGroup:'关卡组', monsterSettingsGroupOption:'组 {id} · {count} 项', monsterSettingsChallenge:'挑战条目', monsterSettingsStageOption:'条目 {id}{boss}', monsterSettingsBoss:'首领', monsterSettingsStageTitle:'条目 {id}', monsterSettingsDefaultBuff:'默认增益 ID {id}', monsterSettingsEncounterTitle:'战斗关卡 {id}', monsterSettingsLevel:'等级 {level}', monsterSettingsWaveCount:'{count} 波', monsterSettingsWave:'第 {number} 波', monsterSettingsSuReadOnly:'此服务器暂不支持编辑模拟宇宙敌人,也没有已验证的模拟宇宙敌人资源。', monsterSettingsCatalogError:'无法读取客户端关卡资源:{error}', monsterSettingsReadOnly:'只读:连接 GaC 服务器以读取客户端资源目录。', monsterSettingsLoaded:'只读关卡数据 · 来源:{source}', monsterSettingsClientResources:'客户端资源', monsterSettingsDataUnavailable:'无法读取 FreeSR 玩家数据', monsterCustomTitle:'自定义敌人波次', monsterCustomDescription:'仅在 GaC-Srtool 会话有效时进入 Golden 或 Crimson Calyx 才会使用这些波次。', monsterCustomCalyxOnly:'此设置不会替换历战余响、凝滞虚影、侵蚀隧洞、混沌回忆、虚构叙事、末日幻影或异相仲裁的敌人。', monsterCustomConnected:'已连接。更改会保存到服务器,并在下次进入符合条件的 Calyx 时生效。', monsterCustomConnect:'连接 GaC 后才能编辑和保存 Calyx 自定义波次。', monsterCustomEmpty:'尚未设置自定义波次。选择“使用客户端关卡”可恢复客户端 Calyx 波次。', monsterCustomAddWave:'添加波次', monsterCustomUseClient:'使用客户端关卡', monsterCustomSave:'保存自定义波次', monsterCustomSaved:'已保存 · 仅用于 Golden / Crimson Calyx', monsterCustomUnsaved:'有未保存的更改', monsterCustomAddEnemy:'添加敌人', monsterCustomRemoveEnemy:'删除敌人', monsterCustomRemoveWave:'删除波次', monsterCustomId:'怪物 ID', monsterCustomLevel:'等级', monsterCustomMaxHP:'最大生命值(可选)', monsterCustomNaturalHP:'留空 = 使用怪物默认生命值', monsterCustomEmptyWave:'第 {number} 波没有敌人。', monsterCustomInvalidEntry:'请输入有效的怪物 ID、等级和最大生命值。', monsterCustomSaveSuccess:'Calyx 自定义波次已保存。', monsterCustomSaveError:'无法保存自定义波次:{error}' },
|
||||||
|
'zh-TW': { tabMonsterSettings:'👾 怪物設定', monsterSettingsTitle:'👾 怪物設定', monsterSettingsDescription:'檢視目前用戶端資源中的關卡與波次。自訂敵人波次僅適用於 Golden / Crimson Calyx。', monsterSettingsRule:'歷戰餘響、凝滯虛影、侵蝕隧洞與終局關卡使用用戶端資料,無法在此修改。', monsterModeMoc:'混沌回憶', monsterModePf:'虛構敘事', monsterModeAs:'末日幻影', monsterModeAa:'異相仲裁', monsterModeCustom:'自訂敵人', monsterModeSu:'模擬宇宙', monsterSettingsLoading:'正在讀取用戶端關卡資源…', monsterSettingsNoStages:'連線的用戶端資源中找不到關卡。', monsterSettingsGroup:'關卡組', monsterSettingsGroupOption:'組 {id} · {count} 項', monsterSettingsChallenge:'挑戰項目', monsterSettingsStageOption:'項目 {id}{boss}', monsterSettingsBoss:'首領', monsterSettingsStageTitle:'項目 {id}', monsterSettingsDefaultBuff:'預設增益 ID {id}', monsterSettingsEncounterTitle:'戰鬥關卡 {id}', monsterSettingsLevel:'等級 {level}', monsterSettingsWaveCount:'{count} 波', monsterSettingsWave:'第 {number} 波', monsterSettingsSuReadOnly:'此伺服器尚未支援編輯模擬宇宙敵人,也沒有已驗證的模擬宇宙敵人資源。', monsterSettingsCatalogError:'無法讀取用戶端關卡資源:{error}', monsterSettingsReadOnly:'唯讀:連線 GaC 伺服器以讀取用戶端資源目錄。', monsterSettingsLoaded:'唯讀關卡資料 · 來源:{source}', monsterSettingsClientResources:'用戶端資源', monsterSettingsDataUnavailable:'無法讀取 FreeSR 玩家資料', monsterCustomTitle:'自訂敵人波次', monsterCustomDescription:'僅在 GaC-Srtool 工作階段有效時進入 Golden 或 Crimson Calyx 才會使用這些波次。', monsterCustomCalyxOnly:'此設定不會替換歷戰餘響、凝滯虛影、侵蝕隧洞、混沌回憶、虛構敘事、末日幻影或異相仲裁的敵人。', monsterCustomConnected:'已連線。變更會儲存至伺服器,並於下次進入符合條件的 Calyx 時生效。', monsterCustomConnect:'連線 GaC 後才能編輯及儲存 Calyx 自訂波次。', monsterCustomEmpty:'尚未設定自訂波次。選擇「使用用戶端關卡」可還原用戶端 Calyx 波次。', monsterCustomAddWave:'新增波次', monsterCustomUseClient:'使用用戶端關卡', monsterCustomSave:'儲存自訂波次', monsterCustomSaved:'已儲存 · 僅用於 Golden / Crimson Calyx', monsterCustomUnsaved:'有尚未儲存的變更', monsterCustomAddEnemy:'新增敵人', monsterCustomRemoveEnemy:'移除敵人', monsterCustomRemoveWave:'移除波次', monsterCustomId:'怪物 ID', monsterCustomLevel:'等級', monsterCustomMaxHP:'最大生命值(選填)', monsterCustomNaturalHP:'留白 = 使用怪物預設生命值', monsterCustomEmptyWave:'第 {number} 波沒有敵人。', monsterCustomInvalidEntry:'請輸入有效的怪物 ID、等級和最大生命值。', monsterCustomSaveSuccess:'Calyx 自訂波次已儲存。', monsterCustomSaveError:'無法儲存自訂波次:{error}' },
|
||||||
|
ja: { tabMonsterSettings:'👾 モンスター設定', monsterSettingsTitle:'👾 モンスター設定', monsterSettingsDescription:'現在のクライアントリソースにあるステージとウェーブを確認します。カスタム敵ウェーブはGolden / Crimson Calyxのみで使用できます。', monsterSettingsRule:'週ボス、凝結虚影、侵食トンネル、エンドゲームはクライアントのステージデータを使い、ここから変更できません。', monsterModeMoc:'忘却の庭', monsterModePf:'虚構叙事', monsterModeAs:'末日の幻影', monsterModeAa:'異相仲裁', monsterModeCustom:'カスタム敵', monsterModeSu:'模擬宇宙', monsterSettingsLoading:'クライアントのステージリソースを読み込み中…', monsterSettingsNoStages:'接続中のクライアントリソースにステージがありません。', monsterSettingsGroup:'ステージグループ', monsterSettingsGroupOption:'グループ {id} · {count} 件', monsterSettingsChallenge:'挑戦項目', monsterSettingsStageOption:'項目 {id}{boss}', monsterSettingsBoss:'ボス', monsterSettingsStageTitle:'項目 {id}', monsterSettingsDefaultBuff:'初期バフ ID {id}', monsterSettingsEncounterTitle:'戦闘ステージ {id}', monsterSettingsLevel:'レベル {level}', monsterSettingsWaveCount:'{count} ウェーブ', monsterSettingsWave:'ウェーブ {number}', monsterSettingsSuReadOnly:'このサーバーは模擬宇宙の敵編集に対応していません。確認済みの模擬宇宙リソースもありません。', monsterSettingsCatalogError:'クライアントのステージリソースを読み込めません: {error}', monsterSettingsReadOnly:'読み取り専用です。GaCサーバーに接続するとクライアントリソースを読み込めます。', monsterSettingsLoaded:'ステージデータ(読み取り専用)· ソース: {source}', monsterSettingsClientResources:'クライアントリソース', monsterSettingsDataUnavailable:'FreeSRのプレイヤーデータを読み込めません', monsterCustomTitle:'カスタム敵ウェーブ', monsterCustomDescription:'GaC-Srtoolのセッションが有効な状態でGoldenまたはCrimson Calyxに入る場合のみ適用されます。', monsterCustomCalyxOnly:'週ボス、凝結虚影、侵食トンネル、忘却の庭、虚構叙事、末日の幻影、異相仲裁の敵には適用されません。', monsterCustomConnected:'接続済みです。変更はサーバーに保存され、次回対象のCalyxに入ると適用されます。', monsterCustomConnect:'GaCに接続するとCalyxのカスタムウェーブを編集・保存できます。', monsterCustomEmpty:'カスタムウェーブは未設定です。「クライアントのステージを使用」でCalyxの既定ウェーブに戻します。', monsterCustomAddWave:'ウェーブを追加', monsterCustomUseClient:'クライアントのステージを使用', monsterCustomSave:'カスタムウェーブを保存', monsterCustomSaved:'保存済み · Golden / Crimson Calyxのみ', monsterCustomUnsaved:'未保存の変更があります', monsterCustomAddEnemy:'敵を追加', monsterCustomRemoveEnemy:'敵を削除', monsterCustomRemoveWave:'ウェーブを削除', monsterCustomId:'モンスターID', monsterCustomLevel:'レベル', monsterCustomMaxHP:'最大HP(任意)', monsterCustomNaturalHP:'空欄 = 通常HPを使用', monsterCustomEmptyWave:'ウェーブ {number} に敵がいません。', monsterCustomInvalidEntry:'有効なモンスターID、レベル、最大HPを入力してください。', monsterCustomSaveSuccess:'Calyxのカスタムウェーブを保存しました。', monsterCustomSaveError:'保存できませんでした: {error}' },
|
||||||
|
ko: { tabMonsterSettings:'👾 몬스터 설정', monsterSettingsTitle:'👾 몬스터 설정', monsterSettingsDescription:'현재 클라이언트 리소스의 스테이지와 웨이브를 확인합니다. 커스텀 적 웨이브는 Golden / Crimson Calyx에서만 사용됩니다.', monsterSettingsRule:'주간 보스, 정체된 허영, 침식된 터널, 엔드게임은 클라이언트 스테이지 데이터를 사용하며 여기서 변경할 수 없습니다.', monsterModeMoc:'혼돈의 기억', monsterModePf:'허구 이야기', monsterModeAs:'종말의 그림자', monsterModeAa:'이상 중재', monsterModeCustom:'커스텀 적', monsterModeSu:'시뮬레이션 우주', monsterSettingsLoading:'클라이언트 스테이지 리소스를 읽는 중…', monsterSettingsNoStages:'연결된 클라이언트 리소스에서 스테이지를 찾지 못했습니다.', monsterSettingsGroup:'스테이지 그룹', monsterSettingsGroupOption:'그룹 {id} · {count}개 항목', monsterSettingsChallenge:'도전 항목', monsterSettingsStageOption:'항목 {id}{boss}', monsterSettingsBoss:'보스', monsterSettingsStageTitle:'항목 {id}', monsterSettingsDefaultBuff:'기본 버프 ID {id}', monsterSettingsEncounterTitle:'전투 스테이지 {id}', monsterSettingsLevel:'레벨 {level}', monsterSettingsWaveCount:'웨이브 {count}개', monsterSettingsWave:'웨이브 {number}', monsterSettingsSuReadOnly:'이 서버는 시뮬레이션 우주 적 편집을 지원하지 않으며 확인된 SU 리소스도 없습니다.', monsterSettingsCatalogError:'클라이언트 스테이지 리소스를 읽을 수 없습니다: {error}', monsterSettingsReadOnly:'읽기 전용입니다. GaC 서버에 연결하면 클라이언트 리소스 카탈로그를 읽습니다.', monsterSettingsLoaded:'읽기 전용 스테이지 데이터 · 출처: {source}', monsterSettingsClientResources:'클라이언트 리소스', monsterSettingsDataUnavailable:'FreeSR 플레이어 데이터를 읽을 수 없습니다', monsterCustomTitle:'커스텀 적 웨이브', monsterCustomDescription:'GaC-Srtool 세션이 활성화된 상태에서 Golden 또는 Crimson Calyx에 입장할 때만 적용됩니다.', monsterCustomCalyxOnly:'주간 보스, 정체된 허영, 침식된 터널, 혼돈의 기억, 허구 이야기, 종말의 그림자, 이상 중재에는 적용되지 않습니다.', monsterCustomConnected:'연결됨. 변경 사항은 서버에 저장되며 다음 대상 Calyx 입장 때 적용됩니다.', monsterCustomConnect:'GaC에 연결하면 Calyx 커스텀 웨이브를 편집하고 저장할 수 있습니다.', monsterCustomEmpty:'커스텀 웨이브가 없습니다. “클라이언트 스테이지 사용”을 선택하면 기본 Calyx 웨이브로 복원됩니다.', monsterCustomAddWave:'웨이브 추가', monsterCustomUseClient:'클라이언트 스테이지 사용', monsterCustomSave:'커스텀 웨이브 저장', monsterCustomSaved:'저장됨 · Golden / Crimson Calyx 전용', monsterCustomUnsaved:'저장하지 않은 변경 사항', monsterCustomAddEnemy:'적 추가', monsterCustomRemoveEnemy:'적 삭제', monsterCustomRemoveWave:'웨이브 삭제', monsterCustomId:'몬스터 ID', monsterCustomLevel:'레벨', monsterCustomMaxHP:'최대 HP (선택)', monsterCustomNaturalHP:'비우면 기본 HP 사용', monsterCustomEmptyWave:'웨이브 {number}에 적이 없습니다.', monsterCustomInvalidEntry:'올바른 몬스터 ID, 레벨, 최대 HP를 입력하세요.', monsterCustomSaveSuccess:'Calyx 커스텀 웨이브를 저장했습니다.', monsterCustomSaveError:'커스텀 웨이브 저장 실패: {error}' },
|
||||||
|
vi: { tabMonsterSettings:'👾 Cài đặt quái', monsterSettingsTitle:'👾 Cài đặt quái', monsterSettingsDescription:'Xem stage và wave từ tài nguyên Client hiện tại. Wave quái tùy chỉnh chỉ dùng tại Golden / Crimson Calyx.', monsterSettingsRule:'Boss tuần, Bóng Ngưng Đọng, Hang Ăn Mòn và Endgame dùng dữ liệu stage của Client, không thể sửa tại đây.', monsterModeMoc:'Ký Ức Hỗn Độn', monsterModePf:'Hư Cấu Ảo Ảnh', monsterModeAs:'Ảnh Ảo Tận Thế', monsterModeAa:'Anomaly Arbitration', monsterModeCustom:'Quái tùy chỉnh', monsterModeSu:'Vũ Trụ Mô Phỏng', monsterSettingsLoading:'Đang đọc tài nguyên stage của Client…', monsterSettingsNoStages:'Không tìm thấy stage trong tài nguyên Client đã kết nối.', monsterSettingsGroup:'Nhóm stage', monsterSettingsGroupOption:'Nhóm {id} · {count} mục', monsterSettingsChallenge:'Mục thử thách', monsterSettingsStageOption:'Mục {id}{boss}', monsterSettingsBoss:'Boss', monsterSettingsStageTitle:'Mục {id}', monsterSettingsDefaultBuff:'Buff mặc định ID {id}', monsterSettingsEncounterTitle:'Stage chiến đấu {id}', monsterSettingsLevel:'Cấp {level}', monsterSettingsWaveCount:'{count} wave', monsterSettingsWave:'Wave {number}', monsterSettingsSuReadOnly:'Máy chủ này chưa hỗ trợ sửa quái trong Vũ Trụ Mô Phỏng và chưa có tài nguyên SU được xác minh.', monsterSettingsCatalogError:'Không đọc được tài nguyên stage của Client: {error}', monsterSettingsReadOnly:'Chỉ đọc: kết nối máy chủ GaC để tải danh mục tài nguyên Client.', monsterSettingsLoaded:'Dữ liệu stage chỉ đọc · nguồn: {source}', monsterSettingsClientResources:'tài nguyên Client', monsterSettingsDataUnavailable:'Không có dữ liệu người chơi FreeSR', monsterCustomTitle:'Wave quái tùy chỉnh', monsterCustomDescription:'Chỉ dùng các wave này khi vào Golden hoặc Crimson Calyx với phiên GaC-Srtool đang hoạt động.', monsterCustomCalyxOnly:'Thiết lập này không thay quái của Boss tuần, Bóng Ngưng Đọng, Hang Ăn Mòn, MoC, Pure Fiction, Apocalyptic Shadow hoặc AA.', monsterCustomConnected:'Đã kết nối. Thay đổi được lưu lên máy chủ và áp dụng ở lần vào Calyx hợp lệ tiếp theo.', monsterCustomConnect:'Kết nối GaC để sửa và lưu wave Calyx tùy chỉnh.', monsterCustomEmpty:'Chưa có wave tùy chỉnh. Chọn “Dùng stage Client” để khôi phục wave Calyx đã cài.', monsterCustomAddWave:'Thêm wave', monsterCustomUseClient:'Dùng stage Client', monsterCustomSave:'Lưu wave tùy chỉnh', monsterCustomSaved:'Đã lưu · chỉ áp dụng Golden / Crimson Calyx', monsterCustomUnsaved:'Có thay đổi chưa lưu', monsterCustomAddEnemy:'Thêm quái', monsterCustomRemoveEnemy:'Xóa quái', monsterCustomRemoveWave:'Xóa wave', monsterCustomId:'Monster ID', monsterCustomLevel:'Cấp', monsterCustomMaxHP:'HP tối đa (không bắt buộc)', monsterCustomNaturalHP:'Để trống = HP mặc định', monsterCustomEmptyWave:'Wave {number} chưa có quái.', monsterCustomInvalidEntry:'Nhập Monster ID, cấp và HP tối đa hợp lệ.', monsterCustomSaveSuccess:'Đã lưu wave tùy chỉnh Calyx.', monsterCustomSaveError:'Không lưu được wave tùy chỉnh: {error}' },
|
||||||
|
id: { tabMonsterSettings:'👾 Pengaturan Monster', monsterSettingsTitle:'👾 Pengaturan Monster', monsterSettingsDescription:'Lihat stage dan wave dari resource Client yang terpasang. Wave musuh kustom hanya berlaku di Golden / Crimson Calyx.', monsterSettingsRule:'Boss mingguan, Stagnant Shadow, Cavern of Corrosion, dan Endgame memakai data stage Client dan tidak dapat diubah di sini.', monsterModeMoc:'Memory of Chaos', monsterModePf:'Pure Fiction', monsterModeAs:'Apocalyptic Shadow', monsterModeAa:'Anomaly Arbitration', monsterModeCustom:'Musuh Kustom', monsterModeSu:'Simulated Universe', monsterSettingsLoading:'Membaca resource stage Client…', monsterSettingsNoStages:'Tidak ada stage dalam resource Client yang terhubung.', monsterSettingsGroup:'Grup stage', monsterSettingsGroupOption:'Grup {id} · {count} entri', monsterSettingsChallenge:'Entri tantangan', monsterSettingsStageOption:'Entri {id}{boss}', monsterSettingsBoss:'Boss', monsterSettingsStageTitle:'Entri {id}', monsterSettingsDefaultBuff:'Buff default ID {id}', monsterSettingsEncounterTitle:'Stage pertarungan {id}', monsterSettingsLevel:'Level {level}', monsterSettingsWaveCount:'{count} wave', monsterSettingsWave:'Wave {number}', monsterSettingsSuReadOnly:'Server ini belum mendukung pengeditan musuh Simulated Universe dan belum memiliki resource SU terverifikasi.', monsterSettingsCatalogError:'Resource stage Client gagal dibaca: {error}', monsterSettingsReadOnly:'Hanya-baca: sambungkan ke server GaC untuk memuat katalog resource Client.', monsterSettingsLoaded:'Data stage hanya-baca · sumber: {source}', monsterSettingsClientResources:'resource Client', monsterSettingsDataUnavailable:'Data pemain FreeSR tidak tersedia', monsterCustomTitle:'Wave Musuh Kustom', monsterCustomDescription:'Wave ini hanya digunakan saat masuk Golden atau Crimson Calyx dengan sesi GaC-Srtool aktif.', monsterCustomCalyxOnly:'Pengaturan ini tidak mengganti musuh Boss mingguan, Stagnant Shadow, Cavern of Corrosion, MoC, Pure Fiction, Apocalyptic Shadow, atau AA.', monsterCustomConnected:'Terhubung. Perubahan disimpan ke server dan berlaku saat masuk Calyx yang sesuai berikutnya.', monsterCustomConnect:'Sambungkan ke GaC untuk mengedit dan menyimpan wave Calyx kustom.', monsterCustomEmpty:'Belum ada wave kustom. Pilih “Gunakan stage Client” untuk memulihkan wave Calyx bawaan.', monsterCustomAddWave:'Tambah wave', monsterCustomUseClient:'Gunakan stage Client', monsterCustomSave:'Simpan wave kustom', monsterCustomSaved:'Tersimpan · hanya untuk Golden / Crimson Calyx', monsterCustomUnsaved:'Ada perubahan yang belum disimpan', monsterCustomAddEnemy:'Tambah musuh', monsterCustomRemoveEnemy:'Hapus musuh', monsterCustomRemoveWave:'Hapus wave', monsterCustomId:'Monster ID', monsterCustomLevel:'Level', monsterCustomMaxHP:'HP Maks (opsional)', monsterCustomNaturalHP:'Kosong = HP normal monster', monsterCustomEmptyWave:'Wave {number} belum memiliki musuh.', monsterCustomInvalidEntry:'Masukkan Monster ID, Level, dan Max HP yang valid.', monsterCustomSaveSuccess:'Wave kustom Calyx tersimpan.', monsterCustomSaveError:'Wave kustom gagal disimpan: {error}' },
|
||||||
|
es: { tabMonsterSettings:'👾 Ajustes de monstruos', monsterSettingsTitle:'👾 Ajustes de monstruos', monsterSettingsDescription:'Consulta fases y oleadas de los recursos del cliente instalado. Las oleadas personalizadas solo se aplican en Golden / Crimson Calyx.', monsterSettingsRule:'Los jefes semanales, Sombra estancada, Caverna de la corrosión y Endgame usan datos del cliente y no se modifican aquí.', monsterModeMoc:'Memoria del caos', monsterModePf:'Ficción pura', monsterModeAs:'Sombra apocalíptica', monsterModeAa:'Anomaly Arbitration', monsterModeCustom:'Enemigo personalizado', monsterModeSu:'Universo Simulado', monsterSettingsLoading:'Leyendo recursos de fases del cliente…', monsterSettingsNoStages:'No se encontraron fases en los recursos del cliente conectado.', monsterSettingsGroup:'Grupo de fases', monsterSettingsGroupOption:'Grupo {id} · {count} entradas', monsterSettingsChallenge:'Entrada del desafío', monsterSettingsStageOption:'Entrada {id}{boss}', monsterSettingsBoss:'Jefe', monsterSettingsStageTitle:'Entrada {id}', monsterSettingsDefaultBuff:'Beneficio predeterminado ID {id}', monsterSettingsEncounterTitle:'Fase de combate {id}', monsterSettingsLevel:'Nivel {level}', monsterSettingsWaveCount:'{count} oleadas', monsterSettingsWave:'Oleada {number}', monsterSettingsSuReadOnly:'Este servidor no permite editar enemigos del Universo Simulado y no dispone de recursos SU verificados.', monsterSettingsCatalogError:'No se pudieron leer los recursos de fases del cliente: {error}', monsterSettingsReadOnly:'Solo lectura: conecta con el servidor GaC para cargar el catálogo del cliente.', monsterSettingsLoaded:'Datos de fase de solo lectura · origen: {source}', monsterSettingsClientResources:'recursos del cliente', monsterSettingsDataUnavailable:'No hay datos de jugador de FreeSR', monsterCustomTitle:'Oleadas de enemigos personalizadas', monsterCustomDescription:'Solo se usan al entrar en Golden o Crimson Calyx con la sesión de GaC-Srtool activa.', monsterCustomCalyxOnly:'No reemplaza enemigos de jefes semanales, Sombra estancada, Caverna de la corrosión, MoC, Pure Fiction, Apocalyptic Shadow ni AA.', monsterCustomConnected:'Conectado. Los cambios se guardan en el servidor y se aplican en la próxima entrada a un Calyx compatible.', monsterCustomConnect:'Conecta con GaC para editar y guardar oleadas personalizadas de Calyx.', monsterCustomEmpty:'No hay oleadas personalizadas. Usa «Usar fases del cliente» para restaurar las oleadas instaladas de Calyx.', monsterCustomAddWave:'Añadir oleada', monsterCustomUseClient:'Usar fases del cliente', monsterCustomSave:'Guardar oleadas', monsterCustomSaved:'Guardado · solo para Golden / Crimson Calyx', monsterCustomUnsaved:'Hay cambios sin guardar', monsterCustomAddEnemy:'Añadir enemigo', monsterCustomRemoveEnemy:'Quitar enemigo', monsterCustomRemoveWave:'Quitar oleada', monsterCustomId:'Monster ID', monsterCustomLevel:'Nivel', monsterCustomMaxHP:'PV máx. (opcional)', monsterCustomNaturalHP:'Vacío = PV normal del enemigo', monsterCustomEmptyWave:'La oleada {number} no tiene enemigos.', monsterCustomInvalidEntry:'Introduce un Monster ID, nivel y PV máx. válidos.', monsterCustomSaveSuccess:'Oleadas personalizadas de Calyx guardadas.', monsterCustomSaveError:'No se pudieron guardar las oleadas: {error}' },
|
||||||
|
fr: { tabMonsterSettings:'👾 Réglages des monstres', monsterSettingsTitle:'👾 Réglages des monstres', monsterSettingsDescription:'Consultez les stages et vagues des ressources du client installé. Les vagues personnalisées ne s’appliquent qu’aux Golden / Crimson Calyx.', monsterSettingsRule:'Les boss hebdomadaires, Ombre stagnante, Caverne de la corrosion et Endgame utilisent les données du client et ne sont pas modifiables ici.', monsterModeMoc:'Mémoire du chaos', monsterModePf:'Pure Fiction', monsterModeAs:'Apocalyptic Shadow', monsterModeAa:'Anomaly Arbitration', monsterModeCustom:'Ennemi personnalisé', monsterModeSu:'Univers simulé', monsterSettingsLoading:'Lecture des ressources de stage du client…', monsterSettingsNoStages:'Aucun stage trouvé dans les ressources du client connecté.', monsterSettingsGroup:'Groupe de stages', monsterSettingsGroupOption:'Groupe {id} · {count} entrées', monsterSettingsChallenge:'Entrée de défi', monsterSettingsStageOption:'Entrée {id}{boss}', monsterSettingsBoss:'Boss', monsterSettingsStageTitle:'Entrée {id}', monsterSettingsDefaultBuff:'Bonus par défaut ID {id}', monsterSettingsEncounterTitle:'Stage de combat {id}', monsterSettingsLevel:'Niveau {level}', monsterSettingsWaveCount:'{count} vagues', monsterSettingsWave:'Vague {number}', monsterSettingsSuReadOnly:'Ce serveur ne permet pas de modifier les ennemis de l’Univers simulé et ne dispose pas de ressources SU vérifiées.', monsterSettingsCatalogError:'Impossible de lire les ressources de stage du client : {error}', monsterSettingsReadOnly:'Lecture seule : connectez-vous au serveur GaC pour charger le catalogue du client.', monsterSettingsLoaded:'Données de stage en lecture seule · source : {source}', monsterSettingsClientResources:'ressources du client', monsterSettingsDataUnavailable:'Données joueur FreeSR indisponibles', monsterCustomTitle:'Vagues d’ennemis personnalisées', monsterCustomDescription:'Ces vagues sont utilisées uniquement à l’entrée d’un Golden ou Crimson Calyx avec une session GaC-Srtool active.', monsterCustomCalyxOnly:'Ne remplace pas les ennemis des boss hebdomadaires, Ombre stagnante, Caverne de la corrosion, MoC, Pure Fiction, Apocalyptic Shadow ou AA.', monsterCustomConnected:'Connecté. Les modifications sont enregistrées sur le serveur et s’appliqueront à la prochaine entrée dans un Calyx compatible.', monsterCustomConnect:'Connectez-vous à GaC pour modifier et enregistrer les vagues de Calyx.', monsterCustomEmpty:'Aucune vague personnalisée. « Utiliser les stages du client » restaure les vagues Calyx installées.', monsterCustomAddWave:'Ajouter une vague', monsterCustomUseClient:'Utiliser les stages du client', monsterCustomSave:'Enregistrer les vagues', monsterCustomSaved:'Enregistré · Golden / Crimson Calyx uniquement', monsterCustomUnsaved:'Modifications non enregistrées', monsterCustomAddEnemy:'Ajouter un ennemi', monsterCustomRemoveEnemy:'Supprimer l’ennemi', monsterCustomRemoveWave:'Supprimer la vague', monsterCustomId:'Monster ID', monsterCustomLevel:'Niveau', monsterCustomMaxHP:'PV max (facultatif)', monsterCustomNaturalHP:'Vide = PV normaux du monstre', monsterCustomEmptyWave:'La vague {number} ne contient aucun ennemi.', monsterCustomInvalidEntry:'Saisissez un Monster ID, un niveau et des PV max valides.', monsterCustomSaveSuccess:'Vagues personnalisées de Calyx enregistrées.', monsterCustomSaveError:'Impossible d’enregistrer les vagues : {error}' },
|
||||||
|
de: { tabMonsterSettings:'👾 Monster-Einstellungen', monsterSettingsTitle:'👾 Monster-Einstellungen', monsterSettingsDescription:'Stages und Wellen aus den installierten Client-Ressourcen anzeigen. Eigene Gegnerwellen gelten nur im Golden / Crimson Calyx.', monsterSettingsRule:'Wöchentliche Bosse, stagnierende Schatten, Höhle der Korrosion und Endgame nutzen Clientdaten und können hier nicht geändert werden.', monsterModeMoc:'Memory of Chaos', monsterModePf:'Pure Fiction', monsterModeAs:'Apocalyptic Shadow', monsterModeAa:'Anomaly Arbitration', monsterModeCustom:'Eigene Gegner', monsterModeSu:'Simuliertes Universum', monsterSettingsLoading:'Client-Stage-Ressourcen werden gelesen…', monsterSettingsNoStages:'In den verbundenen Client-Ressourcen wurden keine Stages gefunden.', monsterSettingsGroup:'Stage-Gruppe', monsterSettingsGroupOption:'Gruppe {id} · {count} Einträge', monsterSettingsChallenge:'Herausforderungseintrag', monsterSettingsStageOption:'Eintrag {id}{boss}', monsterSettingsBoss:'Boss', monsterSettingsStageTitle:'Eintrag {id}', monsterSettingsDefaultBuff:'Standardbuff-ID {id}', monsterSettingsEncounterTitle:'Kampfstage {id}', monsterSettingsLevel:'Stufe {level}', monsterSettingsWaveCount:'{count} Wellen', monsterSettingsWave:'Welle {number}', monsterSettingsSuReadOnly:'Dieser Server unterstützt keine Bearbeitung von Gegnern im Simulierten Universum; bestätigte SU-Ressourcen fehlen ebenfalls.', monsterSettingsCatalogError:'Client-Stage-Ressourcen konnten nicht gelesen werden: {error}', monsterSettingsReadOnly:'Nur lesbar: Mit dem GaC-Server verbinden, um den Client-Katalog zu laden.', monsterSettingsLoaded:'Stage-Daten, nur lesbar · Quelle: {source}', monsterSettingsClientResources:'Client-Ressourcen', monsterSettingsDataUnavailable:'FreeSR-Spielerdaten sind nicht verfügbar', monsterCustomTitle:'Eigene Gegnerwellen', monsterCustomDescription:'Diese Wellen gelten nur beim Betreten eines Golden oder Crimson Calyx mit aktiver GaC-Srtool-Sitzung.', monsterCustomCalyxOnly:'Ersetzt keine Gegner bei wöchentlichen Bossen, stagnierenden Schatten, Höhle der Korrosion, MoC, Pure Fiction, Apocalyptic Shadow oder AA.', monsterCustomConnected:'Verbunden. Änderungen werden auf dem Server gespeichert und beim nächsten passenden Calyx-Eintritt angewendet.', monsterCustomConnect:'Mit GaC verbinden, um Calyx-Wellen zu bearbeiten und zu speichern.', monsterCustomEmpty:'Keine eigenen Wellen gesetzt. „Client-Stages verwenden“ stellt die installierten Calyx-Wellen wieder her.', monsterCustomAddWave:'Welle hinzufügen', monsterCustomUseClient:'Client-Stages verwenden', monsterCustomSave:'Wellen speichern', monsterCustomSaved:'Gespeichert · nur Golden / Crimson Calyx', monsterCustomUnsaved:'Ungespeicherte Änderungen', monsterCustomAddEnemy:'Gegner hinzufügen', monsterCustomRemoveEnemy:'Gegner entfernen', monsterCustomRemoveWave:'Welle entfernen', monsterCustomId:'Monster-ID', monsterCustomLevel:'Stufe', monsterCustomMaxHP:'Max. LP (optional)', monsterCustomNaturalHP:'Leer = normale Monster-LP', monsterCustomEmptyWave:'Welle {number} enthält keine Gegner.', monsterCustomInvalidEntry:'Gültige Monster-ID, Stufe und maximale LP eingeben.', monsterCustomSaveSuccess:'Eigene Calyx-Wellen gespeichert.', monsterCustomSaveError:'Wellen konnten nicht gespeichert werden: {error}' },
|
||||||
|
ru: { tabMonsterSettings:'👾 Настройки монстров', monsterSettingsTitle:'👾 Настройки монстров', monsterSettingsDescription:'Просмотр этапов и волн из ресурсов установленного клиента. Свои волны доступны только в Golden / Crimson Calyx.', monsterSettingsRule:'Еженедельные боссы, Застойная тень, Пещера коррозии и эндгейм используют данные клиента и здесь не изменяются.', monsterModeMoc:'Зал забвения', monsterModePf:'Чистый вымысел', monsterModeAs:'Апокалиптическая тень', monsterModeAa:'Аномальное противостояние', monsterModeCustom:'Свои монстры', monsterModeSu:'Виртуальная вселенная', monsterSettingsLoading:'Чтение ресурсов этапов клиента…', monsterSettingsNoStages:'В ресурсах подключённого клиента этапы не найдены.', monsterSettingsGroup:'Группа этапов', monsterSettingsGroupOption:'Группа {id} · записей: {count}', monsterSettingsChallenge:'Запись испытания', monsterSettingsStageOption:'Запись {id}{boss}', monsterSettingsBoss:'Босс', monsterSettingsStageTitle:'Запись {id}', monsterSettingsDefaultBuff:'Базовый эффект ID {id}', monsterSettingsEncounterTitle:'Этап боя {id}', monsterSettingsLevel:'Уровень {level}', monsterSettingsWaveCount:'Волн: {count}', monsterSettingsWave:'Волна {number}', monsterSettingsSuReadOnly:'Сервер не поддерживает редактирование монстров Виртуальной вселенной; проверенных ресурсов SU нет.', monsterSettingsCatalogError:'Не удалось прочитать ресурсы этапов клиента: {error}', monsterSettingsReadOnly:'Только чтение: подключитесь к GaC, чтобы загрузить каталог клиента.', monsterSettingsLoaded:'Данные этапа, только чтение · источник: {source}', monsterSettingsClientResources:'ресурсы клиента', monsterSettingsDataUnavailable:'Данные игрока FreeSR недоступны', monsterCustomTitle:'Свои волны монстров', monsterCustomDescription:'Волны применяются только при входе в Golden или Crimson Calyx с активной сессией GaC-Srtool.', monsterCustomCalyxOnly:'Не заменяет монстров еженедельных боссов, Застойной тени, Пещеры коррозии, MoC, Pure Fiction, Apocalyptic Shadow или AA.', monsterCustomConnected:'Подключено. Изменения сохранены на сервере и применятся при следующем подходящем входе в Calyx.', monsterCustomConnect:'Подключитесь к GaC, чтобы редактировать и сохранять волны Calyx.', monsterCustomEmpty:'Свои волны не заданы. «Использовать этапы клиента» восстановит волны Calyx из клиента.', monsterCustomAddWave:'Добавить волну', monsterCustomUseClient:'Использовать этапы клиента', monsterCustomSave:'Сохранить волны', monsterCustomSaved:'Сохранено · только Golden / Crimson Calyx', monsterCustomUnsaved:'Есть несохранённые изменения', monsterCustomAddEnemy:'Добавить монстра', monsterCustomRemoveEnemy:'Удалить монстра', monsterCustomRemoveWave:'Удалить волну', monsterCustomId:'ID монстра', monsterCustomLevel:'Уровень', monsterCustomMaxHP:'Макс. HP (необязательно)', monsterCustomNaturalHP:'Пусто = обычное HP монстра', monsterCustomEmptyWave:'В волне {number} нет монстров.', monsterCustomInvalidEntry:'Введите корректные ID монстра, уровень и макс. HP.', monsterCustomSaveSuccess:'Свои волны Calyx сохранены.', monsterCustomSaveError:'Не удалось сохранить волны: {error}' },
|
||||||
|
pt: { tabMonsterSettings:'👾 Configurações de monstros', monsterSettingsTitle:'👾 Configurações de monstros', monsterSettingsDescription:'Consulte fases e ondas dos recursos do Client instalado. Ondas personalizadas só se aplicam em Golden / Crimson Calyx.', monsterSettingsRule:'Chefes semanais, Sombra Estagnada, Caverna da Corrosão e Endgame usam dados do Client e não podem ser alterados aqui.', monsterModeMoc:'Memória do Caos', monsterModePf:'Ficção Pura', monsterModeAs:'Sombra Apocalíptica', monsterModeAa:'Anomaly Arbitration', monsterModeCustom:'Inimigo personalizado', monsterModeSu:'Universo Simulado', monsterSettingsLoading:'Lendo recursos de fase do Client…', monsterSettingsNoStages:'Nenhuma fase encontrada nos recursos do Client conectado.', monsterSettingsGroup:'Grupo de fases', monsterSettingsGroupOption:'Grupo {id} · {count} entradas', monsterSettingsChallenge:'Entrada de desafio', monsterSettingsStageOption:'Entrada {id}{boss}', monsterSettingsBoss:'Chefe', monsterSettingsStageTitle:'Entrada {id}', monsterSettingsDefaultBuff:'Bônus padrão ID {id}', monsterSettingsEncounterTitle:'Fase de combate {id}', monsterSettingsLevel:'Nível {level}', monsterSettingsWaveCount:'{count} ondas', monsterSettingsWave:'Onda {number}', monsterSettingsSuReadOnly:'Este servidor não permite editar inimigos do Universo Simulado e não possui recursos SU verificados.', monsterSettingsCatalogError:'Não foi possível ler recursos de fase do Client: {error}', monsterSettingsReadOnly:'Somente leitura: conecte-se ao servidor GaC para carregar o catálogo do Client.', monsterSettingsLoaded:'Dados de fase somente leitura · origem: {source}', monsterSettingsClientResources:'recursos do Client', monsterSettingsDataUnavailable:'Dados de jogador do FreeSR indisponíveis', monsterCustomTitle:'Ondas de inimigos personalizadas', monsterCustomDescription:'Estas ondas só são usadas ao entrar em Golden ou Crimson Calyx com a sessão GaC-Srtool ativa.', monsterCustomCalyxOnly:'Não substitui inimigos de chefes semanais, Sombra Estagnada, Caverna da Corrosão, MoC, Pure Fiction, Apocalyptic Shadow ou AA.', monsterCustomConnected:'Conectado. As alterações são salvas no servidor e aplicadas na próxima entrada elegível em Calyx.', monsterCustomConnect:'Conecte-se ao GaC para editar e salvar ondas personalizadas de Calyx.', monsterCustomEmpty:'Nenhuma onda personalizada definida. “Usar fases do Client” restaura as ondas instaladas de Calyx.', monsterCustomAddWave:'Adicionar onda', monsterCustomUseClient:'Usar fases do Client', monsterCustomSave:'Salvar ondas', monsterCustomSaved:'Salvo · somente Golden / Crimson Calyx', monsterCustomUnsaved:'Há alterações não salvas', monsterCustomAddEnemy:'Adicionar inimigo', monsterCustomRemoveEnemy:'Remover inimigo', monsterCustomRemoveWave:'Remover onda', monsterCustomId:'Monster ID', monsterCustomLevel:'Nível', monsterCustomMaxHP:'HP máx. (opcional)', monsterCustomNaturalHP:'Vazio = HP normal do monstro', monsterCustomEmptyWave:'A onda {number} não tem inimigos.', monsterCustomInvalidEntry:'Informe Monster ID, nível e HP máx. válidos.', monsterCustomSaveSuccess:'Ondas personalizadas de Calyx salvas.', monsterCustomSaveError:'Não foi possível salvar as ondas: {error}' }
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [language, additions] of Object.entries(MONSTER_SETTINGS_TRANSLATIONS)) {
|
||||||
|
Object.assign(TRANSLATIONS[language], additions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AA_ROOM_SELECTION_TRANSLATIONS = {
|
||||||
|
en: { monsterAaRoomTitle:'Use selected AA room', monsterAaRoomDescription:'Queue this Client stage for your next Anomaly Arbitration start from the game. The selection is one-shot and expires with the Srtool session.', monsterAaRoomPending:'Queued for the next AA start: {room} (Entry {id}).', monsterAaRoomUse:'Use this room next', monsterAaRoomClear:'Clear queued room', monsterAaRoomLoading:'Checking for a queued AA room…', monsterAaRoomConnect:'Connect to GaC to queue a room for the game.', monsterAaRoomLoadError:'Could not check the queued room: {error}', monsterAaRoomSavedToast:'AA room queued. Start Anomaly Arbitration from the game to use it.', monsterAaRoomClearedToast:'Queued AA room cleared.', monsterAaRoomSaveError:'Could not update the AA room: {error}' },
|
||||||
|
th: { monsterAaRoomTitle:'เลือกห้อง AA ที่จะเข้า', monsterAaRoomDescription:'ตั้ง stage ของ Client นี้เป็นห้องที่จะเข้าเมื่อเริ่ม Anomaly Arbitration ครั้งถัดไปจากในเกม การเลือกใช้ครั้งเดียวและหมดอายุตาม session ของ Srtool', monsterAaRoomPending:'ตั้งไว้สำหรับการเข้า AA ครั้งถัดไปแล้ว: {room} (รายการ {id})', monsterAaRoomUse:'ใช้ห้องนี้ครั้งถัดไป', monsterAaRoomClear:'ยกเลิกห้องที่ตั้งไว้', monsterAaRoomLoading:'กำลังตรวจสอบห้อง AA ที่ตั้งไว้…', monsterAaRoomConnect:'เชื่อมต่อ GaC เพื่อตั้งห้องให้เกมเข้า', monsterAaRoomLoadError:'ตรวจสอบห้องที่ตั้งไว้ไม่สำเร็จ: {error}', monsterAaRoomSavedToast:'ตั้งห้อง AA แล้ว ให้เริ่ม Anomaly Arbitration จากในเกมเพื่อเข้าไป', monsterAaRoomClearedToast:'ยกเลิกห้อง AA ที่ตั้งไว้แล้ว', monsterAaRoomSaveError:'เปลี่ยนห้อง AA ไม่สำเร็จ: {error}' },
|
||||||
|
'zh-CN': { monsterAaRoomTitle:'使用所选异相仲裁关卡', monsterAaRoomDescription:'将此客户端关卡设为下次从游戏内开始异相仲裁时进入的房间。该设置只使用一次,并会随 Srtool 会话过期。', monsterAaRoomPending:'已设为下次异相仲裁进入的房间:{room}(条目 {id})。', monsterAaRoomUse:'下次进入此房间', monsterAaRoomClear:'清除已选房间', monsterAaRoomLoading:'正在检查已选的异相仲裁房间…', monsterAaRoomConnect:'连接 GaC 后才能为游戏设置房间。', monsterAaRoomLoadError:'无法检查已选房间:{error}', monsterAaRoomSavedToast:'已设置异相仲裁房间。请从游戏内开始异相仲裁。', monsterAaRoomClearedToast:'已清除异相仲裁房间设置。', monsterAaRoomSaveError:'无法更新异相仲裁房间:{error}' },
|
||||||
|
'zh-TW': { monsterAaRoomTitle:'選擇下次進入的 AA 房間', monsterAaRoomDescription:'將此 Client 關卡設為下次從遊戲內開始 Anomaly Arbitration 時進入的房間。此設定僅使用一次,並會隨 Srtool 工作階段到期。', monsterAaRoomPending:'已設定為下次 AA 開始時進入的房間:{room}(項目 {id})。', monsterAaRoomUse:'下次使用此房間', monsterAaRoomClear:'清除已設定的房間', monsterAaRoomLoading:'正在檢查已設定的 AA 房間…', monsterAaRoomConnect:'請連線至 GaC,才能為遊戲設定房間。', monsterAaRoomLoadError:'無法檢查已設定的房間:{error}', monsterAaRoomSavedToast:'已設定 AA 房間。請從遊戲內開始 Anomaly Arbitration 以進入此房間。', monsterAaRoomClearedToast:'已清除 AA 房間設定。', monsterAaRoomSaveError:'無法更新 AA 房間:{error}' },
|
||||||
|
ja: { monsterAaRoomTitle:'次に入るAAの部屋を選択', monsterAaRoomDescription:'このクライアントのステージを、ゲーム内から次にAnomaly Arbitrationを開始したときの部屋に設定します。設定は1回限りで、Srtoolセッションの期限に合わせて失効します。', monsterAaRoomPending:'次回のAA開始時に入る部屋:{room}(エントリー {id})', monsterAaRoomUse:'次回この部屋に入る', monsterAaRoomClear:'選択した部屋を解除', monsterAaRoomLoading:'選択中のAA部屋を確認中…', monsterAaRoomConnect:'ゲームの部屋を設定するにはGaCに接続してください。', monsterAaRoomLoadError:'選択中の部屋を確認できませんでした:{error}', monsterAaRoomSavedToast:'AAの部屋を設定しました。ゲーム内からAnomaly Arbitrationを開始してください。', monsterAaRoomClearedToast:'AAの部屋設定を解除しました。', monsterAaRoomSaveError:'AAの部屋を更新できませんでした:{error}' },
|
||||||
|
ko: { monsterAaRoomTitle:'다음에 입장할 AA 방 선택', monsterAaRoomDescription:'이 Client 스테이지를 게임에서 다음 Anomaly Arbitration을 시작할 때 입장할 방으로 설정합니다. 한 번만 적용되며 Srtool 세션과 함께 만료됩니다.', monsterAaRoomPending:'다음 AA 시작 시 입장할 방: {room} (항목 {id})', monsterAaRoomUse:'다음에 이 방 입장', monsterAaRoomClear:'예약된 방 지우기', monsterAaRoomLoading:'예약된 AA 방 확인 중…', monsterAaRoomConnect:'게임에 방을 예약하려면 GaC에 연결하세요.', monsterAaRoomLoadError:'예약된 방을 확인하지 못했습니다: {error}', monsterAaRoomSavedToast:'AA 방을 예약했습니다. 게임에서 Anomaly Arbitration을 시작하세요.', monsterAaRoomClearedToast:'예약된 AA 방을 지웠습니다.', monsterAaRoomSaveError:'AA 방을 변경하지 못했습니다: {error}' },
|
||||||
|
vi: { monsterAaRoomTitle:'Chọn phòng AA sẽ vào', monsterAaRoomDescription:'Đặt stage của Client này làm phòng sẽ vào khi bắt đầu Anomaly Arbitration tiếp theo trong game. Lựa chọn chỉ dùng một lần và hết hạn cùng phiên Srtool.', monsterAaRoomPending:'Đã đặt cho lần bắt đầu AA tiếp theo: {room} (Mục {id}).', monsterAaRoomUse:'Dùng phòng này tiếp theo', monsterAaRoomClear:'Xóa phòng đã đặt', monsterAaRoomLoading:'Đang kiểm tra phòng AA đã đặt…', monsterAaRoomConnect:'Kết nối GaC để đặt phòng cho game.', monsterAaRoomLoadError:'Không thể kiểm tra phòng đã đặt: {error}', monsterAaRoomSavedToast:'Đã đặt phòng AA. Hãy bắt đầu Anomaly Arbitration trong game để vào phòng.', monsterAaRoomClearedToast:'Đã xóa phòng AA đã đặt.', monsterAaRoomSaveError:'Không thể cập nhật phòng AA: {error}' },
|
||||||
|
id: { monsterAaRoomTitle:'Pilih ruang AA berikutnya', monsterAaRoomDescription:'Atur stage Client ini sebagai ruang yang dimasuki saat memulai Anomaly Arbitration berikutnya dari dalam game. Pilihan hanya berlaku sekali dan kedaluwarsa bersama sesi Srtool.', monsterAaRoomPending:'Diatur untuk mulai AA berikutnya: {room} (Entri {id}).', monsterAaRoomUse:'Gunakan ruang ini berikutnya', monsterAaRoomClear:'Hapus ruang yang diatur', monsterAaRoomLoading:'Memeriksa ruang AA yang diatur…', monsterAaRoomConnect:'Hubungkan ke GaC untuk mengatur ruang bagi game.', monsterAaRoomLoadError:'Tidak dapat memeriksa ruang yang diatur: {error}', monsterAaRoomSavedToast:'Ruang AA diatur. Mulai Anomaly Arbitration dari dalam game untuk memakainya.', monsterAaRoomClearedToast:'Pengaturan ruang AA dihapus.', monsterAaRoomSaveError:'Tidak dapat memperbarui ruang AA: {error}' },
|
||||||
|
es: { monsterAaRoomTitle:'Elegir la próxima sala de AA', monsterAaRoomDescription:'Programa esta fase del cliente como la sala para la próxima vez que inicies Anomaly Arbitration desde el juego. La selección se usa una vez y caduca con la sesión de Srtool.', monsterAaRoomPending:'Programada para el próximo inicio de AA: {room} (entrada {id}).', monsterAaRoomUse:'Usar esta sala la próxima vez', monsterAaRoomClear:'Borrar sala programada', monsterAaRoomLoading:'Comprobando la sala de AA programada…', monsterAaRoomConnect:'Conéctate a GaC para programar una sala para el juego.', monsterAaRoomLoadError:'No se pudo comprobar la sala programada: {error}', monsterAaRoomSavedToast:'Sala de AA programada. Inicia Anomaly Arbitration desde el juego para entrar.', monsterAaRoomClearedToast:'Se borró la sala de AA programada.', monsterAaRoomSaveError:'No se pudo actualizar la sala de AA: {error}' },
|
||||||
|
fr: { monsterAaRoomTitle:'Choisir la prochaine salle AA', monsterAaRoomDescription:'Programmez ce stage du client comme salle à rejoindre au prochain lancement d’Anomaly Arbitration depuis le jeu. Le choix ne sert qu’une fois et expire avec la session Srtool.', monsterAaRoomPending:'Programmée pour le prochain lancement AA : {room} (entrée {id}).', monsterAaRoomUse:'Utiliser cette salle ensuite', monsterAaRoomClear:'Effacer la salle programmée', monsterAaRoomLoading:'Vérification de la salle AA programmée…', monsterAaRoomConnect:'Connectez-vous à GaC pour programmer une salle pour le jeu.', monsterAaRoomLoadError:'Impossible de vérifier la salle programmée : {error}', monsterAaRoomSavedToast:'Salle AA programmée. Lancez Anomaly Arbitration depuis le jeu pour y accéder.', monsterAaRoomClearedToast:'La salle AA programmée a été effacée.', monsterAaRoomSaveError:'Impossible de modifier la salle AA : {error}' },
|
||||||
|
de: { monsterAaRoomTitle:'Nächsten AA-Raum auswählen', monsterAaRoomDescription:'Diesen Client-Stage als Raum für den nächsten Start von Anomaly Arbitration im Spiel vormerken. Die Auswahl gilt einmalig und läuft mit der Srtool-Sitzung ab.', monsterAaRoomPending:'Für den nächsten AA-Start vorgemerkt: {room} (Eintrag {id}).', monsterAaRoomUse:'Diesen Raum als Nächstes nutzen', monsterAaRoomClear:'Vorgemerkten Raum löschen', monsterAaRoomLoading:'Vorgemerkten AA-Raum wird geprüft…', monsterAaRoomConnect:'Mit GaC verbinden, um einen Raum für das Spiel vorzumerken.', monsterAaRoomLoadError:'Vorgemerkter Raum konnte nicht geprüft werden: {error}', monsterAaRoomSavedToast:'AA-Raum vorgemerkt. Starte Anomaly Arbitration im Spiel, um ihn zu betreten.', monsterAaRoomClearedToast:'Vorgemerkter AA-Raum gelöscht.', monsterAaRoomSaveError:'AA-Raum konnte nicht aktualisiert werden: {error}' },
|
||||||
|
ru: { monsterAaRoomTitle:'Выбрать следующую комнату AA', monsterAaRoomDescription:'Назначьте этот этап клиента комнатой для следующего запуска Anomaly Arbitration из игры. Выбор действует один раз и истекает вместе с сеансом Srtool.', monsterAaRoomPending:'Назначено для следующего запуска AA: {room} (этап {id}).', monsterAaRoomUse:'Войти в эту комнату в следующий раз', monsterAaRoomClear:'Очистить выбранную комнату', monsterAaRoomLoading:'Проверка выбранной комнаты AA…', monsterAaRoomConnect:'Подключитесь к GaC, чтобы назначить комнату для игры.', monsterAaRoomLoadError:'Не удалось проверить выбранную комнату: {error}', monsterAaRoomSavedToast:'Комната AA назначена. Запустите Anomaly Arbitration в игре, чтобы войти.', monsterAaRoomClearedToast:'Выбор комнаты AA очищен.', monsterAaRoomSaveError:'Не удалось изменить комнату AA: {error}' },
|
||||||
|
pt: { monsterAaRoomTitle:'Escolher próxima sala de AA', monsterAaRoomDescription:'Agende esta fase do Client como a sala para a próxima vez que iniciar Anomaly Arbitration no jogo. A seleção é usada uma vez e expira com a sessão do Srtool.', monsterAaRoomPending:'Agendada para a próxima entrada em AA: {room} (Entrada {id}).', monsterAaRoomUse:'Usar esta sala na próxima vez', monsterAaRoomClear:'Limpar sala agendada', monsterAaRoomLoading:'Verificando sala AA agendada…', monsterAaRoomConnect:'Conecte-se ao GaC para agendar uma sala para o jogo.', monsterAaRoomLoadError:'Não foi possível verificar a sala agendada: {error}', monsterAaRoomSavedToast:'Sala AA agendada. Inicie Anomaly Arbitration no jogo para entrar nela.', monsterAaRoomClearedToast:'Sala AA agendada removida.', monsterAaRoomSaveError:'Não foi possível atualizar a sala AA: {error}' }
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [language, additions] of Object.entries(AA_ROOM_SELECTION_TRANSLATIONS)) {
|
||||||
|
Object.assign(TRANSLATIONS[language], additions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AA_ROUND_SELECTION_TRANSLATIONS = {
|
||||||
|
en: { monsterAaRoundTitle:'Choose the AA round', monsterAaRoundDescription:'Select the round the game should show when its AA data loads again. Choose a room in the game; entries below are only previews.', monsterAaRoundPending:'Selected round: {round} (Group {id}). Reopen Anomaly Arbitration in the game to show it.', monsterAaRoundSelectLabel:'Anomaly Arbitration round', monsterAaRoundDefaultOption:'Use the Client default round', monsterAaRoundDefault:'No round override. The Client default round will be shown.', monsterAaRoundUpdating:'Updating the AA round…', monsterAaRoundLoading:'Loading the saved AA round…', monsterAaRoundConnect:'Connect to GaC to choose the AA round shown in game.', monsterAaRoundLoadError:'Could not load the AA round: {error}', monsterAaRoundSavedToast:'AA round selected. Reopen Anomaly Arbitration in the game to show it.', monsterAaRoundClearedToast:'AA round override cleared. The Client default round will be shown.', monsterAaRoundSaveError:'Could not update the AA round: {error}', monsterAaRoundGroupId:'Round {id}', monsterSettingsAARoomPreview:'Rooms in this round · preview only' },
|
||||||
|
th: { monsterAaRoundTitle:'เลือกรอบ AA', monsterAaRoundDescription:'เลือกรอบที่จะแสดงในเกมเมื่อโหลดข้อมูล AA อีกครั้ง ส่วนห้องให้เลือกจากในเกม รายการด้านล่างมีไว้ดูตัวอย่างเท่านั้น', monsterAaRoundPending:'เลือกรอบ {round} (กลุ่ม {id}) แล้ว เปิด Anomaly Arbitration ในเกมอีกครั้งเพื่อให้แสดงรอบนี้', monsterAaRoundSelectLabel:'รอบ Anomaly Arbitration', monsterAaRoundDefaultOption:'ใช้รอบเริ่มต้นของ Client', monsterAaRoundDefault:'ไม่ได้แทนรอบ ระบบจะแสดงรอบเริ่มต้นของ Client', monsterAaRoundUpdating:'กำลังเปลี่ยนรอบ AA…', monsterAaRoundLoading:'กำลังโหลดรอบ AA ที่บันทึกไว้…', monsterAaRoundConnect:'เชื่อมต่อ GaC เพื่อเลือกรอบ AA ที่จะแสดงในเกม', monsterAaRoundLoadError:'โหลดรอบ AA ไม่สำเร็จ: {error}', monsterAaRoundSavedToast:'เลือกรอบ AA แล้ว เปิด Anomaly Arbitration ในเกมอีกครั้งเพื่อให้แสดงรอบนี้', monsterAaRoundClearedToast:'ล้างการแทนรอบ AA แล้ว ระบบจะแสดงรอบเริ่มต้นของ Client', monsterAaRoundSaveError:'เปลี่ยนรอบ AA ไม่สำเร็จ: {error}', monsterAaRoundGroupId:'รอบ {id}', monsterSettingsAARoomPreview:'ห้องในรอบนี้ · ตัวอย่างเท่านั้น' },
|
||||||
|
'zh-CN': { monsterAaRoundTitle:'选择 AA 轮次', monsterAaRoundDescription:'选择游戏重新加载 AA 数据时显示的轮次。具体房间仍在游戏内选择;下方条目仅供预览。', monsterAaRoundPending:'已选择轮次:{round}(组 {id})。请重新打开游戏内的异相仲裁以显示。', monsterAaRoundSelectLabel:'Anomaly Arbitration 轮次', monsterAaRoundDefaultOption:'使用 Client 默认轮次', monsterAaRoundDefault:'未覆盖轮次,将显示 Client 默认轮次。', monsterAaRoundUpdating:'正在更新 AA 轮次…', monsterAaRoundLoading:'正在加载已保存的 AA 轮次…', monsterAaRoundConnect:'连接 GaC 后才能选择游戏中显示的 AA 轮次。', monsterAaRoundLoadError:'无法加载 AA 轮次:{error}', monsterAaRoundSavedToast:'已选择 AA 轮次。请重新打开游戏内的异相仲裁以显示。', monsterAaRoundClearedToast:'已清除 AA 轮次覆盖,将显示 Client 默认轮次。', monsterAaRoundSaveError:'无法更新 AA 轮次:{error}', monsterAaRoundGroupId:'轮次 {id}', monsterSettingsAARoomPreview:'本轮房间 · 仅预览' },
|
||||||
|
'zh-TW': { monsterAaRoundTitle:'選擇 AA 輪次', monsterAaRoundDescription:'選擇遊戲重新載入 AA 資料時要顯示的輪次。房間仍在遊戲內選擇;下方項目僅供預覽。', monsterAaRoundPending:'已選擇輪次:{round}(組別 {id})。請重新開啟遊戲內的 Anomaly Arbitration 以顯示。', monsterAaRoundSelectLabel:'Anomaly Arbitration 輪次', monsterAaRoundDefaultOption:'使用 Client 預設輪次', monsterAaRoundDefault:'尚未覆寫輪次,將顯示 Client 預設輪次。', monsterAaRoundUpdating:'正在更新 AA 輪次…', monsterAaRoundLoading:'正在載入已儲存的 AA 輪次…', monsterAaRoundConnect:'連線 GaC 後才能選擇遊戲中顯示的 AA 輪次。', monsterAaRoundLoadError:'無法載入 AA 輪次:{error}', monsterAaRoundSavedToast:'已選擇 AA 輪次。請重新開啟遊戲內的 Anomaly Arbitration 以顯示。', monsterAaRoundClearedToast:'已清除 AA 輪次覆寫,將顯示 Client 預設輪次。', monsterAaRoundSaveError:'無法更新 AA 輪次:{error}', monsterAaRoundGroupId:'輪次 {id}', monsterSettingsAARoomPreview:'本輪房間 · 僅供預覽' },
|
||||||
|
ja: { monsterAaRoundTitle:'AAのラウンドを選択', monsterAaRoundDescription:'ゲームがAAデータを再読み込みしたときに表示するラウンドを選びます。部屋はゲーム内で選択してください。下の項目はプレビューのみです。', monsterAaRoundPending:'選択中のラウンド:{round}(グループ {id})。表示するにはゲーム内でAnomaly Arbitrationを開き直してください。', monsterAaRoundSelectLabel:'Anomaly Arbitrationのラウンド', monsterAaRoundDefaultOption:'Clientのデフォルトを使用', monsterAaRoundDefault:'ラウンドは上書きされていません。Clientのデフォルトを表示します。', monsterAaRoundUpdating:'AAラウンドを更新中…', monsterAaRoundLoading:'保存済みAAラウンドを読み込み中…', monsterAaRoundConnect:'ゲームに表示するAAラウンドを選ぶにはGaCに接続してください。', monsterAaRoundLoadError:'AAラウンドを読み込めませんでした:{error}', monsterAaRoundSavedToast:'AAラウンドを選択しました。表示するにはゲーム内のAnomaly Arbitrationを開き直してください。', monsterAaRoundClearedToast:'AAラウンド設定を解除しました。Clientのデフォルトを表示します。', monsterAaRoundSaveError:'AAラウンドを更新できませんでした:{error}', monsterAaRoundGroupId:'ラウンド {id}', monsterSettingsAARoomPreview:'このラウンドの部屋 · プレビューのみ' },
|
||||||
|
ko: { monsterAaRoundTitle:'AA 라운드 선택', monsterAaRoundDescription:'게임에서 AA 데이터를 다시 불러올 때 표시할 라운드를 선택하세요. 방은 게임 안에서 선택하며 아래 항목은 미리보기 전용입니다.', monsterAaRoundPending:'선택한 라운드: {round} (그룹 {id}). 게임에서 Anomaly Arbitration을 다시 열어 표시하세요.', monsterAaRoundSelectLabel:'Anomaly Arbitration 라운드', monsterAaRoundDefaultOption:'Client 기본 라운드 사용', monsterAaRoundDefault:'라운드 재정의가 없습니다. Client 기본 라운드를 표시합니다.', monsterAaRoundUpdating:'AA 라운드 업데이트 중…', monsterAaRoundLoading:'저장된 AA 라운드 불러오는 중…', monsterAaRoundConnect:'게임에 표시할 AA 라운드를 선택하려면 GaC에 연결하세요.', monsterAaRoundLoadError:'AA 라운드를 불러오지 못했습니다: {error}', monsterAaRoundSavedToast:'AA 라운드를 선택했습니다. 게임에서 Anomaly Arbitration을 다시 열어 표시하세요.', monsterAaRoundClearedToast:'AA 라운드 재정의를 지웠습니다. Client 기본 라운드를 표시합니다.', monsterAaRoundSaveError:'AA 라운드를 업데이트하지 못했습니다: {error}', monsterAaRoundGroupId:'라운드 {id}', monsterSettingsAARoomPreview:'이 라운드의 방 · 미리보기만' },
|
||||||
|
vi: { monsterAaRoundTitle:'Chọn vòng AA', monsterAaRoundDescription:'Chọn vòng sẽ hiển thị khi game tải lại dữ liệu AA. Hãy chọn phòng trong game; các mục bên dưới chỉ để xem trước.', monsterAaRoundPending:'Đã chọn vòng: {round} (Nhóm {id}). Mở lại Anomaly Arbitration trong game để hiển thị.', monsterAaRoundSelectLabel:'Vòng Anomaly Arbitration', monsterAaRoundDefaultOption:'Dùng vòng mặc định của Client', monsterAaRoundDefault:'Không ghi đè vòng. Sẽ hiển thị vòng mặc định của Client.', monsterAaRoundUpdating:'Đang cập nhật vòng AA…', monsterAaRoundLoading:'Đang tải vòng AA đã lưu…', monsterAaRoundConnect:'Kết nối GaC để chọn vòng AA hiển thị trong game.', monsterAaRoundLoadError:'Không thể tải vòng AA: {error}', monsterAaRoundSavedToast:'Đã chọn vòng AA. Mở lại Anomaly Arbitration trong game để hiển thị.', monsterAaRoundClearedToast:'Đã xóa ghi đè vòng AA. Sẽ hiển thị vòng mặc định của Client.', monsterAaRoundSaveError:'Không thể cập nhật vòng AA: {error}', monsterAaRoundGroupId:'Vòng {id}', monsterSettingsAARoomPreview:'Phòng trong vòng này · chỉ xem trước' },
|
||||||
|
id: { monsterAaRoundTitle:'Pilih ronde AA', monsterAaRoundDescription:'Pilih ronde yang ditampilkan saat game memuat ulang data AA. Pilih ruang dari dalam game; entri di bawah hanya pratinjau.', monsterAaRoundPending:'Ronde terpilih: {round} (Grup {id}). Buka kembali Anomaly Arbitration di game untuk menampilkannya.', monsterAaRoundSelectLabel:'Ronde Anomaly Arbitration', monsterAaRoundDefaultOption:'Gunakan ronde default Client', monsterAaRoundDefault:'Tidak ada ronde yang ditimpa. Ronde default Client akan ditampilkan.', monsterAaRoundUpdating:'Memperbarui ronde AA…', monsterAaRoundLoading:'Memuat ronde AA tersimpan…', monsterAaRoundConnect:'Hubungkan ke GaC untuk memilih ronde AA yang ditampilkan di game.', monsterAaRoundLoadError:'Tidak dapat memuat ronde AA: {error}', monsterAaRoundSavedToast:'Ronde AA dipilih. Buka kembali Anomaly Arbitration di game untuk menampilkannya.', monsterAaRoundClearedToast:'Penggantian ronde AA dihapus. Ronde default Client akan ditampilkan.', monsterAaRoundSaveError:'Tidak dapat memperbarui ronde AA: {error}', monsterAaRoundGroupId:'Ronde {id}', monsterSettingsAARoomPreview:'Ruang di ronde ini · pratinjau saja' },
|
||||||
|
es: { monsterAaRoundTitle:'Elegir ronda de AA', monsterAaRoundDescription:'Elige la ronda que mostrará el juego al volver a cargar los datos de AA. Elige la sala dentro del juego; las entradas de abajo son solo una vista previa.', monsterAaRoundPending:'Ronda elegida: {round} (grupo {id}). Vuelve a abrir Anomaly Arbitration en el juego para mostrarla.', monsterAaRoundSelectLabel:'Ronda de Anomaly Arbitration', monsterAaRoundDefaultOption:'Usar la ronda predeterminada del cliente', monsterAaRoundDefault:'No hay una ronda sobrescrita. Se mostrará la predeterminada del cliente.', monsterAaRoundUpdating:'Actualizando ronda de AA…', monsterAaRoundLoading:'Cargando ronda de AA guardada…', monsterAaRoundConnect:'Conéctate a GaC para elegir la ronda de AA que se muestra en el juego.', monsterAaRoundLoadError:'No se pudo cargar la ronda de AA: {error}', monsterAaRoundSavedToast:'Ronda de AA elegida. Vuelve a abrir Anomaly Arbitration en el juego para mostrarla.', monsterAaRoundClearedToast:'Se borró la sobrescritura de AA. Se mostrará la ronda predeterminada del cliente.', monsterAaRoundSaveError:'No se pudo actualizar la ronda de AA: {error}', monsterAaRoundGroupId:'Ronda {id}', monsterSettingsAARoomPreview:'Salas de esta ronda · solo vista previa' },
|
||||||
|
fr: { monsterAaRoundTitle:'Choisir le tour AA', monsterAaRoundDescription:'Choisissez le tour que le jeu affichera au prochain chargement des données AA. Choisissez la salle dans le jeu ; les entrées ci-dessous sont des aperçus.', monsterAaRoundPending:'Tour sélectionné : {round} (groupe {id}). Rouvrez Anomaly Arbitration dans le jeu pour l’afficher.', monsterAaRoundSelectLabel:'Tour Anomaly Arbitration', monsterAaRoundDefaultOption:'Utiliser le tour par défaut du client', monsterAaRoundDefault:'Aucun tour forcé. Le tour par défaut du client sera affiché.', monsterAaRoundUpdating:'Mise à jour du tour AA…', monsterAaRoundLoading:'Chargement du tour AA enregistré…', monsterAaRoundConnect:'Connectez-vous à GaC pour choisir le tour AA affiché dans le jeu.', monsterAaRoundLoadError:'Impossible de charger le tour AA : {error}', monsterAaRoundSavedToast:'Tour AA sélectionné. Rouvrez Anomaly Arbitration dans le jeu pour l’afficher.', monsterAaRoundClearedToast:'Remplacement du tour AA effacé. Le tour par défaut du client sera affiché.', monsterAaRoundSaveError:'Impossible de modifier le tour AA : {error}', monsterAaRoundGroupId:'Tour {id}', monsterSettingsAARoomPreview:'Salles de ce tour · aperçu uniquement' },
|
||||||
|
de: { monsterAaRoundTitle:'AA-Runde auswählen', monsterAaRoundDescription:'Wähle die Runde, die das Spiel beim erneuten Laden der AA-Daten anzeigen soll. Den Raum wählst du im Spiel; die Einträge unten sind nur Vorschauen.', monsterAaRoundPending:'Ausgewählte Runde: {round} (Gruppe {id}). Öffne Anomaly Arbitration im Spiel erneut, damit sie angezeigt wird.', monsterAaRoundSelectLabel:'Anomaly-Arbitration-Runde', monsterAaRoundDefaultOption:'Client-Standardrunde verwenden', monsterAaRoundDefault:'Keine Runde überschrieben. Die Client-Standardrunde wird angezeigt.', monsterAaRoundUpdating:'AA-Runde wird aktualisiert…', monsterAaRoundLoading:'Gespeicherte AA-Runde wird geladen…', monsterAaRoundConnect:'Mit GaC verbinden, um die im Spiel angezeigte AA-Runde auszuwählen.', monsterAaRoundLoadError:'AA-Runde konnte nicht geladen werden: {error}', monsterAaRoundSavedToast:'AA-Runde ausgewählt. Öffne Anomaly Arbitration im Spiel erneut.', monsterAaRoundClearedToast:'AA-Rundenüberschreibung entfernt. Die Client-Standardrunde wird angezeigt.', monsterAaRoundSaveError:'AA-Runde konnte nicht aktualisiert werden: {error}', monsterAaRoundGroupId:'Runde {id}', monsterSettingsAARoomPreview:'Räume dieser Runde · nur Vorschau' },
|
||||||
|
ru: { monsterAaRoundTitle:'Выбрать раунд AA', monsterAaRoundDescription:'Выберите раунд, который игра покажет при повторной загрузке данных AA. Комнату выбирайте в игре; записи ниже — только предварительный просмотр.', monsterAaRoundPending:'Выбран раунд: {round} (группа {id}). Откройте Anomaly Arbitration в игре повторно, чтобы увидеть его.', monsterAaRoundSelectLabel:'Раунд Anomaly Arbitration', monsterAaRoundDefaultOption:'Использовать раунд клиента по умолчанию', monsterAaRoundDefault:'Раунд не переопределён. Будет показан раунд клиента по умолчанию.', monsterAaRoundUpdating:'Обновление раунда AA…', monsterAaRoundLoading:'Загрузка сохранённого раунда AA…', monsterAaRoundConnect:'Подключитесь к GaC, чтобы выбрать отображаемый в игре раунд AA.', monsterAaRoundLoadError:'Не удалось загрузить раунд AA: {error}', monsterAaRoundSavedToast:'Раунд AA выбран. Повторно откройте Anomaly Arbitration в игре.', monsterAaRoundClearedToast:'Переопределение раунда AA очищено. Будет показан раунд клиента по умолчанию.', monsterAaRoundSaveError:'Не удалось изменить раунд AA: {error}', monsterAaRoundGroupId:'Раунд {id}', monsterSettingsAARoomPreview:'Комнаты этого раунда · только просмотр' },
|
||||||
|
pt: { monsterAaRoundTitle:'Escolher rodada de AA', monsterAaRoundDescription:'Escolha a rodada que o jogo exibirá ao carregar os dados de AA novamente. Escolha a sala no jogo; as entradas abaixo são apenas prévias.', monsterAaRoundPending:'Rodada selecionada: {round} (grupo {id}). Reabra Anomaly Arbitration no jogo para exibi-la.', monsterAaRoundSelectLabel:'Rodada de Anomaly Arbitration', monsterAaRoundDefaultOption:'Usar rodada padrão do Client', monsterAaRoundDefault:'Nenhuma rodada substituída. A rodada padrão do Client será exibida.', monsterAaRoundUpdating:'Atualizando rodada de AA…', monsterAaRoundLoading:'Carregando rodada de AA salva…', monsterAaRoundConnect:'Conecte-se ao GaC para escolher a rodada de AA exibida no jogo.', monsterAaRoundLoadError:'Não foi possível carregar a rodada de AA: {error}', monsterAaRoundSavedToast:'Rodada de AA selecionada. Reabra Anomaly Arbitration no jogo para exibi-la.', monsterAaRoundClearedToast:'Substituição da rodada de AA removida. A rodada padrão do Client será exibida.', monsterAaRoundSaveError:'Não foi possível atualizar a rodada de AA: {error}', monsterAaRoundGroupId:'Rodada {id}', monsterSettingsAARoomPreview:'Salas desta rodada · apenas prévia' }
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [language, additions] of Object.entries(AA_ROUND_SELECTION_TRANSLATIONS)) {
|
||||||
|
Object.assign(TRANSLATIONS[language], additions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MONSTER_PICKER_TRANSLATIONS = {
|
||||||
|
en: 'Search monsters by name or ID',
|
||||||
|
th: 'ค้นหามอนสเตอร์ด้วยชื่อหรือ ID',
|
||||||
|
'zh-CN': '按名称或 ID 搜索怪物',
|
||||||
|
'zh-TW': '依名稱或 ID 搜尋怪物',
|
||||||
|
ja: '名前またはIDでモンスターを検索',
|
||||||
|
ko: '이름 또는 ID로 몬스터 검색',
|
||||||
|
fr: 'Rechercher un monstre par nom ou ID',
|
||||||
|
de: 'Monster nach Name oder ID suchen',
|
||||||
|
ru: 'Поиск монстра по имени или ID',
|
||||||
|
pt: 'Buscar monstros por nome ou ID',
|
||||||
|
vi: 'Tìm quái vật theo tên hoặc ID',
|
||||||
|
id: 'Cari monster berdasarkan nama atau ID'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [language, label] of Object.entries(MONSTER_PICKER_TRANSLATIONS)) {
|
||||||
|
if (TRANSLATIONS[language]) TRANSLATIONS[language].monsterCustomSearch = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SERVER_MODE_AVAILABILITY_TRANSLATIONS = {
|
||||||
|
th: { serverModeFireflyPaused: 'FireflyGo — กำลังพัฒนา', serverModeRobinPaused: 'RobinSR — กำลังพัฒนา', serverModesPausedNotice: 'FireflyGo และ RobinSR ปิดใช้งานชั่วคราวระหว่างพัฒนา จึงกลับไปใช้ GaC' },
|
||||||
|
en: { serverModeFireflyPaused: 'FireflyGo — Under development', serverModeRobinPaused: 'RobinSR — Under development', serverModesPausedNotice: 'FireflyGo and RobinSR are temporarily disabled during development. Switched back to GaC.' },
|
||||||
|
'zh-CN': { serverModeFireflyPaused: 'FireflyGo — 开发中', serverModeRobinPaused: 'RobinSR — 开发中', serverModesPausedNotice: 'FireflyGo 和 RobinSR 正在开发中,暂时停用。已切回 GaC。' },
|
||||||
|
'zh-TW': { serverModeFireflyPaused: 'FireflyGo — 開發中', serverModeRobinPaused: 'RobinSR — 開發中', serverModesPausedNotice: 'FireflyGo 和 RobinSR 正在開發中,暫時停用。已切回 GaC。' },
|
||||||
|
ja: { serverModeFireflyPaused: 'FireflyGo — 開発中', serverModeRobinPaused: 'RobinSR — 開発中', serverModesPausedNotice: 'FireflyGo と RobinSR は開発中のため一時停止しています。GaC に切り替えました。' },
|
||||||
|
ko: { serverModeFireflyPaused: 'FireflyGo — 개발 중', serverModeRobinPaused: 'RobinSR — 개발 중', serverModesPausedNotice: 'FireflyGo와 RobinSR은 개발 중이라 일시적으로 비활성화되었습니다. GaC로 전환했습니다.' },
|
||||||
|
vi: { serverModeFireflyPaused: 'FireflyGo — Đang phát triển', serverModeRobinPaused: 'RobinSR — Đang phát triển', serverModesPausedNotice: 'FireflyGo và RobinSR đang được phát triển nên tạm thời bị tắt. Đã chuyển về GaC.' },
|
||||||
|
id: { serverModeFireflyPaused: 'FireflyGo — Sedang dikembangkan', serverModeRobinPaused: 'RobinSR — Sedang dikembangkan', serverModesPausedNotice: 'FireflyGo dan RobinSR dinonaktifkan sementara selama pengembangan. Kembali ke GaC.' },
|
||||||
|
es: { serverModeFireflyPaused: 'FireflyGo — En desarrollo', serverModeRobinPaused: 'RobinSR — En desarrollo', serverModesPausedNotice: 'FireflyGo y RobinSR están desactivados temporalmente durante su desarrollo. Se cambió a GaC.' },
|
||||||
|
fr: { serverModeFireflyPaused: 'FireflyGo — En développement', serverModeRobinPaused: 'RobinSR — En développement', serverModesPausedNotice: 'FireflyGo et RobinSR sont temporairement désactivés pendant leur développement. Retour à GaC.' },
|
||||||
|
de: { serverModeFireflyPaused: 'FireflyGo — In Entwicklung', serverModeRobinPaused: 'RobinSR — In Entwicklung', serverModesPausedNotice: 'FireflyGo und RobinSR sind während der Entwicklung vorübergehend deaktiviert. Zu GaC gewechselt.' },
|
||||||
|
ru: { serverModeFireflyPaused: 'FireflyGo — В разработке', serverModeRobinPaused: 'RobinSR — В разработке', serverModesPausedNotice: 'FireflyGo и RobinSR временно отключены на время разработки. Выполнен переход на GaC.' },
|
||||||
|
pt: { serverModeFireflyPaused: 'FireflyGo — Em desenvolvimento', serverModeRobinPaused: 'RobinSR — Em desenvolvimento', serverModesPausedNotice: 'FireflyGo e RobinSR estão temporariamente desativados durante o desenvolvimento. Voltou para GaC.' }
|
||||||
|
};
|
||||||
|
|
||||||
for (const [language, additions] of Object.entries(ADDITIONAL_TRANSLATIONS)) {
|
for (const [language, additions] of Object.entries(ADDITIONAL_TRANSLATIONS)) {
|
||||||
Object.assign(TRANSLATIONS[language], additions);
|
Object.assign(TRANSLATIONS[language], additions);
|
||||||
}
|
}
|
||||||
@@ -6634,6 +6862,187 @@ for (const [language, additions] of Object.entries(INTERACTION_FEEDBACK_TRANSLAT
|
|||||||
for (const [language, additions] of Object.entries(LAST_MILE_TRANSLATIONS)) {
|
for (const [language, additions] of Object.entries(LAST_MILE_TRANSLATIONS)) {
|
||||||
Object.assign(TRANSLATIONS[language], additions);
|
Object.assign(TRANSLATIONS[language], additions);
|
||||||
}
|
}
|
||||||
|
for (const [language, additions] of Object.entries(AHA_HELP_TRANSLATIONS)) {
|
||||||
|
Object.assign(TRANSLATIONS[language], additions);
|
||||||
|
}
|
||||||
|
for (const [language, additions] of Object.entries(SERVER_MODE_AVAILABILITY_TRANSLATIONS)) {
|
||||||
|
Object.assign(TRANSLATIONS[language], additions);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ITEM_CATALOG_TRANSLATIONS = {
|
||||||
|
th: {
|
||||||
|
tabItemCatalog: '📚 สมุดไอเทม', itemCatalogTitle: '📚 สมุดรวมไอเทม',
|
||||||
|
itemCatalogDescription: 'ค้นชื่อหรือ ID เพื่อดูรูปและรหัสจากตารางเกม รายการที่มีป้าย GaC รองรับใช้จัดการในเครื่องมือกระเป๋าได้ ส่วนรายการอื่นใช้ค้นดู ID เท่านั้น',
|
||||||
|
itemCatalogSearchPlaceholder: 'ค้นหาชื่อหรือ Item ID...', itemCatalogFilterLabel: 'กรองสมุดไอเทม', itemCatalogFilterAll: 'ทั้งหมด',
|
||||||
|
itemCatalogFilterSupported: 'GaC รองรับ', itemCatalogFilterItems: 'ไอเทม',
|
||||||
|
itemCatalogFilterLightCones: 'กรวยแสง', itemCatalogFilterRelics: 'รีลิกส์',
|
||||||
|
itemCatalogSupportedTag: 'GaC รองรับ', itemCatalogBrowseOnlyTag: 'ดูข้อมูลเท่านั้น',
|
||||||
|
itemCatalogUnknownName: 'ไม่มีชื่อในตาราง #{id}', itemCatalogNoImage: 'ไม่มีภาพในข้อมูลเกม',
|
||||||
|
itemCatalogLoading: 'กำลังโหลดสมุดไอเทม...', itemCatalogLoadFailed: 'โหลดสมุดไอเทมไม่สำเร็จ'
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
tabItemCatalog: '📚 Item Book', itemCatalogTitle: '📚 Item Catalog',
|
||||||
|
itemCatalogDescription: 'Search game table names or IDs and view their icons. Entries marked GaC supported can be managed in the inventory tool; other entries are browse-only.',
|
||||||
|
itemCatalogSearchPlaceholder: 'Search by name or item ID...', itemCatalogFilterLabel: 'Filter item catalog', itemCatalogFilterAll: 'All entries',
|
||||||
|
itemCatalogFilterSupported: 'GaC supported', itemCatalogFilterItems: 'Items',
|
||||||
|
itemCatalogFilterLightCones: 'Light Cones', itemCatalogFilterRelics: 'Relics',
|
||||||
|
itemCatalogSupportedTag: 'GaC supported', itemCatalogBrowseOnlyTag: 'Browse only',
|
||||||
|
itemCatalogUnknownName: 'Unnamed item #{id}', itemCatalogNoImage: 'No icon in game data',
|
||||||
|
itemCatalogLoading: 'Loading item catalog...', itemCatalogLoadFailed: 'Could not load item catalog'
|
||||||
|
},
|
||||||
|
'zh-CN': {
|
||||||
|
tabItemCatalog: '📚 物品图鉴', itemCatalogTitle: '📚 物品目录',
|
||||||
|
itemCatalogDescription: '搜索游戏表格中的名称或 ID 并查看图标。标记为 GaC 支持的条目可在背包工具中管理;其他条目仅供查询。',
|
||||||
|
itemCatalogSearchPlaceholder: '搜索名称或物品 ID...', itemCatalogFilterLabel: '筛选物品目录', itemCatalogFilterAll: '全部条目',
|
||||||
|
itemCatalogFilterSupported: 'GaC 支持', itemCatalogFilterItems: '物品',
|
||||||
|
itemCatalogFilterLightCones: '光锥', itemCatalogFilterRelics: '遗器',
|
||||||
|
itemCatalogSupportedTag: 'GaC 支持', itemCatalogBrowseOnlyTag: '仅供查询',
|
||||||
|
itemCatalogUnknownName: '未命名物品 #{id}', itemCatalogNoImage: '游戏数据未配置图标',
|
||||||
|
itemCatalogLoading: '正在加载物品目录...', itemCatalogLoadFailed: '无法加载物品目录'
|
||||||
|
},
|
||||||
|
'zh-TW': {
|
||||||
|
tabItemCatalog: '📚 物品圖鑑', itemCatalogTitle: '📚 物品目錄',
|
||||||
|
itemCatalogDescription: '搜尋遊戲表格中的名稱或 ID 並查看圖示。標記為 GaC 支援的項目可在背包工具中管理;其他項目僅供查詢。',
|
||||||
|
itemCatalogSearchPlaceholder: '搜尋名稱或物品 ID...', itemCatalogFilterLabel: '篩選物品目錄', itemCatalogFilterAll: '全部項目',
|
||||||
|
itemCatalogFilterSupported: 'GaC 支援', itemCatalogFilterItems: '物品',
|
||||||
|
itemCatalogFilterLightCones: '光錐', itemCatalogFilterRelics: '遺器',
|
||||||
|
itemCatalogSupportedTag: 'GaC 支援', itemCatalogBrowseOnlyTag: '僅供查詢',
|
||||||
|
itemCatalogUnknownName: '未命名物品 #{id}', itemCatalogNoImage: '遊戲資料未設定圖示',
|
||||||
|
itemCatalogLoading: '正在載入物品目錄...', itemCatalogLoadFailed: '無法載入物品目錄'
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
tabItemCatalog: '📚 アイテム図鑑', itemCatalogTitle: '📚 アイテム一覧',
|
||||||
|
itemCatalogDescription: 'ゲームデータの名前や ID を検索してアイコンを確認できます。GaC 対応と表示された項目は所持品ツールで管理でき、その他は閲覧のみです。',
|
||||||
|
itemCatalogSearchPlaceholder: '名前またはアイテム ID を検索...', itemCatalogFilterLabel: 'アイテム一覧を絞り込む', itemCatalogFilterAll: 'すべて',
|
||||||
|
itemCatalogFilterSupported: 'GaC 対応', itemCatalogFilterItems: 'アイテム',
|
||||||
|
itemCatalogFilterLightCones: '光円錐', itemCatalogFilterRelics: '遺物',
|
||||||
|
itemCatalogSupportedTag: 'GaC 対応', itemCatalogBrowseOnlyTag: '閲覧のみ',
|
||||||
|
itemCatalogUnknownName: '名前なし #{id}', itemCatalogNoImage: 'ゲームデータに画像なし',
|
||||||
|
itemCatalogLoading: 'アイテム一覧を読み込み中...', itemCatalogLoadFailed: 'アイテム一覧を読み込めませんでした'
|
||||||
|
},
|
||||||
|
ko: {
|
||||||
|
tabItemCatalog: '📚 아이템 도감', itemCatalogTitle: '📚 아이템 목록',
|
||||||
|
itemCatalogDescription: '게임 데이터의 이름이나 ID를 검색해 아이콘을 확인하세요. GaC 지원 표시가 있는 항목은 인벤토리 도구에서 관리할 수 있으며, 나머지는 조회 전용입니다.',
|
||||||
|
itemCatalogSearchPlaceholder: '이름 또는 아이템 ID 검색...', itemCatalogFilterLabel: '아이템 목록 필터', itemCatalogFilterAll: '전체 항목',
|
||||||
|
itemCatalogFilterSupported: 'GaC 지원', itemCatalogFilterItems: '아이템',
|
||||||
|
itemCatalogFilterLightCones: '광추', itemCatalogFilterRelics: '유물',
|
||||||
|
itemCatalogSupportedTag: 'GaC 지원', itemCatalogBrowseOnlyTag: '조회 전용',
|
||||||
|
itemCatalogUnknownName: '이름 없음 #{id}', itemCatalogNoImage: '게임 데이터에 아이콘 없음',
|
||||||
|
itemCatalogLoading: '아이템 목록을 불러오는 중...', itemCatalogLoadFailed: '아이템 목록을 불러오지 못했습니다'
|
||||||
|
},
|
||||||
|
vi: {
|
||||||
|
tabItemCatalog: '📚 Sổ tay vật phẩm', itemCatalogTitle: '📚 Danh mục vật phẩm',
|
||||||
|
itemCatalogDescription: 'Tìm tên hoặc ID trong bảng dữ liệu trò chơi để xem biểu tượng. Mục có nhãn GaC hỗ trợ có thể quản lý trong công cụ túi đồ; mục khác chỉ dùng để tra cứu.',
|
||||||
|
itemCatalogSearchPlaceholder: 'Tìm theo tên hoặc ID vật phẩm...', itemCatalogFilterLabel: 'Lọc danh mục vật phẩm', itemCatalogFilterAll: 'Tất cả mục',
|
||||||
|
itemCatalogFilterSupported: 'GaC hỗ trợ', itemCatalogFilterItems: 'Vật phẩm',
|
||||||
|
itemCatalogFilterLightCones: 'Nón Ánh Sáng', itemCatalogFilterRelics: 'Di Vật',
|
||||||
|
itemCatalogSupportedTag: 'GaC hỗ trợ', itemCatalogBrowseOnlyTag: 'Chỉ tra cứu',
|
||||||
|
itemCatalogUnknownName: 'Vật phẩm chưa có tên #{id}', itemCatalogNoImage: 'Dữ liệu game không có biểu tượng',
|
||||||
|
itemCatalogLoading: 'Đang tải danh mục vật phẩm...', itemCatalogLoadFailed: 'Không tải được danh mục vật phẩm'
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
tabItemCatalog: '📚 Buku Item', itemCatalogTitle: '📚 Katalog Item',
|
||||||
|
itemCatalogDescription: 'Cari nama atau ID dari tabel game dan lihat ikonnya. Item berlabel didukung GaC dapat dikelola melalui alat inventaris; item lainnya hanya untuk dilihat.',
|
||||||
|
itemCatalogSearchPlaceholder: 'Cari nama atau ID item...', itemCatalogFilterLabel: 'Filter katalog item', itemCatalogFilterAll: 'Semua item',
|
||||||
|
itemCatalogFilterSupported: 'Didukung GaC', itemCatalogFilterItems: 'Item',
|
||||||
|
itemCatalogFilterLightCones: 'Light Cone', itemCatalogFilterRelics: 'Relik',
|
||||||
|
itemCatalogSupportedTag: 'Didukung GaC', itemCatalogBrowseOnlyTag: 'Hanya dilihat',
|
||||||
|
itemCatalogUnknownName: 'Item tanpa nama #{id}', itemCatalogNoImage: 'Tidak ada ikon di data game',
|
||||||
|
itemCatalogLoading: 'Memuat katalog item...', itemCatalogLoadFailed: 'Katalog item gagal dimuat'
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
tabItemCatalog: '📚 Catálogo', itemCatalogTitle: '📚 Catálogo de objetos',
|
||||||
|
itemCatalogDescription: 'Busca nombres o ID de las tablas del juego y consulta sus iconos. Los elementos compatibles con GaC se pueden gestionar en el inventario; los demás solo se pueden consultar.',
|
||||||
|
itemCatalogSearchPlaceholder: 'Buscar por nombre o ID...', itemCatalogFilterLabel: 'Filtrar catálogo de objetos', itemCatalogFilterAll: 'Todos',
|
||||||
|
itemCatalogFilterSupported: 'Compatible con GaC', itemCatalogFilterItems: 'Objetos',
|
||||||
|
itemCatalogFilterLightCones: 'Conos de luz', itemCatalogFilterRelics: 'Reliquias',
|
||||||
|
itemCatalogSupportedTag: 'Compatible con GaC', itemCatalogBrowseOnlyTag: 'Solo consulta',
|
||||||
|
itemCatalogUnknownName: 'Objeto sin nombre #{id}', itemCatalogNoImage: 'Sin icono en los datos del juego',
|
||||||
|
itemCatalogLoading: 'Cargando catálogo...', itemCatalogLoadFailed: 'No se pudo cargar el catálogo'
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
tabItemCatalog: '📚 Catalogue', itemCatalogTitle: '📚 Catalogue des objets',
|
||||||
|
itemCatalogDescription: 'Recherchez les noms ou ID des tables du jeu et consultez leurs icônes. Les éléments compatibles avec GaC peuvent être gérés dans l’inventaire ; les autres sont consultables uniquement.',
|
||||||
|
itemCatalogSearchPlaceholder: 'Rechercher par nom ou ID...', itemCatalogFilterLabel: 'Filtrer le catalogue', itemCatalogFilterAll: 'Tous les éléments',
|
||||||
|
itemCatalogFilterSupported: 'Compatible GaC', itemCatalogFilterItems: 'Objets',
|
||||||
|
itemCatalogFilterLightCones: 'Cônes de lumière', itemCatalogFilterRelics: 'Reliques',
|
||||||
|
itemCatalogSupportedTag: 'Compatible GaC', itemCatalogBrowseOnlyTag: 'Consultation seule',
|
||||||
|
itemCatalogUnknownName: 'Objet sans nom #{id}', itemCatalogNoImage: 'Aucune icône dans les données du jeu',
|
||||||
|
itemCatalogLoading: 'Chargement du catalogue...', itemCatalogLoadFailed: 'Impossible de charger le catalogue'
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
tabItemCatalog: '📚 Gegenstandsbuch', itemCatalogTitle: '📚 Gegenstandskatalog',
|
||||||
|
itemCatalogDescription: 'Suche nach Namen oder IDs aus den Spieltabellen und sieh dir die Symbole an. GaC-unterstützte Einträge lassen sich im Inventarwerkzeug verwalten; alle anderen dienen nur zum Nachschlagen.',
|
||||||
|
itemCatalogSearchPlaceholder: 'Nach Name oder Gegenstands-ID suchen...', itemCatalogFilterLabel: 'Katalog filtern', itemCatalogFilterAll: 'Alle Einträge',
|
||||||
|
itemCatalogFilterSupported: 'Von GaC unterstützt', itemCatalogFilterItems: 'Gegenstände',
|
||||||
|
itemCatalogFilterLightCones: 'Lichtkegel', itemCatalogFilterRelics: 'Relikte',
|
||||||
|
itemCatalogSupportedTag: 'Von GaC unterstützt', itemCatalogBrowseOnlyTag: 'Nur Nachschlagen',
|
||||||
|
itemCatalogUnknownName: 'Unbenannter Gegenstand #{id}', itemCatalogNoImage: 'Kein Symbol in den Spieldaten',
|
||||||
|
itemCatalogLoading: 'Gegenstandskatalog wird geladen...', itemCatalogLoadFailed: 'Gegenstandskatalog konnte nicht geladen werden'
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
tabItemCatalog: '📚 Каталог предметов', itemCatalogTitle: '📚 Каталог предметов',
|
||||||
|
itemCatalogDescription: 'Ищите названия или ID из игровых таблиц и просматривайте значки. Предметы с отметкой GaC можно менять в инструменте инвентаря; остальные доступны только для просмотра.',
|
||||||
|
itemCatalogSearchPlaceholder: 'Поиск по названию или ID...', itemCatalogFilterLabel: 'Фильтр каталога предметов', itemCatalogFilterAll: 'Все записи',
|
||||||
|
itemCatalogFilterSupported: 'Поддерживается GaC', itemCatalogFilterItems: 'Предметы',
|
||||||
|
itemCatalogFilterLightCones: 'Световые конусы', itemCatalogFilterRelics: 'Реликвии',
|
||||||
|
itemCatalogSupportedTag: 'Поддерживается GaC', itemCatalogBrowseOnlyTag: 'Только просмотр',
|
||||||
|
itemCatalogUnknownName: 'Предмет без названия #{id}', itemCatalogNoImage: 'В игровых данных нет значка',
|
||||||
|
itemCatalogLoading: 'Загрузка каталога предметов...', itemCatalogLoadFailed: 'Не удалось загрузить каталог предметов'
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
tabItemCatalog: '📚 Catálogo de itens', itemCatalogTitle: '📚 Catálogo de itens',
|
||||||
|
itemCatalogDescription: 'Pesquise nomes ou IDs das tabelas do jogo e veja os ícones. Itens marcados como compatíveis com GaC podem ser gerenciados no inventário; os demais são apenas para consulta.',
|
||||||
|
itemCatalogSearchPlaceholder: 'Pesquisar por nome ou ID...', itemCatalogFilterLabel: 'Filtrar catálogo de itens', itemCatalogFilterAll: 'Todos os itens',
|
||||||
|
itemCatalogFilterSupported: 'Compatível com GaC', itemCatalogFilterItems: 'Itens',
|
||||||
|
itemCatalogFilterLightCones: 'Cones de Luz', itemCatalogFilterRelics: 'Relíquias',
|
||||||
|
itemCatalogSupportedTag: 'Compatível com GaC', itemCatalogBrowseOnlyTag: 'Somente consulta',
|
||||||
|
itemCatalogUnknownName: 'Item sem nome #{id}', itemCatalogNoImage: 'Sem ícone nos dados do jogo',
|
||||||
|
itemCatalogLoading: 'Carregando catálogo de itens...', itemCatalogLoadFailed: 'Não foi possível carregar o catálogo de itens'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const [language, additions] of Object.entries(ITEM_CATALOG_TRANSLATIONS)) {
|
||||||
|
Object.assign(TRANSLATIONS[language], additions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ITEM_CATALOG_DETAIL_TRANSLATIONS = {
|
||||||
|
th: { itemCatalogDetailTitle: 'รายละเอียดไอเทม', itemCatalogItemId: 'Item ID {id}', itemCatalogSourceTable: 'ตารางต้นทาง', itemCatalogItemDescription: 'คำอธิบาย', itemCatalogLore: 'ข้อมูลเพิ่มเติม', itemCatalogNoDescription: 'ไม่มีคำอธิบายในข้อมูลเกม', itemCatalogNoDetails: 'ไม่มีรายละเอียดเพิ่มเติม', itemCatalogOpenDetails: 'ดูรายละเอียด {name}', itemCatalogQuantity: 'จำนวนที่เลือก', itemCatalogMaxQuantity: 'สูงสุด {count}', itemCatalogClose: 'ปิด', itemCatalogDecrease: 'ลดจำนวน', itemCatalogIncrease: 'เพิ่มจำนวน' },
|
||||||
|
en: { itemCatalogDetailTitle: 'Item Details', itemCatalogItemId: 'Item ID {id}', itemCatalogSourceTable: 'Source tables', itemCatalogItemDescription: 'Description', itemCatalogLore: 'Additional Details', itemCatalogNoDescription: 'No description in game data', itemCatalogNoDetails: 'No additional details', itemCatalogOpenDetails: 'View details for {name}', itemCatalogQuantity: 'Selected quantity', itemCatalogMaxQuantity: 'Maximum {count}', itemCatalogClose: 'Close', itemCatalogDecrease: 'Decrease quantity', itemCatalogIncrease: 'Increase quantity' },
|
||||||
|
'zh-CN': { itemCatalogDetailTitle: '物品详情', itemCatalogItemId: '物品 ID {id}', itemCatalogSourceTable: '来源表', itemCatalogItemDescription: '描述', itemCatalogLore: '补充信息', itemCatalogNoDescription: '游戏数据中没有描述', itemCatalogNoDetails: '没有更多信息', itemCatalogOpenDetails: '查看{name}详情', itemCatalogQuantity: '选择数量', itemCatalogMaxQuantity: '最多 {count}', itemCatalogClose: '关闭', itemCatalogDecrease: '减少数量', itemCatalogIncrease: '增加数量' },
|
||||||
|
'zh-TW': { itemCatalogDetailTitle: '物品詳情', itemCatalogItemId: '物品 ID {id}', itemCatalogSourceTable: '來源表', itemCatalogItemDescription: '說明', itemCatalogLore: '補充資訊', itemCatalogNoDescription: '遊戲資料中沒有說明', itemCatalogNoDetails: '沒有更多資訊', itemCatalogOpenDetails: '查看{name}詳情', itemCatalogQuantity: '選擇數量', itemCatalogMaxQuantity: '最多 {count}', itemCatalogClose: '關閉', itemCatalogDecrease: '減少數量', itemCatalogIncrease: '增加數量' },
|
||||||
|
ja: { itemCatalogDetailTitle: 'アイテム詳細', itemCatalogItemId: 'アイテム ID {id}', itemCatalogSourceTable: '参照テーブル', itemCatalogItemDescription: '説明', itemCatalogLore: '補足情報', itemCatalogNoDescription: 'ゲームデータに説明がありません', itemCatalogNoDetails: '追加情報はありません', itemCatalogOpenDetails: '{name}の詳細を見る', itemCatalogQuantity: '選択数', itemCatalogMaxQuantity: '上限 {count}', itemCatalogClose: '閉じる', itemCatalogDecrease: '数量を減らす', itemCatalogIncrease: '数量を増やす' },
|
||||||
|
ko: { itemCatalogDetailTitle: '아이템 상세 정보', itemCatalogItemId: '아이템 ID {id}', itemCatalogSourceTable: '출처 테이블', itemCatalogItemDescription: '설명', itemCatalogLore: '추가 정보', itemCatalogNoDescription: '게임 데이터에 설명이 없습니다', itemCatalogNoDetails: '추가 정보가 없습니다', itemCatalogOpenDetails: '{name} 상세 정보 보기', itemCatalogQuantity: '선택 수량', itemCatalogMaxQuantity: '최대 {count}', itemCatalogClose: '닫기', itemCatalogDecrease: '수량 줄이기', itemCatalogIncrease: '수량 늘리기' },
|
||||||
|
vi: { itemCatalogDetailTitle: 'Chi tiết vật phẩm', itemCatalogItemId: 'ID vật phẩm {id}', itemCatalogSourceTable: 'Bảng nguồn', itemCatalogItemDescription: 'Mô tả', itemCatalogLore: 'Thông tin thêm', itemCatalogNoDescription: 'Dữ liệu game không có mô tả', itemCatalogNoDetails: 'Không có thông tin thêm', itemCatalogOpenDetails: 'Xem chi tiết {name}', itemCatalogQuantity: 'Số lượng đã chọn', itemCatalogMaxQuantity: 'Tối đa {count}', itemCatalogClose: 'Đóng', itemCatalogDecrease: 'Giảm số lượng', itemCatalogIncrease: 'Tăng số lượng' },
|
||||||
|
id: { itemCatalogDetailTitle: 'Detail Item', itemCatalogItemId: 'ID item {id}', itemCatalogSourceTable: 'Tabel sumber', itemCatalogItemDescription: 'Deskripsi', itemCatalogLore: 'Info Tambahan', itemCatalogNoDescription: 'Tidak ada deskripsi di data game', itemCatalogNoDetails: 'Tidak ada info tambahan', itemCatalogOpenDetails: 'Lihat detail {name}', itemCatalogQuantity: 'Jumlah dipilih', itemCatalogMaxQuantity: 'Maksimal {count}', itemCatalogClose: 'Tutup', itemCatalogDecrease: 'Kurangi jumlah', itemCatalogIncrease: 'Tambah jumlah' },
|
||||||
|
es: { itemCatalogDetailTitle: 'Detalles del objeto', itemCatalogItemId: 'ID del objeto {id}', itemCatalogSourceTable: 'Tablas de origen', itemCatalogItemDescription: 'Descripción', itemCatalogLore: 'Información adicional', itemCatalogNoDescription: 'Sin descripción en los datos del juego', itemCatalogNoDetails: 'Sin más detalles', itemCatalogOpenDetails: 'Ver detalles de {name}', itemCatalogQuantity: 'Cantidad seleccionada', itemCatalogMaxQuantity: 'Máximo {count}', itemCatalogClose: 'Cerrar', itemCatalogDecrease: 'Reducir cantidad', itemCatalogIncrease: 'Aumentar cantidad' },
|
||||||
|
fr: { itemCatalogDetailTitle: 'Détails de l’objet', itemCatalogItemId: 'ID de l’objet {id}', itemCatalogSourceTable: 'Tables sources', itemCatalogItemDescription: 'Description', itemCatalogLore: 'Informations supplémentaires', itemCatalogNoDescription: 'Aucune description dans les données du jeu', itemCatalogNoDetails: 'Aucun autre détail', itemCatalogOpenDetails: 'Voir les détails de {name}', itemCatalogQuantity: 'Quantité choisie', itemCatalogMaxQuantity: 'Maximum {count}', itemCatalogClose: 'Fermer', itemCatalogDecrease: 'Diminuer la quantité', itemCatalogIncrease: 'Augmenter la quantité' },
|
||||||
|
de: { itemCatalogDetailTitle: 'Gegenstandsdetails', itemCatalogItemId: 'Gegenstands-ID {id}', itemCatalogSourceTable: 'Quelltabellen', itemCatalogItemDescription: 'Beschreibung', itemCatalogLore: 'Zusätzliche Details', itemCatalogNoDescription: 'Keine Beschreibung in den Spieldaten', itemCatalogNoDetails: 'Keine weiteren Details', itemCatalogOpenDetails: 'Details für {name} anzeigen', itemCatalogQuantity: 'Ausgewählte Menge', itemCatalogMaxQuantity: 'Maximum {count}', itemCatalogClose: 'Schließen', itemCatalogDecrease: 'Menge verringern', itemCatalogIncrease: 'Menge erhöhen' },
|
||||||
|
ru: { itemCatalogDetailTitle: 'Сведения о предмете', itemCatalogItemId: 'ID предмета {id}', itemCatalogSourceTable: 'Исходные таблицы', itemCatalogItemDescription: 'Описание', itemCatalogLore: 'Дополнительная информация', itemCatalogNoDescription: 'В игровых данных нет описания', itemCatalogNoDetails: 'Дополнительных сведений нет', itemCatalogOpenDetails: 'Подробнее: {name}', itemCatalogQuantity: 'Выбранное количество', itemCatalogMaxQuantity: 'Максимум: {count}', itemCatalogClose: 'Закрыть', itemCatalogDecrease: 'Уменьшить количество', itemCatalogIncrease: 'Увеличить количество' },
|
||||||
|
pt: { itemCatalogDetailTitle: 'Detalhes do item', itemCatalogItemId: 'ID do item {id}', itemCatalogSourceTable: 'Tabelas de origem', itemCatalogItemDescription: 'Descrição', itemCatalogLore: 'Informações adicionais', itemCatalogNoDescription: 'Sem descrição nos dados do jogo', itemCatalogNoDetails: 'Sem detalhes adicionais', itemCatalogOpenDetails: 'Ver detalhes de {name}', itemCatalogQuantity: 'Quantidade selecionada', itemCatalogMaxQuantity: 'Máximo {count}', itemCatalogClose: 'Fechar', itemCatalogDecrease: 'Diminuir quantidade', itemCatalogIncrease: 'Aumentar quantidade' }
|
||||||
|
};
|
||||||
|
for (const [language, additions] of Object.entries(ITEM_CATALOG_DETAIL_TRANSLATIONS)) {
|
||||||
|
Object.assign(TRANSLATIONS[language], additions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ITEM_CATALOG_INTERACTION_DESCRIPTIONS = {
|
||||||
|
th: 'ค้นหาชื่อหรือ ID แล้วคลิกรูปเพื่ออ่านรายละเอียดและเลือกจำนวน การเลือกจำนวนในหน้านี้ยังไม่เพิ่มไอเทม รายการที่ GaC รองรับจัดการได้ในแท็บกระเป๋า ส่วนรายการอื่นใช้ดูข้อมูลเท่านั้น',
|
||||||
|
en: 'Search by name or ID, then click an image to read details and select a quantity. Selecting a quantity here does not add the item. GaC-supported entries can be managed in the inventory tab; other entries are for browsing.',
|
||||||
|
'zh-CN': '按名称或 ID 搜索,然后点击图片查看详情并选择数量。在此选择数量不会添加物品。GaC 支持的条目可在背包页管理,其他条目仅供查看。',
|
||||||
|
'zh-TW': '依名稱或 ID 搜尋,再點擊圖片查看詳情並選擇數量。在此選擇數量不會新增物品。GaC 支援的項目可在背包頁管理,其他項目僅供瀏覽。',
|
||||||
|
ja: '名前または ID で検索し、画像をクリックすると詳細を確認して数量を選べます。この画面で数量を選んでもアイテムは追加されません。GaC 対応アイテムは所持品タブで管理でき、その他は閲覧のみです。',
|
||||||
|
ko: '이름 또는 ID를 검색한 뒤 이미지를 클릭하면 상세 정보를 확인하고 수량을 선택할 수 있습니다. 여기서 수량을 선택해도 아이템은 추가되지 않습니다. GaC 지원 항목은 인벤토리 탭에서 관리할 수 있고 나머지는 조회 전용입니다.',
|
||||||
|
vi: 'Tìm theo tên hoặc ID rồi nhấn vào hình để xem chi tiết và chọn số lượng. Việc chọn số lượng ở đây không thêm vật phẩm. Vật phẩm được GaC hỗ trợ có thể quản lý trong tab túi đồ; các mục khác chỉ để tra cứu.',
|
||||||
|
id: 'Cari berdasarkan nama atau ID, lalu klik gambar untuk melihat detail dan memilih jumlah. Memilih jumlah di sini tidak menambahkan item. Item yang didukung GaC dapat dikelola di tab inventaris; item lainnya hanya untuk dilihat.',
|
||||||
|
es: 'Busca por nombre o ID y haz clic en una imagen para consultar los detalles y elegir una cantidad. Elegir una cantidad aquí no añade el objeto. Los elementos compatibles con GaC se gestionan en la pestaña de inventario; los demás son solo de consulta.',
|
||||||
|
fr: 'Recherchez un nom ou un ID, puis cliquez sur une image pour consulter les détails et choisir une quantité. Ce choix n’ajoute pas l’objet. Les éléments compatibles avec GaC se gèrent dans l’inventaire ; les autres sont consultables uniquement.',
|
||||||
|
de: 'Suche nach Name oder ID und klicke auf ein Bild, um Details anzuzeigen und eine Menge auszuwählen. Dadurch wird der Gegenstand nicht hinzugefügt. GaC-unterstützte Einträge lassen sich im Inventar verwalten; die übrigen dienen nur zum Nachschlagen.',
|
||||||
|
ru: 'Найдите предмет по названию или ID, затем нажмите на изображение, чтобы посмотреть сведения и выбрать количество. Выбор количества здесь не добавляет предмет. Предметы с поддержкой GaC можно менять во вкладке инвентаря; остальные доступны только для просмотра.',
|
||||||
|
pt: 'Pesquise por nome ou ID e clique na imagem para ver os detalhes e escolher uma quantidade. Escolher a quantidade aqui não adiciona o item. Itens compatíveis com GaC podem ser gerenciados na aba de inventário; os demais servem apenas para consulta.'
|
||||||
|
};
|
||||||
|
for (const [language, description] of Object.entries(ITEM_CATALOG_INTERACTION_DESCRIPTIONS)) {
|
||||||
|
TRANSLATIONS[language].itemCatalogDescription = description;
|
||||||
|
}
|
||||||
|
|
||||||
let currentLang = 'th';
|
let currentLang = 'th';
|
||||||
if (typeof localStorage !== 'undefined') {
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
import { getCurrentLanguage, t, translatePath } from './i18n.js?v=2.8';
|
||||||
|
|
||||||
|
const CATALOG_URL = new URL('../assets/item-catalog.json?v=2', import.meta.url);
|
||||||
|
const PAGE_SIZE = 48;
|
||||||
|
let catalog = null;
|
||||||
|
let catalogLoading = null;
|
||||||
|
let page = 0;
|
||||||
|
let activeDetailId = null;
|
||||||
|
|
||||||
|
const elements = () => ({
|
||||||
|
tab: document.querySelector('.tab-btn[data-tab="tab-inventory"]'),
|
||||||
|
browseButton: document.getElementById('btn-inventory-catalog'),
|
||||||
|
manageButton: document.getElementById('btn-inventory-manage'),
|
||||||
|
catalogPanel: document.getElementById('item-catalog-panel'),
|
||||||
|
managePanel: document.getElementById('inventory-manage-panel'),
|
||||||
|
search: document.getElementById('item-catalog-search'),
|
||||||
|
filter: document.getElementById('item-catalog-filter'),
|
||||||
|
grid: document.getElementById('item-catalog-grid'),
|
||||||
|
status: document.getElementById('item-catalog-status'),
|
||||||
|
count: document.getElementById('item-catalog-count'),
|
||||||
|
pageLabel: document.getElementById('item-catalog-page-label'),
|
||||||
|
previous: document.getElementById('item-catalog-prev'),
|
||||||
|
next: document.getElementById('item-catalog-next'),
|
||||||
|
detail: document.getElementById('item-catalog-detail'),
|
||||||
|
detailArt: document.getElementById('item-catalog-detail-art'),
|
||||||
|
detailName: document.getElementById('item-catalog-detail-name'),
|
||||||
|
detailId: document.getElementById('item-catalog-detail-id'),
|
||||||
|
detailSource: document.getElementById('item-catalog-detail-source'),
|
||||||
|
detailMeta: document.getElementById('item-catalog-detail-meta'),
|
||||||
|
detailDescription: document.getElementById('item-catalog-detail-description'),
|
||||||
|
detailLore: document.getElementById('item-catalog-detail-lore'),
|
||||||
|
detailLoreSection: document.getElementById('item-catalog-detail-lore-section'),
|
||||||
|
amount: document.getElementById('item-catalog-detail-amount'),
|
||||||
|
amountLimit: document.getElementById('item-catalog-amount-limit')
|
||||||
|
});
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value ?? '').replace(/[&<>"']/g, char => ({
|
||||||
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||||
|
})[char]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function plainGameText(value) {
|
||||||
|
return String(value ?? '').replace(/<[^>]*>/g, '').replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n').replace(/\\t/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayName(item) {
|
||||||
|
const names = item.names || {};
|
||||||
|
return plainGameText(names[getCurrentLanguage()] || names.en || t('itemCatalogUnknownName', { id: item.id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function assetUrl(path, base) {
|
||||||
|
if (typeof path !== 'string' || !/^(icon|image)\/[A-Za-z0-9_./-]+\.png$/.test(path) || path.includes('..')) return '';
|
||||||
|
try { return new URL(path, base).href; } catch { return ''; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemCategoryLabel(category) {
|
||||||
|
const key = {
|
||||||
|
item: 'itemCatalogFilterItems',
|
||||||
|
lightcone: 'itemCatalogFilterLightCones',
|
||||||
|
relic: 'itemCatalogFilterRelics'
|
||||||
|
}[category];
|
||||||
|
return key ? t(key) : t('itemCatalogFilterItems');
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleItems() {
|
||||||
|
if (!catalog) return [];
|
||||||
|
const query = String(elements().search?.value || '').trim().toLocaleLowerCase();
|
||||||
|
const category = elements().filter?.value || 'all';
|
||||||
|
return catalog.items.filter(item => {
|
||||||
|
if (category === 'supported' && !item.supported) return false;
|
||||||
|
if (category !== 'all' && category !== 'supported' && item.category !== category) return false;
|
||||||
|
if (!query) return true;
|
||||||
|
if (String(item.id).includes(query)) return true;
|
||||||
|
return Object.values(item.names || {}).some(name => plainGameText(name).toLocaleLowerCase().includes(query));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function imageMarkup(item, source) {
|
||||||
|
const src = assetUrl(item.icon, source.assetBaseUrl);
|
||||||
|
if (!src) {
|
||||||
|
return '<button type="button" class="item-catalog-art-button" data-item-catalog-id="' + item.id + '" aria-label="' + escapeHtml(t('itemCatalogOpenDetails', { name: displayName(item) })) + '"><span class="item-catalog-art item-catalog-art-missing" title="' + escapeHtml(t('itemCatalogNoImage')) + '"><span aria-hidden="true">✦</span><small>' + escapeHtml(t('itemCatalogNoImage')) + '</small></span></button>';
|
||||||
|
}
|
||||||
|
const fallback = assetUrl(item.icon, source.assetFallbackBaseUrl);
|
||||||
|
return '<button type="button" class="item-catalog-art-button" data-item-catalog-id="' + item.id + '" aria-label="' + escapeHtml(t('itemCatalogOpenDetails', { name: displayName(item) })) + '"><span class="item-catalog-art"><img class="item-catalog-image" src="' + escapeHtml(src) + '" data-fallback="' + escapeHtml(fallback) + '" alt="" loading="lazy" decoding="async"></span></button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function localizedItemText(item, field) {
|
||||||
|
const values = item[field] || {};
|
||||||
|
return values[getCurrentLanguage()] || values.en || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openItemDetails(itemId) {
|
||||||
|
const el = elements();
|
||||||
|
const item = catalog?.items.find(row => row.id === Number(itemId));
|
||||||
|
if (!item || !el.detail) return;
|
||||||
|
activeDetailId = item.id;
|
||||||
|
const name = displayName(item);
|
||||||
|
const src = assetUrl(item.icon, catalog.source.assetBaseUrl);
|
||||||
|
const fallback = assetUrl(item.icon, catalog.source.assetFallbackBaseUrl);
|
||||||
|
const description = localizedItemText(item, 'descriptions');
|
||||||
|
const lore = localizedItemText(item, 'backgroundDescriptions');
|
||||||
|
const rarity = Number(item.rarity);
|
||||||
|
const rarityText = Number.isInteger(rarity) && rarity > 0 && rarity <= 5 ? '★'.repeat(rarity) : '';
|
||||||
|
const category = itemCategoryLabel(item.category);
|
||||||
|
const type = [item.type, item.subType].filter((value, index, values) => value && values.indexOf(value) === index).join(' · ');
|
||||||
|
const path = item.path ? translatePath(item.path) : '';
|
||||||
|
const slot = item.slot ? translatePath(item.slot) : '';
|
||||||
|
const meta = [category, type, slot, path, rarityText].filter(Boolean).join(' · ');
|
||||||
|
|
||||||
|
el.detailName.textContent = name;
|
||||||
|
el.detailId.textContent = t('itemCatalogItemId', { id: item.id });
|
||||||
|
el.detailSource.textContent = (item.sourceTables || []).map(path => path.split('/').pop()).join(', ') || t('itemCatalogNoDetails');
|
||||||
|
el.detailMeta.textContent = meta || t('itemCatalogNoDetails');
|
||||||
|
el.detailMeta.dataset.rarity = rarityText ? String(rarity) : '';
|
||||||
|
el.detailDescription.textContent = plainGameText(description || lore || t('itemCatalogNoDescription'));
|
||||||
|
el.detailLoreSection.hidden = !description || !lore || description === lore;
|
||||||
|
el.detailLore.textContent = el.detailLoreSection.hidden ? '' : plainGameText(lore);
|
||||||
|
if (src) {
|
||||||
|
el.detailArt.innerHTML = '<img src="' + escapeHtml(src) + '" data-fallback="' + escapeHtml(fallback) + '" alt="' + escapeHtml(name) + '">';
|
||||||
|
const image = el.detailArt.querySelector('img');
|
||||||
|
image.addEventListener('error', () => {
|
||||||
|
if (image.dataset.fallback && !image.dataset.fallbackTried) {
|
||||||
|
image.dataset.fallbackTried = 'true';
|
||||||
|
image.src = image.dataset.fallback;
|
||||||
|
} else {
|
||||||
|
el.detailArt.textContent = t('itemCatalogNoImage');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
el.detailArt.textContent = t('itemCatalogNoImage');
|
||||||
|
}
|
||||||
|
const limit = Number(item.pileLimit);
|
||||||
|
const hasLimit = Number.isSafeInteger(limit) && limit > 0;
|
||||||
|
el.amount.min = '1';
|
||||||
|
if (hasLimit) {
|
||||||
|
el.amount.max = String(limit);
|
||||||
|
el.amountLimit.textContent = t('itemCatalogMaxQuantity', { count: limit.toLocaleString() });
|
||||||
|
} else {
|
||||||
|
el.amount.removeAttribute('max');
|
||||||
|
el.amountLimit.textContent = '';
|
||||||
|
}
|
||||||
|
el.amount.value = '1';
|
||||||
|
if (!el.detail.open) el.detail.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectedAmount(delta = 0) {
|
||||||
|
const input = elements().amount;
|
||||||
|
if (!input) return;
|
||||||
|
const max = Number(input.max);
|
||||||
|
const hasMax = Number.isSafeInteger(max) && max > 0;
|
||||||
|
const current = Number.parseInt(input.value, 10);
|
||||||
|
const next = Math.max(1, (Number.isFinite(current) ? current : 1) + delta);
|
||||||
|
input.value = String(hasMax ? Math.min(max, next) : next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCatalog() {
|
||||||
|
const el = elements();
|
||||||
|
if (!el.grid || !catalog) return;
|
||||||
|
el.filter.setAttribute('aria-label', t('itemCatalogFilterLabel'));
|
||||||
|
const filtered = visibleItems();
|
||||||
|
const pages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
||||||
|
page = Math.min(page, pages - 1);
|
||||||
|
const start = page * PAGE_SIZE;
|
||||||
|
const rows = filtered.slice(start, start + PAGE_SIZE);
|
||||||
|
|
||||||
|
el.count.textContent = t('inventoryItemTotal', { count: filtered.length });
|
||||||
|
el.pageLabel.textContent = t('inventoryPage', { page: page + 1, pages, count: filtered.length });
|
||||||
|
el.previous.disabled = page === 0;
|
||||||
|
el.next.disabled = page >= pages - 1;
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
el.grid.innerHTML = '<div class="item-catalog-empty">' + escapeHtml(t('inventoryNoMatch')) + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.grid.innerHTML = rows.map(item => {
|
||||||
|
const name = displayName(item);
|
||||||
|
const category = itemCategoryLabel(item.category);
|
||||||
|
const subtype = item.slot || item.subType || item.type || '';
|
||||||
|
const path = item.path ? translatePath(item.path) : '';
|
||||||
|
const rarity = Number(item.rarity);
|
||||||
|
const rarityText = Number.isInteger(rarity) && rarity > 0 && rarity <= 5 ? '★'.repeat(rarity) : '';
|
||||||
|
const details = [category, subtype, path, rarityText].filter(Boolean).join(' · ');
|
||||||
|
const supportText = item.supported ? t('itemCatalogSupportedTag') : t('itemCatalogBrowseOnlyTag');
|
||||||
|
return '<article class="item-catalog-card">' +
|
||||||
|
imageMarkup(item, catalog.source) +
|
||||||
|
'<div class="item-catalog-info">' +
|
||||||
|
'<div class="item-catalog-name" title="' + escapeHtml(name) + '">' + escapeHtml(name) + '</div>' +
|
||||||
|
'<div class="item-catalog-id">ID ' + item.id + '</div>' +
|
||||||
|
'<div class="item-catalog-details" title="' + escapeHtml(details) + '">' + escapeHtml(details) + '</div>' +
|
||||||
|
'<span class="item-catalog-support ' + (item.supported ? 'is-supported' : 'is-browse-only') + '">' + escapeHtml(supportText) + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</article>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
el.grid.querySelectorAll('.item-catalog-image').forEach(image => {
|
||||||
|
image.addEventListener('error', () => {
|
||||||
|
if (image.dataset.fallback && !image.dataset.fallbackTried) {
|
||||||
|
image.dataset.fallbackTried = 'true';
|
||||||
|
image.src = image.dataset.fallback;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const placeholder = document.createElement('div');
|
||||||
|
placeholder.className = 'item-catalog-art item-catalog-art-missing';
|
||||||
|
placeholder.title = t('itemCatalogNoImage');
|
||||||
|
const mark = document.createElement('span');
|
||||||
|
mark.setAttribute('aria-hidden', 'true');
|
||||||
|
mark.textContent = '✦';
|
||||||
|
const label = document.createElement('small');
|
||||||
|
label.textContent = t('itemCatalogNoImage');
|
||||||
|
placeholder.append(mark, label);
|
||||||
|
image.parentElement.replaceWith(placeholder);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
el.grid.querySelectorAll('[data-item-catalog-id]').forEach(button => {
|
||||||
|
button.addEventListener('click', () => openItemDetails(button.dataset.itemCatalogId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCatalog() {
|
||||||
|
const el = elements();
|
||||||
|
if (catalog) return catalog;
|
||||||
|
if (catalogLoading) return catalogLoading;
|
||||||
|
el.status.textContent = t('itemCatalogLoading');
|
||||||
|
catalogLoading = fetch(CATALOG_URL).then(response => {
|
||||||
|
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||||
|
return response.json();
|
||||||
|
}).then(data => {
|
||||||
|
if (!Array.isArray(data.items) || !data.source?.assetBaseUrl) throw new Error('Invalid catalog data');
|
||||||
|
catalog = data;
|
||||||
|
el.status.textContent = t('inventoryItemTotal', { count: data.counts.total });
|
||||||
|
renderCatalog();
|
||||||
|
return catalog;
|
||||||
|
}).catch(error => {
|
||||||
|
el.status.textContent = t('itemCatalogLoadFailed') + ': ' + error.message;
|
||||||
|
el.grid.innerHTML = '<div class="item-catalog-empty">' + escapeHtml(t('itemCatalogLoadFailed')) + '</div>';
|
||||||
|
catalogLoading = null;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
return catalogLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInventoryView(view) {
|
||||||
|
const el = elements();
|
||||||
|
const showCatalog = view === 'catalog';
|
||||||
|
el.catalogPanel.hidden = !showCatalog;
|
||||||
|
el.managePanel.hidden = showCatalog;
|
||||||
|
el.browseButton.classList.toggle('active', showCatalog);
|
||||||
|
el.manageButton.classList.toggle('active', !showCatalog);
|
||||||
|
el.browseButton.setAttribute('aria-selected', String(showCatalog));
|
||||||
|
el.manageButton.setAttribute('aria-selected', String(!showCatalog));
|
||||||
|
if (showCatalog) loadCatalog().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initializeItemCatalog() {
|
||||||
|
const el = elements();
|
||||||
|
el.search?.addEventListener('input', () => { page = 0; renderCatalog(); });
|
||||||
|
el.filter?.addEventListener('change', () => { page = 0; renderCatalog(); });
|
||||||
|
el.previous?.addEventListener('click', () => { page = Math.max(0, page - 1); renderCatalog(); });
|
||||||
|
el.next?.addEventListener('click', () => { page += 1; renderCatalog(); });
|
||||||
|
document.getElementById('item-catalog-detail-close')?.addEventListener('click', () => el.detail?.close());
|
||||||
|
document.getElementById('item-catalog-detail-done')?.addEventListener('click', () => el.detail?.close());
|
||||||
|
el.detail?.addEventListener('close', () => { activeDetailId = null; });
|
||||||
|
document.getElementById('item-catalog-amount-minus')?.addEventListener('click', () => updateSelectedAmount(-1));
|
||||||
|
document.getElementById('item-catalog-amount-plus')?.addEventListener('click', () => updateSelectedAmount(1));
|
||||||
|
el.amount?.addEventListener('change', () => {
|
||||||
|
const value = Number.parseInt(el.amount.value, 10);
|
||||||
|
const max = Number(el.amount.max);
|
||||||
|
el.amount.value = String(Math.max(1, Number.isFinite(value) ? (Number.isSafeInteger(max) && max > 0 ? Math.min(max, value) : value) : 1));
|
||||||
|
});
|
||||||
|
el.detail?.addEventListener('click', event => {
|
||||||
|
if (event.target === el.detail) el.detail.close();
|
||||||
|
});
|
||||||
|
el.tab?.addEventListener('click', () => { if (!el.catalogPanel.hidden) loadCatalog().catch(() => {}); });
|
||||||
|
el.browseButton?.addEventListener('click', () => setInventoryView('catalog'));
|
||||||
|
el.manageButton?.addEventListener('click', () => setInventoryView('manage'));
|
||||||
|
document.getElementById('lang-select')?.addEventListener('change', () => {
|
||||||
|
if (catalog) renderCatalog();
|
||||||
|
else if (el.status) el.status.textContent = t('itemCatalogLoading');
|
||||||
|
if (catalog && el.detail?.open && activeDetailId !== null) openItemDetails(activeDetailId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { getCurrentLanguage } from './i18n.js?v=2.8';
|
||||||
|
|
||||||
|
const strings = {
|
||||||
|
en: {
|
||||||
|
button: 'Unstuck',
|
||||||
|
description: 'Clear a stuck loading spinner without restarting the game. Keeps saved CW progress.',
|
||||||
|
offline: 'Connect to the GaC server first.',
|
||||||
|
queued: 'Recovery queued. Waiting for the game…',
|
||||||
|
completed: 'The game cleared its network wait. Try closing or retrying the page.',
|
||||||
|
delayed: 'No acknowledgement from the game yet. Recovery is still queued; the game must be connected.',
|
||||||
|
failed: 'The game could not clear the wait.',
|
||||||
|
unsupported: 'This server does not support Unstuck yet.'
|
||||||
|
},
|
||||||
|
th: {
|
||||||
|
button: 'Unstuck / แก้ค้าง',
|
||||||
|
description: 'ปลดตัวหมุนค้างโดยไม่ต้องออกเกม และเก็บเซฟรอบ CW ไว้',
|
||||||
|
offline: 'เชื่อมต่อเซิร์ฟเวอร์ GaC ก่อน',
|
||||||
|
queued: 'ส่งคำสั่งแล้ว กำลังรอเกมตอบกลับ…',
|
||||||
|
completed: 'เกมปลดการรอแพ็กเก็ตแล้ว ลองปิดหน้าต่างหรือกดใหม่ได้',
|
||||||
|
delayed: 'เกมยังไม่ตอบรับ คำสั่งยังอยู่ในคิวและต้องเชื่อมต่อเกมไว้',
|
||||||
|
failed: 'เกมปลดการรอไม่สำเร็จ',
|
||||||
|
unsupported: 'เซิร์ฟเวอร์นี้ยังไม่รองรับ Unstuck'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function initializeUnstuck({ apiCall, isConnected, showToast }) {
|
||||||
|
const button = document.getElementById('btn-client-unstuck');
|
||||||
|
const description = document.getElementById('client-unstuck-description');
|
||||||
|
const status = document.getElementById('client-unstuck-status');
|
||||||
|
if (!button) return;
|
||||||
|
const text = () => strings[getCurrentLanguage()] || strings.en;
|
||||||
|
const translate = () => {
|
||||||
|
button.textContent = text().button;
|
||||||
|
if (description) description.textContent = text().description;
|
||||||
|
};
|
||||||
|
translate();
|
||||||
|
document.getElementById('lang-select')?.addEventListener('change', translate);
|
||||||
|
button.addEventListener('click', async () => {
|
||||||
|
if (!isConnected()) { showToast(text().offline, 'error'); return; }
|
||||||
|
button.disabled = true;
|
||||||
|
try {
|
||||||
|
const result = await apiCall('/api/client/unstuck', 'POST', {});
|
||||||
|
if (!result?.request_id || result.state !== 'queued') throw new Error(text().unsupported);
|
||||||
|
if (status) status.textContent = text().queued;
|
||||||
|
for (let attempt = 0; attempt < 16; attempt++) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 750));
|
||||||
|
const acknowledgement = await apiCall('/api/client/unstuck');
|
||||||
|
if (acknowledgement?.request_id !== result.request_id) continue;
|
||||||
|
if (acknowledgement.state === 'completed') {
|
||||||
|
if (status) status.textContent = text().completed;
|
||||||
|
showToast(text().completed, 'success');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (acknowledgement.state === 'failed') {
|
||||||
|
throw new Error(acknowledgement.error || text().failed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (status) status.textContent = text().delayed;
|
||||||
|
} catch (error) {
|
||||||
|
if (status) status.textContent = error.message;
|
||||||
|
showToast(error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Only the static web application is published; repository tools and local
|
||||||
|
// configuration are not copied to the public deployment.
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
const output = path.join(root, 'dist');
|
||||||
|
fs.mkdirSync(output, { recursive: true });
|
||||||
|
for (const name of ['index.html', 'version.json', 'assets', 'css', 'js']) {
|
||||||
|
const source = path.join(root, name);
|
||||||
|
if (!fs.existsSync(source)) throw new Error(`Missing static application input: ${name}`);
|
||||||
|
fs.cpSync(source, path.join(output, name), { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
console.log('Static application prepared for Vercel.');
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
const path = require('node:path');
|
||||||
|
const source = fs.readFileSync(path.join(__dirname, '../js/app.js'), 'utf8');
|
||||||
|
const start = source.indexOf('function orderBattleReportAvatars(');
|
||||||
|
const end = source.indexOf('\nfunction renderDamageAnalyst(', start);
|
||||||
|
const context = { state: { statusData: { lineups: { 0: 30, 1: 20, 2: 10 } } } };
|
||||||
|
vm.createContext(context);
|
||||||
|
vm.runInContext(source.slice(start, end), context);
|
||||||
|
|
||||||
|
test('battle slots override a different overworld lineup without mutating the report', () => {
|
||||||
|
const avatars = [{ avatar_id: 20, slot: 2 }, { avatar_id: 30, slot: 0 }, { avatar_id: 10, slot: 1 }];
|
||||||
|
const result = context.orderBattleReportAvatars(avatars);
|
||||||
|
assert.deepEqual(Array.from(result, a => a.avatar_id), [30, 10, 20]);
|
||||||
|
assert.deepEqual(avatars.map(a => a.avatar_id), [20, 30, 10]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('historical reports keep recorded order when battle slots are absent', () => {
|
||||||
|
const result = context.orderBattleReportAvatars([{ avatar_id: 10 }, { avatar_id: 30 }, { avatar_id: 20 }]);
|
||||||
|
assert.deepEqual(Array.from(result, a => a.avatar_id), [10, 30, 20]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Other includes unclassified and missing category damage exactly once', () => {
|
||||||
|
const result = context.damageBreakdownForAvatar({ total_damage: 1000, total_break_damage: 100,
|
||||||
|
attack_type_damage: { Normal: 100, BPSkill: 200, Ultra: 100, Insert: 50, TrueDamage: 150 } });
|
||||||
|
assert.equal(result.other, 500);
|
||||||
|
assert.equal(result.normal + result.skill + result.ultimate + result.breakDamage + result.other, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Other never becomes negative when the native breakdown overlaps', () => {
|
||||||
|
assert.equal(context.damageBreakdownForAvatar({total_damage: 10, attack_type_damage: {Normal: 20}}).other, 0);
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const catalogPath = path.join(__dirname, '..', 'assets', 'item-catalog.json');
|
||||||
|
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
||||||
|
|
||||||
|
test('item book matches its pinned game and image catalog revisions', () => {
|
||||||
|
assert.equal(catalog.source.gameRevision, '312b4597691e0a29cf9828a15b592757dba86949');
|
||||||
|
assert.equal(catalog.source.assetRevision, 'dbe8cdfcfb0bf657b9fe1cf92d5537f118ccb487');
|
||||||
|
assert.equal(catalog.items.length, catalog.counts.total);
|
||||||
|
assert.equal(new Set(catalog.items.map(item => item.id)).size, catalog.items.length);
|
||||||
|
assert.equal(catalog.items.filter(item => item.supported).length, catalog.counts.supported);
|
||||||
|
assert.equal(catalog.items.filter(item => item.icon).length, catalog.counts.withImages);
|
||||||
|
assert.equal(catalog.items.filter(item => !item.icon).length, catalog.counts.withoutConfiguredImage);
|
||||||
|
assert.equal(catalog.counts.total, 5414);
|
||||||
|
assert.equal(catalog.counts.definitionRows, 5472);
|
||||||
|
assert.equal(catalog.counts.candidateTables, 26);
|
||||||
|
assert.equal(catalog.counts.definitionTables, 18);
|
||||||
|
assert.equal(catalog.counts.supported, 3550);
|
||||||
|
assert.equal(catalog.counts.withImages, 5261);
|
||||||
|
assert.equal(catalog.source.gameTables.length, catalog.counts.definitionTables);
|
||||||
|
assert.ok(catalog.items.some(item => item.sourceTables.length > 1), 'overlapping IDs retain every source table');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('catalog image paths are safe and all visible catalog strings are translated', async () => {
|
||||||
|
for (const item of catalog.items) {
|
||||||
|
assert.ok(['item', 'lightcone', 'relic'].includes(item.category), `invalid category for ${item.id}`);
|
||||||
|
assert.ok(item.sourceTables.length > 0, `missing source table for ${item.id}`);
|
||||||
|
assert.deepEqual(Object.keys(item.names).sort(), [...catalog.source.languages].sort());
|
||||||
|
assert.ok(item.gameIconPaths.every(path => typeof path === 'string' && path.length > 0));
|
||||||
|
if (item.icon) {
|
||||||
|
assert.match(item.icon, /^(icon|image)\/[A-Za-z0-9_./-]+\.png$/);
|
||||||
|
assert.ok(!item.icon.includes('..'), `unsafe icon path for ${item.id}`);
|
||||||
|
}
|
||||||
|
for (const textMapField of ['descriptions', 'backgroundDescriptions']) {
|
||||||
|
assert.ok(Object.keys(item[textMapField]).every(language => catalog.source.languages.includes(language)));
|
||||||
|
assert.ok(Object.values(item[textMapField]).every(value => typeof value === 'string'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { TRANSLATIONS } = await import('../js/i18n.js');
|
||||||
|
const languages = catalog.source.languages;
|
||||||
|
assert.deepEqual(Object.keys(TRANSLATIONS).sort(), [...languages].sort());
|
||||||
|
const catalogKeys = Object.keys(TRANSLATIONS.en).filter(key => key.startsWith('itemCatalog'));
|
||||||
|
assert.ok(catalogKeys.length > 0);
|
||||||
|
for (const language of languages) {
|
||||||
|
for (const key of catalogKeys) {
|
||||||
|
assert.ok(TRANSLATIONS[language][key], `missing ${key} translation for ${language}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('item detail dialog supports inspection and local quantity selection without a spawn action', () => {
|
||||||
|
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||||
|
const dialog = html.match(/<dialog id="item-catalog-detail"[\s\S]*?<\/dialog>/)?.[0];
|
||||||
|
assert.ok(dialog, 'item detail dialog exists outside the hidden tab content');
|
||||||
|
assert.match(dialog, /<input[^>]*id="item-catalog-detail-amount"[^>]*type="number"|<input[^>]*type="number"[^>]*id="item-catalog-detail-amount"/);
|
||||||
|
assert.match(dialog, /id="item-catalog-detail-description"/);
|
||||||
|
assert.match(dialog, /id="item-catalog-detail-source"/);
|
||||||
|
assert.doesNotMatch(dialog, /spawn|summon|เสก|เพิ่มเข้ากระเป๋า/i);
|
||||||
|
|
||||||
|
const moduleSource = fs.readFileSync(path.join(__dirname, '..', 'js', 'item-catalog.js'), 'utf8');
|
||||||
|
assert.match(moduleSource, /showModal\(\)/);
|
||||||
|
assert.match(moduleSource, /item\.pileLimit/);
|
||||||
|
assert.doesNotMatch(moduleSource, /apiRequest|method\s*:\s*['"]POST/i);
|
||||||
|
});
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build the read-only item encyclopedia from pinned game and image data."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
GAME_PROJECT = "Dimbreath/turnbasedgamedata"
|
||||||
|
ASSET_PROJECT = "Mar-7th/StarRailRes"
|
||||||
|
ASSET_LANGUAGES = {
|
||||||
|
"th": "th", "en": "en", "zh-CN": "cn", "zh-TW": "cht", "ja": "jp",
|
||||||
|
"ko": "kr", "vi": "vi", "id": "id", "es": "es", "fr": "fr",
|
||||||
|
"de": "de", "ru": "ru", "pt": "pt",
|
||||||
|
}
|
||||||
|
TEXTMAP_TOKENS = {
|
||||||
|
"th": ("TH",), "en": ("EN",), "zh-CN": ("CHS",), "zh-TW": ("CHT",),
|
||||||
|
"ja": ("JP",), "ko": ("KR",), "vi": ("VI",), "id": ("ID",),
|
||||||
|
"es": ("ES",), "fr": ("FR",), "de": ("DE",), "ru": ("RU",), "pt": ("PT",),
|
||||||
|
}
|
||||||
|
TEXT_FIELDS = ("ItemName", "ItemDesc", "ItemBGDesc")
|
||||||
|
ICON_FIELDS = ("ItemIconPath", "ItemCurrencyIconPath", "ItemAvatarIconPath", "ItemFigureIconPath")
|
||||||
|
|
||||||
|
|
||||||
|
def request_bytes(url: str, attempts: int = 4) -> bytes:
|
||||||
|
request = urllib.request.Request(url, headers={"User-Agent": "GaC-Srtool-item-catalog/1.0"})
|
||||||
|
last_error = None
|
||||||
|
for attempt in range(attempts):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=90) as response:
|
||||||
|
return response.read()
|
||||||
|
except (OSError, urllib.error.URLError) as error:
|
||||||
|
last_error = error
|
||||||
|
if attempt + 1 < attempts:
|
||||||
|
time.sleep(1.5 * (attempt + 1))
|
||||||
|
raise RuntimeError(f"Could not download {url}: {last_error}")
|
||||||
|
|
||||||
|
|
||||||
|
def request_json(url: str):
|
||||||
|
return json.loads(request_bytes(url))
|
||||||
|
|
||||||
|
|
||||||
|
def cache_path(cache_dir: Path, key: str) -> Path:
|
||||||
|
return cache_dir / (re.sub(r"[^A-Za-z0-9_.-]+", "_", key) + ".json")
|
||||||
|
|
||||||
|
|
||||||
|
def cached_json(url: str, cache_dir: Path, key: str):
|
||||||
|
path = cache_path(cache_dir, key)
|
||||||
|
if not path.exists():
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
payload = request_bytes(url)
|
||||||
|
temporary = path.with_suffix(".tmp")
|
||||||
|
temporary.write_bytes(payload)
|
||||||
|
temporary.replace(path)
|
||||||
|
return json.loads(path.read_bytes())
|
||||||
|
|
||||||
|
|
||||||
|
def gitlab_api(project: str, endpoint: str) -> str:
|
||||||
|
return f"https://gitlab.com/api/v4/projects/{urllib.parse.quote(project, safe='')}/{endpoint}"
|
||||||
|
|
||||||
|
|
||||||
|
def gitlab_raw(project: str, revision: str, file_path: str) -> str:
|
||||||
|
encoded = urllib.parse.quote(file_path, safe="")
|
||||||
|
return gitlab_api(project, f"repository/files/{encoded}/raw?ref={revision}")
|
||||||
|
|
||||||
|
|
||||||
|
def discover_files(project: str, revision: str, directory: str) -> list[str]:
|
||||||
|
names: list[str] = []
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
query = urllib.parse.urlencode({
|
||||||
|
"ref": revision, "path": directory, "recursive": "true", "per_page": 100, "page": page,
|
||||||
|
})
|
||||||
|
response = request_json(gitlab_api(project, f"repository/tree?{query}"))
|
||||||
|
names.extend(entry["path"] for entry in response if entry.get("type") == "blob")
|
||||||
|
if len(response) < 100:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
if page > 100:
|
||||||
|
raise RuntimeError(f"Unexpectedly large file listing under {directory}")
|
||||||
|
return sorted(names)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_game_json_files(paths: list[str], revision: str, cache_dir: Path) -> dict[str, object]:
|
||||||
|
result: dict[str, object] = {}
|
||||||
|
|
||||||
|
def load_one(path: str):
|
||||||
|
return path, cached_json(gitlab_raw(GAME_PROJECT, revision, path), cache_dir, f"game_{path}")
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=8) as pool:
|
||||||
|
futures = [pool.submit(load_one, path) for path in paths]
|
||||||
|
for future in as_completed(futures):
|
||||||
|
path, value = future.result()
|
||||||
|
result[path] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rarity(value, index_row=None) -> int | None:
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value if 1 <= value <= 5 else None
|
||||||
|
if index_row and isinstance(index_row.get("rarity"), int):
|
||||||
|
rarity = index_row["rarity"]
|
||||||
|
return rarity if 1 <= rarity <= 5 else None
|
||||||
|
rarity = str(value or "").lower()
|
||||||
|
for label, number in (("superrare", 5), ("veryrare", 5), ("rare", 4), ("notnormal", 3), ("normal", 2), ("low", 1)):
|
||||||
|
if label in rarity:
|
||||||
|
return number
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def item_hash(row: dict, field: str):
|
||||||
|
value = row.get(field)
|
||||||
|
if isinstance(value, dict):
|
||||||
|
value = value.get("Hash")
|
||||||
|
if value is None or value == "":
|
||||||
|
return None
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def row_image_paths(row: dict) -> list[str]:
|
||||||
|
values = []
|
||||||
|
for field in ICON_FIELDS:
|
||||||
|
value = row.get(field)
|
||||||
|
if isinstance(value, str) and value and value not in values:
|
||||||
|
values.append(value.replace("\\", "/").lstrip("/"))
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def rows_preference(path: str) -> tuple[int, str]:
|
||||||
|
base = Path(path).name
|
||||||
|
if base == "ItemConfig.json":
|
||||||
|
return 0, path
|
||||||
|
if base in ("ItemConfigEquipment.json", "ItemConfigRelic.json"):
|
||||||
|
return 1, path
|
||||||
|
return 2, path
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_source_icon(game_paths: list[str], category: str, item_id: int, indexes: dict, asset_files: set[str]) -> str | None:
|
||||||
|
index_group = {"item": "items", "lightcone": "light_cones", "relic": "relics"}[category]
|
||||||
|
index_row = indexes.get("en", {}).get(index_group, {}).get(str(item_id))
|
||||||
|
if isinstance(index_row, dict):
|
||||||
|
icon = index_row.get("icon")
|
||||||
|
if isinstance(icon, str) and icon in asset_files:
|
||||||
|
return icon
|
||||||
|
|
||||||
|
for source_path in game_paths:
|
||||||
|
candidates: list[str] = []
|
||||||
|
if source_path.startswith("SpriteOutput/ItemIcon/LightConeIcons/"):
|
||||||
|
candidates.append(f"icon/light_cone/{item_id}.png")
|
||||||
|
if source_path.startswith("SpriteOutput/ItemIcon/RelicIcons/"):
|
||||||
|
relic_row = indexes.get("en", {}).get("relics", {}).get(str(item_id))
|
||||||
|
if isinstance(relic_row, dict):
|
||||||
|
candidates.append(relic_row.get("icon", ""))
|
||||||
|
if source_path.startswith("SpriteOutput/ItemIcon/"):
|
||||||
|
suffix = source_path.removeprefix("SpriteOutput/ItemIcon/")
|
||||||
|
candidates.extend((f"icon/item/{suffix}", f"icon/{suffix}"))
|
||||||
|
if source_path.startswith("SpriteOutput/AvatarIcon/Avatar/"):
|
||||||
|
suffix = source_path.removeprefix("SpriteOutput/AvatarIcon/Avatar/")
|
||||||
|
candidates.extend((f"image/character_portrait/{suffix}", f"icon/avatar/{suffix}"))
|
||||||
|
if source_path.startswith("SpriteOutput/AvatarRoundIcon/Avatar/"):
|
||||||
|
suffix = source_path.removeprefix("SpriteOutput/AvatarRoundIcon/Avatar/")
|
||||||
|
candidates.append(f"icon/avatar/{suffix}")
|
||||||
|
if source_path.startswith("SpriteOutput/AvatarShopIcon/Avatar/"):
|
||||||
|
suffix = source_path.removeprefix("SpriteOutput/AvatarShopIcon/Avatar/")
|
||||||
|
candidates.extend((f"icon/avatar/{suffix}", f"image/character_portrait/{suffix}"))
|
||||||
|
if source_path.startswith("SpriteOutput/AvatarShopIcon/AvatarSkin/"):
|
||||||
|
suffix = source_path.removeprefix("SpriteOutput/AvatarShopIcon/AvatarSkin/")
|
||||||
|
candidates.extend((f"image/character_portrait/{suffix}", f"icon/item/Skin/{suffix}"))
|
||||||
|
if source_path.startswith("SpriteOutput/ItemIcon/Skin/"):
|
||||||
|
suffix = source_path.removeprefix("SpriteOutput/ItemIcon/Skin/")
|
||||||
|
candidates.extend((f"icon/item/Skin/{suffix}", f"icon/item/{suffix}"))
|
||||||
|
if source_path.startswith("SpriteOutput/ItemFigures/"):
|
||||||
|
suffix = source_path.removeprefix("SpriteOutput/ItemFigures/")
|
||||||
|
candidates.extend((f"image/item/{suffix}", f"image/items/{suffix}"))
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate and candidate in asset_files:
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def read_asset_indexes(revision: str, cache_dir: Path, asset_files: set[str]) -> dict[str, dict[str, dict]]:
|
||||||
|
base = f"https://raw.githubusercontent.com/{ASSET_PROJECT}/{revision}/index_new"
|
||||||
|
kinds = ("items", "light_cones", "relics")
|
||||||
|
indexes: dict[str, dict[str, dict]] = {}
|
||||||
|
jobs = {}
|
||||||
|
with ThreadPoolExecutor(max_workers=12) as pool:
|
||||||
|
for language, asset_language in ASSET_LANGUAGES.items():
|
||||||
|
indexes[language] = {}
|
||||||
|
for kind in kinds:
|
||||||
|
url = f"{base}/{asset_language}/{kind}.json"
|
||||||
|
key = f"starrailres_{asset_language}_{kind}"
|
||||||
|
jobs[pool.submit(cached_json, url, cache_dir, key)] = (language, kind)
|
||||||
|
for future in as_completed(jobs):
|
||||||
|
language, kind = jobs[future]
|
||||||
|
try:
|
||||||
|
data = future.result()
|
||||||
|
except RuntimeError:
|
||||||
|
# Some locales omit a data family; localized text maps fill its names.
|
||||||
|
data = {}
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
data = {}
|
||||||
|
indexes[language][kind] = data
|
||||||
|
# The pinned repository tree is the authority for whether an icon URL exists.
|
||||||
|
for language in indexes:
|
||||||
|
for kind, table in indexes[language].items():
|
||||||
|
for row in table.values():
|
||||||
|
icon = row.get("icon") if isinstance(row, dict) else None
|
||||||
|
if icon and icon not in asset_files:
|
||||||
|
row.pop("icon", None)
|
||||||
|
return indexes
|
||||||
|
|
||||||
|
|
||||||
|
def textmap_files_by_language(paths: list[str]) -> dict[str, list[str]]:
|
||||||
|
result = {}
|
||||||
|
for language, tokens in TEXTMAP_TOKENS.items():
|
||||||
|
matched = []
|
||||||
|
for path in paths:
|
||||||
|
name = Path(path).name.upper()
|
||||||
|
if not name.startswith("TEXTMAP") or not name.endswith(".JSON"):
|
||||||
|
continue
|
||||||
|
token = name[len("TEXTMAP"):-len(".JSON")].split("_")[0]
|
||||||
|
if token in tokens:
|
||||||
|
matched.append(path)
|
||||||
|
result[language] = sorted(matched)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def localized_textmaps(paths: list[str], revision: str, cache_dir: Path, wanted_hashes: set[str]) -> dict[str, dict[str, str]]:
|
||||||
|
maps: dict[str, dict[str, str]] = {language: {} for language in TEXTMAP_TOKENS}
|
||||||
|
by_language = textmap_files_by_language(paths)
|
||||||
|
for language, files in by_language.items():
|
||||||
|
if not files or not wanted_hashes:
|
||||||
|
continue
|
||||||
|
print(f"Reading {language} text maps ({len(files)} file(s))", flush=True)
|
||||||
|
for path in files:
|
||||||
|
data = cached_json(gitlab_raw(GAME_PROJECT, revision, path), cache_dir, f"game_{path}")
|
||||||
|
if isinstance(data, dict):
|
||||||
|
for key in wanted_hashes:
|
||||||
|
value = data.get(key)
|
||||||
|
if isinstance(value, str) and value:
|
||||||
|
maps[language][key] = value
|
||||||
|
del data
|
||||||
|
print(f" localized strings: {len(maps[language])}", flush=True)
|
||||||
|
return maps
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--inventory-config", required=True, type=Path, help="Go resource InventoryItemConfig.json")
|
||||||
|
parser.add_argument("--output", type=Path, default=Path("assets/item-catalog.json"))
|
||||||
|
parser.add_argument("--cache-dir", type=Path, default=Path(tempfile.gettempdir()) / "gac-srtool-item-catalog-cache")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
inventory = json.loads(args.inventory_config.read_text(encoding="utf-8"))
|
||||||
|
game_revision = inventory["source"]["revision"]
|
||||||
|
supported_ids = {int(value) for value in inventory["item_ids"]}
|
||||||
|
supported_lcs = {int(row["id"]) for row in inventory.get("lightcones", [])}
|
||||||
|
supported_relics = {int(row["relic_id"]) for row in inventory.get("relics", [])}
|
||||||
|
supported_ids.update(supported_lcs)
|
||||||
|
supported_ids.update(supported_relics)
|
||||||
|
asset_revision = "dbe8cdfcfb0bf657b9fe1cf92d5537f118ccb487"
|
||||||
|
|
||||||
|
print("Discovering game item tables...", flush=True)
|
||||||
|
excel_files = discover_files(GAME_PROJECT, game_revision, "ExcelOutput")
|
||||||
|
candidates = [path for path in excel_files if "ItemConfig" in Path(path).name and path.lower().endswith(".json")]
|
||||||
|
tables = fetch_game_json_files(candidates, game_revision, args.cache_dir)
|
||||||
|
item_rows: dict[int, list[tuple[str, dict]]] = {}
|
||||||
|
table_scan = []
|
||||||
|
for path in sorted(candidates):
|
||||||
|
data = tables[path]
|
||||||
|
valid_rows = [row for row in data if isinstance(row, dict) and isinstance(row.get("ID"), int) and row.get("ID") > 0 and isinstance(row.get("ItemMainType"), str)] if isinstance(data, list) else []
|
||||||
|
table_scan.append({"table": path, "rows": len(data) if isinstance(data, list) else 0, "itemRows": len(valid_rows)})
|
||||||
|
for row in valid_rows:
|
||||||
|
item_rows.setdefault(row["ID"], []).append((path, row))
|
||||||
|
definition_tables = [row["table"] for row in table_scan if row["itemRows"]]
|
||||||
|
if not item_rows:
|
||||||
|
raise RuntimeError("No item definition rows were found in ItemConfig tables")
|
||||||
|
print(f"Accepted {len(definition_tables)} tables; {sum(row['itemRows'] for row in table_scan)} rows; {len(item_rows)} unique IDs", flush=True)
|
||||||
|
|
||||||
|
print("Reading pinned asset index...", flush=True)
|
||||||
|
tree_url = f"https://api.github.com/repos/{ASSET_PROJECT}/git/trees/{asset_revision}?recursive=1"
|
||||||
|
tree = request_json(tree_url)
|
||||||
|
if tree.get("truncated"):
|
||||||
|
raise RuntimeError("Pinned asset tree was truncated; refusing incomplete image coverage")
|
||||||
|
asset_files = {entry["path"] for entry in tree.get("tree", []) if entry.get("type") == "blob"}
|
||||||
|
indexes = read_asset_indexes(asset_revision, args.cache_dir, asset_files)
|
||||||
|
|
||||||
|
hashes = set()
|
||||||
|
for rows in item_rows.values():
|
||||||
|
for _, row in rows:
|
||||||
|
for field in TEXT_FIELDS:
|
||||||
|
value = item_hash(row, field)
|
||||||
|
if value:
|
||||||
|
hashes.add(value)
|
||||||
|
print(f"Resolving {len(hashes)} game text keys across 13 locales...", flush=True)
|
||||||
|
textmap_paths = discover_files(GAME_PROJECT, game_revision, "TextMap")
|
||||||
|
textmaps = localized_textmaps(textmap_paths, game_revision, args.cache_dir, hashes)
|
||||||
|
|
||||||
|
existing_item_names = indexes.get("en", {}).get("items", {})
|
||||||
|
existing_lc_names = indexes.get("en", {}).get("light_cones", {})
|
||||||
|
existing_relic_names = indexes.get("en", {}).get("relics", {})
|
||||||
|
merged = []
|
||||||
|
name_coverage = {language: 0 for language in ASSET_LANGUAGES}
|
||||||
|
description_coverage = {language: 0 for language in ASSET_LANGUAGES}
|
||||||
|
background_description_coverage = {language: 0 for language in ASSET_LANGUAGES}
|
||||||
|
for item_id in sorted(item_rows):
|
||||||
|
source_rows = sorted(item_rows[item_id], key=lambda pair: rows_preference(pair[0]))
|
||||||
|
all_rows = [row for _, row in source_rows]
|
||||||
|
all_paths = []
|
||||||
|
for row in all_rows:
|
||||||
|
for path in row_image_paths(row):
|
||||||
|
if path not in all_paths:
|
||||||
|
all_paths.append(path)
|
||||||
|
preferred = all_rows[0]
|
||||||
|
if item_id in supported_lcs:
|
||||||
|
category, index_kind = "lightcone", "light_cones"
|
||||||
|
elif item_id in supported_relics:
|
||||||
|
category, index_kind = "relic", "relics"
|
||||||
|
else:
|
||||||
|
category, index_kind = "item", "items"
|
||||||
|
index_rows = indexes.get("en", {}).get(index_kind, {})
|
||||||
|
index_row = index_rows.get(str(item_id), {}) if isinstance(index_rows, dict) else {}
|
||||||
|
name_hash = next((item_hash(row, "ItemName") for _, row in source_rows if item_hash(row, "ItemName")), None)
|
||||||
|
desc_hash = next((item_hash(row, "ItemDesc") for _, row in source_rows if item_hash(row, "ItemDesc")), None)
|
||||||
|
bg_desc_hash = next((item_hash(row, "ItemBGDesc") for _, row in source_rows if item_hash(row, "ItemBGDesc")), None)
|
||||||
|
names = {}
|
||||||
|
descriptions = {}
|
||||||
|
background_descriptions = {}
|
||||||
|
for language in ASSET_LANGUAGES:
|
||||||
|
locale_index = indexes.get(language, {}).get(index_kind, {})
|
||||||
|
locale_row = locale_index.get(str(item_id), {}) if isinstance(locale_index, dict) else {}
|
||||||
|
textmap = textmaps.get(language, {})
|
||||||
|
name = textmap.get(name_hash) if name_hash else None
|
||||||
|
if not name and isinstance(locale_row, dict):
|
||||||
|
name = locale_row.get("name")
|
||||||
|
if not name and isinstance(index_row, dict):
|
||||||
|
name = index_row.get("name")
|
||||||
|
if name:
|
||||||
|
names[language] = name
|
||||||
|
name_coverage[language] += 1
|
||||||
|
desc = textmap.get(desc_hash) if desc_hash else None
|
||||||
|
if not desc and isinstance(locale_row, dict):
|
||||||
|
desc = locale_row.get("desc")
|
||||||
|
if desc and language:
|
||||||
|
descriptions[language] = desc
|
||||||
|
description_coverage[language] += 1
|
||||||
|
bg_desc = textmap.get(bg_desc_hash) if bg_desc_hash else None
|
||||||
|
if bg_desc:
|
||||||
|
background_descriptions[language] = bg_desc
|
||||||
|
background_description_coverage[language] += 1
|
||||||
|
|
||||||
|
# Fill sparse locales with the English name only if the source has no localized text.
|
||||||
|
english_name = names.get("en") or (index_row.get("name") if isinstance(index_row, dict) else None) or f"Item {item_id}"
|
||||||
|
for language in ASSET_LANGUAGES:
|
||||||
|
names.setdefault(language, english_name)
|
||||||
|
icon = resolve_source_icon(all_paths, category, item_id, indexes, asset_files)
|
||||||
|
rarity = parse_rarity(preferred.get("Rarity"), index_row)
|
||||||
|
output_row = {
|
||||||
|
"id": item_id,
|
||||||
|
"category": category,
|
||||||
|
"type": preferred.get("ItemMainType", ""),
|
||||||
|
"subType": preferred.get("ItemSubType", ""),
|
||||||
|
"rarity": rarity,
|
||||||
|
"names": names,
|
||||||
|
"descriptions": descriptions,
|
||||||
|
"backgroundDescriptions": background_descriptions,
|
||||||
|
"icon": icon,
|
||||||
|
"gameIconPaths": all_paths,
|
||||||
|
"sourceTables": sorted({path for path, _ in source_rows}),
|
||||||
|
"supported": item_id in supported_ids,
|
||||||
|
}
|
||||||
|
pile_limit = next((row.get("PileLimit") for _, row in source_rows if isinstance(row.get("PileLimit"), int)), None)
|
||||||
|
if pile_limit:
|
||||||
|
output_row["pileLimit"] = pile_limit
|
||||||
|
if category == "lightcone":
|
||||||
|
output_row["path"] = index_row.get("path") if isinstance(index_row, dict) else None
|
||||||
|
elif category == "relic":
|
||||||
|
if isinstance(index_row, dict):
|
||||||
|
output_row["setId"] = int(index_row["set_id"]) if str(index_row.get("set_id", "")).isdigit() else None
|
||||||
|
output_row["slot"] = index_row.get("type")
|
||||||
|
merged.append(output_row)
|
||||||
|
|
||||||
|
counts = {
|
||||||
|
"total": len(merged),
|
||||||
|
"definitionRows": sum(row["itemRows"] for row in table_scan),
|
||||||
|
"uniqueIds": len(merged),
|
||||||
|
"supported": sum(row["supported"] for row in merged),
|
||||||
|
"withImages": sum(bool(row["icon"]) for row in merged),
|
||||||
|
"withoutConfiguredImage": sum(not row["icon"] for row in merged),
|
||||||
|
"withDescriptions": sum(bool(row["descriptions"] or row["backgroundDescriptions"]) for row in merged),
|
||||||
|
"nameCoverageByLanguage": name_coverage,
|
||||||
|
"descriptionCoverageByLanguage": description_coverage,
|
||||||
|
"backgroundDescriptionCoverageByLanguage": background_description_coverage,
|
||||||
|
"candidateTables": len(candidates),
|
||||||
|
"definitionTables": len(definition_tables),
|
||||||
|
}
|
||||||
|
output = {
|
||||||
|
"source": {
|
||||||
|
"gameRepository": f"https://gitlab.com/{GAME_PROJECT}",
|
||||||
|
"gameRevision": game_revision,
|
||||||
|
"candidateTables": len(candidates),
|
||||||
|
"gameTables": definition_tables,
|
||||||
|
"tableScan": table_scan,
|
||||||
|
"assetRepository": ASSET_PROJECT,
|
||||||
|
"assetRevision": asset_revision,
|
||||||
|
"assetBaseUrl": f"https://cdn.jsdelivr.net/gh/{ASSET_PROJECT}@{asset_revision}/",
|
||||||
|
"assetFallbackBaseUrl": f"https://raw.githubusercontent.com/{ASSET_PROJECT}/{asset_revision}/",
|
||||||
|
"languages": list(ASSET_LANGUAGES),
|
||||||
|
"notes": "Includes every ItemConfig table with item-definition rows at the pinned game-data revision. Items absent from InventoryItemConfig are browse-only; the catalog does not add or summon items. Icons use only files present in the pinned public StarRailRes tree. gameIconPaths retain the source paths for items without a mapped public icon.",
|
||||||
|
},
|
||||||
|
"counts": counts,
|
||||||
|
"items": merged,
|
||||||
|
}
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.output.write_text(json.dumps(output, ensure_ascii=False, separators=(",", ":")) + "\n", encoding="utf-8")
|
||||||
|
print(json.dumps(counts, ensure_ascii=False, indent=2), flush=True)
|
||||||
|
print(f"Wrote {len(merged)} item rows to {args.output}", flush=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except Exception as error: # give the CLI one actionable line on network/schema failures
|
||||||
|
print(f"item catalog build failed: {error}", file=sys.stderr)
|
||||||
|
raise
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"buildCommand": "node scripts/build-vercel.cjs",
|
||||||
|
"outputDirectory": "dist"
|
||||||
|
}
|
||||||
Reference in new issue
Block a user