Destiny-board profiles (mastery/spec/other-specs per crafting category), premium flag, focus efficiency formula (250/30/30 per level), active profile feeds tax + per-category focus costs into the ranker; editor UI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { DB_PATH } from '../config.js';
|
|
|
|
const SCHEMA = `
|
|
CREATE TABLE IF NOT EXISTS meta (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS items (
|
|
unique_name TEXT PRIMARY KEY, -- e.g. T4_BAG@1
|
|
base_name TEXT NOT NULL, -- e.g. T4_BAG
|
|
enchantment INTEGER NOT NULL DEFAULT 0,
|
|
tier INTEGER,
|
|
weight REAL,
|
|
item_value INTEGER,
|
|
shop_category TEXT,
|
|
shop_subcategory TEXT,
|
|
crafting_category TEXT,
|
|
name_en TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_items_base ON items(base_name);
|
|
CREATE INDEX IF NOT EXISTS idx_items_name_en ON items(name_en);
|
|
|
|
CREATE TABLE IF NOT EXISTS recipes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
output_item TEXT NOT NULL,
|
|
variant INTEGER NOT NULL DEFAULT 0, -- alternative recipe index for the same output
|
|
amount_crafted REAL NOT NULL DEFAULT 1,
|
|
silver_cost REAL NOT NULL DEFAULT 0,
|
|
focus_cost REAL NOT NULL DEFAULT 0,
|
|
source TEXT NOT NULL DEFAULT 'crafting' -- 'crafting' | 'upgrade' (enchanting)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_recipes_output ON recipes(output_item);
|
|
|
|
CREATE TABLE IF NOT EXISTS recipe_inputs (
|
|
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
|
|
input_item TEXT NOT NULL,
|
|
count REAL NOT NULL,
|
|
max_return_amount REAL, -- 0 means the resource is never returned (e.g. artifacts)
|
|
PRIMARY KEY (recipe_id, input_item)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_recipe_inputs_item ON recipe_inputs(input_item);
|
|
|
|
CREATE TABLE IF NOT EXISTS market_prices (
|
|
item TEXT NOT NULL,
|
|
city TEXT NOT NULL,
|
|
quality INTEGER NOT NULL DEFAULT 1,
|
|
sell_price_min REAL,
|
|
sell_min_date TEXT, -- when AODP last observed this sell order (UTC ISO)
|
|
buy_price_max REAL,
|
|
buy_max_date TEXT,
|
|
fetched_at TEXT NOT NULL,
|
|
PRIMARY KEY (item, city, quality)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_market_prices_city ON market_prices(city);
|
|
|
|
CREATE TABLE IF NOT EXISTS profiles (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
is_active INTEGER NOT NULL DEFAULT 0,
|
|
premium INTEGER NOT NULL DEFAULT 1,
|
|
data TEXT NOT NULL DEFAULT '{}', -- JSON: destiny board levels per crafting category
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS price_overrides (
|
|
item TEXT NOT NULL,
|
|
city TEXT NOT NULL,
|
|
quality INTEGER NOT NULL DEFAULT 1,
|
|
sell_price REAL,
|
|
buy_price REAL,
|
|
note TEXT,
|
|
updated_at TEXT NOT NULL,
|
|
PRIMARY KEY (item, city, quality)
|
|
);
|
|
`;
|
|
|
|
let db: Database.Database | null = null;
|
|
|
|
export function getDb(): Database.Database {
|
|
if (!db) {
|
|
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
|
db = new Database(DB_PATH);
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('foreign_keys = ON');
|
|
db.exec(SCHEMA);
|
|
}
|
|
return db;
|
|
}
|
|
|
|
export function getMeta(key: string): string | null {
|
|
const row = getDb().prepare('SELECT value FROM meta WHERE key = ?').get(key) as
|
|
| { value: string }
|
|
| undefined;
|
|
return row?.value ?? null;
|
|
}
|
|
|
|
export function setMeta(key: string, value: string): void {
|
|
getDb()
|
|
.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value')
|
|
.run(key, value);
|
|
}
|