29
server/package.json
Normal file
29
server/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/server.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/server.js",
|
||||
"lint": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2",
|
||||
"pino": "^9.7.0",
|
||||
"pino-http": "^10.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/node": "^22.13.11",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^3.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
6
server/src/middleware/errorHandler.ts
Normal file
6
server/src/middleware/errorHandler.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
|
||||
export function errorHandler(error: Error, _request: Request, response: Response, _next: NextFunction) {
|
||||
response.status(500).json({ error: error.message || "Internal server error" });
|
||||
}
|
||||
|
||||
20
server/src/routes/missions.ts
Normal file
20
server/src/routes/missions.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Router } from "express";
|
||||
import { completeMission, listMissions } from "../services/missions.js";
|
||||
|
||||
export const missionsRouter = Router();
|
||||
|
||||
missionsRouter.get("/", (_request, response) => {
|
||||
response.json(listMissions());
|
||||
});
|
||||
|
||||
missionsRouter.post("/:id/complete", (request, response) => {
|
||||
const mission = completeMission(request.params.id);
|
||||
|
||||
if (!mission) {
|
||||
response.status(404).json({ error: "Mission not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
response.json({ rewardXp: mission.rewardXp, mission });
|
||||
});
|
||||
|
||||
14
server/src/routes/player.ts
Normal file
14
server/src/routes/player.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Router } from "express";
|
||||
import { getPlayerProfile, savePlayerProfile } from "../services/playerStore.js";
|
||||
|
||||
export const playerRouter = Router();
|
||||
|
||||
playerRouter.get("/", (_request, response) => {
|
||||
response.json(getPlayerProfile());
|
||||
});
|
||||
|
||||
playerRouter.post("/", (request, response) => {
|
||||
const profile = savePlayerProfile(request.body ?? {});
|
||||
response.status(200).json(profile);
|
||||
});
|
||||
|
||||
13
server/src/server.test.ts
Normal file
13
server/src/server.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { completeMission, listMissions } from "./services/missions.js";
|
||||
|
||||
describe("missions service", () => {
|
||||
it("lists seed missions", () => {
|
||||
expect(listMissions().length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("completes a mission by id", () => {
|
||||
expect(completeMission("first-growth")?.completed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
62
server/src/server.ts
Normal file
62
server/src/server.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import cors from "cors";
|
||||
import express from "express";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import pino from "pino";
|
||||
import { pinoHttp } from "pino-http";
|
||||
import { errorHandler } from "./middleware/errorHandler.js";
|
||||
import { missionsRouter } from "./routes/missions.js";
|
||||
import { playerRouter } from "./routes/player.js";
|
||||
|
||||
const logger = pino({
|
||||
level: process.env.LOG_LEVEL ?? "info"
|
||||
});
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const clientDistPath = path.resolve(__dirname, "../../client/dist");
|
||||
|
||||
const app = express();
|
||||
const port = Number(process.env.PORT ?? 3000);
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(
|
||||
pinoHttp({
|
||||
logger
|
||||
})
|
||||
);
|
||||
|
||||
app.get("/healthz", (_request, response) => {
|
||||
response.json({ status: "ok" });
|
||||
});
|
||||
|
||||
app.get("/api/logs", (_request, response) => {
|
||||
response.json({
|
||||
logs: [],
|
||||
message: "Log streaming will be added with persistent transport support."
|
||||
});
|
||||
});
|
||||
|
||||
app.use("/api/player", playerRouter);
|
||||
app.use("/api/missions", missionsRouter);
|
||||
|
||||
if (existsSync(clientDistPath)) {
|
||||
app.use(express.static(clientDistPath));
|
||||
|
||||
app.get("*", (request, response, next) => {
|
||||
if (request.path.startsWith("/api/")) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
response.sendFile(path.join(clientDistPath, "index.html"));
|
||||
});
|
||||
}
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
app.listen(port, "0.0.0.0", () => {
|
||||
logger.info({ port }, "stellar server listening");
|
||||
});
|
||||
40
server/src/services/missions.ts
Normal file
40
server/src/services/missions.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export interface Mission {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
rewardXp: number;
|
||||
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;
|
||||
}
|
||||
|
||||
export function completeMission(id: string) {
|
||||
const mission = missions.find((entry) => entry.id === id);
|
||||
|
||||
if (!mission) {
|
||||
return null;
|
||||
}
|
||||
|
||||
mission.completed = true;
|
||||
return mission;
|
||||
}
|
||||
|
||||
30
server/src/services/playerStore.ts
Normal file
30
server/src/services/playerStore.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export interface PlayerProfile {
|
||||
id: string;
|
||||
mass: number;
|
||||
xp: number;
|
||||
evolutions: string[];
|
||||
}
|
||||
|
||||
const defaultProfile: PlayerProfile = {
|
||||
id: "local-player",
|
||||
mass: 12,
|
||||
xp: 0,
|
||||
evolutions: []
|
||||
};
|
||||
|
||||
let profile = { ...defaultProfile };
|
||||
|
||||
export function getPlayerProfile() {
|
||||
return profile;
|
||||
}
|
||||
|
||||
export function savePlayerProfile(nextProfile: Partial<PlayerProfile>) {
|
||||
profile = {
|
||||
...profile,
|
||||
...nextProfile,
|
||||
evolutions: nextProfile.evolutions ?? profile.evolutions
|
||||
};
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
16
server/tsconfig.json
Normal file
16
server/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node", "vitest/globals"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user