minigame 1 question 1.1

This commit is contained in:
2026-05-02 06:21:45 +00:00
parent 306637996e
commit e55dadf41c
46 changed files with 1021 additions and 66 deletions
File diff suppressed because one or more lines are too long
+8
View File
@@ -13,6 +13,14 @@
"questionFontMinPx": 10,
"questionFontMaxPx": 24
},
"quizMapPanelTheme": {
"panelBg": "rgba(12, 14, 28, 0.88)",
"panelBorder": "rgba(255, 214, 102, 0.7)",
"textColor": "rgba(255, 224, 102, 1)",
"borderWidthPx": 2,
"questionFontMinPx": 10,
"questionFontMaxPx": 28
},
"carryEmbedCountdownTheme": {
"overlayBackdrop": "rgba(8, 10, 20, 0)",
"innerBg": "rgba(12, 14, 28, 0)",
@@ -25,12 +25,12 @@ if (!is_array($data)) {
}
$out = [];
$keys = ['carryMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueTheme', 'carryChoicePlaqueMapScale', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'carryQuestions'];
$keys = ['carryMapPanelTheme', 'quizMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueTheme', 'carryChoicePlaqueMapScale', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'carryQuestions'];
foreach ($keys as $k) {
if (!array_key_exists($k, $data)) {
continue;
}
if ($k === 'carryMapPanelTheme' || $k === 'carryEmbedCountdownTheme' || $k === 'carryChoicePlaqueTheme') {
if ($k === 'carryMapPanelTheme' || $k === 'quizMapPanelTheme' || $k === 'carryEmbedCountdownTheme' || $k === 'carryChoicePlaqueTheme') {
if (is_array($data[$k])) {
$out[$k] = $data[$k];
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

@@ -0,0 +1,24 @@
QUESTION — รูปสำหรับฉาก / อัปโหลด
================================
ฉาก quiz รหัส **mng8a80o** ใช้ flow ภารกิจแบบ crown (HOWTO → READY → นับ 3-2-1 → เล่น → สรุป)
โหลดรูปจาก URL ประมาณ `/Game/img/QUESTION/` — ตั้งชื่อให้ตรงกับนี้ (เหมือนโฟลเดอร์ ViolentCrime):
- **popup-Howto.png** — พื้นหลังหน้า HOWTO / คำแนะนำก่อนเริ่ม
- **popup-result.png** — พื้นหลังแผงสรุปคะแนนท้ายเกม
- **hud-time-plaque.png** — (ทางเลือก) แผ่นตกแต่งด้านหลังตัวเลข **TIME** บน Cyber HUD กลางจอ ระหว่างเล่น
- **hud-question-plaque.png** — (ทางเลือก) แผ่นพื้นหลังข้อความคำถามใต้ TIME ตาม mock
ถ้าไฟล์หาย ระบบ fallback ไป `/Game/img/gauntlet-assets/` ชื่อเดียวกัน (เฉพาะ popup-*) · แผ่น HUD ถ้าไม่มีไฟล์จะใช้ข้อความล้วน
English: Map **mng8a80o** uses mission UI; place **popup-Howto.png** and **popup-result.png** here. Optional HUD plates: **hud-time-plaque.png**, **hud-question-plaque.png**.
โฟลเดอร์นี้ถูกสร้างจาก repo + สคริปต์ deploy เพื่อหลีกเลี่ยงข้อผิดพลาด FTP:
550 Create directory operation failed. (MKD)
ถ้า FTP ยัง 550: ตรวจว่าเชื่อมที่โฟลเดอร์ **public/img** ใต้ document root (เช่น
`/var/www/html/Game/public/img/QUESTION`) ไม่ใช่แค่ `/Game/...` บน chroot คนละระดับ
English: This folder is created on deploy with www-data + setgid (2775) so uploads work.
If MKD still fails, the directory may already exist — use LIST then STOR into QUESTION.
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because it is too large Load Diff
+2
View File
@@ -489,6 +489,7 @@
: null;
if (!bounds || !mapTransform || mapTransform.cw == null) {
panel.classList.add('is-hidden');
panel.classList.remove('quiz-map-question-panel--true-false');
panel.setAttribute('aria-hidden', 'true');
return;
}
@@ -509,6 +510,7 @@
panel.style.height = Math.round(Math.max(40, hPx)) + 'px';
panel.classList.remove('is-hidden');
panel.setAttribute('aria-hidden', 'false');
panel.classList.add('quiz-map-question-panel--true-false');
}
function drawLobbyMap() {
+73 -6
View File
@@ -224,6 +224,10 @@
border-radius: 12px;
box-shadow: var(--qmap-shadow);
}
/* เกมถูก/ผิด: จัดข้อความกึ่งกลางแนวตั้งในโซนทอง (quiz_carry ไม่ใส่คลาสนี้ — ยังใช้ไอคอน/คะแนนด้านล่าง) */
#quiz-map-question-panel.quiz-map-question-panel--true-false {
justify-content: center;
}
#quiz-map-question-panel.is-hidden { display: none !important; }
#quiz-map-question-panel::-webkit-scrollbar {
display: none;
@@ -245,6 +249,8 @@
flex: 0 1 auto;
min-height: 0;
max-height: 100%;
align-self: center;
text-align: center;
overflow: hidden;
overflow-wrap: anywhere;
word-break: break-word;
@@ -590,7 +596,9 @@
justify-self: end;
}
.play-quiz-scoreboard-me .play-quiz-scoreboard-val { color: #9ece6a; }
/* แจ้งผลรอบคำถามแบบ toast — ปิดการแสดง (กรอบกลางบนรบกวน HUD) โค้ดยังอัปเดตข้อความได้สำหรับ a11y/ดีบัก */
#play-quiz-feedback {
display: none !important;
position: fixed;
top: max(10px, env(safe-area-inset-top));
left: 50%;
@@ -1464,14 +1472,67 @@
margin-right: 0.15rem;
filter: drop-shadow(0 0 6px rgba(255, 220, 120, 0.7));
}
.play-cyber-time-block {
/* กลางบน: TIME + (เฉพาะแมป quiz mng8a80o) แถบคำถาม */
.play-cyber-center-stack {
position: absolute;
top: max(52px, calc(env(safe-area-inset-top) + 38px));
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: clamp(4px, 1vmin, 12px);
z-index: 4;
max-width: min(94vw, 760px);
pointer-events: none;
}
.play-cyber-time-block {
position: relative;
left: auto;
top: auto;
transform: none;
text-align: center;
min-width: 120px;
}
.play-cyber-quiz-mission-q-band {
position: relative;
width: min(92vw, 680px);
max-width: 100%;
padding: clamp(10px, 2vmin, 18px) clamp(12px, 2.5vmin, 22px);
box-sizing: border-box;
text-align: center;
}
/* is-hidden ใช้ทั่วโปรเจกต์แต่หลายจุดไม่มีกฎ display:none — แถบนี้ซ่อนแล้วต้องไม่กินที่และไม่เห็นกรอบว่าง */
#play-cyber-quiz-mission-q-band.is-hidden {
display: none !important;
}
.play-cyber-quiz-mission-q-plaque {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: fill;
object-position: center;
z-index: 0;
pointer-events: none;
opacity: 0.95;
}
.play-cyber-quiz-mission-q-text {
position: relative;
z-index: 1;
margin: 0;
font-family: 'Sarabun', 'Noto Sans Thai', system-ui, sans-serif;
font-size: clamp(0.78rem, 2.1vw, 1.05rem);
font-weight: 700;
line-height: 1.45;
color: #ffe066;
text-shadow:
0 0 12px rgba(255, 200, 80, 0.55),
0 2px 14px rgba(0, 0, 0, 0.85);
white-space: pre-line;
overflow-wrap: anywhere;
word-break: break-word;
}
.play-cyber-time-label {
font-family: 'Orbitron', sans-serif;
font-size: 0.62rem;
@@ -1786,10 +1847,16 @@
<div class="play-cyber-panel-title">SCORE</div>
<ul id="play-cyber-score-list" class="play-cyber-score-list"></ul>
</aside>
<div class="play-cyber-time-block">
<div class="play-cyber-time-label">TIME</div>
<div id="play-cyber-time-val" class="play-cyber-time-val">0</div>
<div id="play-cyber-time-sub" class="play-cyber-time-sub"></div>
<div class="play-cyber-center-stack">
<div class="play-cyber-time-block">
<div class="play-cyber-time-label">TIME</div>
<div id="play-cyber-time-val" class="play-cyber-time-val">0</div>
<div id="play-cyber-time-sub" class="play-cyber-time-sub"></div>
</div>
<div id="play-cyber-quiz-mission-q-band" class="play-cyber-quiz-mission-q-band is-hidden" aria-hidden="true">
<img id="play-cyber-quiz-mission-q-plaque" class="play-cyber-quiz-mission-q-plaque is-hidden" alt="" decoding="async" />
<p id="play-cyber-quiz-mission-q-text" class="play-cyber-quiz-mission-q-text"></p>
</div>
</div>
<div class="play-cyber-self">
<div class="play-cyber-self-frame">
@@ -1899,7 +1966,7 @@
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0184"></script>
<script src="js/play.js?v=0.231"></script>
<script src="js/play.js?v=0.238"></script>
<div class="version-tag">v —</div>
</body>
</html>
+5
View File
@@ -692,6 +692,9 @@
border-radius: 12px;
box-shadow: var(--qmap-shadow);
}
#quiz-map-question-panel.quiz-map-question-panel--true-false {
justify-content: center;
}
#quiz-map-question-panel.is-hidden {
display: none !important;
}
@@ -720,6 +723,8 @@
flex: 0 1 auto;
min-height: 0;
max-height: 100%;
align-self: center;
text-align: center;
overflow: hidden;
overflow-wrap: anywhere;
word-break: break-word;
+47 -5
View File
@@ -183,6 +183,8 @@ function defaultQuizSettings() {
carrySessionLength: 0,
/** quiz_carry: สีแผง #quiz-map-question-panel ในเกม/พรีวิว (จัดใน Admin แท็บหยิบมาวาง) */
carryMapPanelTheme: defaultCarryMapPanelTheme(),
/** เกมตอบคำถามถูก/ผิด: สีแผงคำถามบนแผนที่ (#quiz-map-question-panel) — Admin แท็บคำถามเกม */
quizMapPanelTheme: defaultCarryMapPanelTheme(),
/** quiz_carry: สี/ขอบ/ขนาดตัวเลขนับ 3-2-1 (embed พรีวิว) — Admin แท็บหยิบมาวาง */
carryEmbedCountdownTheme: defaultCarryEmbedCountdownTheme(),
/** quiz_carry: สีป้ายตัวเลือกบนแผนที่ (canvas) ต่อช่อง 116 */
@@ -194,6 +196,8 @@ function defaultQuizSettings() {
/** quiz_carry: ถ้าใส่รหัสฉาก + คูณ — เดินเร็วเฉพาะแมปนั้น (เทียบกับค่าเริ่มในไคลเอนต์) */
carryWalkSpeedMultForMapId: '',
carryWalkSpeedMult: null,
/** ถูก/ผิด: สุ่มกี่ข้อต่อรอบจากชุดคำถาม (สูงสุดเท่าที่มีในชุด) */
quizRoundQuestionCount: 10,
questions: [],
carryQuestions: [],
battleQuizMcq: [],
@@ -443,6 +447,17 @@ function clampCarrySessionLength(n, def) {
return Math.max(0, Math.min(500, Math.floor(v)));
}
/** เกมตอบคำถามถูก/ผิด: จำนวนข้อที่สุ่มจากชุดต่อรอบ (เซสชัน) — 1–50 */
function clampQuizRoundQuestionCount(n, def) {
const v = Number(n);
if (!Number.isFinite(v)) {
const d = Number(def);
if (!Number.isFinite(d)) return 10;
return Math.max(1, Math.min(50, Math.floor(d)));
}
return Math.max(1, Math.min(50, Math.floor(v)));
}
function clampCarryChoicePlaqueMapScale(n, def) {
const v = Number(n);
if (Number.isNaN(v)) return def;
@@ -475,6 +490,9 @@ function mergeQuizCarryFromMirrorIfSet(base) {
if (j.carryMapPanelTheme != null && typeof j.carryMapPanelTheme === 'object') {
base.carryMapPanelTheme = sanitizeCarryMapPanelTheme(j.carryMapPanelTheme);
}
if (j.quizMapPanelTheme != null && typeof j.quizMapPanelTheme === 'object') {
base.quizMapPanelTheme = sanitizeCarryMapPanelTheme(j.quizMapPanelTheme);
}
if (j.carryReadMs != null) {
base.carryReadMs = clampQuizBetweenMs(j.carryReadMs, base.carryReadMs);
}
@@ -504,6 +522,9 @@ function mergeQuizCarryFromMirrorIfSet(base) {
if (Array.isArray(j.carryQuestions) && j.carryQuestions.length) {
base.carryQuestions = sanitizeCarryQuestions(j.carryQuestions);
}
if (j.quizRoundQuestionCount != null) {
base.quizRoundQuestionCount = clampQuizRoundQuestionCount(j.quizRoundQuestionCount, base.quizRoundQuestionCount);
}
return base;
} catch (e) {
console.error('mergeQuizCarryFromMirrorIfSet', e.message);
@@ -520,6 +541,7 @@ function loadQuizSettings() {
const carryQuestions = sanitizeCarryQuestions(j.carryQuestions);
const battleQuizMcq = sanitizeBattleQuizMcq(j.battleQuizMcq);
const carryMapPanelTheme = sanitizeCarryMapPanelTheme(j.carryMapPanelTheme);
const quizMapPanelTheme = sanitizeCarryMapPanelTheme(j.quizMapPanelTheme);
const carryEmbedCountdownTheme = sanitizeCarryEmbedCountdownTheme(j.carryEmbedCountdownTheme);
const carryChoicePlaqueThemes = Array.isArray(j.carryChoicePlaqueThemes) && j.carryChoicePlaqueThemes.length
? sanitizeCarryChoicePlaqueThemes(j.carryChoicePlaqueThemes)
@@ -538,6 +560,7 @@ function loadQuizSettings() {
carryAnswerMs: clampQuizMs(j.carryAnswerMs, d.carryAnswerMs),
carrySessionLength: clampCarrySessionLength(j.carrySessionLength, d.carrySessionLength),
carryMapPanelTheme,
quizMapPanelTheme,
carryEmbedCountdownTheme,
carryChoicePlaqueThemes,
carryChoicePlaqueTheme,
@@ -1050,6 +1073,9 @@ function saveQuizSettings(d) {
const carryMapPanelTheme = d.carryMapPanelTheme != null && typeof d.carryMapPanelTheme === 'object'
? sanitizeCarryMapPanelTheme(d.carryMapPanelTheme)
: prev.carryMapPanelTheme;
const quizMapPanelTheme = d.quizMapPanelTheme != null && typeof d.quizMapPanelTheme === 'object'
? sanitizeCarryMapPanelTheme(d.quizMapPanelTheme)
: prev.quizMapPanelTheme;
const carryEmbedCountdownTheme = d.carryEmbedCountdownTheme != null && typeof d.carryEmbedCountdownTheme === 'object'
? sanitizeCarryEmbedCountdownTheme(d.carryEmbedCountdownTheme)
: prev.carryEmbedCountdownTheme;
@@ -1088,6 +1114,7 @@ function saveQuizSettings(d) {
carryAnswerMs,
carrySessionLength,
carryMapPanelTheme,
quizMapPanelTheme,
carryEmbedCountdownTheme,
carryChoicePlaqueThemes,
carryChoicePlaqueTheme,
@@ -1104,6 +1131,7 @@ function saveQuizSettings(d) {
battleQuizMcqSaved: (out.battleQuizMcq || []).length,
carryQuestionsSaved: (out.carryQuestions || []).length,
carryMapPanelTheme: out.carryMapPanelTheme,
quizMapPanelTheme: out.quizMapPanelTheme,
carryEmbedCountdownTheme: out.carryEmbedCountdownTheme,
carryChoicePlaqueThemes: out.carryChoicePlaqueThemes,
carryChoicePlaqueTheme: out.carryChoicePlaqueTheme,
@@ -1376,7 +1404,10 @@ function resolveQuizRound(sid, space, md) {
if (!right) {
st.cannotTrue = true;
st.cannotFalse = true;
const pos = quizWrongAnswerRespawnPosition(mdLive);
const joinOrd = typeof p.spawnJoinOrder === 'number' && Number.isFinite(p.spawnJoinOrder)
? Math.max(0, Math.floor(p.spawnJoinOrder))
: [...space.peers.keys()].indexOf(peerId);
const pos = quizWrongAnswerRespawnPosition(mdLive, joinOrd);
p.x = pos.x;
p.y = pos.y;
ejectMoves.push({
@@ -3312,9 +3343,10 @@ function pickRandomSpawnFromMap(md) {
return { x: pick.x, y: pick.y };
}
/** ตอบผิดในเกมคำถาม — กลับจุดเกิด (spawnArea / spawn) แล้วดีดออกนอกโซนถ้าจุดเกิดทับโซนตอบ */
function quizWrongAnswerRespawnPosition(md) {
const sp = pickRandomSpawnFromMap(md);
/** ตอบผิดในเกมคำถาม — กลับจุดเกิดตามลำดับเข้า (เดียวกับ pickSpawnForJoin) แล้วดีดออกนอกโซนถ้าทับโซนตอบ */
function quizWrongAnswerRespawnPosition(md, joinOrderIndex) {
const ord = joinOrderIndex | 0;
const sp = pickSpawnForJoin(md, ord);
return findNearestOutsideQuizAnswerZones(md, Number(sp.x) + 0.5, Number(sp.y) + 0.5);
}
@@ -3402,10 +3434,12 @@ io.on('connection', (socket) => {
if (!space.hostId) space.hostId = socket.id;
if (serverMapIsPostCaseLobbyB(space)) initTroublesomeState(space);
const mdJoin = (space.mapId && maps.get(space.mapId)) || space.mapData;
const spawnPt = pickSpawnForJoin(mdJoin, space.peers.size);
const spawnJoinOrder = space.peers.size;
const spawnPt = pickSpawnForJoin(mdJoin, spawnJoinOrder);
const bbStartBalloons = Math.max(1, Math.min(12, Math.floor(Number(mdJoin.balloonBossBalloonsPerPlayer)) || 5));
const peer = {
id: socket.id, x: +spawnPt.x, y: +spawnPt.y, direction: 'down', nickname: nickname || 'ผู้เล่น', ready: false, characterId: characterId || null, voiceMicOn: true,
spawnJoinOrder,
gauntletJumpTicks: 0, gauntletScore: 0, gauntletJumpPending: false, gauntletEliminated: false, spaceShooterScore: 0,
balloonBossScore: 0, balloonBossBalloons: mdJoin.gameType === 'balloon_boss' ? bbStartBalloons : 5, balloonBossEliminated: false,
};
@@ -3468,6 +3502,14 @@ io.on('connection', (socket) => {
};
} catch (e) { /* ignore */ }
}
if (mdJoin.gameType === 'quiz') {
try {
const qsQuiz = loadQuizSettings();
joinCb.quizSettingsSnap = {
quizMapPanelTheme: qsQuiz.quizMapPanelTheme,
};
} catch (e) { /* ignore */ }
}
if (mdJoin.gameType === 'gauntlet' && space.gauntletRun) {
joinCb.gauntletEndsAt = space.gauntletRun.endsAt != null ? space.gauntletRun.endsAt : null;
}