feat: Add metro transit, gadgets, shop, social apps, and traffic models checkpoint

This commit is contained in:
CAtlas0784 committed 2026-10-29 01:55:01 +07:00
1 parent caa42b84ed
commit 74e2963a12
79 files changed
+527962 -496

No files matched your search

+52 -18
View File
@@ -175,6 +175,8 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
await WriteJsonAsync(ctx, ItemCatalogList(ctx.Request.QueryString), token);
else if (method == "POST" && (path == "/api/item/give" || path == "/api/player/give-item"))
await WriteJsonAsync(ctx, await GiveItemAsync(await ReadBodyAsync(ctx.Request, token), token), token);
else if (method == "POST" && path == "/api/gacha/grant-tickets")
await WriteJsonAsync(ctx, await GachaGrantTicketsAsync(token), token);
else if (method == "POST" && (path == "/api/armory/equip" || path == "/api/weapon/equip"))
await WriteJsonAsync(ctx, await WeaponEquipAsync(await ReadBodyAsync(ctx.Request, token), token), token);
else
@@ -706,26 +708,14 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
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 (กดตู้ขายน้ำ)" }
},
poiActions = PoiActionCatalog4229938.All()
.OrderBy(e => e.Id)
.Select(e => new { id = e.Id, name = string.IsNullOrWhiteSpace(e.Name) ? $"POI {e.Id}" : NameTranslator.PoiAction(e.Name) })
.ToList(),
items = entries.Select(e => new
{
id = e.Id,
name = string.IsNullOrWhiteSpace(e.Name) ? $"Agent_{e.Id}" : e.Name,
name = string.IsNullOrWhiteSpace(e.Name) ? $"Agent_{e.Id}" : NameTranslator.Agent(e.Name),
category = e.Category,
model = e.GeneralModelId,
camp = e.Camp,
@@ -1549,6 +1539,31 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
CityTrafficEngine.Instance.TargetDensity = Math.Clamp(den.GetInt32(), 0, 100);
if (root.TryGetProperty("speedScale", out var spd))
CityTrafficEngine.Instance.SpeedScale = Math.Clamp((float)spd.GetDouble(), 0.1f, 5.0f);
if (root.TryGetProperty("moveReset", out var mr) && (mr.ValueKind == JsonValueKind.True || mr.ValueKind == JsonValueKind.False))
{
TrafficAndCrowdDriver.VehicleMoveReset = mr.GetBoolean();
Console.WriteLine($"[TRAFFIC] vehicle move channel: SyncTeleportVehicle reset={TrafficAndCrowdDriver.VehicleMoveReset}");
}
if (root.TryGetProperty("useAetherProtocol", out var ua) && (ua.ValueKind == JsonValueKind.True || ua.ValueKind == JsonValueKind.False))
{
CityTrafficEngine.Instance.UseAetherProtocol = ua.GetBoolean();
Console.WriteLine($"[TRAFFIC] protocol: {(ua.GetBoolean() ? "Aether client-driven (lane sync)" : "legacy scene-entity streaming")}");
}
if (root.TryGetProperty("serverVehicles", out var sv) && (sv.ValueKind == JsonValueKind.True || sv.ValueKind == JsonValueKind.False))
{
CityTrafficEngine.Instance.ServerSpawnedVehicles = sv.GetBoolean();
Console.WriteLine($"[TRAFFIC] server-spawned Aether vehicles: {sv.GetBoolean()}");
}
if (root.TryGetProperty("ambientIntersections", out var ai) && (ai.ValueKind == JsonValueKind.True || ai.ValueKind == JsonValueKind.False))
{
CityTrafficEngine.Instance.AmbientIntersectionInit = ai.GetBoolean();
Console.WriteLine($"[TRAFFIC] ambient intersection init: {ai.GetBoolean()} (takes effect on next world entry)");
}
if (root.TryGetProperty("timestampMode", out var tm) && tm.TryGetInt32(out var mode))
{
CityTrafficEngine.Instance.AetherTimestampMode = Math.Clamp(mode, 0, 2);
Console.WriteLine($"[TRAFFIC] aether timestamp mode: {CityTrafficEngine.Instance.AetherTimestampMode} (0=unix, 1=unix×15, 2=session seconds)");
}
return Task.FromResult<object>(new { ok = true, status = CityTrafficEngine.Instance.GetStatus() });
}
catch (Exception ex)
@@ -1639,9 +1654,28 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
};
}
private async Task<object> GiveItemAsync(string json, CancellationToken token)
/// <summary>Debug panel: grants every gacha ticket item so any pool can be opened.</summary>
private async Task<object> GachaGrantTicketsAsync(CancellationToken token)
{
var session = hub.Current;
if (session is null)
return new { ok = false, error = "no active game session" };
var results = new List<object>();
var granted = 0;
foreach (var ticketId in EconomyConfigRepository.GachaTicketItemIds())
{
var result = await GameRouter.PanelGiveItemAsync(session, ticketId, 99, false);
if (result.Ok)
granted++;
results.Add(new { ticketId, ok = result.Ok, message = result.Message });
}
return new { ok = granted > 0, granted, results };
}
private async Task<object> GiveItemAsync(string json, CancellationToken token)
{ var session = hub.Current;
if (session is null)
return new { ok = false, error = "no live game session (is the client in the world?)" };
+119 -63
View File
@@ -1,43 +1,100 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="dark">
<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; }
/* ===== design tokens ported from PrivateServer1 (Drmk) WebUI ===== */
:root {
--bg-page: #dbe0e7; --bg-canvas: #dde2e9; --bg-header: rgba(238,242,247,0.92);
--bg-card: rgba(248,250,253,0.82); --bg-card-elevated: #ffffff; --bg-card-sub: rgba(230,236,243,0.6);
--bg-input: rgba(255,255,255,0.9);
--pill-active-bg: #23272e; --pill-active-text: #fff;
--pill-inactive-bg: rgba(168,179,195,0.28); --pill-inactive-text: #3e4756; --pill-hover-bg: rgba(168,179,195,0.42);
--btn-primary-bg: #22262e; --btn-primary-hover: #111419; --btn-primary-text: #fff;
--btn-secondary-bg: rgba(160,173,190,0.28); --btn-secondary-hover: rgba(160,173,190,0.42); --btn-secondary-text: #22262e;
--text-main: #181c23; --text-2: #3a4352; --text-3: #5f6b7c; --text-4: #8a96a6;
--border-subtle: rgba(160,175,195,0.35); --border-card: rgba(255,255,255,0.8); --border-input: rgba(160,175,195,0.5);
--accent-blue: #2b66ff; --accent-glow: rgba(43,102,255,0.35);
--red: #ef4444; --green: #10b981;
--font-game: 'Bahnschrift','PingFang SC','Microsoft YaHei','Segoe UI',-apple-system,BlinkMacSystemFont,Roboto,sans-serif;
}
html[data-theme="dark"] {
--bg-page: #101319; --bg-canvas: #12161f; --bg-header: rgba(20,25,34,0.92);
--bg-card: rgba(26,32,43,0.85); --bg-card-elevated: #1b212c; --bg-card-sub: rgba(38,46,60,0.5);
--bg-input: rgba(16,20,28,0.9);
--pill-active-bg: #e8ecf2; --pill-active-text: #161a21;
--pill-inactive-bg: rgba(120,134,156,0.22); --pill-inactive-text: #aab6c6; --pill-hover-bg: rgba(120,134,156,0.34);
--btn-primary-bg: #e8ecf2; --btn-primary-hover: #ffffff; --btn-primary-text: #161a21;
--btn-secondary-bg: rgba(120,134,156,0.22); --btn-secondary-hover: rgba(120,134,156,0.34); --btn-secondary-text: #d5dde8;
--text-main: #e8ecf2; --text-2: #c3ccd9; --text-3: #94a1b3; --text-4: #6b7889;
--border-subtle: rgba(120,134,156,0.30); --border-card: rgba(255,255,255,0.06); --border-input: rgba(120,134,156,0.45);
}
body { font-family: var(--font-game); background: var(--bg-canvas); color: var(--text-main);
max-width: 1180px; margin: 0 auto; padding: 78px 18px 40px; }
.game-header { position: fixed; top: 0; left: 0; right: 0; z-index: 100; height: 50px;
display: flex; align-items: center; justify-content: space-between; padding: 0 16px;
background: var(--bg-header); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
border-bottom: 1px solid var(--border-subtle); }
.ananta-svg-wrap svg, .ananta-svg-wrap img { height: 26px; display: block; color: var(--text-main); }
.theme-toggle { height: 30px; padding: 0 10px; border-radius: 6px; border: 1px solid var(--border-subtle);
background: var(--btn-secondary-bg); color: var(--btn-secondary-text); cursor: pointer; font-size: 14px; margin-left: auto; }
h1 { font-size: 16px; font-weight: 700; color: var(--text-main); margin: 6px 0 12px; letter-spacing: .3px; }
h2 { font-size: 13.5px; font-weight: 700; color: var(--text-2); text-transform: uppercase; letter-spacing: .8px;
border-bottom: 1px solid var(--border-subtle); padding-bottom: 6px; margin: 4px 0 12px; }
.card { background: var(--bg-card); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
border: 1px solid var(--border-card); border-radius: 10px; padding: 16px 18px; margin: 14px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
button { height: 34px; padding: 0 15px; border-radius: 6px; border: 1px solid var(--border-subtle);
background: var(--btn-secondary-bg); color: var(--btn-secondary-text); font-size: 13px; font-weight: 600;
font-family: var(--font-game); cursor: pointer; margin: 3px 4px 3px 0; transition: background .15s ease; }
button:hover { background: var(--btn-secondary-hover); }
button.ghost { background: var(--btn-primary-bg); color: var(--btn-primary-text); border-color: rgba(0,0,0,0.12); }
button.ghost:hover { background: var(--btn-primary-hover); }
.btn-add-npc { height: 24px; padding: 0 10px; font-size: 11px; border-radius: 5px;
background: var(--btn-primary-bg); color: var(--btn-primary-text); }
.btn-add-npc:hover { background: var(--btn-primary-hover); }
input, select, textarea { background: var(--bg-input); color: var(--text-main); border: 1px solid var(--border-input);
border-radius: 6px; padding: 7px 10px; margin: 2px; font-family: var(--font-game); font-size: 13px; }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent-blue);
box-shadow: 0 0 0 3px var(--accent-glow); }
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; }
input[type=range] { width: 100%; margin: 10px 0 2px; accent-color: var(--accent-blue); cursor: pointer; height: auto; border: none; padding: 0; background: transparent; }
input[type=checkbox] { accent-color: var(--accent-blue); }
#clock { font-size: 30px; letter-spacing: 2px; color: var(--text-main); font-weight: 700; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: #888; margin-right: 6px; }
.dot.on { background: var(--green); box-shadow: 0 0 8px rgba(16,185,129,0.6); }
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; }
.picklist { max-height: 180px; overflow-y: auto; border: 1px solid #333; border-radius: 6px; padding: 6px; margin-top: 6px; background: #101215; }
td, th { border: none; border-bottom: 1px solid var(--border-subtle); padding: 6px 8px; text-align: left; font-size: 12.5px; }
tr:hover td { background: var(--bg-card-sub); }
.muted { color: var(--text-3); font-size: 12px; }
#log { white-space: pre-wrap; font-size: 12px; max-height: 160px; overflow-y: auto; font-family: 'Cascadia Mono', Consolas, monospace; }
.npc-tab { height: 28px; padding: 0 12px; border-radius: 999px; border: 1px solid transparent;
background: var(--pill-inactive-bg); color: var(--pill-inactive-text); font-size: 12px; font-weight: 600;
font-family: var(--font-game); cursor: pointer; transition: .15s; }
.npc-tab:hover { background: var(--pill-hover-bg); }
.npc-tab.active { background: var(--pill-active-bg); color: var(--pill-active-text); font-weight: bold; }
.badge-cat { font-size: 10px; padding: 2px 7px; border-radius: 999px; font-weight: bold; display: inline-block; }
.badge-monster { background: rgba(239,68,68,0.14); color: #ff8a80; border: 1px solid rgba(239,68,68,0.4); }
.badge-citizen { background: rgba(43,102,255,0.12); color: #90cdf4; border: 1px solid rgba(43,102,255,0.4); }
.badge-police { background: rgba(183,149,11,0.14); color: #ffcc80; border: 1px solid rgba(183,149,11,0.4); }
.badge-animal { background: rgba(16,185,129,0.12); color: #95d5b2; border: 1px solid rgba(16,185,129,0.4); }
.badge-ally { background: rgba(142,36,170,0.14); color: #e1bee7; border: 1px solid rgba(142,36,170,0.4); }
.picklist { max-height: 180px; overflow-y: auto; border: 1px solid var(--border-subtle); border-radius: 8px;
padding: 6px; margin-top: 6px; background: var(--bg-input); }
.picklist label { display: block; padding: 3px 0; font-size: 12px; cursor: pointer; }
</style>
</head>
<body>
<h1><span id="dot" class="dot"></span>Vehicle debug</h1>
<header class="game-header">
<div style="display:flex;align-items:center"><div class="ananta-svg-wrap" title="ANANTA">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 118" fill="currentColor" fill-rule="evenodd" class="ananta-svg-logo">
<path d="M 386 2 L 388 11 L 390 13 L 390 16 L 393 25 L 395 27 L 398 39 L 400 41 L 401 47 L 405 54 L 406 60 L 411 71 L 411 74 L 413 76 L 416 88 L 418 90 L 422 104 L 426 114 L 428 116 L 455 115 L 445 94 L 445 92 L 447 90 L 457 90 L 461 94 L 475 116 L 510 116 L 510 114 L 502 104 L 501 101 L 473 64 L 472 61 L 439 17 L 433 7 L 428 2 Z M 416 26 L 418 25 L 423 26 L 432 42 L 437 48 L 437 50 L 440 53 L 440 55 L 443 58 L 444 61 L 442 63 L 434 63 L 432 61 Z M 333 2 L 335 7 L 336 17 L 338 20 L 338 24 L 340 29 L 349 29 L 353 33 L 354 40 L 356 42 L 357 50 L 361 59 L 362 68 L 364 70 L 367 85 L 369 87 L 370 95 L 372 98 L 372 102 L 374 104 L 375 112 L 377 116 L 406 116 L 405 108 L 403 106 L 403 102 L 400 93 L 398 91 L 395 79 L 393 76 L 387 58 L 387 55 L 385 52 L 385 49 L 379 34 L 379 32 L 382 29 L 388 29 L 387 23 L 385 21 L 382 8 L 380 6 L 379 2 Z M 260 2 L 260 16 L 262 24 L 263 46 L 265 51 L 266 77 L 268 80 L 269 105 L 271 109 L 272 116 L 299 116 L 298 100 L 296 96 L 296 88 L 294 84 L 293 69 L 291 65 L 292 62 L 294 61 L 297 64 L 297 66 L 299 68 L 299 70 L 307 84 L 307 86 L 310 90 L 310 92 L 312 94 L 312 96 L 315 100 L 321 114 L 323 116 L 356 116 L 353 101 L 351 98 L 350 89 L 346 79 L 345 70 L 341 60 L 340 51 L 338 49 L 338 44 L 336 41 L 335 32 L 333 30 L 333 25 L 330 18 L 330 13 L 328 11 L 328 7 L 326 2 L 304 2 L 305 10 L 307 13 L 307 18 L 309 21 L 310 31 L 312 34 L 312 39 L 310 41 L 308 40 L 291 10 L 291 8 L 286 2 Z M 210 2 L 208 7 L 208 12 L 206 15 L 205 24 L 203 27 L 203 32 L 201 34 L 200 44 L 198 47 L 198 52 L 196 55 L 195 65 L 193 67 L 193 72 L 190 80 L 190 85 L 188 87 L 187 97 L 185 100 L 185 105 L 183 107 L 182 116 L 211 116 L 213 111 L 213 105 L 215 101 L 215 95 L 217 90 L 232 90 L 234 92 L 234 116 L 264 115 L 262 98 L 262 84 L 260 80 L 260 67 L 258 61 L 257 34 L 255 31 L 255 18 L 253 12 L 252 2 Z M 228 25 L 232 25 L 233 27 L 233 61 L 231 63 L 221 62 L 222 51 L 224 47 L 224 40 L 226 37 L 226 30 Z M 202 2 L 179 3 L 177 8 L 177 12 L 175 14 L 172 30 L 170 32 L 169 40 L 166 42 L 164 40 L 164 22 L 162 2 L 135 2 L 133 4 L 130 15 L 128 17 L 125 28 L 123 30 L 120 39 L 120 42 L 112 61 L 112 64 L 110 66 L 109 72 L 105 79 L 104 85 L 99 96 L 99 99 L 97 101 L 94 113 L 92 115 L 125 115 L 127 111 L 128 104 L 130 102 L 130 98 L 132 95 L 133 88 L 135 86 L 135 82 L 138 76 L 138 72 L 141 64 L 143 62 L 146 63 L 146 78 L 148 86 L 148 109 L 150 116 L 175 116 L 177 110 L 177 105 L 179 103 L 180 93 L 182 90 L 182 85 L 184 82 L 185 72 L 187 69 L 190 52 L 192 49 L 193 39 L 195 36 L 198 19 L 200 16 Z M 126 2 L 84 2 L 2 115 L 38 116 L 42 112 L 42 110 L 47 104 L 47 102 L 56 90 L 66 90 L 68 93 L 60 109 L 59 116 L 86 115 L 94 94 L 94 91 L 96 89 L 97 83 L 101 75 L 102 69 L 104 67 L 105 61 L 109 53 L 110 47 L 112 45 L 115 33 L 117 31 L 118 25 L 120 23 L 120 20 L 125 9 Z M 94 24 L 96 26 L 96 28 L 94 30 L 91 39 L 89 41 L 81 60 L 78 63 L 70 63 L 68 60 L 76 49 L 76 47 L 81 41 L 89 27 L 92 24 Z" />
</svg>
</div><div class="brand-title" style="font-size:13px;font-weight:700;color:var(--text-2);margin-left:10px;white-space:nowrap">Private Server Console</div></div>
<button id="themeToggle" class="theme-toggle" title="Toggle theme">🌙</button>
</header>
<h1 style="display:none">Vehicle debug</h1>
<div class="card">
<h2>Status</h2>
@@ -134,7 +191,7 @@
<!-- 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()">
<input id="npcSearchInput" type="text" placeholder="🔍 ค้นหา (พิมพ์ EN/จีน เช่น Security, Placeholder, 占位, Duck หรือเลข 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>
@@ -237,13 +294,6 @@
<div class="card" style="border-color:#3b82f6">
<h2 style="color:#60a5fa">🚇 ระบบขนส่งมวลชน & รถไฟใต้ดิน (Metro Transit & Live Train Tracker)</h2>
<div style="margin-bottom:12px;padding:8px 12px;background:#1e293b;border-radius:6px;display:flex;align-items:center;gap:10px;border:1px solid #3b82f6">
<input id="autoSubwayCutscene" type="checkbox" checked style="width:18px;height:18px;accent-color:#38bdf8;cursor:pointer">
<label for="autoSubwayCutscene" style="font-size:13px;color:#f0f9ff;cursor:pointer;font-weight:600">
🎬 เล่นคัทซีนรถไฟลอดอุโมงค์อัตโนมัติ (เล่นฉากนั่งรถไฟ -> ลอดอุโมงค์ -> โผล่ออกที่อุโมงค์สถานีปลายทาง)
</label>
</div>
<!-- Sub-Tabs Navigation -->
<div style="display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap">
@@ -436,6 +486,7 @@
จำนวน (Count): <input id="giveCount" type="number" value="1" min="1" max="99999" style="width:90px">
<label class="muted" style="margin-left:6px"><input id="giveBind" type="checkbox"> ผูกมัด (Bind)</label>
<button style="background:#f57f17;color:#fff;font-weight:bold" onclick="giveItem()">🎁 เสกไอเทม / Grant</button>
<button style="background:#00838f;color:#fff;font-weight:bold" onclick="grantGachaTickets()">🎟️ เสกตั๋วกาชาทั้งหมด</button>
</div>
<div style="margin-top:6px">
ค้นหาไอเทม: <input id="itemFilter" placeholder="ชื่อไอเทม หรือ ID" style="width:250px" onkeydown="if(event.key==='Enter')renderItems()">
@@ -649,7 +700,7 @@ async function loadNpcCatalog(category = 'all', query = '') {
if (r && r.items) {
npcCatalogData = r.items;
renderNpcTable(npcCatalogData);
if (r.poiActions && document.getElementById('npcPoiSelect').children.length <= 1) {
if (r.poiActions) {
renderPoiSelect(r.poiActions);
}
}
@@ -895,31 +946,6 @@ async function launchArcadeMinigame() {
}
async function warpSubway(x, y, z, facing, stationName, endTimeline) {
const autoCutscene = document.getElementById('autoSubwayCutscene')?.checked ?? true;
if (autoCutscene) {
const sName = stationName || 'สถานีปลายทาง';
log(`🚇 กำลังเดินทางด้วยรถไฟไปยัง ${sName}... (ระบบกำลังเล่นคัทซีนรถไฟอัตโนมัติ)`);
await api('/api/timeline/play', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'loading_metro_normal01_m' })
});
setTimeout(async () => {
await api('/api/player/teleport', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ x: parseFloat(x), y: parseFloat(y), z: parseFloat(z), facing: parseFloat(facing || 0) })
});
await api('/api/timeline/play', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: endTimeline || 'loading_metro_end_1' })
});
log(`🚇 รถไฟเทียบชานชาลาสถานี ${sName} และเดินออกจากอุโมงค์เรียบร้อยแล้ว`);
refresh();
}, 1800);
return;
}
const r = await api('/api/player/teleport', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -1216,6 +1242,14 @@ async function applyCrowdConfig() {
await refreshTrafficStatus();
}
async function trafficLab(key, value) {
const body = {};
body[key] = value;
const r = await api('/api/traffic/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
log('🧪 traffic ' + key + ' = ' + value + ': ' + JSON.stringify(r));
await refreshTrafficStatus();
}
async function spawnTrafficWave() {
const r = await api('/api/traffic/spawn-wave', { method: 'POST' });
log('spawn wave: ' + JSON.stringify(r));
@@ -1330,6 +1364,11 @@ async function giveItem(id) {
}
}
async function grantGachaTickets() {
const r = await api('/api/gacha/grant-tickets', { method: 'POST' });
log('🎟️ ตั๋วกาชา: ' + JSON.stringify(r));
}
async function equipSelected() {
const pick = document.querySelector('input[name=wpn]:checked');
if (!pick) { log('กรุณาเลือกอาวุธก่อนกดสวมใส่'); return; }
@@ -1370,5 +1409,22 @@ document.getElementById('timeslider').addEventListener('change', async () => {
refresh();
});
</script>
<script>
(function () {
var btn = document.getElementById('themeToggle');
if (!btn) return;
var saved = localStorage.getItem('panel-theme');
if (saved) document.documentElement.setAttribute('data-theme', saved);
btn.textContent = document.documentElement.getAttribute('data-theme') === 'dark' ? '☀' : '☽';
btn.addEventListener('click', function () {
var el = document.documentElement;
var next = el.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
el.setAttribute('data-theme', next);
localStorage.setItem('panel-theme', next);
btn.textContent = next === 'dark' ? '☀' : '☽';
});
})();
</script>
</body>
</html>
File diff suppressed because it is too large. Load diff
@@ -7,6 +7,7 @@ using Ananta.Server.Handlers.LoginGate;
using Ananta.Server.Protocol.Client4229938;
using Ananta.Server.Gameplay.Traffic;
using Ananta.Server.Gameplay.Crowd;
using Ananta.Server.Gameplay.World;
using Ananta.Server.Network;
namespace Ananta.Server.App;
@@ -48,6 +49,14 @@ internal sealed class PrivateServerApplication(PrivateServerConfig config)
TrafficAndCrowdDriver.Instance,
() => gameSessions.Current is not null ? [gameSessions.Current] : Array.Empty<TcpSession>());
// Interactive gadget + scene-item grid streaming (vending machines, arcades, doors,
// lampposts, props from the client's baked cell data) — server-side equivalent of the
// AnantaGadgetLoader / AnantaWorldMod injects.
GadgetStreamingEngine.Instance.Initialize(
clientDataFolder,
TrafficAndCrowdDriver.Instance,
() => gameSessions.Current is not null ? [gameSessions.Current] : Array.Empty<TcpSession>());
// Localhost debug API for debug-panel.py (optional, never blocks the game stack).
using var debugApi = config.Debug.Enabled ? new DebugApiServer(config, gameSessions) : null;
var webInfo = "web=off";