feat: complete SrTool overhaul with lineup builder, character build modal, equipment, and FireflyGo/RobinSR modes

This commit is contained in:
CAtlas0784 committed 2026-10-16 22:37:28 +07:00
1 parent 5cce9bdd04
commit e1667fa109
3 files changed
+1067 -254

No files matched your search

+264 -95
View File
@@ -1,10 +1,10 @@
:root {
--bg-primary: #0a0d14;
--bg-secondary: #121826;
--bg-card: rgba(22, 30, 48, 0.7);
--bg-card-hover: rgba(30, 41, 66, 0.85);
--bg-primary: #080a10;
--bg-secondary: #0f1422;
--bg-card: rgba(18, 24, 38, 0.75);
--bg-card-hover: rgba(26, 35, 56, 0.9);
--border-color: rgba(99, 120, 160, 0.25);
--border-focus: #4ef2bb;
--border-focus: #38bdf8;
--text-main: #f0f4fc;
--text-muted: #8e9bb5;
@@ -13,18 +13,18 @@
--accent-cyan: #38bdf8;
--accent-green: #34d399;
--accent-gold: #fbbf24;
--accent-purple: #a855f7;
--accent-purple: #c084fc;
--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);
--glow-cyan: 0 0 20px rgba(56, 189, 248, 0.35);
--glow-gold: 0 0 20px rgba(251, 191, 36, 0.35);
--glow-purple: 0 0 20px rgba(192, 132, 252, 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);
--transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
* {
@@ -37,8 +37,9 @@ 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%);
radial-gradient(circle at 10% 10%, rgba(56, 189, 248, 0.08) 0%, transparent 40%),
radial-gradient(circle at 90% 90%, rgba(168, 85, 247, 0.08) 0%, transparent 45%),
linear-gradient(180deg, rgba(8, 10, 16, 0.95) 0%, rgba(12, 16, 26, 0.98) 100%);
color: var(--text-main);
min-height: 100vh;
display: flex;
@@ -52,8 +53,8 @@ body {
position: sticky;
top: 0;
z-index: 50;
background: rgba(10, 13, 20, 0.85);
backdrop-filter: blur(14px);
background: rgba(8, 10, 16, 0.88);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--border-color);
padding: 0.75rem 1.5rem;
display: flex;
@@ -65,28 +66,29 @@ body {
.brand {
display: flex;
align-items: center;
gap: 0.75rem;
gap: 0.85rem;
}
.brand-icon {
width: 38px;
height: 38px;
width: 42px;
height: 42px;
border-radius: var(--radius-md);
background: linear-gradient(135deg, #38bdf8, #818cf8);
background: linear-gradient(135deg, #0284c7, #6366f1);
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--glow-cyan);
font-size: 1.25rem;
font-size: 1.35rem;
font-weight: bold;
color: #fff;
border: 1px solid rgba(56, 189, 248, 0.4);
}
.brand-title {
font-size: 1.25rem;
font-weight: 700;
font-size: 1.35rem;
font-weight: 800;
letter-spacing: -0.025em;
background: linear-gradient(90deg, #ffffff, #cbd5e1);
background: linear-gradient(90deg, #ffffff, #cbd5e1, #38bdf8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
@@ -95,25 +97,55 @@ body {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
letter-spacing: 0.06em;
}
.header-controls {
display: flex;
align-items: center;
gap: 0.85rem;
gap: 0.75rem;
flex-wrap: wrap;
}
/* Server Mode Selector */
.server-mode-badge {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 0.85rem;
background: rgba(2, 132, 199, 0.15);
border: 1px solid rgba(56, 189, 248, 0.4);
border-radius: 9999px;
font-size: 0.825rem;
font-weight: 700;
color: var(--accent-cyan);
}
.server-mode-select {
background: transparent;
border: none;
color: inherit;
font-size: inherit;
font-weight: inherit;
cursor: pointer;
outline: none;
}
.server-mode-select option {
background: var(--bg-secondary);
color: var(--text-main);
}
.connection-pill {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.75rem;
padding: 0.4rem 0.85rem;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 9999px;
font-size: 0.8rem;
font-weight: 500;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
}
@@ -123,8 +155,8 @@ body {
}
.status-dot {
width: 8px;
height: 8px;
width: 9px;
height: 9px;
border-radius: 50%;
background-color: var(--accent-red);
box-shadow: 0 0 8px var(--accent-red);
@@ -133,13 +165,13 @@ body {
.status-dot.connected {
background-color: var(--accent-green);
box-shadow: var(--glow-green);
box-shadow: 0 0 10px var(--accent-green);
}
/* Navigation Tabs */
.nav-tabs {
display: flex;
gap: 0.5rem;
gap: 0.35rem;
background: var(--bg-secondary);
padding: 0.5rem 1.5rem;
border-bottom: 1px solid var(--border-color);
@@ -150,7 +182,7 @@ body {
background: transparent;
border: none;
color: var(--text-muted);
padding: 0.5rem 1rem;
padding: 0.6rem 1.1rem;
border-radius: var(--radius-sm);
font-size: 0.875rem;
font-weight: 600;
@@ -164,21 +196,21 @@ body {
.tab-btn:hover {
color: var(--text-main);
background: rgba(255, 255, 255, 0.04);
background: rgba(255, 255, 255, 0.05);
}
.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);
border: 1px solid rgba(56, 189, 248, 0.35);
box-shadow: 0 0 12px rgba(56, 189, 248, 0.2);
}
/* Container */
/* Main Container */
.main-container {
flex: 1;
padding: 1.5rem;
max-width: 1300px;
max-width: 1400px;
width: 100%;
margin: 0 auto;
}
@@ -197,7 +229,7 @@ body {
to { opacity: 1; transform: translateY(0); }
}
/* Cards & Grid */
/* Grids */
.grid-2 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
@@ -212,13 +244,14 @@ body {
.grid-4 {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1.1rem;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.25rem;
}
/* Cards */
.card {
background: var(--bg-card);
backdrop-filter: blur(12px);
backdrop-filter: blur(14px);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 1.25rem;
@@ -229,8 +262,8 @@ body {
.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);
border-color: rgba(99, 120, 160, 0.5);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
.card-title {
@@ -253,11 +286,11 @@ body {
.stat-box {
display: flex;
flex-direction: column;
gap: 0.25rem;
gap: 0.35rem;
}
.stat-number {
font-size: 2rem;
font-size: 2.25rem;
font-weight: 800;
color: var(--accent-cyan);
line-height: 1;
@@ -268,6 +301,7 @@ body {
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
/* Buttons */
@@ -276,7 +310,7 @@ body {
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.6rem 1.1rem;
padding: 0.6rem 1.15rem;
border-radius: var(--radius-sm);
font-size: 0.875rem;
font-weight: 600;
@@ -304,14 +338,14 @@ body {
}
.btn-success:hover {
box-shadow: var(--glow-green);
box-shadow: 0 0 15px rgba(52, 211, 153, 0.4);
filter: brightness(1.1);
}
.btn-gold {
background: linear-gradient(135deg, #d97706, #f59e0b);
color: #fff;
border-color: rgba(251, 191, 36, 0.4);
border-color: rgba(251, 191, 36, 0.5);
}
.btn-gold:hover {
@@ -335,34 +369,62 @@ body {
font-size: 0.775rem;
}
/* Avatar Card */
/* ==========================================================================
Honkai: Star Rail Themed Avatar Cards & Portraits
========================================================================== */
.avatar-card {
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 180px;
min-height: 220px;
position: relative;
cursor: pointer;
border-width: 1.5px;
}
/* 5-Star Card Theme */
.avatar-card.card-rarity-5 {
border-color: rgba(251, 191, 36, 0.4);
background: linear-gradient(145deg, rgba(251, 191, 36, 0.05) 0%, rgba(18, 24, 38, 0.8) 100%);
}
.avatar-card.card-rarity-5:hover {
border-color: rgba(251, 191, 36, 0.8);
box-shadow: var(--glow-gold);
}
/* 4-Star Card Theme */
.avatar-card.card-rarity-4 {
border-color: rgba(192, 132, 252, 0.35);
background: linear-gradient(145deg, rgba(192, 132, 252, 0.05) 0%, rgba(18, 24, 38, 0.8) 100%);
}
.avatar-card.card-rarity-4:hover {
border-color: rgba(192, 132, 252, 0.8);
box-shadow: var(--glow-purple);
}
.avatar-badge-rarity {
position: absolute;
top: 1rem;
right: 1rem;
top: 0.85rem;
right: 0.85rem;
font-size: 0.75rem;
padding: 0.15rem 0.5rem;
border-radius: var(--radius-sm);
font-weight: 700;
font-weight: 800;
letter-spacing: 0.05em;
}
.rarity-5 {
background: rgba(251, 191, 36, 0.2);
border: 1px solid rgba(251, 191, 36, 0.5);
border: 1px solid rgba(251, 191, 36, 0.6);
color: var(--accent-gold);
}
.rarity-4 {
background: rgba(168, 85, 247, 0.2);
border: 1px solid rgba(168, 85, 247, 0.5);
background: rgba(192, 132, 252, 0.2);
border: 1px solid rgba(192, 132, 252, 0.6);
color: var(--accent-purple);
}
@@ -372,29 +434,39 @@ body {
align-items: center;
}
/* Circular / Rounded Avatar Portrait */
.avatar-image-container {
position: relative;
width: 52px;
height: 52px;
border-radius: var(--radius-md);
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border: 2px solid var(--border-color);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
}
.card-rarity-5 .avatar-image-container {
border-color: var(--accent-gold);
}
.card-rarity-4 .avatar-image-container {
border-color: var(--accent-purple);
}
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.25s ease;
transition: transform 0.3s ease;
}
.card:hover .avatar-img {
transform: scale(1.08);
.avatar-card:hover .avatar-img {
transform: scale(1.12);
}
.avatar-icon-placeholder {
@@ -405,28 +477,59 @@ body {
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.1rem;
font-size: 1.25rem;
color: var(--text-muted);
}
.avatar-name {
font-size: 1rem;
font-weight: 700;
font-size: 1.05rem;
font-weight: 800;
color: var(--text-highlight);
line-height: 1.25;
}
.avatar-subinfo {
font-size: 0.75rem;
color: var(--text-muted);
font-size: 0.775rem;
margin-top: 0.35rem;
display: flex;
gap: 0.5rem;
gap: 0.4rem;
flex-wrap: wrap;
align-items: center;
}
/* Star Rail Element Badges */
.element-badge {
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-size: 0.725rem;
font-weight: 700;
}
.elem-fire { background: rgba(239, 68, 68, 0.2); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.4); }
.elem-ice { background: rgba(56, 189, 248, 0.2); color: #38bdf8; border: 1px solid rgba(56, 189, 248, 0.4); }
.elem-lightning { background: rgba(168, 85, 247, 0.2); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.4); }
.elem-wind { background: rgba(16, 185, 129, 0.2); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.4); }
.elem-physical { background: rgba(156, 163, 175, 0.2); color: #e5e7eb; border: 1px solid rgba(156, 163, 175, 0.4); }
.elem-quantum { background: rgba(99, 102, 241, 0.2); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.4); }
.elem-imaginary { background: rgba(245, 158, 11, 0.2); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.4); }
.path-badge {
background: rgba(255, 255, 255, 0.08);
border: 1px solid var(--border-color);
color: var(--text-main);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-size: 0.725rem;
font-weight: 600;
}
/* Eidolon Selector */
.eidolon-selector {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 1rem;
background: rgba(0, 0, 0, 0.2);
background: rgba(0, 0, 0, 0.35);
padding: 0.5rem 0.75rem;
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
@@ -434,18 +537,18 @@ body {
.eidolon-buttons {
display: flex;
gap: 0.25rem;
gap: 0.3rem;
}
.eidolon-pip {
width: 24px;
height: 24px;
width: 25px;
height: 25px;
border-radius: 4px;
border: 1px solid var(--border-color);
background: transparent;
color: var(--text-muted);
font-size: 0.75rem;
font-weight: bold;
font-weight: 800;
cursor: pointer;
display: flex;
align-items: center;
@@ -459,10 +562,69 @@ body {
}
.eidolon-pip.active {
background: var(--accent-cyan);
border-color: var(--accent-cyan);
background: linear-gradient(135deg, #0284c7, #38bdf8);
border-color: #38bdf8;
color: #0b0d14;
box-shadow: 0 0 8px rgba(56, 189, 248, 0.5);
box-shadow: 0 0 10px rgba(56, 189, 248, 0.6);
}
/* ==========================================================================
Party / Lineup Builder
========================================================================== */
.lineup-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.25rem;
}
.lineup-slot-card {
background: var(--bg-card);
border: 2px dashed var(--border-color);
border-radius: var(--radius-lg);
padding: 1.25rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
min-height: 200px;
cursor: pointer;
transition: var(--transition);
position: relative;
}
.lineup-slot-card.filled {
border-style: solid;
border-color: rgba(56, 189, 248, 0.4);
background: linear-gradient(180deg, rgba(56, 189, 248, 0.08) 0%, rgba(18, 24, 38, 0.85) 100%);
}
.lineup-slot-card:hover {
border-color: var(--accent-cyan);
box-shadow: var(--glow-cyan);
}
.lineup-slot-img {
width: 72px;
height: 72px;
border-radius: 50%;
border: 2px solid var(--accent-cyan);
object-fit: cover;
margin-bottom: 0.75rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
.lineup-slot-num {
position: absolute;
top: 0.75rem;
left: 0.75rem;
font-size: 0.75rem;
font-weight: 800;
color: var(--accent-cyan);
background: rgba(2, 132, 199, 0.2);
padding: 0.2rem 0.5rem;
border-radius: 9999px;
border: 1px solid rgba(56, 189, 248, 0.3);
}
/* Ex Rank Toggle Switch */
@@ -470,7 +632,7 @@ body {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
padding: 1rem 1.25rem;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
@@ -480,6 +642,7 @@ body {
.ex-switch-card:hover {
background: var(--bg-card-hover);
border-color: rgba(99, 120, 160, 0.45);
}
.switch {
@@ -518,14 +681,14 @@ body {
input:checked + .slider {
background-color: var(--accent-green);
box-shadow: var(--glow-green);
box-shadow: 0 0 10px var(--accent-green);
}
input:checked + .slider:before {
transform: translateX(22px);
}
/* Inputs & Form */
/* Forms */
.form-group {
margin-bottom: 1rem;
}
@@ -542,7 +705,7 @@ input:checked + .slider:before {
.form-input, .form-select {
width: 100%;
padding: 0.6rem 0.85rem;
padding: 0.65rem 0.85rem;
background: rgba(10, 13, 20, 0.7);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
@@ -554,10 +717,10 @@ input:checked + .slider:before {
.form-input:focus, .form-select:focus {
border-color: var(--accent-cyan);
box-shadow: 0 0 10px rgba(56, 189, 248, 0.2);
box-shadow: 0 0 10px rgba(56, 189, 248, 0.25);
}
/* JSON Code Editor Area */
/* JSON Editor */
.json-editor-wrapper {
display: flex;
flex-direction: column;
@@ -583,7 +746,7 @@ input:checked + .slider:before {
border-color: var(--accent-cyan);
}
/* Toast notifications */
/* Toast */
.toast-container {
position: fixed;
bottom: 1.5rem;
@@ -622,12 +785,12 @@ input:checked + .slider:before {
to { transform: translateX(0); opacity: 1; }
}
/* Modal */
/* Modals */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(6px);
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(8px);
display: none;
align-items: center;
justify-content: center;
@@ -643,10 +806,11 @@ input:checked + .slider:before {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
max-width: 560px;
max-width: 600px;
width: 100%;
padding: 1.5rem;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
padding: 1.75rem;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7);
animation: fadeIn 0.2s ease-out;
}
.modal-header {
@@ -654,10 +818,18 @@ input:checked + .slider:before {
justify-content: space-between;
align-items: center;
margin-bottom: 1.25rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.75rem;
}
/* Responsive */
@media (max-width: 768px) {
@media (max-width: 900px) {
.lineup-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.app-header {
flex-direction: column;
align-items: flex-start;
@@ -666,10 +838,7 @@ input:checked + .slider:before {
width: 100%;
justify-content: space-between;
}
.nav-tabs {
padding: 0.5rem 1rem;
}
.main-container {
padding: 1rem;
.lineup-grid {
grid-template-columns: 1fr;
}
}
+303 -103
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GaC-Srtool — FreeSR & Game Management Dashboard</title>
<title>GaC-Srtool — Ultimate Star Rail Server & Build Manager</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>
@@ -14,54 +14,78 @@
<div class="brand">
<div class="brand-icon">✦</div>
<div>
<div class="brand-title" data-i18n="appName">GaC-Srtool</div>
<div class="brand-subtitle" data-i18n="appSubtitle">Star Rail FreeSR & Server Controller</div>
<div class="brand-title">GaC-Srtool</div>
<div class="brand-subtitle">Star Rail FreeSR & Server Controller</div>
</div>
</div>
<div class="header-controls">
<!-- Language Selector -->
<select id="lang-select" class="form-select btn-sm" style="width: auto; padding: 0.35rem 0.65rem; background: var(--bg-secondary); cursor: pointer;" title="Language / 语言 / 言語 / ภาษา">
<!-- Injected by i18n.js -->
<!-- Server Mode Selector -->
<div class="server-mode-badge" title="โหมดการเชื่อมต่อเซิร์ฟเวอร์">
<span>🌐</span>
<select id="server-mode-select" class="server-mode-select">
<option value="gac" selected>⚡ GaC-hkrpg-go (เซิร์ฟเวอร์หลัก / Full Control)</option>
<option value="firefly">🔥 FireflyGo (FreeSR Compatible)</option>
<option value="robin">🕊️ RobinSR (FreeSR Compatible)</option>
</select>
</div>
<!-- Language Selector (13 Languages) -->
<select id="lang-select" class="form-select btn-sm" style="width: auto; padding: 0.4rem 0.75rem; background: var(--bg-secondary); cursor: pointer;" title="Language / 语言 / 言語 / ภาษา">
</select>
<div class="connection-pill" id="btn-open-settings" title="Click to configure API connection">
<!-- Connection Pill -->
<div class="connection-pill" id="btn-open-settings" title="คลิกเพื่อตั้งค่าการเชื่อมต่อ API">
<span class="status-dot" id="status-dot"></span>
<span id="connection-status-text" data-i18n="checking">กำลังตรวจสอบ...</span>
<span id="connection-status-text">กำลังตรวจสอบ...</span>
</div>
<button class="btn btn-secondary btn-sm" id="btn-quick-ping" title="Refresh connection">
<span data-i18n="refresh">🔄 รีเฟรช</span>
<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" data-i18n="tabDashboard">📊 แดชบอร์ด (Overview)</button>
<button class="tab-btn" data-tab="tab-avatars" data-i18n="tabAvatars">👥 ตัวละคร (Avatars)</button>
<button class="tab-btn" data-tab="tab-exrank" data-i18n="tabExrank">⚡ Ex-Rank (สกิลพิเศษ)</button>
<button class="tab-btn" data-tab="tab-teleport" data-i18n="tabTeleport">🚀 วาร์ปฉาก (Teleport)</button>
<button class="tab-btn" data-tab="tab-json" data-i18n="tabJson">📝 จัดการไฟล์ JSON</button>
<button class="tab-btn" data-tab="tab-settings" data-i18n="tabSettings">⚙️ ตั้งค่า & คู่มือ Host</button>
<button class="tab-btn active" data-tab="tab-dashboard">📊 แดชบอร์ด & ทีม (Lineup)</button>
<button class="tab-btn" data-tab="tab-avatars">👥 ตัวละคร (Avatars & Build)</button>
<button class="tab-btn" data-tab="tab-equipment">🗡️ กรวยแสง & รีลิกส์ (Equipment)</button>
<button class="tab-btn" data-tab="tab-persistent" id="tab-btn-persistent">💾 จัดการเซฟ GaC (Persistent)</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">⚙️ ตั้งค่า & ข้อมูลโหมด</button>
</nav>
<!-- Main Container -->
<main class="main-container">
<!-- 1. Dashboard Tab -->
<!-- 1. Dashboard Tab & Party Lineup -->
<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" data-i18n="statAvatars">ตัวละครที่ครอบครอง (Avatars)</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" data-i18n="statLightcones">กรวยแสง (Lightcones)</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" data-i18n="statRelics">รีลิกส์ (Relics)</div>
<div class="stat-label">รีลิกส์ (Relics)</div>
</div>
</div>
<!-- Party Lineup Visualizer -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">
<span>🎮 สมาชิกในทีมปัจจุบัน (Active Party Lineup)</span>
<span style="font-size: 0.8rem; color: var(--accent-cyan); font-weight: normal;">* คลิกที่ช่องเพื่อเปลี่ยนตัวละครในทีม</span>
</div>
<div class="lineup-grid" id="lineup-container">
<!-- Rendered by JS -->
</div>
</div>
@@ -69,12 +93,16 @@
<!-- Server & Map Card -->
<div class="card">
<div class="card-title">
<span data-i18n="serverStatusTitle">📍 สถานะเซิร์ฟเวอร์ & ตำแหน่งปัจจุบัน</span>
<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" data-i18n="lblApiGatewayUrl">API Gateway Endpoint</span>
<span class="form-label">โหมดเซิร์ฟเวอร์ที่ใช้งาน</span>
<strong id="lbl-active-mode" style="color: var(--accent-cyan);">GaC-hkrpg-go</strong>
</div>
<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;">
@@ -88,7 +116,7 @@
</div>
</div>
<div>
<span class="form-label" data-i18n="coords">พิกัดตัวละคร (X, Y, Z)</span>
<span class="form-label">พิกัดตัวละคร (X, Y, Z)</span>
<span id="lbl-coordinates" style="color: var(--text-muted); font-size: 0.85rem;">-</span>
</div>
</div>
@@ -96,84 +124,192 @@
<!-- Quick Actions Card -->
<div class="card">
<div class="card-title" data-i18n="quickActionsTitle">⚡ ดำเนินการด่วน (Quick Actions)</div>
<p class="card-subtitle" data-i18n="quickActionsSubtitle">สั่งการส่งไปยัง REST API Gateway ทันที</p>
<div class="card-title">⚡ ดำเนินการด่วน (Quick Actions)</div>
<p class="card-subtitle">ส่งคำสั่งไปยังเซิร์ฟเวอร์ทันที</p>
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
<button class="btn btn-gold" id="btn-quick-max-all">
<span data-i18n="btnMaxAll">⭐ ปรับเลเวล 80 & ปลด E6 ตัวละครทั้งหมด (Max All)</span>
⭐ ปรับเลเวล 80 & ปลด E6 ตัวละครทั้งหมด (Max All)
</button>
<button class="btn btn-primary" id="btn-quick-warp-express">
<span data-i18n="btnWarpExpress">🚂 วาร์ปกลับรถไฟ Astral Express ทันที</span>
🚂 วาร์ปกลับห้องรับรองรถไฟ Astral Express ทันที
</button>
<button class="btn btn-secondary" id="btn-quick-sync-persistent">
<span data-i18n="btnSyncPersistent">💾 ซิงค์ข้อมูล Persistent ล่าสุด</span>
💾 ซิงค์ข้อมูล Persistent / เซฟล่าสุด
</button>
</div>
</div>
</div>
<!-- Current Lineup -->
<div class="card" style="margin-top: 1.5rem;">
<div class="card-title" data-i18n="currentLineupTitle">🎮 ทีมปัจจุบัน (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 -->
<!-- 2. Avatars Tab (Build & Eidolon Editor) -->
<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" data-i18n-placeholder="searchPlaceholder" placeholder="🔍 ค้นหาชื่อหรือ ID ตัวละคร...">
<div style="display: flex; gap: 0.75rem; flex: 1; max-width: 520px;">
<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" data-i18n="filterAllRarity">ทุกระดับดาว</option>
<option value="5" data-i18n="filter5Star">5 ดาว (SSR)</option>
<option value="4" data-i18n="filter4Star">4 ดาว (SR)</option>
<option value="all">ทุกระดับดาว</option>
<option value="5">5 ดาว (SSR)</option>
<option value="4">4 ดาว (SR)</option>
</select>
<select id="avatar-filter-element" class="form-select" style="max-width: 140px;">
<option value="all">ทุกธาตุ</option>
<option value="Fire">ไฟ (Fire)</option>
<option value="Ice">น้ำแข็ง (Ice)</option>
<option value="Lightning">สายฟ้า (Lightning)</option>
<option value="Wind">ลม (Wind)</option>
<option value="Physical">กายภาพ (Physical)</option>
<option value="Quantum">ควอนตัม (Quantum)</option>
<option value="Imaginary">จินตภาพ (Imaginary)</option>
</select>
</div>
<div style="display: flex; gap: 0.5rem;">
<button class="btn btn-gold" id="btn-max-all-avatars" data-i18n="btnMaxAll">⭐ ปรับ Max E6 ทุกตัว</button>
<button class="btn btn-secondary" id="btn-reload-avatars" data-i18n="btnReload">🔄 รีโหลด</button>
<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 -->
<!-- 3. Equipment Tab (Lightcones & Relics) -->
<section id="tab-equipment" class="tab-content">
<div class="grid-2">
<!-- Lightcones Card -->
<div class="card">
<div class="card-title">
<span>🗡️ กรวยแสงในคลัง (Lightcones)</span>
<span class="btn-sm btn-secondary" id="badge-total-lc">0 ชิ้น</span>
</div>
<p class="card-subtitle">รายการกรวยแสงทั้งหมดจาก freesr-data.json</p>
<div id="lightcones-list" style="max-height: 520px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.6rem;">
<div style="color: var(--text-muted); text-align: center; padding: 1.5rem;">กำลังโหลดข้อมูลกรวยแสง...</div>
</div>
</div>
<!-- Relics Card -->
<div class="card">
<div class="card-title">
<span>🛡️ รีลิกส์ในคลัง (Relics)</span>
<span class="btn-sm btn-secondary" id="badge-total-relics">0 ชิ้น</span>
</div>
<p class="card-subtitle">รายการรีลิกส์ทั้งหมดจาก freesr-data.json</p>
<div id="relics-list" style="max-height: 520px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.6rem;">
<div style="color: var(--text-muted); text-align: center; padding: 1.5rem;">กำลังโหลดข้อมูลรีลิกส์...</div>
</div>
</div>
</div>
</section>
<!-- 4. Persistent Save Settings (Exclusive to GaC-hkrpg-go) -->
<section id="tab-persistent" class="tab-content">
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">
<span>💾 จัดการไฟล์เซฟ Persistent (GaC-hkrpg-go Dedicated)</span>
<span class="btn-sm btn-success">เซิร์ฟเวอร์ของเราเอง</span>
</div>
<p class="card-subtitle">
ไฟล์ <code>persistent</code> เก็บการตั้งค่าสถานะผู้เล่นเฉพาะของเซิร์ฟเวอร์ GaC เช่น เพศ/ธาตุตัวเอก, สเตตัส March 7th, สวิตช์ฟังก์ชัน Global ต่างๆ
</p>
</div>
<div class="grid-2">
<!-- Main Character & March Path -->
<div class="card">
<div class="card-title">👤 ผู้บุกเบิก & March 7th</div>
<form id="form-persistent-chars">
<div class="form-group">
<label class="form-label">เพศ & ธาตุของตัวเอก (Main Character)</label>
<select id="persistent-mc-select" class="form-select">
<option value="MalePhysical">ผู้บุกเบิกชาย - กายภาพ (Male Physical)</option>
<option value="FemalePhysical">ผู้บุกเบิกหญิง - กายภาพ (Female Physical)</option>
<option value="MalePreservation">ผู้บุกเบิกชาย - ไฟ (Male Preservation)</option>
<option value="FemalePreservation">ผู้บุกเบิกหญิง - ไฟ (Female Preservation)</option>
<option value="MaleHarmony">ผู้บุกเบิกชาย - ประสาน (Male Harmony)</option>
<option value="FemaleHarmony">ผู้บุกเบิกหญิง - ประสาน (Female Harmony)</option>
<option value="MaleRemembrance">ผู้บุกเบิกชาย - ความทรงจำ (Male Remembrance)</option>
<option value="FemaleRemembrance">ผู้บุกเบิกหญิง - ความทรงจำ (Female Remembrance)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">สายของ March 7th</label>
<select id="persistent-march-select" class="form-select">
<option value="MarchHunt">March 7th - ล่าสังหาร (The Hunt / จินตภาพ)</option>
<option value="MarchPreservation">March 7th - อนุรักษ์ (Preservation / น้ำแข็ง)</option>
</select>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">
💾 บันทึกการตั้งค่าตัวละคร
</button>
</form>
</div>
<!-- Global Features & Toggles -->
<div class="card">
<div class="card-title">⚙️ สวิตช์ระบบพิเศษของ GaC</div>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-top: 1rem;">
<div class="ex-switch-card">
<div>
<div style="font-weight: 700;">Silver Wolf Global Feature</div>
<div style="font-size: 0.8rem; color: var(--text-muted);">เปิดใช้งานฟังก์ชันพิเศษ SW ทั่วทั้งเซิร์ฟเวอร์</div>
</div>
<label class="switch">
<input type="checkbox" id="toggle-sw-global">
<span class="slider"></span>
</label>
</div>
<div class="ex-switch-card">
<div>
<div style="font-weight: 700;">Castorice Global Feature</div>
<div style="font-size: 0.8rem; color: var(--text-muted);">เปิดใช้งานฟังก์ชันพิเศษ Castorice ทั่วทั้งเซิร์ฟเวอร์</div>
</div>
<label class="switch">
<input type="checkbox" id="toggle-castorice-global">
<span class="slider"></span>
</label>
</div>
<button class="btn btn-success" id="btn-save-persistent-toggles">
💾 บันทึกสวิตช์ Global
</button>
</div>
</div>
</div>
</section>
<!-- 5. Ex-Rank Tab -->
<section id="tab-exrank" class="tab-content">
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">
<span data-i18n="exrankTitle">⚡ ระบบปลดล็อค Ex-Rank Skill</span>
<span>⚡ ระบบปลดล็อค Ex-Rank Skill</span>
<div style="display: flex; gap: 0.5rem;">
<button class="btn btn-success btn-sm" id="btn-exrank-enable-all" data-i18n="btnEnableAll">เปิด Ex-Rank ทั้งหมด</button>
<button class="btn btn-secondary btn-sm" id="btn-exrank-disable-all" data-i18n="btnDisableAll">ปิด Ex-Rank ทั้งหมด</button>
<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" data-i18n="exrankSubtitle">
<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>
<!-- Rendered by JS -->
</div>
</section>
<!-- 4. Teleport Tab -->
<!-- 6. Teleport Tab -->
<section id="tab-teleport" class="tab-content">
<div class="grid-2">
<!-- Presets -->
<div class="card">
<div class="card-title" data-i18n="presetsTitle">📍 จุดวาร์ปปลอดภัยยอดนิยม (Safe Presets)</div>
<p class="card-subtitle" data-i18n="presetsSubtitle">คลิกจุดวาร์ปเพื่อย้ายฉากของผู้เล่นในไฟล์เซฟได้ทันที</p>
<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>
@@ -181,8 +317,8 @@
<!-- Custom Coordinate Warp -->
<div class="card">
<div class="card-title" data-i18n="customTeleportTitle">🎯 กำหนดพิกัดวาร์ปเอง (Custom Teleport)</div>
<p class="card-subtitle" data-i18n="customTeleportSubtitle">ใส่ PlaneID, FloorID และ EntryID ตามต้องการ</p>
<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;">
@@ -215,7 +351,7 @@
</div>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 0.5rem;" data-i18n="btnExecuteWarp">
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 0.5rem;">
🚀 ยืนยันการวาร์ป (Execute Teleport)
</button>
</form>
@@ -223,106 +359,170 @@
</div>
</section>
<!-- 5. JSON Management Tab -->
<!-- 7. 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" data-i18n="btnFreesr">📄 freesr-data.json</button>
<button class="btn btn-sm btn-secondary" id="btn-select-persistent" data-i18n="btnPersistent">💾 persistent (Save)</button>
<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" data-i18n="btnBeautify">✨ จัดรูปแบบ JSON</button>
<button class="btn btn-sm btn-secondary" id="btn-json-download" data-i18n="btnDownload">📥 ดาวน์โหลด Backup</button>
<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;">
<span data-i18n="btnUpload">📤 อัปโหลดไฟล์</span> <input type="file" id="json-file-input" accept=".json,text/plain" style="display: none;">
<span>📤 อัปโหลดไฟล์</span> <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" data-i18n="btnSaveServer">💾 บันทึกไปยังเซิร์ฟเวอร์</button>
<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="..."></textarea>
<div style="display: flex; justify-content: space-between; font-size: 0.8rem; color: var(--text-muted);">
<span id="json-status-info" data-i18n="jsonStatusReady">พร้อมใช้งาน</span>
<span data-i18n="jsonTip">Tip: สามารถแก้ไขค่าดิบแล้วกด 'บันทึกไปยังเซิร์ฟเวอร์' ได้ทันที</span>
<span id="json-status-info">พร้อมใช้งาน</span>
<span>Tip: สามารถแก้ไขค่าดิบแล้วกด 'บันทึกไปยังเซิร์ฟเวอร์' ได้ทันที</span>
</div>
</div>
</div>
</section>
<!-- 6. Settings & GitHub Hosting Guide -->
<!-- 8. Settings & Compatibility Guide -->
<section id="tab-settings" class="tab-content">
<div class="grid-2">
<!-- Endpoint config -->
<div class="card">
<div class="card-title" data-i18n="apiSettingsTitle">⚙️ ตั้งค่าการเชื่อมต่อ API Gateway</div>
<p class="card-subtitle" data-i18n="apiSettingsSubtitle">ปรับเปลี่ยนพอร์ตหรือ IP ได้ตามไฟล์ server.json ของคุณ</p>
<div class="card-title">⚙️ ตั้งค่าการเชื่อมต่อ API Gateway</div>
<p class="card-subtitle">ปรับเปลี่ยนพอร์ตหรือ IP ได้ตามเซิร์ฟเวอร์ที่คุณใช้</p>
<form id="form-settings">
<div class="form-group">
<label class="form-label" data-i18n="lblApiGatewayUrl">REST API Gateway URL</label>
<label class="form-label">โหมดเซิร์ฟเวอร์ (Target Server)</label>
<select id="setting-server-mode" class="form-select">
<option value="gac">⚡ GaC-hkrpg-go (เซิร์ฟเวอร์หลัก - แก้ได้ทั้ง FreeSR + Persistent)</option>
<option value="firefly">🔥 FireflyGo (FreeSR Standard)</option>
<option value="robin">🕊️ RobinSR (FreeSR Standard)</option>
</select>
</div>
<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;">
* Port 21000 by default (GaC-hkrpg-go SDK Server)
* ค่าเริ่มต้นพอร์ต 21000 (SDK server)
</small>
</div>
<div class="form-group">
<label class="form-label" data-i18n="lblPingInterval">ความถี่ในการตรวจสอบสถานะ (วินาที)</label>
<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;" data-i18n="btnSaveSettings">💾 บันทึกการตั้งค่า</button>
<button type="button" class="btn btn-secondary" id="btn-reset-settings" data-i18n="btnResetDefault">คืนค่าเริ่มต้น</button>
<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 -->
<!-- Mode Explanation Card -->
<div class="card">
<div class="card-title" data-i18n="hostGuideTitle">🌐 วิธีนำขึ้นโฮสต์ GitHub Pages (ฟรี 100%)</div>
<p class="card-subtitle" data-i18n="hostGuideSubtitle">สามารถเปิดใช้งานเว็บนี้บนอินเทอร์เน็ตได้โดยไม่ต้องเสียค่าโฮสต์ใดๆ</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. Create GitHub Repository:</strong><br>
Create new repo e.g. <code>GaC-srtool</code>
</li>
<li>
<strong style="color: var(--text-main);">2. Push to GitHub:</strong><br>
<code>git remote add origin https://github.com/&lt;user&gt;/GaC-srtool.git</code><br>
<code>git add . && git commit -m "Init" && git push -u origin main</code>
</li>
<li>
<strong style="color: var(--text-main);">3. Enable GitHub Pages:</strong><br>
Go to Repo &gt; <strong>Settings</strong> &gt; <strong>Pages</strong> &gt; Source: <strong>Deploy from a branch</strong> &gt; Branch: <strong>main</strong> / (root) &gt; Click <strong>Save</strong>
</li>
<li>
<strong style="color: var(--text-main);">4. Done:</strong><br>
Your dashboard will be online at <code>https://&lt;user&gt;.github.io/GaC-srtool/</code>!
</li>
</ol>
<div class="card-title">💡 ข้อมูลโหมดเซิร์ฟเวอร์ & สำหรับผู้อื่น</div>
<div style="display: flex; flex-direction: column; gap: 0.85rem; font-size: 0.875rem; color: var(--text-muted);">
<div>
<strong style="color: var(--accent-cyan);">⚡ GaC-hkrpg-go (Full Access):</strong><br>
โหมดเฉพาะของเซิร์ฟเวอร์เรา สามารถอ่านและเขียนทั้ง <code>freesr-data.json</code> และ <code>persistent</code> ได้แบบ 100% เรียลไทม์ผ่าน REST API Gateway พร้อมระบบ Ex-Rank และ Teleport
</div>
<div>
<strong style="color: #f87171;">🔥 FireflyGo:</strong><br>
สำหรับผู้ใช้งานที่รันเซิร์ฟเวอร์ FireflyGo สามารถใช้เครื่องมือนี้จัดทีม, แม็กซ์ตัวละคร, หรือปรับแต่งอาวุธ/รีลิกส์ แล้วสั่งซิงค์ผ่าน <code>/srtools</code> หรือกดดาวน์โหลดไฟล์ <code>freesr-data.json</code> ไปวางในเซิร์ฟเวอร์ได้ทันที
</div>
<div>
<strong style="color: #c084fc;">🕊️ RobinSR:</strong><br>
สำหรับผู้ใช้งาน RobinSR สามารถใช้เครื่องมือนี้ในการสร้างเซฟและตัวละครตามมาตรฐาน FreeSR ได้อย่างสมบูรณ์แบบ
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Character Detail / Build Modal -->
<div class="modal-overlay" id="modal-avatar-detail">
<div class="modal-content" style="max-width: 640px;">
<div class="modal-header">
<h3 id="modal-char-title" style="font-weight: 800; font-size: 1.15rem; color: var(--text-highlight);">
ปรับแต่งตัวละคร
</h3>
<button class="btn btn-sm btn-secondary" id="btn-close-avatar-modal">✕</button>
</div>
<div style="display: flex; gap: 1.25rem; align-items: center; margin-bottom: 1.25rem;">
<div class="avatar-image-container" style="width: 76px; height: 76px;">
<img id="modal-char-img" class="avatar-img" src="" alt="">
</div>
<div>
<div id="modal-char-name" style="font-size: 1.25rem; font-weight: 800;"></div>
<div id="modal-char-meta" style="font-size: 0.85rem; color: var(--text-muted); margin-top: 0.25rem;"></div>
</div>
</div>
<div class="grid-2" style="gap: 1rem; margin-bottom: 1rem;">
<div class="form-group">
<label class="form-label">ระดับเลเวล (Level 1-80)</label>
<input type="number" id="modal-char-level" class="form-input" min="1" max="80" value="80">
</div>
<div class="form-group">
<label class="form-label">การเลื่อนขั้น (Ascension 0-6)</label>
<input type="number" id="modal-char-promotion" class="form-input" min="0" max="6" value="6">
</div>
</div>
<div class="form-group">
<label class="form-label">Eidolon (E0 - E6)</label>
<div class="eidolon-buttons" id="modal-char-eidolon-btns" style="margin-top: 0.35rem;">
<!-- Injected pips -->
</div>
</div>
<div style="display: flex; gap: 0.75rem; margin-top: 1.5rem; justify-content: flex-end;">
<button class="btn btn-gold" id="btn-modal-max-skills">
⭐ อัพสกิล & Traces ทั้งหมด (10/10)
</button>
<button class="btn btn-success" id="btn-modal-save-avatar">
💾 บันทึกตัวละคร
</button>
</div>
</div>
</div>
<!-- Party Member Picker Modal -->
<div class="modal-overlay" id="modal-lineup-picker">
<div class="modal-content" style="max-width: 720px; max-height: 85vh; display: flex; flex-direction: column;">
<div class="modal-header">
<h3 id="modal-picker-title" style="font-weight: 800;">เลือกตัวละครเข้าทีม</h3>
<button class="btn btn-sm btn-secondary" id="btn-close-picker-modal">✕</button>
</div>
<input type="text" id="picker-search" class="form-input" placeholder="🔍 ค้นหาตัวละครเพื่อลงทีม..." style="margin-bottom: 1rem;">
<div id="picker-avatars-grid" class="grid-4" style="overflow-y: auto; flex: 1; padding-right: 0.5rem;">
<!-- Injected by JS -->
</div>
</div>
</div>
<!-- Settings Modal -->
<div class="modal-overlay" id="modal-settings">
<div class="modal-content">
<div class="modal-header">
<h3 style="font-weight: 700;" data-i18n="apiSettingsTitle">⚙️ ตั้งค่าการเชื่อมต่อ API Server</h3>
<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" data-i18n="lblApiGatewayUrl">Server REST API URL</label>
<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">Cancel</button>
<button class="btn btn-primary" id="btn-modal-save" data-i18n="btnSaveSettings">บันทึก & ทดสอบ</button>
<button class="btn btn-secondary" id="btn-modal-cancel">ยกเลิก</button>
<button class="btn btn-primary" id="btn-modal-save">บันทึก & ทดสอบ</button>
</div>
</div>
</div>
+500 -56
View File
@@ -1,20 +1,28 @@
import { AVATAR_METADATA, getAvatarInfo } from './avatars-data.js';
import { PRESET_TELEPORTS, getTeleportName } from './teleports-data.js';
import { SUPPORTED_LANGUAGES, getCurrentLanguage, setLanguage, t } from './i18n.js';
import { SUPPORTED_LANGUAGES, getCurrentLanguage, setLanguage, t, translateElement, translatePath } from './i18n.js';
// Application State
const state = {
serverMode: localStorage.getItem('gac_srtool_mode') || 'gac', // 'gac' | 'firefly' | 'robin'
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'
fullFreesrData: null,
persistentData: null,
currentJsonMode: 'freesr',
activeTab: 'tab-dashboard',
selectedLineupSlot: null,
selectedAvatarDetail: null
};
// UI Elements
const el = {
serverModeSelect: document.getElementById('server-mode-select'),
settingServerMode: document.getElementById('setting-server-mode'),
lblActiveMode: document.getElementById('lbl-active-mode'),
langSelect: document.getElementById('lang-select'),
statusDot: document.getElementById('status-dot'),
connectionStatusText: document.getElementById('connection-status-text'),
@@ -43,9 +51,25 @@ const el = {
avatarsGrid: document.getElementById('avatars-grid'),
avatarSearch: document.getElementById('avatar-search'),
avatarFilterRarity: document.getElementById('avatar-filter-rarity'),
avatarFilterElement: document.getElementById('avatar-filter-element'),
btnMaxAllAvatars: document.getElementById('btn-max-all-avatars'),
btnReloadAvatars: document.getElementById('btn-reload-avatars'),
// Equipment
lightconesList: document.getElementById('lightcones-list'),
relicsList: document.getElementById('relics-list'),
badgeTotalLc: document.getElementById('badge-total-lc'),
badgeTotalRelics: document.getElementById('badge-total-relics'),
// Persistent Tab
tabBtnPersistent: document.getElementById('tab-btn-persistent'),
formPersistentChars: document.getElementById('form-persistent-chars'),
persistentMcSelect: document.getElementById('persistent-mc-select'),
persistentMarchSelect: document.getElementById('persistent-march-select'),
toggleSwGlobal: document.getElementById('toggle-sw-global'),
toggleCastoriceGlobal: document.getElementById('toggle-castorice-global'),
btnSavePersistentToggles: document.getElementById('btn-save-persistent-toggles'),
// Ex-Rank
exrankList: document.getElementById('exrank-list'),
btnExrankEnableAll: document.getElementById('btn-exrank-enable-all'),
@@ -77,6 +101,26 @@ const el = {
settingPingInterval: document.getElementById('setting-ping-interval'),
btnResetSettings: document.getElementById('btn-reset-settings'),
// Character Detail Modal
modalAvatarDetail: document.getElementById('modal-avatar-detail'),
btnCloseAvatarModal: document.getElementById('btn-close-avatar-modal'),
modalCharTitle: document.getElementById('modal-char-title'),
modalCharImg: document.getElementById('modal-char-img'),
modalCharName: document.getElementById('modal-char-name'),
modalCharMeta: document.getElementById('modal-char-meta'),
modalCharLevel: document.getElementById('modal-char-level'),
modalCharPromotion: document.getElementById('modal-char-promotion'),
modalCharEidolonBtns: document.getElementById('modal-char-eidolon-btns'),
btnModalMaxSkills: document.getElementById('btn-modal-max-skills'),
btnModalSaveAvatar: document.getElementById('btn-modal-save-avatar'),
// Lineup Picker Modal
modalLineupPicker: document.getElementById('modal-lineup-picker'),
btnClosePickerModal: document.getElementById('btn-close-picker-modal'),
modalPickerTitle: document.getElementById('modal-picker-title'),
pickerSearch: document.getElementById('picker-search'),
pickerAvatarsGrid: document.getElementById('picker-avatars-grid'),
toastContainer: document.getElementById('toast-container')
};
@@ -150,7 +194,21 @@ function setConnectionStatus(connected, extraText = '') {
}
}
// Apply multi-language translations across whole UI
// Helper to get element CSS class
function getElementClass(elem) {
const map = {
Fire: 'elem-fire',
Ice: 'elem-ice',
Lightning: 'elem-lightning',
Wind: 'elem-wind',
Physical: 'elem-physical',
Quantum: 'elem-quantum',
Imaginary: 'elem-imaginary'
};
return map[elem] || 'elem-physical';
}
// Apply multi-language translations across UI
function applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(elem => {
const key = elem.getAttribute('data-i18n');
@@ -162,7 +220,6 @@ function applyTranslations() {
elem.placeholder = t(key);
});
// Re-render localized UI parts
if (state.statusData) {
renderLineup(state.statusData.lineups);
renderExRanks(state.statusData.ex_ranks || []);
@@ -173,7 +230,7 @@ function applyTranslations() {
}
}
// Language selector initialization
// Setup Language Selector
function setupLanguageSelector() {
el.langSelect.innerHTML = '';
SUPPORTED_LANGUAGES.forEach(lang => {
@@ -190,14 +247,73 @@ function setupLanguageSelector() {
const newLang = e.target.value;
setLanguage(newLang);
applyTranslations();
showToast(`Language switched to ${SUPPORTED_LANGUAGES.find(l => l.code === newLang)?.name || newLang}`, 'info');
showToast(`Language: ${SUPPORTED_LANGUAGES.find(l => l.code === newLang)?.name || newLang}`, 'info');
});
}
// Refresh status from /api/status
// Setup Server Mode Selector
function setupServerModes() {
const updateModeDisplay = (mode) => {
state.serverMode = mode;
localStorage.setItem('gac_srtool_mode', mode);
el.serverModeSelect.value = mode;
el.settingServerMode.value = mode;
if (mode === 'gac') {
el.lblActiveMode.textContent = 'GaC-hkrpg-go (เซิร์ฟเวอร์หลัก / Full Control)';
el.lblActiveMode.style.color = 'var(--accent-cyan)';
el.tabBtnPersistent.style.display = 'inline-flex';
} else if (mode === 'firefly') {
el.lblActiveMode.textContent = 'FireflyGo (FreeSR Compatible)';
el.lblActiveMode.style.color = '#f87171';
el.tabBtnPersistent.style.display = 'none';
} else if (mode === 'robin') {
el.lblActiveMode.textContent = 'RobinSR (FreeSR Compatible)';
el.lblActiveMode.style.color = '#c084fc';
el.tabBtnPersistent.style.display = 'none';
}
};
el.serverModeSelect.addEventListener('change', (e) => {
updateModeDisplay(e.target.value);
showToast(`สลับโหมดเซิร์ฟเวอร์เป็น: ${el.lblActiveMode.textContent}`, 'info');
refreshStatus();
});
el.settingServerMode.addEventListener('change', (e) => {
updateModeDisplay(e.target.value);
showToast(`สลับโหมดเซิร์ฟเวอร์เป็น: ${el.lblActiveMode.textContent}`, 'info');
refreshStatus();
});
updateModeDisplay(state.serverMode);
}
// Refresh status from server
async function refreshStatus() {
try {
const data = await apiCall('/api/status');
let data;
if (state.serverMode === 'gac') {
data = await apiCall('/api/status');
} else {
// FireflyGo / RobinSR fallback check
const freesr = await apiCall('/api/freesr').catch(() => null);
if (freesr && freesr.data) {
data = {
status: 200,
avatar_count: Object.keys(freesr.data.avatars || {}).length,
lightcone_count: (freesr.data.lightcones || []).length,
relic_count: (freesr.data.relics || []).length,
lineups: freesr.data.lineups || { 0: 8001 },
scene: freesr.data.scene,
position: freesr.data.position,
ex_ranks: []
};
} else {
throw new Error('Server not reachable');
}
}
state.statusData = data;
setConnectionStatus(true);
@@ -223,45 +339,135 @@ async function refreshStatus() {
}
}
// Render current lineup cards
// Render party 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;">${t('slotEmpty')}</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';
const avatarId = lineups ? (lineups[slot] || 0) : 0;
const slotCard = document.createElement('div');
slotCard.className = `lineup-slot-card ${avatarId ? 'filled' : ''}`;
slotCard.title = 'คลิกเพื่อเลือกตัวละครลงตำแหน่งนี้';
if (avatarId === 0) {
card.innerHTML = `
<div style="font-size: 0.75rem; color: var(--text-muted);">${t('slot')} ${slot + 1}</div>
<div style="font-weight: 600; color: var(--text-muted); margin-top: 0.25rem;">${t('slotEmpty')}</div>
slotCard.innerHTML = `
<div class="lineup-slot-num">Slot ${slot + 1}</div>
<div style="font-size: 2.2rem; color: var(--text-muted); opacity: 0.5;">➕</div>
<div style="font-weight: 700; color: var(--text-muted); margin-top: 0.5rem;">ว่าง (คลิกเพื่อเพิ่ม)</div>
`;
} else {
const info = getAvatarInfo(avatarId);
card.innerHTML = `
<div style="display: flex; gap: 0.75rem; align-items: center;">
<div class="avatar-image-container" style="width: 44px; height: 44px;">
<img class="avatar-img"
src="./assets/avatars/${avatarId}.png"
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${avatarId}.png';}else{this.style.display='none';this.nextElementSibling.style.display='flex';}"
alt="${info.name}">
<div class="avatar-icon-placeholder" style="display: none;">${info.name.charAt(0)}</div>
</div>
<div>
<div style="font-size: 0.75rem; color: var(--accent-cyan);">${t('slot')} ${slot + 1}</div>
<div style="font-weight: 700; margin-top: 0.15rem; font-size: 0.95rem;">${info.name}</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">${info.element} • ${info.path}</div>
</div>
const elemClass = getElementClass(info.element);
slotCard.innerHTML = `
<div class="lineup-slot-num">Slot ${slot + 1}</div>
<img class="lineup-slot-img"
src="./assets/avatars/${avatarId}.png"
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${avatarId}.png';}else{this.style.display='none';}"
alt="${info.name}">
<div style="font-weight: 800; font-size: 1rem; color: var(--text-highlight);">${info.name}</div>
<div style="display: flex; gap: 0.35rem; margin-top: 0.35rem;">
<span class="element-badge ${elemClass}">${info.element}</span>
<span class="path-badge">${info.path}</span>
</div>
`;
}
el.lineupContainer.appendChild(card);
slotCard.addEventListener('click', () => {
openLineupPicker(slot);
});
el.lineupContainer.appendChild(slotCard);
}
}
// Lineup Picker Modal
function openLineupPicker(slotIndex) {
state.selectedLineupSlot = slotIndex;
el.modalPickerTitle.textContent = `เลือกตัวละครเข้าสู่ Slot ${slotIndex + 1}`;
el.pickerSearch.value = '';
renderPickerGrid();
el.modalLineupPicker.classList.add('active');
}
function renderPickerGrid() {
const search = el.pickerSearch.value.trim().toLowerCase();
el.pickerAvatarsGrid.innerHTML = '';
// Add Remove button
const removeCard = document.createElement('div');
removeCard.className = 'card';
removeCard.style.padding = '0.75rem';
removeCard.style.textAlign = 'center';
removeCard.style.cursor = 'pointer';
removeCard.innerHTML = `
<div style="font-size: 1.5rem; color: var(--accent-red);">❌</div>
<div style="font-size: 0.85rem; font-weight: bold; margin-top: 0.35rem; color: var(--accent-red);">นำออกจากทีม</div>
`;
removeCard.addEventListener('click', async () => {
await assignAvatarToLineup(state.selectedLineupSlot, 0);
el.modalLineupPicker.classList.remove('active');
});
el.pickerAvatarsGrid.appendChild(removeCard);
// Available avatars
state.avatarsList.forEach(item => {
const meta = getAvatarInfo(item.avatar_id);
if (search && !meta.name.toLowerCase().includes(search) && !String(item.avatar_id).includes(search)) {
return;
}
const elemClass = getElementClass(meta.element);
const card = document.createElement('div');
card.className = `card avatar-card card-rarity-${meta.rarity}`;
card.style.minHeight = 'auto';
card.style.padding = '0.85rem';
card.style.cursor = 'pointer';
card.innerHTML = `
<div style="display: flex; align-items: center; gap: 0.65rem;">
<div class="avatar-image-container" style="width: 44px; height: 44px;">
<img class="avatar-img"
src="./assets/avatars/${item.avatar_id}.png"
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${item.avatar_id}.png';}else{this.style.display='none';}"
alt="${meta.name}">
</div>
<div>
<div style="font-weight: 700; font-size: 0.9rem;">${meta.name}</div>
<div style="font-size: 0.725rem; color: var(--text-muted);">Lv. ${item.level} • E${item.rank}</div>
</div>
</div>
`;
card.addEventListener('click', async () => {
await assignAvatarToLineup(state.selectedLineupSlot, item.avatar_id);
el.modalLineupPicker.classList.remove('active');
});
el.pickerAvatarsGrid.appendChild(card);
});
}
// Assign avatar to lineup slot
async function assignAvatarToLineup(slot, avatarId) {
try {
if (!state.statusData || !state.statusData.lineups) return;
const lineups = { ...state.statusData.lineups };
lineups[slot] = avatarId;
state.statusData.lineups = lineups;
if (state.serverMode === 'gac') {
await apiCall('/api/persistent', 'POST', { lineups });
} else {
// In FreeSR standard mode
const freesr = await apiCall('/api/freesr');
if (freesr && freesr.data) {
freesr.data.lineups = lineups;
await apiCall('/api/freesr', 'POST', freesr.data);
}
}
showToast(`อัปเดตทีม Slot ${slot + 1} เรียบร้อย`, 'success');
renderLineup(lineups);
} catch (err) {
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
}
}
@@ -280,24 +486,27 @@ async function loadAvatars() {
function renderAvatarsGrid() {
const searchTerm = el.avatarSearch.value.trim().toLowerCase();
const rarityFilter = el.avatarFilterRarity.value;
const elementFilter = el.avatarFilterElement.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;
const matchesElement = elementFilter === 'all' || meta.element === elementFilter;
return matchesSearch && matchesRarity && matchesElement;
});
if (filtered.length === 0) {
el.avatarsGrid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted); padding: 2rem;">No matching avatars</div>';
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 elemClass = getElementClass(meta.element);
const card = document.createElement('div');
card.className = 'card avatar-card';
card.className = `card avatar-card card-rarity-${meta.rarity}`;
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}">
@@ -319,18 +528,16 @@ function renderAvatarsGrid() {
<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>
<span class="element-badge ${elemClass}">${meta.element}</span>
<span class="path-badge">${meta.path}</span>
<span style="font-weight: 700; color: var(--accent-cyan);">Lv. ${item.level}</span>
</div>
</div>
</div>
</div>
<div class="eidolon-selector">
<span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 600;">${t('eidolonLabel')}</span>
<span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 700;">${t('eidolonLabel')}</span>
<div class="eidolon-buttons">
${pipsHtml}
</div>
@@ -347,17 +554,121 @@ function renderAvatarsGrid() {
});
});
// Clicking card opens full character build modal!
card.addEventListener('click', () => {
openAvatarDetailModal(item);
});
el.avatarsGrid.appendChild(card);
});
}
// Character Detail / Build Modal
function openAvatarDetailModal(avatarItem) {
state.selectedAvatarDetail = avatarItem;
const meta = getAvatarInfo(avatarItem.avatar_id);
const elemClass = getElementClass(meta.element);
el.modalCharTitle.textContent = `ปรับแต่งตัวละคร: ${meta.name} (ID: ${avatarItem.avatar_id})`;
el.modalCharImg.src = `./assets/avatars/${avatarItem.avatar_id}.png`;
el.modalCharImg.onerror = function() {
this.src = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${avatarItem.avatar_id}.png`;
};
el.modalCharName.textContent = meta.name;
el.modalCharMeta.innerHTML = `
<span class="element-badge ${elemClass}">${meta.element}</span>
<span class="path-badge">${meta.path}</span>
<span class="avatar-badge-rarity rarity-${meta.rarity}">★ ${meta.rarity}</span>
`;
el.modalCharLevel.value = avatarItem.level || 80;
el.modalCharPromotion.value = avatarItem.promotion || 6;
// Render modal eidolon pips
el.modalCharEidolonBtns.innerHTML = '';
for (let r = 0; r <= 6; r++) {
const btn = document.createElement('button');
btn.className = `eidolon-pip ${avatarItem.rank >= r ? 'active' : ''}`;
btn.textContent = r;
btn.style.width = '32px';
btn.style.height = '32px';
btn.style.fontSize = '0.9rem';
btn.addEventListener('click', () => {
avatarItem.rank = r;
el.modalCharEidolonBtns.querySelectorAll('.eidolon-pip').forEach((b, idx) => {
if (idx <= r) b.classList.add('active');
else b.classList.remove('active');
});
});
el.modalCharEidolonBtns.appendChild(btn);
}
el.modalAvatarDetail.classList.add('active');
}
// Save avatar details from modal
async function saveAvatarDetails() {
if (!state.selectedAvatarDetail) return;
const aid = state.selectedAvatarDetail.avatar_id;
const level = parseInt(el.modalCharLevel.value, 10) || 80;
const promotion = parseInt(el.modalCharPromotion.value, 10) || 6;
const rank = state.selectedAvatarDetail.rank ?? 6;
try {
await apiCall('/api/freesr/avatars', 'POST', {
avatar_id: aid,
level,
promotion,
rank
});
showToast(`บันทึกตัวละคร ${aid} สำเร็จ (Lv. ${level}, Prom. ${promotion}, E${rank})`, 'success');
el.modalAvatarDetail.classList.remove('active');
// Update local state and re-render
const target = state.avatarsList.find(a => a.avatar_id === aid);
if (target) {
target.level = level;
target.promotion = promotion;
target.rank = rank;
renderAvatarsGrid();
}
} catch (err) {
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
}
}
// Max Skills & Traces for character
async function maxSkillsForAvatar() {
if (!state.selectedAvatarDetail) return;
const aid = state.selectedAvatarDetail.avatar_id;
try {
// Read freesr data, max skill levels for this avatar, and push back
const freesr = await apiCall('/api/freesr');
if (freesr && freesr.data && freesr.data.avatars && freesr.data.avatars[aid]) {
const av = freesr.data.avatars[aid];
if (av.data && av.data.skills) {
for (const k in av.data.skills) {
av.data.skills[k] = 10;
}
}
av.level = 80;
av.promotion = 6;
av.data.rank = 6;
await apiCall('/api/freesr', 'POST', freesr.data);
showToast(`อัพสกิลและ Traces ทั้งหมดของตัวละคร ${aid} เป็น 10/10 สำเร็จ!`, 'success');
loadAvatars();
}
} catch (err) {
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
}
}
// Set avatar rank API
async function setAvatarRank(avatarId, rank) {
try {
await apiCall('/api/avatar/rank', 'POST', { avatar_id: avatarId, rank });
showToast(t('avatarRankSuccess', { id: avatarId, rank }), 'success');
// Update local state and re-render
const target = state.avatarsList.find(a => a.avatar_id === avatarId);
if (target) {
target.rank = rank;
@@ -381,10 +692,94 @@ async function maxAllAvatars() {
}
}
// Load Equipment (Lightcones & Relics)
async function loadEquipment() {
try {
const res = await apiCall('/api/freesr');
if (!res || !res.data) return;
state.fullFreesrData = res.data;
const lcs = res.data.lightcones || [];
const relics = res.data.relics || [];
el.badgeTotalLc.textContent = `${lcs.length} ชิ้น`;
el.badgeTotalRelics.textContent = `${relics.length} ชิ้น`;
// Render Lightcones
el.lightconesList.innerHTML = '';
lcs.slice(0, 100).forEach(lc => {
const item = document.createElement('div');
item.className = 'card';
item.style.padding = '0.75rem';
item.style.display = 'flex';
item.style.justifyContent = 'space-between';
item.style.alignItems = 'center';
const equipped = lc.equip_avatar ? getAvatarInfo(lc.equip_avatar).name : 'ไม่ได้สวมใส่';
item.innerHTML = `
<div>
<div style="font-weight: 700; font-size: 0.95rem;">Lightcone ID: ${lc.item_id} (UID: ${lc.internal_uid})</div>
<div style="font-size: 0.775rem; color: var(--text-muted); margin-top: 0.2rem;">
Lv. ${lc.level} • ขัดเกลา S${lc.rank} • สวมใส่โดย: <strong style="color: var(--accent-cyan);">${equipped}</strong>
</div>
</div>
`;
el.lightconesList.appendChild(item);
});
// Render Relics
el.relicsList.innerHTML = '';
relics.slice(0, 100).forEach(r => {
const item = document.createElement('div');
item.className = 'card';
item.style.padding = '0.75rem';
item.style.display = 'flex';
item.style.justifyContent = 'space-between';
item.style.alignItems = 'center';
const equipped = r.equip_avatar ? getAvatarInfo(r.equip_avatar).name : 'ไม่ได้สวมใส่';
const slotNum = r.relic_id % 10;
item.innerHTML = `
<div>
<div style="font-weight: 700; font-size: 0.95rem;">Relic ID: ${r.relic_id} (Slot: ${slotNum})</div>
<div style="font-size: 0.775rem; color: var(--text-muted); margin-top: 0.2rem;">
Lv. ${r.level} • สเตตัสหลัก: ${r.main_affix_id} • สวมใส่โดย: <strong style="color: var(--accent-gold);">${equipped}</strong>
</div>
</div>
`;
el.relicsList.appendChild(item);
});
} catch (err) {
showToast(`โหลดคลังอุปกรณ์ไม่สำเร็จ: ${err.message}`, 'error');
}
}
// Load Persistent Save Settings
async function loadPersistentSettings() {
try {
const res = await apiCall('/api/persistent');
if (!res || !res.persistent) return;
const p = res.persistent;
state.persistentData = p;
if (p.main_character) {
el.persistentMcSelect.value = p.main_character;
}
if (p.march_type) {
el.persistentMarchSelect.value = p.march_type;
}
el.toggleSwGlobal.checked = !!p.enable_sw_global;
el.toggleCastoriceGlobal.checked = !!p.enable_castorice_global;
} catch (err) {
console.warn('Persistent settings not available in current mode:', err.message);
}
}
// 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;">No Ex-Rank Avatars</div>';
el.exrankList.innerHTML = '<div style="color: var(--text-muted); text-align: center; padding: 1rem;">ไม่มีข้อมูลตัวละคร Ex-Rank</div>';
return;
}
@@ -396,7 +791,7 @@ function renderExRanks(exList) {
card.innerHTML = `
<div style="display: flex; gap: 0.85rem; align-items: center;">
<div class="avatar-image-container" style="width: 44px; height: 44px;">
<div class="avatar-image-container" style="width: 52px; height: 52px;">
<img class="avatar-img"
src="./assets/avatars/${item.avatar_id}.png"
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${item.avatar_id}.png';}else{this.style.display='none';this.nextElementSibling.style.display='flex';}"
@@ -404,7 +799,7 @@ function renderExRanks(exList) {
<div class="avatar-icon-placeholder" style="display: none;">${localizedMeta.name.charAt(0)}</div>
</div>
<div>
<div style="font-weight: 700; font-size: 1rem;">${localizedMeta.name}</div>
<div style="font-weight: 800; font-size: 1.05rem;">${localizedMeta.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>
@@ -514,7 +909,7 @@ async function loadJsonData(mode) {
async function saveJsonData() {
const text = el.jsonEditor.value.trim();
if (!text) {
showToast('No JSON data to save', 'error');
showToast('ไม่มีข้อมูล JSON ให้บันทึก', 'error');
return;
}
@@ -554,6 +949,10 @@ function setupTabs() {
// On-demand tab refresh
if (targetId === 'tab-avatars' && state.avatarsList.length === 0) {
loadAvatars();
} else if (targetId === 'tab-equipment') {
loadEquipment();
} else if (targetId === 'tab-persistent') {
loadPersistentSettings();
} else if (targetId === 'tab-json') {
loadJsonData(state.currentJsonMode);
}
@@ -590,6 +989,15 @@ function setupEvents() {
}
});
// Character Detail Modal Controls
el.btnCloseAvatarModal.addEventListener('click', () => el.modalAvatarDetail.classList.remove('active'));
el.btnModalSaveAvatar.addEventListener('click', saveAvatarDetails);
el.btnModalMaxSkills.addEventListener('click', maxSkillsForAvatar);
// Lineup Picker Modal Controls
el.btnClosePickerModal.addEventListener('click', () => el.modalLineupPicker.classList.remove('active'));
el.pickerSearch.addEventListener('input', renderPickerGrid);
// Quick Action Buttons
el.btnQuickMaxAll.addEventListener('click', maxAllAvatars);
el.btnMaxAllAvatars.addEventListener('click', maxAllAvatars);
@@ -602,16 +1010,50 @@ function setupEvents() {
el.btnQuickSyncPersistent.addEventListener('click', async () => {
try {
await apiCall('/api/persistent');
showToast('Persistent data synced', 'success');
showToast('ซิงค์ข้อมูล Persistent เรียบร้อย', 'success');
refreshStatus();
} catch (err) {
showToast(`Sync failed: ${err.message}`, 'error');
}
});
// Avatar Search & Filter
// Persistent Form Submit
el.formPersistentChars.addEventListener('submit', async (e) => {
e.preventDefault();
const mc = el.persistentMcSelect.value;
const march = el.persistentMarchSelect.value;
try {
await apiCall('/api/persistent', 'POST', {
main_character: mc,
march_type: march
});
showToast('บันทึกการตั้งค่าตัวละครสำเร็จ!', 'success');
refreshStatus();
} catch (err) {
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
}
});
// Persistent Toggles
el.btnSavePersistentToggles.addEventListener('click', async () => {
const sw = el.toggleSwGlobal.checked;
const castorice = el.toggleCastoriceGlobal.checked;
try {
await apiCall('/api/persistent', 'POST', {
enable_sw_global: sw,
enable_castorice_global: castorice
});
showToast('บันทึกสวิตช์ Global เรียบร้อย!', 'success');
refreshStatus();
} catch (err) {
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
}
});
// Avatar Search & Filters
el.avatarSearch.addEventListener('input', renderAvatarsGrid);
el.avatarFilterRarity.addEventListener('change', renderAvatarsGrid);
el.avatarFilterElement.addEventListener('change', renderAvatarsGrid);
el.btnReloadAvatars.addEventListener('click', loadAvatars);
// Ex-Rank All Controls
@@ -657,7 +1099,7 @@ function setupEvents() {
try {
const parsed = JSON.parse(el.jsonEditor.value);
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
showToast('JSON beautified', 'info');
showToast('จัดรูปแบบ JSON สวยงามแล้ว', 'info');
} catch (err) {
showToast(t('jsonInvalid'), 'error');
}
@@ -672,7 +1114,7 @@ function setupEvents() {
a.href = URL.createObjectURL(blob);
a.download = `${state.currentJsonMode}-${Date.now()}.json`;
a.click();
showToast(`Downloaded ${a.download}`, 'success');
showToast(`ดาวน์โหลด ${a.download} เรียบร้อย`, 'success');
});
el.jsonFileInput.addEventListener('change', (e) => {
@@ -683,7 +1125,7 @@ function setupEvents() {
try {
const parsed = JSON.parse(event.target.result);
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
showToast(`Loaded ${file.name}`, 'success');
showToast(`โหลดเนื้อหาไฟล์ ${file.name} เรียบร้อย`, 'success');
} catch (err) {
showToast(t('jsonInvalid'), 'error');
}
@@ -728,11 +1170,13 @@ function startPingLoop() {
// Initialize Application
function init() {
setupServerModes();
setupLanguageSelector();
setupTabs();
setupEvents();
applyTranslations();
refreshStatus();
loadAvatars();
startPingLoop();
}