fixed แต่งตัวไม่ได้
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* สาธารณะ — ระบบ "รหัสผู้เล่น" สำหรับ sync โปรไฟล์ข้าม browser/เครื่อง (Guest)
|
||||
* ผูกกับ jdPlayerKey (guest account: providerUserId = key) เหมือน player-coins.php
|
||||
*
|
||||
* GET ?action=code&playerKey=...&displayName=...&agentId=...
|
||||
* → คืนรหัสของบัญชีนี้ (สร้างถ้ายังไม่มี) + อัปเดตชื่อ/agent ฝั่ง server
|
||||
* { ok, code, codeDisplay }
|
||||
*
|
||||
* POST { action:'redeem', code }
|
||||
* → ดึงโปรไฟล์จากรหัส → คืน playerKey + ข้อมูลโปรไฟล์ของบัญชีนั้น
|
||||
* { ok, playerKey, coins, colorThemeIndex, skinToneIndex, displayName, agentId }
|
||||
*
|
||||
* รหัสที่ไม่ถูกใช้เกิน 30 วัน → หมดอายุ (ลบแบบ lazy ตอนมีการเรียก)
|
||||
*/
|
||||
require __DIR__ . '/_common.php';
|
||||
|
||||
const LINK_CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; // ตัด 0 O 1 I L ที่กำกวม
|
||||
const LINK_CODE_LEN = 8;
|
||||
const LINK_CODE_TTL_DAYS = 30;
|
||||
|
||||
function valid_player_key(string $key): bool
|
||||
{
|
||||
return (bool)preg_match('/^[a-zA-Z0-9_-]{8,128}$/', $key);
|
||||
}
|
||||
|
||||
function sanitize_display_name(string $raw): string
|
||||
{
|
||||
$s = trim($raw);
|
||||
$s = preg_replace('/[\x00-\x1f\x7f]/u', '', $s) ?? '';
|
||||
if (function_exists('mb_substr')) {
|
||||
$s = mb_substr($s, 0, 24);
|
||||
} else {
|
||||
$s = substr($s, 0, 24);
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
|
||||
function sanitize_agent_id(string $raw): string
|
||||
{
|
||||
return preg_match('/^\d{6}$/', trim($raw)) ? trim($raw) : '';
|
||||
}
|
||||
|
||||
function normalize_code(string $raw): string
|
||||
{
|
||||
$s = strtoupper(trim($raw));
|
||||
$s = str_replace([' ', '-', '_'], '', $s);
|
||||
// กันสับสน: ผู้ใช้พิมพ์ 0/O/1/I/L → map เข้าตัวที่ใช้จริง
|
||||
$s = strtr($s, ['0' => 'O', '1' => 'I']);
|
||||
return $s;
|
||||
}
|
||||
|
||||
function generate_code(): string
|
||||
{
|
||||
$n = strlen(LINK_CODE_ALPHABET);
|
||||
$out = '';
|
||||
for ($i = 0; $i < LINK_CODE_LEN; $i++) {
|
||||
$out .= LINK_CODE_ALPHABET[random_int(0, $n - 1)];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
function format_code_display(string $code): string
|
||||
{
|
||||
if (strlen($code) === 8) {
|
||||
return substr($code, 0, 4) . '-' . substr($code, 4, 4);
|
||||
}
|
||||
return $code;
|
||||
}
|
||||
|
||||
function code_is_expired($lastUsedAt): bool
|
||||
{
|
||||
if (!$lastUsedAt) {
|
||||
return false;
|
||||
}
|
||||
$ts = strtotime((string)$lastUsedAt);
|
||||
if ($ts === false) {
|
||||
return false;
|
||||
}
|
||||
return (time() - $ts) > (LINK_CODE_TTL_DAYS * 86400);
|
||||
}
|
||||
|
||||
/** ลบรหัสที่หมดอายุออกจากทุกบัญชี (lazy GC) — แก้ใน $store ตรง ๆ */
|
||||
function gc_expired_codes(array &$store): bool
|
||||
{
|
||||
$changed = false;
|
||||
foreach ($store['accounts'] as $i => $a) {
|
||||
if (!empty($a['linkCode']) && code_is_expired($a['linkCodeLastUsedAt'] ?? null)) {
|
||||
unset($store['accounts'][$i]['linkCode']);
|
||||
$store['accounts'][$i]['linkCodeLastUsedAt'] = null;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
return $changed;
|
||||
}
|
||||
|
||||
function code_in_use(array $store, string $code, ?string $exceptKey = null): bool
|
||||
{
|
||||
foreach ($store['accounts'] as $a) {
|
||||
if (($a['linkCode'] ?? '') !== $code) {
|
||||
continue;
|
||||
}
|
||||
if ($exceptKey !== null && ($a['providerUserId'] ?? '') === $exceptKey) {
|
||||
continue;
|
||||
}
|
||||
if (!code_is_expired($a['linkCodeLastUsedAt'] ?? null)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function profile_payload(array $a): array
|
||||
{
|
||||
return [
|
||||
'playerKey' => $a['providerUserId'] ?? '',
|
||||
'coins' => max(0, (int)($a['coins'] ?? 0)),
|
||||
'colorThemeIndex' => ($v = (int)($a['lobbyColorThemeIndex'] ?? 0)) >= 1 && $v <= 8 ? $v : null,
|
||||
'skinToneIndex' => ($s = (int)($a['lobbySkinToneIndex'] ?? 0)) >= 1 && $s <= 3 ? $s : null,
|
||||
'displayName' => (string)($a['displayName'] ?? ''),
|
||||
'agentId' => (string)($a['agentDisplayId'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
if ($method === 'GET') {
|
||||
$action = (string)($_GET['action'] ?? 'code');
|
||||
if ($action !== 'code') {
|
||||
json_response(['ok' => false, 'error' => 'unknown action'], 400);
|
||||
}
|
||||
$key = trim((string)($_GET['playerKey'] ?? ''));
|
||||
if (!valid_player_key($key)) {
|
||||
json_response(['ok' => false, 'error' => 'playerKey ต้องยาว 8–128 (a-z 0-9 _ -)'], 400);
|
||||
}
|
||||
$displayName = sanitize_display_name((string)($_GET['displayName'] ?? ''));
|
||||
$agentId = sanitize_agent_id((string)($_GET['agentId'] ?? ''));
|
||||
|
||||
$store = read_store();
|
||||
gc_expired_codes($store);
|
||||
$now = gmdate('c');
|
||||
$idx = -1;
|
||||
foreach ($store['accounts'] as $i => $a) {
|
||||
if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) {
|
||||
$idx = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($idx < 0) {
|
||||
$store['accounts'][] = [
|
||||
'id' => new_id(),
|
||||
'email' => '',
|
||||
'displayName' => $displayName,
|
||||
'loginType' => 'guest',
|
||||
'providerUserId' => $key,
|
||||
'notes' => 'auto: player-link',
|
||||
'blocked' => false,
|
||||
'coins' => 0,
|
||||
'agentDisplayId' => $agentId,
|
||||
'createdAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
];
|
||||
$idx = count($store['accounts']) - 1;
|
||||
} else {
|
||||
if ($displayName !== '') {
|
||||
$store['accounts'][$idx]['displayName'] = $displayName;
|
||||
}
|
||||
if ($agentId !== '') {
|
||||
$store['accounts'][$idx]['agentDisplayId'] = $agentId;
|
||||
}
|
||||
$store['accounts'][$idx]['updatedAt'] = $now;
|
||||
}
|
||||
|
||||
$code = (string)($store['accounts'][$idx]['linkCode'] ?? '');
|
||||
if ($code === '' || code_is_expired($store['accounts'][$idx]['linkCodeLastUsedAt'] ?? null)) {
|
||||
do {
|
||||
$code = generate_code();
|
||||
} while (code_in_use($store, $code, $key));
|
||||
$store['accounts'][$idx]['linkCode'] = $code;
|
||||
}
|
||||
$store['accounts'][$idx]['linkCodeLastUsedAt'] = $now;
|
||||
|
||||
if (!write_store($store)) {
|
||||
json_response(['ok' => false, 'error' => 'สร้างรหัสไม่สำเร็จ'], 500);
|
||||
}
|
||||
json_response(['ok' => true, 'code' => $code, 'codeDisplay' => format_code_display($code)]);
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$body = require_json_body();
|
||||
$action = (string)($body['action'] ?? 'redeem');
|
||||
if ($action !== 'redeem') {
|
||||
json_response(['ok' => false, 'error' => 'unknown action'], 400);
|
||||
}
|
||||
$code = normalize_code((string)($body['code'] ?? ''));
|
||||
if (!preg_match('/^[A-Z2-9]{' . LINK_CODE_LEN . '}$/', $code)) {
|
||||
json_response(['ok' => false, 'error' => 'รูปแบบรหัสไม่ถูกต้อง'], 400);
|
||||
}
|
||||
|
||||
$store = read_store();
|
||||
$sweep = gc_expired_codes($store);
|
||||
$now = gmdate('c');
|
||||
$found = -1;
|
||||
foreach ($store['accounts'] as $i => $a) {
|
||||
if (($a['linkCode'] ?? '') === $code && !code_is_expired($a['linkCodeLastUsedAt'] ?? null)) {
|
||||
$found = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($found < 0) {
|
||||
if ($sweep) {
|
||||
write_store($store);
|
||||
}
|
||||
json_response(['ok' => false, 'error' => 'ไม่พบรหัสนี้ หรือรหัสหมดอายุแล้ว'], 404);
|
||||
}
|
||||
|
||||
$store['accounts'][$found]['linkCodeLastUsedAt'] = $now;
|
||||
$store['accounts'][$found]['updatedAt'] = $now;
|
||||
write_store($store);
|
||||
|
||||
$payload = profile_payload($store['accounts'][$found]);
|
||||
$payload['ok'] = true;
|
||||
json_response($payload);
|
||||
}
|
||||
|
||||
json_response(['ok' => false, 'error' => 'Use GET or POST'], 405);
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* สาธารณะ — ไม่ต้องล็อกอินแอดมิน
|
||||
* เก็บ/อ่าน "สีตัวละครในล็อบบี้" (theme color + skin tone) แบบ cross-device
|
||||
* ผูกกับ jdPlayerKey เหมือน player-coins.php (guest account: providerUserId = key)
|
||||
*
|
||||
* GET ?playerKey=... → { ok, colorThemeIndex, skinToneIndex }
|
||||
* POST { playerKey, colorThemeIndex, skinToneIndex } → บันทึกแล้วคืนค่าที่บันทึก
|
||||
*
|
||||
* index ใช้รูปแบบเดียวกับ customize popup: theme 1–8, skin 1–3
|
||||
*/
|
||||
require __DIR__ . '/_common.php';
|
||||
|
||||
const LOBBY_THEME_MIN = 1;
|
||||
const LOBBY_THEME_MAX = 8;
|
||||
const LOBBY_SKIN_MIN = 1;
|
||||
const LOBBY_SKIN_MAX = 3;
|
||||
|
||||
function valid_player_key(string $key): bool
|
||||
{
|
||||
return (bool)preg_match('/^[a-zA-Z0-9_-]{8,128}$/', $key);
|
||||
}
|
||||
|
||||
/** คืน index ที่อยู่ในช่วง หรือ null ถ้าไม่ถูกต้อง */
|
||||
function clamp_index_or_null($raw, int $min, int $max): ?int
|
||||
{
|
||||
if ($raw === null || $raw === '') {
|
||||
return null;
|
||||
}
|
||||
$v = (int)$raw;
|
||||
if ($v < $min || $v > $max) {
|
||||
return null;
|
||||
}
|
||||
return $v;
|
||||
}
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
if ($method === 'GET') {
|
||||
$key = trim((string)($_GET['playerKey'] ?? ''));
|
||||
if (!valid_player_key($key)) {
|
||||
json_response(['ok' => false, 'error' => 'playerKey ต้องยาว 8–128 (a-z 0-9 _ -)'], 400);
|
||||
}
|
||||
|
||||
$store = read_store();
|
||||
foreach ($store['accounts'] ?? [] as $a) {
|
||||
if (($a['loginType'] ?? '') !== 'guest') {
|
||||
continue;
|
||||
}
|
||||
if (($a['providerUserId'] ?? '') !== $key) {
|
||||
continue;
|
||||
}
|
||||
json_response([
|
||||
'ok' => true,
|
||||
'colorThemeIndex' => clamp_index_or_null($a['lobbyColorThemeIndex'] ?? null, LOBBY_THEME_MIN, LOBBY_THEME_MAX),
|
||||
'skinToneIndex' => clamp_index_or_null($a['lobbySkinToneIndex'] ?? null, LOBBY_SKIN_MIN, LOBBY_SKIN_MAX),
|
||||
]);
|
||||
}
|
||||
|
||||
// ยังไม่มีบัญชี/ยังไม่เคยเลือกสี — ไม่ต้องสร้างบัญชีตอน GET
|
||||
json_response([
|
||||
'ok' => true,
|
||||
'colorThemeIndex' => null,
|
||||
'skinToneIndex' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$body = require_json_body();
|
||||
$key = trim((string)($body['playerKey'] ?? ''));
|
||||
if (!valid_player_key($key)) {
|
||||
json_response(['ok' => false, 'error' => 'playerKey ต้องยาว 8–128 (a-z 0-9 _ -)'], 400);
|
||||
}
|
||||
|
||||
$colorThemeIndex = clamp_index_or_null($body['colorThemeIndex'] ?? null, LOBBY_THEME_MIN, LOBBY_THEME_MAX);
|
||||
$skinToneIndex = clamp_index_or_null($body['skinToneIndex'] ?? null, LOBBY_SKIN_MIN, LOBBY_SKIN_MAX);
|
||||
if ($colorThemeIndex === null && $skinToneIndex === null) {
|
||||
json_response(['ok' => false, 'error' => 'ต้องส่ง colorThemeIndex (1–8) หรือ skinToneIndex (1–3) อย่างน้อย 1 ค่า'], 400);
|
||||
}
|
||||
|
||||
$store = read_store();
|
||||
$now = gmdate('c');
|
||||
$found = false;
|
||||
|
||||
foreach ($store['accounts'] as $i => $a) {
|
||||
if (($a['loginType'] ?? '') !== 'guest') {
|
||||
continue;
|
||||
}
|
||||
if (($a['providerUserId'] ?? '') !== $key) {
|
||||
continue;
|
||||
}
|
||||
if ($colorThemeIndex !== null) {
|
||||
$store['accounts'][$i]['lobbyColorThemeIndex'] = $colorThemeIndex;
|
||||
}
|
||||
if ($skinToneIndex !== null) {
|
||||
$store['accounts'][$i]['lobbySkinToneIndex'] = $skinToneIndex;
|
||||
}
|
||||
$store['accounts'][$i]['updatedAt'] = $now;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
$store['accounts'][] = [
|
||||
'id' => new_id(),
|
||||
'email' => '',
|
||||
'displayName' => 'Guest',
|
||||
'loginType' => 'guest',
|
||||
'providerUserId' => $key,
|
||||
'notes' => 'auto: player-lobby-style',
|
||||
'blocked' => false,
|
||||
'coins' => 0,
|
||||
'lobbyColorThemeIndex' => $colorThemeIndex,
|
||||
'lobbySkinToneIndex' => $skinToneIndex,
|
||||
'createdAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
if (!write_store($store)) {
|
||||
json_response(['ok' => false, 'error' => 'บันทึกสีไม่สำเร็จ'], 500);
|
||||
}
|
||||
|
||||
json_response([
|
||||
'ok' => true,
|
||||
'colorThemeIndex' => $colorThemeIndex,
|
||||
'skinToneIndex' => $skinToneIndex,
|
||||
]);
|
||||
}
|
||||
|
||||
json_response(['ok' => false, 'error' => 'Use GET or POST'], 405);
|
||||
@@ -21,14 +21,19 @@
|
||||
{
|
||||
"id": "a30aa9889e7efeb1c371c3e1",
|
||||
"email": "",
|
||||
"displayName": "Guest",
|
||||
"displayName": "QS",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1775013892442_jveup54h5u",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 0,
|
||||
"createdAt": "2026-04-01T03:24:52+00:00",
|
||||
"updatedAt": "2026-04-01T03:24:52+00:00"
|
||||
"updatedAt": "2026-06-19T10:08:36+00:00",
|
||||
"lobbyColorThemeIndex": 1,
|
||||
"lobbySkinToneIndex": 2,
|
||||
"agentDisplayId": "596772",
|
||||
"linkCode": "HGBQ964A",
|
||||
"linkCodeLastUsedAt": "2026-06-19T09:20:42+00:00"
|
||||
},
|
||||
{
|
||||
"id": "483a104becd7a5f92c0e5cad",
|
||||
@@ -40,7 +45,7 @@
|
||||
"blocked": false,
|
||||
"coins": 897,
|
||||
"createdAt": "2026-04-02T05:52:21+00:00",
|
||||
"updatedAt": "2026-06-19T07:10:26+00:00",
|
||||
"updatedAt": "2026-06-19T09:42:28+00:00",
|
||||
"daily": {
|
||||
"anchorMs": 1781197200000,
|
||||
"claimedDays": [
|
||||
@@ -74,7 +79,9 @@
|
||||
"d1_minigame_solver": 20,
|
||||
"a1_first_deduction": 1,
|
||||
"a5_truth_hunter": 1
|
||||
}
|
||||
},
|
||||
"lobbyColorThemeIndex": 3,
|
||||
"lobbySkinToneIndex": 1
|
||||
},
|
||||
{
|
||||
"id": "1d64c56fadb64a93eae68a1d",
|
||||
@@ -437,7 +444,94 @@
|
||||
"blocked": false,
|
||||
"coins": 0,
|
||||
"createdAt": "2026-06-19T08:16:09+00:00",
|
||||
"updatedAt": "2026-06-19T08:16:09+00:00"
|
||||
"updatedAt": "2026-06-19T08:58:50+00:00",
|
||||
"lobbyColorThemeIndex": 3,
|
||||
"lobbySkinToneIndex": 1
|
||||
},
|
||||
{
|
||||
"id": "3c34ec21b0d1a68412df61c5",
|
||||
"email": "",
|
||||
"displayName": "Guest",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "testkey_1781859026_abcd",
|
||||
"notes": "auto: player-lobby-style",
|
||||
"blocked": false,
|
||||
"coins": 0,
|
||||
"lobbyColorThemeIndex": 5,
|
||||
"lobbySkinToneIndex": 3,
|
||||
"createdAt": "2026-06-19T08:50:26+00:00",
|
||||
"updatedAt": "2026-06-19T08:50:26+00:00"
|
||||
},
|
||||
{
|
||||
"id": "0ba5fabca3663fe10ac4fce0",
|
||||
"email": "",
|
||||
"displayName": "Guest",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1781859644515_3jr79lcxie9",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 0,
|
||||
"createdAt": "2026-06-19T09:00:44+00:00",
|
||||
"updatedAt": "2026-06-19T09:01:47+00:00",
|
||||
"lobbyColorThemeIndex": 7,
|
||||
"lobbySkinToneIndex": 3
|
||||
},
|
||||
{
|
||||
"id": "4584bc06474178e979c359f7",
|
||||
"email": "",
|
||||
"displayName": "Guest",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1781859715079_rnsmzz48s5c",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 0,
|
||||
"createdAt": "2026-06-19T09:01:55+00:00",
|
||||
"updatedAt": "2026-06-19T09:01:55+00:00"
|
||||
},
|
||||
{
|
||||
"id": "51b5d5d74babb59f8ee29e99",
|
||||
"email": "",
|
||||
"displayName": "Guest",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1781859729172_0nkqc6xzl6ph",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 0,
|
||||
"createdAt": "2026-06-19T09:02:09+00:00",
|
||||
"updatedAt": "2026-06-19T09:02:49+00:00",
|
||||
"lobbyColorThemeIndex": 5,
|
||||
"lobbySkinToneIndex": 1
|
||||
},
|
||||
{
|
||||
"id": "537d39522aeb42a4a5d7d4a6",
|
||||
"email": "",
|
||||
"displayName": "TesterA",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "linkuser_1781860644_aaaa",
|
||||
"notes": "auto: player-lobby-style",
|
||||
"blocked": false,
|
||||
"coins": 0,
|
||||
"lobbyColorThemeIndex": 5,
|
||||
"lobbySkinToneIndex": 3,
|
||||
"createdAt": "2026-06-19T09:17:24+00:00",
|
||||
"updatedAt": "2026-06-19T09:17:24+00:00",
|
||||
"agentDisplayId": "123456",
|
||||
"linkCode": "3VRUU83F",
|
||||
"linkCodeLastUsedAt": "2026-06-19T09:17:24+00:00"
|
||||
},
|
||||
{
|
||||
"id": "80f31f73da0328dbf24e0206",
|
||||
"email": "",
|
||||
"displayName": "Guest",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1781862203558_jhynnz90qrg",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 0,
|
||||
"createdAt": "2026-06-19T09:43:23+00:00",
|
||||
"updatedAt": "2026-06-19T10:10:56+00:00",
|
||||
"lobbyColorThemeIndex": 1,
|
||||
"lobbySkinToneIndex": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -144,6 +144,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* หา characterId สำหรับ apply — ถ้า localStorage ว่าง ให้ดึงจาก API เหมือน lobby.js
|
||||
* (กันเคสเครื่องที่ไม่เคยเซฟ gameCharacterId → กดยืนยันแล้วตัวใหญ่ไม่อัปเดต)
|
||||
*/
|
||||
function resolveApplyCharacterId(cb) {
|
||||
var id = getStoredCharacterId();
|
||||
if (id) { cb(id); return; }
|
||||
try {
|
||||
fetch(projectPath('/Game/api/characters'), { credentials: 'same-origin', cache: 'no-store' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (list) {
|
||||
if (!Array.isArray(list) || !list.length) { cb(''); return; }
|
||||
var last = list[list.length - 1];
|
||||
cb(last && last.id ? String(last.id).trim() : '');
|
||||
})
|
||||
.catch(function () { cb(''); });
|
||||
} catch (e) { cb(''); }
|
||||
}
|
||||
|
||||
/** key เดียวกับ lobby.js (ensurePlayerKey) — ใช้ผูกสีกับบัญชี guest ฝั่ง server */
|
||||
function getPlayerKey() {
|
||||
try {
|
||||
return (localStorage.getItem('jdPlayerKey') || '').trim();
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** บันทึกสีที่เลือกไปเซิร์ฟเวอร์ (cross-device) — fire-and-forget, ไม่บล็อก UI */
|
||||
function postLobbyStyleToServer(themeIndex, skinIndex) {
|
||||
var key = getPlayerKey();
|
||||
if (!key) return;
|
||||
try {
|
||||
fetch(projectPath('/Admin/api/player-lobby-style.php'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'omit',
|
||||
keepalive: true,
|
||||
body: JSON.stringify({
|
||||
playerKey: key,
|
||||
colorThemeIndex: clampIndex(themeIndex, 1, 8),
|
||||
skinToneIndex: clampIndex(skinIndex, 1, 3)
|
||||
})
|
||||
}).catch(function () { /* ออฟไลน์ — local ยังเก็บไว้ */ });
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function projectPath(path) {
|
||||
if (typeof window.appPath === 'function') return window.appPath(path);
|
||||
return path;
|
||||
@@ -359,6 +406,8 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
// รักษาเส้น tech สีฟ้า/cyan accent — อย่าย้อมให้เป็นสีธีม (เคสที่ user รายงาน)
|
||||
if (hueDeg >= 150 && hueDeg <= 265 && sat >= 0.22) continue;
|
||||
// Theme region: colored non-gray pixels (exclude very dark/very bright)
|
||||
var isThemeCandidate = sat >= 0.2 && lig >= 0.3 && lig <= 0.95;
|
||||
if (!isThemeCandidate) continue;
|
||||
@@ -446,9 +495,32 @@
|
||||
return Promise.resolve(previewCompositeCache[cacheKey] || null);
|
||||
}
|
||||
|
||||
return fetchCharacterLayerManifest(characterId).then(function (manifest) {
|
||||
var manifestFrame = firstManifestFrame(manifest, dir);
|
||||
if (manifestFrame) {
|
||||
function composeFromLayers(baseW, baseH, layers) {
|
||||
var c = document.createElement('canvas');
|
||||
c.width = baseW;
|
||||
c.height = baseH;
|
||||
var ctx = c.getContext('2d');
|
||||
var anyTintColorLayer = false;
|
||||
for (var li = 0; li < PLAY_LAYER_ORDER.length; li += 1) {
|
||||
var layerName = PLAY_LAYER_ORDER[li];
|
||||
var img = layers[li];
|
||||
if (!img) continue;
|
||||
var tintKey = PLAY_LAYER_TINT_KEY[layerName];
|
||||
var tintHex = tintKey ? tint[tintKey] : null;
|
||||
if (tintKey) anyTintColorLayer = true;
|
||||
drawTintedLayer(ctx, c.width, c.height, img, tintHex);
|
||||
}
|
||||
// ต้องมีอย่างน้อย 1 layer สี (bodyColor/headColor/hairColor) ที่โหลดได้ ไม่งั้นถือว่าไม่สำเร็จ
|
||||
if (!anyTintColorLayer) return null;
|
||||
var out = c.toDataURL('image/png');
|
||||
previewCompositeCache[cacheKey] = out;
|
||||
return out;
|
||||
}
|
||||
|
||||
function tryManifest() {
|
||||
return fetchCharacterLayerManifest(characterId).then(function (manifest) {
|
||||
var manifestFrame = firstManifestFrame(manifest, dir);
|
||||
if (!manifestFrame) return null;
|
||||
var manifestLayerPromises = PLAY_LAYER_ORDER.map(function (layerName) {
|
||||
var urls = [];
|
||||
var fileName = manifestFrame[layerName];
|
||||
@@ -465,33 +537,17 @@
|
||||
}
|
||||
}
|
||||
if (!baseImg) return null;
|
||||
var c = document.createElement('canvas');
|
||||
c.width = baseImg.naturalWidth;
|
||||
c.height = baseImg.naturalHeight;
|
||||
var ctx = c.getContext('2d');
|
||||
var anyTintColorLayer = false;
|
||||
for (var li = 0; li < PLAY_LAYER_ORDER.length; li += 1) {
|
||||
var layerName = PLAY_LAYER_ORDER[li];
|
||||
var img = layers[li];
|
||||
if (!img) continue;
|
||||
var tintKey = PLAY_LAYER_TINT_KEY[layerName];
|
||||
var tintHex = tintKey ? tint[tintKey] : null;
|
||||
if (tintKey) anyTintColorLayer = true;
|
||||
drawTintedLayer(ctx, c.width, c.height, img, tintHex);
|
||||
}
|
||||
if (!anyTintColorLayer) return null;
|
||||
var out = c.toDataURL('image/png');
|
||||
previewCompositeCache[cacheKey] = out;
|
||||
return out;
|
||||
return composeFromLayers(baseImg.naturalWidth, baseImg.naturalHeight, layers);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function tryRawLayers() {
|
||||
var rawCandidates = [
|
||||
characterAssetsBasePath() + encodeURIComponent(characterId) + '_' + dir + '_idle.png',
|
||||
characterAssetsBasePath() + encodeURIComponent(characterId) + '_' + dir + '.png',
|
||||
characterAssetsBasePath() + encodeURIComponent(characterId) + '_' + dir + '_0.png'
|
||||
];
|
||||
|
||||
return resolveFirstImage(rawCandidates).then(function (rawImg) {
|
||||
if (!rawImg || !rawImg.naturalWidth || !rawImg.naturalHeight) return null;
|
||||
var layerPromises = PLAY_LAYER_ORDER.map(function (layerName) {
|
||||
@@ -501,27 +557,50 @@
|
||||
return resolveFirstImage(urls);
|
||||
});
|
||||
return Promise.all(layerPromises).then(function (layers) {
|
||||
var c = document.createElement('canvas');
|
||||
c.width = rawImg.naturalWidth;
|
||||
c.height = rawImg.naturalHeight;
|
||||
var ctx = c.getContext('2d');
|
||||
var anyTintColorLayer = false;
|
||||
for (var li = 0; li < PLAY_LAYER_ORDER.length; li += 1) {
|
||||
var layerName = PLAY_LAYER_ORDER[li];
|
||||
var img = layers[li];
|
||||
if (!img) continue;
|
||||
var tintKey = PLAY_LAYER_TINT_KEY[layerName];
|
||||
var tintHex = tintKey ? tint[tintKey] : null;
|
||||
if (tintKey) anyTintColorLayer = true;
|
||||
drawTintedLayer(ctx, c.width, c.height, img, tintHex);
|
||||
}
|
||||
if (!anyTintColorLayer) return null;
|
||||
var out = c.toDataURL('image/png');
|
||||
previewCompositeCache[cacheKey] = out;
|
||||
return out;
|
||||
return composeFromLayers(rawImg.naturalWidth, rawImg.naturalHeight, layers);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// exact: ลอง manifest ก่อน → ถ้าไม่สำเร็จ (layer สีโหลดไม่ครบ) ลอง raw layer แบบ convention ต่อ
|
||||
// กันเคส "บาง browser/PC" ที่ตกไปใช้ heuristic แล้วย้อมทั้งรูป (เส้นฟ้า/ถุงมือกลายเป็นสีธีม)
|
||||
return tryManifest().then(function (out) {
|
||||
return out || tryRawLayers();
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
|
||||
/** สร้าง tint จาก index (ใช้ palette hardcoded — deterministic ทุก browser) */
|
||||
function tintFromIndices(themeIndex, skinIndex) {
|
||||
var skinIdx = clampIndex(skinIndex, 1, 3) - 1;
|
||||
var colorIdx = clampIndex(themeIndex, 1, 8) - 1;
|
||||
return {
|
||||
head: PLAY_TINT_HEAD[skinIdx],
|
||||
hair: PLAY_TINT_HAIR[colorIdx],
|
||||
body: PLAY_TINT_BODY[colorIdx]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Public API — ให้ lobby.js เรียก recompute สีตัวละครจาก index ได้ทุกเครื่อง
|
||||
* ใช้เส้น exact (composite layer) ก่อน, ถ้าไม่มีเลเยอร์ค่อย fallback heuristic จากรูปดิบฝั่ง server
|
||||
* @returns {Promise<string|null>} data URL ของ idle ทิศ down หรือ null
|
||||
*/
|
||||
function composeTintedCharacterByIndex(characterId, themeIndex, skinIndex) {
|
||||
if (!characterId) return Promise.resolve(null);
|
||||
var tint = tintFromIndices(themeIndex, skinIndex);
|
||||
return composeTintedCharacterPreview(characterId, tint).then(function (out) {
|
||||
if (out) return out;
|
||||
var enc = encodeURIComponent(characterId);
|
||||
var rawCandidates = [
|
||||
characterAssetsBasePath() + enc + '_down_idle.png',
|
||||
characterAssetsBasePath() + enc + '_down.png',
|
||||
characterAssetsBasePath() + enc + '_down_0.png'
|
||||
];
|
||||
return resolveFirstImage(rawCandidates).then(function (img) {
|
||||
if (!img || !img.src) return null;
|
||||
return composeHeuristicTintFromSource(img.src, tint);
|
||||
});
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
|
||||
function resolveDomAvatarSource() {
|
||||
@@ -565,8 +644,8 @@
|
||||
}
|
||||
|
||||
function applyCustomizeSelection() {
|
||||
var characterId = getStoredCharacterId();
|
||||
return new Promise(function (resolve) {
|
||||
resolveApplyCharacterId(function (characterId) {
|
||||
function runApply() {
|
||||
// ไม่มีตัวละคร (เครื่องที่ยังไม่ได้เลือกตัวละคร) — ยังต้องซิงก์สีไปเซิร์ฟเวอร์
|
||||
// ให้คนอื่นเห็น แต่ข้ามการ composite รูป preview เพราะไม่มี characterId
|
||||
@@ -574,6 +653,7 @@
|
||||
try {
|
||||
persistConfirmedState();
|
||||
syncLobbyTintStorageKeys();
|
||||
postLobbyStyleToServer(state.activeColor, state.activeSkin);
|
||||
} catch (e0) { /* ignore */ }
|
||||
try {
|
||||
window.dispatchEvent(new CustomEvent('customize-popup:applied', {
|
||||
@@ -610,6 +690,7 @@
|
||||
}));
|
||||
persistConfirmedState();
|
||||
syncLobbyTintStorageKeys();
|
||||
postLobbyStyleToServer(state.activeColor, state.activeSkin);
|
||||
} catch (e) { /* ignore */ }
|
||||
syncLiveAvatarElements(finalSrc, 'ตัวละครที่ปรับแต่งแล้ว');
|
||||
try {
|
||||
@@ -643,6 +724,7 @@
|
||||
return;
|
||||
}
|
||||
runApply();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1181,4 +1263,6 @@
|
||||
window.openCustomizePopup = openPopup;
|
||||
window.closeCustomizePopup = closePopup;
|
||||
window.refreshCustomizeColorRow = renderColorRow;
|
||||
/** ใช้โดย lobby.js เพื่อ recompute สีตัวละครจาก index บนทุกเครื่อง */
|
||||
window.jdComposeTintedCharacter = composeTintedCharacterByIndex;
|
||||
})();
|
||||
|
||||
@@ -41,6 +41,20 @@
|
||||
function readCoins() {
|
||||
try { return String(Math.max(0, parseInt(localStorage.getItem('jdCoins') || '0', 10) || 0)); } catch (e) { return '0'; }
|
||||
}
|
||||
function readAgentIdRaw() {
|
||||
try { return (localStorage.getItem('agentDisplayId') || '').trim(); } catch (e) { return ''; }
|
||||
}
|
||||
|
||||
/** key ผู้เล่น (เดียวกับ lobby.js) — สร้างถ้ายังไม่มี เพื่อให้ระบบรหัส sync ใช้ได้ */
|
||||
function ensurePlayerKey() {
|
||||
var k = '';
|
||||
try { k = (localStorage.getItem('jdPlayerKey') || '').trim(); } catch (e) { k = ''; }
|
||||
if (!k || k.length < 8) {
|
||||
k = 'p_' + Date.now() + '_' + Math.random().toString(36).slice(2, 14);
|
||||
try { localStorage.setItem('jdPlayerKey', k); } catch (e2) { /* ignore */ }
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
var refs = {
|
||||
overlay: null,
|
||||
@@ -286,6 +300,173 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ===== ระบบรหัสผู้เล่น — sync โปรไฟล์ (สี/COINS/ชื่อ) ข้าม browser/เครื่อง ===== */
|
||||
var linkRefs = { overlay: null, codeEl: null, input: null, msgEl: null };
|
||||
|
||||
function linkApiUrl() {
|
||||
return appRel('/Admin/api/player-link.php');
|
||||
}
|
||||
|
||||
function buildLinkUi() {
|
||||
if (document.getElementById('jd-link-overlay')) return;
|
||||
var st = document.createElement('style');
|
||||
st.id = 'jd-link-style';
|
||||
st.textContent = '' +
|
||||
'.jd-link-trigger{position:absolute;top:14px;left:16px;z-index:6;display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:999px;border:2px solid rgba(56,232,255,.85);background:rgba(8,24,44,.9);color:#dffaff;font:600 14px/1 Kanit,Segoe UI,sans-serif;cursor:pointer;box-shadow:0 0 14px rgba(56,232,255,.3);}' +
|
||||
'.jd-link-trigger:hover{background:rgba(20,50,80,.95);}' +
|
||||
'.jd-link-overlay{position:fixed;inset:0;z-index:10090;display:flex;align-items:center;justify-content:center;padding:16px;}' +
|
||||
'.jd-link-overlay.is-hidden{display:none!important;}' +
|
||||
'.jd-link-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.74);}' +
|
||||
'.jd-link-dialog{position:relative;z-index:1;width:min(440px,calc(100vw - 32px));padding:24px 22px 20px;border-radius:16px;background:linear-gradient(180deg,#0d2848 0%,#061528 100%);border:2px solid rgba(56,232,255,.85);box-shadow:0 0 30px rgba(56,232,255,.35);color:#eaf8ff;font-family:Kanit,Segoe UI,sans-serif;}' +
|
||||
'.jd-link-close{position:absolute;top:8px;right:12px;background:none;border:none;color:#9fd8ff;font-size:1.7rem;line-height:1;cursor:pointer;}' +
|
||||
'.jd-link-title{margin:0 0 4px;font-size:1.2rem;font-weight:700;text-align:center;}' +
|
||||
'.jd-link-sub{margin:0 0 14px;font-size:.82rem;opacity:.75;text-align:center;}' +
|
||||
'.jd-link-codebox{display:flex;align-items:center;gap:10px;justify-content:center;background:rgba(0,20,40,.85);border:2px solid rgba(56,232,255,.45);border-radius:12px;padding:12px;margin-bottom:6px;}' +
|
||||
'.jd-link-code{font-size:1.7rem;font-weight:700;letter-spacing:.14em;color:#5ce9ff;}' +
|
||||
'.jd-link-copy{padding:8px 12px;border-radius:8px;border:2px solid rgba(56,232,255,.55);background:rgba(20,50,80,.7);color:#dffaff;font:600 .85rem Kanit,sans-serif;cursor:pointer;}' +
|
||||
'.jd-link-hr{height:1px;background:rgba(120,200,255,.25);margin:16px 0;}' +
|
||||
'.jd-link-label{font-size:.92rem;margin:0 0 8px;font-weight:600;}' +
|
||||
'.jd-link-row{display:flex;gap:8px;}' +
|
||||
'.jd-link-input{flex:1;box-sizing:border-box;padding:11px 12px;border-radius:10px;border:2px solid rgba(56,232,255,.55);background:rgba(0,20,40,.85);color:#fff;font:600 1.05rem Kanit,sans-serif;letter-spacing:.1em;text-transform:uppercase;outline:none;}' +
|
||||
'.jd-link-input:focus{border-color:#5ce9ff;box-shadow:0 0 0 3px rgba(92,233,255,.25);}' +
|
||||
'.jd-link-go{padding:11px 18px;border-radius:10px;border:2px solid #7df4ff;background:linear-gradient(180deg,#2ee6ff,#0ea5c9);color:#042033;font:700 1rem Kanit,sans-serif;cursor:pointer;}' +
|
||||
'.jd-link-warn{margin:10px 0 0;font-size:.78rem;color:#ffd27d;text-align:center;}' +
|
||||
'.jd-link-msg{margin:10px 0 0;font-size:.85rem;text-align:center;min-height:1.1em;}' +
|
||||
'.jd-link-msg.is-err{color:#ff8a8a;}.jd-link-msg.is-ok{color:#8effa6;}';
|
||||
document.head.appendChild(st);
|
||||
|
||||
var ov = document.createElement('div');
|
||||
ov.id = 'jd-link-overlay';
|
||||
ov.className = 'jd-link-overlay is-hidden';
|
||||
ov.setAttribute('aria-hidden', 'true');
|
||||
ov.innerHTML = '' +
|
||||
'<div class="jd-link-backdrop" id="jd-link-backdrop"></div>' +
|
||||
'<div class="jd-link-dialog" role="dialog" aria-modal="true" aria-label="ซิงก์โปรไฟล์ข้ามเครื่อง">' +
|
||||
' <button type="button" class="jd-link-close" id="jd-link-close" aria-label="ปิด">×</button>' +
|
||||
' <h2 class="jd-link-title">ซิงก์โปรไฟล์ข้ามเครื่อง</h2>' +
|
||||
' <p class="jd-link-sub">ใช้รหัสนี้เพื่อดึงสี / COINS / ชื่อ ไปยังเครื่องหรือ browser อื่น</p>' +
|
||||
' <p class="jd-link-label">รหัสผู้เล่นของฉัน</p>' +
|
||||
' <div class="jd-link-codebox"><span class="jd-link-code" id="jd-link-code">····-····</span><button type="button" class="jd-link-copy" id="jd-link-copy">คัดลอก</button></div>' +
|
||||
' <p class="jd-link-sub" style="margin-top:6px">รหัสจะหมดอายุถ้าไม่ได้ใช้เกิน 30 วัน</p>' +
|
||||
' <div class="jd-link-hr"></div>' +
|
||||
' <p class="jd-link-label">มีรหัสจากเครื่องอื่น? กรอกที่นี่</p>' +
|
||||
' <div class="jd-link-row"><input type="text" class="jd-link-input" id="jd-link-input" maxlength="9" placeholder="XXXX-XXXX" autocomplete="off" inputmode="latin"><button type="button" class="jd-link-go" id="jd-link-go">ดึงข้อมูล</button></div>' +
|
||||
' <p class="jd-link-warn">การดึงข้อมูลจะแทนที่โปรไฟล์ในเครื่องนี้ (สี/COINS/ชื่อ)</p>' +
|
||||
' <p class="jd-link-msg" id="jd-link-msg" role="alert"></p>' +
|
||||
'</div>';
|
||||
document.body.appendChild(ov);
|
||||
|
||||
linkRefs.overlay = ov;
|
||||
linkRefs.codeEl = document.getElementById('jd-link-code');
|
||||
linkRefs.input = document.getElementById('jd-link-input');
|
||||
linkRefs.msgEl = document.getElementById('jd-link-msg');
|
||||
|
||||
document.getElementById('jd-link-backdrop').addEventListener('click', closeLinkModal);
|
||||
document.getElementById('jd-link-close').addEventListener('click', closeLinkModal);
|
||||
document.getElementById('jd-link-copy').addEventListener('click', copyMyCode);
|
||||
document.getElementById('jd-link-go').addEventListener('click', redeemEnteredCode);
|
||||
linkRefs.input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); redeemEnteredCode(); } });
|
||||
|
||||
// ปุ่มเปิด — ฝังในกล่องโปรไฟล์ (absolute, ไม่กระทบ layout เดิม)
|
||||
if (refs.dialog && !document.getElementById('jd-link-trigger')) {
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.id = 'jd-link-trigger';
|
||||
btn.className = 'jd-link-trigger';
|
||||
btn.textContent = '🔗 ซิงก์เครื่อง';
|
||||
btn.addEventListener('click', openLinkModal);
|
||||
refs.dialog.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
function setLinkMsg(text, kind) {
|
||||
if (!linkRefs.msgEl) return;
|
||||
linkRefs.msgEl.textContent = text || '';
|
||||
linkRefs.msgEl.className = 'jd-link-msg' + (kind ? ' is-' + kind : '');
|
||||
}
|
||||
|
||||
function openLinkModal() {
|
||||
if (!linkRefs.overlay) return;
|
||||
setLinkMsg('', '');
|
||||
if (linkRefs.input) linkRefs.input.value = '';
|
||||
if (linkRefs.codeEl) linkRefs.codeEl.textContent = '····-····';
|
||||
linkRefs.overlay.classList.remove('is-hidden');
|
||||
linkRefs.overlay.setAttribute('aria-hidden', 'false');
|
||||
fetchMyCode();
|
||||
}
|
||||
|
||||
function closeLinkModal() {
|
||||
if (!linkRefs.overlay) return;
|
||||
linkRefs.overlay.classList.add('is-hidden');
|
||||
linkRefs.overlay.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
function fetchMyCode() {
|
||||
var key = ensurePlayerKey();
|
||||
var url = linkApiUrl() + '?action=code&playerKey=' + encodeURIComponent(key) +
|
||||
'&displayName=' + encodeURIComponent(readName()) +
|
||||
'&agentId=' + encodeURIComponent(readAgentIdRaw());
|
||||
fetch(url, { credentials: 'omit', cache: 'no-store' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
if (d && d.ok && d.codeDisplay && linkRefs.codeEl) linkRefs.codeEl.textContent = d.codeDisplay;
|
||||
else setLinkMsg('โหลดรหัสไม่สำเร็จ ลองใหม่อีกครั้ง', 'err');
|
||||
})
|
||||
.catch(function () { setLinkMsg('ออฟไลน์ — โหลดรหัสไม่ได้', 'err'); });
|
||||
}
|
||||
|
||||
function copyMyCode() {
|
||||
var txt = linkRefs.codeEl ? linkRefs.codeEl.textContent : '';
|
||||
if (!txt || txt.indexOf('·') >= 0) return;
|
||||
function done() { setLinkMsg('คัดลอกรหัสแล้ว', 'ok'); }
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(txt).then(done, function () { done(); });
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
done();
|
||||
}
|
||||
|
||||
function redeemEnteredCode() {
|
||||
var raw = (linkRefs.input && linkRefs.input.value || '').toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
if (raw.length !== 8) { setLinkMsg('กรุณากรอกรหัส 8 ตัว', 'err'); return; }
|
||||
setLinkMsg('กำลังดึงข้อมูล…', '');
|
||||
fetch(linkApiUrl(), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'omit',
|
||||
body: JSON.stringify({ action: 'redeem', code: raw })
|
||||
})
|
||||
.then(function (r) { return r.json().then(function (j) { return { status: r.status, j: j }; }); })
|
||||
.then(function (res) {
|
||||
var d = res.j || {};
|
||||
if (!d.ok || !d.playerKey) {
|
||||
setLinkMsg(d.error || 'ไม่พบรหัสนี้', 'err');
|
||||
return;
|
||||
}
|
||||
applyRedeemedProfile(d);
|
||||
})
|
||||
.catch(function () { setLinkMsg('ออฟไลน์ — ดึงข้อมูลไม่ได้', 'err'); });
|
||||
}
|
||||
|
||||
function applyRedeemedProfile(p) {
|
||||
try {
|
||||
localStorage.setItem('jdPlayerKey', String(p.playerKey));
|
||||
if (p.displayName) { localStorage.setItem(DISPLAY_NAME_KEY, p.displayName); localStorage.setItem('playerName', p.displayName); }
|
||||
if (p.agentId) localStorage.setItem('agentDisplayId', String(p.agentId));
|
||||
if (p.colorThemeIndex) localStorage.setItem('lobbyThemeColor', String(p.colorThemeIndex));
|
||||
if (p.skinToneIndex) localStorage.setItem('lobbySkinTone', String(p.skinToneIndex));
|
||||
if (p.coins != null) localStorage.setItem('jdCoins', String(p.coins));
|
||||
// ลบ avatar ที่ bake สีไว้ของเครื่องนี้ — บังคับ recompute จากสีที่ดึงมา
|
||||
Object.keys(localStorage).forEach(function (k) {
|
||||
if (k.indexOf('jdCharLobbyIdleDown:') === 0) localStorage.removeItem(k);
|
||||
});
|
||||
} catch (e) { /* ignore */ }
|
||||
setLinkMsg('สำเร็จ! กำลังโหลดโปรไฟล์ใหม่…', 'ok');
|
||||
setTimeout(function () { window.location.reload(); }, 500);
|
||||
}
|
||||
|
||||
function isRoomLobbyPage() {
|
||||
return /room-lobby\.html$/i.test(String(window.location.pathname || ''));
|
||||
}
|
||||
@@ -297,6 +478,7 @@
|
||||
createMarkupIfNeeded();
|
||||
bindRefs();
|
||||
bindEvents();
|
||||
buildLinkUi();
|
||||
syncProfileScale();
|
||||
setGroup(1);
|
||||
if (window.jdAchievements) window.jdAchievements.load();
|
||||
|
||||
@@ -362,18 +362,9 @@
|
||||
var fromHex = rlHexToRgbCss(hexArr[hi]);
|
||||
if (fromHex) { rlSwatchCache[key] = fromHex; cb(fromHex); return; }
|
||||
}
|
||||
rlLoadImg(RL_CUSTOMIZE_ASSET + (group === 'color' ? 'color-' : 'skin-tone-') + idx + '.png', function (img) {
|
||||
if (!img || !img.naturalWidth) { cb(null); return; }
|
||||
try {
|
||||
var c = document.createElement('canvas'); c.width = 1; c.height = 1;
|
||||
var x = c.getContext('2d');
|
||||
x.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, 1, 1);
|
||||
var d = x.getImageData(0, 0, 1, 1).data;
|
||||
var rgb = 'rgb(' + d[0] + ',' + d[1] + ',' + d[2] + ')';
|
||||
rlSwatchCache[key] = rgb;
|
||||
cb(rgb);
|
||||
} catch (e) { cb(null); }
|
||||
});
|
||||
// ไม่ sample สีจากรูปด้วย getImageData อีกต่อไป — การย่อรูป/decode ต่าง browser
|
||||
// ทำให้ค่าสีเพี้ยนไม่ตรงกัน ใช้ palette hex (ด้านบน) เป็นแหล่งความจริงเดียว
|
||||
cb(null);
|
||||
}
|
||||
|
||||
var LOBBY_PLAYER_TINT_KEY = 'justiceLobbyPlayerTint';
|
||||
|
||||
@@ -1615,9 +1615,9 @@
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/display-name.js?v=2"></script>
|
||||
<script src="js/version.js?v=0.0122"></script>
|
||||
<script src="js/customize-popup.js?v=35" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
|
||||
<script src="js/customize-popup.js?v=38" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
|
||||
<script src="js/achievements.js?v=0.002" data-asset-base="img/03-6-Profile"></script>
|
||||
<script src="js/room-lobby.js?v=0.0293"></script>
|
||||
<script src="js/room-lobby.js?v=0.0294"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -228,9 +228,9 @@
|
||||
<script src="../app-base.js?v=2"></script>
|
||||
<script src="../Game/js/display-name.js?v=2"></script>
|
||||
<script src="daily-popup.js?v=26" data-daily-trigger="#btn-daily" data-daily-asset-base="IMAGE/Daily" data-daily-test-reset-seconds="0"></script>
|
||||
<script src="../Game/js/customize-popup.js?v=35" data-customize-triggers="#btn-cloth" data-customize-asset-base="/Game/img/03-5-Customize"></script>
|
||||
<script src="../Game/js/customize-popup.js?v=38" data-customize-triggers="#btn-cloth" data-customize-asset-base="/Game/img/03-5-Customize"></script>
|
||||
<script src="../Game/js/achievements.js?v=0.002" data-asset-base="/Game/img/03-6-Profile"></script>
|
||||
<script src="../Game/js/profile-popup.js?v=7" data-profile-trigger="#btn-myprofile" data-profile-asset-base="/Game/img/03-6-Profile"></script>
|
||||
<script src="lobby.js?v=0.0192"></script>
|
||||
<script src="../Game/js/profile-popup.js?v=8" data-profile-trigger="#btn-myprofile" data-profile-asset-base="/Game/img/03-6-Profile"></script>
|
||||
<script src="lobby.js?v=0.0193"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -298,6 +298,38 @@
|
||||
.catch(function () { /* ออฟไลน์ → คงตัวอย่างเดิม */ });
|
||||
}
|
||||
|
||||
/** สีที่เลือกไว้ใน localStorage (เครื่องนี้) — index theme 1–8 / skin 1–3 */
|
||||
function getLocalLobbyStyle() {
|
||||
var theme = 0, skin = 0;
|
||||
try {
|
||||
theme = parseInt(localStorage.getItem('lobbyThemeColor') || '', 10) || 0;
|
||||
skin = parseInt(localStorage.getItem('lobbySkinTone') || '', 10) || 0;
|
||||
} catch (e) { /* ignore */ }
|
||||
return { theme: theme, skin: skin };
|
||||
}
|
||||
|
||||
function mirrorLobbyStyleToLocal(theme, skin) {
|
||||
try {
|
||||
if (theme >= 1 && theme <= 8) localStorage.setItem('lobbyThemeColor', String(theme));
|
||||
if (skin >= 1 && skin <= 3) localStorage.setItem('lobbySkinTone', String(skin));
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
/** ดึงสีที่เลือกจากเซิร์ฟเวอร์ (cross-device) ตาม jdPlayerKey */
|
||||
function fetchServerLobbyStyle(cb) {
|
||||
var key;
|
||||
try { key = ensurePlayerKey(); } catch (e) { key = ''; }
|
||||
if (!key) { cb(null); return; }
|
||||
var base = (typeof appPath === 'function' ? appPath('/Admin/api/player-lobby-style.php') : '/Admin/api/player-lobby-style.php');
|
||||
fetch(base + '?playerKey=' + encodeURIComponent(key), { credentials: 'omit', cache: 'no-store' })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
if (!d || !d.ok) { cb(null); return; }
|
||||
cb({ theme: parseInt(d.colorThemeIndex, 10) || 0, skin: parseInt(d.skinToneIndex, 10) || 0 });
|
||||
})
|
||||
.catch(function () { cb(null); });
|
||||
}
|
||||
|
||||
/** รีเฟรชหลังเลือกตัวละคร / สลับแท็บ / ย้อนกลับบน tablet (bfcache) */
|
||||
function syncMainLobbyCharacterUi() {
|
||||
applyProfileTexts();
|
||||
@@ -313,11 +345,36 @@
|
||||
return;
|
||||
}
|
||||
var idleCached = getStoredLobbyIdleDownDataUrl(id);
|
||||
if (idleCached) {
|
||||
applyCharAssets(idleCached);
|
||||
return;
|
||||
// วาด cache ทันทีกันจอว่าง แล้วค่อย recompute จากสีจริง (cross-device) ทับ
|
||||
if (idleCached) applyCharAssets(idleCached);
|
||||
|
||||
function fallbackToSprite() {
|
||||
if (idleCached) return; // วาดไปแล้ว
|
||||
characterLobbySpriteFirstLiveUrl(id, applyCharAssets);
|
||||
}
|
||||
characterLobbySpriteFirstLiveUrl(id, applyCharAssets);
|
||||
|
||||
// สีจาก server เป็นแหล่งความจริงข้ามเครื่อง, ถ้าไม่มีค่อยใช้ local
|
||||
fetchServerLobbyStyle(function (server) {
|
||||
var local = getLocalLobbyStyle();
|
||||
var theme = (server && server.theme) || local.theme || 0;
|
||||
var skin = (server && server.skin) || local.skin || 0;
|
||||
if (theme >= 1 && theme <= 8) mirrorLobbyStyleToLocal(theme, skin);
|
||||
|
||||
// ยังไม่เคยเลือกสี → ใช้รูปตามเดิม (ไม่ recolor)
|
||||
if (!(theme >= 1 && theme <= 8) || typeof window.jdComposeTintedCharacter !== 'function') {
|
||||
fallbackToSprite();
|
||||
return;
|
||||
}
|
||||
|
||||
window.jdComposeTintedCharacter(id, theme, skin).then(function (dataUrl) {
|
||||
if (dataUrl && typeof dataUrl === 'string' && dataUrl.indexOf('data:image/') === 0) {
|
||||
try { localStorage.setItem(LOBBY_IDLE_DOWN_PREFIX + id, dataUrl); } catch (e) { /* ignore */ }
|
||||
applyCharAssets(dataUrl);
|
||||
} else {
|
||||
fallbackToSprite();
|
||||
}
|
||||
}).catch(function () { fallbackToSprite(); });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,6 @@
|
||||
</div>
|
||||
|
||||
<script src="../app-base.js?v=2"></script>
|
||||
<script src="quiz-battle.js?v=0.0500"></script>
|
||||
<script src="quiz-battle.js?v=0.0501"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -40,12 +40,7 @@
|
||||
return [BASE + '/img/characters/' + enc + '_down.png', BASE + '/img/characters/' + enc + '_down_0.png'];
|
||||
}
|
||||
|
||||
function applyProfileAvatar() {
|
||||
var av = document.getElementById('lobby-profile-avatar');
|
||||
if (!av) return;
|
||||
var fallbackAvatar = typeof appPath === 'function' ? appPath('/Main-Menu/char-main.png') : '/Main-Menu/char-main.png';
|
||||
var cid = getSelectedCharacterId();
|
||||
if (!cid) { av.onerror = null; av.src = fallbackAvatar; return; }
|
||||
function renderAvatarForId(av, cid, fallbackAvatar) {
|
||||
try {
|
||||
var savedLobbyAvatar = localStorage.getItem(LOBBY_IDLE_DOWN_PREFIX + cid) || '';
|
||||
if (savedLobbyAvatar && savedLobbyAvatar.indexOf('data:image/') === 0) {
|
||||
@@ -64,6 +59,25 @@
|
||||
av.src = urls[0];
|
||||
}
|
||||
|
||||
function applyProfileAvatar() {
|
||||
var av = document.getElementById('lobby-profile-avatar');
|
||||
if (!av) return;
|
||||
var fallbackAvatar = typeof appPath === 'function' ? appPath('/Main-Menu/char-main.png') : '/Main-Menu/char-main.png';
|
||||
var cid = getSelectedCharacterId();
|
||||
if (cid) { renderAvatarForId(av, cid, fallbackAvatar); return; }
|
||||
// ไม่มี id ใน localStorage → ดึงจาก API เหมือน lobby (กัน avatar ไม่ขึ้น/ขึ้นรูป default)
|
||||
fetch(BASE + '/api/characters', { credentials: 'same-origin', cache: 'no-store' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (list) {
|
||||
if (!Array.isArray(list) || !list.length) { av.onerror = null; av.src = fallbackAvatar; return; }
|
||||
var last = list[list.length - 1];
|
||||
var id = last && last.id ? String(last.id).trim() : '';
|
||||
if (!id) { av.onerror = null; av.src = fallbackAvatar; return; }
|
||||
renderAvatarForId(av, id, fallbackAvatar);
|
||||
})
|
||||
.catch(function () { av.onerror = null; av.src = fallbackAvatar; });
|
||||
}
|
||||
|
||||
/* ===================== เข้าฉากเดิน (ZEP walkable) ===================== */
|
||||
var entering = false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user