23
server/src/services/database.ts
Normal file
23
server/src/services/database.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export function resolveDatabasePath() {
|
||||
if (process.env.DATABASE_PATH) {
|
||||
return process.env.DATABASE_PATH;
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return "/data/stellar.sqlite";
|
||||
}
|
||||
|
||||
return path.resolve(process.cwd(), "server/data/stellar.sqlite");
|
||||
}
|
||||
|
||||
export function createDatabase(databasePath = resolveDatabasePath()) {
|
||||
if (databasePath !== ":memory:") {
|
||||
mkdirSync(path.dirname(databasePath), { recursive: true });
|
||||
}
|
||||
|
||||
return new Database(databasePath);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type Database from "better-sqlite3";
|
||||
|
||||
export interface Mission {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -6,35 +8,52 @@ export interface Mission {
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
const missions: Mission[] = [
|
||||
{
|
||||
id: "first-growth",
|
||||
title: "First Growth",
|
||||
description: "Reach a stable absorber prototype.",
|
||||
rewardXp: 50,
|
||||
completed: false
|
||||
},
|
||||
{
|
||||
id: "field-test",
|
||||
title: "Field Test",
|
||||
description: "Prepare the first sandbox sector.",
|
||||
rewardXp: 100,
|
||||
completed: false
|
||||
}
|
||||
];
|
||||
|
||||
export function listMissions() {
|
||||
return missions;
|
||||
interface MissionRow {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
reward_xp: number;
|
||||
completed: number;
|
||||
}
|
||||
|
||||
export function completeMission(id: string) {
|
||||
const mission = missions.find((entry) => entry.id === id);
|
||||
|
||||
if (!mission) {
|
||||
return null;
|
||||
}
|
||||
|
||||
mission.completed = true;
|
||||
return mission;
|
||||
function toMission(row: MissionRow): Mission {
|
||||
return {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
rewardXp: row.reward_xp,
|
||||
completed: row.completed === 1
|
||||
};
|
||||
}
|
||||
|
||||
export function createMissionStore(database: Database.Database) {
|
||||
const listStatement = database.prepare(`
|
||||
SELECT id, title, description, reward_xp, completed
|
||||
FROM missions
|
||||
ORDER BY reward_xp ASC, id ASC
|
||||
`);
|
||||
const getStatement = database.prepare(`
|
||||
SELECT id, title, description, reward_xp, completed
|
||||
FROM missions
|
||||
WHERE id = ?
|
||||
`);
|
||||
const completeStatement = database.prepare("UPDATE missions SET completed = 1 WHERE id = ?");
|
||||
|
||||
return {
|
||||
listMissions() {
|
||||
return (listStatement.all() as MissionRow[]).map(toMission);
|
||||
},
|
||||
|
||||
completeMission(id: string) {
|
||||
const row = getStatement.get(id) as MissionRow | undefined;
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
completeStatement.run(id);
|
||||
return toMission({ ...row, completed: 1 });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
11
server/src/services/persistence.ts
Normal file
11
server/src/services/persistence.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createDatabase } from "./database.js";
|
||||
import { createMissionStore } from "./missions.js";
|
||||
import { createPlayerStore } from "./playerStore.js";
|
||||
import { initializeSchema } from "./schema.js";
|
||||
|
||||
const database = createDatabase();
|
||||
initializeSchema(database);
|
||||
|
||||
export const playerStore = createPlayerStore(database);
|
||||
export const missionStore = createMissionStore(database);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type Database from "better-sqlite3";
|
||||
|
||||
export interface PlayerProfile {
|
||||
id: string;
|
||||
mass: number;
|
||||
@@ -5,26 +7,71 @@ export interface PlayerProfile {
|
||||
evolutions: string[];
|
||||
}
|
||||
|
||||
const defaultProfile: PlayerProfile = {
|
||||
export const defaultProfile: PlayerProfile = {
|
||||
id: "local-player",
|
||||
mass: 12,
|
||||
xp: 0,
|
||||
evolutions: []
|
||||
};
|
||||
|
||||
let profile = { ...defaultProfile };
|
||||
|
||||
export function getPlayerProfile() {
|
||||
return profile;
|
||||
interface PlayerRow {
|
||||
id: string;
|
||||
mass: number;
|
||||
xp: number;
|
||||
evolutions: string;
|
||||
}
|
||||
|
||||
export function savePlayerProfile(nextProfile: Partial<PlayerProfile>) {
|
||||
profile = {
|
||||
...profile,
|
||||
...nextProfile,
|
||||
evolutions: nextProfile.evolutions ?? profile.evolutions
|
||||
function toProfile(row: PlayerRow): PlayerProfile {
|
||||
return {
|
||||
id: row.id,
|
||||
mass: row.mass,
|
||||
xp: row.xp,
|
||||
evolutions: JSON.parse(row.evolutions) as string[]
|
||||
};
|
||||
}
|
||||
|
||||
export function createPlayerStore(database: Database.Database) {
|
||||
const selectProfile = database.prepare("SELECT id, mass, xp, evolutions FROM player_profiles WHERE id = ?");
|
||||
const insertProfile = database.prepare(`
|
||||
INSERT INTO player_profiles (id, mass, xp, evolutions)
|
||||
VALUES (@id, @mass, @xp, @evolutions)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
mass = excluded.mass,
|
||||
xp = excluded.xp,
|
||||
evolutions = excluded.evolutions
|
||||
`);
|
||||
|
||||
return {
|
||||
getProfile(id = defaultProfile.id) {
|
||||
const row = selectProfile.get(id) as PlayerRow | undefined;
|
||||
|
||||
if (!row) {
|
||||
insertProfile.run({
|
||||
...defaultProfile,
|
||||
evolutions: JSON.stringify(defaultProfile.evolutions)
|
||||
});
|
||||
return { ...defaultProfile };
|
||||
}
|
||||
|
||||
return toProfile(row);
|
||||
},
|
||||
|
||||
saveProfile(nextProfile: Partial<PlayerProfile> & { id?: string }) {
|
||||
const current = this.getProfile(nextProfile.id ?? defaultProfile.id);
|
||||
const profile: PlayerProfile = {
|
||||
...current,
|
||||
...nextProfile,
|
||||
id: nextProfile.id ?? current.id,
|
||||
evolutions: nextProfile.evolutions ?? current.evolutions
|
||||
};
|
||||
|
||||
insertProfile.run({
|
||||
...profile,
|
||||
evolutions: JSON.stringify(profile.evolutions)
|
||||
});
|
||||
|
||||
return profile;
|
||||
}
|
||||
};
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
|
||||
45
server/src/services/schema.ts
Normal file
45
server/src/services/schema.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type Database from "better-sqlite3";
|
||||
|
||||
const seedMissions = [
|
||||
{
|
||||
id: "first-growth",
|
||||
title: "First Growth",
|
||||
description: "Reach a stable absorber prototype.",
|
||||
rewardXp: 50
|
||||
},
|
||||
{
|
||||
id: "field-test",
|
||||
title: "Field Test",
|
||||
description: "Prepare the first sandbox sector.",
|
||||
rewardXp: 100
|
||||
}
|
||||
];
|
||||
|
||||
export function initializeSchema(database: Database.Database) {
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS player_profiles (
|
||||
id TEXT PRIMARY KEY,
|
||||
mass REAL NOT NULL,
|
||||
xp INTEGER NOT NULL,
|
||||
evolutions TEXT NOT NULL DEFAULT '[]'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS missions (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
reward_xp INTEGER NOT NULL,
|
||||
completed INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
`);
|
||||
|
||||
const insertMission = database.prepare(`
|
||||
INSERT OR IGNORE INTO missions (id, title, description, reward_xp, completed)
|
||||
VALUES (@id, @title, @description, @rewardXp, 0)
|
||||
`);
|
||||
|
||||
for (const mission of seedMissions) {
|
||||
insertMission.run(mission);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user