feat: full character creation, personality traits, and session quality-of-life
Character creation: - Full RQ3 Previous Experience wizard (4-step: identity, characteristics, culture/occupation/skills, review+save) with all 29 occupations across 4 cultures; skills computed as base + category modifier + years × multiplier - Character sheet expanded to show culture, occupation, derived stats (HP/FP/MP/ DB/SR), all 7 skill category modifier badges, computed skill percentages grouped by category, weapon attack/parry %, hit locations with armour AP - Location/destination tracker on character sheet (auto-saves on blur) - parry_percent stored on combatant_weapons Personality traits: - 24 predefined traits (Brave, Greedy, Cautious, etc.) each rated 0-100% - Trait → action bias map drives scene choice weighting - rollPersonalityAction() rolls d100 per trait, sums biases, returns suggestion - Trait editor (pill UI) on both character sheet and NPC view - Adventure tab: Roll Personality button fires traits, highlights suggested choice Tables and data: - Import RQ3 character creation tables (Culture d8, Occupation d100 ×4, Craft sub-tables, Language Proficiency, Dropped Oil Lamp, Aging, Armor Points) - Cross-table links: Culture → Occupation, Barbarian/Civilized Crafter → Craft - Fix rollOnTable to use actual dice notation instead of flat random row index - Remove unused resolveHeadInjury function Session tools: - Clear All button wipes all session data (characters, NPCs, enemies, combat, adventure, log) while keeping tables and spell mappings - PATCH /api/characters/:id/traits and /api/npcs/:id/traits endpoints - GET /api/rules/personality-traits reference endpoint - POST /api/adventure/personality-roll endpoint
This commit is contained in:
@@ -245,11 +245,11 @@ function setHitLocations(statBlockId, locations) {
|
||||
function setWeapons(statBlockId, weapons) {
|
||||
db.prepare('DELETE FROM combatant_weapons WHERE stat_block_id = ?').run(statBlockId);
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO combatant_weapons (stat_block_id, weapon_name, category, skill_percent, mode, experience_checked)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO combatant_weapons (stat_block_id, weapon_name, category, skill_percent, parry_percent, mode, experience_checked)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
for (const w of weapons) {
|
||||
stmt.run(statBlockId, w.weapon_name, w.category ?? null, w.skill_percent ?? 0, w.mode ?? null, w.experience_checked ? 1 : 0);
|
||||
stmt.run(statBlockId, w.weapon_name, w.category ?? null, w.skill_percent ?? 0, w.parry_percent ?? 0, w.mode ?? null, w.experience_checked ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,16 +279,25 @@ const NPC_FIELDS = [
|
||||
'secret_obstacle', 'also_carrying', 'race', 'pronouns', 'age', 'intelligence', 'hair', 'build', 'status',
|
||||
];
|
||||
|
||||
function parseTraits(row) {
|
||||
return { ...row, traits: row.traits_json ? JSON.parse(row.traits_json) : [] };
|
||||
}
|
||||
|
||||
function listNpcs() {
|
||||
const npcs = db.prepare('SELECT * FROM npcs ORDER BY id DESC').all();
|
||||
return npcs.map((n) => ({ ...n, stat_block: n.stat_block_id ? getStatBlock(n.stat_block_id) : null }));
|
||||
return npcs.map((n) => ({ ...parseTraits(n), stat_block: n.stat_block_id ? getStatBlock(n.stat_block_id) : null }));
|
||||
}
|
||||
|
||||
function getNpc(id) {
|
||||
const npc = db.prepare('SELECT * FROM npcs WHERE id = ?').get(id);
|
||||
if (!npc) return null;
|
||||
npc.stat_block = npc.stat_block_id ? getStatBlock(npc.stat_block_id) : null;
|
||||
return npc;
|
||||
return { ...parseTraits(npc), stat_block: npc.stat_block_id ? getStatBlock(npc.stat_block_id) : null };
|
||||
}
|
||||
|
||||
function updateNpcTraits(id, traits) {
|
||||
db.prepare(`UPDATE npcs SET traits_json=?, updated_at=datetime('now') WHERE id=?`)
|
||||
.run(JSON.stringify(traits), id);
|
||||
return getNpc(id);
|
||||
}
|
||||
|
||||
function createNpc(data) {
|
||||
@@ -432,25 +441,38 @@ function clearAdventureState() {
|
||||
db.prepare('DELETE FROM adventure_state WHERE id = 1').run();
|
||||
}
|
||||
|
||||
for (const sql of [
|
||||
'ALTER TABLE player_characters ADD COLUMN age INTEGER NOT NULL DEFAULT 21',
|
||||
'ALTER TABLE player_characters ADD COLUMN culture TEXT',
|
||||
'ALTER TABLE player_characters ADD COLUMN occupation TEXT',
|
||||
'ALTER TABLE player_characters ADD COLUMN current_location TEXT',
|
||||
'ALTER TABLE player_characters ADD COLUMN destination TEXT',
|
||||
'ALTER TABLE combatant_weapons ADD COLUMN parry_percent INTEGER NOT NULL DEFAULT 0',
|
||||
'ALTER TABLE player_characters ADD COLUMN traits_json TEXT',
|
||||
'ALTER TABLE npcs ADD COLUMN traits_json TEXT',
|
||||
]) {
|
||||
try { db.exec(sql); } catch (_) {}
|
||||
}
|
||||
|
||||
// ---------- player characters ----------
|
||||
|
||||
function listPlayerCharacters() {
|
||||
const rows = db.prepare('SELECT * FROM player_characters ORDER BY id DESC').all();
|
||||
return rows.map((r) => ({ ...r, stat_block: getStatBlock(r.stat_block_id) }));
|
||||
return rows.map((r) => ({ ...parseTraits(r), stat_block: getStatBlock(r.stat_block_id) }));
|
||||
}
|
||||
|
||||
function getPlayerCharacter(id) {
|
||||
const row = db.prepare('SELECT * FROM player_characters WHERE id = ?').get(id);
|
||||
if (!row) return null;
|
||||
return { ...row, stat_block: getStatBlock(row.stat_block_id) };
|
||||
return { ...parseTraits(row), stat_block: getStatBlock(row.stat_block_id) };
|
||||
}
|
||||
|
||||
function createPlayerCharacter({ name, generation_method, stat_block }) {
|
||||
function createPlayerCharacter({ name, generation_method, stat_block, age, culture, occupation }) {
|
||||
const statBlockId = createStatBlock(stat_block || {}).id;
|
||||
const info = db.prepare(`
|
||||
INSERT INTO player_characters (name, generation_method, stat_block_id)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(name, generation_method ?? 'random', statBlockId);
|
||||
INSERT INTO player_characters (name, generation_method, stat_block_id, age, culture, occupation)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(name, generation_method ?? 'random', statBlockId, age ?? 21, culture ?? null, occupation ?? null);
|
||||
return getPlayerCharacter(info.lastInsertRowid);
|
||||
}
|
||||
|
||||
@@ -462,6 +484,19 @@ function deletePlayerCharacter(id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function updatePlayerCharacterTraits(id, traits) {
|
||||
db.prepare(`UPDATE player_characters SET traits_json=?, updated_at=datetime('now') WHERE id=?`)
|
||||
.run(JSON.stringify(traits), id);
|
||||
return getPlayerCharacter(id);
|
||||
}
|
||||
|
||||
function updatePlayerCharacterLocation(id, { current_location, destination }) {
|
||||
db.prepare(`
|
||||
UPDATE player_characters SET current_location=?, destination=?, updated_at=datetime('now') WHERE id=?
|
||||
`).run(current_location ?? null, destination ?? null, id);
|
||||
return getPlayerCharacter(id);
|
||||
}
|
||||
|
||||
// ---------- log ----------
|
||||
|
||||
function appendLogEntry({ type, summary, details }) {
|
||||
@@ -645,6 +680,18 @@ function exportAll() {
|
||||
};
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
db.transaction(() => {
|
||||
db.prepare('DELETE FROM adventure_state').run(); // references player_characters
|
||||
db.prepare('DELETE FROM player_characters').run(); // cascades inventory_items
|
||||
db.prepare('DELETE FROM npcs').run();
|
||||
db.prepare('DELETE FROM enemies').run();
|
||||
db.prepare('DELETE FROM stat_blocks').run(); // cascades hit_locations, weapons, spells
|
||||
db.prepare('DELETE FROM log_entries').run();
|
||||
db.prepare('DELETE FROM combat_state').run();
|
||||
})();
|
||||
}
|
||||
|
||||
function importAll(dump) {
|
||||
const tx = db.transaction(() => {
|
||||
db.prepare('DELETE FROM npcs').run();
|
||||
@@ -689,9 +736,9 @@ module.exports = {
|
||||
markWeaponExperienceChecked,
|
||||
updateWeaponSkillPercent,
|
||||
},
|
||||
npcs: { list: listNpcs, get: getNpc, create: createNpc, update: updateNpc, delete: deleteNpc, linkStatBlock: linkNpcStatBlock },
|
||||
npcs: { list: listNpcs, get: getNpc, create: createNpc, update: updateNpc, delete: deleteNpc, linkStatBlock: linkNpcStatBlock, updateTraits: updateNpcTraits },
|
||||
enemies: { list: listEnemies, get: getEnemy, create: createEnemy, update: updateEnemy, delete: deleteEnemy },
|
||||
playerCharacters: { list: listPlayerCharacters, get: getPlayerCharacter, create: createPlayerCharacter, delete: deletePlayerCharacter },
|
||||
playerCharacters: { list: listPlayerCharacters, get: getPlayerCharacter, create: createPlayerCharacter, delete: deletePlayerCharacter, updateLocation: updatePlayerCharacterLocation, updateTraits: updatePlayerCharacterTraits },
|
||||
inventory: { list: listInventory, add: addInventoryItem, update: updateInventoryItem, delete: deleteInventoryItem, totalEnc: inventoryTotalEnc },
|
||||
adventure: { get: getAdventureState, set: setAdventureState, clear: clearAdventureState },
|
||||
log: { append: appendLogEntry, get: getLogEntry, search: searchLog },
|
||||
@@ -712,6 +759,7 @@ module.exports = {
|
||||
getRowsForTable,
|
||||
},
|
||||
spellMappings: { list: listSpellMappings, upsert: upsertSpellMapping },
|
||||
clearAll,
|
||||
exportAll,
|
||||
importAll,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user