feat: upgrade character build modal with skills info, traces, energy, and lightcones

This commit is contained in:
CAtlas0784 committed 2026-10-16 23:49:27 +07:00
1 parent edc2843f0c
commit 42b6d6a5a9
4 files changed
+735 -80

No files matched your search

+158
View File
@@ -966,6 +966,164 @@ input:checked + .slider:before {
padding-bottom: 0.75rem;
}
/* Modal Sub-tabs */
.modal-subtabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.25rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.5rem;
overflow-x: auto;
}
.modal-subtab-btn {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 0.875rem;
font-weight: 700;
padding: 0.45rem 0.95rem;
border-radius: var(--radius-sm);
cursor: pointer;
transition: var(--transition);
white-space: nowrap;
}
.modal-subtab-btn:hover {
color: var(--text-highlight);
background: rgba(255, 255, 255, 0.05);
}
.modal-subtab-btn.active {
color: #fff;
background: var(--accent-blue);
box-shadow: 0 0 12px rgba(2, 132, 199, 0.45);
}
/* Skill Stepper & Row Cards (Inspired by srtools.punklorde.org/skills-info) */
.skill-row-card {
background: rgba(0, 0, 0, 0.35);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 0.75rem 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.skill-row-card:hover {
border-color: rgba(56, 189, 248, 0.4);
box-shadow: 0 0 12px rgba(56, 189, 248, 0.1);
}
.skill-row-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.skill-name {
font-weight: 700;
font-size: 0.925rem;
color: var(--text-highlight);
}
.skill-lvl-badge {
font-size: 0.85rem;
font-family: monospace;
color: var(--accent-cyan);
background: rgba(56, 189, 248, 0.12);
padding: 0.2rem 0.55rem;
border-radius: 4px;
border: 1px solid rgba(56, 189, 248, 0.35);
}
.skill-stepper {
display: flex;
align-items: center;
gap: 0.75rem;
}
.btn-step {
width: 32px;
height: 32px;
border-radius: 6px;
border: 1px solid var(--border-color);
background: var(--bg-card);
color: #fff;
font-size: 1.15rem;
font-weight: 800;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
user-select: none;
}
.btn-step:hover {
border-color: var(--accent-cyan);
background: rgba(56, 189, 248, 0.25);
transform: scale(1.05);
}
.btn-max-pill {
padding: 0.25rem 0.65rem;
border-radius: 6px;
border: 1px solid rgba(251, 191, 36, 0.5);
background: rgba(251, 191, 36, 0.15);
color: var(--accent-gold);
font-size: 0.75rem;
font-weight: 800;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-max-pill:hover {
background: rgba(251, 191, 36, 0.35);
transform: scale(1.05);
}
.skill-range {
flex: 1;
accent-color: var(--accent-cyan);
cursor: pointer;
height: 6px;
}
.trace-toggle-label {
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 0.85rem;
color: var(--text-main);
background: rgba(0, 0, 0, 0.3);
padding: 0.55rem 0.85rem;
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease;
}
.trace-toggle-label:hover {
border-color: var(--accent-cyan);
background: rgba(56, 189, 248, 0.08);
}
.trace-checkbox {
width: 17px;
height: 17px;
accent-color: var(--accent-cyan);
cursor: pointer;
}
.lc-super-pip {
width: 40px;
height: 36px;
font-size: 0.875rem;
}
/* Responsive */
@media (max-width: 900px) {
.lineup-grid {
+177 -26
View File
@@ -115,7 +115,7 @@
</div>
</div>
<div>
<span class="form-label">พิกัดตัวละคร (X, Y, Z)</span>
<span class="form-label">พิกัดตัวละคร XYZ</span>
<span id="lbl-coordinates" style="color: var(--text-muted); font-size: 0.85rem;">-</span>
</div>
</div>
@@ -380,7 +380,8 @@
<!-- Character Detail / Build Modal -->
<div class="modal-overlay" id="modal-avatar-detail">
<div class="modal-content" style="max-width: 640px;">
<div class="modal-content" style="max-width: 760px;">
<!-- Modal Header -->
<div class="modal-header">
<h3 id="modal-char-title" style="font-weight: 800; font-size: 1.15rem; color: var(--text-highlight);">
ปรับแต่งตัวละคร
@@ -388,29 +389,147 @@
<button class="btn btn-sm btn-secondary" id="btn-close-avatar-modal">✕</button>
</div>
<!-- Character Top Banner & Quick Max All -->
<div style="display: flex; gap: 1.25rem; align-items: center; margin-bottom: 1.25rem; flex-wrap: wrap;">
<div class="avatar-image-container" style="width: 76px; height: 76px;">
<img id="modal-char-img" class="avatar-img" src="" alt="">
</div>
<div style="flex: 1; min-width: 180px;">
<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; display: flex; gap: 0.35rem; align-items: center; flex-wrap: wrap;"></div>
<div id="modal-char-meta" style="font-size: 0.85rem; color: var(--text-muted); margin-top: 0.25rem; display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap;"></div>
</div>
<button type="button" class="btn btn-gold btn-sm" id="btn-modal-max-all-char">
<button type="button" class="btn btn-gold" id="btn-modal-max-all-char" style="padding: 0.5rem 1rem; font-weight: 800;">
⭐ ปรับเต็มทั้งหมด
</button>
</div>
<!-- Sub Tabs Navigation (srtools style) -->
<div class="modal-subtabs" style="display: flex; gap: 0.5rem; margin-bottom: 1.25rem; border-bottom: 1px solid var(--border-color); padding-bottom: 0.5rem; overflow-x: auto;">
<button type="button" class="modal-subtab-btn active" data-modaltab="modaltab-skills">
⚡ สกิลและร่องรอย
</button>
<button type="button" class="modal-subtab-btn" data-modaltab="modaltab-status">
⚙️ เลเวล & Eidolon
</button>
<button type="button" class="modal-subtab-btn" data-modaltab="modaltab-lightcone">
🗡️ กรวยแสง
</button>
</div>
<!-- Sub Tab 1: Skills & Traces (Like https://srtools.punklorde.org/skills-info) -->
<div id="modaltab-skills" class="modal-subtab-content active">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-wrap: wrap; gap: 0.5rem;">
<div style="font-weight: 700; font-size: 0.95rem; color: var(--text-highlight);">
ระดับสกิลต่อสู้
</div>
<button type="button" class="btn btn-sm btn-gold" id="btn-modal-max-skills">
⭐ ปรับสกิลเต็ม 10/10
</button>
</div>
<!-- 4 Core Skills -->
<div class="skills-grid" style="display: flex; flex-direction: column; gap: 0.75rem;">
<!-- 1. Basic Attack -->
<div class="skill-row-card">
<div class="skill-row-header">
<span class="skill-name">โจมตีปกติ</span>
<span class="skill-lvl-badge">Lv. <strong id="val-skill-basic">6</strong> / 6</span>
</div>
<div class="skill-stepper">
<button type="button" class="btn-step" id="btn-step-dec-basic">-</button>
<input type="range" min="1" max="6" value="6" id="range-skill-basic" class="skill-range">
<button type="button" class="btn-step" id="btn-step-inc-basic">+</button>
<button type="button" class="btn-max-pill" id="btn-skill-max-basic">MAX</button>
</div>
</div>
<!-- 2. Skill -->
<div class="skill-row-card">
<div class="skill-row-header">
<span class="skill-name">สกิลต่อสู้</span>
<span class="skill-lvl-badge">Lv. <strong id="val-skill-skill">10</strong> / 10</span>
</div>
<div class="skill-stepper">
<button type="button" class="btn-step" id="btn-step-dec-skill">-</button>
<input type="range" min="1" max="10" value="10" id="range-skill-skill" class="skill-range">
<button type="button" class="btn-step" id="btn-step-inc-skill">+</button>
<button type="button" class="btn-max-pill" id="btn-skill-max-skill">MAX</button>
</div>
</div>
<!-- 3. Ultimate -->
<div class="skill-row-card">
<div class="skill-row-header">
<span class="skill-name">ท่าไม้ตาย</span>
<span class="skill-lvl-badge">Lv. <strong id="val-skill-ultra">10</strong> / 10</span>
</div>
<div class="skill-stepper">
<button type="button" class="btn-step" id="btn-step-dec-ultra">-</button>
<input type="range" min="1" max="10" value="10" id="range-skill-ultra" class="skill-range">
<button type="button" class="btn-step" id="btn-step-inc-ultra">+</button>
<button type="button" class="btn-max-pill" id="btn-skill-max-ultra">MAX</button>
</div>
</div>
<!-- 4. Talent -->
<div class="skill-row-card">
<div class="skill-row-header">
<span class="skill-name">พรสวรรค์</span>
<span class="skill-lvl-badge">Lv. <strong id="val-skill-talent">10</strong> / 10</span>
</div>
<div class="skill-stepper">
<button type="button" class="btn-step" id="btn-step-dec-talent">-</button>
<input type="range" min="1" max="10" value="10" id="range-skill-talent" class="skill-range">
<button type="button" class="btn-step" id="btn-step-inc-talent">+</button>
<button type="button" class="btn-max-pill" id="btn-skill-max-talent">MAX</button>
</div>
</div>
</div>
<!-- Traces & Technique Section -->
<div style="margin-top: 1.25rem;">
<div style="font-weight: 700; font-size: 0.95rem; color: var(--text-highlight); margin-bottom: 0.75rem;">
ร่องรอยและเทคนิค
</div>
<div style="display: flex; flex-direction: column; gap: 0.6rem;">
<label class="trace-toggle-label">
<input type="checkbox" id="check-trace-technique" checked class="trace-checkbox">
<span>⚡ เทคนิค — เปิดใช้งานก่อนเริ่มการต่อสู้</span>
</label>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.6rem;">
<label class="trace-toggle-label">
<input type="checkbox" id="check-trace-a2" checked class="trace-checkbox">
<span>🌟 ร่องรอยพิเศษ A2</span>
</label>
<label class="trace-toggle-label">
<input type="checkbox" id="check-trace-a4" checked class="trace-checkbox">
<span>🌟 ร่องรอยพิเศษ A4</span>
</label>
<label class="trace-toggle-label">
<input type="checkbox" id="check-trace-a6" checked class="trace-checkbox">
<span>🌟 ร่องรอยพิเศษ A6</span>
</label>
</div>
<label class="trace-toggle-label">
<input type="checkbox" id="check-trace-stats" checked class="trace-checkbox">
<span>💎 โหนดสเตตัสเสริมทั้งหมด (10 โหนด)</span>
</label>
</div>
</div>
</div>
<!-- Sub Tab 2: Status & Eidolon -->
<div id="modaltab-status" class="modal-subtab-content" style="display: none;">
<div class="grid-2" style="gap: 1rem; margin-bottom: 1rem;">
<div class="form-group">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.35rem;">
<label class="form-label" style="margin: 0;">ระดับเลเวล (1-80)</label>
<div style="display: flex; gap: 0.25rem;">
<button type="button" class="btn btn-sm btn-secondary btn-lvl-preset" data-lvl="1" style="padding: 0.15rem 0.45rem; font-size: 0.75rem;">1</button>
<button type="button" class="btn btn-sm btn-secondary btn-lvl-preset" data-lvl="20" style="padding: 0.15rem 0.45rem; font-size: 0.75rem;">20</button>
<button type="button" class="btn btn-sm btn-secondary btn-lvl-preset" data-lvl="60" style="padding: 0.15rem 0.45rem; font-size: 0.75rem;">60</button>
<button type="button" class="btn btn-sm btn-secondary btn-lvl-preset" data-lvl="70" style="padding: 0.15rem 0.45rem; font-size: 0.75rem;">70</button>
<button type="button" class="btn btn-sm btn-primary btn-lvl-preset" data-lvl="80" style="padding: 0.15rem 0.45rem; font-size: 0.75rem;">80</button>
<button type="button" class="btn btn-sm btn-secondary btn-lvl-preset" data-lvl="1">1</button>
<button type="button" class="btn btn-sm btn-secondary btn-lvl-preset" data-lvl="20">20</button>
<button type="button" class="btn btn-sm btn-secondary btn-lvl-preset" data-lvl="60">60</button>
<button type="button" class="btn btn-sm btn-secondary btn-lvl-preset" data-lvl="70">70</button>
<button type="button" class="btn btn-sm btn-primary btn-lvl-preset" data-lvl="80">80</button>
</div>
</div>
<input type="number" id="modal-char-level" class="form-input" min="1" max="80" value="80">
@@ -419,25 +538,22 @@
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.35rem;">
<label class="form-label" style="margin: 0;">การเลื่อนขั้น (0-6)</label>
<div style="display: flex; gap: 0.25rem;">
<button type="button" class="btn btn-sm btn-secondary btn-prom-preset" data-prom="0" style="padding: 0.15rem 0.45rem; font-size: 0.75rem;">0</button>
<button type="button" class="btn btn-sm btn-secondary btn-prom-preset" data-prom="3" style="padding: 0.15rem 0.45rem; font-size: 0.75rem;">3</button>
<button type="button" class="btn btn-sm btn-primary btn-prom-preset" data-prom="6" style="padding: 0.15rem 0.45rem; font-size: 0.75rem;">6</button>
<button type="button" class="btn btn-sm btn-secondary btn-prom-preset" data-prom="0">0</button>
<button type="button" class="btn btn-sm btn-secondary btn-prom-preset" data-prom="3">3</button>
<button type="button" class="btn btn-sm btn-primary btn-prom-preset" data-prom="6">6</button>
</div>
</div>
<input type="number" id="modal-char-promotion" class="form-input" min="0" max="6" value="6">
</div>
</div>
<!-- Eidolon Section -->
<div class="form-group">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.35rem;">
<label class="form-label" style="margin: 0;">ระดับ Eidolon</label>
<div style="display: flex; gap: 0.4rem;">
<button type="button" class="btn btn-sm btn-secondary" id="btn-modal-quick-e0" style="padding: 0.2rem 0.6rem; font-size: 0.75rem;">
รีเซ็ต E0
</button>
<button type="button" class="btn btn-sm btn-gold" id="btn-modal-quick-e6" style="padding: 0.2rem 0.6rem; font-size: 0.75rem;">
⭐ ปลดเต็ม E6
</button>
<button type="button" class="btn btn-sm btn-secondary" id="btn-modal-quick-e0">รีเซ็ต E0</button>
<button type="button" class="btn btn-sm btn-gold" id="btn-modal-quick-e6">⭐ ปลดเต็ม E6</button>
</div>
</div>
<div class="eidolon-buttons" id="modal-char-eidolon-btns" style="margin-top: 0.5rem; gap: 0.5rem;">
@@ -445,18 +561,53 @@
</div>
</div>
<div class="form-group" style="margin-top: 1rem;">
<label class="form-label">สกิลและร่องรอย</label>
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 0.75rem; display: flex; justify-content: space-between; align-items: center;">
<div style="font-size: 0.85rem; color: var(--text-muted);">
โจมตีปกติเลเวล 6, สกิลต่อสู้เลเวล 10, ท่าไม้ตายเลเวล 10, พรสวรรค์เลเวล 10 และโหนดเสริมทั้งหมด
<!-- Ultimate Energy Section -->
<div class="form-group" style="margin-top: 1.25rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.35rem;">
<label class="form-label" style="margin: 0;">พลังงานท่าไม้ตาย</label>
<div style="display: flex; gap: 0.35rem;">
<button type="button" class="btn btn-sm btn-secondary" id="btn-modal-energy-50">ตั้งค่า 50%</button>
<button type="button" class="btn btn-sm btn-primary" id="btn-modal-energy-100">เต็ม 100%</button>
</div>
</div>
<div style="display: flex; gap: 0.75rem; align-items: center;">
<input type="range" id="range-char-energy" min="0" max="140" value="140" class="skill-range" style="flex: 1;">
<span id="val-char-energy" style="font-weight: 800; font-family: monospace; min-width: 60px; text-align: right;">140/140</span>
</div>
<button type="button" class="btn btn-sm btn-gold" id="btn-modal-max-skills">
⭐ อัพสกิล 10/10
</button>
</div>
</div>
<!-- Sub Tab 3: Lightcone Equipment -->
<div id="modaltab-lightcone" class="modal-subtab-content" style="display: none;">
<div class="card" style="padding: 1.25rem; background: rgba(0,0,0,0.3); border: 1px solid var(--border-color);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-wrap: wrap; gap: 0.5rem;">
<span style="font-weight: 800; font-size: 1rem; color: var(--text-highlight);">กรวยแสงที่สวมใส่</span>
<button type="button" class="btn btn-sm btn-gold" id="btn-modal-max-lc">⭐ ปรับกรวยแสงเต็มขั้น</button>
</div>
<div class="form-group">
<label class="form-label">ID กรวยแสง</label>
<input type="number" id="modal-lc-id" class="form-input" placeholder="เช่น 21002 หรือ 24001">
</div>
<div class="grid-2" style="gap: 1rem;">
<div class="form-group">
<label class="form-label">เลเวลกรวยแสง (1-80)</label>
<input type="number" id="modal-lc-level" class="form-input" min="1" max="80" value="80">
</div>
<div class="form-group">
<label class="form-label">ระดับการขัดเกลา</label>
<div class="eidolon-buttons" id="modal-lc-rank-btns" style="margin-top: 0.25rem;">
<button type="button" class="eidolon-pip lc-super-pip" data-rank="1">S1</button>
<button type="button" class="eidolon-pip lc-super-pip" data-rank="2">S2</button>
<button type="button" class="eidolon-pip lc-super-pip" data-rank="3">S3</button>
<button type="button" class="eidolon-pip lc-super-pip" data-rank="4">S4</button>
<button type="button" class="eidolon-pip lc-super-pip active" data-rank="5">S5</button>
</div>
</div>
</div>
</div>
</div>
<!-- Modal Footer -->
<div style="display: flex; gap: 0.75rem; margin-top: 1.5rem; justify-content: flex-end; flex-wrap: wrap;">
<button type="button" class="btn btn-secondary" id="btn-modal-cancel-avatar">
✕ ปิด
+392 -46
View File
@@ -106,6 +106,47 @@ const el = {
btnModalMaxSkills: document.getElementById('btn-modal-max-skills'),
btnModalSaveAvatar: document.getElementById('btn-modal-save-avatar'),
// Skills Info Steppers (Inspired by srtools.punklorde.org/skills-info)
valSkillBasic: document.getElementById('val-skill-basic'),
rangeSkillBasic: document.getElementById('range-skill-basic'),
btnStepDecBasic: document.getElementById('btn-step-dec-basic'),
btnStepIncBasic: document.getElementById('btn-step-inc-basic'),
btnSkillMaxBasic: document.getElementById('btn-skill-max-basic'),
valSkillSkill: document.getElementById('val-skill-skill'),
rangeSkillSkill: document.getElementById('range-skill-skill'),
btnStepDecSkill: document.getElementById('btn-step-dec-skill'),
btnStepIncSkill: document.getElementById('btn-step-inc-skill'),
btnSkillMaxSkill: document.getElementById('btn-skill-max-skill'),
valSkillUltra: document.getElementById('val-skill-ultra'),
rangeSkillUltra: document.getElementById('range-skill-ultra'),
btnStepDecUltra: document.getElementById('btn-step-dec-ultra'),
btnStepIncUltra: document.getElementById('btn-step-inc-ultra'),
btnSkillMaxUltra: document.getElementById('btn-skill-max-ultra'),
valSkillTalent: document.getElementById('val-skill-talent'),
rangeSkillTalent: document.getElementById('range-skill-talent'),
btnStepDecTalent: document.getElementById('btn-step-dec-talent'),
btnStepIncTalent: document.getElementById('btn-step-inc-talent'),
btnSkillMaxTalent: document.getElementById('btn-skill-max-talent'),
checkTraceTechnique: document.getElementById('check-trace-technique'),
checkTraceA2: document.getElementById('check-trace-a2'),
checkTraceA4: document.getElementById('check-trace-a4'),
checkTraceA6: document.getElementById('check-trace-a6'),
checkTraceStats: document.getElementById('check-trace-stats'),
btnModalEnergy50: document.getElementById('btn-modal-energy-50'),
btnModalEnergy100: document.getElementById('btn-modal-energy-100'),
rangeCharEnergy: document.getElementById('range-char-energy'),
valCharEnergy: document.getElementById('val-char-energy'),
modalLcId: document.getElementById('modal-lc-id'),
modalLcLevel: document.getElementById('modal-lc-level'),
btnModalMaxLc: document.getElementById('btn-modal-max-lc'),
modalLcRankBtns: document.getElementById('modal-lc-rank-btns'),
// Lineup Picker Modal
modalLineupPicker: document.getElementById('modal-lineup-picker'),
btnClosePickerModal: document.getElementById('btn-close-picker-modal'),
@@ -671,15 +712,52 @@ async function maxSingleAvatar(avatarId) {
}
}
// Character Detail / Build Modal
// Active Lightcone Superimposition Rank in Modal (1 - 5)
let selectedLcRank = 5;
// Modal Sub-tabs Handler
function setupModalSubtabs() {
const tabBtns = document.querySelectorAll('.modal-subtab-btn');
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
tabBtns.forEach(b => b.classList.remove('active'));
document.querySelectorAll('.modal-subtab-content').forEach(c => {
c.style.display = 'none';
c.classList.remove('active');
});
btn.classList.add('active');
const targetId = btn.getAttribute('data-modaltab');
const targetContent = document.getElementById(targetId);
if (targetContent) {
targetContent.style.display = 'block';
targetContent.classList.add('active');
}
});
});
}
function updateLcRankPips(rank) {
selectedLcRank = rank;
document.querySelectorAll('.lc-super-pip').forEach(btn => {
const r = parseInt(btn.getAttribute('data-rank'), 10);
if (r === rank) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
}
// Character Detail / Build Modal (Inspired by srtools.punklorde.org/skills-info)
function openAvatarDetailModal(avatarItem) {
state.selectedAvatarDetail = avatarItem;
const meta = getAvatarInfo(avatarItem.avatar_id);
const aid = avatarItem.avatar_id;
const meta = getAvatarInfo(aid);
el.modalCharTitle.textContent = `ปรับแต่งตัวละคร: ${meta.name} (ID: ${avatarItem.avatar_id})`;
el.modalCharImg.src = `./assets/avatars/${avatarItem.avatar_id}.png`;
el.modalCharTitle.textContent = `ปรับแต่งตัวละคร: ${meta.name} (ID: ${aid})`;
el.modalCharImg.src = `./assets/avatars/${aid}.png`;
el.modalCharImg.onerror = function() {
this.src = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${avatarItem.avatar_id}.png`;
this.src = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${aid}.png`;
};
el.modalCharName.textContent = meta.name;
el.modalCharMeta.innerHTML = `
@@ -688,10 +766,97 @@ function openAvatarDetailModal(avatarItem) {
<span class="avatar-badge-rarity rarity-${meta.rarity}" style="position: static;">★ ${meta.rarity}</span>
`;
// Reset to first sub-tab (Skills & Traces)
document.querySelectorAll('.modal-subtab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.modal-subtab-content').forEach(c => {
c.style.display = 'none';
c.classList.remove('active');
});
const firstTabBtn = document.querySelector('.modal-subtab-btn[data-modaltab="modaltab-skills"]');
if (firstTabBtn) firstTabBtn.classList.add('active');
const firstTabContent = document.getElementById('modaltab-skills');
if (firstTabContent) {
firstTabContent.style.display = 'block';
firstTabContent.classList.add('active');
}
// Basic Status
el.modalCharLevel.value = avatarItem.level || 80;
el.modalCharPromotion.value = avatarItem.promotion || 6;
renderModalEidolonPips(avatarItem.rank ?? 6);
// Load avatar data from fullFreesrData if available
const av = state.fullFreesrData?.avatars?.[aid];
const skills = av?.data?.skills || {};
const anchorSkills = av?.data?.skills_by_anchor_type || {};
// 1. Basic Attack: anchor 1 or aid + '001' (1-6)
const basicVal = anchorSkills['1'] ?? skills[`${aid}001`] ?? 6;
if (el.rangeSkillBasic && el.valSkillBasic) {
el.rangeSkillBasic.value = Math.min(6, Math.max(1, basicVal));
el.valSkillBasic.textContent = el.rangeSkillBasic.value;
}
// 2. Skill: anchor 2 or aid + '002' (1-10)
const skillVal = anchorSkills['2'] ?? skills[`${aid}002`] ?? 10;
if (el.rangeSkillSkill && el.valSkillSkill) {
el.rangeSkillSkill.value = Math.min(10, Math.max(1, skillVal));
el.valSkillSkill.textContent = el.rangeSkillSkill.value;
}
// 3. Ultimate: anchor 3 or aid + '003' (1-10)
const ultraVal = anchorSkills['3'] ?? skills[`${aid}003`] ?? 10;
if (el.rangeSkillUltra && el.valSkillUltra) {
el.rangeSkillUltra.value = Math.min(10, Math.max(1, ultraVal));
el.valSkillUltra.textContent = el.rangeSkillUltra.value;
}
// 4. Talent: anchor 4 or aid + '004' (1-10)
const talentVal = anchorSkills['4'] ?? skills[`${aid}004`] ?? 10;
if (el.rangeSkillTalent && el.valSkillTalent) {
el.rangeSkillTalent.value = Math.min(10, Math.max(1, talentVal));
el.valSkillTalent.textContent = el.rangeSkillTalent.value;
}
// Traces & Technique
if (el.checkTraceTechnique) {
el.checkTraceTechnique.checked = (anchorSkills['5'] ?? skills[`${aid}007`] ?? 1) > 0;
}
if (el.checkTraceA2) {
el.checkTraceA2.checked = (anchorSkills['6'] ?? skills[`${aid}101`] ?? 1) > 0;
}
if (el.checkTraceA4) {
el.checkTraceA4.checked = (anchorSkills['7'] ?? skills[`${aid}102`] ?? 1) > 0;
}
if (el.checkTraceA6) {
el.checkTraceA6.checked = (anchorSkills['8'] ?? skills[`${aid}103`] ?? 1) > 0;
}
if (el.checkTraceStats) {
el.checkTraceStats.checked = (anchorSkills['9'] ?? skills[`${aid}201`] ?? 1) > 0;
}
// Ultimate Energy
const maxSp = av?.sp_max ?? 140;
const currentSp = av?.sp_value ?? maxSp;
if (el.rangeCharEnergy && el.valCharEnergy) {
el.rangeCharEnergy.max = maxSp;
el.rangeCharEnergy.value = currentSp;
el.valCharEnergy.textContent = `${currentSp}/${maxSp}`;
}
// Lightcone
const equippedLc = (state.fullFreesrData?.lightcones || []).find(lc => lc.equip_avatar === aid);
if (equippedLc) {
if (el.modalLcId) el.modalLcId.value = equippedLc.item_id || '';
if (el.modalLcLevel) el.modalLcLevel.value = equippedLc.level || 80;
selectedLcRank = equippedLc.rank || 5;
} else {
if (el.modalLcId) el.modalLcId.value = '';
if (el.modalLcLevel) el.modalLcLevel.value = 80;
selectedLcRank = 5;
}
updateLcRankPips(selectedLcRank);
el.modalAvatarDetail.classList.add('active');
}
@@ -746,6 +911,63 @@ async function setAvatarRank(avatarId, rank) {
}
}
// Max Skills & Traces for character
function maxSkillsForAvatar() {
if (el.rangeSkillBasic && el.valSkillBasic) {
el.rangeSkillBasic.value = 6;
el.valSkillBasic.textContent = 6;
}
if (el.rangeSkillSkill && el.valSkillSkill) {
el.rangeSkillSkill.value = 10;
el.valSkillSkill.textContent = 10;
}
if (el.rangeSkillUltra && el.valSkillUltra) {
el.rangeSkillUltra.value = 10;
el.valSkillUltra.textContent = 10;
}
if (el.rangeSkillTalent && el.valSkillTalent) {
el.rangeSkillTalent.value = 10;
el.valSkillTalent.textContent = 10;
}
if (el.checkTraceTechnique) el.checkTraceTechnique.checked = true;
if (el.checkTraceA2) el.checkTraceA2.checked = true;
if (el.checkTraceA4) el.checkTraceA4.checked = true;
if (el.checkTraceA6) el.checkTraceA6.checked = true;
if (el.checkTraceStats) el.checkTraceStats.checked = true;
showToast('⭐ ปรับสกิลและร่องรอยเต็มทั้งหมดแล้ว', 'info');
}
// Max All Build For Current Avatar (Lv.80, Prom 6, E6, Max Skills, Max Traces, 100% Energy, LC Lv.80 S5)
async function maxAllBuildForCurrentAvatar() {
if (!state.selectedAvatarDetail) return;
const aid = state.selectedAvatarDetail.avatar_id;
// Level & Eidolon
el.modalCharLevel.value = 80;
el.modalCharPromotion.value = 6;
state.selectedAvatarDetail.level = 80;
state.selectedAvatarDetail.promotion = 6;
state.selectedAvatarDetail.rank = 6;
renderModalEidolonPips(6);
// Skills & Traces
maxSkillsForAvatar();
// Energy 100%
if (el.rangeCharEnergy && el.valCharEnergy) {
el.rangeCharEnergy.value = el.rangeCharEnergy.max;
el.valCharEnergy.textContent = `${el.rangeCharEnergy.max}/${el.rangeCharEnergy.max}`;
}
// Lightcone
if (el.modalLcLevel) el.modalLcLevel.value = 80;
updateLcRankPips(5);
// Save changes
await saveAvatarDetails();
}
// Save avatar details from modal
async function saveAvatarDetails() {
if (!state.selectedAvatarDetail) return;
@@ -765,55 +987,133 @@ async function saveAvatarDetails() {
target.rank = rank;
}
if (state.fullFreesrData?.avatars?.[aid]) {
state.fullFreesrData.avatars[aid].level = level;
state.fullFreesrData.avatars[aid].promotion = promotion;
state.fullFreesrData.avatars[aid].data.rank = rank;
if (!state.fullFreesrData) {
state.fullFreesrData = { avatars: {}, lightcones: [], relics: [] };
}
if (!state.fullFreesrData.avatars) {
state.fullFreesrData.avatars = {};
}
if (!state.fullFreesrData.avatars[aid]) {
state.fullFreesrData.avatars[aid] = {
owner_uid: 0,
avatar_id: aid,
level,
promotion,
techniques: [],
sp_value: parseInt(el.rangeCharEnergy?.value, 10) || 140,
sp_max: parseInt(el.rangeCharEnergy?.max, 10) || 140,
data: {
rank,
skills: {},
skills_by_anchor_type: {}
}
};
}
const av = state.fullFreesrData.avatars[aid];
av.level = level;
av.promotion = promotion;
av.data = av.data || {};
av.data.rank = rank;
if (el.rangeCharEnergy) {
av.sp_value = parseInt(el.rangeCharEnergy.value, 10) || av.sp_max || 140;
}
// Extract skills values
const basicVal = parseInt(el.rangeSkillBasic?.value, 10) || 6;
const skillVal = parseInt(el.rangeSkillSkill?.value, 10) || 10;
const ultraVal = parseInt(el.rangeSkillUltra?.value, 10) || 10;
const talentVal = parseInt(el.rangeSkillTalent?.value, 10) || 10;
const techVal = el.checkTraceTechnique?.checked ? 1 : 0;
const a2Val = el.checkTraceA2?.checked ? 1 : 0;
const a4Val = el.checkTraceA4?.checked ? 1 : 0;
const a6Val = el.checkTraceA6?.checked ? 1 : 0;
const statVal = el.checkTraceStats?.checked ? 1 : 0;
av.data.skills = av.data.skills || {};
av.data.skills[`${aid}001`] = basicVal;
av.data.skills[`${aid}002`] = skillVal;
av.data.skills[`${aid}003`] = ultraVal;
av.data.skills[`${aid}004`] = talentVal;
av.data.skills[`${aid}007`] = techVal;
av.data.skills[`${aid}101`] = a2Val;
av.data.skills[`${aid}102`] = a4Val;
av.data.skills[`${aid}103`] = a6Val;
for (let i = 1; i <= 10; i++) {
const pad = i < 10 ? `0${i}` : `${i}`;
av.data.skills[`${aid}2${pad}`] = statVal;
}
av.data.skills_by_anchor_type = {
"1": basicVal,
"2": skillVal,
"3": ultraVal,
"4": talentVal,
"5": techVal,
"6": a2Val,
"7": a4Val,
"8": a6Val,
"9": statVal,
"10": statVal,
"11": statVal,
"12": statVal,
"13": statVal,
"14": statVal,
"15": statVal,
"16": statVal,
"17": statVal,
"18": statVal
};
// Lightcone update
state.fullFreesrData.lightcones = state.fullFreesrData.lightcones || [];
const lcId = parseInt(el.modalLcId?.value, 10);
const lcLevel = parseInt(el.modalLcLevel?.value, 10) || 80;
const lcRank = selectedLcRank || 5;
const lcProm = lcLevel > 70 ? 6 : (lcLevel > 60 ? 5 : (lcLevel > 50 ? 4 : (lcLevel > 40 ? 3 : (lcLevel > 20 ? 1 : 0))));
const existingLcIndex = state.fullFreesrData.lightcones.findIndex(lc => lc.equip_avatar === aid);
if (lcId && lcId > 0) {
if (existingLcIndex !== -1) {
state.fullFreesrData.lightcones[existingLcIndex].item_id = lcId;
state.fullFreesrData.lightcones[existingLcIndex].level = lcLevel;
state.fullFreesrData.lightcones[existingLcIndex].rank = lcRank;
state.fullFreesrData.lightcones[existingLcIndex].promotion = lcProm;
} else {
state.fullFreesrData.lightcones.push({
internal_uid: state.fullFreesrData.lightcones.length,
item_id: lcId,
equip_avatar: aid,
level: lcLevel,
rank: lcRank,
promotion: lcProm
});
}
} else if (existingLcIndex !== -1) {
state.fullFreesrData.lightcones[existingLcIndex].equip_avatar = null;
}
renderAvatarsGrid();
loadEquipment();
el.modalAvatarDetail.classList.remove('active');
if (state.isConnected) {
try {
if (state.serverMode === 'gac') {
await apiCall('/api/freesr/avatars', 'POST', { avatar_id: aid, level, promotion, rank });
await saveFreesrChangesToServer();
} else {
await saveFreesrChangesToServer();
}
showToast(`บันทึกตัวละคร ${getAvatarInfo(aid).name} เรียบร้อย (Lv. ${level}, Prom. ${promotion}, E${rank})`, 'success');
showToast(`บันทึกตัวละคร ${getAvatarInfo(aid).name} เรียบร้อย`, 'success');
} catch (err) {
showToast(`บันทึกในเครื่องแล้ว (ส่งเซิร์ฟเวอร์ไม่ได้: ${err.message})`, 'warning');
}
} else {
showToast(`บันทึกตัวละคร ${getAvatarInfo(aid).name} เรียบร้อย (Lv. ${level}, Prom. ${promotion}, E${rank})`, 'success');
showToast(`บันทึกตัวละคร ${getAvatarInfo(aid).name} เรียบร้อย`, 'success');
}
}
// Max Skills & Traces for character
async function maxSkillsForAvatar() {
if (!state.selectedAvatarDetail) return;
const aid = state.selectedAvatarDetail.avatar_id;
el.modalCharLevel.value = 80;
el.modalCharPromotion.value = 6;
state.selectedAvatarDetail.level = 80;
state.selectedAvatarDetail.promotion = 6;
state.selectedAvatarDetail.rank = 6;
renderModalEidolonPips(6);
if (state.fullFreesrData?.avatars?.[aid]) {
const av = state.fullFreesrData.avatars[aid];
av.level = 80;
av.promotion = 6;
av.data.rank = 6;
if (av.data.skills) {
for (const k in av.data.skills) av.data.skills[k] = 10;
}
}
showToast(`อัพเลเวล 80, เลื่อนขั้น 6, E6 และ Traces 10/10 สำหรับ ${getAvatarInfo(aid).name} สำเร็จ!`, 'success');
}
// Max all avatars
async function maxAllAvatars() {
if (!confirm(t('confirmMaxAll'))) return;
@@ -1184,17 +1484,62 @@ function setupEvents() {
document.getElementById('btn-modal-cancel-avatar')?.addEventListener('click', () => el.modalAvatarDetail.classList.remove('active'));
el.btnModalSaveAvatar.addEventListener('click', saveAvatarDetails);
el.btnModalMaxSkills.addEventListener('click', maxSkillsForAvatar);
document.getElementById('btn-modal-max-all-char')?.addEventListener('click', maxAllBuildForCurrentAvatar);
document.getElementById('btn-modal-max-all-char')?.addEventListener('click', async () => {
if (!state.selectedAvatarDetail) return;
el.modalCharLevel.value = 80;
el.modalCharPromotion.value = 6;
state.selectedAvatarDetail.level = 80;
state.selectedAvatarDetail.promotion = 6;
state.selectedAvatarDetail.rank = 6;
renderModalEidolonPips(6);
await maxSkillsForAvatar();
await saveAvatarDetails();
// Steppers for 4 Skills (Basic, Skill, Ultra, Talent)
const setupSkillStepper = (rangeEl, valEl, decBtn, incBtn, maxBtn, maxVal) => {
if (!rangeEl || !valEl) return;
rangeEl.addEventListener('input', () => { valEl.textContent = rangeEl.value; });
decBtn?.addEventListener('click', () => {
let cur = parseInt(rangeEl.value, 10);
if (cur > parseInt(rangeEl.min, 10)) {
rangeEl.value = cur - 1;
valEl.textContent = rangeEl.value;
}
});
incBtn?.addEventListener('click', () => {
let cur = parseInt(rangeEl.value, 10);
if (cur < parseInt(rangeEl.max, 10)) {
rangeEl.value = cur + 1;
valEl.textContent = rangeEl.value;
}
});
maxBtn?.addEventListener('click', () => {
rangeEl.value = maxVal;
valEl.textContent = maxVal;
});
};
setupSkillStepper(el.rangeSkillBasic, el.valSkillBasic, el.btnStepDecBasic, el.btnStepIncBasic, el.btnSkillMaxBasic, 6);
setupSkillStepper(el.rangeSkillSkill, el.valSkillSkill, el.btnStepDecSkill, el.btnStepIncSkill, el.btnSkillMaxSkill, 10);
setupSkillStepper(el.rangeSkillUltra, el.valSkillUltra, el.btnStepDecUltra, el.btnStepIncUltra, el.btnSkillMaxUltra, 10);
setupSkillStepper(el.rangeSkillTalent, el.valSkillTalent, el.btnStepDecTalent, el.btnStepIncTalent, el.btnSkillMaxTalent, 10);
// Ultimate Energy Controls
el.rangeCharEnergy?.addEventListener('input', () => {
el.valCharEnergy.textContent = `${el.rangeCharEnergy.value}/${el.rangeCharEnergy.max}`;
});
el.btnModalEnergy50?.addEventListener('click', () => {
const half = Math.floor(parseInt(el.rangeCharEnergy.max, 10) * 0.5);
el.rangeCharEnergy.value = half;
el.valCharEnergy.textContent = `${half}/${el.rangeCharEnergy.max}`;
});
el.btnModalEnergy100?.addEventListener('click', () => {
el.rangeCharEnergy.value = el.rangeCharEnergy.max;
el.valCharEnergy.textContent = `${el.rangeCharEnergy.max}/${el.rangeCharEnergy.max}`;
});
// Lightcone Controls in Modal
document.querySelectorAll('.lc-super-pip').forEach(btn => {
btn.addEventListener('click', () => {
const r = parseInt(btn.getAttribute('data-rank'), 10);
updateLcRankPips(r);
});
});
el.btnModalMaxLc?.addEventListener('click', () => {
if (el.modalLcLevel) el.modalLcLevel.value = 80;
updateLcRankPips(5);
showToast('ปรับกรวยแสงเป็นเลเวล 80 ขัดเกลา 5 เรียบร้อย', 'info');
});
// Quick Preset Buttons for Character Modal
@@ -1430,6 +1775,7 @@ function init() {
setupServerModes();
setupLanguageSelector();
setupTabs();
setupModalSubtabs();
setupEvents();
applyTranslations();
refreshStatus();
+8 -8
View File
@@ -46,8 +46,8 @@ export const TRANSLATIONS = {
btnSyncPersistent: "💾 ซิงค์ข้อมูล Persistent ล่าสุด",
currentLineupTitle: "🎮 ทีมปัจจุบัน",
slot: "ตำแหน่งที่",
slotEmpty: "(ว่าง)",
coords: "พิกัดตัวละคร (X, Y, Z)",
slotEmpty: "ว่าง",
coords: "พิกัดตัวละคร XYZ",
// Avatars
searchPlaceholder: "🔍 ค้นหาชื่อหรือ ID ตัวละคร...",
filterAllRarity: "ทุกระดับดาว",
@@ -56,7 +56,7 @@ export const TRANSLATIONS = {
btnReload: "🔄 รีโหลด",
eidolonLabel: "Eidolon:",
confirmMaxAll: "ยืนยันปรับตัวละครทั้งหมดให้เป็นเลเวล 80 และปลด E6 หรือไม่?",
maxAllSuccess: "ปรับ Max ทุกตัวละครสำเร็จแล้ว!",
maxAllSuccess: "ปรับเต็มทุกตัวละครสำเร็จแล้ว!",
avatarRankSuccess: "ปรับตัวละคร {id} เป็น E{rank} สำเร็จ",
// Ex-Rank
exrankTitle: "⚡ ระบบปลดล็อค Ex-Rank Skill",
@@ -74,20 +74,20 @@ export const TRANSLATIONS = {
customTeleportSubtitle: "ใส่ PlaneID, FloorID และ EntryID ตามต้องการ",
btnExecuteWarp: "🚀 ยืนยันการวาร์ป",
warpBtn: "🚀 วาร์ป",
warpSuccess: "วาร์ปไปยัง {label} สำเร็จ (บันทึกลง Persistent)",
warpSuccess: "วาร์ปไปยัง {label} สำเร็จ",
warpFailed: "วาร์ปไม่สำเร็จ: {err}",
// JSON
btnFreesr: "📄 freesr-data.json",
btnPersistent: "💾 ไฟล์เซฟ persistent",
btnBeautify: "✨ จัดรูปแบบ JSON",
btnDownload: "📥 ดาวน์โหลด Backup",
btnDownload: "📥 ดาวน์โหลดสำรอง",
btnUpload: "📤 อัปโหลดไฟล์",
btnSaveServer: "💾 บันทึกไปยังเซิร์ฟเวอร์",
jsonTip: "Tip: สามารถแก้ไขค่าดิบแล้วกด 'บันทึกไปยังเซิร์ฟเวอร์' ได้ทันที",
jsonTip: "คำแนะนำ: สามารถแก้ไขค่าดิบแล้วกด 'บันทึกไปยังเซิร์ฟเวอร์' ได้ทันที",
jsonStatusReady: "พร้อมใช้งาน",
jsonStatusOpening: "กำลังเปิด: {name}",
jsonSaveSuccess: "บันทึกไฟล์ {name} เรียบร้อยแล้ว",
jsonInvalid: "JSON ไม่ถูกต้อง",
jsonInvalid: "รูปแบบ JSON ไม่ถูกต้อง",
// Settings
apiSettingsTitle: "⚙️ ตั้งค่าการเชื่อมต่อ API Gateway",
apiSettingsSubtitle: "ปรับเปลี่ยนพอร์ตหรือ IP ได้ตามไฟล์ server.json ของคุณ",
@@ -95,7 +95,7 @@ export const TRANSLATIONS = {
lblPingInterval: "ความถี่ในการตรวจสอบสถานะ (วินาที)",
btnSaveSettings: "💾 บันทึกการตั้งค่า",
btnResetDefault: "คืนค่าเริ่มต้น",
hostGuideTitle: "🌐 วิธีนำขึ้นโฮสต์ GitHub Pages (ฟรี 100%)",
hostGuideTitle: "🌐 วิธีนำขึ้นโฮสต์ GitHub Pages",
hostGuideSubtitle: "สามารถเปิดใช้งานเว็บนี้บนอินเทอร์เน็ตได้โดยไม่ต้องเสียค่าโฮสต์ใดๆ",
settingsSaved: "บันทึกการตั้งค่าสำเร็จ!",
settingsReset: "คืนค่าเริ่มต้นเรียบร้อย",