From ae56e7b1be5ca05d8ed8f27e30a425ca7850b377 Mon Sep 17 00:00:00 2001 From: giteaadmin Date: Fri, 24 Apr 2026 08:41:15 +0000 Subject: [PATCH] Game: default character = first uploaded (API order by mtime); defer join until loaded --- www/html/Game/public/editor.html | 2 +- www/html/Game/public/js/play.js | 112 ++++++++++++++++---------- www/html/Game/public/js/room-lobby.js | 38 +++++++-- www/html/Game/public/play.html | 2 +- www/html/Game/public/room-lobby.html | 2 +- www/html/Game/server.js | 29 ++++++- 6 files changed, 132 insertions(+), 53 deletions(-) diff --git a/www/html/Game/public/editor.html b/www/html/Game/public/editor.html index 7f34f82..7020074 100644 --- a/www/html/Game/public/editor.html +++ b/www/html/Game/public/editor.html @@ -146,7 +146,7 @@
- +
diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index bee2acb..712d953 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -6,6 +6,21 @@ const previewMode = params.get('preview') === '1'; const forceDefaultCharacter = params.get('defaultChar') === '1'; const editorEmbedReturn = params.get('editorEmbed') === '1'; + /** จนกว่าโหลด /api/characters — placeholder ชั่วคราวก่อน join */ + const LEGACY_PLACEHOLDER_CHARACTER_ID = 'Chatest'; + let firstCharacterDefaultResolved = null; + const firstCharacterDefaultPromise = fetch(BASE + '/api/characters') + .then((r) => r.json()) + .then((list) => { + if (Array.isArray(list) && list.length > 0 && list[0] && list[0].id) { + firstCharacterDefaultResolved = String(list[0].id); + } else { + firstCharacterDefaultResolved = ''; + } + }) + .catch(() => { + firstCharacterDefaultResolved = ''; + }); const lobbyLevelParam = params.get('lobbyLevel'); const lobbyCaseParam = params.get('case'); if (lobbyLevelParam || lobbyCaseParam) { @@ -443,12 +458,19 @@ if (img) return img; return defaultAvatarImg; } - const DEFAULT_CHARACTER_ID = 'Chatest'; function getStoredCharacterId() { - try { return localStorage.getItem('gameCharacterId') || DEFAULT_CHARACTER_ID; } catch (e) { return DEFAULT_CHARACTER_ID; } + try { + const v = localStorage.getItem('gameCharacterId'); + if (v) return v; + } catch (e) { /* ignore */ } + if (firstCharacterDefaultResolved === null) return LEGACY_PLACEHOLDER_CHARACTER_ID; + return firstCharacterDefaultResolved || ''; } function getPlayCharacterId() { - if (forceDefaultCharacter) return DEFAULT_CHARACTER_ID; + if (forceDefaultCharacter) { + if (firstCharacterDefaultResolved === null) return LEGACY_PLACEHOLDER_CHARACTER_ID; + return firstCharacterDefaultResolved || ''; + } return getStoredCharacterId(); } const MOVE_SPEED = 0.15; @@ -4973,43 +4995,45 @@ } socket.on('connect', () => { - socket.emit('join-space', { spaceId, nickname: nick, characterId: getPlayCharacterId() }, (res) => { - if (!res || !res.ok) { - const errMsg = (res && res.error) || 'เข้าร่วมไม่ได้'; - alert(errMsg); - const caseLocked = /เริ่มคดี|ไม่รับผู้เล่น/.test(errMsg); - if (caseLocked) { - window.location.replace('room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick)); - } else { - window.location.replace(BASE + '/lobby.html'); + firstCharacterDefaultPromise.then(() => { + socket.emit('join-space', { spaceId, nickname: nick, characterId: getPlayCharacterId() }, (res) => { + if (!res || !res.ok) { + const errMsg = (res && res.error) || 'เข้าร่วมไม่ได้'; + alert(errMsg); + const caseLocked = /เริ่มคดี|ไม่รับผู้เล่น/.test(errMsg); + if (caseLocked) { + window.location.replace('room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick)); + } else { + window.location.replace(BASE + '/lobby.html'); + } + return; } - return; - } - myId = socket.id; - if (previewMode && editorEmbedReturn && res.mapData && res.mapData.gameType === 'lobby') { - const q = []; - q.push('space=' + encodeURIComponent(spaceId)); - q.push('nick=' + encodeURIComponent(nick)); - const mid = params.get('map'); - if (mid) q.push('map=' + encodeURIComponent(mid)); - q.push('preview=1'); - q.push('editorEmbed=1'); - if (params.get('defaultChar') != null) q.push('defaultChar=' + encodeURIComponent(params.get('defaultChar'))); - window.location.replace(BASE + '/room-lobby.html?' + q.join('&')); - return; - } - const playMapId = params.get('map'); - if (playMapId) { - fetch(BASE + '/api/maps/' + encodeURIComponent(playMapId)) - .then(r => r.ok ? r.json() : null) - .then(data => { - if (data) applyMapAndStart(data, res); - else applyMapAndStart(res.mapData, res); - }) - .catch(() => applyMapAndStart(res.mapData, res)); - } else { - applyMapAndStart(res.mapData, res); - } + myId = socket.id; + if (previewMode && editorEmbedReturn && res.mapData && res.mapData.gameType === 'lobby') { + const q = []; + q.push('space=' + encodeURIComponent(spaceId)); + q.push('nick=' + encodeURIComponent(nick)); + const mid = params.get('map'); + if (mid) q.push('map=' + encodeURIComponent(mid)); + q.push('preview=1'); + q.push('editorEmbed=1'); + if (params.get('defaultChar') != null) q.push('defaultChar=' + encodeURIComponent(params.get('defaultChar'))); + window.location.replace(BASE + '/room-lobby.html?' + q.join('&')); + return; + } + const playMapId = params.get('map'); + if (playMapId) { + fetch(BASE + '/api/maps/' + encodeURIComponent(playMapId)) + .then(r => r.ok ? r.json() : null) + .then(data => { + if (data) applyMapAndStart(data, res); + else applyMapAndStart(res.mapData, res); + }) + .catch(() => applyMapAndStart(res.mapData, res)); + } else { + applyMapAndStart(res.mapData, res); + } + }); }); }); @@ -6418,9 +6442,13 @@ const av = document.createElement('img'); av.className = 'play-cyber-score-av'; av.alt = ''; - const cid = row.characterId || DEFAULT_CHARACTER_ID; - const im = getCharacterImg(cid, 'down'); - if (im && im.src) av.src = im.src; + const cid = row.characterId ? String(row.characterId) : ''; + if (!cid) { + av.src = defaultAvatarImg.src; + } else { + const im = getCharacterImg(cid, 'down'); + if (im && im.src) av.src = im.src; + } const mid = document.createElement('div'); mid.className = 'play-cyber-score-mid'; const name = document.createElement('span'); diff --git a/www/html/Game/public/js/room-lobby.js b/www/html/Game/public/js/room-lobby.js index 1f8cf40..b1d3146 100644 --- a/www/html/Game/public/js/room-lobby.js +++ b/www/html/Game/public/js/room-lobby.js @@ -7,6 +7,21 @@ const previewMode = params.get('preview') === '1'; const editorEmbedReturn = params.get('editorEmbed') === '1'; const forceDefaultCharacter = params.get('defaultChar') === '1'; + /** จนกว่าโหลด /api/characters — ใช้ชั่วคราวก่อน join (หลังโหลดใช้ตัวแรกตามเวลาไฟล์บนเซิร์ฟเวอร์) */ + const LEGACY_PLACEHOLDER_CHARACTER_ID = 'Chatest'; + let firstCharacterDefaultResolved = null; + const firstCharacterDefaultPromise = fetch(BASE + '/api/characters') + .then((r) => r.json()) + .then((list) => { + if (Array.isArray(list) && list.length > 0 && list[0] && list[0].id) { + firstCharacterDefaultResolved = String(list[0].id); + } else { + firstCharacterDefaultResolved = ''; + } + }) + .catch(() => { + firstCharacterDefaultResolved = ''; + }); if (!spaceId) { location.href = BASE + '/lobby.html'; return; } const CREATE_ROOM_URL = '/Create%20Room/'; @@ -1147,10 +1162,17 @@ if (quizModeActive) renderQuizScoreboard(lastQuizScores); } - const DEFAULT_CHARACTER_ID = 'Chatest'; function getStoredCharacterId() { - if (forceDefaultCharacter) return DEFAULT_CHARACTER_ID; - try { return localStorage.getItem('gameCharacterId') || DEFAULT_CHARACTER_ID; } catch (e) { return DEFAULT_CHARACTER_ID; } + if (forceDefaultCharacter) { + if (firstCharacterDefaultResolved === null) return LEGACY_PLACEHOLDER_CHARACTER_ID; + return firstCharacterDefaultResolved || ''; + } + try { + const v = localStorage.getItem('gameCharacterId'); + if (v) return v; + } catch (e) { /* ignore */ } + if (firstCharacterDefaultResolved === null) return LEGACY_PLACEHOLDER_CHARACTER_ID; + return firstCharacterDefaultResolved || ''; } function updatePreviewClaimHostUi() { @@ -1168,7 +1190,11 @@ const img = document.getElementById('room-lobby-profile-avatar'); if (!img) return; const id = getStoredCharacterId(); - img.src = BASE + '/img/characters/' + encodeURIComponent(id) + '_down.png?ch=' + encodeURIComponent(id); + if (!id) { + img.src = defaultAvatarImg.src; + } else { + img.src = BASE + '/img/characters/' + encodeURIComponent(id) + '_down.png?ch=' + encodeURIComponent(id); + } img.alt = (nick || 'ผู้เล่น') + ' — ตัวละคร'; } @@ -1178,7 +1204,8 @@ } socket.on('connect', () => { - socket.emit('join-space', { spaceId, nickname: nick, characterId: getStoredCharacterId() }, (res) => { + firstCharacterDefaultPromise.then(() => { + socket.emit('join-space', { spaceId, nickname: nick, characterId: getStoredCharacterId() }, (res) => { if (!res || !res.ok) { var joinErr = (res && res.error) || 'เข้าไม่ได้'; if (/เริ่มคดี|ไม่รับผู้เล่น/.test(joinErr)) { @@ -1269,6 +1296,7 @@ }).catch(() => {}); }, 600); }); + }); }); const LERP = 0.2; diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index 20a8314..483ae63 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -669,7 +669,7 @@ - +
v —
diff --git a/www/html/Game/public/room-lobby.html b/www/html/Game/public/room-lobby.html index db4b64e..68b9e7b 100644 --- a/www/html/Game/public/room-lobby.html +++ b/www/html/Game/public/room-lobby.html @@ -1015,7 +1015,7 @@ - +
v —
diff --git a/www/html/Game/server.js b/www/html/Game/server.js index 9ee91e9..64d4273 100644 --- a/www/html/Game/server.js +++ b/www/html/Game/server.js @@ -1337,14 +1337,37 @@ const server = http.createServer((req, res) => { m = f.match(/^(.+)_(up|down|left|right)(?:_\d+)?_layer_[a-zA-Z]+\.(png|jpg|jpeg|gif|webp)$/i); if (m) ids.add(m[1]); }); - const list = [...ids].map((id) => { + const idArr = [...ids]; + const oldestMsById = {}; + idArr.forEach((id) => { + let oldest = Infinity; + const prefix = id + '_'; + files.forEach((f) => { + if (!f.startsWith(prefix) || !ext.test(f)) return; + try { + const st = fs.statSync(path.join(CHARACTERS_DIR, f)); + const t = st.mtimeMs != null ? st.mtimeMs : st.mtime.getTime(); + if (t < oldest) oldest = t; + } catch (e) { /* ignore */ } + }); + oldestMsById[id] = oldest === Infinity ? 0 : oldest; + }); + idArr.sort((a, b) => { + const ta = oldestMsById[a] || 0; + const tb = oldestMsById[b] || 0; + if (ta !== tb) return ta - tb; + return String(a).localeCompare(String(b)); + }); + const list = idArr.map((id) => { const prefix = id + '_'; const hasLayerFiles = files.some((f) => f.startsWith(prefix) && f.includes('_layer_') && ext.test(f)); return { id, name: id, hasLayerFiles }; }); - return res.writeHead(200, { 'Content-Type': 'application/json' }), res.end(JSON.stringify(list)); + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify(list)); } catch (e) { - return res.writeHead(200, { 'Content-Type': 'application/json' }), res.end(JSON.stringify([])); + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify([])); } } const urlPath = url.split('?')[0];