2026-09-15 18:13:46 +07:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "utf-8" >
< title > Debug Panel — vehicles</ title >
< style >
body { font-family : Consolas , monospace ; background : #14161a ; color : #d7dce2 ; max-width : 860 px ; margin : 24 px auto ; padding : 0 16 px ; }
h1 { font-size : 20 px ; }
h2 { font-size : 15 px ; color : #8fb8ff ; border-bottom : 1 px solid #333 ; padding-bottom : 4 px ; }
. card { background : #1d2026 ; border : 1 px solid #333 ; border-radius : 8 px ; padding : 12 px 16 px ; margin : 12 px 0 ; }
. dot { display : inline-block ; width : 10 px ; height : 10 px ; border-radius : 50 % ; background : #666 ; margin-right : 6 px ; }
. dot . on { background : #4caf50 ; }
button { background : #2b6cb0 ; color : #fff ; border : 0 ; border-radius : 6 px ; padding : 8 px 14 px ; margin : 4 px 4 px 4 px 0 ; cursor : pointer ; font-family : inherit ; }
button : hover { background : #3884d6 ; }
button . ghost { background : #3a3f47 ; }
input , select { background : #101215 ; color : #d7dce2 ; border : 1 px solid #444 ; border-radius : 6 px ; padding : 7 px 9 px ; margin : 2 px ; font-family : inherit ; }
input [ type = number ] { width : 110 px ; }
input [ type = range ] { width : 100 % ; margin : 10 px 0 2 px 0 ; accent-color : #ffb74d ; cursor : pointer ; }
# clock { font-size : 30 px ; letter-spacing : 2 px ; color : #ffd88a ; }
table { border-collapse : collapse ; width : 100 % ; margin-top : 8 px ; }
td , th { border : 1 px solid #333 ; padding : 4 px 8 px ; text-align : left ; font-size : 13 px ; }
. muted { color : #888 ; font-size : 12 px ; }
# log { white-space : pre-wrap ; font-size : 12 px ; max-height : 160 px ; overflow-y : auto ; }
2026-09-17 22:58:11 +07:00
. npc-tab { background : #1e293b ; color : #94a3b8 ; border : 1 px solid #334155 ; border-radius : 4 px ; padding : 5 px 12 px ; font-size : 12 px ; cursor : pointer ; transition : 0.15 s ; }
. npc-tab : hover { background : #334155 ; color : #fff ; }
. npc-tab . active { background : #00838f ; color : #fff ; border-color : #00e5ff ; font-weight : bold ; }
. btn-add-npc { background : #2e7d32 ; color : #fff ; border : 0 ; border-radius : 4 px ; padding : 3 px 8 px ; font-size : 11 px ; cursor : pointer ; font-weight : bold ; }
. btn-add-npc : hover { background : #388e3c ; }
. badge-cat { font-size : 10 px ; padding : 2 px 6 px ; border-radius : 3 px ; font-weight : bold ; display : inline-block ; }
. badge-monster { background : #5c1d1d ; color : #ff8a80 ; border : 1 px solid #b71c1c ; }
. badge-citizen { background : #1a365d ; color : #90cdf4 ; border : 1 px solid #2b6cb0 ; }
. badge-police { background : #3e2723 ; color : #ffcc80 ; border : 1 px solid #795548 ; }
. badge-animal { background : #1b4332 ; color : #95d5b2 ; border : 1 px solid #2d6a4f ; }
. badge-ally { background : #4a148c ; color : #e1bee7 ; border : 1 px solid #8e24aa ; }
2026-09-15 18:13:46 +07:00
</ style >
</ head >
< body >
< h1 >< span id = "dot" class = "dot" ></ span > Vehicle debug</ h1 >
< div class = "card" >
< h2 > Status</ h2 >
< div id = "status" > connecting…</ div >
< div class = "muted" > refreshes every 2 sec</ div >
</ div >
< div class = "card" >
< h2 > Time of day</ h2 >
< div id = "clock" > 12:00</ div >
< input id = "timeslider" type = "range" min = "0" max = "1439" step = "1" value = "720" aria-label = "Time of day" >
< div class = "muted" > 00:00 — night · 06:00 — morning · 12:00 — noon · 18:00 — evening · 23:59 — night</ div >
2026-09-17 00:59:06 +07:00
< div style = "margin-top:8px" >
< button class = "ghost" onclick = "setTimePreset(6, 0)" > 🌅 Morning (06:00)</ button >
< button class = "ghost" onclick = "setTimePreset(12, 0)" > ☀️ Noon (12:00)</ button >
< button class = "ghost" onclick = "setTimePreset(18, 30)" > 🌇 Evening (18:30)</ button >
< button class = "ghost" onclick = "setTimePreset(22, 0)" > 🌙 Night (22:00)</ button >
< button class = "ghost" onclick = "setTimePreset(0, 0)" > 🌌 Midnight (00:00)</ button >
</ div >
2026-09-15 18:13:46 +07:00
</ div >
2026-09-16 18:35:25 +07:00
< div class = "card" >
< h2 > Weather / Atmosphere (สภาพอากาศ)</ h2 >
< div >
2026-09-17 00:59:06 +07:00
< button style = "background:#fbc02d;color:#222;font-weight:bold" onclick = "setWeather(1)" > ☀️ Sunny (แดดออก) [1]</ button >
< button style = "background:#78909c" onclick = "setWeather(2)" > ⛅ Cloudy (มีเมฆมาก) [2]</ button >
< button style = "background:#1976d2" onclick = "setWeather(3)" > 🌧️ Rain (ฝนตก) [3]</ button >
< button style = "background:#512da8" onclick = "setWeather(4)" > ⛈️ Storm (พายุฝนฟ้าคะนอง) [4]</ button >
< button style = "background:#00897b" onclick = "setWeather(5)" > 🌤️ Afterrain (หลังฝนตก / ใสเคลียร์) [5]</ button >
</ div >
< div style = "margin-top:10px;padding:8px;background:#252932;border-radius:6px" >
< div style = "font-weight:bold;margin-bottom:6px;color:#80deea" > 🌫️ Volumetric Exponential Fog (หมอกหนา / ทัศนวิสัย):</ div >
< button style = "background:#00695c" onclick = "setFog(0.08)" > 🌫️ Thick Fog (เปิดหมอกหนาจัด 0.08)</ button >
< button style = "background:#00838f" onclick = "setFog(0.02)" > 🌁 Light Fog (หมอกบาง 0.02)</ button >
< button class = "ghost" onclick = "setFog(0)" > ☀️ Clear Fog (ปิดหมอก)</ button >
< div class = "muted" style = "margin-top:4px" > ปรับความหนาแน่นหมอกแบบเรียลไทม์ผ่าน WeatherBridge.EnableExternalExpFogDensity</ div >
2026-09-16 18:35:25 +07:00
</ div >
< div style = "margin-top:8px" >
2026-09-17 00:59:06 +07:00
Custom Weather ID: < input id = "customWeatherId" type = "number" style = "width:90px" value = "1" min = "1" max = "5" >
2026-09-16 18:35:25 +07:00
Transition (sec): < input id = "weatherTransition" type = "number" style = "width:70px" value = "5" >
< button class = "ghost" onclick = "setCustomWeather()" > Apply Custom Weather</ button >
</ div >
2026-09-17 00:59:06 +07:00
< div class = "muted" style = "margin-top:6px" > ซิงค์สภาพอากาศของโลก (WeatherConfig 1..5) และบรรยากาศ client ทันที</ div >
2026-09-16 18:35:25 +07:00
</ div >
2026-09-15 18:13:46 +07:00
< div class = "card" >
< h2 > Garage (server vehicles)</ h2 >
< div style = "margin-bottom:10px;padding:8px;background:#252932;border-radius:6px" >
< div style = "font-weight:bold;margin-bottom:6px;color:#ffb74d" > ⚡ Quick Spawn Special Vehicles:</ div >
< button style = "background:#8e24aa" onclick = "spawnSpecificVehicle(81006020)" > 🚀 FG Vision Aric (Flying Car) [81006020]</ button >
2026-09-15 20:29:33 +07:00
< button style = "background:#e91e63" onclick = "spawnSpecificVehicle(81007104)" > 🏍️ Shikage Freeman800 (Honda NT1100) [81007104]</ button >
2026-09-15 18:13:46 +07:00
< button style = "background:#00897b" onclick = "spawnSpecificVehicle(81007007)" > 🚁 Reed Ranger Helicopter [81007007]</ button >
< button style = "background:#f4511e" onclick = "spawnSpecificVehicle(81006011)" > 🏍️ Sunset M125 Superbike [81006011]</ button >
2026-09-15 20:29:33 +07:00
< div style = "font-size:11px;color:#81c784;margin-top:6px" > 💡 หมายเหตุ: รถล้อเดียวของ Taffy (塔菲独轮车) เป็นสกิลกดขี่ของตัวละคร Taffy (กด E / Active Skill) ให้สลับเป็นตัวละคร Taffy แล้วกด E เพื่อแปลงค้อนเป็นรถล้อเดียวขี่ได้ทันที</ div >
2026-09-15 18:13:46 +07:00
</ div >
< div > Category: < select id = "fleetcat" onchange = "filterFleet()" ></ select ></ div >
< div style = "margin-top:6px" > Vehicle: < select id = "fleet" ></ select ></ div >
< div style = "margin-top:8px" >
< button onclick = "spawnNear()" > 1. Spawn selected nearby</ button >
< button onclick = "enterLast()" > 2. Seat into latest</ button >
</ div >
< div style = "margin-top:8px" >
< button class = "ghost" onclick = "resync()" > Verified (39)</ button >
< button class = "ghost" onclick = "unlockAll()" > SyncAllUnlockedVehicles (all 150)</ button >
</ div >
< div class = "muted" > By default only the 39 verified vehicles load into the game. All 150 (incl. broken/unverified) — only via SyncAllUnlockedVehicles.</ div >
< div style = "margin-top:8px" >
Custom ID: < input id = "rawId" type = "number" style = "width:130px" placeholder = "81007068" >
< button class = "ghost" onclick = "spawnRaw()" > Spawn by ID</ button >
</ div >
< div class = "muted" > Custom ID — for testing excluded/broken ones (Broken Model, Undrivable): the server lets it through with a warning, the client has the final say.</ div >
< div class = "muted" > Pushes SyncAllUnlockedVehicles — the vehicle list in the phone/garage updates without relogging.</ div >
</ div >
< div class = "card" >
< h2 > Summoned vehicles</ h2 >
< div id = "summoned" > —</ div >
</ div >
2026-09-17 22:38:58 +07:00
< div class = "card" style = "border-color:#00bcd4" >
2026-09-17 22:58:11 +07:00
< h2 style = "color:#4dd0e1" > 👥 NPC & Monster Spawner (เสก NPC / มอนสเตอร์ / สัตว์ / ตัวละคร)</ h2 >
< div class = "muted" > มอนสเตอร์ สัตว์ และชาวเมืองในเกมใช้โครงสร้าง Agent เดียวกัน ค้นหาหรือเลือกหมวดหมู่ แล้วกด < b > ➕ Add</ b > เพื่อเพิ่ม ID ลงในคิวเสกได้ทันที</ div >
<!-- Category Filter Tabs -->
< div style = "display:flex;flex-wrap:wrap;gap:6px;margin:10px 0 8px 0" id = "npcCatTabs" >
< button class = "npc-tab active" data-cat = "all" onclick = "setNpcCategory('all')" > 🌟 ทั้งหมด (All)</ button >
< button class = "npc-tab" data-cat = "monster" onclick = "setNpcCategory('monster')" > ⚔️ มอนสเตอร์ & บอส</ button >
< button class = "npc-tab" data-cat = "citizen" onclick = "setNpcCategory('citizen')" > 🚶 ชาวเมือง (Citizens)</ button >
< button class = "npc-tab" data-cat = "police" onclick = "setNpcCategory('police')" > 👮 ตำรวจ & ยาม (Police)</ button >
< button class = "npc-tab" data-cat = "animal" onclick = "setNpcCategory('animal')" > 🐱 สัตว์ & มาสคอต</ button >
< button class = "npc-tab" data-cat = "ally" onclick = "setNpcCategory('ally')" > 🤝 พันธมิตร & ตัวละคร</ button >
</ div >
<!-- Search & Pose Toolbar -->
< div style = "display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:8px" >
< div style = "flex:1;min-width:220px" >
< input id = "npcSearchInput" type = "text" placeholder = "🔍 ค้นหา (พิมพ์ชื่อ เช่น 谜面, Nanami, เป็ด, สุนัข หรือเลข ID)..." style = "width:100%;box-sizing:border-box" oninput = "onNpcSearchInput()" >
</ div >
< div style = "display:flex;align-items:center;gap:6px" >
< label style = "font-size:12px;color:#aaa" > ท่าทาง (Pose):</ label >
< select id = "npcPoiSelect" style = "min-width:180px" >
< option value = "0" > 🧍 Auto / None (0)</ option >
< option value = "2" selected > 🧍 Idle ยืนนิ่ง (2)</ option >
< option value = "1" > 🚶 Walk เดิน (1)</ option >
< option value = "4" > 🏃 Run วิ่ง (4)</ option >
< option value = "11" > 📱 Phone คุยโทรศัพท์/ถ่ายรูป (11)</ option >
< option value = "10" > 🧱 Lean Wall พิงกำแพง (10)</ option >
< option value = "12" > 🪑 Formal Sit นั่งเรียบร้อย (12)</ option >
< option value = "13" > 🪑 Relax Sit นั่งผ่อนคลาย (13)</ option >
< option value = "15" > 💬 Sit & Talk นั่งคุย (15)</ option >
< option value = "6" > 👏 Clap ปรบมือ (6)</ option >
< option value = "5" > 👀 Spectate ยืนมุง (5)</ option >
< option value = "3" > 😱 Scared ตกใจกลัว (3)</ option >
< option value = "8" > 🥤 Vending กดตู้ขายน้ำ (8)</ option >
</ select >
</ div >
</ div >
<!-- Catalog Table -->
< div style = "max-height:240px;overflow-y:auto;border:1px solid #333;border-radius:6px;background:#0d1117;margin-bottom:10px" >
< table style = "width:100%;border-collapse:collapse;font-size:12px;text-align:left" >
< thead >
< tr style = "background:#161b22;border-bottom:1px solid #30363d;position:sticky;top:0;z-index:1" >
< th style = "padding:6px 8px;width:75px" > หมวด</ th >
< th style = "padding:6px 8px;width:95px" > ID</ th >
< th style = "padding:6px 8px" > ชื่อ (Name)</ th >
< th style = "padding:6px 8px;width:80px" > Model ID</ th >
< th style = "padding:6px 8px;width:70px;text-align:center" > Action</ th >
</ tr >
</ thead >
< tbody id = "npcTableBody" >
< tr >< td colspan = "5" style = "padding:12px;text-align:center;color:#888" > กำลังโหลดรายชื่อ Agent...</ td ></ tr >
</ tbody >
</ table >
</ div >
<!-- Textarea & Spawn Controls -->
< div style = "margin-bottom:4px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:6px" >
< span style = "font-size:12px;font-weight:bold;color:#4dd0e1" > 📋 รายการที่จะเสก (คั่นด้วยบรรทัดใหม่: ID, POI_ID):</ span >
< div style = "display:flex;gap:6px;flex-wrap:wrap" >
< button class = "ghost" style = "padding:2px 8px;font-size:11px" onclick = "clearNpcList()" > 🧹 ล้างรายการ</ button >
< button class = "ghost" style = "padding:2px 8px;font-size:11px" onclick = "setNpcPreset('gang')" > ⚔️ แก๊ง 4 ตัว</ button >
< button class = "ghost" style = "padding:2px 8px;font-size:11px" onclick = "setNpcPreset('citizens')" > 🚶 ชาวเมือง 3 คน</ button >
< button class = "ghost" style = "padding:2px 8px;font-size:11px" onclick = "setNpcPreset('animals')" > 🦆 สัตว์ 3 ตัว</ button >
</ div >
2026-09-17 22:38:58 +07:00
</ div >
2026-09-17 22:58:11 +07:00
< textarea id = "npcItems" rows = "4" style = "width:100%;box-sizing:border-box;background:#101215;color:#d7dce2;border:1px solid #444;border-radius:6px;padding:6px;font-family:monospace;margin:4px 0 8px 0;resize:vertical" placeholder = "40900579, 0" spellcheck = "false" > 40130709, 2</ textarea >
< div style = "display:flex;flex-wrap:wrap;gap:10px;align-items:center" >
< label > X spacing < input id = "npcXSpacing" type = "number" min = "0" step = "any" value = "1.5" style = "width:55px" ></ label >
< label > Z spacing < input id = "npcZSpacing" type = "number" min = "0" step = "any" value = "1.5" style = "width:55px" ></ label >
< label > Max / row < input id = "npcMaxPerRow" type = "number" min = "1" step = "1" value = "10" style = "width:55px" ></ label >
< button id = "npcSpawn" style = "background:#0097a7;padding:8px 16px;font-weight:bold" onclick = "spawnNpcs()" > 🚀 เสกเข้าเกม (Spawn)</ button >
</ div >
< div class = "muted" style = "margin-top:4px" > NPC / มอนสเตอร์จะถูกวางเป็นแถวกริดด้านหน้าตัวละครของคุณโดยอิงจากตำแหน่งและมุมหันในปัจจุบัน</ div >
< pre id = "npcResult" style = "background:#101215;border:1px solid #333;border-radius:6px;padding:8px;max-height:120px;overflow-y:auto;white-space:pre-wrap;margin-top:6px;font-size:12px;display:none" ></ pre >
2026-09-17 22:38:58 +07:00
</ div >
2026-09-15 18:13:46 +07:00
< div class = "card" >
< h2 > Scene ("raid") switch</ h2 >
< div style = "margin-bottom:8px" >
Preset:
< select id = "scenepreset" onchange = "onPresetChange()" style = "min-width:380px" >
2026-09-17 22:38:58 +07:00
< option value = "loading" > Loading scene presets...</ option >
2026-09-15 18:13:46 +07:00
</ select >
< button style = "background:#2e7d32;margin-left:8px" onclick = "switchToPreset()" > Switch to preset</ button >
</ div >
< div style = "margin-bottom:8px" >
Raid < input id = "s_raid" type = "number" style = "width:130px" value = "23300999" >
Instance < input id = "s_instance" type = "number" style = "width:130px" value = "20001223" >
Universe < input id = "s_universe" type = "number" style = "width:130px" value = "76000888" >
</ div >
< div >
X < input id = "s_x" type = "number" step = "0.5" style = "width:90px" value = "-4719.8" >
Y < input id = "s_y" type = "number" step = "0.5" style = "width:90px" value = "168.5" >
Z < input id = "s_z" type = "number" step = "0.5" style = "width:90px" value = "-2844.7" >
Facing < input id = "s_f" type = "number" step = "1" style = "width:80px" value = "0" >
< button onclick = "switchSceneCustom()" style = "margin-left:8px" > Switch scene</ button >
</ div >
< div class = "muted" style = "margin-top:6px" > Switches world region and initiates client-side scene load. Preset or custom coordinates can be specified.</ div >
</ div >
< div class = "card" >
< h2 > Player teleport</ h2 >
< div >
X < input id = "tx" type = "number" step = "0.5" value = "0" >
Y < input id = "ty" type = "number" step = "0.5" value = "0" >
Z < input id = "tz" type = "number" step = "0.5" value = "0" >
Facing < input id = "tf" type = "number" step = "1" value = "0" >
</ div >
< div style = "margin-top:8px" >
< button onclick = "teleport()" > Teleport</ button >
< button class = "ghost" onclick = "fillCurrent()" > Use current</ button >
< button class = "ghost" onclick = "fillSpawn()" > Spawn point</ button >
</ div >
</ div >
2026-09-16 02:19:44 +07:00
< div class = "card" style = "border-color:#ab47bc" >
< h2 style = "color:#ce93d8" > 👗 Outfit / Appearance (สลับชุด / ซ่อนเสื้อผ้า)</ h2 >
< div >
< button style = "background:#8e24aa;color:white" onclick = "toggleClothes()" > Toggle Clothes / ซ่อน-แสดงชุด</ button >
</ div >
< div class = "muted" style = "margin-top:6px" > สลับการแสดงผลเสื้อผ้า (SkinnedMeshRenderer) ของตัวละครที่กำลังเล่นอยู่ โดยยังคงแสดงโมเดลบอดี้เดิมไว้</ div >
</ div >
2026-09-15 18:13:46 +07:00
< div class = "card" style = "border-color:#e53935" >
< h2 style = "color:#ff8a80" > Emergency Unstuck & Black Screen Recovery</ h2 >
< div >
< button style = "background:#d32f2f" onclick = "unstuckBlackScreen()" > Fix / Recover Black Screen</ button >
< button style = "background:#f57c00" onclick = "rollback10s()" > Rollback Position (10s Ago)</ button >
</ div >
< div class = "muted" style = "margin-top:6px" > Immediately forces client transition reset, restores user input, and rolls back coordinates to before the glitch.</ div >
</ div >
< div class = "card" >
< h2 > Panel log</ h2 >
< div id = "log" ></ div >
</ div >
< script >
let lastStatus = null ;
let fleetData = [];
const catNames = { sport : 'Sport / Luxury / Unique' , regular : 'Regular Cars' , suv : 'SUV / Pickups' , van : 'Vans / Bus' , truck : 'Trucks / Heavy Duty' , moto : 'Motorcycles / Scooters' , water : 'Watercraft' , air : 'Aircraft / Flying Car' , special : 'Special / Test / Props' };
let timeDragging = false ;
function fmtClock ( totalMin ) {
const h = Math . floor ( totalMin / 60 ) % 24 ;
const m = totalMin % 60 ;
return String ( h ). padStart ( 2 , '0' ) + ':' + String ( m ). padStart ( 2 , '0' );
}
function sliderMinutes () { return parseInt ( document . getElementById ( 'timeslider' ). value || '720' , 10 ); }
async function pushTime () {
const total = sliderMinutes ();
const body = {
hour : Math . floor ( total / 60 ) % 24 ,
minute : total % 60 ,
fix : true ,
transition : 5
};
const r = await api ( '/api/time/set' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ( body ) });
log ( 'time: ' + JSON . stringify ( r ));
}
function syncTimeUI ( s ) {
const slider = document . getElementById ( 'timeslider' );
const clock = document . getElementById ( 'clock' );
if ( ! s . online ) {
slider . disabled = true ;
return ;
}
slider . disabled = false ;
if ( typeof s . hour === 'number' && typeof s . minute === 'number' && s . hasTime && ! timeDragging ) {
slider . value = s . hour * 60 + s . minute ;
clock . textContent = fmtClock ( s . hour * 60 + s . minute );
} else if ( ! timeDragging ) {
clock . textContent = fmtClock ( sliderMinutes ());
}
}
function log ( t ) {
const el = document . getElementById ( 'log' );
el . textContent = new Date (). toLocaleTimeString () + ' ' + t + '\n' + el . textContent ;
}
async function api ( path , opts ) {
const r = await fetch ( path , opts );
return await r . json ();
}
async function refresh () {
try {
const s = await api ( '/api/status' );
lastStatus = s ;
document . getElementById ( 'dot' ). className = 'dot' + ( s . online ? ' on' : '' );
if ( ! s . online ) {
document . getElementById ( 'status' ). textContent = 'client not in game (no live session)' ;
document . getElementById ( 'summoned' ). textContent = '—' ;
syncTimeUI ( s );
return ;
}
const p = s . player ;
const w = s . world || { raidId : '—' , instanceId : '—' };
document . getElementById ( 'status' ). innerHTML =
`pid= ${ s . pid } ready= ${ s . ready } unit= ${ s . activeUnit } raid= ${ w . raidId } instance= ${ w . instanceId } <br>` +
`pos=( ${ p . x . toFixed ( 1 ) } , ${ p . y . toFixed ( 1 ) } , ${ p . z . toFixed ( 1 ) } ) yaw= ${ p . yaw . toFixed ( 1 ) } fix= ${ p . hasFix } <br>` +
`fleet= ${ s . fleetCount } summoned= ${ s . summoned . length } ` +
( s . inVehicle ? `<br>in vehicle: entity= ${ s . inVehicle } seat= ${ s . inSeat } <button onclick="vehicleExit()">Exit</button>` : '' );
document . getElementById ( 'summoned' ). innerHTML = s . summoned . length
? '<table><tr><th>entityId</th><th>configId</th><th>coordinates</th><th></th></tr>' +
s . summoned . map ( v => `<tr><td> ${ v . entityId } </td><td> ${ v . configId } </td>` +
`<td> ${ v . hasFix ? ` ${ v . x . toFixed ( 1 ) } , ${ v . y . toFixed ( 1 ) } , ${ v . z . toFixed ( 1 ) } ` : '—' } </td>` +
`<td><button onclick="vehicleEnter( ${ v . entityId } )">Enter</button> ` +
`<button onclick="vehicleToMe( ${ v . entityId } )">To me</button> ` +
`<button onclick="vehicleGoto( ${ v . entityId } )">To it</button> ` +
`<button class="ghost" onclick="vehicleRemove( ${ v . entityId } )">Remove</button></td></tr>` ). join ( '' ) + '</table>'
: 'empty — click "Spawn selected vehicle nearby" above' ;
syncTimeUI ( s );
} catch ( e ) { log ( 'status error: ' + e ); }
}
async function loadFleet () {
try {
fleetData = await api ( '/api/fleet' );
const cats = [... new Set ( fleetData . map ( v => v . cat ))];
document . getElementById ( 'fleetcat' ). innerHTML =
cats . map ( c => `<option value=" ${ c } "> ${ catNames [ c ] || c } </option>` ). join ( '' );
filterFleet ();
} catch ( e ) { log ( 'fleet error: ' + e ); }
}
function filterFleet () {
const c = document . getElementById ( 'fleetcat' ). value ;
const list = fleetData . filter ( v => v . cat === c );
document . getElementById ( 'fleet' ). innerHTML =
list . map ( v => `<option value=" ${ v . id } "> ${ v . id } — ${ v . name } </option>` ). join ( '' );
}
async function enterLast () {
const r = await api ( '/api/vehicle/enter' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : '{}' });
log ( 'enter: ' + JSON . stringify ( r ));
refresh ();
}
async function resync () {
const r = await api ( '/api/garage/resync' , { method : 'POST' });
log ( 'resync verified: ' + JSON . stringify ( r ));
}
async function unlockAll () {
const r = await api ( '/api/garage/unlock-all' , { method : 'POST' });
log ( 'unlock-all: ' + JSON . stringify ( r ));
}
async function spawnSpecificVehicle ( id ) {
const r = await api ( '/api/vehicle/spawn' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ({ configId : id , distance : 6 }) });
log ( 'spawn ' + id + ': ' + JSON . stringify ( r ));
refresh ();
}
async function spawnNear () {
const sel = document . getElementById ( 'fleet' );
const body = { configId : parseInt ( sel . value || '0' , 10 ), distance : 6 };
const r = await api ( '/api/vehicle/spawn' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ( body ) });
log ( 'spawn: ' + JSON . stringify ( r ));
refresh ();
}
async function spawnRaw () {
const id = parseInt ( document . getElementById ( 'rawId' ). value || '0' , 10 );
if ( ! id ) { log ( 'spawnRaw: enter an ID' ); return ; }
const r = await api ( '/api/vehicle/spawn' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ({ configId : id , distance : 6 }) });
log ( 'spawnRaw: ' + JSON . stringify ( r ));
refresh ();
}
async function vehicleEnter ( entityId ) {
const r = await api ( '/api/vehicle/enter' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ({ entityId }) });
log ( 'enter: ' + JSON . stringify ( r ));
refresh ();
}
async function vehicleExit () {
const r = await api ( '/api/vehicle/exit' , { method : 'POST' });
log ( 'exit: ' + JSON . stringify ( r ));
refresh ();
}
async function vehicleToMe ( entityId ) {
const r = await api ( '/api/vehicle/to-me' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ({ entityId }) });
log ( 'to-me: ' + JSON . stringify ( r ));
}
async function vehicleGoto ( entityId ) {
const r = await api ( '/api/vehicle/goto' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ({ entityId }) });
log ( 'goto: ' + JSON . stringify ( r ));
}
async function vehicleRemove ( entityId ) {
const r = await api ( '/api/vehicle/remove' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ({ entityId }) });
log ( 'remove: ' + JSON . stringify ( r ));
refresh ();
}
async function teleport () {
const body = {
x : parseFloat ( document . getElementById ( 'tx' ). value ),
y : parseFloat ( document . getElementById ( 'ty' ). value ),
z : parseFloat ( document . getElementById ( 'tz' ). value ),
facing : parseFloat ( document . getElementById ( 'tf' ). value )
};
const r = await api ( '/api/player/teleport' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ( body ) });
log ( 'teleport: ' + JSON . stringify ( r ));
}
function fillCurrent () {
if ( ! lastStatus || ! lastStatus . online ) return ;
const p = lastStatus . player ;
document . getElementById ( 'tx' ). value = p . x . toFixed ( 1 );
document . getElementById ( 'ty' ). value = p . y . toFixed ( 1 );
document . getElementById ( 'tz' ). value = p . z . toFixed ( 1 );
document . getElementById ( 'tf' ). value = p . yaw . toFixed ( 1 );
}
function fillSpawn () {
if ( ! lastStatus || ! lastStatus . online || ! lastStatus . spawn ) return ;
document . getElementById ( 'tx' ). value = lastStatus . spawn . x ;
document . getElementById ( 'ty' ). value = lastStatus . spawn . y ;
document . getElementById ( 'tz' ). value = lastStatus . spawn . z ;
}
const scenePresets = {
longqi : { raid : 23300999 , instance : 20001223 , universe : 76000888 , x : - 4719.8 , y : 168.5 , z : - 2844.7 , facing : 0 },
longqi_tea : { raid : 23300999 , instance : 20001223 , universe : 76000888 , x : - 4818.4 , y : 192.3 , z : - 2690.1 , facing : 117 },
longqi_temple : { raid : 23300999 , instance : 20001223 , universe : 76000888 , x : - 4306.0 , y : 156.8 , z : - 3240.8 , facing : 133 },
longqi_wukang : { raid : 23300999 , instance : 20001223 , universe : 76000888 , x : 158.7 , y : 18.0 , z : 1057.2 , facing : - 48.8 },
maincity : { raid : 23300888 , instance : 20001222 , universe : 76000888 , x : 3142.67 , y : 0 , z : 2522.78 , facing : 0 },
airport : { raid : 23300888 , instance : 20001222 , universe : 76000888 , x : - 4468.3 , y : - 6.15 , z : - 2597.5 , facing : 85.5 }
};
2026-09-17 22:38:58 +07:00
let npcBusy = false ;
2026-09-17 22:58:11 +07:00
let currentNpcCategory = 'all' ;
let npcCatalogData = [];
let npcSearchTimeout = null ;
async function loadNpcCatalog ( category = 'all' , query = '' ) {
try {
const q = encodeURIComponent ( query || '' );
const cat = encodeURIComponent ( category || 'all' );
const r = await api ( `/api/npc/catalog?cat= ${ cat } &q= ${ q } &limit=150` );
if ( r && r . items ) {
npcCatalogData = r . items ;
renderNpcTable ( npcCatalogData );
if ( r . poiActions && document . getElementById ( 'npcPoiSelect' ). children . length <= 1 ) {
renderPoiSelect ( r . poiActions );
}
}
} catch ( err ) {
console . error ( 'Failed to load NPC catalog:' , err );
}
}
function renderPoiSelect ( poiList ) {
const sel = document . getElementById ( 'npcPoiSelect' );
if ( ! sel ) return ;
sel . innerHTML = '' ;
poiList . forEach ( p => {
const opt = document . createElement ( 'option' );
opt . value = p . id ;
opt . textContent = ` ${ p . name } [ ${ p . id } ]` ;
if ( p . id === 2 ) opt . selected = true ;
sel . appendChild ( opt );
});
}
function renderNpcTable ( items ) {
const tbody = document . getElementById ( 'npcTableBody' );
if ( ! tbody ) return ;
if ( ! items || items . length === 0 ) {
tbody . innerHTML = '<tr><td colspan="5" style="padding:12px;text-align:center;color:#888">ไม่พบ Agent ในเงื่อนไขนี้</td></tr>' ;
return ;
}
const catBadges = {
monster : '<span class="badge-cat badge-monster">⚔️ Monster</span>' ,
citizen : '<span class="badge-cat badge-citizen">🚶 Citizen</span>' ,
police : '<span class="badge-cat badge-police">👮 Guard</span>' ,
animal : '<span class="badge-cat badge-animal">🐱 Animal</span>' ,
ally : '<span class="badge-cat badge-ally">🤝 Ally</span>'
};
tbody . innerHTML = items . map ( item => {
const badge = catBadges [ item . category ] || '<span class="badge-cat">Agent</span>' ;
const escapedName = ( item . name || '' ). replace ( /'/g , "\\'" );
return `<tr style="border-bottom:1px solid #222">
<td style="padding:4px 8px"> ${ badge } </td>
<td style="padding:4px 8px;font-family:monospace;color:#4dd0e1;font-weight:bold"> ${ item . id } </td>
<td style="padding:4px 8px"> ${ item . name } </td>
<td style="padding:4px 8px;color:#888;font-family:monospace"> ${ item . model || '-' } </td>
<td style="padding:4px 8px;text-align:center">
<button class="btn-add-npc" onclick="addNpcToInput( ${ item . id } , ' ${ escapedName } ')">➕ Add</button>
</td>
</tr>` ;
}). join ( '' );
}
function setNpcCategory ( cat ) {
currentNpcCategory = cat ;
const tabs = document . querySelectorAll ( '.npc-tab' );
tabs . forEach ( t => t . classList . remove ( 'active' ));
const activeTab = document . querySelector ( `.npc-tab[data-cat=" ${ cat } "]` );
if ( activeTab ) activeTab . classList . add ( 'active' );
const q = document . getElementById ( 'npcSearchInput' ). value . trim ();
loadNpcCatalog ( currentNpcCategory , q );
}
function onNpcSearchInput () {
if ( npcSearchTimeout ) clearTimeout ( npcSearchTimeout );
npcSearchTimeout = setTimeout (() => {
const q = document . getElementById ( 'npcSearchInput' ). value . trim ();
loadNpcCatalog ( currentNpcCategory , q );
}, 250 );
}
function addNpcToInput ( id , name ) {
const textarea = document . getElementById ( 'npcItems' );
const poi = document . getElementById ( 'npcPoiSelect' ). value || '0' ;
const newLine = ` ${ id } , ${ poi } ` ;
const cur = textarea . value . trim ();
if ( ! cur ) {
textarea . value = newLine ;
} else {
textarea . value = cur + '\n' + newLine ;
}
textarea . scrollTop = textarea . scrollHeight ;
log ( `Added to spawn queue: ${ name } [ ${ id } ] (POI: ${ poi } )` );
}
function clearNpcList () {
document . getElementById ( 'npcItems' ). value = '' ;
}
function setNpcPreset ( preset ) {
const textarea = document . getElementById ( 'npcItems' );
if ( preset === 'gang' ) {
textarea . value = '40900579, 0\n40100004, 0\n40100011, 0\n40100034, 0' ;
} else if ( preset === 'citizens' ) {
textarea . value = '40130709, 2\n40130001, 1\n4065053, 11' ;
} else if ( preset === 'animals' ) {
textarea . value = '4065051, 0\n40120100, 2\n40120325, 0' ;
}
}
2026-09-17 22:38:58 +07:00
async function spawnNpcs () {
if ( npcBusy ) return ;
2026-09-17 22:58:11 +07:00
const rawLines = document . getElementById ( 'npcItems' ). value . split ( /\r?\n/ ). map ( line => line . trim ()). filter ( Boolean );
const items = rawLines . map ( line => {
const clean = line . split ( '//' )[ 0 ]. split ( '#' )[ 0 ]. trim ();
const parts = clean . split ( /[\s,;]+/ );
2026-09-17 22:38:58 +07:00
return { npcFormworkId : Number ( parts [ 0 ]), poiActionId : Number ( parts [ 1 ] || 0 ) };
2026-09-17 22:58:11 +07:00
}). filter ( item => Number . isInteger ( item . npcFormworkId ) && item . npcFormworkId > 0 );
2026-09-17 22:38:58 +07:00
const output = document . getElementById ( 'npcResult' );
2026-09-17 22:58:11 +07:00
output . style . display = 'block' ;
2026-09-17 22:38:58 +07:00
if ( ! items . length || items . some ( item => ! Number . isInteger ( item . npcFormworkId ) || item . npcFormworkId < 1 || item . npcFormworkId > 4294967295 || ! Number . isInteger ( item . poiActionId ) || item . poiActionId < 0 || item . poiActionId > 4294967295 )) {
output . textContent = 'Enter valid numeric IDs, one NPC per line.' ;
return ;
}
const xSp = Number ( document . getElementById ( 'npcXSpacing' ). value );
const zSp = Number ( document . getElementById ( 'npcZSpacing' ). value );
const maxPerRow = Number ( document . getElementById ( 'npcMaxPerRow' ). value );
npcBusy = true ;
document . getElementById ( 'npcSpawn' ). disabled = true ;
2026-09-17 22:58:11 +07:00
output . textContent = 'Spawning NPCs/Monsters into world...' ;
2026-09-17 22:38:58 +07:00
try {
const r = await api ( '/api/npc/spawn' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ({ items , xSpacing : xSp , zSpacing : zSp , maxPerRow })
});
output . textContent = JSON . stringify ( r , null , 2 );
2026-09-17 22:58:11 +07:00
log ( 'Spawned ' + items . length + ' entities: ' + ( r . ok ? 'SUCCESS' : r . message ));
2026-09-17 22:38:58 +07:00
} catch ( err ) {
output . textContent = 'Spawn request failed: ' + err . message ;
} finally {
npcBusy = false ;
document . getElementById ( 'npcSpawn' ). disabled = false ;
}
}
async function loadScenes () {
try {
const r = await api ( '/api/scenes' );
const sel = document . getElementById ( 'scenepreset' );
if ( ! sel || ! r . presets ) return ;
sel . innerHTML = '' ;
r . presets . forEach ( p => {
const opt = document . createElement ( 'option' );
opt . value = p . key ;
opt . text = p . label ;
opt . dataset . p = JSON . stringify ( p );
sel . appendChild ( opt );
});
sel . onchange = onPresetChange ;
if ( sel . options . length ) onPresetChange ();
} catch ( err ) {
console . warn ( 'loadScenes failed:' , err );
}
}
2026-09-15 18:13:46 +07:00
function onPresetChange () {
2026-09-17 22:38:58 +07:00
const sel = document . getElementById ( 'scenepreset' );
if ( ! sel || ! sel . selectedOptions [ 0 ] || ! sel . selectedOptions [ 0 ]. dataset . p ) return ;
const p = JSON . parse ( sel . selectedOptions [ 0 ]. dataset . p );
document . getElementById ( 's_raid' ). value = p . raidId ;
document . getElementById ( 's_instance' ). value = p . instanceId ;
document . getElementById ( 's_universe' ). value = p . universeId ;
document . getElementById ( 's_x' ). value = p . x . toFixed ( 1 );
document . getElementById ( 's_y' ). value = p . y . toFixed ( 1 );
document . getElementById ( 's_z' ). value = p . z . toFixed ( 1 );
document . getElementById ( 's_f' ). value = p . facing . toFixed ( 1 );
2026-09-15 18:13:46 +07:00
}
2026-09-17 22:38:58 +07:00
2026-09-15 18:13:46 +07:00
async function switchToPreset () {
2026-09-17 22:38:58 +07:00
const sel = document . getElementById ( 'scenepreset' );
if ( sel && sel . value ) {
const r = await api ( '/api/world/switch-scene' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ({ preset : sel . value })
});
log ( 'switch scene preset: ' + JSON . stringify ( r ));
refresh ();
} else {
await switchSceneCustom ();
}
2026-09-15 18:13:46 +07:00
}
async function switchSceneCustom () {
const body = {
raidId : parseInt ( document . getElementById ( 's_raid' ). value || '23300999' , 10 ),
instanceId : parseInt ( document . getElementById ( 's_instance' ). value || '20001223' , 10 ),
universeId : parseInt ( document . getElementById ( 's_universe' ). value || '76000888' , 10 ),
x : parseFloat ( document . getElementById ( 's_x' ). value || '0' ),
y : parseFloat ( document . getElementById ( 's_y' ). value || '0' ),
z : parseFloat ( document . getElementById ( 's_z' ). value || '0' ),
facing : parseFloat ( document . getElementById ( 's_f' ). value || '0' )
};
const r = await api ( '/api/world/switch-scene' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ( body ) });
log ( 'switch scene: ' + JSON . stringify ( r ));
refresh ();
}
async function unstuckBlackScreen () {
const r = await api ( '/api/unstuck/blackscreen' , { method : 'POST' });
log ( 'unstuck black screen: ' + JSON . stringify ( r ));
refresh ();
}
async function rollback10s () {
const r = await api ( '/api/player/rollback-10s' , { method : 'POST' });
log ( 'rollback 10s: ' + JSON . stringify ( r ));
refresh ();
}
2026-09-16 02:19:44 +07:00
async function toggleClothes () {
const r = await api ( '/api/player/toggle-clothes' , { method : 'POST' });
log ( 'toggle clothes: ' + JSON . stringify ( r ));
}
2026-09-17 00:59:06 +07:00
async function setTimePreset ( hour , minute ) {
const slider = document . getElementById ( 'timeslider' );
if ( slider ) {
slider . value = hour * 60 + minute ;
const clock = document . getElementById ( 'clock' );
if ( clock ) clock . textContent = fmtClock ( hour * 60 + minute );
}
const body = { hour , minute , fix : true , transition : 5 };
const r = await api ( '/api/time/set' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ( body ) });
log ( 'time preset: ' + JSON . stringify ( r ));
}
2026-09-16 18:35:25 +07:00
async function setWeather ( weatherId ) {
const transition = parseInt ( document . getElementById ( 'weatherTransition' ) ? . value || '5' , 10 );
const body = { weatherId : parseInt ( weatherId , 10 ), transition };
const r = await api ( '/api/weather/set' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ( body ) });
log ( 'set weather: ' + JSON . stringify ( r ));
}
2026-09-17 00:59:06 +07:00
async function setFog ( density ) {
const body = { density : parseFloat ( density ) };
const r = await api ( '/api/weather/fog' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ( body ) });
log ( 'set fog: ' + JSON . stringify ( r ));
}
2026-09-16 18:35:25 +07:00
async function setCustomWeather () {
const id = parseInt ( document . getElementById ( 'customWeatherId' ). value || '1' , 10 );
await setWeather ( id );
}
2026-09-15 18:13:46 +07:00
loadFleet ();
2026-09-17 22:38:58 +07:00
loadScenes ();
2026-09-17 22:58:11 +07:00
loadNpcCatalog ();
2026-09-15 18:13:46 +07:00
refresh ();
setInterval ( refresh , 2000 );
document . getElementById ( 'timeslider' ). addEventListener ( 'input' , e => {
timeDragging = true ;
document . getElementById ( 'clock' ). textContent = fmtClock ( parseInt ( e . target . value || '720' , 10 ));
});
document . getElementById ( 'timeslider' ). addEventListener ( 'change' , async () => {
timeDragging = false ;
await pushTime ();
refresh ();
});
</ script >
</ body >
</ html >