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:
@@ -718,6 +718,73 @@ function sumAttackModifiers(selectedIds, { targetSiz } = {}) {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// ---------- Personality Traits ----------
|
||||
// Traits are stored as { name, rating } where rating is 0-100%.
|
||||
// A trait roll succeeds when d100 <= rating, indicating the character acts on it.
|
||||
|
||||
const PERSONALITY_TRAITS = [
|
||||
'Brave', 'Cowardly', 'Honest', 'Deceitful', 'Generous', 'Greedy',
|
||||
'Loyal', 'Treacherous', 'Curious', 'Cautious', 'Reckless', 'Aggressive',
|
||||
'Peaceful', 'Patient', 'Impulsive', 'Proud', 'Vengeful', 'Forgiving',
|
||||
'Pious', 'Cynical', 'Suspicious', 'Trusting', 'Compassionate', 'Ruthless',
|
||||
];
|
||||
|
||||
// Bias each scene choice ID by ±% when this trait fires.
|
||||
// Positive = more inclined; negative = less inclined.
|
||||
const TRAIT_ACTION_BIAS = {
|
||||
Brave: { fight: +25, flee: -20 },
|
||||
Cowardly: { fight: -25, flee: +30, sneak: +15 },
|
||||
Aggressive: { fight: +30, talk: -15 },
|
||||
Peaceful: { fight: -20, talk: +20 },
|
||||
Curious: { investigate: +30 },
|
||||
Greedy: { investigate: +20 },
|
||||
Cautious: { investigate: +15, fight: -15, flee: +10 },
|
||||
Reckless: { fight: +20, flee: -25 },
|
||||
Deceitful: { talk: +25 },
|
||||
Honest: { talk: -10 },
|
||||
Suspicious: { talk: -15, investigate: +15 },
|
||||
Trusting: { talk: +15 },
|
||||
Loyal: { fight: +15, flee: -15 },
|
||||
Treacherous: { flee: +15 },
|
||||
Vengeful: { fight: +20 },
|
||||
Forgiving: { talk: +20, fight: -10 },
|
||||
Patient: { investigate: +20, sneak: +15 },
|
||||
Impulsive: { fight: +15, sneak: -10 },
|
||||
Proud: { flee: -20, fight: +10 },
|
||||
Ruthless: { fight: +15, flee: +10 },
|
||||
Compassionate:{ fight: -15, talk: +20 },
|
||||
Pious: { fight: +10 },
|
||||
Cynical: { talk: -10 },
|
||||
Generous: { talk: +10 },
|
||||
};
|
||||
|
||||
// Roll each trait against its rating. Returns { firedTraits, choiceBiases, suggested }.
|
||||
// choiceBiases: { choiceId → net bias } summed across all fired traits.
|
||||
// suggested: the choice ID with the highest net positive bias (null if none).
|
||||
function rollPersonalityAction(traits) {
|
||||
const fired = [];
|
||||
const biases = {};
|
||||
|
||||
for (const trait of (traits || [])) {
|
||||
const roll = rollPercentile();
|
||||
const success = roll <= trait.rating;
|
||||
if (success) {
|
||||
fired.push({ name: trait.name, roll, rating: trait.rating });
|
||||
const b = TRAIT_ACTION_BIAS[trait.name] || {};
|
||||
for (const [choiceId, delta] of Object.entries(b)) {
|
||||
biases[choiceId] = (biases[choiceId] || 0) + delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const suggested = Object.entries(biases).sort((a, b) => b[1] - a[1]).find(([, v]) => v > 0);
|
||||
return {
|
||||
firedTraits: fired,
|
||||
choiceBiases: biases,
|
||||
suggested: suggested ? suggested[0] : null,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- Character Culture & Cultural Weapon Bonuses ----------
|
||||
|
||||
const CHARACTER_CULTURES = [
|
||||
@@ -1074,6 +1141,678 @@ function buildStrikeRankSchedule(actions) {
|
||||
return schedule;
|
||||
}
|
||||
|
||||
// ---------- Age ----------
|
||||
|
||||
function rollAge() { return rollNotation('2d6') + 15; }
|
||||
|
||||
// ---------- Occupations ----------
|
||||
// skills entries: { key, mult, group? }
|
||||
// group flags OR-choice groups; the user picks exactly one from each group.
|
||||
// Weapon skill keys: 'attack:fist', 'attack:dagger', 'attack:primary', 'attack:missile',
|
||||
// 'attack:1hweapon', 'attack:2hweapon', 'attack:spear1h', 'attack:spear2h',
|
||||
// 'parry:weapon', 'parry:shield'
|
||||
// Ritual keys: 'ritual:ceremony', 'ritual:enchant', 'ritual:summon'
|
||||
// Craft keys: 'craft:leather', 'craft:stone', 'craft:wood'
|
||||
|
||||
const OCCUPATIONS = {
|
||||
Primitive: {
|
||||
fisher: {
|
||||
label: 'Fisher',
|
||||
skills: [
|
||||
{ key: 'boat', mult: 4 },
|
||||
{ key: 'swim', mult: 3 },
|
||||
{ key: 'firstAid', mult: 1 },
|
||||
{ key: 'listen', mult: 2 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 2 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 1 },
|
||||
equipment: 'Net or fishing spear, knife, canoe or coracle',
|
||||
},
|
||||
hunter: {
|
||||
label: 'Hunter',
|
||||
skills: [
|
||||
{ key: 'throw', mult: 3 },
|
||||
{ key: 'craft:leather', mult: 1 },
|
||||
{ key: 'craft:stone', mult: 1 },
|
||||
{ key: 'animalLore', mult: 2 },
|
||||
{ key: 'plantLore', mult: 2 },
|
||||
{ key: 'listen', mult: 3 },
|
||||
{ key: 'scan', mult: 3 },
|
||||
{ key: 'track', mult: 3 },
|
||||
{ key: 'hide', mult: 3 },
|
||||
{ key: 'sneak', mult: 4 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:missile', mult: 3 },
|
||||
{ key: 'attack:primary', mult: 2 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 1 },
|
||||
equipment: 'Primary weapon, missile weapon, knife, hide armor (2 AP)',
|
||||
},
|
||||
shaman: {
|
||||
label: 'Assistant Shaman',
|
||||
skills: [
|
||||
{ key: 'firstAid', mult: 2 },
|
||||
{ key: 'animalLore', mult: 3 },
|
||||
{ key: 'plantLore', mult: 3 },
|
||||
{ key: 'worldLore', mult: 2 },
|
||||
{ key: 'listen', mult: 2 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'ritual:ceremony', mult: 3 },
|
||||
{ key: 'ritual:summon', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 1 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 3, extraPerYears: 1 },
|
||||
equipment: "Shaman's fetish bundle, knife, hide or leather armor",
|
||||
},
|
||||
},
|
||||
|
||||
Nomad: {
|
||||
crafter: {
|
||||
label: 'Crafter',
|
||||
skills: [
|
||||
{ key: 'craft:leather', mult: 4 },
|
||||
{ key: 'craft:wood', mult: 2 },
|
||||
{ key: 'firstAid', mult: 1 },
|
||||
{ key: 'animalLore', mult: 2 },
|
||||
{ key: 'evaluate', mult: 2 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 1 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 1 },
|
||||
equipment: 'Craft tools, trade goods worth 100L, knife',
|
||||
},
|
||||
herder: {
|
||||
label: 'Herder',
|
||||
skills: [
|
||||
{ key: 'ride', mult: 4 },
|
||||
{ key: 'firstAid', mult: 1 },
|
||||
{ key: 'animalLore', mult: 4 },
|
||||
{ key: 'worldLore', mult: 1 },
|
||||
{ key: 'scan', mult: 3 },
|
||||
{ key: 'track', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 2 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 1 },
|
||||
equipment: 'Riding animal, lasso, primary weapon, knife',
|
||||
},
|
||||
hunter: {
|
||||
label: 'Hunter',
|
||||
skills: [
|
||||
{ key: 'ride', mult: 2 },
|
||||
{ key: 'throw', mult: 2 },
|
||||
{ key: 'animalLore', mult: 2 },
|
||||
{ key: 'plantLore', mult: 1 },
|
||||
{ key: 'worldLore', mult: 1 },
|
||||
{ key: 'listen', mult: 2 },
|
||||
{ key: 'scan', mult: 3 },
|
||||
{ key: 'track', mult: 3 },
|
||||
{ key: 'hide', mult: 2 },
|
||||
{ key: 'sneak', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:missile', mult: 3 },
|
||||
{ key: 'attack:primary', mult: 2 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 1 },
|
||||
equipment: 'Riding animal, missile weapon, primary weapon, knife',
|
||||
},
|
||||
noble: {
|
||||
label: 'Noble',
|
||||
skills: [
|
||||
{ key: 'ride', mult: 3 },
|
||||
{ key: 'orate', mult: 2 },
|
||||
{ key: 'humanLore', mult: 2 },
|
||||
{ key: 'worldLore', mult: 2 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'evaluate', mult: 2 },
|
||||
{ key: 'speakOwnLanguage', mult: 3 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:missile', mult: 2 },
|
||||
{ key: 'attack:primary', mult: 3 },
|
||||
{ key: 'parry:weapon', mult: 2, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 2, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 2, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 2 },
|
||||
equipment: 'Riding animal, primary weapon, shield or parry weapon, light armor',
|
||||
},
|
||||
shaman: {
|
||||
label: 'Assistant Shaman',
|
||||
skills: [
|
||||
{ key: 'firstAid', mult: 2 },
|
||||
{ key: 'animalLore', mult: 3 },
|
||||
{ key: 'plantLore', mult: 2 },
|
||||
{ key: 'worldLore', mult: 2 },
|
||||
{ key: 'listen', mult: 2 },
|
||||
{ key: 'ritual:ceremony', mult: 3 },
|
||||
{ key: 'ritual:summon', mult: 3 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 1 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 3, extraPerYears: 1 },
|
||||
equipment: "Shaman's fetish bundle, knife",
|
||||
},
|
||||
warrior: {
|
||||
label: 'Warrior',
|
||||
skills: [
|
||||
{ key: 'ride', mult: 3 },
|
||||
{ key: 'throw', mult: 2 },
|
||||
{ key: 'worldLore', mult: 1 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'track', mult: 1 },
|
||||
{ key: 'attack:fist', mult: 2 },
|
||||
{ key: 'attack:dagger', mult: 2 },
|
||||
{ key: 'attack:missile', mult: 2 },
|
||||
{ key: 'attack:primary', mult: 4 },
|
||||
{ key: 'parry:weapon', mult: 3, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 3, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 3, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 2 },
|
||||
equipment: 'Riding animal, primary weapon, parry weapon or shield, leather armor',
|
||||
},
|
||||
},
|
||||
|
||||
Barbarian: {
|
||||
crafter: {
|
||||
label: 'Crafter',
|
||||
skills: [
|
||||
{ key: 'craft:leather', mult: 2, group: 'craftPick' },
|
||||
{ key: 'craft:wood', mult: 2, group: 'craftPick' },
|
||||
{ key: 'craft:stone', mult: 2, group: 'craftPick' },
|
||||
{ key: 'firstAid', mult: 1 },
|
||||
{ key: 'animalLore', mult: 1 },
|
||||
{ key: 'evaluate', mult: 3 },
|
||||
{ key: 'scan', mult: 1 },
|
||||
{ key: 'speakOwnLanguage', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 1 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 1 },
|
||||
equipment: 'Craft tools, trade goods worth 150L, knife',
|
||||
},
|
||||
entertainer: {
|
||||
label: 'Entertainer',
|
||||
skills: [
|
||||
{ key: 'climb', mult: 1 },
|
||||
{ key: 'jump', mult: 1 },
|
||||
{ key: 'fastTalk', mult: 3 },
|
||||
{ key: 'orate', mult: 2 },
|
||||
{ key: 'sing', mult: 3 },
|
||||
{ key: 'humanLore', mult: 1 },
|
||||
{ key: 'conceal', mult: 1 },
|
||||
{ key: 'sleight', mult: 2 },
|
||||
{ key: 'scan', mult: 1 },
|
||||
{ key: 'speakOwnLanguage', mult: 2 },
|
||||
{ key: 'speakOtherLanguage', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 1 },
|
||||
equipment: 'Costume, instrument or juggling props, knife',
|
||||
},
|
||||
farmer: {
|
||||
label: 'Farmer',
|
||||
skills: [
|
||||
{ key: 'firstAid', mult: 1 },
|
||||
{ key: 'animalLore', mult: 3 },
|
||||
{ key: 'plantLore', mult: 3 },
|
||||
{ key: 'worldLore', mult: 1 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'track', mult: 1 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 2 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 1 },
|
||||
equipment: 'Farm tools, knife, primary weapon',
|
||||
},
|
||||
fisher: {
|
||||
label: 'Fisher',
|
||||
skills: [
|
||||
{ key: 'boat', mult: 4 },
|
||||
{ key: 'swim', mult: 3 },
|
||||
{ key: 'firstAid', mult: 1 },
|
||||
{ key: 'listen', mult: 2 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 2 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 1 },
|
||||
equipment: 'Fishing net or harpoon, knife, small boat',
|
||||
},
|
||||
herder: {
|
||||
label: 'Herder',
|
||||
skills: [
|
||||
{ key: 'firstAid', mult: 1 },
|
||||
{ key: 'animalLore', mult: 4 },
|
||||
{ key: 'plantLore', mult: 2 },
|
||||
{ key: 'worldLore', mult: 1 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'track', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 2 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 1 },
|
||||
equipment: 'Primary weapon, knife, herd of 2d6+6 animals',
|
||||
},
|
||||
hunter: {
|
||||
label: 'Hunter',
|
||||
skills: [
|
||||
{ key: 'throw', mult: 2 },
|
||||
{ key: 'animalLore', mult: 2 },
|
||||
{ key: 'plantLore', mult: 2 },
|
||||
{ key: 'listen', mult: 2 },
|
||||
{ key: 'scan', mult: 3 },
|
||||
{ key: 'track', mult: 3 },
|
||||
{ key: 'hide', mult: 2 },
|
||||
{ key: 'sneak', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:missile', mult: 3 },
|
||||
{ key: 'attack:primary', mult: 2 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 1 },
|
||||
equipment: 'Missile weapon, primary weapon, knife, light armor',
|
||||
},
|
||||
noble: {
|
||||
label: 'Noble',
|
||||
skills: [
|
||||
{ key: 'ride', mult: 2 },
|
||||
{ key: 'orate', mult: 3 },
|
||||
{ key: 'humanLore', mult: 2 },
|
||||
{ key: 'worldLore', mult: 2 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'evaluate', mult: 1 },
|
||||
{ key: 'speakOwnLanguage', mult: 3 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:missile', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 3 },
|
||||
{ key: 'parry:weapon', mult: 2, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 2, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 2, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 2 },
|
||||
equipment: 'Primary weapon, shield or parry weapon, light to medium armor, riding animal',
|
||||
},
|
||||
warrior: {
|
||||
label: 'Warrior',
|
||||
skills: [
|
||||
{ key: 'throw', mult: 2 },
|
||||
{ key: 'worldLore', mult: 1 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 2 },
|
||||
{ key: 'attack:dagger', mult: 2 },
|
||||
{ key: 'attack:missile', mult: 2 },
|
||||
{ key: 'attack:spear1h', mult: 3, group: 'weaponStyle' },
|
||||
{ key: 'parry:shield', mult: 3, group: 'weaponStyle' },
|
||||
{ key: 'attack:2hweapon', mult: 3, group: 'weaponStyle' },
|
||||
{ key: 'parry:weapon', mult: 3, group: 'weaponStyle' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 2 },
|
||||
equipment: 'Primary weapon, shield or two-handed weapon, medium armor',
|
||||
},
|
||||
shaman: {
|
||||
label: 'Assistant Shaman',
|
||||
skills: [
|
||||
{ key: 'firstAid', mult: 2 },
|
||||
{ key: 'animalLore', mult: 3 },
|
||||
{ key: 'plantLore', mult: 2 },
|
||||
{ key: 'worldLore', mult: 2 },
|
||||
{ key: 'listen', mult: 2 },
|
||||
{ key: 'ritual:ceremony', mult: 3 },
|
||||
{ key: 'ritual:summon', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 1 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'spirit', basePoints: 3, extraPerYears: 1 },
|
||||
equipment: "Shaman's fetish bundle, knife",
|
||||
},
|
||||
},
|
||||
|
||||
Civilized: {
|
||||
crafter: {
|
||||
label: 'Crafter',
|
||||
skills: [
|
||||
{ key: 'craft:leather', mult: 4, group: 'craftPick' },
|
||||
{ key: 'craft:wood', mult: 4, group: 'craftPick' },
|
||||
{ key: 'craft:stone', mult: 4, group: 'craftPick' },
|
||||
{ key: 'devise', mult: 2 },
|
||||
{ key: 'evaluate', mult: 3 },
|
||||
{ key: 'speakOwnLanguage', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'divine_or_sorcery', basePoints: 1 },
|
||||
equipment: 'Craft tools, workshop access, knife',
|
||||
},
|
||||
entertainer: {
|
||||
label: 'Entertainer',
|
||||
skills: [
|
||||
{ key: 'climb', mult: 1 },
|
||||
{ key: 'jump', mult: 1 },
|
||||
{ key: 'fastTalk', mult: 3 },
|
||||
{ key: 'orate', mult: 3 },
|
||||
{ key: 'sing', mult: 3 },
|
||||
{ key: 'humanLore', mult: 1 },
|
||||
{ key: 'conceal', mult: 1 },
|
||||
{ key: 'sleight', mult: 2 },
|
||||
{ key: 'scan', mult: 1 },
|
||||
{ key: 'speakOwnLanguage', mult: 2 },
|
||||
{ key: 'speakOtherLanguage', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'divine_or_sorcery', basePoints: 1 },
|
||||
equipment: 'Costume, instrument or props, knife',
|
||||
},
|
||||
farmer: {
|
||||
label: 'Farmer',
|
||||
skills: [
|
||||
{ key: 'firstAid', mult: 1 },
|
||||
{ key: 'animalLore', mult: 3 },
|
||||
{ key: 'plantLore', mult: 4 },
|
||||
{ key: 'worldLore', mult: 1 },
|
||||
{ key: 'scan', mult: 1 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 2 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'divine', basePoints: 1 },
|
||||
equipment: 'Farm tools, primary weapon, knife',
|
||||
},
|
||||
healer: {
|
||||
label: 'Healer',
|
||||
skills: [
|
||||
{ key: 'firstAid', mult: 5 },
|
||||
{ key: 'animalLore', mult: 1 },
|
||||
{ key: 'humanLore', mult: 3 },
|
||||
{ key: 'plantLore', mult: 3 },
|
||||
{ key: 'listen', mult: 1 },
|
||||
{ key: 'search', mult: 2 },
|
||||
{ key: 'speakOwnLanguage', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'divine', basePoints: 3 },
|
||||
equipment: "Healer's kit, knife, 2d6×10L",
|
||||
},
|
||||
herder: {
|
||||
label: 'Herder',
|
||||
skills: [
|
||||
{ key: 'firstAid', mult: 1 },
|
||||
{ key: 'animalLore', mult: 5 },
|
||||
{ key: 'plantLore', mult: 1 },
|
||||
{ key: 'worldLore', mult: 1 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'track', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 1 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'divine', basePoints: 1 },
|
||||
equipment: 'Primary weapon, knife, herd',
|
||||
},
|
||||
merchant: {
|
||||
label: 'Merchant',
|
||||
skills: [
|
||||
{ key: 'ride', mult: 1 },
|
||||
{ key: 'fastTalk', mult: 2 },
|
||||
{ key: 'orate', mult: 1 },
|
||||
{ key: 'humanLore', mult: 2 },
|
||||
{ key: 'worldLore', mult: 2 },
|
||||
{ key: 'evaluate', mult: 5 },
|
||||
{ key: 'speakOwnLanguage', mult: 2 },
|
||||
{ key: 'speakOtherLanguage', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'divine_or_sorcery', basePoints: 1 },
|
||||
equipment: 'Pack animal or cart, trade goods worth 500L, knife',
|
||||
},
|
||||
noble: {
|
||||
label: 'Noble',
|
||||
skills: [
|
||||
{ key: 'ride', mult: 2 },
|
||||
{ key: 'fastTalk', mult: 1 },
|
||||
{ key: 'orate', mult: 3 },
|
||||
{ key: 'humanLore', mult: 2 },
|
||||
{ key: 'worldLore', mult: 2 },
|
||||
{ key: 'evaluate', mult: 2 },
|
||||
{ key: 'speakOwnLanguage', mult: 3 },
|
||||
{ key: 'speakOtherLanguage', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'attack:primary', mult: 3 },
|
||||
{ key: 'parry:weapon', mult: 2, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 2, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 2, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'divine', basePoints: 2 },
|
||||
equipment: 'Primary weapon, shield, medium to heavy armor, riding animal',
|
||||
},
|
||||
sailor: {
|
||||
label: 'Sailor',
|
||||
skills: [
|
||||
{ key: 'boat', mult: 5 },
|
||||
{ key: 'climb', mult: 2 },
|
||||
{ key: 'swim', mult: 3 },
|
||||
{ key: 'worldLore', mult: 1 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'speakOtherLanguage', mult: 1 },
|
||||
{ key: 'attack:fist', mult: 2 },
|
||||
{ key: 'attack:dagger', mult: 2 },
|
||||
{ key: 'attack:primary', mult: 2 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'divine', basePoints: 1 },
|
||||
equipment: "Primary weapon, knife, sailor's gear",
|
||||
},
|
||||
scribe: {
|
||||
label: 'Scribe',
|
||||
skills: [
|
||||
{ key: 'devise', mult: 1 },
|
||||
{ key: 'humanLore', mult: 3 },
|
||||
{ key: 'mineralLore', mult: 1 },
|
||||
{ key: 'worldLore', mult: 3 },
|
||||
{ key: 'search', mult: 2 },
|
||||
{ key: 'evaluate', mult: 2 },
|
||||
{ key: 'speakOwnLanguage', mult: 3 },
|
||||
{ key: 'speakOtherLanguage', mult: 3 },
|
||||
{ key: 'attack:fist', mult: 1 },
|
||||
{ key: 'attack:dagger', mult: 1 },
|
||||
{ key: 'parry:weapon', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'parry:shield', mult: 1, group: 'parryChoice' },
|
||||
{ key: 'dodge', mult: 1, group: 'parryChoice' },
|
||||
],
|
||||
magic: { type: 'divine_or_sorcery', basePoints: 2 },
|
||||
equipment: 'Writing materials, knife, access to library',
|
||||
},
|
||||
soldier: {
|
||||
label: 'Soldier',
|
||||
skills: [
|
||||
{ key: 'firstAid', mult: 1 },
|
||||
{ key: 'worldLore', mult: 1 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'attack:fist', mult: 2 },
|
||||
{ key: 'attack:dagger', mult: 2 },
|
||||
{ key: 'attack:missile', mult: 2 },
|
||||
{ key: 'attack:spear1h', mult: 3, group: 'weaponStyle' },
|
||||
{ key: 'parry:shield', mult: 3, group: 'weaponStyle' },
|
||||
{ key: 'attack:2hweapon', mult: 3, group: 'weaponStyle' },
|
||||
{ key: 'parry:weapon', mult: 3, group: 'weaponStyle' },
|
||||
],
|
||||
magic: { type: 'divine', basePoints: 1 },
|
||||
equipment: 'Primary weapon, shield or two-handed weapon, medium armor',
|
||||
},
|
||||
thief: {
|
||||
label: 'Thief',
|
||||
skills: [
|
||||
{ key: 'climb', mult: 3 },
|
||||
{ key: 'jump', mult: 2 },
|
||||
{ key: 'dodge', mult: 2 },
|
||||
{ key: 'fastTalk', mult: 2 },
|
||||
{ key: 'conceal', mult: 3 },
|
||||
{ key: 'sleight', mult: 3 },
|
||||
{ key: 'devise', mult: 3 },
|
||||
{ key: 'listen', mult: 2 },
|
||||
{ key: 'scan', mult: 2 },
|
||||
{ key: 'search', mult: 2 },
|
||||
{ key: 'hide', mult: 3 },
|
||||
{ key: 'sneak', mult: 3 },
|
||||
{ key: 'attack:fist', mult: 2 },
|
||||
{ key: 'attack:dagger', mult: 3 },
|
||||
{ key: 'attack:missile', mult: 1 },
|
||||
],
|
||||
magic: { type: 'divine_or_sorcery', basePoints: 1 },
|
||||
equipment: 'Dagger, climbing gear, lockpicks, dark clothing',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ---------- computeOccupationSkills ----------
|
||||
// Returns:
|
||||
// skills: {} general skill key → total % (base + category mod + occ bonus)
|
||||
// craftBonuses: {} craft key → occupation bonus (years * mult)
|
||||
// ritualBonuses: {} ritual key → occupation bonus
|
||||
// weaponBonuses: {} weapon key → occupation bonus
|
||||
// choiceGroups: {} groupName → [{ key, mult, bonus, total?, note }]
|
||||
// occupation: {} the occupation object
|
||||
|
||||
function computeOccupationSkills(chars, culture, occupationKey, years) {
|
||||
const occ = (OCCUPATIONS[culture] || {})[occupationKey];
|
||||
if (!occ) throw new Error(`Unknown occupation "${occupationKey}" for culture "${culture}"`);
|
||||
|
||||
const mods = computeSkillCategoryModifiers(chars);
|
||||
const baseSkills = computeBaseSkills(chars);
|
||||
|
||||
const skills = {};
|
||||
const craftBonuses = {};
|
||||
const ritualBonuses = {};
|
||||
const weaponBonuses = {};
|
||||
const choiceGroups = {};
|
||||
|
||||
// Extended base values for skills not in BASE_SKILLS
|
||||
function extendedBase(key) {
|
||||
if (key in baseSkills) return baseSkills[key];
|
||||
if (key === 'evaluate') return Math.max(0, 5 + (mods.knowledge || 0));
|
||||
if (key === 'speakOwnLanguage') return 30;
|
||||
if (key === 'speakOtherLanguage') return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const entry of occ.skills) {
|
||||
const { key, mult, group } = entry;
|
||||
const bonus = years * mult;
|
||||
|
||||
if (group) {
|
||||
if (!choiceGroups[group]) choiceGroups[group] = [];
|
||||
if (key.startsWith('attack:') || key.startsWith('parry:')) {
|
||||
choiceGroups[group].push({ key, mult, bonus, note: 'weapon' });
|
||||
} else if (key.startsWith('craft:')) {
|
||||
choiceGroups[group].push({ key, mult, bonus, note: 'craft' });
|
||||
} else {
|
||||
const base = extendedBase(key);
|
||||
choiceGroups[group].push({ key, mult, bonus, total: Math.max(0, base + bonus), note: 'skill' });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Non-grouped entries
|
||||
if (key.startsWith('attack:') || key.startsWith('parry:')) {
|
||||
weaponBonuses[key] = (weaponBonuses[key] || 0) + bonus;
|
||||
} else if (key.startsWith('craft:')) {
|
||||
craftBonuses[key] = (craftBonuses[key] || 0) + bonus;
|
||||
} else if (key.startsWith('ritual:')) {
|
||||
ritualBonuses[key] = (ritualBonuses[key] || 0) + bonus;
|
||||
} else {
|
||||
const base = extendedBase(key);
|
||||
skills[key] = Math.max(0, (skills[key] !== undefined ? skills[key] : base) + bonus);
|
||||
}
|
||||
}
|
||||
|
||||
return { skills, craftBonuses, ritualBonuses, weaponBonuses, choiceGroups, occupation: occ };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
rollDie,
|
||||
rollDice,
|
||||
@@ -1091,6 +1830,12 @@ module.exports = {
|
||||
computeBaseSkills,
|
||||
SCENE_CHOICES,
|
||||
resolveSceneChoice,
|
||||
PERSONALITY_TRAITS,
|
||||
TRAIT_ACTION_BIAS,
|
||||
rollPersonalityAction,
|
||||
rollAge,
|
||||
OCCUPATIONS,
|
||||
computeOccupationSkills,
|
||||
dexStrikeRank,
|
||||
sizStrikeRankModifier,
|
||||
baseStrikeRank,
|
||||
|
||||
Reference in New Issue
Block a user