feat(webui): unify npc/monster spawner with categorized catalog table, realtime search, and instant Add button
This commit is contained in:
1 parent
b5fc15e4ec
commit
29b2f480d5
4 files changed
+318
-64
No files matched your search
Binary file not shown.
@@ -141,6 +141,8 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
|
|||||||
await WriteJsonAsync(ctx, await NpcSpawnAsync(await ReadBodyAsync(ctx.Request, token), token), token);
|
await WriteJsonAsync(ctx, await NpcSpawnAsync(await ReadBodyAsync(ctx.Request, token), token), token);
|
||||||
else if (method == "GET" && path == "/api/scenes")
|
else if (method == "GET" && path == "/api/scenes")
|
||||||
await WriteJsonAsync(ctx, Scenes(), token);
|
await WriteJsonAsync(ctx, Scenes(), token);
|
||||||
|
else if (method == "GET" && path == "/api/npc/catalog")
|
||||||
|
await WriteJsonAsync(ctx, NpcCatalog(ctx.Request), token);
|
||||||
else
|
else
|
||||||
await WriteJsonAsync(ctx, new { ok = false, error = "unknown route" }, token, 404);
|
await WriteJsonAsync(ctx, new { ok = false, error = "unknown route" }, token, 404);
|
||||||
}
|
}
|
||||||
@@ -575,6 +577,55 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static object NpcCatalog(HttpListenerRequest req)
|
||||||
|
{
|
||||||
|
var q = req.QueryString["q"];
|
||||||
|
var cat = req.QueryString["cat"];
|
||||||
|
int limit = 150;
|
||||||
|
if (int.TryParse(req.QueryString["limit"], out var l) && l > 0)
|
||||||
|
limit = Math.Clamp(l, 1, 300);
|
||||||
|
|
||||||
|
var entries = NpcCatalog4229938.Search(q, cat, limit);
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
categories = new[]
|
||||||
|
{
|
||||||
|
new { id = "all", label = "🌟 All (ทั้งหมด)" },
|
||||||
|
new { id = "monster", label = "⚔️ Monsters & Bosses (มอนสเตอร์/บอส)" },
|
||||||
|
new { id = "citizen", label = "🚶 Citizens (ชาวเมือง/ประชาชน)" },
|
||||||
|
new { id = "police", label = "👮 Police & Security (ตำรวจ/ยาม)" },
|
||||||
|
new { id = "animal", label = "🐱 Animals & Pets (สัตว์/เป็ด/แมว)" },
|
||||||
|
new { id = "ally", label = "🤝 Allies & Story (พันธมิตร/ตัวละคร)" },
|
||||||
|
},
|
||||||
|
poiActions = new[]
|
||||||
|
{
|
||||||
|
new { id = 0, name = "🧍 Auto / None (ไม่ระบุท่า)" },
|
||||||
|
new { id = 2, name = "🧍 Idle (ยืนนิ่ง)" },
|
||||||
|
new { id = 1, name = "🚶 Walk (เดิน)" },
|
||||||
|
new { id = 4, name = "🏃 Run (วิ่ง)" },
|
||||||
|
new { id = 11, name = "📱 Phone / Camera (คุยโทรศัพท์/ถ่ายรูป)" },
|
||||||
|
new { id = 10, name = "🧱 Lean on Wall (พิงกำแพง)" },
|
||||||
|
new { id = 12, name = "🪑 Formal Sit (นั่งเก้าอี้เรียบร้อย)" },
|
||||||
|
new { id = 13, name = "🪑 Relaxed Sit (นั่งเก้าอี้ผ่อนคลาย)" },
|
||||||
|
new { id = 15, name = "💬 Sit & Talk (นั่งคุย)" },
|
||||||
|
new { id = 6, name = "👏 Clapping (ปรบมือ)" },
|
||||||
|
new { id = 5, name = "👀 Spectating (ยืนมุงดู)" },
|
||||||
|
new { id = 3, name = "😱 Scared / Panicking (ตกใจกลัว)" },
|
||||||
|
new { id = 8, name = "🥤 Vending Machine (กดตู้ขายน้ำ)" }
|
||||||
|
},
|
||||||
|
items = entries.Select(e => new
|
||||||
|
{
|
||||||
|
id = e.Id,
|
||||||
|
name = string.IsNullOrWhiteSpace(e.Name) ? $"Agent_{e.Id}" : e.Name,
|
||||||
|
category = e.Category,
|
||||||
|
model = e.GeneralModelId,
|
||||||
|
camp = e.Camp,
|
||||||
|
defaultPoi = e.Category == "monster" ? 0 : 2
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class NpcSpawnRequest
|
private sealed class NpcSpawnRequest
|
||||||
{
|
{
|
||||||
public List<AdminNpcSpawnItem>? Items { get; set; }
|
public List<AdminNpcSpawnItem>? Items { get; set; }
|
||||||
|
|||||||
@@ -21,6 +21,17 @@
|
|||||||
td, th { border: 1px solid #333; padding: 4px 8px; text-align: left; font-size: 13px; }
|
td, th { border: 1px solid #333; padding: 4px 8px; text-align: left; font-size: 13px; }
|
||||||
.muted { color: #888; font-size: 12px; }
|
.muted { color: #888; font-size: 12px; }
|
||||||
#log { white-space: pre-wrap; font-size: 12px; max-height: 160px; overflow-y: auto; }
|
#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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -105,17 +116,82 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="border-color:#00bcd4">
|
<div class="card" style="border-color:#00bcd4">
|
||||||
<h2 style="color:#4dd0e1">👥 NPC Spawner (เสก NPC / ตัวละครเดินดิน)</h2>
|
<h2 style="color:#4dd0e1">👥 NPC & Monster Spawner (เสก NPC / มอนสเตอร์ / สัตว์ / ตัวละคร)</h2>
|
||||||
<div class="muted">One NPC per line: AgentConfig ID, optionally followed by a POI action ID (เช่น 40130709, 2).</div>
|
<div class="muted">มอนสเตอร์ สัตว์ และชาวเมืองในเกมใช้โครงสร้าง Agent เดียวกัน ค้นหาหรือเลือกหมวดหมู่ แล้วกด <b>➕ Add</b> เพื่อเพิ่ม ID ลงในคิวเสกได้ทันที</div>
|
||||||
<textarea id="npcItems" rows="6" style="width:100%;box-sizing:border-box;background:#101215;color:#d7dce2;border:1px solid #444;border-radius:6px;padding:6px;font-family:inherit;margin:6px 0;resize:vertical" placeholder="40130001, 0" spellcheck="false">40130709, 2</textarea>
|
|
||||||
<div>
|
<!-- Category Filter Tabs -->
|
||||||
<label>X spacing</label><input id="npcXSpacing" type="number" min="0" step="any" value="1.5" style="width:70px">
|
<div style="display:flex;flex-wrap:wrap;gap:6px;margin:10px 0 8px 0" id="npcCatTabs">
|
||||||
<label>Z spacing</label><input id="npcZSpacing" type="number" min="0" step="any" value="1.5" style="width:70px">
|
<button class="npc-tab active" data-cat="all" onclick="setNpcCategory('all')">🌟 ทั้งหมด (All)</button>
|
||||||
<label>Max per row</label><input id="npcMaxPerRow" type="number" min="1" step="1" value="10" style="width:70px">
|
<button class="npc-tab" data-cat="monster" onclick="setNpcCategory('monster')">⚔️ มอนสเตอร์ & บอส</button>
|
||||||
<button id="npcSpawn" style="background:#0097a7;margin-left:8px" onclick="spawnNpcs()">Spawn NPCs</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>
|
</div>
|
||||||
<div class="muted" style="margin-top:4px">NPCs are placed in a player-relative grid in front of you.</div>
|
|
||||||
<pre id="npcResult" style="background:#101215;border:1px solid #333;border-radius:6px;padding:8px;max-height:160px;overflow-y:auto;white-space:pre-wrap;margin-top:6px;font-size:12px"></pre>
|
<!-- 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>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -174,36 +250,6 @@
|
|||||||
<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 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>
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h2>Combat & Monster Spawner</h2>
|
|
||||||
<div style="margin-bottom:6px">
|
|
||||||
Monster:
|
|
||||||
<select id="enemypreset" onchange="onEnemyPresetChange()">
|
|
||||||
<option value="40900579">Melee Combatant (Verified) [40900579]</option>
|
|
||||||
<option value="40950138">Tactical Ranged Soldier (Verified) [40950138]</option>
|
|
||||||
<option value="100101">Street Thug (Melee) [100101]</option>
|
|
||||||
<option value="100102">Street Thug (Ranged Pistol) [100102]</option>
|
|
||||||
<option value="100201">Elite Enforcer (Katana) [100201]</option>
|
|
||||||
<option value="100301">Heavy Shield Brawler [100301]</option>
|
|
||||||
<option value="200101">Chaos Mutant Boss [200101]</option>
|
|
||||||
<option value="200201">Armored Mech Boss [200201]</option>
|
|
||||||
<option value="custom">Custom ID...</option>
|
|
||||||
</select>
|
|
||||||
ID: <input id="enemyId" type="number" style="width:100px" value="40900579">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Camp:
|
|
||||||
<select id="enemyCamp">
|
|
||||||
<option value="0">Enemy / Hostile (0)</option>
|
|
||||||
<option value="1">Ally / Friendly (1)</option>
|
|
||||||
<option value="2">Neutral (2)</option>
|
|
||||||
</select>
|
|
||||||
Count: <input id="enemyCount" type="number" style="width:70px" value="1" min="1" max="10">
|
|
||||||
<button style="background:#e53935;margin-left:8px" onclick="spawnEnemy()">Spawn Opponent</button>
|
|
||||||
</div>
|
|
||||||
<div class="muted" style="margin-top:6px">Spawns enemies near the player for live combat testing. Use with active weapon & combat sandbox.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" style="border-color:#4caf50">
|
<div class="card" style="border-color:#4caf50">
|
||||||
<h2 style="color:#81c784">🎬 Realtime 3D Cutscenes & Timelines (In-Engine 3D)</h2>
|
<h2 style="color:#81c784">🎬 Realtime 3D Cutscenes & Timelines (In-Engine 3D)</h2>
|
||||||
<div style="margin-bottom:8px">
|
<div style="margin-bottom:8px">
|
||||||
@@ -418,14 +464,129 @@ const scenePresets = {
|
|||||||
airport: { raid: 23300888, instance: 20001222, universe: 76000888, x: -4468.3, y: -6.15, z: -2597.5, facing: 85.5 }
|
airport: { raid: 23300888, instance: 20001222, universe: 76000888, x: -4468.3, y: -6.15, z: -2597.5, facing: 85.5 }
|
||||||
};
|
};
|
||||||
let npcBusy = false;
|
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() {
|
async function spawnNpcs() {
|
||||||
if (npcBusy) return;
|
if (npcBusy) return;
|
||||||
const lines = document.getElementById('npcItems').value.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
const rawLines = document.getElementById('npcItems').value.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
||||||
const items = lines.map(line => {
|
const items = rawLines.map(line => {
|
||||||
const parts = line.split(/[\s,;]+/);
|
const clean = line.split('//')[0].split('#')[0].trim();
|
||||||
|
const parts = clean.split(/[\s,;]+/);
|
||||||
return { npcFormworkId: Number(parts[0]), poiActionId: Number(parts[1] || 0) };
|
return { npcFormworkId: Number(parts[0]), poiActionId: Number(parts[1] || 0) };
|
||||||
});
|
}).filter(item => Number.isInteger(item.npcFormworkId) && item.npcFormworkId > 0);
|
||||||
|
|
||||||
const output = document.getElementById('npcResult');
|
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)) {
|
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.';
|
output.textContent = 'Enter valid numeric IDs, one NPC per line.';
|
||||||
return;
|
return;
|
||||||
@@ -435,7 +596,7 @@ async function spawnNpcs() {
|
|||||||
const maxPerRow = Number(document.getElementById('npcMaxPerRow').value);
|
const maxPerRow = Number(document.getElementById('npcMaxPerRow').value);
|
||||||
npcBusy = true;
|
npcBusy = true;
|
||||||
document.getElementById('npcSpawn').disabled = true;
|
document.getElementById('npcSpawn').disabled = true;
|
||||||
output.textContent = 'Spawning NPCs...';
|
output.textContent = 'Spawning NPCs/Monsters into world...';
|
||||||
try {
|
try {
|
||||||
const r = await api('/api/npc/spawn', {
|
const r = await api('/api/npc/spawn', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -443,6 +604,7 @@ async function spawnNpcs() {
|
|||||||
body: JSON.stringify({ items, xSpacing: xSp, zSpacing: zSp, maxPerRow })
|
body: JSON.stringify({ items, xSpacing: xSp, zSpacing: zSp, maxPerRow })
|
||||||
});
|
});
|
||||||
output.textContent = JSON.stringify(r, null, 2);
|
output.textContent = JSON.stringify(r, null, 2);
|
||||||
|
log('Spawned ' + items.length + ' entities: ' + (r.ok ? 'SUCCESS' : r.message));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
output.textContent = 'Spawn request failed: ' + err.message;
|
output.textContent = 'Spawn request failed: ' + err.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -560,23 +722,6 @@ async function setCustomWeather() {
|
|||||||
await setWeather(id);
|
await setWeather(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEnemyPresetChange() {
|
|
||||||
const sel = document.getElementById('enemypreset').value;
|
|
||||||
if (sel !== 'custom') {
|
|
||||||
document.getElementById('enemyId').value = sel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function spawnEnemy() {
|
|
||||||
const body = {
|
|
||||||
enemyId: parseInt(document.getElementById('enemyId').value || '40900579', 10),
|
|
||||||
camp: parseInt(document.getElementById('enemyCamp').value || '0', 10),
|
|
||||||
count: parseInt(document.getElementById('enemyCount').value || '1', 10)
|
|
||||||
};
|
|
||||||
const r = await api('/api/enemy/spawn', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
||||||
log('spawn enemy: ' + JSON.stringify(r));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTimelinePresetChange() {
|
function onTimelinePresetChange() {
|
||||||
const sel = document.getElementById('timelinepreset').value;
|
const sel = document.getElementById('timelinepreset').value;
|
||||||
if (sel !== 'custom') {
|
if (sel !== 'custom') {
|
||||||
@@ -595,6 +740,7 @@ async function playTimeline() {
|
|||||||
|
|
||||||
loadFleet();
|
loadFleet();
|
||||||
loadScenes();
|
loadScenes();
|
||||||
|
loadNpcCatalog();
|
||||||
refresh();
|
refresh();
|
||||||
setInterval(refresh, 2000);
|
setInterval(refresh, 2000);
|
||||||
document.getElementById('timeslider').addEventListener('input', e => {
|
document.getElementById('timeslider').addEventListener('input', e => {
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ internal sealed record NpcCatalogEntry4229938(
|
|||||||
uint BehaviorActionConfigId,
|
uint BehaviorActionConfigId,
|
||||||
uint ActionGroupId,
|
uint ActionGroupId,
|
||||||
uint AiSettingId,
|
uint AiSettingId,
|
||||||
uint InteractSettingId);
|
uint InteractSettingId,
|
||||||
|
uint Camp = 0,
|
||||||
|
string Category = "citizen");
|
||||||
|
|
||||||
internal static class NpcCatalog4229938
|
internal static class NpcCatalog4229938
|
||||||
{
|
{
|
||||||
@@ -21,6 +23,43 @@ internal static class NpcCatalog4229938
|
|||||||
internal static bool TryGet(uint id, out NpcCatalogEntry4229938 entry)
|
internal static bool TryGet(uint id, out NpcCatalogEntry4229938 entry)
|
||||||
=> Cache.Value.TryGetValue(id, out entry!);
|
=> Cache.Value.TryGetValue(id, out entry!);
|
||||||
|
|
||||||
|
internal static IReadOnlyCollection<NpcCatalogEntry4229938> All => Cache.Value.Values;
|
||||||
|
|
||||||
|
internal static List<NpcCatalogEntry4229938> Search(string? query, string? category, int limit = 150)
|
||||||
|
{
|
||||||
|
var q = query?.Trim();
|
||||||
|
var cat = category?.Trim().ToLowerInvariant();
|
||||||
|
var hasQuery = !string.IsNullOrWhiteSpace(q);
|
||||||
|
var hasCat = !string.IsNullOrWhiteSpace(cat) && cat != "all";
|
||||||
|
|
||||||
|
uint queryId = 0;
|
||||||
|
if (hasQuery) uint.TryParse(q, out queryId);
|
||||||
|
|
||||||
|
var list = new List<NpcCatalogEntry4229938>();
|
||||||
|
foreach (var entry in Cache.Value.Values)
|
||||||
|
{
|
||||||
|
if (hasCat && !string.Equals(entry.Category, cat, StringComparison.OrdinalIgnoreCase))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (hasQuery)
|
||||||
|
{
|
||||||
|
if (queryId != 0 && entry.Id == queryId)
|
||||||
|
{
|
||||||
|
list.Add(entry);
|
||||||
|
if (list.Count >= limit) break;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.Name.Contains(q!, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!entry.Id.ToString().Contains(q!))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(entry);
|
||||||
|
if (list.Count >= limit) break;
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
private static Dictionary<uint, NpcCatalogEntry4229938> Load()
|
private static Dictionary<uint, NpcCatalogEntry4229938> Load()
|
||||||
{
|
{
|
||||||
var root = PrivateServerConfigStore.ResolveProjectPath(PrivateServerConfigStore.Current.Paths.ClientConfigs);
|
var root = PrivateServerConfigStore.ResolveProjectPath(PrivateServerConfigStore.Current.Paths.ClientConfigs);
|
||||||
@@ -59,9 +98,27 @@ internal static class NpcCatalog4229938
|
|||||||
var animSetTag = UInt32(row, "AnimStereotype");
|
var animSetTag = UInt32(row, "AnimStereotype");
|
||||||
if (animSetTag == 0)
|
if (animSetTag == 0)
|
||||||
animSetTag = UInt32(row, "BattleAnimType");
|
animSetTag = UInt32(row, "BattleAnimType");
|
||||||
|
|
||||||
|
var camp = UInt32(row, "Camp");
|
||||||
|
var enemyClass = UInt32(row, "EnemyClassType");
|
||||||
|
var isAnimal = (row.TryGetProperty("AnimalGamePlay", out var ag) && ag.ValueKind == JsonValueKind.True) ||
|
||||||
|
name.Contains("鸭") || name.Contains("电台") || name.Contains("猫") || name.Contains("鸽") || name.Contains("鸟") || name.Contains("犬");
|
||||||
|
string category;
|
||||||
|
if (isAnimal || camp == 11)
|
||||||
|
category = "animal";
|
||||||
|
else if (camp is 0 or 26 || enemyClass > 0 || name.Contains("帮") || name.Contains("怪") || name.Contains("Boss") || name.Contains("哨兵") || name.Contains("斗士") || name.Contains("无赖") || name.Contains("狂飙") || name.Contains("狂战士") || name.Contains("史莱姆"))
|
||||||
|
category = "monster";
|
||||||
|
else if (camp is 16 or 17 or 18 or 19 or 24 || name.Contains("警") || name.Contains("守卫") || name.Contains("保镖") || name.Contains("巡卫") || name.Contains("安保"))
|
||||||
|
category = "police";
|
||||||
|
else if (camp == 1 || name.Contains("Seymour") || name.Contains("Taffy") || name.Contains("Bansy") || name.Contains("Garm") || name.Contains("Rin") || name.Contains("HotDog"))
|
||||||
|
category = "ally";
|
||||||
|
else
|
||||||
|
category = "citizen";
|
||||||
|
|
||||||
result[id] = new(id, name, model, persona, animSetTag,
|
result[id] = new(id, name, model, persona, animSetTag,
|
||||||
UInt32(row, "Action"), UInt32(row, "ActionGroupId"),
|
UInt32(row, "Action"), UInt32(row, "ActionGroupId"),
|
||||||
UInt32(row, "AI"), UInt32(row, "InteractSetting"));
|
UInt32(row, "AI"), UInt32(row, "InteractSetting"),
|
||||||
|
camp, category);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user