diff --git a/database/clean-database.js b/database/clean-database.js deleted file mode 100644 index b1fa4b9..0000000 --- a/database/clean-database.js +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/env node -// Simple DB cleaner/validator for SQLite players table -// Usage: node clean-database.js [--apply] [--delete-tests] - -const { playerHelpers } = require('./sqlite-db'); - -function isObject(v) { - return v && typeof v === 'object' && !Array.isArray(v); -} - -function normalizeName(name) { - if (!name) return ''; - // trim and collapse spaces - return String(name).trim().replace(/\s+/g, ' '); -} - -function looksLikeTestName(name) { - if (!name) return true; - return /test|dummy|sample|example/i.test(name); -} - -function cleanPlayer(player) { - const issues = []; - const updates = {}; - - // name - const cleanName = normalizeName(player.name); - if (!cleanName) issues.push('empty name'); - if (cleanName !== player.name) updates.name = cleanName; - if (cleanName.length > 100) issues.push('name too long'); - - // pw/pwHash - if (player.pw && player.pw.length > 0) issues.push('plain password present'); - if (player.pwHash && typeof player.pwHash !== 'string') updates.pwHash = String(player.pwHash); - - // rollerInfo / shopInfo / tabInfo should be objects - if (!isObject(player.rollerInfo)) { - issues.push('rollerInfo not object'); - updates.rollerInfo = {}; - } - if (!isObject(player.shopInfo)) { - issues.push('shopInfo not object'); - updates.shopInfo = {}; - } - if (!isObject(player.tabInfo)) { - issues.push('tabInfo not object'); - updates.tabInfo = {}; - } - - // Flatten nested tabInfo.tabInfo - if (isObject(player.tabInfo) && isObject(player.tabInfo.tabInfo)) { - issues.push('nested tabInfo.tabInfo found; will be flattened'); - updates.tabInfo = Object.assign({}, player.tabInfo.tabInfo, player.tabInfo); - delete updates.tabInfo.tabInfo; - } - - // Remove mongo-specific fields inside tabInfo or rollerInfo - ['mongoId', 'mongo_id', '_id', 'mongoIdString'].forEach(k => { - if (isObject(player.tabInfo) && player.tabInfo[k]) { - issues.push(`tabInfo contains ${k}; will be removed`); - const t = Object.assign({}, player.tabInfo); - delete t[k]; - updates.tabInfo = Object.assign({}, updates.tabInfo || player.tabInfo || {}, t); - } - if (isObject(player.rollerInfo) && player.rollerInfo[k]) { - issues.push(`rollerInfo contains ${k}; will be removed`); - const r = Object.assign({}, player.rollerInfo); - delete r[k]; - updates.rollerInfo = Object.assign({}, updates.rollerInfo || player.rollerInfo || {}, r); - } - }); - - // Normalize common known fields types in tabInfo - const expectedTabFields = ['renown','rp','xp','xpSpent','wounds','movement','notes','playerName','charName','rank']; - if (isObject(player.tabInfo)) { - expectedTabFields.forEach(f => { - if (Object.prototype.hasOwnProperty.call(player.tabInfo, f)) { - const val = player.tabInfo[f]; - if (f === 'rp' || f === 'xp' || f === 'xpSpent' || f === 'wounds' || f === 'movement') { - // ensure numeric - const n = Number(val); - if (!Number.isFinite(n)) { - issues.push(`${f} not numeric; setting to 0`); - updates.tabInfo = Object.assign({}, updates.tabInfo || player.tabInfo || {}, { [f]: 0 }); - } else if (n !== val) { - updates.tabInfo = Object.assign({}, updates.tabInfo || player.tabInfo || {}, { [f]: n }); - } - } - if (f === 'renown' && val && typeof val === 'string' && val.trim() === '') { - issues.push('renown blank string; normalizing to "None"'); - updates.tabInfo = Object.assign({}, updates.tabInfo || player.tabInfo || {}, { renown: 'None' }); - } - } - }); - } - - // Detect junk keys: very long strings or binary-like values - ['rollerInfo','shopInfo','tabInfo'].forEach(k => { - const v = player[k]; - if (isObject(v)) { - Object.keys(v).forEach(key => { - const val = v[key]; - if (typeof val === 'string' && val.length > 2000) { - issues.push(`${k}.${key} unusually long (>${2000})`); - // truncate - updates[k] = Object.assign({}, updates[k] || v, { [key]: val.substring(0, 2000) }); - } - }); - } - }); - - // Identify obvious test users - if (looksLikeTestName(player.name)) { - issues.push('test/dummy/example user'); - } - - return { issues, updates }; -} - -async function run() { - const args = process.argv.slice(2); - const apply = args.includes('--apply'); - const deleteTests = args.includes('--delete-tests'); - - const players = playerHelpers.getAll(); - const report = { - total: players.length, - toDelete: [], - toUpdate: [] - }; - - const seenNames = new Map(); - - for (const pl of players) { - const { issues, updates } = cleanPlayer(pl); - - // Duplicate name detection - const nameKey = normalizeName(pl.name).toLowerCase(); - if (seenNames.has(nameKey)) { - issues.push('duplicate name'); - report.toDelete.push({ name: pl.name, reason: 'duplicate' }); - continue; - } - seenNames.set(nameKey, true); - - if (issues.length > 0) { - report.toUpdate.push({ name: pl.name, issues, updates }); - } - - if (looksLikeTestName(pl.name) && deleteTests) { - report.toDelete.push({ name: pl.name, reason: 'test user' }); - } - } - - // Print report - console.log('=== Clean DB Report (dry-run unless --apply) ==='); - console.log('Total players:', report.total); - console.log('Candidates for update:', report.toUpdate.length); - console.log('Candidates for delete:', report.toDelete.length); - - if (report.toUpdate.length > 0) { - console.log('\n--- Updates ---'); - report.toUpdate.forEach(u => { - console.log('Player:', u.name); - console.log(' Issues:', u.issues.join('; ')); - console.log(' Planned updates:', JSON.stringify(u.updates)); - }); - } - - if (report.toDelete.length > 0) { - console.log('\n--- Deletes ---'); - report.toDelete.forEach(d => console.log('Player:', d.name, 'Reason:', d.reason)); - } - - if (apply) { - console.log('\nApplying fixes...'); - let updated=0, deleted=0; - for (const u of report.toUpdate) { - try { - if (Object.keys(u.updates).length > 0) { - // If name is changing, perform delete/create to maintain unique constraint - if (u.updates.name) { - const orig = u.name; - const newName = u.updates.name; - const pl = playerHelpers.getByName(orig); - if (pl) { - // delete then recreate - playerHelpers.delete(orig); - playerHelpers.create(Object.assign({}, pl, { name: newName, rollerInfo: u.updates.rollerInfo || pl.rollerInfo, shopInfo: u.updates.shopInfo || pl.shopInfo, tabInfo: u.updates.tabInfo || pl.tabInfo, pw: pl.pw, pwHash: pl.pwHash })); - updated++; - } - } else { - // normal update - const pl = playerHelpers.getByName(u.name); - if (pl) { - const merged = { - rollerInfo: u.updates.rollerInfo || pl.rollerInfo, - shopInfo: u.updates.shopInfo || pl.shopInfo, - tabInfo: u.updates.tabInfo || pl.tabInfo, - pw: pl.pw, - pwHash: pl.pwHash - }; - playerHelpers.update(u.name, merged); - updated++; - } - } - } - } catch (err) { - console.error('Failed to apply update for', u.name, err); - } - } - - for (const d of report.toDelete) { - try { - if (playerHelpers.delete(d.name)) deleted++; - } catch (err) { - console.error('Failed to delete', d.name, err); - } - } - - console.log(`Applied: ${updated} updates, ${deleted} deletes`); - } - - console.log('\nDone.'); -} - -run().catch(err => { console.error('Clean script error', err); process.exit(1); }); diff --git a/database/ensure-gm.js b/database/ensure-gm.js deleted file mode 100644 index 34e60a2..0000000 --- a/database/ensure-gm.js +++ /dev/null @@ -1,57 +0,0 @@ -// Script to ensure GM user exists -const { db } = require('./sqlite-db'); - -function logToFile(...args) { - const msg = `[${new Date().toISOString()}] ` + args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') + '\n'; - require('fs').appendFileSync(require('path').join(__dirname, 'backend.log'), msg, { encoding: 'utf8' }); -} - -// Check if GM user exists -const existingGm = db.prepare('SELECT * FROM players WHERE name = ?').get('gm'); - -if (!existingGm) { - // Create GM user with password from environment variable - const gmPassword = process.env.GM_PASSWORD || 'defaultpassword'; - const stmt = db.prepare(` - INSERT INTO players (name, pw, pw_hash, tab_info) - VALUES (?, ?, ?, ?) - `); - - stmt.run('gm', gmPassword, gmPassword, JSON.stringify({ - rp: 999999, - inventory: [], - renown: 'None' - })); - - logToFile('Created GM user'); - console.log('Created GM user'); -} else { - // Update GM user password if needed - const gmPassword = process.env.GM_PASSWORD || 'defaultpassword'; - const stmt = db.prepare(` - UPDATE players - SET pw = ?, pw_hash = ? - WHERE name = 'gm' - `); - - stmt.run(gmPassword, gmPassword); - - logToFile('Updated GM user'); - console.log('Updated GM user'); -} - -// Make sure GM has admin privileges -const stmt = db.prepare(` - UPDATE players - SET tab_info = ? - WHERE name = 'gm' -`); - -stmt.run(JSON.stringify({ - rp: 999999, - inventory: [], - renown: 'None' -})); - -logToFile('GM privileges ensured'); -console.log('GM privileges ensured'); diff --git a/database/scripts/apply_normalize.js b/database/scripts/apply_normalize.js deleted file mode 100644 index 19e8d6f..0000000 --- a/database/scripts/apply_normalize.js +++ /dev/null @@ -1,41 +0,0 @@ -const { playerHelpers } = require('../sqlite-db'); -const { validatePlayer } = require('../validate'); - -function backupDb() { - const fs = require('fs'); - const path = require('path'); - const ts = new Date().toISOString().replace(/[:.]/g,''); - const src = path.join(__dirname, '..', 'sqlite', 'deathwatch.db'); - const dst = path.join(__dirname, '..', 'sqlite', `deathwatch.db.pre_normalize.${ts}.bak`); - if (fs.existsSync(src)) { - fs.copyFileSync(src, dst); - console.log('Backup created:', dst); - } else { - console.log('No sqlite DB found at', src); - } -} - -function apply() { - backupDb(); - const players = playerHelpers.getAll(); - let applied = 0; - const details = []; - for (const p of players) { - const { valid, errors, normalized } = validatePlayer(p); - // Compare normalized to existing for keys we change (rollerInfo, shopInfo, tabInfo) - const changed = JSON.stringify({rollerInfo: p.rollerInfo, shopInfo: p.shopInfo, tabInfo: p.tabInfo}) !== JSON.stringify({rollerInfo: normalized.rollerInfo, shopInfo: normalized.shopInfo, tabInfo: normalized.tabInfo}); - if (changed) { - try { - playerHelpers.update(p.name, normalized); - applied++; - details.push({ name: p.name, errors, appliedChanges: true }); - } catch (err) { - details.push({ name: p.name, error: String(err) }); - } - } - } - console.log(`Applied normalization to ${applied} players`); - if (details.length) console.log('Details:', JSON.stringify(details, null, 2)); -} - -apply(); diff --git a/database/scripts/create_andreas_lucian.js b/database/scripts/create_andreas_lucian.js deleted file mode 100644 index 544d622..0000000 --- a/database/scripts/create_andreas_lucian.js +++ /dev/null @@ -1,136 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { playerHelpers } = require('../sqlite-db'); - -const armouryPath = path.join(__dirname, '..', '..', 'public', 'deathwatch-armoury.json'); -const armoury = JSON.parse(fs.readFileSync(armouryPath, 'utf8')); - -function findItemByName(name) { - const needle = String(name).toLowerCase(); - for (const cat of Object.keys(armoury.items)) { - const arr = armoury.items[cat]; - if (Array.isArray(arr)) { - let found = arr.find(i => String(i.name).toLowerCase() === needle); - if (found) return found; - found = arr.find(i => String(i.name).toLowerCase().includes(needle)); - if (found) return found; - } - } - return null; -} - -(function main(){ - const name = 'andreas'; - if (playerHelpers.getByName(name)) return console.error('Player already exists', name); - - const charName = 'Brother Lucian'; - const chapter = 'ultramarines'; - const speciality = 'apothecary'; - const rank = '1'; - - const characteristics = { - ws: 46, - bs: 40, - s: 41, - t: 40, - ag: 43, - int: 43, - per: 41, - wp: 42, - fel: 43 - }; - - const canonicalSkills = [ - 'Acrobatics (Ag)', 'Awareness (Per)', 'Charm (Fel)', 'Climb (S)', 'Command (Fel)', - 'Common Lore (Int)', 'Deathwatch', 'Imperium', 'War', 'Dodge (Ag)', - 'Forbidden Lore (Int)', 'Xenos', 'Intimidate (S)', 'Literacy (Int)', 'Medicae (Int)', - 'Navigation (Int)', 'Pilot (Ag)', 'Scholastic Lore (Int)', 'Codex Astartes', - 'Scrutiny (Per)', 'Search (Per)', 'Silent Move (Ag)', 'Speak Language (Int)', - 'High Gothic', 'Low Gothic', 'Survival (Int)', 'Tactics (Int)', 'Tracking (Int)', - 'Tech-Use (Int)' - ]; - - const skillMap = {}; - for (const s of canonicalSkills) skillMap[s] = { trained: false, plus10: false, plus20: false }; - // from the sheet: Common Lore, Medicae, Scholastic Lore, Awareness, Dodge maybe - ['Common Lore (Int)', 'Medicae (Int)', 'Scholastic Lore (Int)', 'Awareness (Per)', 'Dodge (Ag)'].forEach(s => { - if (!skillMap[s]) skillMap[s] = { trained: true, plus10: false, plus20: false }; - else skillMap[s].trained = true; - }); - - // weapons and gear from sheet: Mark VII power armour, chainsword, bolt pistol, 3 frag, 3 krak, combat knife - const weapons = []; - const wpNames = ['Chainsword', 'Astartes Bolt Pistol']; - for (const wn of wpNames) { - const item = findItemByName(wn); - if (item) { - const stats = item.stats || {}; - const dmg = stats.damage || ''; - const rangeMatch = dmg.match(/Range\s*([^;]+)/i); - const penMatch = dmg.match(/Pen\s*(\d+)/i); - const clipMatch = dmg.match(/Clip\s*(\d+)/i); - const rldMatch = dmg.match(/Reload\s*([^;]+)/i); - weapons.push({ - name: item.name, - class: stats.class || '', - damage: dmg, - type: item.category || '', - pen: penMatch ? penMatch[1] : '', - range: rangeMatch ? rangeMatch[1].trim() : '', - rof: '', - clip: clipMatch ? clipMatch[1] : '', - rld: rldMatch ? rldMatch[1].trim() : '', - special: stats.source || '' - }); - } - } - - const frag = findItemByName('Astartes Frag Grenade') || { name: 'Frag Grenade' }; - const krak = findItemByName('Astartes Krak Grenade') || { name: 'Krak Grenade' }; - const inventory = [{ name: frag.name, count: 3 }, { name: krak.name, count: 3 }]; - const gear = [{ name: frag.name, qty: 3 }, { name: krak.name, qty: 3 }]; - const chains = findItemByName('Chainsword') || { name: 'Chainsword' }; - gear.push({ name: chains.name, qty: 1 }); - const powerArmourLookup = findItemByName('Mark VII Power Armour') || findItemByName('Astartes Power Armour'); - gear.push({ name: powerArmourLookup ? powerArmourLookup.name : 'Mark VII Power Armour', qty: 1 }); - - const armourItem = powerArmourLookup; - const armour = armourItem ? { - body: armourItem.name, - ap: (armourItem.stats && armourItem.stats.protection && (armourItem.stats.protection.body || armourItem.stats.protection.total)) ? (armourItem.stats.protection.body || armourItem.stats.protection.total) : 8, - head: 0, - ra: 8, - la: 8, - rl: 8, - ll: 8 - } : { body: 'Mark VII Power Armour', ap: 8, head: 0, ra: 8, la: 8, rl: 8, ll: 8 }; - - const talents = ['Enhance Healing', 'Deathwatch Training']; - const fate = { total: 2, current: 2 }; - const wounds = { total: 19, current: 19, fatigue: 0 }; - const movement = { half: 5, full: 10, charge: 15, halfAction: 5, fullAction: 10, run: 30 }; - - const tabInfo = { - charName, - playerName: name, - chapter, - speciality, - rank, - characteristics, - skills: skillMap, - weapons, - inventory, - gear, - armour, - talents, - fate, - wounds, - movement, - renown: 'Rank 1', - powerArmour: true - }; - - const created = playerHelpers.create({ name, rollerInfo: {}, shopInfo: {}, tabInfo, pw: '', pwHash: '' }); - console.log('Created player', name); - console.log(JSON.stringify(created, null, 2)); -})(); diff --git a/database/scripts/create_chris_techmarine.js b/database/scripts/create_chris_techmarine.js deleted file mode 100644 index 38fcb03..0000000 --- a/database/scripts/create_chris_techmarine.js +++ /dev/null @@ -1,147 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { playerHelpers } = require('../sqlite-db'); - -const armouryPath = path.join(__dirname, '..', '..', 'public', 'deathwatch-armoury.json'); -const armoury = JSON.parse(fs.readFileSync(armouryPath, 'utf8')); - -function findItemByName(name) { - const needle = String(name).toLowerCase(); - for (const cat of Object.keys(armoury.items)) { - const arr = armoury.items[cat]; - if (Array.isArray(arr)) { - let found = arr.find(i => String(i.name).toLowerCase() === needle); - if (found) return found; - found = arr.find(i => String(i.name).toLowerCase().includes(needle)); - if (found) return found; - } - } - return null; -} - -(function main(){ - const name = 'chris'; - if (playerHelpers.getByName(name)) return console.error('Player already exists', name); - - // Template for a Rank 1 Techmarine (inspired by Deathwatch rules and existing pregens) - const charName = 'Brother Corvin'; - const chapter = 'iron hands'; - const speciality = 'techmarine'; - const rank = '1'; - - const characteristics = { - ws: 44, - bs: 40, - s: 42, - t: 41, - ag: 39, - int: 46, - per: 42, - wp: 43, - fel: 38 - }; - - const canonicalSkills = [ - 'Acrobatics (Ag)', 'Awareness (Per)', 'Charm (Fel)', 'Climb (S)', 'Command (Fel)', - 'Common Lore (Int)', 'Deathwatch', 'Imperium', 'War', 'Dodge (Ag)', - 'Forbidden Lore (Int)', 'Xenos', 'Intimidate (S)', 'Literacy (Int)', 'Medicae (Int)', - 'Navigation (Int)', 'Pilot (Ag)', 'Scholastic Lore (Int)', 'Codex Astartes', - 'Scrutiny (Per)', 'Search (Per)', 'Silent Move (Ag)', 'Speak Language (Int)', - 'High Gothic', 'Low Gothic', 'Survival (Int)', 'Tactics (Int)', 'Tracking (Int)', - 'Tech-Use (Int)' - ]; - - const skillMap = {}; - for (const s of canonicalSkills) skillMap[s] = { trained: false, plus10: false, plus20: false }; - - // Techmarine-focused trained skills - ['Tech-Use (Int)', 'Scholastic Lore (Int)', 'Common Lore (Int)', 'Tactics (Int)', 'Awareness (Per)', 'Guns (BS)', 'Melee (WS)'].forEach(s => { - if (!skillMap[s]) skillMap[s] = { trained: true, plus10: false, plus20: false }; - else skillMap[s].trained = true; - }); - - // Weapons: prefer Astartes Bolter and Bolt Pistol; include Chainsword as backup - const weapons = []; - const wpNames = ['Astartes Bolter', 'Astartes Bolt Pistol', 'Chainsword']; - for (const wn of wpNames) { - const item = findItemByName(wn); - if (item) { - const stats = item.stats || {}; - const dmg = stats.damage || ''; - const rangeMatch = dmg.match(/Range\s*([^;]+)/i); - const penMatch = dmg.match(/Pen\s*(\d+)/i); - const clipMatch = dmg.match(/Clip\s*(\d+)/i); - const rldMatch = dmg.match(/Reload\s*([^;]+)/i); - weapons.push({ - name: item.name, - class: stats.class || '', - damage: dmg, - type: item.category || '', - pen: penMatch ? penMatch[1] : '', - range: rangeMatch ? rangeMatch[1].trim() : '', - rof: '', - clip: clipMatch ? clipMatch[1] : '', - rld: rldMatch ? rldMatch[1].trim() : '', - special: stats.source || '' - }); - } - } - - const frag = findItemByName('Astartes Frag Grenade') || { name: 'Astartes Frag Grenade' }; - const krak = findItemByName('Astartes Krak Grenade') || { name: 'Astartes Krak Grenade' }; - const combatKnife = findItemByName('Astartes Combat Knife') || { name: 'Astartes Combat Knife' }; - - const inventory = [{ name: frag.name, count: 3 }, { name: krak.name, count: 3 }]; - const gear = [{ name: frag.name, qty: 3 }, { name: krak.name, qty: 3 }, { name: combatKnife.name, qty: 1 }]; - const powerArmourLookup = findItemByName('Mark VII Power Armour') || findItemByName('Astartes Power Armour'); - gear.push({ name: powerArmourLookup ? powerArmourLookup.name : 'Mark VII Power Armour', qty: 1 }); - - const armourItem = powerArmourLookup; - const armour = armourItem ? { - body: armourItem.name, - ap: (armourItem.stats && armourItem.stats.protection && (armourItem.stats.protection.body || armourItem.stats.protection.total)) ? (armourItem.stats.protection.body || armourItem.stats.protection.total) : 8, - head: 0, - ra: 8, - la: 8, - rl: 8, - ll: 8 - } : { body: 'Mark VII Power Armour', ap: 8, head: 0, ra: 8, la: 8, rl: 8, ll: 8 }; - - const talents = ['Tech-Use Specialist', 'Deathwatch Training']; - const fate = { total: 2, current: 2 }; - const wounds = { total: 22, current: 22, fatigue: 0 }; - const movement = { half: 5, full: 10, charge: 15, halfAction: 5, fullAction: 10, run: 30 }; - - const tabInfo = { - charName, - playerName: name, - chapter, - speciality, - rank, - characteristics, - skills: skillMap, - weapons, - inventory, - gear, - armour, - talents, - fate, - wounds, - movement, - renown: 'Rank 1', - powerArmour: true - }; - - // create with backup - const ts = Date.now(); - const backupsDir = path.join(__dirname, '..', 'backups'); - if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true }); - const beforePath = path.join(backupsDir, `${name}.before.${ts}.json`); - fs.writeFileSync(beforePath, JSON.stringify({ note: 'creating new player', name }, null, 2), 'utf8'); - - const created = playerHelpers.create({ name, rollerInfo: {}, shopInfo: {}, tabInfo, pw: '', pwHash: '' }); - const afterPath = path.join(backupsDir, `${name}.after.${ts}.json`); - fs.writeFileSync(afterPath, JSON.stringify(created, null, 2), 'utf8'); - console.log('Created player', name); - console.log(JSON.stringify(created, null, 2)); -})(); diff --git a/database/scripts/delete_test_users.js b/database/scripts/delete_test_users.js deleted file mode 100644 index ed64fb8..0000000 --- a/database/scripts/delete_test_users.js +++ /dev/null @@ -1,42 +0,0 @@ -const { playerHelpers } = require('../sqlite-db'); -const fs = require('fs'); -const path = require('path'); - -function backupDb() { - const ts = new Date().toISOString().replace(/[:.]/g,''); - const src = path.join(__dirname, '..', 'sqlite', 'deathwatch.db'); - const dst = path.join(__dirname, '..', 'sqlite', `deathwatch.db.pre_delete_tests.${ts}.bak`); - if (fs.existsSync(src)) { - fs.copyFileSync(src, dst); - console.log('Backup created:', dst); - return dst; - } - console.log('No sqlite DB found at', src); - return null; -} - -function isTestName(name) { - if (!name) return false; - return /test|dummy|sample|example|dev/i.test(name); -} - -function run() { - const backup = backupDb(); - const players = playerHelpers.getAll(); - const toDelete = players.filter(p => isTestName(p.name)); - console.log('Found', toDelete.length, 'test-like players'); - const deleted = []; - for (const p of toDelete) { - try { - const ok = playerHelpers.delete(p.name); - if (ok) deleted.push(p.name); - } catch (err) { - console.error('Failed to delete', p.name, err); - } - } - console.log('Deleted', deleted.length, 'players'); - if (backup) console.log('DB backup at', backup); - if (deleted.length) console.log('Deleted names:', deleted.join(', ')); -} - -run(); diff --git a/database/scripts/export_and_dedupe_christoffer.js b/database/scripts/export_and_dedupe_christoffer.js deleted file mode 100644 index 2fd1c68..0000000 --- a/database/scripts/export_and_dedupe_christoffer.js +++ /dev/null @@ -1,115 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { playerHelpers } = require('../sqlite-db'); - -async function main() { - const name = 'christoffer'; - const player = playerHelpers.getByName(name); - if (!player) { - console.error('Player not found:', name); - process.exit(1); - } - - const backupsDir = path.join(__dirname, '..', 'backups'); - if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true }); - - const beforePath = path.join(backupsDir, 'christoffer.before.json'); - fs.writeFileSync(beforePath, JSON.stringify(player, null, 2)); - console.log('Wrote', beforePath); - - // Dedupe gear by name (sum qty) - const tab = player.tabInfo || {}; - const gear = Array.isArray(tab.gear) ? tab.gear : []; - const map = new Map(); - for (const g of gear) { - const key = String(g.name || g).trim(); - if (!key) continue; - const existing = map.get(key.toLowerCase()); - const qty = parseInt((g.qty !== undefined && g.qty !== null) ? g.qty : (g.count !== undefined ? g.count : 1), 10) || 1; - if (existing) { - existing.qty = (existing.qty || 0) + qty; - } else { - map.set(key.toLowerCase(), { name: key, qty }); - } - } - const dedupedGear = Array.from(map.values()); - - // Also dedupe inventory by name (sum counts) - const inventory = Array.isArray(tab.inventory) ? tab.inventory : []; - const invMap = new Map(); - for (const it of inventory) { - const key = String(it.name || it).trim(); - if (!key) continue; - const cnt = parseInt(it.count || 0, 10) || 0; - const existing = invMap.get(key.toLowerCase()); - if (existing) existing.count += cnt; else invMap.set(key.toLowerCase(), { name: key, count: cnt }); - } - const dedupedInventory = Array.from(invMap.values()); - - // Update player with deduped lists - const newTab = Object.assign({}, tab, { gear: dedupedGear, inventory: dedupedInventory }); - const ok = playerHelpers.update(name, { name, rollerInfo: player.rollerInfo || {}, shopInfo: player.shopInfo || {}, tabInfo: newTab, pw: player.pw || '', pwHash: player.pwHash || '' }); - if (!ok) { - console.error('Failed to update player'); - process.exit(1); - } - - const updated = playerHelpers.getByName(name); - const afterPath = path.join(backupsDir, 'christoffer.after.json'); - fs.writeFileSync(afterPath, JSON.stringify(updated, null, 2)); - console.log('Wrote', afterPath); - - // Create a simple HTML snapshot - const html = generateHtmlSnapshot(updated); - const htmlPath = path.join(backupsDir, 'christoffer.html'); - fs.writeFileSync(htmlPath, html, 'utf8'); - console.log('Wrote', htmlPath); - - // Try to take a PNG screenshot using puppeteer if available - try { - const puppeteer = require('puppeteer'); - const browser = await puppeteer.launch({ args: ['--no-sandbox','--disable-setuid-sandbox'] }); - const page = await browser.newPage(); - await page.goto('file://' + htmlPath); - const pngPath = path.join(backupsDir, 'christoffer.png'); - await page.screenshot({ path: pngPath, fullPage: true }); - await browser.close(); - console.log('Wrote', pngPath); - } catch (err) { - console.log('Puppeteer not available or failed to run; skip PNG. Error:', err.message); - console.log('You can install puppeteer and re-run this script to generate a PNG.'); - } -} - -function generateHtmlSnapshot(player) { - const t = player.tabInfo || {}; - const safe = v => (v === undefined || v === null) ? '' : String(v); - const char = t.characteristics || {}; - const weapons = Array.isArray(t.weapons) ? t.weapons : []; - const gear = Array.isArray(t.gear) ? t.gear : []; - const inventory = Array.isArray(t.inventory) ? t.inventory : []; - return ` -
| ${escapeHtml(k)} | `).join('')}
|---|
| ${escapeHtml(safe(char[k]))} | `).join('')}