Skip to content

Commit 328f368

Browse files
committed
feat(player): add heatmap tab
ref #1243
1 parent a23ed01 commit 328f368

26 files changed

+524
-5
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function isDefuseMapFromName(mapName: string): boolean {
2+
return mapName.startsWith('de_');
3+
}

src/common/types/heatmap-filters.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,20 @@ export type TeamHeatmapFilter = {
3131
thresholdZ: number | null;
3232
players: PlayerResult[];
3333
};
34+
35+
export type PlayerHeatmapFilter = {
36+
steamId: string;
37+
event: HeatmapEvent;
38+
games: Game[];
39+
mapName: string;
40+
demoTypes: DemoType[];
41+
startDate: string | undefined;
42+
endDate: string | undefined;
43+
gameModes: GameMode[];
44+
maxRounds: number[];
45+
sides: TeamNumber[];
46+
sources: DemoSource[];
47+
radarLevel: RadarLevel;
48+
thresholdZ: number | null;
49+
tagIds: string[];
50+
};
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { sql } from 'kysely';
2+
import { GrenadeName } from 'csdm/common/types/counter-strike';
3+
import type { Point } from 'csdm/common/types/point';
4+
import type { PlayerHeatmapFilter } from 'csdm/common/types/heatmap-filters';
5+
import { db } from 'csdm/node/database/database';
6+
import { HeatmapEvent } from 'csdm/common/types/heatmap-event';
7+
8+
export async function fetchPlayerGrenadePoints(filters: PlayerHeatmapFilter): Promise<Point[]> {
9+
const grenadeNames: GrenadeName[] = [];
10+
switch (filters.event) {
11+
case HeatmapEvent.Smoke:
12+
grenadeNames.push(GrenadeName.Smoke);
13+
break;
14+
case HeatmapEvent.Decoy:
15+
grenadeNames.push(GrenadeName.Decoy);
16+
break;
17+
case HeatmapEvent.Flashbang:
18+
grenadeNames.push(GrenadeName.Flashbang);
19+
break;
20+
case HeatmapEvent.HeGrenade:
21+
grenadeNames.push(GrenadeName.HE);
22+
break;
23+
case HeatmapEvent.Molotov:
24+
grenadeNames.push(GrenadeName.Molotov, GrenadeName.Incendiary);
25+
break;
26+
default:
27+
throw new Error(`Unsupported grenade event: ${filters.event}`);
28+
}
29+
30+
let query = db
31+
.selectFrom('grenade_projectiles_destroy')
32+
.select(['x', 'y'])
33+
.leftJoin('matches', 'checksum', 'match_checksum')
34+
.where('matches.map_name', '=', filters.mapName)
35+
.where('thrower_steam_id', '=', filters.steamId)
36+
.where('grenade_name', 'in', grenadeNames);
37+
38+
if (filters.startDate !== undefined && filters.endDate !== undefined) {
39+
query = query.where(sql<boolean>`matches.date between ${filters.startDate} and ${filters.endDate}`);
40+
}
41+
42+
if (filters.sources.length > 0) {
43+
query = query.where('matches.source', 'in', filters.sources);
44+
}
45+
46+
if (filters.games.length > 0) {
47+
query = query.where('matches.game', 'in', filters.games);
48+
}
49+
50+
if (filters.demoTypes.length > 0) {
51+
query = query.where('matches.type', 'in', filters.demoTypes);
52+
}
53+
54+
if (filters.gameModes.length > 0) {
55+
query = query.where('matches.game_mode_str', 'in', filters.gameModes);
56+
}
57+
58+
if (filters.maxRounds.length > 0) {
59+
query = query.where('max_rounds', 'in', filters.maxRounds);
60+
}
61+
62+
if (filters.sides.length > 0) {
63+
query = query.where('thrower_side', 'in', filters.sides);
64+
}
65+
66+
if (filters.tagIds.length > 0) {
67+
query = query
68+
.leftJoin('checksum_tags', 'checksum_tags.checksum', 'matches.checksum')
69+
.where('checksum_tags.tag_id', 'in', filters.tagIds);
70+
}
71+
72+
const points = query.execute();
73+
74+
return points;
75+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { sql } from 'kysely';
2+
import type { PlayerHeatmapFilter } from 'csdm/common/types/heatmap-filters';
3+
import type { Point } from 'csdm/common/types/point';
4+
import { HeatmapEvent } from 'csdm/common/types/heatmap-event';
5+
import { db } from 'csdm/node/database/database';
6+
import { RadarLevel } from 'csdm/ui/maps/radar-level';
7+
8+
function buildQuery(filters: PlayerHeatmapFilter) {
9+
let query = db
10+
.selectFrom('kills')
11+
.leftJoin('matches', 'checksum', 'match_checksum')
12+
.where('kills.killer_steam_id', '=', filters.steamId)
13+
.where('matches.map_name', '=', filters.mapName);
14+
15+
if (filters.thresholdZ) {
16+
query = query.where('killer_z', filters.radarLevel === RadarLevel.Upper ? '>=' : '<', filters.thresholdZ);
17+
}
18+
19+
if (filters.startDate !== undefined && filters.endDate !== undefined) {
20+
query = query.where(sql<boolean>`matches.date between ${filters.startDate} and ${filters.endDate}`);
21+
}
22+
23+
if (filters.sources.length > 0) {
24+
query = query.where('matches.source', 'in', filters.sources);
25+
}
26+
27+
if (filters.games.length > 0) {
28+
query = query.where('matches.game', 'in', filters.games);
29+
}
30+
31+
if (filters.demoTypes.length > 0) {
32+
query = query.where('matches.type', 'in', filters.demoTypes);
33+
}
34+
35+
if (filters.gameModes.length > 0) {
36+
query = query.where('matches.game_mode_str', 'in', filters.gameModes);
37+
}
38+
39+
if (filters.maxRounds.length > 0) {
40+
query = query.where('max_rounds', 'in', filters.maxRounds);
41+
}
42+
43+
if (filters.sides.length > 0) {
44+
query = query.where('killer_side', 'in', filters.sides);
45+
}
46+
47+
if (filters.tagIds.length > 0) {
48+
query = query
49+
.leftJoin('checksum_tags', 'checksum_tags.checksum', 'matches.checksum')
50+
.where('checksum_tags.tag_id', 'in', filters.tagIds);
51+
}
52+
53+
return query;
54+
}
55+
56+
export async function fetchPlayerKillsPoints(filters: PlayerHeatmapFilter): Promise<Point[]> {
57+
switch (filters.event) {
58+
case HeatmapEvent.Kills: {
59+
const query = buildQuery(filters).select(['killer_x as x', 'killer_y as y']);
60+
61+
const points = await query.execute();
62+
63+
return points;
64+
}
65+
case HeatmapEvent.Deaths: {
66+
const query = buildQuery(filters).select(['victim_x as x', 'victim_y as y']);
67+
68+
const points = await query.execute();
69+
70+
return points;
71+
}
72+
default:
73+
throw new Error(`Unsupported kills points event: ${filters.event}`);
74+
}
75+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { sql } from 'kysely';
2+
import type { Point } from 'csdm/common/types/point';
3+
import type { PlayerHeatmapFilter } from 'csdm/common/types/heatmap-filters';
4+
import { db } from 'csdm/node/database/database';
5+
6+
export async function fetchPlayerShotsPoints(filters: PlayerHeatmapFilter): Promise<Point[]> {
7+
let query = db
8+
.selectFrom('shots')
9+
.select(['x', 'y'])
10+
.leftJoin('matches', 'checksum', 'match_checksum')
11+
.where('matches.map_name', '=', filters.mapName)
12+
.where('player_steam_id', '=', filters.steamId);
13+
14+
if (filters.startDate !== undefined && filters.endDate !== undefined) {
15+
query = query.where(sql<boolean>`matches.date between ${filters.startDate} and ${filters.endDate}`);
16+
}
17+
18+
if (filters.sources.length > 0) {
19+
query = query.where('matches.source', 'in', filters.sources);
20+
}
21+
22+
if (filters.games.length > 0) {
23+
query = query.where('matches.game', 'in', filters.games);
24+
}
25+
26+
if (filters.demoTypes.length > 0) {
27+
query = query.where('matches.type', 'in', filters.demoTypes);
28+
}
29+
30+
if (filters.gameModes.length > 0) {
31+
query = query.where('matches.game_mode_str', 'in', filters.gameModes);
32+
}
33+
34+
if (filters.maxRounds.length > 0) {
35+
query = query.where('max_rounds', 'in', filters.maxRounds);
36+
}
37+
38+
if (filters.sides.length > 0) {
39+
query = query.where('player_side', 'in', filters.sides);
40+
}
41+
42+
if (filters.tagIds.length > 0) {
43+
query = query
44+
.leftJoin('checksum_tags', 'checksum_tags.checksum', 'matches.checksum')
45+
.where('checksum_tags.tag_id', 'in', filters.tagIds);
46+
}
47+
48+
const points = await query.execute();
49+
50+
return points;
51+
}

src/server/handlers/renderer-handlers-mapping.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ import { insertTagHandler } from './renderer-process/tags/insert-tag-handler';
8686
import type { UpdateChecksumsTagsPayload } from './renderer-process/tags/update-checksums-tags-handler';
8787
import { updateChecksumsTagsHandler } from './renderer-process/tags/update-checksums-tags-handler';
8888
import { isCounterStrikeRunningHandler } from './renderer-process/counter-strike/is-counter-strike-running-handler';
89-
import type { MatchHeatmapFilter, TeamHeatmapFilter } from 'csdm/common/types/heatmap-filters';
89+
import type { MatchHeatmapFilter, PlayerHeatmapFilter, TeamHeatmapFilter } from 'csdm/common/types/heatmap-filters';
9090
import type { Demo } from 'csdm/common/types/demo';
9191
import type { Map } from 'csdm/common/types/map';
9292
import type { DatabaseSettings } from 'csdm/node/settings/settings';
@@ -221,6 +221,7 @@ import { updateCameraHandler } from './renderer-process/cameras/update-camera-ha
221221
import { deleteCameraHandler } from './renderer-process/cameras/delete-camera-handler';
222222
import { capturePlayerViewHandler } from './renderer-process/counter-strike/capture-player-view-handler';
223223
import type { CapturePlayerViewPayload } from 'csdm/node/counter-strike/launcher/capture-player-view';
224+
import { fetchPlayerHeatmapPointsHandler } from './renderer-process/player/fetch-player-heatmap-points-handler';
224225

225226
export interface RendererMessageHandlers {
226227
[RendererClientMessageName.InitializeApplication]: Handler<void, InitializeApplicationSuccessPayload>;
@@ -233,6 +234,7 @@ export interface RendererMessageHandlers {
233234
[RendererClientMessageName.FetchMatchByChecksum]: Handler<string, Match>;
234235
[RendererClientMessageName.FetchMatchHeatmapPoints]: Handler<MatchHeatmapFilter, Point[]>;
235236
[RendererClientMessageName.FetchTeamHeatmapPoints]: Handler<TeamHeatmapFilter, Point[]>;
237+
[RendererClientMessageName.FetchPlayerHeatmapPoints]: Handler<PlayerHeatmapFilter, Point[]>;
236238
[RendererClientMessageName.Fetch2DViewerData]: Handler<Fetch2dViewerDataPayload, Fetch2dViewerDataSuccessPayload>;
237239
[RendererClientMessageName.UpdateComment]: Handler<UpdateCommentPayload>;
238240
[RendererClientMessageName.UpdatePlayerComment]: Handler<UpdatePlayerCommentPayload>;
@@ -357,6 +359,7 @@ export const rendererHandlers: RendererMessageHandlers = {
357359
[RendererClientMessageName.FetchMatchByChecksum]: fetchMatchByChecksumHandler,
358360
[RendererClientMessageName.FetchMatchHeatmapPoints]: fetchMatchHeatmapPointsHandler,
359361
[RendererClientMessageName.FetchTeamHeatmapPoints]: fetchTeamHeatmapPointsHandler,
362+
[RendererClientMessageName.FetchPlayerHeatmapPoints]: fetchPlayerHeatmapPointsHandler,
360363
[RendererClientMessageName.Fetch2DViewerData]: fetch2DViewerDataHandler,
361364
[RendererClientMessageName.UpdateComment]: updateCommentHandler,
362365
[RendererClientMessageName.UpdatePlayerComment]: updatePlayerCommentHandler,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { PlayerHeatmapFilter } from 'csdm/common/types/heatmap-filters';
2+
import { HeatmapEvent } from 'csdm/common/types/heatmap-event';
3+
import type { Point } from 'csdm/common/types/point';
4+
import { assertNever } from 'csdm/common/assert-never';
5+
import { handleError } from '../../handle-error';
6+
import { fetchPlayerKillsPoints } from 'csdm/node/database/heatmap/fetch-player-kills-points';
7+
import { fetchPlayerShotsPoints } from 'csdm/node/database/heatmap/fetch-player-shots-points';
8+
import { fetchPlayerGrenadePoints } from 'csdm/node/database/heatmap/fetch-player-grenade-points';
9+
10+
export async function fetchPlayerHeatmapPointsHandler(filter: PlayerHeatmapFilter) {
11+
try {
12+
let points: Point[] = [];
13+
switch (filter.event) {
14+
case HeatmapEvent.Kills:
15+
case HeatmapEvent.Deaths:
16+
points = await fetchPlayerKillsPoints(filter);
17+
break;
18+
case HeatmapEvent.Shots:
19+
points = await fetchPlayerShotsPoints(filter);
20+
break;
21+
case HeatmapEvent.Molotov:
22+
case HeatmapEvent.HeGrenade:
23+
case HeatmapEvent.Flashbang:
24+
case HeatmapEvent.Smoke:
25+
case HeatmapEvent.Decoy:
26+
points = await fetchPlayerGrenadePoints(filter);
27+
break;
28+
default:
29+
assertNever(filter.event, `Unsupported heatmap event: ${filter.event}`);
30+
}
31+
32+
return points;
33+
} catch (error) {
34+
handleError(error, 'Error while fetching player heatmap points');
35+
}
36+
}

src/server/renderer-client-message-name.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const RendererClientMessageName = {
2828
FetchTeamsTable: 'fetch-teams-table',
2929
FetchTeam: 'fetch-team',
3030
FetchTeamHeatmapPoints: 'fetch-team-heatmap-points',
31+
FetchPlayerHeatmapPoints: 'fetch-player-heatmap-points',
3132
AddDemosToAnalyses: 'add-demos-to-analyses',
3233
RemoveDemosFromAnalyses: 'remove-demos-from-analyses',
3334
GenerateMatchPositions: 'generate-match-positions',

src/ui/match/overview/match-overview-provider.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { MatchOverviewContext } from './match-overview-context';
1010
import { useContextMenu } from 'csdm/ui/components/context-menu/use-context-menu';
1111
import type { TableInstance } from 'csdm/ui/components/table/table-types';
1212
import { useTable } from 'csdm/ui/components/table/use-table';
13+
import { isDefuseMapFromName } from 'csdm/common/counter-strike/is-defuse-map-from-name';
1314

1415
function getRowId(player: MatchPlayer) {
1516
return player.steamId;
@@ -28,7 +29,7 @@ export function MatchOverviewProvider({ children }: Props) {
2829
const match = useCurrentMatch();
2930
const playersTeamA = match.players.filter((player) => player.teamName === match.teamA.name);
3031
const playersTeamB = match.players.filter((player) => player.teamName === match.teamB.name);
31-
const isDefuseMap = match.mapName.startsWith('de_');
32+
const isDefuseMap = isDefuseMapFromName(match.mapName);
3233
const { showContextMenu } = useContextMenu();
3334
const columns = useScoreboardColumns(isDefuseMap);
3435
const navigateToMatchPlayer = useNavigateToMatchPlayer();

src/ui/match/viewer-2d/viewer-context.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
import { useViewer2DState } from './use-viewer-state';
3636
import { deleteDemoAudioOffset, persistDemoAudioOffset } from './audio/audio-offset';
3737
import type { DrawingTool } from './drawing/use-drawable-canvas';
38+
import { isDefuseMapFromName } from 'csdm/common/counter-strike/is-defuse-map-from-name';
3839

3940
type ViewerMode = 'drawing' | 'playback';
4041

@@ -180,7 +181,7 @@ export function ViewerProvider({
180181
const remainingTickCount = round.endOfficiallyTick - currentTick;
181182
const tickrate = match.tickrate > 0 ? match.tickrate : 64;
182183
const timeRemaining = (remainingTickCount / tickrate) * 1000;
183-
const shouldDrawBombs = match.mapName.startsWith('de_');
184+
const shouldDrawBombs = isDefuseMapFromName(match.mapName);
184185
const navigate = useNavigate();
185186
const { audioOffsetSeconds, volume } = viewerState;
186187

0 commit comments

Comments
 (0)