fix minigame 1-3
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# สรุปงานแก้ไข Justice Divers — Minigames
|
||||
|
||||
**วันที่: 2026-06-13**
|
||||
ผู้ทำ: Claude (ผ่าน SSH) · งาน: เก็บตก/ปรับ Minigames + Quiz Battle
|
||||
|
||||
---
|
||||
|
||||
## ส่วนกลาง (ทุกเกม)
|
||||
1. **howto/summary CSS หาย** — `public/css/howto.css` + `public/css/game-hud.css` ไม่มีจริง (เสิร์ฟ HTML fallback) → สร้างจาก design แล้ว deploy ทุกเกมโหลด CSS ถูก (เดิม Minigame-4 เพี้ยน)
|
||||
2. **ขนาด popup "HOW TO PLAY" ไม่เท่ากันทุกเกม** — เดิมมีแค่ mg1/mg2 ที่ popup ใหญ่ ส่วน mg3/5/6/7 + mg4 เป็น popup เล็ก → inject CSS (`qc-howto-mg2-fix`, `gch-howto-size-fix`) ให้ทุกเกม popup ใหญ่เท่า mg2 (gated ไม่กระทบของเดิม)
|
||||
|
||||
## แยก 7 มินิเกม ออกจาก Quiz Battle (50 คน)
|
||||
3. **Join Room** — เคยโชว์ห้องปนกัน → กรองซ่อนห้อง Quiz Battle ออก (`Join Room/join.js`)
|
||||
4. **editor.html** — dropdown "แก้จากฉากเดิม" เคยมี qbroom ปน → กรอง qbroom ออก (Quiz Battle แก้ที่ `qb-editor.html` แยก)
|
||||
|
||||
## MG1 — quiz ถูก/ผิด (map mng8a80o)
|
||||
5. **ใส่คำถาม 30 ข้อจริง** แทน test1..14 (20 SCAM + 10 SAFE) ใน `data/quiz-settings.json`
|
||||
6. **จัดข้อความคำถามให้ตรงบอร์ด** — เลื่อนโซน quizQuestionArea ลง (y1-3 → y2-4)
|
||||
7. **โซนผี** — เพิ่มเครื่องมือวาด "โซนผี" ใน editor + ตอบผิดกลายเป็นผี → วาร์ปเข้าโซน + ขัง (เดินออกไม่ได้)
|
||||
- หมายเหตุ: ต้องวาดโซนผีเองใน editor?id=mng8a80o (เลือกโหมด "โซนผี")
|
||||
|
||||
## MG2 — วิ่งหลบสิ่งกีดขวาง (map mno9kb07)
|
||||
8. **avatar ผู้เล่นในแถบ SCORE โชว์เต็มตัว** → แก้ให้เห็นแต่หัวเหมือนบอท
|
||||
9. **laser ซ้อนสิ่งกีดขวาง** → spawn ไม่ให้ laser+lane อยู่คอลัมน์เดียวกัน (server.js)
|
||||
10. **เดินเลยเส้นชัย** → ถึงเส้นชัยจบรอบทันที เด้งหน้าสรุป (server.js)
|
||||
|
||||
## MG3 — Tower block (map mnn93hpi)
|
||||
11. **terminal log** — ใส่ hacking-terminal ครบทุกสถานะ (boot → Compile Line SUCCESS/PERFECT → WARNING n/3 → FATAL/LOCKED → MISSION ACCOMPLISHED)
|
||||
12. **กล้อง** — เปลี่ยนจากกระโดดทุก 6 บล็อก → smooth follow ทีละบล็อก (อ้างอิง Tower of Babel)
|
||||
13. **font terminal เล็กลง 50%** (22px → 11px)
|
||||
14. **บล็อกพลาด → ตก physics หลุดขอบจอ** (หมุน + gravity)
|
||||
15. **แสงหลัง effect x2 ใหญ่+ตรงไอคอน+สว่างขึ้น**
|
||||
16. **กรอบเหลือง perfect ให้ชัด** (กรอบหนารอบบล็อก + เรืองแสง)
|
||||
|
||||
---
|
||||
|
||||
## หมายเหตุการ deploy
|
||||
- ทุกไฟล์มี **backup** บน server (`*.bak-*`) + bump cache (รีเฟรช Ctrl+F5 เห็นผล)
|
||||
- งานที่แตะ **server.js** (#9 laser, #10 เส้นชัย) restart `game-zep.service` แล้ว
|
||||
- ไฟล์หลักที่แก้: `Game/public/js/play.js`, `Game/public/js/editor.js`, `Game/public/editor.html`, `Game/public/play.html`, `Game/server.js`, `Game/data/quiz-settings.json`, `Game/data/maps/{mng8a80o,mnn93hpi}.json`, `Join Room/join.js`
|
||||
- ยังรอ verify ในเกมบางจุด (MG3 กล้อง/effect, MG1 ตำแหน่งคำถาม + ต้องวาดโซนผีเอง)
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -178,59 +178,123 @@
|
||||
"quizRoundQuestionCount": 3,
|
||||
"questions": [
|
||||
{
|
||||
"text": "test1",
|
||||
"text": "เพื่อนทักขอยืมเงิน แต่ให้โอนเข้าบัญชีชื่อคนอื่น",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "ค่ายเน็ตโทรบอกคุณได้รางวัลใหญ่ ทั้งที่ไม่ได้ร่วมกิจกรรม",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "คนขายเร่งให้โอนมัดจำทันที อ้างว่ามีคิวรออยู่เยอะมาก",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "สายอัตโนมัติแจ้งว่าพัสดุมีปัญหา ให้กด 9 เพื่อคุยเจ้าหน้าที่",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "คนแปลกหน้าทักมาชวนคุย แล้วชวนลงทุนคริปโต",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "SMS: \"พัสดุถูกอายัด ชำระค่าปรับด่วนที่ [Link]\"",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "Email: \"คุณได้รับ iPhone 15 ฟรีจากสุ่มผู้ใช้ Google\"",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "Pop-up: \"เครื่องร้อนเกินไป! โหลดแอปนี้เพื่อระบายความร้อน\"",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "SMS: \"บัญชีธนาคารระงับ กรุณาเข้าลิงก์เพื่อยืนยันตัวตน\"",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "มีรหัส OTP ส่งเข้ามือถือ ทั้งที่เราไม่ได้ทำอะไรเลย",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "งานพาร์ทไทม์ \"กดไลก์สินค้า\" รายได้วันละ 1,000 บาท",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "รับสมัครงานแพ็คของที่บ้าน แต่ต้องจ่าย \"ค่ามัดจำอุปกรณ์\"",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "ชวนลงทุนหุ้นไทย แต่ให้โอนเงินเข้า \"บัญชีชื่อบุคคลธรรมดา\"",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "โฆษณาใช้รูปดาราหรือนักธุรกิจชื่อดังมาชวนลงทุน",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "คลิป TikTok สอนรวยด้วยการ \"ฝากเซียนเล่นพนันให้\"",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "แอปไฟฉาย ขอสิทธิ์เข้าถึง \"รายชื่อติดต่อ\" และ \"GPS\"",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "เจอ Wi-Fi สาธารณะชื่อ \"Free_Wifi_No_Password\"",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "แอปที่มีรีวิว 5 ดาวเยอะ แต่คอมเมนต์มีแต่คำซ้ำๆ แปลกๆ",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "เจอแฟลชไดร์ฟวางลืมไว้ เลยเอามาเสียบดูว่าของใคร",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "โฆษณา \"สอนปั่นบาคาร่า\" หรือ \"แจกสูตรโกงเว็บพนัน\"",
|
||||
"answerTrue": false
|
||||
},
|
||||
{
|
||||
"text": "หน้าจอ: \"Windows Update กำลังเตรียมการอัปเดต\"",
|
||||
"answerTrue": true
|
||||
},
|
||||
{
|
||||
"text": "test2",
|
||||
"text": "โปรแกรมแอนตี้ไวรัสแจ้งว่า \"กักกันไฟล์อันตรายไว้ให้แล้ว\"",
|
||||
"answerTrue": true
|
||||
},
|
||||
{
|
||||
"text": "test3",
|
||||
"text": "SMS แจ้งยอดถอนเงิน 1,000 บาท หลังเราเพิ่งถอนจริง",
|
||||
"answerTrue": true
|
||||
},
|
||||
{
|
||||
"text": "test4",
|
||||
"text": "แอปธนาคารล็อคทันที เมื่อเรากด \"อัดหน้าจอ\" ทิ้งไว้",
|
||||
"answerTrue": true
|
||||
},
|
||||
{
|
||||
"text": "test5",
|
||||
"text": "แจ้งเตือนการ Login ใหม่จาก Google เมื่อใช้เครื่องใหม่",
|
||||
"answerTrue": true
|
||||
},
|
||||
{
|
||||
"text": "test6",
|
||||
"text": "ไรเดอร์โทรมาถามทางเพื่อส่งอาหารที่เราเพิ่งสั่ง",
|
||||
"answerTrue": true
|
||||
},
|
||||
{
|
||||
"text": "test7",
|
||||
"text": "รหัส OTP ส่งมาขณะที่เรากำลังจ่ายเงินออนไลน์เอง",
|
||||
"answerTrue": true
|
||||
},
|
||||
{
|
||||
"text": "test8",
|
||||
"text": "แอปในมือถือขออัปเดตผ่าน App Store / Play Store",
|
||||
"answerTrue": true
|
||||
},
|
||||
{
|
||||
"text": "test9",
|
||||
"text": "บัญชีทางการของแบรนด์ที่มีเครื่องหมายติ๊กถูก (Verified)",
|
||||
"answerTrue": true
|
||||
},
|
||||
{
|
||||
"text": "test10",
|
||||
"answerTrue": true
|
||||
},
|
||||
{
|
||||
"text": "test11",
|
||||
"answerTrue": true
|
||||
},
|
||||
{
|
||||
"text": "test12",
|
||||
"answerTrue": true
|
||||
},
|
||||
{
|
||||
"text": "test13",
|
||||
"answerTrue": true
|
||||
},
|
||||
{
|
||||
"text": "test14",
|
||||
"text": "เพจที่พักชื่อดัง (มีติ๊กถูก Verified) โพสต์โปรโมชั่นราคาปกติ",
|
||||
"answerTrue": true
|
||||
}
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -122,6 +122,7 @@
|
||||
<option value="quizTrue" id="draw-mode-option-quiz-true">โซน ถูก (ตอบ จริง)</option>
|
||||
<option value="quizFalse" id="draw-mode-option-quiz-false">โซน ผิด (ตอบ เท็จ)</option>
|
||||
<option value="quizQuestion" id="draw-mode-option-quiz-question">พื้นที่โชว์ข้อความคำถาม</option>
|
||||
<option value="quizGhost" id="draw-mode-option-quiz-ghost">โซนผี (ตอบผิดแล้ววาร์ปมาขัง — เดินออกไม่ได้)</option>
|
||||
</optgroup>
|
||||
<optgroup label="Quiz Battle">
|
||||
<option value="quizBattlePath" id="draw-mode-option-quiz-battle-path" hidden>เส้นทางเดิน (ม่วง — วาดอย่างน้อย 1 ช่อง = จำกัดเดินเฉพาะเส้นทาง)</option>
|
||||
@@ -426,7 +427,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/version.js?v=0.0258"></script>
|
||||
<script src="js/editor.js?v=0.0344"></script>
|
||||
<script src="js/editor.js?v=0.006131429"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
|
||||
<!-- ===== QB overlay layout: map เต็มจอ + แถบเครื่องมือลอย ดัก/พับ/ปิดเปิดได้ (เพิ่มทับ ไม่แตะ editor.js) ===== -->
|
||||
|
||||
@@ -0,0 +1,587 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="th">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<title>Editor ฉาก — Game</title>
|
||||
<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=Orbitron:wght@500;700;800&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/style.css?v=20260427-canvas-pixel">
|
||||
<link rel="stylesheet" href="css/editor-stack-hud-mock.css?v=0.0001">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container editor-page">
|
||||
<h1>Editor ฉาก</h1>
|
||||
<div class="editor-toolbar-wrap">
|
||||
<div class="editor-toolbar-grid">
|
||||
<section class="editor-card" aria-labelledby="editor-card-game-label">
|
||||
<h2 class="editor-card__title" id="editor-card-game-label">ประเภทเกม</h2>
|
||||
<div class="editor-card__row">
|
||||
<label class="editor-field-grow">เลือกโหมด
|
||||
<select id="game-type">
|
||||
<option value="zep">เดินอิสระ (ZEP)</option>
|
||||
<option value="lobby">ห้องโถง (รอผู้เล่น)</option>
|
||||
<option value="gauntlet">พรมแดงสุดท้าย (Last Light)</option>
|
||||
<option value="frogger">กบข้ามถนน</option>
|
||||
<option value="quiz">เกมตอบคำถาม (ถูก/ผิด)</option>
|
||||
<option value="quiz_carry">ตอบคำถาม — หยิบคำตอบมาวางกลาง</option>
|
||||
<option value="quiz_battle">Quiz Battle — โดม A B C (กด E)</option>
|
||||
<option value="stack">สลับจังหวะลงตึก (Stack)</option>
|
||||
<option value="jump_survive">กระโดดให้รอด (Jump Survival)</option>
|
||||
<option value="space_shooter">ยิงยานอวกาศ (Space Shooter)</option>
|
||||
<option value="balloon_boss">ลูกโป้งยิงบอส (Mega Virus)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="game-type-hint editor-card__hint editor-collapsible-hint" id="game-type-hint" role="note" aria-labelledby="editor-card-game-label" hidden></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">
|
||||
<h2 class="editor-card__title" id="editor-card-map-label">โหลด / ตั้งชื่อฉาก</h2>
|
||||
<div class="editor-card__row editor-card__row--wrap">
|
||||
<label>แก้จากฉากเดิม <select id="map-load"><option value="">โหลดรายการ...</option></select></label>
|
||||
<button type="button" id="btn-load">โหลดฉาก</button>
|
||||
<label class="editor-field-grow">ชื่อฉาก <input type="text" id="map-name" placeholder="ชื่อฉาก (ไม่ซ้ำ)"></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="editor-card" aria-labelledby="editor-card-grid-label">
|
||||
<h2 class="editor-card__title" id="editor-card-grid-label">ขนาดกริด</h2>
|
||||
<div class="editor-card__row editor-card__row--wrap">
|
||||
<label>กว้าง <input type="number" id="map-w" value="20" min="10" max="50"></label>
|
||||
<label>สูง <input type="number" id="map-h" value="15" min="10" max="40"></label>
|
||||
<label>ขนาดไทล์ <input type="number" id="tile-size" value="32" min="16" max="64"></label>
|
||||
<label title="ลงจากมุมซ้ายบนของจุดเกิด — เน้นขยายแนวตั้งก่อน (เช่น 4 = สูง 4 ช่อง)">ตัวละคร สูง (แนวตั้ง) <input type="number" id="character-cells-h" value="1" min="1" max="4"></label>
|
||||
<label title="ไปทางขวาจากมุมซ้ายบน — แนวนอน">× กว้าง (แนวนอน) <input type="number" id="character-cells-w" value="1" min="1" max="4"></label>
|
||||
<div class="editor-footprint-row editor-footprint-row--wall" role="group" aria-labelledby="editor-fp-wall-head">
|
||||
<div id="editor-fp-wall-head" class="editor-footprint-row__head">ชนกำแพง · กล่องตรวจ</div>
|
||||
<label class="editor-footprint-row__field" title="เฉพาะกำแพง (โหมดวาดกำแพง / objects = เดินไม่ได้) — แนวนอนกลางใต้ร่าง · เล็กกว่าตัว = หัว/ลำตัวทับสไปรต์กำแพงได้ · โซน interactive / hub / อื่น ๆ ใช้ขนาดตัวด้านบน" aria-describedby="editor-character-collision-hint">กว้าง <input type="number" id="character-collision-w" value="1" min="1" max="4" aria-label="ชนกำแพง กว้าง (ช่องกริด)"></label>
|
||||
<label class="editor-footprint-row__field" title="เฉพาะกำแพง — สูงจากเท้าขึ้นมา · น้อยกว่าสูงตัว = ส่วนบนไม่ชนกำแพง · โซนอื่นใช้สูงตัวด้านบน" aria-describedby="editor-character-collision-hint">× สูง <input type="number" id="character-collision-h" value="1" min="1" max="4" aria-label="ชนกำแพง สูงจากเท้า (ช่องกริด)"></label>
|
||||
</div>
|
||||
<div class="editor-footprint-row editor-footprint-row--no-overlap" role="group" aria-labelledby="editor-fp-nooverlap-head">
|
||||
<div id="editor-fp-nooverlap-head" class="editor-footprint-row__head">โซนห้ามซ้อน · กล่องตรวจ</div>
|
||||
<label class="editor-footprint-row__field" title="กล่องตรวจทับโซน «ห้ามซ้อนผู้เล่น» (เทียบชนกำแพง) — แนวนอนกลางใต้ร่าง · 0 = เต็มความกว้างตัว" aria-describedby="editor-character-collision-hint">กว้าง <input type="number" id="block-player-separation-w" value="0" min="0" max="4" step="1" aria-label="โซนห้ามซ้อน กว้างกล่องตรวจ (ช่องกริด)"></label>
|
||||
<label class="editor-footprint-row__field" title="กล่องตรวจทับโซน «ห้ามซ้อนผู้เล่น» — สูงจากเท้าขึ้นมา · 0 = เต็มสูงตัว" aria-describedby="editor-character-collision-hint">× สูง <input type="number" id="block-player-separation-h" value="0" min="0" max="4" step="1" aria-label="โซนห้ามซ้อน สูงกล่องตรวจจากเท้า (ช่องกริด)"></label>
|
||||
</div>
|
||||
<button type="button" id="btn-new">สร้างกริดใหม่</button>
|
||||
<p id="editor-character-collision-hint" class="editor-card__hint editor-collapsible-hint" style="margin:0.4rem 0 0;width:100%;max-width:44rem;line-height:1.45;" hidden>
|
||||
<strong>ชนกำแพง กว้าง×สูง</strong> ใช้แค่ตรวจชน <strong>กำแพง</strong> (ช่องวาด «กำแพง») เท่านั้น
|
||||
· <strong>interactive</strong> · <strong>โซนห้ามซ้อน</strong> · <strong>hub / ส่งคำตอบ quiz carry</strong> · โซนคำถาม/ตัวเลือก ฯลฯ ยังใช้พื้นที่ตัวละครตาม
|
||||
<strong>สูง (แนวตั้ง) × กว้าง (แนวนอน)</strong> เหมือนเดิม — ในเกม (<code>play.js</code>) แยกฟังก์ชันชนกำแพงกับ footprint โซนแล้ว · <strong>ห้ามซ้อน กว้าง×สูง</strong> = กำหนดกล่องตรวจทับโซนห้ามซ้อน (ชิดเท้า+กลางแนวนอน เหมือนชนกำแพง) ไม่ใช่การขยายรอบร่าง
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="editor-card editor-card--wide" aria-labelledby="editor-card-draw-label">
|
||||
<h2 class="editor-card__title" id="editor-card-draw-label">เครื่องมือวาด</h2>
|
||||
<p class="editor-draw-build-hint editor-collapsible-hint" style="margin:0 0 0.4rem;font-size:0.74rem;color:#9ece6a;line-height:1.35;" hidden>มีโหมด <strong>รูปบนกริด</strong> ในเมนู «โหมดวาด» — ถ้าไม่เห็นข้อความนี้หรือไม่มีเมนูนั้น = หน้าเว็บยังเป็นไฟล์เก่า (ลอง <strong>Ctrl+F5</strong> หรืออัปโหลด <code>editor.html</code> / <code>js/editor.js</code> ขึ้นเซิร์ฟใหม่)</p>
|
||||
<div class="editor-card__row editor-card__row--wrap">
|
||||
<label class="editor-field-drawmode">โหมดวาด
|
||||
<select id="draw-mode" title="จัดกลุ่มตามประเภท — ตัวเลือกที่ไม่ใช้กับโหมดเกมจะถูกซ่อน">
|
||||
<optgroup label="พื้นฐาน / ทุกโหมด">
|
||||
<option value="wall">กำแพง (เดินไม่ได้)</option>
|
||||
<option value="noOverlap">โซนห้ามซ้อนผู้เล่น</option>
|
||||
<option value="cellImage">รูปบนกริด (อัปโหลด / เลือกจากคลัง)</option>
|
||||
<option value="interactive">โซน interactive (จุดกด/ใช้ได้)</option>
|
||||
<option value="customizeSpot">จุดห้องแต่งตัว (เดินไปกด F — คลิกซ้ายวาง / ขวาลบ)</option>
|
||||
<option value="hostConsoleSpot" id="draw-mode-option-host-console">จุดตั้งค่าโฮสต์ (Host กด F — คลิกซ้ายวาง / ขวาลบ)</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>
|
||||
<optgroup label="พรมแดงสุดท้าย">
|
||||
<option value="gauntletPlayerSpawn" id="draw-mode-option-gauntlet-player-spawn" hidden>ลำดับเกิด 1–6 (คลิกซ้ายตามลำดับ)</option>
|
||||
<option value="gauntletLaserStart" id="draw-mode-option-gauntlet-laser-start" hidden>เลเซอร์: แถวต้น (ขอบบนของแถบ)</option>
|
||||
<option value="gauntletLaserEnd" id="draw-mode-option-gauntlet-laser-end" hidden>เลเซอร์: แถวปลาย (ขอบล่างของแถบ)</option>
|
||||
</optgroup>
|
||||
<optgroup label="Stack">
|
||||
<option value="stackRelease" id="draw-mode-option-stack-release" hidden>จุดปล่อยตึก (crane / ด้านบน)</option>
|
||||
<option value="stackLand" id="draw-mode-option-stack-land" hidden>จุดซ้อนตึก (แพลตฟอร์มรองรับ)</option>
|
||||
</optgroup>
|
||||
<optgroup label="ตอบคำถาม (ยืนโซน ถูก/ผิด)">
|
||||
<option value="quizTrue" id="draw-mode-option-quiz-true">โซน ถูก (ตอบ จริง)</option>
|
||||
<option value="quizFalse" id="draw-mode-option-quiz-false">โซน ผิด (ตอบ เท็จ)</option>
|
||||
<option value="quizQuestion" id="draw-mode-option-quiz-question">พื้นที่โชว์ข้อความคำถาม</option>
|
||||
</optgroup>
|
||||
<optgroup label="Quiz Battle">
|
||||
<option value="quizBattlePath" id="draw-mode-option-quiz-battle-path" hidden>เส้นทางเดิน (ม่วง — วาดอย่างน้อย 1 ช่อง = จำกัดเดินเฉพาะเส้นทาง)</option>
|
||||
<option value="quizBattleDome" id="draw-mode-option-quiz-battle-dome" hidden>โดมคำถาม (ช่องกด E ในเกม)</option>
|
||||
<option value="quizBattlePathGate" id="draw-mode-option-quiz-battle-path-gate" hidden>จุดปลดล็อก / ประตู (แดง — วางบนเส้นทาง ต้องตอบสถานีก่อนผ่าน)</option>
|
||||
<option value="quizBattleFinish" id="draw-mode-option-quiz-battle-finish" hidden>จุดจบ / ปลายทาง (เขียว — ตอบครบทุกข้อแล้วผู้เล่นไปอยู่ตรงนี้)</option>
|
||||
</optgroup>
|
||||
<optgroup label="กระโดดให้รอด">
|
||||
<option value="jumpSurvivePlatform1" id="draw-mode-option-jump-platform-1" hidden>แพลตฟอร์ม 1 (ยืนได้ — รูปช่อง 1 จาก Admin)</option>
|
||||
<option value="jumpSurvivePlatform2" id="draw-mode-option-jump-platform-2" hidden>แพลตฟอร์ม 2 (ยืนได้ — รูปช่อง 2 จาก Admin)</option>
|
||||
<option value="jumpSurvivePlatform3" id="draw-mode-option-jump-platform-3" hidden>แพลตฟอร์ม 3 (ยืนได้ — รูปช่อง 3 จาก Admin)</option>
|
||||
<option value="jumpSurviveHazard" id="draw-mode-option-jump-hazard" hidden>โซนตาย (แดง — ตัวละครโดนกล่องชนแล้วตายทันที)</option>
|
||||
</optgroup>
|
||||
<optgroup label="ยิงยานอวกาศ">
|
||||
<option value="shooterSpawnPaint" id="draw-mode-option-shooter-spawn" hidden>จุดเกิดยาน ผู้เล่น 1–6 (เลือกช่อง P ด้านขวา)</option>
|
||||
</optgroup>
|
||||
<optgroup label="ลูกโป้งยิงบอส">
|
||||
<option value="balloonBossPlayerPaint" id="draw-mode-option-balloon-boss-player" hidden>จุดเกิดผู้เล่น 1–6 (เลือก P ด้านขวา)</option>
|
||||
<option value="balloonBossBossPaint" id="draw-mode-option-balloon-boss-boss" hidden>จุดเกิดบอส (คลิกช่องเดียว — กลางจอในเกม)</option>
|
||||
</optgroup>
|
||||
<optgroup label="หยิบมาวาง (โซนกลาง + ตัวเลขช่อง)">
|
||||
<option value="quizCarryHub" id="draw-mode-option-quiz-carry-hub" class="quiz-carry-mode-opt" hidden>โซนกลาง (วางคำตอบ + โชว์คำถาม)</option>
|
||||
<option value="quizCarryOpt1" class="quiz-carry-mode-opt" hidden>ตัวเลือก 1</option>
|
||||
<option value="quizCarryOpt2" class="quiz-carry-mode-opt" hidden>ตัวเลือก 2</option>
|
||||
<option value="quizCarryOpt3" class="quiz-carry-mode-opt" hidden>ตัวเลือก 3</option>
|
||||
<option value="quizCarryOpt4" class="quiz-carry-mode-opt" hidden>ตัวเลือก 4</option>
|
||||
<option value="quizCarryOpt5" class="quiz-carry-mode-opt" hidden>ตัวเลือก 5</option>
|
||||
<option value="quizCarryOpt6" class="quiz-carry-mode-opt" hidden>ตัวเลือก 6</option>
|
||||
<option value="quizCarryOpt7" class="quiz-carry-mode-opt" hidden>ตัวเลือก 7</option>
|
||||
<option value="quizCarryOpt8" class="quiz-carry-mode-opt" hidden>ตัวเลือก 8</option>
|
||||
<option value="quizCarryOpt9" class="quiz-carry-mode-opt" hidden>ตัวเลือก 9</option>
|
||||
<option value="quizCarryOpt10" class="quiz-carry-mode-opt" hidden>ตัวเลือก 10</option>
|
||||
<option value="quizCarryOpt11" class="quiz-carry-mode-opt" hidden>ตัวเลือก 11</option>
|
||||
<option value="quizCarryOpt12" class="quiz-carry-mode-opt" hidden>ตัวเลือก 12</option>
|
||||
<option value="quizCarryOpt13" class="quiz-carry-mode-opt" hidden>ตัวเลือก 13</option>
|
||||
<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>
|
||||
<span id="quiz-battle-gate-wrap" class="editor-quiz-battle-gate-wrap" style="display:none;flex-wrap:wrap;align-items:center;gap:0.35rem 0.65rem;max-width:100%;" title="เลือกคำถามที่ต้องตอบถูกก่อนผ่าน — คลิกโดมเพื่อเลือก · คลิกเส้นทางม่วงวางประตู · ขวาลบ">
|
||||
<label style="display:flex;flex-direction:column;gap:0.15rem;min-width:12rem;flex:1 1 220px;">คำถาม (โดม / จุดปลดล็อก)
|
||||
<select id="quiz-battle-gate-question" style="max-width:100%;">
|
||||
<option value="1">กำลังโหลดคำถาม…</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" id="quiz-battle-gate-reload" class="editor-btn-secondary" style="font-size:0.72rem;padding:0.2rem 0.45rem;">โหลดคำถามใหม่</button>
|
||||
</span>
|
||||
<span id="cell-color-wrap" class="editor-cell-color" style="display:none;">
|
||||
<label>สี <input type="color" id="cell-color" value="#24283b" title="สี"></label>
|
||||
<label>Alpha <input type="range" id="cell-alpha" min="0" max="100" value="100" title="ความโปร่งใส 0=ใส 100=ทึบ"> <span id="cell-alpha-value">100</span>%</label>
|
||||
</span>
|
||||
<div id="editor-grid-image-tools" class="editor-grid-image-tools" style="display:none;">
|
||||
<div class="editor-grid-image-size-row">
|
||||
<label>กว้าง (ช่อง) <input type="number" id="grid-image-brush-w" value="1" min="1" max="24" title="จำนวนช่องกริดแนวนอน"></label>
|
||||
<label>สูง (ช่อง) <input type="number" id="grid-image-brush-h" value="1" min="1" max="24" title="จำนวนช่องกริดแนวตั้ง"></label>
|
||||
</div>
|
||||
<div id="grid-sprite-carry-bind-wrap" class="editor-grid-image-carry-bind-row" style="display:none;" title="quiz carry: สไปรต์ที่ไม่ทับช่องโซนตัวเลือก — ต้องผูกข้อเองถึงจะสลับรูปหยิบแล้ว">
|
||||
<label>ผูกสไปรต์กับข้อ (หยิบแล้ว)
|
||||
<select id="grid-sprite-carry-bind">
|
||||
<option value="auto">อัตโนมัติ (ช่องที่ทับโซนตัวเลือก)</option>
|
||||
<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>
|
||||
<option value="7">ข้อ 7</option>
|
||||
<option value="8">ข้อ 8</option>
|
||||
<option value="9">ข้อ 9</option>
|
||||
<option value="10">ข้อ 10</option>
|
||||
<option value="11">ข้อ 11</option>
|
||||
<option value="12">ข้อ 12</option>
|
||||
<option value="13">ข้อ 13</option>
|
||||
<option value="14">ข้อ 14</option>
|
||||
<option value="15">ข้อ 15</option>
|
||||
<option value="16">ข้อ 16</option>
|
||||
</select>
|
||||
</label>
|
||||
<span class="editor-hint-inline">Alt+คลิกซ้ายบนสไปรต์ = ใส่/เปลี่ยนผูก · รายการ "อัตโนมัติ" = ล้างผูก</span>
|
||||
</div>
|
||||
<div class="editor-grid-image-gallery-wrap">
|
||||
<span class="editor-grid-image-gallery-label">คลังรูป (คลิกเลือก)</span>
|
||||
<p class="editor-grid-image-gallery-note">สองช่องคือรูปที่บันทึกในแมป (ก่อนหยิบ / หยิบแล้ว) — ไม่อัปเดตตามการหยิบในห้อง · ทดสอบ idle/held ให้เปิดหน้าเล่น หลังบันทึกแมป</p>
|
||||
<div id="grid-cell-image-gallery" class="grid-cell-image-gallery" role="listbox" aria-label="คลังรูปบนกริด"></div>
|
||||
</div>
|
||||
<div class="editor-grid-image-upload-row">
|
||||
<label>อัปโหลดเข้าคลัง <input type="file" id="grid-cell-image-upload" accept="image/*"></label>
|
||||
<input type="file" id="grid-cell-image-held-upload" accept="image/*" hidden tabindex="-1" aria-hidden="true" title="อัปโหลดรูปตอนถือ (quiz carry)">
|
||||
<button type="button" id="btn-grid-image-remove-from-lib" title="ลบรูปที่เลือกในคลัง (สไปรต์ที่ใช้รูปนี้จะหาย)">ลบจากคลัง</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="editor-toggle" title="ปิด = ไม่วาดกริด/ช่องฉาก แต่ยังแสดงรูปที่วาดบนกริด · Off = hide tile grid, grid images still show"><input type="checkbox" id="show-map-in-game" checked> โชว์กริดในเกม</label>
|
||||
<button type="button" id="btn-clear-gauntlet-spawns" hidden title="ล้างรายการจุดเกิดพรมแดงที่กำหนดเอง">ล้างลำดับ 1–6</button>
|
||||
<button type="button" id="btn-clear-gauntlet-laser-span" hidden title="ล้างช่วงแนวตั้งของเลเซอร์ — กลับไปเต็มความสูงแมป (ไม่ต้องเปลี่ยนรูปจาก Admin)">ล้างช่วง laser</button>
|
||||
<label id="shooter-slot-paint-wrap" class="editor-field-grow" style="display:none;" title="ช่องผู้เล่นลำดับที่ 1–6 ตรงกับคนในเกม (คุณ = ลำดับแรกจาก join)">ช่องเกิดยาน P
|
||||
<select id="shooter-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>
|
||||
<label id="balloon-boss-slot-paint-wrap" class="editor-field-grow" style="display:none;" title="ลำดับ P1–P6 เหมือนยิงยาน">ช่องเกิดผู้เล่น P
|
||||
<select id="balloon-boss-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-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">
|
||||
<button type="button" id="btn-save" class="editor-btn-primary">บันทึกฉาก</button>
|
||||
<button type="button" id="btn-play-test" title="บันทึกฉากแล้วเท่านั้น — เปิดแท็บใหม่เล่นทดสอบด้วยตัวละครตัวแรกที่สร้าง (จาก /Game/character.html)">ทดลองเล่น</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="frogger-lanes-wrap" class="frogger-lanes" style="display:none;">
|
||||
<h3 id="frogger-lanes-title">ตั้งค่าแถว — กบข้ามถนน</h3>
|
||||
<p class="legend" id="frogger-lanes-legend">แถวบน (y=0) = ปลายทาง · แถวล่าง = จุดเกิด · ถนน/น้ำ ตั้งความเร็ว ทิศทาง และ <strong>ระยะห่าง</strong> (ยิ่งมากยิ่งห่าง เกมง่ายขึ้น)</p>
|
||||
<div id="frogger-lanes-table"></div>
|
||||
</div>
|
||||
|
||||
<section class="editor-card editor-card--bg" aria-labelledby="editor-card-bg-label">
|
||||
<h2 class="editor-card__title" id="editor-card-bg-label">พื้นหลัง</h2>
|
||||
<div class="editor-card__row editor-card__row--wrap">
|
||||
<label>รูปแผนที่ <input type="file" id="bg-image" accept="image/*"></label>
|
||||
<button type="button" id="btn-clear-bg">ลบรูปพื้นหลัง</button>
|
||||
</div>
|
||||
<div id="editor-stack-tower-vertical-bg-block" class="editor-bg-scroll-block" hidden>
|
||||
<p class="legend" style="margin:0.45rem 0 0.35rem;line-height:1.45"><strong>พื้นหลังแนวตั้ง (Stack Tower)</strong> ฉาก <code>mnn93hpi</code> — <strong>รูปแรก</strong> = ช่วงเริ่ม (intro) · <strong>รูปที่สอง</strong> = ไทล์วนลูป (loop) · เปิดแล้วใช้แทนรูปแผนที่เดียวในเกม · ความเร็ว 0 = ไม่เลื่อนอัตโนมัติ — ตั้ง <strong>ทุกกี่ชั้น</strong> แล้วเลื่อนแถบขึ้นทีละขั้น (แอนิเมชัน) · <em>English:</em> Speed 0 = no auto-scroll; optional step scroll every N placed blocks.</p>
|
||||
<div class="editor-card__row editor-card__row--wrap" style="align-items:center;gap:0.75rem 1rem">
|
||||
<label class="editor-bg-scroll-check"><input type="checkbox" id="editor-stack-tower-vbg-enabled"> ใช้ intro + loop แนวตั้ง / Use vertical strip BG</label>
|
||||
<label title="0 = นิ่ง (ไม่เลื่อนอัตโนมัติ)">ความเร็ว px/s <input type="number" id="editor-stack-tower-vbg-speed" min="0" max="400" step="4" value="0" style="width:5rem"></label>
|
||||
<label>ทิศทาง
|
||||
<select id="editor-stack-tower-vbg-direction">
|
||||
<option value="down" selected>บน → ล่าง</option>
|
||||
<option value="up">ล่าง → บน</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="editor-card__row editor-card__row--wrap" style="align-items:center;gap:0.75rem 1rem;margin-top:0.35rem">
|
||||
<label title="0 = ปิดการเลื่อนเป็นขั้น">เลื่อนทุก N ชั้น / Step every N layers <input type="number" id="editor-stack-tower-vbg-step-layers" min="0" max="200" step="1" value="0" style="width:4rem"></label>
|
||||
<label title="0 = ใช้ความสูงไทล์ loop บนจอ">ขั้น px (0=สูง loop) / Step px <input type="number" id="editor-stack-tower-vbg-step-px" min="0" max="8000" step="8" value="0" style="width:4.5rem"></label>
|
||||
<label>ระยะเวลาแอนิเมชัน ms <input type="number" id="editor-stack-tower-vbg-step-anim-ms" min="120" max="4000" step="20" value="520" style="width:4.5rem"></label>
|
||||
</div>
|
||||
<div class="editor-card__row editor-card__row--wrap" style="align-items:center;gap:0.75rem 1rem;margin-top:0.2rem">
|
||||
<label title="เว้นว่าง = ใช้จุดปล่อยจากโซนวาดบนแผนที่ · ตัวเลข = ระยะห่างจุดยึดเชือกเหนือขอบบนของบล็อกบนสุด (world px)">ช่องว่างปล่อยเหนือบล็อกบนสุด (world px, ว่าง=ค่าแมป) <input type="number" id="editor-stack-tower-vbg-release-gap" min="0" max="800" step="2" value="" placeholder="—" style="width:4.2rem" title="Release rope gap above top block"></label>
|
||||
</div>
|
||||
<div class="editor-card__row editor-card__row--wrap" style="margin-top:0.35rem">
|
||||
<label>รูปเริ่ม (intro) <input type="file" id="editor-stack-tower-vbg-intro-file" accept="image/*"></label>
|
||||
<label>รูปลูป (loop) <input type="file" id="editor-stack-tower-vbg-loop-file" accept="image/*"></label>
|
||||
<button type="button" id="editor-stack-tower-vbg-reset-assets">คืนรูปเริ่มจากเซิร์ฟ / Reset to bundled</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="editor-bg-scroll-block" class="editor-bg-scroll-block" hidden>
|
||||
<p class="legend" style="margin:0.5rem 0 0.35rem;line-height:1.45"><strong>เลื่อนพื้นหลังแนวตั้ง</strong> (เฉพาะฉากรหัส <code>mnpz6rkp</code>) — รูป intro ต่อด้วย loop · บันทึกฉากแล้วหน้าเล่นใช้ค่าเดียวกัน · <em>English:</em> Intro + looping strip; saved in map JSON.</p>
|
||||
<div class="editor-card__row editor-card__row--wrap" style="align-items:center;gap:0.75rem 1rem">
|
||||
<label class="editor-bg-scroll-check"><input type="checkbox" id="editor-bg-scroll-enabled"> เปิดเลื่อน / Enable scroll</label>
|
||||
<label>ความเร็ว px/s <input type="number" id="editor-bg-scroll-speed" min="8" max="400" step="4" value="56" style="width:5rem" title="ความเร็วการเลื่อนแนวตั้ง"></label>
|
||||
<label>ทิศทาง
|
||||
<select id="editor-bg-scroll-direction" title="เริ่มชิดล่าง intro แล้วเลื่อนเข้า loop ใต้ intro — ล่าง→บน = พิกเซลไหลขึ้น · บน→ล่าง = พลิกมุมมองในกรอบจอให้ไหลลง (strip เดิม)">
|
||||
<option value="up" selected>ล่าง → บน (ไหลขึ้น)</option>
|
||||
<option value="down">บน → ล่าง (ไหลลง)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="editor-card__row editor-card__row--wrap" style="margin-top:0.35rem">
|
||||
<label>รูปต้นทาง (intro) <input type="file" id="editor-bg-scroll-intro-file" accept="image/*"></label>
|
||||
<label>รูปลูป (loop) <input type="file" id="editor-bg-scroll-loop-file" accept="image/*"></label>
|
||||
<button type="button" id="editor-bg-scroll-reset-assets">คืนรูปเริ่มจากเซิร์ฟ / Reset to bundled</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="editor-gauntlet-runway-bg-block" class="editor-bg-scroll-block" hidden>
|
||||
<p class="legend" style="margin:0.5rem 0 0.35rem;line-height:1.45"><strong>พื้นหลังรันเวย์แนวนอน</strong> (เฉพาะฉากรหัส <code>mno9kb07</code> + โหมดพรมแดง) — รูป 1 = จุดเริ่ม (START) แล้วเลื่อนไปวนรูป 2–4 ระหว่างเล่น · พอหมดเวลาหรือเหลือผู้เล่นคนสุดท้าย (หลายคน) จะซ่อนอุปสรรค์แล้วแสดงรูปที่ 5 (FINISH) · บันทึกเป็น <code>gauntletCrownRunwayBg</code> ในแมป · <em>English:</em> Horizontal strip: start → loop images 2–4 → finish; obstacles hidden on end.</p>
|
||||
<div class="editor-card__row editor-card__row--wrap" style="align-items:center;gap:0.75rem 1rem">
|
||||
<label class="editor-bg-scroll-check"><input type="checkbox" id="editor-gauntlet-runway-bg-enabled" checked> เปิดเลื่อน / Enable runway BG</label>
|
||||
<label>ความเร็ว px/s <input type="number" id="editor-gauntlet-runway-bg-speed" min="8" max="400" step="4" value="48" style="width:5rem" title="ความเร็วเลื่อนแนวนอน (จอ)"></label>
|
||||
<label title="จุดเกิด (คอลัมน์ซ้ายสุด) เทียบตำแหน่งในแถบรูป START — เก็บเป็น runwaySpawnAlignFrac ในแมป">จุดเกิด vs START (0.02–0.95) <input type="number" id="editor-gauntlet-runway-bg-align-frac" min="0.02" max="0.95" step="0.01" value="0.32" style="width:4.2rem"></label>
|
||||
<label title="จุดอ้างอิงในรูป FINISH (5) ที่ต้องแนบคอลัมน์จบ — แนะนำคลิกกลางแถบ FINISH สีเหลือง">จุดอ้างอิงในรูป 5 (0–0.92) <input type="number" id="editor-gauntlet-runway-bg-finish-line-frac" min="0" max="0.92" step="0.01" value="0.34" style="width:4.2rem"></label>
|
||||
<label title="คอลัมน์กริดที่เส้น FINISH ต้องแนบ (ว่าง = อัตโนมัติตามขอบขวาพรมแดง) · Alt+คลิกบนแผนที่">คอลัมน์จบบนแผนที่ <input type="number" id="editor-gauntlet-runway-bg-finish-align-tile" min="0" max="99" step="1" placeholder="auto" style="width:4.2rem"></label>
|
||||
<label title="สำรอง: ตำแหน่งเส้นจบบนจอ (ใช้เมื่อไม่ตั้งคอลัมน์แผนที่)">จุดจบบนจอ (0.08–0.85) <input type="number" id="editor-gauntlet-runway-bg-finish-stop-vp" min="0.08" max="0.85" step="0.01" value="0.55" style="width:4.2rem"></label>
|
||||
</div>
|
||||
<div class="editor-card__row editor-card__row--wrap" style="margin-top:0.35rem">
|
||||
<label>รูปเริ่ม (1) <input type="file" id="editor-gauntlet-runway-bg-start-file" accept="image/*"></label>
|
||||
<label>รูปลูป (2) <input type="file" id="editor-gauntlet-runway-bg-loop2-file" accept="image/*"></label>
|
||||
<label>รูปลูป (3) <input type="file" id="editor-gauntlet-runway-bg-loop3-file" accept="image/*"></label>
|
||||
<label>รูปลูป (4) <input type="file" id="editor-gauntlet-runway-bg-loop4-file" accept="image/*"></label>
|
||||
<label>รูปจบ (5) <input type="file" id="editor-gauntlet-runway-bg-finish-file" accept="image/*"></label>
|
||||
<button type="button" id="editor-gauntlet-runway-bg-reset-assets">คืนรูปเริ่มจากเซิร์ฟ / Reset to bundled</button>
|
||||
</div>
|
||||
<div id="editor-gauntlet-runway-bg-previews" class="legend" style="margin-top:0.45rem;display:flex;flex-wrap:nowrap;gap:0.45rem 0.55rem;align-items:flex-end;overflow-x:auto;padding-bottom:2px;max-width:100%" aria-label="พรีวิวรูปรันเวย์ที่ใช้จริง (รวมค่าเริ่มจากเซิร์ฟถ้ายังไม่อัปโหลด)">
|
||||
<figure style="margin:0;text-align:center"><figcaption style="font-size:0.72rem;opacity:0.9">1 START</figcaption><img id="editor-gauntlet-runway-bg-prev-0" width="128" height="72" alt="" style="display:block;width:128px;height:72px;object-fit:cover;border:1px solid #3d3f52;border-radius:6px;background:#1a1b26"></figure>
|
||||
<figure style="margin:0;text-align:center"><figcaption style="font-size:0.72rem;opacity:0.9">2 LOOP</figcaption><img id="editor-gauntlet-runway-bg-prev-1" width="128" height="72" alt="" style="display:block;width:128px;height:72px;object-fit:cover;border:1px solid #3d3f52;border-radius:6px;background:#1a1b26"></figure>
|
||||
<figure style="margin:0;text-align:center"><figcaption style="font-size:0.72rem;opacity:0.9">3 LOOP</figcaption><img id="editor-gauntlet-runway-bg-prev-2" width="128" height="72" alt="" style="display:block;width:128px;height:72px;object-fit:cover;border:1px solid #3d3f52;border-radius:6px;background:#1a1b26"></figure>
|
||||
<figure style="margin:0;text-align:center"><figcaption style="font-size:0.72rem;opacity:0.9">4 LOOP</figcaption><img id="editor-gauntlet-runway-bg-prev-3" width="128" height="72" alt="" style="display:block;width:128px;height:72px;object-fit:cover;border:1px solid #3d3f52;border-radius:6px;background:#1a1b26"></figure>
|
||||
<figure class="editor-runway-prev-figure editor-runway-prev-figure--finish-pick" style="margin:0;text-align:center">
|
||||
<figcaption style="font-size:0.72rem;opacity:0.9">5 FINISH · คลิกกำหนดจุดจบ</figcaption>
|
||||
<div class="editor-runway-prev-pick" id="editor-gauntlet-runway-bg-prev-pick-4" title="คลิกบนรูปเพื่อตั้งเส้นจบ (แถบ FINISH)">
|
||||
<img id="editor-gauntlet-runway-bg-prev-4" width="128" height="72" alt="Runway FINISH — คลิกกำหนดจุดจบ" style="display:block;width:128px;height:72px;object-fit:cover;border:1px solid #3d3f52;border-radius:6px;background:#1a1b26;cursor:crosshair">
|
||||
<span class="editor-runway-finish-line-mark" id="editor-gauntlet-runway-finish-line-mark" aria-hidden="true"></span>
|
||||
</div>
|
||||
</figure>
|
||||
</div>
|
||||
<p id="editor-gauntlet-runway-bg-upload-summary" class="legend" style="margin:0.4rem 0 0;line-height:1.5;font-size:0.82rem" aria-live="polite"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="editor-card editor-card--help editor-collapsible-hint" aria-labelledby="editor-card-help-label" hidden>
|
||||
<h2 class="editor-card__title" id="editor-card-help-label">วิธีใช้บนกริด</h2>
|
||||
<ul class="editor-help-list legend editor-legend-short">
|
||||
<li><strong>คลิกซ้าย</strong> = วาด · <strong>คลิกขวา</strong> = ลบ · ลากค้างได้</li>
|
||||
<li>โหมดหลัก: กำแพง · โซนห้ามซ้อน · โซน interactive</li>
|
||||
<li><strong>พื้นที่สุ่มจุดเกิด</strong> (ฟ้าอ่อน) — ไม่วาดช่องนี้ได้ ใช้ปุ่ม «ตั้งจุดเกิด»</li>
|
||||
<li><strong>พื้นที่เริ่มเกม</strong> (ห้องโถง — ส้ม) — host ยืนแล้วกดเริ่ม</li>
|
||||
<li><strong>ถามตอบ</strong>: โซนถูก/ผิด + พื้นที่คำถาม (ทอง)</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>: โหมดวาดจุดปล่อย (ฟ้า) · จุดซ้อนตึก (ชมพู) · ถ้าอัปโหลดรูปทาง FTP ใช้โฟลเดอร์ <code>Game/public/img/TowerBlock</code> (ไม่มีช่องว่าง) — ชื่อ <code>Tower block</code> มี space มักได้ FTP <strong>550</strong></li>
|
||||
<li><strong>กระโดดให้รอด</strong>: โหมด <strong>แพลตฟอร์ม</strong> (ฟ้าอมเขียว) · โหมด <strong>โซนตาย</strong> (แดง — โดนแล้วตายทันที คงที่ในโลก) · กำแพง = ขอบซ้ายขวา/บน · Space / W = กระโดด</li>
|
||||
<li><strong>ลูกโป้งยิงบอส (Mega Virus)</strong>: จุดเกิดผู้เล่น P1–P6 + <strong>จุดเกิดบอส</strong> 1 ช่อง · ในเกมเดินช้า ยิงมีดีเลย์ · ลูกโป้งหมด = ตกรอบ · อัปโหลดรูปทาง FTP ใช้ <code>Game/public/img/MegaVirus</code> (<strong>ไม่มีช่องว่าง</strong>) — โฟลเดอร์ <code>Mega Virus</code> มี space มักได้ FTP <strong>550</strong></li>
|
||||
<li><strong>สีช่อง</strong>: เลือกโหมดสี + Alpha แล้วคลิกบนกริด</li>
|
||||
<li><strong>รูปบนกริด</strong>: โหมด «รูปบนกริด» — ตั้งกว้าง×สูง (ช่อง) · เลือกรูปจากแกลเลอรี · คลิกซ้ายวาง (ทับสไปรต์ที่ทับกัน) · คลิกขาวางลบทั้งแผ่น · บันทึกฉากเก็บเป็น <code>gridImageSprites</code> (ไฟล์ใหญ่ = JSON ใหญ่)</li>
|
||||
<li>ปิด «โชว์กริดในเกม» = ซ่อนกริด/กำแพงในเกม (รูปพื้นหลังยังแสดง)</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<div class="editor-workspace">
|
||||
<div class="editor-map-zoom-bar" id="editor-map-zoom-bar" aria-label="ซูมแผนที่">
|
||||
<button type="button" class="editor-map-zoom-btn" id="editor-zoom-out" title="ซูมออก (−)">−</button>
|
||||
<span class="editor-map-zoom-label" id="editor-zoom-label">100%</span>
|
||||
<button type="button" class="editor-map-zoom-btn" id="editor-zoom-in" title="ซูมเข้า (+)">+</button>
|
||||
<button type="button" class="editor-map-zoom-btn" id="editor-zoom-reset" title="ขนาดจริง 1:1">1:1</button>
|
||||
<button type="button" class="editor-map-zoom-btn" id="editor-zoom-fit" title="พอดีหน้าจอ">Fit</button>
|
||||
</div>
|
||||
<div class="canvas-wrap">
|
||||
<canvas id="editor-canvas"></canvas>
|
||||
<div id="editor-stack-hud-mock" class="is-hidden" aria-hidden="true">
|
||||
<aside class="editor-stack-hud-mock__score" aria-label="SCORE">
|
||||
<div class="editor-stack-hud-mock__score-title">SCORE</div>
|
||||
<ul class="editor-stack-hud-mock__score-list">
|
||||
<li class="editor-stack-hud-mock__score-row">
|
||||
<div class="editor-stack-hud-mock__score-left">
|
||||
<span class="editor-stack-hud-mock__av-wrap"><img class="editor-stack-hud-mock__av" src="/Game/img/characters/1_down.png" alt=""></span>
|
||||
<span class="editor-stack-hud-mock__name">ผู้เล่นทดสอบ</span>
|
||||
</div>
|
||||
<span class="editor-stack-hud-mock__points">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div class="editor-stack-hud-mock__center">
|
||||
<div class="editor-stack-hud-mock__time-label">TIME</div>
|
||||
<div class="editor-stack-hud-mock__time-val">0</div>
|
||||
<div class="editor-stack-hud-mock__time-sub">TOWER STACK · DECRYPT — TIME (sec) · เหลือเวลาถอดรหัส</div>
|
||||
</div>
|
||||
<div class="editor-stack-hud-mock__right">
|
||||
<div class="editor-stack-hud-mock__integrity">
|
||||
<div class="editor-stack-hud-mock__integrity-title">SYSTEM INTEGRITY:</div>
|
||||
<div class="editor-stack-hud-mock__hearts" aria-hidden="true"><span class="editor-stack-hud-mock__bracket">[</span><span class="editor-stack-hud-mock__heart">♥</span><span class="editor-stack-hud-mock__heart">♥</span><span class="editor-stack-hud-mock__heart">♥</span><span class="editor-stack-hud-mock__bracket">]</span></div>
|
||||
<div class="editor-stack-hud-mock__meta">P1 • ผู้เล่นทดสอบ • TEAM 0 • COMBO x0</div>
|
||||
</div>
|
||||
<div class="editor-stack-hud-mock__avatar-col">
|
||||
<div class="editor-stack-hud-mock__avatar-frame">
|
||||
<img class="editor-stack-hud-mock__avatar-img" src="/Game/img/characters/1_down.png" alt="">
|
||||
</div>
|
||||
<div class="editor-stack-hud-mock__link-chip" title="LINK">O</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="legend" id="editor-status">บันทึกแล้วจะได้รหัสฉาก ใช้รหัสนี้ตอนสร้างห้องในล็อบบี้</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/version.js?v=0.0258"></script>
|
||||
<script src="js/editor.js?v=0.006131121"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
|
||||
<!-- ===== QB overlay layout: map เต็มจอ + แถบเครื่องมือลอย ดัก/พับ/ปิดเปิดได้ (เพิ่มทับ ไม่แตะ editor.js) ===== -->
|
||||
<style id="qb-editor-overlay-style">
|
||||
.container.editor-page { max-width:none !important; width:auto !important; margin:0 !important; padding:0 !important; }
|
||||
.editor-page > h1 { position:fixed; top:6px; left:50%; transform:translateX(-50%); margin:0; font-size:13px; font-weight:700; opacity:.45; z-index:3; pointer-events:none; letter-spacing:.5px; }
|
||||
/* map เต็มจอ อยู่ข้างหลัง — เลื่อนดูได้ครบ (ไม่ใช้ flex-center ที่ตัดบน/ล่าง) */
|
||||
.editor-workspace { position:fixed !important; inset:0 !important; margin:0 !important; padding:16px !important;
|
||||
overflow:auto !important; z-index:1; text-align:center; }
|
||||
.editor-workspace .canvas-wrap { display:inline-block; margin:0 auto; box-shadow:0 12px 50px rgba(0,0,0,.6); }
|
||||
.editor-map-zoom-bar { position:fixed; right:14px; bottom:38px; z-index:6; display:flex; align-items:center; gap:5px;
|
||||
padding:6px 10px; border-radius:999px; background:rgba(8,12,28,.88); border:1px solid #2a3357;
|
||||
box-shadow:0 6px 24px rgba(0,0,0,.45); -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px); }
|
||||
.editor-map-zoom-btn { border:1px solid #2a3357; background:#0d1430; color:#eef2ff; border-radius:8px; min-width:34px; height:32px;
|
||||
padding:0 8px; cursor:pointer; font:inherit; font-weight:700; line-height:1; }
|
||||
.editor-map-zoom-btn:hover { filter:brightness(1.2); }
|
||||
.editor-map-zoom-label { min-width:46px; text-align:center; font-size:12px; font-weight:700; color:#c7d2fe; user-select:none; }
|
||||
/* ซ่อน mock HUD ของ Tower Stack ที่ไม่เกี่ยวกับการ set ฉาก */
|
||||
#editor-stack-hud-mock { display:none !important; }
|
||||
.editor-workspace #editor-status { position:fixed; left:50%; bottom:6px; transform:translateX(-50%); margin:0; z-index:5;
|
||||
background:rgba(8,12,28,.72); padding:4px 12px; border-radius:999px; font-size:12px; max-width:80vw; }
|
||||
/* แถบเครื่องมือ = แผงโปร่งแสง ลอยทับแมป */
|
||||
.editor-toolbar-wrap { position:fixed !important; z-index:40; margin:0 !important; padding:8px !important; overflow:auto;
|
||||
background:rgba(10,15,32,.82) !important; -webkit-backdrop-filter:blur(9px); backdrop-filter:blur(9px);
|
||||
border:1px solid #2a3357; box-shadow:0 10px 40px rgba(0,0,0,.55); transition:transform .2s ease; }
|
||||
.editor-toolbar-wrap.qb-left { top:0; left:0; height:100vh; width:min(440px,94vw); border-radius:0 16px 16px 0; }
|
||||
.editor-toolbar-wrap.qb-right { top:0; right:0; height:100vh; width:min(440px,94vw); border-radius:16px 0 0 16px; }
|
||||
.editor-toolbar-wrap.qb-top { top:0; left:0; right:0; width:100%; max-height:48vh; border-radius:0 0 16px 16px; }
|
||||
.editor-toolbar-wrap.qb-bottom { bottom:0; left:0; right:0; width:100%; max-height:48vh; border-radius:16px 16px 0 0; }
|
||||
.editor-toolbar-wrap.qb-left.qb-hidden { transform:translateX(-103%); }
|
||||
.editor-toolbar-wrap.qb-right.qb-hidden { transform:translateX(103%); }
|
||||
.editor-toolbar-wrap.qb-top.qb-hidden { transform:translateY(-103%); }
|
||||
.editor-toolbar-wrap.qb-bottom.qb-hidden { transform:translateY(103%); }
|
||||
.editor-toolbar-wrap.qb-left .editor-toolbar-grid, .editor-toolbar-wrap.qb-right .editor-toolbar-grid { display:flex !important; flex-direction:column; gap:8px; }
|
||||
.editor-toolbar-wrap.qb-top .editor-toolbar-grid, .editor-toolbar-wrap.qb-bottom .editor-toolbar-grid { display:flex !important; flex-direction:row; flex-wrap:nowrap; gap:8px; overflow-x:auto; align-items:flex-start; }
|
||||
.editor-toolbar-wrap.qb-top .editor-card, .editor-toolbar-wrap.qb-bottom .editor-card { min-width:300px; flex:0 0 auto; }
|
||||
.editor-toolbar-wrap .editor-card { background:rgba(18,24,52,.7) !important; border:1px solid #2a3357 !important; border-radius:12px !important; padding:9px 12px !important; margin:0 !important; }
|
||||
/* การ์ดพับได้ (กดหัวข้อ) */
|
||||
.editor-toolbar-wrap .editor-card__title { cursor:pointer; user-select:none; display:flex; align-items:center; gap:7px; }
|
||||
.editor-toolbar-wrap .editor-card__title::before { content:'\25BE'; font-size:.78em; opacity:.85; transition:transform .15s; }
|
||||
.editor-toolbar-wrap .editor-card.qb-card-collapsed > *:not(.editor-card__title) { display:none !important; }
|
||||
.editor-toolbar-wrap .editor-card.qb-card-collapsed .editor-card__title::before { transform:rotate(-90deg); }
|
||||
/* แถบหัวคุม (ดัก/พับ) */
|
||||
#qb-tb-head { position:sticky; top:0; z-index:3; display:flex; align-items:center; gap:6px; padding:6px 4px; margin-bottom:8px; background:rgba(10,15,32,.6); border-radius:9px; }
|
||||
#qb-tb-head b { flex:1; font-size:13px; white-space:nowrap; color:#eef2ff; }
|
||||
.qb-ctrl-btn { border:1px solid #2a3357; background:#0d1430; color:#eef2ff; border-radius:8px; min-width:32px; height:32px; padding:0 7px; cursor:pointer; font:inherit; font-weight:700; display:inline-grid; place-items:center; }
|
||||
.qb-ctrl-btn:hover { filter:brightness(1.25); }
|
||||
.qb-ctrl-btn.on { border-color:#22d3ee; background:rgba(34,211,238,.18); }
|
||||
#qb-tb-reopen { position:fixed; z-index:42; top:10px; left:10px; display:none; box-shadow:0 6px 20px rgba(0,0,0,.4); }
|
||||
#qb-tb-reopen.show { display:inline-grid; }
|
||||
/* คำแนะนำ Editor — ซ่อนเป็นค่าเริ่มต้น กดปุ่ม 💡 ในแถบหัวเพื่อเปิด */
|
||||
.editor-toolbar-wrap:not(.editor-hints-visible) .editor-collapsible-hint { display: none !important; }
|
||||
</style>
|
||||
<style>
|
||||
.editor-runway-prev-pick { position: relative; display: inline-block; }
|
||||
.editor-runway-finish-line-mark {
|
||||
position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px;
|
||||
background: #ffeb3b; box-shadow: 0 0 6px rgba(255, 235, 59, 0.95);
|
||||
pointer-events: none; z-index: 2;
|
||||
}
|
||||
.editor-runway-finish-line-mark::before,
|
||||
.editor-runway-finish-line-mark::after {
|
||||
content: ''; position: absolute; left: 50%; transform: translateX(-50%);
|
||||
border-left: 5px solid transparent; border-right: 5px solid transparent;
|
||||
}
|
||||
.editor-runway-finish-line-mark::before {
|
||||
top: -1px; border-bottom: 7px solid #ffeb3b;
|
||||
}
|
||||
.editor-runway-finish-line-mark::after {
|
||||
bottom: -1px; border-top: 7px solid #ffeb3b;
|
||||
}
|
||||
</style>
|
||||
<button id="qb-tb-reopen" class="qb-ctrl-btn" type="button">🛠️ เครื่องมือ</button>
|
||||
<script>
|
||||
(function(){
|
||||
var tw = document.querySelector('.editor-toolbar-wrap');
|
||||
if(!tw){ return; }
|
||||
var reopen = document.getElementById('qb-tb-reopen');
|
||||
// หัวคุม
|
||||
var hdr = document.createElement('div');
|
||||
hdr.id = 'qb-tb-head';
|
||||
hdr.innerHTML = '<b>🛠️ เครื่องมือฉาก</b>'+
|
||||
'<button type="button" class="qb-ctrl-btn" id="qb-tb-hints-toggle" title="แสดง/ซ่อนคำแนะนำ (โหมดเกม · ชนกำแพง · วิธีใช้กริด)">💡</button>'+
|
||||
'<button type="button" class="qb-ctrl-btn" data-dock="top" title="ดักบน">⬆</button>'+
|
||||
'<button type="button" class="qb-ctrl-btn" data-dock="left" title="ดักซ้าย">⬅</button>'+
|
||||
'<button type="button" class="qb-ctrl-btn" data-dock="right" title="ดักขวา">➡</button>'+
|
||||
'<button type="button" class="qb-ctrl-btn" data-dock="bottom" title="ดักล่าง">⬇</button>'+
|
||||
'<button type="button" class="qb-ctrl-btn" id="qb-tb-collapse" title="พับเก็บแถบ">✕</button>';
|
||||
tw.insertBefore(hdr, tw.firstChild);
|
||||
|
||||
function setDock(d){
|
||||
tw.classList.remove('qb-left','qb-right','qb-top','qb-bottom');
|
||||
tw.classList.add('qb-'+d);
|
||||
hdr.querySelectorAll('[data-dock]').forEach(function(b){ b.classList.toggle('on', b.getAttribute('data-dock')===d); });
|
||||
try{ localStorage.setItem('qbEdDock2', d); }catch(e){}
|
||||
}
|
||||
function setHidden(h){
|
||||
tw.classList.toggle('qb-hidden', h);
|
||||
reopen.classList.toggle('show', h);
|
||||
try{ localStorage.setItem('qbEdHidden2', h?'1':'0'); }catch(e){}
|
||||
}
|
||||
hdr.querySelectorAll('[data-dock]').forEach(function(b){ b.addEventListener('click', function(){ setDock(b.getAttribute('data-dock')); }); });
|
||||
document.getElementById('qb-tb-collapse').addEventListener('click', function(){ setHidden(true); });
|
||||
reopen.addEventListener('click', function(){ setHidden(false); });
|
||||
|
||||
var hintsBtn = document.getElementById('qb-tb-hints-toggle');
|
||||
function setHintsVisible(on) {
|
||||
tw.classList.toggle('editor-hints-visible', !!on);
|
||||
if (hintsBtn) {
|
||||
hintsBtn.classList.toggle('on', !!on);
|
||||
hintsBtn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||||
hintsBtn.title = on ? 'ซ่อนคำแนะนำ' : 'แสดงคำแนะนำ (โหมดเกม · ชนกำแพง · วิธีใช้กริด)';
|
||||
}
|
||||
tw.querySelectorAll('.editor-collapsible-hint').forEach(function(el) {
|
||||
if (on) el.removeAttribute('hidden');
|
||||
else el.setAttribute('hidden', '');
|
||||
});
|
||||
try { localStorage.setItem('qbEdHintsVisible', on ? '1' : '0'); } catch (e) { /* ignore */ }
|
||||
}
|
||||
if (hintsBtn) {
|
||||
hintsBtn.addEventListener('click', function() {
|
||||
setHintsVisible(!tw.classList.contains('editor-hints-visible'));
|
||||
});
|
||||
}
|
||||
var hintsOn = false;
|
||||
try { hintsOn = localStorage.getItem('qbEdHintsVisible') === '1'; } catch (e) { /* ignore */ }
|
||||
setHintsVisible(hintsOn);
|
||||
|
||||
// การ์ดแต่ละใบ กดหัวข้อเพื่อพับ/เปิด
|
||||
function keyFor(card, title){ return title.id || (title.textContent||'').trim().slice(0,40); }
|
||||
var cards = tw.querySelectorAll('.editor-card');
|
||||
cards.forEach(function(card){
|
||||
var title = card.querySelector('.editor-card__title');
|
||||
if(!title) return;
|
||||
title.addEventListener('click', function(){
|
||||
card.classList.toggle('qb-card-collapsed');
|
||||
try{
|
||||
var st = JSON.parse(localStorage.getItem('qbEdCards2')||'{}');
|
||||
st[keyFor(card,title)] = card.classList.contains('qb-card-collapsed')?1:0;
|
||||
localStorage.setItem('qbEdCards2', JSON.stringify(st));
|
||||
}catch(e){}
|
||||
});
|
||||
});
|
||||
|
||||
// คืนค่าที่จำไว้
|
||||
var dock='left'; try{ dock=localStorage.getItem('qbEdDock2')||'left'; }catch(e){}
|
||||
setDock(dock);
|
||||
try{ if(localStorage.getItem('qbEdHidden2')==='1') setHidden(true); }catch(e){}
|
||||
try{
|
||||
var st = JSON.parse(localStorage.getItem('qbEdCards2')||'{}');
|
||||
cards.forEach(function(card){ var t=card.querySelector('.editor-card__title'); if(t && st[keyFor(card,t)]) card.classList.add('qb-card-collapsed'); });
|
||||
}catch(e){}
|
||||
// ให้ editor.js คำนวณขนาด canvas ใหม่ตาม layout เต็มจอ
|
||||
setTimeout(function(){ try{ window.dispatchEvent(new Event('resize')); }catch(e){} }, 60);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -101,7 +101,7 @@
|
||||
let gameType = 'zep';
|
||||
let lanes = [];
|
||||
let ground = [], objects = [], blockPlayer = [], interactive = [], startGameArea = [], spawnArea = [], spawn = { x: 1, y: 1 };
|
||||
let quizTrueArea = [], quizFalseArea = [], quizQuestionArea = [];
|
||||
let quizTrueArea = [], quizFalseArea = [], quizQuestionArea = [], quizGhostArea = [];
|
||||
let quizCarryHubArea = [], quizCarryOptionArea = [];
|
||||
/** quiz_carry embed: ช่อง = 1 วางกล่องนับ 3-2-1 บนแมปเมื่อเลือก anchor = grid */
|
||||
let carryEmbedCountdownArea = [];
|
||||
@@ -1233,6 +1233,18 @@
|
||||
if (!quizQuestionArea[y] || quizQuestionArea[y].length !== width) quizQuestionArea[y] = Array(width).fill(0);
|
||||
}
|
||||
}
|
||||
if (!quizGhostArea.length || quizGhostArea.length !== height) {
|
||||
const existingG = quizGhostArea.slice().map(r => r && r.slice());
|
||||
quizGhostArea = [];
|
||||
for (let y = 0; y < height; y++) {
|
||||
const row = existingG[y] && existingG[y].length === width ? existingG[y].slice() : Array(width).fill(0);
|
||||
quizGhostArea.push(row);
|
||||
}
|
||||
} else {
|
||||
for (let y = 0; y < height; y++) {
|
||||
if (!quizGhostArea[y] || quizGhostArea[y].length !== width) quizGhostArea[y] = Array(width).fill(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureStackAreas() {
|
||||
@@ -2482,6 +2494,9 @@
|
||||
if (drawModeEl && drawModeEl.value === 'quizQuestion' && gt !== 'quiz' && gt !== 'quiz_carry') {
|
||||
drawModeEl.value = 'wall';
|
||||
}
|
||||
if (drawModeEl && drawModeEl.value === 'quizGhost' && gt !== 'quiz') {
|
||||
drawModeEl.value = 'wall';
|
||||
}
|
||||
if (drawModeEl && (drawModeEl.value === 'stackRelease' || drawModeEl.value === 'stackLand') && gt !== 'stack') {
|
||||
drawModeEl.value = 'wall';
|
||||
}
|
||||
@@ -2528,6 +2543,7 @@
|
||||
quizTrueArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizFalseArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizQuestionArea = Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizGhostArea = 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));
|
||||
@@ -2814,6 +2830,12 @@
|
||||
ctx.strokeStyle = 'rgba(255, 130, 200, 0.95)';
|
||||
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
||||
}
|
||||
if (quizGhostArea[y] && quizGhostArea[y][x] === 1) {
|
||||
ctx.fillStyle = 'rgba(160, 120, 255, 0.42)';
|
||||
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
||||
ctx.strokeStyle = 'rgba(185, 150, 255, 0.95)';
|
||||
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
||||
}
|
||||
}
|
||||
if (gtDraw === 'stack') {
|
||||
if (stackReleaseArea[y] && stackReleaseArea[y][x] === 1) {
|
||||
@@ -3454,6 +3476,10 @@
|
||||
quizQuestionArea[y][x] = left ? 1 : 0;
|
||||
if (left) carryEmbedCountdownArea[y][x] = 0;
|
||||
} else return;
|
||||
} else if (drawModeEl.value === 'quizGhost') {
|
||||
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz') return;
|
||||
ensureQuizAreas();
|
||||
quizGhostArea[y][x] = left ? 1 : 0;
|
||||
} else if (drawModeEl.value === 'stackRelease') {
|
||||
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'stack') return;
|
||||
ensureStackAreas();
|
||||
@@ -3704,6 +3730,9 @@
|
||||
.then(list => {
|
||||
mapLoadEl.innerHTML = '<option value="">— เลือกฉากเดิม —</option>';
|
||||
(list || []).forEach(m => {
|
||||
if (!m) return;
|
||||
// ซ่อนฉาก Quiz Battle (qbroom*) — มี qb-editor.html แยกแล้ว
|
||||
if (/^qbroom\d+$/i.test(String(m.id || '')) || m.gameType === 'quiz_battle') return;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = m.id;
|
||||
opt.textContent = (m.name || m.id) + ' (' + m.id + ')';
|
||||
@@ -4089,6 +4118,7 @@
|
||||
if (drawModeEl && drawModeEl.value === 'spawnArea' && gameType === 'frogger') drawModeEl.value = 'wall';
|
||||
if (drawModeEl && (drawModeEl.value === 'quizTrue' || drawModeEl.value === 'quizFalse') && gameType !== 'quiz') drawModeEl.value = 'wall';
|
||||
if (drawModeEl && drawModeEl.value === 'quizQuestion' && gameType !== 'quiz' && gameType !== 'quiz_carry') drawModeEl.value = 'wall';
|
||||
if (drawModeEl && drawModeEl.value === 'quizGhost' && gameType !== 'quiz') drawModeEl.value = 'wall';
|
||||
if (drawModeEl && (drawModeEl.value === 'stackRelease' || drawModeEl.value === 'stackLand') && gameType !== 'stack') drawModeEl.value = 'wall';
|
||||
if (drawModeEl && quizCarryEditorCarryModes().indexOf(drawModeEl.value) >= 0 && gameType !== 'quiz_carry') drawModeEl.value = 'wall';
|
||||
if (drawModeEl && /^jumpSurvivePlatform\d$/.test(drawModeEl.value) && gameType !== 'jump_survive') drawModeEl.value = 'wall';
|
||||
@@ -4265,7 +4295,7 @@
|
||||
? { x: Math.floor(s.x), y: Math.floor(s.y) }
|
||||
: null));
|
||||
})(),
|
||||
quizTrueArea, quizFalseArea, quizQuestionArea,
|
||||
quizTrueArea, quizFalseArea, quizQuestionArea, quizGhostArea,
|
||||
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()) : [],
|
||||
@@ -4616,6 +4646,7 @@
|
||||
quizTrueArea = m.quizTrueArea && m.quizTrueArea.length ? m.quizTrueArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizFalseArea = m.quizFalseArea && m.quizFalseArea.length ? m.quizFalseArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizQuestionArea = m.quizQuestionArea && m.quizQuestionArea.length ? m.quizQuestionArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
quizGhostArea = m.quizGhostArea && m.quizGhostArea.length ? m.quizGhostArea.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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+202
-30
@@ -658,6 +658,9 @@
|
||||
let mapData = null, tileSize = 32, myId = null;
|
||||
let stackMini = null;
|
||||
let stackFall = null;
|
||||
/** เศษบล็อกที่พลาด/หล่น → ตกแบบ physics หลุดขอบจอ (visual เท่านั้น ไม่กระทบคะแนน/sync) */
|
||||
let stackMissDebris = [];
|
||||
let _stackDebrisLastT = 0;
|
||||
/** Tower / Stack — เชือกเหวี่ยงจาก `img/TowerBlock/sling.png` */
|
||||
let stackTowerSlingImg = null;
|
||||
/** Tower mnn93hpi — +คะแนน (`score+10.png` / `score+20.png`) + เอฟเฟกต์ ×2 (`score-light.png`, `scorex2.png`) */
|
||||
@@ -1823,6 +1826,59 @@
|
||||
let mapGridImageHeldImgs = [];
|
||||
let me = { x: 1, y: 1, direction: 'down', nickname: nick, isWalking: false, playTint: null, gauntletScore: 0, gauntletEliminated: false, tx: null, ty: null, quizCarryHeld: null, gauntletCrownPenaltyFxUntil: 0 };
|
||||
const others = new Map();
|
||||
/** ===== MG3 terminal log (เกมจริง) — hacking-terminal ตามสเปก #mg3-terminal-log ===== */
|
||||
let mg3TermLog = [];
|
||||
let mg3TermLineNo = 0;
|
||||
const MG3_TERM_MAX = 240;
|
||||
function mg3TermPush(lines) {
|
||||
const arr = Array.isArray(lines) ? lines : [lines];
|
||||
for (let i = 0; i < arr.length; i++) mg3TermLog.push(String(arr[i] == null ? '' : arr[i]));
|
||||
while (mg3TermLog.length > MG3_TERM_MAX) mg3TermLog.shift();
|
||||
}
|
||||
function mg3TermReset() { mg3TermLog = []; mg3TermLineNo = 0; }
|
||||
function mg3TermBoot() {
|
||||
mg3TermLog = []; mg3TermLineNo = 0;
|
||||
mg3TermPush([
|
||||
'[INIT] Booting decryption sequence...',
|
||||
'[SYS] Locating target server...',
|
||||
'[SYS] Connection established. Status: SECURE.',
|
||||
'[HACK] Bypassing "Vault Lockdown" protocol...',
|
||||
'[HACK] Disabling Cyber Security Check...',
|
||||
'[LOAD] Injecting Decryption_Core.exe...',
|
||||
'[READY] Awaiting manual code block alignment...',
|
||||
'>>> STACKING SEQUENCE START <<<',
|
||||
]);
|
||||
}
|
||||
function mg3TermOnPlace(perfect, progressPct) {
|
||||
mg3TermLineNo += 1;
|
||||
const n = mg3TermLineNo;
|
||||
if (n >= 100 || (Number.isFinite(Number(progressPct)) && Number(progressPct) >= 99.5)) {
|
||||
mg3TermPush([
|
||||
'>>> Compile Line [100]: SUCCESS. Maximum stack reached.',
|
||||
'[DATA]: Decryption progress: 100%. Master key generated.',
|
||||
'[HACK]: "Vault Lockdown" bypassed. Firewall disabled.',
|
||||
'[SYS]: Evidence files unlocked. Initiating secure download...',
|
||||
'>>> MISSION ACCOMPLISHED. Disconnecting safely. <<<',
|
||||
]);
|
||||
} else if (perfect) {
|
||||
mg3TermPush('>>> Compile Line [' + n + ']: PERFECT. Synergy bonus!');
|
||||
} else {
|
||||
mg3TermPush('>>> Compile Line [' + n + ']: SUCCESS. Stack stable.');
|
||||
}
|
||||
}
|
||||
function mg3TermOnMiss(attemptsRemaining) {
|
||||
if (attemptsRemaining <= 0) {
|
||||
mg3TermPush([
|
||||
'[CRITICAL]: Packet loss detected. Compile FAILED. (Attempts remaining: 0)',
|
||||
'[ALERT]: Cyber Security Check triggered! Intruder traced.',
|
||||
'[SYS]: Initiating "Vault Lockdown" protocol...',
|
||||
'>>> FATAL ERROR: Connection Terminated. SYSTEM LOCKED. <<<',
|
||||
]);
|
||||
} else {
|
||||
mg3TermPush('[WARNING]: Packet loss detected. Compile FAILED. (Attempts remaining: ' + attemptsRemaining + '/3)');
|
||||
}
|
||||
}
|
||||
|
||||
/** Stack preview HUD: เทอร์มินัลล็อก (cyber UI) */
|
||||
const STACK_PREVIEW_HUD_LOG_MAX = 8;
|
||||
let stackPreviewHudLog = [];
|
||||
@@ -3004,6 +3060,49 @@
|
||||
quizWrongGhostImg = img;
|
||||
return img;
|
||||
}
|
||||
/** ===== โซนผี (quizGhostArea): ตอบผิด→วาร์ปมาขังในโซน เดินออกไม่ได้ — gated: ทำงานเฉพาะเมื่อแมปมีการวาดโซนผี ===== */
|
||||
function quizGhostAreaGridPlay() {
|
||||
const g = mapData && mapData.quizGhostArea;
|
||||
if (!Array.isArray(g)) return null;
|
||||
for (let y = 0; y < g.length; y++) { if (g[y] && g[y].some((v) => v === 1)) return g; }
|
||||
return null;
|
||||
}
|
||||
function quizMeIsGhostPlay() {
|
||||
return !!(isQuiz() && playQuizPlayerLocal && (playQuizPlayerLocal.cannotTrue || playQuizPlayerLocal.cannotFalse));
|
||||
}
|
||||
function quizGhostConfineActivePlay() {
|
||||
return quizMeIsGhostPlay() && !!quizGhostAreaGridPlay();
|
||||
}
|
||||
/** footprint ของผู้เล่นที่ (x,y) อยู่ในโซนผีครบทุกช่องไหม */
|
||||
function quizGhostFootprintInsidePlay(x, y) {
|
||||
const g = quizGhostAreaGridPlay();
|
||||
if (!g) return true;
|
||||
for (const k of quizTilesFootprintPlay(x, y)) {
|
||||
const p = k.split(','); const tx = +p[0], ty = +p[1];
|
||||
if (!g[ty] || g[ty][tx] !== 1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/** วาร์ป me เข้าโซนผี (ช่องที่ footprint ลงได้ ใกล้กลางโซนสุด) — คืน true ถ้ามีโซนผี */
|
||||
function warpMeIntoQuizGhostZonePlay() {
|
||||
const g = quizGhostAreaGridPlay();
|
||||
if (!g) return false;
|
||||
if (quizGhostFootprintInsidePlay(me.x, me.y)) return true;
|
||||
const xs = [], ys = [];
|
||||
for (let y = 0; y < g.length; y++) { const row = g[y] || []; for (let x = 0; x < row.length; x++) { if (row[x] === 1) { xs.push(x); ys.push(y); } } }
|
||||
if (!xs.length) return false;
|
||||
const cx = Math.round((Math.min.apply(null, xs) + Math.max.apply(null, xs)) / 2);
|
||||
const cy = Math.round((Math.min.apply(null, ys) + Math.max.apply(null, ys)) / 2);
|
||||
const cand = [];
|
||||
for (let i = 0; i < xs.length; i++) cand.push([xs[i], ys[i], Math.abs(xs[i] - cx) + Math.abs(ys[i] - cy)]);
|
||||
cand.sort((a, b) => a[2] - b[2]);
|
||||
let tx = xs[0], ty = ys[0];
|
||||
for (const c of cand) { if (quizGhostFootprintInsidePlay(c[0], c[1])) { tx = c[0]; ty = c[1]; break; } }
|
||||
me.x = tx; me.y = ty; me.tx = tx; me.ty = ty;
|
||||
try { if (socket && socket.connected) socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } catch (e) { /* ignore */ }
|
||||
return true;
|
||||
}
|
||||
|
||||
/** ตรวจว่า peer/me ตอบผิดในแมป mng8a80o แล้ว → คุมผีแทน avatar */
|
||||
function isQuizWrongGhostActiveForEnt(entId) {
|
||||
if (!isQuizQuestionMissionUiMapPlay()) return false;
|
||||
@@ -5180,14 +5279,16 @@
|
||||
} else {
|
||||
playQuizPlayerLocal.cannotTrue = true;
|
||||
playQuizPlayerLocal.cannotFalse = true;
|
||||
const ordMe = quizPreviewJoinOrderForRespawn(myId);
|
||||
const sp = pickSpawnForJoinPlay(mapData, ordMe);
|
||||
const pos = findNearestOutsideQuizAnswerZonesPlay(mapData, sp.x + 0.5, sp.y + 0.5);
|
||||
me.x = pos.x;
|
||||
me.y = pos.y;
|
||||
me.tx = me.x;
|
||||
me.ty = me.y;
|
||||
socket.emit('move', { x: me.x, y: me.y, direction: me.direction });
|
||||
if (!warpMeIntoQuizGhostZonePlay()) {
|
||||
const ordMe = quizPreviewJoinOrderForRespawn(myId);
|
||||
const sp = pickSpawnForJoinPlay(mapData, ordMe);
|
||||
const pos = findNearestOutsideQuizAnswerZonesPlay(mapData, sp.x + 0.5, sp.y + 0.5);
|
||||
me.x = pos.x;
|
||||
me.y = pos.y;
|
||||
me.tx = me.x;
|
||||
me.ty = me.y;
|
||||
socket.emit('move', { x: me.x, y: me.y, direction: me.direction });
|
||||
}
|
||||
}
|
||||
results.push({
|
||||
id: myId,
|
||||
@@ -5775,16 +5876,18 @@
|
||||
const life = fxUntil != null ? Math.max(0, Math.min(1, (fxUntil - nowT) / STACK_TOWER_PERFECT_X2_MS)) : 1;
|
||||
const pulse = 0.4 + 0.48 * Math.sin((1 - life) * Math.PI);
|
||||
if (lightImg && lightImg.complete && lightImg.naturalWidth) {
|
||||
const lp = 1.06;
|
||||
const lp = 2.7;
|
||||
const lw = dw * lp;
|
||||
const lh = dh * lp;
|
||||
const lnx = lightImg.naturalWidth;
|
||||
const lny = lightImg.naturalHeight;
|
||||
/* จัดแสงให้ตรงกลางไอคอน ×2 (ไอคอนวาดเยื้องซ้าย cx - dw*0.48 → กลางไอคอน ≈ cx + dw*0.02) */
|
||||
const iconCx = cx + dw * 0.02;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = Math.min(0.9, pulse + 0.12);
|
||||
ctx.globalAlpha = Math.min(0.98, pulse + 0.34);
|
||||
ctx.globalCompositeOperation = 'screen';
|
||||
try {
|
||||
ctx.drawImage(lightImg, 0, 0, lnx, lny, cx - lw * 0.5, cy - lh * 0.5, lw, lh);
|
||||
ctx.drawImage(lightImg, 0, 0, lnx, lny, iconCx - lw * 0.5, cy - lh * 0.5, lw, lh);
|
||||
} catch (e) { /* ignore */ }
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -5847,6 +5950,57 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** สร้างเศษบล็อกตกจากบล็อกที่พลาด (ใช้ตำแหน่ง/หน้าตาเดิมของ stackFall) */
|
||||
function spawnStackMissDebrisPlay(sf) {
|
||||
if (!sf || !stackMini) return;
|
||||
const lh = stackMini.layerWorldH || Math.max(14, tileSize * 0.3);
|
||||
const cxW = sf.xLeft1 + sf.tw / 2;
|
||||
const landOff = sf.hit && sf.hit.landOffsetTiles ? sf.hit.landOffsetTiles : 0;
|
||||
const dir = landOff >= 0 ? 1 : -1;
|
||||
stackMissDebris.push({
|
||||
cxW,
|
||||
topY: sf.y1,
|
||||
w: sf.tw,
|
||||
h: lh,
|
||||
vx: dir * (tileSize * (1.4 + Math.random() * 0.9)),
|
||||
vy: -tileSize * (0.4 + Math.random() * 0.5),
|
||||
rot: sf.tiltMax || 0,
|
||||
vrot: dir * (2.0 + Math.random() * 1.8),
|
||||
seat: sf.actorSeat,
|
||||
heavy: !!sf.actorHeavy,
|
||||
hue: (stackMini.layers.length * 47) % 360,
|
||||
});
|
||||
while (stackMissDebris.length > 6) stackMissDebris.shift();
|
||||
_stackDebrisLastT = 0;
|
||||
}
|
||||
/** อัปเดต+วาดเศษบล็อกที่ตก (gravity + หมุน + ลอยออกข้าง) จนหลุดขอบจอล่าง */
|
||||
function drawStackMissDebrisPlay(ctx, worldToScreen, zoom, floorWorldY) {
|
||||
if (!stackMissDebris.length) return;
|
||||
const now = performance.now();
|
||||
let dt = _stackDebrisLastT ? (now - _stackDebrisLastT) / 1000 : 0.016;
|
||||
_stackDebrisLastT = now;
|
||||
if (dt > 0.05) dt = 0.05;
|
||||
const g = tileSize * 34;
|
||||
const chCv = canvas && canvas.height ? canvas.height : 720;
|
||||
const offLimit = floorWorldY + chCv / Math.max(0.2, zoom) + tileSize * 5;
|
||||
for (let i = stackMissDebris.length - 1; i >= 0; i--) {
|
||||
const d = stackMissDebris[i];
|
||||
d.vy += g * dt;
|
||||
d.topY += d.vy * dt;
|
||||
d.cxW += d.vx * dt;
|
||||
d.rot += d.vrot * dt;
|
||||
if (d.topY > offLimit) { stackMissDebris.splice(i, 1); continue; }
|
||||
const sp = worldToScreen(d.cxW - d.w / 2, d.topY);
|
||||
const drawW = d.w * zoom;
|
||||
const drawH = d.h * zoom;
|
||||
ctx.save();
|
||||
ctx.translate(sp[0] + drawW / 2, sp[1] + drawH / 2);
|
||||
ctx.rotate(d.rot);
|
||||
drawStackBlockSpriteOrHue(ctx, -drawW / 2, -drawH / 2, drawW, drawH, d.seat, d.heavy, d.hue, { missTint: true, missOverlay: true });
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
function ensureStackTowerHeartMinusImgPlay() {
|
||||
if (!stackTowerHeartMinusImg) {
|
||||
stackTowerHeartMinusImg = new Image();
|
||||
@@ -6056,24 +6210,29 @@
|
||||
const life = Math.max(0, Math.min(1, (untilT - nowT) / STACK_TOWER_PERFECT_GLOW_MS));
|
||||
const pulse = 0.38 + 0.48 * Math.sin((1 - life) * Math.PI);
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(sx, sy, drawW, drawH);
|
||||
ctx.clip();
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.shadowColor = 'rgba(255, 210, 40, 0.9)';
|
||||
ctx.shadowBlur = Math.max(3, Math.min(10, 4 + pulse * 5));
|
||||
/* เติมเหลืองทับบล็อกให้เด่น */
|
||||
ctx.globalAlpha = Math.min(0.6, 0.3 + pulse * 0.32);
|
||||
ctx.fillStyle = 'rgba(255, 224, 60, 0.55)';
|
||||
ctx.fillRect(sx, sy, drawW, drawH);
|
||||
/* กรอบเหลืองหนา "รอบ" บล็อก (เลยขอบออกมา) + เรืองแสงออกนอก → เห็นชัด */
|
||||
const pad = Math.max(3, Math.min(12, drawW * 0.05));
|
||||
const sw = Math.max(3, Math.min(9, drawW * 0.055));
|
||||
ctx.shadowColor = 'rgba(255, 196, 24, 0.98)';
|
||||
ctx.shadowBlur = Math.max(10, Math.min(30, 14 + pulse * 16));
|
||||
ctx.shadowOffsetX = 0;
|
||||
ctx.shadowOffsetY = 0;
|
||||
ctx.globalAlpha = Math.min(0.85, 0.4 + pulse * 0.34);
|
||||
ctx.fillStyle = 'rgba(255, 228, 72, 0.52)';
|
||||
ctx.fillRect(sx, sy, drawW, drawH);
|
||||
ctx.globalAlpha = Math.min(1, 0.62 + pulse * 0.38);
|
||||
ctx.strokeStyle = 'rgba(255, 238, 110, 1)';
|
||||
ctx.lineWidth = sw;
|
||||
ctx.strokeRect(sx - pad + sw * 0.5, sy - pad + sw * 0.5, drawW + pad * 2 - sw, drawH + pad * 2 - sw);
|
||||
/* เส้นในขาวอมเหลือง เพิ่มความคม */
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.shadowColor = 'transparent';
|
||||
ctx.globalAlpha = Math.min(0.72, 0.26 + pulse * 0.32);
|
||||
ctx.strokeStyle = 'rgba(255, 250, 200, 0.78)';
|
||||
const sw = Math.max(1.2, Math.min(2.8, drawW * 0.022));
|
||||
ctx.lineWidth = sw;
|
||||
ctx.strokeRect(sx + sw * 0.5, sy + sw * 0.5, drawW - sw, drawH - sw);
|
||||
ctx.globalAlpha = Math.min(0.95, 0.55 + pulse * 0.4);
|
||||
ctx.strokeStyle = 'rgba(255, 252, 214, 0.96)';
|
||||
ctx.lineWidth = Math.max(1.5, sw * 0.42);
|
||||
ctx.strokeRect(sx - pad + sw, sy - pad + sw, drawW + pad * 2 - sw * 2, drawH + pad * 2 - sw * 2);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
@@ -9923,9 +10082,13 @@
|
||||
|
||||
const logEl = document.getElementById('mg3-terminal-log');
|
||||
if (logEl) {
|
||||
const lines = stackPreviewHudLog && stackPreviewHudLog.length
|
||||
? stackPreviewHudLog.slice(-7)
|
||||
: ['>>> DECRYPT UPLINK READY', '>>> SPACE / CLICK = DROP (ตาคุณ)'];
|
||||
const towerLiveNow = isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'live';
|
||||
if (towerLiveNow && (!mg3TermLog || !mg3TermLog.length)) mg3TermBoot();
|
||||
else if (!towerLiveNow && mg3TermLog && mg3TermLog.length) mg3TermReset();
|
||||
let lines;
|
||||
if (mg3TermLog && mg3TermLog.length) lines = mg3TermLog.slice(-9);
|
||||
else if (stackPreviewHudLog && stackPreviewHudLog.length) lines = stackPreviewHudLog.slice(-7);
|
||||
else lines = ['>>> DECRYPT UPLINK READY', '>>> SPACE / CLICK = DROP (ตาคุณ)'];
|
||||
const txt = lines.join('\n');
|
||||
if (logEl.textContent !== txt) logEl.textContent = txt;
|
||||
}
|
||||
@@ -10691,7 +10854,11 @@
|
||||
stackMini.stackTiltVel = 0;
|
||||
}
|
||||
}
|
||||
if (towerLive) triggerStackTowerHeartMinusFxPlay();
|
||||
if (towerLive) {
|
||||
triggerStackTowerHeartMinusFxPlay();
|
||||
const remain = stackMini.teamMissesLeft != null ? stackMini.teamMissesLeft : Math.max(0, (stackMini.lives || 0));
|
||||
mg3TermOnMiss(Number(remain) || 0);
|
||||
}
|
||||
} else {
|
||||
const seatLay = layerVisualMeta && layerVisualMeta.seat != null ? layerVisualMeta.seat : getStackDropActorSeat();
|
||||
const heavyLay = !!(layerVisualMeta && layerVisualMeta.heavy);
|
||||
@@ -10752,6 +10919,7 @@
|
||||
}
|
||||
while (stackMini.scorePopups.length > 12) stackMini.scorePopups.shift();
|
||||
}
|
||||
if (towerLive && !hit.miss) mg3TermOnPlace(perfectFull || !!hit.perfect, stackMini.progressPct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11051,6 +11219,7 @@
|
||||
const hit = stackFall.hit;
|
||||
const stableMin = STACK_STABLE_SUPPORT_RATIO_MIN;
|
||||
if (hit.miss) {
|
||||
spawnStackMissDebrisPlay(stackFall);
|
||||
applyStackHitFromDrop(hit);
|
||||
if (playBotsEnabled() && (wasHuman || fallBotId)) {
|
||||
stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId });
|
||||
@@ -11291,6 +11460,7 @@
|
||||
if (towerHudLive) {
|
||||
drawStackTowerHeartMinusFxPlay(ctx, worldToScreen, zoom, m, layerWorldH, floorY, nLay);
|
||||
}
|
||||
drawStackMissDebrisPlay(ctx, worldToScreen, zoom, floorY);
|
||||
}
|
||||
|
||||
function applyGauntletTimingFromServer(payload) {
|
||||
@@ -18318,6 +18488,7 @@
|
||||
eliminated: !!st.eliminated,
|
||||
score: typeof st.score === 'number' ? st.score : (playQuizPlayerLocal.score || 0),
|
||||
};
|
||||
if (quizGhostConfineActivePlay()) warpMeIntoQuizGhostZonePlay();
|
||||
});
|
||||
|
||||
socket.on('quiz-ended', () => {
|
||||
@@ -19353,6 +19524,7 @@
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (quizGhostConfineActivePlay() && !quizGhostFootprintInsidePlay(x, y)) return false;
|
||||
if (isQuizCarry() && quizCarryFootprintOverlapsHub(x, y)) return false;
|
||||
if (isQuizBattle() && quizBattlePathModeActive(mapData)) {
|
||||
if (!quizBattlePositionAllowedPlay(mapData, x, y)) return false;
|
||||
@@ -20430,9 +20602,9 @@
|
||||
cyberHudScoreAvatarUrlCache.set(cacheKey, lobbyFace);
|
||||
return;
|
||||
}
|
||||
/* crop ยังไม่เสร็จ (async) → ตั้งเต็มตัวชั่วคราว แล้ว retry จนได้หัว (กัน human โชว์เต็มตัวคนเดียว)
|
||||
/* crop ยังไม่เสร็จ (async) → ใช้ "หน้า default" ชั่วคราว (อย่าโชว์เต็มตัว — ผู้เล่นต้องเห็นแค่หัวเหมือนบอท) แล้ว retry จนได้หัวจริง
|
||||
จำกัดจำนวน retry กัน loop ถ้า crop ล้มเหลวถาวร */
|
||||
avImg.src = lobbySrc;
|
||||
avImg.src = defaultAvatarImg.src;
|
||||
var hudAvTries = avImg.__hudAvHeadTries || 0;
|
||||
if (hudAvTries < 12) {
|
||||
avImg.__hudAvHeadTries = hudAvTries + 1;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2343,7 +2343,7 @@
|
||||
.mg3-terminal-log {
|
||||
position: absolute; left: 9%; right: 9%; top: 10%; bottom: 10%;
|
||||
overflow: hidden;
|
||||
font: 22px/1.4 ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||
font: 11px/1.4 ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||
color: #8fe9ff;
|
||||
text-shadow: 0 0 6px rgba(0, 200, 255, 0.5);
|
||||
white-space: pre-wrap; word-break: break-word;
|
||||
@@ -5152,7 +5152,7 @@
|
||||
<script src="/app-base.js?v=2"></script>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0306"></script>
|
||||
<script src="js/play.js?v=0.00610200052"></script>
|
||||
<script src="js/play.js?v=0.006131711"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
<style id="qc-howto-mg2-fix">
|
||||
/* HOW TO PLAY ของ quiz_carry (Minigame-4) -> ให้เหมือน Minigame-2 (gch-mg2-mock)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+12
-9
@@ -6174,11 +6174,10 @@ function runGauntletTick(sid, space) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* ถึงเส้นชัยแล้ว (client แจ้ง) — หยุดสร้าง/ชน obstacle ให้ทุกคนเดินเข้าเส้นชัยได้ ไม่มี -10 อีก */
|
||||
/* ถึงเส้นชัยแล้ว (client แจ้ง) — จบรอบทันที (ผู้ใช้ต้องการ: ไม่ให้เดินเลยเส้นชัย) → สรุปผล */
|
||||
if (gr.finishPhase) {
|
||||
if (gr.obstacles.length) { gr.obstacles = []; }
|
||||
space.peers.forEach((p) => { if ((p.gauntletJumpTicks || 0) > 0) p.gauntletJumpTicks--; });
|
||||
emitGauntletSync(sid, space);
|
||||
endGauntletGame(sid, space, 'finish');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6226,16 +6225,20 @@ function runGauntletTick(sid, space) {
|
||||
gr.spawnAcc = 0;
|
||||
gr.nextSpawnIn = 2 + Math.floor(Math.random() * 6);
|
||||
/* ประเภท 2: เลเซอร์ — ทุกแถว */
|
||||
let _gauntletLaserThisTick = false;
|
||||
if (Math.random() < GAUNTLET_LASER_SPAWN_CHANCE) {
|
||||
const span = gauntletLaserVerticalSpanFromMap(md);
|
||||
gr.obstacles.push({ id: ++gr.nextObsId, kind: 'laser', x: w - 1, y0: span.y0, y1: span.y1 });
|
||||
_gauntletLaserThisTick = true;
|
||||
}
|
||||
/* ประเภท 1: แยกเลน — แถวเท้า · กันซ้อนคอลัมน์เดียวกับเลเซอร์ (laser+lane ไม่โผล่คอลัมน์เดียวกัน) */
|
||||
if (!_gauntletLaserThisTick) {
|
||||
laneSpawnRows.forEach((ly) => {
|
||||
if (Math.random() < GAUNTLET_LANE_ROW_SPAWN_CHANCE) {
|
||||
gr.obstacles.push({ id: ++gr.nextObsId, kind: 'lane', x: w - 1, y: ly });
|
||||
}
|
||||
});
|
||||
}
|
||||
/* ประเภท 1: แยกเลน — แถวเท้า (ไม่ใช่แถวหัว; ไม่ใช่แถวใต้เท้าอีกช่อง) */
|
||||
laneSpawnRows.forEach((ly) => {
|
||||
if (Math.random() < GAUNTLET_LANE_ROW_SPAWN_CHANCE) {
|
||||
gr.obstacles.push({ id: ++gr.nextObsId, kind: 'lane', x: w - 1, y: ly });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* กระโดดข้ามสำเร็จ → เลื่อนผู้เล่นไปขวา แต่ไม่ลบ obstacle (ยังไหลต่อให้คนอื่น/รอบถัดไป) */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -82,6 +82,6 @@
|
||||
|
||||
<script src="../app-base.js?v=2"></script>
|
||||
<script src="../Game/js/display-name.js?v=2"></script>
|
||||
<script src="join.js?v=0.004"></script>
|
||||
<script src="join.js?v=0.006131120"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -78,9 +78,18 @@
|
||||
window.location.href = BASE + '/room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(getNick());
|
||||
}
|
||||
|
||||
// ห้อง Quiz Battle (50 คน) มีหน้าเลือกเฉพาะที่ /Quiz-Battle/ แล้ว — ไม่ต้องโชว์ปนใน Join Room
|
||||
var QUIZ_BATTLE_ROOM_PREFIX = 'Quiz Battle ห้อง ';
|
||||
function isQuizBattleRoom(room) {
|
||||
return !!(room && room.spaceName && room.spaceName.indexOf(QUIZ_BATTLE_ROOM_PREFIX) === 0);
|
||||
}
|
||||
|
||||
function renderPublicRooms(list) {
|
||||
if (!listPublic || !tplRow) return;
|
||||
listPublic.innerHTML = '';
|
||||
if (Array.isArray(list)) {
|
||||
list = list.filter(function (room) { return !isQuizBattleRoom(room); });
|
||||
}
|
||||
if (!Array.isArray(list) || list.length === 0) {
|
||||
listPublic.innerHTML = '<div class="join-list-status">ยังไม่มีห้อง</div>';
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function checkLoginStatus() {
|
||||
const isLoggedIn = localStorage.getItem('isLoggedIn');
|
||||
if (!isLoggedIn || isLoggedIn !== 'true') {
|
||||
window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!checkLoginStatus()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
|
||||
const SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + (typeof appPath === 'function' ? appPath('/Game') : '/Game');
|
||||
const IMG = {
|
||||
publishOn: 'IMAGE/btn-publish-room-h.png',
|
||||
publishOff: 'IMAGE/btn-publish-room.png',
|
||||
privateOn: 'IMAGE/btn-private-room-h.png',
|
||||
privateOff: 'IMAGE/btn-private-room.png',
|
||||
joinOn: 'IMAGE/btn-join-room.png',
|
||||
joinOff: 'IMAGE/btn-join-room-2.png',
|
||||
};
|
||||
const USE_PUBLIC_ROOM_MOCK = false;
|
||||
const MOCK_PUBLIC_ROOMS = [
|
||||
{ spaceId: 'JD-001', spaceName: '[JUSTICE DIVERS #01]', mapName: 'Neo City', peerCount: 2, maxPlayers: 4 },
|
||||
{ spaceId: 'JD-002', spaceName: '[JUSTICE DIVERS #02]', mapName: 'Cyber Lab', peerCount: 5, maxPlayers: 6 },
|
||||
{ spaceId: 'JD-003', spaceName: '[JUSTICE DIVERS #03]', mapName: 'Sky Deck', peerCount: 5, maxPlayers: 5 },
|
||||
{ spaceId: 'JD-004', spaceName: '[JUSTICE DIVERS #04]', mapName: 'Dockyard', peerCount: 2, maxPlayers: 4 },
|
||||
{ spaceId: 'JD-005', spaceName: '[JUSTICE DIVERS #05]', mapName: 'Temple', peerCount: 3, maxPlayers: 4 },
|
||||
{ spaceId: 'JD-006', spaceName: '[JUSTICE DIVERS #06]', mapName: 'Arena', peerCount: 6, maxPlayers: 6 },
|
||||
{ spaceId: 'JD-007', spaceName: '[JUSTICE DIVERS #07]', mapName: 'Outpost', peerCount: 1, maxPlayers: 4 }
|
||||
];
|
||||
|
||||
function getNick() {
|
||||
if (typeof window.jdReadDisplayName === 'function') {
|
||||
return window.jdReadDisplayName();
|
||||
}
|
||||
try {
|
||||
return (localStorage.getItem('roomLobbyDisplayName') || localStorage.getItem('playerName') || '').trim() || 'ผู้เล่น';
|
||||
} catch (e) {
|
||||
return 'ผู้เล่น';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = s;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
var btnBack = document.getElementById('btn-back');
|
||||
var tabPublic = document.getElementById('tab-public');
|
||||
var tabPrivate = document.getElementById('tab-private');
|
||||
var imgPublic = document.getElementById('tab-img-public');
|
||||
var imgPrivate = document.getElementById('tab-img-private');
|
||||
var viewPublic = document.getElementById('join-public-view');
|
||||
var viewPrivate = document.getElementById('join-private-view');
|
||||
var listPublic = document.getElementById('room-list-public');
|
||||
var publicScrollThumb = document.getElementById('join-scrollbar-thumb');
|
||||
var tplRow = document.getElementById('tpl-room-row');
|
||||
var passPrivate = document.getElementById('join-private-password');
|
||||
var idPrivate = document.getElementById('join-private-room-id');
|
||||
var btnPrivateJoin = document.getElementById('btn-private-join');
|
||||
|
||||
btnBack?.addEventListener('click', function () {
|
||||
window.location.href = typeof appPath === 'function' ? appPath('/Main-Menu/') : '/Main-Menu/';
|
||||
});
|
||||
|
||||
passPrivate?.addEventListener('input', function (e) {
|
||||
e.target.value = e.target.value.replace(/[^0-9]/g, '').slice(0, 4);
|
||||
});
|
||||
|
||||
function goToRoom(spaceId) {
|
||||
window.location.href = BASE + '/room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(getNick());
|
||||
}
|
||||
|
||||
function renderPublicRooms(list) {
|
||||
if (!listPublic || !tplRow) return;
|
||||
listPublic.innerHTML = '';
|
||||
if (!Array.isArray(list) || list.length === 0) {
|
||||
listPublic.innerHTML = '<div class="join-list-status">ยังไม่มีห้อง</div>';
|
||||
return;
|
||||
}
|
||||
list.forEach(function (room) {
|
||||
var code = room.spaceId || ('room-' + Math.random().toString(36).slice(2, 8));
|
||||
var name = room.spaceName || code;
|
||||
var mapName = room.mapName || '—';
|
||||
var count = room.peerCount != null ? room.peerCount : 0;
|
||||
var max = room.maxPlayers != null ? room.maxPlayers : 10;
|
||||
var full = count >= max;
|
||||
var node = tplRow.content.cloneNode(true);
|
||||
var nameEl = node.querySelector('.join-room-name');
|
||||
var playersEl = node.querySelector('.join-room-players');
|
||||
var btn = node.querySelector('.join-room-btn');
|
||||
var img = node.querySelector('.join-room-btn-img');
|
||||
|
||||
nameEl.textContent = 'ห้อง : ' + name + ' [' + mapName + ']';
|
||||
playersEl.textContent = 'ผู้เล่น : ' + count + '/' + max;
|
||||
img.src = IMG.joinOn;
|
||||
img.alt = full ? 'เต็ม' : 'เข้าร่วม';
|
||||
|
||||
if (full) {
|
||||
btn.disabled = true;
|
||||
} else {
|
||||
btn.addEventListener('click', function () {
|
||||
goToRoom(room.spaceId);
|
||||
});
|
||||
}
|
||||
listPublic.appendChild(node);
|
||||
});
|
||||
syncPublicScrollbarThumb();
|
||||
}
|
||||
|
||||
function syncPublicScrollbarThumb() {
|
||||
if (!listPublic || !publicScrollThumb) return;
|
||||
var total = listPublic.scrollHeight;
|
||||
var visible = listPublic.clientHeight;
|
||||
var track = listPublic.parentElement ? listPublic.parentElement.clientHeight : visible;
|
||||
var styles = window.getComputedStyle(publicScrollThumb);
|
||||
var topInset = parseFloat(styles.top) || 0;
|
||||
var bottomInset = topInset;
|
||||
var minThumb = parseFloat(styles.minHeight) || 40;
|
||||
var innerTrack = Math.max(0, track - topInset - bottomInset);
|
||||
if (!total || total <= visible || track <= 0) {
|
||||
publicScrollThumb.style.opacity = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
var thumbH = Math.max(minThumb, (visible / total) * innerTrack);
|
||||
var maxY = Math.max(0, innerTrack - thumbH);
|
||||
var ratio = listPublic.scrollTop / Math.max(1, total - visible);
|
||||
var y = topInset + (maxY * ratio);
|
||||
var clampedY = Math.min(topInset + maxY, Math.max(topInset, y));
|
||||
|
||||
publicScrollThumb.style.height = thumbH + 'px';
|
||||
publicScrollThumb.style.transform = 'translateY(' + clampedY + 'px)';
|
||||
publicScrollThumb.style.opacity = '1';
|
||||
}
|
||||
|
||||
function refreshPublicRooms() {
|
||||
if (!listPublic || !tplRow) return;
|
||||
if (USE_PUBLIC_ROOM_MOCK) {
|
||||
renderPublicRooms(MOCK_PUBLIC_ROOMS);
|
||||
return;
|
||||
}
|
||||
listPublic.innerHTML = '<div class="join-list-status" id="room-list-status">โหลดรายการห้อง...</div>';
|
||||
var statusEl = document.getElementById('room-list-status');
|
||||
fetch(SERVER + '/api/spaces?_=' + Date.now())
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (list) {
|
||||
if (!Array.isArray(list)) {
|
||||
if (statusEl) statusEl.textContent = 'โหลดรายการห้องไม่ได้';
|
||||
return;
|
||||
}
|
||||
renderPublicRooms(list);
|
||||
})
|
||||
.catch(function () {
|
||||
if (statusEl) statusEl.textContent = 'โหลดรายการห้องไม่ได้';
|
||||
});
|
||||
}
|
||||
|
||||
function setTab(mode) {
|
||||
var isPublic = mode === 'public';
|
||||
if (imgPublic) imgPublic.src = isPublic ? IMG.publishOn : IMG.publishOff;
|
||||
if (imgPrivate) imgPrivate.src = isPublic ? IMG.privateOff : IMG.privateOn;
|
||||
tabPublic?.setAttribute('aria-selected', isPublic ? 'true' : 'false');
|
||||
tabPrivate?.setAttribute('aria-selected', isPublic ? 'false' : 'true');
|
||||
if (viewPublic) viewPublic.classList.toggle('join-view--hidden', !isPublic);
|
||||
if (viewPrivate) viewPrivate.classList.toggle('join-view--hidden', isPublic);
|
||||
}
|
||||
|
||||
tabPublic?.addEventListener('click', function () {
|
||||
setTab('public');
|
||||
});
|
||||
|
||||
tabPrivate?.addEventListener('click', function () {
|
||||
setTab('private');
|
||||
});
|
||||
|
||||
btnPrivateJoin?.addEventListener('click', function () {
|
||||
var code = (idPrivate?.value || '').trim();
|
||||
var pass = (passPrivate?.value || '').trim();
|
||||
if (!code) {
|
||||
alert('กรุณากรอกรหัสห้อง');
|
||||
idPrivate?.focus();
|
||||
return;
|
||||
}
|
||||
if (pass && pass.length !== 4) {
|
||||
alert('รหัสผ่านต้องเป็นตัวเลข 4 หลัก');
|
||||
passPrivate?.focus();
|
||||
return;
|
||||
}
|
||||
goToRoom(code);
|
||||
});
|
||||
|
||||
if (USE_PUBLIC_ROOM_MOCK) {
|
||||
renderPublicRooms(MOCK_PUBLIC_ROOMS);
|
||||
} else {
|
||||
refreshPublicRooms();
|
||||
setInterval(refreshPublicRooms, 8000);
|
||||
}
|
||||
listPublic?.addEventListener('scroll', syncPublicScrollbarThumb);
|
||||
window.addEventListener('resize', syncPublicScrollbarThumb);
|
||||
setTab('public');
|
||||
})();
|
||||
Reference in New Issue
Block a user