feat(stack): per-seat block sprites (normal/heavy) in game-timing + admin

Thai: Minigame-3 ตั้งรูปบล็อก P1–P6 ปกติ+ใหญ่, เปอร์เซ็นต์สุ่มใหญ่, เกมวาดสไปรต์ + fallback สี
English: stackBlockNormalImageUrls, stackBlockHeavyImageUrls, stackHeavyBlockPercent; play draws per layer/drop seat.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-03 05:39:40 +00:00
parent 6070a61432
commit d7f7f0bc0a
6 changed files with 288 additions and 11 deletions
+91 -1
View File
@@ -2701,6 +2701,87 @@
var pb = Number(data.stackTowerProgressBlocks);
el('stack-tower-progress-blocks').value = String(Number.isFinite(pb) ? Math.floor(Math.max(1, Math.min(500, pb))) : 50);
}
if (el('stack-heavy-block-percent')) {
var shp = Number(data.stackHeavyBlockPercent);
el('stack-heavy-block-percent').value = String(Number.isFinite(shp) ? Math.max(0, Math.min(100, Math.floor(shp))) : 35);
}
var normArr = Array.isArray(data.stackBlockNormalImageUrls) ? data.stackBlockNormalImageUrls : [];
var heavyArr = Array.isArray(data.stackBlockHeavyImageUrls) ? data.stackBlockHeavyImageUrls : [];
for (var si = 1; si <= 6; si++) {
var nin = el('stack-block-normal-url-' + si);
var hin = el('stack-block-heavy-url-' + si);
if (nin) nin.value = normArr[si - 1] != null ? String(normArr[si - 1]) : '';
if (hin) hin.value = heavyArr[si - 1] != null ? String(heavyArr[si - 1]) : '';
updateStackBlockSeatPreview(si);
}
}
function readStackBlockNormalUrlsFromForm() {
var out = [];
for (var i = 1; i <= 6; i++) {
var inp = el('stack-block-normal-url-' + i);
out.push(inp && inp.value ? String(inp.value).trim() : '');
}
return out;
}
function readStackBlockHeavyUrlsFromForm() {
var out = [];
for (var j = 1; j <= 6; j++) {
var inp = el('stack-block-heavy-url-' + j);
out.push(inp && inp.value ? String(inp.value).trim() : '');
}
return out;
}
function updateStackBlockSeatPreview(seat) {
['normal', 'heavy'].forEach(function (kind) {
var inp = el('stack-block-' + kind + '-url-' + seat);
var img = el('stack-block-' + kind + '-prev-' + seat);
if (!img) return;
var v = inp && inp.value ? String(inp.value).trim() : '';
if (!v) {
img.removeAttribute('src');
img.alt = '';
return;
}
if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, '');
img.alt = (kind === 'heavy' ? 'Heavy ' : 'Normal ') + 'P' + seat;
img.src = v;
});
}
function bindStackBlockVisualInputs() {
for (var s = 1; s <= 6; s++) {
(function (seat) {
var nIn = el('stack-block-normal-url-' + seat);
var hIn = el('stack-block-heavy-url-' + seat);
if (nIn) {
nIn.addEventListener('change', function () { updateStackBlockSeatPreview(seat); });
nIn.addEventListener('blur', function () { updateStackBlockSeatPreview(seat); });
}
if (hIn) {
hIn.addEventListener('change', function () { updateStackBlockSeatPreview(seat); });
hIn.addEventListener('blur', function () { updateStackBlockSeatPreview(seat); });
}
var bn = el('btn-stack-block-normal-clear-' + seat);
if (bn) {
bn.addEventListener('click', function () {
var i = el('stack-block-normal-url-' + seat);
if (i) i.value = '';
updateStackBlockSeatPreview(seat);
});
}
var bh = el('btn-stack-block-heavy-clear-' + seat);
if (bh) {
bh.addEventListener('click', function () {
var i = el('stack-block-heavy-url-' + seat);
if (i) i.value = '';
updateStackBlockSeatPreview(seat);
});
}
})(s);
}
}
function loadStackGamePanel() {
@@ -2743,6 +2824,11 @@
setMsg('stack-game-msg', 'Progress เต็ม 100%: จำนวนชั้นต้องอยู่ระหว่าง 1–500', 'error');
return;
}
var heavyPctIn = el('stack-heavy-block-percent') ? parseInt(String(el('stack-heavy-block-percent').value), 10) : 35;
if (!Number.isFinite(heavyPctIn) || heavyPctIn < 0 || heavyPctIn > 100) {
setMsg('stack-game-msg', 'โอกาสบล็อกใหญ่ (%) ต้องอยู่ระหว่าง 0–100', 'error');
return;
}
var btn = el('btn-stack-game-save');
stackGameSaveInFlight = true;
if (btn) {
@@ -2757,6 +2843,9 @@
data.stackTowerMissionTimeSec = secIn;
data.stackTeamMissesMax = missIn;
data.stackTowerProgressBlocks = progBlocksIn;
data.stackHeavyBlockPercent = heavyPctIn;
data.stackBlockNormalImageUrls = readStackBlockNormalUrlsFromForm();
data.stackBlockHeavyImageUrls = readStackBlockHeavyUrlsFromForm();
return gameTimingFetch('PUT', data);
})
.then(function () {
@@ -2773,7 +2862,7 @@
var stf = fresh && fresh.stackTowerMissionTimeSec != null ? Number(fresh.stackTowerMissionTimeSec) : secIn;
var msf = fresh && fresh.stackTeamMissesMax != null ? Number(fresh.stackTeamMissesMax) : missIn;
var pbf = fresh && fresh.stackTowerProgressBlocks != null ? Number(fresh.stackTowerProgressBlocks) : progBlocksIn;
setMsg('stack-game-msg', 'บันทึกสำเร็จ — สวิง ' + hz + ' Hz · กว้าง ' + wLabel + ' · เวลา ' + stf + 's · พลาด ' + msf + ' ครั้ง · Progress 100% ที่ ' + pbf + ' ชั้น · โหลดยืนยันจากเซิร์ฟเวอร์แล้ว · ผู้เล่นต้องรีเฟรชหน้าเล่นหรือเข้าห้องใหม่', 'ok');
setMsg('stack-game-msg', 'บันทึกสำเร็จ — สวิง ' + hz + ' Hz · กว้าง ' + wLabel + ' · เวลา ' + stf + 's · พลาด ' + msf + ' ครั้ง · Progress 100% ที่ ' + pbf + ' ชั้น · รูปบล็อก P1–P6 + ใหญ่ ' + (fresh && fresh.stackHeavyBlockPercent != null ? fresh.stackHeavyBlockPercent : heavyPctIn) + '% · โหลดยืนยันจากเซิร์ฟเวอร์แล้ว · ผู้เล่นต้องรีเฟรชหน้าเล่นหรือเข้าห้องใหม่', 'ok');
})
.catch(function (e) {
setMsg('stack-game-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็คว่า Node รัน Game/server.js และ URL /Game/api/game-timing ไม่ 404', 'error');
@@ -3485,6 +3574,7 @@
btnSpaceShooterSave.addEventListener('click', saveSpaceShooterTimingPanel);
}
bindSpaceShooterShipUrlInputs();
bindStackBlockVisualInputs();
bindSpaceShooterDamageOverlayPanel();
bindSpaceShooterAsteroidSpritePanel();
var btnStackGameSave = el('btn-stack-game-save');
+17
View File
@@ -742,6 +742,23 @@
</div>
</fieldset>
<p class="muted" style="margin-top:0.35rem">ใช้เฉพาะโหมด Stack + แมปภารกิจ Tower · คะแนน 10/ชั้น (คอมโบ ×2) · Progress ต่อชั้น = <strong>100% ÷ จำนวนชั้นด้านบน</strong> (คอมโบ ×2)</p>
<fieldset class="quiz-timing-fieldset" style="margin-top:0.75rem">
<legend>รูปบล็อกต่อผู้เล่น (ที่นั่ง 1–6)</legend>
<p class="muted" style="margin-top:0">ช่อง <strong>P1</strong> = ผู้เล่นคน (ตาที่ 1 ในโหมดพรีวิวบอท) · <strong>P2P6</strong> = บอท/ผู้เล่นอื่นในลำดับเดียวกับ HUD · แต่ละช่องมี <strong>ปกติ</strong> + <strong>ใหญ่</strong> (PNG/WebP โปร่งพื้นหลังแนะนำ) · เกมสุ่มใช้ “ใหญ่” ตามเปอร์เซ็นต์ด้านล่าง (ถ้าไม่ใส่ URL ใหญ่ = ใช้ปกติเสมอ) · <em>English:</em> Six seats; two URLs each (normal / heavy). Heavy roll uses percent; empty heavy falls back to normal art.</p>
<div class="form-grid form-inline quiz-timing-grid" style="margin-bottom:0.5rem">
<label title="0 = ไม่สุ่มใหญ่เลย · 100 = ทุกครั้งที่มีรูปใหญ่ช่องนั้น">โอกาสบล็อก “ใหญ่” (% ต่อการปล่อย)
<input type="number" id="stack-heavy-block-percent" min="0" max="100" step="1" value="35">
</label>
</div>
<div class="space-shooter-ship-grid" role="group" aria-label="Stack block images per seat">
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 1">P1</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-1" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-normal-prev-1" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-1">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-1" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-heavy-prev-1" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-1">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 2">P2</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-2" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-normal-prev-2" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-2">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-2" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-heavy-prev-2" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-2">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 3">P3</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-3" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-normal-prev-3" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-3">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-3" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-heavy-prev-3" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-3">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 4">P4</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-4" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-normal-prev-4" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-4">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-4" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-heavy-prev-4" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-4">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 5">P5</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-5" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-normal-prev-5" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-5">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-5" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-heavy-prev-5" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-5">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 6">P6</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-6" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-normal-prev-6" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-6">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-6" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-heavy-prev-6" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-6">ล้าง</button></div>
</div>
</fieldset>
<div class="quiz-admin-actions">
<button type="button" class="btn btn-primary" id="btn-stack-game-save">บันทึก</button>
</div>
+3
View File
@@ -19,6 +19,9 @@
"stackTowerMissionTimeSec": 90,
"stackTeamMissesMax": 3,
"stackTowerProgressBlocks": 50,
"stackBlockNormalImageUrls": ["", "", "", "", "", ""],
"stackBlockHeavyImageUrls": ["", "", "", "", "", ""],
"stackHeavyBlockPercent": 35,
"jumpSurviveJumpHeightMult": 1.5,
"jumpSurviveMissionTimeSec": 0,
"spaceShooterMissionTimeSec": 0,
+121 -9
View File
@@ -147,12 +147,14 @@
}
stackPreviewTurnIndex = 0;
stackPreviewBotThinkUntil = 0;
markStackNextDropVisualDirty();
}
function advanceStackPreviewTurn() {
if (!previewFillBots || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return;
stackPreviewTurnIndex = (stackPreviewTurnIndex + 1) % STACK_PREVIEW_TURN_COUNT;
stackPreviewBotThinkUntil = 0;
markStackNextDropVisualDirty();
}
function isStackPreviewHumanTurn() {
@@ -166,6 +168,75 @@
const cur = stackPreviewTurnOrder[stackPreviewTurnIndex];
return cur && cur.seat ? cur.seat : stackPreviewTurnIndex + 1;
}
function getStackDropActorSeat() {
if (previewFillBots && stackPreviewTurnOrder && stackPreviewTurnOrder.length === STACK_PREVIEW_TURN_COUNT) {
return getStackPreviewCurrentSeat();
}
return 1;
}
function markStackNextDropVisualDirty() {
if (stackMini) stackMini.nextDropVisualDirty = true;
}
function pickStackBlockHeavyVisual(seat) {
const pct = Math.max(0, Math.min(100, Math.floor(Number(playStackHeavyBlockPercent) || 0)));
if (pct <= 0) return false;
const slot = Math.max(0, Math.min(5, (Math.floor(Number(seat)) || 1) - 1));
const hU = normalizeGauntletAssetUrlForPlay(playStackBlockHeavyUrls[slot] || '');
if (!hU) return false;
if (Math.random() * 100 >= pct) return false;
return true;
}
function ensureStackNextDropVisual() {
if (!stackMini || !stackMini.nextDropVisualDirty) return;
stackMini.pendingDropSeat = getStackDropActorSeat();
stackMini.pendingDropHeavy = pickStackBlockHeavyVisual(stackMini.pendingDropSeat);
stackMini.nextDropVisualDirty = false;
}
function normalizePlayStackSixUrlsFromTiming(arr) {
const out = ['', '', '', '', '', ''];
if (!Array.isArray(arr)) return out;
for (let i = 0; i < 6; i++) {
out[i] = normalizeGauntletAssetUrlForPlay(typeof arr[i] === 'string' ? arr[i] : '');
if (out[i]) ensureGauntletAssetImage(out[i]);
}
return out;
}
function resolveStackBlockSpriteRec(seat, heavy) {
const slot = Math.max(0, Math.min(5, (Math.floor(Number(seat)) || 1) - 1));
const tryHeavy = !!heavy;
const order = tryHeavy
? [playStackBlockHeavyUrls[slot], playStackBlockNormalUrls[slot]]
: [playStackBlockNormalUrls[slot], playStackBlockHeavyUrls[slot]];
for (let ti = 0; ti < order.length; ti++) {
const u = normalizeGauntletAssetUrlForPlay(order[ti] || '');
if (!u) continue;
return ensureGauntletAssetImage(u);
}
return null;
}
function drawStackBlockSpriteOrHue(ctx, sx, sy, drawW, drawH, seat, heavy, hueFallback, opts) {
const o = opts || {};
const rec = resolveStackBlockSpriteRec(seat, heavy);
if (rec && rec.ready && rec.img && rec.img.naturalWidth > 0) {
ctx.drawImage(rec.img, sx, sy, drawW, drawH);
if (o.missOverlay) {
ctx.fillStyle = 'rgba(247, 118, 190, 0.42)';
ctx.fillRect(sx, sy, drawW, drawH);
}
return;
}
const hue = ((hueFallback % 360) + 360) % 360;
ctx.fillStyle = o.missTint ? 'rgba(247, 118, 190, 0.88)' : ('hsla(' + hue + ', 68%, 56%, 0.9)');
ctx.fillRect(sx, sy, drawW, drawH);
}
if (!spaceId) { window.location.replace(BASE + '/lobby.html'); return; }
const socket = io({ path: BASE + '/socket.io' });
@@ -1117,6 +1188,11 @@
let playStackTeamMissesMax = 3;
/** ภารกิจ Tower: ชั้นสำเร็จกี่ชั้น (ฐาน) = Progress 100% — แต่ละชั้น +100/N %; คอมโบ ×2 ของชั้นนั้น */
let playStackTowerProgressBlocks = 50;
/** รูปบล็อก Stack ต่อที่นั่ง 16 (game-timing) — ว่าง = วาดสีเดิม */
let playStackBlockNormalUrls = ['', '', '', '', '', ''];
let playStackBlockHeavyUrls = ['', '', '', '', '', ''];
/** 0–100: โอกาสใช้สไปรต์ “ใหญ่” ต่อการปล่อย (ถ้ามี URL ใหญ่ช่องนั้น) */
let playStackHeavyBlockPercent = 35;
/** กระโดดให้รอด: ความสูงกระโดด = ทวีคูณ × ความสูงตัว (ch×tile) — จาก GET /api/game-timing */
let playJumpSurviveJumpHeightMult = 1.5;
/** จำกัดเวลารอบภารกิจกระโดดขึ้นแท่น (วินาที) — จาก GET /api/game-timing; 0 = ใช้ค่าเริ่ม 60 ในเกม */
@@ -6702,6 +6778,9 @@
lastDropAt: 0,
stackTiltRad: 0,
stackTiltVel: 0,
nextDropVisualDirty: true,
pendingDropSeat: 1,
pendingDropHeavy: false,
};
if (previewFillBots) {
me.stackPreviewHumanPts = 0;
@@ -6726,7 +6805,7 @@
* @param {object} hit
* @param {{ human?: boolean, botId?: string } | undefined} previewAttribution โหมด preview: แยกคะแนนแสดงในแถบ (กยงเปนของรวม)
*/
function applyStackHitFromDrop(hit, previewAttribution) {
function applyStackHitFromDrop(hit, previewAttribution, layerVisualMeta) {
if (!stackMini || !hit) return;
const towerLive = isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'live';
if (hit.miss) {
@@ -6749,7 +6828,9 @@
}
} else {
const W = stackMini.initialWidthTiles;
stackMini.layers.push({ w: W, cx: hit.placedCx });
const seatLay = layerVisualMeta && layerVisualMeta.seat != null ? layerVisualMeta.seat : getStackDropActorSeat();
const heavyLay = !!(layerVisualMeta && layerVisualMeta.heavy);
stackMini.layers.push({ w: W, cx: hit.placedCx, seat: seatLay, heavy: heavyLay });
stackMini.score += hit.pts;
if (towerLive && Number.isFinite(Number(hit.progressDelta)) && (hit.progressDelta || 0) > 0) {
let np = Number(stackMini.progressPct) + Number(hit.progressDelta);
@@ -6901,7 +6982,10 @@
*/
function startStackFallAnimation(hit, previewActor) {
if (!stackMini || !hit || stackFall || stackMini.settling) return false;
ensureStackNextDropVisual();
const m = stackMini;
const actorSeat = m.pendingDropSeat != null ? m.pendingDropSeat : getStackDropActorSeat();
const actorHeavy = !!m.pendingDropHeavy;
const tw = m.widthTiles * tileSize;
const swingXW = (m.topCenterX + m.swingAmp * Math.sin(m.phase)) * tileSize;
const lh = m.layerWorldH || Math.max(14, tileSize * 0.3);
@@ -6941,6 +7025,8 @@
releaseRopeAng,
releaseAttachWorldX: swingXW,
releaseAttachWorldY: swingAttachY,
actorSeat,
actorHeavy,
};
return true;
}
@@ -7026,6 +7112,7 @@
}
stackMini.settling = null;
stackMini.lastDropAt = Date.now();
markStackNextDropVisualDirty();
if (previewFillBots && (wasHuman || settleBotId)) advanceStackPreviewTurn();
}
} else if (stackFall) {
@@ -7042,6 +7129,7 @@
}
stackMini.lastDropAt = Date.now();
stackFall = null;
markStackNextDropVisualDirty();
if (previewFillBots && (wasHuman || fallBotId)) advanceStackPreviewTurn();
} else if (hit.supportRatio != null && hit.supportRatio < stableMin) {
const stripH = stackMini.blockStripH || Math.max(16, tileSize * 0.32);
@@ -7056,6 +7144,8 @@
delta: hit.delta || 0,
previewHumanDrop: wasHuman,
previewBotId: fallBotId,
actorSeat: stackFall.actorSeat,
actorHeavy: stackFall.actorHeavy,
};
stackFall = null;
} else {
@@ -7064,12 +7154,13 @@
if (wasHuman) previewAttr = { human: true };
else if (fallBotId) previewAttr = { botId: fallBotId };
}
applyStackHitFromDrop(hit, previewAttr);
applyStackHitFromDrop(hit, previewAttr, { seat: stackFall.actorSeat, heavy: stackFall.actorHeavy });
if (previewFillBots && (wasHuman || fallBotId)) {
stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId });
}
stackMini.lastDropAt = Date.now();
stackFall = null;
markStackNextDropVisualDirty();
if (previewFillBots && (wasHuman || fallBotId)) advanceStackPreviewTurn();
}
}
@@ -7103,6 +7194,7 @@
function drawStackMinigame(ctx, worldToScreen, zoom) {
if (!stackMini) return;
const m = stackMini;
ensureStackNextDropVisual();
const layerWorldH = m.layerWorldH || Math.max(14, tileSize * 0.3);
const stripH = m.blockStripH || Math.max(16, tileSize * 0.32);
const floorY = m.floorWorldY;
@@ -7134,8 +7226,9 @@
const drawW = lw * zoom;
const drawH = layerWorldH * zoom;
const hue = (i * 47) % 360;
ctx.fillStyle = 'hsla(' + hue + ', 68%, 56%, 0.9)';
ctx.fillRect(sx, sy, drawW, drawH);
const seatL = L.seat != null ? L.seat : 1;
const heavyL = !!L.heavy;
drawStackBlockSpriteOrHue(ctx, sx, sy, drawW, drawH, seatL, heavyL, hue, {});
ctx.strokeStyle = 'rgba(255,255,255,0.38)';
ctx.lineWidth = 1;
ctx.strokeRect(sx, sy, drawW, drawH);
@@ -7154,11 +7247,12 @@
const drawW = s.twWorld * zoom;
const drawH = s.stripH * zoom;
const hue = (m.layers.length * 47) % 360;
const seatS = s.actorSeat != null ? s.actorSeat : 1;
const heavyS = !!s.actorHeavy;
ctx.save();
ctx.translate(cxs, cys);
ctx.rotate(rotRad);
ctx.fillStyle = 'hsla(' + hue + ', 70%, 52%, 0.93)';
ctx.fillRect(-drawW / 2, -drawH / 2, drawW, drawH);
drawStackBlockSpriteOrHue(ctx, -drawW / 2, -drawH / 2, drawW, drawH, seatS, heavyS, hue, {});
ctx.strokeStyle = 'rgba(255, 200, 140, 0.9)';
ctx.lineWidth = 2;
ctx.strokeRect(-drawW / 2, -drawH / 2, drawW, drawH);
@@ -7201,12 +7295,16 @@
: Math.atan2(bcy - tcy, bcx - tcx);
const swingTiltRad = ropeAng - Math.PI / 2;
const hitMissTint = stackFall && stackFall.hit && stackFall.hit.miss;
ctx.fillStyle = hitMissTint ? 'rgba(247, 118, 190, 0.88)' : 'rgba(125, 207, 255, 0.95)';
const seatDraw = stackFall && stackFall.actorSeat != null
? stackFall.actorSeat
: (m.pendingDropSeat != null ? m.pendingDropSeat : getStackDropActorSeat());
const heavyDraw = stackFall ? !!stackFall.actorHeavy : !!m.pendingDropHeavy;
const hueHint = (m.layers.length * 47) % 360;
ctx.strokeStyle = hitMissTint ? 'rgba(255, 180, 210, 0.98)' : 'rgba(192, 202, 245, 0.98)';
ctx.save();
ctx.translate(bcx, bcy);
ctx.rotate(fallTiltRad + swingTiltRad);
ctx.fillRect(-twWorld * zoom / 2, 0, twWorld * zoom, drawStripPx);
drawStackBlockSpriteOrHue(ctx, -twWorld * zoom / 2, 0, twWorld * zoom, drawStripPx, seatDraw, heavyDraw, hueHint, { missTint: !!hitMissTint, missOverlay: !!hitMissTint });
ctx.lineWidth = 2;
ctx.strokeRect(-twWorld * zoom / 2, 0, twWorld * zoom, drawStripPx);
ctx.lineWidth = 1;
@@ -7313,6 +7411,20 @@
playStackTowerProgressBlocks = 50;
}
}
if (Object.prototype.hasOwnProperty.call(payload, 'stackBlockNormalImageUrls')) {
playStackBlockNormalUrls = normalizePlayStackSixUrlsFromTiming(payload.stackBlockNormalImageUrls);
}
if (Object.prototype.hasOwnProperty.call(payload, 'stackBlockHeavyImageUrls')) {
playStackBlockHeavyUrls = normalizePlayStackSixUrlsFromTiming(payload.stackBlockHeavyImageUrls);
}
if (Object.prototype.hasOwnProperty.call(payload, 'stackHeavyBlockPercent')) {
const hp = Number(payload.stackHeavyBlockPercent);
if (Number.isFinite(hp)) {
playStackHeavyBlockPercent = Math.max(0, Math.min(100, Math.floor(hp)));
} else {
playStackHeavyBlockPercent = 35;
}
}
if (Object.prototype.hasOwnProperty.call(payload, 'jumpSurviveJumpHeightMult')) {
const jm = Number(payload.jumpSurviveJumpHeightMult);
if (Number.isFinite(jm)) {
+1 -1
View File
@@ -2009,7 +2009,7 @@
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0184"></script>
<script src="js/play.js?v=0.273"></script>
<script src="js/play.js?v=0.274"></script>
<div class="version-tag">v —</div>
</body>
</html>
+55
View File
@@ -602,6 +602,10 @@ function defaultGameTiming() {
stackTeamMissesMax: 3,
/** Stack ภารกิจ Tower: จำนวนชั้นสำเร็จ (ไม่คิดคอมโบ) ที่รวมแล้วได้ Progress 100% — แต่ละชั้น +100/N %; คอมโบ ×2 ของชั้นนั้น */
stackTowerProgressBlocks: 50,
stackBlockNormalImageUrls: ['', '', '', '', '', ''],
stackBlockHeavyImageUrls: ['', '', '', '', '', ''],
/** โอกาสใช้รูป “ใหญ่” ต่อการปล่อยหนึ่งครั้ง (0–100) — ถ้าไม่มี URL ใหญ่ช่องนั้น = ใช้ปกติ */
stackHeavyBlockPercent: 35,
/** ยิงยานอวกาศ / space_shooter: จำกัดเวลารอบ (วินาที) — 0 = ไคลเอนต์ใช้ค่าเริ่ม 90; แมป spaceShooterTimeSec > 0 ทับ */
spaceShooterMissionTimeSec: 0,
spaceShooterShipImageUrls: ['', '', '', '', '', ''],
@@ -645,6 +649,31 @@ function clampStackTowerProgressBlocks(n) {
return Math.max(1, Math.min(500, Math.floor(v)));
}
/** Stack: รูปบล็อกต่อที่นั่ง 1–6 (ปกติ / ใหญ่) — ว่าง = ไคลเอนต์วาดสีเดิม */
function normalizeStackBlockSixImageUrls(val) {
let arr = val;
if (typeof arr === 'string') {
try {
const p = JSON.parse(arr);
if (Array.isArray(p)) arr = p;
} catch (e) {
arr = arr.split(/[\n\r]+/);
}
}
if (!Array.isArray(arr)) arr = [];
const out = [];
for (let i = 0; i < 6; i++) {
out.push(sanitizeGauntletAssetUrl(arr[i]));
}
return out;
}
function clampStackHeavyBlockPercent(n) {
const v = Number(n);
if (Number.isNaN(v)) return 35;
return Math.max(0, Math.min(100, Math.floor(v)));
}
function clampJumpSurviveJumpHeightMult(n) {
const v = Number(n);
if (Number.isNaN(v)) return 1.5;
@@ -849,6 +878,11 @@ function loadGameTiming() {
stackTowerProgressBlocks: Object.prototype.hasOwnProperty.call(j, 'stackTowerProgressBlocks')
? clampStackTowerProgressBlocks(j.stackTowerProgressBlocks)
: 50,
stackBlockNormalImageUrls: normalizeStackBlockSixImageUrls(j.stackBlockNormalImageUrls),
stackBlockHeavyImageUrls: normalizeStackBlockSixImageUrls(j.stackBlockHeavyImageUrls),
stackHeavyBlockPercent: Object.prototype.hasOwnProperty.call(j, 'stackHeavyBlockPercent')
? clampStackHeavyBlockPercent(j.stackHeavyBlockPercent)
: 35,
jumpSurviveJumpHeightMult: Object.prototype.hasOwnProperty.call(j, 'jumpSurviveJumpHeightMult')
? clampJumpSurviveJumpHeightMult(j.jumpSurviveJumpHeightMult)
: 1.5,
@@ -967,6 +1001,24 @@ function saveGameTimingToFile(d) {
const stackTowerProgressBlocks = Object.prototype.hasOwnProperty.call(d, 'stackTowerProgressBlocks')
? clampStackTowerProgressBlocks(d.stackTowerProgressBlocks)
: clampStackTowerProgressBlocks(prevStackProgBlocks);
const prevStackNorm = Array.isArray(prev.stackBlockNormalImageUrls)
? normalizeStackBlockSixImageUrls(prev.stackBlockNormalImageUrls)
: normalizeStackBlockSixImageUrls([]);
const prevStackHeavy = Array.isArray(prev.stackBlockHeavyImageUrls)
? normalizeStackBlockSixImageUrls(prev.stackBlockHeavyImageUrls)
: normalizeStackBlockSixImageUrls([]);
const stackBlockNormalImageUrls = Object.prototype.hasOwnProperty.call(d, 'stackBlockNormalImageUrls')
? normalizeStackBlockSixImageUrls(d.stackBlockNormalImageUrls)
: prevStackNorm;
const stackBlockHeavyImageUrls = Object.prototype.hasOwnProperty.call(d, 'stackBlockHeavyImageUrls')
? normalizeStackBlockSixImageUrls(d.stackBlockHeavyImageUrls)
: prevStackHeavy;
const prevHeavyPct = Object.prototype.hasOwnProperty.call(prev, 'stackHeavyBlockPercent')
? clampStackHeavyBlockPercent(prev.stackHeavyBlockPercent)
: 35;
const stackHeavyBlockPercent = Object.prototype.hasOwnProperty.call(d, 'stackHeavyBlockPercent')
? clampStackHeavyBlockPercent(d.stackHeavyBlockPercent)
: prevHeavyPct;
const out = {
gauntletTickMs: clampGauntletTickMs(d.gauntletTickMs),
gauntletJumpTicks: clampGauntletJumpTicks(d.gauntletJumpTicks),
@@ -977,6 +1029,9 @@ function saveGameTimingToFile(d) {
stackTowerMissionTimeSec,
stackTeamMissesMax,
stackTowerProgressBlocks,
stackBlockNormalImageUrls,
stackBlockHeavyImageUrls,
stackHeavyBlockPercent,
jumpSurviveJumpHeightMult: jumpMult,
jumpSurviveMissionTimeSec: jumpMissionSec,
spaceShooterMissionTimeSec: spaceShooterMissionSec,