/**
* L5R Character generator base object
*/
export class CharacterGenerator {
/**
* Base age (minimal)
*/
static baseAge = 15;
/**
* Payload Object
*/
data = {
avgRingsValue: 3, // 1-5
clan: "random",
family: "",
gender: "male",
age: CharacterGenerator.baseAge,
maritalStatus: "",
};
/**
* Initialize the generator
* @param {number} avgRingsValue number between 1 and 5
* @param {string} clanName random|crab|crane...
* @param {string} gender random|male|female
*/
constructor({ avgRingsValue = 3, clanName = "random", gender = "random" }) {
if (!CONFIG.l5r5e.families.has(clanName)) {
clanName = "random";
}
if (clanName === "random") {
clanName = CharacterGenerator._getRandomArrayValue(Array.from(CONFIG.l5r5e.families.keys()));
}
if (gender === "random" || !["male", "female"].includes(gender)) {
gender = Math.random() > 0.5 ? "male" : "female";
}
this.data.avgRingsValue = CharacterGenerator.sanitizeMinMax(avgRingsValue);
this.data.clan = clanName;
this.data.family = CharacterGenerator._getRandomFamily(clanName);
this.data.gender = gender;
}
/**
* Return true if the gender is Female
* @return {boolean}
*/
get isFemale() {
return this.data.gender === "female";
}
/**
* Return a random value for this array
* @param {String[]} array
* @return {String}
* @private
*/
static _getRandomArrayValue(array) {
return array[Math.floor(Math.random() * array.length)] ?? "";
}
/**
* Return a random value between min and max
* @param {number} min
* @param {number} max
* @return {number}
* @private
*/
static _randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
/**
* Always return a number between 1 and 5
* @param {number} number
* @return {number}
*/
static sanitizeMinMax(number) {
return Math.min(5, Math.max(1, number));
}
/**
* Return a Item from this pack (by id if provided, or random otherwise)
* @param {string} packName
* @param {string} id
* @return {Promise}
* @private
*/
static async _getItemFromPack(packName, id = null) {
const comp = await game.packs.get(packName);
if (!comp) {
console.log(`L5R5E | Pack not found[${packName}]`);
return;
}
let document;
if (id) {
document = await comp.getDocument(id);
} else {
if (!comp.indexed) {
await comp.getDocuments();
}
document = comp.getDocument(CharacterGenerator._getRandomArrayValue(Array.from(comp.keys())));
}
await game.l5r5e.HelpersL5r5e.refreshItemProperties(document);
return document;
}
/**
* Generate and return a family name for this clan
* @param {string} clanName
* @return {string}
* @private
*/
static _getRandomFamily(clanName) {
let originClan = clanName;
// Ronin specific, can be any other family name
if (clanName === "ronin") {
originClan = CharacterGenerator._getRandomArrayValue(
Array.from(CONFIG.l5r5e.families.keys()).filter((e) => e !== "ronin")
);
}
return CharacterGenerator._getRandomArrayValue(CONFIG.l5r5e.families.get(originClan));
}
/**
* Generate and return a firstname
* @return {Promise}
*/
static async getRandomizedFirstname(isFemale, clan) {
let table = `Japanese names (${isFemale ? "Female" : "Male"})`;
switch (clan) {
case "ivory_kingdoms":
table = "Ivory Kingdoms names";
break;
case "qamarist":
table = "Qamarist names";
break;
case "ujik":
table = "Ujik names";
break;
}
const randomNames = await game.l5r5e.HelpersL5r5e.drawManyFromPack("l5r5e.core-name-tables", table, 1, {
displayChat: false,
});
return randomNames?.results[0]?.data.text || "";
}
/**
* Generate the actor age
* @param {number} avgRingsValue
* @return {number}
*/
static genAge(avgRingsValue) {
return CharacterGenerator._randomInt(
CharacterGenerator.baseAge,
avgRingsValue * 10 + CharacterGenerator.baseAge
);
}
/**
* Return the marriage state
* @param {number} age
* @return {string} unmarried|betrothed|married|widowed
*/
static genMaritalStatus(age) {
const rng = Math.random();
if (age < 20) {
return rng < 0.1 ? "married" : rng < 0.4 ? "betrothed" : "unmarried";
}
if (age < 30) {
return rng < 0.4 ? "married" : rng < 0.7 ? "betrothed" : "unmarried";
}
return rng < 0.8 ? "married" : rng < 0.9 ? "widowed" : "unmarried";
}
/**
* Generate the marital partner
* @param {string} maritalStatus unmarried|betrothed|married|widowed
* @param {number} avgRingsValue
* @param {string} clan
* @param {string} family
* @param {boolean} isFemale
* @return {Promise<{age: number, name: string, clan: string, family: string, female: boolean}>}
*/
static async genMaritalPartner(maritalStatus, avgRingsValue, clan, family, isFemale) {
const alreadyMerged = maritalStatus !== "betrothed";
const partner = {
age: CharacterGenerator.genAge(avgRingsValue),
clan: "",
family: "",
female: Math.random() > 0.9 ? isFemale : !isFemale,
name: "",
};
partner.clan =
alreadyMerged || Math.random() > 0.7
? clan
: CharacterGenerator._getRandomArrayValue(Array.from(CONFIG.l5r5e.families.keys()));
partner.family = alreadyMerged ? family : CharacterGenerator._getRandomFamily(partner.clan);
partner.name = await CharacterGenerator.getRandomizedFirstname(partner.female, partner.clan);
return partner;
}
/**
* Generate children
* @param {number} age Current npc age
* @param {string} clan Current npc clan
* @return {Promise}
*/
static async genChildren(age, clan) {
const childs = [];
let ageLoop = Math.max(0, age - CharacterGenerator.baseAge - 1);
while (ageLoop > 0) {
const childAge = CharacterGenerator._randomInt(1, ageLoop);
if (Math.random() > 0.66) {
const childIsFemale = Math.random() > 0.5;
const childName = await CharacterGenerator.getRandomizedFirstname(childIsFemale, clan);
childs.push(
`${childName} (${childAge}, ${game.i18n.localize(
"l5r5e.social.gender." + (childIsFemale ? "female" : "male")
)})`
);
}
ageLoop -= childAge + 1;
}
return childs;
}
/**
* Generate Honor, Glory and Status values
* @param {number} age
* @param {string} clan
* @return {{honor: number, glory: number, status: number}}
*/
static genSocialStanding(age, clan) {
const karma = Math.random() < 0.66 ? 1 : -1;
const rng = (initial, variation) => {
return initial + CharacterGenerator._randomInt(5, variation) * karma;
};
let honor = rng(34, age / 2);
switch (clan) {
case "lion":
honor += 10;
break;
case "scorpion":
honor -= 10;
break;
}
return {
honor,
glory: rng(40, age / 3),
status: rng(30, age / 4),
};
}
/**
* Modify the current actor datas with selected options
*
* @param {ActorL5r5e} actor Actor object
* @param {Object} generate
* @param {boolean} generate.name If true generate a new name
* @param {boolean} generate.identity If true generate Clan, Gender, Age, Marital status
* @param {boolean} generate.attributes If true generate Rings, attributes, skills and confrontation ranks
* @param {boolean} generate.demeanor If true generate Demeanor and rings affinities
* @param {boolean} generate.peculiarities If true generate Advantage and Disadvantage
* @param {boolean} generate.items If true generate Armor, Weapons and Items
* @param {boolean} generate.techniques If true generate Shuji, Katas...
* @param {boolean} generate.narrative If true generate Narrative and fluff
* @return {Promise
` +
`${game.i18n.localize("l5r5e.social.gender.title")}: ${game.i18n.localize(
"l5r5e.social.gender." + this.data.gender
)}
` +
`${game.i18n.localize("l5r5e.clans.label")}: ${game.i18n.localize(
"l5r5e.clans." + this.data.clan
)}
` +
`${game.i18n.localize("l5r5e.social.marital_status.title")}: ${game.i18n.localize(
"l5r5e.social.marital_status." + this.data.maritalStatus
)}
`;
// Define partner identity
if (this.data.maritalStatus !== "unmarried") {
const partner = await CharacterGenerator.genMaritalPartner(
this.data.maritalStatus,
this.data.avgRingsValue,
this.data.clan,
this.data.family,
this.isFemale
);
actorDatas.notes +=
"" +
`${game.i18n.localize("l5r5e.social.marital_status.partner")}:` +
` ${partner.family} ${partner.name}` +
` (${partner.age}, ${game.i18n.localize(
"l5r5e.social.gender." + (partner.female ? "female" : "male")
)})` +
"
";
// Childs
const childs = await CharacterGenerator.genChildren(Math.min(this.data.age, partner.age), this.data.clan);
if (childs.length > 0) {
actorDatas.notes += `${game.i18n.localize("l5r5e.social.children")}: ${childs.join(", ")}
`;
}
}
}
/**
* Generate Narrative fluff
* @param {DocumentData.data} actorDatas
* @private
*/
_generateNarrative(actorDatas) {
// TODO generateNarrative
// actorDatas.description = '';
}
//
}