minigame 1 question 1.4

This commit is contained in:
2026-05-02 16:07:50 +00:00
parent 0f50ea58b8
commit c1b4086f30
8 changed files with 136 additions and 12 deletions
+2
View File
@@ -32,6 +32,8 @@ sudo /root/repos/justice/scripts/deploy-nginx.sh
sudo /root/repos/justice/scripts/deploy-www.sh
```
**ฉากแมป (`Game/data/maps/*.json`):** สคริปต์ deploy **ไม่ทับ** ไฟล์ที่มีอยู่บนเซิร์ฟแล้ว (กันหายหลังบันทึกจาก Editor) — คัดลอกจาก repo เฉพาะรหัสที่ยังไม่มีบนเซิร์ฟ · ถ้าต้องการอัปเดตฉากจาก repo ให้ลบไฟล์นั้นบนเซิร์ฟก่อน หรือรัน `save-www-from-etc.sh` เพื่อดึงจากเซิร์ฟเข้า repo แล้วค่อย deploy
กฎ Cursor: `.cursor/rules/sync-var-www-html.mdc` (และที่ workspace `/root`: `~/.cursor/rules/justice-sync-var-www-html.mdc`) ให้ช่วยเตือน/รันหลังแก้ `www/html`.
ดึงจากเซิร์ฟเวอร์เข้า repo + commit + push:
+20
View File
@@ -19,11 +19,31 @@ hash_game_server_js() {
}
GAME_SRV_HASH_BEFORE="$(hash_game_server_js "$GAME_SERVER_JS")"
# ไม่ rsync ทับ Game/data/maps — ฉากที่สร้าง/บันทึกบนเซิร์ฟ (Node saveMap) มักใหม่กว่าใน repo
# หลัง deploy คัดลอกเฉพาะ *.json ที่มีใน repo แต่ยังไม่มีบนเซิร์ฟ (seed รหัสใหม่) ไม่แตะไฟล์ที่มีอยู่แล้ว
MAPS_SRC="$SRC/Game/data/maps"
MAPS_DST="$DST/Game/data/maps"
rsync -a \
--checksum \
--exclude 'node_modules' \
--exclude 'Game/data/maps/' \
"$SRC" "$DST"
chown -R www-data:www-data "$DST"
if [[ -d "$MAPS_SRC" ]]; then
mkdir -p "$MAPS_DST"
shopt -s nullglob
for f in "$MAPS_SRC"/*.json; do
base=$(basename "$f")
if [[ ! -f "$MAPS_DST/$base" ]]; then
cp -a "$f" "$MAPS_DST/$base"
echo "OK: seeded map (missing on server) $base"
fi
done
shopt -u nullglob
chown -R www-data:www-data "$MAPS_DST"
fi
# รูปป้าย quiz_carry — ให้เขียน/ลบได้ชัดเจน (FTP/Node); setgid ให้ไฟล์ใหม่ได้กลุ่ม www-data
PLAQUE="$DST/Game/public/img/quiz-carry-plaque-assets"
mkdir -p "$PLAQUE"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -8,10 +8,12 @@ QUESTION — รูปสำหรับฉาก / อัปโหลด
- **popup-result.png** — พื้นหลังแผงสรุปคะแนนท้ายเกม
- **hud-time-plaque.png** — (ทางเลือก) แผ่นตกแต่งด้านหลังตัวเลข **TIME** บน Cyber HUD กลางจอ ระหว่างเล่น
- **hud-question-plaque.png** — (ทางเลือก) แผ่นพื้นหลังข้อความคำถามใต้ TIME ตาม mock
- **score+.png** — เอฟเฟกต์ “+10” กลางโซนคำตอบที่ถูก (SAFE หรือ SCAM) เมื่อมีผู้เล่นตอบถูก · คะแนนต่อข้อ = 10 แต้ม (สอดคล้อง `QUIZ_TF_POINTS_PER_CORRECT` บนเซิร์ฟเวอร์)
- **result-Lose_stamp.png** — แสตมป์ทับอวาตาร์ในหน้าสรุปภารกิจ (ฉาก quiz แบบ mission) สำหรับผู้เล่นที่เคยตอบผิดในรอบนั้น · ถ้าไฟล์หายระบบ fallback ไป `/Game/img/gauntlet-assets/result-Lose_stamp.png`
ถ้าไฟล์หาย ระบบ 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**.
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**. For correct-answer FX: **score+.png** (+10 popup over the correct zone). For mission summary wrong-answer stamp on avatars: **result-Lose_stamp.png** (falls back to gauntlet-assets if missing).
โฟลเดอร์นี้ถูกสร้างจาก repo + สคริปต์ deploy เพื่อหลีกเลี่ยงข้อผิดพลาด FTP:
+104 -7
View File
@@ -1037,6 +1037,17 @@
let previewQuizStep = 'read';
let previewQuizCurrent = null;
let playLiveQuizScores = {};
/** เกมถูก/ผิด (multiplayer): ใครเคยตอบผิดในเซสชันนี้ — ใช้โชว์แสตมป์ในสรุปภารกิจ mng8a80o */
let playQuizEverWrong = {};
/** เกมถูก/ผิด — ตรงกับ server QUIZ_TF_POINTS_PER_CORRECT */
const QUIZ_TF_POINTS_PER_CORRECT = 10;
const QUIZ_TF_SCORE_PLUS_URL = BASE + '/img/QUESTION/score+.png';
const QUIZ_TF_SCORE_POPUP_MS = 1700;
/** ขนาดป๊อปอัป score+ เทียบกับ tile — เดิม ~1.35×tile; +100% = 2× → 2.7×tile */
const QUIZ_TF_SCORE_POPUP_TILE_MULT = 2.7;
const QUIZ_TF_SCORE_POPUP_MIN_BASE_PX = 112;
let quizTfScorePopups = [];
let quizTfScorePlusImg = null;
/** คะแนนต่อคำตอบที่ถูกเมื่อส่งป้ายที่ฮับ/โซนส่ง — quiz_carry เท่านั้น */
const QUIZ_CARRY_POINTS_PER_CORRECT = 10;
/** quiz_carry: หยิบตัวเลือกมาวางโซนกลาง — เล่นพร้อมกัน (ไม่สลับตา) */
@@ -1218,6 +1229,7 @@
playLiveQuizScores = {};
playLiveQuizScores[myId] = 0;
others.forEach((_, id) => { playLiveQuizScores[id] = 0; });
if (isQuiz()) playQuizEverWrong = {};
renderPlayQuizScoreboard(playLiveQuizScores);
}
@@ -1241,7 +1253,7 @@
: '';
if (mine.right) {
el.className = 'play-quiz-feedback play-quiz-feedback-ok';
el.textContent = 'คุณตอบถูก · คะแนนรวม ' + (typeof mine.score === 'number' ? mine.score : 0) + ' แต้ม' + botExtra;
el.textContent = 'คุณตอบถูก · +' + QUIZ_TF_POINTS_PER_CORRECT + ' · คะแนนรวม ' + (typeof mine.score === 'number' ? mine.score : 0) + ' แต้ม' + botExtra;
} else {
el.className = 'play-quiz-feedback play-quiz-feedback-bad';
el.textContent = (mine.choice == null
@@ -1252,6 +1264,54 @@
window.__playQuizFeedbackT = setTimeout(() => { el.classList.add('is-hidden'); }, 4200);
}
function ensureQuizTfScorePlusImgPlay() {
if (quizTfScorePlusImg && quizTfScorePlusImg.src) return;
quizTfScorePlusImg = new Image();
quizTfScorePlusImg.src = QUIZ_TF_SCORE_PLUS_URL;
quizTfScorePlusImg.onerror = function () {
this.onerror = null;
};
}
/** โชว์ score+.png กลางโซนจริง/เท็จที่เป็นคำตอบข้อนี้ (world px) */
function spawnQuizTrueFalseScorePopupPlay(correctTrue) {
if (!isQuiz() || !mapData) return;
const grid = correctTrue ? mapData.quizTrueArea : mapData.quizFalseArea;
const b = getTileBoundsForOnesGrid(grid);
if (!b) return;
const ts = tileSize || 32;
const wx = ((b.minX + b.maxX + 1) / 2) * ts;
const wy = ((b.minY + b.maxY + 1) / 2) * ts;
const now = Date.now();
ensureQuizTfScorePlusImgPlay();
quizTfScorePopups.push({ wx, wy, startMs: now, untilMs: now + QUIZ_TF_SCORE_POPUP_MS });
}
function drawQuizTfScorePopupsLayer(ctx, worldToScreen, zDraw, timeMs) {
if (!quizTfScorePopups.length) return;
const img = quizTfScorePlusImg;
if (!img || !img.complete || !img.naturalWidth) return;
const now = typeof timeMs === 'number' ? timeMs : Date.now();
quizTfScorePopups = quizTfScorePopups.filter((p) => p.untilMs > now);
const baseW = Math.max(QUIZ_TF_SCORE_POPUP_MIN_BASE_PX, (tileSize || 32) * zDraw * QUIZ_TF_SCORE_POPUP_TILE_MULT);
for (let i = 0; i < quizTfScorePopups.length; i++) {
const p = quizTfScorePopups[i];
const span = Math.max(1, p.untilMs - p.startMs);
const t = Math.max(0, Math.min(1, (now - p.startMs) / span));
const [sx, sy] = worldToScreen(p.wx, p.wy);
const rise = t * 56 * zDraw;
const fade = t < 0.72 ? 1 : Math.max(0, 1 - (t - 0.72) / 0.28);
const pulse = 1 + Math.sin(t * Math.PI) * 0.1;
const scale = (baseW / img.naturalWidth) * pulse;
const w = img.naturalWidth * scale;
const h = img.naturalHeight * scale;
ctx.save();
ctx.globalAlpha = fade;
ctx.drawImage(img, sx - w / 2, sy - h / 2 - rise, w, h);
ctx.restore();
}
}
function getTileBoundsForOnesGrid(grid) {
if (!grid || !grid.length) return null;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
@@ -2722,7 +2782,7 @@
let right = mine.right;
let choice = mine.choice;
if (mine.right) {
playLiveQuizScores[myId] = (playLiveQuizScores[myId] || 0) + 1;
playLiveQuizScores[myId] = (playLiveQuizScores[myId] || 0) + QUIZ_TF_POINTS_PER_CORRECT;
} else {
playQuizPlayerLocal.cannotTrue = true;
playQuizPlayerLocal.cannotFalse = true;
@@ -2746,7 +2806,7 @@
o.botPath = [];
o.botAnswerWander = false;
if (br.right) {
playLiveQuizScores[id] = (playLiveQuizScores[id] || 0) + 1;
playLiveQuizScores[id] = (playLiveQuizScores[id] || 0) + QUIZ_TF_POINTS_PER_CORRECT;
} else {
o.quizCannotTrue = true;
o.quizCannotFalse = true;
@@ -2765,7 +2825,14 @@
});
}
const r = { results, scores: { ...playLiveQuizScores } };
results.forEach(function (row) {
if (!row || row.id == null) return;
if (!row.right) playQuizEverWrong[String(row.id)] = true;
});
renderPlayQuizScoreboard(playLiveQuizScores);
if (results.some(function (row) { return row && row.right; })) {
spawnQuizTrueFalseScorePopupPlay(correctTrue);
}
showPlayQuizFeedback(r);
}
@@ -2797,6 +2864,7 @@
async function loadPreviewQuizAndStart() {
resetPreviewBotsQuizState();
playQuizEverWrong = {};
let settings = null;
try {
const r = await fetch(BASE + '/api/quiz-settings?_=' + Date.now(), { cache: 'no-store' });
@@ -2876,6 +2944,8 @@
previewQuizCurrent = null;
previewQuizStep = 'read';
playLiveQuizScores = {};
playQuizEverWrong = {};
quizTfScorePopups = [];
playPath = [];
hidePlayQuizHud();
quizCarryJoinSettingsSnap = null;
@@ -2992,6 +3062,7 @@
playQuizTimerInterval = setInterval(updatePlayQuizTimerDisplay, 200);
updatePlayQuizTimerDisplay();
if (!previewMode && isQuiz() && !Object.keys(playLiveQuizScores).length) initPlayLiveQuizScoresZeros();
if (!previewMode && isQuiz() && p.phase === 'read' && Number(p.questionIndex) === 1) playQuizEverWrong = {};
}
function isFrogger() { return mapData && mapData.gameType === 'frogger'; }
@@ -3773,12 +3844,14 @@
function quizQuestionMissionBuildPayload() {
const rows = [];
function pushEnt(id, nickname, characterId, score) {
const sid = String(id);
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,
hadQuizWrong: !!playQuizEverWrong[sid],
rankBonus: 0,
finalScore: Math.max(0, Number(score) || 0),
});
@@ -3803,6 +3876,7 @@
characterId: row.characterId,
baseScore: row.baseScore,
eliminated: false,
hadQuizWrong: !!row.hadQuizWrong,
rank: pos,
rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos),
rankBonus: 0,
@@ -3842,6 +3916,7 @@
characterId: r.characterId,
baseScore: Math.max(0, Number(r.baseScore) || 0),
eliminated: false,
hadQuizWrong: !!r.hadQuizWrong,
rankBonus: 0,
finalScore: Math.max(0, Number(r.finalScore != null ? r.finalScore : r.baseScore) || 0),
};
@@ -3857,6 +3932,7 @@
characterId: o.characterId ?? null,
baseScore: Math.max(0, Number(playLiveQuizScores[id]) || 0),
eliminated: false,
hadQuizWrong: !!playQuizEverWrong[sid],
rankBonus: 0,
finalScore: Math.max(0, Number(playLiveQuizScores[id]) || 0),
});
@@ -3875,6 +3951,7 @@
characterId: row.characterId,
baseScore: bs,
eliminated: false,
hadQuizWrong: !!row.hadQuizWrong,
rank: pos,
rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos),
rankBonus: 0,
@@ -7086,16 +7163,26 @@
this.src = defaultAvatarImg.src;
};
frame.appendChild(img);
if (r.eliminated) {
const qMissionLose = mission && mission.uiSkin === 'question_mission' && r.hadQuizWrong;
if (r.eliminated || qMissionLose) {
const st = document.createElement('img');
st.className = 'gcm-stamp';
const jSkin = mission && mission.uiSkin === 'jumper';
st.src = jSkin ? jumperAssetUrl('stamp-sacrifice.png') : (BASE + '/img/gauntlet-assets/result-Lose_stamp.png');
st.alt = jSkin ? 'ผู้เสียสละ' : 'เสียชีวิต';
let loseUrl = BASE + '/img/gauntlet-assets/result-Lose_stamp.png';
let altLose = 'เสียชีวิต';
if (jSkin && r.eliminated) {
loseUrl = jumperAssetUrl('stamp-sacrifice.png');
altLose = 'ผู้เสียสละ';
} else if (qMissionLose) {
loseUrl = BASE + '/img/QUESTION/result-Lose_stamp.png';
altLose = 'ตอบผิด';
}
st.src = loseUrl;
st.alt = altLose;
st.onerror = function () {
this.onerror = null;
this.src = BASE + '/img/gauntlet-assets/result-Lose_stamp.png';
this.alt = 'เสียชีวิต';
this.alt = 'ตอบผิด';
};
frame.appendChild(st);
}
@@ -10187,6 +10274,15 @@
playLiveQuizScores = { ...r.scores };
renderPlayQuizScoreboard(playLiveQuizScores);
}
if (isQuiz() && Array.isArray(r.results)) {
r.results.forEach(function (row) {
if (!row || row.id == null) return;
if (!row.right) playQuizEverWrong[String(row.id)] = true;
});
}
if (isQuiz() && Array.isArray(r.results) && r.results.some(function (row) { return row && row.right; })) {
spawnQuizTrueFalseScorePopupPlay(!!r.correctTrue);
}
showPlayQuizFeedback(r);
});
@@ -12714,6 +12810,7 @@
if (previewMode && editorEmbedReturn && isQuizCarry() && quizCarryEmbedCountdownEndAt > Date.now()) {
syncQuizCarryEmbedCountdownLayout();
}
drawQuizTfScorePopupsLayer(ctx, worldToScreen, zDraw, timeMs);
syncPlayCyberHud();
syncQuizCarryGrabButton();
}
+1 -1
View File
@@ -1966,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.242"></script>
<script src="js/play.js?v=0.245"></script>
<div class="version-tag">v —</div>
</body>
</html>
+4 -1
View File
@@ -1300,6 +1300,9 @@ function clearSpaceQuizTimers(space) {
space.quizTimers = [];
}
/** เกมถูก/ผิด: คะแนนต่อข้อที่ตอบถูก (ตรงกับ client + เอฟเฟกต์ score+.png) */
const QUIZ_TF_POINTS_PER_CORRECT = 10;
function shuffleQuizQuestions(arr) {
const a = arr.slice();
for (let i = a.length - 1; i > 0; i--) {
@@ -1402,7 +1405,7 @@ function resolveQuizRound(sid, space, md) {
else right = (choice === correctTrue);
if (right) {
st.score = (typeof st.score === 'number' ? st.score : 0) + 1;
st.score = (typeof st.score === 'number' ? st.score : 0) + QUIZ_TF_POINTS_PER_CORRECT;
anyRight = true;
}
/* ผิดทุกกรณี = ล็อกโซนจริงเท็จ + กลับจุดเกิด (สุ่มใน spawnArea ถ้ามี) */