diff --git a/Ananta.Server/Ananta.App/DebugApiServer.cs b/Ananta.Server/Ananta.App/DebugApiServer.cs index 3e90b87..a7452c1 100644 --- a/Ananta.Server/Ananta.App/DebugApiServer.cs +++ b/Ananta.Server/Ananta.App/DebugApiServer.cs @@ -1270,6 +1270,15 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub new("switchchar_common_26", 105u, "Polite Photo with NPC (ถ่ายรูปคู่กับ NPC อย่างสุภาพ)", "urban", 1889.75f, 0f, 2211.68f), new("switchchar_common_19", 56u, "Tossing Can in Trash Bin (โยนกระป๋องลงถังขยะลงเป๊ะ)", "urban", 1717.25f, 0.08f, 2271.25f), new("switchchar_character_1", 88u, "Buying Drink at Vending Machine (หยอดเหรียญกดตู้เครื่องดื่ม)", "urban", 3010.08f, 0f, 1656.21f), + + // Metro & Transit Timelines + new("loading_metro_normal01_m", 0u, "Metro Transit: Male MC Riding Train (นั่งรถไฟใต้ดิน - ตัวละครชาย)", "transit"), + new("loading_metro_normal01_f", 0u, "Metro Transit: Female MC Riding Train (นั่งรถไฟใต้ดิน - ตัวละครหญิง)", "transit"), + new("loading_metro_normal02", 0u, "Metro Transit: Carriage Passengers (บรรยากาศผู้โดยสารบนขบวนรถ)", "transit"), + new("loading_metro_high01", 0u, "Metro Transit: High-Angle Window View (วิวมุมสูงบนรถไฟ)", "transit"), + new("loading_metro_middle01", 0u, "Metro Transit: Carriage Interior (บรรยากาศในตู้โดยสาร)", "transit"), + new("loading_metro_low01", 0u, "Metro Transit: Low-Angle Track View (วิวมุมมองรางรถไฟ)", "transit"), + new("loading_metro_end_1", 0u, "Metro Transit: Arrival at Station (รถไฟเทียบชานชาลาปลายทาง)", "transit"), ]; private async Task PlayTimelineAsync(string json, CancellationToken token) diff --git a/Ananta.Server/Ananta.App/DebugPanel/index.html b/Ananta.Server/Ananta.App/DebugPanel/index.html index ec90e42..a761088 100644 --- a/Ananta.Server/Ananta.App/DebugPanel/index.html +++ b/Ananta.Server/Ananta.App/DebugPanel/index.html @@ -234,39 +234,95 @@
-

🚇 สถานีรถไฟ & รถไฟใต้ดิน (Subway & Metro Fast Travel)

-
🏙️ Nova City Metro (12 สถานี):
-
- - - - - - - - - - - - +

🚇 ระบบขนส่งมวลชน & รถไฟใต้ดิน (Metro Transit & Live Train Tracker)

