minigame 5 jumper 1.4

This commit is contained in:
2026-05-01 08:54:48 +00:00
parent 06a16bb837
commit e2dcf6f678
9 changed files with 299 additions and 51 deletions
+38
View File
@@ -2369,3 +2369,41 @@ code {
font-variant-numeric: tabular-nums;
padding-bottom: 0.2rem;
}
/* Space shooter admin — per-slot ship image URLs */
.space-shooter-ship-grid {
display: flex;
flex-direction: column;
gap: 0.65rem;
margin-top: 0.5rem;
}
.space-shooter-ship-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 0.75rem;
}
.space-shooter-ship-slot {
flex: 0 0 1.5rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: var(--accent);
text-align: center;
}
.space-shooter-ship-url-label {
flex: 1 1 220px;
min-width: 160px;
margin: 0;
}
.space-shooter-ship-url-label input[type='text'] {
width: 100%;
max-width: 560px;
margin-top: 0.2rem;
}
.space-shooter-ship-prev {
flex: 0 0 48px;
object-fit: contain;
background: rgba(0, 0, 0, 0.35);
border: 1px solid var(--border);
border-radius: 8px;
}
+54
View File
@@ -2630,6 +2630,50 @@
});
}
function updateSpaceShooterShipPreview(slot) {
var img = el('space-shooter-ship-prev-' + slot);
var inp = el('space-shooter-ship-url-' + slot);
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 = 'Ship slot ' + slot;
img.src = v;
}
function bindSpaceShooterShipUrlInputs() {
for (var s = 1; s <= 6; s++) {
(function (slot) {
var inp = el('space-shooter-ship-url-' + slot);
var btn = el('btn-space-shooter-ship-clear-' + slot);
if (inp) {
inp.addEventListener('change', function () { updateSpaceShooterShipPreview(slot); });
inp.addEventListener('blur', function () { updateSpaceShooterShipPreview(slot); });
}
if (btn) {
btn.addEventListener('click', function () {
var i = el('space-shooter-ship-url-' + slot);
if (i) i.value = '';
updateSpaceShooterShipPreview(slot);
});
}
})(s);
}
}
function readSpaceShooterShipImageUrlsFromForm() {
var urls = [];
for (var i = 1; i <= 6; i++) {
var inp = el('space-shooter-ship-url-' + i);
urls.push(inp && inp.value ? String(inp.value).trim() : '');
}
return urls;
}
function loadSpaceShooterTimingPanel() {
gameTimingFetch('GET')
.then(function (data) {
@@ -2638,6 +2682,13 @@
var ss = Number(data.spaceShooterMissionTimeSec);
inpT.value = String(Number.isFinite(ss) && ss > 0 ? Math.floor(ss) : 0);
}
var urls = data.spaceShooterShipImageUrls;
if (!Array.isArray(urls)) urls = [];
for (var k = 1; k <= 6; k++) {
var inpU = el('space-shooter-ship-url-' + k);
if (inpU) inpU.value = urls[k - 1] != null ? String(urls[k - 1]) : '';
updateSpaceShooterShipPreview(k);
}
setMsg('space-shooter-timing-msg', '', '');
})
.catch(function (e) {
@@ -2649,9 +2700,11 @@
var limSs = el('space-shooter-mission-sec') ? parseInt(String(el('space-shooter-mission-sec').value), 10) : 0;
if (Number.isNaN(limSs) || limSs < 0) limSs = 0;
limSs = limSs <= 0 ? 0 : Math.max(10, Math.min(7200, limSs));
var shipUrls = readSpaceShooterShipImageUrlsFromForm();
gameTimingFetch('GET')
.then(function (data) {
data.spaceShooterMissionTimeSec = limSs;
data.spaceShooterShipImageUrls = shipUrls;
return gameTimingFetch('PUT', data);
})
.then(function () {
@@ -2887,6 +2940,7 @@
if (btnSpaceShooterSave) {
btnSpaceShooterSave.addEventListener('click', saveSpaceShooterTimingPanel);
}
bindSpaceShooterShipUrlInputs();
var btnStackGameSave = el('btn-stack-game-save');
if (btnStackGameSave) {
btnStackGameSave.addEventListener('click', saveStackGamePanel);
+15 -3
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=21">
<link rel="stylesheet" href="admin.css?v=22">
</head>
<body>
<a class="skip-link" href="#admin-main">ข้ามไปเนื้อหา</a>
@@ -466,14 +466,26 @@
</section>
<section id="tab-panel-space-shooter" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-space-shooter">
<h2>ยิงยานอวกาศ (Violent Crime / space_shooter) — ตั้งค่าเวลา</h2>
<p class="muted">เก็บที่ <code>/Game/data/game-timing.json</code> ฟิลด์ <code>spaceShooterMissionTimeSec</code> · ผู้เล่นได้ค่าใหม่เมื่อโหลดหน้าเล่น / <code>GET /api/game-timing</code> · รูปเกมอัปโหลดไปที่ <code>/Game/public/img/ViolentCrime/</code> (ชื่อโฟลเดอร์<strong>ไม่มีช่องว่าง</strong> — FTP หลายตัวจะ <code>550</code> ถ้าใช้ <code>Violent Crime</code>)</p>
<h2>ยิงยานอวกาศ (Violent Crime / space_shooter) — เวลารอบ + รูปยานต่อช่อง</h2>
<p class="muted">เก็บที่ <code>/Game/data/game-timing.json</code> ฟิลด์ <code>spaceShooterMissionTimeSec</code> · <code>spaceShooterShipImageUrls</code> (รูปยาน 6 ช่อง) · ผู้เล่นได้ค่าใหม่เมื่อโหลดหน้าเล่น / <code>GET /api/game-timing</code> · รูปเกมอัปโหลดไปที่ <code>/Game/public/img/ViolentCrime/</code> (ชื่อโฟลเดอร์<strong>ไม่มีช่องว่าง</strong> — FTP หลายตัวจะ <code>550</code> ถ้าใช้ <code>Violent Crime</code>)</p>
<fieldset class="quiz-timing-fieldset">
<legend>จำกัดเวลารอบ (ยิงอุกาบาต)</legend>
<div class="form-grid form-inline quiz-timing-grid">
<label title="0 = ใช้ค่าเริ่ม 90 วินาทีในเกม · ตั้งอย่างน้อย 10 วินาทีถ้าต้องการกำหนดเอง">เวลารอบ (วินาที) <input type="number" id="space-shooter-mission-sec" min="0" max="7200" step="5" value="0"></label>
</div>
</fieldset>
<fieldset class="quiz-timing-fieldset">
<legend>รูปยานต่อช่อง (spawn slot 16)</legend>
<p class="muted" style="margin-top:0">ช่องตรงกับ <code>shooterSpawnSlots</code> บนแมป (1–6) · ว่าง = ใช้ยานวาดเวกเตอร์เดิม · <em>English:</em> One image URL per map slot; empty keeps the default vector ship.</p>
<div class="space-shooter-ship-grid" role="group" aria-label="Space shooter ship images">
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot">1</span><label class="space-shooter-ship-url-label">URL <input type="text" id="space-shooter-ship-url-1" maxlength="500" spellcheck="false" placeholder="/Game/img/ViolentCrime/ship1.png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="space-shooter-ship-prev-1" alt="" width="48" height="48" decoding="async"><button type="button" class="btn btn-ghost" id="btn-space-shooter-ship-clear-1">ล้าง</button></div>
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot">2</span><label class="space-shooter-ship-url-label">URL <input type="text" id="space-shooter-ship-url-2" maxlength="500" spellcheck="false" placeholder="/Game/img/ViolentCrime/ship2.png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="space-shooter-ship-prev-2" alt="" width="48" height="48" decoding="async"><button type="button" class="btn btn-ghost" id="btn-space-shooter-ship-clear-2">ล้าง</button></div>
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot">3</span><label class="space-shooter-ship-url-label">URL <input type="text" id="space-shooter-ship-url-3" maxlength="500" spellcheck="false" placeholder="/Game/img/ViolentCrime/ship3.png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="space-shooter-ship-prev-3" alt="" width="48" height="48" decoding="async"><button type="button" class="btn btn-ghost" id="btn-space-shooter-ship-clear-3">ล้าง</button></div>
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot">4</span><label class="space-shooter-ship-url-label">URL <input type="text" id="space-shooter-ship-url-4" maxlength="500" spellcheck="false" placeholder="/Game/img/ViolentCrime/ship4.png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="space-shooter-ship-prev-4" alt="" width="48" height="48" decoding="async"><button type="button" class="btn btn-ghost" id="btn-space-shooter-ship-clear-4">ล้าง</button></div>
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot">5</span><label class="space-shooter-ship-url-label">URL <input type="text" id="space-shooter-ship-url-5" maxlength="500" spellcheck="false" placeholder="/Game/img/ViolentCrime/ship5.png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="space-shooter-ship-prev-5" alt="" width="48" height="48" decoding="async"><button type="button" class="btn btn-ghost" id="btn-space-shooter-ship-clear-5">ล้าง</button></div>
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot">6</span><label class="space-shooter-ship-url-label">URL <input type="text" id="space-shooter-ship-url-6" maxlength="500" spellcheck="false" placeholder="/Game/img/ViolentCrime/ship6.png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="space-shooter-ship-prev-6" alt="" width="48" height="48" decoding="async"><button type="button" class="btn btn-ghost" id="btn-space-shooter-ship-clear-6">ล้าง</button></div>
</div>
</fieldset>
<p class="muted" style="margin-top:-0.25rem;margin-bottom:0.65rem">นับจากเริ่มรอบในเกม · ถ้าในแมปตั้ง <code>spaceShooterTimeSec</code> &gt; 0 จะใช้ค่าบนแมปแทนทั้งหมด · ตั้ง <code>spaceShooterTimeSec</code> = 0 บนแมป = ไม่จับเวลา</p>
<div class="quiz-admin-actions">
<button type="button" class="btn btn-primary" id="btn-space-shooter-save">บันทึก</button>
+9 -1
View File
@@ -18,5 +18,13 @@
"stackBlockWidthTiles": 3.2,
"jumpSurviveJumpHeightMult": 1.5,
"jumpSurviveMissionTimeSec": 0,
"spaceShooterMissionTimeSec": 0
"spaceShooterMissionTimeSec": 0,
"spaceShooterShipImageUrls": [
"/Game/img/ViolentCrime/Rocket-1.png",
"/Game/img/ViolentCrime/Rocket-2.png",
"/Game/img/ViolentCrime/Rocket-3.png",
"/Game/img/ViolentCrime/Rocket-4.png",
"/Game/img/ViolentCrime/Rocket-5.png",
"/Game/img/ViolentCrime/Rocket-6.png"
]
}
+1 -1
View File
@@ -289,7 +289,7 @@
</div>
</div>
<script src="js/version.js?v=0.0169"></script>
<script src="js/editor.js?v=20260427-gauntlet-laser-span"></script>
<script src="js/editor.js?v=20260427-violent-crime-howto"></script>
<div class="version-tag">v —</div>
</body>
</html>
+151 -43
View File
@@ -25,6 +25,59 @@
const JUMP_SURVIVE_MISSION_MAP_ID = 'mnptfts2';
/** Space shooter ฉาก Violent Crime — flow เดียวกับ crown/howto (รูปใน /img/ViolentCrime/) */
const SPACE_SHOOTER_MISSION_MAP_ID = 'mnpz6rkp';
/** Violent Crime mission: asteroid strikes ship → invuln → 3 strikes = eliminated */
const SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS = 3;
const SPACE_SHOOTER_MISSION_HIT_INVULN_MS = 1400;
const SPACE_SHOOTER_MISSION_SHIP_HIT_RADIUS = 12;
function spaceShooterResetMissionShipStateForAllPlay() {
function resetEnt(ent) {
if (!ent) return;
ent.spaceShooterHits = 0;
ent.spaceShooterEliminated = false;
ent.spaceShooterInvulnUntil = 0;
}
resetEnt(me);
others.forEach(function (o) {
resetEnt(o);
});
}
function spaceShooterMissionResolveShipAsteroidHitsPlay() {
if (!mapData || mapData.gameType !== 'space_shooter') return;
if (!isSpaceShooterMissionUiMapPlay() || spaceShooterMissionPhase !== 'live' || spaceShooterGameEnded) return;
const now = performance.now();
const shipR = SPACE_SHOOTER_MISSION_SHIP_HIT_RADIUS;
function tryHit(ent, cx, cy) {
if (!ent || ent.spaceShooterEliminated || cx == null || cy == null) return;
if (ent.spaceShooterInvulnUntil != null && now < ent.spaceShooterInvulnUntil) return;
for (let ai = spaceShooterAsteroids.length - 1; ai >= 0; ai--) {
const a = spaceShooterAsteroids[ai];
if (!a) continue;
const dx = cx - a.x;
const dy = cy - a.y;
const rr = (a.r + shipR) * (a.r + shipR);
if (dx * dx + dy * dy > rr) continue;
ent.spaceShooterHits = Math.max(0, Number(ent.spaceShooterHits) || 0) + 1;
ent.spaceShooterInvulnUntil = now + SPACE_SHOOTER_MISSION_HIT_INVULN_MS;
spaceShooterAsteroids.splice(ai, 1);
spaceShooterPopups.push({ x: cx, y: cy - 28, text: 'HIT', until: Date.now() + 650 });
if (ent.spaceShooterHits >= SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS) {
ent.spaceShooterEliminated = true;
spaceShooterPopups.push({ x: cx, y: cy - 50, text: 'OUT', until: Date.now() + 1400 });
}
break;
}
}
if (myId != null) tryHit(me, me.spaceShooterCx, me.spaceShooterCy);
others.forEach(function (o) {
if (!o) return;
tryHit(o, o.spaceShooterCx, o.spaceShooterCy);
});
}
/** จนกว่าโหลด /api/characters — placeholder ชั่วคราวก่อน join */
const LEGACY_PLACEHOLDER_CHARACTER_ID = 'Chatest';
let firstCharacterDefaultResolved = null;
@@ -696,6 +749,8 @@
let playJumpSurviveMissionTimeSec = 0;
/** ยิงยานอวกาศ (space_shooter): เวลารอบจาก game-timing; 0 = ใช้ค่าเริ่ม 90 ในเกมถ้าแมปไม่ทับ */
let playSpaceShooterMissionTimeSec = 0;
/** รูปยานช่อง 16 จาก game-timing (spawn slot); ว่าง = วาดยานเวกเตอร์ */
let playSpaceShooterShipImageUrls = ['', '', '', '', '', ''];
/** 0 = ไม่จำกัด — จาก game-timing / gauntlet-sync (พรมแดง Gauntlet เท่านั้น) */
let gauntletRuntimeTimeLimitSec = 0;
/** เวลาสิ้นสุดรอบ (epoch ms) จากเซิร์ฟเวอร์ — null = ไม่จับเวลา */
@@ -2903,6 +2958,7 @@
btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host';
btn.setAttribute('aria-pressed', 'false');
}
spaceShooterResetMissionShipStateForAllPlay();
}
function beginSpaceShooterMissionCountdownThenRun() {
@@ -2942,6 +2998,7 @@
others.forEach(function (o) {
if (o) o.spaceShooterScore = 0;
});
spaceShooterResetMissionShipStateForAllPlay();
if (mapData) applySpaceShooterSpawnLayoutPlay();
};
if (!cd || !numEl) {
@@ -2966,23 +3023,23 @@
function spaceShooterBuildMissionPayload() {
const rows = [];
function pushEnt(id, nickname, characterId, score) {
function pushEnt(id, nickname, characterId, score, eliminated) {
rows.push({
id: id,
nickname: (nickname && String(nickname).trim()) ? String(nickname).trim() : String(id),
characterId: characterId ?? null,
baseScore: Math.max(0, Number(score) || 0),
eliminated: false,
eliminated: !!eliminated,
rankBonus: 0,
finalScore: Math.max(0, Number(score) || 0),
});
}
if (myId != null) {
pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), me.spaceShooterScore);
pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), me.spaceShooterScore, !!me.spaceShooterEliminated);
}
others.forEach(function (o, id) {
if (!o) return;
pushEnt(id, o.nickname, o.characterId, o.spaceShooterScore);
pushEnt(id, o.nickname, o.characterId, o.spaceShooterScore, !!o.spaceShooterEliminated);
});
rows.sort(function (a, b) {
if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
@@ -2996,7 +3053,7 @@
nickname: row.nickname,
characterId: row.characterId,
baseScore: row.baseScore,
eliminated: false,
eliminated: !!row.eliminated,
rank: pos,
rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos),
rankBonus: 0,
@@ -3007,8 +3064,10 @@
const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0);
const n = ranked.length || 1;
const averageScore = Math.floor(totalSum / n);
const grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C';
const rewardCard = gauntletCrownRollRewardCardLocal(grade);
const survivorCount = rows.filter(function (r) { return !r.eliminated; }).length;
let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C';
if (survivorCount <= 0) grade = 'F';
const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade);
return {
ranked: ranked,
totalSum: totalSum,
@@ -3017,7 +3076,7 @@
rewardCard: rewardCard,
totalParts: totalParts,
uiSkin: 'violent_crime',
survivorCount: ranked.length,
survivorCount: survivorCount,
participantCount: rows.length,
};
}
@@ -3034,7 +3093,7 @@
nickname: r.nickname,
characterId: r.characterId,
baseScore: Math.max(0, Number(r.baseScore) || 0),
eliminated: false,
eliminated: !!r.eliminated,
rankBonus: 0,
finalScore: Math.max(0, Number(r.finalScore != null ? r.finalScore : r.baseScore) || 0),
};
@@ -3049,7 +3108,7 @@
nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid,
characterId: o.characterId ?? null,
baseScore: Math.max(0, Number(o.spaceShooterScore) || 0),
eliminated: false,
eliminated: !!o.spaceShooterEliminated,
rankBonus: 0,
finalScore: Math.max(0, Number(o.spaceShooterScore) || 0),
});
@@ -3067,7 +3126,7 @@
nickname: row.nickname,
characterId: row.characterId,
baseScore: bs,
eliminated: false,
eliminated: !!row.eliminated,
rank: pos,
rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos),
rankBonus: 0,
@@ -3078,16 +3137,19 @@
const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0);
const n2 = ranked.length || 1;
const averageScore = Math.floor(totalSum / n2);
const grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C';
const survivorCount = baseRows.filter(function (r) { return !r.eliminated; }).length;
let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C';
if (survivorCount <= 0) grade = 'F';
const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade);
return {
ranked: ranked,
totalSum: totalSum,
averageScore: averageScore,
grade: grade,
rewardCard: gauntletCrownRollRewardCardLocal(grade),
rewardCard: rewardCard,
totalParts: totalParts,
uiSkin: 'violent_crime',
survivorCount: ranked.length,
survivorCount: survivorCount,
participantCount: baseRows.length,
};
}
@@ -5674,6 +5736,18 @@
playSpaceShooterMissionTimeSec = 0;
}
}
if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterShipImageUrls')) {
const a = payload.spaceShooterShipImageUrls;
const next = ['', '', '', '', '', ''];
if (Array.isArray(a)) {
for (let si = 0; si < 6; si++) {
const u = normalizeGauntletAssetUrlForPlay(typeof a[si] === 'string' ? a[si] : '');
next[si] = u;
if (u) ensureGauntletAssetImage(u);
}
}
playSpaceShooterShipImageUrls = next;
}
if (isStack() && stackMini) reapplyStackMiniSizingFromGlobals();
}
@@ -6051,6 +6125,8 @@
function gauntletCrownEmbedMissionAnyOk(mission) {
if (!mission) return false;
if (mission.uiSkin === 'violent_crime') {
const sc = Number(mission.survivorCount);
if (Number.isFinite(sc) && sc <= 0) return false;
const g = String(mission.grade || '').trim().toUpperCase().charAt(0);
return g !== 'F';
}
@@ -6246,10 +6322,15 @@
const sum = Math.max(0, Number(disp.totalSum) || 0);
const avg = Math.max(0, Math.floor(Number(disp.averageScore) || 0));
const jumperSkin = mission && mission.uiSkin === 'jumper';
const violentSkin = mission && mission.uiSkin === 'violent_crime';
if (jumperSkin) {
const sc = Math.max(0, Math.floor(Number(disp.survivorCount) || 0));
const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0));
totalEl.innerHTML = 'คะแนนรวม <span class="gcm-total-nums">(' + parts.join('+') + ') = ' + sum + '</span> · ผู้รอด ' + sc + '/' + pc + ' → เกรด';
} else if (violentSkin) {
const sc = Math.max(0, Math.floor(Number(disp.survivorCount) || 0));
const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0));
totalEl.innerHTML = 'คะแนนรวม <span class="gcm-total-nums">(' + parts.join('+') + ') = ' + sum + '</span> · รอดชีวิต ' + sc + '/' + pc + ' → เกรด';
} else {
totalEl.innerHTML = 'คะแนนรวม <span class="gcm-total-nums">(' + parts.join('+') + ') = ' + sum + '</span> · เฉลี่ย ' + avg + ' → เกรด';
}
@@ -6848,6 +6929,7 @@
cy = clampSpaceShooterWorldCy(cy, mh);
ent.spaceShooterCx = cx;
ent.spaceShooterCy = cy;
ent.spaceShooterSlot = slot;
if (typeof ent.spaceShooterScore !== 'number' || !Number.isFinite(ent.spaceShooterScore)) ent.spaceShooterScore = 0;
ent.x = cx / ts - cw * 0.5;
ent.y = cy / ts - ch * 0.92;
@@ -6945,7 +7027,7 @@
const mh = (mapData.height || 15) * ts;
[...others.keys()].filter(isPreviewBotId).forEach((bid) => {
const o = others.get(bid);
if (!o || o.spaceShooterCx == null) return;
if (!o || o.spaceShooterCx == null || o.spaceShooterEliminated) return;
let nearest = null, nd = 1e9;
for (let i = 0; i < spaceShooterAsteroids.length; i++) {
const a = spaceShooterAsteroids[i];
@@ -7031,17 +7113,20 @@
if (keys['ArrowDown'] || keys['KeyS']) vy += 1;
}
const moveSpd = 280;
if (me.spaceShooterCx != null) {
me.spaceShooterCx = Math.max(18, Math.min(mw - 18, me.spaceShooterCx + vx * moveSpd * dt));
const canPilotMe = !(isSpaceShooterMissionUiMapPlay() && me.spaceShooterEliminated);
if (canPilotMe) {
if (me.spaceShooterCx != null) {
me.spaceShooterCx = Math.max(18, Math.min(mw - 18, me.spaceShooterCx + vx * moveSpd * dt));
}
if (me.spaceShooterCy != null) {
me.spaceShooterCy = clampSpaceShooterWorldCy(me.spaceShooterCy + vy * moveSpd * dt, mh);
}
syncSpaceShooterFootFromShipCenter(me);
}
if (me.spaceShooterCy != null) {
me.spaceShooterCy = clampSpaceShooterWorldCy(me.spaceShooterCy + vy * moveSpd * dt, mh);
}
syncSpaceShooterFootFromShipCenter(me);
spaceShooterFireCd -= dt;
const fireHeld = !isChatFocused() && !!keys['Space'];
if (fireHeld && spaceShooterFireCd <= 0 && me.spaceShooterCy != null) {
if (canPilotMe && fireHeld && spaceShooterFireCd <= 0 && me.spaceShooterCy != null) {
spaceShooterFireCd = 0.21;
spaceShooterBullets.push({ x: me.spaceShooterCx, y: me.spaceShooterCy - 20, vy: -580, ownerId: myId });
}
@@ -7059,6 +7144,8 @@
if (a.y > mh + 100) spaceShooterAsteroids.splice(ai, 1);
}
spaceShooterMissionResolveShipAsteroidHitsPlay();
for (let bi = spaceShooterBullets.length - 1; bi >= 0; bi--) {
const b = spaceShooterBullets[bi];
let hit = -1;
@@ -7161,27 +7248,48 @@
const col = SPACE_SHOOTER_SHIP_COLORS[idx % SPACE_SHOOTER_SHIP_COLORS.length];
const bodyW = 14 * zDraw;
const bodyH = 22 * zDraw;
ctx.save();
ctx.translate(sx, sy);
ctx.fillStyle = col;
ctx.strokeStyle = 'rgba(180, 255, 255, 0.85)';
ctx.lineWidth = Math.max(1.2, zDraw);
ctx.beginPath();
ctx.moveTo(0, -bodyH * 0.55);
ctx.lineTo(-bodyW * 0.55, bodyH * 0.42);
ctx.lineTo(0, bodyH * 0.22);
ctx.lineTo(bodyW * 0.55, bodyH * 0.42);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.fillStyle = 'rgba(255, 220, 120, 0.95)';
ctx.beginPath();
ctx.moveTo(-bodyW * 0.22, bodyH * 0.38);
ctx.lineTo(0, bodyH * 0.72);
ctx.lineTo(bodyW * 0.22, bodyH * 0.38);
ctx.closePath();
ctx.fill();
ctx.restore();
const elimShip = !!(isSpaceShooterMissionUiMapPlay() && ent.spaceShooterEliminated);
const slot = Math.max(1, Math.min(6, Number(ent.spaceShooterSlot) || ((idx % 6) + 1)));
const shipUrl = (playSpaceShooterShipImageUrls[slot - 1] || '').trim();
const shipRec = shipUrl ? ensureGauntletAssetImage(shipUrl) : null;
const shipImg = shipRec && shipRec.ready && shipRec.img && shipRec.img.naturalWidth > 0 ? shipRec.img : null;
if (shipImg) {
ctx.save();
ctx.translate(sx, sy);
if (elimShip) ctx.globalAlpha = 0.38;
const iw = shipImg.naturalWidth;
const ih = shipImg.naturalHeight;
const maxW = bodyW * 2.4;
const maxH = bodyH * 2.4;
const sc = Math.min(maxW / iw, maxH / ih);
const dw = iw * sc;
const dh = ih * sc;
ctx.drawImage(shipImg, -dw * 0.5, -dh * 0.5, dw, dh);
ctx.restore();
} else {
ctx.save();
ctx.translate(sx, sy);
if (elimShip) ctx.globalAlpha = 0.38;
ctx.fillStyle = col;
ctx.strokeStyle = 'rgba(180, 255, 255, 0.85)';
ctx.lineWidth = Math.max(1.2, zDraw);
ctx.beginPath();
ctx.moveTo(0, -bodyH * 0.55);
ctx.lineTo(-bodyW * 0.55, bodyH * 0.42);
ctx.lineTo(0, bodyH * 0.22);
ctx.lineTo(bodyW * 0.55, bodyH * 0.42);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.fillStyle = 'rgba(255, 220, 120, 0.95)';
ctx.beginPath();
ctx.moveTo(-bodyW * 0.22, bodyH * 0.38);
ctx.lineTo(0, bodyH * 0.72);
ctx.lineTo(bodyW * 0.22, bodyH * 0.38);
ctx.closePath();
ctx.fill();
ctx.restore();
}
const name = (ent.nickname || '').slice(0, 10);
if (name) {
ctx.font = `${Math.max(9, 10 * zDraw)}px system-ui, "Kanit", sans-serif`;
+1 -1
View File
@@ -1,6 +1,6 @@
// ทุกครั้งที่มีการเพิ่มหรือเปลี่ยน ให้เพิ่ม v +0.0001
// หลังแก้ค่าแล้วต้อง copy ไป path ที่ Nginx ชี้ (หรือรัน copy-frogger-files-only.sh) ถึงจะเห็นบนเว็บ
window.APP_VERSION = '0.0183';
window.APP_VERSION = '0.0186';
document.addEventListener('DOMContentLoaded', function () {
var t = document.querySelector('.version-tag');
if (t) t.textContent = 'v ' + window.APP_VERSION;
+2 -2
View File
@@ -1896,8 +1896,8 @@
</div>
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0183"></script>
<script src="js/play.js?v=0.213"></script>
<script src="js/version.js?v=0.0184"></script>
<script src="js/play.js?v=0.218"></script>
<div class="version-tag">v —</div>
</body>
</html>
+28
View File
@@ -542,6 +542,7 @@ function defaultGameTiming() {
jumpSurviveMissionTimeSec: 0,
/** ยิงยานอวกาศ / space_shooter: จำกัดเวลารอบ (วินาที) — 0 = ไคลเอนต์ใช้ค่าเริ่ม 90; แมป spaceShooterTimeSec > 0 ทับ */
spaceShooterMissionTimeSec: 0,
spaceShooterShipImageUrls: ['', '', '', '', '', ''],
};
}
@@ -578,6 +579,25 @@ function clampSpaceShooterMissionTimeSec(n) {
return Math.max(10, Math.min(7200, Math.floor(v)));
}
/** รูปยาน space_shooter ช่อง 16 (ตรงกับ spawn slot บนแมป) — ว่าง = ไคลเอนต์วาดยานเวกเตอร์ */
function normalizeSpaceShooterShipImageUrls(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 clampGauntletTickMs(n) {
const v = Number(n);
if (Number.isNaN(v)) return GAUNTLET_DEFAULT_TICK_MS;
@@ -692,6 +712,7 @@ function loadGameTiming() {
spaceShooterMissionTimeSec: Object.prototype.hasOwnProperty.call(j, 'spaceShooterMissionTimeSec')
? clampSpaceShooterMissionTimeSec(j.spaceShooterMissionTimeSec)
: 0,
spaceShooterShipImageUrls: normalizeSpaceShooterShipImageUrls(j.spaceShooterShipImageUrls),
};
}
} catch (e) { console.error('loadGameTiming', e.message); }
@@ -744,6 +765,12 @@ function saveGameTimingToFile(d) {
const spaceShooterMissionSec = Object.prototype.hasOwnProperty.call(d, 'spaceShooterMissionTimeSec')
? clampSpaceShooterMissionTimeSec(d.spaceShooterMissionTimeSec)
: clampSpaceShooterMissionTimeSec(prevSpaceShooterMissionSec);
const prevShipUrls = Array.isArray(prev.spaceShooterShipImageUrls)
? normalizeSpaceShooterShipImageUrls(prev.spaceShooterShipImageUrls)
: normalizeSpaceShooterShipImageUrls([]);
const spaceShooterShipImageUrls = Object.prototype.hasOwnProperty.call(d, 'spaceShooterShipImageUrls')
? normalizeSpaceShooterShipImageUrls(d.spaceShooterShipImageUrls)
: prevShipUrls;
const out = {
gauntletTickMs: clampGauntletTickMs(d.gauntletTickMs),
gauntletJumpTicks: clampGauntletJumpTicks(d.gauntletJumpTicks),
@@ -754,6 +781,7 @@ function saveGameTimingToFile(d) {
jumpSurviveJumpHeightMult: jumpMult,
jumpSurviveMissionTimeSec: jumpMissionSec,
spaceShooterMissionTimeSec: spaceShooterMissionSec,
spaceShooterShipImageUrls,
};
fs.writeFileSync(GAME_TIMING_PATH, JSON.stringify(out, null, 2), 'utf8');
runtimeGameTiming = out;