This commit is contained in:
StrangeD0s
2026-02-07 20:11:26 +01:00
parent 817cdc7906
commit 81a53e441e
15 changed files with 1069 additions and 86 deletions
+230
View File
@@ -0,0 +1,230 @@
"use client";
import React from "react";
import { Planet, Starship } from "@/types/swapi";
import { t } from "@/content/locales";
interface ActionPanelProps {
planet?: Planet | null;
enemy?: Starship | null;
onAction: () => void;
onEscape?: () => void;
isLoading?: boolean;
}
const getPlanetTheme = (terrain: string = "", climate: string = "") => {
const terr = terrain.toLowerCase();
const clim = climate.toLowerCase();
if (terr.includes("ocean") || terr.includes("water"))
return {
color: "text-blue-400",
border: "border-blue-500",
glow: "shadow-[0_0_20px_#3b82f6]",
label: t.ACTION_PANEL.MODES.AQUATIC,
};
if (terr.includes("forest") || terr.includes("jungle"))
return {
color: "text-green-400",
border: "border-green-500",
glow: "shadow-[0_0_20px_#22c55e]",
label: t.ACTION_PANEL.MODES.VERDANT,
};
if (terr.includes("desert") || clim.includes("arid"))
return {
color: "text-orange-400",
border: "border-orange-500",
glow: "shadow-[0_0_20px_#f97316]",
label: t.ACTION_PANEL.MODES.ARID,
};
if (
terr.includes("ice") ||
terr.includes("glacier") ||
clim.includes("frozen")
)
return {
color: "text-cyan-200",
border: "border-cyan-300",
glow: "shadow-[0_0_20px_#a5f3fc]",
label: t.ACTION_PANEL.MODES.CRYO,
};
if (terr.includes("city") || terr.includes("urban"))
return {
color: "text-purple-400",
border: "border-purple-500",
glow: "shadow-[0_0_20px_#a855f7]",
label: t.ACTION_PANEL.MODES.ECUMENOPOLIS,
};
return {
color: "text-yellow-500",
border: "border-yellow-500",
glow: "shadow-[0_0_20px_#eab308]",
label: t.ACTION_PANEL.MODES.STANDARD,
};
};
export default function ActionPanel({
planet,
enemy,
onAction,
onEscape,
isLoading,
}: ActionPanelProps) {
const isCombat = !!enemy;
const theme =
!isCombat && planet
? getPlanetTheme(planet.terrain, planet.climate)
: {
color: "text-red-500",
border: "border-red-600",
glow: "shadow-[0_0_30px_#dc2626]",
label: t.ACTION_PANEL.MODES.HOSTILE,
};
const displayName = isCombat ? enemy.name : planet?.name;
const displaySubtitle = isCombat
? `${t.ACTION_PANEL.LABELS.CLASS}: ${enemy.starship_class} // ${t.ACTION_PANEL.LABELS.CREW}: ${enemy.crew}`
: `${t.ACTION_PANEL.LABELS.SECTOR}: ${planet?.climate} // ${planet?.terrain}`;
return (
<div
className={`w-full max-w-4xl mt-6 bg-black/90 backdrop-blur-md border-2 rounded-lg p-6 shadow-2xl animate-in fade-in slide-in-from-bottom-4 relative overflow-hidden transition-colors duration-500 ${theme.border}`}
>
<div
className={`absolute inset-0 bg-[linear-gradient(transparent_0%,${isCombat ? "rgba(220,38,38,0.1)" : "rgba(234,179,8,0.05)"}_50%,transparent_100%)] bg-[length:100%_4px] animate-pulse pointer-events-none`}
/>
<div className="flex flex-col md:flex-row gap-8 items-center relative z-10">
<div className="relative shrink-0">
<div
className={`w-32 h-32 rounded-full border-4 ${theme.border} ${theme.glow} flex items-center justify-center transition-all duration-700 bg-black overflow-hidden relative`}
>
{isCombat ? (
<div className="text-red-600 animate-pulse font-black text-5xl drop-shadow-[0_0_10px_rgba(220,38,38,0.8)]">
!
</div>
) : (
<>
<div
className="w-full h-full transition-colors duration-1000"
style={{
background: `radial-gradient(circle at 30% 30%, ${theme.color.replace("text-", "")}, #000)`,
backgroundColor: theme.color.includes("blue")
? "#60a5fa"
: theme.color.includes("green")
? "#4ade80"
: theme.color.includes("orange")
? "#fb923c"
: theme.color.includes("cyan")
? "#22d3ee"
: theme.color.includes("purple")
? "#c084fc"
: "#eab308",
}}
/>
<div className="absolute inset-0 bg-gradient-to-tr from-black/60 via-transparent to-white/20 pointer-events-none" />
<div className="absolute inset-0 opacity-20 bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')] animate-spin-slow mix-blend-overlay" />
</>
)}
</div>
<div
className={`absolute -top-2 -right-2 bg-black border px-2 py-0.5 text-[8px] uppercase tracking-widest ${isCombat ? "border-red-600 text-red-600" : "border-yellow-500/50 text-yellow-500"}`}
>
{theme.label}
</div>
</div>
<div className="flex-1 text-center md:text-left space-y-3">
<div>
<h3
className={`text-3xl font-black tracking-widest uppercase ${isCombat ? "text-red-600" : "text-white"}`}
>
{displayName}
</h3>
<p
className={`${theme.color} text-xs font-bold uppercase tracking-widest opacity-80`}
>
{displaySubtitle}
</p>
</div>
<div
className={`grid grid-cols-2 gap-4 border-t border-b py-3 ${isCombat ? "border-red-900/30" : "border-yellow-500/10"}`}
>
<div className="text-[10px] uppercase">
<span
className={isCombat ? "text-red-500/50" : "text-yellow-500/50"}
>
{isCombat
? t.ACTION_PANEL.LABELS.MANUFACTURER
: t.ACTION_PANEL.LABELS.POPULATION}
</span>
<p className="text-white font-mono truncate">
{isCombat
? enemy.manufacturer
: planet?.population.toLocaleString()}
</p>
</div>
<div className="text-[10px] uppercase">
<span
className={isCombat ? "text-red-500/50" : "text-yellow-500/50"}
>
{isCombat
? t.ACTION_PANEL.LABELS.COST
: t.ACTION_PANEL.LABELS.GRAVITY}
</span>
<p className="text-white font-mono">
{isCombat ? `${enemy.cost_in_credits} Cr` : planet?.gravity}
</p>
</div>
</div>
<p
className={`text-xs italic leading-relaxed ${isCombat ? "text-red-400/60" : "text-yellow-500/60"}`}
>
{isCombat
? t.ACTION_PANEL.STATUS.COMBAT_WARNING
: t.ACTION_PANEL.STATUS.PLANET_SCAN}
</p>
</div>
<div className="flex flex-col gap-3 shrink-0 w-full md:w-auto">
<button
onClick={onAction}
disabled={isLoading}
className={`w-full md:w-auto px-10 py-5 border-2 font-black uppercase tracking-[0.3em] transition-all relative group
${
isCombat
? "border-red-600 text-red-600 hover:bg-red-600 hover:text-white shadow-[0_0_15px_rgba(220,38,38,0.3)]"
: "border-yellow-500 text-yellow-500 hover:bg-yellow-500 hover:text-black hover:shadow-[0_0_30px_#eab308]"
} ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`}
>
{isLoading ? (
<span className="flex items-center gap-2">
<span
className={`animate-ping inline-flex h-2 w-2 rounded-full ${isCombat ? "bg-red-600" : "bg-yellow-500"}`}
></span>
{t.ACTION_PANEL.STATUS.ENGAGING}
</span>
) : isCombat ? (
t.ACTION_PANEL.BUTTONS.FIRE
) : (
t.ACTION_PANEL.BUTTONS.JUMP
)}
</button>
{isCombat && (
<button
onClick={onEscape}
className="px-10 py-2 border border-white/20 text-white/40 text-[10px] uppercase tracking-widest hover:bg-white/5 hover:text-white transition-all"
>
{t.ACTION_PANEL.BUTTONS.ESCAPE}
</button>
)}
</div>
</div>
</div>
);
}
+76
View File
@@ -0,0 +1,76 @@
"use client";
import React, { useEffect, useRef } from "react";
import { t } from "@/content/locales"; // Import der Locales
export interface LogEntry {
id: string;
message: string;
type: "info" | "success" | "danger" | "warning";
timestamp: string;
}
interface EventLogProps {
logs: LogEntry[];
}
export default function EventLog({ logs }: EventLogProps) {
const scrollRef = useRef<HTMLDivElement>(null);
// Auto-Scroll nach unten bei neuen Einträgen
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [logs]);
const getTypeStyles = (type: LogEntry["type"]) => {
switch (type) {
case "success":
return "text-green-400";
case "danger":
return "text-red-500 font-bold animate-pulse";
case "warning":
return "text-orange-400";
default:
return "text-yellow-500/80";
}
};
return (
<div className="fixed bottom-6 right-6 w-full max-w-md h-48 bg-black/80 border border-yellow-500/30 rounded-lg shadow-2xl overflow-hidden flex flex-col backdrop-blur-md z-40">
{/* Header */}
<div className="bg-yellow-500/10 px-4 py-1 border-b border-yellow-500/20 flex justify-between items-center">
<span className="text-[10px] font-mono text-yellow-500 uppercase tracking-widest">
{t.EVENT_LOG.TITLE}
</span>
</div>
{/* Scrollable Content */}
<div
ref={scrollRef}
className="flex-1 overflow-y-auto p-4 font-mono text-sm scrollbar-thin scrollbar-thumb-yellow-500/20"
>
{logs.length === 0 && (
<p className="text-yellow-500/20 italic text-xs">
{t.EVENT_LOG.EMPTY_STATE}
</p>
)}
{logs.map((log) => (
<div
key={log.id}
className="mb-2 flex gap-3 border-l border-yellow-500/10 pl-2"
>
<span className="text-[10px] opacity-80 mt-1 shrink-0 text-white">
{log.timestamp}
</span>
<span className={`${getTypeStyles(log.type)} leading-tight`}>
{log.type === "danger" && "⚠ "}
{log.message}
</span>
</div>
))}
</div>
</div>
);
}
+82
View File
@@ -0,0 +1,82 @@
"use client";
import React from "react";
import { Planet } from "@/types/swapi";
import { t } from "@/content/locales"; // Import der Locales
interface GalaxyMapProps {
planets: Planet[];
onSelectLocation: (planet: Planet) => void;
currentLocationId?: string;
}
export default function GalaxyMap({
planets,
onSelectLocation,
currentLocationId,
}: GalaxyMapProps) {
return (
<div className="relative w-full max-w-4xl h-[500px] bg-[#080808] border-2 border-yellow-500/20 rounded-xl overflow-hidden shadow-[inset_0_0_50px_rgba(0,0,0,1)] z-10">
{/* Hintergrund-Raster (Grid) */}
<div
className="absolute inset-0 opacity-10 pointer-events-none"
style={{
backgroundImage:
"linear-gradient(#eab308 1px, transparent 1px), linear-gradient(90deg, #eab308 1px, transparent 1px)",
backgroundSize: "40px 40px",
}}
/>
{/* Die Planeten (Nodes) aus der API */}
{planets.map((planet) => {
const isCurrent = planet.id === currentLocationId;
return (
<button
key={planet.id}
onClick={() => !isCurrent && onSelectLocation(planet)}
disabled={isCurrent}
className={`absolute group transform -translate-x-1/2 -translate-y-1/2 transition-all
${isCurrent ? "cursor-default" : "cursor-pointer hover:scale-110"}`}
style={{ left: `${planet.x}%`, top: `${planet.y}%` }}
>
{/* Planet Visuell */}
<div
className={`w-4 h-4 rounded-full border-2 transition-all duration-300
${
isCurrent
? "bg-blue-500 border-white shadow-[0_0_15px_#3b82f6] scale-125"
: "bg-yellow-500/20 border-yellow-500 group-hover:bg-yellow-500 group-hover:shadow-[0_0_10px_#eab308]"
}`}
/>
{/* Label */}
<div className="absolute top-6 left-1/2 -translate-x-1/2 whitespace-nowrap">
<span
className={`text-[10px] font-mono uppercase tracking-tighter px-1 rounded bg-black/50
${isCurrent ? "text-blue-400 font-bold" : "text-yellow-500/60 group-hover:text-yellow-500"}`}
>
{planet.name}
</span>
</div>
{/* Pulsierender Ring für aktuellen Standort */}
{isCurrent && (
<div className="absolute -inset-2 border border-blue-500/50 rounded-full animate-ping pointer-events-none" />
)}
</button>
);
})}
{/* Scan-Linie Effekt */}
<div className="absolute top-0 left-0 w-full h-1 bg-yellow-500/5 opacity-20 animate-scan pointer-events-none" />
{/* Info Overlay */}
<div className="absolute bottom-4 left-4 text-[10px] text-yellow-500/40 font-mono uppercase pointer-events-none bg-black/40 p-2 rounded">
{t.GALAXY_MAP.SCAN_STATUS} <br />
{t.GALAXY_MAP.OBJECTS_DETECTED(planets.length)} <br />
{t.GALAXY_MAP.HOLONET_STATUS}
</div>
</div>
);
}
+41
View File
@@ -0,0 +1,41 @@
"use client";
import React from "react";
import { t } from "@/content/locales";
interface GameOverScreenProps {
credits: number;
onRestart: () => void;
}
export default function GameOverScreen({
credits,
onRestart,
}: GameOverScreenProps) {
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 backdrop-blur-xl animate-in fade-in duration-1000">
<div className="text-center space-y-6 p-12 border-2 border-red-600 bg-red-950/20 rounded-lg shadow-[0_0_50px_rgba(220,38,38,0.5)]">
<h1 className="text-6xl font-black text-red-600 tracking-tighter uppercase italic">
{t.GAME_OVER.TITLE}
</h1>
<p className="text-gray-400 font-mono">{t.GAME_OVER.SUBTITLE}</p>
<div className="py-4">
<p className="text-sm text-gray-500 uppercase tracking-widest">
{t.GAME_OVER.SCORE_LABEL}
</p>
<p className="text-3xl font-bold text-yellow-500">
{credits.toLocaleString()} Credits
</p>
</div>
<button
onClick={onRestart}
className="px-8 py-4 bg-red-600 hover:bg-red-700 text-white font-bold uppercase tracking-widest transition-all transform hover:scale-105 active:scale-95 shadow-[0_0_20px_rgba(220,38,38,0.4)]"
>
{t.GAME_OVER.RESTART_BUTTON}
</button>
</div>
</div>
);
}
+76
View File
@@ -0,0 +1,76 @@
"use client";
import React from "react";
import { t } from "@/content/locales";
interface HUDProps {
credits: number;
hp: number;
maxHp: number;
location: string;
targetCredits: number;
}
export default function HUD({
credits,
hp,
maxHp,
location,
targetCredits,
}: HUDProps) {
const progress = Math.min((credits / targetCredits) * 100, 100);
return (
<header className="fixed top-0 left-0 w-full bg-black border-b-2 border-yellow-500/50 p-4 font-mono text-yellow-500 z-50 shadow-[0_0_15px_rgba(234,179,8,0.2)]">
<div className="max-w-7xl mx-auto flex flex-wrap justify-between items-center gap-4">
{/* LORE & LOCATION */}
<div className="flex flex-col">
<span className="text-xs uppercase opacity-70">
{t.HUD.SECTOR_LABEL}
</span>
<span className="text-xl font-bold tracking-widest">
{location.toUpperCase()}
</span>
</div>
{/* SHIP STATUS (HEALTH) */}
<div className="flex flex-col min-w-[200px]">
<div className="flex justify-between text-xs uppercase mb-1">
<span>{t.HUD.HULL_LABEL}</span>
<span className={hp <= 3 ? "text-red-500 animate-pulse" : ""}>
{hp} / {maxHp}
</span>
</div>
<div className="w-full h-3 bg-gray-900 border border-yellow-500/30">
<div
className={`h-full transition-all duration-500 ${hp <= 3 ? "bg-red-600" : "bg-yellow-500"}`}
style={{ width: `${(hp / maxHp) * 100}%` }}
/>
</div>
</div>
{/* ECONOMY (CREDITS) */}
<div className="flex flex-col items-end">
<span className="text-xs uppercase opacity-70">
{t.HUD.GOAL_LABEL}
</span>
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold text-green-400">
{credits.toLocaleString()}
</span>
<span className="text-xs opacity-50">
/ {targetCredits.toLocaleString()}
</span>
</div>
{/* Progress Bar for the Goal */}
<div className="w-32 h-1 bg-gray-800 mt-1">
<div
className="h-full bg-green-500 shadow-[0_0_5px_#22c55e]"
style={{ width: `${progress}%` }}
/>
</div>
</div>
</div>
</header>
);
}
+75
View File
@@ -0,0 +1,75 @@
"use client";
import React from "react";
import { t } from "@/content/locales";
interface StartScreenProps {
onStart: () => void;
}
export default function StartScreen({ onStart }: StartScreenProps) {
return (
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-[#050505] overflow-hidden">
{/* Hintergrund-Deko */}
<div className="absolute inset-0 opacity-20 bg-[radial-gradient(circle_at_center,_#eab308_0%,_transparent_70%)]" />
<div className="max-w-2xl w-full mx-4 relative z-10 text-center space-y-8 p-10 border border-yellow-500/20 bg-black/60 backdrop-blur-md rounded-2xl shadow-[0_0_100px_rgba(234,179,8,0.1)]">
<div className="space-y-2">
<h1 className="font-starjedi text-6xl text-white">
{t.UI.GAME_TITLE}
</h1>
<p className="text-yellow-500/60 font-mono tracking-[0.3em] uppercase text-xs">
{t.START_SCREEN.SUBTITLE}
</p>
</div>
<div className="space-y-4 text-gray-400 font-light leading-relaxed">
<p>
{t.START_SCREEN.INTRO_TEXT}
<span className="text-white font-medium">
{" "}
{t.START_SCREEN.MISSION_HIGHLIGHT}
</span>
</p>
<ul className="text-sm space-y-2 inline-block text-left border-l-2 border-yellow-500/30 pl-6">
<li>
{" "}
<strong className="text-yellow-500/80">
{t.START_SCREEN.FEATURE_NAV_TITLE}
</strong>{" "}
{t.START_SCREEN.FEATURE_NAV_DESC}
</li>
<li>
{" "}
<strong className="text-yellow-500/80">
{t.START_SCREEN.FEATURE_RISK_TITLE}
</strong>{" "}
{t.START_SCREEN.FEATURE_RISK_DESC}
</li>
<li>
{" "}
<strong className="text-yellow-500/80">
{t.START_SCREEN.FEATURE_COMBAT_TITLE}
</strong>{" "}
{t.START_SCREEN.FEATURE_COMBAT_DESC}
</li>
</ul>
</div>
<div className="pt-6">
<button
onClick={onStart}
className="group relative px-12 py-5 bg-yellow-500 text-black font-black uppercase tracking-[0.2em] transition-all hover:scale-105 hover:shadow-[0_0_40px_rgba(234,179,8,0.4)] overflow-hidden"
>
<span className="relative z-10">{t.UI.START_BUTTON}</span>
<div className="absolute inset-0 bg-white opacity-0 group-hover:opacity-20 transition-opacity" />
</button>
<p className="mt-4 text-[10px] text-gray-600 uppercase tracking-widest">
{t.START_SCREEN.FOOTER_ENGINE}
</p>
</div>
</div>
</div>
);
}