Add server/src/index.js
This commit is contained in:
227
server/src/index.js
Normal file
227
server/src/index.js
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
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 { init as dbInit, Users, 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());
|
||||||
|
|
||||||
|
// simple world config
|
||||||
|
const WORLD = {
|
||||||
|
width: 4000,
|
||||||
|
height: 3000,
|
||||||
|
gatherRange: 40
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== Auth helpers =====
|
||||||
|
|
||||||
|
const COOKIE = 'auth';
|
||||||
|
|
||||||
|
function signToken(uid) {
|
||||||
|
return jwt.sign({ uid }, 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 =====
|
||||||
|
|
||||||
|
app.post('/api/register', async (req, res) => {
|
||||||
|
const { username, password } = req.body || {};
|
||||||
|
if (!username || !password)
|
||||||
|
return res.status(400).json({ error: 'Missing fields' });
|
||||||
|
try {
|
||||||
|
const user = await Users.create(username, password);
|
||||||
|
const token = signToken(user.id);
|
||||||
|
res
|
||||||
|
.cookie(COOKIE, token, { httpOnly: true, sameSite: 'lax' })
|
||||||
|
.json({ ok: true });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
res.status(400).json({ error: 'Username taken' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/login', async (req, res) => {
|
||||||
|
const { username, password } = req.body || {};
|
||||||
|
const user = await Users.verify(username, password);
|
||||||
|
if (!user) return res.status(401).json({ error: 'Invalid login' });
|
||||||
|
const token = signToken(user.id);
|
||||||
|
res
|
||||||
|
.cookie(COOKIE, token, { httpOnly: true, sameSite: 'lax' })
|
||||||
|
.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/logout', (req, res) => {
|
||||||
|
res.clearCookie(COOKIE).json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
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.getByUserId(auth.uid);
|
||||||
|
if (!ch) return res.status(404).json({ error: 'No character' });
|
||||||
|
const inv = await Inventory.all(ch.id);
|
||||||
|
res.json({ character: ch, inventory: inv, world: WORLD });
|
||||||
|
});
|
||||||
|
|
||||||
|
// static client
|
||||||
|
app.use(express.static(new URL('../static', import.meta.url).pathname));
|
||||||
|
|
||||||
|
const server = http.createServer(app);
|
||||||
|
const io = new IOServer(server, {
|
||||||
|
cors: { origin: true, credentials: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== In-memory world =====
|
||||||
|
|
||||||
|
let nodes = [];
|
||||||
|
function spawnNodes() {
|
||||||
|
nodes = [];
|
||||||
|
for (let i = 0; i < 120; i++) {
|
||||||
|
const types = ['wood', 'stone', 'ore', 'fiber'];
|
||||||
|
const type = types[i % types.length];
|
||||||
|
nodes.push({
|
||||||
|
id: i,
|
||||||
|
type,
|
||||||
|
x: Math.floor(Math.random() * WORLD.width),
|
||||||
|
y: Math.floor(Math.random() * WORLD.height),
|
||||||
|
alive: true,
|
||||||
|
respawnAt: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spawnNodes();
|
||||||
|
|
||||||
|
// socket player info (very simple)
|
||||||
|
const socketsToPlayers = new Map(); // socket.id -> { x,y,name,uid,charId }
|
||||||
|
|
||||||
|
io.on('connection', socket => {
|
||||||
|
// auth handshake
|
||||||
|
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 = match[1];
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
const ch = await Characters.getByUserId(decoded.uid);
|
||||||
|
if (!ch) return ack?.({ error: 'no character' });
|
||||||
|
|
||||||
|
const player = {
|
||||||
|
x: ch.x,
|
||||||
|
y: ch.y,
|
||||||
|
name: ch.name,
|
||||||
|
uid: decoded.uid,
|
||||||
|
charId: ch.id
|
||||||
|
};
|
||||||
|
socketsToPlayers.set(socket.id, player);
|
||||||
|
|
||||||
|
socket.join('world');
|
||||||
|
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.name, x: ch.x, y: ch.y },
|
||||||
|
world: WORLD
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
ack?.({ error: 'auth failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('move:click', target => {
|
||||||
|
const p = socketsToPlayers.get(socket.id);
|
||||||
|
if (!p) return;
|
||||||
|
// for now, we just trust and update directly (you can add validation later)
|
||||||
|
p.x = Math.max(0, Math.min(WORLD.width, target.x));
|
||||||
|
p.y = 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 simple state
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// player snapshot
|
||||||
|
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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user