meow
This commit is contained in:
+119
-12
@@ -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 || '')
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user