bot update connect all game

This commit is contained in:
2026-05-21 12:12:08 +00:00
parent e46cd54645
commit c1cc9739d9
51 changed files with 106021 additions and 15 deletions
+12
View File
@@ -89,6 +89,18 @@
"coins": 0,
"createdAt": "2026-05-06T20:37:46+00:00",
"updatedAt": "2026-05-06T20:37:46+00:00"
},
{
"id": "7c655e167f8dd9f927cd94b2",
"email": "",
"displayName": "Guest",
"loginType": "guest",
"providerUserId": "p_1779349953863_4wjw8uzxu4c",
"notes": "auto: player-coins",
"blocked": false,
"coins": 0,
"createdAt": "2026-05-21T07:52:34+00:00",
"updatedAt": "2026-05-21T07:52:34+00:00"
}
]
}
File diff suppressed because one or more lines are too long
+1
View File
@@ -102,6 +102,7 @@
<option value="noOverlap">โซนห้ามซ้อนผู้เล่น</option>
<option value="cellImage">รูปบนกริด (อัปโหลด / เลือกจากคลัง)</option>
<option value="interactive">โซน interactive (จุดกด/ใช้ได้)</option>
<option value="customizeSpot">จุดห้องแต่งตัว (เดินไปกด 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>
@@ -0,0 +1,404 @@
<!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" id="game-type-hint" role="note" aria-labelledby="editor-card-game-label"></div>
<div id="editor-quiz-carry-countdown-wrap" class="editor-card__row editor-card__row--wrap" style="display:none;gap:0.65rem 1rem;align-items:center;margin-top:0.45rem;" aria-label="หยิบมาวาง พรีวิวนับถอยหลัง">
<label class="editor-field-nogrow" style="display:flex;align-items:center;gap:0.35rem;cursor:pointer;">
<input type="checkbox" id="carry-embed-countdown-highlight" checked>
<span>พรีวิว: ไฮไลต์โซนตัวเลือกตอนนับ 3-2-1</span>
</label>
<label class="editor-field-nogrow">สีขอบไฮไลต์
<input type="color" id="carry-embed-countdown-color" value="#ffb44a" title="สีขอบเรืองรอบช่องตัวเลือก (ตรงกับโซนที่วาดเป็น ตัวเลือก 1–16 บนกริด)">
</label>
<label class="editor-field-grow">ตำแหน่งเลข 3-2-1
<select id="carry-embed-countdown-anchor" title="ตามกริด = วาดช่องในโหมด «ตำแหน่งเลข 3-2-1» · อัตโนมัติ = โซนทองหรือโซนกลาง">
<option value="grid">ตามกริดที่วาด (โหมดวาดด้านล่าง)</option>
<option value="map" selected>บนแมป — โซนทอง (คำถาม) หรือโซนกลาง</option>
<option value="screen">กลางจอ (ม่านทึบ)</option>
</select>
</label>
</div>
</section>
<section class="editor-card" aria-labelledby="editor-card-map-label">
<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" style="margin:0.4rem 0 0;width:100%;max-width:44rem;line-height:1.45;">
<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" style="margin:0 0 0.4rem;font-size:0.74rem;color:#9ece6a;line-height:1.35;">มีโหมด <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="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>
</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="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+คลิกซ้ายบนสไปรต์ = ใส่/เปลี่ยนผูก · รายการ &quot;อัตโนมัติ&quot; = ล้างผูก</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="ล้างรายการจุดเกิดพรมแดงที่กำหนดเอง">ล้างลำดับ 16</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="ลำดับ P1P6 เหมือนยิงยาน">ช่องเกิดผู้เล่น 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="ล้างจุด P1P6 บนแผนที่">ล้างจุด P1P6</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 24 → 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.020.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>
</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 style="margin:0;text-align:center"><figcaption style="font-size:0.72rem;opacity:0.9">5 FINISH</figcaption><img id="editor-gauntlet-runway-bg-prev-4" 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>
</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" aria-labelledby="editor-card-help-label">
<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>ตัวเลือก 116</strong> = ช่องบนกริดตรงกับลำดับ <code>choices</code> · พรีวิวนับ <strong>3-2-1</strong> ไฮไลต์โซนข้อถูก (หรือใส่ <code>countdownHighlightSlot</code> 116 ต่อข้อใน <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>: จุดเกิดผู้เล่น P1P6 + <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="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.0334"></script>
<div class="version-tag">v —</div>
</body>
</html>
+35
View File
@@ -130,6 +130,7 @@
/** balloon_boss: กริด 0 หรือ 1–6 = จุดเกิดผู้เล่น · บอส = tile เดียว */
let balloonBossPlayerSlots = [];
let balloonBossBossSpawn = null;
let customizeSpot = null; // จุด "ห้องแต่งตัว" ในฉาก (เดินไปกด F) — วางเองในeditor
let showMapInGame = true;
let backgroundImage = null;
let backgroundImageImg = null;
@@ -2513,6 +2514,19 @@
ctx.fillText('BOSS', tx + tileSize / 2, ty + tileSize / 2 + 4);
ctx.textAlign = 'left';
}
if (customizeSpot && customizeSpot.x === x && customizeSpot.y === y) {
ctx.fillStyle = 'rgba(34, 211, 238, 0.42)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(120, 230, 255, 0.95)';
ctx.lineWidth = 2;
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.lineWidth = 1;
ctx.fillStyle = '#eafaff';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('แต่งตัว', tx + tileSize / 2, ty + tileSize / 2 + 4);
ctx.textAlign = 'left';
}
if (gtDraw === 'quiz_carry') {
if (quizQuestionArea[y] && quizQuestionArea[y][x] === 1) {
ctx.fillStyle = 'rgba(255, 214, 102, 0.44)';
@@ -3070,6 +3084,19 @@
if (statusEl) statusEl.textContent = 'ลบจุดเกิดบอส (เกมจะใช้กลางแมป)';
}
return;
} else if (drawModeEl.value === 'customizeSpot') {
if (objects[y][x] === 1) {
if (statusEl) statusEl.textContent = 'ช่องกำแพง — ใช้เป็นจุดห้องแต่งตัวไม่ได้';
return;
}
if (left) {
customizeSpot = { x: x, y: y };
if (statusEl) statusEl.textContent = 'จุดห้องแต่งตัว → ช่อง (' + x + ',' + y + ')';
} else {
customizeSpot = null;
if (statusEl) statusEl.textContent = 'ลบจุดห้องแต่งตัว';
}
return;
} else if (drawModeEl.value === 'quizBattlePath') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'quiz_battle') return;
ensureQuizBattlePathArea();
@@ -3700,6 +3727,9 @@
balloonBossBossSpawn: gameType === 'balloon_boss' && balloonBossBossSpawn && Number.isFinite(balloonBossBossSpawn.x)
? { x: Math.floor(balloonBossBossSpawn.x), y: Math.floor(balloonBossBossSpawn.y) }
: null,
customizeSpot: customizeSpot && Number.isFinite(customizeSpot.x)
? { x: Math.floor(customizeSpot.x), y: Math.floor(customizeSpot.y) }
: null,
gridImageLibrary: gridImageLibrary.map(serializeGridLibEntry).filter((x) => x != null),
gridImageSprites: gridImageSprites.map((s) => {
const o = { i: s.i, x: s.x, y: s.y, w: s.w, h: s.h };
@@ -4000,6 +4030,11 @@
} else {
balloonBossBossSpawn = null;
}
if (m.customizeSpot && Number.isFinite(Number(m.customizeSpot.x)) && Number.isFinite(Number(m.customizeSpot.y))) {
customizeSpot = { x: Math.floor(Number(m.customizeSpot.x)), y: Math.floor(Number(m.customizeSpot.y)) };
} else {
customizeSpot = null;
}
stackReleaseArea = m.stackReleaseArea && m.stackReleaseArea.length ? m.stackReleaseArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
stackLandArea = m.stackLandArea && m.stackLandArea.length ? m.stackLandArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
cellColors = (m.cellColors && m.cellColors.length === height) ? m.cellColors.map(r => r && r.slice ? r.slice() : Array(width).fill(null)) : Array(height).fill(0).map(() => Array(width).fill(null));
File diff suppressed because it is too large Load Diff
+418 -4
View File
@@ -279,6 +279,376 @@
if (img) return img;
return defaultAvatarImg;
}
/* ===== Phase 3a: tint ตัวละคร (composite layer + ทาสี) — ตอนนี้ใช้กับตัวผู้เล่นเอง ===== */
var RL_CUSTOMIZE_ASSET = SERVER + '/img/03-5-Customize/';
var RL_LAYER_NAMES = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face'];
var myTintTheme = null;
var myTintSkin = null;
var rlSwatchCache = {};
var tintedCharCache = {};
var rlLayerMissing = {};
function rlLoadImg(src, cb) {
if (rlLayerMissing[src]) { cb(null); return; } // เคย 404 แล้ว ไม่ขอซ้ำ (กัน console spam)
var img = new Image();
img.onload = function () { cb(img); };
img.onerror = function () { rlLayerMissing[src] = true; cb(null); };
img.src = src;
}
function rlSampleSwatch(group, idx, cb) {
var key = group + '-' + idx;
if (rlSwatchCache[key]) { cb(rlSwatchCache[key]); return; }
rlLoadImg(RL_CUSTOMIZE_ASSET + (group === 'color' ? 'color-' : 'skin-tone-') + idx + '.png', function (img) {
if (!img || !img.naturalWidth) { cb(null); return; }
try {
var c = document.createElement('canvas'); c.width = 1; c.height = 1;
var x = c.getContext('2d');
x.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, 1, 1);
var d = x.getImageData(0, 0, 1, 1).data;
var rgb = 'rgb(' + d[0] + ',' + d[1] + ',' + d[2] + ')';
rlSwatchCache[key] = rgb;
cb(rgb);
} catch (e) { cb(null); }
});
}
function rlResolveMyColors(cb) {
var c = '', s = '';
try { c = localStorage.getItem('lobbyThemeColor') || ''; s = localStorage.getItem('lobbySkinTone') || ''; } catch (e) {}
var need = 0, done = false;
function go() { if (done) return; if (need <= 0) { done = true; if (cb) cb(); } }
if (c) { need++; rlSampleSwatch('color', c, function (rgb) { myTintTheme = rgb; if (mapData && canvas) drawLobbyMap(); need--; go(); }); }
if (s) { need++; rlSampleSwatch('skin', s, function (rgb) { myTintSkin = rgb; if (mapData && canvas) drawLobbyMap(); need--; go(); }); }
if (need === 0 && cb) cb();
}
/* ===== Preload + Loading overlay ก่อนเข้า room-lobby (กันสีตัวละครกระตุก) ===== */
var roomJoinReady = false, roomPreloadReady = false, roomLoadingHidden = false;
function injectRoomLoadingOverlay() {
if (document.getElementById('room-loading-overlay')) return;
var style = document.createElement('style');
style.textContent = '#room-loading-overlay{position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:18px;background:radial-gradient(circle at 50% 38%, #0b1430, #060818);color:#cfe9ff;font-family:Kanit,Sarabun,system-ui,sans-serif;transition:opacity .45s ease}#room-loading-overlay.is-hidden{opacity:0;pointer-events:none}#room-loading-spinner{width:56px;height:56px;border:5px solid rgba(34,211,238,.22);border-top-color:#22d3ee;border-radius:50%;animation:rlspin 1s linear infinite}@keyframes rlspin{to{transform:rotate(360deg)}}#room-loading-overlay .rl-txt{font-size:1.1rem;letter-spacing:.04em;opacity:.9}';
document.head.appendChild(style);
var ov = document.createElement('div');
ov.id = 'room-loading-overlay';
ov.innerHTML = '<div id="room-loading-spinner"></div><div class="rl-txt">กำลังโหลดตัวละคร…</div>';
(document.body || document.documentElement).appendChild(ov);
}
function hideRoomLoading() {
if (roomLoadingHidden) return;
roomLoadingHidden = true;
var ov = document.getElementById('room-loading-overlay');
if (ov) { ov.classList.add('is-hidden'); setTimeout(function () { if (ov.parentNode) ov.parentNode.removeChild(ov); }, 600); }
}
function maybeHideRoomLoading() { if (roomJoinReady && roomPreloadReady) hideRoomLoading(); }
function preloadMyTintedCharacter(cb) {
var id = getStoredCharacterId();
if (!id || (!myTintTheme && !myTintSkin)) { if (cb) cb(); return; }
var dirs = ['down', 'up', 'left', 'right'];
var suffixes = ['idle', '0', '1', '2', '3'];
var total = dirs.length * suffixes.length, doneCount = 0, finished = false;
function one() { doneCount++; if (!finished && doneCount >= total) { finished = true; if (cb) cb(); } }
setTimeout(function () { if (!finished) { finished = true; if (cb) cb(); } }, 4500);
dirs.forEach(function (dir) {
var ckey = id + '|' + (myTintTheme || '') + '|' + (myTintSkin || '') + '|' + dir;
var anim = tintedCharCache[ckey] || (tintedCharCache[ckey] = { frames: [], fallback: null });
suffixes.forEach(function (suf) {
rlBuildTintedFrame(id, dir, suf, myTintTheme, myTintSkin, function (img) {
if (img) { if (suf === 'idle') anim.fallback = img; else anim.frames[parseInt(suf, 10)] = img; }
one();
});
});
});
}
/* ===== ห้องแต่งตัว (Customize) ในห้อง room-lobby — ปุ่มล่างซ้าย + popup + tint สด ===== */
var ROOM_CZ_ASSET = SERVER + '/img/03-5-Customize/';
var ROOM_CZ_FACE = [
{ idx: 1, price: 0 }, { idx: 2, price: 0 }, { idx: 3, price: 50 }, { idx: 4, price: 50 },
{ idx: 5, price: 50 }, { idx: 6, price: 100 }, { idx: 7, price: 100 }, { idx: 8, price: 200 }
];
/** จุด "ห้องแต่งตัว" ในฉาก — ตั้งจาก mapData.customizeSpot (วางใน editor) ถ้าไม่มี = null = ไม่แสดง */
var ROOM_CZ_SPOT = null;
var roomCzIcon = new Image();
roomCzIcon.src = '/Main-Lobby/IMAGE/btn-cloth.png';
roomCzIcon.onload = function () { if (typeof mapData !== 'undefined' && mapData && canvas) drawLobbyMap(); };
function markRoomCzInteractiveCell() {
if (!mapData) return;
if (mapData.customizeSpot && Number.isFinite(Number(mapData.customizeSpot.x)) && Number.isFinite(Number(mapData.customizeSpot.y))) {
ROOM_CZ_SPOT = { x: Math.floor(Number(mapData.customizeSpot.x)), y: Math.floor(Number(mapData.customizeSpot.y)) };
} else {
ROOM_CZ_SPOT = null; // ไม่มีจุดแต่งตัวใน map นี้ → ไม่แสดง icon / ไม่มี interact
return;
}
var w = mapData.width || 20, h = mapData.height || 15;
if (ROOM_CZ_SPOT.x < 0 || ROOM_CZ_SPOT.x >= w || ROOM_CZ_SPOT.y < 0 || ROOM_CZ_SPOT.y >= h) { ROOM_CZ_SPOT = null; return; }
if (!Array.isArray(mapData.interactive)) mapData.interactive = [];
for (var y = 0; y < h; y++) { if (!Array.isArray(mapData.interactive[y])) mapData.interactive[y] = []; }
mapData.interactive[ROOM_CZ_SPOT.y][ROOM_CZ_SPOT.x] = 1;
}
function isRoomCzInteractTarget(t) {
return !!(t && ROOM_CZ_SPOT && t.x === ROOM_CZ_SPOT.x && t.y === ROOM_CZ_SPOT.y);
}
function isNearRoomCzSpot(me) {
if (!me || !ROOM_CZ_SPOT || typeof me.x !== 'number' || typeof me.y !== 'number') return false;
var dx = me.x - ROOM_CZ_SPOT.x, dy = me.y - ROOM_CZ_SPOT.y;
return (dx * dx + dy * dy) <= 2.25; // ภายในรัศมี ~1.5 ช่อง (ยืนใกล้ตู้ก็กด F ได้)
}
function roomCzInjectStyle() {
if (document.getElementById('room-cz-style')) return;
var st = document.createElement('style');
st.id = 'room-cz-style';
st.textContent = [
'.room-cz-open{position:fixed;left:clamp(10px,1.4vw,22px);bottom:clamp(96px,15vh,150px);z-index:60;width:clamp(54px,6vw,76px);padding:0;border:none;background:none;cursor:pointer}',
'.room-cz-open img{display:block;width:100%;height:auto;filter:drop-shadow(0 0 8px rgba(34,211,238,.5))}',
'.room-cz-overlay{position:fixed;inset:0;z-index:140;display:flex;align-items:center;justify-content:center;padding:16px;font-family:Kanit,Sarabun,system-ui,sans-serif}',
'.room-cz-overlay.hidden{display:none!important}',
'.room-cz-backdrop{position:absolute;inset:0;background:rgba(4,6,18,.78);backdrop-filter:blur(4px)}',
'.room-cz-dialog{position:relative;width:min(92vw,720px);max-height:90vh;overflow:hidden auto;display:flex;flex-direction:column;gap:clamp(.5rem,1.6vh,1rem);padding:clamp(1rem,3vw,1.6rem) clamp(1rem,3vw,1.8rem) clamp(1.2rem,3vw,1.8rem);border-radius:18px;background:linear-gradient(180deg,rgba(14,20,44,.97),rgba(9,13,30,.97));border:2px solid rgba(34,211,238,.7);box-shadow:0 0 22px rgba(34,211,238,.45),0 0 48px rgba(236,72,153,.28),inset 0 0 24px rgba(34,211,238,.1);color:#e8faff}',
'.room-cz-titlebar{display:flex;align-items:center;justify-content:center;position:relative}',
'.room-cz-titlebar h2{margin:0;font-size:clamp(1.1rem,3vw,1.6rem);font-weight:700;text-shadow:0 0 12px rgba(34,211,238,.6)}',
'.room-cz-close{position:absolute;right:0;top:0;width:38px;height:38px;border:2px solid rgba(236,72,153,.6);border-radius:10px;background:rgba(20,16,40,.6);color:#ff8fd0;font-size:1.4rem;line-height:1;cursor:pointer}',
'.room-cz-row{display:flex;align-items:center;gap:clamp(.5rem,2vw,1rem);flex-wrap:wrap}',
'.room-cz-rowlabel{height:clamp(20px,3.4vh,30px);width:auto;flex:0 0 auto}',
'.room-cz-swatches{display:flex;flex-wrap:wrap;gap:clamp(6px,1vw,10px)}',
'.room-cz-swatch{padding:0;border:2px solid transparent;border-radius:8px;background:none;cursor:pointer;line-height:0}',
'.room-cz-swatch img{display:block;width:clamp(28px,4vw,40px);height:auto;border-radius:6px}',
'.room-cz-swatch.sel{border-color:#22d3ee;box-shadow:0 0 10px rgba(34,211,238,.7)}',
'.room-cz-tabs{position:relative;width:100%;max-width:560px;margin:0 auto;aspect-ratio:776/118}',
'.room-cz-tabbar{display:block;width:100%;height:100%;object-fit:contain;pointer-events:none}',
'.room-cz-tabhit{position:absolute;top:0;height:100%;width:33.34%;padding:0;border:none;background:transparent;cursor:pointer}',
'.room-cz-tabhit[data-tab=face]{left:0}.room-cz-tabhit[data-tab=hair]{left:33.33%}.room-cz-tabhit[data-tab=cloth]{left:66.66%}',
'.room-cz-items{flex:1;min-height:clamp(140px,30vh,280px);max-height:42vh;overflow-y:auto;display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(8px,1.5vw,14px);padding:clamp(8px,1.5vw,14px);border-radius:12px;background:rgba(8,12,28,.6);border:1px solid rgba(34,211,238,.25)}',
'.room-cz-item{position:relative;padding:6px;border:2px solid rgba(34,211,238,.3);border-radius:12px;background:rgba(18,26,52,.7);cursor:pointer;aspect-ratio:1;display:flex;align-items:center;justify-content:center}',
'.room-cz-item.sel{border-color:#22d3ee;box-shadow:0 0 10px rgba(34,211,238,.6)}',
'.room-cz-item img{width:78%;height:auto;object-fit:contain}',
'.room-cz-item .pr{position:absolute;bottom:4px;left:50%;transform:translateX(-50%);font-size:.7rem;font-weight:700;color:#ffe066;background:rgba(0,0,0,.45);border-radius:8px;padding:1px 8px}',
'.room-cz-empty{grid-column:1/-1;align-self:center;text-align:center;color:rgba(255,255,255,.6);padding:2rem 1rem}',
'.room-cz-confirm{align-self:center;padding:0;border:none;background:none;cursor:pointer}',
'.room-cz-confirm img{display:block;width:min(60vw,240px);height:auto}',
'@media(max-width:560px){.room-cz-items{grid-template-columns:repeat(3,1fr)}}'
].join('');
document.head.appendChild(st);
}
function roomCzBuildDom() {
if (document.getElementById('room-cz-overlay')) return;
var ov = document.createElement('div');
ov.id = 'room-cz-overlay'; ov.className = 'room-cz-overlay hidden';
ov.setAttribute('role', 'dialog'); ov.setAttribute('aria-modal', 'true'); ov.setAttribute('aria-label', 'ห้องแต่งตัว');
ov.innerHTML =
'<div class="room-cz-backdrop" id="room-cz-backdrop"></div>' +
'<div class="room-cz-dialog">' +
'<div class="room-cz-titlebar"><h2>ห้องแต่งตัว</h2><button type="button" class="room-cz-close" id="room-cz-close" aria-label="ปิด">&times;</button></div>' +
'<div class="room-cz-row"><img class="room-cz-rowlabel" src="' + ROOM_CZ_ASSET + 'theme-color-txt.png" alt="ธีมสี" decoding="async"><div class="room-cz-swatches" id="room-cz-colors"></div></div>' +
'<div class="room-cz-row"><img class="room-cz-rowlabel" src="' + ROOM_CZ_ASSET + 'theme-skin-txt.png" alt="สีผิว" decoding="async"><div class="room-cz-swatches" id="room-cz-skins"></div></div>' +
'<div class="room-cz-tabs"><img id="room-cz-tabbar" class="room-cz-tabbar" src="' + ROOM_CZ_ASSET + 'tab-1-face.png" alt="" decoding="async"><button type="button" class="room-cz-tabhit" data-tab="face" aria-label="ใบหน้า"></button><button type="button" class="room-cz-tabhit" data-tab="hair" aria-label="ทรงผม"></button><button type="button" class="room-cz-tabhit" data-tab="cloth" aria-label="ชุด"></button></div>' +
'<div class="room-cz-items" id="room-cz-items"></div>' +
'<button type="button" class="room-cz-confirm" id="room-cz-confirm" aria-label="ยืนยัน"><img src="' + ROOM_CZ_ASSET + 'btn-cf.png" alt="ยืนยัน" decoding="async"></button>' +
'</div>';
document.body.appendChild(ov);
}
function roomCzMarkSwatch(group, idx) {
var wrap = document.getElementById(group === 'color' ? 'room-cz-colors' : 'room-cz-skins');
if (wrap) [].forEach.call(wrap.children, function (c) { c.classList.toggle('sel', c.getAttribute('data-idx') === String(idx)); });
}
function roomCzSelectSwatch(group, idx) {
roomCzMarkSwatch(group, idx);
try { localStorage.setItem(group === 'color' ? 'lobbyThemeColor' : 'lobbySkinTone', String(idx)); } catch (e) {}
rlResolveMyColors(function () { updateRoomProfileAvatarTinted(); if (mapData && canvas) drawLobbyMap(); });
}
function roomCzMakeSwatch(group, idx) {
var b = document.createElement('button');
b.type = 'button'; b.className = 'room-cz-swatch'; b.setAttribute('data-idx', String(idx));
var im = document.createElement('img');
im.src = ROOM_CZ_ASSET + (group === 'color' ? 'color-' : 'skin-tone-') + idx + '.png'; im.alt = '';
b.appendChild(im);
b.addEventListener('click', function () { roomCzSelectSwatch(group, idx); });
return b;
}
function roomCzRenderItems(tab) {
var grid = document.getElementById('room-cz-items'); if (!grid) return;
grid.innerHTML = '';
if (tab !== 'face') { var e = document.createElement('div'); e.className = 'room-cz-empty'; e.textContent = 'ยังไม่เปิดให้บริการ'; grid.appendChild(e); return; }
var saved = ''; try { saved = localStorage.getItem('lobbyItem_face') || ''; } catch (e2) {}
ROOM_CZ_FACE.forEach(function (it) {
var cell = document.createElement('button'); cell.type = 'button';
cell.className = 'room-cz-item' + (String(it.idx) === saved ? ' sel' : ''); cell.setAttribute('data-idx', String(it.idx));
var im = document.createElement('img'); im.src = ROOM_CZ_ASSET + 'face-' + it.idx + '.png'; im.alt = ''; cell.appendChild(im);
if (it.price > 0) { var p = document.createElement('span'); p.className = 'pr'; p.textContent = String(it.price); cell.appendChild(p); }
cell.addEventListener('click', function () {
[].forEach.call(grid.children, function (c) { c.classList.remove('sel'); });
cell.classList.add('sel');
try { localStorage.setItem('lobbyItem_face', String(it.idx)); } catch (e3) {}
});
grid.appendChild(cell);
});
}
function roomCzSetTab(tab) {
var bar = document.getElementById('room-cz-tabbar');
if (bar) { var m = { face: 'tab-1-face.png', hair: 'tab-2-hair.png', cloth: 'tab-3-cloth.png' }; bar.src = ROOM_CZ_ASSET + (m[tab] || m.face); }
roomCzRenderItems(tab);
}
function openRoomCustomize() {
var ov = document.getElementById('room-cz-overlay'); if (!ov) return;
var cw = document.getElementById('room-cz-colors'), sw = document.getElementById('room-cz-skins');
if (cw && !cw.childElementCount) { for (var i = 1; i <= 8; i++) cw.appendChild(roomCzMakeSwatch('color', i)); }
if (sw && !sw.childElementCount) { for (var j = 1; j <= 3; j++) sw.appendChild(roomCzMakeSwatch('skin', j)); }
try {
var c = localStorage.getItem('lobbyThemeColor'); if (c) roomCzMarkSwatch('color', c);
var s = localStorage.getItem('lobbySkinTone'); if (s) roomCzMarkSwatch('skin', s);
} catch (e) {}
roomCzSetTab('face');
ov.classList.remove('hidden');
}
function closeRoomCustomize() { var ov = document.getElementById('room-cz-overlay'); if (ov) ov.classList.add('hidden'); }
function setupRoomCustomize() {
roomCzInjectStyle();
roomCzBuildDom();
var ob = document.getElementById('room-cz-open');
if (ob) ob.addEventListener('click', openRoomCustomize);
var cb = document.getElementById('room-cz-close');
if (cb) cb.addEventListener('click', closeRoomCustomize);
var bd = document.getElementById('room-cz-backdrop');
if (bd) bd.addEventListener('click', closeRoomCustomize);
var cf = document.getElementById('room-cz-confirm');
if (cf) cf.addEventListener('click', closeRoomCustomize);
[].forEach.call(document.querySelectorAll('.room-cz-tabhit'), function (t) {
t.addEventListener('click', function () { roomCzSetTab(t.getAttribute('data-tab')); });
});
}
function rlTintMask(img, color) {
var c = document.createElement('canvas');
c.width = img.naturalWidth; c.height = img.naturalHeight;
var x = c.getContext('2d');
x.drawImage(img, 0, 0);
x.globalCompositeOperation = 'source-in';
x.fillStyle = color;
x.fillRect(0, 0, c.width, c.height);
return c;
}
var rlCharManifest = null;
var rlManifestId = null;
function rlEnsureManifest(cb) {
var id = getStoredCharacterId();
if (!id) { if (cb) cb(null); return; }
if (rlManifestId === id && rlCharManifest) { if (cb) cb(rlCharManifest); return; }
fetch(SERVER + '/api/characters', { cache: 'no-store' })
.then(function (r) { return r.json(); })
.then(function (list) {
var c = Array.isArray(list) ? list.filter(function (x) { return x && x.id === id; })[0] : null;
if (c && c.layerManifest) { rlCharManifest = c.layerManifest; rlManifestId = id; if (cb) cb(rlCharManifest); }
else if (cb) cb(null);
})
.catch(function () { if (cb) cb(null); });
}
function rlFrameLayerMap(id, dir, frameSuffix) {
if (!rlCharManifest || rlManifestId !== id) return null;
var entry = (frameSuffix === 'idle')
? (rlCharManifest.byDirIdle && rlCharManifest.byDirIdle[dir])
: (rlCharManifest.byDir && rlCharManifest.byDir[dir]);
if (!entry || !entry.frames || !entry.frames.length) return null;
if (frameSuffix === 'idle') return entry.frames[0] || null;
var fi = parseInt(frameSuffix, 10);
return entry.frames[fi] || entry.frames[0] || null;
}
function rlBuildTintedFrame(id, dir, frameSuffix, theme, skin, cb) {
var map = rlFrameLayerMap(id, dir, frameSuffix);
if (!map) { cb(null); return; }
var names = RL_LAYER_NAMES.filter(function (n) { return map[n]; });
if (!names.length) { cb(null); return; }
var loaded = {}, pending = names.length;
names.forEach(function (name) {
rlLoadImg(SERVER + '/img/characters/' + map[name], function (img) {
loaded[name] = img;
if (--pending === 0) done();
});
});
function done() {
var ref = null, i;
for (i = 0; i < RL_LAYER_NAMES.length; i++) { if (loaded[RL_LAYER_NAMES[i]]) { ref = loaded[RL_LAYER_NAMES[i]]; break; } }
if (!ref) { cb(null); return; }
var c = document.createElement('canvas'); c.width = ref.naturalWidth; c.height = ref.naturalHeight;
var x = c.getContext('2d');
RL_LAYER_NAMES.forEach(function (name) {
var img = loaded[name];
if (!img || !img.naturalWidth) return;
if (theme && (name === 'bodyColor' || name === 'hairColor')) x.drawImage(rlTintMask(img, theme), 0, 0);
else if (skin && name === 'headColor') x.drawImage(rlTintMask(img, skin), 0, 0);
else x.drawImage(img, 0, 0);
});
var out = new Image();
try { out.src = c.toDataURL('image/png'); } catch (e) { cb(null); return; }
cb(out);
}
}
function getTintedFrame(id, theme, skin, dir, now, isWalking) {
var ckey = id + '|' + (theme || '') + '|' + (skin || '') + '|' + dir;
var anim = tintedCharCache[ckey];
if (!anim) {
anim = { frames: [], fallback: null };
tintedCharCache[ckey] = anim;
rlBuildTintedFrame(id, dir, 'idle', theme, skin, function (img) { if (img) anim.fallback = img; if (mapData && canvas) drawLobbyMap(); });
for (var i = 0; i < CHARACTER_ANIM_FRAMES; i++) {
(function (idx) {
rlBuildTintedFrame(id, dir, String(idx), theme, skin, function (img) { if (img) anim.frames[idx] = img; if (mapData && canvas) drawLobbyMap(); });
})(i);
}
}
var phase = walkAnimPhaseIndex(now, isWalking);
for (var k = Math.min(phase, CHARACTER_ANIM_FRAMES - 1); k >= 0; k--) {
var f = anim.frames[k];
if (f && f.complete && f.naturalWidth) return f;
}
if (anim.fallback && anim.fallback.complete && anim.fallback.naturalWidth) return anim.fallback;
return null;
}
function getAvatarImgColored(characterId, theme, skin, direction, now, isWalking) {
if (characterId && (theme || skin)) {
var t = getTintedFrame(characterId, theme || null, skin || null, direction || 'down', now, isWalking);
if (t) return t;
}
return getAvatarImg(characterId, direction, now, isWalking);
}
function updateRoomProfileAvatarTinted() {
var id = getStoredCharacterId();
if (!id || (!myTintTheme && !myTintSkin)) return;
rlBuildTintedFrame(id, 'down', 'idle', myTintTheme, myTintSkin, function (img) {
if (!img) return;
var av = document.getElementById('room-lobby-profile-avatar');
if (av) av.src = img.src;
});
}
injectRoomLoadingOverlay();
setTimeout(hideRoomLoading, 8000); // safety: ไม่บัง overlay เกิน 8 วิ
const keys = {};
const MOVE_SPEED = 0.15;
let lastMoveSend = 0;
@@ -577,6 +947,22 @@
const ay = ((Math.floor(h / 5) % 5) - 2) * 0.1;
return { ax, ay };
}
// ไอคอน "ห้องแต่งตัว" ในฉาก (จุด interact — เดินไปกด F)
if (ROOM_CZ_SPOT && roomCzIcon && roomCzIcon.complete && roomCzIcon.naturalWidth) {
const _czx = ROOM_CZ_SPOT.x * tileSize;
const _czy = ROOM_CZ_SPOT.y * tileSize;
const _czS = tileSize * 1.5;
ctx.save();
ctx.globalAlpha = 0.5;
ctx.fillStyle = 'rgba(34,211,238,0.35)';
ctx.beginPath();
ctx.ellipse(_czx + tileSize / 2, _czy + tileSize - 3, tileSize * 0.55, tileSize * 0.22, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.drawImage(roomCzIcon, _czx + tileSize / 2 - _czS / 2, _czy + tileSize - _czS, _czS, _czS);
}
const peerList = [...peers.entries(), ...lobbyBots.entries()].sort(function (a, b) {
const pa = a[1], pb = b[1];
const ya = pa.y != null ? pa.y : 1, yb = pb.y != null ? pb.y : 1;
@@ -606,7 +992,9 @@
ctx.globalAlpha = 0.45;
ctx.filter = 'grayscale(1) brightness(1.25)';
}
const charImg = getAvatarImg(p.characterId, dir, timeMs, isWalking);
const peerTheme = (id === socket.id) ? myTintTheme : (p.colorTheme || null);
const peerSkin = (id === socket.id) ? myTintSkin : (p.colorSkin || null);
const charImg = getAvatarImgColored(p.characterId, peerTheme, peerSkin, dir, timeMs, isWalking);
const iw = charImg && charImg.complete && charImg.naturalWidth ? charImg.naturalWidth : 0;
const ih = charImg && charImg.complete && charImg.naturalWidth ? charImg.naturalHeight : 0;
const imgScale = (iw && ih) ? Math.min(boxSize / iw, boxSize / ih, 1) : 1;
@@ -1400,7 +1788,7 @@
const sp = pickLobbyBotSpawn(peers.size + i);
const wanderDirs = [[0, -1], [0, 1], [-1, 0], [1, 0]];
const wd = wanderDirs[Math.floor(Math.random() * wanderDirs.length)];
lobbyBots.set(id, {
const bot = {
x: sp.x,
y: sp.y,
direction: 'down',
@@ -1411,7 +1799,13 @@
botWanderDx: wd[0],
botWanderDy: wd[1],
botWanderNextTurn: Date.now() + 400 + Math.floor(Math.random() * 900),
});
};
// สุ่มสีให้บอท (tint ผ่าน renderer Phase 3a)
var botColorIdx = 1 + Math.floor(Math.random() * 8);
var botSkinIdx = 1 + Math.floor(Math.random() * 3);
rlSampleSwatch('color', botColorIdx, function (rgb) { if (rgb) { bot.colorTheme = rgb; if (mapData && canvas) drawLobbyMap(); } });
rlSampleSwatch('skin', botSkinIdx, function (rgb) { if (rgb) { bot.colorSkin = rgb; if (mapData && canvas) drawLobbyMap(); } });
lobbyBots.set(id, bot);
}
while (lobbyBots.size > lobbyBotSlotCount) {
const keys = [...lobbyBots.keys()];
@@ -1444,6 +1838,7 @@
function getStoredCharacterId() {
try {
const v = (localStorage.getItem('gameCharacterId') || '').trim();
if (v === 'Chatest') return ''; // legacy placeholder — ไม่มี sprite จริง
return v;
} catch (e) {
return '';
@@ -1459,6 +1854,7 @@
img.alt = (profileDisplayName || nick || 'ผู้เล่น');
return;
}
if (myTintTheme || myTintSkin) { updateRoomProfileAvatarTinted(); return; } // ใช้รูป tint สี กันถูกเขียนทับเป็นรูป baked
try {
const du = localStorage.getItem(LOBBY_IDLE_DOWN_LS + id);
if (du && typeof du === 'string' && du.indexOf('data:image/') === 0) {
@@ -1907,6 +2303,10 @@
return;
}
mapData = res.mapData;
roomJoinReady = true;
maybeHideRoomLoading();
markRoomCzInteractiveCell();
if (ROOM_CZ_SPOT) appendLobbySystemChat('— เดินไปช่องเขียว แล้วกด F เพื่อเปิดห้องแต่งตัว');
clientLobbyMapId = res.mapId != null ? res.mapId : null;
hostId = res.hostId || null;
spaceName = (res.spaceName || '').trim() || spaceId;
@@ -3760,6 +4160,13 @@
if (!mapData || !me) return;
e.preventDefault();
/* ทุก F ในโถง — ต้องกดพร้อมก่อน (รวม Host เลือกระดับ/คดี และช่องเขียว) */
const target = getLobbyInteractTarget(me);
if (isRoomCzInteractTarget(target) || isNearRoomCzSpot(me)) {
e.preventDefault();
openRoomCustomize();
return;
}
/* F อื่นในโถง (Host เลือกระดับ/คดี และช่องเขียว) — ต้องกดพร้อมก่อน */
if (!me.ready) {
appendLobbySystemChat('— กดพร้อมก่อน แล้วค่อยกด F');
return;
@@ -3768,7 +4175,6 @@
openLobbyPreplayWizard();
return;
}
const target = getLobbyInteractTarget(me);
if (!target) {
if (isCurrentRoomLobbyA() && hostId !== socket.id) {
appendLobbySystemChat('— เฉพาะ Host กด F เพื่อเลือกระดับและคดี');
@@ -3871,9 +4277,17 @@
resizeAndDraw();
setTimeout(resizeAndDraw, 100);
updateLobbyProfileAvatar();
setupRoomCustomize();
rlEnsureManifest(function () {
rlResolveMyColors(function () {
updateRoomProfileAvatarTinted();
preloadMyTintedCharacter(function () { roomPreloadReady = true; maybeHideRoomLoading(); });
});
});
window.addEventListener('pageshow', function () {
syncLobbyAvatarFromStorage();
rlResolveMyColors();
});
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') syncLobbyAvatarFromStorage();
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
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
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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -24,7 +24,7 @@
/** Jump Survival ฉาก Jumper — UI สรุปภารกิจแบบ crown + รูปใน /img/Jumper/ (editor ?map=mnptfts2) */
const JUMP_SURVIVE_MISSION_MAP_ID = 'mnptfts2';
/** จนกว่าโหลด /api/characters — placeholder ชั่วคราวก่อน join */
const LEGACY_PLACEHOLDER_CHARACTER_ID = 'Chatest';
const LEGACY_PLACEHOLDER_CHARACTER_ID = '';
let firstCharacterDefaultResolved = null;
const firstCharacterDefaultPromise = fetch(BASE + '/api/characters')
.then((r) => r.json())
@@ -715,7 +715,8 @@
}
function getStoredCharacterId() {
try {
const v = localStorage.getItem('gameCharacterId');
let v = localStorage.getItem('gameCharacterId');
if (v === 'Chatest') v = ''; // legacy placeholder — ไม่มี sprite จริง
if (v) return v;
} catch (e) { /* ignore */ }
if (firstCharacterDefaultResolved === null) return LEGACY_PLACEHOLDER_CHARACTER_ID;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -3302,7 +3302,12 @@ const server = http.createServer((req, res) => {
const types = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css' };
fs.readFile(filePath, (err, data) => {
if (err) return res.writeHead(404), res.end('Not Found');
res.writeHead(200, { 'Content-Type': types[ext] || 'application/octet-stream' });
res.writeHead(200, {
'Content-Type': types[ext] || 'application/octet-stream',
'Cache-Control': 'no-store, no-cache, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
});
res.end(data);
});
});
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -78,6 +78,6 @@
</div>
</template>
<script src="join.js?v=0.002"></script>
<script src="join.js?v=0.003"></script>
</body>
</html>
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>เข้าร่วมเกม — JD JUSTICE DIVERS</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="join-bg" role="img" aria-label="พื้นหลัง">
<img src="/Main-Menu/bg.png" alt="" class="join-bg-img" role="presentation">
</div>
<div class="join-char-layer" aria-hidden="true" role="img">
<img src="/Main-Menu/char-main.png" alt="" class="join-char-img" role="presentation">
</div>
<div class="join-vignette" aria-hidden="true"></div>
<div class="join-ui">
<div class="join-back-section">
<button type="button" class="join-btn-back" id="btn-back" aria-label="ย้อนกลับ">
<img src="/Main-Menu/IMAGE/btn-back.png" alt="" class="join-btn-back-img" role="presentation">
</button>
</div>
<div class="join-content">
<header class="join-header">
<img src="IMAGE/txt-joinroom.png" alt="เข้าร่วมเกม" class="join-title-img" role="presentation">
</header>
<div class="join-panel">
<div class="join-tabs" role="tablist" aria-label="เลือกประเภทห้อง">
<button type="button" class="join-tab" id="tab-public" role="tab" aria-selected="true" aria-controls="join-public-view">
<img src="IMAGE/btn-publish-room-h.png" alt="ห้องสาธารณะ" class="join-tab-img" id="tab-img-public" role="presentation">
</button>
<button type="button" class="join-tab" id="tab-private" role="tab" aria-selected="false" aria-controls="join-private-view">
<img src="IMAGE/btn-private-room.png" alt="ห้องส่วนตัว" class="join-tab-img" id="tab-img-private" role="presentation">
</button>
</div>
<div id="join-public-view" class="join-view" role="tabpanel">
<div class="join-list-scroll">
<div class="join-list" id="room-list-public" aria-label="รายการห้องสาธารณะ"></div>
</div>
</div>
<div id="join-private-view" class="join-view join-view--hidden" role="tabpanel">
<div class="join-private-form">
<div class="join-private-row">
<img src="IMAGE/txt-room-id.png" alt="" class="join-private-label-img" role="presentation">
<div class="join-textfield-wrap">
<input type="text" id="join-private-room-id" class="join-textfield-input" placeholder="" maxlength="32" autocomplete="off" spellcheck="false" aria-label="รหัสห้อง">
</div>
</div>
<div class="join-private-row">
<img src="IMAGE/txt-password_1.png" alt="" class="join-private-label-img join-private-label-img--pass" role="presentation">
<div class="join-textfield-wrap">
<input type="text" id="join-private-password" class="join-textfield-input join-textfield-input--code" placeholder="0000" maxlength="4" inputmode="numeric" autocomplete="off" aria-label="รหัสผ่าน 4 หลัก">
</div>
</div>
<button type="button" class="join-private-submit" id="btn-private-join" aria-label="เข้าร่วมห้องส่วนตัว">
<img src="IMAGE/btn-join-room.png" alt="เข้าร่วม" class="join-private-submit-img" role="presentation">
</button>
</div>
</div>
</div>
</div>
</div>
<template id="tpl-room-row">
<div class="join-room-row">
<div class="join-room-row-inner">
<span class="join-room-name"></span>
<span class="join-room-players"></span>
<button type="button" class="join-room-btn">
<img src="IMAGE/btn-join-room.png" alt="เข้าร่วม" class="join-room-btn-img" role="presentation">
</button>
</div>
</div>
</template>
<script src="join.js?v=0.002"></script>
</body>
</html>
+1 -1
View File
@@ -63,7 +63,7 @@
if (!listPublic || !tplRow) return;
listPublic.innerHTML = '<div class="join-list-status" id="room-list-status">โหลดรายการห้อง...</div>';
var statusEl = document.getElementById('room-list-status');
fetch(BASE + '/api/spaces?_=' + Date.now())
fetch(BASE + '/api/spaces', { cache: 'no-store' })
.then(function (r) { return r.json(); })
.then(function (list) {
if (!Array.isArray(list)) {
@@ -0,0 +1,148 @@
(function () {
'use strict';
function checkLoginStatus() {
const isLoggedIn = localStorage.getItem('isLoggedIn');
if (!isLoggedIn || isLoggedIn !== 'true') {
window.location.href = '/Login/';
return false;
}
return true;
}
if (!checkLoginStatus()) {
return;
}
const BASE = '/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',
};
function getNick() {
return (localStorage.getItem('playerName') || '').trim() || 'ผู้เล่น';
}
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 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 = '/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 refreshPublicRooms() {
if (!listPublic || !tplRow) return;
listPublic.innerHTML = '<div class="join-list-status" id="room-list-status">โหลดรายการห้อง...</div>';
var statusEl = document.getElementById('room-list-status');
fetch(BASE + '/api/spaces?_=' + Date.now())
.then(function (r) { return r.json(); })
.then(function (list) {
if (!Array.isArray(list)) {
if (statusEl) statusEl.textContent = 'โหลดรายการห้องไม่ได้';
return;
}
listPublic.innerHTML = '';
if (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 = escapeHtml(name) + (mapName !== '—' ? ' (ฉาก: ' + escapeHtml(mapName) + ')' : '');
playersEl.textContent = count + '/' + max + ' คน' + (full ? ' (เต็ม)' : '');
img.src = full ? IMG.joinOff : IMG.joinOn;
img.alt = full ? 'เต็ม' : 'เข้าร่วม';
if (full) {
btn.disabled = true;
} else {
btn.addEventListener('click', function () {
goToRoom(room.spaceId);
});
}
listPublic.appendChild(node);
});
})
.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);
});
refreshPublicRooms();
setInterval(refreshPublicRooms, 8000);
setTab('public');
})();
+2
View File
@@ -180,6 +180,7 @@ body {
}
.join-list {
min-height: min(48vh, 420px);
max-height: min(48vh, 420px);
overflow-y: auto;
overflow-x: hidden;
@@ -211,6 +212,7 @@ body {
}
.join-list-status {
margin: auto;
padding: 1rem;
font-size: clamp(0.8rem, 2vw, 0.92rem);
color: rgba(255, 255, 255, 0.85);
@@ -0,0 +1,420 @@
* { box-sizing: border-box; }
html {
min-height: 100%;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
min-height: 100dvh;
overflow-x: hidden;
font-family: 'Kanit', 'Prompt', 'Sarabun', 'Segoe UI', system-ui, sans-serif;
color: #fff;
background: #0a0b1e;
}
.join-bg {
position: fixed;
inset: 0;
z-index: 0;
overflow: hidden;
}
.join-bg-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.join-char-layer {
position: fixed;
inset: 0;
z-index: 1;
pointer-events: none;
display: flex;
justify-content: center;
align-items: center;
}
.join-char-img {
width: 100%;
height: 100%;
max-height: 72vh;
object-fit: contain;
object-position: center;
}
.join-vignette {
position: fixed;
inset: 0;
z-index: 2;
pointer-events: none;
background: radial-gradient(ellipse at center, transparent 0%, transparent 30%, rgba(0, 0, 0, 0.3) 50%, rgba(0, 0, 0, 0.6) 70%, rgba(0, 0, 0, 0.85) 85%, rgba(0, 0, 0, 0.95) 100%);
}
.join-ui {
position: relative;
z-index: 3;
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
}
.join-back-section {
position: fixed;
top: max(12px, env(safe-area-inset-top));
left: max(12px, env(safe-area-inset-left));
z-index: 10;
}
.join-btn-back {
padding: 0;
border: none;
background: transparent;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.join-btn-back-img {
display: block;
width: clamp(40px, 10vw, 60px);
height: auto;
}
.join-content {
width: 100%;
max-width: min(96vw, 720px);
margin: 0 auto;
padding: clamp(44px, 9vh, 76px) clamp(14px, 3.5vw, 22px) clamp(24px, 5vh, 40px);
display: flex;
flex-direction: column;
align-items: center;
gap: clamp(0.85rem, 2.5vw, 1.35rem);
}
.join-header {
text-align: center;
}
.join-title-img {
display: block;
width: min(88vw, 420px);
height: auto;
image-rendering: -webkit-optimize-contrast;
filter: drop-shadow(0 0 16px rgba(191, 0, 255, 0.55));
}
/* กล่องหลักตาม mock — bg-pop */
.join-panel {
width: 100%;
padding: clamp(14px, 3vw, 22px) clamp(12px, 2.5vw, 20px) clamp(16px, 3vw, 24px);
border-radius: 10px;
background-image: url('IMAGE/bg-pop.png');
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
box-shadow:
0 0 20px rgba(0, 255, 255, 0.25),
0 0 40px rgba(191, 0, 255, 0.15);
}
.join-tabs {
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-end;
gap: clamp(0.5rem, 2vw, 1rem);
margin-bottom: clamp(12px, 2.5vw, 18px);
}
.join-tab {
padding: 0;
border: none;
background: transparent;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
flex: 0 1 auto;
}
.join-tab:focus-visible {
outline: 2px solid #00ffff;
outline-offset: 4px;
}
.join-tab-img {
display: block;
width: clamp(140px, 38vw, 220px);
height: auto;
image-rendering: -webkit-optimize-contrast;
}
.join-view--hidden {
display: none !important;
}
/* พื้นที่สกรอล + แถบเลื่อนตาม asset */
.join-list-scroll {
position: relative;
padding-right: clamp(10px, 2.5vw, 16px);
margin-right: -4px;
}
.join-list-scroll::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 14px;
height: 100%;
background: url('IMAGE/join-room-scrollbar-bg.png') center top / 100% 100% no-repeat;
pointer-events: none;
opacity: 0.95;
border-radius: 4px;
}
.join-list {
max-height: min(48vh, 420px);
overflow-y: auto;
overflow-x: hidden;
padding-right: 6px;
display: flex;
flex-direction: column;
/* gap: clamp(0.25rem, 1vw, 0.45rem); */
}
.join-list::-webkit-scrollbar {
width: 10px;
}
.join-list::-webkit-scrollbar-track {
background: rgba(0, 40, 60, 0.4);
border-radius: 4px;
}
.join-list::-webkit-scrollbar-thumb {
background: url('IMAGE/join-room-scrollbar.png') center / contain no-repeat;
background-color: rgba(0, 255, 255, 0.35);
border-radius: 4px;
min-height: 40px;
}
.join-list {
scrollbar-width: thin;
scrollbar-color: rgba(0, 255, 255, 0.5) rgba(0, 40, 60, 0.4);
}
.join-list-status {
padding: 1rem;
font-size: clamp(0.8rem, 2vw, 0.92rem);
color: rgba(255, 255, 255, 0.85);
text-align: center;
}
/* แถวห้อง — bg-room-name */
.join-room-row {
width: 100%;
}
.join-room-row-inner {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: clamp(0.35rem, 1.2vw, 0.75rem);
height: clamp(64px, 14vw, 90px);
padding: clamp(8px, 2vw, 12px) clamp(10px, 2.5vw, 16px);
background-image: url('IMAGE/bg-room-name.png');
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
}
.join-room-name {
flex: 1 1 auto;
min-width: 0;
font-size: clamp(0.72rem, 1.9vw, 0.88rem);
font-weight: 700;
color: #fff;
text-shadow: 0 0 8px rgba(0, 255, 255, 0.25);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left: 1em;
line-height: 1.3;
}
.join-room-players {
flex: 0 0 auto;
font-size: clamp(0.72rem, 1.9vw, 0.88rem);
font-weight: 600;
color: #fff;
white-space: nowrap;
line-height: 1.3;
}
.join-room-btn {
flex: 0 0 auto;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.join-room-btn:disabled {
cursor: not-allowed;
opacity: 0.85;
filter: grayscale(0.35) brightness(0.65);
}
.join-room-btn-img {
display: block;
width: clamp(72px, 20vw, 110px);
height: auto;
image-rendering: -webkit-optimize-contrast;
}
/* แท็บห้องส่วนตัว — ฟอร์ม */
.join-private-form {
display: flex;
flex-direction: column;
gap: clamp(0.75rem, 2vw, 1.1rem);
padding-top: clamp(4px, 1vw, 8px);
}
.join-private-row {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: clamp(0.6rem, 2.2vw, 1rem);
}
.join-private-label-img {
display: block;
height: clamp(28px, 6.5vw, 40px);
width: auto;
object-fit: contain;
image-rendering: -webkit-optimize-contrast;
flex: 0 0 auto;
}
.join-private-label-img--pass {
height: clamp(26px, 6vw, 36px);
}
.join-textfield-wrap {
position: relative;
flex: 1 1 auto;
min-width: 200px;
max-width: min(100%, 75%);
height: clamp(48px, 11vw, 60px);
}
.join-textfield-wrap--narrow {
flex: 0 1 auto;
min-width: 160px;
max-width: 260px;
}
.join-textfield-wrap::before {
content: '';
position: absolute;
inset: 0;
background-image: url('IMAGE/textfield.png');
background-size: cover;
background-repeat: no-repeat;
background-position: center;
pointer-events: none;
z-index: 1;
}
.join-textfield-input,
input.join-textfield-input,
#join-private-room-id.join-textfield-input {
position: relative;
z-index: 2;
width: 100% !important;
height: 100% !important;
padding-top: clamp(0.35rem, 1vw, 0.55rem) !important;
padding-bottom: clamp(0.35rem, 1vw, 0.55rem) !important;
padding-left: clamp(0.85rem, 2.2vw, 1.15rem) !important;
padding-right: clamp(0.85rem, 2.2vw, 1.15rem) !important;
border: none !important;
background: transparent !important;
color: #fff;
font-size: clamp(0.88rem, 2.2vw, 1.02rem);
font-weight: 600;
font-family: inherit;
line-height: 1.5;
outline: none;
box-sizing: border-box;
}
#join-private-room-id.join-textfield-input {
padding-left: 2.5em !important;
}
.join-textfield-input--code,
#join-private-password.join-textfield-input {
padding-left: 0 !important;
padding-right: 0 !important;
}
.join-textfield-input--code {
text-align: center;
font-family: 'Courier New', monospace;
letter-spacing: 0.18em;
font-size: clamp(0.95rem, 2.5vw, 1.15rem);
}
.join-textfield-input::placeholder {
color: rgba(255, 255, 255, 0.35);
}
.join-private-submit {
align-self: center;
padding: 0;
margin-top: 0.25rem;
border: none;
background: transparent;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.join-private-submit:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.join-private-submit-img {
display: block;
width: min(72vw, 280px);
height: auto;
image-rendering: -webkit-optimize-contrast;
}
@media (max-width: 520px) {
.join-room-row-inner {
flex-wrap: wrap;
justify-content: center;
text-align: center;
}
.join-room-name,
.join-room-players {
width: 100%;
text-align: center;
}
.join-room-btn {
margin-top: 0.25rem;
}
}
+31 -2
View File
@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0172">
<link rel="stylesheet" href="style.css?v=0.0175">
</head>
<body class="lobby-page">
<div class="lobby-bg" role="img" aria-label="พื้นหลัง">
@@ -219,9 +219,38 @@
</div>
</div>
<!-- ห้องแต่งตัว (Customize) — Phase 1: UI + เลือกธีมสี/สีผิว/ใบหน้า -->
<div id="lobby-customize-overlay" class="lobby-customize-overlay hidden" role="dialog" aria-modal="true" aria-label="ห้องแต่งตัว" aria-hidden="true">
<div class="lobby-customize-backdrop" id="lobby-customize-backdrop" aria-hidden="true"></div>
<div class="lobby-customize-dialog">
<div class="lobby-customize-titlebar">
<h2 class="lobby-customize-title">ห้องแต่งตัว</h2>
<button type="button" class="lobby-customize-close" id="lobby-customize-close" aria-label="ปิด">&times;</button>
</div>
<div class="lobby-customize-row">
<img src="../Game/img/03-5-Customize/theme-color-txt.png" alt="ธีมสี" class="lobby-customize-row-label" decoding="async">
<div class="lobby-customize-swatches" id="lobby-theme-colors"></div>
</div>
<div class="lobby-customize-row">
<img src="../Game/img/03-5-Customize/theme-skin-txt.png" alt="สีผิว" class="lobby-customize-row-label" decoding="async">
<div class="lobby-customize-swatches" id="lobby-skin-tones"></div>
</div>
<div class="lobby-customize-tabs" role="tablist" aria-label="ประเภทการแต่งตัว">
<img id="lobby-customize-tabbar" class="lobby-customize-tabbar-img" src="../Game/img/03-5-Customize/tab-1-face.png" alt="" decoding="async">
<button type="button" class="lobby-customize-tabhit" data-tab="face" role="tab" aria-selected="true" aria-label="ใบหน้า"></button>
<button type="button" class="lobby-customize-tabhit" data-tab="hair" role="tab" aria-selected="false" aria-label="ทรงผม"></button>
<button type="button" class="lobby-customize-tabhit" data-tab="cloth" role="tab" aria-selected="false" aria-label="ชุด"></button>
</div>
<div class="lobby-customize-items" id="lobby-customize-items"></div>
<button type="button" class="lobby-customize-confirm" id="lobby-customize-confirm" aria-label="ยืนยัน">
<img src="../Game/img/03-5-Customize/btn-cf.png" alt="ยืนยัน" decoding="async">
</button>
</div>
</div>
<div id="lobby-toast" class="lobby-toast" role="status" aria-live="polite"></div>
<script src="../app-base.js?v=1"></script>
<script src="lobby.js?v=0.0164"></script>
<script src="lobby.js?v=0.0170"></script>
</body>
</html>
@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Main Lobby — JD JUSTICE DIVERS</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=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0172">
</head>
<body class="lobby-page">
<div class="lobby-bg" role="img" aria-label="พื้นหลัง">
<img src="IMAGE/BG-Main-Lobby.png" alt="" class="lobby-bg-img" role="presentation" decoding="async">
</div>
<div class="lobby-vignette" aria-hidden="true"></div>
<div class="lobby-ui">
<header class="lobby-header">
<div class="lobby-profile-panel">
<img src="IMAGE/profile-bg.png" alt="" class="lobby-profile-bg" role="presentation" decoding="async">
<div class="lobby-profile-inner">
<div class="lobby-avatar-wrap">
<img src="" alt="" class="lobby-avatar-mask" id="lobby-profile-avatar" width="50" height="50" decoding="async">
</div>
<div class="lobby-profile-info">
<p class="lobby-profile-name" id="lobby-profile-name">MONE</p>
<p class="lobby-profile-meta">AGENT ID : <span id="lobby-profile-agent-id">001998</span></p>
<p class="lobby-profile-coins">
<img src="IMAGE/coin-JD.png" alt="" class="lobby-coin-icon" role="presentation" width="18" height="18" decoding="async">
COINS : <span id="lobby-profile-coins">0</span>
</p>
</div>
</div>
</div>
<div class="lobby-news-wrap">
<img src="IMAGE/news-banner-bg.png" alt="" class="lobby-news-bg" role="presentation" decoding="async">
<img src="IMAGE/news.png" alt="ข่าวสารศูนย์บัญชาการ" class="lobby-news-img" role="presentation" decoding="async">
</div>
<div class="lobby-header-row2-left">
<button type="button" class="lobby-btn-tutorial" id="btn-tutorial" title="วิธีเล่น" aria-label="วิธีเล่น">
<img src="IMAGE/btn-tutorial.png" alt="" class="lobby-btn-tutorial-img" role="presentation" decoding="async">
</button>
</div>
<button type="button" class="lobby-btn-daily" id="btn-daily" aria-label="รางวัลประจำวัน">
<img src="IMAGE/daily-rewards.png" alt="" class="lobby-daily-img" role="presentation" decoding="async">
<span class="lobby-daily-dot" aria-hidden="true"></span>
</button>
</header>
<div class="lobby-character-area">
<div class="lobby-center-left">
<button type="button" class="lobby-btn-cloth" id="btn-cloth" aria-label="ห้องแต่งตัว">
<img src="IMAGE/btn-cloth.png" alt="" class="lobby-btn-cloth-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-character-center" id="lobby-character-center" aria-hidden="true">
<img id="lobby-character-img" src="" alt="" class="lobby-character-img" width="256" height="256" decoding="async">
</div>
</div>
<aside class="lobby-leaderboard" aria-label="กระดานผู้นำ">
<img src="IMAGE/leaderboard-bg.png" alt="" class="lobby-leaderboard-bg" role="presentation" decoding="async">
<div class="lobby-leaderboard-mask">
<div class="lobby-leaderboard-inner">
<div class="lobby-leaderboard-scroll">
<ul class="lobby-leaderboard-list">
<li>
<img src="IMAGE/leaderboard-1.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mixie Kim</span>
<span class="lobby-rank-case">(คดี : มหาเศรษฐีนามปลอม)</span>
</div>
<span class="lobby-rank-score">9999</span>
</li>
<li>
<img src="IMAGE/leaderboard-2.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Golden L.</span>
<span class="lobby-rank-case">(คดี : โจรกรรมไซเบอร์)</span>
</div>
<span class="lobby-rank-score">9951</span>
</li>
<li>
<img src="IMAGE/leaderboard-3.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Lilly 991</span>
<span class="lobby-rank-case">(คดี : ปั่นแอพพลวงเกม)</span>
</div>
<span class="lobby-rank-score">9552</span>
</li>
<li>
<span class="lobby-rank-num">4</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Nova Byte</span>
<span class="lobby-rank-case">(คดี : ฟิชชิงองค์กร)</span>
</div>
<span class="lobby-rank-score">9410</span>
</li>
<li>
<span class="lobby-rank-num">5</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Aster R.</span>
<span class="lobby-rank-case">(คดี : แก๊งค์หลอกลงทุน)</span>
</div>
<span class="lobby-rank-score">9280</span>
</li>
<li>
<span class="lobby-rank-num">6</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Hexa One</span>
<span class="lobby-rank-case">(คดี : เจาะระบบธนาคาร)</span>
</div>
<span class="lobby-rank-score">9145</span>
</li>
<li>
<span class="lobby-rank-num">7</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mint C.</span>
<span class="lobby-rank-case">(คดี : ปลอมเพจขายสินค้า)</span>
</div>
<span class="lobby-rank-score">9032</span>
</li>
<li>
<span class="lobby-rank-num">8</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Q-Logic</span>
<span class="lobby-rank-case">(คดี : หลอกโอนผ่านแชท)</span>
</div>
<span class="lobby-rank-score">8918</span>
</li>
<li>
<span class="lobby-rank-num">9</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Rin 47</span>
<span class="lobby-rank-case">(คดี : ลักข้อมูลส่วนบุคคล)</span>
</div>
<span class="lobby-rank-score">8804</span>
</li>
<li>
<span class="lobby-rank-num">10</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Skyline X</span>
<span class="lobby-rank-case">(คดี : ฟอกเงินคริปโต)</span>
</div>
<span class="lobby-rank-score">8690</span>
</li>
</ul>
<div class="lobby-leaderboard-scroll-thumb" id="lobby-leaderboard-scroll-thumb" aria-hidden="true"></div>
</div>
</div>
</div>
</aside>
<footer class="lobby-footer">
<div class="lobby-footer-left">
<button type="button" class="lobby-btn-ai-chat" id="btn-ai-chat" aria-label="เทพความรู้">
<img src="IMAGE/BTN-AI-ChatBOT.png" alt="" class="lobby-btn-ai-chat-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-center">
<button type="button" class="lobby-btn-start" id="btn-start-mission" aria-label="ไปเมนูหลัก" data-href="../Main-Menu/">
<img src="IMAGE/btn-start-mission.png" alt="" class="lobby-btn-start-img" role="presentation" decoding="async">
</button>
<button type="button" class="lobby-btn-quiz" id="btn-quiz-battle" aria-label="ห้องเกมตอบคำถาม" data-href="../Quiz-Battle/">
<img src="IMAGE/btn-quix%20battle.png" alt="" class="lobby-btn-quiz-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-right">
<button type="button" class="lobby-btn-profile" id="btn-myprofile" aria-label="โปรไฟล์ของฉัน">
<img src="IMAGE/btn-myprofile.png" alt="" class="lobby-btn-profile-img" role="presentation" decoding="async">
</button>
</div>
</footer>
<div id="lobby-guide-dim" class="lobby-guide-dim hidden" aria-hidden="true"></div>
<div id="lobby-guide-focus" class="lobby-guide-focus hidden" aria-hidden="true"></div>
<div class="lobby-guide-bar hidden" id="lobby-guide-bar" role="dialog" aria-modal="true" aria-labelledby="guide-img-label">
<span id="guide-img-label" class="sr-only">คำแนะนำทีละขั้น</span>
<div class="lobby-guide-frame">
<img src="IMAGE/guide/guide-01.png" alt="คำแนะนำ" class="lobby-guide-img" id="guide-img" decoding="async">
<button type="button" class="lobby-guide-next" id="btn-guide-next" aria-label="ถัดไป">
<img src="IMAGE/guide/btn-next.png" alt="" decoding="async">
</button>
</div>
</div>
</div>
<!-- แชท AI — API เดียวกับ room-lobby -->
<div id="lobby-ai-chat-panel" class="lobby-ai-chat-panel lobby-ai-chat-hidden" role="dialog" aria-label="เทพความรู้">
<div class="lobby-ai-chat-header">
<span class="lobby-ai-chat-title">
<img src="../Game/img/ai-chat-header.png" alt="คุยกับ AI" decoding="async">
<img src="../Game/img/ai-chat-title-label.png" alt="เทพความรู้" class="lobby-ai-chat-title-label" decoding="async">
</span>
<button type="button" class="lobby-ai-chat-close" id="lobby-ai-chat-close-btn" title="ปิดแชท AI" aria-label="ปิดแชท AI">
<img src="../Game/img/chat-close-btn.png" alt="ปิด" id="lobby-ai-chat-toggle-img" width="50" height="50" decoding="async">
</button>
</div>
<div id="lobby-ai-chat-messages" class="lobby-ai-chat-messages"></div>
<form id="lobby-ai-chat-form" class="lobby-ai-chat-form">
<input id="lobby-ai-chat-input" type="text" placeholder="Type a here..." autocomplete="off" aria-label="ข้อความถึง AI">
<button type="submit" class="lobby-ai-chat-send" title="ส่ง" aria-label="ส่ง">
<img src="../Game/img/chat-btn-send.png" alt="" decoding="async">
</button>
</form>
<p class="lobby-ai-chat-admin-link"><a href="../Game/ai-admin.html" target="_blank" rel="noopener noreferrer">ตั้งค่า AI (Admin)</a></p>
</div>
<div id="lobby-howto-overlay" class="lobby-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="lobby-howto-title">
<div class="lobby-howto-inner">
<h2 id="lobby-howto-title" class="sr-only">วิธีเล่น</h2>
<img src="IMAGE/Howto/howtoplay.png" alt="วิธีเล่น" decoding="async">
<div class="lobby-howto-close">
<button type="button" class="lobby-icon-btn" id="btn-howto-got-it" aria-label="เข้าใจแล้ว">
<img src="IMAGE/Howto/btn-got-it.png" alt="" decoding="async" onerror="this.style.display='none'; this.parentElement.textContent='ปิด';">
</button>
</div>
</div>
</div>
<div id="lobby-toast" class="lobby-toast" role="status" aria-live="polite"></div>
<script src="../app-base.js?v=1"></script>
<script src="lobby.js?v=0.0164"></script>
</body>
</html>
@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Main Lobby — JD JUSTICE DIVERS</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=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0173">
</head>
<body class="lobby-page">
<div class="lobby-bg" role="img" aria-label="พื้นหลัง">
<img src="IMAGE/BG-Main-Lobby.png" alt="" class="lobby-bg-img" role="presentation" decoding="async">
</div>
<div class="lobby-vignette" aria-hidden="true"></div>
<div class="lobby-ui">
<header class="lobby-header">
<div class="lobby-profile-panel">
<img src="IMAGE/profile-bg.png" alt="" class="lobby-profile-bg" role="presentation" decoding="async">
<div class="lobby-profile-inner">
<div class="lobby-avatar-wrap">
<img src="" alt="" class="lobby-avatar-mask" id="lobby-profile-avatar" width="50" height="50" decoding="async">
</div>
<div class="lobby-profile-info">
<p class="lobby-profile-name" id="lobby-profile-name">MONE</p>
<p class="lobby-profile-meta">AGENT ID : <span id="lobby-profile-agent-id">001998</span></p>
<p class="lobby-profile-coins">
<img src="IMAGE/coin-JD.png" alt="" class="lobby-coin-icon" role="presentation" width="18" height="18" decoding="async">
COINS : <span id="lobby-profile-coins">0</span>
</p>
</div>
</div>
</div>
<div class="lobby-news-wrap">
<img src="IMAGE/news-banner-bg.png" alt="" class="lobby-news-bg" role="presentation" decoding="async">
<img src="IMAGE/news.png" alt="ข่าวสารศูนย์บัญชาการ" class="lobby-news-img" role="presentation" decoding="async">
</div>
<div class="lobby-header-row2-left">
<button type="button" class="lobby-btn-tutorial" id="btn-tutorial" title="วิธีเล่น" aria-label="วิธีเล่น">
<img src="IMAGE/btn-tutorial.png" alt="" class="lobby-btn-tutorial-img" role="presentation" decoding="async">
</button>
</div>
<button type="button" class="lobby-btn-daily" id="btn-daily" aria-label="รางวัลประจำวัน">
<img src="IMAGE/daily-rewards.png" alt="" class="lobby-daily-img" role="presentation" decoding="async">
<span class="lobby-daily-dot" aria-hidden="true"></span>
</button>
</header>
<div class="lobby-character-area">
<div class="lobby-center-left">
<button type="button" class="lobby-btn-cloth" id="btn-cloth" aria-label="ห้องแต่งตัว">
<img src="IMAGE/btn-cloth.png" alt="" class="lobby-btn-cloth-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-character-center" id="lobby-character-center" aria-hidden="true">
<img id="lobby-character-img" src="" alt="" class="lobby-character-img" width="256" height="256" decoding="async">
</div>
</div>
<aside class="lobby-leaderboard" aria-label="กระดานผู้นำ">
<img src="IMAGE/leaderboard-bg.png" alt="" class="lobby-leaderboard-bg" role="presentation" decoding="async">
<div class="lobby-leaderboard-mask">
<div class="lobby-leaderboard-inner">
<div class="lobby-leaderboard-scroll">
<ul class="lobby-leaderboard-list">
<li>
<img src="IMAGE/leaderboard-1.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mixie Kim</span>
<span class="lobby-rank-case">(คดี : มหาเศรษฐีนามปลอม)</span>
</div>
<span class="lobby-rank-score">9999</span>
</li>
<li>
<img src="IMAGE/leaderboard-2.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Golden L.</span>
<span class="lobby-rank-case">(คดี : โจรกรรมไซเบอร์)</span>
</div>
<span class="lobby-rank-score">9951</span>
</li>
<li>
<img src="IMAGE/leaderboard-3.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Lilly 991</span>
<span class="lobby-rank-case">(คดี : ปั่นแอพพลวงเกม)</span>
</div>
<span class="lobby-rank-score">9552</span>
</li>
<li>
<span class="lobby-rank-num">4</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Nova Byte</span>
<span class="lobby-rank-case">(คดี : ฟิชชิงองค์กร)</span>
</div>
<span class="lobby-rank-score">9410</span>
</li>
<li>
<span class="lobby-rank-num">5</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Aster R.</span>
<span class="lobby-rank-case">(คดี : แก๊งค์หลอกลงทุน)</span>
</div>
<span class="lobby-rank-score">9280</span>
</li>
<li>
<span class="lobby-rank-num">6</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Hexa One</span>
<span class="lobby-rank-case">(คดี : เจาะระบบธนาคาร)</span>
</div>
<span class="lobby-rank-score">9145</span>
</li>
<li>
<span class="lobby-rank-num">7</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mint C.</span>
<span class="lobby-rank-case">(คดี : ปลอมเพจขายสินค้า)</span>
</div>
<span class="lobby-rank-score">9032</span>
</li>
<li>
<span class="lobby-rank-num">8</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Q-Logic</span>
<span class="lobby-rank-case">(คดี : หลอกโอนผ่านแชท)</span>
</div>
<span class="lobby-rank-score">8918</span>
</li>
<li>
<span class="lobby-rank-num">9</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Rin 47</span>
<span class="lobby-rank-case">(คดี : ลักข้อมูลส่วนบุคคล)</span>
</div>
<span class="lobby-rank-score">8804</span>
</li>
<li>
<span class="lobby-rank-num">10</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Skyline X</span>
<span class="lobby-rank-case">(คดี : ฟอกเงินคริปโต)</span>
</div>
<span class="lobby-rank-score">8690</span>
</li>
</ul>
<div class="lobby-leaderboard-scroll-thumb" id="lobby-leaderboard-scroll-thumb" aria-hidden="true"></div>
</div>
</div>
</div>
</aside>
<footer class="lobby-footer">
<div class="lobby-footer-left">
<button type="button" class="lobby-btn-ai-chat" id="btn-ai-chat" aria-label="เทพความรู้">
<img src="IMAGE/BTN-AI-ChatBOT.png" alt="" class="lobby-btn-ai-chat-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-center">
<button type="button" class="lobby-btn-start" id="btn-start-mission" aria-label="ไปเมนูหลัก" data-href="../Main-Menu/">
<img src="IMAGE/btn-start-mission.png" alt="" class="lobby-btn-start-img" role="presentation" decoding="async">
</button>
<button type="button" class="lobby-btn-quiz" id="btn-quiz-battle" aria-label="ห้องเกมตอบคำถาม" data-href="../Quiz-Battle/">
<img src="IMAGE/btn-quix%20battle.png" alt="" class="lobby-btn-quiz-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-right">
<button type="button" class="lobby-btn-profile" id="btn-myprofile" aria-label="โปรไฟล์ของฉัน">
<img src="IMAGE/btn-myprofile.png" alt="" class="lobby-btn-profile-img" role="presentation" decoding="async">
</button>
</div>
</footer>
<div id="lobby-guide-dim" class="lobby-guide-dim hidden" aria-hidden="true"></div>
<div id="lobby-guide-focus" class="lobby-guide-focus hidden" aria-hidden="true"></div>
<div class="lobby-guide-bar hidden" id="lobby-guide-bar" role="dialog" aria-modal="true" aria-labelledby="guide-img-label">
<span id="guide-img-label" class="sr-only">คำแนะนำทีละขั้น</span>
<div class="lobby-guide-frame">
<img src="IMAGE/guide/guide-01.png" alt="คำแนะนำ" class="lobby-guide-img" id="guide-img" decoding="async">
<button type="button" class="lobby-guide-next" id="btn-guide-next" aria-label="ถัดไป">
<img src="IMAGE/guide/btn-next.png" alt="" decoding="async">
</button>
</div>
</div>
</div>
<!-- แชท AI — API เดียวกับ room-lobby -->
<div id="lobby-ai-chat-panel" class="lobby-ai-chat-panel lobby-ai-chat-hidden" role="dialog" aria-label="เทพความรู้">
<div class="lobby-ai-chat-header">
<span class="lobby-ai-chat-title">
<img src="../Game/img/ai-chat-header.png" alt="คุยกับ AI" decoding="async">
<img src="../Game/img/ai-chat-title-label.png" alt="เทพความรู้" class="lobby-ai-chat-title-label" decoding="async">
</span>
<button type="button" class="lobby-ai-chat-close" id="lobby-ai-chat-close-btn" title="ปิดแชท AI" aria-label="ปิดแชท AI">
<img src="../Game/img/chat-close-btn.png" alt="ปิด" id="lobby-ai-chat-toggle-img" width="50" height="50" decoding="async">
</button>
</div>
<div id="lobby-ai-chat-messages" class="lobby-ai-chat-messages"></div>
<form id="lobby-ai-chat-form" class="lobby-ai-chat-form">
<input id="lobby-ai-chat-input" type="text" placeholder="Type a here..." autocomplete="off" aria-label="ข้อความถึง AI">
<button type="submit" class="lobby-ai-chat-send" title="ส่ง" aria-label="ส่ง">
<img src="../Game/img/chat-btn-send.png" alt="" decoding="async">
</button>
</form>
<p class="lobby-ai-chat-admin-link"><a href="../Game/ai-admin.html" target="_blank" rel="noopener noreferrer">ตั้งค่า AI (Admin)</a></p>
</div>
<div id="lobby-howto-overlay" class="lobby-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="lobby-howto-title">
<div class="lobby-howto-inner">
<h2 id="lobby-howto-title" class="sr-only">วิธีเล่น</h2>
<img src="IMAGE/Howto/howtoplay.png" alt="วิธีเล่น" decoding="async">
<div class="lobby-howto-close">
<button type="button" class="lobby-icon-btn" id="btn-howto-got-it" aria-label="เข้าใจแล้ว">
<img src="IMAGE/Howto/btn-got-it.png" alt="" decoding="async" onerror="this.style.display='none'; this.parentElement.textContent='ปิด';">
</button>
</div>
</div>
</div>
<div id="lobby-toast" class="lobby-toast" role="status" aria-live="polite"></div>
<script src="../app-base.js?v=1"></script>
<script src="lobby.js?v=0.0164"></script>
</body>
</html>
@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Main Lobby — JD JUSTICE DIVERS</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=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0173">
</head>
<body class="lobby-page">
<div class="lobby-bg" role="img" aria-label="พื้นหลัง">
<img src="IMAGE/BG-Main-Lobby.png" alt="" class="lobby-bg-img" role="presentation" decoding="async">
</div>
<div class="lobby-vignette" aria-hidden="true"></div>
<div class="lobby-ui">
<header class="lobby-header">
<div class="lobby-profile-panel">
<img src="IMAGE/profile-bg.png" alt="" class="lobby-profile-bg" role="presentation" decoding="async">
<div class="lobby-profile-inner">
<div class="lobby-avatar-wrap">
<img src="" alt="" class="lobby-avatar-mask" id="lobby-profile-avatar" width="50" height="50" decoding="async">
</div>
<div class="lobby-profile-info">
<p class="lobby-profile-name" id="lobby-profile-name">MONE</p>
<p class="lobby-profile-meta">AGENT ID : <span id="lobby-profile-agent-id">001998</span></p>
<p class="lobby-profile-coins">
<img src="IMAGE/coin-JD.png" alt="" class="lobby-coin-icon" role="presentation" width="18" height="18" decoding="async">
COINS : <span id="lobby-profile-coins">0</span>
</p>
</div>
</div>
</div>
<div class="lobby-news-wrap">
<img src="IMAGE/news-banner-bg.png" alt="" class="lobby-news-bg" role="presentation" decoding="async">
<img src="IMAGE/news.png" alt="ข่าวสารศูนย์บัญชาการ" class="lobby-news-img" role="presentation" decoding="async">
</div>
<div class="lobby-header-row2-left">
<button type="button" class="lobby-btn-tutorial" id="btn-tutorial" title="วิธีเล่น" aria-label="วิธีเล่น">
<img src="IMAGE/btn-tutorial.png" alt="" class="lobby-btn-tutorial-img" role="presentation" decoding="async">
</button>
</div>
<button type="button" class="lobby-btn-daily" id="btn-daily" aria-label="รางวัลประจำวัน">
<img src="IMAGE/daily-rewards.png" alt="" class="lobby-daily-img" role="presentation" decoding="async">
<span class="lobby-daily-dot" aria-hidden="true"></span>
</button>
</header>
<div class="lobby-character-area">
<div class="lobby-center-left">
<button type="button" class="lobby-btn-cloth" id="btn-cloth" aria-label="ห้องแต่งตัว">
<img src="IMAGE/btn-cloth.png" alt="" class="lobby-btn-cloth-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-character-center" id="lobby-character-center" aria-hidden="true">
<img id="lobby-character-img" src="" alt="" class="lobby-character-img" width="256" height="256" decoding="async">
</div>
</div>
<aside class="lobby-leaderboard" aria-label="กระดานผู้นำ">
<img src="IMAGE/leaderboard-bg.png" alt="" class="lobby-leaderboard-bg" role="presentation" decoding="async">
<div class="lobby-leaderboard-mask">
<div class="lobby-leaderboard-inner">
<div class="lobby-leaderboard-scroll">
<ul class="lobby-leaderboard-list">
<li>
<img src="IMAGE/leaderboard-1.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mixie Kim</span>
<span class="lobby-rank-case">(คดี : มหาเศรษฐีนามปลอม)</span>
</div>
<span class="lobby-rank-score">9999</span>
</li>
<li>
<img src="IMAGE/leaderboard-2.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Golden L.</span>
<span class="lobby-rank-case">(คดี : โจรกรรมไซเบอร์)</span>
</div>
<span class="lobby-rank-score">9951</span>
</li>
<li>
<img src="IMAGE/leaderboard-3.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Lilly 991</span>
<span class="lobby-rank-case">(คดี : ปั่นแอพพลวงเกม)</span>
</div>
<span class="lobby-rank-score">9552</span>
</li>
<li>
<span class="lobby-rank-num">4</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Nova Byte</span>
<span class="lobby-rank-case">(คดี : ฟิชชิงองค์กร)</span>
</div>
<span class="lobby-rank-score">9410</span>
</li>
<li>
<span class="lobby-rank-num">5</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Aster R.</span>
<span class="lobby-rank-case">(คดี : แก๊งค์หลอกลงทุน)</span>
</div>
<span class="lobby-rank-score">9280</span>
</li>
<li>
<span class="lobby-rank-num">6</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Hexa One</span>
<span class="lobby-rank-case">(คดี : เจาะระบบธนาคาร)</span>
</div>
<span class="lobby-rank-score">9145</span>
</li>
<li>
<span class="lobby-rank-num">7</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mint C.</span>
<span class="lobby-rank-case">(คดี : ปลอมเพจขายสินค้า)</span>
</div>
<span class="lobby-rank-score">9032</span>
</li>
<li>
<span class="lobby-rank-num">8</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Q-Logic</span>
<span class="lobby-rank-case">(คดี : หลอกโอนผ่านแชท)</span>
</div>
<span class="lobby-rank-score">8918</span>
</li>
<li>
<span class="lobby-rank-num">9</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Rin 47</span>
<span class="lobby-rank-case">(คดี : ลักข้อมูลส่วนบุคคล)</span>
</div>
<span class="lobby-rank-score">8804</span>
</li>
<li>
<span class="lobby-rank-num">10</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Skyline X</span>
<span class="lobby-rank-case">(คดี : ฟอกเงินคริปโต)</span>
</div>
<span class="lobby-rank-score">8690</span>
</li>
</ul>
<div class="lobby-leaderboard-scroll-thumb" id="lobby-leaderboard-scroll-thumb" aria-hidden="true"></div>
</div>
</div>
</div>
</aside>
<footer class="lobby-footer">
<div class="lobby-footer-left">
<button type="button" class="lobby-btn-ai-chat" id="btn-ai-chat" aria-label="เทพความรู้">
<img src="IMAGE/BTN-AI-ChatBOT.png" alt="" class="lobby-btn-ai-chat-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-center">
<button type="button" class="lobby-btn-start" id="btn-start-mission" aria-label="ไปเมนูหลัก" data-href="../Main-Menu/">
<img src="IMAGE/btn-start-mission.png" alt="" class="lobby-btn-start-img" role="presentation" decoding="async">
</button>
<button type="button" class="lobby-btn-quiz" id="btn-quiz-battle" aria-label="ห้องเกมตอบคำถาม" data-href="../Quiz-Battle/">
<img src="IMAGE/btn-quix%20battle.png" alt="" class="lobby-btn-quiz-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-right">
<button type="button" class="lobby-btn-profile" id="btn-myprofile" aria-label="โปรไฟล์ของฉัน">
<img src="IMAGE/btn-myprofile.png" alt="" class="lobby-btn-profile-img" role="presentation" decoding="async">
</button>
</div>
</footer>
<div id="lobby-guide-dim" class="lobby-guide-dim hidden" aria-hidden="true"></div>
<div id="lobby-guide-focus" class="lobby-guide-focus hidden" aria-hidden="true"></div>
<div class="lobby-guide-bar hidden" id="lobby-guide-bar" role="dialog" aria-modal="true" aria-labelledby="guide-img-label">
<span id="guide-img-label" class="sr-only">คำแนะนำทีละขั้น</span>
<div class="lobby-guide-frame">
<img src="IMAGE/guide/guide-01.png" alt="คำแนะนำ" class="lobby-guide-img" id="guide-img" decoding="async">
<button type="button" class="lobby-guide-next" id="btn-guide-next" aria-label="ถัดไป">
<img src="IMAGE/guide/btn-next.png" alt="" decoding="async">
</button>
</div>
</div>
</div>
<!-- แชท AI — API เดียวกับ room-lobby -->
<div id="lobby-ai-chat-panel" class="lobby-ai-chat-panel lobby-ai-chat-hidden" role="dialog" aria-label="เทพความรู้">
<div class="lobby-ai-chat-header">
<span class="lobby-ai-chat-title">
<img src="../Game/img/ai-chat-header.png" alt="คุยกับ AI" decoding="async">
<img src="../Game/img/ai-chat-title-label.png" alt="เทพความรู้" class="lobby-ai-chat-title-label" decoding="async">
</span>
<button type="button" class="lobby-ai-chat-close" id="lobby-ai-chat-close-btn" title="ปิดแชท AI" aria-label="ปิดแชท AI">
<img src="../Game/img/chat-close-btn.png" alt="ปิด" id="lobby-ai-chat-toggle-img" width="50" height="50" decoding="async">
</button>
</div>
<div id="lobby-ai-chat-messages" class="lobby-ai-chat-messages"></div>
<form id="lobby-ai-chat-form" class="lobby-ai-chat-form">
<input id="lobby-ai-chat-input" type="text" placeholder="Type a here..." autocomplete="off" aria-label="ข้อความถึง AI">
<button type="submit" class="lobby-ai-chat-send" title="ส่ง" aria-label="ส่ง">
<img src="../Game/img/chat-btn-send.png" alt="" decoding="async">
</button>
</form>
<p class="lobby-ai-chat-admin-link"><a href="../Game/ai-admin.html" target="_blank" rel="noopener noreferrer">ตั้งค่า AI (Admin)</a></p>
</div>
<div id="lobby-howto-overlay" class="lobby-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="lobby-howto-title">
<div class="lobby-howto-inner">
<h2 id="lobby-howto-title" class="sr-only">วิธีเล่น</h2>
<img src="IMAGE/Howto/howtoplay.png" alt="วิธีเล่น" decoding="async">
<div class="lobby-howto-close">
<button type="button" class="lobby-icon-btn" id="btn-howto-got-it" aria-label="เข้าใจแล้ว">
<img src="IMAGE/Howto/btn-got-it.png" alt="" decoding="async" onerror="this.style.display='none'; this.parentElement.textContent='ปิด';">
</button>
</div>
</div>
</div>
<div id="lobby-toast" class="lobby-toast" role="status" aria-live="polite"></div>
<script src="../app-base.js?v=1"></script>
<script src="lobby.js?v=0.0165"></script>
</body>
</html>
@@ -0,0 +1,255 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Main Lobby — JD JUSTICE DIVERS</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=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0174">
</head>
<body class="lobby-page">
<div class="lobby-bg" role="img" aria-label="พื้นหลัง">
<img src="IMAGE/BG-Main-Lobby.png" alt="" class="lobby-bg-img" role="presentation" decoding="async">
</div>
<div class="lobby-vignette" aria-hidden="true"></div>
<div class="lobby-ui">
<header class="lobby-header">
<div class="lobby-profile-panel">
<img src="IMAGE/profile-bg.png" alt="" class="lobby-profile-bg" role="presentation" decoding="async">
<div class="lobby-profile-inner">
<div class="lobby-avatar-wrap">
<img src="" alt="" class="lobby-avatar-mask" id="lobby-profile-avatar" width="50" height="50" decoding="async">
</div>
<div class="lobby-profile-info">
<p class="lobby-profile-name" id="lobby-profile-name">MONE</p>
<p class="lobby-profile-meta">AGENT ID : <span id="lobby-profile-agent-id">001998</span></p>
<p class="lobby-profile-coins">
<img src="IMAGE/coin-JD.png" alt="" class="lobby-coin-icon" role="presentation" width="18" height="18" decoding="async">
COINS : <span id="lobby-profile-coins">0</span>
</p>
</div>
</div>
</div>
<div class="lobby-news-wrap">
<img src="IMAGE/news-banner-bg.png" alt="" class="lobby-news-bg" role="presentation" decoding="async">
<img src="IMAGE/news.png" alt="ข่าวสารศูนย์บัญชาการ" class="lobby-news-img" role="presentation" decoding="async">
</div>
<div class="lobby-header-row2-left">
<button type="button" class="lobby-btn-tutorial" id="btn-tutorial" title="วิธีเล่น" aria-label="วิธีเล่น">
<img src="IMAGE/btn-tutorial.png" alt="" class="lobby-btn-tutorial-img" role="presentation" decoding="async">
</button>
</div>
<button type="button" class="lobby-btn-daily" id="btn-daily" aria-label="รางวัลประจำวัน">
<img src="IMAGE/daily-rewards.png" alt="" class="lobby-daily-img" role="presentation" decoding="async">
<span class="lobby-daily-dot" aria-hidden="true"></span>
</button>
</header>
<div class="lobby-character-area">
<div class="lobby-center-left">
<button type="button" class="lobby-btn-cloth" id="btn-cloth" aria-label="ห้องแต่งตัว">
<img src="IMAGE/btn-cloth.png" alt="" class="lobby-btn-cloth-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-character-center" id="lobby-character-center" aria-hidden="true">
<img id="lobby-character-img" src="" alt="" class="lobby-character-img" width="256" height="256" decoding="async">
</div>
</div>
<aside class="lobby-leaderboard" aria-label="กระดานผู้นำ">
<img src="IMAGE/leaderboard-bg.png" alt="" class="lobby-leaderboard-bg" role="presentation" decoding="async">
<div class="lobby-leaderboard-mask">
<div class="lobby-leaderboard-inner">
<div class="lobby-leaderboard-scroll">
<ul class="lobby-leaderboard-list">
<li>
<img src="IMAGE/leaderboard-1.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mixie Kim</span>
<span class="lobby-rank-case">(คดี : มหาเศรษฐีนามปลอม)</span>
</div>
<span class="lobby-rank-score">9999</span>
</li>
<li>
<img src="IMAGE/leaderboard-2.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Golden L.</span>
<span class="lobby-rank-case">(คดี : โจรกรรมไซเบอร์)</span>
</div>
<span class="lobby-rank-score">9951</span>
</li>
<li>
<img src="IMAGE/leaderboard-3.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Lilly 991</span>
<span class="lobby-rank-case">(คดี : ปั่นแอพพลวงเกม)</span>
</div>
<span class="lobby-rank-score">9552</span>
</li>
<li>
<span class="lobby-rank-num">4</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Nova Byte</span>
<span class="lobby-rank-case">(คดี : ฟิชชิงองค์กร)</span>
</div>
<span class="lobby-rank-score">9410</span>
</li>
<li>
<span class="lobby-rank-num">5</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Aster R.</span>
<span class="lobby-rank-case">(คดี : แก๊งค์หลอกลงทุน)</span>
</div>
<span class="lobby-rank-score">9280</span>
</li>
<li>
<span class="lobby-rank-num">6</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Hexa One</span>
<span class="lobby-rank-case">(คดี : เจาะระบบธนาคาร)</span>
</div>
<span class="lobby-rank-score">9145</span>
</li>
<li>
<span class="lobby-rank-num">7</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mint C.</span>
<span class="lobby-rank-case">(คดี : ปลอมเพจขายสินค้า)</span>
</div>
<span class="lobby-rank-score">9032</span>
</li>
<li>
<span class="lobby-rank-num">8</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Q-Logic</span>
<span class="lobby-rank-case">(คดี : หลอกโอนผ่านแชท)</span>
</div>
<span class="lobby-rank-score">8918</span>
</li>
<li>
<span class="lobby-rank-num">9</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Rin 47</span>
<span class="lobby-rank-case">(คดี : ลักข้อมูลส่วนบุคคล)</span>
</div>
<span class="lobby-rank-score">8804</span>
</li>
<li>
<span class="lobby-rank-num">10</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Skyline X</span>
<span class="lobby-rank-case">(คดี : ฟอกเงินคริปโต)</span>
</div>
<span class="lobby-rank-score">8690</span>
</li>
</ul>
<div class="lobby-leaderboard-scroll-thumb" id="lobby-leaderboard-scroll-thumb" aria-hidden="true"></div>
</div>
</div>
</div>
</aside>
<footer class="lobby-footer">
<div class="lobby-footer-left">
<button type="button" class="lobby-btn-ai-chat" id="btn-ai-chat" aria-label="เทพความรู้">
<img src="IMAGE/BTN-AI-ChatBOT.png" alt="" class="lobby-btn-ai-chat-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-center">
<button type="button" class="lobby-btn-start" id="btn-start-mission" aria-label="ไปเมนูหลัก" data-href="../Main-Menu/">
<img src="IMAGE/btn-start-mission.png" alt="" class="lobby-btn-start-img" role="presentation" decoding="async">
</button>
<button type="button" class="lobby-btn-quiz" id="btn-quiz-battle" aria-label="ห้องเกมตอบคำถาม" data-href="../Quiz-Battle/">
<img src="IMAGE/btn-quix%20battle.png" alt="" class="lobby-btn-quiz-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-right">
<button type="button" class="lobby-btn-profile" id="btn-myprofile" aria-label="โปรไฟล์ของฉัน">
<img src="IMAGE/btn-myprofile.png" alt="" class="lobby-btn-profile-img" role="presentation" decoding="async">
</button>
</div>
</footer>
<div id="lobby-guide-dim" class="lobby-guide-dim hidden" aria-hidden="true"></div>
<div id="lobby-guide-focus" class="lobby-guide-focus hidden" aria-hidden="true"></div>
<div class="lobby-guide-bar hidden" id="lobby-guide-bar" role="dialog" aria-modal="true" aria-labelledby="guide-img-label">
<span id="guide-img-label" class="sr-only">คำแนะนำทีละขั้น</span>
<div class="lobby-guide-frame">
<img src="IMAGE/guide/guide-01.png" alt="คำแนะนำ" class="lobby-guide-img" id="guide-img" decoding="async">
<button type="button" class="lobby-guide-next" id="btn-guide-next" aria-label="ถัดไป">
<img src="IMAGE/guide/btn-next.png" alt="" decoding="async">
</button>
</div>
</div>
</div>
<!-- แชท AI — API เดียวกับ room-lobby -->
<div id="lobby-ai-chat-panel" class="lobby-ai-chat-panel lobby-ai-chat-hidden" role="dialog" aria-label="เทพความรู้">
<div class="lobby-ai-chat-header">
<span class="lobby-ai-chat-title">
<img src="../Game/img/ai-chat-header.png" alt="คุยกับ AI" decoding="async">
<img src="../Game/img/ai-chat-title-label.png" alt="เทพความรู้" class="lobby-ai-chat-title-label" decoding="async">
</span>
<button type="button" class="lobby-ai-chat-close" id="lobby-ai-chat-close-btn" title="ปิดแชท AI" aria-label="ปิดแชท AI">
<img src="../Game/img/chat-close-btn.png" alt="ปิด" id="lobby-ai-chat-toggle-img" width="50" height="50" decoding="async">
</button>
</div>
<div id="lobby-ai-chat-messages" class="lobby-ai-chat-messages"></div>
<form id="lobby-ai-chat-form" class="lobby-ai-chat-form">
<input id="lobby-ai-chat-input" type="text" placeholder="Type a here..." autocomplete="off" aria-label="ข้อความถึง AI">
<button type="submit" class="lobby-ai-chat-send" title="ส่ง" aria-label="ส่ง">
<img src="../Game/img/chat-btn-send.png" alt="" decoding="async">
</button>
</form>
<p class="lobby-ai-chat-admin-link"><a href="../Game/ai-admin.html" target="_blank" rel="noopener noreferrer">ตั้งค่า AI (Admin)</a></p>
</div>
<div id="lobby-howto-overlay" class="lobby-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="lobby-howto-title">
<div class="lobby-howto-inner">
<h2 id="lobby-howto-title" class="sr-only">วิธีเล่น</h2>
<img src="IMAGE/Howto/howtoplay.png" alt="วิธีเล่น" decoding="async">
<div class="lobby-howto-close">
<button type="button" class="lobby-icon-btn" id="btn-howto-got-it" aria-label="เข้าใจแล้ว">
<img src="IMAGE/Howto/btn-got-it.png" alt="" decoding="async" onerror="this.style.display='none'; this.parentElement.textContent='ปิด';">
</button>
</div>
</div>
</div>
<!-- ห้องแต่งตัว (Customize) — Phase 1: UI + เลือกธีมสี/สีผิว/ใบหน้า -->
<div id="lobby-customize-overlay" class="lobby-customize-overlay hidden" role="dialog" aria-modal="true" aria-label="ห้องแต่งตัว" aria-hidden="true">
<div class="lobby-customize-backdrop" id="lobby-customize-backdrop" aria-hidden="true"></div>
<div class="lobby-customize-dialog">
<div class="lobby-customize-titlebar">
<h2 class="lobby-customize-title">ห้องแต่งตัว</h2>
<button type="button" class="lobby-customize-close" id="lobby-customize-close" aria-label="ปิด">&times;</button>
</div>
<div class="lobby-customize-row">
<img src="../Game/img/03-5-Customize/theme-color-txt.png" alt="ธีมสี" class="lobby-customize-row-label" decoding="async">
<div class="lobby-customize-swatches" id="lobby-theme-colors"></div>
</div>
<div class="lobby-customize-row">
<img src="../Game/img/03-5-Customize/theme-skin-txt.png" alt="สีผิว" class="lobby-customize-row-label" decoding="async">
<div class="lobby-customize-swatches" id="lobby-skin-tones"></div>
</div>
<div class="lobby-customize-tabs" role="tablist" aria-label="ประเภทการแต่งตัว">
<button type="button" class="lobby-customize-tab is-active" data-tab="face" role="tab" aria-selected="true"><img src="../Game/img/03-5-Customize/tab-1-face.png" alt="ใบหน้า" decoding="async"></button>
<button type="button" class="lobby-customize-tab" data-tab="hair" role="tab" aria-selected="false"><img src="../Game/img/03-5-Customize/tab-2-hair.png" alt="ทรงผม" decoding="async"></button>
<button type="button" class="lobby-customize-tab" data-tab="cloth" role="tab" aria-selected="false"><img src="../Game/img/03-5-Customize/tab-3-cloth.png" alt="ชุด" decoding="async"></button>
</div>
<div class="lobby-customize-items" id="lobby-customize-items"></div>
<button type="button" class="lobby-customize-confirm" id="lobby-customize-confirm" aria-label="ยืนยัน">
<img src="../Game/img/03-5-Customize/btn-cf.png" alt="ยืนยัน" decoding="async">
</button>
</div>
</div>
<div id="lobby-toast" class="lobby-toast" role="status" aria-live="polite"></div>
<script src="../app-base.js?v=1"></script>
<script src="lobby.js?v=0.0166"></script>
</body>
</html>
@@ -0,0 +1,256 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Main Lobby — JD JUSTICE DIVERS</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=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0175">
</head>
<body class="lobby-page">
<div class="lobby-bg" role="img" aria-label="พื้นหลัง">
<img src="IMAGE/BG-Main-Lobby.png" alt="" class="lobby-bg-img" role="presentation" decoding="async">
</div>
<div class="lobby-vignette" aria-hidden="true"></div>
<div class="lobby-ui">
<header class="lobby-header">
<div class="lobby-profile-panel">
<img src="IMAGE/profile-bg.png" alt="" class="lobby-profile-bg" role="presentation" decoding="async">
<div class="lobby-profile-inner">
<div class="lobby-avatar-wrap">
<img src="" alt="" class="lobby-avatar-mask" id="lobby-profile-avatar" width="50" height="50" decoding="async">
</div>
<div class="lobby-profile-info">
<p class="lobby-profile-name" id="lobby-profile-name">MONE</p>
<p class="lobby-profile-meta">AGENT ID : <span id="lobby-profile-agent-id">001998</span></p>
<p class="lobby-profile-coins">
<img src="IMAGE/coin-JD.png" alt="" class="lobby-coin-icon" role="presentation" width="18" height="18" decoding="async">
COINS : <span id="lobby-profile-coins">0</span>
</p>
</div>
</div>
</div>
<div class="lobby-news-wrap">
<img src="IMAGE/news-banner-bg.png" alt="" class="lobby-news-bg" role="presentation" decoding="async">
<img src="IMAGE/news.png" alt="ข่าวสารศูนย์บัญชาการ" class="lobby-news-img" role="presentation" decoding="async">
</div>
<div class="lobby-header-row2-left">
<button type="button" class="lobby-btn-tutorial" id="btn-tutorial" title="วิธีเล่น" aria-label="วิธีเล่น">
<img src="IMAGE/btn-tutorial.png" alt="" class="lobby-btn-tutorial-img" role="presentation" decoding="async">
</button>
</div>
<button type="button" class="lobby-btn-daily" id="btn-daily" aria-label="รางวัลประจำวัน">
<img src="IMAGE/daily-rewards.png" alt="" class="lobby-daily-img" role="presentation" decoding="async">
<span class="lobby-daily-dot" aria-hidden="true"></span>
</button>
</header>
<div class="lobby-character-area">
<div class="lobby-center-left">
<button type="button" class="lobby-btn-cloth" id="btn-cloth" aria-label="ห้องแต่งตัว">
<img src="IMAGE/btn-cloth.png" alt="" class="lobby-btn-cloth-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-character-center" id="lobby-character-center" aria-hidden="true">
<img id="lobby-character-img" src="" alt="" class="lobby-character-img" width="256" height="256" decoding="async">
</div>
</div>
<aside class="lobby-leaderboard" aria-label="กระดานผู้นำ">
<img src="IMAGE/leaderboard-bg.png" alt="" class="lobby-leaderboard-bg" role="presentation" decoding="async">
<div class="lobby-leaderboard-mask">
<div class="lobby-leaderboard-inner">
<div class="lobby-leaderboard-scroll">
<ul class="lobby-leaderboard-list">
<li>
<img src="IMAGE/leaderboard-1.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mixie Kim</span>
<span class="lobby-rank-case">(คดี : มหาเศรษฐีนามปลอม)</span>
</div>
<span class="lobby-rank-score">9999</span>
</li>
<li>
<img src="IMAGE/leaderboard-2.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Golden L.</span>
<span class="lobby-rank-case">(คดี : โจรกรรมไซเบอร์)</span>
</div>
<span class="lobby-rank-score">9951</span>
</li>
<li>
<img src="IMAGE/leaderboard-3.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Lilly 991</span>
<span class="lobby-rank-case">(คดี : ปั่นแอพพลวงเกม)</span>
</div>
<span class="lobby-rank-score">9552</span>
</li>
<li>
<span class="lobby-rank-num">4</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Nova Byte</span>
<span class="lobby-rank-case">(คดี : ฟิชชิงองค์กร)</span>
</div>
<span class="lobby-rank-score">9410</span>
</li>
<li>
<span class="lobby-rank-num">5</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Aster R.</span>
<span class="lobby-rank-case">(คดี : แก๊งค์หลอกลงทุน)</span>
</div>
<span class="lobby-rank-score">9280</span>
</li>
<li>
<span class="lobby-rank-num">6</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Hexa One</span>
<span class="lobby-rank-case">(คดี : เจาะระบบธนาคาร)</span>
</div>
<span class="lobby-rank-score">9145</span>
</li>
<li>
<span class="lobby-rank-num">7</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mint C.</span>
<span class="lobby-rank-case">(คดี : ปลอมเพจขายสินค้า)</span>
</div>
<span class="lobby-rank-score">9032</span>
</li>
<li>
<span class="lobby-rank-num">8</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Q-Logic</span>
<span class="lobby-rank-case">(คดี : หลอกโอนผ่านแชท)</span>
</div>
<span class="lobby-rank-score">8918</span>
</li>
<li>
<span class="lobby-rank-num">9</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Rin 47</span>
<span class="lobby-rank-case">(คดี : ลักข้อมูลส่วนบุคคล)</span>
</div>
<span class="lobby-rank-score">8804</span>
</li>
<li>
<span class="lobby-rank-num">10</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Skyline X</span>
<span class="lobby-rank-case">(คดี : ฟอกเงินคริปโต)</span>
</div>
<span class="lobby-rank-score">8690</span>
</li>
</ul>
<div class="lobby-leaderboard-scroll-thumb" id="lobby-leaderboard-scroll-thumb" aria-hidden="true"></div>
</div>
</div>
</div>
</aside>
<footer class="lobby-footer">
<div class="lobby-footer-left">
<button type="button" class="lobby-btn-ai-chat" id="btn-ai-chat" aria-label="เทพความรู้">
<img src="IMAGE/BTN-AI-ChatBOT.png" alt="" class="lobby-btn-ai-chat-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-center">
<button type="button" class="lobby-btn-start" id="btn-start-mission" aria-label="ไปเมนูหลัก" data-href="../Main-Menu/">
<img src="IMAGE/btn-start-mission.png" alt="" class="lobby-btn-start-img" role="presentation" decoding="async">
</button>
<button type="button" class="lobby-btn-quiz" id="btn-quiz-battle" aria-label="ห้องเกมตอบคำถาม" data-href="../Quiz-Battle/">
<img src="IMAGE/btn-quix%20battle.png" alt="" class="lobby-btn-quiz-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-right">
<button type="button" class="lobby-btn-profile" id="btn-myprofile" aria-label="โปรไฟล์ของฉัน">
<img src="IMAGE/btn-myprofile.png" alt="" class="lobby-btn-profile-img" role="presentation" decoding="async">
</button>
</div>
</footer>
<div id="lobby-guide-dim" class="lobby-guide-dim hidden" aria-hidden="true"></div>
<div id="lobby-guide-focus" class="lobby-guide-focus hidden" aria-hidden="true"></div>
<div class="lobby-guide-bar hidden" id="lobby-guide-bar" role="dialog" aria-modal="true" aria-labelledby="guide-img-label">
<span id="guide-img-label" class="sr-only">คำแนะนำทีละขั้น</span>
<div class="lobby-guide-frame">
<img src="IMAGE/guide/guide-01.png" alt="คำแนะนำ" class="lobby-guide-img" id="guide-img" decoding="async">
<button type="button" class="lobby-guide-next" id="btn-guide-next" aria-label="ถัดไป">
<img src="IMAGE/guide/btn-next.png" alt="" decoding="async">
</button>
</div>
</div>
</div>
<!-- แชท AI — API เดียวกับ room-lobby -->
<div id="lobby-ai-chat-panel" class="lobby-ai-chat-panel lobby-ai-chat-hidden" role="dialog" aria-label="เทพความรู้">
<div class="lobby-ai-chat-header">
<span class="lobby-ai-chat-title">
<img src="../Game/img/ai-chat-header.png" alt="คุยกับ AI" decoding="async">
<img src="../Game/img/ai-chat-title-label.png" alt="เทพความรู้" class="lobby-ai-chat-title-label" decoding="async">
</span>
<button type="button" class="lobby-ai-chat-close" id="lobby-ai-chat-close-btn" title="ปิดแชท AI" aria-label="ปิดแชท AI">
<img src="../Game/img/chat-close-btn.png" alt="ปิด" id="lobby-ai-chat-toggle-img" width="50" height="50" decoding="async">
</button>
</div>
<div id="lobby-ai-chat-messages" class="lobby-ai-chat-messages"></div>
<form id="lobby-ai-chat-form" class="lobby-ai-chat-form">
<input id="lobby-ai-chat-input" type="text" placeholder="Type a here..." autocomplete="off" aria-label="ข้อความถึง AI">
<button type="submit" class="lobby-ai-chat-send" title="ส่ง" aria-label="ส่ง">
<img src="../Game/img/chat-btn-send.png" alt="" decoding="async">
</button>
</form>
<p class="lobby-ai-chat-admin-link"><a href="../Game/ai-admin.html" target="_blank" rel="noopener noreferrer">ตั้งค่า AI (Admin)</a></p>
</div>
<div id="lobby-howto-overlay" class="lobby-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="lobby-howto-title">
<div class="lobby-howto-inner">
<h2 id="lobby-howto-title" class="sr-only">วิธีเล่น</h2>
<img src="IMAGE/Howto/howtoplay.png" alt="วิธีเล่น" decoding="async">
<div class="lobby-howto-close">
<button type="button" class="lobby-icon-btn" id="btn-howto-got-it" aria-label="เข้าใจแล้ว">
<img src="IMAGE/Howto/btn-got-it.png" alt="" decoding="async" onerror="this.style.display='none'; this.parentElement.textContent='ปิด';">
</button>
</div>
</div>
</div>
<!-- ห้องแต่งตัว (Customize) — Phase 1: UI + เลือกธีมสี/สีผิว/ใบหน้า -->
<div id="lobby-customize-overlay" class="lobby-customize-overlay hidden" role="dialog" aria-modal="true" aria-label="ห้องแต่งตัว" aria-hidden="true">
<div class="lobby-customize-backdrop" id="lobby-customize-backdrop" aria-hidden="true"></div>
<div class="lobby-customize-dialog">
<div class="lobby-customize-titlebar">
<h2 class="lobby-customize-title">ห้องแต่งตัว</h2>
<button type="button" class="lobby-customize-close" id="lobby-customize-close" aria-label="ปิด">&times;</button>
</div>
<div class="lobby-customize-row">
<img src="../Game/img/03-5-Customize/theme-color-txt.png" alt="ธีมสี" class="lobby-customize-row-label" decoding="async">
<div class="lobby-customize-swatches" id="lobby-theme-colors"></div>
</div>
<div class="lobby-customize-row">
<img src="../Game/img/03-5-Customize/theme-skin-txt.png" alt="สีผิว" class="lobby-customize-row-label" decoding="async">
<div class="lobby-customize-swatches" id="lobby-skin-tones"></div>
</div>
<div class="lobby-customize-tabs" role="tablist" aria-label="ประเภทการแต่งตัว">
<img id="lobby-customize-tabbar" class="lobby-customize-tabbar-img" src="../Game/img/03-5-Customize/tab-1-face.png" alt="" decoding="async">
<button type="button" class="lobby-customize-tabhit" data-tab="face" role="tab" aria-selected="true" aria-label="ใบหน้า"></button>
<button type="button" class="lobby-customize-tabhit" data-tab="hair" role="tab" aria-selected="false" aria-label="ทรงผม"></button>
<button type="button" class="lobby-customize-tabhit" data-tab="cloth" role="tab" aria-selected="false" aria-label="ชุด"></button>
</div>
<div class="lobby-customize-items" id="lobby-customize-items"></div>
<button type="button" class="lobby-customize-confirm" id="lobby-customize-confirm" aria-label="ยืนยัน">
<img src="../Game/img/03-5-Customize/btn-cf.png" alt="ยืนยัน" decoding="async">
</button>
</div>
</div>
<div id="lobby-toast" class="lobby-toast" role="status" aria-live="polite"></div>
<script src="../app-base.js?v=1"></script>
<script src="lobby.js?v=0.0167"></script>
</body>
</html>
@@ -0,0 +1,256 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Main Lobby — JD JUSTICE DIVERS</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=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0175">
</head>
<body class="lobby-page">
<div class="lobby-bg" role="img" aria-label="พื้นหลัง">
<img src="IMAGE/BG-Main-Lobby.png" alt="" class="lobby-bg-img" role="presentation" decoding="async">
</div>
<div class="lobby-vignette" aria-hidden="true"></div>
<div class="lobby-ui">
<header class="lobby-header">
<div class="lobby-profile-panel">
<img src="IMAGE/profile-bg.png" alt="" class="lobby-profile-bg" role="presentation" decoding="async">
<div class="lobby-profile-inner">
<div class="lobby-avatar-wrap">
<img src="" alt="" class="lobby-avatar-mask" id="lobby-profile-avatar" width="50" height="50" decoding="async">
</div>
<div class="lobby-profile-info">
<p class="lobby-profile-name" id="lobby-profile-name">MONE</p>
<p class="lobby-profile-meta">AGENT ID : <span id="lobby-profile-agent-id">001998</span></p>
<p class="lobby-profile-coins">
<img src="IMAGE/coin-JD.png" alt="" class="lobby-coin-icon" role="presentation" width="18" height="18" decoding="async">
COINS : <span id="lobby-profile-coins">0</span>
</p>
</div>
</div>
</div>
<div class="lobby-news-wrap">
<img src="IMAGE/news-banner-bg.png" alt="" class="lobby-news-bg" role="presentation" decoding="async">
<img src="IMAGE/news.png" alt="ข่าวสารศูนย์บัญชาการ" class="lobby-news-img" role="presentation" decoding="async">
</div>
<div class="lobby-header-row2-left">
<button type="button" class="lobby-btn-tutorial" id="btn-tutorial" title="วิธีเล่น" aria-label="วิธีเล่น">
<img src="IMAGE/btn-tutorial.png" alt="" class="lobby-btn-tutorial-img" role="presentation" decoding="async">
</button>
</div>
<button type="button" class="lobby-btn-daily" id="btn-daily" aria-label="รางวัลประจำวัน">
<img src="IMAGE/daily-rewards.png" alt="" class="lobby-daily-img" role="presentation" decoding="async">
<span class="lobby-daily-dot" aria-hidden="true"></span>
</button>
</header>
<div class="lobby-character-area">
<div class="lobby-center-left">
<button type="button" class="lobby-btn-cloth" id="btn-cloth" aria-label="ห้องแต่งตัว">
<img src="IMAGE/btn-cloth.png" alt="" class="lobby-btn-cloth-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-character-center" id="lobby-character-center" aria-hidden="true">
<img id="lobby-character-img" src="" alt="" class="lobby-character-img" width="256" height="256" decoding="async">
</div>
</div>
<aside class="lobby-leaderboard" aria-label="กระดานผู้นำ">
<img src="IMAGE/leaderboard-bg.png" alt="" class="lobby-leaderboard-bg" role="presentation" decoding="async">
<div class="lobby-leaderboard-mask">
<div class="lobby-leaderboard-inner">
<div class="lobby-leaderboard-scroll">
<ul class="lobby-leaderboard-list">
<li>
<img src="IMAGE/leaderboard-1.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mixie Kim</span>
<span class="lobby-rank-case">(คดี : มหาเศรษฐีนามปลอม)</span>
</div>
<span class="lobby-rank-score">9999</span>
</li>
<li>
<img src="IMAGE/leaderboard-2.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Golden L.</span>
<span class="lobby-rank-case">(คดี : โจรกรรมไซเบอร์)</span>
</div>
<span class="lobby-rank-score">9951</span>
</li>
<li>
<img src="IMAGE/leaderboard-3.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Lilly 991</span>
<span class="lobby-rank-case">(คดี : ปั่นแอพพลวงเกม)</span>
</div>
<span class="lobby-rank-score">9552</span>
</li>
<li>
<span class="lobby-rank-num">4</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Nova Byte</span>
<span class="lobby-rank-case">(คดี : ฟิชชิงองค์กร)</span>
</div>
<span class="lobby-rank-score">9410</span>
</li>
<li>
<span class="lobby-rank-num">5</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Aster R.</span>
<span class="lobby-rank-case">(คดี : แก๊งค์หลอกลงทุน)</span>
</div>
<span class="lobby-rank-score">9280</span>
</li>
<li>
<span class="lobby-rank-num">6</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Hexa One</span>
<span class="lobby-rank-case">(คดี : เจาะระบบธนาคาร)</span>
</div>
<span class="lobby-rank-score">9145</span>
</li>
<li>
<span class="lobby-rank-num">7</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mint C.</span>
<span class="lobby-rank-case">(คดี : ปลอมเพจขายสินค้า)</span>
</div>
<span class="lobby-rank-score">9032</span>
</li>
<li>
<span class="lobby-rank-num">8</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Q-Logic</span>
<span class="lobby-rank-case">(คดี : หลอกโอนผ่านแชท)</span>
</div>
<span class="lobby-rank-score">8918</span>
</li>
<li>
<span class="lobby-rank-num">9</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Rin 47</span>
<span class="lobby-rank-case">(คดี : ลักข้อมูลส่วนบุคคล)</span>
</div>
<span class="lobby-rank-score">8804</span>
</li>
<li>
<span class="lobby-rank-num">10</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Skyline X</span>
<span class="lobby-rank-case">(คดี : ฟอกเงินคริปโต)</span>
</div>
<span class="lobby-rank-score">8690</span>
</li>
</ul>
<div class="lobby-leaderboard-scroll-thumb" id="lobby-leaderboard-scroll-thumb" aria-hidden="true"></div>
</div>
</div>
</div>
</aside>
<footer class="lobby-footer">
<div class="lobby-footer-left">
<button type="button" class="lobby-btn-ai-chat" id="btn-ai-chat" aria-label="เทพความรู้">
<img src="IMAGE/BTN-AI-ChatBOT.png" alt="" class="lobby-btn-ai-chat-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-center">
<button type="button" class="lobby-btn-start" id="btn-start-mission" aria-label="ไปเมนูหลัก" data-href="../Main-Menu/">
<img src="IMAGE/btn-start-mission.png" alt="" class="lobby-btn-start-img" role="presentation" decoding="async">
</button>
<button type="button" class="lobby-btn-quiz" id="btn-quiz-battle" aria-label="ห้องเกมตอบคำถาม" data-href="../Quiz-Battle/">
<img src="IMAGE/btn-quix%20battle.png" alt="" class="lobby-btn-quiz-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-right">
<button type="button" class="lobby-btn-profile" id="btn-myprofile" aria-label="โปรไฟล์ของฉัน">
<img src="IMAGE/btn-myprofile.png" alt="" class="lobby-btn-profile-img" role="presentation" decoding="async">
</button>
</div>
</footer>
<div id="lobby-guide-dim" class="lobby-guide-dim hidden" aria-hidden="true"></div>
<div id="lobby-guide-focus" class="lobby-guide-focus hidden" aria-hidden="true"></div>
<div class="lobby-guide-bar hidden" id="lobby-guide-bar" role="dialog" aria-modal="true" aria-labelledby="guide-img-label">
<span id="guide-img-label" class="sr-only">คำแนะนำทีละขั้น</span>
<div class="lobby-guide-frame">
<img src="IMAGE/guide/guide-01.png" alt="คำแนะนำ" class="lobby-guide-img" id="guide-img" decoding="async">
<button type="button" class="lobby-guide-next" id="btn-guide-next" aria-label="ถัดไป">
<img src="IMAGE/guide/btn-next.png" alt="" decoding="async">
</button>
</div>
</div>
</div>
<!-- แชท AI — API เดียวกับ room-lobby -->
<div id="lobby-ai-chat-panel" class="lobby-ai-chat-panel lobby-ai-chat-hidden" role="dialog" aria-label="เทพความรู้">
<div class="lobby-ai-chat-header">
<span class="lobby-ai-chat-title">
<img src="../Game/img/ai-chat-header.png" alt="คุยกับ AI" decoding="async">
<img src="../Game/img/ai-chat-title-label.png" alt="เทพความรู้" class="lobby-ai-chat-title-label" decoding="async">
</span>
<button type="button" class="lobby-ai-chat-close" id="lobby-ai-chat-close-btn" title="ปิดแชท AI" aria-label="ปิดแชท AI">
<img src="../Game/img/chat-close-btn.png" alt="ปิด" id="lobby-ai-chat-toggle-img" width="50" height="50" decoding="async">
</button>
</div>
<div id="lobby-ai-chat-messages" class="lobby-ai-chat-messages"></div>
<form id="lobby-ai-chat-form" class="lobby-ai-chat-form">
<input id="lobby-ai-chat-input" type="text" placeholder="Type a here..." autocomplete="off" aria-label="ข้อความถึง AI">
<button type="submit" class="lobby-ai-chat-send" title="ส่ง" aria-label="ส่ง">
<img src="../Game/img/chat-btn-send.png" alt="" decoding="async">
</button>
</form>
<p class="lobby-ai-chat-admin-link"><a href="../Game/ai-admin.html" target="_blank" rel="noopener noreferrer">ตั้งค่า AI (Admin)</a></p>
</div>
<div id="lobby-howto-overlay" class="lobby-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="lobby-howto-title">
<div class="lobby-howto-inner">
<h2 id="lobby-howto-title" class="sr-only">วิธีเล่น</h2>
<img src="IMAGE/Howto/howtoplay.png" alt="วิธีเล่น" decoding="async">
<div class="lobby-howto-close">
<button type="button" class="lobby-icon-btn" id="btn-howto-got-it" aria-label="เข้าใจแล้ว">
<img src="IMAGE/Howto/btn-got-it.png" alt="" decoding="async" onerror="this.style.display='none'; this.parentElement.textContent='ปิด';">
</button>
</div>
</div>
</div>
<!-- ห้องแต่งตัว (Customize) — Phase 1: UI + เลือกธีมสี/สีผิว/ใบหน้า -->
<div id="lobby-customize-overlay" class="lobby-customize-overlay hidden" role="dialog" aria-modal="true" aria-label="ห้องแต่งตัว" aria-hidden="true">
<div class="lobby-customize-backdrop" id="lobby-customize-backdrop" aria-hidden="true"></div>
<div class="lobby-customize-dialog">
<div class="lobby-customize-titlebar">
<h2 class="lobby-customize-title">ห้องแต่งตัว</h2>
<button type="button" class="lobby-customize-close" id="lobby-customize-close" aria-label="ปิด">&times;</button>
</div>
<div class="lobby-customize-row">
<img src="../Game/img/03-5-Customize/theme-color-txt.png" alt="ธีมสี" class="lobby-customize-row-label" decoding="async">
<div class="lobby-customize-swatches" id="lobby-theme-colors"></div>
</div>
<div class="lobby-customize-row">
<img src="../Game/img/03-5-Customize/theme-skin-txt.png" alt="สีผิว" class="lobby-customize-row-label" decoding="async">
<div class="lobby-customize-swatches" id="lobby-skin-tones"></div>
</div>
<div class="lobby-customize-tabs" role="tablist" aria-label="ประเภทการแต่งตัว">
<img id="lobby-customize-tabbar" class="lobby-customize-tabbar-img" src="../Game/img/03-5-Customize/tab-1-face.png" alt="" decoding="async">
<button type="button" class="lobby-customize-tabhit" data-tab="face" role="tab" aria-selected="true" aria-label="ใบหน้า"></button>
<button type="button" class="lobby-customize-tabhit" data-tab="hair" role="tab" aria-selected="false" aria-label="ทรงผม"></button>
<button type="button" class="lobby-customize-tabhit" data-tab="cloth" role="tab" aria-selected="false" aria-label="ชุด"></button>
</div>
<div class="lobby-customize-items" id="lobby-customize-items"></div>
<button type="button" class="lobby-customize-confirm" id="lobby-customize-confirm" aria-label="ยืนยัน">
<img src="../Game/img/03-5-Customize/btn-cf.png" alt="ยืนยัน" decoding="async">
</button>
</div>
</div>
<div id="lobby-toast" class="lobby-toast" role="status" aria-live="polite"></div>
<script src="../app-base.js?v=1"></script>
<script src="lobby.js?v=0.0168"></script>
</body>
</html>
@@ -0,0 +1,256 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Main Lobby — JD JUSTICE DIVERS</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=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0175">
</head>
<body class="lobby-page">
<div class="lobby-bg" role="img" aria-label="พื้นหลัง">
<img src="IMAGE/BG-Main-Lobby.png" alt="" class="lobby-bg-img" role="presentation" decoding="async">
</div>
<div class="lobby-vignette" aria-hidden="true"></div>
<div class="lobby-ui">
<header class="lobby-header">
<div class="lobby-profile-panel">
<img src="IMAGE/profile-bg.png" alt="" class="lobby-profile-bg" role="presentation" decoding="async">
<div class="lobby-profile-inner">
<div class="lobby-avatar-wrap">
<img src="" alt="" class="lobby-avatar-mask" id="lobby-profile-avatar" width="50" height="50" decoding="async">
</div>
<div class="lobby-profile-info">
<p class="lobby-profile-name" id="lobby-profile-name">MONE</p>
<p class="lobby-profile-meta">AGENT ID : <span id="lobby-profile-agent-id">001998</span></p>
<p class="lobby-profile-coins">
<img src="IMAGE/coin-JD.png" alt="" class="lobby-coin-icon" role="presentation" width="18" height="18" decoding="async">
COINS : <span id="lobby-profile-coins">0</span>
</p>
</div>
</div>
</div>
<div class="lobby-news-wrap">
<img src="IMAGE/news-banner-bg.png" alt="" class="lobby-news-bg" role="presentation" decoding="async">
<img src="IMAGE/news.png" alt="ข่าวสารศูนย์บัญชาการ" class="lobby-news-img" role="presentation" decoding="async">
</div>
<div class="lobby-header-row2-left">
<button type="button" class="lobby-btn-tutorial" id="btn-tutorial" title="วิธีเล่น" aria-label="วิธีเล่น">
<img src="IMAGE/btn-tutorial.png" alt="" class="lobby-btn-tutorial-img" role="presentation" decoding="async">
</button>
</div>
<button type="button" class="lobby-btn-daily" id="btn-daily" aria-label="รางวัลประจำวัน">
<img src="IMAGE/daily-rewards.png" alt="" class="lobby-daily-img" role="presentation" decoding="async">
<span class="lobby-daily-dot" aria-hidden="true"></span>
</button>
</header>
<div class="lobby-character-area">
<div class="lobby-center-left">
<button type="button" class="lobby-btn-cloth" id="btn-cloth" aria-label="ห้องแต่งตัว">
<img src="IMAGE/btn-cloth.png" alt="" class="lobby-btn-cloth-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-character-center" id="lobby-character-center" aria-hidden="true">
<img id="lobby-character-img" src="" alt="" class="lobby-character-img" width="256" height="256" decoding="async">
</div>
</div>
<aside class="lobby-leaderboard" aria-label="กระดานผู้นำ">
<img src="IMAGE/leaderboard-bg.png" alt="" class="lobby-leaderboard-bg" role="presentation" decoding="async">
<div class="lobby-leaderboard-mask">
<div class="lobby-leaderboard-inner">
<div class="lobby-leaderboard-scroll">
<ul class="lobby-leaderboard-list">
<li>
<img src="IMAGE/leaderboard-1.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mixie Kim</span>
<span class="lobby-rank-case">(คดี : มหาเศรษฐีนามปลอม)</span>
</div>
<span class="lobby-rank-score">9999</span>
</li>
<li>
<img src="IMAGE/leaderboard-2.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Golden L.</span>
<span class="lobby-rank-case">(คดี : โจรกรรมไซเบอร์)</span>
</div>
<span class="lobby-rank-score">9951</span>
</li>
<li>
<img src="IMAGE/leaderboard-3.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">
<div class="lobby-rank-info">
<span class="lobby-rank-name">Lilly 991</span>
<span class="lobby-rank-case">(คดี : ปั่นแอพพลวงเกม)</span>
</div>
<span class="lobby-rank-score">9552</span>
</li>
<li>
<span class="lobby-rank-num">4</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Nova Byte</span>
<span class="lobby-rank-case">(คดี : ฟิชชิงองค์กร)</span>
</div>
<span class="lobby-rank-score">9410</span>
</li>
<li>
<span class="lobby-rank-num">5</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Aster R.</span>
<span class="lobby-rank-case">(คดี : แก๊งค์หลอกลงทุน)</span>
</div>
<span class="lobby-rank-score">9280</span>
</li>
<li>
<span class="lobby-rank-num">6</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Hexa One</span>
<span class="lobby-rank-case">(คดี : เจาะระบบธนาคาร)</span>
</div>
<span class="lobby-rank-score">9145</span>
</li>
<li>
<span class="lobby-rank-num">7</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Mint C.</span>
<span class="lobby-rank-case">(คดี : ปลอมเพจขายสินค้า)</span>
</div>
<span class="lobby-rank-score">9032</span>
</li>
<li>
<span class="lobby-rank-num">8</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Q-Logic</span>
<span class="lobby-rank-case">(คดี : หลอกโอนผ่านแชท)</span>
</div>
<span class="lobby-rank-score">8918</span>
</li>
<li>
<span class="lobby-rank-num">9</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Rin 47</span>
<span class="lobby-rank-case">(คดี : ลักข้อมูลส่วนบุคคล)</span>
</div>
<span class="lobby-rank-score">8804</span>
</li>
<li>
<span class="lobby-rank-num">10</span>
<div class="lobby-rank-info">
<span class="lobby-rank-name">Skyline X</span>
<span class="lobby-rank-case">(คดี : ฟอกเงินคริปโต)</span>
</div>
<span class="lobby-rank-score">8690</span>
</li>
</ul>
<div class="lobby-leaderboard-scroll-thumb" id="lobby-leaderboard-scroll-thumb" aria-hidden="true"></div>
</div>
</div>
</div>
</aside>
<footer class="lobby-footer">
<div class="lobby-footer-left">
<button type="button" class="lobby-btn-ai-chat" id="btn-ai-chat" aria-label="เทพความรู้">
<img src="IMAGE/BTN-AI-ChatBOT.png" alt="" class="lobby-btn-ai-chat-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-center">
<button type="button" class="lobby-btn-start" id="btn-start-mission" aria-label="ไปเมนูหลัก" data-href="../Main-Menu/">
<img src="IMAGE/btn-start-mission.png" alt="" class="lobby-btn-start-img" role="presentation" decoding="async">
</button>
<button type="button" class="lobby-btn-quiz" id="btn-quiz-battle" aria-label="ห้องเกมตอบคำถาม" data-href="../Quiz-Battle/">
<img src="IMAGE/btn-quix%20battle.png" alt="" class="lobby-btn-quiz-img" role="presentation" decoding="async">
</button>
</div>
<div class="lobby-footer-right">
<button type="button" class="lobby-btn-profile" id="btn-myprofile" aria-label="โปรไฟล์ของฉัน">
<img src="IMAGE/btn-myprofile.png" alt="" class="lobby-btn-profile-img" role="presentation" decoding="async">
</button>
</div>
</footer>
<div id="lobby-guide-dim" class="lobby-guide-dim hidden" aria-hidden="true"></div>
<div id="lobby-guide-focus" class="lobby-guide-focus hidden" aria-hidden="true"></div>
<div class="lobby-guide-bar hidden" id="lobby-guide-bar" role="dialog" aria-modal="true" aria-labelledby="guide-img-label">
<span id="guide-img-label" class="sr-only">คำแนะนำทีละขั้น</span>
<div class="lobby-guide-frame">
<img src="IMAGE/guide/guide-01.png" alt="คำแนะนำ" class="lobby-guide-img" id="guide-img" decoding="async">
<button type="button" class="lobby-guide-next" id="btn-guide-next" aria-label="ถัดไป">
<img src="IMAGE/guide/btn-next.png" alt="" decoding="async">
</button>
</div>
</div>
</div>
<!-- แชท AI — API เดียวกับ room-lobby -->
<div id="lobby-ai-chat-panel" class="lobby-ai-chat-panel lobby-ai-chat-hidden" role="dialog" aria-label="เทพความรู้">
<div class="lobby-ai-chat-header">
<span class="lobby-ai-chat-title">
<img src="../Game/img/ai-chat-header.png" alt="คุยกับ AI" decoding="async">
<img src="../Game/img/ai-chat-title-label.png" alt="เทพความรู้" class="lobby-ai-chat-title-label" decoding="async">
</span>
<button type="button" class="lobby-ai-chat-close" id="lobby-ai-chat-close-btn" title="ปิดแชท AI" aria-label="ปิดแชท AI">
<img src="../Game/img/chat-close-btn.png" alt="ปิด" id="lobby-ai-chat-toggle-img" width="50" height="50" decoding="async">
</button>
</div>
<div id="lobby-ai-chat-messages" class="lobby-ai-chat-messages"></div>
<form id="lobby-ai-chat-form" class="lobby-ai-chat-form">
<input id="lobby-ai-chat-input" type="text" placeholder="Type a here..." autocomplete="off" aria-label="ข้อความถึง AI">
<button type="submit" class="lobby-ai-chat-send" title="ส่ง" aria-label="ส่ง">
<img src="../Game/img/chat-btn-send.png" alt="" decoding="async">
</button>
</form>
<p class="lobby-ai-chat-admin-link"><a href="../Game/ai-admin.html" target="_blank" rel="noopener noreferrer">ตั้งค่า AI (Admin)</a></p>
</div>
<div id="lobby-howto-overlay" class="lobby-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="lobby-howto-title">
<div class="lobby-howto-inner">
<h2 id="lobby-howto-title" class="sr-only">วิธีเล่น</h2>
<img src="IMAGE/Howto/howtoplay.png" alt="วิธีเล่น" decoding="async">
<div class="lobby-howto-close">
<button type="button" class="lobby-icon-btn" id="btn-howto-got-it" aria-label="เข้าใจแล้ว">
<img src="IMAGE/Howto/btn-got-it.png" alt="" decoding="async" onerror="this.style.display='none'; this.parentElement.textContent='ปิด';">
</button>
</div>
</div>
</div>
<!-- ห้องแต่งตัว (Customize) — Phase 1: UI + เลือกธีมสี/สีผิว/ใบหน้า -->
<div id="lobby-customize-overlay" class="lobby-customize-overlay hidden" role="dialog" aria-modal="true" aria-label="ห้องแต่งตัว" aria-hidden="true">
<div class="lobby-customize-backdrop" id="lobby-customize-backdrop" aria-hidden="true"></div>
<div class="lobby-customize-dialog">
<div class="lobby-customize-titlebar">
<h2 class="lobby-customize-title">ห้องแต่งตัว</h2>
<button type="button" class="lobby-customize-close" id="lobby-customize-close" aria-label="ปิด">&times;</button>
</div>
<div class="lobby-customize-row">
<img src="../Game/img/03-5-Customize/theme-color-txt.png" alt="ธีมสี" class="lobby-customize-row-label" decoding="async">
<div class="lobby-customize-swatches" id="lobby-theme-colors"></div>
</div>
<div class="lobby-customize-row">
<img src="../Game/img/03-5-Customize/theme-skin-txt.png" alt="สีผิว" class="lobby-customize-row-label" decoding="async">
<div class="lobby-customize-swatches" id="lobby-skin-tones"></div>
</div>
<div class="lobby-customize-tabs" role="tablist" aria-label="ประเภทการแต่งตัว">
<img id="lobby-customize-tabbar" class="lobby-customize-tabbar-img" src="../Game/img/03-5-Customize/tab-1-face.png" alt="" decoding="async">
<button type="button" class="lobby-customize-tabhit" data-tab="face" role="tab" aria-selected="true" aria-label="ใบหน้า"></button>
<button type="button" class="lobby-customize-tabhit" data-tab="hair" role="tab" aria-selected="false" aria-label="ทรงผม"></button>
<button type="button" class="lobby-customize-tabhit" data-tab="cloth" role="tab" aria-selected="false" aria-label="ชุด"></button>
</div>
<div class="lobby-customize-items" id="lobby-customize-items"></div>
<button type="button" class="lobby-customize-confirm" id="lobby-customize-confirm" aria-label="ยืนยัน">
<img src="../Game/img/03-5-Customize/btn-cf.png" alt="ยืนยัน" decoding="async">
</button>
</div>
</div>
<div id="lobby-toast" class="lobby-toast" role="status" aria-live="polite"></div>
<script src="../app-base.js?v=1"></script>
<script src="lobby.js?v=0.0169"></script>
</body>
</html>
+252 -3
View File
@@ -84,6 +84,7 @@
function normalizeCharacterAssetId(id) {
var s = String(id == null ? '' : id).trim();
if (!s) return '';
if (s === 'Chatest') return ''; // legacy placeholder — ไม่มี sprite จริง ให้ถือเป็นว่าง
if (s.length > 96) s = s.slice(0, 96);
if (!/^[a-zA-Z0-9._-]+$/.test(s)) return '';
return s;
@@ -209,6 +210,7 @@
}
var last = list[list.length - 1];
var id = last && last.id ? normalizeCharacterAssetId(String(last.id)) : '';
if (id) { try { localStorage.setItem(CHAR_KEY, id); } catch (e) {} } // persist ตัวละคร default ให้ห้อง/เกมอ่านไปใช้ join
callback(id || '');
})
.catch(function () {
@@ -256,10 +258,11 @@
applyProfileTexts();
syncCoinsFromServer();
function applyCharAssets(urlOrNull) {
applyProfileAvatar(urlOrNull);
applyCenterCharacterResolved(urlOrNull);
lobbyBakedCharUrl = urlOrNull;
applyLobbyCharacterAppearance();
}
resolveLobbyCharacterId(function (id) {
lobbyResolvedCharId = id || '';
if (!id) {
applyCharAssets(null);
return;
@@ -613,7 +616,7 @@
}
document.getElementById('btn-cloth')?.addEventListener('click', function () {
window.location.href = BASE + '/character.html';
openLobbyCustomize();
});
document.getElementById('btn-daily')?.addEventListener('click', function () {
@@ -814,4 +817,250 @@
syncMainLobbyCharacterUi();
}
});
/* ===== ห้องแต่งตัว (Customize popup) — Phase 1: UI + เลือกธีมสี/สีผิว/ใบหน้า (เก็บ localStorage) ===== */
var CUSTOMIZE_ASSET = (typeof appPath === 'function' ? appPath('/Game') : '/Game') + '/img/03-5-Customize/';
var customizeOverlay = document.getElementById('lobby-customize-overlay');
/* ---- Phase 2: ทาสีตัวละครจริง (tint layer mask ด้วยสีที่เลือก) ---- */
var lobbyResolvedCharId = '';
var lobbyBakedCharUrl = null;
var LOBBY_LAYER_NAMES = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face'];
var lobbySwatchColorCache = {};
function lobbyHasSavedTint() {
try { return !!(localStorage.getItem('lobbyThemeColor') || localStorage.getItem('lobbySkinTone')); } catch (e) { return false; }
}
function lobbyCharLayerUrl(charId, name) {
return BASE + '/img/characters/' + encodeURIComponent(charId) + '_down_idle_layer_' + name + '.png';
}
function lobbyLoadImg(src, cb) {
var img = new Image();
img.onload = function () { cb(img); };
img.onerror = function () { cb(null); };
img.src = src;
}
function lobbySampleSwatchColor(group, idx, cb) {
var key = group + '-' + idx;
if (lobbySwatchColorCache[key]) { cb(lobbySwatchColorCache[key]); return; }
var src = CUSTOMIZE_ASSET + (group === 'color' ? 'color-' : 'skin-tone-') + idx + '.png';
lobbyLoadImg(src, function (img) {
if (!img || !img.naturalWidth) { cb(null); return; }
try {
var c = document.createElement('canvas');
c.width = 1; c.height = 1;
var x = c.getContext('2d');
x.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, 1, 1);
var d = x.getImageData(0, 0, 1, 1).data;
var rgb = 'rgb(' + d[0] + ',' + d[1] + ',' + d[2] + ')';
lobbySwatchColorCache[key] = rgb;
cb(rgb);
} catch (e) { cb(null); }
});
}
function lobbyTintMask(img, color) {
var c = document.createElement('canvas');
c.width = img.naturalWidth; c.height = img.naturalHeight;
var x = c.getContext('2d');
x.drawImage(img, 0, 0);
x.globalCompositeOperation = 'source-in';
x.fillStyle = color;
x.fillRect(0, 0, c.width, c.height);
return c;
}
function lobbyCompositeTinted(charId, cb) {
if (!charId) { cb(null); return; }
// กัน init รันก่อนตัวแปรถูกประกาศ (resolve แบบ sync ตอน localStorage มี char)
if (!lobbySwatchColorCache) lobbySwatchColorCache = {};
if (!LOBBY_LAYER_NAMES) LOBBY_LAYER_NAMES = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face'];
var colorIdx = '', skinIdx = '';
try { colorIdx = localStorage.getItem('lobbyThemeColor') || ''; skinIdx = localStorage.getItem('lobbySkinTone') || ''; } catch (e) {}
function withColors(themeColor, skinColor) {
var loaded = {};
var pending = LOBBY_LAYER_NAMES.length;
LOBBY_LAYER_NAMES.forEach(function (name) {
lobbyLoadImg(lobbyCharLayerUrl(charId, name), function (img) {
loaded[name] = img;
if (--pending === 0) finish(themeColor, skinColor, loaded);
});
});
}
function finish(themeColor, skinColor, loaded) {
var ref = null, i;
for (i = 0; i < LOBBY_LAYER_NAMES.length; i++) {
if (loaded[LOBBY_LAYER_NAMES[i]]) { ref = loaded[LOBBY_LAYER_NAMES[i]]; break; }
}
if (!ref) { cb(null); return; }
var c = document.createElement('canvas');
c.width = ref.naturalWidth; c.height = ref.naturalHeight;
var x = c.getContext('2d');
LOBBY_LAYER_NAMES.forEach(function (name) {
var img = loaded[name];
if (!img || !img.naturalWidth) return;
if (themeColor && (name === 'bodyColor' || name === 'hairColor')) x.drawImage(lobbyTintMask(img, themeColor), 0, 0);
else if (skinColor && name === 'headColor') x.drawImage(lobbyTintMask(img, skinColor), 0, 0);
else x.drawImage(img, 0, 0);
});
try { cb(c.toDataURL('image/png')); } catch (e) { cb(null); }
}
var need = 0, themeColor = null, skinColor = null;
function maybeGo() { if (need === 0) withColors(themeColor, skinColor); }
if (colorIdx) { need++; lobbySampleSwatchColor('color', colorIdx, function (rgb) { themeColor = rgb; need--; maybeGo(); }); }
if (skinIdx) { need++; lobbySampleSwatchColor('skin', skinIdx, function (rgb) { skinColor = rgb; need--; maybeGo(); }); }
if (need === 0) withColors(null, null);
}
function applyLobbyCharacterAppearance() {
if (lobbyResolvedCharId && lobbyHasSavedTint()) {
lobbyCompositeTinted(lobbyResolvedCharId, function (dataUrl) {
applyProfileAvatar(dataUrl || lobbyBakedCharUrl);
applyCenterCharacterResolved(dataUrl || lobbyBakedCharUrl);
});
return;
}
applyProfileAvatar(lobbyBakedCharUrl);
applyCenterCharacterResolved(lobbyBakedCharUrl);
}
var CUSTOMIZE_ITEMS = {
face: [
{ idx: 1, price: 0 }, { idx: 2, price: 0 }, { idx: 3, price: 50 }, { idx: 4, price: 50 },
{ idx: 5, price: 50 }, { idx: 6, price: 100 }, { idx: 7, price: 100 }, { idx: 8, price: 200 }
],
hair: [],
cloth: []
};
function lobbyMakeSwatch(group, idx, label) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'lobby-customize-swatch';
btn.setAttribute('data-idx', String(idx));
btn.setAttribute('aria-label', label);
var img = document.createElement('img');
img.src = CUSTOMIZE_ASSET + (group === 'color' ? 'color-' : 'skin-tone-') + idx + '.png';
img.alt = '';
img.decoding = 'async';
btn.appendChild(img);
btn.addEventListener('click', function () { lobbySelectSwatch(group, idx); });
return btn;
}
function lobbySelectSwatch(group, idx) {
var wrap = document.getElementById(group === 'color' ? 'lobby-theme-colors' : 'lobby-skin-tones');
if (!wrap) return;
[].forEach.call(wrap.children, function (c) {
c.classList.toggle('is-selected', c.getAttribute('data-idx') === String(idx));
});
try { localStorage.setItem(group === 'color' ? 'lobbyThemeColor' : 'lobbySkinTone', String(idx)); } catch (e) {}
applyLobbyCharacterAppearance();
}
function lobbyBuildSwatches() {
var colorWrap = document.getElementById('lobby-theme-colors');
var skinWrap = document.getElementById('lobby-skin-tones');
if (colorWrap && !colorWrap.childElementCount) {
for (var i = 1; i <= 8; i++) colorWrap.appendChild(lobbyMakeSwatch('color', i, 'ธีมสี ' + i));
}
if (skinWrap && !skinWrap.childElementCount) {
for (var j = 1; j <= 3; j++) skinWrap.appendChild(lobbyMakeSwatch('skin', j, 'สีผิว ' + j));
}
}
function lobbyRenderCustomizeItems(tab) {
var grid = document.getElementById('lobby-customize-items');
if (!grid) return;
grid.innerHTML = '';
var items = CUSTOMIZE_ITEMS[tab] || [];
if (!items.length) {
var empty = document.createElement('div');
empty.className = 'lobby-customize-empty';
empty.textContent = 'ยังไม่เปิดให้บริการ';
grid.appendChild(empty);
return;
}
var savedKey = 'lobbyItem_' + tab;
var saved = '';
try { saved = localStorage.getItem(savedKey) || ''; } catch (e) {}
items.forEach(function (it) {
var cell = document.createElement('button');
cell.type = 'button';
cell.className = 'lobby-customize-item' + (String(it.idx) === saved ? ' is-selected' : '');
cell.setAttribute('data-idx', String(it.idx));
var img = document.createElement('img');
img.className = 'lobby-customize-item-img';
img.src = CUSTOMIZE_ASSET + tab + '-' + it.idx + '.png';
img.alt = '';
img.decoding = 'async';
cell.appendChild(img);
if (it.price > 0) {
var price = document.createElement('span');
price.className = 'lobby-customize-item-price';
price.textContent = String(it.price);
cell.appendChild(price);
}
cell.addEventListener('click', function () {
[].forEach.call(grid.children, function (c) { c.classList.remove('is-selected'); });
cell.classList.add('is-selected');
try { localStorage.setItem(savedKey, String(it.idx)); } catch (e) {}
});
grid.appendChild(cell);
});
}
function lobbySetCustomizeTab(tab) {
var bar = document.getElementById('lobby-customize-tabbar');
if (bar) {
var map = { face: 'tab-1-face.png', hair: 'tab-2-hair.png', cloth: 'tab-3-cloth.png' };
bar.src = CUSTOMIZE_ASSET + (map[tab] || map.face);
}
[].forEach.call(document.querySelectorAll('.lobby-customize-tabhit'), function (t) {
t.setAttribute('aria-selected', t.getAttribute('data-tab') === tab ? 'true' : 'false');
});
lobbyRenderCustomizeItems(tab);
}
function openLobbyCustomize() {
if (!customizeOverlay) {
window.location.href = BASE + '/character.html';
return;
}
lobbyBuildSwatches();
try {
var c = localStorage.getItem('lobbyThemeColor'); if (c) lobbySelectSwatch('color', c);
var s = localStorage.getItem('lobbySkinTone'); if (s) lobbySelectSwatch('skin', s);
} catch (e) {}
lobbySetCustomizeTab('face');
customizeOverlay.classList.remove('hidden');
customizeOverlay.setAttribute('aria-hidden', 'false');
}
function closeLobbyCustomize() {
if (!customizeOverlay) return;
customizeOverlay.classList.add('hidden');
customizeOverlay.setAttribute('aria-hidden', 'true');
}
function setupLobbyCustomize() {
if (!customizeOverlay) return;
document.getElementById('lobby-customize-close')?.addEventListener('click', closeLobbyCustomize);
document.getElementById('lobby-customize-backdrop')?.addEventListener('click', closeLobbyCustomize);
document.getElementById('lobby-customize-confirm')?.addEventListener('click', function () {
closeLobbyCustomize();
toast('บันทึกการแต่งตัวแล้ว');
});
[].forEach.call(document.querySelectorAll('.lobby-customize-tabhit'), function (t) {
t.addEventListener('click', function () { lobbySetCustomizeTab(t.getAttribute('data-tab')); });
});
}
setupLobbyCustomize();
})();
@@ -0,0 +1,817 @@
(function () {
'use strict';
var BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
var SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + '/Game';
var CHAR_KEY = 'gameCharacterId';
/** รูป composite idle ทิศ down จากหน้า Character (preview-idle-layer-down) — ตรงกับ character.js */
var LOBBY_IDLE_DOWN_PREFIX = 'jdCharLobbyIdleDown:';
function getStoredLobbyIdleDownDataUrl(safeId) {
if (!safeId) return '';
try {
var v = localStorage.getItem(LOBBY_IDLE_DOWN_PREFIX + safeId) || '';
if (typeof v === 'string' && v.indexOf('data:image/') === 0 && v.length > 80) return v;
} catch (e) { /* ignore */ }
return '';
}
var GUIDE_KEY = 'mainLobbyGuideDone';
var MAX_GUIDE = 5;
var ML_GUIDE = typeof appPath === 'function' ? appPath('/Main-Lobby/IMAGE/guide') : '/Main-Lobby/IMAGE/guide';
/**
* ตำแหน่งแถบคำแนะนำ + จุดไฮไลต์ต่อ guide-01…05 (ปรับลำดับให้ตรงไฟล์รูป)
* position: 'bottom' | 'top' — ตาม mock แถบล่าง/บน
* highlight: selector ของปุ่มใน lobby (null = ไม่วงกรอบ)
*/
var GUIDE_STEPS = [
{ position: 'bottom', highlight: null, pad: 10 },
{ position: 'top', highlight: '.lobby-footer-center', pad: 10 },
{ position: 'top', highlight: '#btn-ai-chat', pad: 8 },
{ position: 'bottom', highlight: '#btn-cloth', pad: 10 },
{ position: 'bottom', highlight: '#btn-daily', pad: 10 },
];
function checkLogin() {
if (localStorage.getItem('isLoggedIn') !== 'true') {
window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/';
return false;
}
return true;
}
if (!checkLogin()) return;
function toast(msg) {
var el = document.getElementById('lobby-toast');
if (!el) return;
el.textContent = msg;
el.classList.add('lobby-toast--show');
clearTimeout(toast._t);
toast._t = setTimeout(function () {
el.classList.remove('lobby-toast--show');
}, 2600);
}
function padAgent(n) {
var s = String(n);
while (s.length < 6) s = '0' + s;
return s;
}
function ensureAgentId() {
var k = 'agentDisplayId';
var v = localStorage.getItem(k);
if (!v || !/^\d{6}$/.test(v)) {
v = padAgent(100000 + Math.floor(Math.random() * 899999));
try {
localStorage.setItem(k, v);
} catch (e) { /* ignore */ }
}
return v;
}
function getSelectedCharacterId() {
try {
return (localStorage.getItem(CHAR_KEY) || '').trim();
} catch (e) {
return '';
}
}
/** ใช้เฉพาะ id ที่ปลอดเป็น segment ใน URL — กันพาธแปลก/อักขระที่เซิร์ฟไม่มีไฟล์ */
function normalizeCharacterAssetId(id) {
var s = String(id == null ? '' : id).trim();
if (!s) return '';
if (s.length > 96) s = s.slice(0, 96);
if (!/^[a-zA-Z0-9._-]+$/.test(s)) return '';
return s;
}
function mainMenuCharFallbackUrl() {
return typeof appPath === 'function' ? appPath('/Main-Menu/char-main.png') : '/Main-Menu/char-main.png';
}
/**
* URL รูปโลบี้ — ลองทิศ down ก่อน (idle แล้ว walk) เพื่อไม่ยิง _up_idle_ ฯลฯ โดยไม่จำเป็น
* ถ้า down ไม่มีค่อยลอง up / left / right
* @param {string} safeId
* @param {boolean} preferIdle
*/
function characterSpriteCandidateUrls(safeId, preferIdle) {
if (!safeId) return [];
var enc = encodeURIComponent(safeId);
var q = '?ch=' + encodeURIComponent(safeId);
var dirsPrimary = ['down'];
var dirsFallback = ['up', 'left', 'right'];
var wantIdle = preferIdle !== false;
var urls = [];
function pushForDir(d) {
if (wantIdle) {
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_idle.png' + q);
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_idle_0.png' + q);
}
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '.png' + q);
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_0.png' + q);
}
for (var a = 0; a < dirsPrimary.length; a++) pushForDir(dirsPrimary[a]);
for (var b = 0; b < dirsFallback.length; b++) pushForDir(dirsFallback[b]);
return urls;
}
/** หลังลองครบแล้วไม่มีรูป — ลดการยิงซ้ำในเซสชัน (ไม่บล็อกการลองใหม่หลังอัปโหลดไฟล์แล้ว — ลบคีย์ตอนโหลดสำเร็จ) */
var CHAR_ASSET_MISS_PREFIX = 'jdCharAssetMissing:';
function markCharacterDownAssetMissing(safeId) {
if (!safeId) return;
try {
sessionStorage.setItem(CHAR_ASSET_MISS_PREFIX + safeId, '1');
} catch (e) { /* ignore */ }
}
function clearCharacterDownAssetMissing(safeId) {
if (!safeId) return;
try {
sessionStorage.removeItem(CHAR_ASSET_MISS_PREFIX + safeId);
} catch (e) { /* ignore */ }
}
/**
* ลอง URL ตามลำดับด้วย Image — ตรงกับพฤติกรรม <img> ใน character-grid
* @param {string[]} urls
* @param {(url: string | null) => void} cb
*/
function probeFirstLoadableImageUrl(urls, cb) {
var i = 0;
function next() {
if (i >= urls.length) {
cb(null);
return;
}
var u = urls[i++];
var img = new Image();
img.onload = function () {
if (img.naturalWidth > 0) cb(u);
else next();
};
img.onerror = function () {
next();
};
img.src = u;
}
next();
}
/**
* @param {string} safeId
* @param {(url: string | null) => void} cb
*/
function characterLobbySpriteFirstLiveUrl(safeId, cb) {
if (!safeId) {
cb(null);
return;
}
var urls = characterSpriteCandidateUrls(safeId, true);
if (!urls.length) {
cb(null);
return;
}
probeFirstLoadableImageUrl(urls, function (url) {
if (url) {
clearCharacterDownAssetMissing(safeId);
cb(url);
} else {
markCharacterDownAssetMissing(safeId);
cb(null);
}
});
}
/**
* id จาก localStorage หรือถ้าว่าง — ตัวสุดท้ายในรายการ API (ใหม่สุดตามลำดับเซิร์ฟ)
* @param {(safeId: string) => void} callback
*/
function resolveLobbyCharacterId(callback) {
var norm = normalizeCharacterAssetId(getSelectedCharacterId());
if (norm) {
callback(norm);
return;
}
fetch(BASE + '/api/characters', { credentials: 'same-origin', cache: 'no-store' })
.then(function (r) { return r.json(); })
.then(function (list) {
if (!Array.isArray(list) || !list.length) {
callback('');
return;
}
var last = list[list.length - 1];
var id = last && last.id ? normalizeCharacterAssetId(String(last.id)) : '';
callback(id || '');
})
.catch(function () {
callback('');
});
}
var PLAYER_KEY = 'jdPlayerKey';
function ensurePlayerKey() {
var k;
try {
k = localStorage.getItem(PLAYER_KEY);
} catch (e) {
k = '';
}
if (!k || String(k).length < 8) {
k = 'p_' + Date.now() + '_' + Math.random().toString(36).slice(2, 14);
try {
localStorage.setItem(PLAYER_KEY, k);
} catch (e2) { /* ignore */ }
}
return k;
}
/** ดึง COINS จากเซิร์ฟ (บัญชี guest สร้างอัตโนมัติถ้ายังไม่มี) */
function syncCoinsFromServer() {
var key = ensurePlayerKey();
fetch((typeof appPath === 'function' ? appPath('/Admin/api/player-coins.php') : '/Admin/api/player-coins.php') + '?playerKey=' + encodeURIComponent(key), { credentials: 'omit' })
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d || !d.ok) return;
var c = String(Math.max(0, parseInt(d.coins, 10) || 0));
try {
localStorage.setItem('jdCoins', c);
} catch (e) { /* ignore */ }
var coinsEl = document.getElementById('lobby-profile-coins');
if (coinsEl) coinsEl.textContent = c;
})
.catch(function () { /* offline */ });
}
/** รีเฟรชหลังเลือกตัวละคร / สลับแท็บ / ย้อนกลับบน tablet (bfcache) */
function syncMainLobbyCharacterUi() {
applyProfileTexts();
syncCoinsFromServer();
function applyCharAssets(urlOrNull) {
applyProfileAvatar(urlOrNull);
applyCenterCharacterResolved(urlOrNull);
}
resolveLobbyCharacterId(function (id) {
if (!id) {
applyCharAssets(null);
return;
}
var idleCached = getStoredLobbyIdleDownDataUrl(id);
if (idleCached) {
applyCharAssets(idleCached);
return;
}
characterLobbySpriteFirstLiveUrl(id, applyCharAssets);
});
}
function applyProfileTexts() {
var name = (localStorage.getItem('playerName') || '').trim() || 'MONE';
var coins = localStorage.getItem('jdCoins') || '0';
var nameEl = document.getElementById('lobby-profile-name');
var agentEl = document.getElementById('lobby-profile-agent-id');
var coinsEl = document.getElementById('lobby-profile-coins');
if (nameEl) nameEl.textContent = name.toUpperCase();
if (agentEl) agentEl.textContent = ensureAgentId();
if (coinsEl) coinsEl.textContent = coins;
}
/** @param {string | null} urlOrNull — URL ที่ HEAD ผ่านแล้ว หรือ null = ใช้ char-main */
function applyProfileAvatar(urlOrNull) {
var av = document.getElementById('lobby-profile-avatar');
if (!av) return;
var fb = mainMenuCharFallbackUrl();
av.onerror = null;
if (!urlOrNull) {
av.src = fb;
return;
}
av.onerror = function () {
av.onerror = null;
av.src = fb;
};
av.src = urlOrNull;
}
function applyCenterCharacterResolved(urlOrNull) {
var centerEl = document.getElementById('lobby-character-img');
if (!centerEl) return;
function showSceneOnly() {
centerEl.classList.remove('lobby-character-img--visible');
centerEl.removeAttribute('src');
}
function revealChar() {
centerEl.classList.add('lobby-character-img--visible');
}
function showFallbackCenterCharacter() {
var fb = mainMenuCharFallbackUrl();
centerEl.onload = revealChar;
centerEl.onerror = function () {
centerEl.onerror = null;
showSceneOnly();
};
centerEl.src = fb;
if (centerEl.complete && centerEl.naturalWidth > 0) {
revealChar();
}
}
function showWithChar(url) {
centerEl.onload = revealChar;
centerEl.onerror = function () {
showFallbackCenterCharacter();
};
centerEl.src = url;
if (centerEl.complete && centerEl.naturalWidth > 0) {
revealChar();
}
}
if (!urlOrNull) {
showFallbackCenterCharacter();
return;
}
showWithChar(urlOrNull);
}
// คำนวณ offset จากความสูงจอด้วย linear interpolation:
// 1080 -> 18cqh, 650 -> 25cqh
function applyCharacterFootOffsetByViewport() {
var root = document.documentElement;
if (!root) return;
var h = Math.max(1, window.innerHeight || 0);
var h1 = 650;
var y1 = 25;
var h2 = 1080;
var y2 = 18;
var t = (h - h1) / (h2 - h1);
var cqhValue = y1 + ((y2 - y1) * t);
// กันหลุดช่วงเมื่อจอเล็ก/ใหญ่เกินช่วงอ้างอิง
cqhValue = Math.max(18, Math.min(25, cqhValue));
root.style.setProperty('--lobby-character-foot-offset', cqhValue.toFixed(3) + 'cqh');
}
function syncLeaderboardScrollbarThumb() {
var scrollEl = document.querySelector('.lobby-leaderboard-scroll');
var thumbEl = document.getElementById('lobby-leaderboard-scroll-thumb');
if (!scrollEl || !thumbEl) return;
var styles = window.getComputedStyle(thumbEl);
var topInset = parseFloat(styles.top) || 0;
var minThumb = parseFloat(styles.minHeight) || 18;
var maxScroll = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight);
if (maxScroll <= 0) {
thumbEl.style.display = 'none';
return;
}
thumbEl.style.display = 'block';
var trackHeight = scrollEl.clientHeight;
var visibleRatio = scrollEl.clientHeight / Math.max(1, scrollEl.scrollHeight);
var thumbHeight = Math.max(minThumb, Math.round(trackHeight * visibleRatio));
var maxY = Math.max(0, trackHeight - topInset - thumbHeight);
var y = topInset + (maxY * (scrollEl.scrollTop / maxScroll));
thumbEl.style.height = thumbHeight + 'px';
thumbEl.style.transform = 'translateY(' + y.toFixed(2) + 'px)';
}
// วาง leaderboard ให้ต่อจากปุ่ม daily โดยไม่ทับกัน
function placeLeaderboardBelowDaily() {
var dailyBtn = document.getElementById('btn-daily');
var leaderboard = document.querySelector('.lobby-leaderboard');
var lobbyUi = document.querySelector('.lobby-ui');
if (!dailyBtn || !leaderboard || !lobbyUi) return;
var boardStyle = window.getComputedStyle(leaderboard);
if (boardStyle.position !== 'absolute') {
leaderboard.style.top = '';
return;
}
var uiRect = lobbyUi.getBoundingClientRect();
var dailyRect = dailyBtn.getBoundingClientRect();
var boardRect = leaderboard.getBoundingClientRect();
var cssGap = boardStyle.getPropertyValue('--lobby-daily-leaderboard-gap').trim();
var gapPx = 8;
if (cssGap) {
var n = parseFloat(cssGap);
if (!Number.isNaN(n)) {
if (cssGap.endsWith('cqh')) gapPx = (uiRect.height * n) / 100;
else if (cssGap.endsWith('px') || /^[+-]?\d+(\.\d+)?$/.test(cssGap)) gapPx = n;
else if (cssGap.endsWith('vh')) gapPx = ((window.innerHeight || 0) * n) / 100;
else if (cssGap.endsWith('%')) gapPx = (uiRect.height * n) / 100;
}
}
var nextTop = (dailyRect.bottom - uiRect.top) + gapPx;
var maxTop = Math.max(0, uiRect.height - boardRect.height - Math.max(0, gapPx));
var clampedTop = Math.max(0, Math.min(nextTop, maxTop));
leaderboard.style.top = clampedTop.toFixed(2) + 'px';
}
var leaderboardLayoutRaf = 0;
function scheduleLeaderboardPlacement() {
if (leaderboardLayoutRaf) cancelAnimationFrame(leaderboardLayoutRaf);
leaderboardLayoutRaf = requestAnimationFrame(function () {
leaderboardLayoutRaf = 0;
placeLeaderboardBelowDaily();
syncLeaderboardScrollbarThumb();
});
}
function bindLeaderboardScrollSync() {
var scrollEl = document.querySelector('.lobby-leaderboard-scroll');
if (!scrollEl || scrollEl.dataset.scrollSyncBound === '1') return;
scrollEl.addEventListener('scroll', syncLeaderboardScrollbarThumb, { passive: true });
scrollEl.dataset.scrollSyncBound = '1';
}
var guideStep = 1;
var guideImg = document.getElementById('guide-img');
var guideBar = document.getElementById('lobby-guide-bar');
var guideDim = document.getElementById('lobby-guide-dim');
var guideFocus = document.getElementById('lobby-guide-focus');
function getGuideStepConfig(step) {
return GUIDE_STEPS[step - 1] || { position: 'bottom', highlight: null, pad: 10 };
}
function hideGuideFocus() {
if (!guideFocus) return;
guideFocus.classList.add('hidden');
guideFocus.style.left = '0';
guideFocus.style.top = '0';
guideFocus.style.width = '0';
guideFocus.style.height = '0';
}
function scheduleGuideFocusUpdate() {
requestAnimationFrame(function () {
requestAnimationFrame(updateGuideFocus);
});
}
function updateGuideFocus() {
if (!guideFocus || !guideBar || guideBar.classList.contains('hidden')) {
hideGuideFocus();
return;
}
var cfg = getGuideStepConfig(guideStep);
if (!cfg.highlight) {
hideGuideFocus();
return;
}
var target = document.querySelector(cfg.highlight);
if (!target) {
hideGuideFocus();
return;
}
var pad = typeof cfg.pad === 'number' ? cfg.pad : 8;
var r = target.getBoundingClientRect();
if (r.width < 4 || r.height < 4) {
hideGuideFocus();
return;
}
var cs = window.getComputedStyle(target);
var br = parseInt(cs.borderRadius, 10) || 0;
var radius = Math.max(br + 4, 14);
guideFocus.classList.remove('hidden');
guideFocus.style.left = (r.left - pad) + 'px';
guideFocus.style.top = (r.top - pad) + 'px';
guideFocus.style.width = (r.width + pad * 2) + 'px';
guideFocus.style.height = (r.height + pad * 2) + 'px';
guideFocus.style.borderRadius = radius + 'px';
}
function syncGuideBodyClass() {
if (!guideBar) return;
var open = !guideBar.classList.contains('hidden');
if (open) {
document.body.classList.add('lobby-guide-active');
var cfg = getGuideStepConfig(guideStep);
document.body.classList.toggle('lobby-guide-top-step', cfg.position === 'top');
var hasFocus = !!cfg.highlight;
if (guideDim) guideDim.classList.toggle('hidden', hasFocus);
} else {
document.body.classList.remove('lobby-guide-active');
document.body.classList.remove('lobby-guide-top-step');
if (guideDim) guideDim.classList.add('hidden');
}
if (open) {
scheduleGuideFocusUpdate();
} else {
hideGuideFocus();
}
}
function showGuideStep(n) {
if (!guideImg || !guideBar) return;
if (n < 1 || n > MAX_GUIDE) {
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
hideGuideFocus();
try {
localStorage.setItem(GUIDE_KEY, '1');
} catch (e) { /* ignore */ }
syncGuideBodyClass();
return;
}
guideStep = n;
var cfg = getGuideStepConfig(guideStep);
guideImg.src = ML_GUIDE + '/guide-' + (n < 10 ? '0' + n : String(n)) + '.png';
guideImg.alt = 'คำแนะนำ ขั้นที่ ' + n;
guideBar.classList.toggle('lobby-guide-bar--top', cfg.position === 'top');
var nextBtn = document.getElementById('btn-guide-next');
if (nextBtn) {
nextBtn.setAttribute('aria-label', n >= MAX_GUIDE ? 'รับทราบ' : 'ถัดไป');
}
syncGuideBodyClass();
guideImg.onload = function () {
scheduleGuideFocusUpdate();
};
guideImg.onerror = function () {
scheduleGuideFocusUpdate();
};
scheduleGuideFocusUpdate();
}
function initGuide() {
if (!guideBar || !guideImg) return;
try {
if (localStorage.getItem(GUIDE_KEY) === '1') {
guideBar.classList.add('hidden');
syncGuideBodyClass();
return;
}
} catch (e) { /* ignore */ }
guideBar.classList.remove('hidden');
showGuideStep(1);
}
document.getElementById('btn-guide-next')?.addEventListener('click', function () {
if (guideStep >= MAX_GUIDE) {
if (guideBar) {
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
}
try {
localStorage.setItem(GUIDE_KEY, '1');
} catch (e) { /* ignore */ }
syncGuideBodyClass();
return;
}
showGuideStep(guideStep + 1);
});
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
if (!guideBar || guideBar.classList.contains('hidden')) return;
e.preventDefault();
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
hideGuideFocus();
syncGuideBodyClass();
});
window.addEventListener('resize', function () {
if (guideBar && !guideBar.classList.contains('hidden')) {
scheduleGuideFocusUpdate();
}
});
window.addEventListener('orientationchange', function () {
setTimeout(scheduleGuideFocusUpdate, 350);
});
/* ใช้ capture + data-href บน .lobby-footer-center — ลดโอกาสถูกเลเยอร์อื่นแย่งคลิกก่อนถึงปุ่ม */
var footerCenter = document.querySelector('.lobby-footer-center');
if (footerCenter) {
footerCenter.addEventListener('click', function (e) {
var btn = e.target.closest('button[data-href]');
if (!btn || !footerCenter.contains(btn)) return;
var href = btn.getAttribute('data-href');
if (!href) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
window.location.href = href;
}, true);
}
document.getElementById('btn-cloth')?.addEventListener('click', function () {
window.location.href = BASE + '/character.html';
});
document.getElementById('btn-daily')?.addEventListener('click', function () {
toast('รางวัลประจำวัน — เข้าเกมทุกวันเพื่อรับรางวัลต่อเนื่อง!');
});
document.getElementById('btn-myprofile')?.addEventListener('click', function () {
window.location.href = typeof appPath === 'function' ? appPath('/Main-Menu/') : '/Main-Menu/';
});
function getMlAiSessionId() {
try {
var k = 'mlAiChatSession';
var v = localStorage.getItem(k);
if (!v) {
v = 'ml-' + Date.now() + '-' + Math.random().toString(36).slice(2, 11);
localStorage.setItem(k, v);
}
return v;
} catch (e) {
return 'ml-' + String(Date.now());
}
}
var aiChatCloseBtn = document.getElementById('lobby-ai-chat-close-btn');
var aiChatPanel = document.getElementById('lobby-ai-chat-panel');
var aiChatToggleImg = document.getElementById('lobby-ai-chat-toggle-img');
var btnAiChat = document.getElementById('btn-ai-chat');
function syncAiChatPanelUi() {
if (!aiChatPanel) return;
var hidden = aiChatPanel.classList.contains('lobby-ai-chat-hidden');
if (btnAiChat) btnAiChat.style.display = hidden ? '' : 'none';
if (hidden || !aiChatToggleImg || !aiChatCloseBtn) return;
aiChatToggleImg.src = BASE + '/img/chat-close-btn.png';
aiChatToggleImg.alt = 'ปิด';
aiChatCloseBtn.setAttribute('title', 'ปิดแชท AI');
aiChatCloseBtn.setAttribute('aria-label', 'ปิดแชท AI');
}
if (aiChatCloseBtn && aiChatPanel) {
aiChatCloseBtn.addEventListener('click', function () {
aiChatPanel.classList.add('lobby-ai-chat-hidden');
aiChatPanel.classList.remove('chat-panel-collapsed');
syncAiChatPanelUi();
});
}
if (btnAiChat && aiChatPanel) {
btnAiChat.addEventListener('click', function () {
if (aiChatPanel.classList.contains('lobby-ai-chat-hidden')) {
aiChatPanel.classList.remove('lobby-ai-chat-hidden');
aiChatPanel.classList.remove('chat-panel-collapsed');
syncAiChatPanelUi();
var inp = document.getElementById('lobby-ai-chat-input');
if (inp) {
try {
inp.focus();
} catch (e) { /* ignore */ }
}
} else {
aiChatPanel.classList.add('lobby-ai-chat-hidden');
syncAiChatPanelUi();
}
});
}
syncAiChatPanelUi();
var aiChatForm = document.getElementById('lobby-ai-chat-form');
var aiChatInput = document.getElementById('lobby-ai-chat-input');
var aiChatSendBtn = aiChatForm ? aiChatForm.querySelector('button[type="submit"]') : null;
var aiChatLoadingEl = null;
function appendAiMessage(text, isAi) {
var el = document.getElementById('lobby-ai-chat-messages');
if (!el) return;
if (isAi) removeAiChatLoading();
var div = document.createElement('div');
div.className = 'lobby-ai-chat-msg ' + (isAi ? 'lobby-ai-chat-msg-ai' : 'lobby-ai-chat-msg-user');
div.textContent = (isAi ? 'AI: ' : '') + text;
el.appendChild(div);
el.scrollTop = 1e9;
}
function showAiChatLoading() {
var el = document.getElementById('lobby-ai-chat-messages');
if (!el || aiChatLoadingEl) return;
aiChatLoadingEl = document.createElement('div');
aiChatLoadingEl.className = 'lobby-ai-chat-msg lobby-ai-chat-msg-ai lobby-ai-chat-typing';
aiChatLoadingEl.setAttribute('aria-label', 'กำลังพิมพ์');
aiChatLoadingEl.innerHTML = '<span class="lobby-ai-typing-dot"></span><span class="lobby-ai-typing-dot"></span><span class="lobby-ai-typing-dot"></span>';
el.appendChild(aiChatLoadingEl);
el.scrollTop = 1e9;
}
function removeAiChatLoading() {
if (aiChatLoadingEl && aiChatLoadingEl.parentNode) {
aiChatLoadingEl.parentNode.removeChild(aiChatLoadingEl);
aiChatLoadingEl = null;
}
}
function setAiChatWaiting(waiting) {
if (aiChatInput) aiChatInput.disabled = waiting;
if (aiChatSendBtn) aiChatSendBtn.disabled = waiting;
if (waiting) showAiChatLoading();
else removeAiChatLoading();
}
if (aiChatForm && aiChatInput) {
aiChatForm.addEventListener('submit', function (e) {
e.preventDefault();
var text = (aiChatInput.value || '').trim();
if (!text) return;
appendAiMessage(text, false);
setAiChatWaiting(true);
aiChatInput.value = '';
fetch(SERVER + '/api/ai-chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text, sessionId: getMlAiSessionId() }),
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.response != null) appendAiMessage(data.response, true);
else if (data.error) appendAiMessage('[ข้อผิดพลาด: ' + data.error + ']', true);
})
.catch(function () {
appendAiMessage('[ส่งไม่สำเร็จ]', true);
})
.finally(function () {
setAiChatWaiting(false);
});
});
}
var howto = document.getElementById('lobby-howto-overlay');
function openHowto() {
if (howto) howto.classList.remove('hidden');
}
function closeHowto() {
if (howto) howto.classList.add('hidden');
}
/** ปุ่ม ? — คู่มือทีละขั้น ตาม /Main-Lobby/IMAGE/guide */
function openGuideTutorial() {
if (!guideBar || !guideImg) return;
closeHowto();
guideBar.classList.remove('hidden');
showGuideStep(1);
}
document.getElementById('btn-tutorial')?.addEventListener('click', openGuideTutorial);
document.getElementById('btn-howto-got-it')?.addEventListener('click', closeHowto);
howto?.addEventListener('click', function (e) {
if (e.target === howto) closeHowto();
});
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
initGuide();
window.addEventListener('pageshow', function () {
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
scheduleLeaderboardPlacement();
});
window.addEventListener('resize', function () {
applyCharacterFootOffsetByViewport();
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
});
window.addEventListener('orientationchange', function () {
setTimeout(function () {
applyCharacterFootOffsetByViewport();
scheduleLeaderboardPlacement();
}, 250);
});
window.addEventListener('load', function () {
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
});
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') {
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
scheduleLeaderboardPlacement();
}
});
window.addEventListener('storage', function (e) {
if (e.key == null || e.key === CHAR_KEY || e.key === 'playerName') {
syncMainLobbyCharacterUi();
return;
}
if (e.key && String(e.key).indexOf(LOBBY_IDLE_DOWN_PREFIX) === 0) {
syncMainLobbyCharacterUi();
}
});
})();
@@ -0,0 +1,818 @@
(function () {
'use strict';
var BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
var SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + '/Game';
var CHAR_KEY = 'gameCharacterId';
/** รูป composite idle ทิศ down จากหน้า Character (preview-idle-layer-down) — ตรงกับ character.js */
var LOBBY_IDLE_DOWN_PREFIX = 'jdCharLobbyIdleDown:';
function getStoredLobbyIdleDownDataUrl(safeId) {
if (!safeId) return '';
try {
var v = localStorage.getItem(LOBBY_IDLE_DOWN_PREFIX + safeId) || '';
if (typeof v === 'string' && v.indexOf('data:image/') === 0 && v.length > 80) return v;
} catch (e) { /* ignore */ }
return '';
}
var GUIDE_KEY = 'mainLobbyGuideDone';
var MAX_GUIDE = 5;
var ML_GUIDE = typeof appPath === 'function' ? appPath('/Main-Lobby/IMAGE/guide') : '/Main-Lobby/IMAGE/guide';
/**
* ตำแหน่งแถบคำแนะนำ + จุดไฮไลต์ต่อ guide-01…05 (ปรับลำดับให้ตรงไฟล์รูป)
* position: 'bottom' | 'top' — ตาม mock แถบล่าง/บน
* highlight: selector ของปุ่มใน lobby (null = ไม่วงกรอบ)
*/
var GUIDE_STEPS = [
{ position: 'bottom', highlight: null, pad: 10 },
{ position: 'top', highlight: '.lobby-footer-center', pad: 10 },
{ position: 'top', highlight: '#btn-ai-chat', pad: 8 },
{ position: 'bottom', highlight: '#btn-cloth', pad: 10 },
{ position: 'bottom', highlight: '#btn-daily', pad: 10 },
];
function checkLogin() {
if (localStorage.getItem('isLoggedIn') !== 'true') {
window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/';
return false;
}
return true;
}
if (!checkLogin()) return;
function toast(msg) {
var el = document.getElementById('lobby-toast');
if (!el) return;
el.textContent = msg;
el.classList.add('lobby-toast--show');
clearTimeout(toast._t);
toast._t = setTimeout(function () {
el.classList.remove('lobby-toast--show');
}, 2600);
}
function padAgent(n) {
var s = String(n);
while (s.length < 6) s = '0' + s;
return s;
}
function ensureAgentId() {
var k = 'agentDisplayId';
var v = localStorage.getItem(k);
if (!v || !/^\d{6}$/.test(v)) {
v = padAgent(100000 + Math.floor(Math.random() * 899999));
try {
localStorage.setItem(k, v);
} catch (e) { /* ignore */ }
}
return v;
}
function getSelectedCharacterId() {
try {
return (localStorage.getItem(CHAR_KEY) || '').trim();
} catch (e) {
return '';
}
}
/** ใช้เฉพาะ id ที่ปลอดเป็น segment ใน URL — กันพาธแปลก/อักขระที่เซิร์ฟไม่มีไฟล์ */
function normalizeCharacterAssetId(id) {
var s = String(id == null ? '' : id).trim();
if (!s) return '';
if (s === 'Chatest') return ''; // legacy placeholder — ไม่มี sprite จริง ให้ถือเป็นว่าง
if (s.length > 96) s = s.slice(0, 96);
if (!/^[a-zA-Z0-9._-]+$/.test(s)) return '';
return s;
}
function mainMenuCharFallbackUrl() {
return typeof appPath === 'function' ? appPath('/Main-Menu/char-main.png') : '/Main-Menu/char-main.png';
}
/**
* URL รูปโลบี้ — ลองทิศ down ก่อน (idle แล้ว walk) เพื่อไม่ยิง _up_idle_ ฯลฯ โดยไม่จำเป็น
* ถ้า down ไม่มีค่อยลอง up / left / right
* @param {string} safeId
* @param {boolean} preferIdle
*/
function characterSpriteCandidateUrls(safeId, preferIdle) {
if (!safeId) return [];
var enc = encodeURIComponent(safeId);
var q = '?ch=' + encodeURIComponent(safeId);
var dirsPrimary = ['down'];
var dirsFallback = ['up', 'left', 'right'];
var wantIdle = preferIdle !== false;
var urls = [];
function pushForDir(d) {
if (wantIdle) {
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_idle.png' + q);
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_idle_0.png' + q);
}
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '.png' + q);
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_0.png' + q);
}
for (var a = 0; a < dirsPrimary.length; a++) pushForDir(dirsPrimary[a]);
for (var b = 0; b < dirsFallback.length; b++) pushForDir(dirsFallback[b]);
return urls;
}
/** หลังลองครบแล้วไม่มีรูป — ลดการยิงซ้ำในเซสชัน (ไม่บล็อกการลองใหม่หลังอัปโหลดไฟล์แล้ว — ลบคีย์ตอนโหลดสำเร็จ) */
var CHAR_ASSET_MISS_PREFIX = 'jdCharAssetMissing:';
function markCharacterDownAssetMissing(safeId) {
if (!safeId) return;
try {
sessionStorage.setItem(CHAR_ASSET_MISS_PREFIX + safeId, '1');
} catch (e) { /* ignore */ }
}
function clearCharacterDownAssetMissing(safeId) {
if (!safeId) return;
try {
sessionStorage.removeItem(CHAR_ASSET_MISS_PREFIX + safeId);
} catch (e) { /* ignore */ }
}
/**
* ลอง URL ตามลำดับด้วย Image — ตรงกับพฤติกรรม <img> ใน character-grid
* @param {string[]} urls
* @param {(url: string | null) => void} cb
*/
function probeFirstLoadableImageUrl(urls, cb) {
var i = 0;
function next() {
if (i >= urls.length) {
cb(null);
return;
}
var u = urls[i++];
var img = new Image();
img.onload = function () {
if (img.naturalWidth > 0) cb(u);
else next();
};
img.onerror = function () {
next();
};
img.src = u;
}
next();
}
/**
* @param {string} safeId
* @param {(url: string | null) => void} cb
*/
function characterLobbySpriteFirstLiveUrl(safeId, cb) {
if (!safeId) {
cb(null);
return;
}
var urls = characterSpriteCandidateUrls(safeId, true);
if (!urls.length) {
cb(null);
return;
}
probeFirstLoadableImageUrl(urls, function (url) {
if (url) {
clearCharacterDownAssetMissing(safeId);
cb(url);
} else {
markCharacterDownAssetMissing(safeId);
cb(null);
}
});
}
/**
* id จาก localStorage หรือถ้าว่าง — ตัวสุดท้ายในรายการ API (ใหม่สุดตามลำดับเซิร์ฟ)
* @param {(safeId: string) => void} callback
*/
function resolveLobbyCharacterId(callback) {
var norm = normalizeCharacterAssetId(getSelectedCharacterId());
if (norm) {
callback(norm);
return;
}
fetch(BASE + '/api/characters', { credentials: 'same-origin', cache: 'no-store' })
.then(function (r) { return r.json(); })
.then(function (list) {
if (!Array.isArray(list) || !list.length) {
callback('');
return;
}
var last = list[list.length - 1];
var id = last && last.id ? normalizeCharacterAssetId(String(last.id)) : '';
callback(id || '');
})
.catch(function () {
callback('');
});
}
var PLAYER_KEY = 'jdPlayerKey';
function ensurePlayerKey() {
var k;
try {
k = localStorage.getItem(PLAYER_KEY);
} catch (e) {
k = '';
}
if (!k || String(k).length < 8) {
k = 'p_' + Date.now() + '_' + Math.random().toString(36).slice(2, 14);
try {
localStorage.setItem(PLAYER_KEY, k);
} catch (e2) { /* ignore */ }
}
return k;
}
/** ดึง COINS จากเซิร์ฟ (บัญชี guest สร้างอัตโนมัติถ้ายังไม่มี) */
function syncCoinsFromServer() {
var key = ensurePlayerKey();
fetch((typeof appPath === 'function' ? appPath('/Admin/api/player-coins.php') : '/Admin/api/player-coins.php') + '?playerKey=' + encodeURIComponent(key), { credentials: 'omit' })
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d || !d.ok) return;
var c = String(Math.max(0, parseInt(d.coins, 10) || 0));
try {
localStorage.setItem('jdCoins', c);
} catch (e) { /* ignore */ }
var coinsEl = document.getElementById('lobby-profile-coins');
if (coinsEl) coinsEl.textContent = c;
})
.catch(function () { /* offline */ });
}
/** รีเฟรชหลังเลือกตัวละคร / สลับแท็บ / ย้อนกลับบน tablet (bfcache) */
function syncMainLobbyCharacterUi() {
applyProfileTexts();
syncCoinsFromServer();
function applyCharAssets(urlOrNull) {
applyProfileAvatar(urlOrNull);
applyCenterCharacterResolved(urlOrNull);
}
resolveLobbyCharacterId(function (id) {
if (!id) {
applyCharAssets(null);
return;
}
var idleCached = getStoredLobbyIdleDownDataUrl(id);
if (idleCached) {
applyCharAssets(idleCached);
return;
}
characterLobbySpriteFirstLiveUrl(id, applyCharAssets);
});
}
function applyProfileTexts() {
var name = (localStorage.getItem('playerName') || '').trim() || 'MONE';
var coins = localStorage.getItem('jdCoins') || '0';
var nameEl = document.getElementById('lobby-profile-name');
var agentEl = document.getElementById('lobby-profile-agent-id');
var coinsEl = document.getElementById('lobby-profile-coins');
if (nameEl) nameEl.textContent = name.toUpperCase();
if (agentEl) agentEl.textContent = ensureAgentId();
if (coinsEl) coinsEl.textContent = coins;
}
/** @param {string | null} urlOrNull — URL ที่ HEAD ผ่านแล้ว หรือ null = ใช้ char-main */
function applyProfileAvatar(urlOrNull) {
var av = document.getElementById('lobby-profile-avatar');
if (!av) return;
var fb = mainMenuCharFallbackUrl();
av.onerror = null;
if (!urlOrNull) {
av.src = fb;
return;
}
av.onerror = function () {
av.onerror = null;
av.src = fb;
};
av.src = urlOrNull;
}
function applyCenterCharacterResolved(urlOrNull) {
var centerEl = document.getElementById('lobby-character-img');
if (!centerEl) return;
function showSceneOnly() {
centerEl.classList.remove('lobby-character-img--visible');
centerEl.removeAttribute('src');
}
function revealChar() {
centerEl.classList.add('lobby-character-img--visible');
}
function showFallbackCenterCharacter() {
var fb = mainMenuCharFallbackUrl();
centerEl.onload = revealChar;
centerEl.onerror = function () {
centerEl.onerror = null;
showSceneOnly();
};
centerEl.src = fb;
if (centerEl.complete && centerEl.naturalWidth > 0) {
revealChar();
}
}
function showWithChar(url) {
centerEl.onload = revealChar;
centerEl.onerror = function () {
showFallbackCenterCharacter();
};
centerEl.src = url;
if (centerEl.complete && centerEl.naturalWidth > 0) {
revealChar();
}
}
if (!urlOrNull) {
showFallbackCenterCharacter();
return;
}
showWithChar(urlOrNull);
}
// คำนวณ offset จากความสูงจอด้วย linear interpolation:
// 1080 -> 18cqh, 650 -> 25cqh
function applyCharacterFootOffsetByViewport() {
var root = document.documentElement;
if (!root) return;
var h = Math.max(1, window.innerHeight || 0);
var h1 = 650;
var y1 = 25;
var h2 = 1080;
var y2 = 18;
var t = (h - h1) / (h2 - h1);
var cqhValue = y1 + ((y2 - y1) * t);
// กันหลุดช่วงเมื่อจอเล็ก/ใหญ่เกินช่วงอ้างอิง
cqhValue = Math.max(18, Math.min(25, cqhValue));
root.style.setProperty('--lobby-character-foot-offset', cqhValue.toFixed(3) + 'cqh');
}
function syncLeaderboardScrollbarThumb() {
var scrollEl = document.querySelector('.lobby-leaderboard-scroll');
var thumbEl = document.getElementById('lobby-leaderboard-scroll-thumb');
if (!scrollEl || !thumbEl) return;
var styles = window.getComputedStyle(thumbEl);
var topInset = parseFloat(styles.top) || 0;
var minThumb = parseFloat(styles.minHeight) || 18;
var maxScroll = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight);
if (maxScroll <= 0) {
thumbEl.style.display = 'none';
return;
}
thumbEl.style.display = 'block';
var trackHeight = scrollEl.clientHeight;
var visibleRatio = scrollEl.clientHeight / Math.max(1, scrollEl.scrollHeight);
var thumbHeight = Math.max(minThumb, Math.round(trackHeight * visibleRatio));
var maxY = Math.max(0, trackHeight - topInset - thumbHeight);
var y = topInset + (maxY * (scrollEl.scrollTop / maxScroll));
thumbEl.style.height = thumbHeight + 'px';
thumbEl.style.transform = 'translateY(' + y.toFixed(2) + 'px)';
}
// วาง leaderboard ให้ต่อจากปุ่ม daily โดยไม่ทับกัน
function placeLeaderboardBelowDaily() {
var dailyBtn = document.getElementById('btn-daily');
var leaderboard = document.querySelector('.lobby-leaderboard');
var lobbyUi = document.querySelector('.lobby-ui');
if (!dailyBtn || !leaderboard || !lobbyUi) return;
var boardStyle = window.getComputedStyle(leaderboard);
if (boardStyle.position !== 'absolute') {
leaderboard.style.top = '';
return;
}
var uiRect = lobbyUi.getBoundingClientRect();
var dailyRect = dailyBtn.getBoundingClientRect();
var boardRect = leaderboard.getBoundingClientRect();
var cssGap = boardStyle.getPropertyValue('--lobby-daily-leaderboard-gap').trim();
var gapPx = 8;
if (cssGap) {
var n = parseFloat(cssGap);
if (!Number.isNaN(n)) {
if (cssGap.endsWith('cqh')) gapPx = (uiRect.height * n) / 100;
else if (cssGap.endsWith('px') || /^[+-]?\d+(\.\d+)?$/.test(cssGap)) gapPx = n;
else if (cssGap.endsWith('vh')) gapPx = ((window.innerHeight || 0) * n) / 100;
else if (cssGap.endsWith('%')) gapPx = (uiRect.height * n) / 100;
}
}
var nextTop = (dailyRect.bottom - uiRect.top) + gapPx;
var maxTop = Math.max(0, uiRect.height - boardRect.height - Math.max(0, gapPx));
var clampedTop = Math.max(0, Math.min(nextTop, maxTop));
leaderboard.style.top = clampedTop.toFixed(2) + 'px';
}
var leaderboardLayoutRaf = 0;
function scheduleLeaderboardPlacement() {
if (leaderboardLayoutRaf) cancelAnimationFrame(leaderboardLayoutRaf);
leaderboardLayoutRaf = requestAnimationFrame(function () {
leaderboardLayoutRaf = 0;
placeLeaderboardBelowDaily();
syncLeaderboardScrollbarThumb();
});
}
function bindLeaderboardScrollSync() {
var scrollEl = document.querySelector('.lobby-leaderboard-scroll');
if (!scrollEl || scrollEl.dataset.scrollSyncBound === '1') return;
scrollEl.addEventListener('scroll', syncLeaderboardScrollbarThumb, { passive: true });
scrollEl.dataset.scrollSyncBound = '1';
}
var guideStep = 1;
var guideImg = document.getElementById('guide-img');
var guideBar = document.getElementById('lobby-guide-bar');
var guideDim = document.getElementById('lobby-guide-dim');
var guideFocus = document.getElementById('lobby-guide-focus');
function getGuideStepConfig(step) {
return GUIDE_STEPS[step - 1] || { position: 'bottom', highlight: null, pad: 10 };
}
function hideGuideFocus() {
if (!guideFocus) return;
guideFocus.classList.add('hidden');
guideFocus.style.left = '0';
guideFocus.style.top = '0';
guideFocus.style.width = '0';
guideFocus.style.height = '0';
}
function scheduleGuideFocusUpdate() {
requestAnimationFrame(function () {
requestAnimationFrame(updateGuideFocus);
});
}
function updateGuideFocus() {
if (!guideFocus || !guideBar || guideBar.classList.contains('hidden')) {
hideGuideFocus();
return;
}
var cfg = getGuideStepConfig(guideStep);
if (!cfg.highlight) {
hideGuideFocus();
return;
}
var target = document.querySelector(cfg.highlight);
if (!target) {
hideGuideFocus();
return;
}
var pad = typeof cfg.pad === 'number' ? cfg.pad : 8;
var r = target.getBoundingClientRect();
if (r.width < 4 || r.height < 4) {
hideGuideFocus();
return;
}
var cs = window.getComputedStyle(target);
var br = parseInt(cs.borderRadius, 10) || 0;
var radius = Math.max(br + 4, 14);
guideFocus.classList.remove('hidden');
guideFocus.style.left = (r.left - pad) + 'px';
guideFocus.style.top = (r.top - pad) + 'px';
guideFocus.style.width = (r.width + pad * 2) + 'px';
guideFocus.style.height = (r.height + pad * 2) + 'px';
guideFocus.style.borderRadius = radius + 'px';
}
function syncGuideBodyClass() {
if (!guideBar) return;
var open = !guideBar.classList.contains('hidden');
if (open) {
document.body.classList.add('lobby-guide-active');
var cfg = getGuideStepConfig(guideStep);
document.body.classList.toggle('lobby-guide-top-step', cfg.position === 'top');
var hasFocus = !!cfg.highlight;
if (guideDim) guideDim.classList.toggle('hidden', hasFocus);
} else {
document.body.classList.remove('lobby-guide-active');
document.body.classList.remove('lobby-guide-top-step');
if (guideDim) guideDim.classList.add('hidden');
}
if (open) {
scheduleGuideFocusUpdate();
} else {
hideGuideFocus();
}
}
function showGuideStep(n) {
if (!guideImg || !guideBar) return;
if (n < 1 || n > MAX_GUIDE) {
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
hideGuideFocus();
try {
localStorage.setItem(GUIDE_KEY, '1');
} catch (e) { /* ignore */ }
syncGuideBodyClass();
return;
}
guideStep = n;
var cfg = getGuideStepConfig(guideStep);
guideImg.src = ML_GUIDE + '/guide-' + (n < 10 ? '0' + n : String(n)) + '.png';
guideImg.alt = 'คำแนะนำ ขั้นที่ ' + n;
guideBar.classList.toggle('lobby-guide-bar--top', cfg.position === 'top');
var nextBtn = document.getElementById('btn-guide-next');
if (nextBtn) {
nextBtn.setAttribute('aria-label', n >= MAX_GUIDE ? 'รับทราบ' : 'ถัดไป');
}
syncGuideBodyClass();
guideImg.onload = function () {
scheduleGuideFocusUpdate();
};
guideImg.onerror = function () {
scheduleGuideFocusUpdate();
};
scheduleGuideFocusUpdate();
}
function initGuide() {
if (!guideBar || !guideImg) return;
try {
if (localStorage.getItem(GUIDE_KEY) === '1') {
guideBar.classList.add('hidden');
syncGuideBodyClass();
return;
}
} catch (e) { /* ignore */ }
guideBar.classList.remove('hidden');
showGuideStep(1);
}
document.getElementById('btn-guide-next')?.addEventListener('click', function () {
if (guideStep >= MAX_GUIDE) {
if (guideBar) {
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
}
try {
localStorage.setItem(GUIDE_KEY, '1');
} catch (e) { /* ignore */ }
syncGuideBodyClass();
return;
}
showGuideStep(guideStep + 1);
});
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
if (!guideBar || guideBar.classList.contains('hidden')) return;
e.preventDefault();
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
hideGuideFocus();
syncGuideBodyClass();
});
window.addEventListener('resize', function () {
if (guideBar && !guideBar.classList.contains('hidden')) {
scheduleGuideFocusUpdate();
}
});
window.addEventListener('orientationchange', function () {
setTimeout(scheduleGuideFocusUpdate, 350);
});
/* ใช้ capture + data-href บน .lobby-footer-center — ลดโอกาสถูกเลเยอร์อื่นแย่งคลิกก่อนถึงปุ่ม */
var footerCenter = document.querySelector('.lobby-footer-center');
if (footerCenter) {
footerCenter.addEventListener('click', function (e) {
var btn = e.target.closest('button[data-href]');
if (!btn || !footerCenter.contains(btn)) return;
var href = btn.getAttribute('data-href');
if (!href) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
window.location.href = href;
}, true);
}
document.getElementById('btn-cloth')?.addEventListener('click', function () {
window.location.href = BASE + '/character.html';
});
document.getElementById('btn-daily')?.addEventListener('click', function () {
toast('รางวัลประจำวัน — เข้าเกมทุกวันเพื่อรับรางวัลต่อเนื่อง!');
});
document.getElementById('btn-myprofile')?.addEventListener('click', function () {
window.location.href = typeof appPath === 'function' ? appPath('/Main-Menu/') : '/Main-Menu/';
});
function getMlAiSessionId() {
try {
var k = 'mlAiChatSession';
var v = localStorage.getItem(k);
if (!v) {
v = 'ml-' + Date.now() + '-' + Math.random().toString(36).slice(2, 11);
localStorage.setItem(k, v);
}
return v;
} catch (e) {
return 'ml-' + String(Date.now());
}
}
var aiChatCloseBtn = document.getElementById('lobby-ai-chat-close-btn');
var aiChatPanel = document.getElementById('lobby-ai-chat-panel');
var aiChatToggleImg = document.getElementById('lobby-ai-chat-toggle-img');
var btnAiChat = document.getElementById('btn-ai-chat');
function syncAiChatPanelUi() {
if (!aiChatPanel) return;
var hidden = aiChatPanel.classList.contains('lobby-ai-chat-hidden');
if (btnAiChat) btnAiChat.style.display = hidden ? '' : 'none';
if (hidden || !aiChatToggleImg || !aiChatCloseBtn) return;
aiChatToggleImg.src = BASE + '/img/chat-close-btn.png';
aiChatToggleImg.alt = 'ปิด';
aiChatCloseBtn.setAttribute('title', 'ปิดแชท AI');
aiChatCloseBtn.setAttribute('aria-label', 'ปิดแชท AI');
}
if (aiChatCloseBtn && aiChatPanel) {
aiChatCloseBtn.addEventListener('click', function () {
aiChatPanel.classList.add('lobby-ai-chat-hidden');
aiChatPanel.classList.remove('chat-panel-collapsed');
syncAiChatPanelUi();
});
}
if (btnAiChat && aiChatPanel) {
btnAiChat.addEventListener('click', function () {
if (aiChatPanel.classList.contains('lobby-ai-chat-hidden')) {
aiChatPanel.classList.remove('lobby-ai-chat-hidden');
aiChatPanel.classList.remove('chat-panel-collapsed');
syncAiChatPanelUi();
var inp = document.getElementById('lobby-ai-chat-input');
if (inp) {
try {
inp.focus();
} catch (e) { /* ignore */ }
}
} else {
aiChatPanel.classList.add('lobby-ai-chat-hidden');
syncAiChatPanelUi();
}
});
}
syncAiChatPanelUi();
var aiChatForm = document.getElementById('lobby-ai-chat-form');
var aiChatInput = document.getElementById('lobby-ai-chat-input');
var aiChatSendBtn = aiChatForm ? aiChatForm.querySelector('button[type="submit"]') : null;
var aiChatLoadingEl = null;
function appendAiMessage(text, isAi) {
var el = document.getElementById('lobby-ai-chat-messages');
if (!el) return;
if (isAi) removeAiChatLoading();
var div = document.createElement('div');
div.className = 'lobby-ai-chat-msg ' + (isAi ? 'lobby-ai-chat-msg-ai' : 'lobby-ai-chat-msg-user');
div.textContent = (isAi ? 'AI: ' : '') + text;
el.appendChild(div);
el.scrollTop = 1e9;
}
function showAiChatLoading() {
var el = document.getElementById('lobby-ai-chat-messages');
if (!el || aiChatLoadingEl) return;
aiChatLoadingEl = document.createElement('div');
aiChatLoadingEl.className = 'lobby-ai-chat-msg lobby-ai-chat-msg-ai lobby-ai-chat-typing';
aiChatLoadingEl.setAttribute('aria-label', 'กำลังพิมพ์');
aiChatLoadingEl.innerHTML = '<span class="lobby-ai-typing-dot"></span><span class="lobby-ai-typing-dot"></span><span class="lobby-ai-typing-dot"></span>';
el.appendChild(aiChatLoadingEl);
el.scrollTop = 1e9;
}
function removeAiChatLoading() {
if (aiChatLoadingEl && aiChatLoadingEl.parentNode) {
aiChatLoadingEl.parentNode.removeChild(aiChatLoadingEl);
aiChatLoadingEl = null;
}
}
function setAiChatWaiting(waiting) {
if (aiChatInput) aiChatInput.disabled = waiting;
if (aiChatSendBtn) aiChatSendBtn.disabled = waiting;
if (waiting) showAiChatLoading();
else removeAiChatLoading();
}
if (aiChatForm && aiChatInput) {
aiChatForm.addEventListener('submit', function (e) {
e.preventDefault();
var text = (aiChatInput.value || '').trim();
if (!text) return;
appendAiMessage(text, false);
setAiChatWaiting(true);
aiChatInput.value = '';
fetch(SERVER + '/api/ai-chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text, sessionId: getMlAiSessionId() }),
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.response != null) appendAiMessage(data.response, true);
else if (data.error) appendAiMessage('[ข้อผิดพลาด: ' + data.error + ']', true);
})
.catch(function () {
appendAiMessage('[ส่งไม่สำเร็จ]', true);
})
.finally(function () {
setAiChatWaiting(false);
});
});
}
var howto = document.getElementById('lobby-howto-overlay');
function openHowto() {
if (howto) howto.classList.remove('hidden');
}
function closeHowto() {
if (howto) howto.classList.add('hidden');
}
/** ปุ่ม ? — คู่มือทีละขั้น ตาม /Main-Lobby/IMAGE/guide */
function openGuideTutorial() {
if (!guideBar || !guideImg) return;
closeHowto();
guideBar.classList.remove('hidden');
showGuideStep(1);
}
document.getElementById('btn-tutorial')?.addEventListener('click', openGuideTutorial);
document.getElementById('btn-howto-got-it')?.addEventListener('click', closeHowto);
howto?.addEventListener('click', function (e) {
if (e.target === howto) closeHowto();
});
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
initGuide();
window.addEventListener('pageshow', function () {
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
scheduleLeaderboardPlacement();
});
window.addEventListener('resize', function () {
applyCharacterFootOffsetByViewport();
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
});
window.addEventListener('orientationchange', function () {
setTimeout(function () {
applyCharacterFootOffsetByViewport();
scheduleLeaderboardPlacement();
}, 250);
});
window.addEventListener('load', function () {
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
});
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') {
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
scheduleLeaderboardPlacement();
}
});
window.addEventListener('storage', function (e) {
if (e.key == null || e.key === CHAR_KEY || e.key === 'playerName') {
syncMainLobbyCharacterUi();
return;
}
if (e.key && String(e.key).indexOf(LOBBY_IDLE_DOWN_PREFIX) === 0) {
syncMainLobbyCharacterUi();
}
});
})();
@@ -0,0 +1,952 @@
(function () {
'use strict';
var BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
var SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + '/Game';
var CHAR_KEY = 'gameCharacterId';
/** รูป composite idle ทิศ down จากหน้า Character (preview-idle-layer-down) — ตรงกับ character.js */
var LOBBY_IDLE_DOWN_PREFIX = 'jdCharLobbyIdleDown:';
function getStoredLobbyIdleDownDataUrl(safeId) {
if (!safeId) return '';
try {
var v = localStorage.getItem(LOBBY_IDLE_DOWN_PREFIX + safeId) || '';
if (typeof v === 'string' && v.indexOf('data:image/') === 0 && v.length > 80) return v;
} catch (e) { /* ignore */ }
return '';
}
var GUIDE_KEY = 'mainLobbyGuideDone';
var MAX_GUIDE = 5;
var ML_GUIDE = typeof appPath === 'function' ? appPath('/Main-Lobby/IMAGE/guide') : '/Main-Lobby/IMAGE/guide';
/**
* ตำแหน่งแถบคำแนะนำ + จุดไฮไลต์ต่อ guide-01…05 (ปรับลำดับให้ตรงไฟล์รูป)
* position: 'bottom' | 'top' — ตาม mock แถบล่าง/บน
* highlight: selector ของปุ่มใน lobby (null = ไม่วงกรอบ)
*/
var GUIDE_STEPS = [
{ position: 'bottom', highlight: null, pad: 10 },
{ position: 'top', highlight: '.lobby-footer-center', pad: 10 },
{ position: 'top', highlight: '#btn-ai-chat', pad: 8 },
{ position: 'bottom', highlight: '#btn-cloth', pad: 10 },
{ position: 'bottom', highlight: '#btn-daily', pad: 10 },
];
function checkLogin() {
if (localStorage.getItem('isLoggedIn') !== 'true') {
window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/';
return false;
}
return true;
}
if (!checkLogin()) return;
function toast(msg) {
var el = document.getElementById('lobby-toast');
if (!el) return;
el.textContent = msg;
el.classList.add('lobby-toast--show');
clearTimeout(toast._t);
toast._t = setTimeout(function () {
el.classList.remove('lobby-toast--show');
}, 2600);
}
function padAgent(n) {
var s = String(n);
while (s.length < 6) s = '0' + s;
return s;
}
function ensureAgentId() {
var k = 'agentDisplayId';
var v = localStorage.getItem(k);
if (!v || !/^\d{6}$/.test(v)) {
v = padAgent(100000 + Math.floor(Math.random() * 899999));
try {
localStorage.setItem(k, v);
} catch (e) { /* ignore */ }
}
return v;
}
function getSelectedCharacterId() {
try {
return (localStorage.getItem(CHAR_KEY) || '').trim();
} catch (e) {
return '';
}
}
/** ใช้เฉพาะ id ที่ปลอดเป็น segment ใน URL — กันพาธแปลก/อักขระที่เซิร์ฟไม่มีไฟล์ */
function normalizeCharacterAssetId(id) {
var s = String(id == null ? '' : id).trim();
if (!s) return '';
if (s === 'Chatest') return ''; // legacy placeholder — ไม่มี sprite จริง ให้ถือเป็นว่าง
if (s.length > 96) s = s.slice(0, 96);
if (!/^[a-zA-Z0-9._-]+$/.test(s)) return '';
return s;
}
function mainMenuCharFallbackUrl() {
return typeof appPath === 'function' ? appPath('/Main-Menu/char-main.png') : '/Main-Menu/char-main.png';
}
/**
* URL รูปโลบี้ — ลองทิศ down ก่อน (idle แล้ว walk) เพื่อไม่ยิง _up_idle_ ฯลฯ โดยไม่จำเป็น
* ถ้า down ไม่มีค่อยลอง up / left / right
* @param {string} safeId
* @param {boolean} preferIdle
*/
function characterSpriteCandidateUrls(safeId, preferIdle) {
if (!safeId) return [];
var enc = encodeURIComponent(safeId);
var q = '?ch=' + encodeURIComponent(safeId);
var dirsPrimary = ['down'];
var dirsFallback = ['up', 'left', 'right'];
var wantIdle = preferIdle !== false;
var urls = [];
function pushForDir(d) {
if (wantIdle) {
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_idle.png' + q);
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_idle_0.png' + q);
}
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '.png' + q);
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_0.png' + q);
}
for (var a = 0; a < dirsPrimary.length; a++) pushForDir(dirsPrimary[a]);
for (var b = 0; b < dirsFallback.length; b++) pushForDir(dirsFallback[b]);
return urls;
}
/** หลังลองครบแล้วไม่มีรูป — ลดการยิงซ้ำในเซสชัน (ไม่บล็อกการลองใหม่หลังอัปโหลดไฟล์แล้ว — ลบคีย์ตอนโหลดสำเร็จ) */
var CHAR_ASSET_MISS_PREFIX = 'jdCharAssetMissing:';
function markCharacterDownAssetMissing(safeId) {
if (!safeId) return;
try {
sessionStorage.setItem(CHAR_ASSET_MISS_PREFIX + safeId, '1');
} catch (e) { /* ignore */ }
}
function clearCharacterDownAssetMissing(safeId) {
if (!safeId) return;
try {
sessionStorage.removeItem(CHAR_ASSET_MISS_PREFIX + safeId);
} catch (e) { /* ignore */ }
}
/**
* ลอง URL ตามลำดับด้วย Image — ตรงกับพฤติกรรม <img> ใน character-grid
* @param {string[]} urls
* @param {(url: string | null) => void} cb
*/
function probeFirstLoadableImageUrl(urls, cb) {
var i = 0;
function next() {
if (i >= urls.length) {
cb(null);
return;
}
var u = urls[i++];
var img = new Image();
img.onload = function () {
if (img.naturalWidth > 0) cb(u);
else next();
};
img.onerror = function () {
next();
};
img.src = u;
}
next();
}
/**
* @param {string} safeId
* @param {(url: string | null) => void} cb
*/
function characterLobbySpriteFirstLiveUrl(safeId, cb) {
if (!safeId) {
cb(null);
return;
}
var urls = characterSpriteCandidateUrls(safeId, true);
if (!urls.length) {
cb(null);
return;
}
probeFirstLoadableImageUrl(urls, function (url) {
if (url) {
clearCharacterDownAssetMissing(safeId);
cb(url);
} else {
markCharacterDownAssetMissing(safeId);
cb(null);
}
});
}
/**
* id จาก localStorage หรือถ้าว่าง — ตัวสุดท้ายในรายการ API (ใหม่สุดตามลำดับเซิร์ฟ)
* @param {(safeId: string) => void} callback
*/
function resolveLobbyCharacterId(callback) {
var norm = normalizeCharacterAssetId(getSelectedCharacterId());
if (norm) {
callback(norm);
return;
}
fetch(BASE + '/api/characters', { credentials: 'same-origin', cache: 'no-store' })
.then(function (r) { return r.json(); })
.then(function (list) {
if (!Array.isArray(list) || !list.length) {
callback('');
return;
}
var last = list[list.length - 1];
var id = last && last.id ? normalizeCharacterAssetId(String(last.id)) : '';
callback(id || '');
})
.catch(function () {
callback('');
});
}
var PLAYER_KEY = 'jdPlayerKey';
function ensurePlayerKey() {
var k;
try {
k = localStorage.getItem(PLAYER_KEY);
} catch (e) {
k = '';
}
if (!k || String(k).length < 8) {
k = 'p_' + Date.now() + '_' + Math.random().toString(36).slice(2, 14);
try {
localStorage.setItem(PLAYER_KEY, k);
} catch (e2) { /* ignore */ }
}
return k;
}
/** ดึง COINS จากเซิร์ฟ (บัญชี guest สร้างอัตโนมัติถ้ายังไม่มี) */
function syncCoinsFromServer() {
var key = ensurePlayerKey();
fetch((typeof appPath === 'function' ? appPath('/Admin/api/player-coins.php') : '/Admin/api/player-coins.php') + '?playerKey=' + encodeURIComponent(key), { credentials: 'omit' })
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d || !d.ok) return;
var c = String(Math.max(0, parseInt(d.coins, 10) || 0));
try {
localStorage.setItem('jdCoins', c);
} catch (e) { /* ignore */ }
var coinsEl = document.getElementById('lobby-profile-coins');
if (coinsEl) coinsEl.textContent = c;
})
.catch(function () { /* offline */ });
}
/** รีเฟรชหลังเลือกตัวละคร / สลับแท็บ / ย้อนกลับบน tablet (bfcache) */
function syncMainLobbyCharacterUi() {
applyProfileTexts();
syncCoinsFromServer();
function applyCharAssets(urlOrNull) {
applyProfileAvatar(urlOrNull);
applyCenterCharacterResolved(urlOrNull);
}
resolveLobbyCharacterId(function (id) {
if (!id) {
applyCharAssets(null);
return;
}
var idleCached = getStoredLobbyIdleDownDataUrl(id);
if (idleCached) {
applyCharAssets(idleCached);
return;
}
characterLobbySpriteFirstLiveUrl(id, applyCharAssets);
});
}
function applyProfileTexts() {
var name = (localStorage.getItem('playerName') || '').trim() || 'MONE';
var coins = localStorage.getItem('jdCoins') || '0';
var nameEl = document.getElementById('lobby-profile-name');
var agentEl = document.getElementById('lobby-profile-agent-id');
var coinsEl = document.getElementById('lobby-profile-coins');
if (nameEl) nameEl.textContent = name.toUpperCase();
if (agentEl) agentEl.textContent = ensureAgentId();
if (coinsEl) coinsEl.textContent = coins;
}
/** @param {string | null} urlOrNull — URL ที่ HEAD ผ่านแล้ว หรือ null = ใช้ char-main */
function applyProfileAvatar(urlOrNull) {
var av = document.getElementById('lobby-profile-avatar');
if (!av) return;
var fb = mainMenuCharFallbackUrl();
av.onerror = null;
if (!urlOrNull) {
av.src = fb;
return;
}
av.onerror = function () {
av.onerror = null;
av.src = fb;
};
av.src = urlOrNull;
}
function applyCenterCharacterResolved(urlOrNull) {
var centerEl = document.getElementById('lobby-character-img');
if (!centerEl) return;
function showSceneOnly() {
centerEl.classList.remove('lobby-character-img--visible');
centerEl.removeAttribute('src');
}
function revealChar() {
centerEl.classList.add('lobby-character-img--visible');
}
function showFallbackCenterCharacter() {
var fb = mainMenuCharFallbackUrl();
centerEl.onload = revealChar;
centerEl.onerror = function () {
centerEl.onerror = null;
showSceneOnly();
};
centerEl.src = fb;
if (centerEl.complete && centerEl.naturalWidth > 0) {
revealChar();
}
}
function showWithChar(url) {
centerEl.onload = revealChar;
centerEl.onerror = function () {
showFallbackCenterCharacter();
};
centerEl.src = url;
if (centerEl.complete && centerEl.naturalWidth > 0) {
revealChar();
}
}
if (!urlOrNull) {
showFallbackCenterCharacter();
return;
}
showWithChar(urlOrNull);
}
// คำนวณ offset จากความสูงจอด้วย linear interpolation:
// 1080 -> 18cqh, 650 -> 25cqh
function applyCharacterFootOffsetByViewport() {
var root = document.documentElement;
if (!root) return;
var h = Math.max(1, window.innerHeight || 0);
var h1 = 650;
var y1 = 25;
var h2 = 1080;
var y2 = 18;
var t = (h - h1) / (h2 - h1);
var cqhValue = y1 + ((y2 - y1) * t);
// กันหลุดช่วงเมื่อจอเล็ก/ใหญ่เกินช่วงอ้างอิง
cqhValue = Math.max(18, Math.min(25, cqhValue));
root.style.setProperty('--lobby-character-foot-offset', cqhValue.toFixed(3) + 'cqh');
}
function syncLeaderboardScrollbarThumb() {
var scrollEl = document.querySelector('.lobby-leaderboard-scroll');
var thumbEl = document.getElementById('lobby-leaderboard-scroll-thumb');
if (!scrollEl || !thumbEl) return;
var styles = window.getComputedStyle(thumbEl);
var topInset = parseFloat(styles.top) || 0;
var minThumb = parseFloat(styles.minHeight) || 18;
var maxScroll = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight);
if (maxScroll <= 0) {
thumbEl.style.display = 'none';
return;
}
thumbEl.style.display = 'block';
var trackHeight = scrollEl.clientHeight;
var visibleRatio = scrollEl.clientHeight / Math.max(1, scrollEl.scrollHeight);
var thumbHeight = Math.max(minThumb, Math.round(trackHeight * visibleRatio));
var maxY = Math.max(0, trackHeight - topInset - thumbHeight);
var y = topInset + (maxY * (scrollEl.scrollTop / maxScroll));
thumbEl.style.height = thumbHeight + 'px';
thumbEl.style.transform = 'translateY(' + y.toFixed(2) + 'px)';
}
// วาง leaderboard ให้ต่อจากปุ่ม daily โดยไม่ทับกัน
function placeLeaderboardBelowDaily() {
var dailyBtn = document.getElementById('btn-daily');
var leaderboard = document.querySelector('.lobby-leaderboard');
var lobbyUi = document.querySelector('.lobby-ui');
if (!dailyBtn || !leaderboard || !lobbyUi) return;
var boardStyle = window.getComputedStyle(leaderboard);
if (boardStyle.position !== 'absolute') {
leaderboard.style.top = '';
return;
}
var uiRect = lobbyUi.getBoundingClientRect();
var dailyRect = dailyBtn.getBoundingClientRect();
var boardRect = leaderboard.getBoundingClientRect();
var cssGap = boardStyle.getPropertyValue('--lobby-daily-leaderboard-gap').trim();
var gapPx = 8;
if (cssGap) {
var n = parseFloat(cssGap);
if (!Number.isNaN(n)) {
if (cssGap.endsWith('cqh')) gapPx = (uiRect.height * n) / 100;
else if (cssGap.endsWith('px') || /^[+-]?\d+(\.\d+)?$/.test(cssGap)) gapPx = n;
else if (cssGap.endsWith('vh')) gapPx = ((window.innerHeight || 0) * n) / 100;
else if (cssGap.endsWith('%')) gapPx = (uiRect.height * n) / 100;
}
}
var nextTop = (dailyRect.bottom - uiRect.top) + gapPx;
var maxTop = Math.max(0, uiRect.height - boardRect.height - Math.max(0, gapPx));
var clampedTop = Math.max(0, Math.min(nextTop, maxTop));
leaderboard.style.top = clampedTop.toFixed(2) + 'px';
}
var leaderboardLayoutRaf = 0;
function scheduleLeaderboardPlacement() {
if (leaderboardLayoutRaf) cancelAnimationFrame(leaderboardLayoutRaf);
leaderboardLayoutRaf = requestAnimationFrame(function () {
leaderboardLayoutRaf = 0;
placeLeaderboardBelowDaily();
syncLeaderboardScrollbarThumb();
});
}
function bindLeaderboardScrollSync() {
var scrollEl = document.querySelector('.lobby-leaderboard-scroll');
if (!scrollEl || scrollEl.dataset.scrollSyncBound === '1') return;
scrollEl.addEventListener('scroll', syncLeaderboardScrollbarThumb, { passive: true });
scrollEl.dataset.scrollSyncBound = '1';
}
var guideStep = 1;
var guideImg = document.getElementById('guide-img');
var guideBar = document.getElementById('lobby-guide-bar');
var guideDim = document.getElementById('lobby-guide-dim');
var guideFocus = document.getElementById('lobby-guide-focus');
function getGuideStepConfig(step) {
return GUIDE_STEPS[step - 1] || { position: 'bottom', highlight: null, pad: 10 };
}
function hideGuideFocus() {
if (!guideFocus) return;
guideFocus.classList.add('hidden');
guideFocus.style.left = '0';
guideFocus.style.top = '0';
guideFocus.style.width = '0';
guideFocus.style.height = '0';
}
function scheduleGuideFocusUpdate() {
requestAnimationFrame(function () {
requestAnimationFrame(updateGuideFocus);
});
}
function updateGuideFocus() {
if (!guideFocus || !guideBar || guideBar.classList.contains('hidden')) {
hideGuideFocus();
return;
}
var cfg = getGuideStepConfig(guideStep);
if (!cfg.highlight) {
hideGuideFocus();
return;
}
var target = document.querySelector(cfg.highlight);
if (!target) {
hideGuideFocus();
return;
}
var pad = typeof cfg.pad === 'number' ? cfg.pad : 8;
var r = target.getBoundingClientRect();
if (r.width < 4 || r.height < 4) {
hideGuideFocus();
return;
}
var cs = window.getComputedStyle(target);
var br = parseInt(cs.borderRadius, 10) || 0;
var radius = Math.max(br + 4, 14);
guideFocus.classList.remove('hidden');
guideFocus.style.left = (r.left - pad) + 'px';
guideFocus.style.top = (r.top - pad) + 'px';
guideFocus.style.width = (r.width + pad * 2) + 'px';
guideFocus.style.height = (r.height + pad * 2) + 'px';
guideFocus.style.borderRadius = radius + 'px';
}
function syncGuideBodyClass() {
if (!guideBar) return;
var open = !guideBar.classList.contains('hidden');
if (open) {
document.body.classList.add('lobby-guide-active');
var cfg = getGuideStepConfig(guideStep);
document.body.classList.toggle('lobby-guide-top-step', cfg.position === 'top');
var hasFocus = !!cfg.highlight;
if (guideDim) guideDim.classList.toggle('hidden', hasFocus);
} else {
document.body.classList.remove('lobby-guide-active');
document.body.classList.remove('lobby-guide-top-step');
if (guideDim) guideDim.classList.add('hidden');
}
if (open) {
scheduleGuideFocusUpdate();
} else {
hideGuideFocus();
}
}
function showGuideStep(n) {
if (!guideImg || !guideBar) return;
if (n < 1 || n > MAX_GUIDE) {
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
hideGuideFocus();
try {
localStorage.setItem(GUIDE_KEY, '1');
} catch (e) { /* ignore */ }
syncGuideBodyClass();
return;
}
guideStep = n;
var cfg = getGuideStepConfig(guideStep);
guideImg.src = ML_GUIDE + '/guide-' + (n < 10 ? '0' + n : String(n)) + '.png';
guideImg.alt = 'คำแนะนำ ขั้นที่ ' + n;
guideBar.classList.toggle('lobby-guide-bar--top', cfg.position === 'top');
var nextBtn = document.getElementById('btn-guide-next');
if (nextBtn) {
nextBtn.setAttribute('aria-label', n >= MAX_GUIDE ? 'รับทราบ' : 'ถัดไป');
}
syncGuideBodyClass();
guideImg.onload = function () {
scheduleGuideFocusUpdate();
};
guideImg.onerror = function () {
scheduleGuideFocusUpdate();
};
scheduleGuideFocusUpdate();
}
function initGuide() {
if (!guideBar || !guideImg) return;
try {
if (localStorage.getItem(GUIDE_KEY) === '1') {
guideBar.classList.add('hidden');
syncGuideBodyClass();
return;
}
} catch (e) { /* ignore */ }
guideBar.classList.remove('hidden');
showGuideStep(1);
}
document.getElementById('btn-guide-next')?.addEventListener('click', function () {
if (guideStep >= MAX_GUIDE) {
if (guideBar) {
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
}
try {
localStorage.setItem(GUIDE_KEY, '1');
} catch (e) { /* ignore */ }
syncGuideBodyClass();
return;
}
showGuideStep(guideStep + 1);
});
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
if (!guideBar || guideBar.classList.contains('hidden')) return;
e.preventDefault();
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
hideGuideFocus();
syncGuideBodyClass();
});
window.addEventListener('resize', function () {
if (guideBar && !guideBar.classList.contains('hidden')) {
scheduleGuideFocusUpdate();
}
});
window.addEventListener('orientationchange', function () {
setTimeout(scheduleGuideFocusUpdate, 350);
});
/* ใช้ capture + data-href บน .lobby-footer-center — ลดโอกาสถูกเลเยอร์อื่นแย่งคลิกก่อนถึงปุ่ม */
var footerCenter = document.querySelector('.lobby-footer-center');
if (footerCenter) {
footerCenter.addEventListener('click', function (e) {
var btn = e.target.closest('button[data-href]');
if (!btn || !footerCenter.contains(btn)) return;
var href = btn.getAttribute('data-href');
if (!href) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
window.location.href = href;
}, true);
}
document.getElementById('btn-cloth')?.addEventListener('click', function () {
openLobbyCustomize();
});
document.getElementById('btn-daily')?.addEventListener('click', function () {
toast('รางวัลประจำวัน — เข้าเกมทุกวันเพื่อรับรางวัลต่อเนื่อง!');
});
document.getElementById('btn-myprofile')?.addEventListener('click', function () {
window.location.href = typeof appPath === 'function' ? appPath('/Main-Menu/') : '/Main-Menu/';
});
function getMlAiSessionId() {
try {
var k = 'mlAiChatSession';
var v = localStorage.getItem(k);
if (!v) {
v = 'ml-' + Date.now() + '-' + Math.random().toString(36).slice(2, 11);
localStorage.setItem(k, v);
}
return v;
} catch (e) {
return 'ml-' + String(Date.now());
}
}
var aiChatCloseBtn = document.getElementById('lobby-ai-chat-close-btn');
var aiChatPanel = document.getElementById('lobby-ai-chat-panel');
var aiChatToggleImg = document.getElementById('lobby-ai-chat-toggle-img');
var btnAiChat = document.getElementById('btn-ai-chat');
function syncAiChatPanelUi() {
if (!aiChatPanel) return;
var hidden = aiChatPanel.classList.contains('lobby-ai-chat-hidden');
if (btnAiChat) btnAiChat.style.display = hidden ? '' : 'none';
if (hidden || !aiChatToggleImg || !aiChatCloseBtn) return;
aiChatToggleImg.src = BASE + '/img/chat-close-btn.png';
aiChatToggleImg.alt = 'ปิด';
aiChatCloseBtn.setAttribute('title', 'ปิดแชท AI');
aiChatCloseBtn.setAttribute('aria-label', 'ปิดแชท AI');
}
if (aiChatCloseBtn && aiChatPanel) {
aiChatCloseBtn.addEventListener('click', function () {
aiChatPanel.classList.add('lobby-ai-chat-hidden');
aiChatPanel.classList.remove('chat-panel-collapsed');
syncAiChatPanelUi();
});
}
if (btnAiChat && aiChatPanel) {
btnAiChat.addEventListener('click', function () {
if (aiChatPanel.classList.contains('lobby-ai-chat-hidden')) {
aiChatPanel.classList.remove('lobby-ai-chat-hidden');
aiChatPanel.classList.remove('chat-panel-collapsed');
syncAiChatPanelUi();
var inp = document.getElementById('lobby-ai-chat-input');
if (inp) {
try {
inp.focus();
} catch (e) { /* ignore */ }
}
} else {
aiChatPanel.classList.add('lobby-ai-chat-hidden');
syncAiChatPanelUi();
}
});
}
syncAiChatPanelUi();
var aiChatForm = document.getElementById('lobby-ai-chat-form');
var aiChatInput = document.getElementById('lobby-ai-chat-input');
var aiChatSendBtn = aiChatForm ? aiChatForm.querySelector('button[type="submit"]') : null;
var aiChatLoadingEl = null;
function appendAiMessage(text, isAi) {
var el = document.getElementById('lobby-ai-chat-messages');
if (!el) return;
if (isAi) removeAiChatLoading();
var div = document.createElement('div');
div.className = 'lobby-ai-chat-msg ' + (isAi ? 'lobby-ai-chat-msg-ai' : 'lobby-ai-chat-msg-user');
div.textContent = (isAi ? 'AI: ' : '') + text;
el.appendChild(div);
el.scrollTop = 1e9;
}
function showAiChatLoading() {
var el = document.getElementById('lobby-ai-chat-messages');
if (!el || aiChatLoadingEl) return;
aiChatLoadingEl = document.createElement('div');
aiChatLoadingEl.className = 'lobby-ai-chat-msg lobby-ai-chat-msg-ai lobby-ai-chat-typing';
aiChatLoadingEl.setAttribute('aria-label', 'กำลังพิมพ์');
aiChatLoadingEl.innerHTML = '<span class="lobby-ai-typing-dot"></span><span class="lobby-ai-typing-dot"></span><span class="lobby-ai-typing-dot"></span>';
el.appendChild(aiChatLoadingEl);
el.scrollTop = 1e9;
}
function removeAiChatLoading() {
if (aiChatLoadingEl && aiChatLoadingEl.parentNode) {
aiChatLoadingEl.parentNode.removeChild(aiChatLoadingEl);
aiChatLoadingEl = null;
}
}
function setAiChatWaiting(waiting) {
if (aiChatInput) aiChatInput.disabled = waiting;
if (aiChatSendBtn) aiChatSendBtn.disabled = waiting;
if (waiting) showAiChatLoading();
else removeAiChatLoading();
}
if (aiChatForm && aiChatInput) {
aiChatForm.addEventListener('submit', function (e) {
e.preventDefault();
var text = (aiChatInput.value || '').trim();
if (!text) return;
appendAiMessage(text, false);
setAiChatWaiting(true);
aiChatInput.value = '';
fetch(SERVER + '/api/ai-chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text, sessionId: getMlAiSessionId() }),
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.response != null) appendAiMessage(data.response, true);
else if (data.error) appendAiMessage('[ข้อผิดพลาด: ' + data.error + ']', true);
})
.catch(function () {
appendAiMessage('[ส่งไม่สำเร็จ]', true);
})
.finally(function () {
setAiChatWaiting(false);
});
});
}
var howto = document.getElementById('lobby-howto-overlay');
function openHowto() {
if (howto) howto.classList.remove('hidden');
}
function closeHowto() {
if (howto) howto.classList.add('hidden');
}
/** ปุ่ม ? — คู่มือทีละขั้น ตาม /Main-Lobby/IMAGE/guide */
function openGuideTutorial() {
if (!guideBar || !guideImg) return;
closeHowto();
guideBar.classList.remove('hidden');
showGuideStep(1);
}
document.getElementById('btn-tutorial')?.addEventListener('click', openGuideTutorial);
document.getElementById('btn-howto-got-it')?.addEventListener('click', closeHowto);
howto?.addEventListener('click', function (e) {
if (e.target === howto) closeHowto();
});
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
initGuide();
window.addEventListener('pageshow', function () {
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
scheduleLeaderboardPlacement();
});
window.addEventListener('resize', function () {
applyCharacterFootOffsetByViewport();
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
});
window.addEventListener('orientationchange', function () {
setTimeout(function () {
applyCharacterFootOffsetByViewport();
scheduleLeaderboardPlacement();
}, 250);
});
window.addEventListener('load', function () {
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
});
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') {
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
scheduleLeaderboardPlacement();
}
});
window.addEventListener('storage', function (e) {
if (e.key == null || e.key === CHAR_KEY || e.key === 'playerName') {
syncMainLobbyCharacterUi();
return;
}
if (e.key && String(e.key).indexOf(LOBBY_IDLE_DOWN_PREFIX) === 0) {
syncMainLobbyCharacterUi();
}
});
/* ===== ห้องแต่งตัว (Customize popup) — Phase 1: UI + เลือกธีมสี/สีผิว/ใบหน้า (เก็บ localStorage) ===== */
var CUSTOMIZE_ASSET = (typeof appPath === 'function' ? appPath('/Game') : '/Game') + '/img/03-5-Customize/';
var customizeOverlay = document.getElementById('lobby-customize-overlay');
var CUSTOMIZE_ITEMS = {
face: [
{ idx: 1, price: 0 }, { idx: 2, price: 0 }, { idx: 3, price: 50 }, { idx: 4, price: 50 },
{ idx: 5, price: 50 }, { idx: 6, price: 100 }, { idx: 7, price: 100 }, { idx: 8, price: 200 }
],
hair: [],
cloth: []
};
function lobbyMakeSwatch(group, idx, label) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'lobby-customize-swatch';
btn.setAttribute('data-idx', String(idx));
btn.setAttribute('aria-label', label);
var img = document.createElement('img');
img.src = CUSTOMIZE_ASSET + (group === 'color' ? 'color-' : 'skin-tone-') + idx + '.png';
img.alt = '';
img.decoding = 'async';
btn.appendChild(img);
btn.addEventListener('click', function () { lobbySelectSwatch(group, idx); });
return btn;
}
function lobbySelectSwatch(group, idx) {
var wrap = document.getElementById(group === 'color' ? 'lobby-theme-colors' : 'lobby-skin-tones');
if (!wrap) return;
[].forEach.call(wrap.children, function (c) {
c.classList.toggle('is-selected', c.getAttribute('data-idx') === String(idx));
});
try { localStorage.setItem(group === 'color' ? 'lobbyThemeColor' : 'lobbySkinTone', String(idx)); } catch (e) {}
}
function lobbyBuildSwatches() {
var colorWrap = document.getElementById('lobby-theme-colors');
var skinWrap = document.getElementById('lobby-skin-tones');
if (colorWrap && !colorWrap.childElementCount) {
for (var i = 1; i <= 8; i++) colorWrap.appendChild(lobbyMakeSwatch('color', i, 'ธีมสี ' + i));
}
if (skinWrap && !skinWrap.childElementCount) {
for (var j = 1; j <= 3; j++) skinWrap.appendChild(lobbyMakeSwatch('skin', j, 'สีผิว ' + j));
}
}
function lobbyRenderCustomizeItems(tab) {
var grid = document.getElementById('lobby-customize-items');
if (!grid) return;
grid.innerHTML = '';
var items = CUSTOMIZE_ITEMS[tab] || [];
if (!items.length) {
var empty = document.createElement('div');
empty.className = 'lobby-customize-empty';
empty.textContent = 'ยังไม่เปิดให้บริการ';
grid.appendChild(empty);
return;
}
var savedKey = 'lobbyItem_' + tab;
var saved = '';
try { saved = localStorage.getItem(savedKey) || ''; } catch (e) {}
items.forEach(function (it) {
var cell = document.createElement('button');
cell.type = 'button';
cell.className = 'lobby-customize-item' + (String(it.idx) === saved ? ' is-selected' : '');
cell.setAttribute('data-idx', String(it.idx));
var img = document.createElement('img');
img.className = 'lobby-customize-item-img';
img.src = CUSTOMIZE_ASSET + tab + '-' + it.idx + '.png';
img.alt = '';
img.decoding = 'async';
cell.appendChild(img);
if (it.price > 0) {
var price = document.createElement('span');
price.className = 'lobby-customize-item-price';
price.textContent = String(it.price);
cell.appendChild(price);
}
cell.addEventListener('click', function () {
[].forEach.call(grid.children, function (c) { c.classList.remove('is-selected'); });
cell.classList.add('is-selected');
try { localStorage.setItem(savedKey, String(it.idx)); } catch (e) {}
});
grid.appendChild(cell);
});
}
function lobbySetCustomizeTab(tab) {
[].forEach.call(document.querySelectorAll('.lobby-customize-tab'), function (t) {
var on = t.getAttribute('data-tab') === tab;
t.classList.toggle('is-active', on);
t.setAttribute('aria-selected', on ? 'true' : 'false');
});
lobbyRenderCustomizeItems(tab);
}
function openLobbyCustomize() {
if (!customizeOverlay) {
window.location.href = BASE + '/character.html';
return;
}
lobbyBuildSwatches();
try {
var c = localStorage.getItem('lobbyThemeColor'); if (c) lobbySelectSwatch('color', c);
var s = localStorage.getItem('lobbySkinTone'); if (s) lobbySelectSwatch('skin', s);
} catch (e) {}
lobbySetCustomizeTab('face');
customizeOverlay.classList.remove('hidden');
customizeOverlay.setAttribute('aria-hidden', 'false');
}
function closeLobbyCustomize() {
if (!customizeOverlay) return;
customizeOverlay.classList.add('hidden');
customizeOverlay.setAttribute('aria-hidden', 'true');
}
function setupLobbyCustomize() {
if (!customizeOverlay) return;
document.getElementById('lobby-customize-close')?.addEventListener('click', closeLobbyCustomize);
document.getElementById('lobby-customize-backdrop')?.addEventListener('click', closeLobbyCustomize);
document.getElementById('lobby-customize-confirm')?.addEventListener('click', function () {
closeLobbyCustomize();
toast('บันทึกการแต่งตัวแล้ว');
});
[].forEach.call(document.querySelectorAll('.lobby-customize-tab'), function (t) {
t.addEventListener('click', function () { lobbySetCustomizeTab(t.getAttribute('data-tab')); });
});
}
setupLobbyCustomize();
})();
@@ -0,0 +1,955 @@
(function () {
'use strict';
var BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
var SERVER = (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : '') + '/Game';
var CHAR_KEY = 'gameCharacterId';
/** รูป composite idle ทิศ down จากหน้า Character (preview-idle-layer-down) — ตรงกับ character.js */
var LOBBY_IDLE_DOWN_PREFIX = 'jdCharLobbyIdleDown:';
function getStoredLobbyIdleDownDataUrl(safeId) {
if (!safeId) return '';
try {
var v = localStorage.getItem(LOBBY_IDLE_DOWN_PREFIX + safeId) || '';
if (typeof v === 'string' && v.indexOf('data:image/') === 0 && v.length > 80) return v;
} catch (e) { /* ignore */ }
return '';
}
var GUIDE_KEY = 'mainLobbyGuideDone';
var MAX_GUIDE = 5;
var ML_GUIDE = typeof appPath === 'function' ? appPath('/Main-Lobby/IMAGE/guide') : '/Main-Lobby/IMAGE/guide';
/**
* ตำแหน่งแถบคำแนะนำ + จุดไฮไลต์ต่อ guide-01…05 (ปรับลำดับให้ตรงไฟล์รูป)
* position: 'bottom' | 'top' — ตาม mock แถบล่าง/บน
* highlight: selector ของปุ่มใน lobby (null = ไม่วงกรอบ)
*/
var GUIDE_STEPS = [
{ position: 'bottom', highlight: null, pad: 10 },
{ position: 'top', highlight: '.lobby-footer-center', pad: 10 },
{ position: 'top', highlight: '#btn-ai-chat', pad: 8 },
{ position: 'bottom', highlight: '#btn-cloth', pad: 10 },
{ position: 'bottom', highlight: '#btn-daily', pad: 10 },
];
function checkLogin() {
if (localStorage.getItem('isLoggedIn') !== 'true') {
window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/';
return false;
}
return true;
}
if (!checkLogin()) return;
function toast(msg) {
var el = document.getElementById('lobby-toast');
if (!el) return;
el.textContent = msg;
el.classList.add('lobby-toast--show');
clearTimeout(toast._t);
toast._t = setTimeout(function () {
el.classList.remove('lobby-toast--show');
}, 2600);
}
function padAgent(n) {
var s = String(n);
while (s.length < 6) s = '0' + s;
return s;
}
function ensureAgentId() {
var k = 'agentDisplayId';
var v = localStorage.getItem(k);
if (!v || !/^\d{6}$/.test(v)) {
v = padAgent(100000 + Math.floor(Math.random() * 899999));
try {
localStorage.setItem(k, v);
} catch (e) { /* ignore */ }
}
return v;
}
function getSelectedCharacterId() {
try {
return (localStorage.getItem(CHAR_KEY) || '').trim();
} catch (e) {
return '';
}
}
/** ใช้เฉพาะ id ที่ปลอดเป็น segment ใน URL — กันพาธแปลก/อักขระที่เซิร์ฟไม่มีไฟล์ */
function normalizeCharacterAssetId(id) {
var s = String(id == null ? '' : id).trim();
if (!s) return '';
if (s === 'Chatest') return ''; // legacy placeholder — ไม่มี sprite จริง ให้ถือเป็นว่าง
if (s.length > 96) s = s.slice(0, 96);
if (!/^[a-zA-Z0-9._-]+$/.test(s)) return '';
return s;
}
function mainMenuCharFallbackUrl() {
return typeof appPath === 'function' ? appPath('/Main-Menu/char-main.png') : '/Main-Menu/char-main.png';
}
/**
* URL รูปโลบี้ — ลองทิศ down ก่อน (idle แล้ว walk) เพื่อไม่ยิง _up_idle_ ฯลฯ โดยไม่จำเป็น
* ถ้า down ไม่มีค่อยลอง up / left / right
* @param {string} safeId
* @param {boolean} preferIdle
*/
function characterSpriteCandidateUrls(safeId, preferIdle) {
if (!safeId) return [];
var enc = encodeURIComponent(safeId);
var q = '?ch=' + encodeURIComponent(safeId);
var dirsPrimary = ['down'];
var dirsFallback = ['up', 'left', 'right'];
var wantIdle = preferIdle !== false;
var urls = [];
function pushForDir(d) {
if (wantIdle) {
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_idle.png' + q);
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_idle_0.png' + q);
}
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '.png' + q);
urls.push(SERVER + '/img/characters/' + enc + '_' + d + '_0.png' + q);
}
for (var a = 0; a < dirsPrimary.length; a++) pushForDir(dirsPrimary[a]);
for (var b = 0; b < dirsFallback.length; b++) pushForDir(dirsFallback[b]);
return urls;
}
/** หลังลองครบแล้วไม่มีรูป — ลดการยิงซ้ำในเซสชัน (ไม่บล็อกการลองใหม่หลังอัปโหลดไฟล์แล้ว — ลบคีย์ตอนโหลดสำเร็จ) */
var CHAR_ASSET_MISS_PREFIX = 'jdCharAssetMissing:';
function markCharacterDownAssetMissing(safeId) {
if (!safeId) return;
try {
sessionStorage.setItem(CHAR_ASSET_MISS_PREFIX + safeId, '1');
} catch (e) { /* ignore */ }
}
function clearCharacterDownAssetMissing(safeId) {
if (!safeId) return;
try {
sessionStorage.removeItem(CHAR_ASSET_MISS_PREFIX + safeId);
} catch (e) { /* ignore */ }
}
/**
* ลอง URL ตามลำดับด้วย Image — ตรงกับพฤติกรรม <img> ใน character-grid
* @param {string[]} urls
* @param {(url: string | null) => void} cb
*/
function probeFirstLoadableImageUrl(urls, cb) {
var i = 0;
function next() {
if (i >= urls.length) {
cb(null);
return;
}
var u = urls[i++];
var img = new Image();
img.onload = function () {
if (img.naturalWidth > 0) cb(u);
else next();
};
img.onerror = function () {
next();
};
img.src = u;
}
next();
}
/**
* @param {string} safeId
* @param {(url: string | null) => void} cb
*/
function characterLobbySpriteFirstLiveUrl(safeId, cb) {
if (!safeId) {
cb(null);
return;
}
var urls = characterSpriteCandidateUrls(safeId, true);
if (!urls.length) {
cb(null);
return;
}
probeFirstLoadableImageUrl(urls, function (url) {
if (url) {
clearCharacterDownAssetMissing(safeId);
cb(url);
} else {
markCharacterDownAssetMissing(safeId);
cb(null);
}
});
}
/**
* id จาก localStorage หรือถ้าว่าง — ตัวสุดท้ายในรายการ API (ใหม่สุดตามลำดับเซิร์ฟ)
* @param {(safeId: string) => void} callback
*/
function resolveLobbyCharacterId(callback) {
var norm = normalizeCharacterAssetId(getSelectedCharacterId());
if (norm) {
callback(norm);
return;
}
fetch(BASE + '/api/characters', { credentials: 'same-origin', cache: 'no-store' })
.then(function (r) { return r.json(); })
.then(function (list) {
if (!Array.isArray(list) || !list.length) {
callback('');
return;
}
var last = list[list.length - 1];
var id = last && last.id ? normalizeCharacterAssetId(String(last.id)) : '';
callback(id || '');
})
.catch(function () {
callback('');
});
}
var PLAYER_KEY = 'jdPlayerKey';
function ensurePlayerKey() {
var k;
try {
k = localStorage.getItem(PLAYER_KEY);
} catch (e) {
k = '';
}
if (!k || String(k).length < 8) {
k = 'p_' + Date.now() + '_' + Math.random().toString(36).slice(2, 14);
try {
localStorage.setItem(PLAYER_KEY, k);
} catch (e2) { /* ignore */ }
}
return k;
}
/** ดึง COINS จากเซิร์ฟ (บัญชี guest สร้างอัตโนมัติถ้ายังไม่มี) */
function syncCoinsFromServer() {
var key = ensurePlayerKey();
fetch((typeof appPath === 'function' ? appPath('/Admin/api/player-coins.php') : '/Admin/api/player-coins.php') + '?playerKey=' + encodeURIComponent(key), { credentials: 'omit' })
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d || !d.ok) return;
var c = String(Math.max(0, parseInt(d.coins, 10) || 0));
try {
localStorage.setItem('jdCoins', c);
} catch (e) { /* ignore */ }
var coinsEl = document.getElementById('lobby-profile-coins');
if (coinsEl) coinsEl.textContent = c;
})
.catch(function () { /* offline */ });
}
/** รีเฟรชหลังเลือกตัวละคร / สลับแท็บ / ย้อนกลับบน tablet (bfcache) */
function syncMainLobbyCharacterUi() {
applyProfileTexts();
syncCoinsFromServer();
function applyCharAssets(urlOrNull) {
applyProfileAvatar(urlOrNull);
applyCenterCharacterResolved(urlOrNull);
}
resolveLobbyCharacterId(function (id) {
if (!id) {
applyCharAssets(null);
return;
}
var idleCached = getStoredLobbyIdleDownDataUrl(id);
if (idleCached) {
applyCharAssets(idleCached);
return;
}
characterLobbySpriteFirstLiveUrl(id, applyCharAssets);
});
}
function applyProfileTexts() {
var name = (localStorage.getItem('playerName') || '').trim() || 'MONE';
var coins = localStorage.getItem('jdCoins') || '0';
var nameEl = document.getElementById('lobby-profile-name');
var agentEl = document.getElementById('lobby-profile-agent-id');
var coinsEl = document.getElementById('lobby-profile-coins');
if (nameEl) nameEl.textContent = name.toUpperCase();
if (agentEl) agentEl.textContent = ensureAgentId();
if (coinsEl) coinsEl.textContent = coins;
}
/** @param {string | null} urlOrNull — URL ที่ HEAD ผ่านแล้ว หรือ null = ใช้ char-main */
function applyProfileAvatar(urlOrNull) {
var av = document.getElementById('lobby-profile-avatar');
if (!av) return;
var fb = mainMenuCharFallbackUrl();
av.onerror = null;
if (!urlOrNull) {
av.src = fb;
return;
}
av.onerror = function () {
av.onerror = null;
av.src = fb;
};
av.src = urlOrNull;
}
function applyCenterCharacterResolved(urlOrNull) {
var centerEl = document.getElementById('lobby-character-img');
if (!centerEl) return;
function showSceneOnly() {
centerEl.classList.remove('lobby-character-img--visible');
centerEl.removeAttribute('src');
}
function revealChar() {
centerEl.classList.add('lobby-character-img--visible');
}
function showFallbackCenterCharacter() {
var fb = mainMenuCharFallbackUrl();
centerEl.onload = revealChar;
centerEl.onerror = function () {
centerEl.onerror = null;
showSceneOnly();
};
centerEl.src = fb;
if (centerEl.complete && centerEl.naturalWidth > 0) {
revealChar();
}
}
function showWithChar(url) {
centerEl.onload = revealChar;
centerEl.onerror = function () {
showFallbackCenterCharacter();
};
centerEl.src = url;
if (centerEl.complete && centerEl.naturalWidth > 0) {
revealChar();
}
}
if (!urlOrNull) {
showFallbackCenterCharacter();
return;
}
showWithChar(urlOrNull);
}
// คำนวณ offset จากความสูงจอด้วย linear interpolation:
// 1080 -> 18cqh, 650 -> 25cqh
function applyCharacterFootOffsetByViewport() {
var root = document.documentElement;
if (!root) return;
var h = Math.max(1, window.innerHeight || 0);
var h1 = 650;
var y1 = 25;
var h2 = 1080;
var y2 = 18;
var t = (h - h1) / (h2 - h1);
var cqhValue = y1 + ((y2 - y1) * t);
// กันหลุดช่วงเมื่อจอเล็ก/ใหญ่เกินช่วงอ้างอิง
cqhValue = Math.max(18, Math.min(25, cqhValue));
root.style.setProperty('--lobby-character-foot-offset', cqhValue.toFixed(3) + 'cqh');
}
function syncLeaderboardScrollbarThumb() {
var scrollEl = document.querySelector('.lobby-leaderboard-scroll');
var thumbEl = document.getElementById('lobby-leaderboard-scroll-thumb');
if (!scrollEl || !thumbEl) return;
var styles = window.getComputedStyle(thumbEl);
var topInset = parseFloat(styles.top) || 0;
var minThumb = parseFloat(styles.minHeight) || 18;
var maxScroll = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight);
if (maxScroll <= 0) {
thumbEl.style.display = 'none';
return;
}
thumbEl.style.display = 'block';
var trackHeight = scrollEl.clientHeight;
var visibleRatio = scrollEl.clientHeight / Math.max(1, scrollEl.scrollHeight);
var thumbHeight = Math.max(minThumb, Math.round(trackHeight * visibleRatio));
var maxY = Math.max(0, trackHeight - topInset - thumbHeight);
var y = topInset + (maxY * (scrollEl.scrollTop / maxScroll));
thumbEl.style.height = thumbHeight + 'px';
thumbEl.style.transform = 'translateY(' + y.toFixed(2) + 'px)';
}
// วาง leaderboard ให้ต่อจากปุ่ม daily โดยไม่ทับกัน
function placeLeaderboardBelowDaily() {
var dailyBtn = document.getElementById('btn-daily');
var leaderboard = document.querySelector('.lobby-leaderboard');
var lobbyUi = document.querySelector('.lobby-ui');
if (!dailyBtn || !leaderboard || !lobbyUi) return;
var boardStyle = window.getComputedStyle(leaderboard);
if (boardStyle.position !== 'absolute') {
leaderboard.style.top = '';
return;
}
var uiRect = lobbyUi.getBoundingClientRect();
var dailyRect = dailyBtn.getBoundingClientRect();
var boardRect = leaderboard.getBoundingClientRect();
var cssGap = boardStyle.getPropertyValue('--lobby-daily-leaderboard-gap').trim();
var gapPx = 8;
if (cssGap) {
var n = parseFloat(cssGap);
if (!Number.isNaN(n)) {
if (cssGap.endsWith('cqh')) gapPx = (uiRect.height * n) / 100;
else if (cssGap.endsWith('px') || /^[+-]?\d+(\.\d+)?$/.test(cssGap)) gapPx = n;
else if (cssGap.endsWith('vh')) gapPx = ((window.innerHeight || 0) * n) / 100;
else if (cssGap.endsWith('%')) gapPx = (uiRect.height * n) / 100;
}
}
var nextTop = (dailyRect.bottom - uiRect.top) + gapPx;
var maxTop = Math.max(0, uiRect.height - boardRect.height - Math.max(0, gapPx));
var clampedTop = Math.max(0, Math.min(nextTop, maxTop));
leaderboard.style.top = clampedTop.toFixed(2) + 'px';
}
var leaderboardLayoutRaf = 0;
function scheduleLeaderboardPlacement() {
if (leaderboardLayoutRaf) cancelAnimationFrame(leaderboardLayoutRaf);
leaderboardLayoutRaf = requestAnimationFrame(function () {
leaderboardLayoutRaf = 0;
placeLeaderboardBelowDaily();
syncLeaderboardScrollbarThumb();
});
}
function bindLeaderboardScrollSync() {
var scrollEl = document.querySelector('.lobby-leaderboard-scroll');
if (!scrollEl || scrollEl.dataset.scrollSyncBound === '1') return;
scrollEl.addEventListener('scroll', syncLeaderboardScrollbarThumb, { passive: true });
scrollEl.dataset.scrollSyncBound = '1';
}
var guideStep = 1;
var guideImg = document.getElementById('guide-img');
var guideBar = document.getElementById('lobby-guide-bar');
var guideDim = document.getElementById('lobby-guide-dim');
var guideFocus = document.getElementById('lobby-guide-focus');
function getGuideStepConfig(step) {
return GUIDE_STEPS[step - 1] || { position: 'bottom', highlight: null, pad: 10 };
}
function hideGuideFocus() {
if (!guideFocus) return;
guideFocus.classList.add('hidden');
guideFocus.style.left = '0';
guideFocus.style.top = '0';
guideFocus.style.width = '0';
guideFocus.style.height = '0';
}
function scheduleGuideFocusUpdate() {
requestAnimationFrame(function () {
requestAnimationFrame(updateGuideFocus);
});
}
function updateGuideFocus() {
if (!guideFocus || !guideBar || guideBar.classList.contains('hidden')) {
hideGuideFocus();
return;
}
var cfg = getGuideStepConfig(guideStep);
if (!cfg.highlight) {
hideGuideFocus();
return;
}
var target = document.querySelector(cfg.highlight);
if (!target) {
hideGuideFocus();
return;
}
var pad = typeof cfg.pad === 'number' ? cfg.pad : 8;
var r = target.getBoundingClientRect();
if (r.width < 4 || r.height < 4) {
hideGuideFocus();
return;
}
var cs = window.getComputedStyle(target);
var br = parseInt(cs.borderRadius, 10) || 0;
var radius = Math.max(br + 4, 14);
guideFocus.classList.remove('hidden');
guideFocus.style.left = (r.left - pad) + 'px';
guideFocus.style.top = (r.top - pad) + 'px';
guideFocus.style.width = (r.width + pad * 2) + 'px';
guideFocus.style.height = (r.height + pad * 2) + 'px';
guideFocus.style.borderRadius = radius + 'px';
}
function syncGuideBodyClass() {
if (!guideBar) return;
var open = !guideBar.classList.contains('hidden');
if (open) {
document.body.classList.add('lobby-guide-active');
var cfg = getGuideStepConfig(guideStep);
document.body.classList.toggle('lobby-guide-top-step', cfg.position === 'top');
var hasFocus = !!cfg.highlight;
if (guideDim) guideDim.classList.toggle('hidden', hasFocus);
} else {
document.body.classList.remove('lobby-guide-active');
document.body.classList.remove('lobby-guide-top-step');
if (guideDim) guideDim.classList.add('hidden');
}
if (open) {
scheduleGuideFocusUpdate();
} else {
hideGuideFocus();
}
}
function showGuideStep(n) {
if (!guideImg || !guideBar) return;
if (n < 1 || n > MAX_GUIDE) {
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
hideGuideFocus();
try {
localStorage.setItem(GUIDE_KEY, '1');
} catch (e) { /* ignore */ }
syncGuideBodyClass();
return;
}
guideStep = n;
var cfg = getGuideStepConfig(guideStep);
guideImg.src = ML_GUIDE + '/guide-' + (n < 10 ? '0' + n : String(n)) + '.png';
guideImg.alt = 'คำแนะนำ ขั้นที่ ' + n;
guideBar.classList.toggle('lobby-guide-bar--top', cfg.position === 'top');
var nextBtn = document.getElementById('btn-guide-next');
if (nextBtn) {
nextBtn.setAttribute('aria-label', n >= MAX_GUIDE ? 'รับทราบ' : 'ถัดไป');
}
syncGuideBodyClass();
guideImg.onload = function () {
scheduleGuideFocusUpdate();
};
guideImg.onerror = function () {
scheduleGuideFocusUpdate();
};
scheduleGuideFocusUpdate();
}
function initGuide() {
if (!guideBar || !guideImg) return;
try {
if (localStorage.getItem(GUIDE_KEY) === '1') {
guideBar.classList.add('hidden');
syncGuideBodyClass();
return;
}
} catch (e) { /* ignore */ }
guideBar.classList.remove('hidden');
showGuideStep(1);
}
document.getElementById('btn-guide-next')?.addEventListener('click', function () {
if (guideStep >= MAX_GUIDE) {
if (guideBar) {
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
}
try {
localStorage.setItem(GUIDE_KEY, '1');
} catch (e) { /* ignore */ }
syncGuideBodyClass();
return;
}
showGuideStep(guideStep + 1);
});
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
if (!guideBar || guideBar.classList.contains('hidden')) return;
e.preventDefault();
guideBar.classList.add('hidden');
guideBar.classList.remove('lobby-guide-bar--top');
hideGuideFocus();
syncGuideBodyClass();
});
window.addEventListener('resize', function () {
if (guideBar && !guideBar.classList.contains('hidden')) {
scheduleGuideFocusUpdate();
}
});
window.addEventListener('orientationchange', function () {
setTimeout(scheduleGuideFocusUpdate, 350);
});
/* ใช้ capture + data-href บน .lobby-footer-center — ลดโอกาสถูกเลเยอร์อื่นแย่งคลิกก่อนถึงปุ่ม */
var footerCenter = document.querySelector('.lobby-footer-center');
if (footerCenter) {
footerCenter.addEventListener('click', function (e) {
var btn = e.target.closest('button[data-href]');
if (!btn || !footerCenter.contains(btn)) return;
var href = btn.getAttribute('data-href');
if (!href) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
window.location.href = href;
}, true);
}
document.getElementById('btn-cloth')?.addEventListener('click', function () {
openLobbyCustomize();
});
document.getElementById('btn-daily')?.addEventListener('click', function () {
toast('รางวัลประจำวัน — เข้าเกมทุกวันเพื่อรับรางวัลต่อเนื่อง!');
});
document.getElementById('btn-myprofile')?.addEventListener('click', function () {
window.location.href = typeof appPath === 'function' ? appPath('/Main-Menu/') : '/Main-Menu/';
});
function getMlAiSessionId() {
try {
var k = 'mlAiChatSession';
var v = localStorage.getItem(k);
if (!v) {
v = 'ml-' + Date.now() + '-' + Math.random().toString(36).slice(2, 11);
localStorage.setItem(k, v);
}
return v;
} catch (e) {
return 'ml-' + String(Date.now());
}
}
var aiChatCloseBtn = document.getElementById('lobby-ai-chat-close-btn');
var aiChatPanel = document.getElementById('lobby-ai-chat-panel');
var aiChatToggleImg = document.getElementById('lobby-ai-chat-toggle-img');
var btnAiChat = document.getElementById('btn-ai-chat');
function syncAiChatPanelUi() {
if (!aiChatPanel) return;
var hidden = aiChatPanel.classList.contains('lobby-ai-chat-hidden');
if (btnAiChat) btnAiChat.style.display = hidden ? '' : 'none';
if (hidden || !aiChatToggleImg || !aiChatCloseBtn) return;
aiChatToggleImg.src = BASE + '/img/chat-close-btn.png';
aiChatToggleImg.alt = 'ปิด';
aiChatCloseBtn.setAttribute('title', 'ปิดแชท AI');
aiChatCloseBtn.setAttribute('aria-label', 'ปิดแชท AI');
}
if (aiChatCloseBtn && aiChatPanel) {
aiChatCloseBtn.addEventListener('click', function () {
aiChatPanel.classList.add('lobby-ai-chat-hidden');
aiChatPanel.classList.remove('chat-panel-collapsed');
syncAiChatPanelUi();
});
}
if (btnAiChat && aiChatPanel) {
btnAiChat.addEventListener('click', function () {
if (aiChatPanel.classList.contains('lobby-ai-chat-hidden')) {
aiChatPanel.classList.remove('lobby-ai-chat-hidden');
aiChatPanel.classList.remove('chat-panel-collapsed');
syncAiChatPanelUi();
var inp = document.getElementById('lobby-ai-chat-input');
if (inp) {
try {
inp.focus();
} catch (e) { /* ignore */ }
}
} else {
aiChatPanel.classList.add('lobby-ai-chat-hidden');
syncAiChatPanelUi();
}
});
}
syncAiChatPanelUi();
var aiChatForm = document.getElementById('lobby-ai-chat-form');
var aiChatInput = document.getElementById('lobby-ai-chat-input');
var aiChatSendBtn = aiChatForm ? aiChatForm.querySelector('button[type="submit"]') : null;
var aiChatLoadingEl = null;
function appendAiMessage(text, isAi) {
var el = document.getElementById('lobby-ai-chat-messages');
if (!el) return;
if (isAi) removeAiChatLoading();
var div = document.createElement('div');
div.className = 'lobby-ai-chat-msg ' + (isAi ? 'lobby-ai-chat-msg-ai' : 'lobby-ai-chat-msg-user');
div.textContent = (isAi ? 'AI: ' : '') + text;
el.appendChild(div);
el.scrollTop = 1e9;
}
function showAiChatLoading() {
var el = document.getElementById('lobby-ai-chat-messages');
if (!el || aiChatLoadingEl) return;
aiChatLoadingEl = document.createElement('div');
aiChatLoadingEl.className = 'lobby-ai-chat-msg lobby-ai-chat-msg-ai lobby-ai-chat-typing';
aiChatLoadingEl.setAttribute('aria-label', 'กำลังพิมพ์');
aiChatLoadingEl.innerHTML = '<span class="lobby-ai-typing-dot"></span><span class="lobby-ai-typing-dot"></span><span class="lobby-ai-typing-dot"></span>';
el.appendChild(aiChatLoadingEl);
el.scrollTop = 1e9;
}
function removeAiChatLoading() {
if (aiChatLoadingEl && aiChatLoadingEl.parentNode) {
aiChatLoadingEl.parentNode.removeChild(aiChatLoadingEl);
aiChatLoadingEl = null;
}
}
function setAiChatWaiting(waiting) {
if (aiChatInput) aiChatInput.disabled = waiting;
if (aiChatSendBtn) aiChatSendBtn.disabled = waiting;
if (waiting) showAiChatLoading();
else removeAiChatLoading();
}
if (aiChatForm && aiChatInput) {
aiChatForm.addEventListener('submit', function (e) {
e.preventDefault();
var text = (aiChatInput.value || '').trim();
if (!text) return;
appendAiMessage(text, false);
setAiChatWaiting(true);
aiChatInput.value = '';
fetch(SERVER + '/api/ai-chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text, sessionId: getMlAiSessionId() }),
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.response != null) appendAiMessage(data.response, true);
else if (data.error) appendAiMessage('[ข้อผิดพลาด: ' + data.error + ']', true);
})
.catch(function () {
appendAiMessage('[ส่งไม่สำเร็จ]', true);
})
.finally(function () {
setAiChatWaiting(false);
});
});
}
var howto = document.getElementById('lobby-howto-overlay');
function openHowto() {
if (howto) howto.classList.remove('hidden');
}
function closeHowto() {
if (howto) howto.classList.add('hidden');
}
/** ปุ่ม ? — คู่มือทีละขั้น ตาม /Main-Lobby/IMAGE/guide */
function openGuideTutorial() {
if (!guideBar || !guideImg) return;
closeHowto();
guideBar.classList.remove('hidden');
showGuideStep(1);
}
document.getElementById('btn-tutorial')?.addEventListener('click', openGuideTutorial);
document.getElementById('btn-howto-got-it')?.addEventListener('click', closeHowto);
howto?.addEventListener('click', function (e) {
if (e.target === howto) closeHowto();
});
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
initGuide();
window.addEventListener('pageshow', function () {
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
scheduleLeaderboardPlacement();
});
window.addEventListener('resize', function () {
applyCharacterFootOffsetByViewport();
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
});
window.addEventListener('orientationchange', function () {
setTimeout(function () {
applyCharacterFootOffsetByViewport();
scheduleLeaderboardPlacement();
}, 250);
});
window.addEventListener('load', function () {
bindLeaderboardScrollSync();
scheduleLeaderboardPlacement();
});
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') {
applyCharacterFootOffsetByViewport();
syncMainLobbyCharacterUi();
scheduleLeaderboardPlacement();
}
});
window.addEventListener('storage', function (e) {
if (e.key == null || e.key === CHAR_KEY || e.key === 'playerName') {
syncMainLobbyCharacterUi();
return;
}
if (e.key && String(e.key).indexOf(LOBBY_IDLE_DOWN_PREFIX) === 0) {
syncMainLobbyCharacterUi();
}
});
/* ===== ห้องแต่งตัว (Customize popup) — Phase 1: UI + เลือกธีมสี/สีผิว/ใบหน้า (เก็บ localStorage) ===== */
var CUSTOMIZE_ASSET = (typeof appPath === 'function' ? appPath('/Game') : '/Game') + '/img/03-5-Customize/';
var customizeOverlay = document.getElementById('lobby-customize-overlay');
var CUSTOMIZE_ITEMS = {
face: [
{ idx: 1, price: 0 }, { idx: 2, price: 0 }, { idx: 3, price: 50 }, { idx: 4, price: 50 },
{ idx: 5, price: 50 }, { idx: 6, price: 100 }, { idx: 7, price: 100 }, { idx: 8, price: 200 }
],
hair: [],
cloth: []
};
function lobbyMakeSwatch(group, idx, label) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'lobby-customize-swatch';
btn.setAttribute('data-idx', String(idx));
btn.setAttribute('aria-label', label);
var img = document.createElement('img');
img.src = CUSTOMIZE_ASSET + (group === 'color' ? 'color-' : 'skin-tone-') + idx + '.png';
img.alt = '';
img.decoding = 'async';
btn.appendChild(img);
btn.addEventListener('click', function () { lobbySelectSwatch(group, idx); });
return btn;
}
function lobbySelectSwatch(group, idx) {
var wrap = document.getElementById(group === 'color' ? 'lobby-theme-colors' : 'lobby-skin-tones');
if (!wrap) return;
[].forEach.call(wrap.children, function (c) {
c.classList.toggle('is-selected', c.getAttribute('data-idx') === String(idx));
});
try { localStorage.setItem(group === 'color' ? 'lobbyThemeColor' : 'lobbySkinTone', String(idx)); } catch (e) {}
}
function lobbyBuildSwatches() {
var colorWrap = document.getElementById('lobby-theme-colors');
var skinWrap = document.getElementById('lobby-skin-tones');
if (colorWrap && !colorWrap.childElementCount) {
for (var i = 1; i <= 8; i++) colorWrap.appendChild(lobbyMakeSwatch('color', i, 'ธีมสี ' + i));
}
if (skinWrap && !skinWrap.childElementCount) {
for (var j = 1; j <= 3; j++) skinWrap.appendChild(lobbyMakeSwatch('skin', j, 'สีผิว ' + j));
}
}
function lobbyRenderCustomizeItems(tab) {
var grid = document.getElementById('lobby-customize-items');
if (!grid) return;
grid.innerHTML = '';
var items = CUSTOMIZE_ITEMS[tab] || [];
if (!items.length) {
var empty = document.createElement('div');
empty.className = 'lobby-customize-empty';
empty.textContent = 'ยังไม่เปิดให้บริการ';
grid.appendChild(empty);
return;
}
var savedKey = 'lobbyItem_' + tab;
var saved = '';
try { saved = localStorage.getItem(savedKey) || ''; } catch (e) {}
items.forEach(function (it) {
var cell = document.createElement('button');
cell.type = 'button';
cell.className = 'lobby-customize-item' + (String(it.idx) === saved ? ' is-selected' : '');
cell.setAttribute('data-idx', String(it.idx));
var img = document.createElement('img');
img.className = 'lobby-customize-item-img';
img.src = CUSTOMIZE_ASSET + tab + '-' + it.idx + '.png';
img.alt = '';
img.decoding = 'async';
cell.appendChild(img);
if (it.price > 0) {
var price = document.createElement('span');
price.className = 'lobby-customize-item-price';
price.textContent = String(it.price);
cell.appendChild(price);
}
cell.addEventListener('click', function () {
[].forEach.call(grid.children, function (c) { c.classList.remove('is-selected'); });
cell.classList.add('is-selected');
try { localStorage.setItem(savedKey, String(it.idx)); } catch (e) {}
});
grid.appendChild(cell);
});
}
function lobbySetCustomizeTab(tab) {
var bar = document.getElementById('lobby-customize-tabbar');
if (bar) {
var map = { face: 'tab-1-face.png', hair: 'tab-2-hair.png', cloth: 'tab-3-cloth.png' };
bar.src = CUSTOMIZE_ASSET + (map[tab] || map.face);
}
[].forEach.call(document.querySelectorAll('.lobby-customize-tabhit'), function (t) {
t.setAttribute('aria-selected', t.getAttribute('data-tab') === tab ? 'true' : 'false');
});
lobbyRenderCustomizeItems(tab);
}
function openLobbyCustomize() {
if (!customizeOverlay) {
window.location.href = BASE + '/character.html';
return;
}
lobbyBuildSwatches();
try {
var c = localStorage.getItem('lobbyThemeColor'); if (c) lobbySelectSwatch('color', c);
var s = localStorage.getItem('lobbySkinTone'); if (s) lobbySelectSwatch('skin', s);
} catch (e) {}
lobbySetCustomizeTab('face');
customizeOverlay.classList.remove('hidden');
customizeOverlay.setAttribute('aria-hidden', 'false');
}
function closeLobbyCustomize() {
if (!customizeOverlay) return;
customizeOverlay.classList.add('hidden');
customizeOverlay.setAttribute('aria-hidden', 'true');
}
function setupLobbyCustomize() {
if (!customizeOverlay) return;
document.getElementById('lobby-customize-close')?.addEventListener('click', closeLobbyCustomize);
document.getElementById('lobby-customize-backdrop')?.addEventListener('click', closeLobbyCustomize);
document.getElementById('lobby-customize-confirm')?.addEventListener('click', function () {
closeLobbyCustomize();
toast('บันทึกการแต่งตัวแล้ว');
});
[].forEach.call(document.querySelectorAll('.lobby-customize-tabhit'), function (t) {
t.addEventListener('click', function () { lobbySetCustomizeTab(t.getAttribute('data-tab')); });
});
}
setupLobbyCustomize();
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+233
View File
@@ -1944,3 +1944,236 @@ body.lobby-guide-active:not(.lobby-guide-top-step) .lobby-footer {
width: var(--lobby-profile-coin-size) !important;
height: var(--lobby-profile-coin-size) !important;
}
/* ---- News panel: ซ่อนกล่องขาว (news-banner-bg.png) ใช้พื้นมืดเข้าธีมแทน ---- */
.lobby-news-bg {
display: none !important;
}
.lobby-news-wrap {
background: linear-gradient(180deg, rgba(12, 18, 40, 0.92), rgba(10, 16, 34, 0.92));
border: 2px solid rgba(34, 211, 238, 0.65);
border-radius: 12px;
box-shadow:
0 0 14px rgba(34, 211, 238, 0.35),
inset 0 0 18px rgba(34, 211, 238, 0.12);
}
/* ===== ห้องแต่งตัว (Customize popup) — Phase 1 ===== */
.lobby-customize-overlay {
position: fixed;
inset: 0;
z-index: 150;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.lobby-customize-overlay.hidden { display: none !important; }
.lobby-customize-backdrop {
position: absolute;
inset: 0;
background: rgba(4, 6, 18, 0.78);
backdrop-filter: blur(4px);
}
.lobby-customize-dialog {
position: relative;
width: min(92vw, 720px);
max-height: 90vh;
overflow: hidden auto;
display: flex;
flex-direction: column;
gap: clamp(0.5rem, 1.6vh, 1rem);
padding: clamp(1rem, 3vw, 1.6rem) clamp(1rem, 3vw, 1.8rem) clamp(1.2rem, 3vw, 1.8rem);
border-radius: 18px;
background: linear-gradient(180deg, rgba(14, 20, 44, 0.97), rgba(9, 13, 30, 0.97));
border: 2px solid rgba(34, 211, 238, 0.7);
box-shadow:
0 0 22px rgba(34, 211, 238, 0.45),
0 0 48px rgba(236, 72, 153, 0.28),
inset 0 0 24px rgba(34, 211, 238, 0.1);
}
.lobby-customize-titlebar {
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.lobby-customize-title {
margin: 0;
font-size: clamp(1.1rem, 3vw, 1.6rem);
font-weight: 700;
color: #e8faff;
text-shadow: 0 0 12px rgba(34, 211, 238, 0.6);
}
.lobby-customize-close {
position: absolute;
right: 0;
top: 0;
width: 38px;
height: 38px;
border: 2px solid rgba(236, 72, 153, 0.6);
border-radius: 10px;
background: rgba(20, 16, 40, 0.6);
color: #ff8fd0;
font-size: 1.4rem;
line-height: 1;
cursor: pointer;
}
.lobby-customize-close:hover { background: rgba(236, 72, 153, 0.25); }
.lobby-customize-row {
display: flex;
align-items: center;
gap: clamp(0.5rem, 2vw, 1rem);
flex-wrap: wrap;
}
.lobby-customize-row-label {
height: clamp(20px, 3.4vh, 30px);
width: auto;
flex: 0 0 auto;
}
.lobby-customize-swatches {
display: flex;
flex-wrap: wrap;
gap: clamp(6px, 1vw, 10px);
}
.lobby-customize-swatch {
padding: 0;
border: 2px solid transparent;
border-radius: 8px;
background: none;
cursor: pointer;
line-height: 0;
}
.lobby-customize-swatch img {
display: block;
width: clamp(28px, 4vw, 40px);
height: auto;
border-radius: 6px;
}
.lobby-customize-swatch.is-selected {
border-color: #22d3ee;
box-shadow: 0 0 10px rgba(34, 211, 238, 0.7);
}
.lobby-customize-tabs {
position: relative;
width: 100%;
max-width: 560px;
margin: 0 auto;
aspect-ratio: 776 / 118;
}
.lobby-customize-tabbar-img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none;
}
.lobby-customize-tabhit {
position: absolute;
top: 0;
height: 100%;
width: 33.34%;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
}
.lobby-customize-tabhit[data-tab="face"] { left: 0; }
.lobby-customize-tabhit[data-tab="hair"] { left: 33.33%; }
.lobby-customize-tabhit[data-tab="cloth"] { left: 66.66%; }
.lobby-customize-items {
flex: 1;
min-height: clamp(140px, 30vh, 280px);
max-height: 42vh;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: clamp(8px, 1.5vw, 14px);
padding: clamp(8px, 1.5vw, 14px);
border-radius: 12px;
background: rgba(8, 12, 28, 0.6);
border: 1px solid rgba(34, 211, 238, 0.25);
}
.lobby-customize-item {
position: relative;
padding: 6px;
border: 2px solid rgba(34, 211, 238, 0.3);
border-radius: 12px;
background: rgba(18, 26, 52, 0.7);
cursor: pointer;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
}
.lobby-customize-item.is-selected {
border-color: #22d3ee;
box-shadow: 0 0 10px rgba(34, 211, 238, 0.6);
}
.lobby-customize-item-img {
width: 78%;
height: auto;
object-fit: contain;
}
.lobby-customize-item-price {
position: absolute;
bottom: 4px;
left: 50%;
transform: translateX(-50%);
font-size: 0.7rem;
font-weight: 700;
color: #ffe066;
background: rgba(0, 0, 0, 0.45);
border-radius: 8px;
padding: 1px 8px;
}
.lobby-customize-empty {
grid-column: 1 / -1;
align-self: center;
text-align: center;
color: rgba(255, 255, 255, 0.6);
padding: 2rem 1rem;
font-size: 0.95rem;
}
.lobby-customize-confirm {
align-self: center;
padding: 0;
border: none;
background: none;
cursor: pointer;
}
.lobby-customize-confirm img {
display: block;
width: min(60vw, 240px);
height: auto;
}
@media (max-width: 560px) {
.lobby-customize-items { grid-template-columns: repeat(3, 1fr); }
}
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