diff --git a/app/components/apitest.tsx b/app/components/apitest.tsx deleted file mode 100644 index 18a54ef..0000000 --- a/app/components/apitest.tsx +++ /dev/null @@ -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 ( -
-

Smuggler Intel

-

Name: {data.name}

-

Height: {data.height}cm

-

Mass: {data.mass}kg

-
- ); -} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index a2dc41e..851f2d7 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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); diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..590439b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( - + diff --git a/app/page.tsx b/app/page.tsx index 5d884e6..fbbbed3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 ( +
+
{t.SYSTEM.LOADING}
+
+ ); + } + // Aktuellen Planeten finden für das HUD + const currentPlanet = planets.find((p) => p.id === currentLocationId); + + // 2. Startbildschirm + if (!gameStarted) { + return ; + } return ( -
+
+ -{/* Add your component here */} - - -
- Next.js logo + setSelectedPlanet(p)} + currentLocationId={currentLocationId} /> -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
-
+ + {/* ActionPanel erscheint bei Zielwahl oder Feindkontakt */} + {(selectedPlanet || activeEnemy) && ( + + )} + + + + + {/* Game Over Overlay */} + {isGameOver && ( + + )} + ); } diff --git a/components/ActionPanel.tsx b/components/ActionPanel.tsx new file mode 100644 index 0000000..f9978aa --- /dev/null +++ b/components/ActionPanel.tsx @@ -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 ( +
+
+ +
+
+
+ {isCombat ? ( +
+ ! +
+ ) : ( + <> +
+
+
+ + )} +
+ +
+ {theme.label} +
+
+ +
+
+

+ {displayName} +

+

+ {displaySubtitle} +

+
+ +
+
+ + {isCombat + ? t.ACTION_PANEL.LABELS.MANUFACTURER + : t.ACTION_PANEL.LABELS.POPULATION} + +

+ {isCombat + ? enemy.manufacturer + : planet?.population.toLocaleString()} +

+
+
+ + {isCombat + ? t.ACTION_PANEL.LABELS.COST + : t.ACTION_PANEL.LABELS.GRAVITY} + +

+ {isCombat ? `${enemy.cost_in_credits} Cr` : planet?.gravity} +

+
+
+ +

+ {isCombat + ? t.ACTION_PANEL.STATUS.COMBAT_WARNING + : t.ACTION_PANEL.STATUS.PLANET_SCAN} +

+
+ +
+ + + {isCombat && ( + + )} +
+
+
+ ); +} diff --git a/components/EventLog.tsx b/components/EventLog.tsx new file mode 100644 index 0000000..3fb91b0 --- /dev/null +++ b/components/EventLog.tsx @@ -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(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 ( +
+ {/* Header */} +
+ + {t.EVENT_LOG.TITLE} + +
+ + {/* Scrollable Content */} +
+ {logs.length === 0 && ( +

+ {t.EVENT_LOG.EMPTY_STATE} +

+ )} + {logs.map((log) => ( +
+ + {log.timestamp} + + + {log.type === "danger" && "⚠ "} + {log.message} + +
+ ))} +
+
+ ); +} diff --git a/components/GalaxyMap.tsx b/components/GalaxyMap.tsx new file mode 100644 index 0000000..c7e3228 --- /dev/null +++ b/components/GalaxyMap.tsx @@ -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 ( +
+ {/* Hintergrund-Raster (Grid) */} +
+ + {/* Die Planeten (Nodes) aus der API */} + {planets.map((planet) => { + const isCurrent = planet.id === currentLocationId; + + return ( + + ); + })} + + {/* Scan-Linie Effekt */} +
+ + {/* Info Overlay */} +
+ {t.GALAXY_MAP.SCAN_STATUS}
+ {t.GALAXY_MAP.OBJECTS_DETECTED(planets.length)}
+ {t.GALAXY_MAP.HOLONET_STATUS} +
+
+ ); +} diff --git a/components/GameOverScreen.tsx b/components/GameOverScreen.tsx new file mode 100644 index 0000000..edfc5eb --- /dev/null +++ b/components/GameOverScreen.tsx @@ -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 ( +
+
+

+ {t.GAME_OVER.TITLE} +

+

{t.GAME_OVER.SUBTITLE}

+ +
+

+ {t.GAME_OVER.SCORE_LABEL} +

+

+ {credits.toLocaleString()} Credits +

+
+ + +
+
+ ); +} diff --git a/components/Hud.tsx b/components/Hud.tsx new file mode 100644 index 0000000..c282a2b --- /dev/null +++ b/components/Hud.tsx @@ -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 ( +
+
+ {/* LORE & LOCATION */} +
+ + {t.HUD.SECTOR_LABEL} + + + {location.toUpperCase()} + +
+ + {/* SHIP STATUS (HEALTH) */} +
+
+ {t.HUD.HULL_LABEL} + + {hp} / {maxHp} + +
+
+
+
+
+ + {/* ECONOMY (CREDITS) */} +
+ + {t.HUD.GOAL_LABEL} + +
+ + {credits.toLocaleString()} + + + / {targetCredits.toLocaleString()} + +
+ {/* Progress Bar for the Goal */} +
+
+
+
+
+
+ ); +} diff --git a/components/StartScreen.tsx b/components/StartScreen.tsx new file mode 100644 index 0000000..6e45e6a --- /dev/null +++ b/components/StartScreen.tsx @@ -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 ( +
+ {/* Hintergrund-Deko */} +
+ +
+
+

+ {t.UI.GAME_TITLE} +

+

+ {t.START_SCREEN.SUBTITLE} +

+
+ +
+

+ {t.START_SCREEN.INTRO_TEXT} + + {" "} + {t.START_SCREEN.MISSION_HIGHLIGHT} + +

+
    +
  • + •{" "} + + {t.START_SCREEN.FEATURE_NAV_TITLE} + {" "} + {t.START_SCREEN.FEATURE_NAV_DESC} +
  • +
  • + •{" "} + + {t.START_SCREEN.FEATURE_RISK_TITLE} + {" "} + {t.START_SCREEN.FEATURE_RISK_DESC} +
  • +
  • + •{" "} + + {t.START_SCREEN.FEATURE_COMBAT_TITLE} + {" "} + {t.START_SCREEN.FEATURE_COMBAT_DESC} +
  • +
+
+ +
+ + +

+ {t.START_SCREEN.FOOTER_ENGINE} +

+
+
+
+ ); +} diff --git a/content/locales.ts b/content/locales.ts new file mode 100644 index 0000000..ab56ff3 --- /dev/null +++ b/content/locales.ts @@ -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; diff --git a/hooks/useGameEngine.ts b/hooks/useGameEngine.ts new file mode 100644 index 0000000..21a45f7 --- /dev/null +++ b/hooks/useGameEngine.ts @@ -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([]); + const [starships, setStarships] = useState([]); + const [loading, setLoading] = useState(true); + + const [gameStarted, setGameStarted] = useState(false); + const [isGameOver, setIsGameOver] = useState(false); + const [credits, setCredits] = useState(100); + const [hp, setHp] = useState(10); + const [currentLocationId, setCurrentLocationId] = useState(""); + const [selectedPlanet, setSelectedPlanet] = useState(null); + const [isJumping, setIsJumping] = useState(false); + const [activeEnemy, setActiveEnemy] = useState(null); + const [logs, setLogs] = useState([]); + + 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, + }; +} diff --git a/public/fonts/Starjedi.ttf b/public/fonts/Starjedi.ttf new file mode 100644 index 0000000..2ac5bb1 Binary files /dev/null and b/public/fonts/Starjedi.ttf differ diff --git a/services/swapi.ts b/services/swapi.ts new file mode 100644 index 0000000..39e641f --- /dev/null +++ b/services/swapi.ts @@ -0,0 +1,35 @@ +import { SWAPIPlanet, SWAPIStarship, Planet, Starship } from "@/types/swapi"; + +const BASE_URL = "https://swapi.dev/api"; + +export async function fetchPlanets(): Promise { + 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 { + 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 }; + }); +} diff --git a/types/swapi.ts b/types/swapi.ts new file mode 100644 index 0000000..4d2a2c9 --- /dev/null +++ b/types/swapi.ts @@ -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 +} \ No newline at end of file