Cyberpunk visual overhaul

Dark CARTO basemap (was default bright OSM tiles), Orbitron/Share Tech
Mono fonts, neon cyan/magenta HUD chrome with glassmorphism panels,
glowing divIcon portal markers (pulsing when claimed) replacing plain
circle dots, neon drop-shadow filters on links/fields, and a subtle
scanline overlay. Login and team-pick screens restyled to match.
This commit is contained in:
2026-08-05 14:14:38 +00:00
parent cd3d82d1c1
commit df9a4ae74b
7 changed files with 237 additions and 84 deletions
+95 -55
View File
@@ -26,13 +26,25 @@ const linkSourcePlace = computed<Place | null>(
() => game.places.find((p) => p.id === linkSourceId.value) ?? null,
);
const TEAM_COLOR: Record<string, string> = { orange: '#f97316', green: '#22c55e' };
const NEUTRAL_COLOR = '#9ca3af';
const TEAM_COLOR: Record<string, string> = { orange: '#ff5f1f', green: '#39ff14' };
const NEUTRAL_COLOR = '#3fb8c9';
function colorFor(team: string | null): string {
return team ? TEAM_COLOR[team] : NEUTRAL_COLOR;
}
function portalIcon(place: Place, isSource: boolean): L.DivIcon {
const color = isSource ? '#ffffff' : colorFor(place.claim_team);
const size = isSource ? 22 : place.claim_team ? 18 : 14;
const claimedClass = place.claim_team ? 'claimed' : '';
return L.divIcon({
className: `regress-portal-marker ${claimedClass}`,
html: `<div style="--portal-glow:${color}; width:${size}px; height:${size}px; border-radius:50%; background:radial-gradient(circle at 35% 35%, ${color}, ${color}33 70%); border:2px solid ${color};"></div>`,
iconSize: [size, size],
iconAnchor: [size / 2, size / 2],
});
}
function redraw() {
if (!map || !markersLayer || !linksLayer || !fieldsLayer) return;
markersLayer.clearLayers();
@@ -43,13 +55,7 @@ function redraw() {
for (const place of game.places) {
const isSource = place.id === linkSourceId.value;
const marker = L.circleMarker([place.lat, place.lon], {
radius: isSource ? 10 : 7,
color: isSource ? '#ffffff' : colorFor(place.claim_team),
weight: isSource ? 3 : 2,
fillColor: colorFor(place.claim_team),
fillOpacity: 0.85,
});
const marker = L.marker([place.lat, place.lon], { icon: portalIcon(place, isSource) });
marker.on('click', () => onMarkerClick(place));
marker.bindTooltip(place.name || `Place ${place.id}`, { direction: 'top' });
markersLayer.addLayer(marker);
@@ -65,7 +71,12 @@ function redraw() {
[from.lat, from.lon],
[to.lat, to.lon],
],
{ color: TEAM_COLOR[link.team], weight: 2, opacity: 0.8 },
{
color: TEAM_COLOR[link.team],
weight: 2,
opacity: 0.9,
className: link.team === 'orange' ? 'regress-link-orange' : 'regress-link-green',
},
),
);
}
@@ -77,7 +88,13 @@ function redraw() {
.map((p) => [p.lat, p.lon] as [number, number]);
if (points.length !== 3) continue;
fieldsLayer.addLayer(
L.polygon(points, { color: TEAM_COLOR[field.team], weight: 1, fillOpacity: 0.15, stroke: false }),
L.polygon(points, {
color: TEAM_COLOR[field.team],
weight: 1,
fillOpacity: 0.18,
stroke: false,
className: field.team === 'orange' ? 'regress-field-orange' : 'regress-field-green',
}),
);
}
}
@@ -139,9 +156,11 @@ const greenScore = computed(() => game.scores.find((s) => s.team === 'green'));
onMounted(async () => {
if (mapEl.value) {
map = L.map(mapEl.value).setView([32.7607, -16.9595], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
map = L.map(mapEl.value, { zoomControl: false }).setView([32.7607, -16.9595], 12);
L.control.zoom({ position: 'bottomright' }).addTo(map);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; OpenStreetMap contributors &copy; CARTO',
subdomains: 'abcd',
maxZoom: 19,
}).addTo(map);
fieldsLayer = L.layerGroup().addTo(map);
@@ -157,29 +176,41 @@ watch(linkSourceId, redraw);
</script>
<template>
<div class="flex h-full flex-col">
<header class="flex items-center justify-between gap-4 bg-neutral-900 px-4 py-2 text-sm">
<div class="flex items-center gap-4">
<span class="font-bold">Regress</span>
<span class="flex items-center gap-1 text-orange-500">
{{ orangeScore?.claimedPlaces ?? 0 }} places · {{ orangeScore?.fields ?? 0 }} fields ·
<div class="flex h-full flex-col bg-void">
<header class="hud-panel z-[1000] m-2 flex items-center justify-between gap-4 px-4 py-2 text-sm">
<div class="flex items-center gap-5">
<span class="font-display text-lg font-black tracking-widest text-cyan-neon text-glow-cyan">REGRESS</span>
<span class="flex items-center gap-1 font-bold text-orange-neon text-glow-orange">
{{ orangeScore?.claimedPlaces ?? 0 }} <span class="text-orange-neon/60">nodes</span> ·
{{ orangeScore?.fields ?? 0 }} <span class="text-orange-neon/60">fields</span> ·
{{ (orangeScore?.areaKm2 ?? 0).toFixed(2) }} km²
</span>
<span class="flex items-center gap-1 text-green-500">
{{ greenScore?.claimedPlaces ?? 0 }} places · {{ greenScore?.fields ?? 0 }} fields ·
<span class="flex items-center gap-1 font-bold text-green-neon text-glow-green">
{{ greenScore?.claimedPlaces ?? 0 }} <span class="text-green-neon/60">nodes</span> ·
{{ greenScore?.fields ?? 0 }} <span class="text-green-neon/60">fields</span> ·
{{ (greenScore?.areaKm2 ?? 0).toFixed(2) }} km²
</span>
</div>
<div class="flex items-center gap-3">
<span class="text-neutral-400">
{{ auth.displayName ?? auth.pubkey?.slice(0, 8) }} ·
<span :class="auth.team === 'orange' ? 'text-orange-500' : 'text-green-500'">{{ auth.team }}</span>
<span class="text-cyan-neon/70">
{{ auth.displayName ?? auth.pubkey?.slice(0, 8) }}//<span
:class="auth.team === 'orange' ? 'text-orange-neon text-glow-orange' : 'text-green-neon text-glow-green'"
class="font-bold uppercase"
>{{ auth.team }}</span
>
</span>
<button class="rounded bg-neutral-800 px-3 py-1 hover:bg-neutral-700" :disabled="actionBusy" @click="doSync">
Sync from BTC Map
<button
class="rounded border border-cyan-neon/50 bg-black/40 px-3 py-1 text-cyan-neon transition hover:border-cyan-neon hover:shadow-glow-cyan"
:disabled="actionBusy"
@click="doSync"
>
Sync BTC Map
</button>
<button class="rounded bg-neutral-800 px-3 py-1 hover:bg-neutral-700" @click="auth.logout()">
Log out
<button
class="rounded border border-magenta-neon/50 bg-black/40 px-3 py-1 text-magenta-neon transition hover:border-magenta-neon hover:shadow-glow-magenta"
@click="auth.logout()"
>
Disconnect
</button>
</div>
</header>
@@ -187,35 +218,40 @@ watch(linkSourceId, redraw);
<div class="relative flex-1">
<div ref="mapEl" class="h-full w-full"></div>
<div
v-if="linkSourcePlace"
class="absolute left-4 top-4 z-[1000] rounded-lg bg-neutral-900/95 px-4 py-2 text-sm shadow-lg"
>
Linking from <strong>{{ linkSourcePlace.name }}</strong> click a friendly place to complete, or
<button class="ml-2 text-neutral-400 underline" @click="cancelLink">cancel</button>
<div v-if="linkSourcePlace" class="hud-panel absolute left-4 top-4 z-[1000] px-4 py-2 text-sm text-cyan-neon">
LINKING FROM <strong class="text-glow-cyan">{{ linkSourcePlace.name }}</strong> select a friendly node, or
<button class="ml-2 text-magenta-neon underline" @click="cancelLink">abort</button>
</div>
<div
v-if="selectedPlace"
class="absolute right-4 top-4 z-[1000] w-72 rounded-lg bg-neutral-900/95 p-4 text-sm shadow-lg"
>
<div v-if="selectedPlace" class="hud-panel absolute right-4 top-4 z-[1000] w-72 p-4 text-sm">
<div class="mb-1 flex items-start justify-between gap-2">
<h2 class="font-bold">{{ selectedPlace.name || `Place ${selectedPlace.id}` }}</h2>
<button class="text-neutral-500" @click="selectedPlaceId = null"></button>
<h2 class="font-display font-bold text-cyan-neon text-glow-cyan">
{{ selectedPlace.name || `Node ${selectedPlace.id}` }}
</h2>
<button class="text-cyan-neon/50 hover:text-cyan-neon" @click="selectedPlaceId = null"></button>
</div>
<p v-if="selectedPlace.address" class="mb-2 text-neutral-400">{{ selectedPlace.address }}</p>
<p v-if="selectedPlace.address" class="mb-2 text-cyan-neon/50">{{ selectedPlace.address }}</p>
<p class="mb-3">
Status:
<span v-if="!selectedPlace.claim_team" class="text-neutral-400">unclaimed</span>
<span v-else :class="selectedPlace.claim_team === 'orange' ? 'text-orange-500' : 'text-green-500'">
claimed by {{ selectedPlace.claim_team }}
STATUS:
<span v-if="!selectedPlace.claim_team" class="text-cyan-neon/60">UNCLAIMED</span>
<span
v-else
class="font-bold uppercase"
:class="selectedPlace.claim_team === 'orange' ? 'text-orange-neon text-glow-orange' : 'text-green-neon text-glow-green'"
>
held by {{ selectedPlace.claim_team }}
</span>
</p>
<div class="flex flex-col gap-2">
<button
v-if="selectedPlace.claim_team !== auth.team"
class="rounded bg-white px-3 py-2 font-semibold text-black hover:bg-neutral-200 disabled:opacity-50"
class="rounded border px-3 py-2 font-bold uppercase transition disabled:opacity-50"
:class="
auth.team === 'orange'
? 'border-orange-neon text-orange-neon hover:shadow-glow-orange'
: 'border-green-neon text-green-neon hover:shadow-glow-green'
"
:disabled="actionBusy"
@click="doClaim(selectedPlace)"
>
@@ -223,7 +259,7 @@ watch(linkSourceId, redraw);
</button>
<button
v-else-if="linkSourceId !== selectedPlace.id"
class="rounded bg-neutral-800 px-3 py-2 hover:bg-neutral-700 disabled:opacity-50"
class="rounded border border-cyan-neon/50 px-3 py-2 text-cyan-neon transition hover:border-cyan-neon hover:shadow-glow-cyan disabled:opacity-50"
:disabled="actionBusy"
@click="startLink(selectedPlace)"
>
@@ -231,7 +267,7 @@ watch(linkSourceId, redraw);
</button>
<button
v-if="linkSourceId && linkSourceId !== selectedPlace.id && selectedPlace.claim_team === auth.team"
class="rounded bg-neutral-800 px-3 py-2 hover:bg-neutral-700 disabled:opacity-50"
class="rounded border border-cyan-neon/50 px-3 py-2 text-cyan-neon transition hover:border-cyan-neon hover:shadow-glow-cyan disabled:opacity-50"
:disabled="actionBusy"
@click="completeLink(selectedPlace)"
>
@@ -239,17 +275,21 @@ watch(linkSourceId, redraw);
</button>
</div>
<p v-if="actionError" class="mt-3 text-red-400">{{ actionError }}</p>
<p v-if="actionError" class="mt-3 text-magenta-neon">{{ actionError }}</p>
</div>
<div
v-if="!game.places.length && !game.loading"
class="absolute inset-0 z-[999] flex items-center justify-center bg-black/60"
class="absolute inset-0 z-[999] flex items-center justify-center bg-black/70"
>
<div class="rounded-lg bg-neutral-900 p-6 text-center">
<p class="mb-3">No places loaded yet pull the current Madeira dataset from BTC Map.</p>
<button class="rounded bg-white px-4 py-2 font-semibold text-black" :disabled="actionBusy" @click="doSync">
Sync from BTC Map
<div class="hud-panel p-6 text-center">
<p class="mb-3 text-cyan-neon">NO NODES DETECTED pull the live Madeira dataset from BTC Map.</p>
<button
class="rounded border border-cyan-neon px-4 py-2 font-bold text-cyan-neon hover:shadow-glow-cyan"
:disabled="actionBusy"
@click="doSync"
>
Sync from BTC Map
</button>
</div>
</div>