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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user