This commit is contained in:
FURRO404
2026-06-18 03:08:30 -07:00
parent db7d42f363
commit a86d762fe2
2 changed files with 122 additions and 15 deletions
+119 -12
View File
@@ -2789,6 +2789,13 @@ function battleLineColor(line) {
return 'text-text-soft' return 'text-text-soft'
} }
function formatLogTime(ms) {
const totalSeconds = Math.floor(Number(ms || 0) / 1000)
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0')
const seconds = String(totalSeconds % 60).padStart(2, '0')
return `${minutes}:${seconds}`
}
function deadVehicleKey(uid, cdk) { function deadVehicleKey(uid, cdk) {
return `${String(uid || '').trim()}:${String(cdk || '').trim()}` return `${String(uid || '').trim()}:${String(cdk || '').trim()}`
} }
@@ -2806,16 +2813,61 @@ function deadVehicleKeysFromEventLog(eventLog) {
return keys return keys
} }
function logLookups(participants) {
const players = new Map()
;(participants || []).forEach((participant) => {
const result = String(participant.result || '').toLowerCase() === 'win' ? 'win' : 'loss'
;(participant.players || []).forEach((player) => {
const vehicles = new Map()
;(player.vehicles || []).forEach((vehicle) => {
vehicles.set(String(vehicle.cdk || ''), vehicle.name || vehicle.cdk || 'Unknown')
})
players.set(String(player.uid), {
name: player.nick || player.uid,
team: participant.team_name || '',
result,
className: result === 'win' ? 'text-win' : 'text-loss',
vehicles,
})
})
})
return players
}
function logPlayer(players, uid) {
return players.get(String(uid)) || {
name: uid === undefined || uid === null ? 'Unknown' : `Player#${uid}`,
team: '',
result: '',
className: 'text-text-soft',
vehicles: new Map(),
}
}
function logVehicle(player, cdk) {
if (!cdk) return 'Unknown'
return player.vehicles.get(String(cdk)) || String(cdk)
}
function structuredBattleEvents(eventLog) {
const kills = Array.isArray(eventLog?.kills) ? eventLog.kills : []
const damage = Array.isArray(eventLog?.damage) ? eventLog.damage : []
return [
...kills.map((event) => ({ ...event, kind: 'kill' })),
...damage.map((event) => ({ ...event, kind: 'damage' })),
].sort((a, b) => Number(a.time || 0) - Number(b.time || 0))
}
function GamePage({ gameId, navigate }) { function GamePage({ gameId, navigate }) {
const [gameState, setGameState] = useState({ status: 'loading', data: null, error: null }) const [gameState, setGameState] = useState({ status: 'loading', data: null, error: null })
const [logs, setLogs] = useState({ chat_log: [], battle_log: [], event_log: { kills: [], damage: [] } }) const [logs, setLogs] = useState({ chat_log: [], battle_log: [], event_log: { kills: [], damage: [], chat: [] } })
useEffect(() => { useEffect(() => {
if (!gameId) return if (!gameId) return
const controller = new AbortController() const controller = new AbortController()
setGameState({ status: 'loading', data: null, error: null }) setGameState({ status: 'loading', data: null, error: null })
setLogs({ chat_log: [], battle_log: [], event_log: { kills: [], damage: [] } }) setLogs({ chat_log: [], battle_log: [], event_log: { kills: [], damage: [], chat: [] } })
fetchJson(apiEndpoints.game(gameId), controller.signal) fetchJson(apiEndpoints.game(gameId), controller.signal)
.then((data) => { .then((data) => {
@@ -2835,7 +2887,7 @@ function GamePage({ gameId, navigate }) {
setLogs({ setLogs({
chat_log: Array.isArray(data?.chat_log) ? data.chat_log : [], chat_log: Array.isArray(data?.chat_log) ? data.chat_log : [],
battle_log: Array.isArray(data?.battle_log) ? data.battle_log : [], battle_log: Array.isArray(data?.battle_log) ? data.battle_log : [],
event_log: data?.event_log || { kills: [], damage: [] }, event_log: data?.event_log || { kills: [], damage: [], chat: [] },
}) })
} }
}) })
@@ -2847,8 +2899,11 @@ function GamePage({ gameId, navigate }) {
}, [gameId]) }, [gameId])
const game = gameState.data?.game const game = gameState.data?.game
const participants = gameState.data?.participants || [] const participants = useMemo(() => gameState.data?.participants || [], [gameState.data?.participants])
const deadVehicleKeys = useMemo(() => deadVehicleKeysFromEventLog(logs.event_log), [logs.event_log]) const deadVehicleKeys = useMemo(() => deadVehicleKeysFromEventLog(logs.event_log), [logs.event_log])
const playersByUid = useMemo(() => logLookups(participants), [participants])
const battleEvents = useMemo(() => structuredBattleEvents(logs.event_log), [logs.event_log])
const chatEvents = Array.isArray(logs.event_log?.chat) ? logs.event_log.chat : []
const participantNames = participants.length const participantNames = participants.length
? participants.map((participant) => ({ ? participants.map((participant) => ({
name: participant.team_name, name: participant.team_name,
@@ -2995,29 +3050,81 @@ function GamePage({ gameId, navigate }) {
</div> </div>
</div> </div>
{logs.battle_log.length ? ( {battleEvents.length || logs.battle_log.length ? (
<details className="rounded-lg border border-border bg-fury-white shadow-sm"> <details className="rounded-lg border border-border bg-fury-white shadow-sm">
<summary className="cursor-pointer px-5 py-4 font-semibold">Battle Log</summary> <summary className="cursor-pointer px-5 py-4 font-semibold">Battle Log</summary>
<div className="log-mono overflow-x-auto px-5 py-3 text-xs leading-relaxed"> <div className="log-mono overflow-x-auto px-5 py-3 text-xs leading-relaxed">
{logs.battle_log.map((line, i) => ( {battleEvents.length ? (
<div className={`whitespace-pre ${battleLineColor(line)}`} key={i}>{line}</div> battleEvents.map((event, i) => (
))} <BattleEventLine event={event} key={`${event.kind}-${event.time}-${i}`} players={playersByUid} />
))
) : (
logs.battle_log.map((line, i) => (
<div className={`whitespace-pre ${battleLineColor(line)}`} key={i}>{line}</div>
))
)}
</div> </div>
</details> </details>
) : null} ) : null}
{logs.chat_log.length ? ( {chatEvents.length || logs.chat_log.length ? (
<details className="rounded-lg border border-border bg-fury-white shadow-sm"> <details className="rounded-lg border border-border bg-fury-white shadow-sm">
<summary className="cursor-pointer px-5 py-4 font-semibold">Chat Log</summary> <summary className="cursor-pointer px-5 py-4 font-semibold">Chat Log</summary>
<pre className="log-mono overflow-x-auto whitespace-pre-wrap px-5 py-3 text-xs leading-relaxed text-text-soft"> <div className="log-mono overflow-x-auto px-5 py-3 text-xs leading-relaxed">
{logs.chat_log.join('\n')} {chatEvents.length ? (
</pre> chatEvents.map((event, i) => (
<ChatEventLine event={event} key={`${event.time}-${event.uid}-${i}`} players={playersByUid} />
))
) : (
logs.chat_log.map((line, i) => (
<div className={`whitespace-pre-wrap ${battleLineColor(line)}`} key={i}>{line}</div>
))
)}
</div>
</details> </details>
) : null} ) : null}
</section> </section>
) )
} }
function BattleEventLine({ event, players }) {
const offender = logPlayer(players, event.offender_uid)
const victim = logPlayer(players, event.offended_uid)
const ts = formatLogTime(event.time)
const victimLabel = `${victim.name} (${logVehicle(victim, event.offended_unit)})`
if (event.kind === 'kill' && (event.crashed || event.offender_uid === undefined || event.offender_uid === null)) {
return (
<div className="whitespace-pre">
<span className="text-text-soft">[{ts}] </span>
<span className={victim.className}>[{victim.team}] {victimLabel}</span>
<span className="text-text-soft"> crashed</span>
</div>
)
}
const offenderLabel = `${offender.name} (${logVehicle(offender, event.offender_unit)})`
const action = event.kind === 'kill' ? 'destroyed' : `damaged ${event.afire ? '(FIRE) ' : ''}`
return (
<div className="whitespace-pre">
<span className="text-text-soft">[{ts}] </span>
<span className={offender.className}>[{offender.team}] {offenderLabel}</span>
<span className="text-text-soft"> {action} </span>
<span className={victim.className}>{victimLabel}</span>
</div>
)
}
function ChatEventLine({ event, players }) {
const player = logPlayer(players, event.uid)
return (
<div className={`whitespace-pre-wrap ${player.className}`}>
[{formatLogTime(event.time)}] [{event.type || 'ALL'}] [{player.team}] `{player.name}`: {event.message || ''}
</div>
)
}
function RosterTable({ players, status }) { function RosterTable({ players, status }) {
const sortedPlayers = [...players].sort((a, b) => { const sortedPlayers = [...players].sort((a, b) => {
return (b.total_kills || 0) - (a.total_kills || 0) || String(a.nick || '').localeCompare(b.nick || '') return (b.total_kills || 0) - (a.total_kills || 0) || String(a.nick || '').localeCompare(b.nick || '')
+3 -3
View File
@@ -132,7 +132,7 @@ b.execute("INSERT INTO player_games_hist VALUES ('2','bob','TeamLose','2','abc',
b.execute("INSERT INTO match_logs VALUES ('abc', ?, ?, ?, 1000)", b.execute("INSERT INTO match_logs VALUES ('abc', ?, ?, ?, 1000)",
(json.dumps(["[00:01] [ALL] [WIN] `alice`: gg"]), (json.dumps(["[00:01] [ALL] [WIN] `alice`: gg"]),
json.dumps(["+[00:30] [WIN] alice (T-34) destroyed bob (Pz.IV)"]), json.dumps(["+[00:30] [WIN] alice (T-34) destroyed bob (Pz.IV)"]),
json.dumps({"kills": [{"offender_uid": "1", "offender_unit": "ussr_t_34", "offended_uid": "2", "offended_unit": "germ_pz_iv", "crashed": False, "time": 30000}], "damage": []}))) json.dumps({"kills": [{"offender_uid": "1", "offender_unit": "ussr_t_34", "offended_uid": "2", "offended_unit": "germ_pz_iv", "crashed": False, "time": 30000}], "damage": [], "chat": [{"uid": "1", "type": "ALL", "message": "gg", "time": 1000}]})))
b.commit() b.commit()
t = sqlite3.connect(f"{wd}/tss_teams.db") t = sqlite3.connect(f"{wd}/tss_teams.db")
t.executescript("CREATE TABLE teams_data (team_id INT PRIMARY KEY, name TEXT, members INT DEFAULT 0, captain_uid TEXT);") t.executescript("CREATE TABLE teams_data (team_id INT PRIMARY KEY, name TEXT, members INT DEFAULT 0, captain_uid TEXT);")
@@ -188,8 +188,8 @@ if [[ $? -eq 0 ]]; then ok "backend game detail (en)"; else bad "backend game de
RU_NAME="$(curl -s "localhost:$BE_PORT/api/tss/games/abc?lang=ru" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print([v['name'] for p in d['participants'] for pl in p['players'] for v in pl['vehicles'] if v['cdk']=='ussr_t_34'][0])")" RU_NAME="$(curl -s "localhost:$BE_PORT/api/tss/games/abc?lang=ru" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print([v['name'] for p in d['participants'] for pl in p['players'] for v in pl['vehicles'] if v['cdk']=='ussr_t_34'][0])")"
assert_eq "ru translation of ussr_t_34" "$RU_NAME" "Т-34" assert_eq "ru translation of ussr_t_34" "$RU_NAME" "Т-34"
LOG_COUNTS="$(curl -s "localhost:$BE_PORT/api/tss/games/abc/logs" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print(len(d['chat_log']), len(d['battle_log']), len(d.get('event_log', {}).get('kills', [])))")" LOG_COUNTS="$(curl -s "localhost:$BE_PORT/api/tss/games/abc/logs" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); e=d.get('event_log', {}); print(len(d['chat_log']), len(d['battle_log']), len(e.get('kills', [])), len(e.get('chat', [])))")"
assert_eq "logs chat/battle/kill counts" "$LOG_COUNTS" "1 1 1" assert_eq "logs chat/battle/kill/raw-chat counts" "$LOG_COUNTS" "1 1 1 1"
MISS="$(curl -s "localhost:$BE_PORT/api/tss/games/deadbeef/logs" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print(len(d['chat_log'])+len(d['battle_log']))")" MISS="$(curl -s "localhost:$BE_PORT/api/tss/games/deadbeef/logs" | "$PYTHON" -c "import sys,json; d=json.load(sys.stdin); print(len(d['chat_log'])+len(d['battle_log']))")"
assert_eq "missing-session logs empty" "$MISS" "0" assert_eq "missing-session logs empty" "$MISS" "0"