diff --git a/src/components/IranBaseStatusPanel.tsx b/src/components/IranBaseStatusPanel.tsx
index 6661429..464f9b8 100644
--- a/src/components/IranBaseStatusPanel.tsx
+++ b/src/components/IranBaseStatusPanel.tsx
@@ -32,7 +32,6 @@ export function IranBaseStatusPanel({ keyLocations = [], className = '' }: IranB
diff --git a/src/components/WarMap.tsx b/src/components/WarMap.tsx
index cd496c1..3d2db1d 100644
--- a/src/components/WarMap.tsx
+++ b/src/components/WarMap.tsx
@@ -60,6 +60,8 @@ const TEHRAN_SOURCE: [number, number] = [51.389, 35.6892]
/** 攻击动画时间衰减:N 天内按天衰减(脉冲缩小、频次降低),超出仅保留减弱呼吸效果;天数可在编辑面板配置 */
const MS_PER_DAY = 24 * 60 * 60 * 1000
+/** 手动添加的打击源:始终参与实时渲染列表,用 strikeCutoffDays 做衰减;仅在图例选中该项时不衰减 */
+const MANUAL_STRIKE_SOURCE_IDS = new Set(['virginia_srilanka', 'tomahawk_arabian', 'arabian_sea_sub_torpedo'])
function isWithinAnimationWindow(
iso: string | null | undefined,
referenceTime: string,
@@ -105,8 +107,11 @@ const FALLBACK_STRIKE_SOURCES: { id: string; name: string; lng: number; lat: num
{ id: 'israel', name: '以色列', lng: 34.78, lat: 32.08 },
{ id: 'lincoln', name: '林肯号航母', lng: 58.4215, lat: 24.1568 },
{ id: 'ford', name: '福特号航母', lng: 24.1002, lat: 35.7397 },
+ { id: 'virginia_srilanka', name: '洛杉矶级攻击核潜艇(斯里兰卡外海)', lng: 78.5, lat: 5.2 },
+ { id: 'tomahawk_arabian', name: '阿拉伯海潜艇(战斧)', lng: 59.2, lat: 23.0 },
+ { id: 'arabian_sea_sub_torpedo', name: '阿拉伯海潜艇(鱼雷)', lng: 59.2, lat: 23.0 },
]
-const FALLBACK_STRIKE_LINES: { sourceId: string; targets: { lng: number; lat: number; name?: string }[] }[] = [
+const FALLBACK_STRIKE_LINES: { sourceId: string; targets: { lng: number; lat: number; name?: string; struck_at?: string | null }[] }[] = [
{
sourceId: 'israel',
targets: [
@@ -143,6 +148,24 @@ const FALLBACK_STRIKE_LINES: { sourceId: string; targets: { lng: number; lat: nu
{ lng: 48.35, lat: 33.48, name: '霍拉马巴德储备库' },
],
},
+ {
+ sourceId: 'virginia_srilanka',
+ targets: [
+ { lng: 79.8, lat: 6.5, name: '德纳号轻型护卫舰', struck_at: '2026-02-28T08:00:00.000Z' },
+ ],
+ },
+ {
+ sourceId: 'tomahawk_arabian',
+ targets: [
+ { lng: 56.26, lat: 27.18, name: '沙希德·鲁德基号/无人机航母', struck_at: '2026-02-28T03:00:00.000Z' },
+ ],
+ },
+ {
+ sourceId: 'arabian_sea_sub_torpedo',
+ targets: [
+ { lng: 56.26, lat: 27.18, name: '沙希德·鲁德基号/无人机航母', struck_at: '2026-02-28T03:00:00.000Z' },
+ ],
+ },
]
/** 二次贝塞尔曲线路径,更平滑的弧线 height 控制弧高 */
@@ -232,6 +255,9 @@ export function WarMap() {
const lincolnPathsRef = useRef<[number, number][][]>([])
const fordPathsRef = useRef<[number, number][][]>([])
const israelPathsRef = useRef<[number, number][][]>([])
+ const virginiaPathsRef = useRef<[number, number][][]>([])
+ const tomahawkPathsRef = useRef<[number, number][][]>([])
+ const arabianSubTorpedoPathsRef = useRef<[number, number][][]>([])
const hezbollahPathsRef = useRef<[number, number][][]>([])
const hormuzPathsRef = useRef<[number, number][][]>([])
const [legendOpen, setLegendOpen] = useState(true)
@@ -240,6 +266,8 @@ export function WarMap() {
| 'lincoln'
| 'ford'
| 'israel'
+ | 'virginia_srilanka'
+ | 'tomahawk_arabian'
| 'hezbollah'
| 'iranToUs'
| 'iran'
@@ -356,21 +384,40 @@ export function WarMap() {
}, [usForces.keyLocations, iranForces.keyLocations])
const mapData = situation.mapData
- const strikeSources =
- mapData?.strikeSources?.length > 0 ? mapData.strikeSources : FALLBACK_STRIKE_SOURCES
- const strikeLines =
- mapData?.strikeLines?.length > 0 ? mapData.strikeLines : FALLBACK_STRIKE_LINES
+ /** 打击源:API 有则用,并合并手动场景源(斯里兰卡潜艇、阿巴斯港战斧+潜艇鱼雷),缺则从 fallback 补全 */
+ const strikeSources = useMemo(() => {
+ const fromApi = mapData?.strikeSources?.length > 0 ? mapData.strikeSources : FALLBACK_STRIKE_SOURCES
+ const ids = new Set((fromApi as { id: string }[]).map((s) => s.id))
+ const toAdd = FALLBACK_STRIKE_SOURCES.filter((s) => !ids.has(s.id))
+ return toAdd.length > 0 ? [...fromApi, ...toAdd] : fromApi
+ }, [mapData?.strikeSources])
+ /** 打击线:API 有则用 API,并始终合并斯里兰卡/阿巴斯港(战斧+潜艇) fallback,保证场景必有数据可渲染 */
+ const strikeLines = useMemo(() => {
+ const fromApi = mapData?.strikeLines?.length > 0 ? mapData.strikeLines : FALLBACK_STRIKE_LINES
+ const hasVirginia = fromApi.some((l) => l.sourceId === 'virginia_srilanka')
+ const hasTomahawk = fromApi.some((l) => l.sourceId === 'tomahawk_arabian')
+ const hasArabianSubTorpedo = fromApi.some((l) => l.sourceId === 'arabian_sea_sub_torpedo')
+ const extra = [
+ ...(hasVirginia ? [] : FALLBACK_STRIKE_LINES.filter((l) => l.sourceId === 'virginia_srilanka')),
+ ...(hasTomahawk ? [] : FALLBACK_STRIKE_LINES.filter((l) => l.sourceId === 'tomahawk_arabian')),
+ ...(hasArabianSubTorpedo ? [] : FALLBACK_STRIKE_LINES.filter((l) => l.sourceId === 'arabian_sea_sub_torpedo')),
+ ]
+ return extra.length > 0 ? [...fromApi, ...extra] : fromApi
+ }, [mapData?.strikeLines])
/** 伊朗→美军基地:仅用 DB 数据,N 天内显示飞行动画 */
const strikeCutoffDays = situation.animationConfig?.strikeCutoffDays ?? 5
- /** 实时模式:仅渲染最近 1 天内的进展(主视角);默认开启 */
- const REALTIME_CUTOFF_DAYS = 1
+ /** 实时模式:渲染最近 5 天内的进展(主视角);全部=不限时长 */
+ const REALTIME_CUTOFF_DAYS = 5
+ const UNLIMITED_CUTOFF_DAYS = 365
const [isRealtimeView, setIsRealtimeView] = useState(true)
const isRealtimeViewRef = useRef(true)
isRealtimeViewRef.current = isRealtimeView
- /** 未选中单项时:全部=用配置天数,实时=1 天;选中某项时用配置天数(该单项完整动画) */
+ /** 未选中单项时:全部=不限时长(365天),实时=5 天;选中某项时用 strikeCutoffDays(该单项完整动画) */
const effectiveCutoffDays =
- strikeLegendFilter === null && isRealtimeView ? REALTIME_CUTOFF_DAYS : strikeCutoffDays
+ strikeLegendFilter === null
+ ? (isRealtimeView ? REALTIME_CUTOFF_DAYS : UNLIMITED_CUTOFF_DAYS)
+ : strikeCutoffDays
const attackPaths = useMemo(() => {
const attacked = (usForces.keyLocations || []).filter(
@@ -396,7 +443,7 @@ export function WarMap() {
return m
}, [strikeSources])
- /** 盟军打击线:仅用 effectiveCutoffDays 内目标显示飞行动画(全部=配置天数,实时=1 天) */
+ /** 盟军打击线:仅用 effectiveCutoffDays 内目标显示飞行动画(全部=不限时长,实时=5 天) */
const filterTargetsByAnimationWindow = useMemo(
() => (targets: { lng: number; lat: number; struck_at?: string | null }[]) =>
targets.filter((t) => isWithinAnimationWindow(t.struck_at ?? null, referenceTime, effectiveCutoffDays)),
@@ -448,6 +495,41 @@ export function WarMap() {
return line.targets.map((t) => parabolaPath(coords, [t.lng, t.lat]))
}, [strikeLines, sourceCoords])
+ /** 场景 A:斯里兰卡深海伏击 — 鱼雷轨迹为直线(潜艇 [78.5,5.2] → 驱逐舰 [79.8,6.5]),不采用抛物线 */
+ const virginiaSrilankaPaths = useMemo(() => {
+ const line = strikeLines.find((l) => l.sourceId === 'virginia_srilanka')
+ const coords = sourceCoords.virginia_srilanka
+ if (!coords || !line) return []
+ return line.targets.map((t) => [coords, [t.lng, t.lat]] as [number, number][])
+ }, [strikeLines, sourceCoords])
+ /** 场景 B:阿拉伯海战斧 → 阿巴斯港,抛物线弹道 */
+ const tomahawkArabianPaths = useMemo(() => {
+ const line = strikeLines.find((l) => l.sourceId === 'tomahawk_arabian')
+ const coords = sourceCoords.tomahawk_arabian
+ if (!coords || !line) return []
+ return line.targets.map((t) => parabolaPath(coords, [t.lng, t.lat]))
+ }, [strikeLines, sourceCoords])
+ /** 上述两场景在时间窗口内的路径(与林肯/福特/以色列一致,用于实时模式) */
+ const effectiveVirginiaSrilankaPaths = useMemo(() => {
+ const line = strikeLines.find((l) => l.sourceId === 'virginia_srilanka')
+ const coords = sourceCoords.virginia_srilanka
+ if (!coords || !line) return []
+ return filterTargetsByAnimationWindow(line.targets).map((t) => [coords, [t.lng, t.lat]] as [number, number][])
+ }, [strikeLines, sourceCoords, filterTargetsByAnimationWindow])
+ const effectiveTomahawkArabianPaths = useMemo(() => {
+ const line = strikeLines.find((l) => l.sourceId === 'tomahawk_arabian')
+ const coords = sourceCoords.tomahawk_arabian
+ if (!coords || !line) return []
+ return filterTargetsByAnimationWindow(line.targets).map((t) => parabolaPath(coords, [t.lng, t.lat]))
+ }, [strikeLines, sourceCoords, filterTargetsByAnimationWindow])
+ /** 场景 B:阿巴斯港 — 同艇潜射/鱼雷轨迹(直线,与战斧同时) */
+ const arabianSubTorpedoPaths = useMemo(() => {
+ const line = strikeLines.find((l) => l.sourceId === 'arabian_sea_sub_torpedo')
+ const coords = sourceCoords.arabian_sea_sub_torpedo
+ if (!coords || !line) return []
+ return line.targets.map((t) => [coords, [t.lng, t.lat]] as [number, number][])
+ }, [strikeLines, sourceCoords])
+
/** 黎巴嫩→以色列:攻击源为黎巴嫩境内多处(提尔、西顿、巴勒贝克等),目标为以色列北部 */
const hezbollahPaths = useMemo(() => {
const sources = EXTENDED_WAR_ZONES.lebanonStrikeSources
@@ -481,21 +563,24 @@ export function WarMap() {
const warMapData = useWarMapData()
- /** 当前参与动画的目标的最小衰减系数,用于脉冲范围与攻击频次(缩小脉冲、拉长飞行周期) */
+ /** 当前参与动画的目标的最小衰减系数;手动打击(virginia/tomahawk)用 strikeCutoffDays 参与衰减,其余用 effectiveCutoffDays */
const animationDecayFactor = useMemo(() => {
const decays: number[] = []
;(usForces.keyLocations || [])
.filter((l) => l.status === 'attacked' && l.attacked_at && isWithinAnimationWindow(l.attacked_at, referenceTime, effectiveCutoffDays))
.forEach((l) => decays.push(getDecayFactor(l.attacked_at ?? null, referenceTime, effectiveCutoffDays)))
for (const line of strikeLines) {
- for (const t of filterTargetsByAnimationWindow(line.targets)) {
- decays.push(getDecayFactor(t.struck_at ?? null, referenceTime, effectiveCutoffDays))
+ const cutoffDays = MANUAL_STRIKE_SOURCE_IDS.has(line.sourceId) ? strikeCutoffDays : effectiveCutoffDays
+ const inWindow = (t: { struck_at?: string | null }) => isWithinAnimationWindow(t.struck_at ?? null, referenceTime, cutoffDays)
+ for (const t of line.targets) {
+ if (!inWindow(t)) continue
+ decays.push(getDecayFactor((t as { struck_at?: string | null }).struck_at ?? null, referenceTime, cutoffDays))
}
}
if (hormuzPaths.length > 0) decays.push(1)
if (hezbollahPaths.length > 0) decays.push(1)
return decays.length > 0 ? Math.min(...decays) : 1
- }, [usForces.keyLocations, strikeLines, referenceTime, effectiveCutoffDays, filterTargetsByAnimationWindow, hormuzPaths.length, hezbollahPaths.length])
+ }, [usForces.keyLocations, strikeLines, referenceTime, effectiveCutoffDays, strikeCutoffDays, hormuzPaths.length, hezbollahPaths.length])
const animationDecayRef = useRef(1)
animationDecayRef.current = animationDecayFactor
@@ -534,9 +619,31 @@ export function WarMap() {
const effectiveHormuzPaths =
strikeLegendFilter === 'hormuz' || strikeLegendFilter === 'iran' ? hormuzPaths : strikeLegendFilter === null ? hormuzPaths : []
+ /** 斯里兰卡伏击 / 阿巴斯港突袭(手动打击):实时模式下始终进入渲染列表并做衰减;图例选中时完整动画不衰减 */
+ const effectiveVirginiaSrilankaPathsDisplay =
+ strikeLegendFilter === 'virginia_srilanka'
+ ? virginiaSrilankaPaths
+ : strikeLegendFilter === null
+ ? virginiaSrilankaPaths
+ : []
+ const effectiveTomahawkArabianPathsDisplay =
+ strikeLegendFilter === 'tomahawk_arabian'
+ ? tomahawkArabianPaths
+ : strikeLegendFilter === null
+ ? tomahawkArabianPaths
+ : []
+ /** 阿巴斯港潜艇直线(与战斧同时):与 tomahawk_arabian 同显 */
+ const effectiveArabianSubTorpedoPathsDisplay =
+ strikeLegendFilter === 'tomahawk_arabian' || strikeLegendFilter === null
+ ? arabianSubTorpedoPaths
+ : []
+
lincolnPathsRef.current = effectiveLincolnPaths
fordPathsRef.current = effectiveFordPaths
israelPathsRef.current = effectiveIsraelPaths
+ virginiaPathsRef.current = effectiveVirginiaSrilankaPathsDisplay
+ tomahawkPathsRef.current = effectiveTomahawkArabianPathsDisplay
+ arabianSubTorpedoPathsRef.current = effectiveArabianSubTorpedoPathsDisplay
hezbollahPathsRef.current = effectiveHezbollahPaths
hormuzPathsRef.current = effectiveHormuzPaths
attackPathsRef.current = effectiveAttackPaths
@@ -566,6 +673,38 @@ export function WarMap() {
if (sourceCoords.israel) push(sourceCoords.israel)
flattenPaths(israelPathsAll)
break
+ case 'virginia_srilanka': {
+ const src = sourceCoords.virginia_srilanka
+ const paths = effectiveVirginiaSrilankaPathsDisplay
+ if (src && paths.length > 0) {
+ const tgt = paths[0][paths[0].length - 1]
+ const padLine = 1.2
+ return [
+ Math.min(src[0], tgt[0]) - padLine,
+ Math.min(src[1], tgt[1]) - padLine,
+ Math.max(src[0], tgt[0]) + padLine,
+ Math.max(src[1], tgt[1]) + padLine,
+ ]
+ }
+ break
+ }
+ case 'tomahawk_arabian': {
+ const src = sourceCoords.tomahawk_arabian ?? sourceCoords.arabian_sea_sub_torpedo
+ const paths = [...effectiveTomahawkArabianPathsDisplay, ...effectiveArabianSubTorpedoPathsDisplay]
+ if (src && paths.length > 0) {
+ const allCoords = paths.flatMap((p) => p)
+ const lngs = allCoords.map((c) => c[0])
+ const lats = allCoords.map((c) => c[1])
+ const padLine = 1.2
+ return [
+ Math.min(...lngs) - padLine,
+ Math.min(...lats) - padLine,
+ Math.max(...lngs) + padLine,
+ Math.max(...lats) + padLine,
+ ]
+ }
+ break
+ }
case 'hezbollah':
flattenPaths(hezbollahPaths)
break
@@ -616,6 +755,9 @@ export function WarMap() {
lincolnPathsAll,
fordPathsAll,
israelPathsAll,
+ effectiveVirginiaSrilankaPathsDisplay,
+ effectiveTomahawkArabianPathsDisplay,
+ effectiveArabianSubTorpedoPathsDisplay,
hezbollahPaths,
attackPathsAll,
]
@@ -660,16 +802,113 @@ export function WarMap() {
}),
[effectiveIsraelPaths]
)
- /** 盟军打击目标点位(林肯/福特/以色列→伊朗+以色列→黎巴嫩)。全部/单项:显示全部目标;实时:仅 effectiveCutoffDays 内 */
+ /** 场景 A:斯里兰卡鱼雷轨迹 — 直线、蓝色虚线(Mk 48 ADCAP 声纳制导路径) */
+ const submarineTorpedoLinesGeoJson = useMemo(
+ () => ({
+ type: 'FeatureCollection' as const,
+ features:
+ strikeLegendFilter === null || strikeLegendFilter === 'virginia_srilanka'
+ ? effectiveVirginiaSrilankaPathsDisplay.map((coords) => ({
+ type: 'Feature' as const,
+ properties: { weapon: 'Mk 48 Torpedo', status: 'Impact Confirmed' },
+ geometry: { type: 'LineString' as const, coordinates: coords },
+ }))
+ : [],
+ }),
+ [effectiveVirginiaSrilankaPathsDisplay, strikeLegendFilter]
+ )
+ /** 场景 B:阿拉伯海战斧弹道 — 抛物线(阿巴斯港/沙希德·鲁德基号) */
+ const tomahawkStrikeLinesGeoJson = useMemo(
+ () => ({
+ type: 'FeatureCollection' as const,
+ features:
+ strikeLegendFilter === null || strikeLegendFilter === 'tomahawk_arabian'
+ ? effectiveTomahawkArabianPathsDisplay.map((coords) => ({
+ type: 'Feature' as const,
+ properties: { target: 'Drone Carrier Shahid Roudaki', damage: 'Critical/Sunk' },
+ geometry: { type: 'LineString' as const, coordinates: coords },
+ }))
+ : [],
+ }),
+ [effectiveTomahawkArabianPathsDisplay, strikeLegendFilter]
+ )
+ /** 场景 B:阿巴斯港同艇潜射/鱼雷 — 直线(与战斧同时) */
+ const arabianSubTorpedoLinesGeoJson = useMemo(
+ () => ({
+ type: 'FeatureCollection' as const,
+ features:
+ strikeLegendFilter === null || strikeLegendFilter === 'tomahawk_arabian'
+ ? effectiveArabianSubTorpedoPathsDisplay.map((coords) => ({
+ type: 'Feature' as const,
+ properties: { weapon: 'Sub torpedo', status: 'Impact Confirmed' },
+ geometry: { type: 'LineString' as const, coordinates: coords },
+ }))
+ : [],
+ }),
+ [effectiveArabianSubTorpedoPathsDisplay, strikeLegendFilter]
+ )
+ /** 战场态势标注:红圈×斯里兰卡外海驱逐舰沉没;紫色◆阿巴斯港高价值目标摧毁 */
+ const tacticalSymbolsGeoJson = useMemo(() => {
+ const showAll = strikeLegendFilter === null
+ const showVirginia = strikeLegendFilter === 'virginia_srilanka'
+ const showTomahawk = strikeLegendFilter === 'tomahawk_arabian'
+ const features: GeoJSON.Feature[] = []
+ if (showAll || showVirginia) {
+ features.push({
+ type: 'Feature' as const,
+ properties: { symbol: 'destruction', name: 'Destroyed Surface Combatant' },
+ geometry: { type: 'Point' as const, coordinates: [79.8, 6.5] as [number, number] },
+ })
+ }
+ if (showAll || showTomahawk) {
+ features.push({
+ type: 'Feature' as const,
+ properties: {
+ symbol: 'explosion',
+ name: 'Port Facility/High Value Asset Neutralized',
+ target: 'Drone Carrier Shahid Roudaki',
+ damage: 'Critical/Sunk',
+ },
+ geometry: { type: 'Point' as const, coordinates: [56.26, 27.18] as [number, number] },
+ })
+ }
+ return { type: 'FeatureCollection' as const, features }
+ }, [strikeLegendFilter])
+ /** 盟军打击发起点地标(林肯/福特/以色列/斯里兰卡潜艇/阿拉伯海潜艇):样式统一,仅阵营颜色不同 */
+ const alliedStrikeSourcesGeoJson = useMemo(
+ () => ({
+ type: 'FeatureCollection' as const,
+ features: strikeSources.map((s) => ({
+ type: 'Feature' as const,
+ properties: { id: s.id, name: s.name },
+ geometry: { type: 'Point' as const, coordinates: [s.lng, s.lat] as [number, number] },
+ })),
+ }),
+ [strikeSources]
+ )
+
+ /** 盟军打击目标点位(林肯/福特/以色列→伊朗+以色列→黎巴嫩+斯里兰卡/阿巴斯港)。全部/单项:显示全部目标;实时:仅 effectiveCutoffDays 内 */
const alliedStrikeTargetsFeatures = useMemo(() => {
const out: GeoJSON.Feature[] = []
const onlyRealtimeFilter =
strikeLegendFilter === null && isRealtimeView
for (const line of strikeLines) {
- if (strikeLegendFilter != null && strikeLegendFilter !== 'lincoln' && strikeLegendFilter !== 'ford' && strikeLegendFilter !== 'israel') continue
+ if (
+ strikeLegendFilter != null &&
+ strikeLegendFilter !== 'lincoln' &&
+ strikeLegendFilter !== 'ford' &&
+ strikeLegendFilter !== 'israel' &&
+ strikeLegendFilter !== 'virginia_srilanka' &&
+ strikeLegendFilter !== 'tomahawk_arabian' &&
+ strikeLegendFilter !== 'arabian_sea_sub_torpedo'
+ )
+ continue
if (strikeLegendFilter === 'lincoln' && line.sourceId !== 'lincoln') continue
if (strikeLegendFilter === 'ford' && line.sourceId !== 'ford') continue
if (strikeLegendFilter === 'israel' && line.sourceId !== 'israel') continue
+ if (strikeLegendFilter === 'virginia_srilanka' && line.sourceId !== 'virginia_srilanka') continue
+ if (strikeLegendFilter === 'tomahawk_arabian' && line.sourceId !== 'tomahawk_arabian' && line.sourceId !== 'arabian_sea_sub_torpedo') continue
+ if (strikeLegendFilter === 'arabian_sea_sub_torpedo' && line.sourceId !== 'arabian_sea_sub_torpedo') continue
for (const t of line.targets) {
if (onlyRealtimeFilter && !isWithinAnimationWindow((t as { struck_at?: string | null }).struck_at ?? null, referenceTime, effectiveCutoffDays))
continue
@@ -929,6 +1168,60 @@ export function WarMap() {
})
israelSrc.setData({ type: 'FeatureCollection', features })
}
+ // 斯里兰卡伏击:鱼雷轨迹蓝色光点(直线路径)
+ const virginiaSrc = map.getSource('allied-strike-dots-virginia') as
+ | { setData: (d: GeoJSON.FeatureCollection) => void }
+ | undefined
+ const virginiaPaths = virginiaPathsRef.current
+ if (virginiaSrc && virginiaPaths.length > 0) {
+ const features: GeoJSON.Feature[] = []
+ virginiaPaths.forEach((path, i) => {
+ if (i % step !== 0) return
+ const progress = (elapsed / FLIGHT_DURATION_MS + 0.6 + i / Math.max(virginiaPaths.length, 1)) % 1
+ features.push({
+ type: 'Feature' as const,
+ properties: {},
+ geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
+ })
+ })
+ virginiaSrc.setData({ type: 'FeatureCollection', features })
+ }
+ // 阿巴斯港突袭:战斧弹道紫色光点(抛物线路径)
+ const tomahawkSrc = map.getSource('allied-strike-dots-tomahawk') as
+ | { setData: (d: GeoJSON.FeatureCollection) => void }
+ | undefined
+ const tomahawkPaths = tomahawkPathsRef.current
+ if (tomahawkSrc && tomahawkPaths.length > 0) {
+ const features: GeoJSON.Feature[] = []
+ tomahawkPaths.forEach((path, i) => {
+ if (i % step !== 0) return
+ const progress = (elapsed / FLIGHT_DURATION_MS + 0.4 + i / Math.max(tomahawkPaths.length, 1)) % 1
+ features.push({
+ type: 'Feature' as const,
+ properties: {},
+ geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
+ })
+ })
+ tomahawkSrc.setData({ type: 'FeatureCollection', features })
+ }
+ // 阿巴斯港同艇潜射/鱼雷光点(直线路径,与战斧同时)
+ const arabianSubSrc = map.getSource('allied-strike-dots-arabian-sub') as
+ | { setData: (d: GeoJSON.FeatureCollection) => void }
+ | undefined
+ const arabianSubPaths = arabianSubTorpedoPathsRef.current
+ if (arabianSubSrc && arabianSubPaths.length > 0) {
+ const features: GeoJSON.Feature[] = []
+ arabianSubPaths.forEach((path, i) => {
+ if (i % step !== 0) return
+ const progress = (elapsed / FLIGHT_DURATION_MS + 0.5 + i / Math.max(arabianSubPaths.length, 1)) % 1
+ features.push({
+ type: 'Feature' as const,
+ properties: {},
+ geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
+ })
+ })
+ arabianSubSrc.setData({ type: 'FeatureCollection', features })
+ }
// 真主党打击以色列北部:橙红光点,与林肯/福特/以色列同一动画方式
const hezSrc = map.getSource('hezbollah-strike-dots') as
| { setData: (d: GeoJSON.FeatureCollection) => void }
@@ -1085,6 +1378,9 @@ export function WarMap() {
(map.getSource('allied-strike-dots-lincoln') && lincolnPathsRef.current.length > 0) ||
(map.getSource('allied-strike-dots-ford') && fordPathsRef.current.length > 0) ||
(map.getSource('allied-strike-dots-israel') && israelPathsRef.current.length > 0) ||
+ (map.getSource('allied-strike-dots-virginia') && virginiaPathsRef.current.length > 0) ||
+ (map.getSource('allied-strike-dots-tomahawk') && tomahawkPathsRef.current.length > 0) ||
+ (map.getSource('allied-strike-dots-arabian-sub') && arabianSubTorpedoPathsRef.current.length > 0) ||
(map.getSource('hezbollah-strike-dots') && hezbollahPathsRef.current.length > 0) ||
(map.getSource('iran-hormuz-dots') && hormuzPathsRef.current.length > 0) ||
map.getSource('kurdish-pincer-growth') ||
@@ -1262,7 +1558,7 @@ export function WarMap() {
fitToTheater({ duration: 500 })
}}
className={`flex items-center gap-1 rounded px-1 py-0.5 transition-colors ${strikeLegendFilter === null && isRealtimeView ? 'ring-1 ring-amber-400/80 bg-amber-400/10' : 'hover:bg-white/10'}`}
- title="主视角,仅渲染最新进展(近 1 天)"
+ title="主视角,仅渲染最新进展(近 5 天)"
>
实时
@@ -1290,6 +1586,22 @@ export function WarMap() {
>
以色列打击
+
+