minigame 5 jumper 1.4
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 1–6)</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> > 0 จะใช้ค่าบนแมปแทนทั้งหมด · ตั้ง <code>spaceShooterTimeSec</code> = 0 บนแมป = ไม่จับเวลา</p>
|
||||
<div class="quiz-admin-actions">
|
||||
<button type="button" class="btn btn-primary" id="btn-space-shooter-save">บันทึก</button>
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
/** รูปยานช่อง 1–6 จาก 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,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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ช่อง 1–6 (ตรงกับ 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;
|
||||
|
||||
Reference in New Issue
Block a user