Files
justice/www/html/Game/public/js/achievements.js
T
2026-06-17 08:15:22 +00:00

171 lines
11 KiB
JavaScript

/* ===== Justice — ระบบ Achievements (แคตตาล็อก + แสดงผลในโปรไฟล์) =====
* โมดูลกลาง ใช้ร่วมทั้ง room-lobby (room-lobby.js) และ main-lobby (profile-popup.js)
* - มีแคตตาล็อก 25 รางวัล 5 หมวดฝังไว้ (default) ทำงานได้แม้ backend ล่ม
* - ดึง override แคตตาล็อก + progress รายผู้เล่นจาก /Admin/api/achievements.php
* - เปิดให้ controller เรียก window.jdAchievements.renderGroup(listEl, groupIndex)
* Auto-track ค่อยมาเพิ่มภายหลัง (ฝั่ง server เรียก action=progress พร้อม secret)
*/
(function () {
'use strict';
var script = document.currentScript;
var rawAssetBase = (script && script.getAttribute('data-asset-base')) || 'img/03-6-Profile';
function resolveAssetBase(raw) {
var base = String(raw || 'img/03-6-Profile').replace(/\/$/, '');
if (/^\/(Game|img)\//i.test(base)) {
return typeof window.appPath === 'function' ? window.appPath(base) : base;
}
try {
return new URL(base + '/', window.location.href).pathname.replace(/\/$/, '');
} catch (e) {
return base;
}
}
var ASSET_BASE = resolveAssetBase(rawAssetBase);
/* แคตตาล็อกตั้งต้น — id ต้องคงที่ (ใช้เป็นคีย์เก็บ progress) */
var DEFAULT_CATALOG = [
/* หมวด 1 : สายวิเคราะห์ (Analyst) */
{ id: 'a1_first_deduction', g: 1, title: 'First Deduction', desc: 'โหวตถูกตัวคนร้ายเป็นครั้งแรก', target: 1 },
{ id: 'a2_sharp_eye', g: 1, title: 'Sharp Eye', desc: 'สะสมหลักฐานระดับมีน้ำหนัก (Silver) ครบ 10 ใบ', target: 10 },
{ id: 'a3_mind_architect', g: 1, title: 'Mind Architect', desc: 'สะสมหลักฐานครบทุกระดับ (ทั่วไป, มีน้ำหนัก, ชี้ชัด) ใน 1 คดี', target: 1 },
{ id: 'a4_logic_over_luck', g: 1, title: 'Logic Over Luck', desc: 'โหวตถูกโดยไม่พึ่งหลักฐานชี้ชัด (Legendary) เลย 3 ครั้ง', target: 3 },
{ id: 'a5_truth_hunter', g: 1, title: 'Truth Hunter', desc: 'จับคนร้ายถูกตัวสะสมครบ 20 คดี', target: 20 },
{ id: 'a6_unbreakable_logic', g: 1, title: 'Unbreakable Logic', desc: 'โหวตถูกตัวติดกัน 5 คดีรวด', target: 5 },
/* หมวด 2 : สายสำรวจ (Explorer) */
{ id: 'b1_evidence_collector', g: 2, title: 'Evidence Collector', desc: 'สะสมหลักฐานระดับทั่วไป (Common) ครบ 20 ใบ', target: 20 },
{ id: 'b2_relentless_investigator', g: 2, title: 'Relentless Investigator', desc: 'เล่น Mini Game ครบทุกรอบ ใน 1 คดี', target: 1 },
{ id: 'b3_hidden_fragment', g: 2, title: 'Hidden Fragment', desc: 'ค้นพบหลักฐานระดับชี้ชัด (Legendary/Gold) เป็นครั้งแรก', target: 1 },
{ id: 'b4_data_miner', g: 2, title: 'Data Miner', desc: 'สะสมการ์ดหลักฐานรวมครบ 100 ใบ', target: 100 },
{ id: 'b5_deep_scanner', g: 2, title: 'Deep Scanner', desc: 'เก็บไอเทมช่วยเหลือ (ตำรวจ/ทนาย) สะสมครบ 10 ครั้ง', target: 10 },
/* หมวด 3 : สายเสี่ยง (Risk Taker) */
{ id: 'c1_early_accusation', g: 3, title: 'Early Accusation', desc: 'ชี้ตัวคนร้ายก่อนที่จะเปิดหลักฐานครบ', target: 1 },
{ id: 'c2_high_stakes', g: 3, title: 'High Stakes', desc: 'ชี้ตัวคนร้ายถูกโดยมีหลักฐานในมือไม่เกิน 3 ใบ', target: 1 },
{ id: 'c3_quick_draw', g: 3, title: 'Quick Draw', desc: 'ชี้ตัวคนร้ายเร็วที่สุดในทีมสะสมครบ 10 ครั้ง', target: 10 },
{ id: 'c4_clutch_mind', g: 3, title: 'Clutch Mind', desc: 'โหวตถูกในช่วง 10 วินาทีสุดท้ายก่อนหมดเวลา', target: 1 },
{ id: 'c5_lone_wolf', g: 3, title: 'Lone Wolf', desc: 'โหวตสวนทางกับเสียงส่วนใหญ่ของทีม (คุณถูกคนเดียว)', target: 1 },
/* หมวด 4 : สายสนับสนุน (Support) */
{ id: 'd1_minigame_solver', g: 4, title: 'Minigame Solver', desc: 'เอาชีวิตรอด / เล่นมินิเกมสำเร็จ 20 ครั้ง', target: 20 },
{ id: 'd2_silent_guardian', g: 4, title: 'Silent Guardian', desc: 'ไม่โดนโหวตใช้การ์ด Event พิเศษ เลยตลอด 5 คดี', target: 5 },
{ id: 'd3_the_backbone', g: 4, title: 'The Backbone', desc: 'ส่งมอบหลักฐานให้เพื่อนวิเคราะห์ครบ 30 ใบ', target: 30 },
{ id: 'd4_flawless_diver', g: 4, title: 'Flawless Diver', desc: 'ไม่โหวตจับผิดตัวเลยตลอด 15 คดี', target: 15 },
/* หมวด 5 : Elite / Hidden (สายลับ & ตัวป่วน) */
{ id: 'e1_the_observer', g: 5, title: 'The Observer', desc: 'เล่นจบ 15 คดีโดยไม่เคยสัมผัสหลักฐานชี้ชัด (Legendary) เลยสักครั้ง', target: 15 },
{ id: 'e2_the_impostor', g: 5, title: 'The Impostor', desc: 'รับบทเป็น "ตัวป่วน" ครั้งแรก', target: 1 },
{ id: 'e3_master_of_doubt', g: 5, title: 'Master of Doubt', desc: 'เอาชนะคดีในฐานะตัวป่วนได้สำเร็จ', target: 1 },
{ id: 'e4_agent_of_chaos', g: 5, title: 'Agent of Chaos', desc: 'รับบทเป็นตัวป่วนสะสมครบ 10 ครั้ง', target: 10 },
{ id: 'e5_slippery_eel', g: 5, title: 'Slippery Eel', desc: 'เป็นตัวป่วนแต่รอดพ้นจากการถูกจับได้ (ไม่ถูกโหวตออก) จนจบเกม 5 ครั้ง', target: 5 }
];
var state = {
catalog: DEFAULT_CATALOG.slice(),
progress: {}, /* id -> count */
loaded: false,
loadingKey: null
};
function appRel(path) {
if (typeof window.appPath === 'function') return window.appPath(path);
return path;
}
function asset(name) { return ASSET_BASE + '/' + name; }
function readPlayerKey() {
try {
var k = localStorage.getItem('jdPlayerKey');
if (k && /^[A-Za-z0-9_-]{8,128}$/.test(k)) return k;
} catch (e) { /* ignore */ }
return null;
}
function clamp(n, lo, hi) { n = parseInt(n, 10); if (!isFinite(n)) n = 0; return Math.max(lo, Math.min(hi, n)); }
function escapeHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function catalogForGroup(groupIndex) {
var g = clamp(groupIndex, 1, 5);
return state.catalog.filter(function (a) { return (parseInt(a.g, 10) || 1) === g && a.enabled !== false; });
}
/* สร้าง <li> หนึ่งรายการ (ข้อความ ไม่ใช่รูปชื่อ) */
function buildItemHtml(def) {
var target = Math.max(1, parseInt(def.target, 10) || 1);
var cur = clamp(state.progress[def.id] || 0, 0, target);
var pct = Math.round((cur / target) * 100);
var unlocked = cur >= target;
var iconSrc = def.icon ? (/^\//.test(def.icon) ? appRel(def.icon) : def.icon) : appRel('/Game/img/achievements/' + def.id + '.png');
var fallbackIcon = asset('archiv-icon-bg.png');
return '' +
'<li class="room-lobby-profile-archiv-item rlpa-text-item' + (unlocked ? ' is-unlocked' : '') + '" data-achv-id="' + escapeHtml(def.id) + '">' +
'<img class="room-lobby-profile-archiv-icon-bg" src="' + iconSrc + '" alt="" onerror="this.onerror=null;this.src=\'' + fallbackIcon + '\'">' +
'<div class="room-lobby-profile-archiv-main">' +
'<div class="room-lobby-profile-archiv-title-row">' +
'<div class="rlpa-title-col">' +
'<div class="rlpa-title">' + escapeHtml(def.title) + (unlocked ? ' <span class="rlpa-check">✔</span>' : '') + '</div>' +
'<div class="rlpa-desc">' + escapeHtml(def.desc) + '</div>' +
'</div>' +
'<strong class="room-lobby-profile-archiv-count">' + cur + '/' + target + '</strong>' +
'</div>' +
'<div class="room-lobby-profile-archiv-progress-wrap">' +
'<img class="room-lobby-profile-archiv-progress-bg" src="' + asset('archiv-progress-bg.png') + '" alt="">' +
'<img class="room-lobby-profile-archiv-progress" src="' + asset('archiv-progress.png') + '" alt="" style="--archiv-progress:' + pct + '%">' +
'</div>' +
'<img class="room-lobby-profile-archiv-line" src="' + asset('archiv-line.png') + '" alt="">' +
'</div>' +
'</li>';
}
function renderGroup(listEl, groupIndex) {
if (!listEl) return;
var defs = catalogForGroup(groupIndex);
if (!defs.length) { listEl.innerHTML = ''; return; }
listEl.innerHTML = defs.map(buildItemHtml).join('');
listEl.setAttribute('data-achv-group', String(clamp(groupIndex, 1, 5)));
}
/* render ซ้ำหมวดที่กำลังโชว์ (หลังโหลด progress เสร็จ) */
function rerenderActive() {
var lists = document.querySelectorAll('.room-lobby-profile-achievements-list');
lists.forEach(function (el) {
var g = parseInt(el.getAttribute('data-achv-group'), 10) || 1;
renderGroup(el, g);
});
}
/* ดึงแคตตาล็อก override + progress รายผู้เล่น */
function load(playerKey) {
var key = playerKey || readPlayerKey();
if (!key) { return Promise.resolve(state); }
if (state.loaded && state.loadingKey === key) return Promise.resolve(state);
state.loadingKey = key;
var url = appRel('/Admin/api/achievements.php') + '?action=state&playerKey=' + encodeURIComponent(key);
return fetch(url, { credentials: 'omit' })
.then(function (r) { return r.json(); })
.then(function (d) {
if (d && d.ok) {
if (Array.isArray(d.catalog) && d.catalog.length) state.catalog = d.catalog;
state.progress = (d.progress && typeof d.progress === 'object') ? d.progress : {};
state.loaded = true;
rerenderActive();
}
return state;
})
.catch(function () { return state; });
}
window.jdAchievements = {
CATALOG: DEFAULT_CATALOG,
state: state,
renderGroup: renderGroup,
rerenderActive: rerenderActive,
load: load,
readPlayerKey: readPlayerKey
};
})();