minigame 2 just begin1.2

This commit is contained in:
2026-04-30 16:41:07 +00:00
parent 001d46f76d
commit 84a9a6efe8
10 changed files with 570 additions and 53 deletions
+108 -10
View File
@@ -1078,27 +1078,61 @@ code {
border-color: rgba(239, 68, 68, 0.25);
}
.game-timing-laser-slot--beam .game-timing-laser-preview {
.game-timing-laser-slot--beam .game-timing-laser-preview--beam {
position: relative;
flex-direction: column;
justify-content: stretch;
align-items: stretch;
min-height: clamp(200px, 34vh, 380px);
max-height: 420px;
width: 100%;
max-width: 76px;
margin-left: auto;
margin-right: auto;
padding: 0;
overflow: hidden;
}
.game-timing-laser-slot--beam .game-timing-laser-preview::after {
.game-timing-laser-slot--beam .game-timing-laser-preview--beam::after {
content: '';
position: absolute;
left: 50%;
top: 8%;
bottom: 8%;
width: 3px;
top: 3%;
bottom: 3%;
width: 4px;
transform: translateX(-50%);
border-radius: 2px;
background: linear-gradient(180deg, rgba(252, 165, 165, 0.15), rgba(239, 68, 68, 0.9), rgba(252, 165, 165, 0.15));
box-shadow: 0 0 14px rgba(239, 68, 68, 0.45);
background: linear-gradient(180deg, rgba(252, 165, 165, 0.15), rgba(239, 68, 68, 0.92), rgba(252, 165, 165, 0.15));
box-shadow: 0 0 16px rgba(239, 68, 68, 0.5);
pointer-events: none;
opacity: 0.9;
opacity: 0.92;
z-index: 1;
}
.game-timing-laser-slot--beam .game-timing-laser-preview:has(.game-timing-laser-preview-img)::after {
opacity: 0.4;
.game-timing-laser-slot--beam .game-timing-laser-preview--beam:has(.game-timing-laser-preview-img)::after {
opacity: 0.38;
}
.game-timing-laser-slot--beam .game-timing-laser-preview--beam .game-timing-laser-preview-img {
position: relative;
z-index: 0;
width: 100%;
flex: 1 1 auto;
min-height: 0;
max-height: none;
max-width: none;
object-fit: fill;
object-position: center top;
border-radius: 2px;
}
.game-timing-laser-slot--beam .game-timing-laser-preview--beam .game-timing-laser-placeholder {
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: center;
min-height: 5rem;
padding: 0.35rem;
}
@media (min-width: 700px) {
@@ -1145,6 +1179,70 @@ code {
color: #c0caf5;
}
.game-timing-laser-color-label {
display: block;
}
.game-timing-laser-color-wrap {
display: flex;
align-items: center;
gap: 0.55rem;
margin-top: 0.28rem;
}
.game-timing-laser-swatch-stack {
position: relative;
flex-shrink: 0;
width: 2.35rem;
height: 2.35rem;
}
.game-timing-laser-swatch {
display: block;
width: 100%;
height: 100%;
border-radius: 8px;
border: 1px solid #565f89;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
pointer-events: none;
}
.game-timing-laser-color-picker {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: 0;
border-radius: 8px;
cursor: pointer;
opacity: 0;
}
.game-timing-laser-color-picker::-webkit-color-swatch-wrapper {
padding: 0;
}
.game-timing-laser-color-picker::-webkit-color-swatch {
border: 0;
border-radius: 8px;
}
.game-timing-laser-swatch.is-empty {
background-color: transparent !important;
background-image:
linear-gradient(45deg, #3b4261 25%, transparent 25%, transparent 75%, #3b4261 75%, #3b4261),
linear-gradient(45deg, #3b4261 25%, #24283b 25%, #24283b 75%, #3b4261 75%, #3b4261);
background-size: 8px 8px;
background-position: 0 0, 4px 4px;
}
.game-timing-laser-color-wrap input[type='text'] {
flex: 1;
min-width: 0;
}
/* —— Gauntlet asset library (game timing) —— */
.gauntlet-asset-library {
margin: 0.75rem 0 1rem;
+157
View File
@@ -1982,6 +1982,26 @@
});
}
/** พรีวิวคอลัมน์ beam — สูงแนวตั้ง + สีเติม/กรอบตามช่อง (คล้ายในเกม) */
function styleGameTimingLaserBeamPreviewShell() {
var prev = el('game-timing-laser-line-preview');
if (!prev) return;
var fillIn = el('game-timing-laser-fill');
var strokeIn = el('game-timing-laser-stroke');
var lwIn = el('game-timing-laser-line-width');
var fill = fillIn && fillIn.value ? String(fillIn.value).trim() : '';
var stroke = strokeIn && strokeIn.value ? String(strokeIn.value).trim() : '';
var lw = lwIn ? parseInt(lwIn.value, 10) : 2;
if (Number.isNaN(lw)) lw = 2;
lw = Math.max(0, Math.min(24, lw));
prev.style.backgroundColor = fill || '';
if (lw > 0 && stroke) {
prev.style.boxShadow = 'inset 0 0 0 ' + Math.min(8, lw) + 'px ' + stroke;
} else {
prev.style.boxShadow = '';
}
}
function renderGameTimingLaserVisuals() {
var slots = [
{ inpId: 'game-timing-laser-top-url', prevId: 'game-timing-laser-top-preview' },
@@ -2009,6 +2029,141 @@
img.onerror = function () { img.classList.add('is-broken'); };
prev.appendChild(img);
});
styleGameTimingLaserBeamPreviewShell();
}
function clamp255(x) {
return Math.max(0, Math.min(255, Math.round(Number(x))));
}
function parseCssColorToRgba(str) {
str = String(str || '').trim();
if (!str) return null;
var m = str.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i);
if (m) {
var r = clamp255(m[1]);
var g = clamp255(m[2]);
var b = clamp255(m[3]);
var a =
m[4] !== undefined && m[4] !== '' && !Number.isNaN(Number(m[4]))
? Math.max(0, Math.min(1, Number(m[4])))
: 1;
return { r: r, g: g, b: b, a: a };
}
m = str.match(/^#([\da-f]{3}|[\da-f]{6})$/i);
if (m) {
var h = m[1];
var r2;
var g2;
var b2;
if (h.length === 3) {
r2 = parseInt(h[0] + h[0], 16);
g2 = parseInt(h[1] + h[1], 16);
b2 = parseInt(h[2] + h[2], 16);
} else {
r2 = parseInt(h.slice(0, 2), 16);
g2 = parseInt(h.slice(2, 4), 16);
b2 = parseInt(h.slice(4, 6), 16);
}
if (Number.isNaN(r2) || Number.isNaN(g2) || Number.isNaN(b2)) return null;
return { r: r2, g: g2, b: b2, a: 1 };
}
var probe = document.createElement('span');
probe.style.cssText = 'position:absolute;visibility:hidden;left:0;top:0;background-color:' + str;
document.body.appendChild(probe);
var bg = getComputedStyle(probe).backgroundColor;
document.body.removeChild(probe);
m = bg && bg.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/);
if (!m) return null;
r2 = clamp255(m[1]);
g2 = clamp255(m[2]);
b2 = clamp255(m[3]);
var a2 = m[4] !== undefined && !Number.isNaN(Number(m[4])) ? Math.max(0, Math.min(1, Number(m[4]))) : 1;
return { r: r2, g: g2, b: b2, a: a2 };
}
function rgbToHex6(r, g, b) {
function hx(n) {
var h = clamp255(n).toString(16);
return h.length === 1 ? '0' + h : h;
}
return '#' + hx(r) + hx(g) + hx(b);
}
function laserHexFromTextOrFallback(text, fallbackHex) {
var c = parseCssColorToRgba(text);
if (!c) return fallbackHex;
return rgbToHex6(c.r, c.g, c.b);
}
function formatLaserRgba(r, g, b, a) {
var aa = Math.round(Math.max(0, Math.min(1, a)) * 100) / 100;
return 'rgba(' + clamp255(r) + ',' + clamp255(g) + ',' + clamp255(b) + ',' + aa + ')';
}
function syncGameTimingLaserColorSwatches() {
function apply(swatchId, textVal) {
var sw = el(swatchId);
if (!sw) return;
var v = textVal != null ? String(textVal).trim() : '';
sw.style.backgroundColor = v || '';
sw.classList.toggle('is-empty', !v);
sw.title = v || 'ว่าง — พิมพ์ค่า CSS หรือกดสี่เหลี่ยมเพื่อเลือกสี';
}
var fillIn = el('game-timing-laser-fill');
var strokeIn = el('game-timing-laser-stroke');
apply('game-timing-laser-fill-swatch', fillIn && fillIn.value);
apply('game-timing-laser-stroke-swatch', strokeIn && strokeIn.value);
var fillPk = el('game-timing-laser-fill-picker');
var strokePk = el('game-timing-laser-stroke-picker');
if (fillPk && fillIn) {
var hf = laserHexFromTextOrFallback(fillIn.value, '#ef4444');
if (fillPk.value.toLowerCase() !== hf.toLowerCase()) fillPk.value = hf;
}
if (strokePk && strokeIn) {
var hs = laserHexFromTextOrFallback(strokeIn.value, '#fca5a5');
if (strokePk.value.toLowerCase() !== hs.toLowerCase()) strokePk.value = hs;
}
styleGameTimingLaserBeamPreviewShell();
}
function bindGameTimingLaserColorSwatches() {
var fillIn = el('game-timing-laser-fill');
var strokeIn = el('game-timing-laser-stroke');
if (fillIn) {
fillIn.addEventListener('input', syncGameTimingLaserColorSwatches);
fillIn.addEventListener('change', syncGameTimingLaserColorSwatches);
}
if (strokeIn) {
strokeIn.addEventListener('input', syncGameTimingLaserColorSwatches);
strokeIn.addEventListener('change', syncGameTimingLaserColorSwatches);
}
function wirePicker(pickerId, textEl, defaultAlpha) {
var pk = el(pickerId);
var tx = textEl;
if (!pk || !tx) return;
function applyHex() {
var hex = String(pk.value || '').trim();
if (!/^#[\da-f]{6}$/i.test(hex)) return;
var r = parseInt(hex.slice(1, 3), 16);
var g = parseInt(hex.slice(3, 5), 16);
var b = parseInt(hex.slice(5, 7), 16);
var prev = parseCssColorToRgba(tx.value);
var a = prev && typeof prev.a === 'number' ? prev.a : defaultAlpha;
tx.value = formatLaserRgba(r, g, b, a);
syncGameTimingLaserColorSwatches();
}
pk.addEventListener('input', applyHex);
pk.addEventListener('change', applyHex);
}
wirePicker('game-timing-laser-fill-picker', fillIn, 0.35);
wirePicker('game-timing-laser-stroke-picker', strokeIn, 0.95);
var lwIn = el('game-timing-laser-line-width');
if (lwIn) {
lwIn.addEventListener('input', styleGameTimingLaserBeamPreviewShell);
lwIn.addEventListener('change', styleGameTimingLaserBeamPreviewShell);
}
syncGameTimingLaserColorSwatches();
}
function bindGameTimingAssignedVisuals() {
@@ -2445,6 +2600,7 @@
if (el('game-timing-laser-line-width')) el('game-timing-laser-line-width').value = String(Number.isFinite(lw) ? lw : 2);
renderGameTimingLaneVisuals();
renderGameTimingLaserVisuals();
syncGameTimingLaserColorSwatches();
applyGameTimingDataToStackInputs(data);
setMsg('game-timing-msg', '', '');
return refreshGauntletAssetLibrary();
@@ -2691,6 +2847,7 @@
}
bindGauntletAssetLibrary();
bindGameTimingAssignedVisuals();
bindGameTimingLaserColorSwatches();
var btnQuizAdd = el('btn-quiz-admin-add');
var btnQuizSave = el('btn-quiz-admin-save');
+21 -5
View File
@@ -9,7 +9,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="admin.css?v=18">
<link rel="stylesheet" href="admin.css?v=21">
</head>
<body>
<a class="skip-link" href="#admin-main">ข้ามไปเนื้อหา</a>
@@ -513,7 +513,7 @@
</div>
<div class="game-timing-laser-slot game-timing-laser-slot--beam">
<div class="game-timing-laser-slot-label">เส้นกลาง (tile) · Beam</div>
<div id="game-timing-laser-line-preview" class="game-timing-laser-preview" aria-label="ตัวอย่างเส้น laser"></div>
<div id="game-timing-laser-line-preview" class="game-timing-laser-preview game-timing-laser-preview--beam" aria-label="ตัวอย่างเส้น laser"></div>
<input type="hidden" id="game-timing-laser-line-url" value="">
<div class="game-timing-laser-slot-actions">
<button type="button" class="btn btn-ghost btn-sm" id="btn-game-timing-laser-line-view">ดู</button>
@@ -532,8 +532,24 @@
</div>
</div>
<div class="form-grid form-inline quiz-timing-grid game-timing-laser-colors-row gauntlet-editor-laser-colors">
<label>สีเติมคอลัมน์ laser (CSS) <input type="text" id="game-timing-laser-fill" maxlength="100" placeholder="rgba(239,68,68,0.35)" spellcheck="false"></label>
<label>สีเส้นกรอบ laser <input type="text" id="game-timing-laser-stroke" maxlength="100" placeholder="rgba(252,165,165,0.95)" spellcheck="false"></label>
<label class="game-timing-laser-color-label">สีเติมคอลัมน์ laser (CSS)
<div class="game-timing-laser-color-wrap">
<div class="game-timing-laser-swatch-stack">
<span id="game-timing-laser-fill-swatch" class="game-timing-laser-swatch" role="img" aria-hidden="true"></span>
<input type="color" id="game-timing-laser-fill-picker" class="game-timing-laser-color-picker" value="#ef4444" title="เลือกสี (RGB) — ค่าโปร่งใสยังแก้ในช่องข้อความ" aria-label="เปิดตัวเลือกสีเติม laser">
</div>
<input type="text" id="game-timing-laser-fill" maxlength="100" placeholder="rgba(239,68,68,0.35)" spellcheck="false">
</div>
</label>
<label class="game-timing-laser-color-label">สีเส้นกรอบ laser
<div class="game-timing-laser-color-wrap">
<div class="game-timing-laser-swatch-stack">
<span id="game-timing-laser-stroke-swatch" class="game-timing-laser-swatch" role="img" aria-hidden="true"></span>
<input type="color" id="game-timing-laser-stroke-picker" class="game-timing-laser-color-picker" value="#fca5a5" title="เลือกสี (RGB) — ค่าโปร่งใสยังแก้ในช่องข้อความ" aria-label="เปิดตัวเลือกสีเส้นกรอบ laser">
</div>
<input type="text" id="game-timing-laser-stroke" maxlength="100" placeholder="rgba(252,165,165,0.95)" spellcheck="false">
</div>
</label>
<label>ความหนาเส้น (px, 0 = ไม่กรอบ) <input type="number" id="game-timing-laser-line-width" min="0" max="24" step="1" value="2"></label>
</div>
</div>
@@ -670,6 +686,6 @@
</div>
</main>
</div>
<script src="admin.js?v=51"></script>
<script src="admin.js?v=54"></script>
</body>
</html>
+9 -4
View File
@@ -2,12 +2,17 @@
"gauntletTickMs": 150,
"gauntletJumpTicks": 5,
"gauntletTimeLimitSec": 60,
"gauntletLaneImageUrls": [],
"gauntletLaneImageUrls": [
"/Game/img/gauntlet-assets/gauntlet-6aa244ab734b12e4.png",
"/Game/img/gauntlet-assets/gauntlet-c4d4482e2a60eac9.png",
"/Game/img/gauntlet-assets/gauntlet-67a585790f67fe67.png",
"/Game/img/gauntlet-assets/gauntlet-077c293962b8fe07.png"
],
"gauntletLaserTopUrl": "",
"gauntletLaserBottomUrl": "",
"gauntletLaserLineUrl": "",
"gauntletLaserFillColor": "rgba(140,230,255,0.42)",
"gauntletLaserStrokeColor": "rgba(255,220,255,0.9)",
"gauntletLaserLineUrl": "/Game/img/gauntlet-assets/gauntlet-2a75a958cd5e5e8a.png",
"gauntletLaserFillColor": "rgba(255,204,204,0.1)",
"gauntletLaserStrokeColor": "rgba(255,220,255,0)",
"gauntletLaserLineWidthPx": 2,
"stackSwingHz": 0.1,
"stackBlockWidthTiles": 3.2,
+4 -1
View File
@@ -105,6 +105,8 @@
</optgroup>
<optgroup label="พรมแดงสุดท้าย">
<option value="gauntletPlayerSpawn" id="draw-mode-option-gauntlet-player-spawn" hidden>ลำดับเกิด 1–6 (คลิกซ้ายตามลำดับ)</option>
<option value="gauntletLaserStart" id="draw-mode-option-gauntlet-laser-start" hidden>เลเซอร์: แถวต้น (ขอบบนของแถบ)</option>
<option value="gauntletLaserEnd" id="draw-mode-option-gauntlet-laser-end" hidden>เลเซอร์: แถวปลาย (ขอบล่างของแถบ)</option>
</optgroup>
<optgroup label="Stack">
<option value="stackRelease" id="draw-mode-option-stack-release" hidden>จุดปล่อยตึก (crane / ด้านบน)</option>
@@ -197,6 +199,7 @@
</div>
<label class="editor-toggle" title="ปิด = ไม่วาดกริด/ช่องฉาก แต่ยังแสดงรูปที่วาดบนกริด · Off = hide tile grid, grid images still show"><input type="checkbox" id="show-map-in-game" checked> โชว์กริดในเกม</label>
<button type="button" id="btn-clear-gauntlet-spawns" hidden title="ล้างรายการจุดเกิดพรมแดงที่กำหนดเอง">ล้างลำดับ 16</button>
<button type="button" id="btn-clear-gauntlet-laser-span" hidden title="ล้างช่วงแนวตั้งของเลเซอร์ — กลับไปเต็มความสูงแมป (ไม่ต้องเปลี่ยนรูปจาก Admin)">ล้างช่วง laser</button>
<label id="shooter-slot-paint-wrap" class="editor-field-grow" style="display:none;" title="ช่องผู้เล่นลำดับที่ 1–6 ตรงกับคนในเกม (คุณ = ลำดับแรกจาก join)">ช่องเกิดยาน P
<select id="shooter-slot-paint">
<option value="1">1</option>
@@ -286,7 +289,7 @@
</div>
</div>
<script src="js/version.js?v=0.0169"></script>
<script src="js/editor.js?v=20260430-editor-footprint-groups"></script>
<script src="js/editor.js?v=20260427-gauntlet-laser-span"></script>
<div class="version-tag">v —</div>
</body>
</html>
+110
View File
@@ -108,6 +108,9 @@
let jumpSurvivePlatformArea = [];
let cellColors = [];
let gauntletPlayerSpawns = [];
/** gauntlet: แถว y บน–ล่างของแถบเลเซอร์ (null = เต็มความสูงแมป) */
let gauntletLaserRowStart = null;
let gauntletLaserRowEnd = null;
/** ล็อบบี้ / ZEP / quiz_carry / ฯลฯ — P1..P6 ตามลำดับ join เมื่อเลือกโหมด slots6 */
let lobbyPlayerSpawns = [null, null, null, null, null, null];
/** space_shooter: กริด 0 หรือ 1–6 = ลำดับผู้เล่น P1–P6 */
@@ -424,6 +427,19 @@
.slice(0, 6);
}
function sanitizeGauntletLaserRowsInEditor() {
if (gauntletLaserRowStart == null || gauntletLaserRowEnd == null) return;
let y0 = Math.floor(Number(gauntletLaserRowStart));
let y1 = Math.floor(Number(gauntletLaserRowEnd));
if (!Number.isFinite(y0)) y0 = 0;
if (!Number.isFinite(y1)) y1 = height - 1;
y0 = Math.max(0, Math.min(height - 1, y0));
y1 = Math.max(0, Math.min(height - 1, y1));
if (y1 < y0) { const t = y0; y0 = y1; y1 = t; }
gauntletLaserRowStart = y0;
gauntletLaserRowEnd = y1;
}
function supportsLobbySpawnPaint(gt) {
return gt === 'zep' || gt === 'lobby' || gt === 'quiz' || gt === 'quiz_carry' || gt === 'stack' || gt === 'jump_survive' || gt === 'quiz_battle';
}
@@ -895,6 +911,7 @@
if (hint) {
hint.innerHTML = '<p class="editor-hint-lead">โหมด <strong>พรมแดงสุดท้าย</strong></p>' + editorHintBulletList([
'อุปสรรคเลนเฉพาะ<strong>แถวที่มีคน</strong> + เลเซอร์ทุกแถว · ชนแล้วถอยซ้าย',
'โหมดวาด <strong>เลเซอร์: แถวต้น / แถวปลาย</strong> — กำหนดความสูงแถบเลเซอร์บนแมป (ไม่ต้องเปลี่ยนรูปจาก Admin)',
'ลำดับเกิด <strong>16</strong> หรือช่องฟ้า',
'กระโดด: <kbd>Space</kbd> · <kbd>W</kbd> · <kbd>↑</kbd>',
]);
@@ -1032,6 +1049,18 @@
if (!showG && drawModeEl && drawModeEl.value === 'gauntletPlayerSpawn') drawModeEl.value = 'wall';
}
if (btnClrG) btnClrG.hidden = gt !== 'gauntlet';
const glStartOpt = document.getElementById('draw-mode-option-gauntlet-laser-start');
const glEndOpt = document.getElementById('draw-mode-option-gauntlet-laser-end');
const btnClrGl = document.getElementById('btn-clear-gauntlet-laser-span');
if (glStartOpt) {
glStartOpt.hidden = gt !== 'gauntlet';
if (gt !== 'gauntlet' && drawModeEl && drawModeEl.value === 'gauntletLaserStart') drawModeEl.value = 'wall';
}
if (glEndOpt) {
glEndOpt.hidden = gt !== 'gauntlet';
if (gt !== 'gauntlet' && drawModeEl && drawModeEl.value === 'gauntletLaserEnd') drawModeEl.value = 'wall';
}
if (btnClrGl) btnClrGl.hidden = gt !== 'gauntlet';
const shOpt = document.getElementById('draw-mode-option-shooter-spawn');
const shWrap = document.getElementById('shooter-slot-paint-wrap');
const btnClrSh = document.getElementById('btn-clear-shooter-spawns');
@@ -1127,6 +1156,8 @@
if (gameType === 'gauntlet' || gameType === 'stack' || gameType === 'quiz_carry' || gameType === 'quiz_battle' || gameType === 'jump_survive' || gameType === 'space_shooter' || gameType === 'balloon_boss') lanes = [];
jumpSurvivePlatforms = [];
gauntletPlayerSpawns = [];
gauntletLaserRowStart = null;
gauntletLaserRowEnd = null;
lobbyPlayerSpawns = [null, null, null, null, null, null];
const lsmInit = document.getElementById('lobby-spawn-mode');
if (lsmInit) lsmInit.value = 'random';
@@ -1537,6 +1568,30 @@
ctx.fillText(String(num), cx, cy + 2);
ctx.textAlign = 'left';
};
if (gauntletLaserRowStart != null && gauntletLaserRowEnd != null) {
let y0 = Math.max(0, Math.min(height - 1, gauntletLaserRowStart));
let y1 = Math.max(0, Math.min(height - 1, gauntletLaserRowEnd));
if (y1 < y0) { const t = y0; y0 = y1; y1 = t; }
const x0 = 2;
const x1 = width * tileSize - 2;
ctx.save();
ctx.fillStyle = 'rgba(255, 80, 140, 0.12)';
ctx.fillRect(x0, y0 * tileSize, x1 - x0, (y1 - y0 + 1) * tileSize);
ctx.strokeStyle = 'rgba(255, 140, 180, 0.9)';
ctx.lineWidth = 2;
ctx.setLineDash([7, 5]);
ctx.beginPath();
ctx.moveTo(x0, y0 * tileSize + 1);
ctx.lineTo(x1, y0 * tileSize + 1);
ctx.moveTo(x0, (y1 + 1) * tileSize - 1);
ctx.lineTo(x1, (y1 + 1) * tileSize - 1);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = 'rgba(255, 200, 220, 0.95)';
ctx.font = 'bold 11px ui-monospace, Consolas, sans-serif';
ctx.fillText('laser y=' + y0 + '…' + y1, x0 + 4, y0 * tileSize + 14);
ctx.restore();
}
if (gauntletPlayerSpawns.length) {
for (let i = 0; i < gauntletPlayerSpawns.length; i++) {
const s = gauntletPlayerSpawns[i];
@@ -1650,6 +1705,30 @@
}
function setCell(x, y, left, mouseEv) {
if (drawModeEl.value === 'gauntletLaserStart' || drawModeEl.value === 'gauntletLaserEnd') {
const gt = gameTypeEl ? gameTypeEl.value : gameType;
if (gt !== 'gauntlet') return;
if (!left) {
gauntletLaserRowStart = null;
gauntletLaserRowEnd = null;
if (statusEl) statusEl.textContent = 'ล้างช่วงแนวตั้ง laser แล้ว — ใช้เต็มความสูงแมป';
draw();
return;
}
const yy = Math.max(0, Math.min(height - 1, y));
if (drawModeEl.value === 'gauntletLaserStart') gauntletLaserRowStart = yy;
else gauntletLaserRowEnd = yy;
sanitizeGauntletLaserRowsInEditor();
if (statusEl) {
const a = gauntletLaserRowStart;
const b = gauntletLaserRowEnd;
statusEl.textContent = (a != null && b != null)
? ('เลเซอร์: แถว ' + a + '' + b + ' (แนวตั้ง · คลิกขวา = ล้างทั้งช่วง)')
: ('เลเซอร์: ตั้งแถว ' + yy + ' แล้ว — เลือกโหมดอีกแถวเพื่อกำหนดขอบอีกด้าน');
}
draw();
return;
}
if (drawModeEl.value === 'gauntletPlayerSpawn') {
const gt = gameTypeEl ? gameTypeEl.value : gameType;
if (gt !== 'gauntlet') return;
@@ -1993,6 +2072,8 @@
if (drawModeEl && quizCarryEditorCarryModes().indexOf(drawModeEl.value) >= 0 && gameType !== 'quiz_carry') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'jumpSurvivePlatform' && gameType !== 'jump_survive') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'gauntletPlayerSpawn' && gameType !== 'gauntlet') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'gauntletLaserStart' && gameType !== 'gauntlet') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'gauntletLaserEnd' && gameType !== 'gauntlet') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'shooterSpawnPaint' && gameType !== 'space_shooter') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'balloonBossPlayerPaint' && gameType !== 'balloon_boss') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'balloonBossBossPaint' && gameType !== 'balloon_boss') drawModeEl.value = 'wall';
@@ -2011,6 +2092,15 @@
draw();
});
}
const btnClearGauntletLaserSpan = document.getElementById('btn-clear-gauntlet-laser-span');
if (btnClearGauntletLaserSpan) {
btnClearGauntletLaserSpan.addEventListener('click', () => {
gauntletLaserRowStart = null;
gauntletLaserRowEnd = null;
if (statusEl) statusEl.textContent = 'ล้างช่วง laser — เลเซอร์ในเกมใช้เต็มความสูงแมปอีกครั้ง';
draw();
});
}
const lobbySpawnModeEl = document.getElementById('lobby-spawn-mode');
if (lobbySpawnModeEl) {
@@ -2190,6 +2280,16 @@
body.gauntletPlayerSpawns = gameType === 'gauntlet'
? gauntletPlayerSpawns.map((s) => ({ x: s.x, y: s.y }))
: [];
if (gameType === 'gauntlet') {
sanitizeGauntletLaserRowsInEditor();
if (gauntletLaserRowStart != null && gauntletLaserRowEnd != null) {
body.gauntletLaserRowStart = gauntletLaserRowStart;
body.gauntletLaserRowEnd = gauntletLaserRowEnd;
} else {
body.gauntletLaserRowStart = null;
body.gauntletLaserRowEnd = null;
}
}
const url = mapId ? BASE + '/api/maps/' + mapId : BASE + '/api/maps';
const method = mapId ? 'PUT' : 'POST';
fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
@@ -2461,6 +2561,16 @@
? m.gauntletPlayerSpawns.map((s) => ({ x: Math.floor(Number(s.x)), y: Math.floor(Number(s.y)) }))
.filter((s) => Number.isFinite(s.x) && Number.isFinite(s.y))
: [];
const rs = m.gauntletLaserRowStart;
const re = m.gauntletLaserRowEnd;
if (rs != null && re != null && Number.isFinite(Number(rs)) && Number.isFinite(Number(re))) {
gauntletLaserRowStart = Math.floor(Number(rs));
gauntletLaserRowEnd = Math.floor(Number(re));
sanitizeGauntletLaserRowsInEditor();
} else {
gauntletLaserRowStart = null;
gauntletLaserRowEnd = null;
}
lobbyPlayerSpawns = [null, null, null, null, null, null];
if (Array.isArray(m.lobbyPlayerSpawns)) {
for (let li = 0; li < 6 && li < m.lobbyPlayerSpawns.length; li++) {
+59 -14
View File
@@ -4956,7 +4956,34 @@
function cloneGauntletObsSnap(arr) {
if (!Array.isArray(arr)) return [];
return arr.map((o) => (o && o.id != null ? { id: o.id, kind: o.kind, x: o.x, y: o.y } : null)).filter(Boolean);
return arr.map((o) => {
if (!o || o.id == null) return null;
const row = { id: o.id, kind: o.kind, x: o.x, y: o.y };
if (o.kind === 'laser') {
if (o.y0 != null) row.y0 = o.y0;
if (o.y1 != null) row.y1 = o.y1;
}
return row;
}).filter(Boolean);
}
function gauntletLaserRowHitRange(ob, mapH) {
const h = Math.max(1, Math.floor(Number(mapH)) || 15);
let y0 = ob && ob.y0 != null && Number.isFinite(Number(ob.y0)) ? Math.floor(Number(ob.y0)) : 0;
let y1 = ob && ob.y1 != null && Number.isFinite(Number(ob.y1)) ? Math.floor(Number(ob.y1)) : h - 1;
y0 = Math.max(0, Math.min(h - 1, y0));
y1 = Math.max(0, Math.min(h - 1, y1));
if (y1 < y0) {
const t = y0;
y0 = y1;
y1 = t;
}
return { y0, y1 };
}
function gauntletLaserOverlapsPlayerRow(ob, py, mapH) {
const { y0, y1 } = gauntletLaserRowHitRange(ob, mapH);
return py >= y0 && py <= y1;
}
/** โค้งยกตัว: ขึ้นถึงจุดสูงสุดเร็ว ลงชัน (รู้สึกลงพื้นเร็วกว่าแบบสมมาตร) */
@@ -4985,7 +5012,12 @@
const p = prevMap.get(n.id);
let drawX = n.x;
if (p && Number.isFinite(p.x) && Number.isFinite(n.x)) drawX = p.x + (n.x - p.x) * alpha;
out.push({ id: n.id, kind: n.kind, drawX, y: n.y });
const row = { id: n.id, kind: n.kind, drawX, y: n.y };
if (n.kind === 'laser') {
if (n.y0 != null) row.y0 = n.y0;
if (n.y1 != null) row.y1 = n.y1;
}
out.push(row);
}
return out;
}
@@ -4999,12 +5031,19 @@
} else {
const curDraw = getGauntletObsDrawPositionsAt(now);
const curMap = new Map(curDraw.map((o) => [o.id, o.drawX]));
gauntletObsRenderPrev = next.map((n) => ({
id: n.id,
kind: n.kind,
x: curMap.has(n.id) ? curMap.get(n.id) : n.x,
y: n.y,
}));
gauntletObsRenderPrev = next.map((n) => {
const row = {
id: n.id,
kind: n.kind,
x: curMap.has(n.id) ? curMap.get(n.id) : n.x,
y: n.y,
};
if (n.kind === 'laser') {
if (n.y0 != null) row.y0 = n.y0;
if (n.y1 != null) row.y1 = n.y1;
}
return row;
});
gauntletObsRenderNext = next;
}
gauntletObsBlendT0 = now;
@@ -5044,8 +5083,8 @@
if (obs.x === px + 1 && obs.y === py) incomingCol = true;
}
if (obs.kind === 'laser' && typeof obs.x === 'number') {
if (obs.x === px) sameCell = true;
if (obs.x === px + 1) incomingCol = true;
if (obs.x === px && gauntletLaserOverlapsPlayerRow(obs, py, h)) sameCell = true;
if (obs.x === px + 1 && gauntletLaserOverlapsPlayerRow(obs, py, h)) incomingCol = true;
}
}
const tier = o.botTier || 'avg';
@@ -5076,8 +5115,13 @@
else hitBack = true;
}
if (ob.kind === 'laser' && typeof ob.x === 'number' && ob.x === px) {
if (air) advanceX = true;
else hitBack = true;
if (!gauntletLaserOverlapsPlayerRow(ob, py, h)) {
/* เลเซอร์ไม่ครอบแถวนี้ */
} else if (air) {
advanceX = true;
} else {
hitBack = true;
}
}
}
if (advanceX) {
@@ -9695,10 +9739,11 @@
}
} else if (o.kind === 'laser' && typeof o.drawX === 'number') {
if (o.drawX < stx - 2 || o.drawX > enx + 2) continue;
const { y0: laserRow0, y1: laserRow1 } = gauntletLaserRowHitRange(o, h);
const wx0 = o.drawX * tileSize;
const wx1 = (o.drawX + 1) * tileSize;
const wy0 = 0;
const wy1 = h * tileSize;
const wy0 = laserRow0 * tileSize;
const wy1 = (laserRow1 + 1) * tileSize;
const [sx0, syTop] = worldToScreen(wx0, wy0);
const [sx1, syBot] = worldToScreen(wx1, wy1);
const rx = Math.min(sx0, sx1);
+1 -1
View File
@@ -1462,7 +1462,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.184"></script>
<script src="js/play.js?v=0.185"></script>
<div class="version-tag">v —</div>
</body>
</html>
+59 -14
View File
@@ -4956,7 +4956,34 @@
function cloneGauntletObsSnap(arr) {
if (!Array.isArray(arr)) return [];
return arr.map((o) => (o && o.id != null ? { id: o.id, kind: o.kind, x: o.x, y: o.y } : null)).filter(Boolean);
return arr.map((o) => {
if (!o || o.id == null) return null;
const row = { id: o.id, kind: o.kind, x: o.x, y: o.y };
if (o.kind === 'laser') {
if (o.y0 != null) row.y0 = o.y0;
if (o.y1 != null) row.y1 = o.y1;
}
return row;
}).filter(Boolean);
}
function gauntletLaserRowHitRange(ob, mapH) {
const h = Math.max(1, Math.floor(Number(mapH)) || 15);
let y0 = ob && ob.y0 != null && Number.isFinite(Number(ob.y0)) ? Math.floor(Number(ob.y0)) : 0;
let y1 = ob && ob.y1 != null && Number.isFinite(Number(ob.y1)) ? Math.floor(Number(ob.y1)) : h - 1;
y0 = Math.max(0, Math.min(h - 1, y0));
y1 = Math.max(0, Math.min(h - 1, y1));
if (y1 < y0) {
const t = y0;
y0 = y1;
y1 = t;
}
return { y0, y1 };
}
function gauntletLaserOverlapsPlayerRow(ob, py, mapH) {
const { y0, y1 } = gauntletLaserRowHitRange(ob, mapH);
return py >= y0 && py <= y1;
}
/** โค้งยกตัว: ขึ้นถึงจุดสูงสุดเร็ว ลงชัน (รู้สึกลงพื้นเร็วกว่าแบบสมมาตร) */
@@ -4985,7 +5012,12 @@
const p = prevMap.get(n.id);
let drawX = n.x;
if (p && Number.isFinite(p.x) && Number.isFinite(n.x)) drawX = p.x + (n.x - p.x) * alpha;
out.push({ id: n.id, kind: n.kind, drawX, y: n.y });
const row = { id: n.id, kind: n.kind, drawX, y: n.y };
if (n.kind === 'laser') {
if (n.y0 != null) row.y0 = n.y0;
if (n.y1 != null) row.y1 = n.y1;
}
out.push(row);
}
return out;
}
@@ -4999,12 +5031,19 @@
} else {
const curDraw = getGauntletObsDrawPositionsAt(now);
const curMap = new Map(curDraw.map((o) => [o.id, o.drawX]));
gauntletObsRenderPrev = next.map((n) => ({
id: n.id,
kind: n.kind,
x: curMap.has(n.id) ? curMap.get(n.id) : n.x,
y: n.y,
}));
gauntletObsRenderPrev = next.map((n) => {
const row = {
id: n.id,
kind: n.kind,
x: curMap.has(n.id) ? curMap.get(n.id) : n.x,
y: n.y,
};
if (n.kind === 'laser') {
if (n.y0 != null) row.y0 = n.y0;
if (n.y1 != null) row.y1 = n.y1;
}
return row;
});
gauntletObsRenderNext = next;
}
gauntletObsBlendT0 = now;
@@ -5044,8 +5083,8 @@
if (obs.x === px + 1 && obs.y === py) incomingCol = true;
}
if (obs.kind === 'laser' && typeof obs.x === 'number') {
if (obs.x === px) sameCell = true;
if (obs.x === px + 1) incomingCol = true;
if (obs.x === px && gauntletLaserOverlapsPlayerRow(obs, py, h)) sameCell = true;
if (obs.x === px + 1 && gauntletLaserOverlapsPlayerRow(obs, py, h)) incomingCol = true;
}
}
const tier = o.botTier || 'avg';
@@ -5076,8 +5115,13 @@
else hitBack = true;
}
if (ob.kind === 'laser' && typeof ob.x === 'number' && ob.x === px) {
if (air) advanceX = true;
else hitBack = true;
if (!gauntletLaserOverlapsPlayerRow(ob, py, h)) {
/* เลเซอร์ไม่ครอบแถวนี้ */
} else if (air) {
advanceX = true;
} else {
hitBack = true;
}
}
}
if (advanceX) {
@@ -9695,10 +9739,11 @@
}
} else if (o.kind === 'laser' && typeof o.drawX === 'number') {
if (o.drawX < stx - 2 || o.drawX > enx + 2) continue;
const { y0: laserRow0, y1: laserRow1 } = gauntletLaserRowHitRange(o, h);
const wx0 = o.drawX * tileSize;
const wx1 = (o.drawX + 1) * tileSize;
const wy0 = 0;
const wy1 = h * tileSize;
const wy0 = laserRow0 * tileSize;
const wy1 = (laserRow1 + 1) * tileSize;
const [sx0, syTop] = worldToScreen(wx0, wy0);
const [sx1, syBot] = worldToScreen(wx1, wy1);
const rx = Math.min(sx0, sx1);
+42 -4
View File
@@ -2634,6 +2634,27 @@ function endGauntletGame(sid, space, reason) {
});
}
/** แถว y บน–ล่างของเลเซอร์จากแมป (null ทั้งคู่ = เต็มความสูง) */
function gauntletLaserVerticalSpanFromMap(md) {
const hRaw = md && md.height != null ? Number(md.height) : 15;
const h = Math.max(1, Math.floor(Number.isFinite(hRaw) ? hRaw : 15));
const rs = md && md.gauntletLaserRowStart;
const re = md && md.gauntletLaserRowEnd;
if (rs == null || re == null || !Number.isFinite(Number(rs)) || !Number.isFinite(Number(re))) {
return { y0: 0, y1: h - 1 };
}
let y0 = Math.floor(Number(rs));
let y1 = Math.floor(Number(re));
y0 = Math.max(0, Math.min(h - 1, y0));
y1 = Math.max(0, Math.min(h - 1, y1));
if (y1 < y0) {
const t = y0;
y0 = y1;
y1 = t;
}
return { y0, y1 };
}
function runGauntletTick(sid, space) {
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md || md.gameType !== 'gauntlet') {
@@ -2687,7 +2708,8 @@ function runGauntletTick(sid, space) {
gr.nextSpawnIn = 2 + Math.floor(Math.random() * 6);
/* ประเภท 2: เลเซอร์ — ทุกแถว */
if (Math.random() < GAUNTLET_LASER_SPAWN_CHANCE) {
gr.obstacles.push({ id: ++gr.nextObsId, kind: 'laser', x: w - 1 });
const span = gauntletLaserVerticalSpanFromMap(md);
gr.obstacles.push({ id: ++gr.nextObsId, kind: 'laser', x: w - 1, y0: span.y0, y1: span.y1 });
}
/* ประเภท 1: แยกเลน — เฉพาะแถวที่มีผู้เล่นยืนอยู่ (สุ่มแยกแถว) */
occupiedY.forEach((ly) => {
@@ -2712,8 +2734,17 @@ function runGauntletTick(sid, space) {
else hitBack = true;
}
if (o.kind === 'laser' && o.x === px) {
if (air) advanceX = true;
else hitBack = true;
const y0 = o.y0 != null && Number.isFinite(Number(o.y0)) ? Math.max(0, Math.min(h - 1, Math.floor(Number(o.y0)))) : 0;
const y1 = o.y1 != null && Number.isFinite(Number(o.y1)) ? Math.max(0, Math.min(h - 1, Math.floor(Number(o.y1)))) : h - 1;
const lo = Math.min(y0, y1);
const hi = Math.max(y0, y1);
if (py < lo || py > hi) {
/* เลเซอร์ไม่ครอบแถวนี้ */
} else if (air) {
advanceX = true;
} else {
hitBack = true;
}
}
}
if (advanceX) {
@@ -2728,7 +2759,14 @@ function runGauntletTick(sid, space) {
p.y = py;
});
const obsOut = gr.obstacles.map((o) => ({ id: o.id, kind: o.kind, x: o.x, y: o.kind === 'laser' ? null : o.y }));
const obsOut = gr.obstacles.map((o) => {
if (o.kind === 'laser') {
const y0 = o.y0 != null && Number.isFinite(Number(o.y0)) ? Math.floor(Number(o.y0)) : 0;
const y1 = o.y1 != null && Number.isFinite(Number(o.y1)) ? Math.floor(Number(o.y1)) : h - 1;
return { id: o.id, kind: 'laser', x: o.x, y: null, y0, y1 };
}
return { id: o.id, kind: o.kind, x: o.x, y: o.y };
});
const playersOut = [...space.peers.values()].map((p) => ({
id: p.id,
x: p.x,