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:
+91
-1
@@ -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');
|
||||
|
||||
@@ -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>P2–P6</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>
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
"stackTowerMissionTimeSec": 90,
|
||||
"stackTeamMissesMax": 3,
|
||||
"stackTowerProgressBlocks": 50,
|
||||
"stackBlockNormalImageUrls": ["", "", "", "", "", ""],
|
||||
"stackBlockHeavyImageUrls": ["", "", "", "", "", ""],
|
||||
"stackHeavyBlockPercent": 35,
|
||||
"jumpSurviveJumpHeightMult": 1.5,
|
||||
"jumpSurviveMissionTimeSec": 0,
|
||||
"spaceShooterMissionTimeSec": 0,
|
||||
|
||||
@@ -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 ต่อที่นั่ง 1–6 (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)) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user