minigame 4 more rule
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
||||
"answerMs": 5000,
|
||||
"betweenMs": 3000,
|
||||
"carryReadMs": 3000,
|
||||
"carryAnswerMs": 5000,
|
||||
"carryAnswerMs": 15000,
|
||||
"carrySessionLength": 10,
|
||||
"carryMapPanelTheme": {
|
||||
"panelBg": "rgba(255, 0, 0, 0)",
|
||||
@@ -31,7 +31,7 @@
|
||||
"fillBg": "rgba(12, 10, 20, 0)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-1.png"
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-2.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "fixed",
|
||||
@@ -47,7 +47,7 @@
|
||||
"fillBg": "rgba(12, 10, 20, 0)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-3.png"
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-1.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "fixed",
|
||||
@@ -55,87 +55,87 @@
|
||||
"fillBg": "rgba(12, 10, 20, 0)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-1.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "fixed",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-1.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "fixed",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-3.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "fixed",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-3.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "fixed",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-4.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"borderMode": "fixed",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-4.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "fixed",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-5.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"borderMode": "fixed",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-5.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"borderMode": "fixed",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-5.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"borderMode": "fixed",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-6.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"borderMode": "fixed",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-6.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
@@ -160,7 +160,7 @@
|
||||
"fillBg": "rgba(12, 10, 20, 0)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 0,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-1.png"
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-2.png"
|
||||
},
|
||||
"questions": [
|
||||
{
|
||||
@@ -222,26 +222,24 @@
|
||||
],
|
||||
"carryQuestions": [
|
||||
{
|
||||
"text": "test",
|
||||
"text": "ผู้มีอํานาจในการพิจารณาพิพากษาตัดสินคดีความในศาล",
|
||||
"choices": [
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"s",
|
||||
"d",
|
||||
"f",
|
||||
"z",
|
||||
"deeee",
|
||||
"gfsdfsdf",
|
||||
"fsdfsdf",
|
||||
"dsfewfef",
|
||||
"ssdfsdf",
|
||||
"eeee"
|
||||
"จําเลย",
|
||||
"ผู้เสียหาย",
|
||||
"อัยการ",
|
||||
"ทนายความ",
|
||||
"โจทก์",
|
||||
"ตํารวจ",
|
||||
"ผู้ไกล่เกลี่ย",
|
||||
"ผู้พิพากษา",
|
||||
"นิติกร",
|
||||
"พยานบุคคล",
|
||||
"หมายค้น",
|
||||
"หมายจับ",
|
||||
"หมายเรียก",
|
||||
"พยานหลักฐาน"
|
||||
],
|
||||
"correctIndex": 1
|
||||
"correctIndex": 7
|
||||
},
|
||||
{
|
||||
"text": "test2",
|
||||
@@ -278,5 +276,5 @@
|
||||
"correctIndex": 0
|
||||
}
|
||||
],
|
||||
"carryChoicePlaqueMapScale": 1.7
|
||||
"carryChoicePlaqueMapScale": 1.9
|
||||
}
|
||||
@@ -68,6 +68,8 @@
|
||||
<label>ขนาดタイル <input type="number" id="tile-size" value="32" min="16" max="64"></label>
|
||||
<label title="ลงจากมุมซ้ายบนของจุดเกิด — เน้นขยายแนวตั้งก่อน (เช่น 4 = สูง 4 ช่อง)">ตัวละคร สูง (แนวตั้ง) <input type="number" id="character-cells-h" value="1" min="1" max="4"></label>
|
||||
<label title="ไปทางขวาจากมุมซ้ายบน — แนวนอน">× กว้าง (แนวนอน) <input type="number" id="character-cells-w" value="1" min="1" max="4"></label>
|
||||
<label title="ชนกำแพง (objects) เท่านั้น — แนวนอน กลางใต้ร่าง · เล็กกว่าตัว = หัว/ลำตัวทับภาพกำแพงได้">ชนกำแพง กว้าง <input type="number" id="character-collision-w" value="1" min="1" max="4"></label>
|
||||
<label title="สูงจากเท้าขึ้นมา — น้อยกว่าสูงตัว = ส่วนบนไม่ชนกำแพง">× สูง <input type="number" id="character-collision-h" value="1" min="1" max="4"></label>
|
||||
<button type="button" id="btn-new">สร้างกริดใหม่</button>
|
||||
</div>
|
||||
</section>
|
||||
@@ -271,7 +273,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/version.js?v=0.0169"></script>
|
||||
<script src="js/editor.js?v=20260428-quiz-carry-save-warn"></script>
|
||||
<script src="js/editor.js?v=20260429-char-collision-depth"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
const tileSizeEl = document.getElementById('tile-size');
|
||||
const characterCellsWEl = document.getElementById('character-cells-w');
|
||||
const characterCellsHEl = document.getElementById('character-cells-h');
|
||||
const characterCollisionWEl = document.getElementById('character-collision-w');
|
||||
const characterCollisionHEl = document.getElementById('character-collision-h');
|
||||
const drawModeEl = document.getElementById('draw-mode');
|
||||
const gameTypeEl = document.getElementById('game-type');
|
||||
const statusEl = document.getElementById('editor-status');
|
||||
@@ -56,13 +58,29 @@
|
||||
function readCharacterFootprintInputs() {
|
||||
const cw = characterCellsWEl ? clampCharacterFootprint(characterCellsWEl.value) : characterCellsW;
|
||||
const ch = characterCellsHEl ? clampCharacterFootprint(characterCellsHEl.value) : characterCellsH;
|
||||
return { cw, ch };
|
||||
let colW = characterCollisionWEl ? parseInt(String(characterCollisionWEl.value), 10) : cw;
|
||||
let colH = characterCollisionHEl ? parseInt(String(characterCollisionHEl.value), 10) : ch;
|
||||
if (!Number.isFinite(colW) || colW < 1) colW = cw;
|
||||
if (!Number.isFinite(colH) || colH < 1) colH = ch;
|
||||
colW = Math.max(1, Math.min(cw, Math.floor(colW)));
|
||||
colH = Math.max(1, Math.min(ch, Math.floor(colH)));
|
||||
return { cw, ch, colW, colH };
|
||||
}
|
||||
function applyCharacterFootprintInputs(cw, ch) {
|
||||
characterCellsW = clampCharacterFootprint(cw);
|
||||
characterCellsH = clampCharacterFootprint(ch);
|
||||
if (characterCellsWEl) characterCellsWEl.value = String(characterCellsW);
|
||||
if (characterCellsHEl) characterCellsHEl.value = String(characterCellsH);
|
||||
if (characterCollisionWEl) {
|
||||
let v = parseInt(String(characterCollisionWEl.value), 10);
|
||||
if (!Number.isFinite(v) || v < 1) v = characterCellsW;
|
||||
characterCollisionWEl.value = String(Math.max(1, Math.min(characterCellsW, v)));
|
||||
}
|
||||
if (characterCollisionHEl) {
|
||||
let v = parseInt(String(characterCollisionHEl.value), 10);
|
||||
if (!Number.isFinite(v) || v < 1) v = characterCellsH;
|
||||
characterCollisionHEl.value = String(Math.max(1, Math.min(characterCellsH, v)));
|
||||
}
|
||||
}
|
||||
let gameType = 'zep';
|
||||
let lanes = [];
|
||||
@@ -1449,6 +1467,24 @@
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#9ece6a';
|
||||
ctx.stroke();
|
||||
const colW0 = Math.max(1, Math.min(fp.cw, fp.colW));
|
||||
const colH0 = Math.max(1, Math.min(fp.ch, fp.colH));
|
||||
const colWEff = Math.max(1, Math.min(colW0, effW));
|
||||
const colHEff = Math.max(1, Math.min(colH0, effH));
|
||||
if (colWEff < effW || colHEff < effH) {
|
||||
const ox = spawn.x + Math.floor((effW - colWEff) / 2);
|
||||
const oy = spawn.y + (effH - colHEff);
|
||||
const tcx = ox * tileSize;
|
||||
const tcy = oy * tileSize;
|
||||
const trw = colWEff * tileSize;
|
||||
const trh = colHEff * tileSize;
|
||||
ctx.save();
|
||||
ctx.setLineDash([5, 4]);
|
||||
ctx.strokeStyle = 'rgba(224, 175, 104, 0.95)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(tcx + 2, tcy + 2, trw - 4, trh - 4);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
if (isFrogger && lanes.length) {
|
||||
const timeMs = Date.now();
|
||||
@@ -1574,11 +1610,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
const fpHud = readCharacterFootprintInputs();
|
||||
const fpHud = readCharacterFootprintInputs();
|
||||
ctx.fillStyle = '#c0caf5';
|
||||
ctx.font = '12px sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText('ขนาดตัวละคร: สูง ' + fpHud.ch + ' × กว้าง ' + fpHud.cw + ' ช่อง (แนวตั้ง×แนวนอน · มุมซ้ายบนจุดเกิด)', 8, canvas.height - 8);
|
||||
const colLine = (fpHud.colW < fpHud.cw || fpHud.colH < fpHud.ch)
|
||||
? ' · ชนกำแพง ' + fpHud.colW + '×' + fpHud.colH + ' (เท้า/กลาง — เส้นประส้ม)'
|
||||
: '';
|
||||
ctx.fillText('ขนาดตัวละคร: สูง ' + fpHud.ch + ' × กว้าง ' + fpHud.cw + ' ช่อง (แนวตั้ง×แนวนอน · มุมซ้ายบนจุดเกิด)' + colLine, 8, canvas.height - 8);
|
||||
}
|
||||
|
||||
function editorFroggerTick() {
|
||||
@@ -2077,6 +2116,8 @@
|
||||
characterCells: Math.max(fpSave.cw, fpSave.ch),
|
||||
characterCellsW: fpSave.cw,
|
||||
characterCellsH: fpSave.ch,
|
||||
characterCollisionW: fpSave.colW,
|
||||
characterCollisionH: fpSave.colH,
|
||||
ground, objects, blockPlayer, interactive, startGameArea, spawnArea, spawn, cellColors, showMapInGame,
|
||||
lobbySpawnMode: (() => {
|
||||
const el = document.getElementById('lobby-spawn-mode');
|
||||
@@ -2260,8 +2301,26 @@
|
||||
mapW.addEventListener('change', resize);
|
||||
mapH.addEventListener('change', resize);
|
||||
tileSizeEl.addEventListener('change', resize);
|
||||
if (characterCellsWEl) characterCellsWEl.addEventListener('input', draw);
|
||||
if (characterCellsHEl) characterCellsHEl.addEventListener('input', draw);
|
||||
if (characterCellsWEl) characterCellsWEl.addEventListener('input', () => {
|
||||
const fp = readCharacterFootprintInputs();
|
||||
applyCharacterFootprintInputs(fp.cw, fp.ch);
|
||||
draw();
|
||||
});
|
||||
if (characterCellsHEl) characterCellsHEl.addEventListener('input', () => {
|
||||
const fp = readCharacterFootprintInputs();
|
||||
applyCharacterFootprintInputs(fp.cw, fp.ch);
|
||||
draw();
|
||||
});
|
||||
if (characterCollisionWEl) characterCollisionWEl.addEventListener('input', () => {
|
||||
const fp = readCharacterFootprintInputs();
|
||||
applyCharacterFootprintInputs(fp.cw, fp.ch);
|
||||
draw();
|
||||
});
|
||||
if (characterCollisionHEl) characterCollisionHEl.addEventListener('input', () => {
|
||||
const fp = readCharacterFootprintInputs();
|
||||
applyCharacterFootprintInputs(fp.cw, fp.ch);
|
||||
draw();
|
||||
});
|
||||
|
||||
if (mapId) {
|
||||
fetch(BASE + '/api/maps/' + mapId)
|
||||
@@ -2276,6 +2335,12 @@
|
||||
cwL = Number.isFinite(cwL) ? Math.max(1, Math.min(4, Math.floor(cwL))) : leg;
|
||||
chL = Number.isFinite(chL) ? Math.max(1, Math.min(4, Math.floor(chL))) : leg;
|
||||
applyCharacterFootprintInputs(cwL, chL);
|
||||
let colWL = Number(m.characterCollisionW);
|
||||
let colHL = Number(m.characterCollisionH);
|
||||
colWL = Number.isFinite(colWL) ? Math.max(1, Math.min(cwL, Math.floor(colWL))) : cwL;
|
||||
colHL = Number.isFinite(colHL) ? Math.max(1, Math.min(chL, Math.floor(colHL))) : chL;
|
||||
if (characterCollisionWEl) characterCollisionWEl.value = String(colWL);
|
||||
if (characterCollisionHEl) characterCollisionHEl.value = String(colHL);
|
||||
ground = m.ground || [];
|
||||
objects = m.objects || [];
|
||||
ensureObjectsGroundGrids();
|
||||
|
||||
+271
-90
@@ -221,14 +221,26 @@
|
||||
};
|
||||
}
|
||||
|
||||
/** FNV-1a 32-bit — แยกดัชนี head/hair/body ได้กระจายกว่า hash*31 เดิม (กันบอท __pv_bot_N สีซ้ำ) */
|
||||
function hashStringFnv1a32(str) {
|
||||
let h = 2166136261 >>> 0;
|
||||
const s = String(str || '');
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
h ^= s.charCodeAt(i);
|
||||
h = Math.imul(h, 16777619) >>> 0;
|
||||
}
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
function playTintFromPeerId(id) {
|
||||
let h = 0;
|
||||
const s = id || '';
|
||||
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
||||
const s = String(id || 'x');
|
||||
const h0 = hashStringFnv1a32(s);
|
||||
const h1 = hashStringFnv1a32(s + '\n1hair');
|
||||
const h2 = hashStringFnv1a32(s + '\n2body');
|
||||
return {
|
||||
head: PLAY_TINT_HEAD[h % PLAY_TINT_HEAD.length],
|
||||
hair: PLAY_TINT_HAIR[(h >>> 3) % PLAY_TINT_HAIR.length],
|
||||
body: PLAY_TINT_BODY[(h >>> 7) % PLAY_TINT_BODY.length],
|
||||
head: PLAY_TINT_HEAD[h0 % PLAY_TINT_HEAD.length],
|
||||
hair: PLAY_TINT_HAIR[h1 % PLAY_TINT_HAIR.length],
|
||||
body: PLAY_TINT_BODY[h2 % PLAY_TINT_BODY.length],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -396,7 +408,15 @@
|
||||
if (diskLayers && layerName !== 'shadow' && !diskLayers.has(layerName)) continue;
|
||||
const urls = layerName === 'shadow'
|
||||
? shadowUrlCandidates(id, dir, frameIndex)
|
||||
: layerUrlCandidates(id, dir, layerName, frameIndex);
|
||||
: (layerName === 'face' && dir !== 'down'
|
||||
? (() => {
|
||||
const u = layerUrlCandidates(id, dir, layerName, frameIndex);
|
||||
layerUrlCandidates(id, 'down', layerName, frameIndex).forEach((x) => {
|
||||
if (u.indexOf(x) === -1) u.push(x);
|
||||
});
|
||||
return u;
|
||||
})()
|
||||
: layerUrlCandidates(id, dir, layerName, frameIndex));
|
||||
const r = resolvePlayLayerImage(urls);
|
||||
/* เงาโหลดช้า/404 บ่อย — อย่าให้บล็อกทั้งคอมโพส (ไม่งั้นตกไป fallback แถบแนวตั้งแทนเลเยอร์จริง) */
|
||||
if (r.status === 'pending' && layerName === 'shadow') {
|
||||
@@ -423,13 +443,14 @@
|
||||
if (playCharLayerDiscoveryPending(characterId)) return rawImg;
|
||||
|
||||
const apiFlag = playCharLayerFromApi(characterId);
|
||||
/* API บอกชัดว่าไม่มี *_layer_* → ห้ามประกอบเลเยอร์ (กันยิง URL เช่น layer_face แล้ว 404) */
|
||||
const charHasLayerFiles = apiFlag === true || (apiFlag == null && playCharAnyDirectionLayered(characterId));
|
||||
const cacheKey = [characterId, dir, frameIndex, tint.head, tint.hair, tint.body].join('|');
|
||||
/* เหมือนเดิม: มีเลเยอร์แยกไฟล์เมื่อ API บอก hasLayerFiles หรือ probe เจอ — gen สีทำบน canvas จาก *_layer_* เท่านั้น */
|
||||
const charHasLayerFiles = apiFlag === true
|
||||
|| (apiFlag == null && playCharAnyDirectionLayered(characterId));
|
||||
/* ct2: ยกเลิก composite เก่าที่เคย cache จาก fallback ย้อมทั้งสไปรต์ */
|
||||
const cacheKey = [characterId, dir, frameIndex, tint.head, tint.hair, tint.body, 'ct2'].join('|');
|
||||
const hit = playLayerCompositeCache.get(cacheKey);
|
||||
if (hit) return hit;
|
||||
|
||||
/* มีไฟล์ *_layer_* บนเซิร์ฟเวอร์ (API) หรือ probe เจอ — ลองประกอบเลเยอร์เสมอ แม้ probe ทิศนี้ยัง none */
|
||||
if (charHasLayerFiles) {
|
||||
const pack = tryComposePlayLayersFromFiles(rawImg, characterId, dir, frameIndex, tint);
|
||||
if (pack && pack.canvas) {
|
||||
@@ -440,8 +461,7 @@
|
||||
}
|
||||
|
||||
/*
|
||||
* ไม่มีไฟล์ *_layer_* บนเซิร์ฟเวอร์ (เช็คจาก API) → แสดง PNG รวมตามดีไซน์จริง
|
||||
* ไม่ใช้แถบแนวตั้ง — มันดูเหมือนสีย้อมเลเยอร์แต่ไม่ใช่ ทำให้เข้าใจผิด
|
||||
* ไม่มีไฟล์เลเยอร์ตาม API/probe — แสดง PNG รวม (ไม่ย้อมทั้งตัว: ไม่เหมือนระบบ gen สีแบบเลเยอร์เดิม)
|
||||
*/
|
||||
return rawImg;
|
||||
}
|
||||
@@ -700,6 +720,9 @@
|
||||
li.appendChild(spV);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
if (isQuizCarry() && useCyberPlayHud()) {
|
||||
wrap.classList.add('is-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function initPlayLiveQuizScoresZeros() {
|
||||
@@ -1630,7 +1653,7 @@
|
||||
direction: ['down', 'up', 'left', 'right'][Math.floor(Math.random() * 4)],
|
||||
nickname: 'บอท',
|
||||
characterId: getPlayCharacterId(),
|
||||
playTint: pickRandomPlayTint(),
|
||||
playTint: playTintFromPeerId(id),
|
||||
botTier,
|
||||
gauntletJumpTicks: 0,
|
||||
gauntletJumpVis: 0,
|
||||
@@ -2147,15 +2170,6 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
/** มีใครกำลังถือตัวเลือกอยู่หรือไม่ — ใช้ให้รูปบนกริดที่ทับโซนกลาง (hub) เปลี่ยนสถานะ */
|
||||
function quizCarryAnyPlayerOrBotHolding() {
|
||||
if (me && me.quizCarryHeld != null) return true;
|
||||
for (const o of others.values()) {
|
||||
if (o && o.quizCarryHeld != null) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** สไปรต์ทับช่องโซนกลาง (ม่วง) อย่างน้อย 1 ช่อง */
|
||||
function spriteOverlapsQuizCarryHubArea(md, sp) {
|
||||
if (!md || md.gameType !== 'quiz_carry' || !sp) return false;
|
||||
@@ -3399,10 +3413,58 @@
|
||||
return pickRandomTileInQuizCarryHub(md, o);
|
||||
}
|
||||
|
||||
/** ช่วงไม่มีข้อ / รอตัวเลือก — ให้บอทเดินเล่นในแมปเหมือน preview ทั่วไป (กันยืนนิ่งทั้งเกม) */
|
||||
function quizCarryPreviewBotLobbyWanderStep(o, w, h) {
|
||||
const now = Date.now();
|
||||
o.botIsWalking = false;
|
||||
if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) {
|
||||
const d = pickRandomPreviewBotWanderDir();
|
||||
o.botWanderDx = d[0];
|
||||
o.botWanderDy = d[1];
|
||||
}
|
||||
if (typeof o.botWanderNextTurn !== 'number') o.botWanderNextTurn = now + 600;
|
||||
if (now >= o.botWanderNextTurn) {
|
||||
o.botWanderNextTurn = now + 650 + Math.floor(Math.random() * 2200);
|
||||
if (Math.random() < 0.55) {
|
||||
const d = pickRandomPreviewBotWanderDir();
|
||||
o.botWanderDx = d[0];
|
||||
o.botWanderDy = d[1];
|
||||
}
|
||||
}
|
||||
const accX = o.botWanderDx;
|
||||
const accY = o.botWanderDy;
|
||||
if (Math.abs(accY) > Math.abs(accX)) o.direction = accY > 0 ? 'down' : 'up';
|
||||
else if (accX !== 0) o.direction = accX > 0 ? 'right' : 'left';
|
||||
const step = MOVE_SPEED;
|
||||
const nx = o.x + accX * step;
|
||||
const ny = o.y + accY * step;
|
||||
const ox = o.x;
|
||||
const oy = o.y;
|
||||
if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) {
|
||||
o.x = nx;
|
||||
o.y = ny;
|
||||
} else {
|
||||
if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) {
|
||||
o.x = nx;
|
||||
} else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) {
|
||||
o.y = ny;
|
||||
} else {
|
||||
const d = pickRandomPreviewBotWanderDir();
|
||||
o.botWanderDx = d[0];
|
||||
o.botWanderDy = d[1];
|
||||
o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600);
|
||||
}
|
||||
}
|
||||
if (!Number.isFinite(o.x)) o.x = 0.5;
|
||||
if (!Number.isFinite(o.y)) o.y = 0.5;
|
||||
clampPlayEntityFootprintToMap(o, mapData);
|
||||
if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) o.botIsWalking = true;
|
||||
}
|
||||
|
||||
function stepQuizCarryPreviewBots() {
|
||||
if (quizCarrySessionEnded) return;
|
||||
if (quizCarryPregameActive) return;
|
||||
if (!previewFillBots || !isQuizCarry() || !mapData || !quizCarryCurrent) return;
|
||||
if (!previewFillBots || !isQuizCarry() || !mapData) return;
|
||||
const w = mapData.width || 20, h = mapData.height || 15;
|
||||
const nowPf = performance.now();
|
||||
const pathfindMinGap = editorEmbedReturn ? 280 : 140;
|
||||
@@ -3421,8 +3483,11 @@
|
||||
if (!o.botPath || o.botPath.length === 0) {
|
||||
tryQuizCarryInteractionForPlayer(id, o.x, o.y, { silent: true });
|
||||
}
|
||||
/* ตอบถูกแล้ว pickNext เริ่มนับถอยหลัง → quizCarryCurrent เป็น null ในรอบ forEach เดียวกัน */
|
||||
if (!quizCarryCurrent) return;
|
||||
/* ช่วงนับถอยหลัง / เปลี่ยนข้อ — ยังให้เดินได้ */
|
||||
if (!quizCarryCurrent) {
|
||||
quizCarryPreviewBotLobbyWanderStep(o, w, h);
|
||||
return;
|
||||
}
|
||||
const tier = o.botTier || 'avg';
|
||||
const pCorrect = tier === 'sharp' ? 0.9 : tier === 'weak' ? 0.35 : 0.65;
|
||||
const rawWant = Math.random() < pCorrect ? quizCarryCurrent.correctIndex
|
||||
@@ -3430,7 +3495,10 @@
|
||||
const wantIdx = pickQuizCarryTargetOptionIndexAvoidingTaken(rawWant);
|
||||
if (!o.botPath || !o.botPath.length) {
|
||||
if (typeof o.botQuizCarryPathfindAfter !== 'number') o.botQuizCarryPathfindAfter = 0;
|
||||
if (nowPf < o.botQuizCarryPathfindAfter) return;
|
||||
if (nowPf < o.botQuizCarryPathfindAfter) {
|
||||
quizCarryPreviewBotLobbyWanderStep(o, w, h);
|
||||
return;
|
||||
}
|
||||
const jitter = (String(id).length * 31 + ((o.x | 0) ^ (o.y | 0)) * 7) % 160;
|
||||
if (quizCarryOptionsPickableNow()) {
|
||||
if (o.quizCarryHeld == null && wantIdx != null) {
|
||||
@@ -3449,6 +3517,7 @@
|
||||
o.botQuizCarryPathfindAfter = nowPf + pathfindMinGap + jitter;
|
||||
} else {
|
||||
o.botQuizCarryPathfindAfter = nowPf + 120;
|
||||
quizCarryPreviewBotLobbyWanderStep(o, w, h);
|
||||
}
|
||||
} else {
|
||||
stepPreviewBotAlongPath(o, w, h);
|
||||
@@ -4415,7 +4484,7 @@
|
||||
if (!mapData || !mapData.objects) return false;
|
||||
if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
const w = mapData.width || 20, h = mapData.height || 15;
|
||||
for (const k of quizTilesFootprintPlay(x, y)) {
|
||||
for (const k of quizTilesWallCollisionFootprintPlay(x, y)) {
|
||||
const p = k.split(',');
|
||||
const tx = +p[0], ty = +p[1];
|
||||
if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false;
|
||||
@@ -6506,7 +6575,7 @@
|
||||
gauntletObsRenderPrev = [];
|
||||
gauntletObsRenderNext = [];
|
||||
gauntletObsBlendT0 = 0;
|
||||
me.playTint = pickRandomPlayTint();
|
||||
me.playTint = playTintFromPeerId(String((myId != null && myId !== '') ? myId : (nick || 'local')));
|
||||
others.clear();
|
||||
plist.forEach(p => {
|
||||
if (p.id !== myId) {
|
||||
@@ -7233,6 +7302,20 @@
|
||||
return { cw: leg, ch: leg };
|
||||
}
|
||||
|
||||
/**
|
||||
* ขนาดช่องที่ใช้ชนกำแพง (objects=1) เท่านั้น — ชิดเท้า (ล่าง) + กลางแนวนอน · เล็กกว่า footprint = ส่วนบนไม่ติดกำแพง
|
||||
*/
|
||||
function getCharacterCollisionFootprintWH(md) {
|
||||
const { cw, ch } = getCharacterFootprintWH(md);
|
||||
let colW = Math.floor(Number(md.characterCollisionW));
|
||||
let colH = Math.floor(Number(md.characterCollisionH));
|
||||
if (!Number.isFinite(colW) || colW < 1) colW = cw;
|
||||
if (!Number.isFinite(colH) || colH < 1) colH = ch;
|
||||
colW = Math.max(1, Math.min(cw, colW));
|
||||
colH = Math.max(1, Math.min(ch, colH));
|
||||
return { cw, ch, colW, colH };
|
||||
}
|
||||
|
||||
/** กันตัวละคร footprint ล้นออกนอกแมป — เดิมใช้ w-0.01 ทำให้ cw/ch>1 ชนขอบแล้วตรรกะเดินค้าง */
|
||||
function clampPlayEntityFootprintToMap(oEnt, md) {
|
||||
if (!oEnt || !md) return;
|
||||
@@ -7295,6 +7378,25 @@
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Footprint ชนกำแพงเท่านั้น (ย่อใต้ + กลางซ้ายขวา) — hub / blockPlayer / quiz ยังใช้เต็มกรอบ */
|
||||
function quizTilesWallCollisionFootprintPlay(px, py) {
|
||||
const s = new Set();
|
||||
if (!mapData) return s;
|
||||
if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s;
|
||||
const { cw, ch, colW, colH } = getCharacterCollisionFootprintWH(mapData);
|
||||
const w = mapData.width || 20, h = mapData.height || 15;
|
||||
const minTx = Math.floor(px);
|
||||
const minTy = Math.floor(py);
|
||||
const offX = minTx + Math.floor((cw - colW) / 2);
|
||||
const offY = minTy + (ch - colH);
|
||||
for (let ty = offY; ty < offY + colH; ty++) {
|
||||
for (let tx = offX; tx < offX + colW; tx++) {
|
||||
if (tx >= 0 && ty >= 0 && tx < w && ty < h) s.add(tx + ',' + ty);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function quizAnswerTileForbiddenForLock(lock, tx, ty) {
|
||||
if (!lock || lock.eliminated) return false;
|
||||
if (!mapData) return false;
|
||||
@@ -7370,14 +7472,15 @@
|
||||
if (!mapData || !mapData.objects) return false;
|
||||
if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
const w = mapData.width || 20, h = mapData.height || 15;
|
||||
for (const k of quizTilesFootprintPlay(x, y)) {
|
||||
const wallTiles = quizTilesWallCollisionFootprintPlay(x, y);
|
||||
for (const k of wallTiles) {
|
||||
const p = k.split(',');
|
||||
const tx = +p[0], ty = +p[1];
|
||||
if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false;
|
||||
const row = mapData.objects[ty];
|
||||
if (!row || row[tx] === 1) return false;
|
||||
}
|
||||
if (quizTilesFootprintPlay(x, y).size === 0) return false;
|
||||
if (wallTiles.size === 0) return false;
|
||||
const bp = mapData.blockPlayer;
|
||||
if (bp) {
|
||||
for (const k of quizTilesFootprintPlay(x, y)) {
|
||||
@@ -7408,14 +7511,15 @@
|
||||
if (!mapData || !mapData.objects) return false;
|
||||
if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
const w = mapData.width || 20, h = mapData.height || 15;
|
||||
for (const k of quizTilesFootprintPlay(x, y)) {
|
||||
const wallTilesB = quizTilesWallCollisionFootprintPlay(x, y);
|
||||
for (const k of wallTilesB) {
|
||||
const p = k.split(',');
|
||||
const tx = +p[0], ty = +p[1];
|
||||
if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false;
|
||||
const row = mapData.objects[ty];
|
||||
if (!row || row[tx] === 1) return false;
|
||||
}
|
||||
if (quizTilesFootprintPlay(x, y).size === 0) return false;
|
||||
if (wallTilesB.size === 0) return false;
|
||||
const bp = mapData.blockPlayer;
|
||||
if (bp) {
|
||||
for (const k of quizTilesFootprintPlay(x, y)) {
|
||||
@@ -8022,7 +8126,13 @@
|
||||
}
|
||||
|
||||
function useCyberPlayHud() {
|
||||
return !!(mapData && (isJumpSurvive() || isGauntlet() || isSpaceShooter() || isBalloonBoss()));
|
||||
return !!(mapData && (isJumpSurvive() || isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry()));
|
||||
}
|
||||
|
||||
/** Cyber HUD: quiz_carry ใช้ playLiveQuizScores (ถูก +1 ตอนส่งป้ายถูกที่ฮับ) */
|
||||
function cyberHudQuizCarryPeerScore(peerId) {
|
||||
if (peerId == null) return 0;
|
||||
return Math.max(0, Number(playLiveQuizScores[peerId]) || 0);
|
||||
}
|
||||
|
||||
function syncPlayCyberHud() {
|
||||
@@ -8045,12 +8155,31 @@
|
||||
const previewEl = document.getElementById('play-cyber-preview-line');
|
||||
|
||||
if (timeSub) {
|
||||
timeSub.textContent = isGauntlet() ? 'GAUNTLET · SURVIVAL RUN'
|
||||
: (isSpaceShooter() ? 'SPACE SHOOTER · ARCADE'
|
||||
: (isBalloonBoss() ? 'BALLOON BOSS · MEGA VIRUS' : 'JUMP SURVIVE · NODE UPLINK'));
|
||||
timeSub.textContent = isQuizCarry()
|
||||
? 'QUIZ CARRY · COURT — หยิบป้ายถูกแล้วส่งที่ฮับ (F / Grab)'
|
||||
: (isGauntlet() ? 'GAUNTLET · SURVIVAL RUN'
|
||||
: (isSpaceShooter() ? 'SPACE SHOOTER · ARCADE'
|
||||
: (isBalloonBoss() ? 'BALLOON BOSS · MEGA VIRUS' : 'JUMP SURVIVE · NODE UPLINK')));
|
||||
}
|
||||
if (timeVal) {
|
||||
if (isGauntlet()) {
|
||||
if (isQuizCarry()) {
|
||||
if (quizCarrySessionEnded) {
|
||||
timeVal.textContent = '0';
|
||||
} else if (previewMode && editorEmbedReturn && quizCarryEmbedCountdownEndAt > Date.now()) {
|
||||
const rem = Math.max(0, Math.ceil((quizCarryEmbedCountdownEndAt - Date.now()) / 1000));
|
||||
timeVal.textContent = String(rem);
|
||||
} else if (quizCarryPregameActive) {
|
||||
timeVal.textContent = '···';
|
||||
} else if (!quizCarryCurrent) {
|
||||
timeVal.textContent = '—';
|
||||
} else if (quizCarryOptionRevealAt > 0 && Date.now() < quizCarryOptionRevealAt) {
|
||||
timeVal.textContent = String(Math.max(0, Math.ceil((quizCarryOptionRevealAt - Date.now()) / 1000)));
|
||||
} else if (quizCarryAnswerCloseAt > Date.now()) {
|
||||
timeVal.textContent = String(Math.max(0, Math.ceil((quizCarryAnswerCloseAt - Date.now()) / 1000)));
|
||||
} else {
|
||||
timeVal.textContent = '0';
|
||||
}
|
||||
} else if (isGauntlet()) {
|
||||
if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) {
|
||||
const rem = Math.max(0, Math.ceil((gauntletEndsAtMs - Date.now()) / 1000));
|
||||
const mm = Math.floor(rem / 60);
|
||||
@@ -8090,7 +8219,9 @@
|
||||
if (fb && fb.src) portrait.src = fb.src;
|
||||
}
|
||||
if (statusEl) {
|
||||
if (isJumpSurvive() && jumpSurviveEliminated) {
|
||||
if (isQuizCarry() && quizCarrySessionEnded) {
|
||||
statusEl.textContent = 'SESSION COMPLETE · จบชุดคำถาม';
|
||||
} else if (isJumpSurvive() && jumpSurviveEliminated) {
|
||||
statusEl.textContent = 'SPECTATOR · LINK SEVERED';
|
||||
} else if (isBalloonBoss() && me.balloonBossEliminated) {
|
||||
statusEl.textContent = 'SPECTATOR · BALLOONS GONE';
|
||||
@@ -8099,7 +8230,11 @@
|
||||
}
|
||||
}
|
||||
if (hintEl) {
|
||||
if (isJumpSurvive()) {
|
||||
if (isQuizCarry()) {
|
||||
hintEl.textContent = quizCarrySessionEnded
|
||||
? 'ชุดคำถามจบแล้ว · Session ended — ดูคะแนนด้าน SCORE'
|
||||
: 'ยืนโซนตัวเลือกแล้วกด F/Grab หยิบ · ถือป้ายถูกแล้วไปฮับส่ง — ถูก +1 แต้ม (correct answer +1 on hub)';
|
||||
} else if (isJumpSurvive()) {
|
||||
hintEl.textContent = jumpSurviveEliminated
|
||||
? 'Spectate remaining nodes · Exit room to reset session'
|
||||
: 'Space / W / ↑ jump · A D / arrows move · Hit ceiling/floor = out (watch others)';
|
||||
@@ -8137,9 +8272,10 @@
|
||||
id: myId,
|
||||
nickname: me.nickname || nick,
|
||||
characterId: me.characterId || getPlayCharacterId(),
|
||||
score: isGauntlet() ? (me.gauntletScore || 0)
|
||||
: (isSpaceShooter() ? (me.spaceShooterScore || 0)
|
||||
: (isBalloonBoss() ? (me.balloonBossScore || 0) : 0)),
|
||||
score: isQuizCarry() ? cyberHudQuizCarryPeerScore(myId)
|
||||
: (isGauntlet() ? (me.gauntletScore || 0)
|
||||
: (isSpaceShooter() ? (me.spaceShooterScore || 0)
|
||||
: (isBalloonBoss() ? (me.balloonBossScore || 0) : 0))),
|
||||
y: safeYv(me.y),
|
||||
eliminated: !!(isJumpSurvive() && jumpSurviveEliminated) || !!(isBalloonBoss() && me.balloonBossEliminated),
|
||||
isMe: true,
|
||||
@@ -8149,9 +8285,10 @@
|
||||
id,
|
||||
nickname: o.nickname || id.slice(0, 8),
|
||||
characterId: o.characterId,
|
||||
score: isGauntlet() ? (o.gauntletScore || 0)
|
||||
: (isSpaceShooter() ? (o.spaceShooterScore || 0)
|
||||
: (isBalloonBoss() ? (o.balloonBossScore || 0) : 0)),
|
||||
score: isQuizCarry() ? cyberHudQuizCarryPeerScore(id)
|
||||
: (isGauntlet() ? (o.gauntletScore || 0)
|
||||
: (isSpaceShooter() ? (o.spaceShooterScore || 0)
|
||||
: (isBalloonBoss() ? (o.balloonBossScore || 0) : 0))),
|
||||
y: safeYv(o.y),
|
||||
eliminated: !!(isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated)
|
||||
|| !!(isBalloonBoss() && o.balloonBossEliminated),
|
||||
@@ -8159,7 +8296,7 @@
|
||||
});
|
||||
});
|
||||
let leaderId = null;
|
||||
if (isGauntlet() || isSpaceShooter() || isBalloonBoss()) {
|
||||
if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry()) {
|
||||
let mx = -1;
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
if (rows[i].score > mx) {
|
||||
@@ -8177,7 +8314,7 @@
|
||||
}
|
||||
}
|
||||
rows.sort((a, b) => {
|
||||
if (isGauntlet() || isSpaceShooter() || isBalloonBoss()) return b.score - a.score;
|
||||
if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry()) return b.score - a.score;
|
||||
if (a.eliminated !== b.eliminated) return a.eliminated ? 1 : -1;
|
||||
return a.y - b.y;
|
||||
});
|
||||
@@ -8212,19 +8349,21 @@
|
||||
name.appendChild(document.createTextNode(String(row.nickname || '').slice(0, 14)));
|
||||
const st = document.createElement('span');
|
||||
st.className = 'play-cyber-score-state';
|
||||
st.textContent = row.eliminated
|
||||
? (isBalloonBoss() ? 'OUT' : 'OFFLINE')
|
||||
: (isBalloonBoss()
|
||||
? ('♥' + Math.max(0, row.isMe ? (me.balloonBossBalloons | 0) : ((() => {
|
||||
const ox = others.get(row.id);
|
||||
return ox ? (ox.balloonBossBalloons | 0) : 0;
|
||||
})())))
|
||||
: 'LINK_OK');
|
||||
st.textContent = isQuizCarry()
|
||||
? ''
|
||||
: (row.eliminated
|
||||
? (isBalloonBoss() ? 'OUT' : 'OFFLINE')
|
||||
: (isBalloonBoss()
|
||||
? ('♥' + Math.max(0, row.isMe ? (me.balloonBossBalloons | 0) : ((() => {
|
||||
const ox = others.get(row.id);
|
||||
return ox ? (ox.balloonBossBalloons | 0) : 0;
|
||||
})())))
|
||||
: 'LINK_OK'));
|
||||
mid.appendChild(name);
|
||||
mid.appendChild(st);
|
||||
const sc = document.createElement('span');
|
||||
sc.className = 'play-cyber-score-val';
|
||||
if (isGauntlet() || isSpaceShooter() || isBalloonBoss()) {
|
||||
if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry()) {
|
||||
sc.textContent = String(row.score);
|
||||
} else {
|
||||
sc.textContent = row.eliminated ? '—' : String(r + 1);
|
||||
@@ -8269,10 +8408,28 @@
|
||||
const timeMs = Date.now();
|
||||
|
||||
if (mapBackgroundImg && mapBackgroundImg.complete && mapBackgroundImg.naturalWidth) {
|
||||
ctx.drawImage(mapBackgroundImg,
|
||||
(worldMinX / mapWpx) * mapBackgroundImg.naturalWidth, (worldMinY / mapHpx) * mapBackgroundImg.naturalHeight,
|
||||
(visibleW / mapWpx) * mapBackgroundImg.naturalWidth, (visibleH / mapHpx) * mapBackgroundImg.naturalHeight,
|
||||
0, 0, canvas.width, canvas.height);
|
||||
/* กล้องเห็นพื้นที่นอก [0,map] ได้ — ห้ามสุ่มต้นทาง drawImage เกินขอบภาพ (จะเกิดซ้ำ/เส้นตัด/ซ้อนกันที่ขอบจอ) */
|
||||
ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
const natW = mapBackgroundImg.naturalWidth;
|
||||
const natH = mapBackgroundImg.naturalHeight;
|
||||
const bgMinX = Math.max(0, worldMinX);
|
||||
const bgMaxX = Math.min(mapWpx, worldMaxX);
|
||||
const bgMinY = Math.max(0, worldMinY);
|
||||
const bgMaxY = Math.min(mapHpx, worldMaxY);
|
||||
if (bgMaxX > bgMinX && bgMaxY > bgMinY) {
|
||||
const srcX = (bgMinX / mapWpx) * natW;
|
||||
const srcY = (bgMinY / mapHpx) * natH;
|
||||
const srcW = ((bgMaxX - bgMinX) / mapWpx) * natW;
|
||||
const srcH = ((bgMaxY - bgMinY) / mapHpx) * natH;
|
||||
const destX = (bgMinX - camX) * zDraw + canvas.width / 2;
|
||||
const destY = (bgMinY - camY) * zDraw + canvas.height / 2;
|
||||
const destW = (bgMaxX - bgMinX) * zDraw;
|
||||
const destH = (bgMaxY - bgMinY) * zDraw;
|
||||
if (srcW > 0.25 && srcH > 0.25 && destW > 0.25 && destH > 0.25) {
|
||||
ctx.drawImage(mapBackgroundImg, srcX, srcY, srcW, srcH, destX, destY, destW, destH);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
@@ -8334,10 +8491,10 @@
|
||||
if (isQuizCarry()) {
|
||||
const domOpt = quizCarryOptionIndexForGridSprite(mapData, sp);
|
||||
const onHub = spriteOverlapsQuizCarryHubArea(mapData, sp);
|
||||
const anyHeld = quizCarryAnyPlayerOrBotHolding();
|
||||
const meHolding = me && me.quizCarryHeld != null;
|
||||
let carryVisualActive = false;
|
||||
if (domOpt != null && quizCarryOptionHeldByAnyone(domOpt)) carryVisualActive = true;
|
||||
else if (domOpt == null && onHub && anyHeld) carryVisualActive = true;
|
||||
else if (domOpt == null && onHub && meHolding) carryVisualActive = true;
|
||||
if (carryVisualActive) {
|
||||
const gh = mapGridImageHeldImgs[sp.i];
|
||||
if (gh && gh.complete && gh.naturalWidth) gim = gh;
|
||||
@@ -8817,36 +8974,60 @@
|
||||
for (let i = 0; i < (id || '').length; i++) h = (h * 31 + (id || '').charCodeAt(i)) >>> 0;
|
||||
return { ax: ((h % 5) - 2) * 0.1, ay: ((Math.floor(h / 5) % 5) - 2) * 0.1 };
|
||||
}
|
||||
const othersSorted = [...others.entries()].sort((a, b) => {
|
||||
const oa = a[1], ob = b[1];
|
||||
const ya = safeY(oa.y), yb = safeY(ob.y);
|
||||
if (Math.abs(ya - yb) > 0.01) return ya - yb;
|
||||
return safeX(oa.x) - safeX(ob.x);
|
||||
});
|
||||
const { ch: sortCh } = getCharacterFootprintWH(mapData);
|
||||
/** ใช้แถวกริด (floor y) เป็นหลัก — ลดการสลับลำดับวาดทุกเฟรมเมื่อ y เป็น float เลอร์ป */
|
||||
function avatarDrawDepth(ent) {
|
||||
return Math.floor(safeY(ent.y)) + sortCh;
|
||||
}
|
||||
if (!isStack() && !isSpaceShooter() && !isBalloonBoss()) {
|
||||
othersSorted.forEach(([id, o]) => {
|
||||
const off = isGauntlet() ? { ax: 0, ay: 0 } : peerVisualOffset(id);
|
||||
const otherWalk = isGauntlet()
|
||||
? ((o.gauntletJumpTicks || 0) > 0 || (o.gauntletJumpVis || 0) > 0.08)
|
||||
: (isPreviewBotId(id)
|
||||
? !!o.botIsWalking
|
||||
: !!((o.tx != null && Math.abs((o.tx || o.x) - o.x) > 0.02) || (o.ty != null && Math.abs((o.ty || o.y) - o.y) > 0.02)));
|
||||
const ot = o.playTint || playTintFromPeerId(id);
|
||||
const botOut = isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated;
|
||||
const peerName = botOut ? (o.nickname + ' (ตกรอบ)') : o.nickname;
|
||||
if (botOut) ctx.save();
|
||||
if (botOut) ctx.globalAlpha = 0.4;
|
||||
drawAvatar(safeX(o.x) + off.ax, safeY(o.y) + off.ay, false, peerName, o.characterId, o.direction, otherWalk, ot, (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks) || 0, o.gauntletScore || 0, quizCarrySignForEntity(o));
|
||||
if (botOut) ctx.restore();
|
||||
});
|
||||
const mt = me.playTint || pickRandomPlayTint();
|
||||
if (!me.playTint) me.playTint = mt;
|
||||
let mt = me.playTint;
|
||||
if (!mt || typeof mt.head !== 'string' || typeof mt.hair !== 'string' || typeof mt.body !== 'string') {
|
||||
mt = playTintFromPeerId(String((myId != null && myId !== '') ? myId : (nick || 'local')));
|
||||
me.playTint = mt;
|
||||
}
|
||||
const meTag = isFrogger() ? (' (กบ) ' + froggerScore) : (isGauntlet() && meGauntletJumpTicks > 0 ? ' (กระโดด)' : (isJumpSurvive() && jumpSurviveEliminated ? ' (ตกรอบ)' : ' (คุณ)'));
|
||||
const meWalking = isFrogger() ? false : (isGauntlet() ? (meGauntletJumpTicks > 0 || meGauntletJumpVis > 0.08) : !!me.isWalking);
|
||||
if (isJumpSurvive() && jumpSurviveEliminated) ctx.save();
|
||||
if (isJumpSurvive() && jumpSurviveEliminated) ctx.globalAlpha = 0.4;
|
||||
drawAvatar(safeX(me.x), safeY(me.y), true, me.nickname + meTag, me.characterId, me.direction, meWalking, mt, meGauntletJumpVis, me.gauntletScore || 0, quizCarrySignForEntity(me));
|
||||
if (isJumpSurvive() && jumpSurviveEliminated) ctx.restore();
|
||||
const drawRows = [];
|
||||
others.forEach((o, id) => {
|
||||
if (!o) return;
|
||||
drawRows.push({ isMe: false, id, o });
|
||||
});
|
||||
drawRows.push({ isMe: true, id: '__me', o: me });
|
||||
drawRows.sort((a, b) => {
|
||||
const da = avatarDrawDepth(a.o);
|
||||
const db = avatarDrawDepth(b.o);
|
||||
if (da !== db) return da - db;
|
||||
const xa = Math.floor(safeX(a.o.x) * 4);
|
||||
const xb = Math.floor(safeX(b.o.x) * 4);
|
||||
if (xa !== xb) return xa - xb;
|
||||
const sa = a.isMe ? '\uFFFFme' : String(a.id);
|
||||
const sb = b.isMe ? '\uFFFFme' : String(b.id);
|
||||
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
||||
});
|
||||
drawRows.forEach((row) => {
|
||||
if (!row.isMe) {
|
||||
const id = row.id;
|
||||
const o = row.o;
|
||||
const off = isGauntlet() ? { ax: 0, ay: 0 } : peerVisualOffset(id);
|
||||
const otherWalk = isGauntlet()
|
||||
? ((o.gauntletJumpTicks || 0) > 0 || (o.gauntletJumpVis || 0) > 0.08)
|
||||
: (isPreviewBotId(id)
|
||||
? !!o.botIsWalking
|
||||
: !!((o.tx != null && Math.abs((o.tx || o.x) - o.x) > 0.02) || (o.ty != null && Math.abs((o.ty || o.y) - o.y) > 0.02)));
|
||||
const ot = o.playTint || playTintFromPeerId(id);
|
||||
const botOut = isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated;
|
||||
const peerName = botOut ? (o.nickname + ' (ตกรอบ)') : o.nickname;
|
||||
if (botOut) ctx.save();
|
||||
if (botOut) ctx.globalAlpha = 0.4;
|
||||
drawAvatar(safeX(o.x) + off.ax, safeY(o.y) + off.ay, false, peerName, o.characterId, o.direction, otherWalk, ot, (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks) || 0, o.gauntletScore || 0, quizCarrySignForEntity(o));
|
||||
if (botOut) ctx.restore();
|
||||
} else {
|
||||
if (isJumpSurvive() && jumpSurviveEliminated) ctx.save();
|
||||
if (isJumpSurvive() && jumpSurviveEliminated) ctx.globalAlpha = 0.4;
|
||||
drawAvatar(safeX(me.x), safeY(me.y), true, me.nickname + meTag, me.characterId, me.direction, meWalking, mt, meGauntletJumpVis, me.gauntletScore || 0, quizCarrySignForEntity(me));
|
||||
if (isJumpSurvive() && jumpSurviveEliminated) ctx.restore();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (isFrogger()) {
|
||||
ctx.fillStyle = '#7aa2f7';
|
||||
|
||||
@@ -980,7 +980,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.135"></script>
|
||||
<script src="js/play.js?v=0.145"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+29
-1
@@ -821,6 +821,34 @@ function getCharacterFootprintWHForMove(md) {
|
||||
return { cw: leg, ch: leg };
|
||||
}
|
||||
|
||||
function getCharacterCollisionFootprintWHForMove(md) {
|
||||
const { cw, ch } = getCharacterFootprintWHForMove(md);
|
||||
let colW = Math.floor(Number(md.characterCollisionW));
|
||||
let colH = Math.floor(Number(md.characterCollisionH));
|
||||
if (!Number.isFinite(colW) || colW < 1) colW = cw;
|
||||
if (!Number.isFinite(colH) || colH < 1) colH = ch;
|
||||
colW = Math.max(1, Math.min(cw, colW));
|
||||
colH = Math.max(1, Math.min(ch, colH));
|
||||
return { cw, ch, colW, colH };
|
||||
}
|
||||
|
||||
function serverWallCollisionTileKeys(md, px, py) {
|
||||
const w = md.width || 20;
|
||||
const h = md.height || 15;
|
||||
const { cw, ch, colW, colH } = getCharacterCollisionFootprintWHForMove(md);
|
||||
const minTx = Math.floor(Number(px));
|
||||
const minTy = Math.floor(Number(py));
|
||||
const offX = minTx + Math.floor((cw - colW) / 2);
|
||||
const offY = minTy + (ch - colH);
|
||||
const out = [];
|
||||
for (let ty = offY; ty < offY + colH; ty++) {
|
||||
for (let tx = offX; tx < offX + colW; tx++) {
|
||||
if (tx >= 0 && ty >= 0 && tx < w && ty < h) out.push(`${tx},${ty}`);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function quizCarryFootprintTileKeys(md, px, py) {
|
||||
const w = md.width || 20;
|
||||
const h = md.height || 15;
|
||||
@@ -1221,7 +1249,7 @@ function quizBattleFootprintFullyOnPathServer(m, px, py) {
|
||||
|
||||
function serverFootprintClearOfWalls(md, px, py) {
|
||||
const w = md.width || 20, h = md.height || 15;
|
||||
for (const k of quizCarryFootprintTileKeys(md, px, py)) {
|
||||
for (const k of serverWallCollisionTileKeys(md, px, py)) {
|
||||
const [tx, ty] = k.split(',').map(Number);
|
||||
if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false;
|
||||
const row = md.objects && md.objects[ty];
|
||||
|
||||
Reference in New Issue
Block a user