forked from foundryvtt/pf2e
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split homebrew files (foundryvtt#15334)
- Loading branch information
1 parent
afa8795
commit 1331fa4
Showing
3 changed files
with
314 additions
and
301 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { WeaponTrait } from "@item/weapon/types.ts"; | ||
import { immunityTypes, resistanceTypes, weaknessTypes } from "@scripts/config/iwr.ts"; | ||
import { DamageType } from "@system/damage/types.ts"; | ||
import { | ||
BASE_DAMAGE_TYPES_TO_CATEGORIES, | ||
DAMAGE_TYPES, | ||
DAMAGE_TYPE_ICONS, | ||
ENERGY_DAMAGE_TYPES, | ||
PHYSICAL_DAMAGE_TYPES, | ||
} from "@system/damage/values.ts"; | ||
import { ErrorPF2e, sluggify, tupleHasValue } from "@util"; | ||
import * as R from "remeda"; | ||
import { CustomDamageData } from "./data.ts"; | ||
import { isHomebrewCustomDamage } from "./helpers.ts"; | ||
import { HomebrewElements } from "./menu.ts"; | ||
|
||
/** | ||
* To update all custom damage types in the system, we need to ensure that all collections are added to and cleaned. | ||
* This reduces the scope of all damage related operations so that its easier to identify when something goes wrong. | ||
*/ | ||
export class DamageTypeManager { | ||
// All collections that homebrew damage must be updated in | ||
collections = { | ||
physical: PHYSICAL_DAMAGE_TYPES as unknown as string[], | ||
energy: ENERGY_DAMAGE_TYPES as unknown as string[], | ||
DAMAGE_TYPES, | ||
BASE_DAMAGE_TYPES_TO_CATEGORIES, | ||
DAMAGE_TYPE_ICONS, | ||
damageTypesLocalization: CONFIG.PF2E.damageTypes, | ||
damageRollFlavorsLocalization: CONFIG.PF2E.damageRollFlavors, | ||
immunityTypes: immunityTypes as Record<string, string>, | ||
weaknessTypes: weaknessTypes as Record<string, string>, | ||
resistanceTypes: resistanceTypes as Record<string, string>, | ||
}; | ||
|
||
addCustomDamage(data: CustomDamageData, options: { slug?: string } = {}): void { | ||
const collections = this.collections; | ||
const slug = (options.slug ?? sluggify(data.label)) as DamageType; | ||
collections.DAMAGE_TYPES.add(slug); | ||
if (tupleHasValue(["physical", "energy"], data.category)) { | ||
collections[data.category].push(slug); | ||
} | ||
collections.BASE_DAMAGE_TYPES_TO_CATEGORIES[slug] = data.category ?? null; | ||
collections.DAMAGE_TYPE_ICONS[slug] = data.icon?.substring(3) ?? null; // icons registered do not include the fa- | ||
collections.damageTypesLocalization[slug] = data.label; | ||
|
||
const versatileLabel = game.i18n.format("PF2E.TraitVersatileX", { x: data.label }); | ||
CONFIG.PF2E.weaponTraits[`versatile-${slug}` as WeaponTrait] = versatileLabel; | ||
CONFIG.PF2E.npcAttackTraits[`versatile-${slug}` as WeaponTrait] = versatileLabel; | ||
|
||
const damageFlavor = game.i18n.localize(data.label).toLocaleLowerCase(game.i18n.lang); | ||
collections.damageRollFlavorsLocalization[slug] = damageFlavor; | ||
collections.immunityTypes[slug] = damageFlavor; | ||
collections.weaknessTypes[slug] = damageFlavor; | ||
collections.resistanceTypes[slug] = damageFlavor; | ||
} | ||
|
||
updateSettings(): void { | ||
const reservedTerms = HomebrewElements.reservedTerms; | ||
|
||
// Delete all existing homebrew damage types first | ||
const typesToDelete: Set<string> = DAMAGE_TYPES.filter((t) => !reservedTerms.damageTypes.has(t)); | ||
for (const collection of Object.values(this.collections)) { | ||
if (collection instanceof Set) { | ||
const types = [...collection].filter((t) => typesToDelete.has(t)); | ||
for (const damageType of types) collection.delete(damageType); | ||
} else { | ||
const types = Object.keys(collection).filter((t): t is keyof typeof collection => typesToDelete.has(t)); | ||
for (const damageType of types) delete collection[damageType]; | ||
} | ||
} | ||
|
||
// Delete versatile damage traits | ||
for (const type of typesToDelete) { | ||
const weaponTraits: Record<string, string> = CONFIG.PF2E.weaponTraits; | ||
const npcAttackTraits: Record<string, string> = CONFIG.PF2E.npcAttackTraits; | ||
delete weaponTraits[`versatile-${type}`]; | ||
delete npcAttackTraits[`versatile-${type}`]; | ||
} | ||
|
||
// Read module damage types | ||
const activeModules = [...game.modules.entries()].filter(([_key, foundryModule]) => foundryModule.active); | ||
for (const [key, foundryModule] of activeModules) { | ||
const homebrew = foundryModule.flags[key]?.["pf2e-homebrew"]; | ||
if (!R.isPlainObject(homebrew) || !homebrew.damageTypes) continue; | ||
|
||
const elements = homebrew.damageTypes; | ||
if (!R.isPlainObject(elements) || !isHomebrewCustomDamage(elements)) { | ||
console.warn(ErrorPF2e(`Homebrew record damageTypes is malformed in module ${key}`).message); | ||
continue; | ||
} | ||
|
||
for (const [slug, data] of Object.entries(elements)) { | ||
if (!reservedTerms.damageTypes.has(slug)) { | ||
this.addCustomDamage(data, { slug }); | ||
} else { | ||
console.warn( | ||
ErrorPF2e( | ||
`Homebrew damage type "${slug}" from module ${foundryModule.title} is a reserved term.`, | ||
).message, | ||
); | ||
continue; | ||
} | ||
} | ||
} | ||
|
||
// Read setting damage types | ||
const customTypes = game.settings | ||
.get("pf2e", "homebrew.damageTypes") | ||
.filter((t) => !reservedTerms.damageTypes.has(sluggify(t.label))); | ||
for (const data of customTypes) { | ||
this.addCustomDamage(data); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
import { LANGUAGES, LANGUAGE_RARITIES } from "@actor/creature/values.ts"; | ||
import { | ||
ErrorPF2e, | ||
SORTABLE_BASE_OPTIONS, | ||
htmlClosest, | ||
htmlQuery, | ||
htmlQueryAll, | ||
localizer, | ||
objectHasKey, | ||
setHasElement, | ||
sluggify, | ||
tupleHasValue, | ||
} from "@util"; | ||
import * as R from "remeda"; | ||
import Sortable from "sortablejs"; | ||
import { HomebrewTag, LanguageNotCommon, LanguageSettings, LanguageSettingsSheetData } from "./data.ts"; | ||
import { HomebrewElements } from "./menu.ts"; | ||
|
||
/** A helper class for managing languages and their rarities */ | ||
export class LanguagesManager { | ||
/** The parent settings menu */ | ||
menu: HomebrewElements; | ||
|
||
/** A separate list of module-provided languages */ | ||
moduleLanguages: LanguageNotCommon[]; | ||
|
||
constructor(menu: HomebrewElements) { | ||
this.menu = menu; | ||
|
||
const languagesFromSetting = game.settings.get("pf2e", "homebrew.languages").map((l) => l.id); | ||
this.moduleLanguages = R.keys | ||
.strict(CONFIG.PF2E.languages) | ||
.filter( | ||
(l): l is LanguageNotCommon => | ||
l !== "common" && !LANGUAGES.includes(l) && !languagesFromSetting.includes(l), | ||
); | ||
|
||
this.data.reset(); | ||
} | ||
|
||
get data(): LanguageSettings { | ||
return this.menu.cache.languageRarities; | ||
} | ||
|
||
getSheetData(): LanguageSettingsSheetData { | ||
const data = this.data.toObject(false); | ||
const homebrewLanguages = this.menu.cache.languages; | ||
return { | ||
commonLanguage: data.commonLanguage, | ||
...R.mapToObj([...LANGUAGE_RARITIES, "unavailable"] as const, (r) => [ | ||
r, | ||
data[r] | ||
.map((slug) => { | ||
const locKey = | ||
CONFIG.PF2E.languages[slug] ?? homebrewLanguages.find((l) => l.id === slug)?.value ?? slug; | ||
return { slug, label: game.i18n.localize(locKey) }; | ||
}) | ||
.sort((a, b) => a.label.localeCompare(b.label)), | ||
]), | ||
}; | ||
} | ||
|
||
/* -------------------------------------------- */ | ||
/* Event Listeners and Handlers */ | ||
/* -------------------------------------------- */ | ||
|
||
activateListeners(html: HTMLElement): void { | ||
const commonLanguageSelect = htmlQuery<HTMLSelectElement>(html, "select[data-common-language"); | ||
commonLanguageSelect?.addEventListener("change", async (event) => { | ||
event.stopPropagation(); | ||
const data = this.data; | ||
const newCommon = commonLanguageSelect.value || null; | ||
if (newCommon === null || setHasElement(data.common, newCommon)) { | ||
data.updateSource({ commonLanguage: newCommon }); | ||
} | ||
}); | ||
|
||
for (const list of htmlQueryAll(html, "ul[data-languages]")) { | ||
new Sortable(list, { | ||
...SORTABLE_BASE_OPTIONS, | ||
group: "languages", | ||
sort: false, | ||
swapThreshold: 1, | ||
onEnd: (event) => this.#onDropLanguage(event), | ||
}); | ||
} | ||
|
||
const rarities: readonly string[] = LANGUAGE_RARITIES; | ||
const localize = localizer("PF2E.SETTINGS.Homebrew.Languages.Rarities"); | ||
for (const raritySection of htmlQueryAll(html, ".form-group.language-rarity")) { | ||
const rarity = Array.from(raritySection.classList).find((c) => rarities.includes(c)) ?? "unavailable"; | ||
if (rarity === "unavailable") continue; | ||
const labelEl = raritySection.querySelector("label"); | ||
if (!labelEl) throw ErrorPF2e(""); | ||
|
||
labelEl.innerHTML = localize(sluggify(rarity, { camel: "bactrian" })); | ||
game.pf2e.TextEditor.convertXMLNode(labelEl, "rarity", { classes: ["tag", "rarity", rarity] }); | ||
} | ||
} | ||
|
||
async #onDropLanguage(event: Sortable.SortableEvent): Promise<void> { | ||
const droppedEl = event.item; | ||
const dropTarget = htmlClosest(droppedEl, "ul[data-languages]"); | ||
const oldRarity = droppedEl.dataset.rarity; | ||
const newRarity = dropTarget?.dataset.rarity; | ||
if (!(oldRarity && newRarity)) throw ErrorPF2e("Unexpected update to language rarities"); | ||
if (oldRarity === newRarity) return; | ||
|
||
const language = droppedEl.dataset.slug; | ||
if (!objectHasKey(CONFIG.PF2E.languages, language) || language === "common") { | ||
throw ErrorPF2e("Unexpected update to language rarities"); | ||
} | ||
|
||
const data = this.data; | ||
const source = data.toObject(); | ||
const commonLanguageSelect = htmlQuery<HTMLSelectElement>(this.menu.form, "select[data-common-language]"); | ||
if (!commonLanguageSelect) throw ErrorPF2e("Unexpected error updating menu"); | ||
|
||
const rarities = ["uncommon", "rare", "secret", "unavailable"] as const; | ||
|
||
if (newRarity === "common") { | ||
for (const rarity of rarities) { | ||
source[rarity].findSplice((l) => l === language); | ||
} | ||
// Add `commonLanguageOption` without full re-render | ||
const newOption = document.createElement("option"); | ||
newOption.value = language; | ||
newOption.textContent = droppedEl.textContent; | ||
commonLanguageSelect.append(newOption); | ||
} else { | ||
if (!tupleHasValue(rarities, newRarity)) { | ||
throw ErrorPF2e("Unexpected update to language rarities"); | ||
} | ||
for (const rarity of rarities) { | ||
source[rarity].findSplice((l) => l === language); | ||
} | ||
source[newRarity].push(language); | ||
source[newRarity].sort(); | ||
source.commonLanguage = source.commonLanguage === language ? null : source.commonLanguage; | ||
|
||
// Remove `commonLanguage` option without full re-render | ||
if (commonLanguageSelect.value === language) commonLanguageSelect.value = ""; | ||
const option = htmlQuery<HTMLOptionElement>(commonLanguageSelect, `option[value=${language}]`); | ||
option?.remove(); | ||
} | ||
|
||
droppedEl.dataset.rarity = newRarity; | ||
data.updateSource(source); | ||
} | ||
|
||
/** Update the language rarities cache, adding and deleting from sets as necessary. */ | ||
onChangeHomebrewLanguages(languages: HomebrewTag<"languages">[]): void { | ||
const data = this.data; | ||
const source = data.toObject(); | ||
const languageSet = new Set(LANGUAGES); | ||
const updatedLanguages = [...this.moduleLanguages, ...languages.map((l) => l.id)]; | ||
|
||
let render = false; | ||
|
||
if ( | ||
source.commonLanguage && | ||
!languageSet.has(source.commonLanguage) && | ||
!updatedLanguages.includes(source.commonLanguage) | ||
) { | ||
source.commonLanguage = null; | ||
render = true; | ||
} | ||
|
||
for (const rarity of ["uncommon", "rare", "secret", "unavailable"] as const) { | ||
for (const language of source[rarity]) { | ||
if (!languageSet.has(language) && !updatedLanguages.includes(language)) { | ||
source[rarity].findSplice((l) => l === language); | ||
render = true; | ||
} | ||
} | ||
} | ||
data.updateSource(source); | ||
|
||
for (const language of data.common) { | ||
if (!languageSet.has(language) && !updatedLanguages.includes(language)) { | ||
data.common.delete(language); | ||
render = true; | ||
} | ||
} | ||
|
||
for (const language of updatedLanguages) { | ||
if (!LANGUAGE_RARITIES.some((r) => data[r].has(language))) { | ||
data.common.add(language); | ||
render = true; | ||
} | ||
} | ||
|
||
if (render) this.menu.render(); | ||
} | ||
} |
Oops, something went wrong.