Game: default character = first uploaded (API order by mtime); defer join until loaded

This commit is contained in:
2026-04-24 08:41:15 +00:00
parent 467363d651
commit ae56e7b1be
6 changed files with 132 additions and 53 deletions
+1 -1
View File
@@ -146,7 +146,7 @@
</div>
<div class="editor-card__row editor-card__row--actions">
<button type="button" id="btn-save" class="editor-btn-primary">บันทึกฉาก</button>
<button type="button" id="btn-play-test" title="บันทึกฉากแล้วเท่านั้น — เปิดแท็บใหม่เล่นทดสอบด้วยตัวละครเริ่มต้น">ทดลองเล่น</button>
<button type="button" id="btn-play-test" title="บันทึกฉากแล้วเท่านั้น — เปิดแท็บใหม่เล่นทดสอบด้วยตัวละครตัวแรกที่สร้าง (จาก /Game/character.html)">ทดลองเล่น</button>
</div>
</section>
</div>
+70 -42
View File
@@ -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');
+33 -5
View File
@@ -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;
+1 -1
View File
@@ -669,7 +669,7 @@
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0166"></script>
<script src="js/play.js?v=0.078"></script>
<script src="js/play.js?v=0.079"></script>
<div class="version-tag">v —</div>
</body>
</html>
+1 -1
View File
@@ -1015,7 +1015,7 @@
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0122"></script>
<script src="js/room-lobby.js?v=0.0174"></script>
<script src="js/room-lobby.js?v=0.0175"></script>
<div class="version-tag">v —</div>
</body>
</html>
+26 -3
View File
@@ -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];