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:
12
services/spacetimedb/package.json
Normal file
12
services/spacetimedb/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@void-nav/spacetimedb",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"spacetimedb": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
595
services/spacetimedb/src/index.ts
Normal file
595
services/spacetimedb/src/index.ts
Normal file
@@ -0,0 +1,595 @@
|
||||
import { schema, table, t } from "spacetimedb/server";
|
||||
|
||||
const STARTER_SYSTEM_ID = "solace";
|
||||
const STARTER_STATION_ID = "solace-prime";
|
||||
const STARTER_STATION_POI_ID = "poi-solace-prime";
|
||||
const STARTER_BELT_POI_ID = "poi-solace-belt-alpha";
|
||||
const APPROACH_DURATION_MS = 5000n;
|
||||
const MINING_DURATION_MS = 6000n;
|
||||
const STARTER_WALLET_ISK = 25000n;
|
||||
const STARTER_CARGO_CAPACITY = 2500n;
|
||||
const MINING_YIELD_QUANTITY = 1000n;
|
||||
const VELDSPAR_ITEM_ID = "ore-veldspar";
|
||||
const VELDSPAR_ITEM_NAME = "Veldspar";
|
||||
const VELDSPAR_UNIT_PRICE = 12n;
|
||||
|
||||
const player = table(
|
||||
{ name: "player", public: true },
|
||||
{
|
||||
identity: t.identity().primaryKey(),
|
||||
display_name: t.string(),
|
||||
created_at: t.timestamp(),
|
||||
updated_at: t.timestamp(),
|
||||
last_connected_at: t.timestamp(),
|
||||
is_connected: t.bool(),
|
||||
},
|
||||
);
|
||||
|
||||
const ship = table(
|
||||
{ name: "ship", public: true },
|
||||
{
|
||||
ship_id: t.u64().primaryKey().autoInc(),
|
||||
owner_identity: t.identity().index("btree"),
|
||||
ship_name: t.string(),
|
||||
hull_type: t.string(),
|
||||
current_system_id: t.string(),
|
||||
docked_station_id: t.string(),
|
||||
current_poi_id: t.string(),
|
||||
selected_poi_id: t.string(),
|
||||
flight_mode: t.string(),
|
||||
x: t.f32(),
|
||||
y: t.f32(),
|
||||
z: t.f32(),
|
||||
cargo_capacity: t.u64(),
|
||||
},
|
||||
);
|
||||
|
||||
const system = table(
|
||||
{ name: "system", public: true },
|
||||
{
|
||||
system_id: t.string().primaryKey(),
|
||||
name: t.string(),
|
||||
security_level: t.f32(),
|
||||
},
|
||||
);
|
||||
|
||||
const station = table(
|
||||
{ name: "station", public: true },
|
||||
{
|
||||
station_id: t.string().primaryKey(),
|
||||
system_id: t.string().index("btree"),
|
||||
name: t.string(),
|
||||
},
|
||||
);
|
||||
|
||||
const point_of_interest = table(
|
||||
{ name: "point_of_interest", public: true },
|
||||
{
|
||||
poi_id: t.string().primaryKey(),
|
||||
system_id: t.string().index("btree"),
|
||||
name: t.string(),
|
||||
poi_type: t.string(),
|
||||
x: t.f32(),
|
||||
y: t.f32(),
|
||||
z: t.f32(),
|
||||
station_id: t.string(),
|
||||
},
|
||||
);
|
||||
|
||||
const cargo_item = table(
|
||||
{ name: "cargo_item", public: true },
|
||||
{
|
||||
cargo_item_id: t.u64().primaryKey().autoInc(),
|
||||
owner_identity: t.identity().index("btree"),
|
||||
ship_id: t.u64().index("btree"),
|
||||
item_id: t.string(),
|
||||
item_name: t.string(),
|
||||
category: t.string(),
|
||||
quantity: t.u64(),
|
||||
unit_price: t.u64(),
|
||||
},
|
||||
);
|
||||
|
||||
const wallet = table(
|
||||
{ name: "wallet", public: true },
|
||||
{
|
||||
owner_identity: t.identity().primaryKey(),
|
||||
isk: t.u64(),
|
||||
updated_at: t.timestamp(),
|
||||
},
|
||||
);
|
||||
|
||||
const ship_operation = table(
|
||||
{ name: "ship_operation", public: true },
|
||||
{
|
||||
ship_id: t.u64().primaryKey(),
|
||||
operation_type: t.string(),
|
||||
target_poi_id: t.string(),
|
||||
started_at: t.timestamp(),
|
||||
duration_ms: t.u64(),
|
||||
completes_at_ms: t.u64(),
|
||||
},
|
||||
);
|
||||
|
||||
const server_event = table(
|
||||
{ name: "server_event", public: true },
|
||||
{
|
||||
event_id: t.u64().primaryKey().autoInc(),
|
||||
at: t.timestamp(),
|
||||
actor_identity: t.identity().index("btree"),
|
||||
event_type: t.string(),
|
||||
message: t.string(),
|
||||
},
|
||||
);
|
||||
|
||||
const spacetimedb = schema({
|
||||
player,
|
||||
ship,
|
||||
system,
|
||||
station,
|
||||
point_of_interest,
|
||||
cargo_item,
|
||||
wallet,
|
||||
ship_operation,
|
||||
server_event,
|
||||
});
|
||||
export default spacetimedb;
|
||||
|
||||
export const seedWorld = spacetimedb.reducer({}, (ctx) => {
|
||||
seedStarterWorld(ctx);
|
||||
writeEvent(ctx, "world_seeded", "Starter system and station are available.");
|
||||
});
|
||||
|
||||
export const connectPlayer = spacetimedb.reducer({ displayName: t.string() }, (ctx, { displayName }) => {
|
||||
seedStarterWorld(ctx);
|
||||
|
||||
const trimmedName = normalizeDisplayName(displayName);
|
||||
const existingPlayer = ctx.db.player.identity.find(ctx.sender);
|
||||
const timestamp = ctx.timestamp;
|
||||
|
||||
if (existingPlayer) {
|
||||
ctx.db.player.identity.update({
|
||||
...existingPlayer,
|
||||
display_name: trimmedName,
|
||||
updated_at: timestamp,
|
||||
last_connected_at: timestamp,
|
||||
is_connected: true,
|
||||
});
|
||||
} else {
|
||||
ctx.db.player.insert({
|
||||
identity: ctx.sender,
|
||||
display_name: trimmedName,
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
last_connected_at: timestamp,
|
||||
is_connected: true,
|
||||
});
|
||||
}
|
||||
|
||||
const existingShip = findStarterShip(ctx, ctx.sender);
|
||||
if (!existingShip) {
|
||||
ctx.db.ship.insert({
|
||||
ship_id: 0n,
|
||||
owner_identity: ctx.sender,
|
||||
ship_name: `${trimmedName}'s Ibis`,
|
||||
hull_type: "Starter Frigate",
|
||||
current_system_id: STARTER_SYSTEM_ID,
|
||||
docked_station_id: STARTER_STATION_ID,
|
||||
current_poi_id: STARTER_STATION_POI_ID,
|
||||
selected_poi_id: "",
|
||||
flight_mode: "docked",
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
cargo_capacity: STARTER_CARGO_CAPACITY,
|
||||
});
|
||||
}
|
||||
|
||||
if (!ctx.db.wallet.owner_identity.find(ctx.sender)) {
|
||||
ctx.db.wallet.insert({
|
||||
owner_identity: ctx.sender,
|
||||
isk: STARTER_WALLET_ISK,
|
||||
updated_at: timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
writeEvent(ctx, "player_connected", `${trimmedName} connected to VOID::NAV.`);
|
||||
});
|
||||
|
||||
export const renamePlayer = spacetimedb.reducer({ displayName: t.string() }, (ctx, { displayName }) => {
|
||||
const existingPlayer = ctx.db.player.identity.find(ctx.sender);
|
||||
const trimmedName = normalizeDisplayName(displayName);
|
||||
|
||||
if (!existingPlayer) {
|
||||
throw new Error("Player must connect before renaming.");
|
||||
}
|
||||
|
||||
ctx.db.player.identity.update({
|
||||
...existingPlayer,
|
||||
display_name: trimmedName,
|
||||
updated_at: ctx.timestamp,
|
||||
});
|
||||
|
||||
writeEvent(ctx, "player_renamed", `Pilot renamed to ${trimmedName}.`);
|
||||
});
|
||||
|
||||
export const ping = spacetimedb.reducer({}, (ctx) => {
|
||||
writeEvent(ctx, "ping", "Client ping reached the SpacetimeDB module.");
|
||||
});
|
||||
|
||||
export const undock = spacetimedb.reducer({}, (ctx) => {
|
||||
const ship = requirePlayerShip(ctx);
|
||||
|
||||
if (ship.docked_station_id.length === 0) {
|
||||
throw new Error("Ship is already undocked.");
|
||||
}
|
||||
|
||||
ctx.db.ship.ship_id.update({
|
||||
...ship,
|
||||
docked_station_id: "",
|
||||
flight_mode: "flight",
|
||||
});
|
||||
|
||||
writeEvent(ctx, "ship_undocked", `${ship.ship_name} undocked from ${ship.docked_station_id}.`);
|
||||
});
|
||||
|
||||
export const selectTarget = spacetimedb.reducer({ poiId: t.string() }, (ctx, { poiId }) => {
|
||||
const ship = requirePlayerShip(ctx);
|
||||
const poi = requirePoiInCurrentSystem(ctx, ship, poiId);
|
||||
|
||||
ctx.db.ship.ship_id.update({
|
||||
...ship,
|
||||
selected_poi_id: poi.poi_id,
|
||||
});
|
||||
|
||||
writeEvent(ctx, "target_selected", `${ship.ship_name} selected ${poi.name}.`);
|
||||
});
|
||||
|
||||
export const startApproach = spacetimedb.reducer({}, (ctx) => {
|
||||
const ship = requirePlayerShip(ctx);
|
||||
|
||||
if (ship.docked_station_id.length > 0) {
|
||||
throw new Error("Ship must be undocked before approaching a target.");
|
||||
}
|
||||
|
||||
if (ship.selected_poi_id.length === 0) {
|
||||
throw new Error("Select a target before starting approach.");
|
||||
}
|
||||
|
||||
const target = requirePoiInCurrentSystem(ctx, ship, ship.selected_poi_id);
|
||||
if (ctx.db.ship_operation.ship_id.find(ship.ship_id)) {
|
||||
throw new Error("Ship already has an active operation.");
|
||||
}
|
||||
|
||||
const nowMs = ctx.timestamp.toMillis();
|
||||
ctx.db.ship_operation.insert({
|
||||
ship_id: ship.ship_id,
|
||||
operation_type: "approach",
|
||||
target_poi_id: target.poi_id,
|
||||
started_at: ctx.timestamp,
|
||||
duration_ms: APPROACH_DURATION_MS,
|
||||
completes_at_ms: nowMs + APPROACH_DURATION_MS,
|
||||
});
|
||||
|
||||
ctx.db.ship.ship_id.update({
|
||||
...ship,
|
||||
flight_mode: "approaching",
|
||||
});
|
||||
|
||||
writeEvent(ctx, "approach_started", `${ship.ship_name} started approach to ${target.name}.`);
|
||||
});
|
||||
|
||||
export const completeApproach = spacetimedb.reducer({}, (ctx) => {
|
||||
const ship = requirePlayerShip(ctx);
|
||||
const operation = requireOperation(ctx, ship.ship_id, "approach");
|
||||
requireElapsed(ctx, operation.completes_at_ms, "Approach is still in progress.");
|
||||
const target = requirePoiInCurrentSystem(ctx, ship, operation.target_poi_id);
|
||||
|
||||
ctx.db.ship_operation.ship_id.delete(ship.ship_id);
|
||||
ctx.db.ship.ship_id.update({
|
||||
...ship,
|
||||
current_poi_id: target.poi_id,
|
||||
flight_mode: "flight",
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
z: target.z,
|
||||
});
|
||||
|
||||
writeEvent(ctx, "approach_completed", `${ship.ship_name} arrived at ${target.name}.`);
|
||||
});
|
||||
|
||||
export const dock = spacetimedb.reducer({}, (ctx) => {
|
||||
const ship = requirePlayerShip(ctx);
|
||||
|
||||
if (ship.docked_station_id.length > 0) {
|
||||
throw new Error("Ship is already docked.");
|
||||
}
|
||||
|
||||
if (ctx.db.ship_operation.ship_id.find(ship.ship_id)) {
|
||||
throw new Error("Cannot dock while an operation is active.");
|
||||
}
|
||||
|
||||
const currentPoi = requireCurrentPoi(ctx, ship);
|
||||
if (currentPoi.station_id.length === 0) {
|
||||
throw new Error("Current point of interest is not dockable.");
|
||||
}
|
||||
|
||||
ctx.db.ship.ship_id.update({
|
||||
...ship,
|
||||
docked_station_id: currentPoi.station_id,
|
||||
flight_mode: "docked",
|
||||
});
|
||||
|
||||
writeEvent(ctx, "ship_docked", `${ship.ship_name} docked at ${currentPoi.name}.`);
|
||||
});
|
||||
|
||||
export const startMining = spacetimedb.reducer({}, (ctx) => {
|
||||
const ship = requirePlayerShip(ctx);
|
||||
|
||||
if (ship.docked_station_id.length > 0) {
|
||||
throw new Error("Ship must be undocked before mining.");
|
||||
}
|
||||
|
||||
if (ctx.db.ship_operation.ship_id.find(ship.ship_id)) {
|
||||
throw new Error("Ship already has an active operation.");
|
||||
}
|
||||
|
||||
const currentPoi = requireCurrentPoi(ctx, ship);
|
||||
if (currentPoi.poi_type !== "asteroid_belt") {
|
||||
throw new Error("Mining requires being at an asteroid belt.");
|
||||
}
|
||||
|
||||
const freeCapacity = getFreeCargoCapacity(ctx, ship);
|
||||
if (freeCapacity <= 0n) {
|
||||
throw new Error("Cargo hold is full.");
|
||||
}
|
||||
|
||||
const nowMs = ctx.timestamp.toMillis();
|
||||
ctx.db.ship_operation.insert({
|
||||
ship_id: ship.ship_id,
|
||||
operation_type: "mining",
|
||||
target_poi_id: currentPoi.poi_id,
|
||||
started_at: ctx.timestamp,
|
||||
duration_ms: MINING_DURATION_MS,
|
||||
completes_at_ms: nowMs + MINING_DURATION_MS,
|
||||
});
|
||||
|
||||
ctx.db.ship.ship_id.update({
|
||||
...ship,
|
||||
flight_mode: "mining",
|
||||
});
|
||||
|
||||
writeEvent(ctx, "mining_started", `${ship.ship_name} started mining ${currentPoi.name}.`);
|
||||
});
|
||||
|
||||
export const completeMiningCycle = spacetimedb.reducer({}, (ctx) => {
|
||||
const ship = requirePlayerShip(ctx);
|
||||
const operation = requireOperation(ctx, ship.ship_id, "mining");
|
||||
requireElapsed(ctx, operation.completes_at_ms, "Mining cycle is still in progress.");
|
||||
|
||||
const currentPoi = requireCurrentPoi(ctx, ship);
|
||||
if (currentPoi.poi_type !== "asteroid_belt") {
|
||||
throw new Error("Mining can only complete at an asteroid belt.");
|
||||
}
|
||||
|
||||
const freeCapacity = getFreeCargoCapacity(ctx, ship);
|
||||
if (freeCapacity <= 0n) {
|
||||
throw new Error("Cargo hold is full.");
|
||||
}
|
||||
|
||||
const quantity = minBigint(MINING_YIELD_QUANTITY, freeCapacity);
|
||||
const existingCargo = findCargoItem(ctx, ship.ship_id, VELDSPAR_ITEM_ID);
|
||||
|
||||
if (existingCargo) {
|
||||
ctx.db.cargo_item.cargo_item_id.update({
|
||||
...existingCargo,
|
||||
quantity: existingCargo.quantity + quantity,
|
||||
unit_price: VELDSPAR_UNIT_PRICE,
|
||||
});
|
||||
} else {
|
||||
ctx.db.cargo_item.insert({
|
||||
cargo_item_id: 0n,
|
||||
owner_identity: ctx.sender,
|
||||
ship_id: ship.ship_id,
|
||||
item_id: VELDSPAR_ITEM_ID,
|
||||
item_name: VELDSPAR_ITEM_NAME,
|
||||
category: "ore",
|
||||
quantity,
|
||||
unit_price: VELDSPAR_UNIT_PRICE,
|
||||
});
|
||||
}
|
||||
|
||||
ctx.db.ship_operation.ship_id.delete(ship.ship_id);
|
||||
ctx.db.ship.ship_id.update({
|
||||
...ship,
|
||||
flight_mode: "flight",
|
||||
});
|
||||
|
||||
writeEvent(ctx, "mining_completed", `${ship.ship_name} mined ${quantity.toString()} ${VELDSPAR_ITEM_NAME}.`);
|
||||
});
|
||||
|
||||
export const sellOreToNpcMarket = spacetimedb.reducer(
|
||||
{ itemId: t.string(), quantity: t.u64() },
|
||||
(ctx, { itemId, quantity }) => {
|
||||
const ship = requirePlayerShip(ctx);
|
||||
|
||||
if (ship.docked_station_id.length === 0) {
|
||||
throw new Error("Ship must be docked before selling ore.");
|
||||
}
|
||||
|
||||
if (quantity <= 0n) {
|
||||
throw new Error("Sell quantity must be greater than zero.");
|
||||
}
|
||||
|
||||
const cargo = findCargoItem(ctx, ship.ship_id, itemId);
|
||||
if (!cargo || cargo.category !== "ore") {
|
||||
throw new Error("Ore cargo item not found.");
|
||||
}
|
||||
|
||||
if (cargo.quantity < quantity) {
|
||||
throw new Error("Cannot sell more ore than the ship is carrying.");
|
||||
}
|
||||
|
||||
const playerWallet = ctx.db.wallet.owner_identity.find(ctx.sender);
|
||||
if (!playerWallet) {
|
||||
throw new Error("Player wallet was not found.");
|
||||
}
|
||||
|
||||
if (cargo.quantity === quantity) {
|
||||
ctx.db.cargo_item.cargo_item_id.delete(cargo.cargo_item_id);
|
||||
} else {
|
||||
ctx.db.cargo_item.cargo_item_id.update({
|
||||
...cargo,
|
||||
quantity: cargo.quantity - quantity,
|
||||
});
|
||||
}
|
||||
|
||||
const payout = quantity * cargo.unit_price;
|
||||
ctx.db.wallet.owner_identity.update({
|
||||
...playerWallet,
|
||||
isk: playerWallet.isk + payout,
|
||||
updated_at: ctx.timestamp,
|
||||
});
|
||||
|
||||
writeEvent(ctx, "ore_sold", `Sold ${quantity.toString()} ${cargo.item_name} for ${payout.toString()} ISK.`);
|
||||
},
|
||||
);
|
||||
|
||||
function seedStarterWorld(ctx: ReducerContextLike) {
|
||||
if (!ctx.db.system.system_id.find(STARTER_SYSTEM_ID)) {
|
||||
ctx.db.system.insert({
|
||||
system_id: STARTER_SYSTEM_ID,
|
||||
name: "Solace",
|
||||
security_level: 0.9,
|
||||
});
|
||||
}
|
||||
|
||||
if (!ctx.db.station.station_id.find(STARTER_STATION_ID)) {
|
||||
ctx.db.station.insert({
|
||||
station_id: STARTER_STATION_ID,
|
||||
system_id: STARTER_SYSTEM_ID,
|
||||
name: "Solace Prime Orbital",
|
||||
});
|
||||
}
|
||||
|
||||
if (!ctx.db.point_of_interest.poi_id.find(STARTER_STATION_POI_ID)) {
|
||||
ctx.db.point_of_interest.insert({
|
||||
poi_id: STARTER_STATION_POI_ID,
|
||||
system_id: STARTER_SYSTEM_ID,
|
||||
name: "Solace Prime Orbital",
|
||||
poi_type: "station",
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
station_id: STARTER_STATION_ID,
|
||||
});
|
||||
}
|
||||
|
||||
if (!ctx.db.point_of_interest.poi_id.find(STARTER_BELT_POI_ID)) {
|
||||
ctx.db.point_of_interest.insert({
|
||||
poi_id: STARTER_BELT_POI_ID,
|
||||
system_id: STARTER_SYSTEM_ID,
|
||||
name: "Solace Belt Alpha",
|
||||
poi_type: "asteroid_belt",
|
||||
x: 34,
|
||||
y: -2,
|
||||
z: -18,
|
||||
station_id: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function findStarterShip(ctx: ReducerContextLike, ownerIdentity: unknown) {
|
||||
return Array.from(ctx.db.ship.iter()).find((row) => identitiesEqual(row.owner_identity, ownerIdentity));
|
||||
}
|
||||
|
||||
function requirePlayerShip(ctx: ReducerContextLike) {
|
||||
const playerShip = findStarterShip(ctx, ctx.sender);
|
||||
if (!playerShip) {
|
||||
throw new Error("Player must connect before controlling a ship.");
|
||||
}
|
||||
return playerShip;
|
||||
}
|
||||
|
||||
function requirePoiInCurrentSystem(ctx: ReducerContextLike, ship: ReturnType<typeof requirePlayerShip>, poiId: string) {
|
||||
const poi = ctx.db.point_of_interest.poi_id.find(poiId);
|
||||
if (!poi) {
|
||||
throw new Error(`Point of interest not found: ${poiId}.`);
|
||||
}
|
||||
|
||||
if (poi.system_id !== ship.current_system_id) {
|
||||
throw new Error("Selected point of interest is not in the ship's current system.");
|
||||
}
|
||||
|
||||
return poi;
|
||||
}
|
||||
|
||||
function requireCurrentPoi(ctx: ReducerContextLike, ship: ReturnType<typeof requirePlayerShip>) {
|
||||
if (ship.current_poi_id.length === 0) {
|
||||
throw new Error("Ship is not at a point of interest.");
|
||||
}
|
||||
|
||||
return requirePoiInCurrentSystem(ctx, ship, ship.current_poi_id);
|
||||
}
|
||||
|
||||
function requireOperation(ctx: ReducerContextLike, shipId: bigint, operationType: string) {
|
||||
const operation = ctx.db.ship_operation.ship_id.find(shipId);
|
||||
if (!operation) {
|
||||
throw new Error(`No active ${operationType} operation found.`);
|
||||
}
|
||||
|
||||
if (operation.operation_type !== operationType) {
|
||||
throw new Error(`Active operation is ${operation.operation_type}, not ${operationType}.`);
|
||||
}
|
||||
|
||||
return operation;
|
||||
}
|
||||
|
||||
function requireElapsed(ctx: ReducerContextLike, completesAtMs: bigint, message: string) {
|
||||
if (ctx.timestamp.toMillis() < completesAtMs) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function findCargoItem(ctx: ReducerContextLike, shipId: bigint, itemId: string) {
|
||||
return Array.from(ctx.db.cargo_item.iter()).find((row) => row.ship_id === shipId && row.item_id === itemId);
|
||||
}
|
||||
|
||||
function getShipCargoQuantity(ctx: ReducerContextLike, shipId: bigint) {
|
||||
return Array.from(ctx.db.cargo_item.iter())
|
||||
.filter((row) => row.ship_id === shipId)
|
||||
.reduce((total, row) => total + row.quantity, 0n);
|
||||
}
|
||||
|
||||
function getFreeCargoCapacity(ctx: ReducerContextLike, ship: ReturnType<typeof requirePlayerShip>) {
|
||||
const usedCapacity = getShipCargoQuantity(ctx, ship.ship_id);
|
||||
return ship.cargo_capacity > usedCapacity ? ship.cargo_capacity - usedCapacity : 0n;
|
||||
}
|
||||
|
||||
function minBigint(left: bigint, right: bigint) {
|
||||
return left < right ? left : right;
|
||||
}
|
||||
|
||||
function identitiesEqual(left: unknown, right: unknown) {
|
||||
if (left && right && typeof left === "object" && "isEqual" in left && typeof left.isEqual === "function") {
|
||||
return left.isEqual(right);
|
||||
}
|
||||
return left === right;
|
||||
}
|
||||
|
||||
function writeEvent(ctx: ReducerContextLike, eventType: string, message: string) {
|
||||
ctx.db.server_event.insert({
|
||||
event_id: 0n,
|
||||
at: ctx.timestamp,
|
||||
actor_identity: ctx.sender,
|
||||
event_type: eventType,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeDisplayName(displayName: string) {
|
||||
const trimmedName = displayName.trim();
|
||||
return trimmedName.length > 0 ? trimmedName.slice(0, 32) : "New Pilot";
|
||||
}
|
||||
|
||||
type ReducerContextLike = Parameters<Parameters<typeof spacetimedb.reducer>[1]>[0];
|
||||
8
services/spacetimedb/tsconfig.json
Normal file
8
services/spacetimedb/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2020"],
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user