minigame 6 players position
@@ -26,12 +26,14 @@ sudo /root/repos/justice/scripts/deploy-nginx.sh
|
||||
|
||||
## เว็บ (`/var/www/html`)
|
||||
|
||||
หลังแก้ใน repo แล้วไป production:
|
||||
หลังแก้ใน repo แล้วไป production (**ทุกครั้ง** ที่ต้องการให้ `/var/www/html` ตรงกับ `www/html/` ใน repo — path คนละตัว ไม่ sync เอง):
|
||||
|
||||
```bash
|
||||
sudo /root/repos/justice/scripts/deploy-www.sh
|
||||
```
|
||||
|
||||
กฎ Cursor: `.cursor/rules/sync-var-www-html.mdc` (และที่ workspace `/root`: `~/.cursor/rules/justice-sync-var-www-html.mdc`) ให้ช่วยเตือน/รันหลังแก้ `www/html`.
|
||||
|
||||
ดึงจากเซิร์ฟเวอร์เข้า repo + commit + push:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -6,11 +6,22 @@ server {
|
||||
server_name srv1361159.hstgr.cloud;
|
||||
root /var/www/html;
|
||||
index index.php index.html index.htm;
|
||||
# กันค่าเริ่มต้น 1M ตัด POST (รูป base64 ผ่าน Admin API) — คู่กับบล็อก /Admin/api/ และ /Game/api/ ด้านล่าง
|
||||
client_max_body_size 20M;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# Admin API — JSON body ใหญ่ (รูป base64 อัปโหลดป้าย quiz_carry ฯลฯ) · ค่าเริ่มต้น nginx 1M ทำให้ POST ถูกตัดเงียบ ๆ
|
||||
location ~ ^/Admin/api/.+\.php$ {
|
||||
client_max_body_size 20M;
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
|
||||
@@ -44,6 +55,7 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# รูปป้าย quiz_carry อัปโหลดจาก Admin — เก็บใต้ Game/public/img/quiz-carry-plaque-assets/ (เสิร์ฟโดย location /Game/ ด้านล่าง)
|
||||
# Game (ZEP-like) — รูปตัวละครอยู่ที่ Node (data/characters)
|
||||
location /Game/img/characters/ {
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
|
||||
@@ -6,3 +6,4 @@ install -o root -g root -m 644 "$SRC" "$DST"
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
echo "OK: deployed $SRC -> $DST and nginx reloaded."
|
||||
echo "Tip: ถ้า POST อัปโหลดรูปผ่าน Admin API ยังว่าง body — ตั้ง PHP-FPM post_max_size ≥20M (เช่น pool www.conf) · If uploads still fail, raise PHP post_max_size to match nginx 20M."
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Deploy tracked web files from repo -> /var/www/html (ไม่ลบ node_modules บนเซิร์ฟเวอร์)
|
||||
# ใช้ทุกครั้งหลังแก้ www/html ใน repo แล้วต้องการให้เว็บจริงตรงกับ repo
|
||||
set -euo pipefail
|
||||
REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SRC="$REPO/www/html/"
|
||||
@@ -9,7 +10,13 @@ if [[ "$(id -u)" -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
rsync -a \
|
||||
--checksum \
|
||||
--exclude 'node_modules' \
|
||||
"$SRC" "$DST"
|
||||
chown -R www-data:www-data "$DST"
|
||||
# รูปป้าย quiz_carry — ให้เขียน/ลบได้ชัดเจน (FTP/Node); setgid ให้ไฟล์ใหม่ได้กลุ่ม www-data
|
||||
PLAQUE="$DST/Game/public/img/quiz-carry-plaque-assets"
|
||||
mkdir -p "$PLAQUE"
|
||||
chown www-data:www-data "$PLAQUE"
|
||||
chmod 2775 "$PLAQUE"
|
||||
echo "OK: deployed $SRC -> $DST (node_modules on server untouched)"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# ตั้งค่าโฟลเดอร์อัปโหลดรูปป้าย quiz_carry ให้ Node (www-data) อ่าน/เขียน/ลบได้
|
||||
# และ (ถ้าตั้งค่า) ให้บัญชี FTP เขียนได้ด้วย ACL
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash scripts/fix-quiz-carry-plaque-assets-permissions.sh
|
||||
# sudo WEBROOT=/var/www/html OWNER=www-data GROUP=www-data bash scripts/fix-quiz-carry-plaque-assets-permissions.sh
|
||||
# sudo FTP_USER=yourftpuser bash scripts/fix-quiz-carry-plaque-assets-permissions.sh # ต้องมี package acl
|
||||
#
|
||||
# English: Ensures Game/public/img/quiz-carry-plaque-assets is writable by the game (www-data).
|
||||
# Optional FTP_USER: setfacl so that Linux user can STOR/delete via FTP without 553.
|
||||
set -euo pipefail
|
||||
|
||||
WEBROOT="${WEBROOT:-/var/www/html}"
|
||||
OWNER="${OWNER:-www-data}"
|
||||
GROUP="${GROUP:-www-data}"
|
||||
DIR="$WEBROOT/Game/public/img/quiz-carry-plaque-assets"
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
echo "Run as root: sudo $0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DIR"
|
||||
chown "$OWNER:$GROUP" "$DIR"
|
||||
# rwxrwxr-x + setgid: ไฟล์ใหม่ได้กลุ่มเดียวกับโฟลเดอร์ (เหมาะถ้า FTP user อยู่กลุ่ม www-data)
|
||||
chmod 2775 "$DIR"
|
||||
|
||||
if [[ -n "${FTP_USER:-}" ]]; then
|
||||
if command -v setfacl >/dev/null 2>&1; then
|
||||
setfacl -m "u:${FTP_USER}:rwx" "$DIR"
|
||||
setfacl -d -m "u:${FTP_USER}:rwx" "$DIR"
|
||||
echo "ACL: user ${FTP_USER} has rwx on $DIR (default ACL for new files)."
|
||||
else
|
||||
echo "WARN: FTP_USER set but setfacl not installed. Install acl package or run: usermod -aG ${GROUP} ${FTP_USER}" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "OK: $DIR"
|
||||
ls -ldn "$DIR"
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# ทดสอบ POST /Game/api/quiz-carry-plaque-upload ที่ Node (ไม่ผ่าน PHP) — หลัง deploy รันบนเซิร์ฟ: bash scripts/test-plaque-upload-node.sh
|
||||
set -euo pipefail
|
||||
PORT="${GAME_NODE_PORT:-3001}"
|
||||
PNG_B64='iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='
|
||||
BODY=$(printf '%s' "{\"imageDataUrl\":\"data:image/png;base64,${PNG_B64}\"}")
|
||||
code=$(curl -sS -o /tmp/plaque-up.json -w '%{http_code}' -X POST "http://127.0.0.1:${PORT}/Game/api/quiz-carry-plaque-upload" \
|
||||
-H 'Content-Type: application/json' --data-binary "$BODY" || true)
|
||||
echo "HTTP $code"
|
||||
cat /tmp/plaque-up.json
|
||||
echo
|
||||
if [[ "$code" != "200" ]]; then
|
||||
echo "FAIL: expected 200" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q '"ok":true' /tmp/plaque-up.json; then
|
||||
echo "FAIL: expected ok:true in JSON" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: Node plaque upload accepts minimal PNG"
|
||||
@@ -25,3 +25,7 @@ TH / EN
|
||||
สิทธิ์โฟลเดอร์ (ถ้าเขียนไม่ได้):
|
||||
sudo chown -R www-data:www-data /var/www/html/Admin/private
|
||||
sudo chmod 750 /var/www/html/Admin/private
|
||||
|
||||
รูปป้าย quiz_carry (FTP 553 / Node อัปโหลดไม่ได้):
|
||||
sudo bash /path/to/repo/scripts/fix-quiz-carry-plaque-assets-permissions.sh
|
||||
# FTP user เขียนได้: sudo FTP_USER=ftpusername bash .../fix-quiz-carry-plaque-assets-permissions.sh
|
||||
|
||||
@@ -825,6 +825,19 @@ code {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.quiz-carry-slot-img {
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
padding: 0.32rem 0.45rem;
|
||||
margin-top: 0.25rem;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed var(--border);
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
color: var(--text);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.quiz-carry-correct-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -2185,3 +2198,76 @@ code {
|
||||
background: rgba(0, 35, 50, 0.5);
|
||||
box-shadow: 0 0 12px rgba(0, 242, 255, 0.12);
|
||||
}
|
||||
|
||||
/* quiz_carry — ป้ายต่อช่อง: โหมด neon ไม่ใช้สีขอบ fixed */
|
||||
.quiz-carry-plaque-slots-root .quiz-carry-theme-grid.quiz-carry-plaque-grid--neon [data-quiz-carry-plaque-color-row='fixed'] {
|
||||
opacity: 0.48;
|
||||
}
|
||||
|
||||
.quiz-carry-plaque-slots-root .quiz-carry-plaque-neon-hint {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.quiz-carry-plaque-slots-root .quiz-carry-plaque-image-row {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.quiz-carry-plaque-slots-root .quiz-carry-plaque-image-row .admin-inp-text {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.quiz-carry-plaque-slots-root .quiz-carry-plaque-preview-err {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.quiz-carry-plaque-slots-root .quiz-carry-plaque-preview-img {
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* quiz_carry — ปรับขนาดป้ายบนแมป (ทั่วทั้งแมป) */
|
||||
.quiz-carry-plaque-map-scale-panel {
|
||||
margin: 0 0 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid rgba(122, 200, 255, 0.45);
|
||||
border-radius: var(--radius);
|
||||
background: rgba(12, 22, 40, 0.55);
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2) inset;
|
||||
}
|
||||
.quiz-carry-plaque-map-scale-panel__title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: #c8e6ff;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.quiz-carry-plaque-map-scale-panel__row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 0.75rem 1.25rem;
|
||||
}
|
||||
.quiz-carry-plaque-map-scale-range-wrap {
|
||||
flex: 1 1 220px;
|
||||
min-width: 180px;
|
||||
}
|
||||
.quiz-carry-plaque-map-scale-range-wrap input[type='range'] {
|
||||
width: 100%;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
.quiz-carry-plaque-map-scale-label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.quiz-carry-plaque-map-scale-out {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #9fe8a8;
|
||||
font-variant-numeric: tabular-nums;
|
||||
padding-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Proxy อัปโหลดรูปป้าย quiz_carry → Node POST /Game/api/quiz-carry-plaque-upload
|
||||
* ใช้เมื่อแอดมินอยู่คนละ path / nginx ไม่ส่ง POST ไป Node — คล้าย game-quiz-settings.php
|
||||
*/
|
||||
require __DIR__ . '/_common.php';
|
||||
|
||||
require_login();
|
||||
|
||||
if (!function_exists('curl_init')) {
|
||||
json_response(['ok' => false, 'error' => 'ต้องเปิด PHP extension curl · Enable php-curl'], 500);
|
||||
}
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
if ($method !== 'POST') {
|
||||
json_response(['ok' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$raw = file_get_contents('php://input');
|
||||
if ($raw === false) {
|
||||
$raw = '';
|
||||
}
|
||||
$contentLen = isset($_SERVER['CONTENT_LENGTH']) ? (int) $_SERVER['CONTENT_LENGTH'] : 0;
|
||||
if ($raw === '' && $contentLen > 0) {
|
||||
json_response([
|
||||
'ok' => false,
|
||||
'error' => 'รับ body ไม่ได้ (มักเกิดจาก nginx client_max_body_size หรือ PHP post_max_size เล็กเกินไป) — ต้องตั้งอย่างน้อย ~20M สำหรับ /Admin/api/*.php · Request body was discarded (raise nginx body limit + PHP post_max_size)',
|
||||
], 413);
|
||||
}
|
||||
|
||||
$port = preg_replace('/[^0-9]/', '', (string)(getenv('GAME_NODE_PORT') ?: '3001')) ?: '3001';
|
||||
$host = getenv('GAME_NODE_INTERNAL_HOST') ?: '127.0.0.1';
|
||||
$target = 'http://' . $host . ':' . $port . '/Game/api/quiz-carry-plaque-upload';
|
||||
|
||||
$ch = curl_init($target);
|
||||
if ($ch === false) {
|
||||
json_response(['ok' => false, 'error' => 'curl init failed'], 500);
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $raw,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Accept: application/json',
|
||||
'Content-Type: application/json',
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 120,
|
||||
]);
|
||||
|
||||
$out = curl_exec($ch);
|
||||
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($out === false || $out === '') {
|
||||
json_response([
|
||||
'ok' => false,
|
||||
'error' => 'ไม่ต่อถึงเซิร์ฟเวอร์เกม (Node) — ตรวจ systemd/pm2 และพอร์ต ' . $port . ' · ' . $err,
|
||||
], 502);
|
||||
}
|
||||
|
||||
http_response_code($code > 0 ? $code : 500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: no-store');
|
||||
echo $out;
|
||||
@@ -52,7 +52,7 @@ function merge_quiz_settings_disk_from_put(string $rawBody): bool
|
||||
if ($hadFileButInvalidJson) {
|
||||
return false;
|
||||
}
|
||||
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'questions', 'carryQuestions', 'battleQuizMcq'];
|
||||
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueMapScale', 'questions', 'carryQuestions', 'battleQuizMcq'];
|
||||
foreach ($mergeKeys as $k) {
|
||||
if (array_key_exists($k, $patch)) {
|
||||
$base[$k] = $patch[$k];
|
||||
@@ -99,7 +99,11 @@ function overlay_quiz_settings_from_disk(string $nodeBody): string
|
||||
if (array_key_exists('carryMapPanelTheme', $disk) && is_array($disk['carryMapPanelTheme'])) {
|
||||
$j['carryMapPanelTheme'] = $disk['carryMapPanelTheme'];
|
||||
}
|
||||
foreach (['carryReadMs', 'carryAnswerMs', 'carrySessionLength'] as $ck) {
|
||||
if (array_key_exists('carryEmbedCountdownTheme', $disk) && is_array($disk['carryEmbedCountdownTheme'])) {
|
||||
$j['carryEmbedCountdownTheme'] = $disk['carryEmbedCountdownTheme'];
|
||||
}
|
||||
/* ไม่ทับ carryChoicePlaqueThemes / carryChoicePlaqueTheme จากดิสก์ — ใช้ค่าจาก Node (อ่านไฟล์เดียวกันกับ disk merge) เพื่อกันทับด้วย JSON เก่าที่ไม่มี plaqueImageUrl ทำให้ช่อง URL ใน Admin ว่างหลังรีเฟรช */
|
||||
foreach (['carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryChoicePlaqueMapScale'] as $ck) {
|
||||
if (array_key_exists($ck, $disk)) {
|
||||
$j[$ck] = $disk[$ck];
|
||||
}
|
||||
@@ -169,6 +173,15 @@ function proxy_quiz_curl(string $method, ?string $body = null): void
|
||||
if (isset($dj['carryMapPanelTheme']) && is_array($dj['carryMapPanelTheme'])) {
|
||||
$payload['carryMapPanelTheme'] = $dj['carryMapPanelTheme'];
|
||||
}
|
||||
if (isset($dj['carryEmbedCountdownTheme']) && is_array($dj['carryEmbedCountdownTheme'])) {
|
||||
$payload['carryEmbedCountdownTheme'] = $dj['carryEmbedCountdownTheme'];
|
||||
}
|
||||
if (isset($dj['carryChoicePlaqueThemes']) && is_array($dj['carryChoicePlaqueThemes'])) {
|
||||
$payload['carryChoicePlaqueThemes'] = $dj['carryChoicePlaqueThemes'];
|
||||
}
|
||||
if (isset($dj['carryChoicePlaqueTheme']) && is_array($dj['carryChoicePlaqueTheme'])) {
|
||||
$payload['carryChoicePlaqueTheme'] = $dj['carryChoicePlaqueTheme'];
|
||||
}
|
||||
if (array_key_exists('carryReadMs', $dj)) {
|
||||
$payload['carryReadMs'] = $dj['carryReadMs'];
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ function merge_quiz_settings_disk_from_put(string $rawBody): bool
|
||||
}
|
||||
}
|
||||
}
|
||||
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'questions', 'carryQuestions', 'battleQuizMcq'];
|
||||
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'carryEmbedCountdownTheme', 'questions', 'carryQuestions', 'battleQuizMcq'];
|
||||
foreach ($mergeKeys as $k) {
|
||||
if (array_key_exists($k, $patch)) {
|
||||
$base[$k] = $patch[$k];
|
||||
@@ -149,6 +149,9 @@ function proxy_quiz_curl(string $method, ?string $body = null): void
|
||||
if (isset($dj['carryMapPanelTheme']) && is_array($dj['carryMapPanelTheme'])) {
|
||||
$payload['carryMapPanelTheme'] = $dj['carryMapPanelTheme'];
|
||||
}
|
||||
if (isset($dj['carryEmbedCountdownTheme']) && is_array($dj['carryEmbedCountdownTheme'])) {
|
||||
$payload['carryEmbedCountdownTheme'] = $dj['carryEmbedCountdownTheme'];
|
||||
}
|
||||
if (array_key_exists('carryReadMs', $dj)) {
|
||||
$payload['carryReadMs'] = $dj['carryReadMs'];
|
||||
}
|
||||
|
||||
@@ -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=15">
|
||||
<link rel="stylesheet" href="admin.css?v=18">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#admin-main">ข้ามไปเนื้อหา</a>
|
||||
@@ -183,6 +183,7 @@
|
||||
<fieldset class="quiz-carry-theme-fieldset" style="margin:0.75rem 0 1rem;padding:0.75rem 1rem;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-elevated)">
|
||||
<legend class="quiz-carry-theme-legend" style="font-size:0.92rem;font-weight:600;padding:0 0.35rem">แผงข้อความบนแผนที่ (คำถาม / ตัวเลือกที่ถือ)</legend>
|
||||
<p class="muted" style="margin:0 0 0.65rem;font-size:0.82rem;line-height:1.45">ใช้กับ <code>#quiz-map-question-panel</code> ในโหมดหยิบมาวาง (โซนทองหรือโซนกลาง) · เก็บที่ <code>carryMapPanelTheme</code> ใน <code>quiz-settings.json</code> · <em>English:</em> Color pickers + alpha (0–100%) — saved as <code>rgba()</code>.</p>
|
||||
<p class="muted" style="margin:-0.35rem 0 0.65rem;padding:0.5rem 0.65rem;border-radius:8px;border:1px solid rgba(255,180,80,0.35);background:rgba(255,200,120,0.08);font-size:0.8rem;line-height:1.5"><strong>ทำไมไม่เห็นเปลี่ยน?</strong> แผงนี้แสดงเฉพาะในหน้า <strong>เล่นเกม</strong> (<code>/Game/play.html</code>) ขณะแมปเป็น <strong>quiz_carry</strong> และมีข้อความคำถาม — ไม่แสดงในหน้า Admin นี้ · ถ้า <strong>โปร่ง A = 0%</strong> กับพื้นหลังหรือขอบ = โปร่งใสทั้งหมด จะไม่เห็นสีนั้น · ความหนาขอบ <strong>0 px</strong> = ไม่มีเส้นขอบ · ลองโปร่ง 70–90% และขอบ 2 px แล้วเปิดพรีวิว/เล่น · <em>English:</em> This panel is only on <strong>play</strong> (not Admin). <strong>0% alpha</strong> = fully transparent color; <strong>0 px</strong> border = no stroke.</p>
|
||||
<div class="quiz-carry-theme-grid">
|
||||
<div class="quiz-carry-theme-row" data-quiz-carry-theme="bg">
|
||||
<span class="quiz-carry-theme-label">พื้นหลังแผง</span>
|
||||
@@ -227,7 +228,99 @@
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="quiz-carry-theme-fieldset" style="margin:0.75rem 0 1rem;padding:0.75rem 1rem;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-elevated)">
|
||||
<legend class="quiz-carry-theme-legend" style="font-size:0.92rem;font-weight:600;padding:0 0.35rem">ป้ายตัวเลือกบนแผนที่ (กล่องหยิบมาวาง) — ช่อง 1–16</legend>
|
||||
<p class="muted" style="margin:0 0 0.65rem;font-size:0.82rem;line-height:1.45">แต่ละช่องตรงกับ <strong>โซนตัวเลือก 1–16</strong> บนแมป · เก็บที่ <code>carryChoicePlaqueThemes</code> · <strong>Neon</strong> = ขอบตามเลขช่อง · <strong>สีขอบคงที่</strong> = ใช้ picker ด้านล่าง · รูปช่องใช้เมื่อคำถามนั้นไม่มีรูปแยก · รูปคำถามทับช่อง · <strong>ขนาดป้ายบนแมป</strong> ปรับที่กล่องสีเขียวถัดลงนี้ (ไม่ใช่ในแต่ละช่อง) · <em>English:</em> Per-slot = colors/URL; map size = green panel below; Save quiz-carry.</p>
|
||||
<div class="quiz-carry-plaque-map-scale-panel" role="region" aria-labelledby="quiz-carry-plaque-map-scale-title">
|
||||
<div class="quiz-carry-plaque-map-scale-panel__title" id="quiz-carry-plaque-map-scale-title">ปรับขนาดป้าย + รูปบนแมป</div>
|
||||
<p class="muted" style="margin:0.35rem 0 0.65rem;font-size:0.82rem;line-height:1.45">เลื่อนแถบหรือพิมพ์ตัวเลข (×) — ขยายทั้งกล่องป้ายและตัวอักษรบนแมป · จบด้วยปุ่ม <strong>บันทึกชุดหลายตัวเลือก</strong> ด้านล่าง · <em>English:</em> Slider or number, then Save.</p>
|
||||
<div class="quiz-carry-plaque-map-scale-panel__row">
|
||||
<label class="admin-field quiz-carry-plaque-map-scale-range-wrap"><span class="quiz-carry-plaque-map-scale-label">เลื่อนขยาย</span>
|
||||
<input type="range" id="quiz-carry-plaque-map-scale-range" min="85" max="250" step="1" value="125" aria-label="ขยายป้ายบนแมป 0.85 ถึง 2.5 เท่า" />
|
||||
</label>
|
||||
<label class="admin-field"><span>ค่า ×</span>
|
||||
<input type="number" id="quiz-carry-plaque-map-scale" class="admin-inp-num" min="0.85" max="2.5" step="0.05" value="1.25" title="คูณขนาดกล่องป้าย + ฟอนต์บนแผนที่" aria-describedby="quiz-carry-plaque-map-scale-hint" />
|
||||
</label>
|
||||
<span id="quiz-carry-plaque-map-scale-out" class="quiz-carry-plaque-map-scale-out" aria-live="polite">× 1.25</span>
|
||||
</div>
|
||||
<span id="quiz-carry-plaque-map-scale-hint" class="muted" style="font-size:0.76rem;display:block;margin-top:0.45rem"><code>carryChoicePlaqueMapScale</code> · 1 = ปกติ · 1.5–2.0 ถ้าต้องการใหญ่ชัด</span>
|
||||
</div>
|
||||
<div id="quiz-carry-plaque-slots-root" class="quiz-carry-plaque-slots-root" aria-label="ธีมป้ายตัวเลือกทีละช่อง"></div>
|
||||
</fieldset>
|
||||
<fieldset class="quiz-carry-theme-fieldset" style="margin:0.75rem 0 1rem;padding:0.75rem 1rem;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-elevated)">
|
||||
<legend class="quiz-carry-theme-legend" style="font-size:0.92rem;font-weight:600;padding:0 0.35rem">นับถอยหลัง 3-2-1 (พรีวิว embed)</legend>
|
||||
<p class="muted" style="margin:0 0 0.65rem;font-size:0.82rem;line-height:1.45">ใช้กับ <code>#quiz-carry-embed-countdown</code> ตอนพรีวิวจากเอดิเตอร์ · เก็บที่ <code>carryEmbedCountdownTheme</code> ใน <code>quiz-settings.json</code> · <em>English:</em> Overlay, inner card, digit color, and size (map = grid box / screen = center).</p>
|
||||
<div class="quiz-carry-theme-grid">
|
||||
<div class="quiz-carry-theme-row" data-quiz-carry-ecd="overlay">
|
||||
<span class="quiz-carry-theme-label">ม่านมืดเต็มจอ</span>
|
||||
<div class="quiz-carry-color-controls">
|
||||
<input type="color" id="quiz-carry-ecd-overlay-swatch" value="#080a14" title="สีม่าน" aria-label="สีม่านพื้นหลัง" />
|
||||
<label class="quiz-carry-alpha-label"><span>โปร่ง A</span>
|
||||
<input type="range" id="quiz-carry-ecd-overlay-alpha" min="0" max="100" value="42" step="1" aria-label="ความทึบม่าน" />
|
||||
<output id="quiz-carry-ecd-overlay-alpha-out" class="quiz-carry-alpha-out" for="quiz-carry-ecd-overlay-alpha">42%</output>
|
||||
</label>
|
||||
<code id="quiz-carry-ecd-overlay-preview" class="quiz-carry-theme-code" aria-hidden="true"></code>
|
||||
</div>
|
||||
<input type="hidden" id="quiz-carry-ecd-overlay-val" value="" />
|
||||
</div>
|
||||
<div class="quiz-carry-theme-row" data-quiz-carry-ecd="innerBg">
|
||||
<span class="quiz-carry-theme-label">พื้นหลังกล่องตัวเลข</span>
|
||||
<div class="quiz-carry-color-controls">
|
||||
<input type="color" id="quiz-carry-ecd-inner-bg-swatch" value="#0c0e1c" title="พื้นหลังกล่อง" aria-label="พื้นหลังกล่องนับถอยหลัง" />
|
||||
<label class="quiz-carry-alpha-label"><span>โปร่ง A</span>
|
||||
<input type="range" id="quiz-carry-ecd-inner-bg-alpha" min="0" max="100" value="82" step="1" aria-label="โปร่งพื้นหลังกล่อง" />
|
||||
<output id="quiz-carry-ecd-inner-bg-alpha-out" class="quiz-carry-alpha-out" for="quiz-carry-ecd-inner-bg-alpha">82%</output>
|
||||
</label>
|
||||
<code id="quiz-carry-ecd-inner-bg-preview" class="quiz-carry-theme-code" aria-hidden="true"></code>
|
||||
</div>
|
||||
<input type="hidden" id="quiz-carry-ecd-inner-bg-val" value="" />
|
||||
</div>
|
||||
<div class="quiz-carry-theme-row" data-quiz-carry-ecd="innerBorder">
|
||||
<span class="quiz-carry-theme-label">สีขอบกล่อง</span>
|
||||
<div class="quiz-carry-color-controls">
|
||||
<input type="color" id="quiz-carry-ecd-inner-border-swatch" value="#7aa2f7" title="สีขอบ" aria-label="สีขอบกล่อง" />
|
||||
<label class="quiz-carry-alpha-label"><span>โปร่ง A</span>
|
||||
<input type="range" id="quiz-carry-ecd-inner-border-alpha" min="0" max="100" value="45" step="1" aria-label="โปร่งขอบ" />
|
||||
<output id="quiz-carry-ecd-inner-border-alpha-out" class="quiz-carry-alpha-out" for="quiz-carry-ecd-inner-border-alpha">45%</output>
|
||||
</label>
|
||||
<code id="quiz-carry-ecd-inner-border-preview" class="quiz-carry-theme-code" aria-hidden="true"></code>
|
||||
</div>
|
||||
<input type="hidden" id="quiz-carry-ecd-inner-border-val" value="" />
|
||||
</div>
|
||||
<div class="quiz-carry-theme-row quiz-carry-theme-row--narrow" style="flex-wrap:wrap;gap:0.75rem 1.25rem">
|
||||
<label class="admin-field quiz-carry-theme-label">ความหนาขอบกล่อง (px)
|
||||
<input type="number" id="quiz-carry-ecd-inner-border-w" class="admin-inp-num" min="0" max="12" step="1" value="1" />
|
||||
</label>
|
||||
<label class="admin-field quiz-carry-theme-label">มุมโค้งกล่อง (px)
|
||||
<input type="number" id="quiz-carry-ecd-inner-radius" class="admin-inp-num" min="0" max="32" step="1" value="12" />
|
||||
</label>
|
||||
<label class="admin-field quiz-carry-theme-label">สีตัวเลข
|
||||
<input type="color" id="quiz-carry-ecd-digit-swatch" value="#ffe066" title="สีตัวเลข 3-2-1" aria-label="สีตัวเลขนับถอยหลัง" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="quiz-carry-theme-row quiz-carry-theme-row--narrow" style="flex-wrap:wrap;gap:0.75rem 1.25rem">
|
||||
<label class="admin-field quiz-carry-theme-label">ขนาดบนแมป — cqmin (% ของช่อง)
|
||||
<input type="number" id="quiz-carry-ecd-map-cqmin" class="admin-inp-num" min="35" max="100" step="1" value="78" title="ยิ่งมากตัวเลขใหญ่ขึ้นในกรอบช่อง" />
|
||||
</label>
|
||||
<label class="admin-field quiz-carry-theme-label">cqh
|
||||
<input type="number" id="quiz-carry-ecd-map-cqh" class="admin-inp-num" min="35" max="100" step="1" value="82" />
|
||||
</label>
|
||||
<label class="admin-field quiz-carry-theme-label">สูงสุด (px)
|
||||
<input type="number" id="quiz-carry-ecd-map-max-px" class="admin-inp-num" min="48" max="400" step="1" value="200" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="quiz-carry-theme-row quiz-carry-theme-row--narrow" style="flex-wrap:wrap;gap:0.75rem 1.25rem">
|
||||
<label class="admin-field quiz-carry-theme-label">กลางจอ — vw
|
||||
<input type="number" id="quiz-carry-ecd-screen-vw" class="admin-inp-num" min="6" max="44" step="1" value="28" title="ความกว้างตัวเลขเทียบ viewport" />
|
||||
</label>
|
||||
<label class="admin-field quiz-carry-theme-label">สูงสุด (px)
|
||||
<input type="number" id="quiz-carry-ecd-screen-max-px" class="admin-inp-num" min="48" max="220" step="1" value="132" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<h3 class="admin-subheading">รายการคำถาม</h3>
|
||||
<p class="muted" style="margin:0.35rem 0 0.65rem;font-size:0.8rem;line-height:1.45"><strong>รูปป้ายแต่ละช่อง:</strong> เลือกไฟล์แล้วกด «บันทึก» ได้เลย — ระบบจะ<strong>รอให้อัปโหลดจบ</strong>แล้วค่อยบันทึก URL ลงเซิร์ฟ (หรือรอเห็น «อัปโหลดแล้ว» ก่อนก็ได้) · ปุ่มบันทึก<strong>ไม่</strong>ส่งไฟล์แทนการอัปโหลด · <em>English:</em> Pick plaque file then Save — we wait for upload to finish before writing URLs (Save does not replace the upload step).</p>
|
||||
<div id="quiz-carry-admin-list" class="quiz-carry-admin-list"></div>
|
||||
<div class="quiz-admin-actions">
|
||||
<button type="button" class="btn btn-ghost" id="btn-quiz-carry-add">+ เพิ่มคำถาม</button>
|
||||
@@ -568,6 +661,6 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="admin.js?v=31"></script>
|
||||
<script src="admin.js?v=47"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,6 +2,166 @@
|
||||
"readMs": 30000,
|
||||
"answerMs": 5000,
|
||||
"betweenMs": 3000,
|
||||
"carryReadMs": 3000,
|
||||
"carryAnswerMs": 5000,
|
||||
"carrySessionLength": 10,
|
||||
"carryMapPanelTheme": {
|
||||
"panelBg": "rgba(255, 0, 0, 0)",
|
||||
"panelBorder": "rgba(255, 255, 255, 0)",
|
||||
"textColor": "rgba(241, 245, 249, 1)",
|
||||
"borderWidthPx": 0
|
||||
},
|
||||
"carryEmbedCountdownTheme": {
|
||||
"overlayBackdrop": "rgba(8, 10, 20, 0.42)",
|
||||
"innerBg": "rgba(12, 14, 28, 0.82)",
|
||||
"innerBorder": "rgba(122, 162, 247, 0.45)",
|
||||
"innerBorderWpx": 1,
|
||||
"innerRadiusPx": 12,
|
||||
"digitColor": "#ffe066",
|
||||
"mapDigitCqmin": 78,
|
||||
"mapDigitCqh": 82,
|
||||
"mapDigitMaxPx": 200,
|
||||
"screenDigitVw": 28,
|
||||
"screenDigitMaxPx": 132
|
||||
},
|
||||
"carryChoicePlaqueThemes": [
|
||||
{
|
||||
"borderMode": "fixed",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0)",
|
||||
"fillBg": "rgba(12, 10, 20, 0)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-1.png"
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
},
|
||||
{
|
||||
"borderMode": "neon",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0.9)",
|
||||
"fillBg": "rgba(12, 10, 20, 0.88)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": ""
|
||||
}
|
||||
],
|
||||
"carryChoicePlaqueTheme": {
|
||||
"borderMode": "fixed",
|
||||
"fixedBorder": "rgba(122, 200, 255, 0)",
|
||||
"fillBg": "rgba(12, 10, 20, 0)",
|
||||
"textColor": "rgba(248, 249, 255, 1)",
|
||||
"borderWidthPx": 2.5,
|
||||
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-1.png"
|
||||
},
|
||||
"questions": [
|
||||
{
|
||||
"text": "test1",
|
||||
@@ -118,13 +278,5 @@
|
||||
"correctIndex": 0
|
||||
}
|
||||
],
|
||||
"carryReadMs": 3000,
|
||||
"carryAnswerMs": 5000,
|
||||
"carrySessionLength": 10,
|
||||
"carryMapPanelTheme": {
|
||||
"panelBg": "rgba(255, 0, 0, 0)",
|
||||
"panelBorder": "rgba(255, 255, 255, 0)",
|
||||
"textColor": "rgba(241, 245, 249, 1)",
|
||||
"borderWidthPx": 0
|
||||
}
|
||||
"carryChoicePlaqueMapScale": 1.25
|
||||
}
|
||||
@@ -25,12 +25,18 @@ if (!is_array($data)) {
|
||||
}
|
||||
|
||||
$out = [];
|
||||
$keys = ['carryMapPanelTheme', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryQuestions'];
|
||||
$keys = ['carryMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueTheme', 'carryChoicePlaqueMapScale', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryQuestions'];
|
||||
foreach ($keys as $k) {
|
||||
if (!array_key_exists($k, $data)) {
|
||||
continue;
|
||||
}
|
||||
if ($k === 'carryMapPanelTheme') {
|
||||
if ($k === 'carryMapPanelTheme' || $k === 'carryEmbedCountdownTheme' || $k === 'carryChoicePlaqueTheme') {
|
||||
if (is_array($data[$k])) {
|
||||
$out[$k] = $data[$k];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($k === 'carryChoicePlaqueThemes') {
|
||||
if (is_array($data[$k])) {
|
||||
$out[$k] = $data[$k];
|
||||
}
|
||||
|
||||
@@ -33,6 +33,22 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="game-type-hint editor-card__hint" id="game-type-hint" role="note" aria-labelledby="editor-card-game-label"></div>
|
||||
<div id="editor-quiz-carry-countdown-wrap" class="editor-card__row editor-card__row--wrap" style="display:none;gap:0.65rem 1rem;align-items:center;margin-top:0.45rem;" aria-label="หยิบมาวาง พรีวิวนับถอยหลัง">
|
||||
<label class="editor-field-nogrow" style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;">
|
||||
<input type="checkbox" id="carry-embed-countdown-highlight" checked>
|
||||
<span>พรีวิว: ไฮไลต์โซนตัวเลือกตอนนับ 3-2-1</span>
|
||||
</label>
|
||||
<label class="editor-field-nogrow">สีขอบไฮไลต์
|
||||
<input type="color" id="carry-embed-countdown-color" value="#ffb44a" title="สีขอบเรืองรอบช่องตัวเลือก (ตรงกับโซนที่วาดเป็น ตัวเลือก 1–16 บนกริด)">
|
||||
</label>
|
||||
<label class="editor-field-grow">ตำแหน่งเลข 3-2-1
|
||||
<select id="carry-embed-countdown-anchor" title="ตามกริด = วาดช่องในโหมด «ตำแหน่งเลข 3-2-1» · อัตโนมัติ = โซนทองหรือโซนกลาง">
|
||||
<option value="grid">ตามกริดที่วาด (โหมดวาดด้านล่าง)</option>
|
||||
<option value="map" selected>บนแมป — โซนทอง (คำถาม) หรือโซนกลาง</option>
|
||||
<option value="screen">กลางจอ (ม่านทึบ)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="editor-card" aria-labelledby="editor-card-map-label">
|
||||
@@ -68,6 +84,7 @@
|
||||
<option value="cellImage">รูปบนกริด (อัปโหลด / เลือกจากคลัง)</option>
|
||||
<option value="interactive">โซน interactive (จุดกด/ใช้ได้)</option>
|
||||
<option value="spawnArea" id="draw-mode-option-spawn-area">พื้นที่สุ่มจุดเกิด (ฟ้า — ผู้เล่นใหม่สุ่มในช่องนี้)</option>
|
||||
<option value="lobbyPlayerSpawn" id="draw-mode-option-lobby-spawn" hidden>จุดเกิดผู้เล่น 1–6 (ลำดับเข้า P1…P6)</option>
|
||||
<option value="startGame" id="draw-mode-option-start-game">พื้นที่เริ่มเกม (host ยืนแล้วกดเริ่มได้)</option>
|
||||
<option value="color">สีช่อง (ทาสีบนกริด)</option>
|
||||
</optgroup>
|
||||
@@ -115,6 +132,7 @@
|
||||
<option value="quizCarryOpt14" class="quiz-carry-mode-opt" hidden>ตัวเลือก 14</option>
|
||||
<option value="quizCarryOpt15" class="quiz-carry-mode-opt" hidden>ตัวเลือก 15</option>
|
||||
<option value="quizCarryOpt16" class="quiz-carry-mode-opt" hidden>ตัวเลือก 16</option>
|
||||
<option value="carryEmbedCountdown" class="quiz-carry-mode-opt" hidden>ตำแหน่งเลข 3-2-1 (กริด cyan · พรีวิว embed)</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</label>
|
||||
@@ -160,6 +178,26 @@
|
||||
</label>
|
||||
<button type="button" id="btn-clear-shooter-spawns" hidden title="ล้างจุดเกิดยานทั้งหมด">ล้างจุดเกิดยาน</button>
|
||||
<button type="button" id="btn-clear-balloon-boss-spawns" hidden title="ล้างจุดเกิดผู้เล่น + บอส">ล้างจุดเกิดบอสโหมด</button>
|
||||
<div id="lobby-spawn-wrap" class="editor-card__row" style="display:none;flex-wrap:wrap;gap:0.35rem 0.75rem;align-items:center;width:100%;">
|
||||
<label class="editor-field-grow" title="ใช้เมื่อเข้าห้อง/ทดลองเล่น (ไม่รวมพรมแดง·ยิงยาน·บอส·กบที่มีจุดเกิดแยก)">จุดเกิดผู้เล่น
|
||||
<select id="lobby-spawn-mode">
|
||||
<option value="random">สุ่มในพื้นที่ฟ้า — ไม่มีฟ้าใช้ปุ่มตั้งจุดเกิด</option>
|
||||
<option value="fixed">คงที่ — จุดเดียว (ปุ่มตั้งจุดเกิด)</option>
|
||||
<option value="slots6">6 จุดตามลำดับเข้า (P1…P6)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label id="lobby-slot-paint-wrap" class="editor-field-grow" style="display:none;" title="คนแรกที่เข้าห้อง = P1 ตามลำดับ join">ช่องเกิด P
|
||||
<select id="lobby-slot-paint">
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
<option value="6">6</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" id="btn-clear-lobby-spawns" hidden title="ล้างจุด P1–P6 บนแผนที่">ล้างจุด P1–P6</button>
|
||||
<button type="button" id="btn-spawn">ตั้งจุดเกิด</button>
|
||||
</div>
|
||||
<div class="editor-card__row editor-card__row--actions">
|
||||
@@ -191,7 +229,7 @@
|
||||
<li><strong>พื้นที่สุ่มจุดเกิด</strong> (ฟ้าอ่อน) — ไม่วาดช่องนี้ได้ ใช้ปุ่ม «ตั้งจุดเกิด»</li>
|
||||
<li><strong>พื้นที่เริ่มเกม</strong> (ห้องโถง — ส้ม) — host ยืนแล้วกดเริ่ม</li>
|
||||
<li><strong>ถามตอบ</strong>: โซนถูก/ผิด + พื้นที่คำถาม (ทอง)</li>
|
||||
<li><strong>หยิบมาวาง</strong>: ม่วง = โซนกลาง (กำแพง) · <strong>ทอง</strong> = พื้นที่โชว์ข้อความคำถามบนแผนที่ (ถ้าไม่วาดทอง ข้อความไปที่โซนม่วง) · <strong>interactive เขียว</strong> = ยืนแล้วกด F ส่งคำตอบ · สี = ตัวเลือก 1–16 · <code>quizQuestions</code> ใส่ <code>choices</code> + <code>correctIndex</code> หรือ <code>answerTrue</code></li>
|
||||
<li><strong>หยิบมาวาง</strong>: ม่วง = โซนกลาง (กำแพง) · <strong>ทอง</strong> = พื้นที่โชว์ข้อความคำถามบนแผนที่ (ถ้าไม่วาดทอง ข้อความไปที่โซนม่วง) · <strong>interactive เขียว</strong> = ยืนแล้วกด F ส่งคำตอบ · โหมดวาด <strong>ตัวเลือก 1–16</strong> = ช่องบนกริดตรงกับลำดับ <code>choices</code> · พรีวิวนับ <strong>3-2-1</strong> ไฮไลต์โซนข้อถูก (หรือใส่ <code>countdownHighlightSlot</code> 1–16 ต่อข้อใน <code>quizQuestions</code>) · <code>correctIndex</code> / <code>answerTrue</code></li>
|
||||
<li><strong>Stack</strong>: โหมดวาดจุดปล่อย (ฟ้า) · จุดซ้อนตึก (ชมพู)</li>
|
||||
<li><strong>กระโดดให้รอด</strong>: โหมด <strong>แพลตฟอร์ม</strong> (ฟ้าอมเขียว) · กำแพง = ขอบซ้ายขวา/บน · Space / W = กระโดด</li>
|
||||
<li><strong>ลูกโป้งยิงบอส</strong>: จุดเกิดผู้เล่น P1–P6 + <strong>จุดเกิดบอส</strong> 1 ช่อง · ในเกมเดินช้า ยิงมีดีเลย์ · ลูกโป้งหมด = ตกรอบ</li>
|
||||
@@ -207,7 +245,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/version.js?v=0.0169"></script>
|
||||
<script src="js/editor.js?v=20260428-play-cache-bust"></script>
|
||||
<script src="js/editor.js?v=20260427-lobby-spawn-mode"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
After Width: | Height: | Size: 70 B |
|
After Width: | Height: | Size: 70 B |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 60 KiB |
@@ -41,7 +41,7 @@
|
||||
}
|
||||
|
||||
function quizCarryEditorCarryModes() {
|
||||
const m = ['quizCarryHub'];
|
||||
const m = ['quizCarryHub', 'carryEmbedCountdown'];
|
||||
for (let n = 1; n <= QUIZ_CARRY_EDITOR_OPT_COUNT; n++) m.push('quizCarryOpt' + n);
|
||||
return m;
|
||||
}
|
||||
@@ -69,6 +69,8 @@
|
||||
let ground = [], objects = [], blockPlayer = [], interactive = [], startGameArea = [], spawnArea = [], spawn = { x: 1, y: 1 };
|
||||
let quizTrueArea = [], quizFalseArea = [], quizQuestionArea = [];
|
||||
let quizCarryHubArea = [], quizCarryOptionArea = [];
|
||||
/** quiz_carry embed: ช่อง = 1 วางกล่องนับ 3-2-1 บนแมปเมื่อเลือก anchor = grid */
|
||||
let carryEmbedCountdownArea = [];
|
||||
let quizBattleDomeArea = [];
|
||||
/** quiz_battle: กริด 0/1 — ถ้ามีอย่างน้อย 1 ช่อง = ในเกมเดินได้เฉพาะบนเส้นทาง */
|
||||
let quizBattlePathArea = [];
|
||||
@@ -78,6 +80,8 @@
|
||||
let jumpSurvivePlatformArea = [];
|
||||
let cellColors = [];
|
||||
let gauntletPlayerSpawns = [];
|
||||
/** ล็อบบี้ / ZEP / quiz_carry / ฯลฯ — P1..P6 ตามลำดับ join เมื่อเลือกโหมด slots6 */
|
||||
let lobbyPlayerSpawns = [null, null, null, null, null, null];
|
||||
/** space_shooter: กริด 0 หรือ 1–6 = ลำดับผู้เล่น P1–P6 */
|
||||
let shooterSpawnSlots = [];
|
||||
/** balloon_boss: กริด 0 หรือ 1–6 = จุดเกิดผู้เล่น · บอส = tile เดียว */
|
||||
@@ -221,6 +225,18 @@
|
||||
if (!quizQuestionArea[y] || quizQuestionArea[y].length !== width) quizQuestionArea[y] = Array(width).fill(0);
|
||||
}
|
||||
}
|
||||
if (!carryEmbedCountdownArea.length || carryEmbedCountdownArea.length !== height) {
|
||||
const existingCd = carryEmbedCountdownArea.slice().map(r => r && r.slice());
|
||||
carryEmbedCountdownArea = [];
|
||||
for (let y = 0; y < height; y++) {
|
||||
const row = existingCd[y] && existingCd[y].length === width ? existingCd[y].slice() : Array(width).fill(0);
|
||||
carryEmbedCountdownArea.push(row);
|
||||
}
|
||||
} else {
|
||||
for (let y = 0; y < height; y++) {
|
||||
if (!carryEmbedCountdownArea[y] || carryEmbedCountdownArea[y].length !== width) carryEmbedCountdownArea[y] = Array(width).fill(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureQuizBattleDomeArea() {
|
||||
@@ -349,6 +365,44 @@
|
||||
.slice(0, 6);
|
||||
}
|
||||
|
||||
function supportsLobbySpawnPaint(gt) {
|
||||
return gt === 'zep' || gt === 'lobby' || gt === 'quiz' || gt === 'quiz_carry' || gt === 'stack' || gt === 'jump_survive' || gt === 'quiz_battle';
|
||||
}
|
||||
|
||||
function ensureLobbyPlayerSpawnsLength() {
|
||||
while (lobbyPlayerSpawns.length < 6) lobbyPlayerSpawns.push(null);
|
||||
lobbyPlayerSpawns.length = 6;
|
||||
}
|
||||
|
||||
function sanitizeLobbyPlayerSpawnsInEditor() {
|
||||
ensureLobbyPlayerSpawnsLength();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const s = lobbyPlayerSpawns[i];
|
||||
if (!s) continue;
|
||||
const x = Math.floor(Number(s.x));
|
||||
const y = Math.floor(Number(s.y));
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || x >= width || y < 0 || y >= height
|
||||
|| (objects[y] && objects[y][x] === 1)) {
|
||||
lobbyPlayerSpawns[i] = null;
|
||||
} else {
|
||||
lobbyPlayerSpawns[i] = { x, y };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function syncLobbySpawnAuxUi() {
|
||||
const gt = gameTypeEl ? gameTypeEl.value : gameType;
|
||||
const wrap = document.getElementById('lobby-spawn-wrap');
|
||||
const slotWrap = document.getElementById('lobby-slot-paint-wrap');
|
||||
const btnClr = document.getElementById('btn-clear-lobby-spawns');
|
||||
const modeEl = document.getElementById('lobby-spawn-mode');
|
||||
const use = supportsLobbySpawnPaint(gt);
|
||||
if (wrap) wrap.style.display = use ? 'flex' : 'none';
|
||||
const slotsMode = !!(use && modeEl && modeEl.value === 'slots6');
|
||||
if (slotWrap) slotWrap.style.display = slotsMode ? '' : 'none';
|
||||
if (btnClr) btnClr.hidden = !slotsMode;
|
||||
}
|
||||
|
||||
function ensureSpawnArea() {
|
||||
if (!spawnArea.length || spawnArea.length !== height) {
|
||||
const existing = spawnArea.slice().map(r => r && r.slice());
|
||||
@@ -651,6 +705,8 @@
|
||||
if (dqStackL) dqStackL.hidden = gt !== 'stack';
|
||||
const showCarry = gt === 'quiz_carry';
|
||||
dqCarryOpts.forEach(function (node) { node.hidden = !showCarry; });
|
||||
const carryCdWrap = document.getElementById('editor-quiz-carry-countdown-wrap');
|
||||
if (carryCdWrap) carryCdWrap.style.display = showCarry ? 'flex' : 'none';
|
||||
if (dqBattleDome) {
|
||||
dqBattleDome.hidden = gt !== 'quiz_battle';
|
||||
if (gt !== 'quiz_battle' && drawModeEl && drawModeEl.value === 'quizBattleDome') drawModeEl.value = 'wall';
|
||||
@@ -727,7 +783,8 @@
|
||||
'<strong>โซนกลาง (ม่วง)</strong> — กำแพง · ถ้าไม่วาดโซนทองด้านล่าง ข้อความคำถามจะโชว์บนโซนม่วงในเกม',
|
||||
'<strong>พื้นที่โชว์ข้อความคำถาม (ทอง)</strong> — วาดโซนทองแยกได้ · ในเกมข้อความจะอยู่ตรงโซนที่วาด',
|
||||
'<strong>โซน interactive (เขียว)</strong> — ยืนบนหรือชิดขอบแล้วกด <kbd>F</kbd> ส่งคำตอบ (วาดอย่างน้อย 1 ช่อง — ถ้าไม่วาดจะใช้ชิดโซนกลางแทน)',
|
||||
'<strong>ตัวเลือก 1–16</strong> — ตรงกับลำดับ <code>choices</code> ใน Admin หรือแมป',
|
||||
'<strong>ตัวเลือก 1–16</strong> — วาดบนกริดให้ตรงลำดับ <code>choices</code> (Admin / แมป)',
|
||||
'พรีวิวเอดิเตอร์: ช่วงนับ <strong>3-2-1</strong> — เลือกตำแหน่ง (กลางจอ / บนแมปอัตโนมัติที่โซนทองหรือโซนกลาง / <strong>ตามกริด</strong> โหมดวาด «ตำแหน่งเลข 3-2-1») · ไฮไลต์ข้อถูกด้านบน · ต่อข้อ <code>countdownHighlightSlot</code> (1–16)',
|
||||
'เล่นพร้อมกันได้ · คำถามจากแมปหรือ Admin',
|
||||
]);
|
||||
}
|
||||
@@ -864,6 +921,13 @@
|
||||
if (gt !== 'jump_survive' && drawModeEl && drawModeEl.value === 'jumpSurvivePlatform') drawModeEl.value = 'wall';
|
||||
}
|
||||
if (drawModeEl && drawModeEl.value === 'shooterSpawnPaint' && gt !== 'space_shooter') drawModeEl.value = 'wall';
|
||||
const lobbySpawnOpt = document.getElementById('draw-mode-option-lobby-spawn');
|
||||
if (lobbySpawnOpt) {
|
||||
const useLobby = supportsLobbySpawnPaint(gt);
|
||||
lobbySpawnOpt.hidden = !useLobby;
|
||||
if (!useLobby && drawModeEl && drawModeEl.value === 'lobbyPlayerSpawn') drawModeEl.value = 'wall';
|
||||
}
|
||||
syncLobbySpawnAuxUi();
|
||||
}
|
||||
|
||||
function initGrid() {
|
||||
@@ -881,6 +945,7 @@
|
||||
quizQuestionArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizCarryHubArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizCarryOptionArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
carryEmbedCountdownArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizBattleDomeArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizBattlePathArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
stackReleaseArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
@@ -903,6 +968,9 @@
|
||||
if (gameType === 'gauntlet' || gameType === 'stack' || gameType === 'quiz_carry' || gameType === 'quiz_battle' || gameType === 'jump_survive' || gameType === 'space_shooter' || gameType === 'balloon_boss') lanes = [];
|
||||
jumpSurvivePlatforms = [];
|
||||
gauntletPlayerSpawns = [];
|
||||
lobbyPlayerSpawns = [null, null, null, null, null, null];
|
||||
const lsmInit = document.getElementById('lobby-spawn-mode');
|
||||
if (lsmInit) lsmInit.value = 'random';
|
||||
const fpNew = readCharacterFootprintInputs();
|
||||
applyCharacterFootprintInputs(fpNew.cw, fpNew.ch);
|
||||
mapW.value = width; mapH.value = height; tileSizeEl.value = tileSize;
|
||||
@@ -914,6 +982,7 @@
|
||||
canvas.width = width * tileSize;
|
||||
canvas.height = height * tileSize;
|
||||
sanitizeGauntletPlayerSpawns();
|
||||
sanitizeLobbyPlayerSpawnsInEditor();
|
||||
ensureShooterSpawnSlots();
|
||||
ensureBalloonBossPlayerSlots();
|
||||
sanitizeSpritesInPlace();
|
||||
@@ -1190,6 +1259,19 @@
|
||||
ctx.fillText(lab, tx + tileSize / 2, ty + tileSize / 2 + 4);
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
if (carryEmbedCountdownArea[y] && Number(carryEmbedCountdownArea[y][x]) === 1) {
|
||||
ctx.fillStyle = 'rgba(100, 230, 255, 0.32)';
|
||||
ctx.fillRect(tx + 4, ty + 4, tileSize - 8, tileSize - 8);
|
||||
ctx.strokeStyle = 'rgba(80, 220, 255, 0.88)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(tx + 4, ty + 4, tileSize - 8, tileSize - 8);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.fillStyle = '#1a3a44';
|
||||
ctx.font = 'bold 9px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('321', tx + tileSize / 2, ty + tileSize / 2 + 3);
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
}
|
||||
if (gtDraw === 'quiz_battle' && quizBattlePathArea[y] && quizBattlePathArea[y][x] === 1) {
|
||||
ctx.fillStyle = 'rgba(186, 130, 255, 0.4)';
|
||||
@@ -1333,6 +1415,34 @@
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText('ลูกโป้งยิงบอส — P1–P6 + โหมด BOSS 1 ช่อง · ปิดโชว์กริดในเกมให้เหมือนรีเฟอเรนซ์', 8, 20);
|
||||
}
|
||||
if (supportsLobbySpawnPaint(gtDraw)) {
|
||||
ensureLobbyPlayerSpawnsLength();
|
||||
let anyLobbyP = false;
|
||||
for (let ii = 0; ii < 6; ii++) {
|
||||
if (lobbyPlayerSpawns[ii]) { anyLobbyP = true; break; }
|
||||
}
|
||||
if (anyLobbyP) {
|
||||
const drawLobbyPNum = (s, num) => {
|
||||
if (!s || s.x < 0 || s.x >= width || s.y < 0 || s.y >= height) return;
|
||||
if (objects[s.y][s.x] === 1) return;
|
||||
const cx = s.x * tileSize + tileSize / 2;
|
||||
const cy = s.y * tileSize + tileSize / 2 + 4;
|
||||
ctx.fillStyle = 'rgba(0,80,120,0.55)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy - 3, 12, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#b4f9f8';
|
||||
ctx.font = 'bold 13px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('P' + String(num), cx, cy + 2);
|
||||
ctx.textAlign = 'left';
|
||||
};
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const s = lobbyPlayerSpawns[i];
|
||||
if (s) drawLobbyPNum(s, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
const fpHud = readCharacterFootprintInputs();
|
||||
ctx.fillStyle = '#c0caf5';
|
||||
ctx.font = '12px sans-serif';
|
||||
@@ -1384,6 +1494,38 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (drawModeEl.value === 'lobbyPlayerSpawn') {
|
||||
const gt = gameTypeEl ? gameTypeEl.value : gameType;
|
||||
if (!supportsLobbySpawnPaint(gt)) return;
|
||||
const slotEl = document.getElementById('lobby-slot-paint');
|
||||
const slotOne = parseInt(slotEl && slotEl.value, 10) || 1;
|
||||
const slot = Math.max(0, Math.min(5, slotOne - 1));
|
||||
ensureLobbyPlayerSpawnsLength();
|
||||
if (objects[y][x] === 1) {
|
||||
if (statusEl) statusEl.textContent = 'ช่องกำแพง — ใช้เป็นจุดเกิดไม่ได้';
|
||||
return;
|
||||
}
|
||||
if (left) {
|
||||
for (let k = 0; k < 6; k++) {
|
||||
if (k === slot) continue;
|
||||
const o = lobbyPlayerSpawns[k];
|
||||
if (o && o.x === x && o.y === y) lobbyPlayerSpawns[k] = null;
|
||||
}
|
||||
lobbyPlayerSpawns[slot] = { x, y };
|
||||
if (statusEl) statusEl.textContent = 'จุดเกิด P' + (slot + 1) + ' = (' + x + ',' + y + ')';
|
||||
} else {
|
||||
let cleared = false;
|
||||
for (let k = 0; k < 6; k++) {
|
||||
const o = lobbyPlayerSpawns[k];
|
||||
if (o && o.x === x && o.y === y) {
|
||||
lobbyPlayerSpawns[k] = null;
|
||||
cleared = true;
|
||||
}
|
||||
}
|
||||
if (statusEl) statusEl.textContent = cleared ? 'ถอดจุด P ที่ช่องนี้แล้ว' : 'ไม่มีจุด P ที่ช่องนี้';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (drawModeEl.value === 'cellImage') {
|
||||
if (objects[y][x] === 1) {
|
||||
if (statusEl) statusEl.textContent = 'ช่องกำแพง — วางรูปบนกริดไม่ได้';
|
||||
@@ -1455,6 +1597,7 @@
|
||||
} else if (gtq === 'quiz_carry') {
|
||||
ensureQuizCarryAreas();
|
||||
quizQuestionArea[y][x] = left ? 1 : 0;
|
||||
if (left) carryEmbedCountdownArea[y][x] = 0;
|
||||
} else return;
|
||||
} else if (drawModeEl.value === 'stackRelease') {
|
||||
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'stack') return;
|
||||
@@ -1534,11 +1677,24 @@
|
||||
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz_battle') return;
|
||||
ensureQuizBattleDomeArea();
|
||||
quizBattleDomeArea[y][x] = left ? 1 : 0;
|
||||
} else if (drawModeEl.value === 'carryEmbedCountdown') {
|
||||
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz_carry') return;
|
||||
ensureQuizCarryAreas();
|
||||
if (left) {
|
||||
carryEmbedCountdownArea[y][x] = 1;
|
||||
quizCarryHubArea[y][x] = 0;
|
||||
quizCarryOptionArea[y][x] = 0;
|
||||
} else {
|
||||
carryEmbedCountdownArea[y][x] = 0;
|
||||
}
|
||||
} else if (drawModeEl.value === 'quizCarryHub') {
|
||||
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz_carry') return;
|
||||
ensureQuizCarryAreas();
|
||||
quizCarryHubArea[y][x] = left ? 1 : 0;
|
||||
if (left) quizCarryOptionArea[y][x] = 0;
|
||||
if (left) {
|
||||
quizCarryOptionArea[y][x] = 0;
|
||||
carryEmbedCountdownArea[y][x] = 0;
|
||||
}
|
||||
} else {
|
||||
const carryOptM = /^quizCarryOpt(\d+)$/.exec(drawModeEl.value);
|
||||
if (carryOptM && (gameTypeEl ? gameTypeEl.value : gameType) === 'quiz_carry') {
|
||||
@@ -1548,6 +1704,7 @@
|
||||
if (left) {
|
||||
quizCarryOptionArea[y][x] = val;
|
||||
quizCarryHubArea[y][x] = 0;
|
||||
carryEmbedCountdownArea[y][x] = 0;
|
||||
} else {
|
||||
quizCarryOptionArea[y][x] = 0;
|
||||
}
|
||||
@@ -1642,6 +1799,7 @@
|
||||
if (drawModeEl && drawModeEl.value === 'balloonBossPlayerPaint' && gameType !== 'balloon_boss') drawModeEl.value = 'wall';
|
||||
if (drawModeEl && drawModeEl.value === 'balloonBossBossPaint' && gameType !== 'balloon_boss') drawModeEl.value = 'wall';
|
||||
if (drawModeEl && (drawModeEl.value === 'quizBattleDome' || drawModeEl.value === 'quizBattlePath') && gameType !== 'quiz_battle') drawModeEl.value = 'wall';
|
||||
if (drawModeEl && drawModeEl.value === 'lobbyPlayerSpawn' && !supportsLobbySpawnPaint(gameType)) drawModeEl.value = 'wall';
|
||||
toggleFroggerUI();
|
||||
syncGridImageBrushInputCaps();
|
||||
});
|
||||
@@ -1655,6 +1813,27 @@
|
||||
});
|
||||
}
|
||||
|
||||
const lobbySpawnModeEl = document.getElementById('lobby-spawn-mode');
|
||||
if (lobbySpawnModeEl) {
|
||||
lobbySpawnModeEl.addEventListener('change', () => {
|
||||
if (lobbySpawnModeEl.value !== 'slots6' && drawModeEl && drawModeEl.value === 'lobbyPlayerSpawn') drawModeEl.value = 'wall';
|
||||
syncLobbySpawnAuxUi();
|
||||
draw();
|
||||
});
|
||||
}
|
||||
const btnClearLobbySpawns = document.getElementById('btn-clear-lobby-spawns');
|
||||
if (btnClearLobbySpawns) {
|
||||
btnClearLobbySpawns.addEventListener('click', () => {
|
||||
lobbyPlayerSpawns = [null, null, null, null, null, null];
|
||||
if (statusEl) statusEl.textContent = 'ล้างจุดเกิด P1–P6 แล้ว';
|
||||
draw();
|
||||
});
|
||||
}
|
||||
const lobbySlotPaintEl = document.getElementById('lobby-slot-paint');
|
||||
if (lobbySlotPaintEl) {
|
||||
lobbySlotPaintEl.addEventListener('change', () => { draw(); });
|
||||
}
|
||||
|
||||
const btnClearShooterSpawns = document.getElementById('btn-clear-shooter-spawns');
|
||||
if (btnClearShooterSpawns) {
|
||||
btnClearShooterSpawns.addEventListener('click', () => {
|
||||
@@ -1749,9 +1928,39 @@
|
||||
characterCellsW: fpSave.cw,
|
||||
characterCellsH: fpSave.ch,
|
||||
ground, objects, blockPlayer, interactive, startGameArea, spawnArea, spawn, cellColors, showMapInGame,
|
||||
lobbySpawnMode: (() => {
|
||||
const el = document.getElementById('lobby-spawn-mode');
|
||||
const v = el && el.value;
|
||||
return (v === 'fixed' || v === 'slots6') ? v : 'random';
|
||||
})(),
|
||||
lobbyPlayerSpawns: (() => {
|
||||
ensureLobbyPlayerSpawnsLength();
|
||||
sanitizeLobbyPlayerSpawnsInEditor();
|
||||
return lobbyPlayerSpawns.map((s) => (s && Number.isFinite(s.x) && Number.isFinite(s.y)
|
||||
? { x: Math.floor(s.x), y: Math.floor(s.y) }
|
||||
: null));
|
||||
})(),
|
||||
quizTrueArea, quizFalseArea, quizQuestionArea,
|
||||
quizCarryHubArea: gameType === 'quiz_carry' ? quizCarryHubArea.map(r => r.slice()) : [],
|
||||
quizCarryOptionArea: gameType === 'quiz_carry' ? quizCarryOptionArea.map(r => r.slice()) : [],
|
||||
carryEmbedCountdownArea: gameType === 'quiz_carry' ? carryEmbedCountdownArea.map(r => r.slice()) : [],
|
||||
carryEmbedCountdownHighlight: (() => {
|
||||
if (gameType !== 'quiz_carry') return false;
|
||||
const h = document.getElementById('carry-embed-countdown-highlight');
|
||||
return !h || !!h.checked;
|
||||
})(),
|
||||
carryEmbedCountdownHighlightColor: (() => {
|
||||
if (gameType !== 'quiz_carry') return undefined;
|
||||
const el = document.getElementById('carry-embed-countdown-color');
|
||||
return (el && el.value) ? el.value : '#ffb44a';
|
||||
})(),
|
||||
carryEmbedCountdownAnchor: (() => {
|
||||
if (gameType !== 'quiz_carry') return undefined;
|
||||
const el = document.getElementById('carry-embed-countdown-anchor');
|
||||
const v = el && el.value;
|
||||
if (v === 'grid' || v === 'map' || v === 'screen') return v;
|
||||
return 'map';
|
||||
})(),
|
||||
quizBattleDomeArea: gameType === 'quiz_battle' ? quizBattleDomeArea.map(r => r.slice()) : [],
|
||||
quizBattlePathArea: gameType === 'quiz_battle' ? quizBattlePathArea.map(r => r.slice()) : [],
|
||||
stackReleaseArea: gameType === 'stack' ? stackReleaseArea.map(r => r.slice()) : [],
|
||||
@@ -1796,8 +2005,9 @@
|
||||
if (cellColorWrap) cellColorWrap.style.display = drawModeEl.value === 'color' ? 'inline' : 'none';
|
||||
if (gridImageToolsWrap) gridImageToolsWrap.style.display = drawModeEl.value === 'cellImage' ? 'flex' : 'none';
|
||||
}
|
||||
if (drawModeEl) drawModeEl.addEventListener('change', () => { syncDrawModeAuxUi(); });
|
||||
if (drawModeEl) drawModeEl.addEventListener('change', () => { syncDrawModeAuxUi(); syncLobbySpawnAuxUi(); });
|
||||
syncDrawModeAuxUi();
|
||||
syncLobbySpawnAuxUi();
|
||||
const gridCellUpload = document.getElementById('grid-cell-image-upload');
|
||||
if (gridCellUpload) {
|
||||
gridCellUpload.addEventListener('change', (e) => {
|
||||
@@ -1872,6 +2082,9 @@
|
||||
quizQuestionArea = m.quizQuestionArea && m.quizQuestionArea.length ? m.quizQuestionArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizCarryHubArea = m.quizCarryHubArea && m.quizCarryHubArea.length ? m.quizCarryHubArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizCarryOptionArea = m.quizCarryOptionArea && m.quizCarryOptionArea.length ? m.quizCarryOptionArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
carryEmbedCountdownArea = m.carryEmbedCountdownArea && m.carryEmbedCountdownArea.length
|
||||
? m.carryEmbedCountdownArea.map(r => r && r.slice())
|
||||
: Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizBattleDomeArea = m.quizBattleDomeArea && m.quizBattleDomeArea.length ? m.quizBattleDomeArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizBattlePathArea = m.quizBattlePathArea && m.quizBattlePathArea.length ? m.quizBattlePathArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
jumpSurvivePlatforms = Array.isArray(m.jumpSurvivePlatforms) ? m.jumpSurvivePlatforms.map((p) => (p && typeof p === 'object' ? { ...p } : null)).filter(Boolean) : [];
|
||||
@@ -1905,6 +2118,18 @@
|
||||
showMapInGame = m.showMapInGame !== false;
|
||||
const showMapEl = document.getElementById('show-map-in-game');
|
||||
if (showMapEl) showMapEl.checked = showMapInGame;
|
||||
const cdH = document.getElementById('carry-embed-countdown-highlight');
|
||||
if (cdH) cdH.checked = m.carryEmbedCountdownHighlight !== false;
|
||||
const cdC = document.getElementById('carry-embed-countdown-color');
|
||||
if (cdC && typeof m.carryEmbedCountdownHighlightColor === 'string') {
|
||||
const hx = m.carryEmbedCountdownHighlightColor.trim();
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(hx)) cdC.value = hx;
|
||||
}
|
||||
const cdA = document.getElementById('carry-embed-countdown-anchor');
|
||||
if (cdA) {
|
||||
const a = m.carryEmbedCountdownAnchor;
|
||||
cdA.value = (a === 'grid' || a === 'screen' || a === 'map') ? a : 'map';
|
||||
}
|
||||
ensureInteractive();
|
||||
ensureStartGameArea();
|
||||
ensureSpawnArea();
|
||||
@@ -1919,6 +2144,21 @@
|
||||
? m.gauntletPlayerSpawns.map((s) => ({ x: Math.floor(Number(s.x)), y: Math.floor(Number(s.y)) }))
|
||||
.filter((s) => Number.isFinite(s.x) && Number.isFinite(s.y))
|
||||
: [];
|
||||
lobbyPlayerSpawns = [null, null, null, null, null, null];
|
||||
if (Array.isArray(m.lobbyPlayerSpawns)) {
|
||||
for (let li = 0; li < 6 && li < m.lobbyPlayerSpawns.length; li++) {
|
||||
const c = m.lobbyPlayerSpawns[li];
|
||||
if (c && c.x != null && c.y != null) {
|
||||
lobbyPlayerSpawns[li] = { x: Math.floor(Number(c.x)), y: Math.floor(Number(c.y)) };
|
||||
}
|
||||
}
|
||||
}
|
||||
sanitizeLobbyPlayerSpawnsInEditor();
|
||||
const lsmLoad = document.getElementById('lobby-spawn-mode');
|
||||
if (lsmLoad) {
|
||||
const mv = m.lobbySpawnMode;
|
||||
lsmLoad.value = (mv === 'fixed' || mv === 'slots6') ? mv : 'random';
|
||||
}
|
||||
gameType = m.gameType || 'zep';
|
||||
if (gameTypeEl) gameTypeEl.value = gameType;
|
||||
lanes = (m.lanes && m.lanes.length) ? m.lanes.map(l => ({ ...l })) : [];
|
||||
|
||||
@@ -116,7 +116,12 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
background: rgba(8, 10, 20, 0.42);
|
||||
background: var(--carry-ecd-overlay, rgba(8, 10, 20, 0.42));
|
||||
}
|
||||
#quiz-carry-embed-countdown.quiz-carry-embed-countdown--map-anchor {
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
background: var(--carry-ecd-overlay, rgba(8, 10, 20, 0.32));
|
||||
}
|
||||
#quiz-carry-embed-countdown.is-hidden { display: none !important; }
|
||||
#quiz-carry-embed-countdown-inner {
|
||||
@@ -129,6 +134,15 @@
|
||||
padding: 0 14px;
|
||||
text-align: center;
|
||||
}
|
||||
#quiz-carry-embed-countdown-inner.quiz-carry-embed-countdown-inner--map-rect {
|
||||
max-width: none;
|
||||
padding: clamp(6px, 1.2vmin, 12px);
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--carry-ecd-inner-radius, 12px);
|
||||
background: var(--carry-ecd-inner-bg, rgba(12, 14, 28, 0.82));
|
||||
border: var(--carry-ecd-inner-border-w, 1px) solid var(--carry-ecd-inner-border, rgba(122, 162, 247, 0.45));
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
#quiz-carry-embed-countdown-kicker {
|
||||
display: none;
|
||||
margin: 0;
|
||||
@@ -148,11 +162,24 @@
|
||||
overflow: auto;
|
||||
}
|
||||
#quiz-carry-embed-countdown-num {
|
||||
font: 800 min(28vw, 132px) / 1.05 system-ui, "Kanit", sans-serif;
|
||||
color: #ffe066;
|
||||
font: 800 min(var(--carry-ecd-screen-vw, 28vw), var(--carry-ecd-screen-max, 132px)) / 1.05 system-ui, "Kanit", sans-serif;
|
||||
color: var(--carry-ecd-digit-color, #ffe066);
|
||||
text-shadow: 0 0 48px rgba(255, 224, 102, 0.55), 0 4px 24px rgba(0, 0, 0, 0.65);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
/* ยึดกล่องบนแมป (ทอง / กลาง / กริด 321): ตัวเลขตามขนาดกล่องช่อง — ไม่ใช้ vw เต็มจอ */
|
||||
#quiz-carry-embed-countdown.quiz-carry-embed-countdown--map-anchor #quiz-carry-embed-countdown-inner.quiz-carry-embed-countdown-inner--map-rect {
|
||||
container-type: size;
|
||||
}
|
||||
#quiz-carry-embed-countdown.quiz-carry-embed-countdown--map-anchor #quiz-carry-embed-countdown-inner.quiz-carry-embed-countdown-inner--map-rect #quiz-carry-embed-countdown-num {
|
||||
font-weight: 800;
|
||||
font-size: var(--carry-ecd-map-digit-fs, clamp(14px, min(78cqmin, 82cqh), 200px));
|
||||
line-height: 1;
|
||||
font-family: system-ui, "Kanit", sans-serif;
|
||||
color: var(--carry-ecd-digit-color, #ffe066);
|
||||
letter-spacing: -0.02em;
|
||||
text-shadow: 0 0 clamp(6px, 3cqmin, 18px) rgba(255, 224, 102, 0.5), 0 2px clamp(4px, 1.5cqh, 12px) rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
/* ด้านบน: ไม่ชน HUD ล่างบนแคนวาส + เห็นชัดกว่าแถบล่าง */
|
||||
#quiz-carry-embed-q-strip {
|
||||
position: fixed;
|
||||
@@ -909,7 +936,7 @@
|
||||
</div>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0166"></script>
|
||||
<script src="js/play.js?v=0.114"></script>
|
||||
<script src="js/play.js?v=0.123"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -53,6 +53,8 @@ const MAPS_DIR = path.join(__dirname, 'data', 'maps');
|
||||
const CHARACTERS_DIR = path.join(__dirname, 'data', 'characters');
|
||||
const GAUNTLET_ASSETS_DIR = path.join(__dirname, 'data', 'gauntlet-assets');
|
||||
const GAUNTLET_ASSETS_META_PATH = path.join(__dirname, 'data', 'gauntlet-assets-meta.json');
|
||||
/** ต้องอยู่ใต้ public/ เพื่อให้ nginx (alias /Game/ → Game/public) เสิร์ฟรูปได้ — อย่าใช้ data/ */
|
||||
const QUIZ_CARRY_PLAQUE_ASSETS_DIR = path.join(__dirname, 'public', 'img', 'quiz-carry-plaque-assets');
|
||||
const QUIZ_SETTINGS_PATH = path.join(__dirname, 'data', 'quiz-settings.json');
|
||||
/** ถ้า Admin (PHP) merge ลงไฟล์ใต้ docroot แต่ Node รันจากโฟลเดอร์อื่น ให้ตั้ง absolute path ที่นี่ — carry* / ธีมแผงจะอ่านทับจาก mirror ทุกครั้งที่ loadQuizSettings */
|
||||
const QUIZ_SETTINGS_MIRROR_PATH = process.env.GAME_QUIZ_SETTINGS_MIRROR_PATH
|
||||
@@ -71,6 +73,8 @@ const defaultMap = () => ({
|
||||
gridImageCells: [],
|
||||
quizTrueArea: [], quizFalseArea: [], quizQuestionArea: [], quizQuestions: [],
|
||||
quizCarryHubArea: [], quizCarryOptionArea: [],
|
||||
/** quiz_carry embed: โซนวาง UI นับ 3-2-1 (0/1) — ใช้เมื่อ carryEmbedCountdownAnchor === 'grid' */
|
||||
carryEmbedCountdownArea: [],
|
||||
/** Quiz Battle (MCQ โดม) — ช่องที่ 1 = จุดกด E เปิดคำถามจาก battleQuizMcq */
|
||||
quizBattleDomeArea: [],
|
||||
/** Quiz Battle — ถ้าวาดอย่างน้อย 1 ช่อง = จำกัดเดินเฉพาะเส้นทาง (เหมือนแผนที่อ้างอิง) · ไม่วาด = เดินอิสระเหมือนเดิม */
|
||||
@@ -99,6 +103,14 @@ function defaultQuizSettings() {
|
||||
carrySessionLength: 0,
|
||||
/** quiz_carry: สีแผง #quiz-map-question-panel ในเกม/พรีวิว (จัดใน Admin แท็บหยิบมาวาง) */
|
||||
carryMapPanelTheme: defaultCarryMapPanelTheme(),
|
||||
/** quiz_carry: สี/ขอบ/ขนาดตัวเลขนับ 3-2-1 (embed พรีวิว) — Admin แท็บหยิบมาวาง */
|
||||
carryEmbedCountdownTheme: defaultCarryEmbedCountdownTheme(),
|
||||
/** quiz_carry: สีป้ายตัวเลือกบนแผนที่ (canvas) ต่อช่อง 1–16 */
|
||||
carryChoicePlaqueThemes: defaultCarryChoicePlaqueThemes(),
|
||||
/** legacy: ช่องแรกเท่ากับ carryChoicePlaqueThemes[0] */
|
||||
carryChoicePlaqueTheme: defaultCarryChoicePlaqueTheme(),
|
||||
/** quiz_carry: ขยายป้ายตัวเลือกบนแมป (กว้าง/สูง/ฟอนต์) — 0.85–2.5 */
|
||||
carryChoicePlaqueMapScale: 1.25,
|
||||
questions: [],
|
||||
carryQuestions: [],
|
||||
battleQuizMcq: [],
|
||||
@@ -141,6 +153,118 @@ function sanitizeCarryMapPanelTheme(raw) {
|
||||
};
|
||||
}
|
||||
|
||||
/** ป้ายตัวเลือกบนแผนที่ (canvas) — quiz_carry */
|
||||
function defaultCarryChoicePlaqueTheme() {
|
||||
return {
|
||||
borderMode: 'neon',
|
||||
fixedBorder: 'rgba(122, 200, 255, 0.9)',
|
||||
fillBg: 'rgba(12, 10, 20, 0.88)',
|
||||
textColor: 'rgba(248, 249, 255, 1)',
|
||||
borderWidthPx: 2.5,
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeCarryChoicePlaqueTheme(raw) {
|
||||
const d = defaultCarryChoicePlaqueTheme();
|
||||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return sanitizeCarryChoicePlaqueTheme({});
|
||||
const mode = String(raw.borderMode || '').toLowerCase() === 'fixed' ? 'fixed' : 'neon';
|
||||
let bw = Number(raw.borderWidthPx);
|
||||
if (!Number.isFinite(bw)) bw = d.borderWidthPx;
|
||||
bw = Math.round(Math.max(0, Math.min(8, bw)) * 10) / 10;
|
||||
return {
|
||||
borderMode: mode,
|
||||
fixedBorder: sanitizeCssColorToken(raw.fixedBorder, d.fixedBorder),
|
||||
fillBg: sanitizeCssColorToken(raw.fillBg, d.fillBg),
|
||||
textColor: sanitizeCssColorToken(raw.textColor, d.textColor),
|
||||
borderWidthPx: bw,
|
||||
plaqueImageUrl: sanitizeCarryChoiceImageUrl(raw.plaqueImageUrl),
|
||||
};
|
||||
}
|
||||
|
||||
const QUIZ_CARRY_PLAQUE_THEME_SLOTS = 16;
|
||||
|
||||
function defaultCarryChoicePlaqueThemes() {
|
||||
const out = [];
|
||||
for (let i = 0; i < QUIZ_CARRY_PLAQUE_THEME_SLOTS; i++) {
|
||||
out.push(sanitizeCarryChoicePlaqueTheme({}));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** อาร์เรย์ 16 ช่อง — รับได้ทั้ง carryChoicePlaqueThemes หรืออ็อบเจ็กต์เดียว (legacy) */
|
||||
function sanitizeCarryChoicePlaqueThemes(raw) {
|
||||
if (Array.isArray(raw) && raw.length > 0) {
|
||||
const out = [];
|
||||
for (let i = 0; i < QUIZ_CARRY_PLAQUE_THEME_SLOTS; i++) {
|
||||
out.push(sanitizeCarryChoicePlaqueTheme(raw[i] || {}));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (raw != null && typeof raw === 'object' && !Array.isArray(raw)) {
|
||||
const one = sanitizeCarryChoicePlaqueTheme(raw);
|
||||
return Array.from({ length: QUIZ_CARRY_PLAQUE_THEME_SLOTS }, () => ({ ...one }));
|
||||
}
|
||||
return defaultCarryChoicePlaqueThemes();
|
||||
}
|
||||
|
||||
function sanitizeCarryChoiceImageUrl(input) {
|
||||
const s = String(input == null ? '' : input).trim().slice(0, 512);
|
||||
if (!s) return '';
|
||||
if (/[\s<>'"`]/.test(s)) return '';
|
||||
if (s.startsWith('/')) {
|
||||
if (!/^\/[\w\-./?#=&%+]+$/i.test(s)) return '';
|
||||
return s;
|
||||
}
|
||||
const low = s.toLowerCase();
|
||||
if (!low.startsWith('https://') && !low.startsWith('http://')) return '';
|
||||
try {
|
||||
const u = new URL(s);
|
||||
if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
|
||||
return s;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function defaultCarryEmbedCountdownTheme() {
|
||||
return {
|
||||
overlayBackdrop: 'rgba(8, 10, 20, 0.42)',
|
||||
innerBg: 'rgba(12, 14, 28, 0.82)',
|
||||
innerBorder: 'rgba(122, 162, 247, 0.45)',
|
||||
innerBorderWpx: 1,
|
||||
innerRadiusPx: 12,
|
||||
digitColor: '#ffe066',
|
||||
mapDigitCqmin: 78,
|
||||
mapDigitCqh: 82,
|
||||
mapDigitMaxPx: 200,
|
||||
screenDigitVw: 28,
|
||||
screenDigitMaxPx: 132,
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeCarryEmbedCountdownTheme(raw) {
|
||||
const d = defaultCarryEmbedCountdownTheme();
|
||||
if (!raw || typeof raw !== 'object') return d;
|
||||
const clampN = (v, lo, hi, def) => {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n)) return def;
|
||||
return Math.max(lo, Math.min(hi, Math.round(n)));
|
||||
};
|
||||
return {
|
||||
overlayBackdrop: sanitizeCssColorToken(raw.overlayBackdrop, d.overlayBackdrop),
|
||||
innerBg: sanitizeCssColorToken(raw.innerBg, d.innerBg),
|
||||
innerBorder: sanitizeCssColorToken(raw.innerBorder, d.innerBorder),
|
||||
innerBorderWpx: clampN(raw.innerBorderWpx, 0, 12, d.innerBorderWpx),
|
||||
innerRadiusPx: clampN(raw.innerRadiusPx, 0, 32, d.innerRadiusPx),
|
||||
digitColor: sanitizeCssColorToken(raw.digitColor, d.digitColor),
|
||||
mapDigitCqmin: clampN(raw.mapDigitCqmin, 35, 100, d.mapDigitCqmin),
|
||||
mapDigitCqh: clampN(raw.mapDigitCqh, 35, 100, d.mapDigitCqh),
|
||||
mapDigitMaxPx: clampN(raw.mapDigitMaxPx, 48, 400, d.mapDigitMaxPx),
|
||||
screenDigitVw: clampN(raw.screenDigitVw, 6, 44, d.screenDigitVw),
|
||||
screenDigitMaxPx: clampN(raw.screenDigitMaxPx, 48, 220, d.screenDigitMaxPx),
|
||||
};
|
||||
}
|
||||
|
||||
/** หมวด Quiz Battle (อ้างอิงธีมหน้า Quiz-Battle) — id ต้องตรงกับแอดมิน */
|
||||
const QUIZ_BATTLE_CATEGORY_IDS = new Set([
|
||||
'cybercrime',
|
||||
@@ -192,7 +316,12 @@ function sanitizeCarryQuestions(arr) {
|
||||
let ci = Number(q.correctIndex);
|
||||
if (!Number.isFinite(ci)) ci = 0;
|
||||
ci = Math.max(0, Math.min(choices.length - 1, Math.floor(ci)));
|
||||
out.push({ text, choices, correctIndex: ci });
|
||||
const row = { text, choices, correctIndex: ci };
|
||||
if (Array.isArray(q.choiceImageUrls) && q.choiceImageUrls.length) {
|
||||
const urls = choices.map((_, idx) => sanitizeCarryChoiceImageUrl(q.choiceImageUrls[idx]));
|
||||
if (urls.some((u) => u)) row.choiceImageUrls = urls;
|
||||
}
|
||||
out.push(row);
|
||||
if (out.length >= 80) break;
|
||||
}
|
||||
return out;
|
||||
@@ -216,6 +345,12 @@ function clampCarrySessionLength(n, def) {
|
||||
return Math.max(0, Math.min(500, Math.floor(v)));
|
||||
}
|
||||
|
||||
function clampCarryChoicePlaqueMapScale(n, def) {
|
||||
const v = Number(n);
|
||||
if (Number.isNaN(v)) return def;
|
||||
return Math.round(Math.max(0.85, Math.min(2.5, v)) * 100) / 100;
|
||||
}
|
||||
|
||||
function mergeQuizCarryFromMirrorIfSet(base) {
|
||||
if (!QUIZ_SETTINGS_MIRROR_PATH) return base;
|
||||
try {
|
||||
@@ -235,6 +370,16 @@ function mergeQuizCarryFromMirrorIfSet(base) {
|
||||
if (j.carrySessionLength != null) {
|
||||
base.carrySessionLength = clampCarrySessionLength(j.carrySessionLength, base.carrySessionLength);
|
||||
}
|
||||
if (j.carryEmbedCountdownTheme != null && typeof j.carryEmbedCountdownTheme === 'object') {
|
||||
base.carryEmbedCountdownTheme = sanitizeCarryEmbedCountdownTheme(j.carryEmbedCountdownTheme);
|
||||
}
|
||||
if (Array.isArray(j.carryChoicePlaqueThemes) && j.carryChoicePlaqueThemes.length) {
|
||||
base.carryChoicePlaqueThemes = sanitizeCarryChoicePlaqueThemes(j.carryChoicePlaqueThemes);
|
||||
base.carryChoicePlaqueTheme = base.carryChoicePlaqueThemes[0];
|
||||
} else if (j.carryChoicePlaqueTheme != null && typeof j.carryChoicePlaqueTheme === 'object') {
|
||||
base.carryChoicePlaqueThemes = sanitizeCarryChoicePlaqueThemes(j.carryChoicePlaqueTheme);
|
||||
base.carryChoicePlaqueTheme = base.carryChoicePlaqueThemes[0];
|
||||
}
|
||||
if (Array.isArray(j.carryQuestions) && j.carryQuestions.length) {
|
||||
base.carryQuestions = sanitizeCarryQuestions(j.carryQuestions);
|
||||
}
|
||||
@@ -254,6 +399,12 @@ function loadQuizSettings() {
|
||||
const carryQuestions = sanitizeCarryQuestions(j.carryQuestions);
|
||||
const battleQuizMcq = sanitizeBattleQuizMcq(j.battleQuizMcq);
|
||||
const carryMapPanelTheme = sanitizeCarryMapPanelTheme(j.carryMapPanelTheme);
|
||||
const carryEmbedCountdownTheme = sanitizeCarryEmbedCountdownTheme(j.carryEmbedCountdownTheme);
|
||||
const carryChoicePlaqueThemes = Array.isArray(j.carryChoicePlaqueThemes) && j.carryChoicePlaqueThemes.length
|
||||
? sanitizeCarryChoicePlaqueThemes(j.carryChoicePlaqueThemes)
|
||||
: sanitizeCarryChoicePlaqueThemes(j.carryChoicePlaqueTheme);
|
||||
const carryChoicePlaqueTheme = carryChoicePlaqueThemes[0];
|
||||
const carryChoicePlaqueMapScale = clampCarryChoicePlaqueMapScale(j.carryChoicePlaqueMapScale, d.carryChoicePlaqueMapScale);
|
||||
const out = {
|
||||
readMs: clampQuizMs(j.readMs, d.readMs),
|
||||
answerMs: clampQuizMs(j.answerMs, d.answerMs),
|
||||
@@ -262,6 +413,10 @@ function loadQuizSettings() {
|
||||
carryAnswerMs: clampQuizMs(j.carryAnswerMs, d.carryAnswerMs),
|
||||
carrySessionLength: clampCarrySessionLength(j.carrySessionLength, d.carrySessionLength),
|
||||
carryMapPanelTheme,
|
||||
carryEmbedCountdownTheme,
|
||||
carryChoicePlaqueThemes,
|
||||
carryChoicePlaqueTheme,
|
||||
carryChoicePlaqueMapScale,
|
||||
questions,
|
||||
carryQuestions,
|
||||
battleQuizMcq,
|
||||
@@ -508,6 +663,16 @@ function safeGauntletStoredFilename(name) {
|
||||
return base;
|
||||
}
|
||||
|
||||
function safeQuizCarryPlaqueStoredFilename(name) {
|
||||
const base = path.basename(String(name || ''));
|
||||
if (!/^qcarry-[a-f0-9]{16}\.(png|jpg|jpeg|gif|webp)$/i.test(base)) return null;
|
||||
return base;
|
||||
}
|
||||
|
||||
function ensureQuizCarryPlaqueAssetsDir() {
|
||||
if (!fs.existsSync(QUIZ_CARRY_PLAQUE_ASSETS_DIR)) fs.mkdirSync(QUIZ_CARRY_PLAQUE_ASSETS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function gauntletAssetMimeByExt(ext) {
|
||||
const e = String(ext || '').toLowerCase();
|
||||
if (e === '.png') return 'image/png';
|
||||
@@ -577,6 +742,25 @@ function saveQuizSettings(d) {
|
||||
const carryMapPanelTheme = d.carryMapPanelTheme != null && typeof d.carryMapPanelTheme === 'object'
|
||||
? sanitizeCarryMapPanelTheme(d.carryMapPanelTheme)
|
||||
: prev.carryMapPanelTheme;
|
||||
const carryEmbedCountdownTheme = d.carryEmbedCountdownTheme != null && typeof d.carryEmbedCountdownTheme === 'object'
|
||||
? sanitizeCarryEmbedCountdownTheme(d.carryEmbedCountdownTheme)
|
||||
: prev.carryEmbedCountdownTheme;
|
||||
let carryChoicePlaqueThemes = prev.carryChoicePlaqueThemes;
|
||||
if (Array.isArray(d.carryChoicePlaqueThemes) && d.carryChoicePlaqueThemes.length) {
|
||||
carryChoicePlaqueThemes = sanitizeCarryChoicePlaqueThemes(d.carryChoicePlaqueThemes);
|
||||
} else if (d.carryChoicePlaqueTheme != null && typeof d.carryChoicePlaqueTheme === 'object') {
|
||||
carryChoicePlaqueThemes = sanitizeCarryChoicePlaqueThemes(d.carryChoicePlaqueTheme);
|
||||
}
|
||||
if (!Array.isArray(carryChoicePlaqueThemes) || !carryChoicePlaqueThemes.length) {
|
||||
carryChoicePlaqueThemes = sanitizeCarryChoicePlaqueThemes(prev.carryChoicePlaqueTheme);
|
||||
}
|
||||
const carryChoicePlaqueTheme = carryChoicePlaqueThemes[0];
|
||||
const prevScale = prev.carryChoicePlaqueMapScale != null
|
||||
? clampCarryChoicePlaqueMapScale(prev.carryChoicePlaqueMapScale, 1.25)
|
||||
: 1.25;
|
||||
const carryChoicePlaqueMapScale = d.carryChoicePlaqueMapScale != null
|
||||
? clampCarryChoicePlaqueMapScale(d.carryChoicePlaqueMapScale, prevScale)
|
||||
: prevScale;
|
||||
const out = {
|
||||
readMs,
|
||||
answerMs,
|
||||
@@ -585,6 +769,10 @@ function saveQuizSettings(d) {
|
||||
carryAnswerMs,
|
||||
carrySessionLength,
|
||||
carryMapPanelTheme,
|
||||
carryEmbedCountdownTheme,
|
||||
carryChoicePlaqueThemes,
|
||||
carryChoicePlaqueTheme,
|
||||
carryChoicePlaqueMapScale,
|
||||
questions,
|
||||
carryQuestions,
|
||||
battleQuizMcq,
|
||||
@@ -595,6 +783,10 @@ function saveQuizSettings(d) {
|
||||
battleQuizMcqSaved: (out.battleQuizMcq || []).length,
|
||||
carryQuestionsSaved: (out.carryQuestions || []).length,
|
||||
carryMapPanelTheme: out.carryMapPanelTheme,
|
||||
carryEmbedCountdownTheme: out.carryEmbedCountdownTheme,
|
||||
carryChoicePlaqueThemes: out.carryChoicePlaqueThemes,
|
||||
carryChoicePlaqueTheme: out.carryChoicePlaqueTheme,
|
||||
carryChoicePlaqueMapScale: out.carryChoicePlaqueMapScale,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('saveQuizSettings', e.message);
|
||||
@@ -958,8 +1150,23 @@ function normalizeQuizCarryLayersOnMap(m) {
|
||||
}
|
||||
m.quizCarryOptionArea = rows;
|
||||
};
|
||||
const normCountdown = () => {
|
||||
const src = m.carryEmbedCountdownArea || [];
|
||||
const rows = [];
|
||||
for (let y = 0; y < h; y++) {
|
||||
const r = src[y];
|
||||
const row = [];
|
||||
for (let x = 0; x < w; x++) {
|
||||
const v = r && r[x];
|
||||
row.push(Number(v) === 1 ? 1 : 0);
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
m.carryEmbedCountdownArea = rows;
|
||||
};
|
||||
normHub();
|
||||
normOptions();
|
||||
normCountdown();
|
||||
}
|
||||
|
||||
/** กริดแพลตฟอร์มกระโดดให้รอด — 0/1 ต่อช่อง */
|
||||
@@ -1158,6 +1365,7 @@ const server = http.createServer((req, res) => {
|
||||
if (!m.quizQuestions) m.quizQuestions = [];
|
||||
if (!m.quizCarryHubArea) m.quizCarryHubArea = [];
|
||||
if (!m.quizCarryOptionArea) m.quizCarryOptionArea = [];
|
||||
if (!m.carryEmbedCountdownArea) m.carryEmbedCountdownArea = [];
|
||||
if (!m.quizBattleDomeArea) m.quizBattleDomeArea = [];
|
||||
if (!m.quizBattlePathArea) m.quizBattlePathArea = [];
|
||||
if (!m.stackReleaseArea) m.stackReleaseArea = [];
|
||||
@@ -1197,6 +1405,7 @@ const server = http.createServer((req, res) => {
|
||||
if (!m.quizQuestions) m.quizQuestions = [];
|
||||
if (!m.quizCarryHubArea) m.quizCarryHubArea = [];
|
||||
if (!m.quizCarryOptionArea) m.quizCarryOptionArea = [];
|
||||
if (!m.carryEmbedCountdownArea) m.carryEmbedCountdownArea = [];
|
||||
if (!m.quizBattleDomeArea) m.quizBattleDomeArea = [];
|
||||
if (!m.quizBattlePathArea) m.quizBattlePathArea = [];
|
||||
if (!m.stackReleaseArea) m.stackReleaseArea = [];
|
||||
@@ -1228,6 +1437,10 @@ const server = http.createServer((req, res) => {
|
||||
const g = loadQuizSettings();
|
||||
const slice = {
|
||||
carryMapPanelTheme: g.carryMapPanelTheme,
|
||||
carryEmbedCountdownTheme: g.carryEmbedCountdownTheme,
|
||||
carryChoicePlaqueThemes: g.carryChoicePlaqueThemes,
|
||||
carryChoicePlaqueTheme: g.carryChoicePlaqueTheme,
|
||||
carryChoicePlaqueMapScale: g.carryChoicePlaqueMapScale,
|
||||
carryReadMs: g.carryReadMs,
|
||||
carryAnswerMs: g.carryAnswerMs,
|
||||
carrySessionLength: g.carrySessionLength,
|
||||
@@ -1271,6 +1484,10 @@ const server = http.createServer((req, res) => {
|
||||
battleQuizMcqSaved: saved.battleQuizMcqSaved,
|
||||
carryQuestionsSaved: saved.carryQuestionsSaved,
|
||||
carryMapPanelTheme: saved.carryMapPanelTheme,
|
||||
carryEmbedCountdownTheme: saved.carryEmbedCountdownTheme,
|
||||
carryChoicePlaqueThemes: saved.carryChoicePlaqueThemes,
|
||||
carryChoicePlaqueTheme: saved.carryChoicePlaqueTheme,
|
||||
carryChoicePlaqueMapScale: saved.carryChoicePlaqueMapScale,
|
||||
}));
|
||||
} catch (e) {
|
||||
res.writeHead(400);
|
||||
@@ -1404,6 +1621,54 @@ const server = http.createServer((req, res) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const qCarryPlaqueUploadPath = url.split('?')[0].replace(/\/+$/, '') || '/';
|
||||
if (qCarryPlaqueUploadPath === BASE_PATH + '/api/quiz-carry-plaque-upload' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', (c) => { body += c; });
|
||||
req.on('end', () => {
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
try {
|
||||
const d = JSON.parse(body || '{}');
|
||||
const dataUrl = d.imageDataUrl;
|
||||
if (!dataUrl || typeof dataUrl !== 'string') {
|
||||
res.writeHead(400);
|
||||
return res.end(JSON.stringify({ ok: false, error: 'ต้องส่ง imageDataUrl (data URL รูป)' }));
|
||||
}
|
||||
const m = dataUrl.match(/^data:image\/(png|jpeg|jpg|gif|webp);base64,([\s\S]+)$/i);
|
||||
if (!m) {
|
||||
res.writeHead(400);
|
||||
return res.end(JSON.stringify({ ok: false, error: 'รองรับเฉพาะ png / jpg / gif / webp' }));
|
||||
}
|
||||
const extRaw = m[1].toLowerCase();
|
||||
const ext = extRaw === 'jpeg' ? 'jpg' : extRaw;
|
||||
const b64 = m[2].replace(/\s/g, '');
|
||||
let buf;
|
||||
try {
|
||||
buf = Buffer.from(b64, 'base64');
|
||||
} catch (e) {
|
||||
res.writeHead(400);
|
||||
return res.end(JSON.stringify({ ok: false, error: 'base64 ไม่ถูกต้อง' }));
|
||||
}
|
||||
if (buf.length < 16 || buf.length > 4 * 1024 * 1024) {
|
||||
res.writeHead(400);
|
||||
return res.end(JSON.stringify({ ok: false, error: 'ขนาดไฟล์ต้องอยู่ระหว่าง 16 ไบต์ ถึง 4 MB' }));
|
||||
}
|
||||
ensureQuizCarryPlaqueAssetsDir();
|
||||
const fname = `qcarry-${crypto.randomBytes(8).toString('hex')}.${ext}`;
|
||||
fs.writeFileSync(path.join(QUIZ_CARRY_PLAQUE_ASSETS_DIR, fname), buf);
|
||||
res.writeHead(200);
|
||||
return res.end(JSON.stringify({
|
||||
ok: true,
|
||||
filename: fname,
|
||||
url: BASE_PATH + '/img/quiz-carry-plaque-assets/' + fname,
|
||||
}));
|
||||
} catch (e) {
|
||||
res.writeHead(400);
|
||||
return res.end(JSON.stringify({ ok: false, error: 'อัปโหลดไม่สำเร็จ: ' + (e.message || '') }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (gaAssetsApiPath === BASE_PATH + '/api/gauntlet-assets' && req.method === 'PATCH') {
|
||||
let body = '';
|
||||
req.on('data', (c) => { body += c; });
|
||||
@@ -2089,8 +2354,9 @@ function endGauntletGame(sid, space, reason) {
|
||||
if (retMap && retMap.gameType !== 'gauntlet') {
|
||||
space.mapId = retId;
|
||||
space.mapData = retMap;
|
||||
let gi = 0;
|
||||
space.peers.forEach((p) => {
|
||||
const sp = pickRandomSpawnFromMap(retMap);
|
||||
const sp = pickSpawnForJoin(retMap, gi++);
|
||||
p.x = sp.x;
|
||||
p.y = sp.y;
|
||||
p.gauntletJumpTicks = 0;
|
||||
@@ -2313,6 +2579,54 @@ function isMapTileWalkableForSpawn(md, x, y) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** แปลง lobbyPlayerSpawns จากแมปเป็นอาร์เรย์ยาว 6 ช่อง (null หรือ {x,y}) */
|
||||
function parseLobbyPlayerSpawnsFromMap(md) {
|
||||
const w = md.width || 20;
|
||||
const h = md.height || 15;
|
||||
const out = [null, null, null, null, null, null];
|
||||
const raw = md && md.lobbyPlayerSpawns;
|
||||
if (!Array.isArray(raw)) return out;
|
||||
for (let i = 0; i < 6 && i < raw.length; i++) {
|
||||
const cell = raw[i];
|
||||
if (!cell || typeof cell !== 'object') continue;
|
||||
const x = Math.floor(Number(cell.x));
|
||||
const y = Math.floor(Number(cell.y));
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
||||
if (x < 0 || x >= w || y < 0 || y >= h) continue;
|
||||
out[i] = { x, y };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* จุดเกิดตอน join — random = สุ่มใน spawnArea / fixed = ปุ่มตั้งจุดเกิด / slots6 = P ตามลำดับเข้า (0=คนแรก)
|
||||
* โหมดพรมแดงยังถูกทับด้วย gauntletSpawnPositions หลัง join ตามเดิม
|
||||
*/
|
||||
function pickSpawnForJoin(md, joinOrderIndex) {
|
||||
if (!md) return { x: 1, y: 1 };
|
||||
const mode = md.lobbySpawnMode;
|
||||
const ord = joinOrderIndex | 0;
|
||||
if (mode === 'slots6' && ord >= 6) return pickRandomSpawnFromMap(md);
|
||||
const j = Math.min(Math.max(0, ord), 5);
|
||||
if (mode === 'fixed' && md.spawn) {
|
||||
const fx = Number.isFinite(Number(md.spawn.x)) ? Math.floor(Number(md.spawn.x)) : 1;
|
||||
const fy = Number.isFinite(Number(md.spawn.y)) ? Math.floor(Number(md.spawn.y)) : 1;
|
||||
const w = md.width || 20;
|
||||
const h = md.height || 15;
|
||||
const x = Math.max(0, Math.min(w - 1, fx));
|
||||
const y = Math.max(0, Math.min(h - 1, fy));
|
||||
if (isMapTileWalkableForSpawn(md, x, y)) return { x, y };
|
||||
return pickRandomSpawnFromMap(md);
|
||||
}
|
||||
if (mode === 'slots6') {
|
||||
const slots = parseLobbyPlayerSpawnsFromMap(md);
|
||||
const pick = slots[j];
|
||||
if (pick && isMapTileWalkableForSpawn(md, pick.x, pick.y)) return { x: pick.x, y: pick.y };
|
||||
return pickRandomSpawnFromMap(md);
|
||||
}
|
||||
return pickRandomSpawnFromMap(md);
|
||||
}
|
||||
|
||||
/** สุ่มจุดเกิดในช่อง spawnArea=1 ที่เดินได้ — ไม่มีช่องว่าง = ใช้ spawn ค่าเดิมจากเอดิเตอร์ */
|
||||
function pickRandomSpawnFromMap(md) {
|
||||
const fallback = md.spawn || { x: 1, y: 1 };
|
||||
@@ -2428,7 +2742,7 @@ 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 = pickRandomSpawnFromMap(mdJoin);
|
||||
const spawnPt = pickSpawnForJoin(mdJoin, space.peers.size);
|
||||
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,
|
||||
@@ -2475,6 +2789,10 @@ io.on('connection', (socket) => {
|
||||
const qs = loadQuizSettings();
|
||||
joinCb.quizCarrySettingsSnap = {
|
||||
carryMapPanelTheme: qs.carryMapPanelTheme,
|
||||
carryEmbedCountdownTheme: qs.carryEmbedCountdownTheme,
|
||||
carryChoicePlaqueThemes: qs.carryChoicePlaqueThemes,
|
||||
carryChoicePlaqueTheme: qs.carryChoicePlaqueTheme,
|
||||
carryChoicePlaqueMapScale: qs.carryChoicePlaqueMapScale,
|
||||
carryReadMs: qs.carryReadMs,
|
||||
carryAnswerMs: qs.carryAnswerMs,
|
||||
carrySessionLength: qs.carrySessionLength,
|
||||
@@ -2573,8 +2891,9 @@ io.on('connection', (socket) => {
|
||||
clearSpaceQuizTimers(space);
|
||||
space.mapId = SUSPECT_INVESTIGATION_QUIZ_MAP_ID;
|
||||
space.mapData = md;
|
||||
let si = 0;
|
||||
space.peers.forEach((p) => {
|
||||
const sp = pickRandomSpawnFromMap(md);
|
||||
const sp = pickSpawnForJoin(md, si++);
|
||||
p.x = sp.x;
|
||||
p.y = sp.y;
|
||||
p.direction = 'down';
|
||||
@@ -2678,8 +2997,9 @@ io.on('connection', (socket) => {
|
||||
space.troublesomeDebTimer = null;
|
||||
space.troublesomeMaxTimer = null;
|
||||
initTroublesomeState(space);
|
||||
let li = 0;
|
||||
space.peers.forEach((p) => {
|
||||
const sp = pickRandomSpawnFromMap(space.mapData);
|
||||
const sp = pickSpawnForJoin(space.mapData, li++);
|
||||
p.x = sp.x;
|
||||
p.y = sp.y;
|
||||
p.direction = 'down';
|
||||
|
||||