Restructure into pnpm monorepo with game shell, docs, and SpacetimeDB backend
- Restructure flat static prototype into pnpm workspace monorepo - apps/game: playable shell with R3F 3D scene, HUD, SpacetimeDB connection - apps/docs: design docs and prototypes - apps/site: landing page - packages/ui: shared Button and Panel primitives - services/spacetimedb: backend module (9 tables, 11 reducers) - Archive legacy static files to archive/legacy-static/ - Game loop: connect, undock, target, approach, dock, mine, sell - Add pnpm-workspace.yaml, tsconfig.base.json, spacetime.json
This commit is contained in:
111
apps/game/src/GameShell.tsx
Normal file
111
apps/game/src/GameShell.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
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 { TargetPanel } from "./ui/TargetPanel";
|
||||
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;
|
||||
|
||||
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>
|
||||
<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} />
|
||||
<CargoPanel cargo={session.cargo} ship={session.ship} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
10
apps/game/src/main.tsx
Normal file
10
apps/game/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
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>,
|
||||
);
|
||||
22
apps/game/src/module_bindings/cargo_item_table.ts
Normal file
22
apps/game/src/module_bindings/cargo_item_table.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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"),
|
||||
});
|
||||
13
apps/game/src/module_bindings/complete_approach_reducer.ts
Normal file
13
apps/game/src/module_bindings/complete_approach_reducer.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// 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 {};
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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 {};
|
||||
15
apps/game/src/module_bindings/connect_player_reducer.ts
Normal file
15
apps/game/src/module_bindings/connect_player_reducer.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// 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(),
|
||||
};
|
||||
13
apps/game/src/module_bindings/dock_reducer.ts
Normal file
13
apps/game/src/module_bindings/dock_reducer.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// 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 {};
|
||||
258
apps/game/src/module_bindings/index.ts
Normal file
258
apps/game/src/module_bindings/index.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
// 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);
|
||||
};
|
||||
}
|
||||
|
||||
13
apps/game/src/module_bindings/ping_reducer.ts
Normal file
13
apps/game/src/module_bindings/ping_reducer.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// 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 {};
|
||||
20
apps/game/src/module_bindings/player_table.ts
Normal file
20
apps/game/src/module_bindings/player_table.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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"),
|
||||
});
|
||||
22
apps/game/src/module_bindings/point_of_interest_table.ts
Normal file
22
apps/game/src/module_bindings/point_of_interest_table.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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"),
|
||||
});
|
||||
15
apps/game/src/module_bindings/rename_player_reducer.ts
Normal file
15
apps/game/src/module_bindings/rename_player_reducer.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// 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(),
|
||||
};
|
||||
13
apps/game/src/module_bindings/seed_world_reducer.ts
Normal file
13
apps/game/src/module_bindings/seed_world_reducer.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// 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 {};
|
||||
15
apps/game/src/module_bindings/select_target_reducer.ts
Normal file
15
apps/game/src/module_bindings/select_target_reducer.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// 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(),
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
// 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(),
|
||||
};
|
||||
19
apps/game/src/module_bindings/server_event_table.ts
Normal file
19
apps/game/src/module_bindings/server_event_table.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// 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(),
|
||||
});
|
||||
20
apps/game/src/module_bindings/ship_operation_table.ts
Normal file
20
apps/game/src/module_bindings/ship_operation_table.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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"),
|
||||
});
|
||||
27
apps/game/src/module_bindings/ship_table.ts
Normal file
27
apps/game/src/module_bindings/ship_table.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// 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"),
|
||||
});
|
||||
13
apps/game/src/module_bindings/start_approach_reducer.ts
Normal file
13
apps/game/src/module_bindings/start_approach_reducer.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// 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 {};
|
||||
13
apps/game/src/module_bindings/start_mining_reducer.ts
Normal file
13
apps/game/src/module_bindings/start_mining_reducer.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// 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 {};
|
||||
17
apps/game/src/module_bindings/station_table.ts
Normal file
17
apps/game/src/module_bindings/station_table.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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(),
|
||||
});
|
||||
17
apps/game/src/module_bindings/system_table.ts
Normal file
17
apps/game/src/module_bindings/system_table.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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"),
|
||||
});
|
||||
103
apps/game/src/module_bindings/types.ts
Normal file
103
apps/game/src/module_bindings/types.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
// 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>;
|
||||
|
||||
10
apps/game/src/module_bindings/types/procedures.ts
Normal file
10
apps/game/src/module_bindings/types/procedures.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// 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
|
||||
|
||||
|
||||
34
apps/game/src/module_bindings/types/reducers.ts
Normal file
34
apps/game/src/module_bindings/types/reducers.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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>;
|
||||
|
||||
13
apps/game/src/module_bindings/undock_reducer.ts
Normal file
13
apps/game/src/module_bindings/undock_reducer.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// 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 {};
|
||||
17
apps/game/src/module_bindings/wallet_table.ts
Normal file
17
apps/game/src/module_bindings/wallet_table.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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"),
|
||||
});
|
||||
45
apps/game/src/scene/AsteroidMesh.tsx
Normal file
45
apps/game/src/scene/AsteroidMesh.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
175
apps/game/src/scene/GameSpaceScene.tsx
Normal file
175
apps/game/src/scene/GameSpaceScene.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
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;
|
||||
}
|
||||
34
apps/game/src/scene/ShipMesh.tsx
Normal file
34
apps/game/src/scene/ShipMesh.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
10
apps/game/src/scene/SpaceCanvas.tsx
Normal file
10
apps/game/src/scene/SpaceCanvas.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
27
apps/game/src/scene/SpaceEnvironment.tsx
Normal file
27
apps/game/src/scene/SpaceEnvironment.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
45
apps/game/src/scene/StationMesh.tsx
Normal file
45
apps/game/src/scene/StationMesh.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
135
apps/game/src/spacetime/client.ts
Normal file
135
apps/game/src/spacetime/client.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
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.
|
||||
}
|
||||
}
|
||||
156
apps/game/src/spacetime/useGameSession.ts
Normal file
156
apps/game/src/spacetime/useGameSession.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
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);
|
||||
}
|
||||
58
apps/game/src/spacetime/usePlayerSession.ts
Normal file
58
apps/game/src/spacetime/usePlayerSession.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useMemo } from "react";
|
||||
import type { DbConnection } from "../module_bindings";
|
||||
import type { Player, ServerEvent, Ship, Station, System } from "../module_bindings/types";
|
||||
import { invokeReducer } from "./client";
|
||||
|
||||
type ReducerReporter = (message: string, isError?: boolean) => void;
|
||||
|
||||
export function usePlayerSession(connection: DbConnection | null, revision: number, onReducerStatus: ReducerReporter) {
|
||||
const rows = useMemo(() => readRows(connection), [connection, revision]);
|
||||
|
||||
return {
|
||||
...rows,
|
||||
renamePlayer(displayName: string) {
|
||||
if (!connection) {
|
||||
onReducerStatus("renamePlayer unavailable until connected", true);
|
||||
return;
|
||||
}
|
||||
invokeReducer({ onReducerStatus }, "renamePlayer", () => connection.reducers.renamePlayer({ displayName }));
|
||||
},
|
||||
ping() {
|
||||
if (!connection) {
|
||||
onReducerStatus("ping unavailable until connected", true);
|
||||
return;
|
||||
}
|
||||
invokeReducer({ onReducerStatus }, "ping", () => connection.reducers.ping({}));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function readRows(connection: DbConnection | null) {
|
||||
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 events = readTable<ServerEvent>(connection?.db.server_event).sort((a, b) => compareBigintsDesc(a.eventId, b.eventId));
|
||||
|
||||
return {
|
||||
player: players[0],
|
||||
ship: ships[0],
|
||||
system: systems.find((row) => row.systemId === "solace") ?? systems[0],
|
||||
station: stations.find((row) => row.stationId === "solace-prime") ?? stations[0],
|
||||
events: events.slice(0, 6),
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
60
apps/game/src/spacetime/useSpacetimeConnection.ts
Normal file
60
apps/game/src/spacetime/useSpacetimeConnection.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useEffect, useMemo, 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,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const conn = createSpacetimeConnection({
|
||||
...config,
|
||||
displayName,
|
||||
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, displayName]);
|
||||
|
||||
return {
|
||||
connection,
|
||||
status,
|
||||
message,
|
||||
identity,
|
||||
revision,
|
||||
reducerStatus,
|
||||
uri: config.uri,
|
||||
database: config.database,
|
||||
};
|
||||
}
|
||||
8
apps/game/src/styles/tailwind.css
Normal file
8
apps/game/src/styles/tailwind.css
Normal file
@@ -0,0 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
@import "@void-nav/ui/styles.css";
|
||||
|
||||
@source "../../../packages/ui/src";
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
35
apps/game/src/ui/CargoPanel.tsx
Normal file
35
apps/game/src/ui/CargoPanel.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
|
||||
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()}
|
||||
</span>
|
||||
</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
|
||||
</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>
|
||||
);
|
||||
}
|
||||
77
apps/game/src/ui/CommandRail.tsx
Normal file
77
apps/game/src/ui/CommandRail.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
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);
|
||||
|
||||
let content = <span className="text-sm text-fg-dim">Connect to SpacetimeDB to initialize the starter ship.</span>;
|
||||
|
||||
if (ship) {
|
||||
if (operation?.operationType === "approach") {
|
||||
content = <Button disabled>Approaching...</Button>;
|
||||
} else if (operation?.operationType === "mining") {
|
||||
content = <Button disabled>Mining...</Button>;
|
||||
} 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 cargoUsed(cargo: CargoItem[]) {
|
||||
return cargo.reduce((total, item) => total + item.quantity, 0n);
|
||||
}
|
||||
78
apps/game/src/ui/ConnectionPanel.tsx
Normal file
78
apps/game/src/ui/ConnectionPanel.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
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";
|
||||
}
|
||||
37
apps/game/src/ui/EventFeed.tsx
Normal file
37
apps/game/src/ui/EventFeed.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
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";
|
||||
}
|
||||
59
apps/game/src/ui/TargetPanel.tsx
Normal file
59
apps/game/src/ui/TargetPanel.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
15
apps/game/src/ui/WalletPanel.tsx
Normal file
15
apps/game/src/ui/WalletPanel.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
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`;
|
||||
}
|
||||
Reference in New Issue
Block a user