diff --git a/src/ui/AboutPanel.tsx b/src/ui/AboutPanel.tsx new file mode 100644 index 0000000..7280517 --- /dev/null +++ b/src/ui/AboutPanel.tsx @@ -0,0 +1,53 @@ +export function AboutPanel() { + return ( +
+
+

About Idlegame

+

A text-fantasy RPG incremental experience.

+
+ +
+
+

+ How to Play +

+

+ Idlegame is driven by actions and choices. Select actions from the{' '} + Play screen to execute them. Some actions + are timed and can be queued. Once completed, they reward you with resources, unlock new + deeds, or advance the chronicle. +

+
+ +
+

+ The Chronicle +

+

+ As you perform actions, you will unlock narrative points of interest. Head to the{' '} + Story tab to make crucial choices and read + the history of your journey. +

+
+ +
+

+ Technical Specs +

+
+
+
Version
+
0.1.0 (M1 Milestone)
+
Engine
+
Pure TypeScript State Machine
+
Framework
+
React + Zustand + Tailwind CSS
+
Target Platform
+
Responsive Web & PWA
+
+
+
+
+
+ ); +} diff --git a/src/ui/App.tsx b/src/ui/App.tsx index bdc9ee0..fe4bfa9 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -1,65 +1,45 @@ import { useEffect } from 'react'; import { gameRuntime } from '../state/runtime'; import { useGameStore } from '../state/store'; -import { ActionPanel } from './ActionPanel'; -import { EventLog } from './EventLog'; -import { ResourceBar } from './ResourceBar'; -import { SettingsDrawer } from './SettingsDrawer'; -import { StoryPanel } from './StoryPanel'; +import { AboutPanel } from './AboutPanel'; +import { AppShell } from './AppShell'; +import { NavRail } from './NavRail'; +import { PlayPanel } from './PlayPanel'; +import { RightRail } from './RightRail'; +import { SettingsPanel } from './SettingsPanel'; +import { StoryView } from './StoryView'; /** * M1 playable-loop shell. Boots the runtime once on mount; everything else * renders from the Zustand store the runtime feeds. */ export function App() { - const storyHasUnread = useGameStore((s) => s.storyHasUnread); - const settingsOpen = useGameStore((s) => s.settingsOpen); - const setSettingsOpen = useGameStore((s) => s.setSettingsOpen); + const activePanel = useGameStore((s) => s.activePanel); useEffect(() => { void gameRuntime.boot(); }, []); + const renderActivePanel = () => { + switch (activePanel) { + case 'play': + return ; + case 'story': + return ; + case 'settings': + return ; + case 'about': + return ; + default: + return ; + } + }; + return ( - <> - -
-
-
-

Idlegame

-
- - -
-
-

M1 playable loop

- -
- - - -
- + } + center={renderActivePanel()} + right={activePanel === 'play' ? : undefined} + /> ); } diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx new file mode 100644 index 0000000..952cca6 --- /dev/null +++ b/src/ui/AppShell.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from 'react'; + +interface AppShellProps { + nav: ReactNode; + center: ReactNode; + right?: ReactNode; +} + +export function AppShell({ nav, center, right }: AppShellProps) { + return ( +
+
{nav}
+
{center}
+ {right ? ( + + ) : null} +
+ ); +} diff --git a/src/ui/NavRail.tsx b/src/ui/NavRail.tsx new file mode 100644 index 0000000..050a9e9 --- /dev/null +++ b/src/ui/NavRail.tsx @@ -0,0 +1,140 @@ +import { gameRuntime } from '../state/runtime'; +import { type ActivePanel, useGameStore } from '../state/store'; + +export function NavRail() { + const activePanel = useGameStore((s) => s.activePanel); + const storyHasUnread = useGameStore((s) => s.storyHasUnread); + + const navItems: { id: ActivePanel; label: string; icon: React.ReactNode }[] = [ + { + id: 'play', + label: 'Play', + icon: ( + + ), + }, + { + id: 'story', + label: 'Story', + icon: ( + + ), + }, + { + id: 'settings', + label: 'Settings', + icon: ( + + ), + }, + { + id: 'about', + label: 'About', + icon: ( + + ), + }, + ]; + + return ( +
+
+ + IDLEGAME + + + M1 + +
+ + + +
+ senpai edition +
+
+ ); +} diff --git a/src/ui/PlayPanel.tsx b/src/ui/PlayPanel.tsx new file mode 100644 index 0000000..01e5623 --- /dev/null +++ b/src/ui/PlayPanel.tsx @@ -0,0 +1,9 @@ +import { ActionPanel } from './ActionPanel'; + +export function PlayPanel() { + return ( +
+ +
+ ); +} diff --git a/src/ui/RightRail.tsx b/src/ui/RightRail.tsx new file mode 100644 index 0000000..3c0cc8c --- /dev/null +++ b/src/ui/RightRail.tsx @@ -0,0 +1,19 @@ +import { EventLog } from './EventLog'; +import { ResourceBar } from './ResourceBar'; + +export function RightRail() { + return ( +
+
+

+ Resources +

+ +
+
+

Log

+ +
+
+ ); +} diff --git a/src/ui/SettingsPanel.tsx b/src/ui/SettingsPanel.tsx new file mode 100644 index 0000000..6080a1e --- /dev/null +++ b/src/ui/SettingsPanel.tsx @@ -0,0 +1,86 @@ +import type { ActionDetailMode, StoryOpenMode } from '../state/prefs'; +import { useGameStore } from '../state/store'; + +export function SettingsPanel() { + const prefs = useGameStore((s) => s.prefs); + const setPrefs = useGameStore((s) => s.setPrefs); + + return ( +
+
+

Game Settings

+

Configure your gameplay and UI preferences.

+
+ +
+ {/* Story Open Mode */} +
+
+ Story Navigation + + How should the game navigate when story events are unlocked? + +
+
+ {(['auto', 'choices-only', 'manual'] as StoryOpenMode[]).map((mode) => { + const labels: Record = { + auto: 'Automatically', + 'choices-only': 'Choices Only', + manual: 'Manually', + }; + const isActive = prefs.storyOpenMode === mode; + return ( + + ); + })} +
+
+ + {/* Action Detail Mode */} +
+
+ Action Details + + Where should action requirements and details be shown? + +
+
+ {(['inline', 'hover', 'info-button'] as ActionDetailMode[]).map((mode) => { + const labels: Record = { + inline: 'Inline', + hover: 'Hover Tooltips', + 'info-button': 'Info Button', + }; + const isActive = prefs.actionDetailMode === mode; + return ( + + ); + })} +
+
+
+
+ ); +} diff --git a/src/ui/StoryView.tsx b/src/ui/StoryView.tsx new file mode 100644 index 0000000..ec7c2bf --- /dev/null +++ b/src/ui/StoryView.tsx @@ -0,0 +1,76 @@ +import { gameRuntime } from '../state/runtime'; +import { useGameStore } from '../state/store'; + +export function StoryView() { + const story = useGameStore((s) => s.story); + const storyLog = useGameStore((s) => s.storyLog); + + const hasChoices = story.choices.length > 0; + + return ( +
+ {/* Story Log Section */} +
+

+ Story Log +

+ {storyLog.length === 0 ? ( +

The chronicle is empty...

+ ) : ( +
    + {storyLog.map((entry) => ( +
  • + {entry.choiceLabel ? ( + [{entry.choiceLabel}] + ) : null} + {entry.prose} +
  • + ))} +
+ )} +
+ + {/* Active Node Section */} +
+
+

+ {story.currentProse || + 'The path ahead is shrouded in mist. Begin an action to unveil your story.'} +

+
+ + {story.currentProse && ( +
+ {hasChoices ? ( +
+ {story.choices.map((choice) => ( + + ))} +
+ ) : ( + + )} +
+ )} +
+
+ ); +}