minigame 4 more rule

This commit is contained in:
2026-04-29 10:33:56 +00:00
parent 0975874262
commit a383c01288
8 changed files with 451 additions and 177 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+75 -77
View File
@@ -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
}
+3 -1
View File
@@ -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>
+70 -5
View File
@@ -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
View File
@@ -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';
+1 -1
View File
@@ -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
View File
@@ -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];