games: per-game pages, games.library table, games group in header/tiles/start
All checks were successful
Deploy production / deploy (push) Successful in 1m7s

This commit is contained in:
2026-10-25 15:20:45 +00:00
parent 037a525f91
commit d79b73ad83
19 changed files with 27712 additions and 38 deletions

View File

@@ -15,6 +15,8 @@
href: string;
external: boolean;
children?: readonly Item[];
icon?: string;
featured?: boolean;
};
let {
items,
@@ -29,6 +31,14 @@
} = $props();
const tracker = $derived(variant === 'tracker');
// Big groups (games) show their featured chips first and the rest behind a "more" chip.
let expanded = $state<Record<string, boolean>>({});
const visibleChildren = (group: Item) => {
const all = group.children ?? [];
return expanded[group.name] ? all : all.filter((c) => c.featured ?? true);
};
const hiddenCount = (group: Item) =>
(group.children?.length ?? 0) - visibleChildren(group).length;
</script>
{#snippet body(project: Item)}
@@ -79,16 +89,31 @@
{@render body(project)}
</a>
<ul class="relative z-10 mt-auto flex flex-wrap gap-1.5 pl-13">
{#each project.children as child (child.name)}
{#each visibleChildren(project) as child (child.name)}
<li>
<a
href={resolve(child.href as '/')}
class="chip flex items-center gap-1.5 py-1 pr-2.5 pl-1 text-xs font-medium"
title={child.subtitle}
><TrackerIcon name={child.name} class="chip-icon size-5" />{child.name}</a
><TrackerIcon
name={child.icon ?? child.name}
class="chip-icon size-5"
/>{child.name}</a
>
</li>
{/each}
{#if hiddenCount(project) > 0 || expanded[project.name]}
<li>
<button
type="button"
class="chip flex h-full cursor-pointer items-center px-2.5 py-1 text-xs font-medium text-[var(--site-muted)]"
aria-expanded={!!expanded[project.name]}
onclick={() =>
(expanded = { ...expanded, [project.name]: !expanded[project.name] })}
>{hiddenCount(project) > 0 ? `+${hiddenCount(project)} more` : 'less'}</button
>
</li>
{/if}
</ul>
</div>
{:else if project.external}

View File

@@ -184,16 +184,22 @@
// into the flyout (crossing a neighbouring tile) does not close it.
let flyout = $state<string | null>(null);
let flyoutSide = $state<'left' | 'right'>('right');
let flyoutUp = $state(false);
let flyoutTimer: ReturnType<typeof setTimeout> | undefined;
const FLYOUT_WIDTH = 240;
function openFlyout(name: string, el: HTMLElement) {
// Big groups (games) get a two-column flyout; it opens upwards when it would not fit below.
const WIDE_FLYOUT = 6;
function openFlyout(name: string, el: HTMLElement, count: number) {
clearTimeout(flyoutTimer);
if (flyout === name) return;
const wide = count > WIDE_FLYOUT;
const width = wide ? 432 : 240;
const height = (wide ? Math.ceil(count / 2) : count) * 46 + 20;
const rect = el.getBoundingClientRect();
const panel = el.closest('[data-menu-panel]')?.getBoundingClientRect() ?? rect;
const leftColumn = rect.left + rect.width / 2 < panel.left + panel.width / 2;
const roomLeft = rect.left - FLYOUT_WIDTH >= 8;
const roomRight = rect.right + FLYOUT_WIDTH <= window.innerWidth - 8;
const roomLeft = rect.left - width >= 8;
const roomRight = rect.right + width <= window.innerWidth - 8;
flyoutUp = rect.top + height > window.innerHeight - 8 && rect.bottom - height >= 8;
flyoutSide = leftColumn ? (roomLeft ? 'left' : 'right') : roomRight ? 'right' : 'left';
flyout = name;
}
@@ -308,12 +314,21 @@
aria-label={`${project.name} trackers`}
onpointerenter={(event) => {
if (event.pointerType !== 'touch')
openFlyout(project.name, event.currentTarget);
openFlyout(
project.name,
event.currentTarget,
project.children?.length ?? 0
);
}}
onpointerleave={(event) => {
if (event.pointerType !== 'touch') closeFlyoutSoon();
}}
onfocusin={(event) => openFlyout(project.name, event.currentTarget)}
onfocusin={(event) =>
openFlyout(
project.name,
event.currentTarget,
project.children?.length ?? 0
)}
onfocusout={(event) => {
if (!event.currentTarget.contains(event.relatedTarget as Node | null))
flyout = null;
@@ -345,23 +360,30 @@
<!-- The padding facing the tile is a hover bridge into the panel. -->
<div
class={[
'absolute -top-2 w-60',
'absolute',
flyoutUp ? '-bottom-2' : '-top-2',
project.children.length > WIDE_FLYOUT ? 'w-[27rem]' : 'w-60',
flyoutSide === 'right' ? 'left-full pl-3' : 'right-full pr-3'
]}
>
<div
class="flyout grid gap-1 rounded-md border border-border bg-popover p-1.5 shadow-lg"
class={[
'flyout grid gap-1 rounded-md border border-border bg-popover p-1.5 shadow-lg',
project.children.length > WIDE_FLYOUT && 'grid-cols-2'
]}
class:from-left={flyoutSide === 'left'}
>
{#each project.children as child (child.name)}
<a
href={resolve(child.href as '/')}
class="tile flex items-center gap-2.5 p-1.5 text-sm"
class="tile flex min-w-0 items-center gap-2.5 p-1.5 text-sm"
title={child.subtitle}
aria-current={current(child.href) ? 'page' : undefined}
onkeydown={closeFlyoutOnEscape}
><TrackerIcon name={child.name} class="tile-icon size-8" /><span
class="font-medium">{child.name}</span
></a
><TrackerIcon
name={child.icon ?? child.name}
class="tile-icon size-8"
/><span class="truncate font-medium">{child.name}</span></a
>
{/each}
</div>
@@ -520,9 +542,10 @@
href={resolve(child.href as '/')}
class="tile flex min-w-0 items-center gap-2 p-1.5 text-sm"
onclick={() => (menuOpen = false)}
><TrackerIcon name={child.name} class="tile-icon size-6" /><span
class="truncate">{child.name}</span
></a
><TrackerIcon
name={child.icon ?? child.name}
class="tile-icon size-6"
/><span class="truncate">{child.name}</span></a
>
{/each}
</div>

View File

@@ -1,36 +1,49 @@
<script lang="ts">
// Small switcher shown above the cars/trains/planes/wildlife pages (via their +layout),
// to hop between the spotting pokedexes or back to /trackers/spotting.
// to hop between the spotting pokedexes or back to /trackers/spotting. Other tracker
// groups (games) pass their own items, home link and icon.
import type { Component } from 'svelte';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import Binoculars from 'lucide-svelte/icons/binoculars';
import TrackerIcon from '#lib/components/TrackerIcon.svelte';
import { spottingTrackers } from '#lib/projects.js';
type Item = { name: string; href: string; icon?: string; subtitle?: string };
let {
items = spottingTrackers,
home = '/trackers/spotting',
label = 'spotting',
homeIcon: HomeIcon = Binoculars,
class: className = 'relative mx-auto -mb-6 max-w-6xl px-4 pt-6 sm:-mb-8 sm:px-6 sm:pt-8'
}: {
items?: readonly Item[];
home?: string;
label?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- lucide icon components
homeIcon?: Component<any>;
class?: string;
} = $props();
const active = $derived(
spottingTrackers.find(
(t) => page.url.pathname === t.href || page.url.pathname.startsWith(`${t.href}/`)
)?.name
items.find((t) => page.url.pathname === t.href || page.url.pathname.startsWith(`${t.href}/`))
?.name
);
</script>
<nav
aria-label="Spotting trackers"
class="relative mx-auto -mb-6 max-w-6xl px-4 pt-6 sm:-mb-8 sm:px-6 sm:pt-8"
>
<nav aria-label="{label} trackers" class={className}>
<div class="flex flex-wrap items-center gap-1.5">
<a
href={resolve('/trackers/spotting')}
class="pill flex items-center gap-1.5 px-2.5 py-1.5 text-sm"
><Binoculars size={14} aria-hidden="true" class="text-[var(--site-muted)]" />spotting</a
<a href={resolve(home as '/')} class="pill flex items-center gap-1.5 px-2.5 py-1.5 text-sm"
><HomeIcon size={14} aria-hidden="true" class="text-[var(--site-muted)]" />{label}</a
>
<span class="mx-1 h-4 w-px bg-border" aria-hidden="true"></span>
{#each spottingTrackers as t (t.name)}
{#each items as t (t.name)}
<a
href={resolve(t.href as '/')}
aria-current={active === t.name ? 'page' : undefined}
class="pill flex items-center gap-1.5 py-1 pr-2.5 pl-1 text-sm"
><TrackerIcon name={t.name} class="pill-icon size-6" />{t.name}</a
title={t.subtitle}
><TrackerIcon name={t.icon ?? t.name} class="pill-icon size-6" />{t.name}</a
>
{/each}
</div>

View File

@@ -15,9 +15,9 @@
>
<svg
viewBox="0 0 24 24"
class="size-[58%]"
fill="none"
stroke="currentColor"
class={icon.fill ? 'size-[54%]' : 'size-[58%]'}
fill={icon.fill ? 'currentColor' : 'none'}
stroke={icon.fill ? 'none' : 'currentColor'}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"

View File

@@ -39,6 +39,10 @@ type Tracker = {
external: boolean;
/** Sub-trackers linked directly from this item (header flyout, tile chips). */
children?: readonly Tracker[];
/** Icon key in #lib/tracker-icons when it differs from `name` (games). */
icon?: string;
/** Shown on group tiles before the "more" chip (main and actively played games). */
featured?: boolean;
};
// The spotting trackers: pokedexes of things I can spot in the wild. Grouped under one
@@ -72,6 +76,37 @@ export const spottingTrackers: readonly Tracker[] = [
}
];
// Games I play (plus speedcubing), grouped under "games" like spotting: every game has its
// own page at /trackers/games/<slug>, one click away from the header flyout and the chips.
// `featured` games (my main game and the ones I actively play) show first on tiles; the
// rest sit behind a "more" chip. Data per game lives in games.library (same slugs).
const game = (slug: string, name: string, subtitle: string, featured = false): Tracker => ({
name,
subtitle,
href: `/trackers/games/${slug}`,
external: false,
icon: `game-${slug}`,
...(featured ? { featured } : {})
});
export const gameTrackers: readonly Tracker[] = [
game('osu', 'osu!', 'rhythm game, my main focus right now', true),
game('cs2', 'cs2', 'Counter-Strike 2 (CS:GO)', true),
game('pubg', 'pubg', 'PUBG: Battlegrounds', true),
game('r6', 'r6 siege', 'Tom Clancy’s Rainbow Six Siege', true),
game('tarkov', 'tarkov', 'Escape from Tarkov', true),
game('bo2', 'bo2', 'Call of Duty: Black Ops II', true),
game('valorant', 'valorant', 'Riot’s tactical shooter', false),
game('lol', 'league', 'League of Legends', false),
game('overwatch', 'overwatch 2', 'Blizzard’s hero shooter', false),
game('minecraft', 'minecraft', 'blocks, mining and building', false),
game('factorio', 'factorio', 'factory building and automation', false),
game('ksp', 'ksp', 'Kerbal Space Program', false),
game('smash', 'smash bros', 'Super Smash Bros.', false),
game('cubing', 'speedcubing', 'Rubik’s cube solving, not a video game', false),
game('pokemon', 'pokémon', 'catching them all across the games', false),
game('sot', 'sea of thieves', 'pirate sandbox', false)
];
// Trackers, in display order. Names are short single nouns. Most keep their existing
// /projects/... URLs (no moves, so no redirects needed). `private` trackers are shown in the
// header, home page and /trackers only to the logged-in owner (layout data `isOwner`), never
@@ -153,6 +188,7 @@ const allTrackers: readonly (Tracker & { private: boolean })[] = [
subtitle: 'every pokemon in every game, and call of duty camo mastery',
href: '/projects/game-tracker',
external: false,
children: gameTrackers,
private: false
},
{

View File

@@ -0,0 +1,51 @@
import { sql } from 'drizzle-orm';
import { check, integer, text } from 'drizzle-orm/pg-core';
import { gamesSchema } from './schemas';
import { id, timestamps } from './helpers';
// Every game I play (plus speedcubing): one row per game, shown at /trackers/games/<slug>.
// The header, home, /trackers and /start chips come from `gameTrackers` in #lib/projects
// (static, so navigation needs no query); keep the slugs in sync with these rows.
// Deep trackers (osu! journal, Call of Duty mastery, Pokémon catches) stay where they are
// and are linked through `tracker_href`.
export const GAME_STATUSES = ['main', 'active', 'occasional', 'unknown'] as const;
export const GAME_KINDS = ['video', 'puzzle'] as const;
export const gameLibrary = gamesSchema.table(
'library',
{
id: id(),
slug: text('slug').notNull().unique(),
name: text('name').notNull(),
shortName: text('short_name').notNull(),
kind: text('kind', { enum: GAME_KINDS }).notNull().default('video'),
status: text('status', { enum: GAME_STATUSES }).notNull().default('unknown'),
/** Where I play it (PC, Nintendo, physical…); null when not known yet. */
platform: text('platform'),
/** Internal path of a deeper tracker for this game, if there is one. */
trackerHref: text('tracker_href'),
/** Rank as a position (#40 000 is 40000); only values I gave. */
currentRank: integer('current_rank'),
peakRank: integer('peak_rank'),
/** What the rank numbers mean, e.g. "osu! standard global rank". */
rankLabel: text('rank_label'),
/** Names this game goes by elsewhere (hardware.sensitivities.game, old titles). */
aliases: text('aliases')
.array()
.notNull()
.default(sql`'{}'::text[]`),
notes: text('notes'),
sortOrder: integer('sort_order').notNull().default(100),
...timestamps
},
(t) => [
check('library_status_check', sql`${t.status} IN ('main','active','occasional','unknown')`),
check('library_kind_check', sql`${t.kind} IN ('video','puzzle')`),
check(
'library_rank_check',
sql`(${t.currentRank} IS NULL OR ${t.currentRank} > 0) AND (${t.peakRank} IS NULL OR ${t.peakRank} > 0)`
)
]
);
export type GameLibraryRow = typeof gameLibrary.$inferSelect;

View File

@@ -5,6 +5,7 @@ export * from './blog';
export * from './progress';
export * from './catch-em-all';
export * from './game-tracker';
export * from './game-library';
export * from './tracker-journal';
export * from './animaldex';
export * from './lists'; // meta registry; the lists themselves are runtime tables

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { trackerIcons } from './tracker-icons';
import { spottingTrackers, visibleTrackers } from './projects';
import { gameTrackers, spottingTrackers, visibleTrackers } from './projects';
describe('tracker icons', () => {
it('gives every tracker its own colour', () => {
@@ -10,6 +10,19 @@ describe('tracker icons', () => {
expect(new Set(colours).size).toBe(colours.length);
});
it('gives every game its own icon and colour', () => {
const keys = gameTrackers.map((g) => g.icon ?? g.name);
for (const key of keys) expect(trackerIcons[key], key).toBeDefined();
expect(new Set(keys.map((k) => trackerIcons[k].to)).size).toBe(keys.length);
});
it('links every game to its own page under /trackers/games', () => {
const slugs = gameTrackers.map((g) => g.href.replace('/trackers/games/', ''));
for (const slug of slugs) expect(slug).toMatch(/^[a-z0-9-]+$/);
expect(new Set(slugs).size).toBe(slugs.length);
expect(gameTrackers[0].name).toBe('osu!');
});
it('keeps tasks, money and reading first', () => {
expect(
visibleTrackers(true)

View File

@@ -12,6 +12,8 @@ export type TrackerIcon = {
to: string;
/** Icon colour; white unless the tile is light (yellow, lime). */
ink: string;
/** Filled glyph (brand marks from simple-icons) instead of stroked outline paths. */
fill?: boolean;
paths: string[];
};
@@ -163,6 +165,157 @@ export const trackerIcons: Record<string, TrackerIcon> = {
to: '#475569',
ink: WHITE,
paths: ['M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01']
},
// Games (keys are each game's `icon` in gameTrackers): brand marks where simple-icons has
// a clean glyph, otherwise a lucide-style outline that hints at the game.
'game-osu': {
from: '#ff79b0',
to: '#e6338a',
ink: WHITE,
fill: true,
paths: [
'M7.698 10.362a1.94 1.94 0 0 0-.7-.516q-.421-.189-.988-.189c-.567 0-.704.063-.982.189s-.51.298-.692.516q-.273.328-.413.777q-.139.448-.139.96q0 .511.14.952q.139.44.412.767q.274.329.692.512t.982.184c.565 0 .707-.062.988-.184q.422-.184.7-.512q.279-.327.413-.767q.135-.44.135-.952a3.3 3.3 0 0 0-.135-.96a2.1 2.1 0 0 0-.413-.777m-.965 2.81q-.22.372-.723.372q-.494 0-.713-.372q-.22-.373-.22-1.073c0-.7.073-.824.22-1.073q.22-.372.713-.372q.503 0 .723.372q.22.373.22 1.073t-.22 1.073m11.89-.83l-.09-4.39a4.5 4.5 0 0 1 .69-.054q.351 0 .701.054l-.09 4.39q-.315.053-.601.053a3.5 3.5 0 0 1-.61-.054m1.319 1.4q0 .332-.054.664a4 4 0 0 1-.655.054a4 4 0 0 1-.664-.054a4 4 0 0 1-.054-.655q0-.323.054-.665a4 4 0 0 1 .655-.054q.323 0 .664.054q.054.341.054.656m-3.223-4.03q.315 0 .638.053v4.461q-.288.099-.759.193a5.3 5.3 0 0 1-1.863.023a1.7 1.7 0 0 1-.74-.305q-.32-.234-.507-.683q-.189-.449-.189-1.193V9.765a4 4 0 0 1 .638-.054q.313 0 .637.054v2.46q0 .367.058.606a.9.9 0 0 0 .18.377a.66.66 0 0 0 .3.197q.18.058.422.058q.332 0 .557-.062V9.765a4 4 0 0 1 .628-.054m-4.362 2.683q.08.225.08.548a1.4 1.4 0 0 1-.542 1.117q-.265.212-.642.333q-.378.12-.853.12a5 5 0 0 1-.395-.013a3 3 0 0 1-.346-.045a4 4 0 0 1-.327-.076a4 4 0 0 1-.35-.116a2.6 2.6 0 0 1 .085-.49a3 3 0 0 1 .175-.48q.296.117.561.175q.265.06.552.059q.126 0 .274-.023a1 1 0 0 0 .274-.08a.7.7 0 0 0 .21-.153a.35.35 0 0 0 .086-.247q0-.216-.13-.31a1.3 1.3 0 0 0-.364-.166l-.556-.162q-.503-.143-.786-.426q-.282-.283-.283-.848q0-.682.49-1.068q.489-.386 1.332-.386q.35 0 .692.062q.341.063.691.189a2.5 2.5 0 0 1-.09.485a2.3 2.3 0 0 1-.17.44a4 4 0 0 0-.476-.158a2.2 2.2 0 0 0-.548-.067q-.305 0-.476.094a.32.32 0 0 0-.17.301q0 .197.121.278t.346.153l.511.153q.252.072.454.175t.345.255t.225.377M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12c6.628 0 12-5.373 12-12S18.628 0 12 0m0 22.8C6.035 22.8 1.2 17.965 1.2 12S6.035 1.2 12 1.2S22.8 6.035 22.8 12S17.965 22.8 12 22.8'
]
},
'game-cs2': {
from: '#fcd34d',
to: '#d97706',
ink: WHITE,
fill: true,
paths: [
'M21.71 3.235a.02.02 0 0 1-.022-.022c.002-.081.004-.37.005-.424c0-.129-.143-.183-.212-.083l-.229.333a.02.02 0 0 1-.02.01h-6.55a.047.047 0 0 1-.048-.046l-.013-.177a.048.048 0 0 1 .056-.048l.335.032a.06.06 0 0 0 .063-.045l.244-.989a.05.05 0 0 0-.03-.054l-.227-.085a.04.04 0 0 1-.026-.03c-.041-.171-.377-1.323-1.993-1.58c-.787-.125-1.302.21-1.577.478a1.6 1.6 0 0 0-.302.41l-.097.212a2 2 0 0 0-.046.234l.051.982a.11.11 0 0 0 .043.085l.354.153l-.196.325a.055.055 0 0 1-.053.04s-.417.01-.622.02c-.386.015-1.245.485-1.878 1.838l-.724 1.55a.07.07 0 0 1-.068.04l-.578.001c-.035 0-.073.028-.088.06L6.364 9a.11.11 0 0 0 .017.108l.627.392a.06.06 0 0 1 .02.058l-.328.967a.2.2 0 0 1-.023.062l-.435.382a.1.1 0 0 0-.035.06l-.598 1.53a.06.06 0 0 1-.06.045l-.336.002a.163.163 0 0 0-.162.149l-.201 2.288l-.016.121l-.158.908a.13.13 0 0 1-.034.055l-.558.427a4.8 4.8 0 0 0-.767 1.001l-1.86 3.924a.8.8 0 0 0-.078.322l.132.235c.002.084-.032.456-.07.53l-.624 1.09a.1.1 0 0 0-.003.085l.03.07l.094.187L2.829 24c.118.011.247-.14.251-.3l.103-1.297l-.027-.195l3.606-4.232c.095-.114.222-.317.286-.45l1.719-3.79a.17.17 0 0 1 .1-.088l.109-.035a.17.17 0 0 1 .183.053c.15.181.504.781.676 1.032c.143.208.85 1.23 1.158 1.567c.086.093.349.198.466.27a.083.083 0 0 1 .03.112l-1.03 1.808l-.455 2.136a1 1 0 0 0-.036.152l-.412 1.483c.003.188-.14.286-.153.507l-.15 1.084a.06.06 0 0 0 .059.061l2.544.014q.142-.001.286-.006l.075-.007c.124-.016.563-.076.75-.15a.6.6 0 0 0 .227-.13c.185-.194.2-.278.203-.398a.3.3 0 0 0-.028-.105a.12.12 0 0 0-.06-.047l-1.18-.356a.37.37 0 0 1-.19-.134l-.317-.47a.09.09 0 0 1 .018-.097l.618-.609a.2.2 0 0 0 .048-.072l1.904-4.488c.089-.285.059-.605 0-.944c-.044-.25-.686-1.326-.854-1.624l-1.286-2.251c-.079-.138-.19-.133-.228-.276l-.073-1.118a.04.04 0 0 1 .036-.05l.33-.028a.1.1 0 0 0 .075-.048l1.147-2.155a.1.1 0 0 0-.002-.094l-.235-.29a.09.09 0 0 1-.001-.088l.352-.38a.054.054 0 0 1 .073-.02l.934.526a.4.4 0 0 0 .186.05c.26-.001.686-.154.908-.29a.4.4 0 0 0 .139-.148l.458-1.07c.006-.014.027-.012.03.003l.127.595a.064.064 0 0 0 .079.05l1.35-.3a.066.066 0 0 0 .05-.078l-.319-1.344a.07.07 0 0 1 .01-.054l.13-.203a.3.3 0 0 0 .037-.082l.159-.725a.04.04 0 0 1 .04-.032l3.732.005a.09.09 0 0 0 .093-.093v-.634a.02.02 0 0 1 .022-.021h1.439a.047.047 0 0 0 .046-.047V3.28a.047.047 0 0 0-.046-.047h-1.44z'
]
},
'game-pubg': {
from: '#fde68a',
to: '#b45309',
ink: '#422006',
paths: [
'M15.4 15.63a7.875 6 135 1 1 6.23-6.23 4.5 3.43 135 0 0-6.23 6.23',
'm8.29 12.71-2.6 2.6a2.5 2.5 0 1 0-1.65 4.65A2.5 2.5 0 1 0 8.7 18.3l2.59-2.59'
]
},
'game-r6': {
from: '#94a3b8',
to: '#1e293b',
ink: WHITE,
paths: [
'M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z',
'M14 8.5h-2a2 2 0 0 0-2 2v3.5a2 2 0 0 0 4 0v-.5a2 2 0 0 0-4 0'
]
},
'game-tarkov': {
from: '#bdb76b',
to: '#4d4a26',
ink: WHITE,
paths: [
'M4 10a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z',
'M8 10h8',
'M8 18h8',
'M8 22v-6a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v6',
'M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2'
]
},
'game-bo2': {
from: '#fb923c',
to: '#9a3412',
ink: WHITE,
paths: [c(12, 12, 9), 'M21 12h-4M7 12H3M12 7V3M12 21v-4', c(12, 12, 1)]
},
'game-valorant': {
from: '#ff6b77',
to: '#9f1239',
ink: WHITE,
fill: true,
paths: [
'M23.792 2.152a.25.25 0 0 0-.098.083q-5.077 6.345-10.15 12.69c-.107.093-.025.288.119.265c2.439.003 4.877 0 7.316.001a.66.66 0 0 0 .552-.25l2.324-2.903a.72.72 0 0 0 .144-.49c-.002-3.077 0-6.153-.003-9.23c.016-.11-.1-.206-.204-.167zM.077 2.166c-.077.038-.074.132-.076.205q.002 4.612.001 9.225a.68.68 0 0 0 .158.463l7.64 9.55c.12.152.308.25.505.247c2.455 0 4.91.003 7.365 0c.142.02.222-.174.116-.265C10.661 15.176 5.526 8.766.4 2.35c-.08-.094-.174-.272-.322-.184z'
]
},
'game-lol': {
from: '#e2c27a',
to: '#0e4a5c',
ink: WHITE,
fill: true,
paths: [
'm1.912 0l1.212 2.474v19.053L1.912 24h14.73l1.337-4.682H8.33V0ZM12 1.516c-.913 0-1.798.112-2.648.312v1.74A9.7 9.7 0 0 1 12 3.2c5.267 0 9.536 4.184 9.536 9.348a9.2 9.2 0 0 1-2.3 6.086l-.273.954l-.602 2.112c2.952-1.993 4.89-5.335 4.89-9.122C23.25 6.468 18.213 1.516 12 1.516m0 2.673c-.924 0-1.814.148-2.648.414v13.713h8.817a8.25 8.25 0 0 0 2.36-5.768c0-4.617-3.818-8.359-8.529-8.359M2.104 7.312A10.86 10.86 0 0 0 .75 12.576c0 1.906.492 3.7 1.355 5.266z'
]
},
'game-overwatch': {
from: '#fdba74',
to: '#f97316',
ink: '#431407',
paths: [c(12, 12, 9), 'm8 7.5 4 7 4-7', 'M7 15.5h10']
},
'game-minecraft': {
from: '#86efac',
to: '#166534',
ink: WHITE,
paths: [
'm14 13-8.381 8.38a1 1 0 0 1-3.001-3L11 9.999',
'M15.973 4.027A13 13 0 0 0 5.902 2.373c-1.398.342-1.092 2.158.277 2.601a19.9 19.9 0 0 1 5.822 3.024',
'M16.001 11.999a19.9 19.9 0 0 1 3.024 5.824c.444 1.369 2.26 1.676 2.603.278A13 13 0 0 0 20 8.069',
'M18.352 3.352a1.205 1.205 0 0 0-1.704 0l-5.296 5.296a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l5.296-5.296a1.205 1.205 0 0 0 0-1.704z'
]
},
'game-factorio': {
from: '#f0b16a',
to: '#7c4a1e',
ink: WHITE,
paths: [
'M11 10.27 7 3.34',
'm11 13.73-4 6.93',
'M12 22v-2',
'M12 2v2',
'M14 12h8',
'm17 20.66-1-1.73',
'm17 3.34-1 1.73',
'M2 12h2',
'm20.66 17-1.73-1',
'm20.66 7-1.73 1',
'm3.34 17 1.73-1',
'm3.34 7 1.73 1',
c(12, 12, 2),
c(12, 12, 8)
]
},
'game-ksp': {
from: '#93c5fd',
to: '#1e3a8a',
ink: WHITE,
paths: [
'M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5',
'M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09',
'M9 12a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.4 22.4 0 0 1-4 2z',
'M9 12H4s.55-3.03 2-4c1.62-1.08 5 .05 5 .05'
]
},
'game-smash': {
from: '#f87171',
to: '#991b1b',
ink: WHITE,
paths: [c(12, 12, 9), 'M9.5 3.4v17.2', 'M3 14.5h18']
},
'game-cubing': {
from: '#4ade80',
to: '#1d4ed8',
ink: WHITE,
paths: ['M4 4h16v16H4z', 'M9.33 4v16M14.67 4v16', 'M4 9.33h16M4 14.67h16']
},
'game-pokemon': {
from: '#fb7185',
to: '#c81e4a',
ink: WHITE,
paths: [c(12, 12, 9), 'M3 12h6M15 12h6', c(12, 12, 3)]
},
'game-sot': {
from: '#5eead4',
to: '#0f766e',
ink: WHITE,
paths: [
'M10 2v15',
'M7 22a4 4 0 0 1-4-4 1 1 0 0 1 1-1h16a1 1 0 0 1 1 1 4 4 0 0 1-4 4z',
'M9.159 2.46a1 1 0 0 1 1.521-.193l9.977 8.98A1 1 0 0 1 20 13H4a1 1 0 0 1-.824-1.567z'
]
}
};