Show friendly map titles in fleet cards
This commit is contained in:
@@ -0,0 +1,91 @@
|
|||||||
|
// T5 (Black Ops 1) zombies map catalog. Server names encode `slug` so we can
|
||||||
|
// derive game, mode, and map from the folder name (t5_zombie_cosmodrome_1 →
|
||||||
|
// game 't5', mode 'zombie', slug 'cosmodrome', canonical 'zombie_cosmodrome').
|
||||||
|
//
|
||||||
|
// Fields:
|
||||||
|
// friendly — human name shown in the deploy UI dropdown
|
||||||
|
// slug — segment used in server name (t5_zombie_<slug>_<n>)
|
||||||
|
// canonical — actual mapname Pluto/RCON emits (matches T5 script scoping)
|
||||||
|
//
|
||||||
|
// Add rows when new maps land; keep slugs short so paths stay readable.
|
||||||
|
|
||||||
|
export interface T5MapEntry {
|
||||||
|
friendly: string;
|
||||||
|
slug: string;
|
||||||
|
canonical: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const T5_ZOMBIE_MAPS: readonly T5MapEntry[] = Object.freeze([
|
||||||
|
{ friendly: 'Kino Der Toten', slug: 'theater', canonical: 'zombie_theater' },
|
||||||
|
{ friendly: 'Ascension', slug: 'cosmodrome', canonical: 'zombie_cosmodrome' },
|
||||||
|
{ friendly: 'Call of the Dead', slug: 'coast', canonical: 'zombie_coast' },
|
||||||
|
{ friendly: 'Shangri-La', slug: 'temple', canonical: 'zombie_temple' },
|
||||||
|
{ friendly: 'Moon', slug: 'moon', canonical: 'zombie_moon' },
|
||||||
|
{ friendly: 'Five', slug: 'pentagon', canonical: 'zombie_pentagon' },
|
||||||
|
{ friendly: 'Dead Ops Arcade', slug: 'zombietron', canonical: 'zombietron' },
|
||||||
|
{ friendly: 'Nacht Der Untoten', slug: 'prototype', canonical: 'zombie_cod5_prototype' },
|
||||||
|
{ friendly: 'Verrückt', slug: 'asylum', canonical: 'zombie_cod5_asylum' },
|
||||||
|
{ friendly: 'Shi No Numa', slug: 'sumpf', canonical: 'zombie_cod5_sumpf' },
|
||||||
|
{ friendly: 'Der Riese', slug: 'factory', canonical: 'zombie_cod5_factory' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const BY_SLUG: ReadonlyMap<string, T5MapEntry> = new Map(
|
||||||
|
T5_ZOMBIE_MAPS.map((m) => [m.slug, m])
|
||||||
|
);
|
||||||
|
// Older folder names used friendly names (shi_no_numa) instead of engine
|
||||||
|
// slugs (sumpf). Keep deriving their canonical map from the same catalog.
|
||||||
|
const BY_FRIENDLY_SLUG: ReadonlyMap<string, T5MapEntry> = new Map(
|
||||||
|
T5_ZOMBIE_MAPS.map((m) => [m.friendly.toLowerCase().replace(/[^a-z0-9]/g, ''), m])
|
||||||
|
);
|
||||||
|
|
||||||
|
export function t5ZombieMapBySlug(slug: string): T5MapEntry | undefined {
|
||||||
|
return BY_SLUG.get(slug.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GameMode = 'zombie' | 'mp' | 'sp';
|
||||||
|
|
||||||
|
export interface ParsedServerName {
|
||||||
|
game: string; // 't5'
|
||||||
|
gameMode: GameMode | null; // 'zombie' — null when the second segment isn't a known mode
|
||||||
|
mapSlug: string; // 'cosmodrome' — joined without separators for lookup
|
||||||
|
mapCanonical: string | null;// 'zombie_cosmodrome' — null when slug not in catalog
|
||||||
|
lobby: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KNOWN_MODES: ReadonlySet<GameMode> = new Set(['zombie', 'mp', 'sp']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `t5_zombie_cosmodrome_1` → { game:'t5', gameMode:'zombie', mapSlug:'cosmodrome', mapCanonical:'zombie_cosmodrome', lobby:1 }
|
||||||
|
*
|
||||||
|
* Rules:
|
||||||
|
* - First segment = game
|
||||||
|
* - Second segment = mode when it matches a known mode (zombie|mp|sp); otherwise the whole
|
||||||
|
* tail is treated as legacy `<game>_<slug>_<n>` (mode null, slug from segments 1..-1)
|
||||||
|
* - Trailing all-digits segment = lobby number
|
||||||
|
* - Everything in between = map slug (joined without separators for the catalog lookup)
|
||||||
|
*
|
||||||
|
* Multi-word slugs still work because we strip underscores when looking up
|
||||||
|
* (e.g. `t5_zombie_shi_no_numa_1` → slug 'shinonuma').
|
||||||
|
*/
|
||||||
|
export function parseServerName(name: string): ParsedServerName | null {
|
||||||
|
const parts = name.split('_').filter(Boolean);
|
||||||
|
if (parts.length < 2) return null;
|
||||||
|
const game = parts[0];
|
||||||
|
const lastIsLobby = /^\d+$/.test(parts[parts.length - 1]);
|
||||||
|
const lobby = lastIsLobby ? Number.parseInt(parts[parts.length - 1], 10) : null;
|
||||||
|
const tail = lastIsLobby ? parts.slice(1, -1) : parts.slice(1);
|
||||||
|
if (tail.length === 0) return null;
|
||||||
|
|
||||||
|
let gameMode: GameMode | null = null;
|
||||||
|
let slugParts = tail;
|
||||||
|
const maybeMode = tail[0].toLowerCase();
|
||||||
|
if (KNOWN_MODES.has(maybeMode as GameMode)) {
|
||||||
|
gameMode = maybeMode as GameMode;
|
||||||
|
slugParts = tail.slice(1);
|
||||||
|
}
|
||||||
|
if (slugParts.length === 0) return null;
|
||||||
|
|
||||||
|
const mapSlug = slugParts.join('').toLowerCase();
|
||||||
|
const mapCanonical = (BY_SLUG.get(mapSlug) ?? BY_FRIENDLY_SLUG.get(mapSlug))?.canonical ?? null;
|
||||||
|
return { game, gameMode, mapSlug, mapCanonical, lobby };
|
||||||
|
}
|
||||||
@@ -1,91 +1,2 @@
|
|||||||
// T5 (Black Ops 1) zombies map catalog. Server names encode `slug` so we can
|
// Shared catalog lives outside the server-only directory so the UI can use it too.
|
||||||
// derive game, mode, and map from the folder name (t5_zombie_cosmodrome_1 →
|
export * from '../../map-aliases.js';
|
||||||
// game 't5', mode 'zombie', slug 'cosmodrome', canonical 'zombie_cosmodrome').
|
|
||||||
//
|
|
||||||
// Fields:
|
|
||||||
// friendly — human name shown in the deploy UI dropdown
|
|
||||||
// slug — segment used in server name (t5_zombie_<slug>_<n>)
|
|
||||||
// canonical — actual mapname Pluto/RCON emits (matches T5 script scoping)
|
|
||||||
//
|
|
||||||
// Add rows when new maps land; keep slugs short so paths stay readable.
|
|
||||||
|
|
||||||
export interface T5MapEntry {
|
|
||||||
friendly: string;
|
|
||||||
slug: string;
|
|
||||||
canonical: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const T5_ZOMBIE_MAPS: readonly T5MapEntry[] = Object.freeze([
|
|
||||||
{ friendly: 'Kino Der Toten', slug: 'theater', canonical: 'zombie_theater' },
|
|
||||||
{ friendly: 'Ascension', slug: 'cosmodrome', canonical: 'zombie_cosmodrome' },
|
|
||||||
{ friendly: 'Call of the Dead', slug: 'coast', canonical: 'zombie_coast' },
|
|
||||||
{ friendly: 'Shangri-La', slug: 'temple', canonical: 'zombie_temple' },
|
|
||||||
{ friendly: 'Moon', slug: 'moon', canonical: 'zombie_moon' },
|
|
||||||
{ friendly: 'Five', slug: 'pentagon', canonical: 'zombie_pentagon' },
|
|
||||||
{ friendly: 'Dead Ops Arcade', slug: 'zombietron', canonical: 'zombietron' },
|
|
||||||
{ friendly: 'Nacht Der Untoten', slug: 'prototype', canonical: 'zombie_cod5_prototype' },
|
|
||||||
{ friendly: 'Verrückt', slug: 'asylum', canonical: 'zombie_cod5_asylum' },
|
|
||||||
{ friendly: 'Shi No Numa', slug: 'sumpf', canonical: 'zombie_cod5_sumpf' },
|
|
||||||
{ friendly: 'Der Riese', slug: 'factory', canonical: 'zombie_cod5_factory' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
const BY_SLUG: ReadonlyMap<string, T5MapEntry> = new Map(
|
|
||||||
T5_ZOMBIE_MAPS.map((m) => [m.slug, m])
|
|
||||||
);
|
|
||||||
// Older folder names used friendly names (shi_no_numa) instead of engine
|
|
||||||
// slugs (sumpf). Keep deriving their canonical map from the same catalog.
|
|
||||||
const BY_FRIENDLY_SLUG: ReadonlyMap<string, T5MapEntry> = new Map(
|
|
||||||
T5_ZOMBIE_MAPS.map((m) => [m.friendly.toLowerCase().replace(/[^a-z0-9]/g, ''), m])
|
|
||||||
);
|
|
||||||
|
|
||||||
export function t5ZombieMapBySlug(slug: string): T5MapEntry | undefined {
|
|
||||||
return BY_SLUG.get(slug.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GameMode = 'zombie' | 'mp' | 'sp';
|
|
||||||
|
|
||||||
export interface ParsedServerName {
|
|
||||||
game: string; // 't5'
|
|
||||||
gameMode: GameMode | null; // 'zombie' — null when the second segment isn't a known mode
|
|
||||||
mapSlug: string; // 'cosmodrome' — joined without separators for lookup
|
|
||||||
mapCanonical: string | null;// 'zombie_cosmodrome' — null when slug not in catalog
|
|
||||||
lobby: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const KNOWN_MODES: ReadonlySet<GameMode> = new Set(['zombie', 'mp', 'sp']);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `t5_zombie_cosmodrome_1` → { game:'t5', gameMode:'zombie', mapSlug:'cosmodrome', mapCanonical:'zombie_cosmodrome', lobby:1 }
|
|
||||||
*
|
|
||||||
* Rules:
|
|
||||||
* - First segment = game
|
|
||||||
* - Second segment = mode when it matches a known mode (zombie|mp|sp); otherwise the whole
|
|
||||||
* tail is treated as legacy `<game>_<slug>_<n>` (mode null, slug from segments 1..-1)
|
|
||||||
* - Trailing all-digits segment = lobby number
|
|
||||||
* - Everything in between = map slug (joined without separators for the catalog lookup)
|
|
||||||
*
|
|
||||||
* Multi-word slugs still work because we strip underscores when looking up
|
|
||||||
* (e.g. `t5_zombie_shi_no_numa_1` → slug 'shinonuma').
|
|
||||||
*/
|
|
||||||
export function parseServerName(name: string): ParsedServerName | null {
|
|
||||||
const parts = name.split('_').filter(Boolean);
|
|
||||||
if (parts.length < 2) return null;
|
|
||||||
const game = parts[0];
|
|
||||||
const lastIsLobby = /^\d+$/.test(parts[parts.length - 1]);
|
|
||||||
const lobby = lastIsLobby ? Number.parseInt(parts[parts.length - 1], 10) : null;
|
|
||||||
const tail = lastIsLobby ? parts.slice(1, -1) : parts.slice(1);
|
|
||||||
if (tail.length === 0) return null;
|
|
||||||
|
|
||||||
let gameMode: GameMode | null = null;
|
|
||||||
let slugParts = tail;
|
|
||||||
const maybeMode = tail[0].toLowerCase();
|
|
||||||
if (KNOWN_MODES.has(maybeMode as GameMode)) {
|
|
||||||
gameMode = maybeMode as GameMode;
|
|
||||||
slugParts = tail.slice(1);
|
|
||||||
}
|
|
||||||
if (slugParts.length === 0) return null;
|
|
||||||
|
|
||||||
const mapSlug = slugParts.join('').toLowerCase();
|
|
||||||
const mapCanonical = (BY_SLUG.get(mapSlug) ?? BY_FRIENDLY_SLUG.get(mapSlug))?.canonical ?? null;
|
|
||||||
return { game, gameMode, mapSlug, mapCanonical, lobby };
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { uiNow } from '$ui/stores/servers.js';
|
import { uiNow } from '$ui/stores/servers.js';
|
||||||
import { gametypeLabel } from '$ui/gametype.js';
|
import { gametypeLabel } from '$ui/gametype.js';
|
||||||
import { elapsedSince } from '$ui/duration.js';
|
import { elapsedSince } from '$ui/duration.js';
|
||||||
|
import { mapTitle, serverTitle } from '$ui/server-labels.js';
|
||||||
import Card from './primitives/Card.svelte';
|
import Card from './primitives/Card.svelte';
|
||||||
import Badge from './primitives/Badge.svelte';
|
import Badge from './primitives/Badge.svelte';
|
||||||
import Stat from './primitives/Stat.svelte';
|
import Stat from './primitives/Stat.svelte';
|
||||||
@@ -64,7 +65,8 @@
|
|||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
);
|
);
|
||||||
let showRoster = $derived(health === 'online' || health === 'stale');
|
let showRoster = $derived(health === 'online' || health === 'stale');
|
||||||
let mapLabel = $derived(map ?? '—');
|
let title = $derived(serverTitle(name));
|
||||||
|
let mapLabel = $derived(mapTitle(map));
|
||||||
let gtLabel = $derived(gametypeLabel(gametype));
|
let gtLabel = $derived(gametypeLabel(gametype));
|
||||||
|
|
||||||
let activeElapsed = $derived(activeSince ? elapsedSince(activeSince, $uiNow) : null);
|
let activeElapsed = $derived(activeSince ? elapsedSince(activeSince, $uiNow) : null);
|
||||||
@@ -85,7 +87,7 @@
|
|||||||
elevation="interactive"
|
elevation="interactive"
|
||||||
padding="md"
|
padding="md"
|
||||||
class={restarting ? 'restarting' : health === 'offline' ? 'offline' : ''}
|
class={restarting ? 'restarting' : health === 'offline' ? 'offline' : ''}
|
||||||
ariaLabel="Server {name}, {health}"
|
ariaLabel="Server {title}, {health}"
|
||||||
style={cardStyle}
|
style={cardStyle}
|
||||||
>
|
>
|
||||||
{#if restarting}
|
{#if restarting}
|
||||||
@@ -95,7 +97,7 @@
|
|||||||
<div class="head">
|
<div class="head">
|
||||||
<div class="name-row">
|
<div class="name-row">
|
||||||
<StatusPip {health} />
|
<StatusPip {health} />
|
||||||
<strong class="name">{name}</strong>
|
<strong class="name" title={name}>{title}</strong>
|
||||||
</div>
|
</div>
|
||||||
{#if health === 'starting'}
|
{#if health === 'starting'}
|
||||||
<Badge variant="info" uppercase>Starting</Badge>
|
<Badge variant="info" uppercase>Starting</Badge>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { parseServerName, T5_ZOMBIE_MAPS } from '../map-aliases.js';
|
||||||
|
|
||||||
|
export function mapTitle(map: string | null | undefined): string {
|
||||||
|
return T5_ZOMBIE_MAPS.find((entry) => entry.canonical === map?.toLowerCase())?.friendly ?? map ?? '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serverTitle(name: string): string {
|
||||||
|
const parsed = parseServerName(name);
|
||||||
|
if (!parsed || parsed.game.toLowerCase() !== 't5' ||
|
||||||
|
(parsed.gameMode !== null && parsed.gameMode !== 'zombie') || !parsed.mapCanonical) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
const title = mapTitle(parsed.mapCanonical);
|
||||||
|
return parsed.lobby === null ? title : `${title} · Lobby ${parsed.lobby}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user