Map-Bilder und Seitenwahl mit Emblemen

Map-Bilder aus ghostcap-gaming/cs2-map-images lokal als WebP gespeichert
(npm run images lädt und verkleinert sie). Bilder in Veto-Kacheln,
Pool-Vorschau, Serie und Seitenwahl; CT/T-Embleme aus client/public/sides
mit Ersatzsymbol, falls die Dateien fehlen.
This commit is contained in:
mregosar committed 2026-10-25 17:23:55 +01:00
1 parent e304556346
commit 7be0d85ab5
21 files changed
+701 -27

No files matched your search

+6
View File
@@ -162,6 +162,12 @@ Ungültige oder fehlende Abläufe machen nur dieses Format unauswählbar (mit Be
Die Wahl wird als `mp_teamname_1` (Team mit CT-Start) und `mp_teamname_2` (T-Start) an den Server übergeben. Die Wahl wird als `mp_teamname_1` (Team mit CT-Start) und `mp_teamname_2` (T-Start) an den Server übergeben.
### Bilder für Maps und Seitenwahl
- **Map-Bilder** liegen lokal unter `client/public/maps/<map>.webp` (960 px breit, je ca. 50–90 KB). `"image"` in `map-pools.json` verweist darauf. Neue oder fehlende Bilder lädt `npm run images` von [ghostcap-gaming/cs2-map-images](https://github.com/ghostcap-gaming/cs2-map-images), verkleinert sie und trägt den Pfad ein (`npm run images -- --force` lädt alle neu). Die Bilder stammen aus dem Spiel; die Rechte liegen bei Valve. Ohne Bild zeigt die Kachel ein Muster in der Map-Farbe.
- **Seiten-Embleme** für CT und T liegen unter `client/public/sides/ct.png` und `client/public/sides/t.png` (quadratisch, z. B. 256×256). Fehlt eine Datei, erscheint ein schlichtes Ersatzsymbol.
- Bestehende Matches behalten die Map-Liste, mit der sie erstellt wurden. Neue Bilder erscheinen in neu erstellten Matches.
> Die mitgelieferten Map-Pools entsprechen dem Stand der Entwicklung. Valve ändert den aktiven Map-Pool regelmäßig. Map-IDs bitte gegen die eigene Serverinstallation prüfen. Eine Map, die nicht existiert, führt zu „Konfiguration fehlgeschlagen“ mit Meldung. > Die mitgelieferten Map-Pools entsprechen dem Stand der Entwicklung. Valve ändert den aktiven Map-Pool regelmäßig. Map-IDs bitte gegen die eigene Serverinstallation prüfen. Eine Map, die nicht existiert, führt zu „Konfiguration fehlgeschlagen“ mit Meldung.
--- ---
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+17
View File
@@ -0,0 +1,17 @@
import { useState } from 'react';
/**
* Map-Bild aus der Konfiguration (map.image). Lädt das Bild nicht, bleibt das
* gezeichnete Muster in Map-Farbe darunter sichtbar.
*/
export default function MapImage({ map, className = '' }) {
const [failed, setFailed] = useState(false);
return (
<span className={`map-art ${className}`} style={{ '--map': map?.color ?? '#556' }} aria-hidden="true">
{map?.image && !failed && (
// Kein loading="lazy": in Kacheln mit clip-path zeichnet Chromium das Bild nach dem Laden sonst nicht neu.
<img src={map.image} alt="" onError={() => setFailed(true)} />
)}
</span>
);
}
+10 -1
View File
@@ -1,3 +1,6 @@
import MapImage from './MapImage.jsx';
import SideIcon from './SideIcon.jsx';
const STATUS = { pending: 'Ausstehend', next: 'Wird eingerichtet', live: 'Aktuell', done: 'Gespielt' }; const STATUS = { pending: 'Ausstehend', next: 'Wird eingerichtet', live: 'Aktuell', done: 'Gespielt' };
export default function SeriesList({ state }) { export default function SeriesList({ state }) {
@@ -12,13 +15,19 @@ export default function SeriesList({ state }) {
const t = s.ctTeam ? teams[s.ctTeam === 'A' ? 'B' : 'A'].name : null; const t = s.ctTeam ? teams[s.ctTeam === 'A' ? 'B' : 'A'].name : null;
return ( return (
<li key={s.number} className={`series-item is-${s.status}`} style={{ '--map': map?.color ?? '#556' }}> <li key={s.number} className={`series-item is-${s.status}`} style={{ '--map': map?.color ?? '#556' }}>
<MapImage map={map} className="series-art" />
<span className="series-no">Map {s.number}</span> <span className="series-no">Map {s.number}</span>
<strong className="series-map">{s.mapName}</strong> <strong className="series-map">{s.mapName}</strong>
<span className="series-origin"> <span className="series-origin">
{s.pickedBy ? `Pick von ${teams[s.pickedBy].name}` : 'Entscheidungsmap'} {s.pickedBy ? `Pick von ${teams[s.pickedBy].name}` : 'Entscheidungsmap'}
</span> </span>
<span className="series-sides"> <span className="series-sides">
{ct ? <><span className="side-ct">CT: {ct}</span> <span className="side-t">T: {t}</span></> : 'Seiten im Spiel festlegen'} {ct ? (
<>
<span className="side-ct"><SideIcon side="CT" size={18} /> {ct}</span>
<span className="side-t"><SideIcon side="T" size={18} /> {t}</span>
</>
) : 'Seiten im Spiel festlegen'}
</span> </span>
<span className="series-status">{STATUS[s.status]}</span> <span className="series-status">{STATUS[s.status]}</span>
</li> </li>
+36
View File
@@ -0,0 +1,36 @@
import { useState } from 'react';
// Emblem-Dateien in client/public/sides/. Fehlen sie, wird ein schlichtes Ersatzsymbol gezeichnet.
const IMAGES = { CT: '/sides/ct.png', T: '/sides/t.png' };
export default function SideIcon({ side, size = 28 }) {
const [failed, setFailed] = useState(false);
if (!failed) {
return (
<img className={`side-icon side-icon-${side.toLowerCase()}`} src={IMAGES[side]} width={size} height={size}
alt="" aria-hidden="true" onError={() => setFailed(true)} />
);
}
return <FallbackIcon side={side} size={size} />;
}
function FallbackIcon({ side, size }) {
if (side === 'CT') {
return (
<svg className="side-icon side-icon-ct" viewBox="0 0 32 32" width={size} height={size} aria-hidden="true">
<path d="M16 2 4 6.5v8.2C4 22.3 9.1 28 16 30c6.9-2 12-7.7 12-15.3V6.5L16 2z" fill="currentColor" opacity=".22" />
<path d="M16 2 4 6.5v8.2C4 22.3 9.1 28 16 30c6.9-2 12-7.7 12-15.3V6.5L16 2z" fill="none" stroke="currentColor" strokeWidth="2" strokeLinejoin="round" />
<path d="m16 9 2.1 4.3 4.7.7-3.4 3.3.8 4.7-4.2-2.2-4.2 2.2.8-4.7-3.4-3.3 4.7-.7z" fill="currentColor" />
</svg>
);
}
return (
<svg className="side-icon side-icon-t" viewBox="0 0 32 32" width={size} height={size} aria-hidden="true">
<rect x="5" y="11" width="22" height="15" rx="2.5" fill="currentColor" opacity=".22" />
<rect x="5" y="11" width="22" height="15" rx="2.5" fill="none" stroke="currentColor" strokeWidth="2" />
<rect x="9" y="15" width="9" height="5" rx="1" fill="currentColor" />
<circle cx="22.5" cy="17.5" r="1.8" fill="currentColor" />
<path d="M11 11V8.5a2.5 2.5 0 0 1 2.5-2.5h2c1.4 0 2.5-1.1 2.5-2.5" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
);
}
+18 -5
View File
@@ -1,5 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { clock } from '../lib/labels.js'; import { clock } from '../lib/labels.js';
import MapImage from './MapImage.jsx';
import SideIcon from './SideIcon.jsx';
const STEP_SHORT = { ban: 'Ban', pick: 'Pick', side: 'Seite', decider: 'Decider' }; const STEP_SHORT = { ban: 'Ban', pick: 'Pick', side: 'Seite', decider: 'Decider' };
@@ -57,7 +59,7 @@ export default function VetoBoard({ state, viewer, act, busy, compact = false })
aria-pressed={isSel} aria-pressed={isSel}
aria-label={`${meta.name}: ${statusText(m, teams)}`} aria-label={`${meta.name}: ${statusText(m, teams)}`}
> >
<span className="map-art" aria-hidden="true" /> <MapImage map={meta} />
<span className="map-name">{meta.name}</span> <span className="map-name">{meta.name}</span>
<span className="map-status">{statusText(m, teams)}</span> <span className="map-status">{statusText(m, teams)}</span>
</button> </button>
@@ -77,10 +79,21 @@ export default function VetoBoard({ state, viewer, act, busy, compact = false })
{yourTurn && current.type === 'side' && ( {yourTurn && current.type === 'side' && (
<div className="side-choice"> <div className="side-choice">
<p>Auf welcher Seite startet {teams[viewer.team].name} auf {sideMap?.name}?</p> <MapImage map={sideMap} className="side-choice-art" />
<div className="row"> <div className="side-choice-body">
<button className="btn btn-ct btn-lg" disabled={!!busy} onClick={() => submitSide('CT')}>Counter-Terrorists</button> <p>Auf welcher Seite startet <strong>{teams[viewer.team].name}</strong> auf <strong>{sideMap?.name}</strong>?</p>
<button className="btn btn-t btn-lg" disabled={!!busy} onClick={() => submitSide('T')}>Terrorists</button> <div className="side-options">
<button className="side-option side-option-ct" disabled={!!busy} onClick={() => submitSide('CT')}>
<SideIcon side="CT" size={44} />
<span className="side-option-name">Counter-Terrorists</span>
<span className="side-option-sub">Start auf CT, Gegner beginnt als T</span>
</button>
<button className="side-option side-option-t" disabled={!!busy} onClick={() => submitSide('T')}>
<SideIcon side="T" size={44} />
<span className="side-option-name">Terrorists</span>
<span className="side-option-sub">Start auf T, Gegner beginnt als CT</span>
</button>
</div>
</div> </div>
</div> </div>
)} )}
+5 -1
View File
@@ -10,6 +10,7 @@ import ServerPanel from '../components/ServerPanel.jsx';
import OrganizerPanel from '../components/OrganizerPanel.jsx'; import OrganizerPanel from '../components/OrganizerPanel.jsx';
import SeriesList from '../components/SeriesList.jsx'; import SeriesList from '../components/SeriesList.jsx';
import EventLog from '../components/EventLog.jsx'; import EventLog from '../components/EventLog.jsx';
import MapImage from '../components/MapImage.jsx';
export default function MatchPage() { export default function MatchPage() {
const { id } = useParams(); const { id } = useParams();
@@ -179,7 +180,10 @@ function MapPoolPreview({ maps }) {
return ( return (
<div className="pool-preview" aria-label="Map-Pool"> <div className="pool-preview" aria-label="Map-Pool">
{maps.map((m) => ( {maps.map((m) => (
<span key={m.id} className="pool-chip" style={{ '--map': m.color ?? '#556' }}>{m.name}</span> <span key={m.id} className="pool-chip" style={{ '--map': m.color ?? '#556' }}>
<MapImage map={m} />
<span className="pool-chip-name">{m.name}</span>
</span>
))} ))}
</div> </div>
); );
+46 -5
View File
@@ -177,8 +177,12 @@ input:focus, select:focus { border-color: var(--ct); outline: none; }
.stage-note .pool-preview { margin-top: 16px; } .stage-note .pool-preview { margin-top: 16px; }
.missing-list { margin: 0 0 12px; padding-left: 1.1em; color: var(--muted); } .missing-list { margin: 0 0 12px; padding-left: 1.1em; color: var(--muted); }
.missing-list li { margin-bottom: 4px; } .missing-list li { margin-bottom: 4px; }
.pool-preview { display: flex; flex-wrap: wrap; gap: 6px; } .pool-preview { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 8px; }
.pool-chip { padding: 4px 10px; border-left: 3px solid var(--map); background: var(--surface-2); font-size: .875rem; border-radius: 3px; } .pool-chip {
position: relative; overflow: hidden; display: flex; align-items: flex-end;
aspect-ratio: 16 / 9; padding: 6px 8px; border-bottom: 3px solid var(--map); border-radius: 3px; background: var(--surface-2);
}
.pool-chip-name { position: relative; z-index: 1; font: 600 1rem var(--display); }
.team-card { --team: var(--team-a); background: var(--surface); border: 1px solid var(--line); border-top: 4px solid var(--team); border-radius: var(--radius); padding: 16px; } .team-card { --team: var(--team-a); background: var(--surface); border: 1px solid var(--line); border-top: 4px solid var(--team); border-radius: var(--radius); padding: 16px; }
.team-card.team-B { --team: var(--team-b); } .team-card.team-B { --team: var(--team-b); }
@@ -235,6 +239,12 @@ input:focus, select:focus { border-color: var(--ct); outline: none; }
radial-gradient(120% 90% at 85% 0%, color-mix(in srgb, var(--map) 70%, transparent), transparent 60%), radial-gradient(120% 90% at 85% 0%, color-mix(in srgb, var(--map) 70%, transparent), transparent 60%),
linear-gradient(180deg, color-mix(in srgb, var(--map) 35%, var(--surface-2)), var(--surface) 85%); linear-gradient(180deg, color-mix(in srgb, var(--map) 35%, var(--surface-2)), var(--surface) 85%);
} }
.map-art img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Abdunkeln nach unten, damit Name und Status auf jedem Bild lesbar bleiben */
.map-art::after {
content: ''; position: absolute; inset: 0;
background: linear-gradient(180deg, rgba(16, 21, 31, .05) 30%, rgba(16, 21, 31, .88) 100%);
}
.map-name, .map-status { position: relative; z-index: 1; } .map-name, .map-status { position: relative; z-index: 1; }
.map-name { font: 700 1.625rem/1 var(--display); text-shadow: 0 1px 8px rgba(0, 0, 0, .5); } .map-name { font: 700 1.625rem/1 var(--display); text-shadow: 0 1px 8px rgba(0, 0, 0, .5); }
.map-status { font-size: .8125rem; color: color-mix(in srgb, var(--text) 75%, transparent); margin-top: 4px; } .map-status { font-size: .8125rem; color: color-mix(in srgb, var(--text) 75%, transparent); margin-top: 4px; }
@@ -265,8 +275,26 @@ input:focus, select:focus { border-color: var(--ct); outline: none; }
@keyframes side-pulse { 50% { box-shadow: inset 0 0 0 3px var(--pending); } } @keyframes side-pulse { 50% { box-shadow: inset 0 0 0 3px var(--pending); } }
.veto-confirm .btn { width: 100%; } .veto-confirm .btn { width: 100%; }
.side-choice { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 16px; } .side-choice { position: relative; overflow: hidden; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
.side-choice .btn { flex: 1; } .side-choice-art { opacity: .45; }
.side-choice-art::after { background: linear-gradient(90deg, rgba(16, 21, 31, .92), rgba(16, 21, 31, .55)); }
.side-choice-body { position: relative; z-index: 1; padding: 18px; }
.side-options { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.side-option {
--side: var(--ct);
display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 12px; align-items: center;
text-align: left; padding: 14px 16px; cursor: pointer; font: inherit; color: var(--text);
background: color-mix(in srgb, var(--side) 16%, rgba(16, 21, 31, .75));
border: 2px solid color-mix(in srgb, var(--side) 70%, transparent); border-radius: var(--radius);
transition: background .15s, border-color .15s, transform .15s;
}
.side-option-t { --side: var(--t); }
.side-option .side-icon { grid-row: 1 / 3; color: var(--side); }
.side-option-name { font: 700 1.375rem/1.1 var(--display); color: var(--side); }
.side-option-sub { font-size: .8125rem; color: var(--muted); }
.side-option:hover:not(:disabled) { background: color-mix(in srgb, var(--side) 28%, rgba(16, 21, 31, .75)); border-color: var(--side); transform: translateY(-1px); }
.side-option:disabled { opacity: .5; cursor: not-allowed; }
.side-icon { flex: none; }
.step-track { list-style: none; display: flex; flex-wrap: wrap; gap: 4px; padding: 0; margin: 0; } .step-track { list-style: none; display: flex; flex-wrap: wrap; gap: 4px; padding: 0; margin: 0; }
.step { .step {
@@ -293,9 +321,20 @@ input:focus, select:focus { border-color: var(--ct); outline: none; }
.series { margin-bottom: 20px; } .series { margin-bottom: 20px; }
.series-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; } .series-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; }
.series-item { .series-item {
position: relative; overflow: hidden;
display: grid; grid-template-columns: auto 1fr auto; gap: 2px 16px; align-items: baseline; display: grid; grid-template-columns: auto 1fr auto; gap: 2px 16px; align-items: baseline;
padding: 14px 16px; background: var(--surface); border-left: 4px solid var(--map); border-radius: var(--radius); padding: 14px 16px; background: var(--surface); border-left: 4px solid var(--map); border-radius: var(--radius);
} }
.series-item > :not(.series-art) { position: relative; z-index: 1; }
/* Map-Bild rechts, nach links in die Fläche ausgeblendet */
.series-art {
left: auto !important; width: 60%;
-webkit-mask-image: linear-gradient(to left, #000 35%, transparent);
mask-image: linear-gradient(to left, #000 35%, transparent);
opacity: .7;
}
.series-art::after { display: none; }
.series-status { text-shadow: 0 1px 6px rgba(0, 0, 0, .8); }
.series-no { color: var(--muted); font-size: .875rem; } .series-no { color: var(--muted); font-size: .875rem; }
.series-map { font: 700 1.625rem var(--display); } .series-map { font: 700 1.625rem var(--display); }
.series-status { font-size: .875rem; color: var(--muted); text-align: right; grid-column: 3; grid-row: 1; } .series-status { font-size: .875rem; color: var(--muted); text-align: right; grid-column: 3; grid-row: 1; }
@@ -303,8 +342,9 @@ input:focus, select:focus { border-color: var(--ct); outline: none; }
.series-item.is-live { box-shadow: inset 0 0 0 1px var(--map); } .series-item.is-live { box-shadow: inset 0 0 0 1px var(--map); }
.series-item.is-live .series-status, .series-item.is-next .series-status { color: var(--pending); font-weight: 600; } .series-item.is-live .series-status, .series-item.is-next .series-status { color: var(--pending); font-weight: 600; }
.series-item.is-done { opacity: .6; } .series-item.is-done { opacity: .6; }
.side-ct, .side-t { display: inline-flex; align-items: center; gap: 5px; vertical-align: middle; }
.side-ct { color: var(--ct); } .side-ct { color: var(--ct); }
.side-t { color: var(--t); margin-left: 12px; } .side-t { color: var(--t); margin-left: 14px; }
/* ---------- Panels unten ---------- */ /* ---------- Panels unten ---------- */
.match-lower { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-top: 28px; align-items: start; } .match-lower { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-top: 28px; align-items: start; }
@@ -359,6 +399,7 @@ input:focus, select:focus { border-color: var(--ct); outline: none; }
.create-teams { grid-template-columns: 1fr; } .create-teams { grid-template-columns: 1fr; }
.vs.big { justify-self: center; } .vs.big { justify-self: center; }
.map-grid { grid-template-columns: repeat(2, 1fr); } .map-grid { grid-template-columns: repeat(2, 1fr); }
.side-options { grid-template-columns: 1fr; }
.series-item { grid-template-columns: 1fr auto; } .series-item { grid-template-columns: 1fr auto; }
.series-no { grid-column: 1 / -1; } .series-no { grid-column: 1 / -1; }
.series-status { grid-column: 2; grid-row: 2; } .series-status { grid-column: 2; grid-row: 2; }
+488 -1
View File
@@ -12,7 +12,8 @@
"client" "client"
], ],
"devDependencies": { "devDependencies": {
"concurrently": "^9.1.0" "concurrently": "^9.1.0",
"sharp": "^0.33.5"
}, },
"engines": { "engines": {
"node": ">=18.18" "node": ">=18.18"
@@ -322,6 +323,17 @@
"resolved": "server", "resolved": "server",
"link": true "link": true
}, },
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -713,6 +725,386 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
"cpu": [
"wasm32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.2.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -1568,6 +1960,20 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1588,6 +1994,17 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/concurrently": { "node_modules/concurrently": {
"version": "9.2.5", "version": "9.2.5",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.5.tgz", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.5.tgz",
@@ -2281,6 +2698,13 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/is-arrayish": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
"dev": true,
"license": "MIT"
},
"node_modules/is-fullwidth-code-point": { "node_modules/is-fullwidth-code-point": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -2973,6 +3397,59 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/sharp": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.3",
"semver": "^7.6.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.33.5",
"@img/sharp-darwin-x64": "0.33.5",
"@img/sharp-libvips-darwin-arm64": "1.0.4",
"@img/sharp-libvips-darwin-x64": "1.0.4",
"@img/sharp-libvips-linux-arm": "1.0.5",
"@img/sharp-libvips-linux-arm64": "1.0.4",
"@img/sharp-libvips-linux-s390x": "1.0.4",
"@img/sharp-libvips-linux-x64": "1.0.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
"@img/sharp-linux-arm": "0.33.5",
"@img/sharp-linux-arm64": "0.33.5",
"@img/sharp-linux-s390x": "0.33.5",
"@img/sharp-linux-x64": "0.33.5",
"@img/sharp-linuxmusl-arm64": "0.33.5",
"@img/sharp-linuxmusl-x64": "0.33.5",
"@img/sharp-wasm32": "0.33.5",
"@img/sharp-win32-ia32": "0.33.5",
"@img/sharp-win32-x64": "0.33.5"
}
},
"node_modules/sharp/node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shell-quote": { "node_modules/shell-quote": {
"version": "1.12.0", "version": "1.12.0",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz",
@@ -3103,6 +3580,16 @@
"simple-concat": "^1.0.0" "simple-concat": "^1.0.0"
} }
}, },
"node_modules/simple-swizzle": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/socket.io": { "node_modules/socket.io": {
"version": "4.8.4", "version": "4.8.4",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.4.tgz", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.4.tgz",
+4 -2
View File
@@ -13,10 +13,12 @@
"dev:client": "npm run dev --workspace client", "dev:client": "npm run dev --workspace client",
"build": "npm run build --workspace client", "build": "npm run build --workspace client",
"start": "npm run start --workspace server", "start": "npm run start --workspace server",
"test": "npm test --workspace server" "test": "npm test --workspace server",
"images": "node scripts/fetch-map-images.mjs"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^9.1.0" "concurrently": "^9.1.0",
"sharp": "^0.33.5"
}, },
"engines": { "engines": {
"node": ">=18.18" "node": ">=18.18"
+56
View File
@@ -0,0 +1,56 @@
// Lädt für alle Maps aus server/config/map-pools.json ein Bild herunter, verkleinert es
// und speichert es als WebP unter client/public/maps/<map>.webp. Danach zeigt "image" in
// der Konfiguration auf die lokale Datei.
//
// Aufruf: npm run images fehlende Bilder laden
// npm run images -- --force alle Bilder neu laden
//
// Quelle: https://github.com/ghostcap-gaming/cs2-map-images (Bilder aus dem Spiel, Rechte bei Valve)
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import sharp from 'sharp';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const poolFile = path.join(root, 'server', 'config', 'map-pools.json');
const outDir = path.join(root, 'client', 'public', 'maps');
const SOURCE = 'https://cdn.jsdelivr.net/gh/ghostcap-gaming/cs2-map-images@main/cs2/';
const WIDTH = 960;
const force = process.argv.includes('--force');
const config = JSON.parse(fs.readFileSync(poolFile, 'utf8'));
fs.mkdirSync(outDir, { recursive: true });
const ids = [...new Set(Object.values(config.pools).flatMap((p) => p.maps.map((m) => m.id)))];
const saved = new Set();
let failed = 0;
for (const id of ids) {
const target = path.join(outDir, `${id}.webp`);
if (!force && fs.existsSync(target)) {
console.log(`= ${id} (vorhanden)`);
saved.add(id);
continue;
}
try {
const res = await fetch(`${SOURCE}${id}.png`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const input = Buffer.from(await res.arrayBuffer());
const info = await sharp(input).resize({ width: WIDTH, withoutEnlargement: true }).webp({ quality: 78 }).toFile(target);
console.log(`+ ${id}: ${Math.round(input.length / 1024)} KB -> ${Math.round(info.size / 1024)} KB`);
saved.add(id);
} catch (err) {
failed++;
console.warn(`! ${id}: kein Bild (${err.message}) – Map behält das Farbmuster`);
}
}
// Konfiguration auf lokale Dateien umstellen (nur für Maps mit gespeichertem Bild).
let text = fs.readFileSync(poolFile, 'utf8');
for (const id of saved) {
const entry = new RegExp(`("id": "${id}"[^}]*?"image": ")[^"]*(")`, 'g');
text = text.replace(entry, `$1/maps/${id}.webp$2`);
}
fs.writeFileSync(poolFile, text);
console.log(`Fertig: ${saved.size} Bilder in client/public/maps${failed ? `, ${failed} fehlgeschlagen` : ''}.`);
+12 -12
View File
@@ -1,5 +1,5 @@
{ {
"$comment": "Map-Pools und Veto-Abläufe. Schritte: ban:X, pick:X, side:X, decider. X = erstes Team (startet das Veto), Y = zweites Team. 'side' folgt immer auf einen Pick bzw. auf 'decider' und bezieht sich auf diese Map. Ungültige Abläufe werden beim Start gemeldet und das Format ist dann nicht auswählbar.", "$comment": "Map-Pools und Veto-Abläufe. Schritte: ban:X, pick:X, side:X, decider. X = erstes Team (startet das Veto), Y = zweites Team. 'side' folgt immer auf einen Pick bzw. auf 'decider' und bezieht sich auf diese Map. Ungültige Abläufe werden beim Start gemeldet und das Format ist dann nicht auswählbar. Optional pro Map: image = Bild-URL oder Pfad (z. B. /maps/de_dust2.jpg aus client/public). Standardbilder: github.com/ghostcap-gaming/cs2-map-images (Bilder aus dem Spiel, Rechte bei Valve).",
"pools": { "pools": {
"competitive": { "competitive": {
"label": "Competitive 5 gegen 5", "label": "Competitive 5 gegen 5",
@@ -7,13 +7,13 @@
"gameType": 0, "gameType": 0,
"gameMode": 1, "gameMode": 1,
"maps": [ "maps": [
{ "id": "de_ancient", "name": "Ancient", "color": "#3f8f5a" }, { "id": "de_ancient", "name": "Ancient", "color": "#3f8f5a", "image": "/maps/de_ancient.webp" },
{ "id": "de_anubis", "name": "Anubis", "color": "#c79a3d" }, { "id": "de_anubis", "name": "Anubis", "color": "#c79a3d", "image": "/maps/de_anubis.webp" },
{ "id": "de_dust2", "name": "Dust II", "color": "#d3a45c" }, { "id": "de_dust2", "name": "Dust II", "color": "#d3a45c", "image": "/maps/de_dust2.webp" },
{ "id": "de_inferno", "name": "Inferno", "color": "#c4573a" }, { "id": "de_inferno", "name": "Inferno", "color": "#c4573a", "image": "/maps/de_inferno.webp" },
{ "id": "de_mirage", "name": "Mirage", "color": "#b98a4a" }, { "id": "de_mirage", "name": "Mirage", "color": "#b98a4a", "image": "/maps/de_mirage.webp" },
{ "id": "de_nuke", "name": "Nuke", "color": "#4a8fbf" }, { "id": "de_nuke", "name": "Nuke", "color": "#4a8fbf", "image": "/maps/de_nuke.webp" },
{ "id": "de_train", "name": "Train", "color": "#7a7f8a" } { "id": "de_train", "name": "Train", "color": "#7a7f8a", "image": "/maps/de_train.webp" }
], ],
"flows": { "flows": {
"bo1": ["ban:X", "ban:Y", "ban:X", "ban:Y", "ban:X", "ban:Y", "decider", "side:X"], "bo1": ["ban:X", "ban:Y", "ban:X", "ban:Y", "ban:X", "ban:Y", "decider", "side:X"],
@@ -27,10 +27,10 @@
"gameType": 0, "gameType": 0,
"gameMode": 2, "gameMode": 2,
"maps": [ "maps": [
{ "id": "de_inferno", "name": "Inferno", "color": "#c4573a" }, { "id": "de_inferno", "name": "Inferno", "color": "#c4573a", "image": "/maps/de_inferno.webp" },
{ "id": "de_nuke", "name": "Nuke", "color": "#4a8fbf" }, { "id": "de_nuke", "name": "Nuke", "color": "#4a8fbf", "image": "/maps/de_nuke.webp" },
{ "id": "de_overpass", "name": "Overpass", "color": "#5f8f7a" }, { "id": "de_overpass", "name": "Overpass", "color": "#5f8f7a", "image": "/maps/de_overpass.webp" },
{ "id": "de_vertigo", "name": "Vertigo", "color": "#8a74b8" } { "id": "de_vertigo", "name": "Vertigo", "color": "#8a74b8", "image": "/maps/de_vertigo.webp" }
], ],
"flows": { "flows": {
"bo1": ["ban:X", "ban:Y", "ban:X", "decider", "side:Y"], "bo1": ["ban:X", "ban:Y", "ban:X", "decider", "side:Y"],
+3
View File
@@ -17,6 +17,9 @@ export function buildMapPools(raw) {
for (const map of pool.maps ?? []) { for (const map of pool.maps ?? []) {
if (!/^[a-z0-9_]+$/.test(map.id)) throw new Error(`Ungültige Map-ID "${map.id}" im Pool "${mode}"`); if (!/^[a-z0-9_]+$/.test(map.id)) throw new Error(`Ungültige Map-ID "${map.id}" im Pool "${mode}"`);
if (ids.has(map.id)) throw new Error(`Map "${map.id}" ist im Pool "${mode}" doppelt`); if (ids.has(map.id)) throw new Error(`Map "${map.id}" ist im Pool "${mode}" doppelt`);
if (map.image !== undefined && !/^(https?:\/\/|\/)[^\s"'()<>]+$/.test(map.image)) {
throw new Error(`Bild für "${map.id}" im Pool "${mode}" muss eine http(s)-URL oder ein Pfad mit / sein`);
}
ids.add(map.id); ids.add(map.id);
} }
const formats = {}; const formats = {};