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.
No files matched your search
@@ -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.
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 64 KiB |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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' };
|
||||
|
||||
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;
|
||||
return (
|
||||
<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>
|
||||
<strong className="series-map">{s.mapName}</strong>
|
||||
<span className="series-origin">
|
||||
{s.pickedBy ? `Pick von ${teams[s.pickedBy].name}` : 'Entscheidungsmap'}
|
||||
</span>
|
||||
<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 className="series-status">{STATUS[s.status]}</span>
|
||||
</li>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
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' };
|
||||
|
||||
@@ -57,7 +59,7 @@ export default function VetoBoard({ state, viewer, act, busy, compact = false })
|
||||
aria-pressed={isSel}
|
||||
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-status">{statusText(m, teams)}</span>
|
||||
</button>
|
||||
@@ -77,10 +79,21 @@ export default function VetoBoard({ state, viewer, act, busy, compact = false })
|
||||
|
||||
{yourTurn && current.type === 'side' && (
|
||||
<div className="side-choice">
|
||||
<p>Auf welcher Seite startet {teams[viewer.team].name} auf {sideMap?.name}?</p>
|
||||
<div className="row">
|
||||
<button className="btn btn-ct btn-lg" disabled={!!busy} onClick={() => submitSide('CT')}>Counter-Terrorists</button>
|
||||
<button className="btn btn-t btn-lg" disabled={!!busy} onClick={() => submitSide('T')}>Terrorists</button>
|
||||
<MapImage map={sideMap} className="side-choice-art" />
|
||||
<div className="side-choice-body">
|
||||
<p>Auf welcher Seite startet <strong>{teams[viewer.team].name}</strong> auf <strong>{sideMap?.name}</strong>?</p>
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import ServerPanel from '../components/ServerPanel.jsx';
|
||||
import OrganizerPanel from '../components/OrganizerPanel.jsx';
|
||||
import SeriesList from '../components/SeriesList.jsx';
|
||||
import EventLog from '../components/EventLog.jsx';
|
||||
import MapImage from '../components/MapImage.jsx';
|
||||
|
||||
export default function MatchPage() {
|
||||
const { id } = useParams();
|
||||
@@ -179,7 +180,10 @@ function MapPoolPreview({ maps }) {
|
||||
return (
|
||||
<div className="pool-preview" aria-label="Map-Pool">
|
||||
{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>
|
||||
);
|
||||
|
||||
@@ -177,8 +177,12 @@ input:focus, select:focus { border-color: var(--ct); outline: none; }
|
||||
.stage-note .pool-preview { margin-top: 16px; }
|
||||
.missing-list { margin: 0 0 12px; padding-left: 1.1em; color: var(--muted); }
|
||||
.missing-list li { margin-bottom: 4px; }
|
||||
.pool-preview { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.pool-chip { padding: 4px 10px; border-left: 3px solid var(--map); background: var(--surface-2); font-size: .875rem; border-radius: 3px; }
|
||||
.pool-preview { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 8px; }
|
||||
.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-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%),
|
||||
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 { 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; }
|
||||
@@ -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); } }
|
||||
|
||||
.veto-confirm .btn { width: 100%; }
|
||||
.side-choice { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 16px; }
|
||||
.side-choice .btn { flex: 1; }
|
||||
.side-choice { position: relative; overflow: hidden; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
|
||||
.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 {
|
||||
@@ -293,9 +321,20 @@ input:focus, select:focus { border-color: var(--ct); outline: none; }
|
||||
.series { margin-bottom: 20px; }
|
||||
.series-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; }
|
||||
.series-item {
|
||||
position: relative; overflow: hidden;
|
||||
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);
|
||||
}
|
||||
.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-map { font: 700 1.625rem var(--display); }
|
||||
.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 .series-status, .series-item.is-next .series-status { color: var(--pending); font-weight: 600; }
|
||||
.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-t { color: var(--t); margin-left: 12px; }
|
||||
.side-t { color: var(--t); margin-left: 14px; }
|
||||
|
||||
/* ---------- Panels unten ---------- */
|
||||
.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; }
|
||||
.vs.big { justify-self: center; }
|
||||
.map-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.side-options { grid-template-columns: 1fr; }
|
||||
.series-item { grid-template-columns: 1fr auto; }
|
||||
.series-no { grid-column: 1 / -1; }
|
||||
.series-status { grid-column: 2; grid-row: 2; }
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"client"
|
||||
],
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.0"
|
||||
"concurrently": "^9.1.0",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
@@ -322,6 +323,17 @@
|
||||
"resolved": "server",
|
||||
"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": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
@@ -713,6 +725,386 @@
|
||||
"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": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -1568,6 +1960,20 @@
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -1588,6 +1994,17 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "9.2.5",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.5.tgz",
|
||||
@@ -2281,6 +2698,13 @@
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz",
|
||||
@@ -3103,6 +3580,16 @@
|
||||
"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": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.4.tgz",
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
"dev:client": "npm run dev --workspace client",
|
||||
"build": "npm run build --workspace client",
|
||||
"start": "npm run start --workspace server",
|
||||
"test": "npm test --workspace server"
|
||||
"test": "npm test --workspace server",
|
||||
"images": "node scripts/fetch-map-images.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.0"
|
||||
"concurrently": "^9.1.0",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
|
||||
@@ -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` : ''}.`);
|
||||
@@ -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": {
|
||||
"competitive": {
|
||||
"label": "Competitive 5 gegen 5",
|
||||
@@ -7,13 +7,13 @@
|
||||
"gameType": 0,
|
||||
"gameMode": 1,
|
||||
"maps": [
|
||||
{ "id": "de_ancient", "name": "Ancient", "color": "#3f8f5a" },
|
||||
{ "id": "de_anubis", "name": "Anubis", "color": "#c79a3d" },
|
||||
{ "id": "de_dust2", "name": "Dust II", "color": "#d3a45c" },
|
||||
{ "id": "de_inferno", "name": "Inferno", "color": "#c4573a" },
|
||||
{ "id": "de_mirage", "name": "Mirage", "color": "#b98a4a" },
|
||||
{ "id": "de_nuke", "name": "Nuke", "color": "#4a8fbf" },
|
||||
{ "id": "de_train", "name": "Train", "color": "#7a7f8a" }
|
||||
{ "id": "de_ancient", "name": "Ancient", "color": "#3f8f5a", "image": "/maps/de_ancient.webp" },
|
||||
{ "id": "de_anubis", "name": "Anubis", "color": "#c79a3d", "image": "/maps/de_anubis.webp" },
|
||||
{ "id": "de_dust2", "name": "Dust II", "color": "#d3a45c", "image": "/maps/de_dust2.webp" },
|
||||
{ "id": "de_inferno", "name": "Inferno", "color": "#c4573a", "image": "/maps/de_inferno.webp" },
|
||||
{ "id": "de_mirage", "name": "Mirage", "color": "#b98a4a", "image": "/maps/de_mirage.webp" },
|
||||
{ "id": "de_nuke", "name": "Nuke", "color": "#4a8fbf", "image": "/maps/de_nuke.webp" },
|
||||
{ "id": "de_train", "name": "Train", "color": "#7a7f8a", "image": "/maps/de_train.webp" }
|
||||
],
|
||||
"flows": {
|
||||
"bo1": ["ban:X", "ban:Y", "ban:X", "ban:Y", "ban:X", "ban:Y", "decider", "side:X"],
|
||||
@@ -27,10 +27,10 @@
|
||||
"gameType": 0,
|
||||
"gameMode": 2,
|
||||
"maps": [
|
||||
{ "id": "de_inferno", "name": "Inferno", "color": "#c4573a" },
|
||||
{ "id": "de_nuke", "name": "Nuke", "color": "#4a8fbf" },
|
||||
{ "id": "de_overpass", "name": "Overpass", "color": "#5f8f7a" },
|
||||
{ "id": "de_vertigo", "name": "Vertigo", "color": "#8a74b8" }
|
||||
{ "id": "de_inferno", "name": "Inferno", "color": "#c4573a", "image": "/maps/de_inferno.webp" },
|
||||
{ "id": "de_nuke", "name": "Nuke", "color": "#4a8fbf", "image": "/maps/de_nuke.webp" },
|
||||
{ "id": "de_overpass", "name": "Overpass", "color": "#5f8f7a", "image": "/maps/de_overpass.webp" },
|
||||
{ "id": "de_vertigo", "name": "Vertigo", "color": "#8a74b8", "image": "/maps/de_vertigo.webp" }
|
||||
],
|
||||
"flows": {
|
||||
"bo1": ["ban:X", "ban:Y", "ban:X", "decider", "side:Y"],
|
||||
|
||||
@@ -17,6 +17,9 @@ export function buildMapPools(raw) {
|
||||
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 (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);
|
||||
}
|
||||
const formats = {};
|
||||
|
||||