feat: initial commit for GaC-srtool dashboard
This commit is contained in:
commit
08e2440ca5
9 files changed
+1988
No files matched your search
@@ -0,0 +1,34 @@
|
|||||||
|
name: Deploy GaC-Srtool to GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v4
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: '.'
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Dependencies & Package Managers
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# OS / Editor files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Local custom configs (environment / machine specific)
|
||||||
|
config.local.json
|
||||||
|
config.local.js
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# GaC-Srtool 🌌
|
||||||
|
|
||||||
|
**GaC-Srtool** คือเว็บแอปพลิเคชันแดชบอร์ด (Web Dashboard & Management Tool) สำหรับจัดการข้อมูลตัวละคร, สถานะผู้เล่น, จุดวาร์ป, และไฟล์เซฟ (`freesr-data.json`, `persistent`) ของเซิร์ฟเวอร์ **GaC-hkrpg-go** (และ FreeSR Private Server)
|
||||||
|
|
||||||
|
ถูกออกแบบให้ทำงานแบบ **Client-side 100%** ไม่มีเซิร์ฟเวอร์หลังบ้าน จึงสามารถโฮสต์ผ่าน **GitHub Pages** ได้ฟรีตลอดชีพ ไม่ต้องเสียค่าบริการใดๆ!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ ความสามารถหลัก (Features)
|
||||||
|
|
||||||
|
1. **📊 หน้าแดชบอร์ด (Overview Dashboard):**
|
||||||
|
- แสดงสถานะเซิร์ฟเวอร์สด (Live Health Status)
|
||||||
|
- นับจำนวนตัวละคร, กรวยแสง (Lightcones), และรีลิกส์ (Relics) ทั้งหมด
|
||||||
|
- แสดงพิกัดตัวละครปัจจุบัน (Plane, Floor, X, Y, Z) และการจัดทีม (Current Lineup)
|
||||||
|
2. **👥 จัดการตัวละคร (Avatars & Eidolons):**
|
||||||
|
- ดูรายชื่อตัวละครทั้งหมดพร้อมสาย Path, ธาตุ และระดับดาว
|
||||||
|
- ปรับเลเวลและการปลดดาว Eidolon (E0 - E6) รายตัวแบบเรียลไทม์
|
||||||
|
- ปุ่ม **Max All** ตัวละครทุกตัวเป็น Lv. 80 และ E6 ในคลิกเดียว
|
||||||
|
3. **⚡ ระบบ Ex-Rank Skills:**
|
||||||
|
- สลับเปิด/ปิด Ex-Rank ของตัวละครพิเศษ (Jing Yuan CC immunity, Dan Heng • IL, Acheron)
|
||||||
|
- สั่งเปิดหรือปิด Ex-Rank ทุกตัวละครพร้อมกันได้ทันที
|
||||||
|
4. **🚀 วาร์ปฉากและตำแหน่ง (Teleportation):**
|
||||||
|
- รวมจุดวาร์ปปลอดภัยยอดนิยม (Astral Express Parlor Car, Herta Station, Belobog, Xianzhou Luofu, Penacony)
|
||||||
|
- กล่องใส่วาร์ปแบบกำหนดเอง (Plane ID, Floor ID, Entry ID, พิกัด X, Y, Z)
|
||||||
|
5. **📝 จัดการไฟล์ JSON (FreeSR Data & Persistent Editor):**
|
||||||
|
- โหลดและแก้ไขไฟล์ `freesr-data.json` และ `persistent` บนเซิร์ฟเวอร์ได้โดยตรง
|
||||||
|
- มีระบบตรวจเช็ค Syntax JSON และจัดรูปแบบโค้ด (Beautify)
|
||||||
|
- ดาวน์โหลด Backup หรืออัปโหลดไฟล์ JSON จากเครื่องขึ้นเซิร์ฟเวอร์
|
||||||
|
6. **⚙️ เชื่อมต่อแบบ Dynamic (No Hardcode):**
|
||||||
|
- สามารถระบุ REST API Gateway URL (เช่น `http://localhost:21000` หรือ IP เครื่องในวง LAN) ผ่านหน้าต่างตั้งค่า โดยค่าจะถูกบันทึกไว้ในเบราว์เซอร์ (`localStorage`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 วิธีนำขึ้นโฮสต์ GitHub Pages (ฟรี 100%)
|
||||||
|
|
||||||
|
คุณสามารถโฮสต์เว็บนี้ผ่าน GitHub ได้ง่ายๆ เพียง 4 ขั้นตอน:
|
||||||
|
|
||||||
|
### ขั้นตอนที่ 1: สร้าง Repository บน GitHub
|
||||||
|
1. เข้าไปที่ [GitHub.com](https://github.com) แล้วกด **New repository**
|
||||||
|
2. ตั้งชื่อ เช่น `GaC-srtool` (จะเลือก Public หรือ Private ก็ได้)
|
||||||
|
3. **ไม่ต้อง** ติ๊กถูกที่ช่อง "Add a README file" จากนั้นกด **Create repository**
|
||||||
|
|
||||||
|
### ขั้นตอนที่ 2: Push โปรเจกต์ขึ้น GitHub
|
||||||
|
เปิด Terminal / PowerShell ในโฟลเดอร์นี้ แล้วรันคำสั่ง:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote add origin https://github.com/<ชื่อผู้ใช้ของคุณ>/GaC-srtool.git
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: initial commit for GaC-srtool dashboard"
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
### ขั้นตอนที่ 3: เปิดใช้งาน GitHub Pages
|
||||||
|
1. ในหน้า Repo ของคุณบน GitHub ไปที่แท็บ **Settings**
|
||||||
|
2. ที่เมนูด้านซ้าย เลือกหัวข้อ **Pages**
|
||||||
|
3. ใต้หัวข้อ **Build and deployment**:
|
||||||
|
- **Source:** เลือก `Deploy from a branch`
|
||||||
|
- **Branch:** เลือก `main` และโฟลเดอร์ `/ (root)`
|
||||||
|
- กดปุ่ม **Save**
|
||||||
|
*(หรือจะเลือก Source เป็น `GitHub Actions` ก็ได้ ตัวโปรเจกต์มี `.github/workflows/deploy.yml` เตรียมไว้ให้อัตโนมัติแล้ว)*
|
||||||
|
|
||||||
|
### ขั้นตอนที่ 4: ใช้งานได้ทันที
|
||||||
|
รอระบบ GitHub Deploy ประมาณ 1 นาที คุณจะได้รับ URL เว็บไซต์ เช่น:
|
||||||
|
```
|
||||||
|
https://<ชื่อผู้ใช้ของคุณ>.github.io/GaC-srtool/
|
||||||
|
```
|
||||||
|
สามารถเปิดจากเบราว์เซอร์เครื่องใดก็ได้ หรือเปิดจากมือถือ/แท็บเล็ตในวงแลนเดียวกันได้เลย!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 หมายเหตุเรื่องการเชื่อมต่อ Browser (Mixed Content)
|
||||||
|
|
||||||
|
- เมื่อเปิดหน้าเว็บผ่าน `https://...github.io` และทำการเชื่อมต่อกับเซิร์ฟเวอร์ที่เครื่องของเรา (`http://localhost:21000`):
|
||||||
|
- เบราว์เซอร์สมัยใหม่ (Google Chrome, Microsoft Edge, Brave) อนุญาตให้หน้าเว็บ HTTPS ยิง request ไปยัง `http://localhost:*` หรือ `http://127.0.0.1:*` ได้ทันที (ตามมาตรฐาน W3C Localhost Exception)
|
||||||
|
- หากใช้เบราว์เซอร์บางตัวที่แจ้งเตือนความปลอดภัย (Insecure Content / Mixed Content): สามารถกดไอคอนรูปกุญแจ/การตั้งค่าที่หน้า URL Bar แล้วเลือก **Site settings** -> **Insecure content: Allow** ได้ครับ
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 การทดสอบและรันในเครื่อง (Local Development)
|
||||||
|
|
||||||
|
หากต้องการเปิดใช้งานหรือพัฒนาในเครื่องโดยไม่ต้องต่อเน็ต:
|
||||||
|
1. **เปิดตรง:** ดับเบิ้ลคลิกไฟล์ `index.html` เพื่อเปิดใช้งานบนเบราว์เซอร์ได้ทันที
|
||||||
|
2. **รัน Local Server:**
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# หรือ
|
||||||
|
npx serve . -l 5173
|
||||||
|
```
|
||||||
|
เปิดเว็บที่ `http://localhost:5173`
|
||||||
+652
@@ -0,0 +1,652 @@
|
|||||||
|
:root {
|
||||||
|
--bg-primary: #0a0d14;
|
||||||
|
--bg-secondary: #121826;
|
||||||
|
--bg-card: rgba(22, 30, 48, 0.7);
|
||||||
|
--bg-card-hover: rgba(30, 41, 66, 0.85);
|
||||||
|
--border-color: rgba(99, 120, 160, 0.25);
|
||||||
|
--border-focus: #4ef2bb;
|
||||||
|
|
||||||
|
--text-main: #f0f4fc;
|
||||||
|
--text-muted: #8e9bb5;
|
||||||
|
--text-highlight: #ffffff;
|
||||||
|
|
||||||
|
--accent-cyan: #38bdf8;
|
||||||
|
--accent-green: #34d399;
|
||||||
|
--accent-gold: #fbbf24;
|
||||||
|
--accent-purple: #a855f7;
|
||||||
|
--accent-red: #f87171;
|
||||||
|
|
||||||
|
--glow-cyan: 0 0 15px rgba(56, 189, 248, 0.35);
|
||||||
|
--glow-green: 0 0 15px rgba(52, 211, 153, 0.35);
|
||||||
|
--glow-gold: 0 0 15px rgba(251, 191, 36, 0.35);
|
||||||
|
|
||||||
|
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
--radius-sm: 6px;
|
||||||
|
--radius-md: 10px;
|
||||||
|
--radius-lg: 16px;
|
||||||
|
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 15% 15%, rgba(56, 189, 248, 0.08) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 85% 85%, rgba(168, 85, 247, 0.08) 0%, transparent 45%);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.app-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background: rgba(10, 13, 20, 0.85);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-icon {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: linear-gradient(135deg, #38bdf8, #818cf8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: var(--glow-cyan);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
background: linear-gradient(90deg, #ffffff, #cbd5e1);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-subtitle {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-pill {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-pill:hover {
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--accent-red);
|
||||||
|
box-shadow: 0 0 8px var(--accent-red);
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.connected {
|
||||||
|
background-color: var(--accent-green);
|
||||||
|
box-shadow: var(--glow-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Tabs */
|
||||||
|
.nav-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover {
|
||||||
|
color: var(--text-main);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(56, 189, 248, 0.15);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||||
|
box-shadow: 0 0 10px rgba(56, 189, 248, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container */
|
||||||
|
.main-container {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.5rem;
|
||||||
|
max-width: 1300px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
animation: fadeIn 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards & Grid */
|
||||||
|
.grid-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-3 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-4 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1.25rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
background: var(--bg-card-hover);
|
||||||
|
border-color: rgba(99, 120, 160, 0.45);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
color: var(--text-highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat Box */
|
||||||
|
.stat-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.6rem 1.1rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: var(--transition);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #0284c7, #2563eb);
|
||||||
|
color: #fff;
|
||||||
|
border-color: rgba(56, 189, 248, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
box-shadow: var(--glow-cyan);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: linear-gradient(135deg, #059669, #10b981);
|
||||||
|
color: #fff;
|
||||||
|
border-color: rgba(52, 211, 153, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
box-shadow: var(--glow-green);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gold {
|
||||||
|
background: linear-gradient(135deg, #d97706, #f59e0b);
|
||||||
|
color: #fff;
|
||||||
|
border-color: rgba(251, 191, 36, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gold:hover {
|
||||||
|
box-shadow: var(--glow-gold);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.35rem 0.65rem;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Avatar Card */
|
||||||
|
.avatar-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 180px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-badge-rarity {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.15rem 0.5rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rarity-5 {
|
||||||
|
background: rgba(251, 191, 36, 0.2);
|
||||||
|
border: 1px solid rgba(251, 191, 36, 0.5);
|
||||||
|
color: var(--accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rarity-4 {
|
||||||
|
background: rgba(168, 85, 247, 0.2);
|
||||||
|
border: 1px solid rgba(168, 85, 247, 0.5);
|
||||||
|
color: var(--accent-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-header-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-icon-placeholder {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-name {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-subinfo {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eidolon-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eidolon-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eidolon-pip {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eidolon-pip:hover {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eidolon-pip.active {
|
||||||
|
background: var(--accent-cyan);
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
color: #0b0d14;
|
||||||
|
box-shadow: 0 0 8px rgba(56, 189, 248, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ex Rank Toggle Switch */
|
||||||
|
.ex-switch-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ex-switch-card:hover {
|
||||||
|
background: var(--bg-card-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 48px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background-color: #374151;
|
||||||
|
transition: .3s;
|
||||||
|
border-radius: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
transition: .3s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: var(--accent-green);
|
||||||
|
box-shadow: var(--glow-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
transform: translateX(22px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inputs & Form */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input, .form-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.6rem 0.85rem;
|
||||||
|
background: rgba(10, 13, 20, 0.7);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
outline: none;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus, .form-select:focus {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
box-shadow: 0 0 10px rgba(56, 189, 248, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JSON Code Editor Area */
|
||||||
|
.json-editor-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 480px;
|
||||||
|
padding: 1rem;
|
||||||
|
font-family: "Fira Code", Consolas, "Courier New", monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
background: #07090e;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: #38bdf8;
|
||||||
|
resize: vertical;
|
||||||
|
line-height: 1.4;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-textarea:focus {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast notifications */
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
pointer-events: auto;
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: 420px;
|
||||||
|
padding: 0.85rem 1.15rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: rgba(18, 24, 38, 0.95);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
animation: slideInRight 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success { border-left: 4px solid var(--accent-green); }
|
||||||
|
.toast.error { border-left: 4px solid var(--accent-red); }
|
||||||
|
.toast.info { border-left: 4px solid var(--accent-cyan); }
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 999;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
max-width: 560px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.header-controls {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.nav-tabs {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
.main-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
+333
@@ -0,0 +1,333 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="th">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>GaC-Srtool — FreeSR & Game Management Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="css/styles.css">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌌</text></svg>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- App Header -->
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="brand-icon">✦</div>
|
||||||
|
<div>
|
||||||
|
<div class="brand-title">GaC-Srtool</div>
|
||||||
|
<div class="brand-subtitle">Star Rail FreeSR & Server Controller</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-controls">
|
||||||
|
<div class="connection-pill" id="btn-open-settings" title="คลิกเพื่อตั้งค่าการเชื่อมต่อ API">
|
||||||
|
<span class="status-dot" id="status-dot"></span>
|
||||||
|
<span id="connection-status-text">กำลังตรวจสอบ...</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary btn-sm" id="btn-quick-ping" title="ทดสอบการเชื่อมต่อใหม่">
|
||||||
|
🔄 รีเฟรช
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Navigation Bar -->
|
||||||
|
<nav class="nav-tabs">
|
||||||
|
<button class="tab-btn active" data-tab="tab-dashboard">📊 แดชบอร์ด (Overview)</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-avatars">👥 ตัวละคร (Avatars)</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-exrank">⚡ Ex-Rank (สกิลพิเศษ)</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-teleport">🚀 วาร์ปฉาก (Teleport)</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-json">📝 จัดการไฟล์ JSON</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-settings">⚙️ ตั้งค่า & คู่มือ Host</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Container -->
|
||||||
|
<main class="main-container">
|
||||||
|
|
||||||
|
<!-- 1. Dashboard Tab -->
|
||||||
|
<section id="tab-dashboard" class="tab-content active">
|
||||||
|
<div class="grid-3" style="margin-bottom: 1.5rem;">
|
||||||
|
<div class="card stat-box">
|
||||||
|
<div class="stat-number" id="stat-avatars">0</div>
|
||||||
|
<div class="stat-label">ตัวละครที่ครอบครอง (Avatars)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card stat-box">
|
||||||
|
<div class="stat-number" id="stat-lightcones">0</div>
|
||||||
|
<div class="stat-label">กรวยแสง (Lightcones)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card stat-box">
|
||||||
|
<div class="stat-number" id="stat-relics">0</div>
|
||||||
|
<div class="stat-label">รีลิกส์ (Relics)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-2">
|
||||||
|
<!-- Server & Map Card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">
|
||||||
|
<span>📍 สถานะเซิร์ฟเวอร์ & ตำแหน่งปัจจุบัน</span>
|
||||||
|
<span class="btn-sm btn-secondary" id="badge-version">API Live</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
|
||||||
|
<div>
|
||||||
|
<span class="form-label">API Gateway Endpoint</span>
|
||||||
|
<code id="lbl-api-url" style="color: var(--accent-cyan); font-size: 0.9rem;">-</code>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 1rem;">
|
||||||
|
<div>
|
||||||
|
<span class="form-label">Plane ID</span>
|
||||||
|
<span id="lbl-plane-id" style="font-weight: bold;">-</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">Floor ID</span>
|
||||||
|
<span id="lbl-floor-id" style="font-weight: bold;">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">พิกัดตัวละคร (X, Y, Z)</span>
|
||||||
|
<span id="lbl-coordinates" style="color: var(--text-muted); font-size: 0.85rem;">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions Card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">⚡ ดำเนินการด่วน (Quick Actions)</div>
|
||||||
|
<p class="card-subtitle">สั่งการส่งไปยัง REST API Gateway ทันที</p>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
|
||||||
|
<button class="btn btn-gold" id="btn-quick-max-all">
|
||||||
|
⭐ ปรับเลเวล 80 & ปลด E6 ตัวละครทั้งหมด (Max All)
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" id="btn-quick-warp-express">
|
||||||
|
🚂 วาร์ปกลับรถไฟ Astral Express ทันที
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" id="btn-quick-sync-persistent">
|
||||||
|
💾 ซิงค์ข้อมูล Persistent ล่าสุด
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current Lineup -->
|
||||||
|
<div class="card" style="margin-top: 1.5rem;">
|
||||||
|
<div class="card-title">🎮 ทีมปัจจุบัน (Current Lineup)</div>
|
||||||
|
<div id="lineup-container" class="grid-4" style="margin-top: 0.5rem;">
|
||||||
|
<div class="card" style="text-align: center; color: var(--text-muted);">กำลังโหลดข้อมูลทีม...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. Avatars Tab -->
|
||||||
|
<section id="tab-avatars" class="tab-content">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.25rem; flex-wrap: wrap; gap: 1rem;">
|
||||||
|
<div style="display: flex; gap: 0.75rem; flex: 1; max-width: 480px;">
|
||||||
|
<input type="text" id="avatar-search" class="form-input" placeholder="🔍 ค้นหาชื่อหรือ ID ตัวละคร...">
|
||||||
|
<select id="avatar-filter-rarity" class="form-select" style="max-width: 140px;">
|
||||||
|
<option value="all">ทุกระดับดาว</option>
|
||||||
|
<option value="5">5 ดาว (SSR)</option>
|
||||||
|
<option value="4">4 ดาว (SR)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<button class="btn btn-gold" id="btn-max-all-avatars">⭐ ปรับ Max E6 ทุกตัว</button>
|
||||||
|
<button class="btn btn-secondary" id="btn-reload-avatars">🔄 รีโหลด</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="avatars-grid" class="grid-4">
|
||||||
|
<div style="color: var(--text-muted); grid-column: 1/-1; text-align: center; padding: 2rem;">
|
||||||
|
กำลังดึงข้อมูลตัวละครจากเซิร์ฟเวอร์...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. Ex-Rank Tab -->
|
||||||
|
<section id="tab-exrank" class="tab-content">
|
||||||
|
<div class="card" style="margin-bottom: 1.5rem;">
|
||||||
|
<div class="card-title">
|
||||||
|
<span>⚡ ระบบปลดล็อค Ex-Rank Skill</span>
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<button class="btn btn-success btn-sm" id="btn-exrank-enable-all">เปิด Ex-Rank ทั้งหมด</button>
|
||||||
|
<button class="btn btn-secondary btn-sm" id="btn-exrank-disable-all">ปิด Ex-Rank ทั้งหมด</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="card-subtitle">
|
||||||
|
เปิดใช้งานพรสวรรค์และกลไกพิเศษ Ex-Rank สำหรับตัวละครเฉพาะใน FreeSR (เช่น Jing Yuan โดน CC ไม่หยุดชะงัก, Dan Heng IL, Acheron)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="exrank-list" style="display: flex; flex-direction: column; gap: 0.75rem;">
|
||||||
|
<div style="color: var(--text-muted); text-align: center; padding: 2rem;">
|
||||||
|
กำลังโหลดข้อมูล Ex-Rank...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. Teleport Tab -->
|
||||||
|
<section id="tab-teleport" class="tab-content">
|
||||||
|
<div class="grid-2">
|
||||||
|
<!-- Presets -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">📍 จุดวาร์ปปลอดภัยยอดนิยม (Safe Presets)</div>
|
||||||
|
<p class="card-subtitle">คลิกจุดวาร์ปเพื่อย้ายฉากของผู้เล่นในไฟล์เซฟได้ทันที</p>
|
||||||
|
<div id="teleport-presets-list" style="display: flex; flex-direction: column; gap: 0.6rem;">
|
||||||
|
<!-- Rendered by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Coordinate Warp -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">🎯 กำหนดพิกัดวาร์ปเอง (Custom Teleport)</div>
|
||||||
|
<p class="card-subtitle">ใส่ PlaneID, FloorID และ EntryID ตามต้องการ</p>
|
||||||
|
|
||||||
|
<form id="form-custom-teleport">
|
||||||
|
<div class="grid-2" style="gap: 0.75rem;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Plane ID</label>
|
||||||
|
<input type="number" id="tp-plane" class="form-input" required placeholder="เช่น 20001">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Floor ID</label>
|
||||||
|
<input type="number" id="tp-floor" class="form-input" required placeholder="เช่น 20001001">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Entry ID</label>
|
||||||
|
<input type="number" id="tp-entry" class="form-input" required placeholder="เช่น 2000101">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-3" style="gap: 0.75rem;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">X</label>
|
||||||
|
<input type="number" id="tp-x" class="form-input" placeholder="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Y</label>
|
||||||
|
<input type="number" id="tp-y" class="form-input" placeholder="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Z</label>
|
||||||
|
<input type="number" id="tp-z" class="form-input" placeholder="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 0.5rem;">
|
||||||
|
🚀 ยืนยันการวาร์ป (Execute Teleport)
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. JSON Management Tab -->
|
||||||
|
<section id="tab-json" class="tab-content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title" style="flex-wrap: wrap; gap: 0.75rem;">
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<button class="btn btn-sm btn-primary active" id="btn-select-freesr">📄 freesr-data.json</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" id="btn-select-persistent">💾 persistent (Save)</button>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
|
<button class="btn btn-sm btn-secondary" id="btn-json-beautify">✨ จัดรูปแบบ JSON</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" id="btn-json-download">📥 ดาวน์โหลด Backup</button>
|
||||||
|
<label class="btn btn-sm btn-secondary" style="cursor: pointer; margin: 0;">
|
||||||
|
📤 อัปโหลดไฟล์ <input type="file" id="json-file-input" accept=".json,text/plain" style="display: none;">
|
||||||
|
</label>
|
||||||
|
<button class="btn btn-sm btn-success" id="btn-json-save">💾 บันทึกไปยังเซิร์ฟเวอร์</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="json-editor-wrapper">
|
||||||
|
<textarea id="json-editor" class="json-textarea" spellcheck="false" placeholder="กำลังโหลดข้อมูล JSON..."></textarea>
|
||||||
|
<div style="display: flex; justify-content: space-between; font-size: 0.8rem; color: var(--text-muted);">
|
||||||
|
<span id="json-status-info">พร้อมใช้งาน</span>
|
||||||
|
<span>Tip: สามารถแก้ไขค่าดิบแล้วกด 'บันทึกไปยังเซิร์ฟเวอร์' ได้ทันที</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 6. Settings & GitHub Hosting Guide -->
|
||||||
|
<section id="tab-settings" class="tab-content">
|
||||||
|
<div class="grid-2">
|
||||||
|
<!-- Endpoint config -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">⚙️ ตั้งค่าการเชื่อมต่อ API Gateway</div>
|
||||||
|
<p class="card-subtitle">ปรับเปลี่ยนพอร์ตหรือ IP ได้ตามไฟล์ server.json ของคุณ</p>
|
||||||
|
|
||||||
|
<form id="form-settings">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">REST API Gateway URL</label>
|
||||||
|
<input type="text" id="setting-api-url" class="form-input" placeholder="http://localhost:21000">
|
||||||
|
<small style="color: var(--text-muted); display: block; margin-top: 0.35rem;">
|
||||||
|
* ค่าเริ่มต้นของ GaC-hkrpg-go คือพอร์ต 21000 (SDK server)
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">ความถี่ในการตรวจสอบสถานะ (วินาที)</label>
|
||||||
|
<input type="number" id="setting-ping-interval" class="form-input" min="3" max="60" value="10">
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 0.75rem;">
|
||||||
|
<button type="submit" class="btn btn-success" style="flex: 1;">💾 บันทึกการตั้งค่า</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="btn-reset-settings">คืนค่าเริ่มต้น</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- GitHub Pages Hosting Guide -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">🌐 วิธีนำขึ้นโฮสต์ GitHub Pages (ฟรี 100%)</div>
|
||||||
|
<p class="card-subtitle">สามารถเปิดใช้งานเว็บนี้บนอินเทอร์เน็ตได้โดยไม่ต้องเสียค่าโฮสต์ใดๆ</p>
|
||||||
|
|
||||||
|
<ol style="margin-left: 1.25rem; font-size: 0.875rem; color: var(--text-muted); display: flex; flex-direction: column; gap: 0.65rem;">
|
||||||
|
<li>
|
||||||
|
<strong style="color: var(--text-main);">1. สร้าง Repository บน GitHub:</strong><br>
|
||||||
|
สร้าง GitHub Repo ใหม่ (Public หรือ Private ก็ได้) เช่นชื่อ <code>GaC-srtool</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong style="color: var(--text-main);">2. Push โฟลเดอร์นี้ขึ้น GitHub:</strong><br>
|
||||||
|
<code>git remote add origin https://github.com/<user>/GaC-srtool.git</code><br>
|
||||||
|
<code>git add . && git commit -m "Init GaC-srtool" && git push -u origin main</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong style="color: var(--text-main);">3. เปิดใช้งาน GitHub Pages:</strong><br>
|
||||||
|
เข้าไปที่ Repo บน GitHub > <strong>Settings</strong> > <strong>Pages</strong><br>
|
||||||
|
ใต้หัวข้อ <em>Build and deployment</em> เลือก Source เป็น <strong>Deploy from a branch</strong> > เลือก Branch: <strong>main</strong>, Folder: <strong>/ (root)</strong> > กด <strong>Save</strong>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong style="color: var(--text-main);">4. ใช้งานได้ทันที:</strong><br>
|
||||||
|
เว็บจะออนไลน์ที่ <code>https://<user>.github.io/GaC-srtool/</code> ไม่เสียค่าใช้จ่ายตลอดชีพ!
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Settings Modal -->
|
||||||
|
<div class="modal-overlay" id="modal-settings">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 style="font-weight: 700;">⚙️ ตั้งค่าการเชื่อมต่อ API Server</h3>
|
||||||
|
<button class="btn btn-sm btn-secondary" id="btn-close-modal">✕</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Server REST API URL</label>
|
||||||
|
<input type="text" id="modal-api-url" class="form-input">
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1rem;">
|
||||||
|
<button class="btn btn-secondary" id="btn-modal-cancel">ยกเลิก</button>
|
||||||
|
<button class="btn btn-primary" id="btn-modal-save">บันทึก & ทดสอบ</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast Notification Container -->
|
||||||
|
<div class="toast-container" id="toast-container"></div>
|
||||||
|
|
||||||
|
<!-- Script Modules -->
|
||||||
|
<script type="module" src="js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,663 @@
|
|||||||
|
import { AVATAR_METADATA, getAvatarInfo } from './avatars-data.js';
|
||||||
|
import { PRESET_TELEPORTS } from './teleports-data.js';
|
||||||
|
|
||||||
|
// Application State
|
||||||
|
const state = {
|
||||||
|
apiUrl: localStorage.getItem('gac_srtool_api_url') || 'http://localhost:21000',
|
||||||
|
pingIntervalSec: parseInt(localStorage.getItem('gac_srtool_ping_interval') || '10', 10),
|
||||||
|
isConnected: false,
|
||||||
|
statusData: null,
|
||||||
|
avatarsList: [],
|
||||||
|
currentJsonMode: 'freesr', // 'freesr' or 'persistent'
|
||||||
|
activeTab: 'tab-dashboard'
|
||||||
|
};
|
||||||
|
|
||||||
|
// UI Elements
|
||||||
|
const el = {
|
||||||
|
statusDot: document.getElementById('status-dot'),
|
||||||
|
connectionStatusText: document.getElementById('connection-status-text'),
|
||||||
|
btnQuickPing: document.getElementById('btn-quick-ping'),
|
||||||
|
btnOpenSettings: document.getElementById('btn-open-settings'),
|
||||||
|
modalSettings: document.getElementById('modal-settings'),
|
||||||
|
btnCloseModal: document.getElementById('btn-close-modal'),
|
||||||
|
btnModalCancel: document.getElementById('btn-modal-cancel'),
|
||||||
|
btnModalSave: document.getElementById('btn-modal-save'),
|
||||||
|
modalApiUrl: document.getElementById('modal-api-url'),
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
statAvatars: document.getElementById('stat-avatars'),
|
||||||
|
statLightcones: document.getElementById('stat-lightcones'),
|
||||||
|
statRelics: document.getElementById('stat-relics'),
|
||||||
|
lblApiUrl: document.getElementById('lbl-api-url'),
|
||||||
|
lblPlaneId: document.getElementById('lbl-plane-id'),
|
||||||
|
lblFloorId: document.getElementById('lbl-floor-id'),
|
||||||
|
lblCoordinates: document.getElementById('lbl-coordinates'),
|
||||||
|
lineupContainer: document.getElementById('lineup-container'),
|
||||||
|
btnQuickMaxAll: document.getElementById('btn-quick-max-all'),
|
||||||
|
btnQuickWarpExpress: document.getElementById('btn-quick-warp-express'),
|
||||||
|
btnQuickSyncPersistent: document.getElementById('btn-quick-sync-persistent'),
|
||||||
|
|
||||||
|
// Avatars
|
||||||
|
avatarsGrid: document.getElementById('avatars-grid'),
|
||||||
|
avatarSearch: document.getElementById('avatar-search'),
|
||||||
|
avatarFilterRarity: document.getElementById('avatar-filter-rarity'),
|
||||||
|
btnMaxAllAvatars: document.getElementById('btn-max-all-avatars'),
|
||||||
|
btnReloadAvatars: document.getElementById('btn-reload-avatars'),
|
||||||
|
|
||||||
|
// Ex-Rank
|
||||||
|
exrankList: document.getElementById('exrank-list'),
|
||||||
|
btnExrankEnableAll: document.getElementById('btn-exrank-enable-all'),
|
||||||
|
btnExrankDisableAll: document.getElementById('btn-exrank-disable-all'),
|
||||||
|
|
||||||
|
// Teleport
|
||||||
|
teleportPresetsList: document.getElementById('teleport-presets-list'),
|
||||||
|
formCustomTeleport: document.getElementById('form-custom-teleport'),
|
||||||
|
tpPlane: document.getElementById('tp-plane'),
|
||||||
|
tpFloor: document.getElementById('tp-floor'),
|
||||||
|
tpEntry: document.getElementById('tp-entry'),
|
||||||
|
tpX: document.getElementById('tp-x'),
|
||||||
|
tpY: document.getElementById('tp-y'),
|
||||||
|
tpZ: document.getElementById('tp-z'),
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
btnSelectFreesr: document.getElementById('btn-select-freesr'),
|
||||||
|
btnSelectPersistent: document.getElementById('btn-select-persistent'),
|
||||||
|
btnJsonBeautify: document.getElementById('btn-json-beautify'),
|
||||||
|
btnJsonDownload: document.getElementById('btn-json-download'),
|
||||||
|
btnJsonSave: document.getElementById('btn-json-save'),
|
||||||
|
jsonFileInput: document.getElementById('json-file-input'),
|
||||||
|
jsonEditor: document.getElementById('json-editor'),
|
||||||
|
jsonStatusInfo: document.getElementById('json-status-info'),
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
formSettings: document.getElementById('form-settings'),
|
||||||
|
settingApiUrl: document.getElementById('setting-api-url'),
|
||||||
|
settingPingInterval: document.getElementById('setting-ping-interval'),
|
||||||
|
btnResetSettings: document.getElementById('btn-reset-settings'),
|
||||||
|
|
||||||
|
toastContainer: document.getElementById('toast-container')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toast notification helper
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast ${type}`;
|
||||||
|
|
||||||
|
let icon = 'ℹ️';
|
||||||
|
if (type === 'success') icon = '✅';
|
||||||
|
if (type === 'error') icon = '❌';
|
||||||
|
|
||||||
|
toast.innerHTML = `<span>${icon}</span><span style="flex: 1;">${message}</span>`;
|
||||||
|
el.toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
toast.style.transform = 'translateY(10px)';
|
||||||
|
toast.style.transition = 'all 0.3s ease';
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, 3500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Fetch Helper
|
||||||
|
async function apiCall(endpoint, method = 'GET', data = null) {
|
||||||
|
const cleanBase = state.apiUrl.replace(/\/+$/, '');
|
||||||
|
const url = `${cleanBase}${endpoint}`;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data && (method === 'POST' || method === 'PUT')) {
|
||||||
|
options.body = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
||||||
|
options.signal = controller.signal;
|
||||||
|
|
||||||
|
const res = await fetch(url, options);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name === 'AbortError') {
|
||||||
|
throw new Error('Connection timed out');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update connection status UI
|
||||||
|
function setConnectionStatus(connected, extraText = '') {
|
||||||
|
state.isConnected = connected;
|
||||||
|
if (connected) {
|
||||||
|
el.statusDot.classList.add('connected');
|
||||||
|
el.connectionStatusText.textContent = extraText || 'เชื่อมต่อแล้ว (Online)';
|
||||||
|
el.connectionStatusText.style.color = 'var(--accent-green)';
|
||||||
|
} else {
|
||||||
|
el.statusDot.classList.remove('connected');
|
||||||
|
el.connectionStatusText.textContent = extraText || 'ออฟไลน์ (Offline)';
|
||||||
|
el.connectionStatusText.style.color = 'var(--accent-red)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh status from /api/status
|
||||||
|
async function refreshStatus() {
|
||||||
|
try {
|
||||||
|
const data = await apiCall('/api/status');
|
||||||
|
state.statusData = data;
|
||||||
|
setConnectionStatus(true);
|
||||||
|
|
||||||
|
el.lblApiUrl.textContent = state.apiUrl;
|
||||||
|
el.statAvatars.textContent = data.avatar_count ?? 0;
|
||||||
|
el.statLightcones.textContent = data.lightcone_count ?? 0;
|
||||||
|
el.statRelics.textContent = data.relic_count ?? 0;
|
||||||
|
|
||||||
|
if (data.scene) {
|
||||||
|
el.lblPlaneId.textContent = data.scene.plane_id || '-';
|
||||||
|
el.lblFloorId.textContent = data.scene.floor_id || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.position) {
|
||||||
|
const { x = 0, y = 0, z = 0 } = data.position;
|
||||||
|
el.lblCoordinates.textContent = `X: ${x}, Y: ${y}, Z: ${z}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLineup(data.lineups);
|
||||||
|
renderExRanks(data.ex_ranks || []);
|
||||||
|
} catch (err) {
|
||||||
|
setConnectionStatus(false, 'เชื่อมต่อไม่สำเร็จ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render current lineup cards
|
||||||
|
function renderLineup(lineups) {
|
||||||
|
if (!lineups || Object.keys(lineups).length === 0) {
|
||||||
|
el.lineupContainer.innerHTML = '<div style="color: var(--text-muted); grid-column: 1/-1;">ไม่มีข้อมูลทีม</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.lineupContainer.innerHTML = '';
|
||||||
|
for (let slot = 0; slot < 4; slot++) {
|
||||||
|
const avatarId = lineups[slot] || 0;
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'card';
|
||||||
|
card.style.padding = '0.85rem';
|
||||||
|
|
||||||
|
if (avatarId === 0) {
|
||||||
|
card.innerHTML = `
|
||||||
|
<div style="font-size: 0.75rem; color: var(--text-muted);">ตำแหน่งที่ ${slot + 1}</div>
|
||||||
|
<div style="font-weight: 600; color: var(--text-muted); margin-top: 0.25rem;">(ว่าง)</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
const info = getAvatarInfo(avatarId);
|
||||||
|
card.innerHTML = `
|
||||||
|
<div style="font-size: 0.75rem; color: var(--accent-cyan);">ตำแหน่งที่ ${slot + 1}</div>
|
||||||
|
<div style="font-weight: 700; margin-top: 0.25rem;">${info.name}</div>
|
||||||
|
<div style="font-size: 0.75rem; color: var(--text-muted);">${info.element} • ${info.path}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
el.lineupContainer.appendChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and Render Avatars
|
||||||
|
async function loadAvatars() {
|
||||||
|
try {
|
||||||
|
el.avatarsGrid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted);">กำลังโหลด...</div>';
|
||||||
|
const res = await apiCall('/api/freesr/avatars');
|
||||||
|
state.avatarsList = res.avatars || [];
|
||||||
|
renderAvatarsGrid();
|
||||||
|
} catch (err) {
|
||||||
|
el.avatarsGrid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; color: var(--accent-red);">ดึงข้อมูลตัวละครไม่สำเร็จ: ${err.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAvatarsGrid() {
|
||||||
|
const searchTerm = el.avatarSearch.value.trim().toLowerCase();
|
||||||
|
const rarityFilter = el.avatarFilterRarity.value;
|
||||||
|
|
||||||
|
const filtered = state.avatarsList.filter(item => {
|
||||||
|
const meta = getAvatarInfo(item.avatar_id);
|
||||||
|
const matchesSearch = meta.name.toLowerCase().includes(searchTerm) || String(item.avatar_id).includes(searchTerm);
|
||||||
|
const matchesRarity = rarityFilter === 'all' || String(meta.rarity) === rarityFilter;
|
||||||
|
return matchesSearch && matchesRarity;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
el.avatarsGrid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted); padding: 2rem;">ไม่พบตัวละครที่ตรงกับเงื่อนไข</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.avatarsGrid.innerHTML = '';
|
||||||
|
filtered.forEach(item => {
|
||||||
|
const meta = getAvatarInfo(item.avatar_id);
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'card avatar-card';
|
||||||
|
|
||||||
|
const pipsHtml = [0, 1, 2, 3, 4, 5, 6].map(rank => `
|
||||||
|
<button class="eidolon-pip ${item.rank >= rank ? 'active' : ''}" data-avatar="${item.avatar_id}" data-rank="${rank}" title="Eidolon ${rank}">
|
||||||
|
${rank}
|
||||||
|
</button>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<span class="avatar-badge-rarity rarity-${meta.rarity}">★ ${meta.rarity}</span>
|
||||||
|
<div>
|
||||||
|
<div class="avatar-header-info">
|
||||||
|
<div class="avatar-icon-placeholder">${meta.name.charAt(0)}</div>
|
||||||
|
<div>
|
||||||
|
<div class="avatar-name">${meta.name}</div>
|
||||||
|
<div class="avatar-subinfo">
|
||||||
|
<span>Lv. ${item.level}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>${meta.element}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>${meta.path}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="eidolon-selector">
|
||||||
|
<span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 600;">EIDOLON (E):</span>
|
||||||
|
<div class="eidolon-buttons">
|
||||||
|
${pipsHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Eidolon button clicks
|
||||||
|
card.querySelectorAll('.eidolon-pip').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const avatarId = parseInt(btn.getAttribute('data-avatar'), 10);
|
||||||
|
const rank = parseInt(btn.getAttribute('data-rank'), 10);
|
||||||
|
await setAvatarRank(avatarId, rank);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
el.avatarsGrid.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set avatar rank API
|
||||||
|
async function setAvatarRank(avatarId, rank) {
|
||||||
|
try {
|
||||||
|
await apiCall('/api/avatar/rank', 'POST', { avatar_id: avatarId, rank });
|
||||||
|
showToast(`ปรับตัวละคร ${avatarId} เป็น E${rank} สำเร็จ`, 'success');
|
||||||
|
|
||||||
|
// Update local state and re-render
|
||||||
|
const target = state.avatarsList.find(a => a.avatar_id === avatarId);
|
||||||
|
if (target) {
|
||||||
|
target.rank = rank;
|
||||||
|
renderAvatarsGrid();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max all avatars API
|
||||||
|
async function maxAllAvatars() {
|
||||||
|
if (!confirm('ยืนยันปรับตัวละครทั้งหมดให้เป็นเลเวล 80 และปลด E6 หรือไม่?')) return;
|
||||||
|
try {
|
||||||
|
await apiCall('/api/avatar/max_all', 'POST', {});
|
||||||
|
showToast('ปรับ Max ทุกตัวละครสำเร็จแล้ว!', 'success');
|
||||||
|
loadAvatars();
|
||||||
|
refreshStatus();
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Ex-Ranks
|
||||||
|
function renderExRanks(exList) {
|
||||||
|
if (!exList || exList.length === 0) {
|
||||||
|
el.exrankList.innerHTML = '<div style="color: var(--text-muted); text-align: center; padding: 1rem;">ไม่มีข้อมูลตัวละคร Ex-Rank</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.exrankList.innerHTML = '';
|
||||||
|
exList.forEach(item => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'ex-switch-card';
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 700; font-size: 1rem;">${item.name}</div>
|
||||||
|
<div style="font-size: 0.8rem; color: var(--text-muted);">
|
||||||
|
Avatar ID: ${item.avatar_id} • Ex-Rank Skill ID: ${item.ex_rank_id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" ${item.unlocked ? 'checked' : ''} data-avatar="${item.avatar_id}">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
|
||||||
|
card.querySelector('input[type="checkbox"]').addEventListener('change', async (e) => {
|
||||||
|
const unlocked = e.target.checked;
|
||||||
|
const avatarId = item.avatar_id;
|
||||||
|
try {
|
||||||
|
await apiCall('/api/exrank/toggle', 'POST', { avatar_id: avatarId, unlocked });
|
||||||
|
showToast(`${item.name}: ${unlocked ? 'เปิดใช้งาน' : 'ปิดใช้งาน'} Ex-Rank สำเร็จ`, 'success');
|
||||||
|
} catch (err) {
|
||||||
|
e.target.checked = !unlocked;
|
||||||
|
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
el.exrankList.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Teleport Presets
|
||||||
|
function renderTeleportPresets() {
|
||||||
|
el.teleportPresetsList.innerHTML = '';
|
||||||
|
PRESET_TELEPORTS.forEach(preset => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'card';
|
||||||
|
item.style.padding = '0.85rem';
|
||||||
|
item.style.display = 'flex';
|
||||||
|
item.style.justifyContent = 'space-between';
|
||||||
|
item.style.alignItems = 'center';
|
||||||
|
|
||||||
|
item.innerHTML = `
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 700; font-size: 0.95rem;">${preset.name}</div>
|
||||||
|
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.2rem;">
|
||||||
|
Plane: ${preset.plane_id} • Floor: ${preset.floor_id} • Entry: ${preset.entry_id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-primary btn-warp" style="white-space: nowrap;">
|
||||||
|
🚀 วาร์ป
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
item.querySelector('.btn-warp').addEventListener('click', async () => {
|
||||||
|
await executeTeleport({
|
||||||
|
plane_id: preset.plane_id,
|
||||||
|
floor_id: preset.floor_id,
|
||||||
|
entry_id: preset.entry_id,
|
||||||
|
x: preset.x,
|
||||||
|
y: preset.y,
|
||||||
|
z: preset.z
|
||||||
|
}, preset.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
el.teleportPresetsList.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute Teleport API
|
||||||
|
async function executeTeleport(payload, label = '') {
|
||||||
|
try {
|
||||||
|
await apiCall('/api/teleport', 'POST', payload);
|
||||||
|
showToast(`วาร์ปไปยัง ${label || 'จุดที่ระบุ'} สำเร็จ (บันทึกลง Persistent)`, 'success');
|
||||||
|
refreshStatus();
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`วาร์ปไม่สำเร็จ: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and Switch JSON
|
||||||
|
async function loadJsonData(mode) {
|
||||||
|
state.currentJsonMode = mode;
|
||||||
|
if (mode === 'freesr') {
|
||||||
|
el.btnSelectFreesr.className = 'btn btn-sm btn-primary active';
|
||||||
|
el.btnSelectPersistent.className = 'btn btn-sm btn-secondary';
|
||||||
|
el.jsonStatusInfo.textContent = 'กำลังเปิด: freesr-data.json';
|
||||||
|
try {
|
||||||
|
const res = await apiCall('/api/freesr');
|
||||||
|
el.jsonEditor.value = JSON.stringify(res.data, null, 2);
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`โหลด freesr-data ไม่สำเร็จ: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
el.btnSelectPersistent.className = 'btn btn-sm btn-primary active';
|
||||||
|
el.btnSelectFreesr.className = 'btn btn-sm btn-secondary';
|
||||||
|
el.jsonStatusInfo.textContent = 'กำลังเปิด: persistent';
|
||||||
|
try {
|
||||||
|
const res = await apiCall('/api/persistent');
|
||||||
|
el.jsonEditor.value = JSON.stringify(res.persistent, null, 2);
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`โหลด persistent ไม่สำเร็จ: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save JSON to Server
|
||||||
|
async function saveJsonData() {
|
||||||
|
const text = el.jsonEditor.value.trim();
|
||||||
|
if (!text) {
|
||||||
|
showToast('ไม่มีข้อมูล JSON ให้บันทึก', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(text);
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`JSON ไม่ถูกต้อง: ${err.message}`, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = state.currentJsonMode === 'freesr' ? '/api/freesr' : '/api/persistent';
|
||||||
|
try {
|
||||||
|
await apiCall(endpoint, 'POST', parsed);
|
||||||
|
showToast(`บันทึกไฟล์ ${state.currentJsonMode} เรียบร้อยแล้ว`, 'success');
|
||||||
|
refreshStatus();
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`บันทึกไม่สำเร็จ: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Switching
|
||||||
|
function setupTabs() {
|
||||||
|
const tabs = document.querySelectorAll('.tab-btn');
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
tabs.forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||||
|
|
||||||
|
tab.classList.add('active');
|
||||||
|
const targetId = tab.getAttribute('data-tab');
|
||||||
|
const targetContent = document.getElementById(targetId);
|
||||||
|
if (targetContent) {
|
||||||
|
targetContent.classList.add('active');
|
||||||
|
state.activeTab = targetId;
|
||||||
|
|
||||||
|
// On-demand tab refresh
|
||||||
|
if (targetId === 'tab-avatars' && state.avatarsList.length === 0) {
|
||||||
|
loadAvatars();
|
||||||
|
} else if (targetId === 'tab-json') {
|
||||||
|
loadJsonData(state.currentJsonMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Listeners setup
|
||||||
|
function setupEvents() {
|
||||||
|
// Quick Ping & Refresh
|
||||||
|
el.btnQuickPing.addEventListener('click', () => {
|
||||||
|
showToast('กำลังตรวจสอบการเชื่อมต่อ...', 'info');
|
||||||
|
refreshStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Settings Modal
|
||||||
|
el.btnOpenSettings.addEventListener('click', () => {
|
||||||
|
el.modalApiUrl.value = state.apiUrl;
|
||||||
|
el.modalSettings.classList.add('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
el.btnCloseModal.addEventListener('click', () => el.modalSettings.classList.remove('active'));
|
||||||
|
el.btnModalCancel.addEventListener('click', () => el.modalSettings.classList.remove('active'));
|
||||||
|
|
||||||
|
el.btnModalSave.addEventListener('click', () => {
|
||||||
|
const newUrl = el.modalApiUrl.value.trim();
|
||||||
|
if (newUrl) {
|
||||||
|
state.apiUrl = newUrl;
|
||||||
|
localStorage.setItem('gac_srtool_api_url', newUrl);
|
||||||
|
el.modalSettings.classList.remove('active');
|
||||||
|
showToast('บันทึกการตั้งค่าแล้ว กำลังเชื่อมต่อใหม่...', 'info');
|
||||||
|
refreshStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quick Action Buttons
|
||||||
|
el.btnQuickMaxAll.addEventListener('click', maxAllAvatars);
|
||||||
|
el.btnMaxAllAvatars.addEventListener('click', maxAllAvatars);
|
||||||
|
|
||||||
|
el.btnQuickWarpExpress.addEventListener('click', () => {
|
||||||
|
const express = PRESET_TELEPORTS[0];
|
||||||
|
executeTeleport(express, express.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
el.btnQuickSyncPersistent.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await apiCall('/api/persistent');
|
||||||
|
showToast('ซิงค์ข้อมูล Persistent เรียบร้อย', 'success');
|
||||||
|
refreshStatus();
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`ซิงค์ไม่สำเร็จ: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Avatar Search & Filter
|
||||||
|
el.avatarSearch.addEventListener('input', renderAvatarsGrid);
|
||||||
|
el.avatarFilterRarity.addEventListener('change', renderAvatarsGrid);
|
||||||
|
el.btnReloadAvatars.addEventListener('click', loadAvatars);
|
||||||
|
|
||||||
|
// Ex-Rank All Controls
|
||||||
|
el.btnExrankEnableAll.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await apiCall('/api/exrank/toggle', 'POST', { all: true, unlocked: true });
|
||||||
|
showToast('เปิดใช้งาน Ex-Rank ทุกตัวละครสำเร็จ', 'success');
|
||||||
|
refreshStatus();
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
el.btnExrankDisableAll.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await apiCall('/api/exrank/toggle', 'POST', { all: true, unlocked: false });
|
||||||
|
showToast('ปิดใช้งาน Ex-Rank ทุกตัวละครสำเร็จ', 'success');
|
||||||
|
refreshStatus();
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom Teleport Form
|
||||||
|
el.formCustomTeleport.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const payload = {
|
||||||
|
plane_id: parseInt(el.tpPlane.value, 10),
|
||||||
|
floor_id: parseInt(el.tpFloor.value, 10),
|
||||||
|
entry_id: parseInt(el.tpEntry.value, 10),
|
||||||
|
x: el.tpX.value ? parseInt(el.tpX.value, 10) : 0,
|
||||||
|
y: el.tpY.value ? parseInt(el.tpY.value, 10) : 0,
|
||||||
|
z: el.tpZ.value ? parseInt(el.tpZ.value, 10) : 0
|
||||||
|
};
|
||||||
|
executeTeleport(payload, `Plane ${payload.plane_id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// JSON Tab Controls
|
||||||
|
el.btnSelectFreesr.addEventListener('click', () => loadJsonData('freesr'));
|
||||||
|
el.btnSelectPersistent.addEventListener('click', () => loadJsonData('persistent'));
|
||||||
|
|
||||||
|
el.btnJsonBeautify.addEventListener('click', () => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(el.jsonEditor.value);
|
||||||
|
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
|
||||||
|
showToast('จัดรูปแบบ JSON สวยงามแล้ว', 'info');
|
||||||
|
} catch (err) {
|
||||||
|
showToast('JSON ไม่ถูกต้อง ไม่สามารถจัดรูปแบบได้', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
el.btnJsonSave.addEventListener('click', saveJsonData);
|
||||||
|
|
||||||
|
el.btnJsonDownload.addEventListener('click', () => {
|
||||||
|
const text = el.jsonEditor.value;
|
||||||
|
const blob = new Blob([text], { type: 'application/json' });
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = `${state.currentJsonMode}-${Date.now()}.json`;
|
||||||
|
a.click();
|
||||||
|
showToast(`ดาวน์โหลด backup ไฟล์ ${a.download} แล้ว`, 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
el.jsonFileInput.addEventListener('change', (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(event.target.result);
|
||||||
|
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
|
||||||
|
showToast(`โหลดเนื้อหาไฟล์ ${file.name} เข้าสู่อิดิเตอร์แล้ว`, 'success');
|
||||||
|
} catch (err) {
|
||||||
|
showToast('ไฟล์ไม่ใช่ JSON ที่ถูกต้อง', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Settings Form
|
||||||
|
el.settingApiUrl.value = state.apiUrl;
|
||||||
|
el.settingPingInterval.value = state.pingIntervalSec;
|
||||||
|
|
||||||
|
el.formSettings.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
state.apiUrl = el.settingApiUrl.value.trim();
|
||||||
|
state.pingIntervalSec = parseInt(el.settingPingInterval.value, 10) || 10;
|
||||||
|
|
||||||
|
localStorage.setItem('gac_srtool_api_url', state.apiUrl);
|
||||||
|
localStorage.setItem('gac_srtool_ping_interval', state.pingIntervalSec);
|
||||||
|
|
||||||
|
showToast('บันทึกการตั้งค่าสำเร็จ!', 'success');
|
||||||
|
refreshStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
el.btnResetSettings.addEventListener('click', () => {
|
||||||
|
state.apiUrl = 'http://localhost:21000';
|
||||||
|
state.pingIntervalSec = 10;
|
||||||
|
el.settingApiUrl.value = state.apiUrl;
|
||||||
|
el.settingPingInterval.value = state.pingIntervalSec;
|
||||||
|
localStorage.removeItem('gac_srtool_api_url');
|
||||||
|
localStorage.removeItem('gac_srtool_ping_interval');
|
||||||
|
showToast('คืนค่าเริ่มต้นเรียบร้อย', 'info');
|
||||||
|
refreshStatus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background ping loop
|
||||||
|
function startPingLoop() {
|
||||||
|
setInterval(() => {
|
||||||
|
refreshStatus();
|
||||||
|
}, state.pingIntervalSec * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Application
|
||||||
|
function init() {
|
||||||
|
setupTabs();
|
||||||
|
setupEvents();
|
||||||
|
renderTeleportPresets();
|
||||||
|
refreshStatus();
|
||||||
|
startPingLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run on DOM load
|
||||||
|
window.addEventListener('DOMContentLoaded', init);
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// Metadata dictionary for Star Rail Avatars / Characters
|
||||||
|
export const AVATAR_METADATA = {
|
||||||
|
// Trailblazers
|
||||||
|
8001: { name: "Trailblazer (Physical)", path: "Destruction", element: "Physical", rarity: 5 },
|
||||||
|
8002: { name: "Trailblazer (Physical ♀)", path: "Destruction", element: "Physical", rarity: 5 },
|
||||||
|
8003: { name: "Trailblazer (Fire)", path: "Preservation", element: "Fire", rarity: 5 },
|
||||||
|
8004: { name: "Trailblazer (Fire ♀)", path: "Preservation", element: "Fire", rarity: 5 },
|
||||||
|
8005: { name: "Trailblazer (Harmony)", path: "Harmony", element: "Imaginary", rarity: 5 },
|
||||||
|
8006: { name: "Trailblazer (Harmony ♀)", path: "Harmony", element: "Imaginary", rarity: 5 },
|
||||||
|
8007: { name: "Trailblazer (Remembrance)", path: "Remembrance", element: "Ice", rarity: 5 },
|
||||||
|
8008: { name: "Trailblazer (Remembrance ♀)", path: "Remembrance", element: "Ice", rarity: 5 },
|
||||||
|
|
||||||
|
// Astral Express & Herta Station
|
||||||
|
1001: { name: "March 7th", path: "Preservation", element: "Ice", rarity: 4 },
|
||||||
|
1224: { name: "March 7th (Hunt)", path: "Hunt", element: "Imaginary", rarity: 4 },
|
||||||
|
1002: { name: "Dan Heng", path: "Hunt", element: "Wind", rarity: 4 },
|
||||||
|
1003: { name: "Himeko", path: "Erudition", element: "Fire", rarity: 5 },
|
||||||
|
1004: { name: "Welt", path: "Nihility", element: "Imaginary", rarity: 5 },
|
||||||
|
1008: { name: "Arlan", path: "Destruction", element: "Lightning", rarity: 4 },
|
||||||
|
1009: { name: "Asta", path: "Harmony", element: "Fire", rarity: 4 },
|
||||||
|
1013: { name: "Herta", path: "Erudition", element: "Ice", rarity: 4 },
|
||||||
|
|
||||||
|
// Belobog
|
||||||
|
1101: { name: "Bronya", path: "Harmony", element: "Wind", rarity: 5 },
|
||||||
|
1102: { name: "Seele", path: "Hunt", element: "Quantum", rarity: 5 },
|
||||||
|
1103: { name: "Serval", path: "Erudition", element: "Lightning", rarity: 4 },
|
||||||
|
1104: { name: "Gepard", path: "Preservation", element: "Ice", rarity: 5 },
|
||||||
|
1105: { name: "Natasha", path: "Abundance", element: "Physical", rarity: 4 },
|
||||||
|
1106: { name: "Pela", path: "Nihility", element: "Ice", rarity: 4 },
|
||||||
|
1107: { name: "Clara", path: "Destruction", element: "Physical", rarity: 5 },
|
||||||
|
1108: { name: "Sampo", path: "Nihility", element: "Wind", rarity: 4 },
|
||||||
|
1109: { name: "Hook", path: "Destruction", element: "Fire", rarity: 4 },
|
||||||
|
1110: { name: "Lynx", path: "Abundance", element: "Quantum", rarity: 4 },
|
||||||
|
1111: { name: "Luka", path: "Nihility", element: "Physical", rarity: 4 },
|
||||||
|
1112: { name: "Topaz & Numby", path: "Hunt", element: "Fire", rarity: 5 },
|
||||||
|
|
||||||
|
// Xianzhou Luofu
|
||||||
|
1201: { name: "Qingque", path: "Erudition", element: "Quantum", rarity: 4 },
|
||||||
|
1202: { name: "Tingyun", path: "Harmony", element: "Lightning", rarity: 4 },
|
||||||
|
1203: { name: "Luocha", path: "Abundance", element: "Imaginary", rarity: 5 },
|
||||||
|
1204: { name: "Jing Yuan", path: "Erudition", element: "Lightning", rarity: 5, hasExRank: true },
|
||||||
|
1205: { name: "Blade", path: "Destruction", element: "Wind", rarity: 5 },
|
||||||
|
1206: { name: "Sushang", path: "Hunt", element: "Physical", rarity: 4 },
|
||||||
|
1207: { name: "Yukong", path: "Harmony", element: "Imaginary", rarity: 4 },
|
||||||
|
1208: { name: "Fu Xuan", path: "Preservation", element: "Quantum", rarity: 5 },
|
||||||
|
1209: { name: "Yanqing", path: "Hunt", element: "Ice", rarity: 5 },
|
||||||
|
1210: { name: "Guinaifen", path: "Nihility", element: "Fire", rarity: 4 },
|
||||||
|
1211: { name: "Bailu", path: "Abundance", element: "Lightning", rarity: 5 },
|
||||||
|
1212: { name: "Jingliu", path: "Destruction", element: "Ice", rarity: 5 },
|
||||||
|
1213: { name: "Dan Heng • Imbibitor Lunae", path: "Destruction", element: "Imaginary", rarity: 5, hasExRank: true },
|
||||||
|
1214: { name: "Xueyi", path: "Destruction", element: "Quantum", rarity: 4 },
|
||||||
|
1215: { name: "Hanya", path: "Harmony", element: "Physical", rarity: 4 },
|
||||||
|
1217: { name: "Huohuo", path: "Abundance", element: "Wind", rarity: 5 },
|
||||||
|
1218: { name: "Jiaoqiu", path: "Nihility", element: "Fire", rarity: 5 },
|
||||||
|
1220: { name: "Feixiao", path: "Hunt", element: "Wind", rarity: 5 },
|
||||||
|
1221: { name: "Yunli", path: "Destruction", element: "Physical", rarity: 5 },
|
||||||
|
1222: { name: "Lingsha", path: "Abundance", element: "Fire", rarity: 5 },
|
||||||
|
1223: { name: "Moze", path: "Hunt", element: "Lightning", rarity: 4 },
|
||||||
|
1225: { name: "Fugue", path: "Nihility", element: "Fire", rarity: 5 },
|
||||||
|
|
||||||
|
// Penacony & Stellar Hunters
|
||||||
|
1005: { name: "Kafka", path: "Nihility", element: "Lightning", rarity: 5 },
|
||||||
|
1006: { name: "Silver Wolf", path: "Nihility", element: "Quantum", rarity: 5 },
|
||||||
|
1301: { name: "Gallagher", path: "Abundance", element: "Fire", rarity: 4 },
|
||||||
|
1302: { name: "Argenti", path: "Erudition", element: "Physical", rarity: 5 },
|
||||||
|
1303: { name: "Ruan Mei", path: "Harmony", element: "Ice", rarity: 5 },
|
||||||
|
1304: { name: "Aventurine", path: "Preservation", element: "Imaginary", rarity: 5 },
|
||||||
|
1305: { name: "Dr. Ratio", path: "Hunt", element: "Imaginary", rarity: 5 },
|
||||||
|
1306: { name: "Sparkle", path: "Harmony", element: "Quantum", rarity: 5 },
|
||||||
|
1307: { name: "Black Swan", path: "Nihility", element: "Wind", rarity: 5 },
|
||||||
|
1308: { name: "Acheron", path: "Nihility", element: "Lightning", rarity: 5, hasExRank: true },
|
||||||
|
1309: { name: "Robin", path: "Harmony", element: "Physical", rarity: 5 },
|
||||||
|
1310: { name: "Firefly", path: "Destruction", element: "Fire", rarity: 5 },
|
||||||
|
1312: { name: "Misha", path: "Destruction", element: "Ice", rarity: 4 },
|
||||||
|
1313: { name: "Sunday", path: "Harmony", element: "Imaginary", rarity: 5 },
|
||||||
|
1314: { name: "Jade", path: "Erudition", element: "Quantum", rarity: 5 },
|
||||||
|
1315: { name: "Boothill", path: "Hunt", element: "Physical", rarity: 5 },
|
||||||
|
1317: { name: "Rappa", path: "Erudition", element: "Imaginary", rarity: 5 },
|
||||||
|
|
||||||
|
// Amphoreus / 3.x+
|
||||||
|
1401: { name: "Aglaea", path: "Remembrance", element: "Lightning", rarity: 5 },
|
||||||
|
1402: { name: "The Herta", path: "Erudition", element: "Ice", rarity: 5 },
|
||||||
|
1403: { name: "Tribbie", path: "Harmony", element: "Quantum", rarity: 5 },
|
||||||
|
1404: { name: "Mydei", path: "Destruction", element: "Imaginary", rarity: 5 },
|
||||||
|
1405: { name: "Anaxa", path: "Erudition", element: "Wind", rarity: 5 },
|
||||||
|
1407: { name: "Castorice", path: "Remembrance", element: "Quantum", rarity: 5 },
|
||||||
|
1408: { name: "Cipher", path: "Nihility", element: "Imaginary", rarity: 5 },
|
||||||
|
1409: { name: "Phainon", path: "Destruction", element: "Physical", rarity: 5 },
|
||||||
|
1410: { name: "Hyacine", path: "Remembrance", element: "Wind", rarity: 5 }
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getAvatarInfo(avatarId) {
|
||||||
|
return AVATAR_METADATA[avatarId] || {
|
||||||
|
name: `Avatar ${avatarId}`,
|
||||||
|
path: "Unknown",
|
||||||
|
element: "Unknown",
|
||||||
|
rarity: 4
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// Safe preset teleports for Star Rail maps
|
||||||
|
export const PRESET_TELEPORTS = [
|
||||||
|
{
|
||||||
|
name: "Astral Express - Parlor Car (รถไฟขบวนแอสทรัล)",
|
||||||
|
world: "Astral Express",
|
||||||
|
plane_id: 20001,
|
||||||
|
floor_id: 20001001,
|
||||||
|
entry_id: 2000101,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
z: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Herta Space Station - Master Control Zone (สถานีอวกาศ Herta - โซนควบคุมหลัก)",
|
||||||
|
world: "Herta Space Station",
|
||||||
|
plane_id: 10001,
|
||||||
|
floor_id: 10001001,
|
||||||
|
entry_id: 1000101,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
z: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jarilo-VI - Administrative District (เขตการปกครอง)",
|
||||||
|
world: "Jarilo-VI (Belobog)",
|
||||||
|
plane_id: 10101,
|
||||||
|
floor_id: 10101001,
|
||||||
|
entry_id: 1010101,
|
||||||
|
x: 10250,
|
||||||
|
y: 51,
|
||||||
|
z: 32670
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jarilo-VI - Boulder Town (เมือง Boulder)",
|
||||||
|
world: "Jarilo-VI (Underworld)",
|
||||||
|
plane_id: 10102,
|
||||||
|
floor_id: 10102001,
|
||||||
|
entry_id: 1010201,
|
||||||
|
x: -20560,
|
||||||
|
y: -286,
|
||||||
|
z: 57320
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Xianzhou Luofu - Central Starskiff Haven (ท่าเรือเรือบินดาวกลาง)",
|
||||||
|
world: "Xianzhou Luofu",
|
||||||
|
plane_id: 10202,
|
||||||
|
floor_id: 10202001,
|
||||||
|
entry_id: 1020201,
|
||||||
|
x: 8320,
|
||||||
|
y: -3486,
|
||||||
|
z: -25080
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Penacony - The Reverie (Dreamscape Hotel)",
|
||||||
|
world: "Penacony",
|
||||||
|
plane_id: 10301,
|
||||||
|
floor_id: 10301001,
|
||||||
|
entry_id: 1030101,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
z: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Penacony - Golden Hour (ชั่วโมงทอง)",
|
||||||
|
world: "Penacony",
|
||||||
|
plane_id: 10302,
|
||||||
|
floor_id: 10302001,
|
||||||
|
entry_id: 1030201,
|
||||||
|
x: -3556,
|
||||||
|
y: 1539,
|
||||||
|
z: -378220
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "gac-srtool",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Companion Web Management Dashboard & FreeSR Tool for GaC-hkrpg-go",
|
||||||
|
"main": "index.html",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "npx serve . -l 5173",
|
||||||
|
"start": "npx serve . -l 5173",
|
||||||
|
"serve": "npx serve . -l 5173"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"hkrpg",
|
||||||
|
"star-rail",
|
||||||
|
"gac",
|
||||||
|
"freesr",
|
||||||
|
"srtool",
|
||||||
|
"github-pages"
|
||||||
|
],
|
||||||
|
"author": "CAtlas0784",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
Reference in new issue
Block a user