574 lines
16 KiB
JavaScript
Executable File
574 lines
16 KiB
JavaScript
Executable File
// ____ _ _
|
|
// / ___| |__ __ _ _ __ __ _ ___| |_ ___ _ __
|
|
// | | | '_ \ / _` | '__/ _` |/ __| __/ _ \ '__|
|
|
// | |___| | | | (_| | | | (_| | (__| || __/ |
|
|
// \____|_| |_|\__,_|_| \__,_|\___|\__\___|_|
|
|
// ____ _
|
|
// / ___| ___ ___ __| | ___ _ __
|
|
// \___ \ / _ \/ _ \/ _` |/ _ \ '__|
|
|
// ___) | __/ __/ (_| | __/ |
|
|
// |____/ \___|\___|\__,_|\___|_|
|
|
// ------------------------------------------------
|
|
import { Character } from "../models/character.js";
|
|
import { gGame } from "../models/globals.js";
|
|
import { Player } from "../models/player.js";
|
|
import { isIdSane } from "../utils/id.js";
|
|
|
|
// stupid convenience hack that only works if we only have a single Game in the system.
|
|
// Which we easily could have.!!
|
|
let roll = {};
|
|
|
|
export class CharacterSeeder {
|
|
constructor() {
|
|
// stupid hack that ensures we populate roll AFTER gGame is available
|
|
// Which we easily could have.!!
|
|
roll.d = (max, min = 1) => gGame.random.within(min, max)
|
|
roll.d6 = () => roll.d(6)
|
|
roll.d8 = () => roll.d(8)
|
|
}
|
|
|
|
/**
|
|
* @param {Character} character
|
|
* @param {...string} itemBlueprintIds
|
|
*/
|
|
addItemsToCharacter(character, ...itemBlueprintIds) {
|
|
for (const id of itemBlueprintIds) {
|
|
const blueprint = gGame.getItemBlueprint(id);
|
|
if (!blueprint) {
|
|
throw new Error(`No blueprint found for id: ${id}`);
|
|
}
|
|
const item = blueprint.createItem();
|
|
character.addItem(item);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Character} character
|
|
* @param {...string} proficiencies
|
|
*/
|
|
addProficienciesToCharacter(character, ...proficiencies) {
|
|
for (const prof of proficiencies) {
|
|
if (!isIdSane(prof)) {
|
|
throw new Error(`Proficiency id >>${prof}<< is insane!`);
|
|
}
|
|
character.proficiencies.add(prof);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Foundation function
|
|
* @name FoundationFunction
|
|
* @function
|
|
* @param {Character} The character to which we apply this foundation.
|
|
*/
|
|
|
|
createCharacter() {
|
|
const c = new Character();
|
|
|
|
this.generateName(c);
|
|
this.rollSkills(c);
|
|
this.applyAncestry(c);
|
|
this.applyFoundation(c);
|
|
|
|
return c;
|
|
}
|
|
|
|
generateName(c) {
|
|
/** @todo use actual random name generator */
|
|
c.name =
|
|
gGame.random.oneOf("sir ", "madam ", "mister ", "miss ", "", "", "") +
|
|
"random " + // name
|
|
gGame.random.next().toString();
|
|
}
|
|
|
|
rollSkills(c) {
|
|
c.awareness = roll.d6() + 2;
|
|
c.grit = roll.d6() + 2;
|
|
c.knowledge = roll.d6() + 2;
|
|
c.magic = roll.d6() + 2;
|
|
c.meleeCombat = roll.d6() + 2;
|
|
c.rangedCombat = roll.d6() + 2;
|
|
c.skulduggery = roll.d6() + 2;
|
|
}
|
|
|
|
applyAncestry(c) {
|
|
let ancestryId = roll.d8();
|
|
switch (ancestryId) {
|
|
case 1:
|
|
c.ancestry = "human";
|
|
// Humans get +1 to all skills
|
|
c.awareness++;
|
|
c.grit++;
|
|
c.knowledge++;
|
|
c.magic++;
|
|
c.meleeCombat++;
|
|
c.rangedCombat++;
|
|
c.skulduggery++;
|
|
break;
|
|
case 2:
|
|
c.ancestry = "dwarven";
|
|
c.meleeCombat = Math.max(c.meleeCombat, 10);
|
|
break;
|
|
case 3:
|
|
c.ancestry = "elven";
|
|
c.rangedCombat = Math.max(c.rangedCombat, 10);
|
|
break;
|
|
case 4:
|
|
c.ancestry = "giant";
|
|
c.meleeCombat = Math.max(c.grit, 10);
|
|
break;
|
|
case 5:
|
|
c.ancestry = "gnomish";
|
|
c.meleeCombat = Math.max(c.awareness, 10);
|
|
break;
|
|
case 6:
|
|
c.ancestry = "primordial";
|
|
c.meleeCombat = Math.max(c.magic, 10);
|
|
break;
|
|
case 7:
|
|
c.ancestry = "draconic";
|
|
c.meleeCombat = Math.max(c.knowledge, 10);
|
|
break;
|
|
case 8:
|
|
c.ancestry = "demonic";
|
|
c.meleeCombat = Math.max(c.skulduggery, 10);
|
|
break;
|
|
default:
|
|
throw new Error(`Logic error, ancestry d8() roll of ${ancestryId} was out of scope"`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create characters for the given player
|
|
*
|
|
* The characters are automatically added to the player's party
|
|
*
|
|
* @param {Player} player
|
|
* @param {number} partySize
|
|
*
|
|
* @return {Character[]}
|
|
*/
|
|
createParty(player, partySize) {
|
|
//
|
|
for (let i = 0; i < partySize; i++) {
|
|
player.addCharacter(
|
|
this.createCharacter(player), //
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Character} c
|
|
* @param {string|number} Foundation to add to character
|
|
*/
|
|
applyFoundation(c, foundation = ":random") {
|
|
if (foundation == ":random") {
|
|
return this.applyFoundation(c, roll.d(20)); // according to the rulebook, roll a d20 and reroll any invalid results.
|
|
}
|
|
|
|
//
|
|
// Brawler
|
|
// ------
|
|
if (foundation === 1 || foundation === ":brawler") {
|
|
c.foundation = "Brawler";
|
|
|
|
c.maxHitPoints = c.currentHitPoints = 15;
|
|
c.itemSlots = 7;
|
|
c.silver = 40;
|
|
|
|
c.meleeCombat = Math.max(c.meleeCombat, 10);
|
|
c.knowledge = Math.min(c.knowledge, 10);
|
|
|
|
this.addProficienciesToCharacter(
|
|
c,
|
|
":armor.light",
|
|
":weapon.weird.spiked_gauntlets"
|
|
);
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
":armor.light.studded_leather",
|
|
":weapon.weird.spiked_gauntlets",
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
//
|
|
// DRUID
|
|
// ------
|
|
if (foundation === 2 || foundation === ":druid") {
|
|
|
|
c.foundation = "Druid";
|
|
|
|
c.silver = 10;
|
|
c.itemSlots = 5;
|
|
c.maxHitPoints = this.currentHitPoints = 10;
|
|
|
|
this.addProficienciesToCharacter(
|
|
c, //
|
|
":armor.light.cloth",
|
|
":armor.light.hide",
|
|
":armor.light.leather",
|
|
":kit.healers_kit",
|
|
":kit.poisoners_kit",
|
|
":weapon.light.sickle",
|
|
":weapon.light.quarterstaff",
|
|
":weapon.light.sling",
|
|
);
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
":armor.light.leather",
|
|
":weapon.light.sickle",
|
|
":kit.poisoners_kit",
|
|
":kit.healers_kit",
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
//
|
|
// FENCER
|
|
// -------
|
|
if (foundation === 3 || foundation === ":fencer") {
|
|
|
|
c.foundation = "Fencer";
|
|
|
|
c.silver = 40;
|
|
c.itemSlots = 5;
|
|
c.maxHitPoints = c.currentHitPoints = 15;
|
|
|
|
c.magic = Math.min(c.magic, 10)
|
|
c.meleeCombat = Math.max(c.meleeCombat, 10)
|
|
|
|
this.addProficienciesToCharacter(
|
|
c, //
|
|
":perk.riposte",
|
|
":armor.light",
|
|
":weapon.light",
|
|
);
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
":armor.light.leather",
|
|
":weapon.basic.dagger",
|
|
":weapon.light.rapier",
|
|
);
|
|
}
|
|
|
|
if (foundation === 4 || foundation === ":guard") {
|
|
|
|
c.foundation = "Guard";
|
|
|
|
c.silver = 50;
|
|
c.itemSlots = 5;
|
|
c.maxHitPoints = c.currentHitPoints = 10
|
|
|
|
c.awareness = Math.max(c.awareness, 10)
|
|
c.meleeCombat = Math.max(c.meleeCombat, 10)
|
|
c.skulduggery = Math.min(c.skulduggery, 10)
|
|
|
|
this.addProficienciesToCharacter(
|
|
c, //
|
|
":armor.medium",
|
|
":weapon.heavy",
|
|
":weapon.specialist.halberd",
|
|
":wepaon.light",
|
|
);
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
":armor.medium.breastplate",
|
|
":lighting.bulls_eye_lantern",
|
|
":map.city.hovedstad",
|
|
":misc.lamp_oil",
|
|
":misc.signal_whistle",
|
|
":weapon.specialist.halberd",
|
|
);
|
|
return
|
|
}
|
|
|
|
if (foundation === 5 || foundation === ":magician") {
|
|
|
|
c.foundation = "Magician"
|
|
|
|
c.silver = 10;
|
|
c.itemSlots = 6
|
|
c.maxHitPoints = c.currentHitPoints = 10
|
|
|
|
c.grit = Math.min(c.grit, 10)
|
|
c.magic = Math.max(c.magic, 10)
|
|
c.meleeCombat = Math.min(c.meleeCombat, 10)
|
|
c.rangedCombat = Math.min(c.rangedCombat, 10)
|
|
|
|
/* ---- NO PROFICIENCIES ---- */
|
|
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
// "TODO: [TEIR 2 WAND WITH RANDOM SPELL]",
|
|
// "TODO: [TEIR 1 WAND WITH RANDOM SPELL]",
|
|
);
|
|
return
|
|
}
|
|
|
|
if (foundation === 6 || foundation === ":medic") {
|
|
|
|
c.foundation = "Medic";
|
|
|
|
c.silver = 40;
|
|
c.itemSlots = 7;
|
|
c.maxHitPoints = c.currentHitPoints = 10
|
|
|
|
c.awareness = Math.max(10, c.awareness)
|
|
|
|
this.addProficienciesToCharacter(
|
|
c, //
|
|
":armor.light",
|
|
":armor.medium",
|
|
":weapon.light",
|
|
);
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
":armor.light.studded_leather",
|
|
":kit.healers_kit",
|
|
":weapon.basic.club",
|
|
":weapon.basic.dagger",
|
|
":weapon.light.sling",
|
|
);
|
|
return
|
|
}
|
|
|
|
if (foundation === 7 || foundation === ":reckless") {
|
|
|
|
c.foundation = "Reckless";
|
|
|
|
c.silver = 50;
|
|
c.itemSlots = 7;
|
|
c.maxHitPoints = c.currentHitPoints = 20
|
|
|
|
c.awareness = Math.max(10, c.awareness);
|
|
c.grit = Math.max(10, c.grit);
|
|
c.magic = Math.min(10, c.magic);
|
|
c.meleeCombat = Math.max(10, c.meleeCombat);
|
|
|
|
this.addProficienciesToCharacter(
|
|
c, //
|
|
":wepaon.heavy",
|
|
":wepaon.light",
|
|
);
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
":weapon.heavy.great_axe",
|
|
);
|
|
return
|
|
}
|
|
|
|
if (foundation === 8 || foundation === ":rover") {
|
|
|
|
c.foundation = "Rover";
|
|
|
|
c.silver = 25;
|
|
c.itemSlots = 5;
|
|
c.maxHitPoints = c.currentHitPoints = 10
|
|
|
|
c.awareness = Math.max(10, c.awareness)
|
|
c.magic = Math.min(10, c.magic)
|
|
c.rangedCombat = Math.max(10, c.rangedCombat)
|
|
|
|
this.addProficienciesToCharacter(
|
|
c, //
|
|
":armor.light",
|
|
":weapon.light",
|
|
":weapon.heavy.longbow",
|
|
);
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
":armor.light.leather",
|
|
":kit.snare_makers_kit",
|
|
":weapon.heavy.longbow",
|
|
":weapon.light.short_sword",
|
|
":map.shayland", // Shayland is the region around Hovedstad
|
|
);
|
|
return
|
|
}
|
|
|
|
if (foundation === 9 || foundation === ":skrimisher") {
|
|
|
|
c.foundation = "Skirmisher";
|
|
|
|
c.silver = 15;
|
|
c.itemSlots = 6;
|
|
c.maxHitPoints = c.currentHitPoints = 15
|
|
|
|
c.awareness = Math.max(10, c.awareness)
|
|
c.grit = Math.max(10, c.grit)
|
|
c.magic = Math.max(5, c.magic)
|
|
c.meleeCombat = Math.max(10, c.meleeCombat)
|
|
c.skulduggery = Math.max(10, c.skulduggery)
|
|
|
|
this.addProficienciesToCharacter(
|
|
c, //
|
|
":armor.light",
|
|
);
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
":armor.light.small_shield",
|
|
":weapon.light.spear",
|
|
);
|
|
return
|
|
}
|
|
|
|
if (foundation === 10 || foundation === ":sneak") {
|
|
|
|
c.foundation = "Sneak";
|
|
|
|
c.silver = 30;
|
|
c.itemSlots = 6;
|
|
c.maxHitPoints = c.currentHitPoints = 10
|
|
|
|
c.awareness = Math.max(10, c.awareness)
|
|
c.meleeCombat = Math.max(10, c.meleeCombat)
|
|
c.skulduggery = Math.max(10, c.skulduggery)
|
|
|
|
this.addProficienciesToCharacter(
|
|
c, //
|
|
":armor.light",
|
|
);
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
":weapon.basic.dagger",
|
|
":weapon.light.small_crossbow",
|
|
":kit.poisoners_kit",
|
|
);
|
|
return
|
|
}
|
|
|
|
if (foundation === 11 || foundation === ":spellsword") {
|
|
|
|
c.foundation = "Spellsword";
|
|
|
|
c.silver = 30;
|
|
c.itemSlots = 5;
|
|
c.maxHitPoints = c.currentHitPoints = 12;
|
|
|
|
c.grit = Math.max(10, c.grit)
|
|
c.magic = Math.max(10, c.magic)
|
|
c.meleeCombat = Math.max(10, c.meleeCombat)
|
|
c.rangedCombat = Math.min(10, c.rangedCombat)
|
|
c.skulduggery = Math.min(10, c.skulduggery)
|
|
|
|
this.addProficienciesToCharacter(
|
|
c, //
|
|
":weapon.light",
|
|
);
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
":weapon.light.rapier",
|
|
// "[TODO TIER 1 WAND WITH RANDOM SPELL]",
|
|
);
|
|
|
|
return
|
|
}
|
|
|
|
if (foundation === 12 || foundation === ":spelunker") {
|
|
|
|
c.foundation = "Spelunker";
|
|
|
|
c.silver = 5;
|
|
c.itemSlots = 4;
|
|
c.maxHitPoints = c.currentHitPoints = 10;
|
|
|
|
c.awareness = Math.max(10, c.awareness)
|
|
c.magic = Math.min(10, c.magic)
|
|
c.meleeCombat = Math.max(10, c.meleeCombat)
|
|
c.skulduggery = Math.max(10, c.skulduggery)
|
|
|
|
this.addProficienciesToCharacter(
|
|
c, //
|
|
":weapon.light",
|
|
);
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
":kit.map_makers_kit",
|
|
":lighting.bulls_eye_lantern",
|
|
":misc.caltrops",
|
|
":misc.chalk",
|
|
":misc.lamp_oil",
|
|
":weapon.light.spear",
|
|
);
|
|
|
|
return
|
|
}
|
|
|
|
if (foundation === 13 || foundation === ":spit_n_polish") {
|
|
|
|
c.foundation = "Spit'n'Polish";
|
|
|
|
c.silver = 10;
|
|
c.itemSlots = 2;
|
|
c.maxHitPoints = c.currentHitPoints = 10;
|
|
|
|
c.magic = Math.min(6, c.magic)
|
|
c.meleeCombat = Math.max(10, c.meleeCombat)
|
|
|
|
this.addProficienciesToCharacter(
|
|
c, //
|
|
":weapon.light",
|
|
":weapon.heavy",
|
|
":armor.heavy",
|
|
":armor.light",
|
|
);
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
":weapon.heavy.longsword",
|
|
":armor.heavy.half_plate",
|
|
":armor.heavy.large_shield",
|
|
);
|
|
|
|
return
|
|
}
|
|
|
|
if (foundation === 14 || foundation === ":stiletto") {
|
|
|
|
c.foundation = "Stiletto";
|
|
|
|
c.silver = 10;
|
|
c.itemSlots = 5;
|
|
c.maxHitPoints = c.currentHitPoints = 10;
|
|
|
|
|
|
c.magic = Math.min(6, c.magic)
|
|
c.meleeCombat = Math.max(10, c.meleeCombat)
|
|
|
|
this.addProficienciesToCharacter(
|
|
c, //
|
|
":weapon.light",
|
|
":weapon.heavy",
|
|
":armor.heavy",
|
|
":armor.light",
|
|
);
|
|
|
|
this.addItemsToCharacter(
|
|
c, //
|
|
":weapon.heavy.longsword",
|
|
":armor.heavy.half_plate",
|
|
":armor.heavy.large_shield",
|
|
);
|
|
|
|
return
|
|
}
|
|
|
|
return this.applyFoundation(c, ":random")
|
|
}
|
|
}
|