diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4abcb18..7cd36c6 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2789,6 +2789,13 @@ function battleLineColor(line) { 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) { return `${String(uid || '').trim()}:${String(cdk || '').trim()}` } @@ -2806,16 +2813,61 @@ function deadVehicleKeysFromEventLog(eventLog) { 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 }) { 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(() => { if (!gameId) return const controller = new AbortController() 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) .then((data) => { @@ -2835,7 +2887,7 @@ function GamePage({ gameId, navigate }) { setLogs({ chat_log: Array.isArray(data?.chat_log) ? data.chat_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]) 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 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 ? participants.map((participant) => ({ name: participant.team_name, @@ -2995,29 +3050,81 @@ function GamePage({ gameId, navigate }) { - {logs.battle_log.length ? ( + {battleEvents.length || logs.battle_log.length ? (
Battle Log
- {logs.battle_log.map((line, i) => ( -
{line}
- ))} + {battleEvents.length ? ( + battleEvents.map((event, i) => ( + + )) + ) : ( + logs.battle_log.map((line, i) => ( +
{line}
+ )) + )}
) : null} - {logs.chat_log.length ? ( + {chatEvents.length || logs.chat_log.length ? (
Chat Log -
-            {logs.chat_log.join('\n')}
-          
+
+ {chatEvents.length ? ( + chatEvents.map((event, i) => ( + + )) + ) : ( + logs.chat_log.map((line, i) => ( +
{line}
+ )) + )} +
) : null} ) } +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 ( +
+ [{ts}] + [{victim.team}] {victimLabel} + crashed +
+ ) + } + + const offenderLabel = `${offender.name} (${logVehicle(offender, event.offender_unit)})` + const action = event.kind === 'kill' ? 'destroyed' : `damaged ${event.afire ? '(FIRE) ' : ''}` + + return ( +
+ [{ts}] + [{offender.team}] {offenderLabel} + {action} + {victimLabel} +
+ ) +} + +function ChatEventLine({ event, players }) { + const player = logPlayer(players, event.uid) + return ( +
+ [{formatLogTime(event.time)}] [{event.type || 'ALL'}] [{player.team}] `{player.name}`: {event.message || ''} +
+ ) +} + function RosterTable({ players, status }) { const sortedPlayers = [...players].sort((a, b) => { return (b.total_kills || 0) - (a.total_kills || 0) || String(a.nick || '').localeCompare(b.nick || '') diff --git a/scripts/verify_game_detail.sh b/scripts/verify_game_detail.sh index 48e1c44..7f324d5 100755 --- a/scripts/verify_game_detail.sh +++ b/scripts/verify_game_detail.sh @@ -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)", (json.dumps(["[00:01] [ALL] [WIN] `alice`: gg"]), 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() 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);") @@ -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])")" 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', [])))")" -assert_eq "logs chat/battle/kill counts" "$LOG_COUNTS" "1 1 1" +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/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']))")" assert_eq "missing-session logs empty" "$MISS" "0"