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:
2026-07-03 21:44:11 +10:00
parent 8bccf6f484
commit 5e83fe1ef2
8 changed files with 2263 additions and 254 deletions
+49
View File
@@ -107,6 +107,54 @@ function resolveLayer1Links() {
}
}
const CULTURE_TABLE_LINKS = [
[/primitive/i, 'Primitive Occupation Table (d100)'],
[/nomad/i, 'Nomad Occupation Table (d100)'],
[/barbarian/i, 'Barbarian Occupation Table (d100)'],
[/civilized/i, 'Civilized Occupation Table (d100)'],
];
const OCCUPATION_CRAFTER_LINKS = [
['Barbarian Occupation Table (d100)', 'Barbarian Craft Table (d100)'],
['Civilized Occupation Table (d100)', 'Urban Crafts Table (d100)'],
];
function resolveCharacterTableLinks() {
const sourceFile = 'rq3_character_tables.md';
// Culture Table → Occupation Tables
const cultureTable = dbApi.tables.findTableByHeading(sourceFile, 'Culture Table (d8)');
if (cultureTable) {
const rows = dbApi.tables.getRowsForTable(cultureTable.id);
for (const row of rows) {
const text = row.cells[0] || '';
const match = CULTURE_TABLE_LINKS.find(([pattern]) => pattern.test(text));
if (!match) continue;
const [, targetHeading] = match;
const target = dbApi.tables.findTableByHeading(sourceFile, targetHeading);
if (!target) { console.warn(`Culture link target not found: "${targetHeading}"`); continue; }
dbApi.tables.insertLink({ table_id: cultureTable.id, row_id: row.id, target_table_id: target.id });
stats.links++;
}
} else {
console.warn('Could not find Culture Table for link resolution');
}
// Occupation Tables → Craft Sub-tables (Crafter rows)
for (const [occHeading, craftHeading] of OCCUPATION_CRAFTER_LINKS) {
const occTable = dbApi.tables.findTableByHeading(sourceFile, occHeading);
const craftTable = dbApi.tables.findTableByHeading(sourceFile, craftHeading);
if (!occTable || !craftTable) continue;
const rows = dbApi.tables.getRowsForTable(occTable.id);
for (const row of rows) {
if (/^crafter$/i.test((row.cells[0] || '').trim())) {
dbApi.tables.insertLink({ table_id: occTable.id, row_id: row.id, target_table_id: craftTable.id });
stats.links++;
}
}
}
}
function main() {
dbApi.tables.clearImported();
@@ -116,6 +164,7 @@ function main() {
}
resolveLayer1Links();
resolveCharacterTableLinks();
console.log('---');
console.log(`Files: ${stats.files}`);