minigame 1 question 1.4
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ถ้ามี) */
|
||||
|
||||
Reference in New Issue
Block a user