import { LESOUBLIES_CONFIG } from "./les-oublies-config.js" import { LesOubliesUtility } from "./les-oublies-utility.js" import { LesOubliesRolls } from "./les-oublies-rolls.js" export class LesOubliesActor extends Actor { static CREATION_ITEM_TYPES = new Set(["race", "tribu", "metier"]) static THREAD_RESOURCE_KEYS = new Set(["songesThreads", "cauchemarThreads", "emptyGlobes"]) prepareDerivedData() { super.prepareDerivedData() if (this.type === "personnage") { const system = this.system const sizeValue = Math.clamp(Number(system.size?.value ?? 1), 1, 4) const hpMax = Math.max(sizeValue * 4 + Number(system.hp?.bonus ?? 0), 0) system.hp.max = hpMax system.hp.value = Math.min(Number(system.hp.value ?? hpMax), hpMax) const songesValue = Number(system.songes?.value ?? 0) const cauchemarValue = Number(system.cauchemar?.value ?? 0) const totals = LesOubliesUtility.computeDreamPointTotals(songesValue, cauchemarValue) system.songes.max = totals.songesPoints system.cauchemar.max = totals.cauchemarPoints system.songes.points = Math.clamp(Number(system.songes.points ?? totals.songesPoints), 0, totals.songesPoints) system.cauchemar.points = Math.clamp(Number(system.cauchemar.points ?? totals.cauchemarPoints), 0, totals.cauchemarPoints) system.reserves.songesThreads = Math.max(Number(system.reserves?.songesThreads ?? 0), 0) system.reserves.cauchemarThreads = Math.max(Number(system.reserves?.cauchemarThreads ?? 0), 0) system.reserves.emptyGlobes = Math.max(Number(system.reserves?.emptyGlobes ?? 0), 0) return } if (this.type === "compagnie") { const system = this.system system.power.sharedDreamPoints = Math.max(Number(system.power?.sharedDreamPoints ?? 0), 0) system.reserves.songesThreads = Math.max(Number(system.reserves?.songesThreads ?? 0), 0) system.reserves.cauchemarThreads = Math.max(Number(system.reserves?.cauchemarThreads ?? 0), 0) system.reserves.emptyGlobes = Math.max(Number(system.reserves?.emptyGlobes ?? 0), 0) return } if (this.type !== "creature") return const system = this.system const sizeValue = Math.clamp(Number(system.size?.value ?? 1), 1, 8) const hpMax = Math.max(sizeValue * 4, 0) const hpValue = Math.max(Number(system.hp?.value ?? hpMax), 0) system.hp.max = hpMax system.hp.value = Math.min(hpValue, hpMax) const songesPoints = Math.max(Number(system.songes?.points ?? 0), 0) const cauchemarPoints = Math.max(Number(system.cauchemar?.points ?? 0), 0) system.songes.max = Math.max(Number(system.songes?.max ?? songesPoints), songesPoints) system.cauchemar.max = Math.max(Number(system.cauchemar?.max ?? cauchemarPoints), cauchemarPoints) system.songes.points = Math.min(songesPoints, system.songes.max) system.cauchemar.points = Math.min(cauchemarPoints, system.cauchemar.max) } getProfileValue(profileKey) { return Number(this.system.profils?.[profileKey] ?? 0) } getCreationItem(type) { if (!LesOubliesActor.CREATION_ITEM_TYPES.has(type)) return this.items.find((item) => item.type === type) ?? null const referenceId = this.system.references?.[`${type}Id`] ?? "" if (referenceId) { const referencedItem = this.items.get(referenceId) if (referencedItem?.type === type) return referencedItem } return this.items.find((item) => item.type === type) ?? null } getEmbeddedItems(type) { const items = this.itemTypes?.[type] ?? this.items.filter((item) => item.type === type) return LesOubliesUtility.sortByName(items) } async assignCreationItem(sourceItem) { if (!sourceItem || !LesOubliesActor.CREATION_ITEM_TYPES.has(sourceItem.type)) return null const previousItem = this.getCreationItem(sourceItem.type) const itemData = sourceItem.toObject() delete itemData._id const existingIds = this.getEmbeddedItems(sourceItem.type).map((item) => item.id) if (existingIds.length) { await this.deleteEmbeddedDocuments("Item", existingIds, { renderSheet: false }) } const [createdItem] = await this.createEmbeddedDocuments("Item", [itemData], { renderSheet: false }) if (!createdItem) return null await this.update({ [`system.references.${sourceItem.type}Id`]: createdItem.id, }) if (sourceItem.type === "race") { await this.syncRaceProfiles({ currentRace: createdItem }) await this.syncRaceDomains({ currentRace: createdItem, previousRace: previousItem }) } return createdItem } async clearCreationItem(type) { if (!LesOubliesActor.CREATION_ITEM_TYPES.has(type)) return const previousItem = this.getCreationItem(type) const existingIds = this.getEmbeddedItems(type).map((item) => item.id) if (existingIds.length) { await this.deleteEmbeddedDocuments("Item", existingIds, { renderSheet: false }) } await this.update({ [`system.references.${type}Id`]: "", }) if (type === "race") { await this.syncRaceProfiles({ currentRace: null }) await this.syncRaceDomains({ currentRace: null, previousRace: previousItem }) } } getCompagnie() { const compagnieId = this.system.references?.compagnieId return compagnieId ? game.actors.get(compagnieId) ?? null : null } getThreadReserveOwner(source = "actor") { if (source === "company" || source === "compagnie") return this.getCompagnie() return this } getThreadReserves(source = "actor") { const owner = this.getThreadReserveOwner(source) return { owner, songesThreads: Math.max(Number(owner?.system?.reserves?.songesThreads ?? 0), 0), cauchemarThreads: Math.max(Number(owner?.system?.reserves?.cauchemarThreads ?? 0), 0), emptyGlobes: Math.max(Number(owner?.system?.reserves?.emptyGlobes ?? 0), 0), } } async transferThreadReserve(resourceKey, amount, direction = "toCompany") { if (!LesOubliesActor.THREAD_RESOURCE_KEYS.has(resourceKey)) return false const company = this.getCompagnie() if (!company) return false const transferAmount = Math.max(Math.trunc(Number(amount ?? 0)), 0) if (transferAmount < 1) return false const fromActor = direction === "toCompany" ? this : company const toActor = direction === "toCompany" ? company : this const current = Math.max(Number(fromActor.system?.reserves?.[resourceKey] ?? 0), 0) if (current < transferAmount) return false const path = `system.reserves.${resourceKey}` const targetCurrent = Math.max(Number(toActor.system?.reserves?.[resourceKey] ?? 0), 0) await fromActor.update({ [path]: current - transferAmount }) await toActor.update({ [path]: targetCurrent + transferAmount }) return true } getCompetenceByKey(skillKey) { return this.getEmbeddedItems("competence").find((item) => item.system.key === skillKey) ?? null } getRaceLanguageDomains(race = this.getCreationItem("race")) { return LesOubliesUtility.uniqueStrings(race?.system?.languageDomains ?? []) } getRaceProfiles(race = this.getCreationItem("race")) { const profiles = LesOubliesUtility.createEmptyProfiles() for (const key of Object.keys(profiles)) { profiles[key] = Math.trunc(Number(race?.system?.profiles?.[key] ?? 0)) } return profiles } async syncRaceProfiles({ currentRace = this.getCreationItem("race") } = {}) { if (this.type !== "personnage") return false const profiles = this.getRaceProfiles(currentRace) const updateData = Object.fromEntries( Object.entries(profiles).map(([key, value]) => [`system.profils.${key}`, value]), ) await this.update(updateData) if (currentRace) { ui.notifications.info(`Profils raciaux appliqués depuis ${currentRace.name}.`) } return true } async syncRaceDomains({ currentRace = this.getCreationItem("race"), previousRace = null } = {}) { if (this.type !== "personnage") return false const competence = this.getCompetenceByKey("langues") if (!competence) return false const currentAutoDomains = LesOubliesUtility.uniqueStrings(competence.system.fixedDomains ?? []) const previousRaceDomains = previousRace ? this.getRaceLanguageDomains(previousRace) : currentAutoDomains const autoDomainsToReplace = currentAutoDomains.length ? currentAutoDomains : previousRaceDomains const nextAutoDomains = this.getRaceLanguageDomains(currentRace) const manualDomains = LesOubliesUtility.uniqueStrings( (competence.system.domains ?? []).filter((domain) => !autoDomainsToReplace.includes(domain)), ) await competence.update({ "system.fixedDomains": nextAutoDomains, "system.domains": LesOubliesUtility.uniqueStrings([...manualDomains, ...nextAutoDomains]), }) return true } getSkillScoreByKey(skillKey) { const competence = this.getCompetenceByKey(skillKey) return competence ? this.computeSkillValue(competence) : 0 } computeSkillValue(item) { const base = Number(item.system.base ?? 0) const profileValue = this.getProfileValue(item.system.profileKey) if (item.system.closed && base === 0) return 0 return base + profileValue } getCompetences() { return this.getEmbeddedItems("competence").map((item) => ({ item, finalValue: this.computeSkillValue(item), profileLabel: LESOUBLIES_CONFIG.profileLabels[item.system.profileKey] ?? item.system.profileKey, domains: LesOubliesUtility.uniqueStrings(item.system.domains ?? []), fixedDomains: LesOubliesUtility.uniqueStrings(item.system.fixedDomains ?? []), })) } getGroupedCompetences() { return LESOUBLIES_CONFIG.profiles.map((profile) => ({ ...profile, profileValue: this.getProfileValue(profile.id), items: this.getCompetences().filter((entry) => entry.item.system.profileKey === profile.id), })) } getDerivedOverview() { const hpValue = Number(this.system.hp?.value ?? 0) const hpMax = Number(this.system.hp?.max ?? 0) const hpDisplay = this.type === "creature" ? (this.system.hp?.display || (hpValue === hpMax ? String(hpValue) : `${hpValue}/${hpMax}`)) : `${hpValue}/${hpMax}` return { sizeLabel: LESOUBLIES_CONFIG.sizes[this.system.size?.value] ?? this.system.size?.value, hpMax, hpValue, hpDisplay, songesMax: this.system.songes?.max ?? this.system.songes?.points ?? 0, cauchemarMax: this.system.cauchemar?.max ?? this.system.cauchemar?.points ?? 0, songesPoints: this.system.songes?.points ?? 0, cauchemarPoints: this.system.cauchemar?.points ?? 0, reserves: this.getThreadReserves(), companyReserves: this.getThreadReserves("company"), race: this.getCreationItem("race"), tribu: this.getCreationItem("tribu"), metier: this.getCreationItem("metier"), compagnie: this.getCompagnie(), } } async openTestRollDialog(preset = {}) { return LesOubliesRolls.openTestDialog(this, preset) } async openConfrontationRollDialog() { return LesOubliesRolls.openConfrontationDialog(this) } async openInitiativeRollDialog() { return LesOubliesRolls.openInitiativeDialog(this) } async openAttackRollDialog({ itemId = null, mode = null } = {}) { return LesOubliesRolls.openAttackDialog(this, { itemId, mode }) } async openDamageDialog({ itemId = null } = {}) { return LesOubliesRolls.openDamageDialog(this, { itemId }) } async openSpellActivationDialog(itemId) { return LesOubliesRolls.openSpellDialog(this, itemId) } async openCombatPresetDialog(actionKey) { return LesOubliesRolls.openCombatPresetDialog(this, actionKey) } async openThreadHarvestDialog() { return LesOubliesRolls.openThreadHarvestDialog(this) } async rollProfile(profileKey) { return this.openTestRollDialog({ label: LESOUBLIES_CONFIG.profileLabels[profileKey] ?? profileKey, score: this.getProfileValue(profileKey), difficulty: 0, rollMode: LesOubliesRolls.getDefaultRollMode(this), }) } async rollCompetence(itemId) { const item = this.items.get(itemId) if (!item) return null return this.openTestRollDialog({ label: item.name, score: this.computeSkillValue(item), difficulty: 0, rollMode: LesOubliesRolls.getDefaultRollMode(this), }) } }