File size: 5,466 Bytes
31d3580
 
 
 
 
95e3d2a
15d8696
 
 
31d3580
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95e3d2a
 
31d3580
 
 
 
 
 
95e3d2a
 
8899818
 
 
 
95e3d2a
31d3580
 
0a96402
31d3580
 
 
 
 
 
 
8899818
31d3580
 
 
 
 
15d8696
9d3dfa1
15d8696
31d3580
 
 
 
 
 
8899818
31d3580
 
 
8899818
31d3580
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95e3d2a
31d3580
15d8696
31d3580
 
 
 
 
 
 
 
 
 
95e3d2a
31d3580
15d8696
31d3580
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
<script lang="ts">
	import type { PreviewChunk } from '$lib/types';
	import * as ToggleGroup from '$lib/components/ui/toggle-group';
	import { Switch } from '$lib/components/ui/switch';
	import { Label } from '$lib/components/ui/label';
	import { cn } from '$lib/utils';
	import SkullIcon from 'phosphor-svelte/lib/SkullIcon';
	import ShieldIcon from 'phosphor-svelte/lib/ShieldIcon';
	import SwordIcon from 'phosphor-svelte/lib/SwordIcon';
	import { formatDuration } from '$lib/utils/format';
	import { playerColor } from '$lib/utils/player-colors';

	interface Props {
		previews: PreviewChunk[];
		currentPlayer: number;
		preloadedPlayers: Set<number>;
		availablePlayers: Set<number>;
		playerDurations: Map<number, number>;
		preloadAll: boolean;
		onSelect: (player: number) => void;
		onPreloadAllChange: (enabled: boolean) => void;
	}
	let {
		previews,
		currentPlayer,
		preloadedPlayers,
		availablePlayers,
		playerDurations,
		preloadAll,
		onSelect,
		onPreloadAllChange
	}: Props = $props();

	type PlayerSummary = {
		player: number;
		side: 'CT' | 'T' | string;
		weapon: string;
		survived: boolean;
		duration: number;
	};

	const players = $derived.by<PlayerSummary[]>(() => {
		const map = new Map<number, PreviewChunk[]>();
		for (const p of previews) {
			if (!map.has(p.player)) map.set(p.player, []);
			map.get(p.player)!.push(p);
		}
		const out: PlayerSummary[] = [];
		for (const [player, chunks] of map.entries()) {
			chunks.sort((a, b) => a.chunk_index - b.chunk_index);
			const last = chunks[chunks.length - 1];
			out.push({
				player,
				side: last.player_side,
				weapon: last.primary_weapon,
				survived: last.survived_chunk,
				duration: playerDurations.get(player) ?? 0
			});
		}
		out.sort((a, b) => a.player - b.player);
		return out;
	});

	const ctPlayers = $derived(players.filter((p) => p.side === 'CT'));
	const tPlayers = $derived(players.filter((p) => p.side === 'T'));
</script>

{#snippet playerItem(p: PlayerSummary)}
	{@const ct = p.side === 'CT'}
	{@const ready = preloadedPlayers.has(p.player)}
	{@const available = availablePlayers.has(p.player)}
	<ToggleGroup.Item
		data-ready={available && ready ? true : undefined}
		data-side={ct ? 'ct' : 't'}
		value={String(p.player)}
		aria-label="Player {p.player} ({p.weapon || 'no weapon'})"
		title={available
			? `Player ${p.player}${p.weapon || 'no weapon'}`
			: `Player ${p.player} died at ${formatDuration(p.duration)} — not available at current time`}
		disabled={!available}
		class={cn(
			'group flex h-auto min-w-0 flex-1 basis-0 items-center justify-start gap-2 rounded-md border px-2 py-1.5 text-left text-sm transition',
			'disabled:cursor-not-allowed disabled:opacity-40 data-[state=on]:border-current data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
			'data-[side=ct]:data-[state=on]:border-sky-500 data-[side=ct]:data-[state=on]:bg-sky-500/10 data-[side=ct]:data-[state=on]:text-foreground',
			'data-[side=t]:data-[state=on]:border-amber-500 data-[side=t]:data-[state=on]:bg-amber-500/10 data-[side=t]:data-[state=on]:text-foreground',
			'data-[ready=true]:ring-2 data-[ready=true]:ring-emerald-500/60 data-[ready=true]:ring-offset-1 data-[ready=true]:ring-offset-background'
		)}
	>
		<span
			class="flex size-7 shrink-0 items-center justify-center rounded-sm font-mono text-xs font-bold text-white/95 shadow-sm ring-1 ring-black/20"
			style="background-color: {playerColor(p.player)};"
		>
			{p.player}
		</span>
		<div class="min-w-0 flex-1">
			<div class="truncate font-medium">Player {p.player}</div>
			{#if !available}
				<div class="text-[10px] text-muted-foreground tabular-nums">
					died · {formatDuration(p.duration)}
				</div>
			{/if}
		</div>
		{#if p.survived}
			<ShieldIcon size={14} weight="duotone" class="text-emerald-500" />
		{:else if !available}
			<SkullIcon size={14} weight="duotone" class="text-rose-500" />
		{/if}
	</ToggleGroup.Item>
{/snippet}

<div class="space-y-2">
	<div class="flex items-center justify-between gap-3">
		<div class="text-xs tracking-wide text-muted-foreground uppercase">Player perspectives</div>
		<div class="flex items-center gap-2">
			<Label
				for="preload-all-players"
				class="text-[11px] tracking-wide text-muted-foreground uppercase"
			>
				Preload all
			</Label>
			<Switch
				id="preload-all-players"
				size="sm"
				checked={preloadAll}
				onCheckedChange={(v) => onPreloadAllChange(!!v)}
			/>
		</div>
	</div>

	<ToggleGroup.Root
		type="single"
		value={String(currentPlayer)}
		onValueChange={(v) => v && onSelect(Number(v))}
		variant="outline"
		spacing={1}
		aria-label="Player perspective"
		class="w-full! flex-col gap-2"
	>
		<div class="w-full">
			<div
				class="mb-1 flex items-center gap-1.5 text-[10px] font-semibold tracking-wide text-sky-700 uppercase dark:text-sky-400"
			>
				<ShieldIcon size={11} weight="duotone" /> Counter-Terrorists
			</div>
			<div class="flex w-full items-stretch gap-1.5">
				{#each ctPlayers as p (p.player)}
					{@render playerItem(p)}
				{/each}
			</div>
		</div>

		<div class="w-full">
			<div
				class="mb-1 flex items-center gap-1.5 text-[10px] font-semibold tracking-wide text-amber-700 uppercase dark:text-amber-400"
			>
				<SwordIcon size={11} weight="duotone" /> Terrorists
			</div>
			<div class="flex w-full items-stretch gap-1.5">
				{#each tPlayers as p (p.player)}
					{@render playerItem(p)}
				{/each}
			</div>
		</div>
	</ToggleGroup.Root>
</div>