+ + +
+ + +
-
🏮 Longqi Metro (13 สถานี):
-
- - - - - - - - - - - - - + + +
+
+ เลือกสายรถไฟ: + + +
+ + +
+
+ + +
🚆 ขบวนรถไฟที่กำลังวิ่งบนสายนี้ (Live Active Trains):
+
+
+ + +
🚉 เส้นทางสถานี & จุดขึ้นขบวนรถ (Station Route & Boarding):
+
+
+
💡 คลิก ⚡ วาร์ปด่วน เพื่อไปที่ชานชาลาทันที หรือคลิก 🎬 ขึ้นรถไฟ เพื่อเล่นคัทซีนนั่งในตู้โดยสารก่อนวาร์ปไปถึง
+
+ + + + + + -
วาร์ปไปยังสถานีรถไฟใต้ดิน (Subway / Metro Stations) ทั่วทั้ง Nova City และ Longqi ทันที
@@ -902,6 +958,247 @@ async function teleportToBowling() { log('🎳 Warped to Bowling Alley at (1090.8, 6.5, 2083.7)'); } +// --- TRANSIT & METRO TRACKER --- +function switchTransitTab(tab) { + const tabs = ['live', 'stations', 'cinematic']; + tabs.forEach(t => { + const pane = document.getElementById('transitPane_' + t); + const btn = document.getElementById('tabBtn_' + t); + if (pane) pane.style.display = (t === tab) ? 'block' : 'none'; + if (btn) { + if (t === tab) btn.classList.add('active'); + else btn.classList.remove('active'); + } + }); +} + +async function playCutsceneDirect(tlName) { + const r = await api('/api/timeline/play', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: tlName }) + }); + log(`🎬 Played Metro Cutscene (${tlName}): ` + JSON.stringify(r)); +} + +async function rideMetroWithCinematic(x, y, z, stationName) { + log(`🚇 กำลังขึ้นรถไฟเพื่อมุ่งหน้าไปยังสถานี ${stationName}... เริ่มเล่นคัทซีนเดินทาง`); + await api('/api/timeline/play', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'loading_metro_normal01_m' }) + }); + setTimeout(async () => { + await warpSubway(x, y, z, 0); + await api('/api/timeline/play', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'loading_metro_end_1' }) + }); + log(`🚇 เดินทางถึงสถานี ${stationName} เรียบร้อยแล้ว! (ชานชาลาเปิดประตู)`); + }, 2500); +} + +const RAIL_LINES = [ + { + id: 1, + name: "Line 1: Bayshore Line (Inner Ring / ตามเข็มนาฬิกา)", + color: "#38bdf8", + interval: 90, + speed: 12, + trainCount: 6, + totalDuration: 1120, + stations: [ + { id: 1, name: "Ivory Cove", cn: "白帆湾", x: 539.6, y: 20.5, z: 1363.8, t: 0 }, + { id: 2, name: "Paradox Plaza", cn: "狂悖街区", x: 777.0, y: 14.7, z: 2427.2, t: 150 }, + { id: 3, name: "Sunny Bay", cn: "晴海", x: 1988.8, y: 19.9, z: 2779.2, t: 320 }, + { id: 4, name: "City Link (Hub)", cn: "城关 (ชุมทาง)", x: 2968.2, y: 42.1, z: 2467.9, t: 490 }, + { id: 5, name: "Legacy Hub", cn: "纪事区", x: 3469.2, y: -17.8, z: 1724.5, t: 660 }, + { id: 6, name: "Floramoon Park", cn: "花月公园", x: 2560.1, y: 33.7, z: 1675.7, t: 820 }, + { id: 7, name: "Freshville East", cn: "东新筑", x: 1088.4, y: 18.5, z: 1875.8, t: 980 } + ] + }, + { + id: 2, + name: "Line 2: Bayshore Line (Outer Ring / ทวนเข็มนาฬิกา)", + color: "#06b6d4", + interval: 90, + speed: 12, + trainCount: 6, + totalDuration: 1120, + stations: [ + { id: 1, name: "Ivory Cove", cn: "白帆湾", x: 539.6, y: 20.5, z: 1363.8, t: 0 }, + { id: 7, name: "Freshville East", cn: "东新筑", x: 1088.4, y: 18.5, z: 1875.8, t: 140 }, + { id: 6, name: "Floramoon Park", cn: "花月公园", x: 2560.1, y: 33.7, z: 1675.7, t: 300 }, + { id: 5, name: "Legacy Hub", cn: "纪事区", x: 3469.2, y: -17.8, z: 1724.5, t: 460 }, + { id: 4, name: "City Link (Hub)", cn: "城关 (ชุมทาง)", x: 2968.2, y: 42.1, z: 2467.9, t: 630 }, + { id: 3, name: "Sunny Bay", cn: "晴海", x: 1988.8, y: 19.9, z: 2779.2, t: 800 }, + { id: 2, name: "Paradox Plaza", cn: "狂悖街区", x: 777.0, y: 14.7, z: 2427.2, t: 970 } + ] + }, + { + id: 3, + name: "Line 3: Central Loop Line (สายวนรอบใจกลางเมือง)", + color: "#eab308", + interval: 90, + speed: 12, + trainCount: 4, + totalDuration: 740, + stations: [ + { id: 6, name: "Floramoon Park", cn: "花月公园", x: 2560.1, y: 33.7, z: 1675.7, t: 0 }, + { id: 11, name: "Marine MechWorks", cn: "岸泊工业", x: 1739.0, y: 29.3, z: 1379.2, t: 130 }, + { id: 12, name: "Union Square", cn: "统理院", x: 1397.0, y: -8.5, z: 2151.9, t: 280 }, + { id: 13, name: "Sunny Bay Central", cn: "晴海中央", x: 2003.8, y: 10.3, z: 2712.8, t: 430 }, + { id: 4, name: "City Link (Hub)", cn: "城关 (ชุมทาง)", x: 2968.2, y: 42.1, z: 2467.9, t: 590 } + ] + }, + { + id: 4, + name: "Line 4: East Loop Line (สายวนรอบเขตธุรกิจตะวันออก)", + color: "#22c55e", + interval: 90, + speed: 10, + trainCount: 4, + totalDuration: 640, + stations: [ + { id: 4, name: "City Link (Hub)", cn: "城关 (ชุมทาง)", x: 2968.2, y: 42.1, z: 2467.9, t: 0 }, + { id: 21, name: "Aerie Bank", cn: "鹰庭银行", x: 3412.4, y: 25.0, z: 2244.6, t: 120 }, + { id: 22, name: "Saturn Tower", cn: "土星塔", x: 3865.9, y: 45.2, z: 1738.3, t: 250 }, + { id: 23, name: "Pop Cool City", cn: "砰酷中心", x: 3629.9, y: 47.6, z: 1302.7, t: 380 }, + { id: 24, name: "Crystal Place", cn: "璃色", x: 3134.9, y: 42.0, z: 1460.4, t: 510 } + ] + }, + { + id: 9, + name: "Longqi Metro Line 9 (รถไฟใต้ดินเขตหลงฉี สาย 9)", + color: "#a855f7", + interval: 100, + speed: 12, + trainCount: 4, + totalDuration: 700, + stations: [ + { id: 41, name: "Mingchun North", cn: "明春北", x: -503.1, y: -9.1, z: -528.8, t: 0 }, + { id: 42, name: "Cloudstream West", cn: "云河西路", x: -1302.9, y: -23.8, z: -642.3, t: 140 }, + { id: 43, name: "Lakefront Promenade", cn: "临湖步行街", x: -1711.3, y: -24.0, z: -50.5, t: 280 }, + { id: 44, name: "Dinglan Road", cn: "定澜路", x: -1474.2, y: -24.1, z: 785.7, t: 420 }, + { id: 45, name: "Wulin Road", cn: "武林路", x: -1512.1, y: -14.2, z: 1095.9, t: 560 } + ] + } +]; + +function getSelectedRailLine() { + const sel = document.getElementById('railLineSelect'); + const lineId = parseInt(sel?.value || '1', 10); + return RAIL_LINES.find(l => l.id === lineId) || RAIL_LINES[0]; +} + +function onSelectRailLine() { + const line = getSelectedRailLine(); + const banner = document.getElementById('lineInfoBanner'); + if (banner) { + banner.style.borderLeftColor = line.color; + banner.innerHTML = ` +
${line.name}
+
+ ⏱️ ความถี่ขบวนรถ: ${line.interval} วินาที + 🚄 ความเร็วเฉลี่ย: ${line.speed} m/s (~${Math.round(line.speed * 3.6)} km/h) + 🕒 รอบเวลาทั้งสาย: ${Math.round(line.totalDuration / 60)} นาที + 🚆 จำนวนขบวนให้บริการ: ${line.trainCount} ขบวน +
+ `; + } + + const routeBox = document.getElementById('routeStationsContainer'); + if (routeBox) { + let html = '
'; + line.stations.forEach((st, idx) => { + const isHub = st.name.includes('(Hub)'); + html += ` +
+
+ #${idx + 1} + ${st.name} + [${st.cn}] + ${isHub ? 'GRAND INTERCHANGE' : ''} +
+
+ + +
+
+ `; + }); + html += '
'; + routeBox.innerHTML = html; + } + + refreshTransitTracker(); +} + +function refreshTransitTracker() { + const line = getSelectedRailLine(); + const container = document.getElementById('activeTrainsContainer'); + if (!container) return; + + const now = Math.floor(Date.now() / 1000); + let html = ''; + + for (let i = 0; i < line.trainCount; i++) { + const trainNum = (line.id * 100) + (i + 1); + const trainProgress = (now + (i * line.interval)) % line.totalDuration; + + let prevSt = line.stations[0]; + let nextSt = line.stations[1]; + let isAtStation = false; + let dwellLeft = 0; + + for (let s = 0; s < line.stations.length; s++) { + const cur = line.stations[s]; + const nxt = line.stations[(s + 1) % line.stations.length]; + const curT = cur.t; + let nxtT = nxt.t; + if (nxtT <= curT) nxtT += line.totalDuration; + + let p = trainProgress; + if (p < curT) p += line.totalDuration; + + if (p >= curT && p < nxtT) { + prevSt = cur; + nextSt = nxt; + const dwellTime = 20; + if (p - curT < dwellTime) { + isAtStation = true; + dwellLeft = Math.max(1, Math.round(dwellTime - (p - curT))); + } + break; + } + } + + const etaSeconds = Math.max(1, Math.round( + (nextSt.t <= prevSt.t ? nextSt.t + line.totalDuration : nextSt.t) - + (trainProgress < prevSt.t ? trainProgress + line.totalDuration : trainProgress) + )); + + html += ` +
+
+ 🚆 ขบวน #${trainNum} + ความเร็ว: ${line.speed} m/s +
+ ${isAtStation ? ` +
🟢 จอดเทียบชานชาลาที่ ${prevSt.name}
+
ประตูชานชาลาเปิดอยู่ · ออกเดินทางใน ${dwellLeft}s
+ ` : ` +
⏩ กำลังมุ่งหน้าไป ${nextSt.name}
+
ออกจาก ${prevSt.name} · ถึงชานชาลาในอีก ${etaSeconds}s
+ `} +
+ `; + } + + container.innerHTML = html; +} + async function setTimePreset(hour, minute) { const slider = document.getElementById('timeslider'); if (slider) { @@ -1009,6 +1306,8 @@ refresh(); setInterval(refresh, 2000); refreshTrafficStatus(); setInterval(refreshTrafficStatus, 2500); +onSelectRailLine(); +setInterval(refreshTransitTracker, 1000); document.getElementById('timeslider').addEventListener('input', e => { timeDragging = true; document.getElementById('clock').textContent = fmtClock(parseInt(e.target.value || '720', 10));