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

712 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>