From 08e2440ca53f1753f759732d2040e256b5b9568c Mon Sep 17 00:00:00 2001 From: CAtlas0784 Date: Fri, 16 Oct 2026 22:03:47 +0700 Subject: [PATCH] feat: initial commit for GaC-srtool dashboard --- .github/workflows/deploy.yml | 34 ++ .gitignore | 24 ++ README.md | 89 +++++ css/styles.css | 652 ++++++++++++++++++++++++++++++++++ index.html | 333 ++++++++++++++++++ js/app.js | 663 +++++++++++++++++++++++++++++++++++ js/avatars-data.js | 99 ++++++ js/teleports-data.js | 73 ++++ package.json | 21 ++ 9 files changed, 1988 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 css/styles.css create mode 100644 index.html create mode 100644 js/app.js create mode 100644 js/avatars-data.js create mode 100644 js/teleports-data.js create mode 100644 package.json diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..b666fc7 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14f186c --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a194cc7 --- /dev/null +++ b/README.md @@ -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` diff --git a/css/styles.css b/css/styles.css new file mode 100644 index 0000000..87c6142 --- /dev/null +++ b/css/styles.css @@ -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; + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..0d81c06 --- /dev/null +++ b/index.html @@ -0,0 +1,333 @@ + + + + + + GaC-Srtool — FreeSR & Game Management Dashboard + + + + + + +
+
+
✦
+
+
GaC-Srtool
+
Star Rail FreeSR & Server Controller
+
+
+ +
+
+ + กำลังตรวจสอบ... +
+ +
+
+ + + + + +
+ + +
+
+
+
0
+
ตัวละครที่ครอบครอง (Avatars)
+
+
+
0
+
กรวยแสง (Lightcones)
+
+
+
0
+
รีลิกส์ (Relics)
+
+
+ +
+ +
+
+ 📍 สถานะเซิร์ฟเวอร์ & ตำแหน่งปัจจุบัน + API Live +
+
+
+ API Gateway Endpoint + - +
+
+
+ Plane ID + - +
+
+ Floor ID + - +
+
+
+ พิกัดตัวละคร (X, Y, Z) + - +
+
+
+ + +
+
⚡ ดำเนินการด่วน (Quick Actions)
+

สั่งการส่งไปยัง REST API Gateway ทันที

+
+ + + +
+
+
+ + +
+
🎮 ทีมปัจจุบัน (Current Lineup)
+
+
กำลังโหลดข้อมูลทีม...
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+ กำลังดึงข้อมูลตัวละครจากเซิร์ฟเวอร์... +
+
+
+ + +
+
+
+ ⚡ ระบบปลดล็อค Ex-Rank Skill +
+ + +
+
+

+ เปิดใช้งานพรสวรรค์และกลไกพิเศษ Ex-Rank สำหรับตัวละครเฉพาะใน FreeSR (เช่น Jing Yuan โดน CC ไม่หยุดชะงัก, Dan Heng IL, Acheron) +

+
+ +
+
+ กำลังโหลดข้อมูล Ex-Rank... +
+
+
+ + +
+
+ +
+
📍 จุดวาร์ปปลอดภัยยอดนิยม (Safe Presets)
+

คลิกจุดวาร์ปเพื่อย้ายฉากของผู้เล่นในไฟล์เซฟได้ทันที

+
+ +
+
+ + +
+
🎯 กำหนดพิกัดวาร์ปเอง (Custom Teleport)
+

ใส่ PlaneID, FloorID และ EntryID ตามต้องการ

+ +
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + + + +
+
+ +
+ +
+ พร้อมใช้งาน + Tip: สามารถแก้ไขค่าดิบแล้วกด 'บันทึกไปยังเซิร์ฟเวอร์' ได้ทันที +
+
+
+
+ + +
+
+ +
+
⚙️ ตั้งค่าการเชื่อมต่อ API Gateway
+

ปรับเปลี่ยนพอร์ตหรือ IP ได้ตามไฟล์ server.json ของคุณ

+ +
+
+ + + + * ค่าเริ่มต้นของ GaC-hkrpg-go คือพอร์ต 21000 (SDK server) + +
+
+ + +
+
+ + +
+
+
+ + +
+
🌐 วิธีนำขึ้นโฮสต์ GitHub Pages (ฟรี 100%)
+

สามารถเปิดใช้งานเว็บนี้บนอินเทอร์เน็ตได้โดยไม่ต้องเสียค่าโฮสต์ใดๆ

+ +
    +
  1. + 1. สร้าง Repository บน GitHub:
    + สร้าง GitHub Repo ใหม่ (Public หรือ Private ก็ได้) เช่นชื่อ GaC-srtool +
  2. +
  3. + 2. Push โฟลเดอร์นี้ขึ้น GitHub:
    + git remote add origin https://github.com/<user>/GaC-srtool.git
    + git add . && git commit -m "Init GaC-srtool" && git push -u origin main +
  4. +
  5. + 3. เปิดใช้งาน GitHub Pages:
    + เข้าไปที่ Repo บน GitHub > Settings > Pages
    + ใต้หัวข้อ Build and deployment เลือก Source เป็น Deploy from a branch > เลือก Branch: main, Folder: / (root) > กด Save +
  6. +
  7. + 4. ใช้งานได้ทันที:
    + เว็บจะออนไลน์ที่ https://<user>.github.io/GaC-srtool/ ไม่เสียค่าใช้จ่ายตลอดชีพ! +
  8. +
+
+
+
+ +
+ + + + + +
+ + + + + diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..14cf0f8 --- /dev/null +++ b/js/app.js @@ -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 = `${icon}${message}`; + 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 = '
ไม่มีข้อมูลทีม
'; + 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 = ` +
ตำแหน่งที่ ${slot + 1}
+
(ว่าง)
+ `; + } else { + const info = getAvatarInfo(avatarId); + card.innerHTML = ` +
ตำแหน่งที่ ${slot + 1}
+
${info.name}
+
${info.element} • ${info.path}
+ `; + } + el.lineupContainer.appendChild(card); + } +} + +// Load and Render Avatars +async function loadAvatars() { + try { + el.avatarsGrid.innerHTML = '
กำลังโหลด...
'; + const res = await apiCall('/api/freesr/avatars'); + state.avatarsList = res.avatars || []; + renderAvatarsGrid(); + } catch (err) { + el.avatarsGrid.innerHTML = `
ดึงข้อมูลตัวละครไม่สำเร็จ: ${err.message}
`; + } +} + +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 = '
ไม่พบตัวละครที่ตรงกับเงื่อนไข
'; + 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 => ` + + `).join(''); + + card.innerHTML = ` + ★ ${meta.rarity} +
+
+
${meta.name.charAt(0)}
+
+
${meta.name}
+
+ Lv. ${item.level} + • + ${meta.element} + • + ${meta.path} +
+
+
+
+ +
+ EIDOLON (E): +
+ ${pipsHtml} +
+
+ `; + + // 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 = '
ไม่มีข้อมูลตัวละคร Ex-Rank
'; + return; + } + + el.exrankList.innerHTML = ''; + exList.forEach(item => { + const card = document.createElement('div'); + card.className = 'ex-switch-card'; + + card.innerHTML = ` +
+
${item.name}
+
+ Avatar ID: ${item.avatar_id} • Ex-Rank Skill ID: ${item.ex_rank_id} +
+
+ + `; + + 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 = ` +
+
${preset.name}
+
+ Plane: ${preset.plane_id} • Floor: ${preset.floor_id} • Entry: ${preset.entry_id} +
+
+ + `; + + 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); diff --git a/js/avatars-data.js b/js/avatars-data.js new file mode 100644 index 0000000..469f01d --- /dev/null +++ b/js/avatars-data.js @@ -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 + }; +} diff --git a/js/teleports-data.js b/js/teleports-data.js new file mode 100644 index 0000000..c382685 --- /dev/null +++ b/js/teleports-data.js @@ -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 + } +]; diff --git a/package.json b/package.json new file mode 100644 index 0000000..9870146 --- /dev/null +++ b/package.json @@ -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" +}