some autocomplete clan & family testing

This commit is contained in:
Vlyan
2022-02-08 18:29:10 +01:00
parent 9d5374e35c
commit 1afd85d3e9
15 changed files with 381 additions and 140 deletions

View File

@@ -15,17 +15,19 @@ export class CharacterGeneratorDialog extends FormApplication {
* Payload Object
*/
object = {
avg_rings: 3,
avgRings: 3,
clan: "random",
gender: "random",
generateAttributes: true,
generateDemeanor: true,
generateName: true,
generateNarrative: true,
generatePeculiarities: true,
generateItems: true,
generateTechniques: true,
generateSocial: true,
generate: {
attributes: true,
demeanor: true,
items: true,
name: true,
narrative: true,
peculiarities: true,
social: true,
techniques: true,
},
};
/**
@@ -60,6 +62,32 @@ export class CharacterGeneratorDialog extends FormApplication {
constructor(actor = null, options = {}) {
super({}, options);
this.actor = actor;
this.initializeFromActor();
}
/**
* Try to get values from actor to initialize the generator
*/
initializeFromActor() {
const actorDatas = this.actor.data.data;
// Identity
//this.object.age = actorDatas.age;
this.object.clan = actorDatas.identity.clan || "random";
this.object.gender = actorDatas.identity.female
? "female"
: actorDatas.identity.female === false
? "male"
: "random";
// Rings
this.object.avgRings = CharacterGenerator.sanitizeMinMax(
Math.round(
Object.values(actorDatas.rings).reduce((acc, ringValue) => {
return acc + ringValue;
}, 0) / 5
)
);
}
/**
@@ -68,7 +96,7 @@ export class CharacterGeneratorDialog extends FormApplication {
* @return {Object}
*/
async getData(options = null) {
const clans = Object.keys(CharacterGenerator.clansAndFamilies).map((e) => ({
const clans = Array.from(CONFIG.l5r5e.families.keys()).map((e) => ({
id: e,
label: game.i18n.localize("l5r5e.clans." + e),
}));
@@ -93,23 +121,22 @@ export class CharacterGeneratorDialog extends FormApplication {
* @override
*/
async _updateObject(event, formData) {
formData = foundry.utils.expandObject(formData);
// Generate datas
const generator = new CharacterGenerator({
avgRingsValue: formData.avg_rings,
avgRingsValue: formData.avgRings,
clanName: formData.clan,
gender: formData.gender,
});
// Update current Object with new data to keep selection
// Get selected value from generator for random values
this.object = {
...formData,
clan: generator.data.clan,
gender: generator.data.gender,
};
// Update actor with selection
const updatedDatas = await generator.toActor(this.actor, formData);
const updatedDatas = await generator.toActor(this.actor, formData.generate);
await this.actor.update(updatedDatas);
this.render(false);

View File

@@ -3,36 +3,6 @@
*/
export class CharacterGenerator {
//<editor-fold desc="Config Datas">
static clansAndFamilies = {
// Majors
imperial: ["Miya", "Otomo", "Seppun"],
crab: ["Hida", "Kaiu", "Hiruma", "Yasuki", "Kuni"],
crane: ["Asahina", "Daidoji", "Doji", "Kakita"],
dragon: ["Kitsuki", "Mirumoto", "Togashi"],
lion: ["Akodo", "Ikoma", "Kitsu", "Matsu"],
phoenix: ["Agasha", "Asako", "Isawa", "Shiba"],
scorpion: ["Bayushi", "Shosuro", "Soshi", "Yogo"],
unicorn: ["Ide", "Iuchi", "Moto", "Shinjo", "Utaku"],
mantis: ["(mantis)"], // no family name, boat name
// Minors
ronin: ["(ronin)"], // can be anything
badger: ["Ichiro"],
bat: ["Komori"],
boar: ["Heichi"],
dragonfly: ["Tonbo"],
firefly: ["Hotaru"],
hare: ["Ujina", "Usagi"],
monkey: ["Toku", "Fuzake"],
oriole: ["Tsi"],
ox: ["Morito"],
sparrow: ["Suzume"],
tortoise: ["Kasuga"],
// ivory_kingdoms
// qamarist
// ujik
};
static demeanorList = [
{ id: "adaptable", mod: { fire: 2, earth: -2 } },
{ id: "adaptable", mod: { water: 2, earth: -2 } },
@@ -130,17 +100,17 @@ export class CharacterGenerator {
* @param {string} gender random|male|female
*/
constructor({ avgRingsValue = 3, clanName = "random", gender = "random" }) {
if (!CharacterGenerator.clansAndFamilies[clanName]) {
if (!CONFIG.l5r5e.families.has(clanName)) {
clanName = "random";
}
if (clanName === "random") {
clanName = CharacterGenerator._getRandomArrayValue(Object.keys(CharacterGenerator.clansAndFamilies));
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.avgRingsValue = CharacterGenerator.sanitizeMinMax(avgRingsValue);
this.data.clan = clanName;
this.data.family = CharacterGenerator._getRandomFamily(clanName);
this.data.gender = gender;
@@ -182,9 +152,8 @@ export class CharacterGenerator {
* Always return a number between 1 and 5
* @param {number} number
* @return {number}
* @private
*/
static _sanitizeMinMax(number) {
static sanitizeMinMax(number) {
return Math.min(5, Math.max(1, number));
}
@@ -222,10 +191,11 @@ export class CharacterGenerator {
// Ronin specific, can be any other family name
if (clanName === "ronin") {
originClan = CharacterGenerator._getRandomArrayValue(
Object.keys(CharacterGenerator.clansAndFamilies).filter((e) => e !== "ronin")
Array.from(CONFIG.l5r5e.families.keys()).filter((e) => e !== "ronin")
);
}
return CharacterGenerator._getRandomArrayValue(CharacterGenerator.clansAndFamilies[originClan]);
return CharacterGenerator._getRandomArrayValue(CONFIG.l5r5e.families.get(originClan));
}
/**
@@ -288,33 +258,41 @@ export class CharacterGenerator {
* Modify the current actor datas with selected options
*
* @param {ActorL5r5e} actor Actor object
* @param {boolean} generateName If true generate a new name
* @param {boolean} generateAttributes If true generate rings, attributes, skills and confrontation ranks
* @param {boolean} generateSocial If true generate Social Standing
* @param {boolean} generateDemeanor If true generate Demeanor and rings affinities
* @param {boolean} generatePeculiarities If true generate Advantage and Disadvantage
* @param {boolean} generateItems If true generate Armor, Weapons and Items
* @param {boolean} generateTechniques If true generate Shuji, Katas...
* @param {boolean} generateNarrative If true generate Narrative and fluff
* @param {Object} generate
* @param {boolean} generate.name If true generate a new name
* @param {boolean} generate.attributes If true generate rings, attributes, skills and confrontation ranks
* @param {boolean} generate.social If true generate Social Standing
* @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<Object>}
*/
async toActor(
actor,
{
generateName = true,
generateAttributes = true,
generateSocial = true,
generateDemeanor = true,
generatePeculiarities = true,
generateItems = true,
generateTechniques = true,
generateNarrative = true,
} = {}
generate = {
name: true,
attributes: true,
social: true,
demeanor: true,
peculiarities: true,
items: true,
techniques: true,
narrative: true,
}
) {
const actorDatas = actor.data.data;
console.log(generate); // TODO tmp
// Name
const newName = generateName ? await this.getRandomizedName() : actor.data.name;
const newName = generate.name ? await this.getRandomizedName() : actor.data.name;
actorDatas.identity.age = this.data.age;
actorDatas.identity.clan = this.data.clan;
actorDatas.identity.family = this.data.family;
actorDatas.identity.female = this.data.gender === "female";
// Img (only if default)
const folder = "systems/l5r5e/assets/icons/actors";
@@ -327,38 +305,38 @@ export class CharacterGenerator {
: actor.data.img;
// Generate attributes (rings, attributes, skills, confrontation ranks)
if (generateAttributes) {
if (generate.attributes) {
this._generateAttributes(actorDatas);
}
// Social Standing
if (generateSocial) {
if (generate.social) {
actorDatas.social.honor = this.data.honor;
actorDatas.social.glory = this.data.glory;
actorDatas.social.status = this.data.status;
}
// Demeanor (npc only)
if (generateDemeanor && actor.type === "npc") {
if (generate.demeanor && actor.type === "npc") {
this._generateDemeanor(actorDatas);
}
// Items types
if (generatePeculiarities || generateItems || generateTechniques) {
if (generate.peculiarities || generate.items || generate.techniques) {
const newItemsData = [];
// Advantage / Disadvantage
if (generatePeculiarities) {
if (generate.peculiarities) {
await this._generatePeculiarities(actor, newItemsData);
}
// Items
if (generateItems) {
if (generate.items) {
await this._generateItems(actor, newItemsData);
}
// Techniques
if (generateTechniques) {
if (generate.techniques) {
await this._generateTechniques(actor, newItemsData);
}
@@ -369,7 +347,7 @@ export class CharacterGenerator {
}
// Narrative
if (generateNarrative) {
if (generate.narrative) {
this._generateNarrative(actorDatas);
}
@@ -393,7 +371,7 @@ export class CharacterGenerator {
// Rings
CONFIG.l5r5e.stances.forEach((ringName) => {
// avgRingsValue + (-1|0|1)
actorDatas.rings[ringName] = CharacterGenerator._sanitizeMinMax(
actorDatas.rings[ringName] = CharacterGenerator.sanitizeMinMax(
this.data.avgRingsValue - 1 + Math.floor(Math.random() * 3)
);
stats.min = Math.min(stats.min, actorDatas.rings[ringName]);

View File

@@ -76,6 +76,24 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
return;
}
// Autocomplete
game.l5r5e.HelpersL5r5e.autocomplete(
html,
"data.identity.clan",
Object.entries(game.i18n.translations.l5r5e.clans)
.filter(([k, v]) => k !== "title")
.map(([k, v]) => v)
);
game.l5r5e.HelpersL5r5e.autocomplete(
html,
"data.identity.family",
CONFIG.l5r5e.families.get(
Object.entries(game.i18n.translations.l5r5e.clans).find(
([k, v]) => v === this.actor.data.data.identity.clan
)?.[0]
)
);
// Open linked school curriculum journal
html.find(".school-journal-link").on("click", this._openLinkedJournal.bind(this));

View File

@@ -18,12 +18,13 @@ export class NpcSheetL5r5e extends BaseCharacterSheetL5r5e {
}
/**
* Add the NpcGenerator button on top of sheet
* Add the CharacterGenerator button in L5R specific bar
* @override
* @return {{label: string, class: string, icon: string, onclick: Function|null}[]}
*/
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
if (!this.isEditable || this.actor.limited) {
_getL5rHeaderButtons() {
const buttons = super._getL5rHeaderButtons();
if (!this.isEditable || this.actor.limited || this.actor.data.data.soft_locked) {
return buttons;
}

View File

@@ -43,7 +43,7 @@ L5R5E.techniques.set("title_ability", { type: "title", displayInTypes: false });
// Custom
L5R5E.techniques.set("specificity", { type: "custom", displayInTypes: false });
// Map SkillId - CategoryId
// *** SkillId - CategoryId ***
L5R5E.skills = new Map();
L5R5E.skills.set("aesthetics", "artisan");
L5R5E.skills.set("composition", "artisan");
@@ -74,7 +74,7 @@ L5R5E.skills.set("seafaring", "trade");
L5R5E.skills.set("skulduggery", "trade");
L5R5E.skills.set("survival", "trade");
// Symbols Map
// *** Symbols ***
L5R5E.symbols = new Map();
L5R5E.symbols.set("(op)", { class: "i_opportunity", label: "l5r5e.chatdices.opportunities" });
L5R5E.symbols.set("(su)", { class: "i_success", label: "l5r5e.chatdices.successes" });
@@ -114,3 +114,29 @@ L5R5E.symbols.set("(unicorn)", { class: "i_unicorn", label: "" });
L5R5E.symbols.set("(bushi)", { class: "i_bushi", label: "" });
L5R5E.symbols.set("(courtier)", { class: "i_courtier", label: "" });
L5R5E.symbols.set("(shugenja)", { class: "i_shugenja", label: "" });
// *** Clans and Families ***
L5R5E.families = new Map();
// Majors
L5R5E.families.set("imperial", ["Miya", "Otomo", "Seppun"]);
L5R5E.families.set("crab", ["Hida", "Kaiu", "Hiruma", "Yasuki", "Kuni"]);
L5R5E.families.set("crane", ["Asahina", "Daidoji", "Doji", "Kakita"]);
L5R5E.families.set("dragon", ["Kitsuki", "Mirumoto", "Togashi"]);
L5R5E.families.set("lion", ["Akodo", "Ikoma", "Kitsu", "Matsu"]);
L5R5E.families.set("phoenix", ["Agasha", "Asako", "Isawa", "Shiba"]);
L5R5E.families.set("scorpion", ["Bayushi", "Shosuro", "Soshi", "Yogo"]);
L5R5E.families.set("unicorn", ["Ide", "Iuchi", "Moto", "Shinjo", "Utaku"]);
// Minors
L5R5E.families.set("mantis", ["(boat name)"]); // no family name, boat name
L5R5E.families.set("ronin", ["(ronin)"]); // can be anything
L5R5E.families.set("badger", ["Ichiro"]);
L5R5E.families.set("bat", ["Komori"]);
L5R5E.families.set("boar", ["Heichi"]);
L5R5E.families.set("dragonfly", ["Tonbo"]);
L5R5E.families.set("firefly", ["Hotaru"]);
L5R5E.families.set("hare", ["Ujina", "Usagi"]);
L5R5E.families.set("monkey", ["Toku", "Fuzake"]);
L5R5E.families.set("oriole", ["Tsi"]);
L5R5E.families.set("ox", ["Morito"]);
L5R5E.families.set("sparrow", ["Suzume"]);
L5R5E.families.set("tortoise", ["Kasuga"]);

View File

@@ -690,4 +690,123 @@ export class HelpersL5r5e {
}
return await table.drawMany(retrieve, opt);
}
/**
* Return the string without accents
* @param {string} str
* @return {string}
*/
static noAccents(str) {
return str.normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
}
/**
* Autocomplete method
* @param {jQuery} html HTML content of the sheet.
* @param {string} name Html name of the input
* @param {string[]} list Array of string to display
*/
static autocomplete(html, name, list = []) {
const inp = document.getElementsByName(name)?.[0];
if (list.length < 1) {
return;
}
let currentFocus;
const closeAllLists = (elmnt = null) => {
const collection = document.getElementsByClassName("autocomplete-items");
for (let item of collection) {
if (!elmnt || (elmnt !== item && elmnt !== inp)) {
item.parentNode.removeChild(item);
}
}
};
// execute a function when someone writes in the text field
inp.addEventListener("input", (inputEvent) => {
closeAllLists();
const val = inputEvent.target.value;
if (!val) {
return false;
}
currentFocus = -1;
// create a DIV element that will contain the items (values)
const listDiv = document.createElement("DIV");
listDiv.setAttribute("id", inputEvent.target.id + "autocomplete-list");
listDiv.setAttribute("class", "autocomplete-items");
// append the DIV element as a child of the autocomplete container
inputEvent.target.parentNode.appendChild(listDiv);
list.forEach((value, index) => {
if (
HelpersL5r5e.noAccents(value.substring(0, val.length).toLowerCase()) ===
HelpersL5r5e.noAccents(val.toLowerCase())
) {
const choiceDiv = document.createElement("DIV");
choiceDiv.setAttribute("data-id", index);
choiceDiv.innerHTML = `<strong>${value.substring(0, val.length)}</strong>${value.substring(
val.length
)}`;
choiceDiv.addEventListener("click", (clickEvent) => {
const selectedIndex = clickEvent.target.attributes["data-id"].value;
if (!list[selectedIndex]) {
return;
}
inp.value = list[selectedIndex];
closeAllLists();
});
listDiv.appendChild(choiceDiv);
}
});
});
// execute a function presses a key on the keyboard
inp.addEventListener("keydown", function (e) {
const collection = document.getElementById(this.id + "autocomplete-list")?.getElementsByTagName("div");
if (!collection) {
return;
}
switch (e.code) {
case "ArrowUp":
case "ArrowDown":
// focus index
currentFocus += e.code === "ArrowUp" ? -1 : 1;
if (currentFocus >= collection.length) {
currentFocus = 0;
}
if (currentFocus < 0) {
currentFocus = collection.length - 1;
}
// css classes
for (let item of collection) {
item.classList.remove("autocomplete-active");
}
collection[currentFocus]?.classList.add("autocomplete-active");
break;
case "Tab":
case "Enter":
e.preventDefault();
if (currentFocus > -1 && !!collection[currentFocus]) {
collection[currentFocus].click();
}
break;
case "Escape":
closeAllLists();
break;
} //swi
});
// execute a function when someone clicks in the document
html.on("focusout", (e) => {
console.log("aaaaaaaaaa");
closeAllLists(e.target);
});
}
}