Files
Ananta-ps/Ananta.Server/Ananta.App/DebugPanel/index.html
T

711 lines
36 KiB
HTML
Raw Normal View History

<!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: 860px; margin: 24px auto; padding: 0 16px; }
h1 { font-size: 20px; }
h2 { font-size: 15px; color: #8fb8ff; border-bottom: 1px solid #333; padding-bottom: 4px; }
.card { background: #1d2026; border: 1px solid #333; border-radius: 8px; padding: 12px 16px; margin: 12px 0; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: #666; margin-right: 6px; }
.dot.on { background: #4caf50; }
button { background: #2b6cb0; color: #fff; border: 0; border-radius: 6px; padding: 8px 14px; margin: 4px 4px 4px 0; cursor: pointer; font-family: inherit; }
button:hover { background: #3884d6; }
button.ghost { background: #3a3f47; }
input, select { background: #101215; color: #d7dce2; border: 1px solid #444; border-radius: 6px; padding: 7px 9px; margin: 2px; font-family: inherit; }
input[type=number] { width: 110px; }
input[type=range] { width: 100%; margin: 10px 0 2px 0; accent-color: #ffb74d; cursor: pointer; }
#clock { font-size: 30px; letter-spacing: 2px; color: #ffd88a; }
table { border-collapse: collapse; width: 100%; margin-top: 8px; }
td, th { border: 1px solid #333; padding: 4px 8px; text-align: left; font-size: 13px; }
.muted { color: #888; font-size: 12px; }
#log { white-space: pre-wrap; font-size: 12px; max-height: 160px; overflow-y: auto; }
.npc-tab { background: #1e293b; color: #94a3b8; border: 1px solid #334155; border-radius: 4px; padding: 5px 12px; font-size: 12px; cursor: pointer; transition: 0.15s; }
.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: 4px; padding: 3px 8px; font-size: 11px; cursor: pointer; font-weight: bold; }
.btn-add-npc:hover { background: #388e3c; }
.badge-cat { font-size: 10px; padding: 2px 6px; border-radius: 3px; font-weight: bold; display: inline-block; }
.badge-monster { background: #5c1d1d; color: #ff8a80; border: 1px solid #b71c1c; }
.badge-citizen { background: #1a365d; color: #90cdf4; border: 1px solid #2b6cb0; }
.badge-police { background: #3e2723; color: #ffcc80; border: 1px solid #795548; }
.badge-animal { background: #1b4332; color: #95d5b2; border: 1px solid #2d6a4f; }
.badge-ally { background: #4a148c; color: #e1bee7; border: 1px solid #8e24aa; }
</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 &nbsp;·&nbsp; 06:00 — morning &nbsp;·&nbsp; 12:00 — noon &nbsp;·&nbsp; 18:00 — evening &nbsp;·&nbsp; 23:59 — night</div>
<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>
</div>
<div class="card">
<h2>Weather / Atmosphere (สภาพอากาศ)</h2>
<div>
<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>
</div>
<div style="margin-top:8px">
Custom Weather ID: <input id="customWeatherId" type="number" style="width:90px" value="1" min="1" max="5">
Transition (sec): <input id="weatherTransition" type="number" style="width:70px" value="5">
<button class="ghost" onclick="setCustomWeather()">Apply Custom Weather</button>
</div>
<div class="muted" style="margin-top:6px">ซิงค์สภาพอากาศของโลก (WeatherConfig 1..5) และบรรยากาศ client ทันที</div>
</div>
<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>
<button style="background:#e91e63" onclick="spawnSpecificVehicle(81007104)">🏍️ Shikage Freeman800 (Honda NT1100) [81007104]</button>
<button style="background:#00897b" onclick="spawnSpecificVehicle(81007007)">🚁 Reed Ranger Helicopter [81007007]</button>
<button style="background:#f4511e" onclick="spawnSpecificVehicle(81006011)">🏍️ Sunset M125 Superbike [81006011]</button>
<div style="font-size:11px;color:#81c784;margin-top:6px">💡 หมายเหตุ: รถล้อเดียวของ Taffy (塔菲独轮车) เป็นสกิลกดขี่ของตัวละคร Taffy (กด E / Active Skill) ให้สลับเป็นตัวละคร Taffy แล้วกด E เพื่อแปลงค้อนเป็นรถล้อเดียวขี่ได้ทันที</div>
</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>
<div class="card" style="border-color:#00bcd4">
<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>
</div>
<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>
</div>
<div class="card">
<h2>Scene ("raid") switch</h2>
<div style="margin-bottom:8px">
Preset:
<select id="scenepreset" onchange="onPresetChange()" style="min-width:380px">
<option value="loading">Loading scene presets...</option>
</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>
<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>
<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 }
};
let npcBusy = false;
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';
}
}
async function spawnNpcs() {
if (npcBusy) return;
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,;]+/);
return { npcFormworkId: Number(parts[0]), poiActionId: Number(parts[1] || 0) };
}).filter(item => Number.isInteger(item.npcFormworkId) && item.npcFormworkId > 0);
const output = document.getElementById('npcResult');
output.style.display = 'block';
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;
output.textContent = 'Spawning NPCs/Monsters into world...';
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);
log('Spawned ' + items.length + ' entities: ' + (r.ok ? 'SUCCESS' : r.message));
} 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);
}
}
function onPresetChange() {
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);
}
async function switchToPreset() {
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();
}
}
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();
}
async function toggleClothes() {
const r = await api('/api/player/toggle-clothes', { method: 'POST' });
log('toggle clothes: ' + JSON.stringify(r));
}
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));
}
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));
}
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));
}
async function setCustomWeather() {
const id = parseInt(document.getElementById('customWeatherId').value || '1', 10);
await setWeather(id);
}
loadFleet();
loadScenes();
loadNpcCatalog();
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>