feat: Add validation and normalization utilities for player records

- Implemented `validatePlayer` and `normalizeTabInfo` functions in `database/validate.js` for player data validation and normalization.
- Added error handling for required fields and enforced data types and constraints.
- Introduced standardization for characteristics and skills.

feat: Create script for extracting rules from PDF files

- Developed `extract-rules.js` to extract text from PDF rulebooks and categorize rules.
- Integrated `pdftotext` for PDF processing and created a searchable rules database.
- Implemented rule categorization and indexing for efficient searching.

feat: Implement RulesTab component for rule searching

- Created `RulesTab.jsx` for searching and displaying rules with filtering options.
- Added recent searches and quick reference buttons for user convenience.
- Integrated API calls for fetching rules and displaying results dynamically.

feat: Add test script for Rules API

- Created `test-rules.js` to test the functionality of the Rules API endpoints.
- Included tests for search and stats endpoints to ensure proper response handling.
This commit is contained in:
2025-08-16 12:15:32 +02:00
parent ec487b0614
commit dde3abda55
85 changed files with 68620 additions and 93 deletions
@@ -0,0 +1,155 @@
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';
const player = playerHelpers.getByName(name);
if (!player) return console.error('Player not found', name);
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(player, null, 2), 'utf8');
console.log('Backup written:', beforePath);
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 };
// Mark trained skills exactly as on the sheet
['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
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 || ''
});
}
}
// Inventory and gear
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: 'Combat Knife' };
const chainsword = findItemByName('Chainsword') || { name: 'Chainsword' };
const powerArmourLookup = findItemByName('Mark VII Power Armour') || findItemByName('Astartes Power Armour');
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: chainsword.name, qty: 1 }, { name: powerArmourLookup ? powerArmourLookup.name : 'Mark VII Power Armour', qty: 1 }, { name: combatKnife.name, 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 demeanour = 'Honour the Codex';
const personalDemeanour = 'Studious';
const tabInfo = Object.assign({}, player.tabInfo || {}, {
charName,
playerName: name,
chapter,
speciality,
rank,
characteristics,
skills: skillMap,
weapons,
inventory,
gear,
armour,
talents,
fate,
wounds,
movement,
renown: 'Rank 1',
powerArmour: true,
demeanour,
personalDemeanour
});
const ok = playerHelpers.update(name, { name, rollerInfo: player.rollerInfo || {}, shopInfo: player.shopInfo || {}, tabInfo, pw: player.pw || '', pwHash: player.pwHash || '' });
if (!ok) return console.error('Failed to update player');
const updated = playerHelpers.getByName(name);
const afterPath = path.join(backupsDir, `${name}.after.${ts}.json`);
fs.writeFileSync(afterPath, JSON.stringify(updated, null, 2), 'utf8');
console.log('After backup written:', afterPath);
console.log(JSON.stringify(updated, null, 2));
})();
@@ -0,0 +1,143 @@
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;
}
(async function main(){
const name = 'claes';
const player = playerHelpers.getByName(name);
if (!player) return console.error('Player not found', name);
// Assault Marine (Brother Elyas) values inferred from screenshot
const charName = 'Brother Elyas';
const characteristics = {
ws: 44,
bs: 46,
s: 47,
t: 44,
ag: 48,
int: 30,
per: 41,
wp: 41,
fel: 37
};
const trained = [
'Awareness (Per)', 'Common Lore (Int)', 'Dodge (Ag)', 'Intimidate (S)', 'Scholastic Lore (Int)',
'Guns (BS)', 'Melee (WS)'
];
const existingSkills = player.tabInfo && player.tabInfo.skills ? player.tabInfo.skills : [];
const skillMap = {};
const canonical = [
'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)'
];
for (const s of canonical) skillMap[s] = { trained: false, plus10:false, plus20:false };
for (const s of trained) { skillMap[s] = skillMap[s] || { trained: false, plus10:false, plus20:false }; skillMap[s].trained = true; }
if (Array.isArray(existingSkills)) {
for (const s of existingSkills) { if (!skillMap[s]) skillMap[s] = { trained:true, plus10:false, plus20:false }; else skillMap[s].trained = true; }
} else if (typeof existingSkills === 'object') {
for (const [k,v] of Object.entries(existingSkills)) { skillMap[k] = Object.assign(skillMap[k]||{trained:false,plus10:false,plus20:false}, v); }
}
// Weapons & gear
const weaponNames = ['Astartes Bolt Pistol', 'Powerfist'];
const weapons = [];
for (const wn of weaponNames) {
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('Frag Grenade') || { name: 'Frag Grenade' };
const krak = findItemByName('Krak Grenade') || { name: 'Krak Grenade' };
const inventory = [ { name: frag.name, count: 3 }, { name: krak.name, count: 3 } ];
const gearEntries = [ { name: frag.name, qty:3 }, { name: krak.name, qty:3 } ];
const sac = findItemByName('Combat Knife') || { name: 'Combat Knife' };
gearEntries.push({ name: sac.name, qty:1 });
const armourItem = findItemByName('Mark VII Power Armour') || findItemByName('Astartes Power Armour');
const armour = {};
if (armourItem && armourItem.stats && armourItem.stats.protection) {
armour.body = armourItem.name;
armour.ap = armourItem.stats.protection.body || 8;
armour.head = armourItem.stats.protection.head || 0;
armour.ra = armourItem.stats.protection.arms || 8;
armour.la = armour.ra;
armour.rl = armourItem.stats.protection.legs || 8;
armour.ll = armour.rl;
} else {
armour.body = armourItem ? armourItem.name : 'Mark VII Power Armour';
armour.ap = 8; armour.head = 0; armour.ra = 8; armour.la = 8; armour.rl = 8; armour.ll = 8;
}
gearEntries.push({ name: armour.body, qty: 1 });
const talents = ['Wings of Angels', 'Deathwatch Training'];
const fate = { total: 3, current: 3 };
const wounds = { total: 21, current: 21, fatigue: 0 };
const movement = { half:5, full:10, charge:15, halfAction:5, fullAction:10, run:30 };
const renown = 'Rank 1';
const powerArmour = true;
const newTab = Object.assign({}, player.tabInfo || {}, {
charName,
characteristics,
skills: skillMap,
weapons,
inventory,
gear: (player.tabInfo && player.tabInfo.gear && player.tabInfo.gear.length) ? player.tabInfo.gear.concat(gearEntries) : gearEntries,
armour,
talents,
fate,
wounds,
movement,
renown,
powerArmour
});
const ok = playerHelpers.update(name, { name, rollerInfo: player.rollerInfo || {}, shopInfo: player.shopInfo || {}, tabInfo: newTab, pw: player.pw || '', pwHash: player.pwHash || '' });
if (!ok) return console.error('Failed to update player');
console.log('Updated player', name);
console.log(JSON.stringify(playerHelpers.getByName(name), null, 2));
})();
@@ -0,0 +1,159 @@
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 = 'anders';
const player = playerHelpers.getByName(name);
if (!player) return console.error('Player not found', name);
const backupsDir = path.join(__dirname, '..', 'backups');
if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true });
const before = path.join(backupsDir, `anders.before.${Date.now()}.json`);
fs.writeFileSync(before, JSON.stringify(player, null, 2));
console.log('Backup written:', before);
// Character exact from screenshot
const charName = 'Brother Sepheran';
const characteristics = {
ws: 47,
bs: 48,
s: 41,
t: 43,
ag: 42,
int: 50,
per: 43,
wp: 48,
fel: 38
};
// Skills to mark trained based on sheet
const trained = [
'Awareness (Per)',
'Common Lore (Int)',
'Dodge (Ag)',
'Intimidate (S)',
'Logic (Int)',
'Scholastic Lore (Int)',
// space marine defaults
'Guns (BS)', 'Melee (WS)'
];
const canonical = [
'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 existingSkills = player.tabInfo && player.tabInfo.skills ? player.tabInfo.skills : [];
const skillMap = {};
for (const s of canonical) skillMap[s] = { trained: false, plus10: false, plus20: false };
for (const s of trained) { skillMap[s] = skillMap[s] || { trained: false, plus10:false, plus20:false }; skillMap[s].trained = true; }
if (Array.isArray(existingSkills)) {
for (const s of existingSkills) { if (!skillMap[s]) skillMap[s] = { trained:true, plus10:false, plus20:false }; else skillMap[s].trained = true; }
} else if (typeof existingSkills === 'object') {
for (const [k,v] of Object.entries(existingSkills)) { skillMap[k] = Object.assign(skillMap[k]||{trained:false,plus10:false,plus20:false}, v); }
}
// Weapons & gear
const weaponNames = ['Heavy Bolter', 'Astartes Bolt Pistol', 'Bolt Pistol'];
const weapons = [];
for (const wn of weaponNames) {
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('Frag Grenade') || { name: 'Frag Grenade' };
const krak = findItemByName('Krak Grenade') || { name: 'Krak Grenade' };
const inventory = [ { name: frag.name, count: 3 }, { name: krak.name, count: 3 } ];
const gearEntries = [ { name: frag.name, qty:3 }, { name: krak.name, qty:3 } ];
const knife = findItemByName('Combat Knife') || findItemByName('Combat Knife') || { name: 'Combat Knife' };
gearEntries.push({ name: knife.name, qty:1 });
const armourItem = findItemByName('Mark VII Power Armour') || findItemByName('Astartes Power Armour');
const armour = {};
if (armourItem && armourItem.stats && armourItem.stats.protection) {
armour.body = armourItem.name;
armour.ap = armourItem.stats.protection.body || 8;
armour.head = armourItem.stats.protection.head || 0;
armour.ra = armourItem.stats.protection.arms || 8;
armour.la = armour.ra;
armour.rl = armourItem.stats.protection.legs || 8;
armour.ll = armour.rl;
} else {
armour.body = armourItem ? armourItem.name : 'Mark VII Power Armour';
armour.ap = 8; armour.head = 0; armour.ra = 8; armour.la = 8; armour.rl = 8; armour.ll = 8;
}
gearEntries.push({ name: armour.body, qty: 1 });
const talents = ['Unrelenting Devastation', 'Deathwatch Training'];
const fate = { total: 2, current: 2 };
const wounds = { total: 23, current: 23, fatigue: 0 };
const movement = { half:5, full:10, charge:15, halfAction:5, fullAction:10, run:30 };
const renown = 'Rank 1';
const powerArmour = true;
const newTab = Object.assign({}, player.tabInfo || {}, {
charName,
characteristics,
skills: skillMap,
weapons,
inventory,
gear: (player.tabInfo && player.tabInfo.gear && player.tabInfo.gear.length) ? player.tabInfo.gear.concat(gearEntries) : gearEntries,
armour,
talents,
fate,
wounds,
movement,
renown,
powerArmour
});
const ok = playerHelpers.update(name, { name, rollerInfo: player.rollerInfo || {}, shopInfo: player.shopInfo || {}, tabInfo: newTab, pw: player.pw || '', pwHash: player.pwHash || '' });
if (!ok) return console.error('Failed to update player');
const after = playerHelpers.getByName(name);
const afterPath = path.join(backupsDir, `anders.after.${Date.now()}.json`);
fs.writeFileSync(afterPath, JSON.stringify(after, null, 2));
console.log('Updated player', name);
console.log(JSON.stringify(after, null, 2));
})();
+41
View File
@@ -0,0 +1,41 @@
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();
@@ -0,0 +1,196 @@
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)) {
// Exact then partial match
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;
}
(async function main(){
const name = 'christoffer';
const player = playerHelpers.getByName(name);
if (!player) return console.error('Player not found', name);
// Tactical Marine template (reasonable defaults)
// Characteristics exactly as on the sheet
const characteristics = {
ws: 42,
bs: 42,
s: 42,
t: 41,
ag: 40,
int: 41,
per: 46,
wp: 42,
fel: 43
};
// Canonical skill keys aligned with frontend SKILLS array
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)'
];
// Skills to mark trained based on sheet (map to canonical keys)
const tacticalTrain = [
'Awareness (Per)', 'Common Lore (Int)', 'Dodge (Ag)', 'Intimidate (S)', 'Scholastic Lore (Int)', 'Search (Per)',
'Guns (BS)', 'Melee (WS)', 'Tactics (Int)', 'Medicae (Int)', 'Command (Fel)',
'Codex Astartes', 'Adeptus Astartes', 'Imperium', 'War'
];
// Build skills map starting from canonical skills, mark tacticalTrain as trained, then merge existing skills
const existingSkills = player.tabInfo && player.tabInfo.skills ? player.tabInfo.skills : [];
const skillMap = {};
// initialize canonical skills as untrained
for (const s of canonicalSkills) {
skillMap[s] = { trained: false, plus10: false, plus20: false };
}
// mark trained skills from tacticalTrain
for (const s of tacticalTrain) {
if (!skillMap[s]) skillMap[s] = { trained: true, plus10: false, plus20: false };
else skillMap[s].trained = true;
}
// keep previously trained ones too
if (Array.isArray(existingSkills)) {
for (const s of existingSkills) {
if (!skillMap[s]) skillMap[s] = { trained: true, plus10: false, plus20: false };
else skillMap[s].trained = true;
}
} else if (typeof existingSkills === 'object') {
for (const [k,v] of Object.entries(existingSkills)) {
if (!skillMap[k]) skillMap[k] = Object.assign({ trained:false, plus10:false, plus20:false }, v);
else skillMap[k] = Object.assign(skillMap[k], v);
}
}
// Weapons: Bolter (kraken rounds), Bolt Pistol, Sacris Claymore
const weaponNames = ['Astartes Bolter', 'Astartes Bolt Pistol', 'Sacris Claymore'];
const weapons = [];
for (const wn of weaponNames) {
const item = findItemByName(wn);
if (item) {
const stats = item.stats || {};
// parse damage string for range and clip/pen as earlier
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 || ''
});
}
}
// Inventory: grenades (3 frag, 3 krak)
const inventory = [];
const frag = findItemByName('Frag Grenade') || findItemByName('Astartes Frag Grenade') || { name: 'Frag Grenade' };
const krak = findItemByName('Krak Grenade') || findItemByName('Astartes Krak Grenade') || { name: 'Krak Grenade' };
inventory.push({ name: frag.name, count: 3 });
inventory.push({ name: krak.name, count: 3 });
// Also expose grenades and sacris claymore in the frontend "gear" list so they appear in Assigned Gear
const gearEntries = [];
gearEntries.push({ name: frag.name, qty: 3 });
gearEntries.push({ name: krak.name, qty: 3 });
const sacris = findItemByName('Sacris Claymore') || { name: 'Sacris Claymore' };
gearEntries.push({ name: sacris.name, qty: 1 });
// include power armour as gear note (lookup now to avoid referencing armourItem before init)
const powerArmourLookup = findItemByName('Mark VII Power Armour') || findItemByName('Astartes Power Armour');
gearEntries.push({ name: powerArmourLookup ? powerArmourLookup.name : 'Mark VII Power Armour', qty: 1 });
// Armour: Mark VII Power Armour (Armour Points 8)
const armourItem = findItemByName('Mark VII Power Armour') || findItemByName('Astartes Power Armour');
const armour = {};
if (armourItem) {
armour.body = armourItem.name;
// armoury stats may vary; set numeric AP where available or fallback to 8
if (armourItem.stats && armourItem.stats.protection) {
armour.ap = armourItem.stats.protection.body || 8;
armour.head = armourItem.stats.protection.head || 0;
armour.ra = armourItem.stats.protection.arms || armour.ap;
armour.la = armour.ra; // mirror right arm
armour.rl = armourItem.stats.protection.legs || armour.ap;
armour.ll = armour.rl; // mirror left leg
} else {
armour.ap = 8;
armour.head = 0;
armour.ra = 8;
armour.la = 8;
armour.rl = 8;
armour.ll = 8;
}
} else {
armour.body = 'Mark VII Power Armour';
armour.ap = 8;
armour.head = 0;
armour.ra = 8;
armour.la = 8;
armour.rl = 8;
armour.ll = 8;
}
// talents/traits from sheet
const talents = ['Bolter Mastery', 'Deathwatch Training'];
const fate = { total: 2, current: 2 };
const renown = 'Rank 1';
const powerArmour = true;
// wounds and movement per sheet
const wounds = { total: 24, current: 24, fatigue: 0 };
// write both canonical movement keys used across code: half/full/charge for frontend and halfAction/fullAction/run for other scripts
const movement = { half: 5, full: 10, charge: 15, halfAction: 5, fullAction: 10, run: 30 };
// Set display character name to match provided sheet
const charName = 'Brother Gregor';
const newTab = Object.assign({}, player.tabInfo || {}, {
charName,
characteristics,
skills: skillMap,
weapons,
inventory,
gear: (player.tabInfo && player.tabInfo.gear && player.tabInfo.gear.length) ? player.tabInfo.gear.concat(gearEntries) : gearEntries,
armour,
talents,
fate,
wounds,
movement,
renown,
powerArmour
});
const ok = playerHelpers.update(name, { name, rollerInfo: player.rollerInfo || {}, shopInfo: player.shopInfo || {}, tabInfo: newTab, pw: player.pw || '', pwHash: player.pwHash || '' });
if (!ok) return console.error('Failed to update player');
console.log('Updated player', name);
console.log(JSON.stringify(playerHelpers.getByName(name), null, 2));
})();
@@ -0,0 +1,177 @@
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)) {
// Exact then partial match
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;
}
(async function main(){
const name = 'phillip';
const player = playerHelpers.getByName(name);
if (!player) return console.error('Player not found', name);
// Tactical Marine template (reasonable defaults)
const characteristics = player.tabInfo && player.tabInfo.characteristics ? player.tabInfo.characteristics : {
ws: 42,
bs: 42,
s: 42,
t: 41,
ag: 40,
int: 41,
per: 46,
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 tacticalTrain = [
'Awareness (Per)', 'Common Lore (Int)', 'Dodge (Ag)', 'Intimidate (S)', 'Scholastic Lore (Int)', 'Search (Per)',
'Guns (BS)', 'Melee (WS)', 'Tactics (Int)', 'Medicae (Int)', 'Command (Fel)',
'Codex Astartes', 'Adeptus Astartes', 'Imperium', 'War'
];
const existingSkills = player.tabInfo && player.tabInfo.skills ? player.tabInfo.skills : [];
const skillMap = {};
for (const s of canonicalSkills) {
skillMap[s] = { trained: false, plus10: false, plus20: false };
}
for (const s of tacticalTrain) {
if (!skillMap[s]) skillMap[s] = { trained: true, plus10: false, plus20: false };
else skillMap[s].trained = true;
}
if (Array.isArray(existingSkills)) {
for (const s of existingSkills) {
if (!skillMap[s]) skillMap[s] = { trained: true, plus10: false, plus20: false };
else skillMap[s].trained = true;
}
} else if (typeof existingSkills === 'object') {
for (const [k,v] of Object.entries(existingSkills)) {
if (!skillMap[k]) skillMap[k] = Object.assign({ trained:false, plus10:false, plus20:false }, v);
else skillMap[k] = Object.assign(skillMap[k], v);
}
}
const weaponNames = ['Astartes Bolter', 'Astartes Bolt Pistol', 'Sacris Claymore'];
const weapons = [];
for (const wn of weaponNames) {
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 inventory = [];
const frag = findItemByName('Frag Grenade') || findItemByName('Astartes Frag Grenade') || { name: 'Frag Grenade' };
const krak = findItemByName('Krak Grenade') || findItemByName('Astartes Krak Grenade') || { name: 'Krak Grenade' };
inventory.push({ name: frag.name, count: 3 });
inventory.push({ name: krak.name, count: 3 });
const gearEntries = [];
gearEntries.push({ name: frag.name, qty: 3 });
gearEntries.push({ name: krak.name, qty: 3 });
const sacris = findItemByName('Sacris Claymore') || { name: 'Sacris Claymore' };
gearEntries.push({ name: sacris.name, qty: 1 });
const powerArmourLookup = findItemByName('Mark VII Power Armour') || findItemByName('Astartes Power Armour');
gearEntries.push({ name: powerArmourLookup ? powerArmourLookup.name : 'Mark VII Power Armour', qty: 1 });
const armourItem = findItemByName('Mark VII Power Armour') || findItemByName('Astartes Power Armour');
const armour = {};
if (armourItem) {
armour.body = armourItem.name;
if (armourItem.stats && armourItem.stats.protection) {
armour.ap = armourItem.stats.protection.body || 8;
armour.head = armourItem.stats.protection.head || 0;
armour.ra = armourItem.stats.protection.arms || armour.ap;
armour.la = armour.ra;
armour.rl = armourItem.stats.protection.legs || armour.ap;
armour.ll = armour.rl;
} else {
armour.ap = 8;
armour.head = 0;
armour.ra = 8;
armour.la = 8;
armour.rl = 8;
armour.ll = 8;
}
} else {
armour.body = 'Mark VII Power Armour';
armour.ap = 8;
armour.head = 0;
armour.ra = 8;
armour.la = 8;
armour.rl = 8;
armour.ll = 8;
}
const talents = ['Bolter Mastery', 'Deathwatch Training'];
const fate = { total: 2, current: 2 };
const renown = 'Rank 1';
const powerArmour = true;
const wounds = { total: 24, current: 24, fatigue: 0 };
const movement = { half: 4, full: 8, charge: 12, halfAction: 4, fullAction: 8, run: 24 };
const charName = 'Brother Skold';
const newTab = Object.assign({}, player.tabInfo || {}, {
charName,
speciality: 'tactical',
characteristics,
skills: skillMap,
weapons,
inventory,
gear: (player.tabInfo && player.tabInfo.gear && player.tabInfo.gear.length) ? player.tabInfo.gear.concat(gearEntries) : gearEntries,
armour,
talents,
fate,
wounds,
movement,
renown,
powerArmour
});
const ok = playerHelpers.update(name, { name, rollerInfo: player.rollerInfo || {}, shopInfo: player.shopInfo || {}, tabInfo: newTab, pw: player.pw || '', pwHash: player.pwHash || '' });
if (!ok) return console.error('Failed to update player');
console.log('Updated player', name);
console.log(JSON.stringify(playerHelpers.getByName(name), null, 2));
})();
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env node
/*
Apply templates and items to players.
Usage:
node apply_templates.js --mapping path/to/mapping.json [--apply]
If no mapping provided, creates a sample mapping at database/backups/template-mapping.sample.json and exits.
Mapping format (JSON):
{
"players": [
{
"name": "christoffer",
"setCharacteristics": { "WS": 35, "BS": 40 },
"addItems": ["Bolt Pistol"],
"setRenown": "Respected"
}
]
}
This script will only modify player tabInfo.inventory (array) and tabInfo.characteristics and renown. It backs up the DB before applying when --apply is used.
*/
const fs = require('fs');
const path = require('path');
const argv = require('yargs/yargs')(process.argv.slice(2)).argv;
const { playerHelpers } = require('../sqlite-db');
function backupDb(prefix) {
const ts = new Date().toISOString().replace(/[:.]/g,'');
const src = path.join(__dirname, '..', 'sqlite', 'deathwatch.db');
const dst = path.join(__dirname, '..', 'sqlite', `deathwatch.db.${prefix}.${ts}.bak`);
if (fs.existsSync(src)) {
fs.copyFileSync(src, dst);
return dst;
}
return null;
}
const armouryPath = path.join(__dirname, '..', '..', 'public', 'deathwatch-armoury.json');
let armoury = null;
try { armoury = JSON.parse(fs.readFileSync(armouryPath, 'utf8')); } catch (e) { armoury = null; }
const rulesPath = path.join(__dirname, '..', 'rules', 'rules-database.json');
let rulesDb = null;
try { rulesDb = JSON.parse(fs.readFileSync(rulesPath, 'utf8')); } catch (e) { rulesDb = null; }
const mappingPath = argv.mapping;
const apply = !!argv.apply;
if (!mappingPath) {
// create sample mapping file
const sample = {
players: playerHelpers.getAll().map(p=>({ name: p.name, setCharacteristics: {}, addItems: [], setRenown: null }))
};
const outDir = path.join(__dirname, '..', 'backups'); if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
const samplePath = path.join(outDir, `template-mapping.sample.${new Date().toISOString().replace(/[:.]/g,'')}.json`);
fs.writeFileSync(samplePath, JSON.stringify(sample, null, 2), 'utf8');
console.log('Sample mapping created at', samplePath);
console.log('Edit it to add templates/items and re-run with --mapping <path> --apply');
process.exit(0);
}
if (!fs.existsSync(mappingPath)) {
console.error('Mapping file not found:', mappingPath);
process.exit(2);
}
const mapping = JSON.parse(fs.readFileSync(mappingPath, 'utf8'));
const players = playerHelpers.getAll();
function findItemByName(name) {
if (!armoury) return null;
// armoury may be an object with items lists; search recursively
const candidates = [];
function search(obj) {
if (!obj) return;
if (Array.isArray(obj)) { obj.forEach(search); return; }
if (typeof obj === 'object') {
Object.keys(obj).forEach(k => search(obj[k]));
if (obj.name && typeof obj.name === 'string' && obj.name.toLowerCase().includes(name.toLowerCase())) {
candidates.push(obj);
}
}
}
search(armoury);
return candidates;
}
const report = [];
for (const m of (mapping.players || [])) {
const pName = m.name;
const pl = players.find(x=>x.name===pName);
if (!pl) { report.push({ name: pName, error: 'player not found' }); continue; }
const before = JSON.parse(JSON.stringify(pl.tabInfo || {}));
const after = JSON.parse(JSON.stringify(before));
let changed = false;
// set characteristics
if (m.setCharacteristics && typeof m.setCharacteristics === 'object') {
after.characteristics = after.characteristics || {};
Object.keys(m.setCharacteristics).forEach(k=>{ after.characteristics[k] = m.setCharacteristics[k]; });
changed = true;
}
// set renown
if (m.setRenown) { after.renown = m.setRenown; changed = true; }
// add items
if (Array.isArray(m.addItems) && m.addItems.length>0) {
after.inventory = Array.isArray(after.inventory) ? after.inventory.slice() : [];
for (const itemName of m.addItems) {
const found = findItemByName(itemName);
if (found && found.length>0) {
// push item summary
after.inventory.push({ name: found[0].name || itemName, source: found[0].source || 'armoury' });
} else {
after.inventory.push({ name: itemName, source: 'manual' });
}
}
changed = true;
}
report.push({ name: pName, changed, before, after });
}
const summary = { applied: apply, results: report };
if (apply) {
const backup = backupDb('pre_apply_templates');
let appliedCount=0;
for (const r of report) {
if (r.changed) {
try {
const pl = playerHelpers.getByName(r.name);
const merged = Object.assign({}, pl, { tabInfo: r.after });
playerHelpers.update(r.name, merged);
appliedCount++;
} catch (e) { console.error('Failed to apply for', r.name, e); }
}
}
summary.appliedCount = appliedCount;
summary.backup = backup;
const outPath = path.join(__dirname, '..', 'backups', `apply_templates.result.${new Date().toISOString().replace(/[:.]/g,'')}.json`);
fs.writeFileSync(outPath, JSON.stringify(summary, null, 2),'utf8');
console.log('Applied to', appliedCount, 'players. Backup at', backup, 'Result saved to', outPath);
} else {
const outPath = path.join(__dirname, '..', 'backups', `apply_templates.dryrun.${new Date().toISOString().replace(/[:.]/g,'')}.json`);
fs.writeFileSync(outPath, JSON.stringify(summary, null, 2),'utf8');
console.log('Dry-run result written to', outPath);
console.log(JSON.stringify(summary, null, 2));
}
+136
View File
@@ -0,0 +1,136 @@
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));
})();
+147
View File
@@ -0,0 +1,147 @@
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));
})();
+42
View File
@@ -0,0 +1,42 @@
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();
@@ -0,0 +1,115 @@
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 `<!doctype html>
<html><head><meta charset="utf-8"><title>${safe(player.name)} - snapshot</title>
<style>body{font-family:Arial,Helvetica,sans-serif;color:#111;background:#fff;padding:20px}h1{font-size:20px}table{border-collapse:collapse;margin-bottom:12px}td,th{border:1px solid #ccc;padding:6px}</style>
</head><body>
<h1>${escapeHtml(safe(t.charName || player.name))} (${escapeHtml(safe(player.name))})</h1>
<h2>Characteristics</h2>
<table><tr>${Object.keys(char).map(k=>`<th>${escapeHtml(k)}</th>`).join('')}</tr>
<tr>${Object.keys(char).map(k=>`<td>${escapeHtml(safe(char[k]))}</td>`).join('')}</tr></table>
<h2>Weapons</h2>
${weapons.map(w=>`<div><strong>${escapeHtml(safe(w.name))}</strong> — ${escapeHtml(safe(w.damage||w.special||''))}</div>`).join('')}
<h2>Gear</h2>
<ul>${gear.map(g=>`<li>${escapeHtml(safe(g.name))} x${escapeHtml(safe(g.qty||g.count||1))}</li>`).join('')}</ul>
<h2>Inventory</h2>
<ul>${inventory.map(i=>`<li>${escapeHtml(safe(i.name))} x${escapeHtml(safe(i.count||0))}</li>`).join('')}</ul>
<h2>Fate / Wounds</h2>
<div>Fate: ${escapeHtml(safe((t.fate||{}).total))} (current ${escapeHtml(safe((t.fate||{}).current))})</div>
<div>Wounds: ${escapeHtml(safe((t.wounds||{}).total))} (current ${escapeHtml(safe((t.wounds||{}).current))})</div>
<h2>Talents</h2>
<div>${escapeHtml((t.talents||[]).join(', '))}</div>
</body></html>`;
}
function escapeHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
main().catch(err=>{ console.error(err); process.exit(1); });
+10
View File
@@ -0,0 +1,10 @@
const { playerHelpers } = require('../sqlite-db');
const fs = require('fs');
const path = require('path');
const players = playerHelpers.getAll();
const outPath = path.join(__dirname, '..', 'backups');
if (!fs.existsSync(outPath)) fs.mkdirSync(outPath, { recursive: true });
const file = path.join(outPath, `players-after-clean.${new Date().toISOString().replace(/[:.]/g,'')}.json`);
fs.writeFileSync(file, JSON.stringify(players, null, 2), 'utf8');
console.log('Exported players to', file);
+22
View File
@@ -0,0 +1,22 @@
const { playerHelpers } = require('../sqlite-db');
const bcrypt = require('bcrypt');
async function run() {
const players = playerHelpers.getAll();
let changed = 0;
for (const p of players) {
try {
if (p.pw && p.pw.length > 0) {
const hash = await bcrypt.hash(p.pw, 10);
playerHelpers.update(p.name, { rollerInfo: p.rollerInfo, shopInfo: p.shopInfo, tabInfo: p.tabInfo, pw: '', pwHash: hash });
changed++;
console.log('Hashed pw for:', p.name);
}
} catch (err) {
console.error('Failed to hash for', p.name, err);
}
}
console.log('Completed hashing. Total changed:', changed);
}
run().catch(err=>console.error(err));
+3
View File
@@ -0,0 +1,3 @@
const { playerHelpers } = require('../sqlite-db');
const players = playerHelpers.getAll();
console.log(JSON.stringify(players.map(u=>({name:u.name, pw: !!u.pw, hasPwHash: !!u.pwHash, tabInfoKeys: Object.keys(u.tabInfo||{}).length})), null, 2));
@@ -0,0 +1,48 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { playerHelpers } = require('../sqlite-db');
const { validatePlayer } = require('../validate');
const dbDir = path.join(__dirname, '..', 'sqlite');
const dbPath = path.join(dbDir, 'deathwatch.db');
const backupsDir = path.join(__dirname, '..', 'backups');
if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true });
function timestamp() { return new Date().toISOString().replace(/[:.]/g,''); }
const args = process.argv.slice(2);
const doApply = args.includes('--apply');
(async function main(){
console.log('Starting migration: apply defaults to existing players');
// backup DB first if applying
if (doApply) {
const bak = path.join(dbDir, `deathwatch.db.pre_migrate.${timestamp()}.bak`);
fs.copyFileSync(dbPath, bak);
console.log('Backup created at', bak);
}
const players = playerHelpers.getAll();
const results = [];
for (const p of players) {
const before = JSON.parse(JSON.stringify(p.tabInfo || {}));
const { valid, errors, normalized } = validatePlayer({ name: p.name, tabInfo: before, rollerInfo: p.rollerInfo, shopInfo: p.shopInfo });
const after = normalized.tabInfo;
const changed = JSON.stringify(before) !== JSON.stringify(after);
results.push({ name: p.name, changed, before, after, errors });
if (doApply && changed) {
// apply update via playerHelpers.update
const updated = playerHelpers.update(p.name, { name: p.name, rollerInfo: p.rollerInfo, shopInfo: p.shopInfo, tabInfo: after, pw: p.pw, pwHash: p.pwHash });
if (!updated) console.error('Failed to update', p.name);
}
}
const out = { applied: !!doApply, timestamp: new Date().toISOString(), results };
const outFile = path.join(backupsDir, `migrate_apply_defaults.${doApply ? 'applied' : 'dryrun'}.${timestamp()}.json`);
fs.writeFileSync(outFile, JSON.stringify(out, null, 2), 'utf8');
console.log((doApply ? 'Applied' : 'Dry-run complete') + ' - report written to', outFile);
})();
@@ -0,0 +1,209 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { playerHelpers } = require('../sqlite-db');
const { validatePlayer } = require('../validate');
const dbDir = path.join(__dirname, '..', 'sqlite');
const dbPath = path.join(dbDir, 'deathwatch.db');
const backupsDir = path.join(__dirname, '..', 'backups');
if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true });
const armouryPath = path.join(__dirname, '..', '..', 'public', 'deathwatch-armoury.json');
let armoury = {};
try { armoury = JSON.parse(fs.readFileSync(armouryPath, 'utf8')); } catch (e) { console.warn('Armoury not found or invalid'); }
function timestamp() { return new Date().toISOString().replace(/[:.]/g,''); }
const args = process.argv.slice(2);
const doApply = args.includes('--apply');
// canonical skill list (from PlayerTab)
const SKILLS = [
'Acrobatics (Ag)','Awareness (Per)','Barter (Fel)','Blather (Fel)','Carouse (Fel)','Charm (Fel)','Chem-Use (Int)','Ciphers (Int)','Chapter Runes','Climb (S)','Command (Fel)','Common Lore (Int)','Adeptus Astartes','Deathwatch','Imperium','War','Concealment (Ag)','Contortionist (Ag)','Deceive (Fel)','Demolition (Int)','Disguise (Fel)','Dodge (Ag)','Drive (Ag)','Evaluate','Forbidden Lore (Int)','Xenos','Gamble (Int)','Inquiry (Fel)','Interrogation (WP)','Intimidate (S)','Invocation (WP)','Lip Reading (Per)','Literacy (Int)','Logic (Int)','Medicae (Int)','Navigation (Int)','Surface','Performer (Fel)','Pilot (Ag)','Psyniscience (Per)','Scholastic Lore (Int)','Codex Astartes','Scrutiny (Per)','Search (Per)','Secret Tongue (Int)','Security (Ag)','Shadowing (Ag)','Silent Move (Ag)','Sleight of Hand (Ag)','Speak Language (Int)','Survival (Int)','Swim (S)','Tactics (Int)','Tech-Use (Int)','Tracking (Int)','Trade (Int)','Wrangling (Int)'
];
function mapCharacteristics(chars) {
// produce lower-case keys expected by frontend
const keys = { ws: 'WS', bs: 'BS', s: 'S', t: 'T', ag: 'Ag', int: 'Int', per: 'Per', wp: 'Wp', fel: 'Fel' };
const out = {};
if (!chars || typeof chars !== 'object') return Object.fromEntries(Object.keys(keys).map(k=>[k,0]));
// copy by case-insensitive match
const lookup = {};
Object.keys(chars).forEach(k => { lookup[k.toLowerCase()] = chars[k]; lookup[k.toUpperCase()] = chars[k]; });
Object.entries(keys).forEach(([lk, up]) => {
let val = 0;
if (chars[lk] !== undefined) val = Number(chars[lk]) || 0;
else if (chars[up] !== undefined) val = Number(chars[up]) || 0;
else if (chars[lk.toLowerCase()] !== undefined) val = Number(chars[lk.toLowerCase()]) || 0;
else if (chars[up.toLowerCase()] !== undefined) val = Number(chars[up.toLowerCase()]) || 0;
out[lk] = val;
});
return out;
}
function convertSkills(skillsIn) {
// skillsIn may be array of strings or object map
const out = {};
for (const s of SKILLS) out[s] = { trained: false, plus10: false, plus20: false };
if (!skillsIn) return out;
if (Array.isArray(skillsIn)) {
for (const s of skillsIn) {
const key = SKILLS.find(k => k.toLowerCase() === String(s).toLowerCase());
if (key) out[key].trained = true;
}
return out;
}
if (typeof skillsIn === 'object') {
// assume already in map form
for (const k of Object.keys(skillsIn)) {
const key = SKILLS.find(s => s.toLowerCase() === k.toLowerCase()) || k;
out[key] = Object.assign({ trained:false, plus10:false, plus20:false }, skillsIn[k]);
}
// ensure all standard skills exist
for (const s of SKILLS) if (!out[s]) out[s] = { trained:false, plus10:false, plus20:false };
return out;
}
return out;
}
function resolveInventoryToWeaponsAndArmour(inv, existingWeapons, existingArmour) {
const weapons = Array.isArray(existingWeapons) ? [...existingWeapons] : [];
const armour = Object.assign({}, existingArmour || {});
if (!Array.isArray(inv)) return { weapons, armour };
for (const it of inv) {
if (!it || !it.name) continue;
const name = it.name;
// armoury.items is an object keyed by category; search each category array
let fromArmoury = null;
if (armoury && armoury.items) {
for (const cat of Object.keys(armoury.items)) {
const arr = armoury.items[cat];
if (Array.isArray(arr)) {
const found = arr.find(i => i.name === name || String(i.name).toLowerCase() === String(name).toLowerCase());
if (found) { fromArmoury = found; break; }
}
}
}
if (fromArmoury) {
const item = fromArmoury;
// stats may be in item.stats: { damage, class, source } or similar
const stats = item.stats || {};
const damageStr = stats.damage || '';
const cls = stats.class || '';
// heuristics: if stats.damage looks like weapon or category contains 'Weapon'
const isWeapon = /Range|Pen|Clip|Reload|RoF|Tearing|Melta|Flame|Shotgun|Bolt|Pistol|Rifle|Cannon|Gun/i.test(damageStr) || /weapon/i.test(item.category || '');
const isArmour = /Armor|Armour|Power Armor|Carapace|Shield/i.test(item.category || '') || /armou?r/i.test(name) || /power armour/i.test(name);
if (isWeapon) {
// extract simple fields from damageStr
const rangeMatch = damageStr.match(/Range\s*([^;]+)/i);
const penMatch = damageStr.match(/Pen\s*(\d+)/i);
const clipMatch = damageStr.match(/Clip\s*(\d+)/i);
const rldMatch = damageStr.match(/Reload\s*([^;]+)/i);
const rofMatch = damageStr.match(/RoF/i);
const specialParts = [];
if (damageStr) specialParts.push(damageStr);
if (stats.source) specialParts.push(stats.source);
weapons.push({
name: item.name || name,
class: cls || '',
damage: damageStr || '',
type: item.category || '',
pen: penMatch ? penMatch[1] : '',
range: rangeMatch ? rangeMatch[1].trim() : '',
rof: rofMatch ? 'RoF' : '',
clip: clipMatch ? clipMatch[1] : '',
rld: rldMatch ? rldMatch[1].trim() : '',
special: specialParts.join(' | ')
});
} else if (isArmour) {
// place power/carapace armour into body slot by default
armour.body = armour.body || item.name || name;
// if protection object present, map head/arms/legs
if (stats.protection && typeof stats.protection === 'object') {
if (stats.protection.head !== undefined) armour.head = armour.head || stats.protection.head;
if (stats.protection.arms !== undefined) armour.ra = armour.ra || stats.protection.arms;
if (stats.protection.body !== undefined) armour.body = armour.body || stats.protection.body || item.name;
if (stats.protection.legs !== undefined) armour.rl = armour.rl || stats.protection.legs;
}
}
}
}
return { weapons, armour };
}
// Add pre-gen names if requested via a special flag file
function ensurePregens() {
const pregensFile = path.join(__dirname, '..', 'pregen_names.json');
if (!fs.existsSync(pregensFile)) {
const sample = ["Brother-1","Brother-2","Brother-3","Brother-4","Brother-5"];
fs.writeFileSync(pregensFile, JSON.stringify(sample, null, 2), 'utf8');
return sample;
}
try { return JSON.parse(fs.readFileSync(pregensFile, 'utf8')); } catch { return []; }
}
(async function main(){
console.log('Starting sheet transform migration');
if (doApply) {
const bak = path.join(dbDir, `deathwatch.db.pre_transform.${timestamp()}.bak`);
fs.copyFileSync(dbPath, bak);
console.log('Backup created at', bak);
}
const players = playerHelpers.getAll();
const results = [];
for (const p of players) {
const before = JSON.parse(JSON.stringify(p.tabInfo || {}));
const tab = Object.assign({}, before);
// characteristics -> lowercase keys
tab.characteristics = mapCharacteristics(tab.characteristics || {});
// skills -> map form
tab.skills = convertSkills(tab.skills || []);
// fate defaults
if (!tab.fate || typeof tab.fate !== 'object') tab.fate = { total: 1, current: 1 };
else {
tab.fate.total = Number(tab.fate.total) || 1;
tab.fate.current = Number(tab.fate.current) || tab.fate.total || 1;
}
// ensure renown normalized via validate
const validated = validatePlayer({ name: p.name, tabInfo: tab });
tab.renown = validated.normalized.tabInfo.renown;
// powerArmour normalization
if (tab.powerArmour === 'true' || tab.powerArmour === true) tab.powerArmour = true;
else tab.powerArmour = !!tab.powerArmour;
// resolve inventory items into weapons/armour fields where possible
const { weapons: newWeapons, armour: newArmour } = resolveInventoryToWeaponsAndArmour(tab.inventory || [], tab.weapons || [], tab.armour || {});
if (newWeapons.length > 0) tab.weapons = newWeapons;
tab.armour = Object.assign({}, tab.armour || {}, newArmour);
// ensure talents is string
if (!tab.talents) tab.talents = tab.talents || '';
const after = tab;
const changed = JSON.stringify(before) !== JSON.stringify(after);
results.push({ name: p.name, changed, before, after });
if (doApply && changed) {
const ok = playerHelpers.update(p.name, { name: p.name, rollerInfo: p.rollerInfo, shopInfo: p.shopInfo, tabInfo: after, pw: p.pw, pwHash: p.pwHash });
if (!ok) console.error('Failed to update', p.name);
}
}
const out = { applied: !!doApply, timestamp: new Date().toISOString(), results };
const outFile = path.join(backupsDir, `migrate_transform_sheet_fields.${doApply ? 'applied' : 'dryrun'}.${timestamp()}.json`);
fs.writeFileSync(outFile, JSON.stringify(out, null, 2), 'utf8');
console.log((doApply ? 'Applied' : 'Dry-run complete') + ' - report written to', outFile);
})();
@@ -0,0 +1,39 @@
const fs = require('fs');
const path = require('path');
const { playerHelpers } = require('../sqlite-db');
(function main(){
const name = 'phillip';
const player = playerHelpers.getByName(name);
if (!player) return console.error('Player not found', name);
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(player, null, 2), 'utf8');
console.log('Backup written:', beforePath);
// Tactical sheet characteristic values (from attached Tactical image)
const characteristics = {
ws: 42,
bs: 42,
s: 42,
t: 41,
ag: 40,
int: 41,
per: 46,
wp: 42,
fel: 43
};
const newTab = Object.assign({}, player.tabInfo || {}, { characteristics });
const ok = playerHelpers.update(name, { name, rollerInfo: player.rollerInfo || {}, shopInfo: player.shopInfo || {}, tabInfo: newTab, pw: player.pw || '', pwHash: player.pwHash || '' });
if (!ok) return console.error('Failed to update player');
const updated = playerHelpers.getByName(name);
const afterPath = path.join(backupsDir, `${name}.after.${ts}.json`);
fs.writeFileSync(afterPath, JSON.stringify(updated, null, 2), 'utf8');
console.log('After backup written:', afterPath);
console.log(JSON.stringify(updated, null, 2));
})();
+48
View File
@@ -0,0 +1,48 @@
const { playerHelpers } = require('../sqlite-db');
const fs = require('fs');
const path = require('path');
const players = playerHelpers.getAll();
const total = players.length;
const names = players.map(p=>p.name).sort();
const renownCounts = {};
let rpSum=0, xpSum=0, xpSpentSum=0;
let rpMin=Infinity, rpMax=-Infinity, xpMin=Infinity, xpMax=-Infinity;
let charStats = {}; // key -> {min,max,sum,count}
players.forEach(p=>{
const t = p.tabInfo || {};
const ren = t.renown || 'None';
renownCounts[ren] = (renownCounts[ren]||0)+1;
const rp = Number(t.rp||0); rpSum+=rp; rpMin=Math.min(rpMin,rp); rpMax=Math.max(rpMax,rp);
const xp = Number(t.xp||0); xpSum+=xp; xpMin=Math.min(xpMin,xp); xpMax=Math.max(xpMax,xp);
const xps = Number(t.xpSpent||0); xpSpentSum+=xps;
const chars = t.characteristics || {};
Object.keys(chars).forEach(k=>{
const v = Number(chars[k]||0);
if (!charStats[k]) charStats[k]={min:Infinity,max:-Infinity,sum:0,count:0};
charStats[k].min=Math.min(charStats[k].min,v);
charStats[k].max=Math.max(charStats[k].max,v);
charStats[k].sum+=v; charStats[k].count++;
});
});
const summary = {
totalPlayers: total,
names,
renownCounts,
rp: { sum: rpSum, avg: total? rpSum/total:0, min: rpMin===Infinity?0:rpMin, max: rpMax===-Infinity?0:rpMax },
xp: { sum: xpSum, avg: total? xpSum/total:0, min: xpMin===Infinity?0:xpMin, max: xpMax===-Infinity?0:xpMax },
xpSpent: { sum: xpSpentSum, avg: total? xpSpentSum/total:0 },
characteristics: Object.fromEntries(Object.entries(charStats).map(([k,v])=>[k,{min:v.min===Infinity?0:v.min,max:v.max===-Infinity?0:v.max,avg:v.count? v.sum/v.count:0,count:v.count}])),
missingPwHash: players.filter(p=>!p.pwHash||p.pwHash.length===0).map(p=>p.name),
samplePlayers: players.slice(0,5).map(p=>({name:p.name, tabInfo:p.tabInfo, rollerInfoKeys: Object.keys(p.rollerInfo||{}).length}))
};
const outDir = path.join(__dirname,'..','backups'); if (!fs.existsSync(outDir)) fs.mkdirSync(outDir,{recursive:true});
const fname = `db-summary.${new Date().toISOString().replace(/[:.]/g,'')}.json`;
const outPath = path.join(outDir,fname);
fs.writeFileSync(outPath, JSON.stringify(summary,null,2),'utf8');
console.log('Wrote summary to', outPath);
console.log('Summary:', JSON.stringify(summary, null, 2));
@@ -0,0 +1,35 @@
const fs = require('fs');
const path = require('path');
const bcrypt = require('bcrypt');
const { playerHelpers } = require('../sqlite-db');
(async function main(){
const names = ['andreas','chris'];
const backupsDir = path.join(__dirname, '..', 'backups');
if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true });
const ts = Date.now();
for (const name of names) {
const player = playerHelpers.getByName(name);
if (!player) {
console.log('Player not found, skipping:', name);
continue;
}
const beforePath = path.join(backupsDir, `${name}.pw.before.${ts}.json`);
fs.writeFileSync(beforePath, JSON.stringify(player, null, 2), 'utf8');
console.log('Backup written:', beforePath);
const plain = '1234';
const hash = await bcrypt.hash(plain, 10);
const ok = playerHelpers.update(name, { name, rollerInfo: player.rollerInfo || {}, shopInfo: player.shopInfo || {}, tabInfo: player.tabInfo || {}, pw: '', pwHash: hash });
if (!ok) {
console.error('Failed to update pw for', name);
continue;
}
const updated = playerHelpers.getByName(name);
const afterPath = path.join(backupsDir, `${name}.pw.after.${ts}.json`);
fs.writeFileSync(afterPath, JSON.stringify(updated, null, 2), 'utf8');
console.log('Updated player:', name, 'pwHash set. After backup:', afterPath);
console.log(JSON.stringify({ name: updated.name, pwHashPresent: !!updated.pwHash, _id: updated._id }, null, 2));
}
console.log('All done. Password for andreas and chris set to "1234" (hashed).');
})();