From abb35b8fac5794ed2e2261855234430c6f56c52e Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Sat, 30 May 2026 19:27:31 +0200 Subject: [PATCH] fix: tryfix multikill grouping --- tauri-app/src/components/HighlightsPanel.vue | 6 + tauri-app/src/types/timeline.ts | 169 ++++++++++++++++--- 2 files changed, 147 insertions(+), 28 deletions(-) diff --git a/tauri-app/src/components/HighlightsPanel.vue b/tauri-app/src/components/HighlightsPanel.vue index 2dfa0b3..4d3768d 100644 --- a/tauri-app/src/components/HighlightsPanel.vue +++ b/tauri-app/src/components/HighlightsPanel.vue @@ -42,6 +42,7 @@ const typeToggles = ref>({ death: DEFAULT_HIGHLIGHT_SETTINGS.included_types.includes("death"), assist: DEFAULT_HIGHLIGHT_SETTINGS.included_types.includes("assist"), multi_kill: DEFAULT_HIGHLIGHT_SETTINGS.included_types.includes("multi_kill"), + trade_kill: DEFAULT_HIGHLIGHT_SETTINGS.included_types.includes("trade_kill"), }); // Watch type toggles and update settings @@ -82,6 +83,7 @@ const activeClipId = computed(() => { const killCount = computed(() => props.highlights.filter(h => h.highlight_type === "kill").length); const deathCount = computed(() => props.highlights.filter(h => h.highlight_type === "death").length); const multiKillCount = computed(() => props.highlights.filter(h => h.highlight_type === "multi_kill").length); +const tradeKillCount = computed(() => props.highlights.filter(h => h.highlight_type === "trade_kill").length); const assistCount = computed(() => props.highlights.filter(h => h.highlight_type === "assist").length); const totalDuration = computed(() => { const total = props.highlights.reduce((sum, h) => sum + (h.end_time - h.start_time), 0); @@ -173,6 +175,7 @@ function formatClipDuration(clip: HighlightClip): string { { type: 'death' as HighlightType, label: 'Deaths', icon: '💀' }, { type: 'assist' as HighlightType, label: 'Assists', icon: '🤝' }, { type: 'multi_kill' as HighlightType, label: 'Multi Kills', icon: '🔥' }, + { type: 'trade_kill' as HighlightType, label: 'Trade Kills', icon: '🔄' }, ]" :key="typeInfo.type" class="type-toggle" @@ -230,6 +233,9 @@ function formatClipDuration(clip: HighlightClip): string {
🔥 {{ multiKillCount }}
+
+ 🔄 {{ tradeKillCount }} +
⏱️ {{ formatDuration(totalDuration) }}
diff --git a/tauri-app/src/types/timeline.ts b/tauri-app/src/types/timeline.ts index 69553f4..1e7d754 100644 --- a/tauri-app/src/types/timeline.ts +++ b/tauri-app/src/types/timeline.ts @@ -728,7 +728,7 @@ export type EventCategory = /** * The type of highlight clip. */ -export type HighlightType = "kill" | "death" | "assist" | "multi_kill"; +export type HighlightType = "kill" | "death" | "assist" | "multi_kill" | "trade_kill"; /** * Configuration for highlight generation. @@ -755,7 +755,7 @@ export const DEFAULT_HIGHLIGHT_SETTINGS: HighlightSettings = { buffer_before: 10, buffer_after: 3, min_duration: 5, - included_types: ["kill", "death", "multi_kill"], + included_types: ["kill", "death", "multi_kill", "trade_kill"], merge_overlapping: true, merge_gap_secs: 5, }; @@ -808,23 +808,46 @@ export function getKillHighlightType( return null; // Not involved — skip } +/** + * Result of multi-kill detection, including whether it's a trade kill. + */ +export interface MultiKillGroup { + /** Kill events in the group. */ + kills: TimestampedEvent[]; + /** Death event if the player died during the multi-kill window. */ + death: TimestampedEvent | null; + /** Whether this is a trade kill (player died during the sequence). */ + isTradeKill: boolean; +} + /** * Detect multi-kills from a sequence of kill events. * Returns groups of kills that happened within `windowSecs` seconds. + * Also checks if the player died during the multi-kill window (trade kill). */ export function detectMultiKills( killEvents: TimestampedEvent[], + deathEvents: TimestampedEvent[] = [], windowSecs: number = 12, -): TimestampedEvent[][] { - if (killEvents.length === 0) return []; + localPlayerName?: string, +): MultiKillGroup[] { + // Filter to only include kills where the player is the killer + const playerKills = localPlayerName + ? killEvents.filter((event) => { + const rawData = event.raw_data as { KillerName?: string } | null; + return rawData?.KillerName === localPlayerName; + }) + : killEvents; - const sorted = [...killEvents].sort((a, b) => { + if (playerKills.length === 0) return []; + + const sorted = [...playerKills].sort((a, b) => { const aTime = a.video_timestamp[0] + a.video_timestamp[1] / 1e9; const bTime = b.video_timestamp[0] + b.video_timestamp[1] / 1e9; return aTime - bTime; }); - const groups: TimestampedEvent[][] = []; + const groups: MultiKillGroup[] = []; let currentGroup: TimestampedEvent[] = [sorted[0]]; for (let i = 1; i < sorted.length; i++) { @@ -837,19 +860,57 @@ export function detectMultiKills( currentGroup.push(sorted[i]); } else { if (currentGroup.length >= 2) { - groups.push(currentGroup); + // Check for death in this window + const firstTime = currentGroup[0].video_timestamp[0] + currentGroup[0].video_timestamp[1] / 1e9; + const lastTime = currentGroup[currentGroup.length - 1].video_timestamp[0] + currentGroup[currentGroup.length - 1].video_timestamp[1] / 1e9; + const death = findDeathInWindow(deathEvents, firstTime, lastTime, windowSecs); + + groups.push({ + kills: currentGroup, + death, + isTradeKill: death !== null, + }); } currentGroup = [sorted[i]]; } } + // Handle last group if (currentGroup.length >= 2) { - groups.push(currentGroup); + const firstTime = currentGroup[0].video_timestamp[0] + currentGroup[0].video_timestamp[1] / 1e9; + const lastTime = currentGroup[currentGroup.length - 1].video_timestamp[0] + currentGroup[currentGroup.length - 1].video_timestamp[1] / 1e9; + const death = findDeathInWindow(deathEvents, firstTime, lastTime, windowSecs); + + groups.push({ + kills: currentGroup, + death, + isTradeKill: death !== null, + }); } return groups; } +/** + * Find a death event within the multi-kill window. + * Checks from the first kill to the last kill, plus the window buffer. + */ +function findDeathInWindow( + deathEvents: TimestampedEvent[], + firstKillTime: number, + lastKillTime: number, + windowSecs: number, +): TimestampedEvent | null { + for (const death of deathEvents) { + const deathTime = death.video_timestamp[0] + death.video_timestamp[1] / 1e9; + // Death must occur within the window starting from first kill + if (deathTime >= firstKillTime && deathTime <= lastKillTime + windowSecs) { + return death; + } + } + return null; +} + /** * Get the multi-kill label based on number of kills. */ @@ -872,6 +933,7 @@ export function getHighlightTypeColor(type: HighlightType): string { case "death": return "#f87171"; // red case "assist": return "#a78bfa"; // purple case "multi_kill": return "#f97316"; // orange + case "trade_kill": return "#fbbf24"; // amber/yellow } } @@ -884,6 +946,7 @@ export function getHighlightTypeIcon(type: HighlightType): string { case "death": return "💀"; case "assist": return "🤝"; case "multi_kill": return "🔥"; + case "trade_kill": return "🔄"; } } @@ -975,24 +1038,49 @@ export function computeHighlights( }); } - // Detect multi-kills and create clips for them (only if enabled) - if (settings.included_types.includes("multi_kill")) { - const multiKillGroups = detectMultiKills(playerKills); + // Detect multi-kills and trade kills (only if enabled) + if (settings.included_types.includes("multi_kill") || settings.included_types.includes("trade_kill")) { + const multiKillGroups = detectMultiKills(playerKills, playerDeaths); for (const group of multiKillGroups) { - const firstTime = group[0].video_timestamp[0] + group[0].video_timestamp[1] / 1e9; + // Skip if neither multi_kill nor trade_kill is enabled + if (!settings.included_types.includes("multi_kill") && !settings.included_types.includes("trade_kill")) { + continue; + } + + // If it's a trade kill but trade_kill is not enabled, skip + if (group.isTradeKill && !settings.included_types.includes("trade_kill")) { + continue; + } + + // If it's a multi kill but multi_kill is not enabled, skip + if (!group.isTradeKill && !settings.included_types.includes("multi_kill")) { + continue; + } + + const firstTime = group.kills[0].video_timestamp[0] + group.kills[0].video_timestamp[1] / 1e9; const lastTime = - group[group.length - 1].video_timestamp[0] + - group[group.length - 1].video_timestamp[1] / 1e9; - const label = getMultiKillLabel(group.length); + group.kills[group.kills.length - 1].video_timestamp[0] + + group.kills[group.kills.length - 1].video_timestamp[1] / 1e9; + const label = getMultiKillLabel(group.kills.length); + + // Include death event if present + const allEvents = group.death ? [...group.kills, group.death] : group.kills; + + // Extend end time if death occurred after last kill + const endTime = group.death + ? Math.max(lastTime, group.death.video_timestamp[0] + group.death.video_timestamp[1] / 1e9) + : lastTime; clips.push({ id: clipId++, start_time: Math.max(0, firstTime - settings.buffer_before), - end_time: Math.min(duration, lastTime + settings.buffer_after), - highlight_type: "multi_kill", - events: group, - title: label || `${group.length}x Kill`, - subtitle: `${group.length} kills in quick succession`, + end_time: Math.min(duration, endTime + settings.buffer_after), + highlight_type: group.isTradeKill ? "trade_kill" : "multi_kill", + events: allEvents, + title: group.isTradeKill ? `Trade Kill` : (label || `${group.kills.length}x Kill`), + subtitle: group.isTradeKill + ? `${group.kills.length} kills but died in the trade` + : `${group.kills.length} kills in quick succession`, }); } } @@ -1013,7 +1101,7 @@ export function computeHighlights( current.end_time = Math.max(current.end_time, next.end_time); current.events = [...current.events, ...next.events]; // Keep the "more important" highlight type - const typePriority: HighlightType[] = ["multi_kill", "kill", "death", "assist"]; + const typePriority: HighlightType[] = ["multi_kill", "trade_kill", "kill", "death", "assist"]; const currentIdx = typePriority.indexOf(current.highlight_type); const nextIdx = typePriority.indexOf(next.highlight_type); if (nextIdx < currentIdx) { @@ -1021,14 +1109,39 @@ export function computeHighlights( current.title = next.title; current.subtitle = next.subtitle; } - // If both are kills, check if combined they form a multi-kill - const killEvents = current.events.filter(e => e.event_type === "kill"); - if (killEvents.length >= 2) { - const label = getMultiKillLabel(killEvents.length); + // If both are kills, check if combined they form a multi-kill or trade kill + // Only count kills where the player is the killer, not all kill events + // Deduplicate by video_timestamp since same event can appear in multiple merged clips + const seenTimestamps = new Set(); + const playerKillEvents = current.events.filter(e => { + if (e.event_type !== "kill") return false; + const rawData = e.raw_data as { KillerName?: string } | null; + if (rawData?.KillerName !== localPlayerName) return false; + // Deduplicate by timestamp + const key = `${e.video_timestamp[0]}-${e.video_timestamp[1]}`; + if (seenTimestamps.has(key)) return false; + seenTimestamps.add(key); + return true; + }); + const playerDeathEvents = current.events.filter(e => { + if (e.event_type !== "kill") return false; + const rawData = e.raw_data as { VictimName?: string } | null; + if (rawData?.VictimName !== localPlayerName) return false; + // Deduplicate by timestamp + const key = `${e.video_timestamp[0]}-${e.video_timestamp[1]}`; + if (seenTimestamps.has(key)) return false; + seenTimestamps.add(key); + return true; + }); + if (playerKillEvents.length >= 2) { + const label = getMultiKillLabel(playerKillEvents.length); if (label) { - current.highlight_type = "multi_kill"; - current.title = label; - current.subtitle = `${killEvents.length} kills`; + const isTradeKill = playerDeathEvents.length > 0; + current.highlight_type = isTradeKill ? "trade_kill" : "multi_kill"; + current.title = isTradeKill ? `Trade Kill` : label; + current.subtitle = isTradeKill + ? `${playerKillEvents.length} kills but died in the trade` + : `${playerKillEvents.length} kills`; } } } else {