update and polish

This commit is contained in:
StrangeD0s
2026-02-08 17:06:59 +01:00
parent 81a53e441e
commit fcd8cadf43
18 changed files with 915 additions and 354 deletions
+2
View File
@@ -2,6 +2,8 @@
Smuggler's Run is a minimalist space adventure game, where you, the player, are an unlicensed space trader, flying cargo from planet to planet while trying not to be blown to pieces by bounty hunters, imperial forces or rebel do-gooders. Smuggler's Run is a minimalist space adventure game, where you, the player, are an unlicensed space trader, flying cargo from planet to planet while trying not to be blown to pieces by bounty hunters, imperial forces or rebel do-gooders.
This game is not meant to be played on a small mobile screen.
## Getting Started ## Getting Started
First, run the development server: First, run the development server:
+1 -1
View File
@@ -6,8 +6,8 @@
} }
@theme { @theme {
/* Hier registrierst du die Font für Tailwind v4 */
--font-starjedi: var(--font-starjedi); --font-starjedi: var(--font-starjedi);
--font-aurebesh: var(--font-aurebesh)
} }
@theme inline { @theme inline {
+11 -4
View File
@@ -1,6 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import localFont from "next/font/local"; // Wichtig: localFont importieren import localFont from "next/font/local";
import "./globals.css"; import "./globals.css";
const geistSans = Geist({ const geistSans = Geist({
@@ -13,14 +13,18 @@ const geistMono = Geist_Mono({
subsets: ["latin"], subsets: ["latin"],
}); });
// Konfiguration der lokalen Star Jedi Font
const starJedi = localFont({ const starJedi = localFont({
src: "../public/fonts/Starjedi.ttf", src: "../public/fonts/Starjedi.ttf",
variable: "--font-starjedi", variable: "--font-starjedi",
}); });
const aurebesh = localFont({
src: "../public/fonts/Aurebesh-English.ttf",
variable: "--font-aurebesh",
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Outer Rim Smuggler", title: "Smuggler's Run",
description: "A Star Wars inspired roguelike adventure", description: "A Star Wars inspired roguelike adventure",
}; };
@@ -30,7 +34,10 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en" className={`${starJedi.variable}`}> <html
lang="en"
className={`${starJedi.variable} ${aurebesh.variable} ${geistSans.variable} ${geistMono.variable}`}
>
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >
+40 -18
View File
@@ -7,7 +7,9 @@ import EventLog from "../components/EventLog";
import ActionPanel from "../components/ActionPanel"; import ActionPanel from "../components/ActionPanel";
import StartScreen from "@/components/StartScreen"; import StartScreen from "@/components/StartScreen";
import GameOverScreen from "@/components/GameOverScreen"; import GameOverScreen from "@/components/GameOverScreen";
import { t } from "@/content/locales"; // Import der Locales import VictoryScreen from "@/components/VictoryScreen";
import PowerDistributor from "@/components/PowerDistributor";
import { t } from "@/content/locales";
export default function Home() { export default function Home() {
const { const {
@@ -18,19 +20,25 @@ export default function Home() {
hp, hp,
loading, loading,
currentLocationId, currentLocationId,
currentPlanet,
selectedPlanet, selectedPlanet,
setSelectedPlanet, setSelectedPlanet,
handleAttack, handleAttack,
executeJump, executeJump,
handleRepair,
isJumping, isJumping,
power,
updatePower,
activeEnemy, activeEnemy,
executeEscape, executeEscape,
logs, logs,
isGameOver, isGameOver,
restartGame, restartGame,
isVictory,
isRerouting,
WIN_THRESHOLD,
} = useGameEngine(); } = useGameEngine();
// 1. Ladezustand
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-black flex items-center justify-center font-mono text-yellow-500"> <div className="min-h-screen bg-black flex items-center justify-center font-mono text-yellow-500">
@@ -39,46 +47,60 @@ export default function Home() {
); );
} }
// Aktuellen Planeten finden für das HUD if (!gameStarted) return <StartScreen onStart={startGame} />;
const currentPlanet = planets.find((p) => p.id === currentLocationId);
// 2. Startbildschirm const activeDisplayPlanet = selectedPlanet || currentPlanet;
if (!gameStarted) { const isViewingCurrentLocation =
return <StartScreen onStart={startGame} />; !!currentPlanet && activeDisplayPlanet?.id === currentPlanet.id;
}
return ( return (
<main className="min-h-screen bg-[#050505] pt-24 px-4 flex flex-col items-center"> <main className="min-h-screen bg-[#050505] pt-24 px-4 flex flex-col items-center pb-12">
<HUD <HUD
credits={credits} credits={credits}
hp={hp} hp={hp}
maxHp={10} // Könnte später aus einem Ship-Objekt kommen maxHp={10}
location={currentPlanet?.name || t.SYSTEM.DEEP_SPACE} location={currentPlanet?.name || t.SYSTEM.DEEP_SPACE}
targetCredits={100000} targetCredits={WIN_THRESHOLD}
/> />
<div className="w-full max-w-4xl flex flex-col items-center gap-6"> <div className="w-full max-w-6xl grid grid-cols-1 lg:grid-cols-4 gap-6">
<div className="lg:col-span-3">
<GalaxyMap <GalaxyMap
planets={planets} planets={planets}
onSelectLocation={(p) => setSelectedPlanet(p)}
currentLocationId={currentLocationId} currentLocationId={currentLocationId}
selectedPlanetId={selectedPlanet?.id}
onSelectLocation={setSelectedPlanet}
/> />
</div>
<div className="lg:col-span-1">
<PowerDistributor
power={power}
onUpdatePower={updatePower}
disabled={isJumping || isRerouting}
isRerouting={isRerouting}
/>
</div>
</div>
{/* ActionPanel erscheint bei Zielwahl oder Feindkontakt */} <div className="w-full max-w-4xl animate-in fade-in slide-in-from-bottom-4 duration-700">
{(selectedPlanet || activeEnemy) && (
<ActionPanel <ActionPanel
planet={selectedPlanet} planet={activeDisplayPlanet}
enemy={activeEnemy} enemy={activeEnemy}
isCurrentLocation={isViewingCurrentLocation}
isLoading={isJumping} isLoading={isJumping}
onAction={activeEnemy ? handleAttack : executeJump} onAction={activeEnemy ? handleAttack : executeJump}
onEscape={executeEscape} onEscape={executeEscape}
onRepair={handleRepair}
currentHp={hp}
/> />
)}
</div> </div>
<div className="w-full max-w-4xl mt-6">
<EventLog logs={logs} /> <EventLog logs={logs} />
</div>
{isVictory && <VictoryScreen credits={credits} onRestart={restartGame} />}
{/* Game Over Overlay */}
{isGameOver && ( {isGameOver && (
<GameOverScreen credits={credits} onRestart={restartGame} /> <GameOverScreen credits={credits} onRestart={restartGame} />
)} )}
+118 -139
View File
@@ -2,223 +2,202 @@
import React from "react"; import React from "react";
import { Planet, Starship } from "@/types/swapi"; import { Planet, Starship } from "@/types/swapi";
import { t } from "@/content/locales"; import { t, LocaleType } from "@/content/locales";
import { getPlanetTheme } from "@/utils/getPlanetTheme";
interface ActionPanelProps { interface ActionPanelProps {
planet?: Planet | null; planet?: Planet | null;
enemy?: Starship | null; enemy?: Starship | null;
onAction: () => void; onAction: () => void;
onEscape?: () => void; onEscape?: () => void;
onRepair?: () => void;
isLoading?: boolean; isLoading?: boolean;
isCurrentLocation?: boolean;
currentHp: number;
} }
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({ export default function ActionPanel({
planet, planet,
enemy, enemy,
onAction, onAction,
onEscape, onEscape,
onRepair,
isLoading, isLoading,
isCurrentLocation,
currentHp,
}: ActionPanelProps) { }: ActionPanelProps) {
const isCombat = !!enemy; const isCombat = !!enemy;
const theme =
!isCombat && planet const planetVisual = getPlanetTheme(
? getPlanetTheme(planet.terrain, planet.climate) planet?.terrain,
: { planet?.climate,
t as LocaleType,
);
const uiTheme = isCombat
? {
color: "text-red-500", color: "text-red-500",
border: "border-red-600", border: "border-red-600",
glow: "shadow-[0_0_30px_#dc2626]", glow: "shadow-[0_0_30px_#dc2626]",
label: t.ACTION_PANEL.MODES.HOSTILE, label: t.ACTION_PANEL.MODES.HOSTILE,
borderAlpha: "border-red-900/30",
labelAlpha: "text-red-500/50",
}
: {
color: "text-yellow-500",
border: "border-yellow-300",
glow: "shadow-[0_0_20px_rgba(234,179,8,0.2)]",
label: "NEUTRAL SYSTEM",
borderAlpha: "border-yellow-500/10",
labelAlpha: "text-yellow-500/50",
}; };
const displayName = isCombat ? enemy.name : planet?.name; const stats = isCombat
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}`; label: t.ACTION_PANEL.LABELS.MANUFACTURER,
value: enemy.manufacturer,
},
{
label: t.ACTION_PANEL.LABELS.COST,
value: `${enemy.cost_in_credits} Cr`,
},
]
: [
{
label: t.ACTION_PANEL.LABELS.POPULATION,
value: planet?.population.toLocaleString(),
},
{ label: t.ACTION_PANEL.LABELS.GRAVITY, value: planet?.gravity },
];
return ( return (
<div <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}`} className={`w-full max-w-4xl mt-6 bg-black/90 backdrop-blur-md border-2 rounded-lg p-6 shadow-2xl relative overflow-hidden transition-all duration-500 ${uiTheme.border}`}
> >
<div <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`} 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="flex flex-col md:flex-row gap-8 items-center relative z-10">
{/* VISUAL UNIT */}
<div className="relative shrink-0"> <div className="relative shrink-0">
<div <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`} className={`w-32 h-32 rounded-full flex items-center justify-center bg-black overflow-hidden relative shadow-inner transition-all duration-500 ${
isCombat
? `border-4 ${uiTheme.border} ${uiTheme.glow}`
: `border-1 ${planetVisual.border} shadow-[0_0_15px_${planetVisual.hex}44]`
}`}
> >
{isCombat ? ( {isCombat ? (
<div className="text-red-600 animate-pulse font-black text-5xl drop-shadow-[0_0_10px_rgba(220,38,38,0.8)]"> <div className="text-red-600 animate-pulse font-black text-6xl">
! !
</div> </div>
) : ( ) : (
<>
<div <div
className="w-full h-full transition-colors duration-1000" className="w-full h-full transition-all duration-1000"
style={{ style={{
background: `radial-gradient(circle at 30% 30%, ${theme.color.replace("text-", "")}, #000)`, background: `radial-gradient(circle at 30% 30%, ${planetVisual.hex}, #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>
<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"}`} className={`absolute -top-2 -right-2 bg-black border px-2 py-0.5 text-[8px] uppercase tracking-widest ${uiTheme.color} ${uiTheme.border}`}
> >
{theme.label} {isCurrentLocation && !isCombat
? "CURRENT LOCATION"
: uiTheme.label}
</div> </div>
</div> </div>
<div className="flex-1 text-center md:text-left space-y-3"> {/* INFO SECTION */}
<div className="flex-1 text-center md:text-left space-y-4">
<div> <div>
<div className="flex flex-col md:flex-row items-baseline gap-3">
<h3 <h3
className={`text-3xl font-black tracking-widest uppercase ${isCombat ? "text-red-600" : "text-white"}`} className={`text-3xl font-black tracking-widest uppercase ${isCombat ? "text-red-600" : "text-white"}`}
> >
{displayName} {isCombat ? enemy.name : planet?.name}
</h3> </h3>
{isCombat && (
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 border border-red-500 text-red-500 font-mono text-xs">
DS {enemy.ds}
</span>
<div className="flex gap-1 ml-2">
{[...Array(enemy.hp)].map((_, i) => (
<div
key={i}
className="w-4 h-2 bg-red-600 shadow-[0_0_8px_#dc2626] border border-red-400/50"
/>
))}
</div>
</div>
)}
</div>
<p <p
className={`${theme.color} text-xs font-bold uppercase tracking-widest opacity-80`} className={`${uiTheme.color} text-[10px] font-bold uppercase tracking-[0.2em] opacity-80 mt-1`}
> >
{displaySubtitle} {isCombat
? `${t.ACTION_PANEL.LABELS.CLASS}: ${enemy.starship_class}`
: `${t.ACTION_PANEL.LABELS.SECTOR}: ${planet?.terrain}`}
</p> </p>
</div> </div>
<div <div
className={`grid grid-cols-2 gap-4 border-t border-b py-3 ${isCombat ? "border-red-900/30" : "border-yellow-500/10"}`} className={`grid grid-cols-2 gap-4 border-t border-b py-3 ${uiTheme.borderAlpha}`}
> >
<div className="text-[10px] uppercase"> {stats.map((stat, idx) => (
<span <div key={idx} className="text-[10px] uppercase">
className={isCombat ? "text-red-500/50" : "text-yellow-500/50"} <span className={uiTheme.labelAlpha}>{stat.label}</span>
> <p className="text-white font-mono truncate">{stat.value}</p>
{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>
<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>
</div> </div>
<p {/* BUTTONS */}
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"> <div className="flex flex-col gap-3 shrink-0 w-full md:w-auto">
{isCurrentLocation && !isCombat ? (
<button
onClick={onRepair}
disabled={currentHp >= 10 || isLoading}
className={`w-full md:w-auto px-10 py-5 border-2 font-black uppercase tracking-[0.3em] transition-all
${
currentHp < 10
? "border-blue-500 text-blue-500 hover:bg-blue-500 hover:text-white shadow-[0_0_20px_rgba(59,130,246,0.3)]"
: "border-gray-600 text-gray-600 opacity-50 cursor-not-allowed"
}`}
>
{currentHp < 10
? t.ACTION_PANEL.BUTTONS.REPAIR((10 - currentHp) * 20)
: t.ACTION_PANEL.BUTTONS.HULL_INTACT}
</button>
) : (
<button <button
onClick={onAction} onClick={onAction}
disabled={isLoading} disabled={isLoading}
className={`w-full md:w-auto px-10 py-5 border-2 font-black uppercase tracking-[0.3em] transition-all relative group className={`w-full md:w-auto px-10 py-5 border-2 font-black uppercase tracking-[0.3em] transition-all
${ ${
isCombat 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-red-600 text-red-600 hover:bg-red-600 hover:text-white"
: "border-yellow-500 text-yellow-500 hover:bg-yellow-500 hover:text-black hover:shadow-[0_0_30px_#eab308]" : "border-yellow-500 text-yellow-500 hover:bg-yellow-500 hover:text-black"
} ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} }
${isLoading ? "opacity-50" : ""}`}
> >
{isLoading ? ( {isLoading
<span className="flex items-center gap-2"> ? t.ACTION_PANEL.STATUS.ENGAGING
<span : isCombat
className={`animate-ping inline-flex h-2 w-2 rounded-full ${isCombat ? "bg-red-600" : "bg-yellow-500"}`} ? t.ACTION_PANEL.BUTTONS.FIRE
></span> : t.ACTION_PANEL.BUTTONS.JUMP}
{t.ACTION_PANEL.STATUS.ENGAGING}
</span>
) : isCombat ? (
t.ACTION_PANEL.BUTTONS.FIRE
) : (
t.ACTION_PANEL.BUTTONS.JUMP
)}
</button> </button>
)}
{isCombat && ( {isCombat && (
<button <button
onClick={onEscape} 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" className="px-10 py-2 border border-white/40 text-white/80 text-[10px] uppercase tracking-widest hover:bg-white/5 hover:text-white transition-colors"
> >
{t.ACTION_PANEL.BUTTONS.ESCAPE} {t.ACTION_PANEL.BUTTONS.ESCAPE}
</button> </button>
-3
View File
@@ -17,7 +17,6 @@ interface EventLogProps {
export default function EventLog({ logs }: EventLogProps) { export default function EventLog({ logs }: EventLogProps) {
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
// Auto-Scroll nach unten bei neuen Einträgen
useEffect(() => { useEffect(() => {
if (scrollRef.current) { if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight; scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
@@ -39,14 +38,12 @@ export default function EventLog({ logs }: EventLogProps) {
return ( 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"> <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"> <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"> <span className="text-[10px] font-mono text-yellow-500 uppercase tracking-widest">
{t.EVENT_LOG.TITLE} {t.EVENT_LOG.TITLE}
</span> </span>
</div> </div>
{/* Scrollable Content */}
<div <div
ref={scrollRef} ref={scrollRef}
className="flex-1 overflow-y-auto p-4 font-mono text-sm scrollbar-thin scrollbar-thumb-yellow-500/20" className="flex-1 overflow-y-auto p-4 font-mono text-sm scrollbar-thin scrollbar-thumb-yellow-500/20"
+18 -14
View File
@@ -2,22 +2,24 @@
import React from "react"; import React from "react";
import { Planet } from "@/types/swapi"; import { Planet } from "@/types/swapi";
import { t } from "@/content/locales"; // Import der Locales import { t } from "@/content/locales";
interface GalaxyMapProps { interface GalaxyMapProps {
planets: Planet[]; planets: Planet[];
onSelectLocation: (planet: Planet) => void; onSelectLocation: (planet: Planet) => void;
currentLocationId?: string; currentLocationId?: string;
selectedPlanetId?: string;
} }
export default function GalaxyMap({ export default function GalaxyMap({
planets, planets,
onSelectLocation, onSelectLocation,
currentLocationId, currentLocationId,
selectedPlanetId,
}: GalaxyMapProps) { }: GalaxyMapProps) {
return ( 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"> <div className="relative w-full max-w-4xl h-[500px] bg-[#080808] border-2 border-yellow-300 rounded-xl overflow-hidden shadow-[inset_0_0_50px_rgba(0,0,0,1)] z-10">
{/* Hintergrund-Raster (Grid) */} {/* Background-Grid */}
<div <div
className="absolute inset-0 opacity-10 pointer-events-none" className="absolute inset-0 opacity-10 pointer-events-none"
style={{ style={{
@@ -27,40 +29,43 @@ export default function GalaxyMap({
}} }}
/> />
{/* Die Planeten (Nodes) aus der API */}
{planets.map((planet) => { {planets.map((planet) => {
const isCurrent = planet.id === currentLocationId; const isCurrent = planet.id === currentLocationId;
const isSelected = planet.id === selectedPlanetId;
return ( return (
<button <button
key={planet.id} key={planet.id}
onClick={() => !isCurrent && onSelectLocation(planet)} onClick={() => onSelectLocation(planet)}
disabled={isCurrent} className="absolute group transform -translate-x-1/2 -translate-y-1/2 transition-all cursor-pointer hover:scale-110"
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}%` }} style={{ left: `${planet.x}%`, top: `${planet.y}%` }}
> >
{/* Planet Visuell */}
<div <div
className={`w-4 h-4 rounded-full border-2 transition-all duration-300 className={`w-4 h-4 rounded-full border-2 transition-all duration-300
${ ${
isCurrent isCurrent
? "bg-blue-500 border-white shadow-[0_0_15px_#3b82f6] scale-125" ? "bg-blue-500 border-white shadow-[0_0_15px_#3b82f6] scale-125"
: isSelected
? "bg-yellow-500 border-yellow-500 shadow-[0_0_10px_#eab308]"
: "bg-yellow-500/20 border-yellow-500 group-hover:bg-yellow-500 group-hover:shadow-[0_0_10px_#eab308]" : "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"> <div className="absolute top-6 left-1/2 -translate-x-1/2 whitespace-nowrap">
<span <span
className={`text-[10px] font-mono uppercase tracking-tighter px-1 rounded bg-black/50 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"}`} ${
isCurrent
? "text-blue-400 font-bold"
: isSelected
? "text-yellow-500"
: "text-yellow-500/60 group-hover:text-yellow-500"
}`}
> >
{planet.name} {planet.name}
</span> </span>
</div> </div>
{/* Pulsierender Ring für aktuellen Standort */}
{isCurrent && ( {isCurrent && (
<div className="absolute -inset-2 border border-blue-500/50 rounded-full animate-ping pointer-events-none" /> <div className="absolute -inset-2 border border-blue-500/50 rounded-full animate-ping pointer-events-none" />
)} )}
@@ -68,11 +73,10 @@ export default function GalaxyMap({
); );
})} })}
{/* 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" /> <div className="absolute top-0 left-0 w-full h-1 bg-yellow-500/5 opacity-20 animate-scan pointer-events-none" />
{/* Info Overlay */} {/* 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"> <div className="absolute bottom-4 left-4 text-[10px] text-red-500 font-mono uppercase pointer-events-none bg-black/40 p-2 rounded">
{t.GALAXY_MAP.SCAN_STATUS} <br /> {t.GALAXY_MAP.SCAN_STATUS} <br />
{t.GALAXY_MAP.OBJECTS_DETECTED(planets.length)} <br /> {t.GALAXY_MAP.OBJECTS_DETECTED(planets.length)} <br />
{t.GALAXY_MAP.HOLONET_STATUS} {t.GALAXY_MAP.HOLONET_STATUS}
+33 -2
View File
@@ -14,8 +14,39 @@ export default function GameOverScreen({
}: GameOverScreenProps) { }: GameOverScreenProps) {
return ( 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="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)]"> {/* simple starfield background */}
<h1 className="text-6xl font-black text-red-600 tracking-tighter uppercase italic"> <div className="absolute inset-0 pointer-events-none">
<div
className="absolute inset-0 opacity-30"
style={{
backgroundImage: `
radial-gradient(1px 1px at 20px 30px, #fff, 100%, rgba(0,0,0,0)),
radial-gradient(1px 1px at 40px 70px, #fff, 100%, rgba(0,0,0,0)),
radial-gradient(1px 1px at 50px 160px, #ddd, 100%, rgba(0,0,0,0)),
radial-gradient(1px 1px at 90px 40px, #fff, 100%, rgba(0,0,0,0)),
radial-gradient(1px 1px at 130px 80px, #fff, 100%, rgba(0,0,0,0)),
radial-gradient(1px 1px at 160px 120px, #ddd, 100%, rgba(0,0,0,0))
`,
backgroundSize: "200px 200px",
backgroundRepeat: "repeat",
}}
/>
<div
className="absolute inset-0 opacity-50"
style={{
backgroundImage: `
radial-gradient(1.5px 1.5px at 100px 150px, #fff, 100%, rgba(0,0,0,0)),
radial-gradient(1.5px 1.5px at 300px 400px, #ddd, 100%, rgba(0,0,0,0)),
radial-gradient(1.5px 1.5px at 500px 100px, #fff, 100%, rgba(0,0,0,0))
`,
backgroundSize: "450px 450px",
backgroundRepeat: "repeat",
}}
/>
</div>
<div className="text-center space-y-6 p-12 border-2 border-red-600 bg-red-950/20 rounded-lg bg-black/80 backdrop-blur-sm shadow-[0_0_50px_rgba(220,38,38,0.5)]">
<h1 className="font-starjedi text-6xl text-red-600">
{t.GAME_OVER.TITLE} {t.GAME_OVER.TITLE}
</h1> </h1>
<p className="text-gray-400 font-mono">{t.GAME_OVER.SUBTITLE}</p> <p className="text-gray-400 font-mono">{t.GAME_OVER.SUBTITLE}</p>
+2 -2
View File
@@ -23,7 +23,7 @@ export default function HUD({
return ( 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)]"> <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"> <div className="max-w-7xl mx-auto flex flex-wrap justify-between items-center gap-4">
{/* LORE & LOCATION */} {/* LOCATION */}
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-xs uppercase opacity-70"> <span className="text-xs uppercase opacity-70">
{t.HUD.SECTOR_LABEL} {t.HUD.SECTOR_LABEL}
@@ -49,7 +49,7 @@ export default function HUD({
</div> </div>
</div> </div>
{/* ECONOMY (CREDITS) */} {/* CREDITS */}
<div className="flex flex-col items-end"> <div className="flex flex-col items-end">
<span className="text-xs uppercase opacity-70"> <span className="text-xs uppercase opacity-70">
{t.HUD.GOAL_LABEL} {t.HUD.GOAL_LABEL}
+136
View File
@@ -0,0 +1,136 @@
"use client";
import React from "react";
import { t } from "@/content/locales";
interface PowerSettings {
engines: number;
lasers: number;
shields: number;
}
interface PowerDistributorProps {
power: PowerSettings;
onUpdatePower: (newSettings: PowerSettings) => void;
disabled?: boolean;
isRerouting?: boolean;
}
export default function PowerDistributor({
power,
onUpdatePower,
disabled = false,
isRerouting = false,
}: PowerDistributorProps) {
const handleBoost = (system: keyof PowerSettings) => {
if (disabled || isRerouting) return;
const newSettings = { ...power };
if (power[system] <= 4) {
const otherSystems = (
Object.keys(power) as Array<keyof PowerSettings>
).filter((s) => s !== system);
if (power[otherSystems[0]] > 0 && power[otherSystems[1]] > 0) {
newSettings[system] += 2;
newSettings[otherSystems[0]] -= 1;
newSettings[otherSystems[1]] -= 1;
onUpdatePower(newSettings);
}
}
};
const handleReset = () => {
if (disabled || isRerouting) return;
onUpdatePower({ engines: 3, lasers: 3, shields: 3 });
};
const systems = [
{
key: "engines" as const,
label: t.SYSTEMS.ENGINES,
color: "bg-blue-500",
glow: "shadow-[0_0_15px_#3b82f6]",
},
{
key: "lasers" as const,
label: t.SYSTEMS.LASERS,
color: "bg-red-600",
glow: "shadow-[0_0_15px_#dc2626]",
},
{
key: "shields" as const,
label: t.SYSTEMS.SHIELDS,
color: "bg-green-500",
glow: "shadow-[0_0_15px_#22c55e]",
},
];
return (
<div className="bg-[#080808]/90 border-2 border-blue-500/60 p-4 rounded-xl w-full h-full flex flex-col justify-between backdrop-blur-md relative overflow-hidden">
{/* Header */}
<div className="flex justify-between items-start mb-6">
<div>
<h3 className="font-aurebesh text-green-500 text-lg tracking-wider">
{t.SYSTEMS.POWER_MANAGEMENT}
</h3>
<div className="text-[10px] font-mono text-green-500/80 uppercase">
{t.SYSTEMS.SUB_LABEL}
</div>
</div>
<button
onClick={handleReset}
disabled={disabled || isRerouting}
className="text-[10px] font-mono px-3 py-1 border border-yellow-500/30 text-yellow-500/80 hover:text-yellow-500 hover:border-yellow-500 transition-all rounded uppercase disabled:opacity-60"
>
{t.SYSTEMS.RESET}
</button>
</div>
<div className="grid grid-cols-3 gap-4 flex-grow">
{systems.map((sys) => (
<div key={sys.key} className="flex flex-col items-center group">
<button
onClick={() => handleBoost(sys.key)}
disabled={disabled || isRerouting || power[sys.key] >= 6}
className="w-full mb-3 py-2 bg-yellow-500/5 border border-yellow-500/40 hover:bg-yellow-500/20 text-yellow-500 rounded-lg text-sm transition-all active:scale-95 disabled:opacity-50 group-hover:border-yellow-500/50"
>
{t.SYSTEMS.BOOST}
</button>
{/* Visual Bar */}
<div className="flex flex-col-reverse gap-1.5 h-40 w-full bg-black/60 border border-white/40 p-1.5 rounded-md shadow-inner">
{[...Array(6)].map((_, i) => (
<div
key={i}
className={`h-full w-full rounded-sm transition-all duration-500 ${
i < power[sys.key]
? `${sys.color} ${sys.glow}`
: "bg-white/5"
}`}
/>
))}
</div>
<span className="mt-3 text-[10px] font-mono uppercase text-yellow-500 tracking-widest">
{sys.label}
</span>
<span className="text-[10px] font-mono text-white/60">
LVL {power[sys.key]}
</span>
</div>
))}
</div>
{/* Overlay */}
{isRerouting && (
<div className="absolute inset-0 bg-black/60 backdrop-blur-[2px] flex items-center justify-center z-10">
<div className="bg-black border border-red-500/50 text-red-500 text-[10px] font-mono px-4 py-2 rounded shadow-[0_0_20px_rgba(239,68,68,0.3)] animate-pulse uppercase tracking-[0.2em]">
{t.ENGINE.SYSTEMS.REROUTING}
</div>
</div>
)}
</div>
);
}
+53 -27
View File
@@ -9,49 +9,76 @@ interface StartScreenProps {
export default function StartScreen({ onStart }: StartScreenProps) { export default function StartScreen({ onStart }: StartScreenProps) {
return ( return (
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-[#050505] overflow-hidden"> <div className="fixed inset-0 z-[110] flex items-center justify-center bg-black overflow-hidden">
{/* Hintergrund-Deko */} <div className="absolute inset-0 pointer-events-none">
<div className="absolute inset-0 opacity-20 bg-[radial-gradient(circle_at_center,_#eab308_0%,_transparent_70%)]" /> <div
className="absolute inset-0 opacity-30"
style={{
backgroundImage: `
radial-gradient(1px 1px at 20px 30px, #fff, 100%, rgba(0,0,0,0)),
radial-gradient(1px 1px at 40px 70px, #fff, 100%, rgba(0,0,0,0)),
radial-gradient(1px 1px at 50px 160px, #ddd, 100%, rgba(0,0,0,0)),
radial-gradient(1px 1px at 90px 40px, #fff, 100%, rgba(0,0,0,0)),
radial-gradient(1px 1px at 130px 80px, #fff, 100%, rgba(0,0,0,0)),
radial-gradient(1px 1px at 160px 120px, #ddd, 100%, rgba(0,0,0,0))
`,
backgroundSize: "200px 200px",
backgroundRepeat: "repeat",
}}
/>
<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="absolute inset-0 opacity-50"
style={{
backgroundImage: `
radial-gradient(1.5px 1.5px at 100px 150px, #fff, 100%, rgba(0,0,0,0)),
radial-gradient(1.5px 1.5px at 300px 400px, #ddd, 100%, rgba(0,0,0,0)),
radial-gradient(1.5px 1.5px at 500px 100px, #fff, 100%, rgba(0,0,0,0))
`,
backgroundSize: "450px 450px",
backgroundRepeat: "repeat",
}}
/>
</div>
<div className="absolute inset-0 opacity-10 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-2 border-yellow-500 rounded-2xl bg-black/80 backdrop-blur-sm shadow-[0_0_50px_rgba(0,0,0,0.5)]">
<div className="space-y-2"> <div className="space-y-2">
<h1 className="font-starjedi text-6xl text-white"> <h1 className="font-starjedi text-6xl text-yellow-500">
{t.UI.GAME_TITLE} {t.UI.GAME_TITLE}
</h1> </h1>
<p className="text-yellow-500/60 font-mono tracking-[0.3em] uppercase text-xs"> <p className="text-yellow-500 font-mono tracking-[0.3em] uppercase text-xs">
{t.START_SCREEN.SUBTITLE} {t.START_SCREEN.SUBTITLE}
</p> </p>
</div> </div>
<div className="space-y-4 text-gray-400 font-light leading-relaxed"> <div className="space-y-6 text-yellow-500 leading-relaxed">
<p> <p className="text-sm tracking-wider font-medium">
{t.START_SCREEN.INTRO_TEXT} {t.START_SCREEN.INTRO_TEXT}
<span className="text-white font-medium"> <span className="block mt-1 text-yellow-500 font-black">
{" "}
{t.START_SCREEN.MISSION_HIGHLIGHT} {t.START_SCREEN.MISSION_HIGHLIGHT}
</span> </span>
</p> </p>
<ul className="text-sm space-y-2 inline-block text-left border-l-2 border-yellow-500/30 pl-6">
<ul className="text-[10px] space-y-3 inline-block text-left border-l-2 border-yellow-500/50 pl-6 tracking-[0.2em]">
<li> <li>
{" "} <strong>{t.START_SCREEN.FEATURE_NAV_TITLE}</strong>{" "}
<strong className="text-yellow-500/80"> <span className="opacity-70">
{t.START_SCREEN.FEATURE_NAV_TITLE}
</strong>{" "}
{t.START_SCREEN.FEATURE_NAV_DESC} {t.START_SCREEN.FEATURE_NAV_DESC}
</span>
</li> </li>
<li> <li>
{" "} <strong>{t.START_SCREEN.FEATURE_RISK_TITLE}</strong>{" "}
<strong className="text-yellow-500/80"> <span className="opacity-70">
{t.START_SCREEN.FEATURE_RISK_TITLE}
</strong>{" "}
{t.START_SCREEN.FEATURE_RISK_DESC} {t.START_SCREEN.FEATURE_RISK_DESC}
</span>
</li> </li>
<li> <li>
{" "} <strong>{t.START_SCREEN.FEATURE_COMBAT_TITLE}</strong>{" "}
<strong className="text-yellow-500/80"> <span className="opacity-70">
{t.START_SCREEN.FEATURE_COMBAT_TITLE}
</strong>{" "}
{t.START_SCREEN.FEATURE_COMBAT_DESC} {t.START_SCREEN.FEATURE_COMBAT_DESC}
</span>
</li> </li>
</ul> </ul>
</div> </div>
@@ -59,13 +86,12 @@ export default function StartScreen({ onStart }: StartScreenProps) {
<div className="pt-6"> <div className="pt-6">
<button <button
onClick={onStart} 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" className="px-12 py-5 border-2 border-yellow-500 text-yellow-500 font-black uppercase tracking-[0.3em] transition-all hover:bg-yellow-500 hover:text-black active:scale-95 shadow-[0_0_20px_rgba(234,179,8,0.2)]"
> >
<span className="relative z-10">{t.UI.START_BUTTON}</span> {t.UI.START_BUTTON}
<div className="absolute inset-0 bg-white opacity-0 group-hover:opacity-20 transition-opacity" />
</button> </button>
<p className="mt-4 text-[10px] text-gray-600 uppercase tracking-widest"> <p className="mt-6 text-[9px] text-yellow-500/70 uppercase tracking-[0.3em]">
{t.START_SCREEN.FOOTER_ENGINE} {t.START_SCREEN.FOOTER_ENGINE}
</p> </p>
</div> </div>
+113
View File
@@ -0,0 +1,113 @@
"use client";
import React from "react";
import { t } from "@/content/locales";
interface VictoryScreenProps {
credits: number;
onRestart: () => void;
}
export default function VictoryScreen({
credits,
onRestart,
}: VictoryScreenProps) {
const creditsBlue = "#4bd5ee";
return (
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-black overflow-hidden">
<div className="absolute inset-0 pointer-events-none">
<div
className="absolute inset-0 opacity-30"
style={{
backgroundImage: `
radial-gradient(1px 1px at 20px 30px, #fff 100%, rgba(0,0,0,0)),
radial-gradient(1px 1px at 40px 70px, #fff 100%, rgba(0,0,0,0)),
radial-gradient(1px 1px at 50px 160px, #ddd 100%, rgba(0,0,0,0)),
radial-gradient(1px 1px at 90px 40px, #fff 100%, rgba(0,0,0,0))
`,
backgroundSize: "200px 200px",
backgroundRepeat: "repeat",
}}
/>
<div
className="absolute inset-0 opacity-50"
style={{
backgroundImage: `
radial-gradient(1.5px 1.5px at 100px 150px, #fff 100%, rgba(0,0,0,0)),
radial-gradient(1.5px 1.5px at 300px 400px, #ddd 100%, rgba(0,0,0,0))
`,
backgroundSize: "450px 450px",
backgroundRepeat: "repeat",
}}
/>
</div>
<div
className="absolute inset-0 opacity-10"
style={{
background: `radial-gradient(circle at center, ${creditsBlue} 0%, transparent 70%)`,
}}
/>
<div
className="max-w-2xl w-full mx-4 relative z-10 text-center space-y-8 p-10 border-2 rounded-2xl bg-black/80 backdrop-blur-sm shadow-[0_0_50px_rgba(0,0,0,0.5)]"
style={{ borderColor: creditsBlue }}
>
<div className="space-y-2">
<h1 className="font-starjedi text-6xl" style={{ color: creditsBlue }}>
{t.VICTORY.TITLE}
</h1>
<p
className="font-mono tracking-[0.3em] uppercase text-xs opacity-70"
style={{ color: creditsBlue }}
>
{t.VICTORY.SUBTITLE}
</p>
</div>
<div
className="space-y-6 leading-relaxed"
style={{ color: creditsBlue }}
>
<div className="space-y-2">
<p className="text-sm tracking-wider font-medium uppercase">
{t.VICTORY.MESSAGE}
</p>
<p className="text-4xl font-black tracking-tighter">
{credits.toLocaleString()} CR
</p>
</div>
<div
className="text-[10px] py-4 border-y border-opacity-20 inline-block px-8 tracking-[0.2em] uppercase italic"
style={{ borderColor: creditsBlue }}
>
The galaxy will remember your name.
</div>
</div>
<div className="pt-6">
<button
onClick={onRestart}
className="px-12 py-5 border-2 font-black uppercase tracking-[0.3em] transition-all hover:text-black active:scale-95"
style={{
borderColor: creditsBlue,
color: creditsBlue,
}}
onMouseOver={(e) => {
e.currentTarget.style.backgroundColor = creditsBlue;
e.currentTarget.style.color = "black";
}}
onMouseOut={(e) => {
e.currentTarget.style.backgroundColor = "transparent";
e.currentTarget.style.color = creditsBlue;
}}
>
{t.VICTORY.BUTTON}
</button>
</div>
</div>
</div>
);
}
+51 -17
View File
@@ -1,24 +1,24 @@
// constants/locales.ts // constants/locales.ts
export const locales = { export const locales = {
de: { en: {
UI: { UI: {
GAME_TITLE: "Smuggler's Run", GAME_TITLE: "Smuggler's Run",
SUBTITLE: "Fast Ships, faster credits!", SUBTITLE: "Fast Ships, faster credits!",
START_BUTTON: "Initialize Systems", START_BUTTON: "Initialize Systems",
RESTART_BUTTON: "Klon-Zylinder aktivieren", RESTART_BUTTON: "Activate Clone Cylinder",
JUMP_BUTTON: "Initiate Jump", JUMP_BUTTON: "Initiate Jump",
ATTACK_BUTTON: "Feuer frei", ATTACK_BUTTON: "Open Fire",
ESCAPE_BUTTON: "Fluchtversuch", ESCAPE_BUTTON: "Evasive Maneuvers",
}, },
SYSTEM: { SYSTEM: {
LOADING: "INITIALIZING HOLONET LINK...", LOADING: "INITIALIZING HOLONET LINK...",
DEEP_SPACE: "Deep Space", DEEP_SPACE: "Deep Space",
}, },
START_SCREEN: { START_SCREEN: {
SUBTITLE: "Fast Ships, faster credits!", SUBTITLE: "Episode I: Fast Ships, faster credits!",
INTRO_TEXT: INTRO_TEXT:
"Smuggler's Run is a minimalist space adventure game, where you, the player, are an unlicensed space trader, flying cargo from planet to planet while trying not to be blown to pieces by bounty hunters, imperial forces or rebel do-gooders. Your mission is simple:", "Smuggler's Run is a minimalist space adventure game where you are an unlicensed space trader, flying cargo from planet to planet while trying not to be blown to pieces by bounty hunters, imperial forces, or rebel do-gooders. Your mission is simple:",
MISSION_HIGHLIGHT: "Survive and earn credits", MISSION_HIGHLIGHT: "Survive and earn credits",
FEATURE_NAV_TITLE: "Navigation:", FEATURE_NAV_TITLE: "Navigation:",
FEATURE_NAV_DESC: "Jump between sectors to deliver cargo.", FEATURE_NAV_DESC: "Jump between sectors to deliver cargo.",
@@ -59,6 +59,8 @@ export const locales = {
FIRE: "Open Fire", FIRE: "Open Fire",
JUMP: "Initiate Jump", JUMP: "Initiate Jump",
ESCAPE: "Evasive Maneuvers", ESCAPE: "Evasive Maneuvers",
REPAIR: (cost: number) => `Repair ship for (${cost} Cr)`,
HULL_INTACT: "Hull integrity stable",
}, },
}, },
GAME_OVER: { GAME_OVER: {
@@ -67,6 +69,13 @@ export const locales = {
SCORE_LABEL: "Final Profit", SCORE_LABEL: "Final Profit",
RESTART_BUTTON: "Activate Clone Cylinder", RESTART_BUTTON: "Activate Clone Cylinder",
}, },
VICTORY: {
TITLE: "Victory",
SUBTITLE: "Wealth Threshold Exceeded",
MESSAGE:
"You have amassed enough credits to retire in the Outer Rim. The Empire's reach can no longer find you.",
BUTTON: "NEW ADVENTURE",
},
EVENT_LOG: { EVENT_LOG: {
TITLE: "Event Log", TITLE: "Event Log",
EMPTY_STATE: "Waiting for signals...", EMPTY_STATE: "Waiting for signals...",
@@ -74,7 +83,7 @@ export const locales = {
HUD: { HUD: {
SECTOR_LABEL: "Current Sector", SECTOR_LABEL: "Current Sector",
HULL_LABEL: "Hull Integrity", HULL_LABEL: "Hull Integrity",
GOAL_LABEL: "Credits to Freedom", // Oder "Credits to Falcon" GOAL_LABEL: "Credits to Freedom",
}, },
GALAXY_MAP: { GALAXY_MAP: {
SCAN_STATUS: "Sector Scan: Active", SCAN_STATUS: "Sector Scan: Active",
@@ -102,29 +111,54 @@ export const locales = {
SUCCESS: "Jump successful! We've shaken off the pursuer.", SUCCESS: "Jump successful! We've shaken off the pursuer.",
}, },
COMBAT: { COMBAT: {
INIT: (ship: string) => INIT: (ship: string) => `Weapon systems synchronized. Target: ${ship}!`,
`Weapon systems synchronized. Opening fire on ${ship}!`,
VICTORY: (ship: string, loot: number) => VICTORY: (ship: string, loot: number) =>
`Direct hit! The ${ship} has been obliterated. +${loot} Credits salvaged.`, `Target neutralized! The ${ship} has been destroyed. +${loot} Credits salvaged.`,
COUNTER: (ship: string, dmg: number) => COUNTER: (ship: string, dmg: number) =>
`Shields holding! The ${ship} counters. -${dmg} HP`, `The ${ship} returns fire! -${dmg} HP`,
HIT: (ship: string, hp: number) =>
`Direct hit! ${ship} integrity at ${hp} HP.`,
EVASION: (roll: number, bonus: number) =>
`[Evasion: ${roll}+${bonus}] High-speed maneuver successful! Enemy fire avoided.`,
},
REPAIR: {
SUCCESS: (planet: string, cost: number) =>
`Repairs complete at ${planet}. -${cost} Credits.`,
INSUFFICIENT_FUNDS: "Insufficient credits for repairs!",
}, },
SYSTEMS: { SYSTEMS: {
CRITICAL_FAILURE: CRITICAL_FAILURE:
"CRITICAL STRUCTURAL DAMAGE! The ship is breaking apart...", "CRITICAL STRUCTURAL DAMAGE! The ship is breaking apart...",
REROUTING: "Rerouting power systems...",
REROUTE_SUCCESS: "Power configuration updated.",
REROUTE_FAIL: (ship: string, dmg: number) =>
`Power lag! ${ship} exploits the window. -${dmg} HP`,
SECTOR_UPDATE: "Nav-computer recalibrated. New sector discovered.",
}, },
DICE: { DICE: {
ROLLING_1W6: "[Rolling 1D6...]", ROLLING_1W6: "[Rolling 1D6...]",
ROLLING_2W6: "[Rolling 2D6...]", ROLLING_2W6: "[Rolling 2D6...]",
RESULT_1W6: (res: number) => `[Roll: ${res}]`, RESULT_1W6: (res: number) => `[Roll: ${res}]`,
RESULT_2W6: (d1: number, d2: number, total: number) => RESULT_TOTAL: (total: number, bonus: number) =>
`[Roll: ${d1} + ${d2} = ${total}]`, `[Total: ${total} (Bonus: +${bonus})]`,
}, },
}, },
SYSTEMS: {
POWER_MANAGEMENT: "Power Management",
SUB_LABEL: "System Distribution Active",
RESET: "Reset",
BOOST: "Boost",
ENGINES: "Engines",
LASERS: "Lasers",
SHIELDS: "Shields",
TOTAL_POWER: "Total Power",
NO_ENERGY: "Energy Depleted",
REROUTING: "Rerouting Energy...",
},
}, },
// Später einfach erweiterbar:
// en: { ... }
}; };
// Aktuelle Sprache festlegen (könnte später aus einem State kommen) export type LocaleType = typeof locales.en;
export const t = locales.de;
// Defaulting to English
export const t = locales.en;
+154 -53
View File
@@ -1,26 +1,53 @@
"use client"; "use client";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback, useMemo } from "react";
import { Planet, Starship } from "@/types/swapi"; import { Planet, Starship } from "@/types/swapi";
import { fetchPlanets, fetchStarships } from "@/services/swapi"; import { fetchGameData } from "@/services/swapi";
import { LogEntry } from "@/components/EventLog"; import { LogEntry } from "@/components/EventLog";
import { t } from "@/content/locales"; import { t } from "@/content/locales";
export function useGameEngine() { export function useGameEngine() {
const [allPlanets, setAllPlanets] = useState<Planet[]>([]);
const [planets, setPlanets] = useState<Planet[]>([]); const [planets, setPlanets] = useState<Planet[]>([]);
const [starships, setStarships] = useState<Starship[]>([]); const [starships, setStarships] = useState<Starship[]>([]);
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [gameStarted, setGameStarted] = useState(false); const [gameStarted, setGameStarted] = useState(false);
const [isGameOver, setIsGameOver] = useState(false); const [isGameOver, setIsGameOver] = useState(false);
const [isVictory, setIsVictory] = useState(false);
const [credits, setCredits] = useState<number>(100); const [credits, setCredits] = useState<number>(100);
const [hp, setHp] = useState<number>(10); const [hp, setHp] = useState<number>(10);
const [currentLocationId, setCurrentLocationId] = useState<string>(""); const [currentLocationId, setCurrentLocationId] = useState<string>("");
const [selectedPlanet, setSelectedPlanet] = useState<Planet | null>(null); const [selectedPlanet, setSelectedPlanet] = useState<Planet | null>(null);
const [isJumping, setIsJumping] = useState<boolean>(false);
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [isRerouting, setIsRerouting] = useState<boolean>(false);
const [activeEnemy, setActiveEnemy] = useState<Starship | null>(null); const [activeEnemy, setActiveEnemy] = useState<Starship | null>(null);
const [logs, setLogs] = useState<LogEntry[]>([]); const [logs, setLogs] = useState<LogEntry[]>([]);
const [power, setPower] = useState({
engines: 3,
lasers: 3,
shields: 3,
});
const MAX_POWER = 9;
const WIN_THRESHOLD = 100000;
const selectRandomSector = useCallback(
(all: Planet[], count: number = 15) => {
const shuffled = [...all].sort(() => 0.5 - Math.random());
return shuffled.slice(0, count);
},
[],
);
const currentPlanet = useMemo(
() => planets.find((p) => p.id === currentLocationId) || null,
[planets, currentLocationId],
);
// --- Log Helper ---
const addLog = useCallback( const addLog = useCallback(
(message: string, type: LogEntry["type"] = "info") => { (message: string, type: LogEntry["type"] = "info") => {
const newLog: LogEntry = { const newLog: LogEntry = {
@@ -37,25 +64,32 @@ export function useGameEngine() {
[], [],
); );
useEffect(() => { // --- Actions ---
async function initGame() {
try { const updatePower = (newSettings: typeof power) => {
const [planets, ships] = await Promise.all([ if (!activeEnemy) {
fetchPlanets(), setPower(newSettings);
fetchStarships(), return;
]);
setPlanets(planets);
setStarships(ships);
if (planets.length > 0) setCurrentLocationId(planets[0].id);
addLog(t.ENGINE.BOOT.SUCCESS, "success");
} catch (error) {
addLog(t.ENGINE.BOOT.ERROR, "danger");
} finally {
setLoading(false);
} }
setIsRerouting(true);
addLog(t.ENGINE.SYSTEMS.REROUTING, "info");
setTimeout(() => {
setPower(newSettings);
if (Math.random() > 0.7) {
const damage = 1;
setHp((prev) => Math.max(0, prev - damage));
addLog(
t.ENGINE.SYSTEMS.REROUTE_FAIL(activeEnemy.name, damage),
"danger",
);
} else {
addLog(t.ENGINE.SYSTEMS.REROUTE_SUCCESS, "success");
} }
initGame(); setIsRerouting(false);
}, [addLog]); }, 600);
};
const startGame = () => { const startGame = () => {
setGameStarted(true); setGameStarted(true);
@@ -64,8 +98,7 @@ export function useGameEngine() {
const executeJump = async () => { const executeJump = async () => {
if (!selectedPlanet || selectedPlanet.id === currentLocationId) return; if (!selectedPlanet || selectedPlanet.id === currentLocationId) return;
setIsProcessing(true);
setIsJumping(true);
addLog( addLog(
`${t.ENGINE.JUMP.INIT(selectedPlanet.name)} ${t.ENGINE.DICE.ROLLING_1W6}`, `${t.ENGINE.JUMP.INIT(selectedPlanet.name)} ${t.ENGINE.DICE.ROLLING_1W6}`,
"info", "info",
@@ -74,7 +107,6 @@ export function useGameEngine() {
setTimeout(() => { setTimeout(() => {
const roll = Math.floor(Math.random() * 6) + 1; const roll = Math.floor(Math.random() * 6) + 1;
const rollInfo = t.ENGINE.DICE.RESULT_1W6(roll); const rollInfo = t.ENGINE.DICE.RESULT_1W6(roll);
if (roll >= 5) { if (roll >= 5) {
const randomShip = const randomShip =
starships[Math.floor(Math.random() * starships.length)]; starships[Math.floor(Math.random() * starships.length)];
@@ -92,65 +124,113 @@ export function useGameEngine() {
"success", "success",
); );
} }
setIsJumping(false); setIsProcessing(false);
setSelectedPlanet(null); setSelectedPlanet(null);
}, 1200); }, 1200);
}; };
const executeEscape = () => { const executeEscape = () => {
if (!activeEnemy) return; if (!activeEnemy) return;
addLog(`${t.ENGINE.ESCAPE.INIT} ${t.ENGINE.DICE.ROLLING_1W6}`, "info"); setIsProcessing(true);
addLog(t.ENGINE.ESCAPE.INIT, "info");
setTimeout(() => { setTimeout(() => {
const roll = Math.floor(Math.random() * 6) + 1; const roll = Math.floor(Math.random() * 6) + 1;
const rollInfo = t.ENGINE.DICE.RESULT_1W6(roll); const engineBonus = Math.floor(power.engines / 2);
const total = roll + engineBonus;
if (roll <= 2) { if (total >= 5) {
const damage = 1; setActiveEnemy(null);
setHp((prev) => Math.max(0, prev - damage));
addLog( addLog(
`${rollInfo} ${t.ENGINE.ESCAPE.FAIL(activeEnemy.name, damage)}`, `${t.ENGINE.DICE.RESULT_TOTAL(total, engineBonus)} ${t.ENGINE.ESCAPE.SUCCESS}`,
"danger", "success",
); );
} else { } else {
setActiveEnemy(null); const damage = 2;
addLog(`${rollInfo} ${t.ENGINE.ESCAPE.SUCCESS}`, "success"); setHp((prev) => Math.max(0, prev - damage));
addLog(
`${t.ENGINE.DICE.RESULT_TOTAL(total, engineBonus)} ${t.ENGINE.ESCAPE.FAIL(activeEnemy.name, damage)}`,
"danger",
);
} }
setIsProcessing(false);
}, 800); }, 800);
}; };
const handleAttack = () => { const handleAttack = () => {
if (!activeEnemy) return; if (!activeEnemy) return;
addLog( setIsProcessing(true);
`${t.ENGINE.COMBAT.INIT(activeEnemy.name)} ${t.ENGINE.DICE.ROLLING_2W6}`, addLog(t.ENGINE.COMBAT.INIT(activeEnemy.name), "info");
"info",
);
setTimeout(() => { setTimeout(() => {
const d1 = Math.floor(Math.random() * 6) + 1; const d1 = Math.floor(Math.random() * 6) + 1;
const d2 = Math.floor(Math.random() * 6) + 1; const d2 = Math.floor(Math.random() * 6) + 1;
const total = d1 + d2; const laserBonus = Math.floor(power.lasers / 2);
const rollInfo = t.ENGINE.DICE.RESULT_2W6(d1, d2, total); const total = d1 + d2 + laserBonus;
if (total >= activeEnemy.ds) { if (total >= activeEnemy.ds) {
const newHp = (activeEnemy.hp || 1) - 1;
if (newHp <= 0) {
const loot = 300; const loot = 300;
setCredits((prev) => prev + loot); setCredits((prev) => prev + loot);
setActiveEnemy(null); setActiveEnemy(null);
addLog(t.ENGINE.COMBAT.VICTORY(activeEnemy.name, loot), "success");
} else {
setActiveEnemy({ ...activeEnemy, hp: newHp });
addLog(t.ENGINE.COMBAT.HIT(activeEnemy.name, newHp), "success");
}
} else {
const engineBonus = Math.floor(power.engines / 2);
const evasionRoll = Math.floor(Math.random() * 6) + 1;
if (evasionRoll + engineBonus >= 5) {
addLog(t.ENGINE.COMBAT.EVASION(evasionRoll, engineBonus), "success");
} else {
const rawDiff = activeEnemy.ds - total;
const shieldBonus = Math.floor(power.shields / 2);
const dmg = Math.max(0, rawDiff - shieldBonus);
setHp((prev) => Math.max(0, prev - dmg));
addLog(t.ENGINE.COMBAT.COUNTER(activeEnemy.name, dmg), "danger");
}
}
setIsProcessing(false);
}, 1000);
};
const handleRepair = () => {
const missingHp = 10 - hp;
const cost = missingHp * 20;
if (missingHp <= 0) return;
if (credits >= cost) {
setCredits((prev) => prev - cost);
setHp(10);
addLog( addLog(
`${rollInfo} ${t.ENGINE.COMBAT.VICTORY(activeEnemy.name, loot)}`, t.ENGINE.REPAIR.SUCCESS(currentPlanet?.name || "Unknown", cost),
"success", "success",
); );
} else { } else {
const diff = activeEnemy.ds - total; addLog(t.ENGINE.REPAIR.INSUFFICIENT_FUNDS, "danger");
setHp((prev) => Math.max(0, prev - diff));
addLog(
`${rollInfo} ${t.ENGINE.COMBAT.COUNTER(activeEnemy.name, diff)}`,
"danger",
);
} }
}, 1000);
}; };
// --- Effects ---
useEffect(() => {
async function initGame() {
try {
setLoading(true);
const { planets: p, starships: s } = await fetchGameData();
setAllPlanets(p);
setStarships(s);
const initialSector = selectRandomSector(p, 15);
setPlanets(initialSector);
if (initialSector.length > 0) setCurrentLocationId(initialSector[0].id);
addLog(t.ENGINE.BOOT.SUCCESS, "success");
} catch (error) {
console.error(error);
addLog(t.ENGINE.BOOT.ERROR, "danger");
} finally {
setLoading(false);
}
}
initGame();
}, [addLog, selectRandomSector]);
useEffect(() => { useEffect(() => {
if (hp <= 0 && !isGameOver) { if (hp <= 0 && !isGameOver) {
setIsGameOver(true); setIsGameOver(true);
@@ -158,14 +238,27 @@ export function useGameEngine() {
} }
}, [hp, isGameOver, addLog]); }, [hp, isGameOver, addLog]);
useEffect(() => {
if (credits >= WIN_THRESHOLD && !isVictory) {
setIsVictory(true);
addLog(t.VICTORY.MESSAGE, "success");
}
}, [credits, isVictory, addLog]);
const restartGame = () => { const restartGame = () => {
setHp(10); setHp(10);
setCredits(100); setCredits(100);
setPower({ engines: 3, lasers: 3, shields: 3 });
setIsGameOver(false); setIsGameOver(false);
setIsVictory(false);
setActiveEnemy(null); setActiveEnemy(null);
setSelectedPlanet(null); setSelectedPlanet(null);
setLogs([]); setLogs([]);
const newSector = selectRandomSector(allPlanets, 15);
setPlanets(newSector);
if (newSector.length > 0) setCurrentLocationId(newSector[0].id);
addLog(t.ENGINE.BOOT.RESTART, "success"); addLog(t.ENGINE.BOOT.RESTART, "success");
addLog(t.ENGINE.SYSTEMS.SECTOR_UPDATE, "info");
}; };
return { return {
@@ -177,15 +270,23 @@ export function useGameEngine() {
credits, credits,
hp, hp,
currentLocationId, currentLocationId,
currentPlanet,
selectedPlanet, selectedPlanet,
setSelectedPlanet, setSelectedPlanet,
executeEscape, executeEscape,
executeJump, executeJump,
handleAttack, handleAttack,
isJumping, handleRepair,
isJumping: isProcessing,
isRerouting,
activeEnemy, activeEnemy,
power,
updatePower,
maxPower: MAX_POWER,
logs, logs,
isGameOver, isGameOver,
restartGame, restartGame,
isVictory,
WIN_THRESHOLD,
}; };
} }
Binary file not shown.
+70 -21
View File
@@ -1,35 +1,84 @@
import { SWAPIPlanet, SWAPIStarship, Planet, Starship } from "@/types/swapi"; import { SWAPIPlanet, SWAPIStarship, Planet, Starship } from "@/types/swapi";
const BASE_URL = "https://swapi.dev/api"; const BASE_URL = "https://swapi.dev/api";
const CACHE_KEY = "smugglers_run_cache";
const CACHE_DURATION = 1000 * 60 * 60 * 24; // Cache TTL 24 hrs
export async function fetchPlanets(): Promise<Planet[]> { interface GameCache {
const res = await fetch(`${BASE_URL}/planets/`); planets: Planet[];
if (!res.ok) throw new Error("Galaxy data unavailable"); starships: Starship[];
timestamp: number;
}
async function fetchAllPages<T>(endpoint: string): Promise<T[]> {
let allResults: T[] = [];
let nextUrl = `${BASE_URL}/${endpoint}/`;
while (nextUrl) {
const res = await fetch(nextUrl);
if (!res.ok) throw new Error(`${endpoint} data unavailable`);
const data = await res.json(); const data = await res.json();
allResults = [...allResults, ...data.results];
nextUrl = data.next;
}
return allResults;
}
return data.results.map( export async function fetchGameData(): Promise<{
(p: SWAPIPlanet): Planet => ({ planets: Planet[];
starships: Starship[];
}> {
// Try load from localstorage as simple cache
const cachedData =
typeof window !== "undefined" ? localStorage.getItem(CACHE_KEY) : null;
if (cachedData) {
const parsedCache: GameCache = JSON.parse(cachedData);
const isExpired = Date.now() - parsedCache.timestamp > CACHE_DURATION;
if (!isExpired) {
console.log("data loaded from cache");
return { planets: parsedCache.planets, starships: parsedCache.starships };
}
}
// If not in cache or outdated, fetch from Swapi
console.log("data not in cache. fetching from swapi...");
const [rawPlanets, rawStarships] = await Promise.all([
fetchAllPages<SWAPIPlanet>("planets"),
fetchAllPages<SWAPIStarship>("starships"),
]);
const planets: Planet[] = rawPlanets.map((p) => ({
...p, ...p,
id: p.url, id: p.url,
// Add random x/y coordinates for galaxy map
x: Math.floor(Math.random() * 80) + 10, x: Math.floor(Math.random() * 80) + 10,
y: Math.floor(Math.random() * 70) + 15, y: Math.floor(Math.random() * 70) + 15,
}), }));
);
}
export async function fetchStarships(): Promise<Starship[]> { const starships: Starship[] = rawStarships.map((s) => {
const res = await fetch(`${BASE_URL}/starships/`);
if (!res.ok) throw new Error("Imperial records unavailable");
const data = await res.json();
return data.results.map((s: SWAPIStarship): Starship => {
// Add difficulty score based on ship cost
const cost = parseInt(s.cost_in_credits); const cost = parseInt(s.cost_in_credits);
let ds = 8; let ds = 8,
if (isNaN(cost) || cost > 1000000) ds = 12; hp = 2;
else if (cost > 100000) ds = 10; if (isNaN(cost) || cost > 1000000) {
ds = 12;
hp = 6;
} else if (cost > 100000) {
ds = 10;
hp = 4;
}
return { ...s, id: s.url, ds, hp };
});
// Save to localstorage
const cacheToSave: GameCache = {
planets,
starships,
timestamp: Date.now(),
};
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheToSave));
return { ...s, id: s.url, ds }; return { planets, starships };
});
} }
+3 -6
View File
@@ -1,7 +1,4 @@
// types/swapi.ts export interface SWAPIPlanet {
// Die korrigierten Rohdaten von der SWAPI
export interface SWAPIPlanet { // <-- Jetzt mit korrektem 'n'
name: string; name: string;
rotation_period: string; rotation_period: string;
orbital_period: string; orbital_period: string;
@@ -28,7 +25,6 @@ export interface SWAPIStarship {
url: string; url: string;
} }
// Unsere erweiterten Typen für das Spiel
export interface Planet extends SWAPIPlanet { export interface Planet extends SWAPIPlanet {
id: string; id: string;
x: number; x: number;
@@ -37,5 +33,6 @@ export interface Planet extends SWAPIPlanet {
export interface Starship extends SWAPIStarship { export interface Starship extends SWAPIStarship {
id: string; id: string;
ds: number; // Difficulty Score für Tunnel Goons ds: number;
hp: number;
} }
+63
View File
@@ -0,0 +1,63 @@
import { LocaleType } from "@/content/locales";
export const getPlanetTheme = (
terrain: string = "",
climate: string = "",
t: LocaleType,
) => {
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,
hex: "#60a5fa",
};
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,
hex: "#4ade80",
};
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,
hex: "#fb923c",
};
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,
hex: "#22d3ee",
};
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,
hex: "#c084fc",
};
return {
color: "text-yellow-500",
border: "border-yellow-500",
glow: "shadow-[0_0_20px_#eab308]",
label: t.ACTION_PANEL.MODES.STANDARD,
hex: "#eab308",
};
};