feat(webui): unify npc/monster spawner with categorized catalog table, realtime search, and instant Add button

This commit is contained in:
CAtlas0784 committed 2026-09-17 22:58:11 +07:00
1 parent b5fc15e4ec
commit 29b2f480d5
4 files changed
+318 -64

No files matched your search

@@ -141,6 +141,8 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
await WriteJsonAsync(ctx, await NpcSpawnAsync(await ReadBodyAsync(ctx.Request, token), token), token);
else if (method == "GET" && path == "/api/scenes")
await WriteJsonAsync(ctx, Scenes(), token);
else if (method == "GET" && path == "/api/npc/catalog")
await WriteJsonAsync(ctx, NpcCatalog(ctx.Request), token);
else
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
{
public List<AdminNpcSpawnItem>? Items { get; set; }
+208 -62
View File
@@ -21,6 +21,17 @@
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>
@@ -105,17 +116,82 @@
</div>
<div class="card" style="border-color:#00bcd4">
<h2 style="color:#4dd0e1">👥 NPC Spawner (เสก NPC / ตัวละครเดินดิน)</h2>
<div class="muted">One NPC per line: AgentConfig ID, optionally followed by a POI action ID (เช่น 40130709, 2).</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>
<label>X spacing</label><input id="npcXSpacing" type="number" min="0" step="any" value="1.5" style="width:70px">
<label>Z spacing</label><input id="npcZSpacing" type="number" min="0" step="any" value="1.5" style="width:70px">
<label>Max per row</label><input id="npcMaxPerRow" type="number" min="1" step="1" value="10" style="width:70px">
<button id="npcSpawn" style="background:#0097a7;margin-left:8px" onclick="spawnNpcs()">Spawn NPCs</button>
<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>
<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 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>
<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">
<h2 style="color:#81c784">🎬 Realtime 3D Cutscenes & Timelines (In-Engine 3D)</h2>
<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 }
};
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 lines = document.getElementById('npcItems').value.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
const items = lines.map(line => {
const parts = line.split(/[\s,;]+/);
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;
@@ -435,7 +596,7 @@ async function spawnNpcs() {
const maxPerRow = Number(document.getElementById('npcMaxPerRow').value);
npcBusy = true;
document.getElementById('npcSpawn').disabled = true;
output.textContent = 'Spawning NPCs...';
output.textContent = 'Spawning NPCs/Monsters into world...';
try {
const r = await api('/api/npc/spawn', {
method: 'POST',
@@ -443,6 +604,7 @@ async function spawnNpcs() {
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 {
@@ -560,23 +722,6 @@ async function setCustomWeather() {
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() {
const sel = document.getElementById('timelinepreset').value;
if (sel !== 'custom') {
@@ -595,6 +740,7 @@ async function playTimeline() {
loadFleet();
loadScenes();
loadNpcCatalog();
refresh();
setInterval(refresh, 2000);
document.getElementById('timeslider').addEventListener('input', e => {
@@ -12,7 +12,9 @@ internal sealed record NpcCatalogEntry4229938(
uint BehaviorActionConfigId,
uint ActionGroupId,
uint AiSettingId,
uint InteractSettingId);
uint InteractSettingId,
uint Camp = 0,
string Category = "citizen");
internal static class NpcCatalog4229938
{
@@ -21,6 +23,43 @@ internal static class NpcCatalog4229938
internal static bool TryGet(uint id, out NpcCatalogEntry4229938 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()
{
var root = PrivateServerConfigStore.ResolveProjectPath(PrivateServerConfigStore.Current.Paths.ClientConfigs);
@@ -59,9 +98,27 @@ internal static class NpcCatalog4229938
var animSetTag = UInt32(row, "AnimStereotype");
if (animSetTag == 0)
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,
UInt32(row, "Action"), UInt32(row, "ActionGroupId"),
UInt32(row, "AI"), UInt32(row, "InteractSetting"));
UInt32(row, "AI"), UInt32(row, "InteractSetting"),
camp, category);
}
return result;
}