feat: add ore and blacksmith
This commit is contained in:
@@ -1,7 +1,21 @@
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import type { ViewerRecord } from '../sim/world';
|
||||
|
||||
/**
|
||||
* Запись зрителя: инвентарь и XP навыков. Хранится вне ECS (мета-слой).
|
||||
*/
|
||||
export interface ViewerRecord {
|
||||
userId: string;
|
||||
name: string;
|
||||
color: string;
|
||||
/** skill id -> суммарный xp */
|
||||
skills: Record<string, number>;
|
||||
/** item id -> количество */
|
||||
items: Record<string, number>;
|
||||
}
|
||||
|
||||
const SCHEMA_VERSION = 2;
|
||||
|
||||
/**
|
||||
* Тонкий слой persist на встроенном node:sqlite — без нативных сборок и
|
||||
@@ -17,37 +31,103 @@ export class Db {
|
||||
}
|
||||
this.db = new DatabaseSync(dbPath);
|
||||
this.db.exec('PRAGMA journal_mode = WAL');
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS viewers (
|
||||
channel_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT NOT NULL,
|
||||
xp INTEGER NOT NULL DEFAULT 0,
|
||||
logs INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (channel_id, user_id)
|
||||
)
|
||||
`);
|
||||
|
||||
const row = this.db.prepare('PRAGMA user_version').get() as { user_version?: number };
|
||||
const version = row?.user_version ?? 0;
|
||||
if (version < SCHEMA_VERSION) {
|
||||
// дев-стадия: старую схему не мигрируем, а пересоздаём
|
||||
this.db.exec(`
|
||||
DROP TABLE IF EXISTS viewer_skills;
|
||||
DROP TABLE IF EXISTS viewer_items;
|
||||
DROP TABLE IF EXISTS viewers;
|
||||
CREATE TABLE viewers (
|
||||
channel_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (channel_id, user_id)
|
||||
);
|
||||
CREATE TABLE viewer_skills (
|
||||
channel_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
skill TEXT NOT NULL,
|
||||
xp INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (channel_id, user_id, skill)
|
||||
);
|
||||
CREATE TABLE viewer_items (
|
||||
channel_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
item TEXT NOT NULL,
|
||||
qty INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (channel_id, user_id, item)
|
||||
);
|
||||
PRAGMA user_version = ${SCHEMA_VERSION};
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
loadViewers(channelId: string): ViewerRecord[] {
|
||||
const rows = this.db
|
||||
.prepare('SELECT user_id, name, color, xp, logs FROM viewers WHERE channel_id = ?')
|
||||
.all(channelId) as Array<{ user_id: string; name: string; color: string; xp: number; logs: number }>;
|
||||
return rows.map((r) => ({ userId: r.user_id, name: r.name, color: r.color, xp: r.xp, logs: r.logs }));
|
||||
const viewers = this.db
|
||||
.prepare('SELECT user_id, name, color FROM viewers WHERE channel_id = ?')
|
||||
.all(channelId) as Array<{ user_id: string; name: string; color: string }>;
|
||||
|
||||
const skills = this.db
|
||||
.prepare('SELECT user_id, skill, xp FROM viewer_skills WHERE channel_id = ?')
|
||||
.all(channelId) as Array<{ user_id: string; skill: string; xp: number }>;
|
||||
const items = this.db
|
||||
.prepare('SELECT user_id, item, qty FROM viewer_items WHERE channel_id = ?')
|
||||
.all(channelId) as Array<{ user_id: string; item: string; qty: number }>;
|
||||
|
||||
const byId = new Map<string, ViewerRecord>();
|
||||
for (const v of viewers) {
|
||||
byId.set(v.user_id, { userId: v.user_id, name: v.name, color: v.color, skills: {}, items: {} });
|
||||
}
|
||||
for (const s of skills) {
|
||||
const rec = byId.get(s.user_id);
|
||||
if (rec) rec.skills[s.skill] = s.xp;
|
||||
}
|
||||
for (const it of items) {
|
||||
const rec = byId.get(it.user_id);
|
||||
if (rec) rec.items[it.item] = it.qty;
|
||||
}
|
||||
return [...byId.values()];
|
||||
}
|
||||
|
||||
saveViewer(channelId: string, v: ViewerRecord): void {
|
||||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO viewers (channel_id, user_id, name, color, xp, logs, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(channel_id, user_id) DO UPDATE SET
|
||||
name = excluded.name, color = excluded.color, xp = excluded.xp,
|
||||
logs = excluded.logs, updated_at = excluded.updated_at`,
|
||||
)
|
||||
.run(channelId, v.userId, v.name, v.color, v.xp, v.logs, Date.now());
|
||||
this.db.exec('BEGIN');
|
||||
try {
|
||||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO viewers (channel_id, user_id, name, color, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(channel_id, user_id) DO UPDATE SET
|
||||
name = excluded.name, color = excluded.color, updated_at = excluded.updated_at`,
|
||||
)
|
||||
.run(channelId, v.userId, v.name, v.color, Date.now());
|
||||
this.db
|
||||
.prepare('DELETE FROM viewer_skills WHERE channel_id = ? AND user_id = ?')
|
||||
.run(channelId, v.userId);
|
||||
this.db
|
||||
.prepare('DELETE FROM viewer_items WHERE channel_id = ? AND user_id = ?')
|
||||
.run(channelId, v.userId);
|
||||
const skillStmt = this.db.prepare(
|
||||
'INSERT INTO viewer_skills (channel_id, user_id, skill, xp) VALUES (?, ?, ?, ?)',
|
||||
);
|
||||
for (const [skill, xp] of Object.entries(v.skills)) {
|
||||
skillStmt.run(channelId, v.userId, skill, xp);
|
||||
}
|
||||
const itemStmt = this.db.prepare(
|
||||
'INSERT INTO viewer_items (channel_id, user_id, item, qty) VALUES (?, ?, ?, ?)',
|
||||
);
|
||||
for (const [item, qty] of Object.entries(v.items)) {
|
||||
if (qty > 0) itemStmt.run(channelId, v.userId, item, qty);
|
||||
}
|
||||
this.db.exec('COMMIT');
|
||||
} catch (e) {
|
||||
this.db.exec('ROLLBACK');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
|
||||
Reference in New Issue
Block a user