fix: tryfix multikill grouping
record-daemon / Build, check and test (push) Failing after 15m23s

This commit is contained in:
2026-05-30 19:27:31 +02:00
parent 598c5a2391
commit abb35b8fac
2 changed files with 147 additions and 28 deletions
@@ -42,6 +42,7 @@ const typeToggles = ref<Record<HighlightType, boolean>>({
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 {
<div class="stat-pill" v-if="multiKillCount > 0">
<span>🔥</span> {{ multiKillCount }}
</div>
<div class="stat-pill" v-if="tradeKillCount > 0">
<span>🔄</span> {{ tradeKillCount }}
</div>
<div class="stat-pill total">
<span>⏱️</span> {{ formatDuration(totalDuration) }}
</div>
+141 -28
View File
@@ -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<string>();
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 {