Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start refactoring character sheet get data #15476

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
277 changes: 142 additions & 135 deletions src/module/actor/character/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,6 @@ class CharacterSheetPF2e<TActor extends CharacterPF2e> extends CreatureSheetPF2e
if (tab) tab.initial = "biography";
}

sheetData.numberToRank = R.mapToObj([0, 1, 2, 3, 4] as const, (n) => [
n,
game.i18n.localize(`PF2E.ProficiencyLevel${n}`),
]);

sheetData.senses = condenseSenses(this.actor.perception.senses.contents);

// Attacks and defenses
// Prune untrained martial proficiencies
for (const section of ["attacks", "defenses"] as const) {
Expand All @@ -136,7 +129,8 @@ class CharacterSheetPF2e<TActor extends CharacterPF2e> extends CreatureSheetPF2e
}
}
}
sheetData.martialProficiencies = {

const martialProficiencies = {
attacks: sortLabeledRecord(
R.mapValues(sheetData.data.proficiencies.attacks as Record<string, MartialProficiency>, (data, key) => {
const groupMatch = /^weapon-group-([-\w]+)$/.exec(key);
Expand Down Expand Up @@ -179,21 +173,8 @@ class CharacterSheetPF2e<TActor extends CharacterPF2e> extends CreatureSheetPF2e
),
};

// A(H)BCD
sheetData.ancestry = actor.ancestry;
sheetData.heritage = actor.heritage;
sheetData.background = actor.background;
sheetData.class = actor.class;
sheetData.deity = actor.deity;

// Update hero points label
sheetData.data.resources.heroPoints.hover = game.i18n.format(
actor.heroPoints.value === 1 ? "PF2E.HeroPointRatio.One" : "PF2E.HeroPointRatio.Many",
actor.heroPoints,
);

// Indicate whether the PC has all attribute boosts allocated
sheetData.attributeBoostsAllocated = ((): boolean => {
const attributeBoostsAllocated = ((): boolean => {
const { build } = sheetData.data;
if (build.attributes.manual || !isReallyPC(actor)) {
return true;
Expand Down Expand Up @@ -221,38 +202,6 @@ class CharacterSheetPF2e<TActor extends CharacterPF2e> extends CreatureSheetPF2e
);
})();

// Class DCs
const allClassDCs = Object.values(sheetData.data.proficiencies.classDCs);
const classDCs = allClassDCs
.filter((cdc) => cdc.rank > 0 || allClassDCs.length > 1)
.map(
(classDC): ClassDCSheetData => ({
...classDC,
icon: this.getProficiencyIcon(classDC.rank),
hover: CONFIG.PF2E.proficiencyLevels[classDC.rank],
}),
)
.sort((a, b) => (a.primary ? -1 : b.primary ? 1 : a.slug.localeCompare(b.slug)));
const primaryClassDC = sheetData.data.attributes.classDC?.slug ?? null;

sheetData.classDCs = {
dcs: classDCs,
primary: primaryClassDC,
perDCDetails: classDCs.length > 1 || !primaryClassDC,
};

// Acquire all unselected apex attribute options
const abpEnabled = game.pf2e.variantRules.AutomaticBonusProgression.isEnabled(actor);
sheetData.apexAttributeOptions = abpEnabled
? []
: this.actor.inventory.contents.flatMap((e) =>
e.system.apex?.selected === false &&
e.isInvested &&
e.system.apex.attribute !== actor.system.build.attributes.apex
? e.system.apex.attribute
: [],
);

// Spellcasting
const collectionGroups: Record<SpellcastingTabSlug, SpellcastingSheetData[]> = fu.mergeObject(
{ "known-spells": [], rituals: [], activations: [] },
Expand All @@ -263,13 +212,6 @@ class CharacterSheetPF2e<TActor extends CharacterPF2e> extends CreatureSheetPF2e
}),
);

sheetData.magicTraditions = CONFIG.PF2E.magicTraditions;
sheetData.preparationType = CONFIG.PF2E.preparationType;
sheetData.spellCollectionGroups = collectionGroups;
sheetData.hasNormalSpellcasting = sheetData.spellCollectionGroups["known-spells"].some(
(s) => s.usesSpellProficiency,
);

// ensure saves are displayed in the following order:
sheetData.data.saves = {
fortitude: sheetData.data.saves.fortitude,
Expand All @@ -285,25 +227,13 @@ class CharacterSheetPF2e<TActor extends CharacterPF2e> extends CreatureSheetPF2e
// Is the character's key ability score overridden by an Active Effect?
sheetData.data.details.keyability.singleOption = actor.class?.system.keyAbility.value.length === 1;

// Is the stamina variant rule enabled?
sheetData.hasStamina = game.pf2e.settings.variants.stamina;
sheetData.actions = this.#prepareAbilities();
sheetData.feats = [...actor.feats, actor.feats.bonus];

const craftingFormulas = await actor.getCraftingFormulas();
const formulasByLevel = R.groupBy(craftingFormulas, (f) => f.level);
const flags = actor.flags.pf2e;
const hasQuickAlchemy = !!(
actor.rollOptions.all["feature:quick-alchemy"] || actor.rollOptions.all["feat:quick-alchemy"]
);

sheetData.crafting = {
noCost: flags.freeCrafting,
hasQuickAlchemy,
knownFormulas: formulasByLevel,
entries: await this.#prepareCraftingEntries(craftingFormulas),
};

this.#knownFormulas = Object.values(formulasByLevel)
.flat()
.reduce((result: Record<string, CraftingFormula>, entry) => {
Expand All @@ -312,50 +242,6 @@ class CharacterSheetPF2e<TActor extends CharacterPF2e> extends CreatureSheetPF2e
return result;
}, {});

sheetData.abpEnabled = AutomaticBonusProgression.isEnabled(actor);

sheetData.languages = ((): LanguageSheetData[] => {
const languagesBuild = actor.system.build.languages;
const sourceLanguages = actor._source.system.details.languages.value
.filter((l) => l in CONFIG.PF2E.languages)
.sort();
const isOverMax = languagesBuild.value > languagesBuild.max;
const languageSlugs = actor.system.details.languages.value;
const commonLanguage = game.pf2e.settings.campaign.languages.commonLanguage;
const localizedLanguages: LanguageSheetData[] = languageSlugs.flatMap((language) => {
if (language === commonLanguage && languageSlugs.includes("common")) {
return [];
}
const label =
language === "common" && commonLanguage
? game.i18n.format("PF2E.Actor.Creature.Language.CommonLanguage", {
language: game.i18n.localize(CONFIG.PF2E.languages[commonLanguage]),
})
: game.i18n.localize(CONFIG.PF2E.languages[language]);
return { slug: language, label, tooltip: null, overLimit: false };
});

// If applicable, mark languages at the end as being over-limit
const sortedLanguages = localizedLanguages.sort((a, b) => a.label.localeCompare(b.label));
const commonFirst = R.sortBy(sortedLanguages, (l) => l.slug !== "common");
for (const language of commonFirst.filter((l) => l.slug && sourceLanguages.includes(l.slug)).reverse()) {
if (!language.slug) continue;
language.overLimit = isOverMax && sourceLanguages.indexOf(language.slug) + 1 > languagesBuild.max;
language.tooltip = language.overLimit
? game.i18n.localize("PF2E.Actor.Character.Language.OverLimit")
: null;
}

const unallocatedLabel = game.i18n.localize("PF2E.Actor.Character.Language.Unallocated.Label");
const unallocatedTooltip = game.i18n.localize("PF2E.Actor.Character.Language.Unallocated.Tooltip");
const unallocatedLanguages = Array.fromRange(Math.max(0, languagesBuild.max - languagesBuild.value)).map(
() => ({ slug: null, label: unallocatedLabel, tooltip: unallocatedTooltip, overLimit: false }),
);
commonFirst.push(...unallocatedLanguages);

return commonFirst;
})();

// Sort skills by localized label
sheetData.data.skills = Object.fromEntries(
Object.entries(sheetData.data.skills).sort(([_keyA, skillA], [_keyB, skillB]) =>
Expand All @@ -365,9 +251,7 @@ class CharacterSheetPF2e<TActor extends CharacterPF2e> extends CreatureSheetPF2e
),
) as Record<SkillSlug, CharacterSkillData>;

sheetData.tabVisibility = fu.deepClone(actor.flags.pf2e.sheetTabs);

// Enrich content
// Create biography and enrich content
const rollData = actor.getRollData();
const biography = (sheetData.biography = actor.system.details.biography);
const enrichmentOptions = { rollData, secrets: actor.isOwner, async: true };
Expand All @@ -384,27 +268,88 @@ class CharacterSheetPF2e<TActor extends CharacterPF2e> extends CreatureSheetPF2e
sheetData.enrichedContent[key] = await content;
}

// Elemental Blasts
try {
const action = new ElementalBlast(this.actor);
const blastData = (await Promise.all(action.configs.map((c) => this.#getBlastData(action, c)))).sort(
(a, b) => a.label.localeCompare(b.label, game.i18n.lang),
);
sheetData.elementalBlasts = blastData;
} catch (error) {
if (BUILD_MODE === "development") console.error(error);
sheetData.elementalBlasts = [];
}
const elementalBlasts = await (async () => {
try {
const action = new ElementalBlast(this.actor);
const blastData = (await Promise.all(action.configs.map((c) => this.#getBlastData(action, c)))).sort(
(a, b) => a.label.localeCompare(b.label, game.i18n.lang),
);
return blastData;
} catch (error) {
if (BUILD_MODE === "development") console.error(error);
return [];
}
})();

return {
...sheetData,

numberToRank: R.mapToObj([0, 1, 2, 3, 4] as const, (n) => [
n,
game.i18n.localize(`PF2E.ProficiencyLevel${n}`),
]),
magicTraditions: CONFIG.PF2E.magicTraditions,
preparationType: CONFIG.PF2E.preparationType,

// A(H)BCD
ancestry: actor.ancestry,
heritage: actor.heritage,
background: actor.background,
class: actor.class,
deity: actor.deity,

senses: condenseSenses(this.actor.perception.senses.contents),
languages: this.#prepareLanguages(),
speeds: this.#prepareSpeed(),

martialProficiencies,
attributeBoostsAllocated,

classDCs: this.#prepareClassDCs(),

// Acquire all unselected apex attribute options
apexAttributeOptions: game.pf2e.variantRules.AutomaticBonusProgression.isEnabled(actor)
? []
: this.actor.inventory.contents.flatMap((e) =>
e.system.apex?.selected === false &&
e.isInvested &&
e.system.apex.attribute !== actor.system.build.attributes.apex
? e.system.apex.attribute
: [],
),

spellCollectionGroups: collectionGroups,
hasNormalSpellcasting: collectionGroups["known-spells"].some((s) => s.usesSpellProficiency),

abpEnabled: AutomaticBonusProgression.isEnabled(actor),
hasStamina: game.pf2e.settings.variants.stamina,

actions: this.#prepareAbilities(),
feats: [...actor.feats, actor.feats.bonus],

crafting: {
noCost: flags.freeCrafting,
hasQuickAlchemy,
knownFormulas: formulasByLevel,
entries: await this.#prepareCraftingEntries(craftingFormulas),
},

tabVisibility: fu.deepClone(actor.flags.pf2e.sheetTabs),

elementalBlasts,
};
}

// Speed
#prepareSpeed() {
const speedIcons = {
land: "person-running",
swim: "person-swimming",
climb: "mountain",
fly: "feather-pointed",
burrow: "water-ladder",
};
sheetData.speeds = R.keys.strict(speedIcons).map((slug): SpeedSheetData => {

return R.keys.strict(speedIcons).map((slug): SpeedSheetData => {
const speed = this.actor.system.attributes.speed;
const data = slug === "land" ? speed : speed.otherSpeeds.find((s) => s.type === slug);
return {
Expand All @@ -416,9 +361,71 @@ class CharacterSheetPF2e<TActor extends CharacterPF2e> extends CreatureSheetPF2e
breakdown: slug === "land" ? speed.breakdown : null,
};
});
}

#prepareLanguages(): LanguageSheetData[] {
const actor = this.actor;
const languagesBuild = actor.system.build.languages;
const sourceLanguages = actor._source.system.details.languages.value
.filter((l) => l in CONFIG.PF2E.languages)
.sort();
const isOverMax = languagesBuild.value > languagesBuild.max;
const languageSlugs = actor.system.details.languages.value;
const commonLanguage = game.pf2e.settings.campaign.languages.commonLanguage;
const localizedLanguages: LanguageSheetData[] = languageSlugs.flatMap((language) => {
if (language === commonLanguage && languageSlugs.includes("common")) {
return [];
}
const label =
language === "common" && commonLanguage
? game.i18n.format("PF2E.Actor.Creature.Language.CommonLanguage", {
language: game.i18n.localize(CONFIG.PF2E.languages[commonLanguage]),
})
: game.i18n.localize(CONFIG.PF2E.languages[language]);
return { slug: language, label, tooltip: null, overLimit: false };
});

// If applicable, mark languages at the end as being over-limit
const sortedLanguages = localizedLanguages.sort((a, b) => a.label.localeCompare(b.label));
const commonFirst = R.sortBy(sortedLanguages, (l) => l.slug !== "common");
for (const language of commonFirst.filter((l) => l.slug && sourceLanguages.includes(l.slug)).reverse()) {
if (!language.slug) continue;
language.overLimit = isOverMax && sourceLanguages.indexOf(language.slug) + 1 > languagesBuild.max;
language.tooltip = language.overLimit
? game.i18n.localize("PF2E.Actor.Character.Language.OverLimit")
: null;
}

const unallocatedLabel = game.i18n.localize("PF2E.Actor.Character.Language.Unallocated.Label");
const unallocatedTooltip = game.i18n.localize("PF2E.Actor.Character.Language.Unallocated.Tooltip");
const unallocatedLanguages = Array.fromRange(Math.max(0, languagesBuild.max - languagesBuild.value)).map(
() => ({ slug: null, label: unallocatedLabel, tooltip: unallocatedTooltip, overLimit: false }),
);
commonFirst.push(...unallocatedLanguages);

return commonFirst;
}

#prepareClassDCs() {
const actor = this.actor;
const allClassDCs = Object.values(actor.system.proficiencies.classDCs);
const classDCs = allClassDCs
.filter((cdc) => cdc.rank > 0 || allClassDCs.length > 1)
.map(
(classDC): ClassDCSheetData => ({
...classDC,
icon: this.getProficiencyIcon(classDC.rank),
hover: CONFIG.PF2E.proficiencyLevels[classDC.rank],
}),
)
.sort((a, b) => (a.primary ? -1 : b.primary ? 1 : a.slug.localeCompare(b.slug)));
const primaryClassDC = actor.attributes.classDC?.slug ?? null;

// Return data for rendering
return sheetData;
return {
dcs: classDCs,
primary: primaryClassDC,
perDCDetails: classDCs.length > 1 || !primaryClassDC,
};
}

/** Organize and classify Items for Character sheets */
Expand Down
Loading