161 lines
6.4 KiB
TypeScript
161 lines
6.4 KiB
TypeScript
import { useMemo } from 'react'
|
|
import type { MilitarySituation } from '@/data/mockData'
|
|
import { useSituationStore } from '@/store/situationStore'
|
|
import { usePlaybackStore } from '@/store/playbackStore'
|
|
|
|
/** 将系列时间映射到回放日 (2026-03-01) 以便按当天时刻插值 */
|
|
function toReplayDay(iso: string, baseDay: string): string {
|
|
const d = new Date(iso)
|
|
const [y, m, day] = baseDay.slice(0, 10).split('-').map(Number)
|
|
return new Date(y, (m || 1) - 1, day || 1, d.getUTCHours(), d.getUTCMinutes(), 0, 0).toISOString()
|
|
}
|
|
|
|
function interpolateAt(
|
|
series: { time: string; value: number }[],
|
|
at: string,
|
|
baseDay = '2026-03-01'
|
|
): number {
|
|
if (series.length === 0) return 0
|
|
const t = new Date(at).getTime()
|
|
const mapped = series.map((p) => ({
|
|
time: toReplayDay(p.time, baseDay),
|
|
value: p.value,
|
|
}))
|
|
const sorted = [...mapped].sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime())
|
|
const before = sorted.filter((p) => new Date(p.time).getTime() <= t)
|
|
const after = sorted.filter((p) => new Date(p.time).getTime() > t)
|
|
if (before.length === 0) return sorted[0].value
|
|
if (after.length === 0) return sorted[sorted.length - 1].value
|
|
const a = before[before.length - 1]
|
|
const b = after[0]
|
|
const ta = new Date(a.time).getTime()
|
|
const tb = new Date(b.time).getTime()
|
|
const f = tb === ta ? 1 : (t - ta) / (tb - ta)
|
|
return a.value + f * (b.value - a.value)
|
|
}
|
|
|
|
function linearProgress(start: string, end: string, at: string): number {
|
|
const ts = new Date(start).getTime()
|
|
const te = new Date(end).getTime()
|
|
const ta = new Date(at).getTime()
|
|
if (ta <= ts) return 0
|
|
if (ta >= te) return 1
|
|
return (ta - ts) / (te - ts)
|
|
}
|
|
|
|
/** 根据回放时刻派生态势数据 */
|
|
export function useReplaySituation(): MilitarySituation {
|
|
const situation = useSituationStore((s) => s.situation)
|
|
const { isReplayMode, playbackTime } = usePlaybackStore()
|
|
|
|
return useMemo(() => {
|
|
if (!isReplayMode) return situation
|
|
|
|
const progress = linearProgress('2026-03-01T02:00:00.000Z', '2026-03-01T11:45:00.000Z', playbackTime)
|
|
|
|
// 华尔街趋势、反击情绪:按时间插值
|
|
const wsValue = interpolateAt(situation.usForces.wallStreetInvestmentTrend, playbackTime)
|
|
const retValue = interpolateAt(situation.iranForces.retaliationSentimentHistory, playbackTime)
|
|
|
|
// 战斗损失:从 0 线性增长到当前值
|
|
const lerp = (a: number, b: number) => Math.round(a + progress * (b - a))
|
|
const usLoss = situation.usForces.combatLosses
|
|
const irLoss = situation.iranForces.combatLosses
|
|
const civTotal = situation.civilianCasualtiesTotal ?? { killed: 0, wounded: 0 }
|
|
const usLossesAt = {
|
|
bases: {
|
|
destroyed: lerp(0, usLoss.bases.destroyed),
|
|
damaged: lerp(0, usLoss.bases.damaged),
|
|
},
|
|
personnelCasualties: {
|
|
killed: lerp(0, usLoss.personnelCasualties.killed),
|
|
wounded: lerp(0, usLoss.personnelCasualties.wounded),
|
|
},
|
|
civilianCasualties: { killed: 0, wounded: 0 },
|
|
aircraft: lerp(0, usLoss.aircraft),
|
|
warships: lerp(0, usLoss.warships),
|
|
armor: lerp(0, usLoss.armor),
|
|
vehicles: lerp(0, usLoss.vehicles),
|
|
drones: lerp(0, usLoss.drones ?? 0),
|
|
missiles: lerp(0, usLoss.missiles ?? 0),
|
|
helicopters: lerp(0, usLoss.helicopters ?? 0),
|
|
submarines: lerp(0, usLoss.submarines ?? 0),
|
|
tanks: lerp(0, usLoss.tanks ?? 0),
|
|
civilianShips: lerp(0, usLoss.civilianShips ?? 0),
|
|
airportPort: lerp(0, usLoss.airportPort ?? 0),
|
|
}
|
|
const irLossesAt = {
|
|
bases: {
|
|
destroyed: lerp(0, irLoss.bases.destroyed),
|
|
damaged: lerp(0, irLoss.bases.damaged),
|
|
},
|
|
personnelCasualties: {
|
|
killed: lerp(0, irLoss.personnelCasualties.killed),
|
|
wounded: lerp(0, irLoss.personnelCasualties.wounded),
|
|
},
|
|
civilianCasualties: { killed: 0, wounded: 0 },
|
|
aircraft: lerp(0, irLoss.aircraft),
|
|
warships: lerp(0, irLoss.warships),
|
|
armor: lerp(0, irLoss.armor),
|
|
vehicles: lerp(0, irLoss.vehicles),
|
|
drones: lerp(0, irLoss.drones ?? 0),
|
|
missiles: lerp(0, irLoss.missiles ?? 0),
|
|
helicopters: lerp(0, irLoss.helicopters ?? 0),
|
|
submarines: lerp(0, irLoss.submarines ?? 0),
|
|
tanks: lerp(0, irLoss.tanks ?? 0),
|
|
civilianShips: lerp(0, irLoss.civilianShips ?? 0),
|
|
airportPort: lerp(0, irLoss.airportPort ?? 0),
|
|
}
|
|
|
|
// 被袭基地:按 damage_level 排序,高损毁先出现;根据 progress 决定显示哪些为 attacked
|
|
const usLocs = situation.usForces.keyLocations || []
|
|
const attackedBases = usLocs
|
|
.filter((loc) => loc.status === 'attacked')
|
|
.sort((a, b) => (b.damage_level ?? 0) - (a.damage_level ?? 0))
|
|
const totalAttacked = attackedBases.length
|
|
const shownAttackedCount = Math.round(progress * totalAttacked)
|
|
const attackedNames = new Set(
|
|
attackedBases.slice(0, shownAttackedCount).map((l) => l.name)
|
|
)
|
|
|
|
const usLocsAt = usLocs.map((loc) => {
|
|
if (loc.status === 'attacked' && !attackedNames.has(loc.name)) {
|
|
return { ...loc, status: 'operational' as const }
|
|
}
|
|
return { ...loc }
|
|
})
|
|
|
|
return {
|
|
...situation,
|
|
lastUpdated: playbackTime,
|
|
civilianCasualtiesTotal: {
|
|
killed: lerp(0, civTotal.killed),
|
|
wounded: lerp(0, civTotal.wounded),
|
|
},
|
|
usForces: {
|
|
...situation.usForces,
|
|
keyLocations: usLocsAt,
|
|
combatLosses: usLossesAt,
|
|
wallStreetInvestmentTrend: [
|
|
...situation.usForces.wallStreetInvestmentTrend.filter((p) => new Date(p.time).getTime() <= new Date(playbackTime).getTime()),
|
|
{ time: playbackTime, value: wsValue },
|
|
].slice(-20),
|
|
},
|
|
iranForces: {
|
|
...situation.iranForces,
|
|
combatLosses: irLossesAt,
|
|
retaliationSentiment: retValue,
|
|
retaliationSentimentHistory: [
|
|
...situation.iranForces.retaliationSentimentHistory.filter((p) => new Date(p.time).getTime() <= new Date(playbackTime).getTime()),
|
|
{ time: playbackTime, value: retValue },
|
|
].slice(-20),
|
|
},
|
|
recentUpdates: (situation.recentUpdates || []).filter(
|
|
(u) => new Date(u.timestamp).getTime() <= new Date(playbackTime).getTime()
|
|
),
|
|
conflictEvents: situation.conflictEvents || [],
|
|
conflictStats: situation.conflictStats || { total_events: 0, high_impact_events: 0, estimated_casualties: 0, estimated_strike_count: 0 },
|
|
}
|
|
}, [situation, isReplayMode, playbackTime])
|
|
}
|