mvp
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
// app/Apitest.tsx
|
||||
|
||||
export default async function Apitest() {
|
||||
// 1. Fetch the data directly in the component
|
||||
const res = await fetch('https://swapi.py4e.com/api/people/1/');
|
||||
const data = await res.json();
|
||||
|
||||
// 2. This console.log will appear in your TERMINAL, not the browser console
|
||||
// because this is a Server Component!
|
||||
console.log("Fetched Pilot:", data.name);
|
||||
|
||||
return (
|
||||
<div className="p-4 border border-yellow-500 rounded-md bg-black/20">
|
||||
<h2 className="text-xl font-bold text-yellow-400">Smuggler Intel</h2>
|
||||
<p>Name: {data.name}</p>
|
||||
<p>Height: {data.height}cm</p>
|
||||
<p>Mass: {data.mass}kg</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,11 @@
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme {
|
||||
/* Hier registrierst du die Font für Tailwind v4 */
|
||||
--font-starjedi: var(--font-starjedi);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
|
||||
+10
-3
@@ -1,5 +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 "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
@@ -12,9 +13,15 @@ const geistMono = Geist_Mono({
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
// Konfiguration der lokalen Star Jedi Font
|
||||
const starJedi = localFont({
|
||||
src: "../public/fonts/Starjedi.ttf",
|
||||
variable: "--font-starjedi",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Outer Rim Smuggler",
|
||||
description: "A Star Wars inspired roguelike adventure",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -23,7 +30,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" className={`${starJedi.variable}`}>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
|
||||
+80
-66
@@ -1,73 +1,87 @@
|
||||
import Image from "next/image";
|
||||
import Apitest from "./components/apitest";
|
||||
"use client";
|
||||
|
||||
import { useGameEngine } from "@/hooks/useGameEngine";
|
||||
import HUD from "../components/Hud";
|
||||
import GalaxyMap from "../components/GalaxyMap";
|
||||
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
|
||||
|
||||
export default function Home() {
|
||||
const {
|
||||
gameStarted,
|
||||
startGame,
|
||||
planets,
|
||||
credits,
|
||||
hp,
|
||||
loading,
|
||||
currentLocationId,
|
||||
selectedPlanet,
|
||||
setSelectedPlanet,
|
||||
handleAttack,
|
||||
executeJump,
|
||||
isJumping,
|
||||
activeEnemy,
|
||||
executeEscape,
|
||||
logs,
|
||||
isGameOver,
|
||||
restartGame,
|
||||
} = useGameEngine();
|
||||
|
||||
|
||||
|
||||
// 1. Ladezustand
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
|
||||
{/* Add your component here */}
|
||||
<Apitest />
|
||||
|
||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the page.tsx file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<div className="min-h-screen bg-black flex items-center justify-center font-mono text-yellow-500">
|
||||
<div className="animate-pulse tracking-[0.3em]">{t.SYSTEM.LOADING}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Aktuellen Planeten finden für das HUD
|
||||
const currentPlanet = planets.find((p) => p.id === currentLocationId);
|
||||
|
||||
// 2. Startbildschirm
|
||||
if (!gameStarted) {
|
||||
return <StartScreen onStart={startGame} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#050505] pt-24 px-4 flex flex-col items-center">
|
||||
<HUD
|
||||
credits={credits}
|
||||
hp={hp}
|
||||
maxHp={10} // Könnte später aus einem Ship-Objekt kommen
|
||||
location={currentPlanet?.name || t.SYSTEM.DEEP_SPACE}
|
||||
targetCredits={100000}
|
||||
/>
|
||||
|
||||
<div className="w-full max-w-4xl flex flex-col items-center gap-6">
|
||||
<GalaxyMap
|
||||
planets={planets}
|
||||
onSelectLocation={(p) => setSelectedPlanet(p)}
|
||||
currentLocationId={currentLocationId}
|
||||
/>
|
||||
|
||||
{/* ActionPanel erscheint bei Zielwahl oder Feindkontakt */}
|
||||
{(selectedPlanet || activeEnemy) && (
|
||||
<ActionPanel
|
||||
planet={selectedPlanet}
|
||||
enemy={activeEnemy}
|
||||
isLoading={isJumping}
|
||||
onAction={activeEnemy ? handleAttack : executeJump}
|
||||
onEscape={executeEscape}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<EventLog logs={logs} />
|
||||
|
||||
{/* Game Over Overlay */}
|
||||
{isGameOver && (
|
||||
<GameOverScreen credits={credits} onRestart={restartGame} />
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Planet, Starship } from "@/types/swapi";
|
||||
import { t } from "@/content/locales";
|
||||
|
||||
interface ActionPanelProps {
|
||||
planet?: Planet | null;
|
||||
enemy?: Starship | null;
|
||||
onAction: () => void;
|
||||
onEscape?: () => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const getPlanetTheme = (terrain: string = "", climate: string = "") => {
|
||||
const terr = terrain.toLowerCase();
|
||||
const clim = climate.toLowerCase();
|
||||
|
||||
if (terr.includes("ocean") || terr.includes("water"))
|
||||
return {
|
||||
color: "text-blue-400",
|
||||
border: "border-blue-500",
|
||||
glow: "shadow-[0_0_20px_#3b82f6]",
|
||||
label: t.ACTION_PANEL.MODES.AQUATIC,
|
||||
};
|
||||
if (terr.includes("forest") || terr.includes("jungle"))
|
||||
return {
|
||||
color: "text-green-400",
|
||||
border: "border-green-500",
|
||||
glow: "shadow-[0_0_20px_#22c55e]",
|
||||
label: t.ACTION_PANEL.MODES.VERDANT,
|
||||
};
|
||||
if (terr.includes("desert") || clim.includes("arid"))
|
||||
return {
|
||||
color: "text-orange-400",
|
||||
border: "border-orange-500",
|
||||
glow: "shadow-[0_0_20px_#f97316]",
|
||||
label: t.ACTION_PANEL.MODES.ARID,
|
||||
};
|
||||
if (
|
||||
terr.includes("ice") ||
|
||||
terr.includes("glacier") ||
|
||||
clim.includes("frozen")
|
||||
)
|
||||
return {
|
||||
color: "text-cyan-200",
|
||||
border: "border-cyan-300",
|
||||
glow: "shadow-[0_0_20px_#a5f3fc]",
|
||||
label: t.ACTION_PANEL.MODES.CRYO,
|
||||
};
|
||||
if (terr.includes("city") || terr.includes("urban"))
|
||||
return {
|
||||
color: "text-purple-400",
|
||||
border: "border-purple-500",
|
||||
glow: "shadow-[0_0_20px_#a855f7]",
|
||||
label: t.ACTION_PANEL.MODES.ECUMENOPOLIS,
|
||||
};
|
||||
|
||||
return {
|
||||
color: "text-yellow-500",
|
||||
border: "border-yellow-500",
|
||||
glow: "shadow-[0_0_20px_#eab308]",
|
||||
label: t.ACTION_PANEL.MODES.STANDARD,
|
||||
};
|
||||
};
|
||||
|
||||
export default function ActionPanel({
|
||||
planet,
|
||||
enemy,
|
||||
onAction,
|
||||
onEscape,
|
||||
isLoading,
|
||||
}: ActionPanelProps) {
|
||||
const isCombat = !!enemy;
|
||||
const theme =
|
||||
!isCombat && planet
|
||||
? getPlanetTheme(planet.terrain, planet.climate)
|
||||
: {
|
||||
color: "text-red-500",
|
||||
border: "border-red-600",
|
||||
glow: "shadow-[0_0_30px_#dc2626]",
|
||||
label: t.ACTION_PANEL.MODES.HOSTILE,
|
||||
};
|
||||
|
||||
const displayName = isCombat ? enemy.name : planet?.name;
|
||||
const displaySubtitle = isCombat
|
||||
? `${t.ACTION_PANEL.LABELS.CLASS}: ${enemy.starship_class} // ${t.ACTION_PANEL.LABELS.CREW}: ${enemy.crew}`
|
||||
: `${t.ACTION_PANEL.LABELS.SECTOR}: ${planet?.climate} // ${planet?.terrain}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full max-w-4xl mt-6 bg-black/90 backdrop-blur-md border-2 rounded-lg p-6 shadow-2xl animate-in fade-in slide-in-from-bottom-4 relative overflow-hidden transition-colors duration-500 ${theme.border}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute inset-0 bg-[linear-gradient(transparent_0%,${isCombat ? "rgba(220,38,38,0.1)" : "rgba(234,179,8,0.05)"}_50%,transparent_100%)] bg-[length:100%_4px] animate-pulse pointer-events-none`}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-8 items-center relative z-10">
|
||||
<div className="relative shrink-0">
|
||||
<div
|
||||
className={`w-32 h-32 rounded-full border-4 ${theme.border} ${theme.glow} flex items-center justify-center transition-all duration-700 bg-black overflow-hidden relative`}
|
||||
>
|
||||
{isCombat ? (
|
||||
<div className="text-red-600 animate-pulse font-black text-5xl drop-shadow-[0_0_10px_rgba(220,38,38,0.8)]">
|
||||
!
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className="w-full h-full transition-colors duration-1000"
|
||||
style={{
|
||||
background: `radial-gradient(circle at 30% 30%, ${theme.color.replace("text-", "")}, #000)`,
|
||||
backgroundColor: theme.color.includes("blue")
|
||||
? "#60a5fa"
|
||||
: theme.color.includes("green")
|
||||
? "#4ade80"
|
||||
: theme.color.includes("orange")
|
||||
? "#fb923c"
|
||||
: theme.color.includes("cyan")
|
||||
? "#22d3ee"
|
||||
: theme.color.includes("purple")
|
||||
? "#c084fc"
|
||||
: "#eab308",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-black/60 via-transparent to-white/20 pointer-events-none" />
|
||||
<div className="absolute inset-0 opacity-20 bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')] animate-spin-slow mix-blend-overlay" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`absolute -top-2 -right-2 bg-black border px-2 py-0.5 text-[8px] uppercase tracking-widest ${isCombat ? "border-red-600 text-red-600" : "border-yellow-500/50 text-yellow-500"}`}
|
||||
>
|
||||
{theme.label}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 text-center md:text-left space-y-3">
|
||||
<div>
|
||||
<h3
|
||||
className={`text-3xl font-black tracking-widest uppercase ${isCombat ? "text-red-600" : "text-white"}`}
|
||||
>
|
||||
{displayName}
|
||||
</h3>
|
||||
<p
|
||||
className={`${theme.color} text-xs font-bold uppercase tracking-widest opacity-80`}
|
||||
>
|
||||
{displaySubtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`grid grid-cols-2 gap-4 border-t border-b py-3 ${isCombat ? "border-red-900/30" : "border-yellow-500/10"}`}
|
||||
>
|
||||
<div className="text-[10px] uppercase">
|
||||
<span
|
||||
className={isCombat ? "text-red-500/50" : "text-yellow-500/50"}
|
||||
>
|
||||
{isCombat
|
||||
? t.ACTION_PANEL.LABELS.MANUFACTURER
|
||||
: t.ACTION_PANEL.LABELS.POPULATION}
|
||||
</span>
|
||||
<p className="text-white font-mono truncate">
|
||||
{isCombat
|
||||
? enemy.manufacturer
|
||||
: planet?.population.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-[10px] uppercase">
|
||||
<span
|
||||
className={isCombat ? "text-red-500/50" : "text-yellow-500/50"}
|
||||
>
|
||||
{isCombat
|
||||
? t.ACTION_PANEL.LABELS.COST
|
||||
: t.ACTION_PANEL.LABELS.GRAVITY}
|
||||
</span>
|
||||
<p className="text-white font-mono">
|
||||
{isCombat ? `${enemy.cost_in_credits} Cr` : planet?.gravity}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className={`text-xs italic leading-relaxed ${isCombat ? "text-red-400/60" : "text-yellow-500/60"}`}
|
||||
>
|
||||
{isCombat
|
||||
? t.ACTION_PANEL.STATUS.COMBAT_WARNING
|
||||
: t.ACTION_PANEL.STATUS.PLANET_SCAN}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 shrink-0 w-full md:w-auto">
|
||||
<button
|
||||
onClick={onAction}
|
||||
disabled={isLoading}
|
||||
className={`w-full md:w-auto px-10 py-5 border-2 font-black uppercase tracking-[0.3em] transition-all relative group
|
||||
${
|
||||
isCombat
|
||||
? "border-red-600 text-red-600 hover:bg-red-600 hover:text-white shadow-[0_0_15px_rgba(220,38,38,0.3)]"
|
||||
: "border-yellow-500 text-yellow-500 hover:bg-yellow-500 hover:text-black hover:shadow-[0_0_30px_#eab308]"
|
||||
} ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span
|
||||
className={`animate-ping inline-flex h-2 w-2 rounded-full ${isCombat ? "bg-red-600" : "bg-yellow-500"}`}
|
||||
></span>
|
||||
{t.ACTION_PANEL.STATUS.ENGAGING}
|
||||
</span>
|
||||
) : isCombat ? (
|
||||
t.ACTION_PANEL.BUTTONS.FIRE
|
||||
) : (
|
||||
t.ACTION_PANEL.BUTTONS.JUMP
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isCombat && (
|
||||
<button
|
||||
onClick={onEscape}
|
||||
className="px-10 py-2 border border-white/20 text-white/40 text-[10px] uppercase tracking-widest hover:bg-white/5 hover:text-white transition-all"
|
||||
>
|
||||
{t.ACTION_PANEL.BUTTONS.ESCAPE}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { t } from "@/content/locales"; // Import der Locales
|
||||
|
||||
export interface LogEntry {
|
||||
id: string;
|
||||
message: string;
|
||||
type: "info" | "success" | "danger" | "warning";
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface EventLogProps {
|
||||
logs: LogEntry[];
|
||||
}
|
||||
|
||||
export default function EventLog({ logs }: EventLogProps) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Auto-Scroll nach unten bei neuen Einträgen
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [logs]);
|
||||
|
||||
const getTypeStyles = (type: LogEntry["type"]) => {
|
||||
switch (type) {
|
||||
case "success":
|
||||
return "text-green-400";
|
||||
case "danger":
|
||||
return "text-red-500 font-bold animate-pulse";
|
||||
case "warning":
|
||||
return "text-orange-400";
|
||||
default:
|
||||
return "text-yellow-500/80";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-6 right-6 w-full max-w-md h-48 bg-black/80 border border-yellow-500/30 rounded-lg shadow-2xl overflow-hidden flex flex-col backdrop-blur-md z-40">
|
||||
{/* Header */}
|
||||
<div className="bg-yellow-500/10 px-4 py-1 border-b border-yellow-500/20 flex justify-between items-center">
|
||||
<span className="text-[10px] font-mono text-yellow-500 uppercase tracking-widest">
|
||||
{t.EVENT_LOG.TITLE}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Scrollable Content */}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex-1 overflow-y-auto p-4 font-mono text-sm scrollbar-thin scrollbar-thumb-yellow-500/20"
|
||||
>
|
||||
{logs.length === 0 && (
|
||||
<p className="text-yellow-500/20 italic text-xs">
|
||||
{t.EVENT_LOG.EMPTY_STATE}
|
||||
</p>
|
||||
)}
|
||||
{logs.map((log) => (
|
||||
<div
|
||||
key={log.id}
|
||||
className="mb-2 flex gap-3 border-l border-yellow-500/10 pl-2"
|
||||
>
|
||||
<span className="text-[10px] opacity-80 mt-1 shrink-0 text-white">
|
||||
{log.timestamp}
|
||||
</span>
|
||||
<span className={`${getTypeStyles(log.type)} leading-tight`}>
|
||||
{log.type === "danger" && "⚠ "}
|
||||
{log.message}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Planet } from "@/types/swapi";
|
||||
import { t } from "@/content/locales"; // Import der Locales
|
||||
|
||||
interface GalaxyMapProps {
|
||||
planets: Planet[];
|
||||
onSelectLocation: (planet: Planet) => void;
|
||||
currentLocationId?: string;
|
||||
}
|
||||
|
||||
export default function GalaxyMap({
|
||||
planets,
|
||||
onSelectLocation,
|
||||
currentLocationId,
|
||||
}: GalaxyMapProps) {
|
||||
return (
|
||||
<div className="relative w-full max-w-4xl h-[500px] bg-[#080808] border-2 border-yellow-500/20 rounded-xl overflow-hidden shadow-[inset_0_0_50px_rgba(0,0,0,1)] z-10">
|
||||
{/* Hintergrund-Raster (Grid) */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-10 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(#eab308 1px, transparent 1px), linear-gradient(90deg, #eab308 1px, transparent 1px)",
|
||||
backgroundSize: "40px 40px",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Die Planeten (Nodes) aus der API */}
|
||||
{planets.map((planet) => {
|
||||
const isCurrent = planet.id === currentLocationId;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={planet.id}
|
||||
onClick={() => !isCurrent && onSelectLocation(planet)}
|
||||
disabled={isCurrent}
|
||||
className={`absolute group transform -translate-x-1/2 -translate-y-1/2 transition-all
|
||||
${isCurrent ? "cursor-default" : "cursor-pointer hover:scale-110"}`}
|
||||
style={{ left: `${planet.x}%`, top: `${planet.y}%` }}
|
||||
>
|
||||
{/* Planet Visuell */}
|
||||
<div
|
||||
className={`w-4 h-4 rounded-full border-2 transition-all duration-300
|
||||
${
|
||||
isCurrent
|
||||
? "bg-blue-500 border-white shadow-[0_0_15px_#3b82f6] scale-125"
|
||||
: "bg-yellow-500/20 border-yellow-500 group-hover:bg-yellow-500 group-hover:shadow-[0_0_10px_#eab308]"
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* Label */}
|
||||
<div className="absolute top-6 left-1/2 -translate-x-1/2 whitespace-nowrap">
|
||||
<span
|
||||
className={`text-[10px] font-mono uppercase tracking-tighter px-1 rounded bg-black/50
|
||||
${isCurrent ? "text-blue-400 font-bold" : "text-yellow-500/60 group-hover:text-yellow-500"}`}
|
||||
>
|
||||
{planet.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Pulsierender Ring für aktuellen Standort */}
|
||||
{isCurrent && (
|
||||
<div className="absolute -inset-2 border border-blue-500/50 rounded-full animate-ping pointer-events-none" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Scan-Linie Effekt */}
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-yellow-500/5 opacity-20 animate-scan pointer-events-none" />
|
||||
|
||||
{/* Info Overlay */}
|
||||
<div className="absolute bottom-4 left-4 text-[10px] text-yellow-500/40 font-mono uppercase pointer-events-none bg-black/40 p-2 rounded">
|
||||
{t.GALAXY_MAP.SCAN_STATUS} <br />
|
||||
{t.GALAXY_MAP.OBJECTS_DETECTED(planets.length)} <br />
|
||||
{t.GALAXY_MAP.HOLONET_STATUS}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { t } from "@/content/locales";
|
||||
|
||||
interface GameOverScreenProps {
|
||||
credits: number;
|
||||
onRestart: () => void;
|
||||
}
|
||||
|
||||
export default function GameOverScreen({
|
||||
credits,
|
||||
onRestart,
|
||||
}: GameOverScreenProps) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 backdrop-blur-xl animate-in fade-in duration-1000">
|
||||
<div className="text-center space-y-6 p-12 border-2 border-red-600 bg-red-950/20 rounded-lg shadow-[0_0_50px_rgba(220,38,38,0.5)]">
|
||||
<h1 className="text-6xl font-black text-red-600 tracking-tighter uppercase italic">
|
||||
{t.GAME_OVER.TITLE}
|
||||
</h1>
|
||||
<p className="text-gray-400 font-mono">{t.GAME_OVER.SUBTITLE}</p>
|
||||
|
||||
<div className="py-4">
|
||||
<p className="text-sm text-gray-500 uppercase tracking-widest">
|
||||
{t.GAME_OVER.SCORE_LABEL}
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-yellow-500">
|
||||
{credits.toLocaleString()} Credits
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onRestart}
|
||||
className="px-8 py-4 bg-red-600 hover:bg-red-700 text-white font-bold uppercase tracking-widest transition-all transform hover:scale-105 active:scale-95 shadow-[0_0_20px_rgba(220,38,38,0.4)]"
|
||||
>
|
||||
{t.GAME_OVER.RESTART_BUTTON}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { t } from "@/content/locales";
|
||||
|
||||
interface HUDProps {
|
||||
credits: number;
|
||||
hp: number;
|
||||
maxHp: number;
|
||||
location: string;
|
||||
targetCredits: number;
|
||||
}
|
||||
|
||||
export default function HUD({
|
||||
credits,
|
||||
hp,
|
||||
maxHp,
|
||||
location,
|
||||
targetCredits,
|
||||
}: HUDProps) {
|
||||
const progress = Math.min((credits / targetCredits) * 100, 100);
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 left-0 w-full bg-black border-b-2 border-yellow-500/50 p-4 font-mono text-yellow-500 z-50 shadow-[0_0_15px_rgba(234,179,8,0.2)]">
|
||||
<div className="max-w-7xl mx-auto flex flex-wrap justify-between items-center gap-4">
|
||||
{/* LORE & LOCATION */}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs uppercase opacity-70">
|
||||
{t.HUD.SECTOR_LABEL}
|
||||
</span>
|
||||
<span className="text-xl font-bold tracking-widest">
|
||||
{location.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* SHIP STATUS (HEALTH) */}
|
||||
<div className="flex flex-col min-w-[200px]">
|
||||
<div className="flex justify-between text-xs uppercase mb-1">
|
||||
<span>{t.HUD.HULL_LABEL}</span>
|
||||
<span className={hp <= 3 ? "text-red-500 animate-pulse" : ""}>
|
||||
{hp} / {maxHp}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-3 bg-gray-900 border border-yellow-500/30">
|
||||
<div
|
||||
className={`h-full transition-all duration-500 ${hp <= 3 ? "bg-red-600" : "bg-yellow-500"}`}
|
||||
style={{ width: `${(hp / maxHp) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ECONOMY (CREDITS) */}
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-xs uppercase opacity-70">
|
||||
{t.HUD.GOAL_LABEL}
|
||||
</span>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-2xl font-bold text-green-400">
|
||||
{credits.toLocaleString()}
|
||||
</span>
|
||||
<span className="text-xs opacity-50">
|
||||
/ {targetCredits.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
{/* Progress Bar for the Goal */}
|
||||
<div className="w-32 h-1 bg-gray-800 mt-1">
|
||||
<div
|
||||
className="h-full bg-green-500 shadow-[0_0_5px_#22c55e]"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { t } from "@/content/locales";
|
||||
|
||||
interface StartScreenProps {
|
||||
onStart: () => void;
|
||||
}
|
||||
|
||||
export default function StartScreen({ onStart }: StartScreenProps) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-[#050505] overflow-hidden">
|
||||
{/* Hintergrund-Deko */}
|
||||
<div className="absolute inset-0 opacity-20 bg-[radial-gradient(circle_at_center,_#eab308_0%,_transparent_70%)]" />
|
||||
|
||||
<div className="max-w-2xl w-full mx-4 relative z-10 text-center space-y-8 p-10 border border-yellow-500/20 bg-black/60 backdrop-blur-md rounded-2xl shadow-[0_0_100px_rgba(234,179,8,0.1)]">
|
||||
<div className="space-y-2">
|
||||
<h1 className="font-starjedi text-6xl text-white">
|
||||
{t.UI.GAME_TITLE}
|
||||
</h1>
|
||||
<p className="text-yellow-500/60 font-mono tracking-[0.3em] uppercase text-xs">
|
||||
{t.START_SCREEN.SUBTITLE}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-gray-400 font-light leading-relaxed">
|
||||
<p>
|
||||
{t.START_SCREEN.INTRO_TEXT}
|
||||
<span className="text-white font-medium">
|
||||
{" "}
|
||||
{t.START_SCREEN.MISSION_HIGHLIGHT}
|
||||
</span>
|
||||
</p>
|
||||
<ul className="text-sm space-y-2 inline-block text-left border-l-2 border-yellow-500/30 pl-6">
|
||||
<li>
|
||||
•{" "}
|
||||
<strong className="text-yellow-500/80">
|
||||
{t.START_SCREEN.FEATURE_NAV_TITLE}
|
||||
</strong>{" "}
|
||||
{t.START_SCREEN.FEATURE_NAV_DESC}
|
||||
</li>
|
||||
<li>
|
||||
•{" "}
|
||||
<strong className="text-yellow-500/80">
|
||||
{t.START_SCREEN.FEATURE_RISK_TITLE}
|
||||
</strong>{" "}
|
||||
{t.START_SCREEN.FEATURE_RISK_DESC}
|
||||
</li>
|
||||
<li>
|
||||
•{" "}
|
||||
<strong className="text-yellow-500/80">
|
||||
{t.START_SCREEN.FEATURE_COMBAT_TITLE}
|
||||
</strong>{" "}
|
||||
{t.START_SCREEN.FEATURE_COMBAT_DESC}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="pt-6">
|
||||
<button
|
||||
onClick={onStart}
|
||||
className="group relative px-12 py-5 bg-yellow-500 text-black font-black uppercase tracking-[0.2em] transition-all hover:scale-105 hover:shadow-[0_0_40px_rgba(234,179,8,0.4)] overflow-hidden"
|
||||
>
|
||||
<span className="relative z-10">{t.UI.START_BUTTON}</span>
|
||||
<div className="absolute inset-0 bg-white opacity-0 group-hover:opacity-20 transition-opacity" />
|
||||
</button>
|
||||
|
||||
<p className="mt-4 text-[10px] text-gray-600 uppercase tracking-widest">
|
||||
{t.START_SCREEN.FOOTER_ENGINE}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// constants/locales.ts
|
||||
|
||||
export const locales = {
|
||||
de: {
|
||||
UI: {
|
||||
GAME_TITLE: "Smuggler's Run",
|
||||
SUBTITLE: "Fast Ships, faster credits!",
|
||||
START_BUTTON: "Initialize Systems",
|
||||
RESTART_BUTTON: "Klon-Zylinder aktivieren",
|
||||
JUMP_BUTTON: "Initiate Jump",
|
||||
ATTACK_BUTTON: "Feuer frei",
|
||||
ESCAPE_BUTTON: "Fluchtversuch",
|
||||
},
|
||||
SYSTEM: {
|
||||
LOADING: "INITIALIZING HOLONET LINK...",
|
||||
DEEP_SPACE: "Deep Space",
|
||||
},
|
||||
START_SCREEN: {
|
||||
SUBTITLE: "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:",
|
||||
MISSION_HIGHLIGHT: "Survive and earn credits",
|
||||
FEATURE_NAV_TITLE: "Navigation:",
|
||||
FEATURE_NAV_DESC: "Jump between sectors to deliver cargo.",
|
||||
FEATURE_RISK_TITLE: "Risk:",
|
||||
FEATURE_RISK_DESC:
|
||||
"Hyperspace is treacherous. Imperial patrols are everywhere.",
|
||||
FEATURE_COMBAT_TITLE: "Combat:",
|
||||
FEATURE_COMBAT_DESC: "Use your 2D6 cannons to survive interceptions.",
|
||||
FOOTER_ENGINE: "Star Wars API v1.0 // Engine: Tunnel Goons",
|
||||
},
|
||||
ACTION_PANEL: {
|
||||
MODES: {
|
||||
HOSTILE: "Hostile",
|
||||
AQUATIC: "Aquatic",
|
||||
VERDANT: "Verdant",
|
||||
ARID: "Arid",
|
||||
CRYO: "Cryo",
|
||||
ECUMENOPOLIS: "Ecumenopolis",
|
||||
STANDARD: "Standard",
|
||||
},
|
||||
LABELS: {
|
||||
CLASS: "Class",
|
||||
CREW: "Crew",
|
||||
SECTOR: "Sector",
|
||||
MANUFACTURER: "Manufacturer",
|
||||
POPULATION: "Population",
|
||||
COST: "Cost",
|
||||
GRAVITY: "Gravity",
|
||||
},
|
||||
STATUS: {
|
||||
COMBAT_WARNING:
|
||||
"WARNING: Hostile contact confirmed. Weapons emitters charging.",
|
||||
PLANET_SCAN:
|
||||
"Receiving encrypted signals from sector. Landing clearance pending...",
|
||||
ENGAGING: "Engaging...",
|
||||
},
|
||||
BUTTONS: {
|
||||
FIRE: "Open Fire",
|
||||
JUMP: "Initiate Jump",
|
||||
ESCAPE: "Evasive Maneuvers",
|
||||
},
|
||||
},
|
||||
GAME_OVER: {
|
||||
TITLE: "Wasted in Space",
|
||||
SUBTITLE: "Your ship has been reduced to stardust.",
|
||||
SCORE_LABEL: "Final Profit",
|
||||
RESTART_BUTTON: "Activate Clone Cylinder",
|
||||
},
|
||||
EVENT_LOG: {
|
||||
TITLE: "Event Log",
|
||||
EMPTY_STATE: "Waiting for signals...",
|
||||
},
|
||||
HUD: {
|
||||
SECTOR_LABEL: "Current Sector",
|
||||
HULL_LABEL: "Hull Integrity",
|
||||
GOAL_LABEL: "Credits to Freedom", // Oder "Credits to Falcon"
|
||||
},
|
||||
GALAXY_MAP: {
|
||||
SCAN_STATUS: "Sector Scan: Active",
|
||||
OBJECTS_DETECTED: (count: number) => `Objects Detected: ${count}`,
|
||||
HOLONET_STATUS: "HoloNet Status: Connected",
|
||||
},
|
||||
ENGINE: {
|
||||
BOOT: {
|
||||
SUCCESS: "HoloNet connection stable. Nav-computer ready.",
|
||||
ERROR: "Critical error loading galaxy data!",
|
||||
WELCOME: "Engines on standby. Nav-computer online. Welcome, Commander.",
|
||||
RESTART: "Systems rebooting... A new journey begins.",
|
||||
},
|
||||
JUMP: {
|
||||
INIT: (target: string) => `Hyperspace jump to ${target} initiated...`,
|
||||
INTERCEPTION: (ship: string) =>
|
||||
`INTERCEPTION! A ${ship} has pulled us out of hyperspace!`,
|
||||
SUCCESS: (target: string, reward: number) =>
|
||||
`Safe exit in the ${target} system. +${reward} Credits earned.`,
|
||||
},
|
||||
ESCAPE: {
|
||||
INIT: "Emergency maneuvers initiated!",
|
||||
FAIL: (ship: string, dmg: number) =>
|
||||
`Escape failed! The ${ship} hits us hard. -${dmg} HP`,
|
||||
SUCCESS: "Jump successful! We've shaken off the pursuer.",
|
||||
},
|
||||
COMBAT: {
|
||||
INIT: (ship: string) =>
|
||||
`Weapon systems synchronized. Opening fire on ${ship}!`,
|
||||
VICTORY: (ship: string, loot: number) =>
|
||||
`Direct hit! The ${ship} has been obliterated. +${loot} Credits salvaged.`,
|
||||
COUNTER: (ship: string, dmg: number) =>
|
||||
`Shields holding! The ${ship} counters. -${dmg} HP`,
|
||||
},
|
||||
SYSTEMS: {
|
||||
CRITICAL_FAILURE:
|
||||
"CRITICAL STRUCTURAL DAMAGE! The ship is breaking apart...",
|
||||
},
|
||||
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}]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Später einfach erweiterbar:
|
||||
// en: { ... }
|
||||
};
|
||||
|
||||
// Aktuelle Sprache festlegen (könnte später aus einem State kommen)
|
||||
export const t = locales.de;
|
||||
@@ -0,0 +1,191 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Planet, Starship } from "@/types/swapi";
|
||||
import { fetchPlanets, fetchStarships } from "@/services/swapi";
|
||||
import { LogEntry } from "@/components/EventLog";
|
||||
import { t } from "@/content/locales";
|
||||
|
||||
export function useGameEngine() {
|
||||
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 [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 [activeEnemy, setActiveEnemy] = useState<Starship | null>(null);
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
|
||||
const addLog = useCallback(
|
||||
(message: string, type: LogEntry["type"] = "info") => {
|
||||
const newLog: LogEntry = {
|
||||
id: Math.random().toString(36).substring(2, 9),
|
||||
message,
|
||||
type,
|
||||
timestamp: new Date().toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}),
|
||||
};
|
||||
setLogs((prev) => [...prev, newLog]);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
initGame();
|
||||
}, [addLog]);
|
||||
|
||||
const startGame = () => {
|
||||
setGameStarted(true);
|
||||
addLog(t.ENGINE.BOOT.WELCOME, "info");
|
||||
};
|
||||
|
||||
const executeJump = async () => {
|
||||
if (!selectedPlanet || selectedPlanet.id === currentLocationId) return;
|
||||
|
||||
setIsJumping(true);
|
||||
addLog(
|
||||
`${t.ENGINE.JUMP.INIT(selectedPlanet.name)} ${t.ENGINE.DICE.ROLLING_1W6}`,
|
||||
"info",
|
||||
);
|
||||
|
||||
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)];
|
||||
setActiveEnemy(randomShip);
|
||||
addLog(
|
||||
`${rollInfo} ${t.ENGINE.JUMP.INTERCEPTION(randomShip.name)}`,
|
||||
"danger",
|
||||
);
|
||||
} else {
|
||||
const reward = 150;
|
||||
setCredits((prev) => prev + reward);
|
||||
setCurrentLocationId(selectedPlanet.id);
|
||||
addLog(
|
||||
`${rollInfo} ${t.ENGINE.JUMP.SUCCESS(selectedPlanet.name, reward)}`,
|
||||
"success",
|
||||
);
|
||||
}
|
||||
setIsJumping(false);
|
||||
setSelectedPlanet(null);
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const executeEscape = () => {
|
||||
if (!activeEnemy) return;
|
||||
addLog(`${t.ENGINE.ESCAPE.INIT} ${t.ENGINE.DICE.ROLLING_1W6}`, "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));
|
||||
addLog(
|
||||
`${rollInfo} ${t.ENGINE.ESCAPE.FAIL(activeEnemy.name, damage)}`,
|
||||
"danger",
|
||||
);
|
||||
} else {
|
||||
setActiveEnemy(null);
|
||||
addLog(`${rollInfo} ${t.ENGINE.ESCAPE.SUCCESS}`, "success");
|
||||
}
|
||||
}, 800);
|
||||
};
|
||||
|
||||
const handleAttack = () => {
|
||||
if (!activeEnemy) return;
|
||||
addLog(
|
||||
`${t.ENGINE.COMBAT.INIT(activeEnemy.name)} ${t.ENGINE.DICE.ROLLING_2W6}`,
|
||||
"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);
|
||||
|
||||
if (total >= activeEnemy.ds) {
|
||||
const loot = 300;
|
||||
setCredits((prev) => prev + loot);
|
||||
setActiveEnemy(null);
|
||||
addLog(
|
||||
`${rollInfo} ${t.ENGINE.COMBAT.VICTORY(activeEnemy.name, loot)}`,
|
||||
"success",
|
||||
);
|
||||
} else {
|
||||
const diff = activeEnemy.ds - total;
|
||||
setHp((prev) => Math.max(0, prev - diff));
|
||||
addLog(
|
||||
`${rollInfo} ${t.ENGINE.COMBAT.COUNTER(activeEnemy.name, diff)}`,
|
||||
"danger",
|
||||
);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hp <= 0 && !isGameOver) {
|
||||
setIsGameOver(true);
|
||||
addLog(t.ENGINE.SYSTEMS.CRITICAL_FAILURE, "danger");
|
||||
}
|
||||
}, [hp, isGameOver, addLog]);
|
||||
|
||||
const restartGame = () => {
|
||||
setHp(10);
|
||||
setCredits(100);
|
||||
setIsGameOver(false);
|
||||
setActiveEnemy(null);
|
||||
setSelectedPlanet(null);
|
||||
setLogs([]);
|
||||
addLog(t.ENGINE.BOOT.RESTART, "success");
|
||||
};
|
||||
|
||||
return {
|
||||
planets,
|
||||
starships,
|
||||
loading,
|
||||
gameStarted,
|
||||
startGame,
|
||||
credits,
|
||||
hp,
|
||||
currentLocationId,
|
||||
selectedPlanet,
|
||||
setSelectedPlanet,
|
||||
executeEscape,
|
||||
executeJump,
|
||||
handleAttack,
|
||||
isJumping,
|
||||
activeEnemy,
|
||||
logs,
|
||||
isGameOver,
|
||||
restartGame,
|
||||
};
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,35 @@
|
||||
import { SWAPIPlanet, SWAPIStarship, Planet, Starship } from "@/types/swapi";
|
||||
|
||||
const BASE_URL = "https://swapi.dev/api";
|
||||
|
||||
export async function fetchPlanets(): Promise<Planet[]> {
|
||||
const res = await fetch(`${BASE_URL}/planets/`);
|
||||
if (!res.ok) throw new Error("Galaxy data unavailable");
|
||||
const data = await res.json();
|
||||
|
||||
return data.results.map(
|
||||
(p: SWAPIPlanet): Planet => ({
|
||||
...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 cost = parseInt(s.cost_in_credits);
|
||||
let ds = 8;
|
||||
if (isNaN(cost) || cost > 1000000) ds = 12;
|
||||
else if (cost > 100000) ds = 10;
|
||||
|
||||
return { ...s, id: s.url, ds };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// types/swapi.ts
|
||||
|
||||
// Die korrigierten Rohdaten von der SWAPI
|
||||
export interface SWAPIPlanet { // <-- Jetzt mit korrektem 'n'
|
||||
name: string;
|
||||
rotation_period: string;
|
||||
orbital_period: string;
|
||||
diameter: string;
|
||||
climate: string;
|
||||
gravity: string;
|
||||
terrain: string;
|
||||
surface_water: string;
|
||||
population: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface SWAPIStarship {
|
||||
name: string;
|
||||
model: string;
|
||||
manufacturer: string;
|
||||
cost_in_credits: string;
|
||||
length: string;
|
||||
max_atmosphering_speed: string;
|
||||
crew: string;
|
||||
passengers: string;
|
||||
cargo_capacity: string;
|
||||
starship_class: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
// Unsere erweiterten Typen für das Spiel
|
||||
export interface Planet extends SWAPIPlanet {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Starship extends SWAPIStarship {
|
||||
id: string;
|
||||
ds: number; // Difficulty Score für Tunnel Goons
|
||||
}
|
||||
Reference in New Issue
Block a user