Files
Isles-Of-Medievalkor/server/src/index.js
2025-11-13 17:07:28 +00:00

265 lines
6.8 KiB
JavaScript

import 'dotenv/config';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import http from 'http';
import { Server as IOServer } from 'socket.io';
import jwt from 'jsonwebtoken';
import path from 'path';
import { fileURLToPath } from 'url';
import { init as dbInit, Characters, Inventory, query } from './db.js';
const app = express();
app.use(helmet());
app.use(cors({ origin: true, credentials: true }));
app.use(express.json());
app.use(cookieParser());
const TILE_SIZE = 32;
const WORLD = {
width: 400000,
height: 300000,
gatherRange: 40
};
const COOKIE = 'auth';
// JWT payload: { cid: characterId }
function signToken(cid) {
return jwt.sign({ cid }, process.env.JWT_SECRET, { expiresIn: '30d' });
}
function authFromReq(req) {
try {
const token = req.cookies?.[COOKIE];
if (!token) return null;
return jwt.verify(token, process.env.JWT_SECRET);
} catch {
return null;
}
}
// ===== HTTP API =====
// Register: username+password per character
app.post('/api/register', async (req, res) => {
const { username, password } = req.body || {};
if (!username || !password) {
return res.status(400).json({ error: 'Missing username or password' });
}
try {
const ch = await Characters.create(username, password);
const token = signToken(ch.id);
res
.cookie(COOKIE, token, { httpOnly: true, sameSite: 'lax' })
.json({ ok: true });
} catch (e) {
console.error(e);
res.status(400).json({ error: 'Username taken' });
}
});
// Login: per-character
app.post('/api/login', async (req, res) => {
const { username, password } = req.body || {};
if (!username || !password) {
return res.status(400).json({ error: 'Missing username or password' });
}
const ch = await Characters.verify(username, password);
if (!ch) return res.status(401).json({ error: 'Invalid login' });
const token = signToken(ch.id);
res
.cookie(COOKIE, token, { httpOnly: true, sameSite: 'lax' })
.json({ ok: true });
});
// Logout
app.post('/api/logout', (req, res) => {
res.clearCookie(COOKIE).json({ ok: true });
});
// Current character
app.get('/api/me', async (req, res) => {
const auth = authFromReq(req);
if (!auth) return res.status(401).json({ error: 'Not logged in' });
const ch = await Characters.getById(auth.cid);
if (!ch) return res.status(404).json({ error: 'Character not found' });
const inv = await Inventory.all(ch.id);
res.json({ character: ch, inventory: inv, world: WORLD });
});
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Serve client
app.use(express.static(path.join(__dirname, '../static')));
const server = http.createServer(app);
const io = new IOServer(server, {
cors: { origin: true, credentials: true }
});
// ===== World: resource nodes =====
let nodes = [];
function spawnNodes() {
nodes = [];
const types = ['wood', 'stone', 'ore', 'fiber'];
const tilesX = Math.floor(WORLD.width / TILE_SIZE);
const tilesY = Math.floor(WORLD.height / TILE_SIZE);
for (let i = 0; i < 600; i++) {
const type = types[i % types.length];
const tx = Math.floor(Math.random() * tilesX);
const ty = Math.floor(Math.random() * tilesY);
const x = tx * TILE_SIZE + TILE_SIZE / 2;
const y = ty * TILE_SIZE + TILE_SIZE / 2;
nodes.push({
id: i,
type,
x,
y,
alive: true,
respawnAt: 0
});
}
}
spawnNodes();
// socket.id -> player state
const socketsToPlayers = new Map(); // { x,y,name,cid,charId }
// ===== Socket.IO =====
io.on('connection', socket => {
// character auth from cookie
socket.on('auth:join', async ack => {
try {
const cookieHeader = socket.handshake.headers.cookie || '';
const match = cookieHeader.match(/auth=([^;]+)/);
if (!match) return ack?.({ error: 'no auth cookie' });
const token = decodeURIComponent(match[1]);
const decoded = jwt.verify(token, process.env.JWT_SECRET); // { cid }
const ch = await Characters.getById(decoded.cid);
if (!ch) return ack?.({ error: 'character missing' });
const player = {
x: ch.x,
y: ch.y,
name: ch.username,
cid: decoded.cid,
charId: ch.id
};
socketsToPlayers.set(socket.id, player);
socket.join('world');
// send initial nodes
socket.emit(
'nodes:init',
nodes.map(n => ({
id: n.id,
type: n.type,
x: n.x,
y: n.y,
alive: n.alive
}))
);
ack?.({
ok: true,
you: { name: ch.username, x: ch.x, y: ch.y },
world: WORLD
});
} catch (e) {
console.error(e);
ack?.({ error: 'auth failed' });
}
});
// very basic server-side movement (trusting client target)
socket.on('move:click', target => {
const p = socketsToPlayers.get(socket.id);
if (!p) return;
// You can store target on the server if you want later:
p.targetX = Math.max(0, Math.min(WORLD.width, target.x));
p.targetY = Math.max(0, Math.min(WORLD.height, target.y));
});
socket.on('gather', async ({ nodeId }, ack) => {
const p = socketsToPlayers.get(socket.id);
if (!p) return ack?.({ error: 'not authed' });
const node = nodes.find(n => n.id === nodeId);
if (!node || !node.alive) return ack?.({ error: 'invalid node' });
const dist = Math.hypot(node.x - p.x, node.y - p.y);
if (dist > WORLD.gatherRange) return ack?.({ error: 'too far' });
node.alive = false;
node.respawnAt = Date.now() + 5000;
io.to('world').emit('node:update', { id: node.id, alive: false });
// give +1 item + 10 xp
await Inventory.add(p.charId, node.type, 1);
await Characters.addXP(p.charId, 10);
const { rows } = await query('SELECT xp FROM characters WHERE id=$1', [
p.charId
]);
const xp = rows[0].xp;
const level = Math.max(1, Math.floor(Math.sqrt(xp / 100)) + 1);
ack?.({ ok: true, item: node.type, xp: 10, level });
});
socket.on('chat:send', ({ t }) => {
const p = socketsToPlayers.get(socket.id);
if (!p) return;
const msg = `${p.name}: ${String(t || '').slice(0, 200)}`;
io.to('world').emit('chat:push', msg);
});
socket.on('disconnect', () => {
socketsToPlayers.delete(socket.id);
});
});
// game loop: respawn nodes + broadcast positions
setInterval(() => {
const now = Date.now();
for (const n of nodes) {
if (!n.alive && now >= n.respawnAt) {
n.alive = true;
io.to('world').emit('node:update', { id: n.id, alive: true });
}
}
const players = [];
for (const [id, p] of socketsToPlayers) {
players.push({ id, name: p.name, x: p.x, y: p.y });
}
io.to('world').emit('state', { players });
}, 200);
const port = process.env.PORT || 8080;
server.listen(port, async () => {
await dbInit();
console.log('Server listening on :' + port);
});