update and polish
This commit is contained in:
@@ -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.
|
||||
|
||||
This game is not meant to be played on a small mobile screen.
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
+1
-1
@@ -6,8 +6,8 @@
|
||||
}
|
||||
|
||||
@theme {
|
||||
/* Hier registrierst du die Font für Tailwind v4 */
|
||||
--font-starjedi: var(--font-starjedi);
|
||||
--font-aurebesh: var(--font-aurebesh)
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
|
||||
+11
-4
@@ -1,6 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
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";
|
||||
|
||||
const geistSans = Geist({
|
||||
@@ -13,14 +13,18 @@ const geistMono = Geist_Mono({
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
// Konfiguration der lokalen Star Jedi Font
|
||||
const starJedi = localFont({
|
||||
src: "../public/fonts/Starjedi.ttf",
|
||||
variable: "--font-starjedi",
|
||||
});
|
||||
|
||||
const aurebesh = localFont({
|
||||
src: "../public/fonts/Aurebesh-English.ttf",
|
||||
variable: "--font-aurebesh",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Outer Rim Smuggler",
|
||||
title: "Smuggler's Run",
|
||||
description: "A Star Wars inspired roguelike adventure",
|
||||
};
|
||||
|
||||
@@ -30,7 +34,10 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className={`${starJedi.variable}`}>
|
||||
<html
|
||||
lang="en"
|
||||
className={`${starJedi.variable} ${aurebesh.variable} ${geistSans.variable} ${geistMono.variable}`}
|
||||
>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
|
||||
+40
-18
@@ -7,7 +7,9 @@ import EventLog from "../components/EventLog";
|
||||
import ActionPanel from "../components/ActionPanel";
|
||||
import StartScreen from "@/components/StartScreen";
|
||||
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() {
|
||||
const {
|
||||
@@ -18,19 +20,25 @@ export default function Home() {
|
||||
hp,
|
||||
loading,
|
||||
currentLocationId,
|
||||
currentPlanet,
|
||||
selectedPlanet,
|
||||
setSelectedPlanet,
|
||||
handleAttack,
|
||||
executeJump,
|
||||
handleRepair,
|
||||
isJumping,
|
||||
power,
|
||||
updatePower,
|
||||
activeEnemy,
|
||||
executeEscape,
|
||||
logs,
|
||||
isGameOver,
|
||||
restartGame,
|
||||
isVictory,
|
||||
isRerouting,
|
||||
WIN_THRESHOLD,
|
||||
} = useGameEngine();
|
||||
|
||||
// 1. Ladezustand
|
||||
if (loading) {
|
||||
return (
|
||||
<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
|
||||
const currentPlanet = planets.find((p) => p.id === currentLocationId);
|
||||
if (!gameStarted) return <StartScreen onStart={startGame} />;
|
||||
|
||||
// 2. Startbildschirm
|
||||
if (!gameStarted) {
|
||||
return <StartScreen onStart={startGame} />;
|
||||
}
|
||||
const activeDisplayPlanet = selectedPlanet || currentPlanet;
|
||||
const isViewingCurrentLocation =
|
||||
!!currentPlanet && activeDisplayPlanet?.id === currentPlanet.id;
|
||||
|
||||
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
|
||||
credits={credits}
|
||||
hp={hp}
|
||||
maxHp={10} // Könnte später aus einem Ship-Objekt kommen
|
||||
maxHp={10}
|
||||
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
|
||||
planets={planets}
|
||||
onSelectLocation={(p) => setSelectedPlanet(p)}
|
||||
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 */}
|
||||
{(selectedPlanet || activeEnemy) && (
|
||||
<div className="w-full max-w-4xl animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
<ActionPanel
|
||||
planet={selectedPlanet}
|
||||
planet={activeDisplayPlanet}
|
||||
enemy={activeEnemy}
|
||||
isCurrentLocation={isViewingCurrentLocation}
|
||||
isLoading={isJumping}
|
||||
onAction={activeEnemy ? handleAttack : executeJump}
|
||||
onEscape={executeEscape}
|
||||
onRepair={handleRepair}
|
||||
currentHp={hp}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-4xl mt-6">
|
||||
<EventLog logs={logs} />
|
||||
</div>
|
||||
|
||||
{isVictory && <VictoryScreen credits={credits} onRestart={restartGame} />}
|
||||
|
||||
{/* Game Over Overlay */}
|
||||
{isGameOver && (
|
||||
<GameOverScreen credits={credits} onRestart={restartGame} />
|
||||
)}
|
||||
|
||||
+118
-139
@@ -2,223 +2,202 @@
|
||||
|
||||
import React from "react";
|
||||
import { Planet, Starship } from "@/types/swapi";
|
||||
import { t } from "@/content/locales";
|
||||
import { t, LocaleType } from "@/content/locales";
|
||||
import { getPlanetTheme } from "@/utils/getPlanetTheme";
|
||||
|
||||
interface ActionPanelProps {
|
||||
planet?: Planet | null;
|
||||
enemy?: Starship | null;
|
||||
onAction: () => void;
|
||||
onEscape?: () => void;
|
||||
onRepair?: () => void;
|
||||
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({
|
||||
planet,
|
||||
enemy,
|
||||
onAction,
|
||||
onEscape,
|
||||
onRepair,
|
||||
isLoading,
|
||||
isCurrentLocation,
|
||||
currentHp,
|
||||
}: ActionPanelProps) {
|
||||
const isCombat = !!enemy;
|
||||
const theme =
|
||||
!isCombat && planet
|
||||
? getPlanetTheme(planet.terrain, planet.climate)
|
||||
: {
|
||||
|
||||
const planetVisual = getPlanetTheme(
|
||||
planet?.terrain,
|
||||
planet?.climate,
|
||||
t as LocaleType,
|
||||
);
|
||||
|
||||
const uiTheme = isCombat
|
||||
? {
|
||||
color: "text-red-500",
|
||||
border: "border-red-600",
|
||||
glow: "shadow-[0_0_30px_#dc2626]",
|
||||
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 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}`;
|
||||
const stats = isCombat
|
||||
? [
|
||||
{
|
||||
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 (
|
||||
<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
|
||||
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">
|
||||
{/* VISUAL UNIT */}
|
||||
<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`}
|
||||
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 ? (
|
||||
<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
|
||||
className="w-full h-full transition-colors duration-1000"
|
||||
className="w-full h-full transition-all 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",
|
||||
background: `radial-gradient(circle at 30% 30%, ${planetVisual.hex}, #000)`,
|
||||
}}
|
||||
/>
|
||||
<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"}`}
|
||||
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 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 className="flex flex-col md:flex-row items-baseline gap-3">
|
||||
<h3
|
||||
className={`text-3xl font-black tracking-widest uppercase ${isCombat ? "text-red-600" : "text-white"}`}
|
||||
>
|
||||
{displayName}
|
||||
{isCombat ? enemy.name : planet?.name}
|
||||
</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
|
||||
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>
|
||||
</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">
|
||||
<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>
|
||||
{stats.map((stat, idx) => (
|
||||
<div key={idx} className="text-[10px] uppercase">
|
||||
<span className={uiTheme.labelAlpha}>{stat.label}</span>
|
||||
<p className="text-white font-mono truncate">{stat.value}</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>
|
||||
|
||||
{/* BUTTONS */}
|
||||
<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
|
||||
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
|
||||
className={`w-full md:w-auto px-10 py-5 border-2 font-black uppercase tracking-[0.3em] transition-all
|
||||
${
|
||||
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" : ""}`}
|
||||
? "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"
|
||||
}
|
||||
${isLoading ? "opacity-50" : ""}`}
|
||||
>
|
||||
{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
|
||||
)}
|
||||
{isLoading
|
||||
? t.ACTION_PANEL.STATUS.ENGAGING
|
||||
: 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"
|
||||
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}
|
||||
</button>
|
||||
|
||||
@@ -17,7 +17,6 @@ interface EventLogProps {
|
||||
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;
|
||||
@@ -39,14 +38,12 @@ export default function EventLog({ logs }: EventLogProps) {
|
||||
|
||||
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"
|
||||
|
||||
+18
-14
@@ -2,22 +2,24 @@
|
||||
|
||||
import React from "react";
|
||||
import { Planet } from "@/types/swapi";
|
||||
import { t } from "@/content/locales"; // Import der Locales
|
||||
import { t } from "@/content/locales";
|
||||
|
||||
interface GalaxyMapProps {
|
||||
planets: Planet[];
|
||||
onSelectLocation: (planet: Planet) => void;
|
||||
currentLocationId?: string;
|
||||
selectedPlanetId?: string;
|
||||
}
|
||||
|
||||
export default function GalaxyMap({
|
||||
planets,
|
||||
onSelectLocation,
|
||||
currentLocationId,
|
||||
selectedPlanetId,
|
||||
}: 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="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">
|
||||
{/* Background-Grid */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-10 pointer-events-none"
|
||||
style={{
|
||||
@@ -27,40 +29,43 @@ export default function GalaxyMap({
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Die Planeten (Nodes) aus der API */}
|
||||
{planets.map((planet) => {
|
||||
const isCurrent = planet.id === currentLocationId;
|
||||
const isSelected = planet.id === selectedPlanetId;
|
||||
|
||||
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"}`}
|
||||
onClick={() => onSelectLocation(planet)}
|
||||
className="absolute group transform -translate-x-1/2 -translate-y-1/2 transition-all 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"
|
||||
: 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]"
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* 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"}`}
|
||||
${
|
||||
isCurrent
|
||||
? "text-blue-400 font-bold"
|
||||
: isSelected
|
||||
? "text-yellow-500"
|
||||
: "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" />
|
||||
)}
|
||||
@@ -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" />
|
||||
|
||||
{/* 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.OBJECTS_DETECTED(planets.length)} <br />
|
||||
{t.GALAXY_MAP.HOLONET_STATUS}
|
||||
|
||||
@@ -14,8 +14,39 @@ export default function GameOverScreen({
|
||||
}: 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">
|
||||
{/* simple starfield background */}
|
||||
<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}
|
||||
</h1>
|
||||
<p className="text-gray-400 font-mono">{t.GAME_OVER.SUBTITLE}</p>
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ export default function HUD({
|
||||
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 */}
|
||||
{/* LOCATION */}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs uppercase opacity-70">
|
||||
{t.HUD.SECTOR_LABEL}
|
||||
@@ -49,7 +49,7 @@ export default function HUD({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ECONOMY (CREDITS) */}
|
||||
{/* CREDITS */}
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-xs uppercase opacity-70">
|
||||
{t.HUD.GOAL_LABEL}
|
||||
|
||||
@@ -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
@@ -9,49 +9,76 @@ interface StartScreenProps {
|
||||
|
||||
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="fixed inset-0 z-[110] 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)),
|
||||
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">
|
||||
<h1 className="font-starjedi text-6xl text-white">
|
||||
<h1 className="font-starjedi text-6xl text-yellow-500">
|
||||
{t.UI.GAME_TITLE}
|
||||
</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}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-gray-400 font-light leading-relaxed">
|
||||
<p>
|
||||
<div className="space-y-6 text-yellow-500 leading-relaxed">
|
||||
<p className="text-sm tracking-wider font-medium">
|
||||
{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}
|
||||
</span>
|
||||
</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>
|
||||
•{" "}
|
||||
<strong className="text-yellow-500/80">
|
||||
{t.START_SCREEN.FEATURE_NAV_TITLE}
|
||||
</strong>{" "}
|
||||
• <strong>{t.START_SCREEN.FEATURE_NAV_TITLE}</strong>{" "}
|
||||
<span className="opacity-70">
|
||||
{t.START_SCREEN.FEATURE_NAV_DESC}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
•{" "}
|
||||
<strong className="text-yellow-500/80">
|
||||
{t.START_SCREEN.FEATURE_RISK_TITLE}
|
||||
</strong>{" "}
|
||||
• <strong>{t.START_SCREEN.FEATURE_RISK_TITLE}</strong>{" "}
|
||||
<span className="opacity-70">
|
||||
{t.START_SCREEN.FEATURE_RISK_DESC}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
•{" "}
|
||||
<strong className="text-yellow-500/80">
|
||||
{t.START_SCREEN.FEATURE_COMBAT_TITLE}
|
||||
</strong>{" "}
|
||||
• <strong>{t.START_SCREEN.FEATURE_COMBAT_TITLE}</strong>{" "}
|
||||
<span className="opacity-70">
|
||||
{t.START_SCREEN.FEATURE_COMBAT_DESC}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -59,13 +86,12 @@ export default function StartScreen({ onStart }: StartScreenProps) {
|
||||
<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"
|
||||
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>
|
||||
<div className="absolute inset-0 bg-white opacity-0 group-hover:opacity-20 transition-opacity" />
|
||||
{t.UI.START_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}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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
@@ -1,24 +1,24 @@
|
||||
// constants/locales.ts
|
||||
|
||||
export const locales = {
|
||||
de: {
|
||||
en: {
|
||||
UI: {
|
||||
GAME_TITLE: "Smuggler's Run",
|
||||
SUBTITLE: "Fast Ships, faster credits!",
|
||||
START_BUTTON: "Initialize Systems",
|
||||
RESTART_BUTTON: "Klon-Zylinder aktivieren",
|
||||
RESTART_BUTTON: "Activate Clone Cylinder",
|
||||
JUMP_BUTTON: "Initiate Jump",
|
||||
ATTACK_BUTTON: "Feuer frei",
|
||||
ESCAPE_BUTTON: "Fluchtversuch",
|
||||
ATTACK_BUTTON: "Open Fire",
|
||||
ESCAPE_BUTTON: "Evasive Maneuvers",
|
||||
},
|
||||
SYSTEM: {
|
||||
LOADING: "INITIALIZING HOLONET LINK...",
|
||||
DEEP_SPACE: "Deep Space",
|
||||
},
|
||||
START_SCREEN: {
|
||||
SUBTITLE: "Fast Ships, faster credits!",
|
||||
SUBTITLE: "Episode I: Fast Ships, faster credits!",
|
||||
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",
|
||||
FEATURE_NAV_TITLE: "Navigation:",
|
||||
FEATURE_NAV_DESC: "Jump between sectors to deliver cargo.",
|
||||
@@ -59,6 +59,8 @@ export const locales = {
|
||||
FIRE: "Open Fire",
|
||||
JUMP: "Initiate Jump",
|
||||
ESCAPE: "Evasive Maneuvers",
|
||||
REPAIR: (cost: number) => `Repair ship for (${cost} Cr)`,
|
||||
HULL_INTACT: "Hull integrity stable",
|
||||
},
|
||||
},
|
||||
GAME_OVER: {
|
||||
@@ -67,6 +69,13 @@ export const locales = {
|
||||
SCORE_LABEL: "Final Profit",
|
||||
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: {
|
||||
TITLE: "Event Log",
|
||||
EMPTY_STATE: "Waiting for signals...",
|
||||
@@ -74,7 +83,7 @@ export const locales = {
|
||||
HUD: {
|
||||
SECTOR_LABEL: "Current Sector",
|
||||
HULL_LABEL: "Hull Integrity",
|
||||
GOAL_LABEL: "Credits to Freedom", // Oder "Credits to Falcon"
|
||||
GOAL_LABEL: "Credits to Freedom",
|
||||
},
|
||||
GALAXY_MAP: {
|
||||
SCAN_STATUS: "Sector Scan: Active",
|
||||
@@ -102,29 +111,54 @@ export const locales = {
|
||||
SUCCESS: "Jump successful! We've shaken off the pursuer.",
|
||||
},
|
||||
COMBAT: {
|
||||
INIT: (ship: string) =>
|
||||
`Weapon systems synchronized. Opening fire on ${ship}!`,
|
||||
INIT: (ship: string) => `Weapon systems synchronized. Target: ${ship}!`,
|
||||
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) =>
|
||||
`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: {
|
||||
CRITICAL_FAILURE:
|
||||
"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: {
|
||||
ROLLING_1W6: "[Rolling 1D6...]",
|
||||
ROLLING_2W6: "[Rolling 2D6...]",
|
||||
RESULT_1W6: (res: number) => `[Roll: ${res}]`,
|
||||
RESULT_2W6: (d1: number, d2: number, total: number) =>
|
||||
`[Roll: ${d1} + ${d2} = ${total}]`,
|
||||
RESULT_TOTAL: (total: number, bonus: number) =>
|
||||
`[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 const t = locales.de;
|
||||
export type LocaleType = typeof locales.en;
|
||||
|
||||
// Defaulting to English
|
||||
export const t = locales.en;
|
||||
|
||||
+154
-53
@@ -1,26 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { Planet, Starship } from "@/types/swapi";
|
||||
import { fetchPlanets, fetchStarships } from "@/services/swapi";
|
||||
import { fetchGameData } from "@/services/swapi";
|
||||
import { LogEntry } from "@/components/EventLog";
|
||||
import { t } from "@/content/locales";
|
||||
|
||||
export function useGameEngine() {
|
||||
const [allPlanets, setAllPlanets] = useState<Planet[]>([]);
|
||||
const [planets, setPlanets] = useState<Planet[]>([]);
|
||||
const [starships, setStarships] = useState<Starship[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const [gameStarted, setGameStarted] = useState(false);
|
||||
const [isGameOver, setIsGameOver] = useState(false);
|
||||
const [isVictory, setIsVictory] = useState(false);
|
||||
const [credits, setCredits] = useState<number>(100);
|
||||
const [hp, setHp] = useState<number>(10);
|
||||
const [currentLocationId, setCurrentLocationId] = useState<string>("");
|
||||
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 [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(
|
||||
(message: string, type: LogEntry["type"] = "info") => {
|
||||
const newLog: LogEntry = {
|
||||
@@ -37,25 +64,32 @@ export function useGameEngine() {
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
async function initGame() {
|
||||
try {
|
||||
const [planets, ships] = await Promise.all([
|
||||
fetchPlanets(),
|
||||
fetchStarships(),
|
||||
]);
|
||||
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);
|
||||
// --- Actions ---
|
||||
|
||||
const updatePower = (newSettings: typeof power) => {
|
||||
if (!activeEnemy) {
|
||||
setPower(newSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}, [addLog]);
|
||||
setIsRerouting(false);
|
||||
}, 600);
|
||||
};
|
||||
|
||||
const startGame = () => {
|
||||
setGameStarted(true);
|
||||
@@ -64,8 +98,7 @@ export function useGameEngine() {
|
||||
|
||||
const executeJump = async () => {
|
||||
if (!selectedPlanet || selectedPlanet.id === currentLocationId) return;
|
||||
|
||||
setIsJumping(true);
|
||||
setIsProcessing(true);
|
||||
addLog(
|
||||
`${t.ENGINE.JUMP.INIT(selectedPlanet.name)} ${t.ENGINE.DICE.ROLLING_1W6}`,
|
||||
"info",
|
||||
@@ -74,7 +107,6 @@ export function useGameEngine() {
|
||||
setTimeout(() => {
|
||||
const roll = Math.floor(Math.random() * 6) + 1;
|
||||
const rollInfo = t.ENGINE.DICE.RESULT_1W6(roll);
|
||||
|
||||
if (roll >= 5) {
|
||||
const randomShip =
|
||||
starships[Math.floor(Math.random() * starships.length)];
|
||||
@@ -92,65 +124,113 @@ export function useGameEngine() {
|
||||
"success",
|
||||
);
|
||||
}
|
||||
setIsJumping(false);
|
||||
setIsProcessing(false);
|
||||
setSelectedPlanet(null);
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const executeEscape = () => {
|
||||
if (!activeEnemy) return;
|
||||
addLog(`${t.ENGINE.ESCAPE.INIT} ${t.ENGINE.DICE.ROLLING_1W6}`, "info");
|
||||
|
||||
setIsProcessing(true);
|
||||
addLog(t.ENGINE.ESCAPE.INIT, "info");
|
||||
setTimeout(() => {
|
||||
const roll = Math.floor(Math.random() * 6) + 1;
|
||||
const rollInfo = t.ENGINE.DICE.RESULT_1W6(roll);
|
||||
|
||||
if (roll <= 2) {
|
||||
const damage = 1;
|
||||
setHp((prev) => Math.max(0, prev - damage));
|
||||
const engineBonus = Math.floor(power.engines / 2);
|
||||
const total = roll + engineBonus;
|
||||
if (total >= 5) {
|
||||
setActiveEnemy(null);
|
||||
addLog(
|
||||
`${rollInfo} ${t.ENGINE.ESCAPE.FAIL(activeEnemy.name, damage)}`,
|
||||
"danger",
|
||||
`${t.ENGINE.DICE.RESULT_TOTAL(total, engineBonus)} ${t.ENGINE.ESCAPE.SUCCESS}`,
|
||||
"success",
|
||||
);
|
||||
} else {
|
||||
setActiveEnemy(null);
|
||||
addLog(`${rollInfo} ${t.ENGINE.ESCAPE.SUCCESS}`, "success");
|
||||
const damage = 2;
|
||||
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);
|
||||
};
|
||||
|
||||
const handleAttack = () => {
|
||||
if (!activeEnemy) return;
|
||||
addLog(
|
||||
`${t.ENGINE.COMBAT.INIT(activeEnemy.name)} ${t.ENGINE.DICE.ROLLING_2W6}`,
|
||||
"info",
|
||||
);
|
||||
|
||||
setIsProcessing(true);
|
||||
addLog(t.ENGINE.COMBAT.INIT(activeEnemy.name), "info");
|
||||
setTimeout(() => {
|
||||
const d1 = Math.floor(Math.random() * 6) + 1;
|
||||
const d2 = Math.floor(Math.random() * 6) + 1;
|
||||
const total = d1 + d2;
|
||||
const rollInfo = t.ENGINE.DICE.RESULT_2W6(d1, d2, total);
|
||||
|
||||
const laserBonus = Math.floor(power.lasers / 2);
|
||||
const total = d1 + d2 + laserBonus;
|
||||
if (total >= activeEnemy.ds) {
|
||||
const newHp = (activeEnemy.hp || 1) - 1;
|
||||
if (newHp <= 0) {
|
||||
const loot = 300;
|
||||
setCredits((prev) => prev + loot);
|
||||
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(
|
||||
`${rollInfo} ${t.ENGINE.COMBAT.VICTORY(activeEnemy.name, loot)}`,
|
||||
t.ENGINE.REPAIR.SUCCESS(currentPlanet?.name || "Unknown", cost),
|
||||
"success",
|
||||
);
|
||||
} else {
|
||||
const diff = activeEnemy.ds - total;
|
||||
setHp((prev) => Math.max(0, prev - diff));
|
||||
addLog(
|
||||
`${rollInfo} ${t.ENGINE.COMBAT.COUNTER(activeEnemy.name, diff)}`,
|
||||
"danger",
|
||||
);
|
||||
addLog(t.ENGINE.REPAIR.INSUFFICIENT_FUNDS, "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(() => {
|
||||
if (hp <= 0 && !isGameOver) {
|
||||
setIsGameOver(true);
|
||||
@@ -158,14 +238,27 @@ export function useGameEngine() {
|
||||
}
|
||||
}, [hp, isGameOver, addLog]);
|
||||
|
||||
useEffect(() => {
|
||||
if (credits >= WIN_THRESHOLD && !isVictory) {
|
||||
setIsVictory(true);
|
||||
addLog(t.VICTORY.MESSAGE, "success");
|
||||
}
|
||||
}, [credits, isVictory, addLog]);
|
||||
|
||||
const restartGame = () => {
|
||||
setHp(10);
|
||||
setCredits(100);
|
||||
setPower({ engines: 3, lasers: 3, shields: 3 });
|
||||
setIsGameOver(false);
|
||||
setIsVictory(false);
|
||||
setActiveEnemy(null);
|
||||
setSelectedPlanet(null);
|
||||
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.SYSTEMS.SECTOR_UPDATE, "info");
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -177,15 +270,23 @@ export function useGameEngine() {
|
||||
credits,
|
||||
hp,
|
||||
currentLocationId,
|
||||
currentPlanet,
|
||||
selectedPlanet,
|
||||
setSelectedPlanet,
|
||||
executeEscape,
|
||||
executeJump,
|
||||
handleAttack,
|
||||
isJumping,
|
||||
handleRepair,
|
||||
isJumping: isProcessing,
|
||||
isRerouting,
|
||||
activeEnemy,
|
||||
power,
|
||||
updatePower,
|
||||
maxPower: MAX_POWER,
|
||||
logs,
|
||||
isGameOver,
|
||||
restartGame,
|
||||
isVictory,
|
||||
WIN_THRESHOLD,
|
||||
};
|
||||
}
|
||||
|
||||
Binary file not shown.
+70
-21
@@ -1,35 +1,84 @@
|
||||
import { SWAPIPlanet, SWAPIStarship, Planet, Starship } from "@/types/swapi";
|
||||
|
||||
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[]> {
|
||||
const res = await fetch(`${BASE_URL}/planets/`);
|
||||
if (!res.ok) throw new Error("Galaxy data unavailable");
|
||||
interface GameCache {
|
||||
planets: Planet[];
|
||||
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();
|
||||
allResults = [...allResults, ...data.results];
|
||||
nextUrl = data.next;
|
||||
}
|
||||
return allResults;
|
||||
}
|
||||
|
||||
return data.results.map(
|
||||
(p: SWAPIPlanet): Planet => ({
|
||||
export async function fetchGameData(): Promise<{
|
||||
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,
|
||||
id: p.url,
|
||||
// Add random x/y coordinates for galaxy map
|
||||
x: Math.floor(Math.random() * 80) + 10,
|
||||
y: Math.floor(Math.random() * 70) + 15,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
export async function fetchStarships(): Promise<Starship[]> {
|
||||
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 starships: Starship[] = rawStarships.map((s) => {
|
||||
const cost = parseInt(s.cost_in_credits);
|
||||
let ds = 8;
|
||||
if (isNaN(cost) || cost > 1000000) ds = 12;
|
||||
else if (cost > 100000) ds = 10;
|
||||
let ds = 8,
|
||||
hp = 2;
|
||||
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
@@ -1,7 +1,4 @@
|
||||
// types/swapi.ts
|
||||
|
||||
// Die korrigierten Rohdaten von der SWAPI
|
||||
export interface SWAPIPlanet { // <-- Jetzt mit korrektem 'n'
|
||||
export interface SWAPIPlanet {
|
||||
name: string;
|
||||
rotation_period: string;
|
||||
orbital_period: string;
|
||||
@@ -28,7 +25,6 @@ export interface SWAPIStarship {
|
||||
url: string;
|
||||
}
|
||||
|
||||
// Unsere erweiterten Typen für das Spiel
|
||||
export interface Planet extends SWAPIPlanet {
|
||||
id: string;
|
||||
x: number;
|
||||
@@ -37,5 +33,6 @@ export interface Planet extends SWAPIPlanet {
|
||||
|
||||
export interface Starship extends SWAPIStarship {
|
||||
id: string;
|
||||
ds: number; // Difficulty Score für Tunnel Goons
|
||||
ds: number;
|
||||
hp: number;
|
||||
}
|
||||
@@ -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",
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user