games: split Call of Duty into one entry per title and Pokémon into one per generation (remakes and spin-offs separate), full names, official logo for each (Steam icons / English logos); data migration moves bo2 status+aliases to black-ops-2; /trackers/games/bo2 and /pokemon redirect; three-column games flyout
All checks were successful
Deploy production / deploy (push) Successful in 1m16s

This commit is contained in:
2026-10-25 18:54:40 +00:00
parent 942c436d03
commit e4162d3a78
34 changed files with 29842 additions and 61 deletions

View File

@@ -0,0 +1,46 @@
-- Hand-written data migration: split the single Call of Duty (bo2) and Pokémon rows into one
-- row per Call of Duty title and one per Pokémon generation (remakes and spin-offs separate).
-- Neither old row had notes or ranks; bo2's status (active) and aliases move to black-ops-2.
-- Deep-tracker data (ref.tracker_games / ref.pokemon_games ids) is untouched.
INSERT INTO games.library (slug, name, short_name, kind, status, platform, tracker_href, aliases, sort_order) VALUES
('black-ops-2', 'Call of Duty: Black Ops 2', 'Black Ops 2', 'video', 'active', NULL, '/projects/game-tracker/call-of-duty/black-ops-2', '{"Call of Duty","Black Ops II","Black Ops 2","BO2"}', 60),
('black-ops', 'Call of Duty: Black Ops', 'Black Ops', 'video', 'unknown', NULL, '/projects/game-tracker/call-of-duty/black-ops', '{"Call of Duty","Black Ops 1"}', 61),
('ghosts', 'Call of Duty: Ghosts', 'Ghosts', 'video', 'unknown', NULL, '/projects/game-tracker/call-of-duty/ghosts', '{"Call of Duty"}', 62),
('advanced-warfare', 'Call of Duty: Advanced Warfare', 'Advanced Warfare', 'video', 'unknown', NULL, '/projects/game-tracker/call-of-duty/advanced-warfare', '{"Call of Duty"}', 63),
('black-ops-3', 'Call of Duty: Black Ops 3', 'Black Ops 3', 'video', 'unknown', NULL, '/projects/game-tracker/call-of-duty/black-ops-3', '{"Call of Duty","Black Ops III","Black Ops 3"}', 64),
('infinite-warfare', 'Call of Duty: Infinite Warfare', 'Infinite Warfare', 'video', 'unknown', NULL, '/projects/game-tracker/call-of-duty/infinite-warfare', '{"Call of Duty"}', 65),
('wwii', 'Call of Duty: WWII', 'WWII', 'video', 'unknown', NULL, '/projects/game-tracker/call-of-duty/wwii', '{"Call of Duty","World War II"}', 66),
('pokemon-red-blue-yellow', 'Pokémon Red, Blue & Yellow', 'Red, Blue & Yellow', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=yellow', '{"Pokémon Red","Pokémon Blue","Pokémon Yellow"}', 200),
('pokemon-gold-silver-crystal', 'Pokémon Gold, Silver & Crystal', 'Gold, Silver & Crystal', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=crystal', '{"Pokémon Gold","Pokémon Silver","Pokémon Crystal"}', 201),
('pokemon-ruby-sapphire-emerald', 'Pokémon Ruby, Sapphire & Emerald', 'Ruby, Sapphire & Emerald', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=emerald', '{"Pokémon Ruby","Pokémon Sapphire","Pokémon Emerald"}', 202),
('pokemon-colosseum', 'Pokémon Colosseum', 'Colosseum', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=colosseum', '{}', 203),
('pokemon-xd', 'Pokémon XD: Gale of Darkness', 'XD: Gale of Darkness', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=xd', '{"Pokémon XD"}', 204),
('pokemon-firered-leafgreen', 'Pokémon FireRed & LeafGreen', 'FireRed & LeafGreen', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=firered', '{"Pokémon FireRed","Pokémon LeafGreen"}', 205),
('pokemon-diamond-pearl-platinum', 'Pokémon Diamond, Pearl & Platinum', 'Diamond, Pearl & Platinum', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=platinum', '{"Pokémon Diamond","Pokémon Pearl","Pokémon Platinum"}', 206),
('pokemon-heartgold-soulsilver', 'Pokémon HeartGold & SoulSilver', 'HeartGold & SoulSilver', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=soulsilver', '{"Pokémon HeartGold","Pokémon SoulSilver"}', 207),
('pokemon-black-white', 'Pokémon Black, White, Black 2 & White 2', 'Black & White', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=black', '{"Pokémon Black","Pokémon White","Pokémon Black 2","Pokémon White 2"}', 208),
('pokemon-x-y', 'Pokémon X & Y', 'X & Y', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=y', '{"Pokémon X","Pokémon Y"}', 209),
('pokemon-omega-ruby-alpha-sapphire', 'Pokémon Omega Ruby & Alpha Sapphire', 'Omega Ruby & Alpha Sapphire', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=omega-ruby', '{"Pokémon Omega Ruby","Pokémon Alpha Sapphire"}', 210),
('pokemon-sun-moon', 'Pokémon Sun, Moon, Ultra Sun & Ultra Moon', 'Sun & Moon', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=moon', '{"Pokémon Sun","Pokémon Moon","Pokémon Ultra Sun","Pokémon Ultra Moon"}', 211),
('pokemon-lets-go', 'Pokémon Let’s Go, Pikachu! & Let’s Go, Eevee!', 'Let’s Go', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=lets-go-eevee', '{"Pokémon Let’s Go, Pikachu!","Pokémon Let’s Go, Eevee!"}', 212),
('pokemon-sword-shield', 'Pokémon Sword & Shield', 'Sword & Shield', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=sword', '{"Pokémon Sword","Pokémon Shield"}', 213),
('pokemon-brilliant-diamond-shining-pearl', 'Pokémon Brilliant Diamond & Shining Pearl', 'Brilliant Diamond & Shining Pearl', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=brilliant-diamond', '{"Pokémon Brilliant Diamond","Pokémon Shining Pearl"}', 214),
('pokemon-legends-arceus', 'Pokémon Legends: Arceus', 'Legends: Arceus', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=legends-arceus', '{}', 215),
('pokemon-scarlet-violet', 'Pokémon Scarlet & Violet', 'Scarlet & Violet', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=scarlet', '{"Pokémon Scarlet","Pokémon Violet"}', 216),
('pokemon-legends-z-a', 'Pokémon Legends: Z-A', 'Legends: Z-A', 'video', 'unknown', NULL, '/projects/game-tracker/pokemon?game=legends-za', '{}', 217)
ON CONFLICT (slug) DO NOTHING;--> statement-breakpoint
UPDATE games.library n SET
status = o.status,
platform = COALESCE(n.platform, o.platform),
current_rank = COALESCE(n.current_rank, o.current_rank),
peak_rank = COALESCE(n.peak_rank, o.peak_rank),
rank_label = COALESCE(n.rank_label, o.rank_label),
notes = COALESCE(n.notes, o.notes),
aliases = ARRAY(SELECT DISTINCT unnest(n.aliases || o.aliases))
FROM games.library o
WHERE o.slug = 'bo2' AND n.slug = 'black-ops-2';--> statement-breakpoint
UPDATE games.library n SET
notes = COALESCE(n.notes, o.notes)
FROM games.library o
WHERE o.slug = 'pokemon' AND n.slug = 'pokemon-diamond-pearl-platinum' AND o.notes IS NOT NULL;--> statement-breakpoint
DELETE FROM games.library WHERE slug IN ('bo2', 'pokemon');

File diff suppressed because it is too large Load Diff

View File

@@ -189,12 +189,15 @@
let flyoutTimer: ReturnType<typeof setTimeout> | undefined; let flyoutTimer: ReturnType<typeof setTimeout> | undefined;
// Big groups (games) get a two-column flyout; it opens upwards when it would not fit below. // Big groups (games) get a two-column flyout; it opens upwards when it would not fit below.
const WIDE_FLYOUT = 6; const WIDE_FLYOUT = 6;
// Games (one entry per Call of Duty title and Pokémon generation) get three columns.
const HUGE_FLYOUT = 24;
const flyoutColumns = (count: number) => (count > HUGE_FLYOUT ? 3 : count > WIDE_FLYOUT ? 2 : 1);
function openFlyout(name: string, el: HTMLElement, count: number) { function openFlyout(name: string, el: HTMLElement, count: number) {
clearTimeout(flyoutTimer); clearTimeout(flyoutTimer);
if (flyout === name) return; if (flyout === name) return;
const wide = count > WIDE_FLYOUT; const columns = flyoutColumns(count);
const width = wide ? 432 : 240; const width = columns === 3 ? 640 : columns === 2 ? 432 : 240;
const height = (wide ? Math.ceil(count / 2) : count) * 46 + 20; const height = Math.min(Math.ceil(count / columns) * 46 + 20, window.innerHeight - 80);
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const panel = el.closest('[data-menu-panel]')?.getBoundingClientRect() ?? rect; const panel = el.closest('[data-menu-panel]')?.getBoundingClientRect() ?? rect;
const leftColumn = rect.left + rect.width / 2 < panel.left + panel.width / 2; const leftColumn = rect.left + rect.width / 2 < panel.left + panel.width / 2;
@@ -365,14 +368,20 @@
class={[ class={[
'absolute', 'absolute',
flyoutUp ? '-bottom-2' : '-top-2', flyoutUp ? '-bottom-2' : '-top-2',
project.children.length > WIDE_FLYOUT ? 'w-[27rem]' : 'w-60', flyoutColumns(project.children.length) === 3
? 'w-[40rem]'
: project.children.length > WIDE_FLYOUT
? 'w-[27rem]'
: 'w-60',
flyoutSide === 'right' ? 'left-full pl-3' : 'right-full pr-3' flyoutSide === 'right' ? 'left-full pl-3' : 'right-full pr-3'
]} ]}
> >
<div <div
class={[ class={[
'flyout grid gap-1 rounded-md border border-border bg-popover p-1.5 shadow-lg', 'flyout grid max-h-[calc(100vh-5rem)] gap-1 overflow-y-auto rounded-md border border-border bg-popover p-1.5 shadow-lg',
project.children.length > WIDE_FLYOUT && 'grid-cols-2' flyoutColumns(project.children.length) === 3
? 'grid-cols-3'
: project.children.length > WIDE_FLYOUT && 'grid-cols-2'
]} ]}
class:from-left={flyoutSide === 'left'} class:from-left={flyoutSide === 'left'}
> >

View File

@@ -27,7 +27,11 @@
alt="" alt=""
draggable="false" draggable="false"
decoding="async" decoding="async"
class={logo.bleed ? 'size-full object-cover' : 'size-[80%] object-contain'} class={logo.bleed
? 'size-full object-cover'
: logo.wide
? 'size-[92%] object-contain'
: 'size-[80%] object-contain'}
onerror={() => (failed = true)} onerror={() => (failed = true)}
/> />
</span> </span>

View File

@@ -2,19 +2,24 @@
// static/game-logos/icons/, keyed like `gameTrackers[].icon` (game-<slug>). TrackerIcon uses // static/game-logos/icons/, keyed like `gameTrackers[].icon` (game-<slug>). TrackerIcon uses
// this map everywhere games show up (header flyout, home and /trackers tiles, /start chips, // this map everywhere games show up (header flyout, home and /trackers tiles, /start chips,
// the games switcher and each game page); a game without an entry keeps its tracker-icons tile. // the games switcher and each game page); a game without an entry keeps its tracker-icons tile.
// Square app icons/emblems are used instead of wide wordmarks so they stay legible at chip size. // Square app icons/emblems are used instead of wide wordmarks so they stay legible at chip size;
// Pokémon generations have no square marks, so they use their English logos (`wide`).
export type GameLogo = { export type GameLogo = {
/** Path under static/ (no leading slash), resolved with `asset`/`resolve` at render time. */ /** Path under static/ (no leading slash), resolved with `asset`/`resolve` at render time. */
src: string; src: string;
/** Opaque square app icon: fill the whole tile instead of sitting padded on the dark tile. */ /** Opaque square app icon: fill the whole tile instead of sitting padded on the dark tile. */
bleed?: boolean; bleed?: boolean;
/** Wide wordmark (Pokémon logos): use more of the tile so it stays readable. */
wide?: boolean;
/** Where the asset came from (shown on /projects/game-tracker/credits). */ /** Where the asset came from (shown on /projects/game-tracker/credits). */
credit: string; credit: string;
source: string; source: string;
}; };
const steamIcon = (appid: number) => `https://store.steampowered.com/app/${appid}/`; const steamIcon = (appid: number) => `https://store.steampowered.com/app/${appid}/`;
const bulbagarden = (file: string) =>
`https://archives.bulbagarden.net/wiki/File:${encodeURIComponent(file.replaceAll(' ', '_'))}`;
const commons = (file: string) => const commons = (file: string) =>
`https://commons.wikimedia.org/wiki/File:${encodeURIComponent(file.replaceAll(' ', '_'))}`; `https://commons.wikimedia.org/wiki/File:${encodeURIComponent(file.replaceAll(' ', '_'))}`;
@@ -47,12 +52,48 @@ export const gameLogos: Record<string, GameLogo> = {
credit: 'Escape from Tarkov app icon, Steam', credit: 'Escape from Tarkov app icon, Steam',
source: steamIcon(3932890) source: steamIcon(3932890)
}, },
'game-bo2': { 'game-black-ops-2': {
src: 'game-logos/icons/bo2.png', src: 'game-logos/icons/black-ops-2.png',
bleed: true, bleed: true,
credit: 'Call of Duty: Black Ops II app icon, Steam', credit: 'Call of Duty: Black Ops II app icon, Steam',
source: steamIcon(202970) source: steamIcon(202970)
}, },
'game-black-ops': {
src: 'game-logos/icons/black-ops.png',
bleed: true,
credit: 'Call of Duty: Black Ops app icon, Steam',
source: steamIcon(42700)
},
'game-ghosts': {
src: 'game-logos/icons/ghosts.png',
bleed: true,
credit: 'Call of Duty: Ghosts app icon, Steam',
source: steamIcon(209160)
},
'game-advanced-warfare': {
src: 'game-logos/icons/advanced-warfare.png',
bleed: true,
credit: 'Call of Duty: Advanced Warfare app icon, Steam',
source: steamIcon(209650)
},
'game-black-ops-3': {
src: 'game-logos/icons/black-ops-3.png',
bleed: true,
credit: 'Call of Duty: Black Ops III app icon, Steam',
source: steamIcon(311210)
},
'game-infinite-warfare': {
src: 'game-logos/icons/infinite-warfare.png',
bleed: true,
credit: 'Call of Duty: Infinite Warfare app icon, Steam',
source: steamIcon(292730)
},
'game-wwii': {
src: 'game-logos/icons/wwii.png',
bleed: true,
credit: 'Call of Duty: WWII app icon, Steam',
source: steamIcon(476600)
},
'game-valorant': { 'game-valorant': {
src: 'game-logos/icons/valorant.svg', src: 'game-logos/icons/valorant.svg',
credit: 'Valorant logo (V mark, pink), Wikimedia Commons', credit: 'Valorant logo (V mark, pink), Wikimedia Commons',
@@ -93,14 +134,117 @@ export const gameLogos: Record<string, GameLogo> = {
credit: 'World Cube Association logo, Wikimedia Commons', credit: 'World Cube Association logo, Wikimedia Commons',
source: commons('World Cube Association.svg') source: commons('World Cube Association.svg')
}, },
'game-pokemon': {
src: 'game-logos/icons/pokemon.svg',
credit: 'Poké Ball icon, Wikimedia Commons',
source: commons('Poké Ball icon.svg')
},
'game-sot': { 'game-sot': {
src: 'game-logos/icons/sot.png', src: 'game-logos/icons/sot.png',
credit: 'Sea of Thieves app icon (skull), Steam', credit: 'Sea of Thieves app icon (skull), Steam',
source: steamIcon(1172620) source: steamIcon(1172620)
},
'game-pokemon-red-blue-yellow': {
src: 'game-logos/icons/pokemon-red-blue-yellow.png',
wide: true,
credit: 'English logo (Pokémon RB logo 2), Wikimedia Commons',
source: commons('Pokémon RB logo 2.png')
},
'game-pokemon-gold-silver-crystal': {
src: 'game-logos/icons/pokemon-gold-silver-crystal.png',
wide: true,
credit: 'English logo (Pokémon GS logo), Wikimedia Commons',
source: commons('Pokémon GS logo.png')
},
'game-pokemon-ruby-sapphire-emerald': {
src: 'game-logos/icons/pokemon-ruby-sapphire-emerald.png',
wide: true,
credit: 'English logo (Pokemon Emerald Logo EN), Bulbagarden Archives',
source: bulbagarden('Pokemon Emerald Logo EN.png')
},
'game-pokemon-colosseum': {
src: 'game-logos/icons/pokemon-colosseum.png',
wide: true,
credit: 'English logo (Pokémon Colosseum logo), Bulbagarden Archives',
source: bulbagarden('Pokémon Colosseum logo.png')
},
'game-pokemon-xd': {
src: 'game-logos/icons/pokemon-xd.png',
wide: true,
credit: 'English logo (Pokémon XD logo English), Bulbagarden Archives',
source: bulbagarden('Pokémon XD logo English.png')
},
'game-pokemon-firered-leafgreen': {
src: 'game-logos/icons/pokemon-firered-leafgreen.png',
wide: true,
credit: 'English logo (Pokemon FireRed Logo EN), Bulbagarden Archives',
source: bulbagarden('Pokemon FireRed Logo EN.png')
},
'game-pokemon-diamond-pearl-platinum': {
src: 'game-logos/icons/pokemon-diamond-pearl-platinum.png',
wide: true,
credit: 'English logo (Platinum-logo), Bulbagarden Archives',
source: bulbagarden('Platinum-logo.png')
},
'game-pokemon-heartgold-soulsilver': {
src: 'game-logos/icons/pokemon-heartgold-soulsilver.png',
wide: true,
credit: 'English logo (HeartGold SoulSilver Logo), Bulbagarden Archives',
source: bulbagarden('HeartGold SoulSilver Logo.png')
},
'game-pokemon-black-white': {
src: 'game-logos/icons/pokemon-black-white.png',
wide: true,
credit: 'English logo (English BW logo), Bulbagarden Archives',
source: bulbagarden('English BW logo.png')
},
'game-pokemon-x-y': {
src: 'game-logos/icons/pokemon-x-y.png',
wide: true,
credit: 'English logo (Pokémon XY logo), Bulbagarden Archives',
source: bulbagarden('Pokémon XY logo.png')
},
'game-pokemon-omega-ruby-alpha-sapphire': {
src: 'game-logos/icons/pokemon-omega-ruby-alpha-sapphire.png',
wide: true,
credit: 'English logo (Pokémon Omega Ruby EN logo), Bulbagarden Archives',
source: bulbagarden('Pokémon Omega Ruby EN logo.png')
},
'game-pokemon-sun-moon': {
src: 'game-logos/icons/pokemon-sun-moon.png',
wide: true,
credit: 'English logo (Pokémon Sun Moon logo), Bulbagarden Archives',
source: bulbagarden('Pokémon Sun Moon logo.png')
},
'game-pokemon-lets-go': {
src: 'game-logos/icons/pokemon-lets-go.png',
wide: true,
credit: 'English logo (Pokémon Lets Go Eevee Logo), Bulbagarden Archives',
source: bulbagarden('Pokémon Lets Go Eevee Logo.png')
},
'game-pokemon-sword-shield': {
src: 'game-logos/icons/pokemon-sword-shield.png',
wide: true,
credit: 'English logo (Pokémon Sword Shield logo), Bulbagarden Archives',
source: bulbagarden('Pokémon Sword Shield logo.png')
},
'game-pokemon-brilliant-diamond-shining-pearl': {
src: 'game-logos/icons/pokemon-brilliant-diamond-shining-pearl.png',
wide: true,
credit: 'English logo (Pokémon Brilliant Diamond logo), Bulbagarden Archives',
source: bulbagarden('Pokémon Brilliant Diamond logo.png')
},
'game-pokemon-legends-arceus': {
src: 'game-logos/icons/pokemon-legends-arceus.png',
wide: true,
credit: 'English logo (Pokémon Legends Arceus logo), Bulbagarden Archives',
source: bulbagarden('Pokémon Legends Arceus logo.png')
},
'game-pokemon-scarlet-violet': {
src: 'game-logos/icons/pokemon-scarlet-violet.png',
wide: true,
credit: 'English logo (Pokémon Scarlet logo), Bulbagarden Archives',
source: bulbagarden('Pokémon Scarlet logo.png')
},
'game-pokemon-legends-z-a': {
src: 'game-logos/icons/pokemon-legends-z-a.png',
wide: true,
credit: 'English logo (Pokémon Legends Z-A logo), Bulbagarden Archives',
source: bulbagarden('Pokémon Legends Z-A logo.png')
} }
}; };

View File

@@ -82,6 +82,9 @@ export const spottingTrackers: readonly Tracker[] = [
// own page at /trackers/games/<slug>, one click away from the header flyout and the chips. // 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 // `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). // rest sit behind a "more" chip. Data per game lives in games.library (same slugs).
// Call of Duty is one entry per title and Pokémon one per generation (remakes such as
// HeartGold & SoulSilver and spin-offs separate); full names, no abbreviations. The old
// /trackers/games/bo2 and /trackers/games/pokemon URLs redirect (see [slug]/+page.server.ts).
const game = (slug: string, name: string, subtitle: string, featured = false): Tracker => ({ const game = (slug: string, name: string, subtitle: string, featured = false): Tracker => ({
name, name,
subtitle, subtitle,
@@ -96,7 +99,7 @@ export const gameTrackers: readonly Tracker[] = [
game('pubg', 'pubg', 'PUBG: Battlegrounds', true), game('pubg', 'pubg', 'PUBG: Battlegrounds', true),
game('r6', 'r6 siege', 'Tom Clancy’s Rainbow Six Siege', true), game('r6', 'r6 siege', 'Tom Clancy’s Rainbow Six Siege', true),
game('tarkov', 'tarkov', 'Escape from Tarkov', true), game('tarkov', 'tarkov', 'Escape from Tarkov', true),
game('bo2', 'bo2', 'Call of Duty: Black Ops II', true), game('black-ops-2', 'black ops 2', 'Call of Duty: Black Ops II (2012)', true),
game('valorant', 'valorant', 'Riot’s tactical shooter', false), game('valorant', 'valorant', 'Riot’s tactical shooter', false),
game('lol', 'league', 'League of Legends', false), game('lol', 'league', 'League of Legends', false),
game('overwatch', 'overwatch 2', 'Blizzard’s hero shooter', false), game('overwatch', 'overwatch 2', 'Blizzard’s hero shooter', false),
@@ -105,8 +108,81 @@ export const gameTrackers: readonly Tracker[] = [
game('ksp', 'ksp', 'Kerbal Space Program', false), game('ksp', 'ksp', 'Kerbal Space Program', false),
game('smash', 'smash bros', 'Super Smash Bros.', false), game('smash', 'smash bros', 'Super Smash Bros.', false),
game('cubing', 'speedcubing', 'Rubik’s cube solving, not a video game', 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),
game('sot', 'sea of thieves', 'pirate sandbox', false) // Call of Duty, one entry per title (the Black Ops 2 one is featured above).
game('black-ops', 'black ops', 'Call of Duty: Black Ops (2010)'),
game('ghosts', 'ghosts', 'Call of Duty: Ghosts (2013)'),
game('advanced-warfare', 'advanced warfare', 'Call of Duty: Advanced Warfare (2014)'),
game('black-ops-3', 'black ops 3', 'Call of Duty: Black Ops III (2015)'),
game('infinite-warfare', 'infinite warfare', 'Call of Duty: Infinite Warfare (2016)'),
game('wwii', 'wwii', 'Call of Duty: WWII (2017)'),
// Pokémon, one entry per generation; remakes and spin-offs get their own.
game('pokemon-red-blue-yellow', 'pokémon red, blue & yellow', 'generation 1, Game Boy'),
game(
'pokemon-gold-silver-crystal',
'pokémon gold, silver & crystal',
'generation 2, Game Boy Color'
),
game(
'pokemon-ruby-sapphire-emerald',
'pokémon ruby, sapphire & emerald',
'generation 3, Game Boy Advance'
),
game('pokemon-colosseum', 'pokémon colosseum', 'generation 3 spin-off, GameCube'),
game('pokemon-xd', 'pokémon xd: gale of darkness', 'generation 3 spin-off, GameCube'),
game(
'pokemon-firered-leafgreen',
'pokémon firered & leafgreen',
'generation 1 remake, Game Boy Advance'
),
game(
'pokemon-diamond-pearl-platinum',
'pokémon diamond, pearl & platinum',
'generation 4, Nintendo DS'
),
game(
'pokemon-heartgold-soulsilver',
'pokémon heartgold & soulsilver',
'generation 2 remake, Nintendo DS'
),
game(
'pokemon-black-white',
'pokémon black & white',
'generation 5 with Black 2 & White 2, Nintendo DS'
),
game('pokemon-x-y', 'pokémon x & y', 'generation 6, Nintendo 3DS'),
game(
'pokemon-omega-ruby-alpha-sapphire',
'pokémon omega ruby & alpha sapphire',
'generation 3 remake, Nintendo 3DS'
),
game(
'pokemon-sun-moon',
'pokémon sun & moon',
'generation 7 with Ultra Sun & Ultra Moon, Nintendo 3DS'
),
game(
'pokemon-lets-go',
'pokémon let’s go, pikachu & eevee',
'generation 1 remake, Nintendo Switch'
),
game(
'pokemon-sword-shield',
'pokémon sword & shield',
'generation 8 with The Isle of Armor & The Crown Tundra, Nintendo Switch'
),
game(
'pokemon-brilliant-diamond-shining-pearl',
'pokémon brilliant diamond & shining pearl',
'generation 4 remake, Nintendo Switch'
),
game('pokemon-legends-arceus', 'pokémon legends: arceus', 'generation 8, Nintendo Switch'),
game(
'pokemon-scarlet-violet',
'pokémon scarlet & violet',
'generation 9 with The Teal Mask & The Indigo Disk, Nintendo Switch'
),
game('pokemon-legends-z-a', 'pokémon legends: z-a', 'generation 9, Nintendo Switch')
]; ];
// Trackers, in display order. Names are short single nouns. Most keep their existing // Trackers, in display order. Names are short single nouns. Most keep their existing

View File

@@ -19,6 +19,21 @@ export type TrackerIcon = {
const WHITE = '#ffffff'; const WHITE = '#ffffff';
// Shared glyphs for the per-title Call of Duty (crosshair) and per-generation Pokémon (Poké Ball)
// fallback tiles; each entry only changes the colours.
const codTile: TrackerIcon = {
from: '#fb923c',
to: '#9a3412',
ink: WHITE,
paths: [c(12, 12, 9), 'M21 12h-4M7 12H3M12 7V3M12 21v-4', c(12, 12, 1)]
};
const pokemonTile: TrackerIcon = {
from: '#fb7185',
to: '#c81e4a',
ink: WHITE,
paths: [c(12, 12, 9), 'M3 12h6M15 12h6', c(12, 12, 3)]
};
export const trackerIcons: Record<string, TrackerIcon> = { export const trackerIcons: Record<string, TrackerIcon> = {
tasks: { tasks: {
from: '#60a5fa', from: '#60a5fa',
@@ -228,12 +243,13 @@ export const trackerIcons: Record<string, TrackerIcon> = {
'M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2' 'M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2'
] ]
}, },
'game-bo2': { 'game-black-ops-2': { ...codTile, from: '#f58147', to: '#863913' },
from: '#fb923c', 'game-black-ops': { ...codTile, from: '#f59b47', to: '#864b13' },
to: '#9a3412', 'game-ghosts': { ...codTile, from: '#f5b547', to: '#865c13' },
ink: WHITE, 'game-advanced-warfare': { ...codTile, from: '#f5d047', to: '#866d13' },
paths: [c(12, 12, 9), 'M21 12h-4M7 12H3M12 7V3M12 21v-4', c(12, 12, 1)] 'game-black-ops-3': { ...codTile, from: '#f5ea47', to: '#867e13' },
}, 'game-infinite-warfare': { ...codTile, from: '#e7f547', to: '#7c8613' },
'game-wwii': { ...codTile, from: '#cdf547', to: '#6b8613' },
'game-valorant': { 'game-valorant': {
from: '#ff6b77', from: '#ff6b77',
to: '#9f1239', to: '#9f1239',
@@ -313,12 +329,28 @@ export const trackerIcons: Record<string, TrackerIcon> = {
ink: WHITE, ink: WHITE,
paths: ['M4 4h16v16H4z', 'M9.33 4v16M14.67 4v16', 'M4 9.33h16M4 14.67h16'] paths: ['M4 4h16v16H4z', 'M9.33 4v16M14.67 4v16', 'M4 9.33h16M4 14.67h16']
}, },
'game-pokemon': { 'game-pokemon-red-blue-yellow': { ...pokemonTile, from: '#f37868', to: '#9c2b1c' },
from: '#fb7185', 'game-pokemon-gold-silver-crystal': { ...pokemonTile, from: '#f3a668', to: '#9c551c' },
to: '#c81e4a', 'game-pokemon-ruby-sapphire-emerald': { ...pokemonTile, from: '#f3d568', to: '#9c801c' },
ink: WHITE, 'game-pokemon-colosseum': { ...pokemonTile, from: '#e3f368', to: '#8d9c1c' },
paths: [c(12, 12, 9), 'M3 12h6M15 12h6', c(12, 12, 3)] 'game-pokemon-xd': { ...pokemonTile, from: '#b4f368', to: '#629c1c' },
'game-pokemon-firered-leafgreen': { ...pokemonTile, from: '#86f368', to: '#379c1c' },
'game-pokemon-diamond-pearl-platinum': { ...pokemonTile, from: '#68f378', to: '#1c9c2b' },
'game-pokemon-heartgold-soulsilver': { ...pokemonTile, from: '#68f3a6', to: '#1c9c55' },
'game-pokemon-black-white': { ...pokemonTile, from: '#68f3d5', to: '#1c9c80' },
'game-pokemon-x-y': { ...pokemonTile, from: '#68e3f3', to: '#1c8d9c' },
'game-pokemon-omega-ruby-alpha-sapphire': { ...pokemonTile, from: '#68b4f3', to: '#1c629c' },
'game-pokemon-sun-moon': { ...pokemonTile, from: '#6886f3', to: '#1c379c' },
'game-pokemon-lets-go': { ...pokemonTile, from: '#7868f3', to: '#2b1c9c' },
'game-pokemon-sword-shield': { ...pokemonTile, from: '#a668f3', to: '#551c9c' },
'game-pokemon-brilliant-diamond-shining-pearl': {
...pokemonTile,
from: '#d568f3',
to: '#801c9c'
}, },
'game-pokemon-legends-arceus': { ...pokemonTile, from: '#f368e3', to: '#9c1c8d' },
'game-pokemon-scarlet-violet': { ...pokemonTile, from: '#f368b4', to: '#9c1c62' },
'game-pokemon-legends-z-a': { ...pokemonTile, from: '#f36886', to: '#9c1c37' },
'game-sot': { 'game-sot': {
from: '#5eead4', from: '#5eead4',
to: '#0f766e', to: '#0f766e',

View File

@@ -1,4 +1,4 @@
import { error } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit';
import { eq, inArray } from 'drizzle-orm'; import { eq, inArray } from 'drizzle-orm';
import { db } from '#lib/server/db/index.js'; import { db } from '#lib/server/db/index.js';
import { gameLibrary, hardwareSensitivities } from '#lib/server/db/schema/index.js'; import { gameLibrary, hardwareSensitivities } from '#lib/server/db/schema/index.js';
@@ -9,8 +9,17 @@ import type { PageServerLoad } from './$types';
// One game from games.library, plus its mouse sensitivity (hardware.sensitivities, matched by // One game from games.library, plus its mouse sensitivity (hardware.sensitivities, matched by
// name, short name or alias) when there is one. CS2, PUBG and R6 also get their automatic // name, short name or alias) when there is one. CS2, PUBG and R6 also get their automatic
// match history (games.matches, filled by the in-app poller). // match history (games.matches, filled by the in-app poller).
// Old single entries, split per title/generation: bo2 became black-ops-2, and the Pokémon
// entry (all generations) now points at the Pokémon journal that covers every game.
const MOVED: Record<string, string> = {
bo2: '/trackers/games/black-ops-2',
pokemon: '/projects/game-tracker/pokemon'
};
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { params } = event; const { params } = event;
const moved = MOVED[params.slug];
if (moved) redirect(301, moved);
const [game] = await db.select().from(gameLibrary).where(eq(gameLibrary.slug, params.slug)); const [game] = await db.select().from(gameLibrary).where(eq(gameLibrary.slug, params.slug));
if (!game) error(404, 'Not Found'); if (!game) error(404, 'Not Found');
const names = [game.name, game.shortName, ...game.aliases]; const names = [game.name, game.shortName, ...game.aliases];

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Pokéball" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 595.3 594.1" style="enable-background:new 0 0 595.3 594.1;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#DFDFDF;}
.st2{fill:#FF1C1C;}
.st3{fill:#DF1818;}
</style>
<g id="Pokéball_1_">
<g id="Colours">
<path id="Down" class="st0" d="M297.6,380.9c-40.4,0-74.1-28.6-82.1-66.6H81.1c9.5,110.5,102.2,197.2,215.1,197.2
s205.7-86.7,215.1-197.2H379.7C371.7,352.4,338,380.9,297.6,380.9z"/>
<path id="Shadow_Down" class="st1" d="M345.6,505.9c89.6-21,157.7-97.7,165.7-191.6h-53C453,399.5,408.3,471.7,345.6,505.9z"/>
<path id="Center" class="st0" d="M347.1,297L347.1,297C347,297,347,297,347.1,297c-0.1-6.1-1.2-11.9-3.2-17.3
c-7-18.8-25.1-32.1-46.3-32.1s-39.3,13.4-46.3,32.1c-2,5.4-3.1,11.2-3.1,17.3c0,0,0,0,0,0h0.1c0,0,0,0,0,0
c0,6.1,1.1,11.9,3.1,17.3c7,18.8,25.1,32.1,46.3,32.1c21.2,0,39.3-13.4,46.3-32.1C346,309,347.1,303.1,347.1,297
C347.1,297,347.1,297,347.1,297z"/>
<path id="Up" class="st2" d="M297.7,213.2c40.4,0,74.1,28.6,82.1,66.6h134.4C504.7,169.2,412,82.5,299,82.5S93.4,169.2,83.9,279.7
h131.7C223.6,241.7,257.3,213.2,297.7,213.2z"/>
<path id="Shadow_Up" class="st3" d="M458.3,279.7h55.8c-8.2-95.5-78.6-173.3-170.5-192.6C407.4,120.8,452.9,193.7,458.3,279.7z"/>
</g>
<path id="Line" d="M299,82.5c113,0,205.7,86.7,215.1,197.2H379.7c-8-38-41.7-66.6-82.1-66.6c-40.4,0-74.1,28.6-82.1,66.6H83.9
C93.4,169.2,186.1,82.5,299,82.5z M343.9,279.7c2,5.4,3.1,11.2,3.1,17.3c0,0,0,0,0,0h0.1c0,0,0,0,0,0c0,6.1-1.1,11.9-3.1,17.3
c-7,18.8-25.1,32.1-46.3,32.1c-21.2,0-39.3-13.4-46.3-32.1c-2-5.4-3.1-11.2-3.1-17.3c0,0,0,0,0,0h-0.1c0,0,0,0,0,0
c0-6.1,1.1-11.9,3.1-17.3c7-18.8,25.1-32.1,46.3-32.1S336.9,261,343.9,279.7z M296.2,511.6c-113,0-205.7-86.7-215.1-197.2h134.4
c8,38,41.7,66.6,82.1,66.6s74.1-28.6,82.1-66.6h131.7C501.9,424.8,409.2,511.6,296.2,511.6z M297.6,41.3
C156.4,41.3,41.9,155.8,41.9,297s114.5,255.7,255.7,255.7S553.4,438.3,553.4,297S438.9,41.3,297.6,41.3z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB