minigame 2 just begin1.2
This commit is contained in:
+108
-10
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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="ล้างรายการจุดเกิดพรมแดงที่กำหนดเอง">ล้างลำดับ 1–6</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>
|
||||
|
||||
@@ -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>1–6</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++) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user