feat: add ore and blacksmith

This commit is contained in:
2026-09-05 17:24:44 +05:00
parent f6c338e1b0
commit 6273f2849c
15 changed files with 1333 additions and 334 deletions
+66 -5
View File
@@ -6,9 +6,12 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
import sirv from 'sirv';
import {
PROTOCOL_VERSION,
PROPS,
SKILL_TITLES,
WORLD,
ZONES,
colorForName,
levelForTotalXp,
type DeltaMsg,
type GameEvent,
type SnapshotMsg,
@@ -80,7 +83,13 @@ const world: WorldSource = {
v: PROTOCOL_VERSION,
tickMs: cfg.tickMs,
serverNow: Date.now(),
world: { w: WORLD.w, h: WORLD.h, spawn: { ...WORLD.spawn }, zones: ZONES },
world: {
w: WORLD.w,
h: WORLD.h,
spawn: { ...WORLD.spawn },
zones: ZONES,
props: PROPS.map((p) => ({ ...p })),
},
};
},
snapshot(): SnapshotMsg {
@@ -90,7 +99,7 @@ const world: WorldSource = {
serverNow: Date.now(),
camera: camera.state,
avatars: [...sim.avatars.values()].map((av) => sim.serializeAvatar(av)),
nodes: sim.serializeTrees(),
nodes: sim.serializeNodes(),
stats: sim.stats(),
};
},
@@ -102,7 +111,7 @@ const gateway = new WsGateway(world);
function buildDelta(events: GameEvent[], camChanged: boolean): DeltaMsg | null {
const avatars = sim.takeDirtyAvatars();
const nodes = sim.takeDirtyTrees();
const nodes = sim.takeDirtyNodes();
const stats = sim.takeStatsIfDirty();
if (avatars.length === 0 && nodes.length === 0 && !stats && events.length === 0 && !camChanged) {
return null;
@@ -119,13 +128,13 @@ function buildDelta(events: GameEvent[], camChanged: boolean): DeltaMsg | null {
setInterval(() => {
const now = Date.now();
const events = sim.tick(now);
const camChanged = camera.tick(now);
const camChanged = camera.tick(now, sim.zoneActivity());
for (const rec of sim.takeDirtyViewers()) db.saveViewer(cfg.channelId, rec);
for (const ev of events) {
if (ev.k === 'levelup' && cfg.chatAnnounceLevelUp) {
announce(`🌲 ${ev.name} вырос(ла) до уровня ${ev.level} в рубке леса!`);
announce(` ${ev.name} вырос(ла) до ${ev.level}-го уровня: ${SKILL_TITLES[ev.skill] ?? ev.skill}!`);
}
}
@@ -201,6 +210,58 @@ function handleHttp(req: IncomingMessage, res: ServerResponse): void {
return;
}
if (cfg.devHttp && p === '/dev/give' && req.method === 'POST') {
readBody(req, (body) => {
try {
const parsed = JSON.parse(body || '{}') as { name?: string; item?: string; qty?: number };
const name = (parsed.name ?? '').trim();
const item = (parsed.item ?? '').trim();
const qty = Math.max(1, Math.min(999, Number(parsed.qty ?? 1)));
if (!name || !item) {
json(res, 400, { error: 'нужны name и item' });
return;
}
const ok = sim.giveItem(`dev:${name}`, item, qty);
json(res, ok ? 200 : 404, ok ? { ok: true } : { error: 'зритель не в мире' });
} catch (e) {
json(res, 400, { error: String(e) });
}
});
return;
}
if (cfg.devHttp && p === '/dev/tp' && req.method === 'POST') {
readBody(req, (body) => {
try {
const parsed = JSON.parse(body || '{}') as { name?: string; x?: number; y?: number };
const name = (parsed.name ?? '').trim();
if (!name || !Number.isFinite(parsed.x) || !Number.isFinite(parsed.y)) {
json(res, 400, { error: 'нужны name, x, y' });
return;
}
const ok = sim.teleport(`dev:${name}`, Number(parsed.x), Number(parsed.y));
json(res, ok ? 200 : 404, ok ? { ok: true } : { error: 'зритель не в мире' });
} catch (e) {
json(res, 400, { error: String(e) });
}
});
return;
}
if (p === '/api/inventory' && req.method === 'GET') {
const name = (url.searchParams.get('name') ?? '').trim();
const found = name ? sim.lookupViewer(name) : undefined;
if (!found) {
json(res, 200, { found: false });
return;
}
const skills = Object.fromEntries(
Object.entries(found.skills).map(([id, s]) => [id, { xp: s.xp, level: s.level ?? levelForTotalXp(s.xp) }]),
);
json(res, 200, { found: true, name: found.name, color: found.color, skills, items: found.items });
return;
}
for (const m of staticMounts) {
if (!m.middleware) continue;
const base = m.prefix.slice(0, -1);