import { createAppearance } from "./create_appearance.js";
import { createCharacterName } from "./create_character_name.js";
import { createKit } from "./create_kit.js";
import { database as professionDb } from "./professions.js";
import { format, article } from "./string_utils.js";
import { logger, sum } from "./utils.js";
import { nonNegativeGaussian, random, roll, shuffle, test } from "./random_utils.js";
import { TRAITS as T } from "./constants.js";
import { without } from "./set_utils.js";
import Character from "./models/character.js";
const MAX_TRAIT_VALUE = 4;
const INNATE = professionDb.findOne({ name: "Innate" });
const HISTORIES = Object.freeze([
"Before the collapse, was {0}",
"Was {0} up until the war",
"Was {0} before the war",
]);
const NAVY_RANKS = Object.freeze([
"",
"Seaman",
"Petty Officer",
"Ensign",
"Lieutenant",
"Lieutenant Commander",
"Commander",
"Captain",
"Admiral",
]);
const POLICE_RANKS = Object.freeze([
"Officer",
["Officer", "Trooper"],
"Detective",
"Sergeant",
["Captain", "Deputy Marshal"],
["Inspector", "Marshal"],
["Deputy Chief", "Undersheriff"],
["Chief", "Sheriff"],
]);
const MIL_RANKS = Object.freeze([
"",
"Private",
"Corporal",
"Sergeant",
"Lieutenant",
"Captain",
"Major",
"Colonel",
"General",
]);
function addTrait(map, name, start) {
if (!map[name]) {
map[name] = start;
}
}
function train(profession, character, points) {
var map = {};
profession.seeds.forEach(function (seed) {
if (character.trait(seed) < MAX_TRAIT_VALUE) {
addTrait(map, seed, 1);
points--;
}
});
// Force grouping into a subset of supplemental skills, enough to
// assign all the points (at a limit of 4)
var limit = Math.ceil(points / 4) - profession.seeds.length + roll("1d2");
shuffle(profession.traits);
profession.traits.forEach(function (trait, index) {
if (index <= limit) {
addTrait(map, trait, 0);
}
});
var allTraitNames = Object.keys(map);
// Okay, the starting points, minus the mandatory seed points, can
// be less than zero. So test explicitly for that.
while (points > 0 && allTraitNames.length) {
var traitName = random(allTraitNames);
// prevents an infinite loop if points exceeds sum of max of all traits, and
// also ensures that existing trait values are accounted for when honoring
// maximum values.
if (map[traitName] + character.trait(traitName) >= MAX_TRAIT_VALUE) {
allTraitNames = without(allTraitNames, traitName);
continue;
}
map[traitName]++;
points--;
}
for (var prop in map) {
if (map[prop] > 0) {
character.changeTrait(prop, map[prop]);
}
}
// All the tags of the profession are carried forward in the character,
// and the character can be queried rather than keeping the profession
// around.
profession.tags.forEach((tag) => character.tags.add(tag));
console.log(character.tags);
if (profession.postprocess === "rank") {
assignRank(profession, character);
} else if (profession.postprocess === "doctor") {
character.honorific = "Dr.";
} else if (profession.postprocess === "phd") {
character.degree = "Ph.D.";
}
}
// The gang generator uses this...it needs a clean-up
export function assignRank(profession, character) {
var name = profession.names[0];
var rank = null;
// TODO: Could use gaussian spread here.
var level = Math.round(
Math.max(character.trait("Military"), character.trait("Government")) * 1.5
);
// TODO: Could use a table here.
if (name === "Navy" || name === "Coast Guard") {
rank = NAVY_RANKS[roll(level)];
} else if (name === "Police") {
rank = POLICE_RANKS[roll(level)];
if (rank instanceof Array) {
rank = random(rank);
}
} else {
rank = MIL_RANKS[roll(level)];
}
character.honorific = rank ? name + " " + rank : name;
}
function trainAndEquipChild(character) {
delete character.profession;
delete character.honorific;
// make a child instead.
if (character.age > 4) {
train(INNATE, character, roll("1d4-1"));
// But don’t call children "attractive" or "charming".
delete character.traits[T.ATTRACTIVE];
delete character.traits[T.CHARMING];
}
}
function trainAndEquipAdult(character, profession, params) {
let startingTraitPoints = sum(Object.values(params.traits));
console.debug("startingTraitPoints", startingTraitPoints);
let traitPoints = 8 + ~~(params.age / 10) - startingTraitPoints;
console.debug("traitPoints", traitPoints);
let innateTraitPoints = roll("2d2-1");
console.debug("innateTraitPoints", innateTraitPoints);
train(INNATE, character, innateTraitPoints);
// 27 is arbitrary, it gives the character a couple of years to have had a profession.
// For higher-status professions like a doctor, cutoff is 30.
let pre = professionDb.findOne({ tags: "pre" });
console.debug("pre", pre.names[0]);
// Your social status before the war determines your status afterwards. (always?)
let prestige = pre.typeOf("prestige");
let post = profession || professionDb.findOne({ tags: prestige + " post" });
let cutoffAge = pre.is("high") ? 30 : 27;
console.debug("post", post.names[0], "cut-off age", cutoffAge);
if (/*post.not("pre") && */ character.age > cutoffAge) {
console.debug("character has pre-war experience");
let weight = (character.age - 10) / character.age;
let prePoints = Math.floor(traitPoints * weight);
let postPoints = traitPoints - prePoints;
// two profession, pre/post war
train(pre, character, prePoints);
// transfer their pre-war title, if any, into their history and remove it from
// their current designation
let title = character.honorific ? character.honorific : random(pre.names);
let hsy = random(HISTORIES);
if (title === "dr") {
title = "doctor";
}
character.description.push(format(hsy, article(title.toLowerCase())) + ".");
delete character.honorific;
train(post, character, postPoints);
} else {
// just use the post profession
train(post, character, traitPoints);
}
character.profession = random(post.names);
var bags = createKit({
profession: post.names[0],
gender: character.gender,
traveling: params.possessions === "Traveling",
});
character.clothing = bags.clothing;
if (params.possessions !== "None") {
character.possessions = bags.possessions;
}
}
/**
* Post collapse professions only.
* @returns a list of profession names
*/
export function getProfessions() {
return professionDb
.findAll({ tags: "post" })
.map((row) => row.names[0])
.concat("Soldier")
.sort();
}
export function createHeritage() {
return test(20) ? "latino" : "anglo";
}
export function createGender() {
return random(["male", "female"]);
}
export var createAdultAge = () => 18 + nonNegativeGaussian(7);
export function createCharacter({
possessions = "None",
age = roll("14+3d12"),
traits = {},
gender = createGender(),
heritage = createHeritage(), // duplicated in naming function
name,
profession = professionDb.findOne(),
} = {}) {
logger.start("createCharacter", { possessions, age, traits, gender, heritage, name, profession });
// Name can be passed as a partially constructed argument (in family, it provides only the
// last name, for example). Fully constructed it here if needed.
name = createCharacterName({ ...name, gender, heritage });
// if profession was provided as a string, it's the name of a profession, so get that
if (typeof profession === "string") {
profession = professionDb.findOne({ name: profession });
}
let params = { gender, heritage, name, age, traits, profession };
let character = new Character(params);
if (character.age < 17) {
trainAndEquipChild(character);
} else {
// Not tailored towards children... may eventually be fixed
character.description.push(createAppearance({ character }));
trainAndEquipAdult(character, profession, { ...params, possessions });
}
if (character.heritage === "latino") {
// Latinos may also speak some spanish outside of training (of course this is
// true of others, and not covered...maybe we need different innate "professions" for
// things like studying abroad).
character.changeTrait("Spanish", nonNegativeGaussian(1.5));
}
return logger.end(character);
}