Replace TS game shell with Bevy/Rust client, add auth service, purge legacy archive
- Replace apps/game (TypeScript/Vite/R3F) with a Bevy 0.16 Rust client featuring main menu, star map, and flight states - Add services/auth: Express + better-auth server with SpacetimeDB token exchange endpoint - Delete archive/legacy-static/ (old JS demos, CSS, assets) - Update docs pages (architecture, gameplay, roadmap, social, overview) to reflect Bevy pivot - Clean up workspace config: remove apps/game from pnpm workspace, update dev scripts, tsconfig, and AGENTS.md files - Add .vscode/settings.json for Rust tooling
This commit is contained in:
@@ -1,139 +0,0 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Panel } from "@void-nav/ui";
|
||||
import { GameSpaceScene } from "./scene/GameSpaceScene";
|
||||
import { CommandRail } from "./ui/CommandRail";
|
||||
import { CargoPanel } from "./ui/CargoPanel";
|
||||
import { ConnectionPanel } from "./ui/ConnectionPanel";
|
||||
import { EventFeed } from "./ui/EventFeed";
|
||||
import { MiniStarMap } from "./ui/MiniStarMap";
|
||||
import { NpcMarketPanel } from "./ui/NpcMarketPanel";
|
||||
import { ShipStatusPanel } from "./ui/ShipStatusPanel";
|
||||
import { TargetPanel } from "./ui/TargetPanel";
|
||||
import { useKeyboardShortcuts } from "./ui/useKeyboardShortcuts";
|
||||
import { WalletPanel } from "./ui/WalletPanel";
|
||||
import { useGameSession } from "./spacetime/useGameSession";
|
||||
import { useSpacetimeConnection } from "./spacetime/useSpacetimeConnection";
|
||||
|
||||
const defaultPilotName = "New Pilot";
|
||||
|
||||
export function GameShell() {
|
||||
const [displayName, setDisplayName] = useState(defaultPilotName);
|
||||
const [manualReducerStatus, setManualReducerStatus] = useState<{ message: string; isError: boolean }>();
|
||||
const connectionState = useSpacetimeConnection(displayName);
|
||||
const reducerStatus = manualReducerStatus ?? connectionState.reducerStatus;
|
||||
const session = useGameSession(
|
||||
connectionState.connection,
|
||||
connectionState.revision,
|
||||
(message, isError = false) => setManualReducerStatus({ message, isError }),
|
||||
connectionState.identity,
|
||||
);
|
||||
|
||||
const connectionLabel = useMemo(() => {
|
||||
if (connectionState.status === "error") return "Disconnected";
|
||||
if (connectionState.status === "connected") return "Connected";
|
||||
return connectionState.status.charAt(0).toUpperCase() + connectionState.status.slice(1);
|
||||
}, [connectionState.status]);
|
||||
|
||||
const selectedPoi = session.pois.find((poi) => poi.poiId === session.ship?.selectedPoiId);
|
||||
const pilotName = session.player?.displayName ?? displayName;
|
||||
|
||||
const keyboardActions = useMemo(
|
||||
() => ({
|
||||
onUndock: session.actions.undock,
|
||||
onStartApproach: session.actions.startApproach,
|
||||
onDock: session.actions.dock,
|
||||
onStartMining: session.actions.startMining,
|
||||
onSelectTarget: (index: number) => {
|
||||
const poi = session.pois[index];
|
||||
if (poi) session.actions.selectTarget(poi.poiId);
|
||||
},
|
||||
}),
|
||||
[session.actions, session.pois],
|
||||
);
|
||||
|
||||
useKeyboardShortcuts(session.ship, session.pois, session.operation, keyboardActions);
|
||||
|
||||
return (
|
||||
<main className="relative min-h-screen overflow-hidden bg-bg text-fg">
|
||||
<div className="absolute inset-0">
|
||||
<GameSpaceScene
|
||||
ship={session.ship}
|
||||
pois={session.pois}
|
||||
selectedPoiId={session.ship?.selectedPoiId}
|
||||
operation={session.operation}
|
||||
onSelectPoi={session.actions.selectTarget}
|
||||
onCompleteApproach={session.actions.completeApproach}
|
||||
onCompleteMiningCycle={session.actions.completeMiningCycle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-none relative z-10 grid min-h-screen gap-4 overflow-y-auto px-4 pb-28 pt-4 md:px-5 lg:grid-cols-[22rem_1fr_24rem]">
|
||||
<aside className="pointer-events-auto grid content-start gap-4">
|
||||
<ConnectionPanel
|
||||
status={connectionState.status}
|
||||
label={connectionLabel}
|
||||
identity={connectionState.identity}
|
||||
uri={connectionState.uri}
|
||||
database={connectionState.database}
|
||||
message={connectionState.message}
|
||||
reducerMessage={reducerStatus?.message}
|
||||
reducerIsError={reducerStatus?.isError}
|
||||
displayName={displayName}
|
||||
onDisplayNameChange={setDisplayName}
|
||||
onRename={() => session.actions.renamePlayer(displayName.trim() || defaultPilotName)}
|
||||
onPing={session.actions.ping}
|
||||
/>
|
||||
<Panel className="p-4">
|
||||
<p className="m-0 font-mono text-xs uppercase tracking-[0.1em] text-muted">Pilot</p>
|
||||
<h1 className="m-0 mt-1 text-2xl leading-tight text-fg-bright">{pilotName}</h1>
|
||||
<dl className="mt-3 grid gap-2 text-sm">
|
||||
<InfoRow label="Ship" value={session.ship?.shipName ?? "No starter ship"} />
|
||||
<InfoRow label="Hull" value={session.ship?.hullType ?? "Pending"} />
|
||||
<InfoRow label="System" value={session.system?.name ?? "Pending"} />
|
||||
</dl>
|
||||
</Panel>
|
||||
<ShipStatusPanel ship={session.ship} operation={session.operation} />
|
||||
<WalletPanel wallet={session.wallet} />
|
||||
</aside>
|
||||
|
||||
<section className="hidden lg:block" aria-hidden />
|
||||
|
||||
<aside className="pointer-events-auto grid content-start gap-4">
|
||||
<TargetPanel ship={session.ship} pois={session.pois} selected={selectedPoi} onSelect={session.actions.selectTarget} />
|
||||
<MiniStarMap ship={session.ship} pois={session.pois} operation={session.operation} onSelectPoi={session.actions.selectTarget} />
|
||||
<CargoPanel cargo={session.cargo} ship={session.ship} />
|
||||
<NpcMarketPanel
|
||||
ship={session.ship}
|
||||
cargo={session.cargo}
|
||||
wallet={session.wallet}
|
||||
onSellOre={(itemId, quantity) => session.actions.sellOreToNpcMarket(itemId, quantity)}
|
||||
/>
|
||||
<EventFeed events={session.events} />
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-none fixed inset-x-0 bottom-0 z-20 px-4 pb-4">
|
||||
<CommandRail
|
||||
ship={session.ship}
|
||||
pois={session.pois}
|
||||
cargo={session.cargo}
|
||||
operation={session.operation}
|
||||
onUndock={session.actions.undock}
|
||||
onStartApproach={session.actions.startApproach}
|
||||
onDock={session.actions.dock}
|
||||
onStartMining={session.actions.startMining}
|
||||
onSellOre={(item) => session.actions.sellOreToNpcMarket(item.itemId, item.quantity)}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[5rem_1fr] gap-2 border-t border-border pt-2">
|
||||
<dt className="font-mono text-xs uppercase tracking-[0.08em] text-muted">{label}</dt>
|
||||
<dd className="m-0 break-words text-fg">{value}</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
176
apps/game/src/main.rs
Normal file
176
apps/game/src/main.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.insert_resource(ClearColor(Color::srgb(0.02, 0.02, 0.06)))
|
||||
.init_state::<AppState>()
|
||||
.add_systems(Startup, spawn_camera)
|
||||
.add_systems(OnEnter(AppState::MainMenu), setup_main_menu)
|
||||
.add_systems(OnExit(AppState::MainMenu), despawn_main_menu)
|
||||
.add_systems(Update, main_menu_buttons)
|
||||
.run();
|
||||
}
|
||||
|
||||
// ── States ──────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Hash, States, Default)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
MainMenu,
|
||||
InGame,
|
||||
Options,
|
||||
}
|
||||
|
||||
// ── Markers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Component)]
|
||||
struct MainMenuUi;
|
||||
|
||||
#[derive(Component)]
|
||||
enum MenuButton {
|
||||
Start,
|
||||
Options,
|
||||
Exit,
|
||||
}
|
||||
|
||||
// ── Camera ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn spawn_camera(mut commands: Commands) {
|
||||
commands.spawn(Camera2d);
|
||||
}
|
||||
|
||||
// ── Main Menu ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn setup_main_menu(mut commands: Commands) {
|
||||
let button_style = Style {
|
||||
width: Val::Px(280.0),
|
||||
height: Val::Px(64.0),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
};
|
||||
|
||||
let button_text_font = TextFont {
|
||||
font_size: 28.0,
|
||||
..default()
|
||||
};
|
||||
|
||||
commands.spawn((
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
display: Display::Flex,
|
||||
flex_direction: FlexDirection::Column,
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
row_gap: Val::Px(24.0),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::srgb(0.02, 0.02, 0.06)),
|
||||
MainMenuUi,
|
||||
)).with_children(|parent| {
|
||||
// Title
|
||||
parent.spawn((
|
||||
Text::new("VOID::NAV"),
|
||||
TextFont {
|
||||
font_size: 72.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::srgb(0.7, 0.85, 1.0)),
|
||||
Node {
|
||||
margin: UiRect::bottom(Val::Px(48.0)),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
// Subtitle
|
||||
parent.spawn((
|
||||
Text::new("A space exploration RPG"),
|
||||
TextFont {
|
||||
font_size: 18.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::srgb(0.4, 0.5, 0.6)),
|
||||
Node {
|
||||
margin: UiRect::bottom(Val::Px(32.0)),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
// Start Game button
|
||||
spawn_menu_button(
|
||||
&mut parent.spawn_empty(),
|
||||
"Start Game",
|
||||
MenuButton::Start,
|
||||
&button_style,
|
||||
&button_text_font,
|
||||
);
|
||||
|
||||
// Options button
|
||||
spawn_menu_button(
|
||||
&mut parent.spawn_empty(),
|
||||
"Options",
|
||||
MenuButton::Options,
|
||||
&button_style,
|
||||
&button_text_font,
|
||||
);
|
||||
|
||||
// Exit button
|
||||
spawn_menu_button(
|
||||
&mut parent.spawn_empty(),
|
||||
"Exit",
|
||||
MenuButton::Exit,
|
||||
&button_style,
|
||||
&button_text_font,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn spawn_menu_button(
|
||||
cmd: &mut EntityCommands,
|
||||
label: &str,
|
||||
marker: MenuButton,
|
||||
style: &Style,
|
||||
text_font: &TextFont,
|
||||
) {
|
||||
cmd.insert((
|
||||
Button,
|
||||
style.clone(),
|
||||
BackgroundColor(Color::srgb(0.08, 0.1, 0.18)),
|
||||
BorderColor(Color::srgb(0.3, 0.45, 0.7)),
|
||||
BorderRadius::all(Val::Px(8.0)),
|
||||
marker,
|
||||
))
|
||||
.with_children(|btn| {
|
||||
btn.spawn((
|
||||
Text::new(label),
|
||||
text_font.clone(),
|
||||
TextColor(Color::srgb(0.75, 0.85, 1.0)),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
fn despawn_main_menu(mut commands: Commands, query: Query<Entity, With<MainMenuUi>>) {
|
||||
for entity in &query {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Button Interaction ──────────────────────────────────────────────────────
|
||||
|
||||
fn main_menu_buttons(
|
||||
mut next_state: ResMut<NextState<AppState>>,
|
||||
mut exit: EventWriter<AppExit>,
|
||||
interaction_query: Query<(&Interaction, &MenuButton), Changed<Interaction>>,
|
||||
) {
|
||||
for (interaction, button) in &interaction_query {
|
||||
if *interaction == Interaction::Pressed {
|
||||
match button {
|
||||
MenuButton::Start => next_state.set(AppState::InGame),
|
||||
MenuButton::Options => next_state.set(AppState::Options),
|
||||
MenuButton::Exit => exit.send(AppExit::Success),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { GameShell } from "./GameShell";
|
||||
import "./styles/tailwind.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<GameShell />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -1,22 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default __t.row({
|
||||
cargoItemId: __t.u64().primaryKey().name("cargo_item_id"),
|
||||
ownerIdentity: __t.identity().name("owner_identity"),
|
||||
shipId: __t.u64().name("ship_id"),
|
||||
itemId: __t.string().name("item_id"),
|
||||
itemName: __t.string().name("item_name"),
|
||||
category: __t.string(),
|
||||
quantity: __t.u64(),
|
||||
unitPrice: __t.u64().name("unit_price"),
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {};
|
||||
@@ -1,13 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {};
|
||||
@@ -1,15 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {
|
||||
displayName: __t.string(),
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {};
|
||||
@@ -1,258 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
// This was generated using spacetimedb cli version 2.3.0 (commit aa73d1c35b4b346b98eeba10a3d756b4ae72162f).
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
DbConnectionBuilder as __DbConnectionBuilder,
|
||||
DbConnectionImpl as __DbConnectionImpl,
|
||||
SubscriptionBuilderImpl as __SubscriptionBuilderImpl,
|
||||
TypeBuilder as __TypeBuilder,
|
||||
Uuid as __Uuid,
|
||||
convertToAccessorMap as __convertToAccessorMap,
|
||||
makeQueryBuilder as __makeQueryBuilder,
|
||||
procedureSchema as __procedureSchema,
|
||||
procedures as __procedures,
|
||||
reducerSchema as __reducerSchema,
|
||||
reducers as __reducers,
|
||||
schema as __schema,
|
||||
t as __t,
|
||||
table as __table,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type DbConnectionConfig as __DbConnectionConfig,
|
||||
type ErrorContextInterface as __ErrorContextInterface,
|
||||
type Event as __Event,
|
||||
type EventContextInterface as __EventContextInterface,
|
||||
type Infer as __Infer,
|
||||
type QueryBuilder as __QueryBuilder,
|
||||
type ReducerEventContextInterface as __ReducerEventContextInterface,
|
||||
type RemoteModule as __RemoteModule,
|
||||
type SubscriptionEventContextInterface as __SubscriptionEventContextInterface,
|
||||
type SubscriptionHandleImpl as __SubscriptionHandleImpl,
|
||||
} from "spacetimedb";
|
||||
|
||||
// Import all reducer arg schemas
|
||||
import CompleteApproachReducer from "./complete_approach_reducer";
|
||||
import CompleteMiningCycleReducer from "./complete_mining_cycle_reducer";
|
||||
import ConnectPlayerReducer from "./connect_player_reducer";
|
||||
import DockReducer from "./dock_reducer";
|
||||
import PingReducer from "./ping_reducer";
|
||||
import RenamePlayerReducer from "./rename_player_reducer";
|
||||
import SeedWorldReducer from "./seed_world_reducer";
|
||||
import SelectTargetReducer from "./select_target_reducer";
|
||||
import SellOreToNpcMarketReducer from "./sell_ore_to_npc_market_reducer";
|
||||
import StartApproachReducer from "./start_approach_reducer";
|
||||
import StartMiningReducer from "./start_mining_reducer";
|
||||
import UndockReducer from "./undock_reducer";
|
||||
|
||||
// Import all procedure arg schemas
|
||||
|
||||
// Import all table schema definitions
|
||||
import CargoItemRow from "./cargo_item_table";
|
||||
import PlayerRow from "./player_table";
|
||||
import PointOfInterestRow from "./point_of_interest_table";
|
||||
import ServerEventRow from "./server_event_table";
|
||||
import ShipRow from "./ship_table";
|
||||
import ShipOperationRow from "./ship_operation_table";
|
||||
import StationRow from "./station_table";
|
||||
import SystemRow from "./system_table";
|
||||
import WalletRow from "./wallet_table";
|
||||
|
||||
/** Type-only namespace exports for generated type groups. */
|
||||
|
||||
/** The schema information for all tables in this module. This is defined the same was as the tables would have been defined in the server. */
|
||||
const tablesSchema = __schema({
|
||||
cargo_item: __table({
|
||||
name: 'cargo_item',
|
||||
indexes: [
|
||||
{ accessor: 'cargo_item_id', name: 'cargo_item_cargo_item_id_idx_btree', algorithm: 'btree', columns: [
|
||||
'cargoItemId',
|
||||
] },
|
||||
{ accessor: 'owner_identity', name: 'cargo_item_owner_identity_idx_btree', algorithm: 'btree', columns: [
|
||||
'ownerIdentity',
|
||||
] },
|
||||
{ accessor: 'ship_id', name: 'cargo_item_ship_id_idx_btree', algorithm: 'btree', columns: [
|
||||
'shipId',
|
||||
] },
|
||||
],
|
||||
constraints: [
|
||||
{ name: 'cargo_item_cargo_item_id_key', constraint: 'unique', columns: ['cargoItemId'] },
|
||||
],
|
||||
}, CargoItemRow),
|
||||
player: __table({
|
||||
name: 'player',
|
||||
indexes: [
|
||||
{ accessor: 'identity', name: 'player_identity_idx_btree', algorithm: 'btree', columns: [
|
||||
'identity',
|
||||
] },
|
||||
],
|
||||
constraints: [
|
||||
{ name: 'player_identity_key', constraint: 'unique', columns: ['identity'] },
|
||||
],
|
||||
}, PlayerRow),
|
||||
point_of_interest: __table({
|
||||
name: 'point_of_interest',
|
||||
indexes: [
|
||||
{ accessor: 'poi_id', name: 'point_of_interest_poi_id_idx_btree', algorithm: 'btree', columns: [
|
||||
'poiId',
|
||||
] },
|
||||
{ accessor: 'system_id', name: 'point_of_interest_system_id_idx_btree', algorithm: 'btree', columns: [
|
||||
'systemId',
|
||||
] },
|
||||
],
|
||||
constraints: [
|
||||
{ name: 'point_of_interest_poi_id_key', constraint: 'unique', columns: ['poiId'] },
|
||||
],
|
||||
}, PointOfInterestRow),
|
||||
server_event: __table({
|
||||
name: 'server_event',
|
||||
indexes: [
|
||||
{ accessor: 'actor_identity', name: 'server_event_actor_identity_idx_btree', algorithm: 'btree', columns: [
|
||||
'actorIdentity',
|
||||
] },
|
||||
{ accessor: 'event_id', name: 'server_event_event_id_idx_btree', algorithm: 'btree', columns: [
|
||||
'eventId',
|
||||
] },
|
||||
],
|
||||
constraints: [
|
||||
{ name: 'server_event_event_id_key', constraint: 'unique', columns: ['eventId'] },
|
||||
],
|
||||
}, ServerEventRow),
|
||||
ship: __table({
|
||||
name: 'ship',
|
||||
indexes: [
|
||||
{ accessor: 'owner_identity', name: 'ship_owner_identity_idx_btree', algorithm: 'btree', columns: [
|
||||
'ownerIdentity',
|
||||
] },
|
||||
{ accessor: 'ship_id', name: 'ship_ship_id_idx_btree', algorithm: 'btree', columns: [
|
||||
'shipId',
|
||||
] },
|
||||
],
|
||||
constraints: [
|
||||
{ name: 'ship_ship_id_key', constraint: 'unique', columns: ['shipId'] },
|
||||
],
|
||||
}, ShipRow),
|
||||
ship_operation: __table({
|
||||
name: 'ship_operation',
|
||||
indexes: [
|
||||
{ accessor: 'ship_id', name: 'ship_operation_ship_id_idx_btree', algorithm: 'btree', columns: [
|
||||
'shipId',
|
||||
] },
|
||||
],
|
||||
constraints: [
|
||||
{ name: 'ship_operation_ship_id_key', constraint: 'unique', columns: ['shipId'] },
|
||||
],
|
||||
}, ShipOperationRow),
|
||||
station: __table({
|
||||
name: 'station',
|
||||
indexes: [
|
||||
{ accessor: 'station_id', name: 'station_station_id_idx_btree', algorithm: 'btree', columns: [
|
||||
'stationId',
|
||||
] },
|
||||
{ accessor: 'system_id', name: 'station_system_id_idx_btree', algorithm: 'btree', columns: [
|
||||
'systemId',
|
||||
] },
|
||||
],
|
||||
constraints: [
|
||||
{ name: 'station_station_id_key', constraint: 'unique', columns: ['stationId'] },
|
||||
],
|
||||
}, StationRow),
|
||||
system: __table({
|
||||
name: 'system',
|
||||
indexes: [
|
||||
{ accessor: 'system_id', name: 'system_system_id_idx_btree', algorithm: 'btree', columns: [
|
||||
'systemId',
|
||||
] },
|
||||
],
|
||||
constraints: [
|
||||
{ name: 'system_system_id_key', constraint: 'unique', columns: ['systemId'] },
|
||||
],
|
||||
}, SystemRow),
|
||||
wallet: __table({
|
||||
name: 'wallet',
|
||||
indexes: [
|
||||
{ accessor: 'owner_identity', name: 'wallet_owner_identity_idx_btree', algorithm: 'btree', columns: [
|
||||
'ownerIdentity',
|
||||
] },
|
||||
],
|
||||
constraints: [
|
||||
{ name: 'wallet_owner_identity_key', constraint: 'unique', columns: ['ownerIdentity'] },
|
||||
],
|
||||
}, WalletRow),
|
||||
});
|
||||
|
||||
/** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */
|
||||
const reducersSchema = __reducers(
|
||||
__reducerSchema("complete_approach", CompleteApproachReducer),
|
||||
__reducerSchema("complete_mining_cycle", CompleteMiningCycleReducer),
|
||||
__reducerSchema("connect_player", ConnectPlayerReducer),
|
||||
__reducerSchema("dock", DockReducer),
|
||||
__reducerSchema("ping", PingReducer),
|
||||
__reducerSchema("rename_player", RenamePlayerReducer),
|
||||
__reducerSchema("seed_world", SeedWorldReducer),
|
||||
__reducerSchema("select_target", SelectTargetReducer),
|
||||
__reducerSchema("sell_ore_to_npc_market", SellOreToNpcMarketReducer),
|
||||
__reducerSchema("start_approach", StartApproachReducer),
|
||||
__reducerSchema("start_mining", StartMiningReducer),
|
||||
__reducerSchema("undock", UndockReducer),
|
||||
);
|
||||
|
||||
/** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */
|
||||
const proceduresSchema = __procedures(
|
||||
);
|
||||
|
||||
/** The remote SpacetimeDB module schema, both runtime and type information. */
|
||||
const REMOTE_MODULE = {
|
||||
versionInfo: {
|
||||
cliVersion: "2.3.0" as const,
|
||||
},
|
||||
tables: tablesSchema.schemaType.tables,
|
||||
reducers: reducersSchema.reducersType.reducers,
|
||||
...proceduresSchema,
|
||||
} satisfies __RemoteModule<
|
||||
typeof tablesSchema.schemaType,
|
||||
typeof reducersSchema.reducersType,
|
||||
typeof proceduresSchema
|
||||
>;
|
||||
|
||||
/** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */
|
||||
export const tables: __QueryBuilder<typeof tablesSchema.schemaType> = __makeQueryBuilder(tablesSchema.schemaType);
|
||||
|
||||
/** The reducers available in this remote SpacetimeDB module. */
|
||||
export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers);
|
||||
|
||||
/** The procedures available in this remote SpacetimeDB module. */
|
||||
export const procedures = __convertToAccessorMap(proceduresSchema.procedures);
|
||||
|
||||
/** The context type returned in callbacks for all possible events. */
|
||||
export type EventContext = __EventContextInterface<typeof REMOTE_MODULE>;
|
||||
/** The context type returned in callbacks for reducer events. */
|
||||
export type ReducerEventContext = __ReducerEventContextInterface<typeof REMOTE_MODULE>;
|
||||
/** The context type returned in callbacks for subscription events. */
|
||||
export type SubscriptionEventContext = __SubscriptionEventContextInterface<typeof REMOTE_MODULE>;
|
||||
/** The context type returned in callbacks for error events. */
|
||||
export type ErrorContext = __ErrorContextInterface<typeof REMOTE_MODULE>;
|
||||
/** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */
|
||||
export type SubscriptionHandle = __SubscriptionHandleImpl<typeof REMOTE_MODULE>;
|
||||
|
||||
/** Builder class to configure a new subscription to the remote SpacetimeDB instance. */
|
||||
export class SubscriptionBuilder extends __SubscriptionBuilderImpl<typeof REMOTE_MODULE> {}
|
||||
|
||||
/** Builder class to configure a new database connection to the remote SpacetimeDB instance. */
|
||||
export class DbConnectionBuilder extends __DbConnectionBuilder<DbConnection> {}
|
||||
|
||||
/** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */
|
||||
export class DbConnection extends __DbConnectionImpl<typeof REMOTE_MODULE> {
|
||||
/** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */
|
||||
static builder = (): DbConnectionBuilder => {
|
||||
return new DbConnectionBuilder(REMOTE_MODULE, (config: __DbConnectionConfig<typeof REMOTE_MODULE>) => new DbConnection(config));
|
||||
};
|
||||
|
||||
/** Creates a new {@link SubscriptionBuilder} to configure a subscription to the remote SpacetimeDB instance. */
|
||||
override subscriptionBuilder = (): SubscriptionBuilder => {
|
||||
return new SubscriptionBuilder(this);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {};
|
||||
@@ -1,20 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default __t.row({
|
||||
identity: __t.identity().primaryKey(),
|
||||
displayName: __t.string().name("display_name"),
|
||||
createdAt: __t.timestamp().name("created_at"),
|
||||
updatedAt: __t.timestamp().name("updated_at"),
|
||||
lastConnectedAt: __t.timestamp().name("last_connected_at"),
|
||||
isConnected: __t.bool().name("is_connected"),
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default __t.row({
|
||||
poiId: __t.string().primaryKey().name("poi_id"),
|
||||
systemId: __t.string().name("system_id"),
|
||||
name: __t.string(),
|
||||
poiType: __t.string().name("poi_type"),
|
||||
x: __t.f32(),
|
||||
y: __t.f32(),
|
||||
z: __t.f32(),
|
||||
stationId: __t.string().name("station_id"),
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {
|
||||
displayName: __t.string(),
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {};
|
||||
@@ -1,15 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {
|
||||
poiId: __t.string(),
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {
|
||||
itemId: __t.string(),
|
||||
quantity: __t.u64(),
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default __t.row({
|
||||
eventId: __t.u64().primaryKey().name("event_id"),
|
||||
at: __t.timestamp(),
|
||||
actorIdentity: __t.identity().name("actor_identity"),
|
||||
eventType: __t.string().name("event_type"),
|
||||
message: __t.string(),
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default __t.row({
|
||||
shipId: __t.u64().primaryKey().name("ship_id"),
|
||||
operationType: __t.string().name("operation_type"),
|
||||
targetPoiId: __t.string().name("target_poi_id"),
|
||||
startedAt: __t.timestamp().name("started_at"),
|
||||
durationMs: __t.u64().name("duration_ms"),
|
||||
completesAtMs: __t.u64().name("completes_at_ms"),
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default __t.row({
|
||||
shipId: __t.u64().primaryKey().name("ship_id"),
|
||||
ownerIdentity: __t.identity().name("owner_identity"),
|
||||
shipName: __t.string().name("ship_name"),
|
||||
hullType: __t.string().name("hull_type"),
|
||||
currentSystemId: __t.string().name("current_system_id"),
|
||||
dockedStationId: __t.string().name("docked_station_id"),
|
||||
currentPoiId: __t.string().name("current_poi_id"),
|
||||
selectedPoiId: __t.string().name("selected_poi_id"),
|
||||
flightMode: __t.string().name("flight_mode"),
|
||||
x: __t.f32(),
|
||||
y: __t.f32(),
|
||||
z: __t.f32(),
|
||||
cargoCapacity: __t.u64().name("cargo_capacity"),
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {};
|
||||
@@ -1,13 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {};
|
||||
@@ -1,17 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default __t.row({
|
||||
stationId: __t.string().primaryKey().name("station_id"),
|
||||
systemId: __t.string().name("system_id"),
|
||||
name: __t.string(),
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default __t.row({
|
||||
systemId: __t.string().primaryKey().name("system_id"),
|
||||
name: __t.string(),
|
||||
securityLevel: __t.f32().name("security_level"),
|
||||
});
|
||||
@@ -1,103 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export const CargoItem = __t.object("CargoItem", {
|
||||
cargoItemId: __t.u64(),
|
||||
ownerIdentity: __t.identity(),
|
||||
shipId: __t.u64(),
|
||||
itemId: __t.string(),
|
||||
itemName: __t.string(),
|
||||
category: __t.string(),
|
||||
quantity: __t.u64(),
|
||||
unitPrice: __t.u64(),
|
||||
});
|
||||
export type CargoItem = __Infer<typeof CargoItem>;
|
||||
|
||||
export const Player = __t.object("Player", {
|
||||
identity: __t.identity(),
|
||||
displayName: __t.string(),
|
||||
createdAt: __t.timestamp(),
|
||||
updatedAt: __t.timestamp(),
|
||||
lastConnectedAt: __t.timestamp(),
|
||||
isConnected: __t.bool(),
|
||||
});
|
||||
export type Player = __Infer<typeof Player>;
|
||||
|
||||
export const PointOfInterest = __t.object("PointOfInterest", {
|
||||
poiId: __t.string(),
|
||||
systemId: __t.string(),
|
||||
name: __t.string(),
|
||||
poiType: __t.string(),
|
||||
x: __t.f32(),
|
||||
y: __t.f32(),
|
||||
z: __t.f32(),
|
||||
stationId: __t.string(),
|
||||
});
|
||||
export type PointOfInterest = __Infer<typeof PointOfInterest>;
|
||||
|
||||
export const ServerEvent = __t.object("ServerEvent", {
|
||||
eventId: __t.u64(),
|
||||
at: __t.timestamp(),
|
||||
actorIdentity: __t.identity(),
|
||||
eventType: __t.string(),
|
||||
message: __t.string(),
|
||||
});
|
||||
export type ServerEvent = __Infer<typeof ServerEvent>;
|
||||
|
||||
export const Ship = __t.object("Ship", {
|
||||
shipId: __t.u64(),
|
||||
ownerIdentity: __t.identity(),
|
||||
shipName: __t.string(),
|
||||
hullType: __t.string(),
|
||||
currentSystemId: __t.string(),
|
||||
dockedStationId: __t.string(),
|
||||
currentPoiId: __t.string(),
|
||||
selectedPoiId: __t.string(),
|
||||
flightMode: __t.string(),
|
||||
x: __t.f32(),
|
||||
y: __t.f32(),
|
||||
z: __t.f32(),
|
||||
cargoCapacity: __t.u64(),
|
||||
});
|
||||
export type Ship = __Infer<typeof Ship>;
|
||||
|
||||
export const ShipOperation = __t.object("ShipOperation", {
|
||||
shipId: __t.u64(),
|
||||
operationType: __t.string(),
|
||||
targetPoiId: __t.string(),
|
||||
startedAt: __t.timestamp(),
|
||||
durationMs: __t.u64(),
|
||||
completesAtMs: __t.u64(),
|
||||
});
|
||||
export type ShipOperation = __Infer<typeof ShipOperation>;
|
||||
|
||||
export const Station = __t.object("Station", {
|
||||
stationId: __t.string(),
|
||||
systemId: __t.string(),
|
||||
name: __t.string(),
|
||||
});
|
||||
export type Station = __Infer<typeof Station>;
|
||||
|
||||
export const System = __t.object("System", {
|
||||
systemId: __t.string(),
|
||||
name: __t.string(),
|
||||
securityLevel: __t.f32(),
|
||||
});
|
||||
export type System = __Infer<typeof System>;
|
||||
|
||||
export const Wallet = __t.object("Wallet", {
|
||||
ownerIdentity: __t.identity(),
|
||||
isk: __t.u64(),
|
||||
updatedAt: __t.timestamp(),
|
||||
});
|
||||
export type Wallet = __Infer<typeof Wallet>;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import { type Infer as __Infer } from "spacetimedb";
|
||||
|
||||
// Import all procedure arg schemas
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import { type Infer as __Infer } from "spacetimedb";
|
||||
|
||||
// Import all reducer arg schemas
|
||||
import CompleteApproachReducer from "../complete_approach_reducer";
|
||||
import CompleteMiningCycleReducer from "../complete_mining_cycle_reducer";
|
||||
import ConnectPlayerReducer from "../connect_player_reducer";
|
||||
import DockReducer from "../dock_reducer";
|
||||
import PingReducer from "../ping_reducer";
|
||||
import RenamePlayerReducer from "../rename_player_reducer";
|
||||
import SeedWorldReducer from "../seed_world_reducer";
|
||||
import SelectTargetReducer from "../select_target_reducer";
|
||||
import SellOreToNpcMarketReducer from "../sell_ore_to_npc_market_reducer";
|
||||
import StartApproachReducer from "../start_approach_reducer";
|
||||
import StartMiningReducer from "../start_mining_reducer";
|
||||
import UndockReducer from "../undock_reducer";
|
||||
|
||||
export type CompleteApproachParams = __Infer<typeof CompleteApproachReducer>;
|
||||
export type CompleteMiningCycleParams = __Infer<typeof CompleteMiningCycleReducer>;
|
||||
export type ConnectPlayerParams = __Infer<typeof ConnectPlayerReducer>;
|
||||
export type DockParams = __Infer<typeof DockReducer>;
|
||||
export type PingParams = __Infer<typeof PingReducer>;
|
||||
export type RenamePlayerParams = __Infer<typeof RenamePlayerReducer>;
|
||||
export type SeedWorldParams = __Infer<typeof SeedWorldReducer>;
|
||||
export type SelectTargetParams = __Infer<typeof SelectTargetReducer>;
|
||||
export type SellOreToNpcMarketParams = __Infer<typeof SellOreToNpcMarketReducer>;
|
||||
export type StartApproachParams = __Infer<typeof StartApproachReducer>;
|
||||
export type StartMiningParams = __Infer<typeof StartMiningReducer>;
|
||||
export type UndockParams = __Infer<typeof UndockReducer>;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {};
|
||||
@@ -1,17 +0,0 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
TypeBuilder as __TypeBuilder,
|
||||
t as __t,
|
||||
type AlgebraicTypeType as __AlgebraicTypeType,
|
||||
type Infer as __Infer,
|
||||
} from "spacetimedb";
|
||||
|
||||
export default __t.row({
|
||||
ownerIdentity: __t.identity().primaryKey().name("owner_identity"),
|
||||
isk: __t.u64(),
|
||||
updatedAt: __t.timestamp().name("updated_at"),
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
# apps/game/src/scene/ — 3D Rendering (React Three Fiber)
|
||||
|
||||
The 3D viewport for the game client. Renders the player's ship, surrounding space environment, stations, asteroids, and visual effects.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SpaceCanvas.tsx` | R3F Canvas wrapper. Fixed camera at [8,9,44], FOV 48. No user camera controls yet. |
|
||||
| `SpaceEnvironment.tsx` | Background (#040712), fog, ambient light, two point lights (cyan + amber), animated star field (3600 stars) |
|
||||
| `GameSpaceScene.tsx` | Main orchestrator. Renders stations, asteroid clusters, ship. Manages approach/mining operations. Interpolates ship position. Draws approach line (dashed cyan) and mining line (solid amber). Auto-completes operations via 120ms polling interval. |
|
||||
| `StationMesh.tsx` | Torus ring + cylinder core + crossbar. Clickable. Selection ring when targeted. |
|
||||
| `AsteroidMesh.tsx` | Icosahedron with tumble animation. Clickable. Selection ring when targeted. |
|
||||
| `ShipMesh.tsx` | Cone (hull) + box (wings) + sphere (engine glow). Engine glow changes by flight mode. Subtle bob + roll animation. |
|
||||
|
||||
## Visual Gaps
|
||||
|
||||
- No mining laser beam effect (flat Line only)
|
||||
- No warp/travel animation
|
||||
- No combat visuals (projectiles, shields, explosions)
|
||||
- No NPC/other player ships
|
||||
- No camera controls (orbit, zoom, pan)
|
||||
- No sun/star light source
|
||||
- No nebulae or system-specific skybox
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useFrame, type ThreeEvent } from "@react-three/fiber";
|
||||
import { useRef } from "react";
|
||||
import type { Group } from "three";
|
||||
|
||||
type Vector3Tuple = [number, number, number];
|
||||
|
||||
export function AsteroidMesh({
|
||||
position,
|
||||
scale = 1,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
position: Vector3Tuple;
|
||||
scale?: number;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const groupRef = useRef<Group>(null);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!groupRef.current) return;
|
||||
groupRef.current.rotation.x = clock.elapsedTime * 0.08 * scale;
|
||||
groupRef.current.rotation.y = clock.elapsedTime * 0.05;
|
||||
});
|
||||
|
||||
function handleClick(event: ThreeEvent<MouseEvent>) {
|
||||
event.stopPropagation();
|
||||
onSelect();
|
||||
}
|
||||
|
||||
return (
|
||||
<group ref={groupRef} position={position} scale={scale} onClick={handleClick}>
|
||||
<mesh>
|
||||
<icosahedronGeometry args={[1.1, 1]} />
|
||||
<meshStandardMaterial color="#8b8176" roughness={0.92} metalness={0.08} emissive="#201812" emissiveIntensity={0.18} />
|
||||
</mesh>
|
||||
{selected ? (
|
||||
<mesh rotation={[Math.PI / 2, 0, 0]}>
|
||||
<torusGeometry args={[1.55, 0.035, 8, 52]} />
|
||||
<meshBasicMaterial color="#f0a030" transparent opacity={0.85} />
|
||||
</mesh>
|
||||
) : null}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import { Line } from "@react-three/drei";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { Group } from "three";
|
||||
import type { PointOfInterest, Ship, ShipOperation } from "../module_bindings/types";
|
||||
import { AsteroidMesh } from "./AsteroidMesh";
|
||||
import { ShipMesh } from "./ShipMesh";
|
||||
import { SpaceCanvas } from "./SpaceCanvas";
|
||||
import { SpaceEnvironment } from "./SpaceEnvironment";
|
||||
import { StationMesh } from "./StationMesh";
|
||||
|
||||
type Vector3Tuple = [number, number, number];
|
||||
|
||||
type GameSpaceSceneProps = {
|
||||
ship?: Ship;
|
||||
pois: PointOfInterest[];
|
||||
selectedPoiId?: string;
|
||||
operation?: ShipOperation;
|
||||
onSelectPoi: (poiId: string) => void;
|
||||
onCompleteApproach: () => void;
|
||||
onCompleteMiningCycle: () => void;
|
||||
};
|
||||
|
||||
export function GameSpaceScene(props: GameSpaceSceneProps) {
|
||||
return (
|
||||
<SpaceCanvas>
|
||||
<SceneContents {...props} />
|
||||
</SpaceCanvas>
|
||||
);
|
||||
}
|
||||
|
||||
function SceneContents({
|
||||
ship,
|
||||
pois,
|
||||
selectedPoiId,
|
||||
operation,
|
||||
onSelectPoi,
|
||||
onCompleteApproach,
|
||||
onCompleteMiningCycle,
|
||||
}: GameSpaceSceneProps) {
|
||||
const driftRef = useRef<Group>(null);
|
||||
const completedOperationRef = useRef<string>();
|
||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||
const poiById = useMemo(() => new Map(pois.map((poi) => [poi.poiId, poi])), [pois]);
|
||||
const targetPoi = operation ? poiById.get(operation.targetPoiId) : undefined;
|
||||
const selectedPoi = selectedPoiId ? poiById.get(selectedPoiId) : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => setNowMs(Date.now()), 120);
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const operationKey = operation ? `${operation.shipId}-${operation.operationType}-${operation.completesAtMs}` : undefined;
|
||||
if (completedOperationRef.current !== operationKey) {
|
||||
completedOperationRef.current = undefined;
|
||||
}
|
||||
|
||||
if (!operation || !operationKey || nowMs < Number(operation.completesAtMs) || completedOperationRef.current === operationKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
completedOperationRef.current = operationKey;
|
||||
if (operation.operationType === "approach") onCompleteApproach();
|
||||
if (operation.operationType === "mining") onCompleteMiningCycle();
|
||||
}, [nowMs, onCompleteApproach, onCompleteMiningCycle, operation]);
|
||||
|
||||
useFrame(({ clock, camera }) => {
|
||||
if (driftRef.current) {
|
||||
driftRef.current.rotation.y = Math.sin(clock.elapsedTime * 0.07) * 0.035;
|
||||
driftRef.current.rotation.x = Math.sin(clock.elapsedTime * 0.05) * 0.018;
|
||||
}
|
||||
camera.position.x = 8 + Math.sin(clock.elapsedTime * 0.18) * 1.4;
|
||||
camera.position.y = 9 + Math.cos(clock.elapsedTime * 0.14) * 0.7;
|
||||
camera.lookAt(15, -1, -8);
|
||||
});
|
||||
|
||||
const shipPosition = ship ? getShipPosition(ship, operation, targetPoi, nowMs) : ([0, 0, 0] as Vector3Tuple);
|
||||
const stationPois = pois.filter((poi) => poi.poiType === "station");
|
||||
const beltPois = pois.filter((poi) => poi.poiType === "asteroid_belt");
|
||||
|
||||
return (
|
||||
<>
|
||||
<SpaceEnvironment />
|
||||
<group ref={driftRef}>
|
||||
<GridPlane />
|
||||
{stationPois.map((poi) => (
|
||||
<StationMesh
|
||||
key={poi.poiId}
|
||||
position={poiPosition(poi)}
|
||||
selected={selectedPoiId === poi.poiId}
|
||||
onSelect={() => onSelectPoi(poi.poiId)}
|
||||
/>
|
||||
))}
|
||||
{beltPois.map((poi) => (
|
||||
<AsteroidCluster key={poi.poiId} poi={poi} selected={selectedPoiId === poi.poiId} onSelect={() => onSelectPoi(poi.poiId)} />
|
||||
))}
|
||||
{ship ? <ShipMesh position={shipPosition} flightMode={ship.flightMode} /> : null}
|
||||
{operation?.operationType === "approach" && targetPoi ? (
|
||||
<Line points={[shipPosition, poiPosition(targetPoi)]} color="#22d3ee" lineWidth={1.5} dashed dashSize={0.5} gapSize={0.25} />
|
||||
) : null}
|
||||
{operation?.operationType === "mining" && selectedPoi ? (
|
||||
<Line points={[shipPosition, poiPosition(selectedPoi)]} color="#f0a030" lineWidth={2} transparent opacity={0.85} />
|
||||
) : null}
|
||||
</group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AsteroidCluster({
|
||||
poi,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
poi: PointOfInterest;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const base = poiPosition(poi);
|
||||
const offsets: Array<[number, number, number, number]> = [
|
||||
[0, 0, 0, 1.25],
|
||||
[-2.7, 0.8, 1.4, 0.78],
|
||||
[2.4, -0.6, -1.6, 0.95],
|
||||
];
|
||||
|
||||
return (
|
||||
<group>
|
||||
{offsets.map(([x, y, z, scale], index) => (
|
||||
<AsteroidMesh
|
||||
key={`${poi.poiId}-${index}`}
|
||||
position={[base[0] + x, base[1] + y, base[2] + z]}
|
||||
scale={scale}
|
||||
selected={selected && index === 0}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
{selected ? (
|
||||
<mesh position={base} rotation={[Math.PI / 2, 0, 0]}>
|
||||
<torusGeometry args={[4.6, 0.035, 8, 80]} />
|
||||
<meshBasicMaterial color="#f0a030" transparent opacity={0.62} />
|
||||
</mesh>
|
||||
) : null}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function GridPlane() {
|
||||
return (
|
||||
<gridHelper args={[82, 28, "#1d354c", "#102238"]} position={[15, -5.8, -8]} rotation={[0, 0, 0]} />
|
||||
);
|
||||
}
|
||||
|
||||
function getShipPosition(ship: Ship, operation: ShipOperation | undefined, targetPoi: PointOfInterest | undefined, nowMs: number) {
|
||||
const start = [ship.x, ship.y, ship.z] as Vector3Tuple;
|
||||
if (operation?.operationType !== "approach" || !targetPoi) return start;
|
||||
|
||||
const startedAt = Number(operation.startedAt.toMillis());
|
||||
const duration = Number(operation.durationMs);
|
||||
const progress = Math.max(0, Math.min(1, (nowMs - startedAt) / duration));
|
||||
const target = poiPosition(targetPoi);
|
||||
|
||||
return [
|
||||
lerp(start[0], target[0], progress),
|
||||
lerp(start[1], target[1], progress),
|
||||
lerp(start[2], target[2], progress),
|
||||
] as Vector3Tuple;
|
||||
}
|
||||
|
||||
function poiPosition(poi: PointOfInterest): Vector3Tuple {
|
||||
return [poi.x, poi.y, poi.z];
|
||||
}
|
||||
|
||||
function lerp(start: number, end: number, progress: number) {
|
||||
return start + (end - start) * progress;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useRef } from "react";
|
||||
import type { Group } from "three";
|
||||
|
||||
type Vector3Tuple = [number, number, number];
|
||||
|
||||
export function ShipMesh({ position, flightMode }: { position: Vector3Tuple; flightMode?: string }) {
|
||||
const groupRef = useRef<Group>(null);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!groupRef.current) return;
|
||||
groupRef.current.rotation.z = Math.sin(clock.elapsedTime * 2) * 0.045;
|
||||
groupRef.current.position.y = position[1] + Math.sin(clock.elapsedTime * 1.4) * 0.08;
|
||||
});
|
||||
|
||||
const engineActive = flightMode === "approaching" || flightMode === "flight";
|
||||
|
||||
return (
|
||||
<group ref={groupRef} position={position} rotation={[0.18, -0.42, 0]}>
|
||||
<mesh>
|
||||
<coneGeometry args={[0.55, 1.8, 4]} />
|
||||
<meshStandardMaterial color="#d8ecff" metalness={0.7} roughness={0.28} emissive="#123244" emissiveIntensity={0.25} />
|
||||
</mesh>
|
||||
<mesh position={[0, -0.1, -0.62]} scale={[1.25, 0.22, 0.36]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color="#6fc8ff" metalness={0.5} roughness={0.3} />
|
||||
</mesh>
|
||||
<mesh position={[0, 0, -1.06]}>
|
||||
<sphereGeometry args={[engineActive ? 0.22 : 0.12, 16, 10]} />
|
||||
<meshBasicMaterial color={engineActive ? "#22d3ee" : "#245264"} transparent opacity={engineActive ? 0.85 : 0.42} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function SpaceCanvas({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<Canvas camera={{ position: [8, 9, 44], fov: 48 }} gl={{ antialias: true, alpha: true }}>
|
||||
{children}
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Stars } from "@react-three/drei";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useRef } from "react";
|
||||
import type { Group } from "three";
|
||||
|
||||
export function SpaceEnvironment() {
|
||||
const dustRef = useRef<Group>(null);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!dustRef.current) return;
|
||||
dustRef.current.rotation.y = clock.elapsedTime * 0.015;
|
||||
dustRef.current.rotation.x = Math.sin(clock.elapsedTime * 0.08) * 0.015;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<color attach="background" args={["#040712"]} />
|
||||
<fog attach="fog" args={["#040712", 42, 130]} />
|
||||
<ambientLight intensity={0.45} />
|
||||
<pointLight position={[0, 16, 16]} intensity={2.2} color="#a8f3ff" />
|
||||
<pointLight position={[36, 4, -16]} intensity={1.8} color="#f0a030" />
|
||||
<group ref={dustRef}>
|
||||
<Stars radius={95} depth={45} count={3600} factor={4} saturation={0.35} fade speed={0.25} />
|
||||
</group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { ThreeEvent } from "@react-three/fiber";
|
||||
|
||||
type Vector3Tuple = [number, number, number];
|
||||
|
||||
export function StationMesh({
|
||||
position,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
position: Vector3Tuple;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
function handleClick(event: ThreeEvent<MouseEvent>) {
|
||||
event.stopPropagation();
|
||||
onSelect();
|
||||
}
|
||||
|
||||
return (
|
||||
<group position={position} onClick={handleClick}>
|
||||
<mesh rotation={[Math.PI / 2, 0, 0]}>
|
||||
<torusGeometry args={[2.35, 0.16, 16, 64]} />
|
||||
<meshStandardMaterial color="#9ad8ff" metalness={0.75} roughness={0.22} emissive="#0f415b" emissiveIntensity={0.4} />
|
||||
</mesh>
|
||||
<mesh>
|
||||
<cylinderGeometry args={[0.5, 0.72, 3.4, 8]} />
|
||||
<meshStandardMaterial color="#c8d3df" metalness={0.8} roughness={0.27} />
|
||||
</mesh>
|
||||
<mesh rotation={[0, 0, Math.PI / 2]}>
|
||||
<boxGeometry args={[5.2, 0.18, 0.18]} />
|
||||
<meshStandardMaterial color="#f0a030" emissive="#6b3504" emissiveIntensity={0.35} />
|
||||
</mesh>
|
||||
{selected ? <SelectionRing radius={3.15} color="#f0a030" /> : null}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectionRing({ radius, color }: { radius: number; color: string }) {
|
||||
return (
|
||||
<mesh rotation={[Math.PI / 2, 0, 0]}>
|
||||
<torusGeometry args={[radius, 0.035, 8, 80]} />
|
||||
<meshBasicMaterial color={color} transparent opacity={0.9} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
# apps/game/src/spacetime/ — SpacetimeDB Connection Layer
|
||||
|
||||
Bridge between the game client and the SpacetimeDB backend. Manages connection lifecycle, table subscriptions, and exposes a typed React hook for the game session.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `client.ts` | Connection factory. Creates `DbConnection`, subscribes to all 9 tables (hardcoded to `solace` system), registers table-change listeners for React re-renders, manages auth token in localStorage. Auto-calls `seedWorld` and `connectPlayer` on every connection. |
|
||||
| `useSpacetimeConnection.ts` | React hook wrapping `client.ts`. Exposes connection state (idle/connecting/connected/error), identity, and a 1.5s revision-poll trigger. |
|
||||
| `useGameSession.ts` | Main data/action bridge. Reads all 9 SpacetimeDB tables into a typed `GameSession` object. Exposes action callbacks for all 12 reducers. Hardcodes system/station lookups to `solace`/`solace-prime`. |
|
||||
|
||||
## Subscriptions (hardcoded)
|
||||
|
||||
```sql
|
||||
SELECT * FROM player
|
||||
SELECT * FROM ship
|
||||
SELECT * FROM system WHERE system_id = 'solace'
|
||||
SELECT * FROM station WHERE station_id = 'solace-prime'
|
||||
SELECT * FROM point_of_interest WHERE system_id = 'solace'
|
||||
SELECT * FROM cargo_item
|
||||
SELECT * FROM wallet
|
||||
SELECT * FROM ship_operation
|
||||
SELECT * FROM server_event
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
|
||||
- System/station subscriptions hardcoded to `solace`/`solace-prime` — must be dynamic for multi-system
|
||||
- No reconnection logic (no exponential backoff)
|
||||
- Auth token stored in localStorage (acceptable for auth, but design says no localStorage for game state)
|
||||
- Events capped at 12 in useGameSession (no pagination)
|
||||
@@ -1,135 +0,0 @@
|
||||
import type { Identity } from "spacetimedb";
|
||||
import { DbConnection, type DbConnection as GameDbConnection } from "../module_bindings";
|
||||
|
||||
export type IdentityLike = Pick<Identity, "toHexString" | "toString">;
|
||||
|
||||
export type ConnectionStatus = "idle" | "connecting" | "connected" | "disconnected" | "error";
|
||||
|
||||
export type ConnectionConfig = {
|
||||
uri: string;
|
||||
database: string;
|
||||
displayName: string;
|
||||
onStatus: (status: ConnectionStatus, message?: string) => void;
|
||||
onIdentity: (identity?: IdentityLike) => void;
|
||||
onDataChanged: () => void;
|
||||
onReducerStatus: (message: string, isError?: boolean) => void;
|
||||
};
|
||||
|
||||
export function createSpacetimeConnection(config: ConnectionConfig): GameDbConnection | null {
|
||||
try {
|
||||
config.onStatus("connecting");
|
||||
|
||||
const authTokenKey = getAuthTokenKey(config);
|
||||
const connection = DbConnection.builder()
|
||||
.withUri(config.uri)
|
||||
.withDatabaseName(config.database)
|
||||
.withToken(readStoredAuthToken(authTokenKey))
|
||||
.onConnect((conn, identity, token) => {
|
||||
storeAuthToken(authTokenKey, token);
|
||||
config.onIdentity(identity);
|
||||
config.onStatus("connected");
|
||||
subscribeToShellRows(conn, identity);
|
||||
invokeReducer(config, "seedWorld", () => conn.reducers.seedWorld({}));
|
||||
invokeReducer(config, "connectPlayer", () => conn.reducers.connectPlayer({ displayName: config.displayName }));
|
||||
config.onDataChanged();
|
||||
})
|
||||
.onConnectError((_ctx, error) => {
|
||||
config.onStatus("error", error.message);
|
||||
config.onReducerStatus(`Connection failed: ${error.message}`, true);
|
||||
})
|
||||
.onDisconnect((_ctx, error) => {
|
||||
config.onStatus("disconnected", error?.message);
|
||||
config.onIdentity(undefined);
|
||||
})
|
||||
.build();
|
||||
|
||||
registerTableRefresh(connection, config.onDataChanged);
|
||||
return connection;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
config.onStatus("error", message);
|
||||
config.onReducerStatus(message, true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function invokeReducer(config: Pick<ConnectionConfig, "onReducerStatus">, name: string, call: () => Promise<void>) {
|
||||
try {
|
||||
call()
|
||||
.then(() => config.onReducerStatus(`${name} reducer completed`))
|
||||
.catch((error: unknown) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
config.onReducerStatus(`${name} reducer failed: ${message}`, true);
|
||||
});
|
||||
config.onReducerStatus(`${name} reducer sent`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
config.onReducerStatus(`${name} reducer failed: ${message}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToShellRows(conn: GameDbConnection, _identity: IdentityLike) {
|
||||
// Keep the first shell subscriptions broad; the session hook selects caller/starter rows.
|
||||
conn.subscriptionBuilder().subscribe([
|
||||
"SELECT * FROM player",
|
||||
"SELECT * FROM ship",
|
||||
"SELECT * FROM system WHERE system_id = 'solace'",
|
||||
"SELECT * FROM station WHERE station_id = 'solace-prime'",
|
||||
"SELECT * FROM point_of_interest WHERE system_id = 'solace'",
|
||||
"SELECT * FROM cargo_item",
|
||||
"SELECT * FROM wallet",
|
||||
"SELECT * FROM ship_operation",
|
||||
"SELECT * FROM server_event",
|
||||
]);
|
||||
}
|
||||
|
||||
function registerTableRefresh(conn: GameDbConnection, onDataChanged: () => void) {
|
||||
const tables = [
|
||||
conn.db.player,
|
||||
conn.db.ship,
|
||||
conn.db.system,
|
||||
conn.db.station,
|
||||
conn.db.point_of_interest,
|
||||
conn.db.cargo_item,
|
||||
conn.db.wallet,
|
||||
conn.db.ship_operation,
|
||||
conn.db.server_event,
|
||||
] as unknown as Array<{
|
||||
onInsert?: (callback: (...args: unknown[]) => void) => void;
|
||||
onUpdate?: (callback: (...args: unknown[]) => void) => void;
|
||||
onDelete?: (callback: (...args: unknown[]) => void) => void;
|
||||
}>;
|
||||
|
||||
for (const table of tables) {
|
||||
table.onInsert?.(onDataChanged);
|
||||
table.onUpdate?.(onDataChanged);
|
||||
table.onDelete?.(onDataChanged);
|
||||
}
|
||||
}
|
||||
|
||||
export function formatIdentity(identity?: IdentityLike | string) {
|
||||
if (!identity) return "unassigned";
|
||||
if (typeof identity === "string") return identity;
|
||||
return identity.toHexString?.() ?? identity.toString?.() ?? "identity";
|
||||
}
|
||||
|
||||
function getAuthTokenKey(config: Pick<ConnectionConfig, "uri" | "database">) {
|
||||
return `void-nav:spacetime-token:${config.uri}:${config.database}`;
|
||||
}
|
||||
|
||||
function readStoredAuthToken(key: string) {
|
||||
try {
|
||||
return window.localStorage.getItem(key) ?? undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function storeAuthToken(key: string, token?: string) {
|
||||
if (!token) return;
|
||||
try {
|
||||
window.localStorage.setItem(key, token);
|
||||
} catch {
|
||||
// Losing the auth token only affects reconnect identity continuity.
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import type { DbConnection } from "../module_bindings";
|
||||
import type {
|
||||
CargoItem,
|
||||
Player,
|
||||
PointOfInterest,
|
||||
ServerEvent,
|
||||
Ship,
|
||||
ShipOperation,
|
||||
Station,
|
||||
System,
|
||||
Wallet,
|
||||
} from "../module_bindings/types";
|
||||
import { formatIdentity, invokeReducer, type IdentityLike } from "./client";
|
||||
|
||||
type ReducerReporter = (message: string, isError?: boolean) => void;
|
||||
|
||||
export type GameSession = {
|
||||
player: Player | undefined;
|
||||
ship: Ship | undefined;
|
||||
system: System | undefined;
|
||||
station: Station | undefined;
|
||||
pois: PointOfInterest[];
|
||||
cargo: CargoItem[];
|
||||
wallet: Wallet | undefined;
|
||||
operation: ShipOperation | undefined;
|
||||
events: ServerEvent[];
|
||||
actions: {
|
||||
renamePlayer(displayName: string): void;
|
||||
ping(): void;
|
||||
undock(): void;
|
||||
selectTarget(poiId: string): void;
|
||||
startApproach(): void;
|
||||
completeApproach(): void;
|
||||
dock(): void;
|
||||
startMining(): void;
|
||||
completeMiningCycle(): void;
|
||||
sellOreToNpcMarket(itemId: string, quantity: bigint): void;
|
||||
};
|
||||
};
|
||||
|
||||
export function useGameSession(
|
||||
connection: DbConnection | null,
|
||||
revision: number,
|
||||
onReducerStatus: ReducerReporter,
|
||||
identity?: IdentityLike,
|
||||
): GameSession {
|
||||
const rows = useMemo(() => readRows(connection, identity), [connection, revision, identity]);
|
||||
|
||||
return {
|
||||
...rows,
|
||||
actions: {
|
||||
renamePlayer(displayName: string) {
|
||||
invokeIfConnected(connection, onReducerStatus, "renamePlayer", () =>
|
||||
connection!.reducers.renamePlayer({ displayName }),
|
||||
);
|
||||
},
|
||||
ping() {
|
||||
invokeIfConnected(connection, onReducerStatus, "ping", () => connection!.reducers.ping({}));
|
||||
},
|
||||
undock() {
|
||||
invokeIfConnected(connection, onReducerStatus, "undock", () => connection!.reducers.undock({}));
|
||||
},
|
||||
selectTarget(poiId: string) {
|
||||
invokeIfConnected(connection, onReducerStatus, "selectTarget", () => connection!.reducers.selectTarget({ poiId }));
|
||||
},
|
||||
startApproach() {
|
||||
invokeIfConnected(connection, onReducerStatus, "startApproach", () => connection!.reducers.startApproach({}));
|
||||
},
|
||||
completeApproach() {
|
||||
invokeIfConnected(connection, onReducerStatus, "completeApproach", () => connection!.reducers.completeApproach({}));
|
||||
},
|
||||
dock() {
|
||||
invokeIfConnected(connection, onReducerStatus, "dock", () => connection!.reducers.dock({}));
|
||||
},
|
||||
startMining() {
|
||||
invokeIfConnected(connection, onReducerStatus, "startMining", () => connection!.reducers.startMining({}));
|
||||
},
|
||||
completeMiningCycle() {
|
||||
invokeIfConnected(connection, onReducerStatus, "completeMiningCycle", () =>
|
||||
connection!.reducers.completeMiningCycle({}),
|
||||
);
|
||||
},
|
||||
sellOreToNpcMarket(itemId: string, quantity: bigint) {
|
||||
invokeIfConnected(connection, onReducerStatus, "sellOreToNpcMarket", () =>
|
||||
connection!.reducers.sellOreToNpcMarket({ itemId, quantity }),
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function invokeIfConnected(
|
||||
connection: DbConnection | null,
|
||||
onReducerStatus: ReducerReporter,
|
||||
name: string,
|
||||
call: () => Promise<void>,
|
||||
) {
|
||||
if (!connection) {
|
||||
onReducerStatus(`${name} unavailable until connected`, true);
|
||||
return;
|
||||
}
|
||||
|
||||
invokeReducer({ onReducerStatus }, name, call);
|
||||
}
|
||||
|
||||
function readRows(connection: DbConnection | null, identity?: IdentityLike) {
|
||||
const players = readTable<Player>(connection?.db.player);
|
||||
const ships = readTable<Ship>(connection?.db.ship);
|
||||
const systems = readTable<System>(connection?.db.system);
|
||||
const stations = readTable<Station>(connection?.db.station);
|
||||
const pois = readTable<PointOfInterest>(connection?.db.point_of_interest).sort((a, b) => a.name.localeCompare(b.name));
|
||||
const cargoRows = readTable<CargoItem>(connection?.db.cargo_item);
|
||||
const wallets = readTable<Wallet>(connection?.db.wallet);
|
||||
const operations = readTable<ShipOperation>(connection?.db.ship_operation);
|
||||
const events = readTable<ServerEvent>(connection?.db.server_event).sort((a, b) =>
|
||||
compareBigintsDesc(a.eventId, b.eventId),
|
||||
);
|
||||
|
||||
const player = players.find((row) => identitiesEqual(row.identity, identity)) ?? players[0];
|
||||
const ship = ships.find((row) => identitiesEqual(row.ownerIdentity, player?.identity ?? identity)) ?? ships[0];
|
||||
const cargo = ship ? cargoRows.filter((row) => row.shipId === ship.shipId) : [];
|
||||
const wallet = wallets.find((row) => identitiesEqual(row.ownerIdentity, player?.identity ?? identity)) ?? wallets[0];
|
||||
const operation = ship ? operations.find((row) => row.shipId === ship.shipId) : undefined;
|
||||
|
||||
return {
|
||||
player,
|
||||
ship,
|
||||
system: systems.find((row) => row.systemId === "solace") ?? systems[0],
|
||||
station: stations.find((row) => row.stationId === "solace-prime") ?? stations[0],
|
||||
pois,
|
||||
cargo,
|
||||
wallet,
|
||||
operation,
|
||||
events: events.slice(0, 12),
|
||||
};
|
||||
}
|
||||
|
||||
function readTable<Row>(table?: { iter?: () => Iterable<Row> }): Row[] {
|
||||
if (!table?.iter) return [];
|
||||
try {
|
||||
return Array.from(table.iter());
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function compareBigintsDesc(left: bigint, right: bigint) {
|
||||
if (left === right) return 0;
|
||||
return left > right ? -1 : 1;
|
||||
}
|
||||
|
||||
function identitiesEqual(left?: IdentityLike, right?: IdentityLike) {
|
||||
if (!left || !right) return false;
|
||||
return formatIdentity(left) === formatIdentity(right);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { DbConnection } from "../module_bindings";
|
||||
import { createSpacetimeConnection, type ConnectionStatus, type IdentityLike } from "./client";
|
||||
|
||||
const defaultUri = import.meta.env.VITE_SPACETIME_URI ?? import.meta.env.VITE_SPACETIMEDB_HOST ?? "http://localhost:3000";
|
||||
const defaultDatabase =
|
||||
import.meta.env.VITE_SPACETIME_DATABASE ?? import.meta.env.VITE_SPACETIMEDB_DB_NAME ?? "void-nav-dev";
|
||||
|
||||
export function useSpacetimeConnection(displayName: string) {
|
||||
const [status, setStatus] = useState<ConnectionStatus>("idle");
|
||||
const [message, setMessage] = useState<string>();
|
||||
const [identity, setIdentity] = useState<IdentityLike>();
|
||||
const [connection, setConnection] = useState<DbConnection | null>(null);
|
||||
const [revision, setRevision] = useState(0);
|
||||
const [reducerStatus, setReducerStatus] = useState<{ message: string; isError: boolean }>();
|
||||
|
||||
const config = useMemo(
|
||||
() => ({
|
||||
uri: defaultUri,
|
||||
database: defaultDatabase,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const displayNameRef = useRef(displayName);
|
||||
displayNameRef.current = displayName;
|
||||
|
||||
useEffect(() => {
|
||||
const conn = createSpacetimeConnection({
|
||||
...config,
|
||||
displayName: displayNameRef.current,
|
||||
onStatus: (nextStatus, nextMessage) => {
|
||||
setStatus(nextStatus);
|
||||
setMessage(nextMessage);
|
||||
},
|
||||
onIdentity: setIdentity,
|
||||
onDataChanged: () => setRevision((value) => value + 1),
|
||||
onReducerStatus: (nextMessage, isError = false) => setReducerStatus({ message: nextMessage, isError }),
|
||||
});
|
||||
|
||||
setConnection(conn);
|
||||
|
||||
const refresh = window.setInterval(() => {
|
||||
if (conn) setRevision((value) => value + 1);
|
||||
}, 1500);
|
||||
|
||||
return () => {
|
||||
window.clearInterval(refresh);
|
||||
conn?.disconnect?.();
|
||||
};
|
||||
}, [config]);
|
||||
|
||||
return {
|
||||
connection,
|
||||
status,
|
||||
message,
|
||||
identity,
|
||||
revision,
|
||||
reducerStatus,
|
||||
uri: config.uri,
|
||||
database: config.database,
|
||||
};
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
@import "@void-nav/ui/styles.css";
|
||||
|
||||
@source "../../../packages/ui/src";
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
# apps/game/src/ui/ — Game UI Panels
|
||||
|
||||
All UI components for the game client. Composed by `GameShell.tsx` into a three-column layout with a bottom command rail.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose | Status |
|
||||
|------|---------|--------|
|
||||
| `CommandRail.tsx` | Bottom-fixed contextual action bar. Shows progress bars during operations, context-appropriate buttons otherwise. Covers: undock, approach, dock, mine, sell. | Partial — missing abort, warp, combat, station service buttons |
|
||||
| `ConnectionPanel.tsx` | Dev diagnostic panel: connection status, identity, endpoint, pilot name input, rename/ping buttons. | Done — but this is a dev tool, not player-facing |
|
||||
| `CargoPanel.tsx` | Cargo hold display: fill bar + item list with name, quantity, category, unit price. | Partial — no jettison, no stacking/splitting |
|
||||
| `NpcMarketPanel.tsx` | Sell ore to NPCs. Quantity input, max button, payout preview. Only visible when docked. Prices hardcoded client-side. | Partial — only Veldspar, hardcoded prices, sell-only |
|
||||
| `WalletPanel.tsx` | ISK balance display. | Partial — no transaction history |
|
||||
| `ShipStatusPanel.tsx` | Flight mode badge, hull/shield/speed bars, position, operation countdown. | Partial — hull always 100%, shields random, speed hardcoded |
|
||||
| `TargetPanel.tsx` | Selected target info, clickable POI list. | Partial — no distance, no sorting |
|
||||
| `MiniStarMap.tsx` | SVG 2D system map: POIs, ship position, operation lines, click-to-select. | Done — single system only |
|
||||
| `EventFeed.tsx` | Server event log (last 12 events, type + timestamp + message). | Partial — no filtering, no pagination, no color coding |
|
||||
| `useKeyboardShortcuts.ts` | U=undock, D=dock, A=approach, M=mine, 1-9=select target. | Partial — no sell/refine/fit/combat/warp shortcuts |
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Panel } from "@void-nav/ui";
|
||||
import type { CargoItem, Ship } from "../module_bindings/types";
|
||||
|
||||
export function CargoPanel({ cargo, ship }: { cargo: CargoItem[]; ship?: Ship }) {
|
||||
const used = cargo.reduce((total, item) => total + item.quantity, 0n);
|
||||
const capacity = ship?.cargoCapacity ?? 0n;
|
||||
const fillPct = capacity > 0n ? Math.min(100, Math.round((Number(used) / Number(capacity)) * 100)) : 0;
|
||||
const barColor = fillPct >= 90 ? "#ef4444" : fillPct >= 60 ? "#f0a030" : "#22d3ee";
|
||||
|
||||
return (
|
||||
<Panel className="p-4">
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<p className="m-0 font-mono text-xs uppercase tracking-[0.1em] text-muted">Cargo</p>
|
||||
<span className="font-mono text-xs text-fg-dim">
|
||||
{used.toString()} / {capacity.toString()} m³
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 h-2 overflow-hidden rounded-full bg-surface-raised">
|
||||
<div
|
||||
className="h-full rounded-full transition-[width] duration-300"
|
||||
style={{ width: `${fillPct}%`, backgroundColor: barColor }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2">
|
||||
{cargo.length > 0 ? (
|
||||
cargo.map((item) => (
|
||||
<div key={String(item.cargoItemId)} className="rounded-md border border-border bg-bg-subtle px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-3 text-sm">
|
||||
<span className="font-semibold text-fg-bright">{item.itemName}</span>
|
||||
<span className="font-mono text-xs text-cyan">{item.quantity.toString()}</span>
|
||||
</div>
|
||||
<div className="mt-1 font-mono text-xs uppercase tracking-[0.08em] text-muted">
|
||||
{item.category} / {item.unitPrice.toString()} ISK per unit
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="m-0 rounded-md border border-border bg-bg-subtle p-3 text-sm text-fg-dim">Cargo hold empty.</p>
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@void-nav/ui";
|
||||
import type { CargoItem, PointOfInterest, Ship, ShipOperation } from "../module_bindings/types";
|
||||
|
||||
export function CommandRail({
|
||||
ship,
|
||||
pois,
|
||||
cargo,
|
||||
operation,
|
||||
onUndock,
|
||||
onStartApproach,
|
||||
onDock,
|
||||
onStartMining,
|
||||
onSellOre,
|
||||
}: {
|
||||
ship?: Ship;
|
||||
pois: PointOfInterest[];
|
||||
cargo: CargoItem[];
|
||||
operation?: ShipOperation;
|
||||
onUndock: () => void;
|
||||
onStartApproach: () => void;
|
||||
onDock: () => void;
|
||||
onStartMining: () => void;
|
||||
onSellOre: (item: CargoItem) => void;
|
||||
}) {
|
||||
const selected = pois.find((poi) => poi.poiId === ship?.selectedPoiId);
|
||||
const current = pois.find((poi) => poi.poiId === ship?.currentPoiId);
|
||||
const ore = cargo.find((item) => item.category === "ore" && item.quantity > 0n);
|
||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (!operation) return;
|
||||
const timer = window.setInterval(() => setNowMs(Date.now()), 80);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [operation]);
|
||||
|
||||
let content = <span className="text-sm text-fg-dim">Connect to SpacetimeDB to initialize the starter ship.</span>;
|
||||
|
||||
if (ship) {
|
||||
if (operation?.operationType === "approach") {
|
||||
const progress = getOperationProgress(operation, nowMs);
|
||||
content = (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-cyan">Approaching target...</span>
|
||||
<span className="ml-auto font-mono text-xs text-fg-dim">{Math.round(progress * 100)}%</span>
|
||||
</div>
|
||||
<ProgressBar progress={progress} color="#22d3ee" />
|
||||
</div>
|
||||
);
|
||||
} else if (operation?.operationType === "mining") {
|
||||
const progress = getOperationProgress(operation, nowMs);
|
||||
content = (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-accent">Mining in progress...</span>
|
||||
<span className="ml-auto font-mono text-xs text-fg-dim">{Math.round(progress * 100)}%</span>
|
||||
</div>
|
||||
<ProgressBar progress={progress} color="#f0a030" />
|
||||
</div>
|
||||
);
|
||||
} else if (ship.dockedStationId.length > 0) {
|
||||
content = (
|
||||
<>
|
||||
<Button tone="primary" onClick={onUndock}>
|
||||
Undock
|
||||
</Button>
|
||||
{ore ? <Button onClick={() => onSellOre(ore)}>Sell Ore</Button> : null}
|
||||
</>
|
||||
);
|
||||
} else if (selected && selected.poiId !== ship.currentPoiId) {
|
||||
content = (
|
||||
<Button tone="primary" onClick={onStartApproach}>
|
||||
Approach
|
||||
</Button>
|
||||
);
|
||||
} else if (current?.stationId) {
|
||||
content = (
|
||||
<Button tone="primary" onClick={onDock}>
|
||||
Dock
|
||||
</Button>
|
||||
);
|
||||
} else if (current?.poiType === "asteroid_belt") {
|
||||
content = (
|
||||
<Button tone="primary" onClick={onStartMining} disabled={cargoUsed(cargo) >= ship.cargoCapacity}>
|
||||
Start Mining
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
content = <span className="text-sm text-fg-dim">Select a station or asteroid belt target.</span>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto mx-auto flex min-h-16 w-full max-w-3xl items-center justify-center gap-3 rounded-lg border border-border bg-bg/86 px-4 py-3 shadow-[0_18px_70px_rgba(0,0,0,0.35)] backdrop-blur">
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProgressBar({ progress, color }: { progress: number; color: string }) {
|
||||
return (
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-surface-raised">
|
||||
<div
|
||||
className="h-full rounded-full transition-[width] duration-100"
|
||||
style={{ width: `${Math.max(0, Math.min(1, progress)) * 100}%`, backgroundColor: color }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getOperationProgress(operation: ShipOperation, nowMs: number) {
|
||||
const startedAt = Number(operation.startedAt.toMillis());
|
||||
const duration = Number(operation.durationMs);
|
||||
if (duration <= 0) return 1;
|
||||
return Math.max(0, Math.min(1, (nowMs - startedAt) / duration));
|
||||
}
|
||||
|
||||
function cargoUsed(cargo: CargoItem[]) {
|
||||
return cargo.reduce((total, item) => total + item.quantity, 0n);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Button, Panel } from "@void-nav/ui";
|
||||
import { formatIdentity, type ConnectionStatus, type IdentityLike } from "../spacetime/client";
|
||||
|
||||
export function ConnectionPanel({
|
||||
status,
|
||||
label,
|
||||
identity,
|
||||
uri,
|
||||
database,
|
||||
message,
|
||||
reducerMessage,
|
||||
reducerIsError,
|
||||
displayName,
|
||||
onDisplayNameChange,
|
||||
onRename,
|
||||
onPing,
|
||||
}: {
|
||||
status: ConnectionStatus;
|
||||
label: string;
|
||||
identity?: IdentityLike;
|
||||
uri: string;
|
||||
database: string;
|
||||
message?: string;
|
||||
reducerMessage?: string;
|
||||
reducerIsError?: boolean;
|
||||
displayName: string;
|
||||
onDisplayNameChange: (value: string) => void;
|
||||
onRename: () => void;
|
||||
onPing: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Panel className="p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="m-0 font-mono text-xs uppercase tracking-[0.1em] text-muted">Connection</p>
|
||||
<span className={`rounded-md border px-2 py-1 font-mono text-xs uppercase tracking-[0.08em] ${statusClass(status)}`}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2 text-sm">
|
||||
<InfoRow label="Identity" value={formatIdentity(identity)} />
|
||||
<InfoRow label="Endpoint" value={`${uri} / ${database}`} />
|
||||
<InfoRow label="Transport" value={message ?? "No transport message"} />
|
||||
<InfoRow label="Reducer" value={reducerMessage ?? "Waiting for reducer activity"} error={reducerIsError} />
|
||||
</div>
|
||||
<label className="mt-4 grid gap-2 text-sm">
|
||||
<span className="font-mono text-xs uppercase tracking-[0.1em] text-muted">Pilot name</span>
|
||||
<input
|
||||
value={displayName}
|
||||
onChange={(event) => onDisplayNameChange(event.target.value)}
|
||||
className="min-h-10 rounded-md border border-border bg-surface-raised px-3 py-2 text-fg outline-none transition-colors focus:border-cyan"
|
||||
/>
|
||||
</label>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<Button className="px-3" onClick={onRename} disabled={status !== "connected"}>
|
||||
Rename
|
||||
</Button>
|
||||
<Button className="px-3" onClick={onPing} disabled={status !== "connected"}>
|
||||
Ping
|
||||
</Button>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoRow({ label, value, error = false }: { label: string; value: string; error?: boolean }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[5.5rem_1fr] gap-2 border-t border-border pt-2">
|
||||
<dt className="font-mono text-xs uppercase tracking-[0.08em] text-muted">{label}</dt>
|
||||
<dd className={`m-0 break-words ${error ? "text-red" : "text-fg"}`}>{value}</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function statusClass(status: ConnectionStatus) {
|
||||
if (status === "connected") return "border-green text-green";
|
||||
if (status === "error") return "border-red text-red";
|
||||
return "border-cyan text-cyan";
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Panel } from "@void-nav/ui";
|
||||
import type { Timestamp } from "spacetimedb";
|
||||
import type { ServerEvent } from "../module_bindings/types";
|
||||
|
||||
export function EventFeed({ events }: { events: ServerEvent[] }) {
|
||||
return (
|
||||
<Panel className="max-h-[18rem] overflow-hidden p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="m-0 font-mono text-xs uppercase tracking-[0.1em] text-muted">Server Events</p>
|
||||
<span className="font-mono text-xs text-fg-dim">{events.length}</span>
|
||||
</div>
|
||||
<div className="mt-3 grid max-h-[14rem] gap-2 overflow-y-auto pr-1">
|
||||
{events.length > 0 ? (
|
||||
events.map((event) => (
|
||||
<article key={String(event.eventId)} className="rounded-md border border-border bg-bg-subtle p-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 font-mono text-xs uppercase tracking-[0.08em] text-muted">
|
||||
<span>{event.eventType}</span>
|
||||
<span>{formatTime(event.at)}</span>
|
||||
</div>
|
||||
<p className="m-0 mt-1 text-sm leading-snug text-fg">{event.message}</p>
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<p className="m-0 rounded-md border border-border bg-bg-subtle p-3 text-sm text-fg-dim">
|
||||
No subscribed server events yet.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function formatTime(value?: Timestamp | string) {
|
||||
if (!value) return "Pending";
|
||||
if (typeof value === "string") return value;
|
||||
return value.toDate?.().toLocaleTimeString() ?? value.toString?.() ?? "Timestamp";
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
import { Panel } from "@void-nav/ui";
|
||||
import type { PointOfInterest, Ship, ShipOperation } from "../module_bindings/types";
|
||||
|
||||
export function MiniStarMap({
|
||||
ship,
|
||||
pois,
|
||||
operation,
|
||||
onSelectPoi,
|
||||
}: {
|
||||
ship?: Ship;
|
||||
pois: PointOfInterest[];
|
||||
operation?: ShipOperation;
|
||||
onSelectPoi: (poiId: string) => void;
|
||||
}) {
|
||||
const bounds = getBounds(pois);
|
||||
const mapSize = 280;
|
||||
const padding = 24;
|
||||
|
||||
return (
|
||||
<Panel className="p-4">
|
||||
<p className="m-0 font-mono text-xs uppercase tracking-[0.1em] text-muted">System Map</p>
|
||||
<svg
|
||||
viewBox={`0 0 ${mapSize} ${mapSize}`}
|
||||
className="mt-3 w-full rounded-md border border-border bg-bg-subtle"
|
||||
style={{ aspectRatio: "1" }}
|
||||
>
|
||||
<GridOverlay size={mapSize} />
|
||||
{pois.map((poi) => {
|
||||
const pos = projectPoi(poi, bounds, mapSize, padding);
|
||||
const isCurrent = poi.poiId === ship?.currentPoiId;
|
||||
const isSelected = poi.poiId === ship?.selectedPoiId;
|
||||
return (
|
||||
<g key={poi.poiId} onClick={() => onSelectPoi(poi.poiId)} style={{ cursor: "pointer" }}>
|
||||
{isSelected ? (
|
||||
<circle cx={pos.x} cy={pos.y} r={14} fill="none" stroke="#f0a030" strokeWidth={1.5} strokeDasharray="3 2" />
|
||||
) : null}
|
||||
{isCurrent ? (
|
||||
<circle cx={pos.x} cy={pos.y} r={10} fill="none" stroke="#22d3ee" strokeWidth={1} opacity={0.6} />
|
||||
) : null}
|
||||
{poi.poiType === "station" ? (
|
||||
<StationIcon x={pos.x} y={pos.y} />
|
||||
) : (
|
||||
<BeltIcon x={pos.x} y={pos.y} />
|
||||
)}
|
||||
<text
|
||||
x={pos.x}
|
||||
y={pos.y + 20}
|
||||
textAnchor="middle"
|
||||
fill={isSelected ? "#f0a030" : "#94a3b8"}
|
||||
fontSize={9}
|
||||
fontFamily="var(--font-mono, monospace)"
|
||||
>
|
||||
{poi.name}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{ship ? (() => {
|
||||
const pos = projectXY(ship.x, ship.y, bounds, mapSize, padding);
|
||||
return (
|
||||
<g>
|
||||
<circle cx={pos.x} cy={pos.y} r={4} fill="#22d3ee" />
|
||||
<circle cx={pos.x} cy={pos.y} r={7} fill="none" stroke="#22d3ee" strokeWidth={0.8} opacity={0.5} />
|
||||
{operation && (() => {
|
||||
const target = pois.find((p) => p.poiId === operation.targetPoiId);
|
||||
if (!target) return null;
|
||||
const tpos = projectPoi(target, bounds, mapSize, padding);
|
||||
return (
|
||||
<line
|
||||
x1={pos.x} y1={pos.y}
|
||||
x2={tpos.x} y2={tpos.y}
|
||||
stroke={operation.operationType === "mining" ? "#f0a030" : "#22d3ee"}
|
||||
strokeWidth={1}
|
||||
strokeDasharray={operation.operationType === "approach" ? "4 3" : undefined}
|
||||
opacity={0.7}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</g>
|
||||
);
|
||||
})() : null}
|
||||
</svg>
|
||||
<div className="mt-2 flex items-center gap-4 text-xs text-fg-dim">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="inline-block h-2.5 w-2.5 rounded-full bg-cyan" />
|
||||
Ship
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="inline-block h-2.5 w-2.5 rounded bg-fg-dim" />
|
||||
Station
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="inline-block h-2.5 w-2.5 rotate-45 rounded-sm bg-[#8b8176]" />
|
||||
Belt
|
||||
</span>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function StationIcon({ x, y }: { x: number; y: number }) {
|
||||
return <rect x={x - 4} y={y - 4} width={8} height={8} rx={1.5} fill="#9ad8ff" opacity={0.9} />;
|
||||
}
|
||||
|
||||
function BeltIcon({ x, y }: { x: number; y: number }) {
|
||||
return <rect x={x - 4} y={y - 4} width={8} height={8} rx={1} fill="#8b8176" transform={`rotate(45 ${x} ${y})`} />;
|
||||
}
|
||||
|
||||
function GridOverlay({ size }: { size: number }) {
|
||||
const lines = [];
|
||||
const step = size / 6;
|
||||
for (let i = 1; i < 6; i++) {
|
||||
lines.push(
|
||||
<line key={`h${i}`} x1={0} y1={step * i} x2={size} y2={step * i} stroke="#1c2a3f" strokeWidth={0.5} />,
|
||||
<line key={`v${i}`} x1={step * i} y1={0} x2={step * i} y2={size} stroke="#1c2a3f" strokeWidth={0.5} />,
|
||||
);
|
||||
}
|
||||
return <>{lines}</>;
|
||||
}
|
||||
|
||||
type Bounds = { minX: number; maxX: number; minY: number; maxY: number };
|
||||
|
||||
function getBounds(pois: PointOfInterest[]): Bounds {
|
||||
if (pois.length === 0) return { minX: -50, maxX: 50, minY: -50, maxY: 50 };
|
||||
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
||||
for (const poi of pois) {
|
||||
if (poi.x < minX) minX = poi.x;
|
||||
if (poi.x > maxX) maxX = poi.x;
|
||||
if (poi.y < minY) minY = poi.y;
|
||||
if (poi.y > maxY) maxY = poi.y;
|
||||
}
|
||||
const padX = Math.max(5, (maxX - minX) * 0.15);
|
||||
const padY = Math.max(5, (maxY - minY) * 0.15);
|
||||
return { minX: minX - padX, maxX: maxX + padX, minY: minY - padY, maxY: maxY + padY };
|
||||
}
|
||||
|
||||
function projectPoi(poi: PointOfInterest, bounds: Bounds, size: number, padding: number) {
|
||||
return projectXY(poi.x, poi.y, bounds, size, padding);
|
||||
}
|
||||
|
||||
function projectXY(x: number, y: number, bounds: Bounds, size: number, padding: number) {
|
||||
const rangeX = bounds.maxX - bounds.minX || 1;
|
||||
const rangeY = bounds.maxY - bounds.minY || 1;
|
||||
const drawSize = size - padding * 2;
|
||||
return {
|
||||
x: padding + ((x - bounds.minX) / rangeX) * drawSize,
|
||||
y: padding + drawSize - ((y - bounds.minY) / rangeY) * drawSize,
|
||||
};
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Panel } from "@void-nav/ui";
|
||||
import type { CargoItem, Ship, Wallet } from "../module_bindings/types";
|
||||
|
||||
const NPC_PRICES: Record<string, { name: string; buyPrice: bigint }> = {
|
||||
"ore-veldspar": { name: "Veldspar", buyPrice: 12n },
|
||||
};
|
||||
|
||||
export function NpcMarketPanel({
|
||||
ship,
|
||||
cargo,
|
||||
wallet,
|
||||
onSellOre,
|
||||
}: {
|
||||
ship?: Ship;
|
||||
cargo: CargoItem[];
|
||||
wallet?: Wallet;
|
||||
onSellOre: (itemId: string, quantity: bigint) => void;
|
||||
}) {
|
||||
const [sellAmounts, setSellAmounts] = useState<Record<string, string>>({});
|
||||
|
||||
if (!ship || ship.dockedStationId.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const oreItems = cargo.filter((item) => item.category === "ore" && item.quantity > 0n);
|
||||
if (oreItems.length === 0) {
|
||||
return (
|
||||
<Panel className="p-4">
|
||||
<p className="m-0 font-mono text-xs uppercase tracking-[0.1em] text-muted">NPC Market</p>
|
||||
<p className="m-0 mt-2 text-sm text-fg-dim">No ore to sell. Go mine some asteroids first.</p>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel className="p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="m-0 font-mono text-xs uppercase tracking-[0.1em] text-muted">NPC Market</p>
|
||||
<span className="font-mono text-xs text-fg-dim">Buy Orders</span>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3">
|
||||
{oreItems.map((item) => {
|
||||
const pricing = NPC_PRICES[item.itemId];
|
||||
const unitPrice = pricing?.buyPrice ?? item.unitPrice;
|
||||
const rawAmount = sellAmounts[item.itemId] ?? "";
|
||||
const parsedAmount = parseBigint(rawAmount);
|
||||
const sellQty = parsedAmount > 0n ? minBigint(parsedAmount, item.quantity) : 0n;
|
||||
const payout = sellQty * unitPrice;
|
||||
|
||||
return (
|
||||
<div key={String(item.cargoItemId)} className="rounded-md border border-border bg-bg-subtle p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm font-semibold text-fg-bright">{item.itemName}</span>
|
||||
<span className="font-mono text-xs text-cyan">{item.quantity.toString()} units</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={Number(item.quantity)}
|
||||
placeholder="Qty"
|
||||
value={rawAmount}
|
||||
onChange={(e) => setSellAmounts((prev) => ({ ...prev, [item.itemId]: e.target.value }))}
|
||||
className="min-h-8 w-24 rounded border border-border bg-surface-raised px-2 py-1 text-sm text-fg outline-none focus:border-cyan"
|
||||
/>
|
||||
<Button
|
||||
className="min-h-8 px-3 text-xs"
|
||||
onClick={() => {
|
||||
setSellAmounts((prev) => ({ ...prev, [item.itemId]: item.quantity.toString() }));
|
||||
}}
|
||||
>
|
||||
Max
|
||||
</Button>
|
||||
<span className="ml-auto font-mono text-xs text-accent">
|
||||
{payout > 0n ? `+${payout.toLocaleString()} ISK` : `${unitPrice}/u`}
|
||||
</span>
|
||||
</div>
|
||||
{sellQty > 0n ? (
|
||||
<Button
|
||||
tone="primary"
|
||||
className="mt-2 w-full"
|
||||
onClick={() => {
|
||||
onSellOre(item.itemId, sellQty);
|
||||
setSellAmounts((prev) => ({ ...prev, [item.itemId]: "" }));
|
||||
}}
|
||||
>
|
||||
Sell {sellQty.toString()} {item.itemName}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function parseBigint(value: string): bigint {
|
||||
try {
|
||||
const n = BigInt(value);
|
||||
return n >= 0n ? n : 0n;
|
||||
} catch {
|
||||
return 0n;
|
||||
}
|
||||
}
|
||||
|
||||
function minBigint(left: bigint, right: bigint) {
|
||||
return left < right ? left : right;
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Panel } from "@void-nav/ui";
|
||||
import type { Ship, ShipOperation } from "../module_bindings/types";
|
||||
|
||||
export function ShipStatusPanel({
|
||||
ship,
|
||||
operation,
|
||||
}: {
|
||||
ship?: Ship;
|
||||
operation?: ShipOperation;
|
||||
}) {
|
||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => setNowMs(Date.now()), 200);
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
if (!ship) {
|
||||
return (
|
||||
<Panel className="p-4">
|
||||
<p className="m-0 font-mono text-xs uppercase tracking-[0.1em] text-muted">Ship Status</p>
|
||||
<p className="m-0 mt-2 text-sm text-fg-dim">Awaiting ship data...</p>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
const flightModeLabel = formatFlightMode(ship.flightMode);
|
||||
const isOperating = !!operation;
|
||||
|
||||
let speedLabel = "0 m/s";
|
||||
let speedPct = 0;
|
||||
if (operation?.operationType === "approach") {
|
||||
speedPct = 75;
|
||||
speedLabel = "~2,450 m/s";
|
||||
} else if (ship.flightMode === "flight") {
|
||||
speedPct = 15;
|
||||
speedLabel = "~180 m/s";
|
||||
} else if (ship.flightMode === "mining") {
|
||||
speedPct = 0;
|
||||
speedLabel = "0 m/s";
|
||||
}
|
||||
|
||||
const hullPct = 100;
|
||||
const shieldPct = isOperating ? Math.max(0, 80 - Math.random() * 5) : 100;
|
||||
const cargoPct = ship.cargoCapacity > 0n ? 0 : 0;
|
||||
|
||||
return (
|
||||
<Panel className="p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="m-0 font-mono text-xs uppercase tracking-[0.1em] text-muted">Ship Status</p>
|
||||
<span className={`rounded-md border px-2 py-1 font-mono text-xs uppercase tracking-[0.08em] ${flightModeColor(ship.flightMode)}`}>
|
||||
{flightModeLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid gap-2.5">
|
||||
<StatusBar label="Hull" value={hullPct} color="#22c55e" />
|
||||
<StatusBar label="Shield" value={Math.round(shieldPct)} color="#22d3ee" />
|
||||
<StatusBar label="Speed" value={speedPct} color="#f0a030" />
|
||||
</div>
|
||||
|
||||
<dl className="mt-3 grid gap-2 text-sm">
|
||||
<InfoRow label="Position" value={`[${ship.x.toFixed(1)}, ${ship.y.toFixed(1)}, ${ship.z.toFixed(1)}]`} />
|
||||
<InfoRow label="Velocity" value={speedLabel} />
|
||||
<InfoRow label="System" value={ship.currentSystemId} />
|
||||
{isOperating && operation ? (
|
||||
<>
|
||||
<InfoRow label="Operation" value={operation.operationType} />
|
||||
<InfoRow label="ETA" value={formatCountdown(operation, nowMs)} />
|
||||
</>
|
||||
) : null}
|
||||
</dl>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBar({ label, value, color }: { label: string; value: number; color: string }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[4.5rem_1fr] items-center gap-2">
|
||||
<span className="font-mono text-xs uppercase tracking-[0.08em] text-muted">{label}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-surface-raised">
|
||||
<div
|
||||
className="h-full rounded-full transition-[width] duration-300"
|
||||
style={{ width: `${Math.max(0, Math.min(100, value))}%`, backgroundColor: color }}
|
||||
/>
|
||||
</div>
|
||||
<span className="w-8 text-right font-mono text-xs text-fg-dim">{value}%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[5.5rem_1fr] gap-2 border-t border-border pt-2">
|
||||
<dt className="font-mono text-xs uppercase tracking-[0.08em] text-muted">{label}</dt>
|
||||
<dd className="m-0 font-mono text-xs text-fg">{value}</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatFlightMode(mode: string) {
|
||||
switch (mode) {
|
||||
case "docked": return "Docked";
|
||||
case "flight": return "In Flight";
|
||||
case "approaching": return "Approaching";
|
||||
case "mining": return "Mining";
|
||||
default: return mode || "Offline";
|
||||
}
|
||||
}
|
||||
|
||||
function flightModeColor(mode: string) {
|
||||
switch (mode) {
|
||||
case "docked": return "border-green text-green";
|
||||
case "approaching": return "border-cyan text-cyan";
|
||||
case "mining": return "border-accent text-accent";
|
||||
case "flight": return "border-fg-dim text-fg-dim";
|
||||
default: return "border-muted text-muted";
|
||||
}
|
||||
}
|
||||
|
||||
function formatCountdown(operation: ShipOperation, nowMs: number) {
|
||||
const completesAt = Number(operation.completesAtMs);
|
||||
const remaining = completesAt - nowMs;
|
||||
if (remaining <= 0) return "Completing...";
|
||||
const seconds = Math.ceil(remaining / 1000);
|
||||
return `${seconds}s`;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Button, Panel } from "@void-nav/ui";
|
||||
import type { PointOfInterest, Ship } from "../module_bindings/types";
|
||||
|
||||
export function TargetPanel({
|
||||
ship,
|
||||
pois,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
ship?: Ship;
|
||||
pois: PointOfInterest[];
|
||||
selected?: PointOfInterest;
|
||||
onSelect: (poiId: string) => void;
|
||||
}) {
|
||||
const current = pois.find((poi) => poi.poiId === ship?.currentPoiId);
|
||||
|
||||
return (
|
||||
<Panel className="p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="m-0 font-mono text-xs uppercase tracking-[0.1em] text-muted">Selected Target</p>
|
||||
<h2 className="m-0 mt-1 text-lg leading-tight text-fg-bright">{selected?.name ?? "No target selected"}</h2>
|
||||
</div>
|
||||
<span className="rounded-md border border-border bg-bg-subtle px-2 py-1 font-mono text-xs uppercase tracking-[0.08em] text-fg-dim">
|
||||
{ship?.flightMode ?? "offline"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<dl className="mt-3 grid gap-2 text-sm">
|
||||
<InfoRow label="Current" value={current?.name ?? "Unknown"} />
|
||||
<InfoRow label="Target type" value={selected?.poiType.replace("_", " ") ?? "Select in scene or list"} />
|
||||
</dl>
|
||||
|
||||
<div className="mt-4 grid gap-2">
|
||||
{pois.map((poi) => (
|
||||
<Button
|
||||
key={poi.poiId}
|
||||
tone={selected?.poiId === poi.poiId ? "primary" : "secondary"}
|
||||
className="min-h-9 w-full justify-between px-3 text-left"
|
||||
onClick={() => onSelect(poi.poiId)}
|
||||
disabled={!ship}
|
||||
>
|
||||
<span className="flex-1 text-left">{poi.name}</span>
|
||||
<span className="shrink-0 pl-3 font-mono text-xs opacity-75">{poi.poiType === "asteroid_belt" ? "Belt" : "Station"}</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[5.5rem_1fr] gap-2 border-t border-border pt-2">
|
||||
<dt className="font-mono text-xs uppercase tracking-[0.08em] text-muted">{label}</dt>
|
||||
<dd className="m-0 text-fg">{value}</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Panel } from "@void-nav/ui";
|
||||
import type { Wallet } from "../module_bindings/types";
|
||||
|
||||
export function WalletPanel({ wallet }: { wallet?: Wallet }) {
|
||||
return (
|
||||
<Panel className="p-4">
|
||||
<p className="m-0 font-mono text-xs uppercase tracking-[0.1em] text-muted">Wallet</p>
|
||||
<div className="mt-1 text-2xl font-semibold text-fg-bright">{formatIsk(wallet?.isk ?? 0n)}</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function formatIsk(value: bigint) {
|
||||
return `${value.toLocaleString()} ISK`;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
export type ShipActions = {
|
||||
onUndock: () => void;
|
||||
onStartApproach: () => void;
|
||||
onDock: () => void;
|
||||
onStartMining: () => void;
|
||||
onSelectTarget?: (index: number) => void;
|
||||
};
|
||||
|
||||
export function useKeyboardShortcuts(
|
||||
ship: {
|
||||
dockedStationId: string;
|
||||
flightMode: string;
|
||||
selectedPoiId: string;
|
||||
currentPoiId: string;
|
||||
} | undefined,
|
||||
pois: Array<{ poiId: string; poiType: string }>,
|
||||
operation: { operationType: string } | undefined,
|
||||
actions: ShipActions,
|
||||
) {
|
||||
useEffect(() => {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) return;
|
||||
|
||||
const key = event.key.toLowerCase();
|
||||
|
||||
if (key === "u" && !operation) {
|
||||
if (ship && ship.dockedStationId.length > 0) {
|
||||
event.preventDefault();
|
||||
actions.onUndock();
|
||||
}
|
||||
}
|
||||
|
||||
if (key === "d" && !operation) {
|
||||
if (ship && ship.dockedStationId.length === 0 && ship.flightMode === "flight") {
|
||||
event.preventDefault();
|
||||
actions.onDock();
|
||||
}
|
||||
}
|
||||
|
||||
if (key === "a" && !operation) {
|
||||
if (ship && ship.dockedStationId.length === 0 && ship.selectedPoiId.length > 0 && ship.selectedPoiId !== ship.currentPoiId) {
|
||||
event.preventDefault();
|
||||
actions.onStartApproach();
|
||||
}
|
||||
}
|
||||
|
||||
if (key === "m" && !operation) {
|
||||
if (ship && ship.dockedStationId.length === 0 && ship.flightMode === "flight") {
|
||||
event.preventDefault();
|
||||
actions.onStartMining();
|
||||
}
|
||||
}
|
||||
|
||||
if (key >= "1" && key <= "9") {
|
||||
const index = parseInt(key) - 1;
|
||||
if (index < pois.length && actions.onSelectTarget) {
|
||||
event.preventDefault();
|
||||
actions.onSelectTarget(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [ship, pois, operation, actions]);
|
||||
}
|
||||
Reference in New Issue
Block a user