- Profils raciaux appliqués automatiquement - DsN opératonnel - Gestion plus fine des fils/orbes

This commit is contained in:
2026-05-04 08:09:27 +02:00
parent 320b2941dc
commit 3534bdf181
68 changed files with 2199 additions and 24 deletions
@@ -37,6 +37,7 @@ export default class LesOubliesActorSheet extends HandlebarsApplicationMixin(fou
openCombatPreset: LesOubliesActorSheet.#onOpenCombatPreset,
openThreadHarvest: LesOubliesActorSheet.#onOpenThreadHarvest,
openLinkedActor: LesOubliesActorSheet.#onOpenLinkedActor,
transferThread: LesOubliesActorSheet.#onTransferThread,
},
}
@@ -217,4 +218,26 @@ export default class LesOubliesActorSheet extends HandlebarsApplicationMixin(fou
const actor = game.actors.get(actorId)
if (actor) actor.sheet.render(true)
}
static async #onTransferThread(event, target) {
const resourceKey = target.dataset.resourceKey
const direction = target.dataset.direction || "toCompany"
if (!resourceKey || !this.document?.transferThreadReserve) return
const row = target.closest("[data-transfer-row]")
const amountField = row?.querySelector?.("[data-transfer-amount]")
const amount = Math.max(Math.trunc(Number(amountField?.value ?? 1)), 0)
if (amount < 1) {
ui.notifications.warn("Indiquez une quantité à transférer.")
return
}
const success = await this.document.transferThreadReserve(resourceKey, amount, direction)
if (!success) {
ui.notifications.warn("Transfert impossible avec les réserves actuelles.")
return
}
this.render()
}
}
@@ -17,7 +17,7 @@ export default class LesOubliesCompagnieSheet extends LesOubliesActorSheet {
static PARTS = {
sheet: {
template: "systems/fvtt-les-oublies/templates/actor-compagnie-sheet-v4.hbs",
template: "systems/fvtt-les-oublies/templates/actor-compagnie-sheet-v5.hbs",
},
}
@@ -3,7 +3,7 @@ import LesOubliesItemSheet from "./base-item-sheet.mjs"
export default class LesOubliesCompetenceSheet extends LesOubliesItemSheet {
static PARTS = {
sheet: {
template: "systems/fvtt-les-oublies/templates/item-competence-sheet.hbs",
template: "systems/fvtt-les-oublies/templates/item-competence-sheet-v2.hbs",
},
}
}
@@ -20,7 +20,7 @@ export default class LesOubliesPersonnageSheet extends LesOubliesActorSheet {
static PARTS = {
sheet: {
template: "systems/fvtt-les-oublies/templates/actor-personnage-sheet-v14.hbs",
template: "systems/fvtt-les-oublies/templates/actor-personnage-sheet-v18.hbs",
},
}
+114
View File
@@ -4,6 +4,7 @@ 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()
@@ -22,6 +23,18 @@ export class LesOubliesActor extends Actor {
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
}
@@ -62,6 +75,7 @@ export class LesOubliesActor extends Actor {
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
@@ -77,12 +91,19 @@ export class LesOubliesActor extends Actor {
[`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 })
@@ -91,6 +112,11 @@ export class LesOubliesActor extends Actor {
await this.update({
[`system.references.${type}Id`]: "",
})
if (type === "race") {
await this.syncRaceProfiles({ currentRace: null })
await this.syncRaceDomains({ currentRace: null, previousRace: previousItem })
}
}
getCompagnie() {
@@ -98,10 +124,94 @@ export class LesOubliesActor extends Actor {
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
@@ -119,6 +229,8 @@ export class LesOubliesActor extends Actor {
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 ?? []),
}))
}
@@ -146,6 +258,8 @@ export class LesOubliesActor extends Actor {
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"),
+208 -9
View File
@@ -439,7 +439,12 @@ export class LesOubliesRolls {
const data = await this.#promptSpellOptions(actor, spell)
if (!data) return null
const activation = await this.#withActorLock(`spell:${actor.id}`, async () => {
const paymentMode = String(data.paymentMode || "points")
const paymentSource = this.#normalizeThreadReserveSource(data.paymentSource)
const paymentOwner = paymentMode === "fils"
? this.#getThreadReserveOwner(actor, paymentSource)
: actor
const activation = await this.#withActorLock(`spell:${paymentOwner?.id ?? actor.id}:${paymentMode}`, async () => {
const skill = actor.getCompetenceByKey?.(spell.system.skillKey) ?? null
const skillBase = Number(skill?.system?.base ?? 0)
if (skillBase < 1) {
@@ -450,7 +455,6 @@ export class LesOubliesRolls {
const métierMatch = this.#actorMatchesSpellGrant(actor, spell)
const surcharge = !métierMatch
const effectiveCost = Number(data.actualCost ?? 0) * (surcharge ? 2 : 1)
const paymentMode = String(data.paymentMode || "points")
if (paymentMode === "points") {
const resource = spell.system.polarity || "songes"
const available = Number(actor.system?.[resource]?.points ?? 0)
@@ -468,9 +472,43 @@ export class LesOubliesRolls {
[`system.${resource}.points`]: Math.max(available - effectiveCost, 0),
})
}
} else {
const reserve = this.#getThreadReserveState(actor, paymentSource)
if (!reserve.owner) {
ui.notifications.warn("Aucune réserve de compagnie n'est liée à ce personnage.")
return null
}
const resourceKey = this.#getThreadResourceKey(spell.system.polarity)
const available = Number(reserve[resourceKey] ?? 0)
if (available < effectiveCost) {
ui.notifications.warn(game.i18n.format("LESOUBLIES.rolls.notEnoughResourceDetailed", {
resource: `${effectiveCost > 1 ? "fils" : "fil"} de ${spell.system.polarity === "cauchemar" ? "Cauchemar" : "Songes"}`,
actor: reserve.label,
required: effectiveCost,
available,
}))
return null
}
if (effectiveCost > 0) {
await reserve.owner.update({
[`system.reserves.${resourceKey}`]: Math.max(available - effectiveCost, 0),
"system.reserves.emptyGlobes": Number(reserve.emptyGlobes ?? 0) + effectiveCost,
})
}
}
return { métierMatch, surcharge, effectiveCost, paymentMode }
return {
métierMatch,
surcharge,
effectiveCost,
paymentMode,
paymentSource,
paymentSourceLabel: paymentMode === "fils"
? this.#getThreadReserveLabel(actor, paymentSource)
: actor.name,
}
})
if (!activation) return null
@@ -487,6 +525,7 @@ export class LesOubliesRolls {
costLabel: activation.paymentMode === "points"
? `${activation.effectiveCost} point${activation.effectiveCost > 1 ? "s" : ""} de ${spell.system.polarity === "cauchemar" ? "Cauchemar" : "Songes"}`
: `${activation.effectiveCost} fil${activation.effectiveCost > 1 ? "s" : ""} de ${spell.system.polarity === "cauchemar" ? "Cauchemar" : "Songes"}`,
paymentSourceLabel: activation.paymentSourceLabel,
métierMatch: activation.métierMatch,
surcharge: activation.surcharge,
notes: data.notes?.trim() || "",
@@ -578,6 +617,17 @@ export class LesOubliesRolls {
if (!data) return null
const threadCount = Math.max(Number(data.threadCount ?? 1), 1)
const destinationSource = this.#normalizeThreadReserveSource(data.destinationSource)
const destinationReserve = this.#getThreadReserveState(actor, destinationSource)
if (!destinationReserve.owner) {
ui.notifications.warn("Aucune réserve de compagnie n'est liée à ce personnage.")
return null
}
if (Number(destinationReserve.emptyGlobes ?? 0) < threadCount) {
ui.notifications.warn(`${destinationReserve.label} ne dispose pas de suffisamment de globes vides pour stocker cette récolte.`)
return null
}
const damageTaken = threadCount
const difficulty = -3 * (threadCount - 1)
const result = await this.resolveTest(actor, {
@@ -599,8 +649,11 @@ export class LesOubliesRolls {
if (!result) return null
await this.#applyDamageToActor(actor, damageTaken)
const durationRoll = await (new Roll("1d12")).evaluate()
const effectRoll = await (new Roll("1d12")).evaluate()
if (result.success) {
await this.#storeHarvestedThreads(actor, destinationSource, data.threadType, threadCount)
}
const durationRoll = await this.#evaluateDisplayedRoll("1d12")
const effectRoll = await this.#evaluateDisplayedRoll("1d12")
const effectIndex = Number(effectRoll.total ?? 1)
result.metadata.action.harvest = {
threadType: data.threadType,
@@ -611,6 +664,8 @@ export class LesOubliesRolls {
sideEffectRoll: effectIndex,
sideEffectText: HARVEST_SIDE_EFFECTS[effectIndex],
sleeperLabel: data.sleeperLabel?.trim() || "Dormeur non précisé",
destinationLabel: destinationReserve.label,
stored: result.success,
}
return this.#createChatMessage(actor, result)
@@ -1129,17 +1184,20 @@ export class LesOubliesRolls {
const polarityLabel = spell.system.polarity === "cauchemar"
? game.i18n.localize("LESOUBLIES.ui.cauchemar")
: game.i18n.localize("LESOUBLIES.ui.songes")
const threadReserves = this.#getThreadDialogState(actor)
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-les-oublies/templates/dialog-spell-activation.hbs",
"systems/fvtt-les-oublies/templates/dialog-spell-activation-v2.hbs",
{
actor,
spell,
resources: this.#getDialogResources(actor),
threadReserves,
isMetierMatch,
effectiveCostLabel: `${effectiveCost} point${effectiveCost > 1 ? "s" : ""} de ${polarityLabel}`,
values: {
actualCost: Number(spell.system.cost ?? 0),
paymentMode: "points",
paymentSource: "actor",
targetLabel: "",
notes: "",
},
@@ -1151,6 +1209,9 @@ export class LesOubliesRolls {
title: `Activer ${spell.name}`,
},
content,
render: (_event, dialog) => {
this.#bindSpellPaymentSelection(dialog, { actor, spell, effectiveCost })
},
buttons: [
{
action: "activate",
@@ -1161,8 +1222,9 @@ export class LesOubliesRolls {
if (!form) return null
const data = this.#formToObject(form)
return {
actualCost: Number(data.actualCost ?? spell.system.cost ?? 0),
actualCost: Math.max(Number(data.actualCost ?? spell.system.cost ?? 0), 0),
paymentMode: String(data.paymentMode || "points"),
paymentSource: String(data.paymentSource || "actor"),
targetLabel: String(data.targetLabel || ""),
notes: String(data.notes || ""),
}
@@ -1338,16 +1400,19 @@ export class LesOubliesRolls {
}
static async #promptThreadHarvestOptions(actor) {
const threadReserves = this.#getThreadDialogState(actor)
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-les-oublies/templates/dialog-thread-harvest.hbs",
"systems/fvtt-les-oublies/templates/dialog-thread-harvest-v2.hbs",
{
actor,
rollModes: this.getRollModes(),
extraDieModes: this.getExtraDieModes(),
resources: this.#getDialogResources(actor),
threadReserves,
values: {
threadType: "songes",
threadCount: 1,
destinationSource: "actor",
rollMode: this.getDefaultRollMode(actor),
extraDie: "",
sleeperLabel: "",
@@ -1373,6 +1438,7 @@ export class LesOubliesRolls {
return {
threadType: String(data.threadType || "songes"),
threadCount: Number(data.threadCount ?? 1),
destinationSource: String(data.destinationSource || "actor"),
rollMode: String(data.rollMode || this.getDefaultRollMode(actor)),
extraDie: String(data.extraDie || ""),
sleeperLabel: String(data.sleeperLabel || ""),
@@ -1443,12 +1509,14 @@ export class LesOubliesRolls {
static async #rollExplodingDie({ type, index, source = "base" }) {
const faces = []
const rolls = []
let total = 0
let lastFace = 12
while (lastFace === 12) {
const roll = await (new Roll("1d12")).evaluate()
const roll = await this.#evaluateDisplayedRoll("1d12")
lastFace = Number(roll.total ?? 0)
rolls.push(roll)
faces.push(lastFace)
total += lastFace
}
@@ -1461,6 +1529,7 @@ export class LesOubliesRolls {
source,
sourceLabel: source === "extra" ? game.i18n.localize("LESOUBLIES.rolls.extraDie") : null,
faces,
rolls,
firstFace: faces[0] ?? 0,
total,
exploded: faces.length > 1,
@@ -1468,6 +1537,39 @@ export class LesOubliesRolls {
}
}
static async #evaluateDisplayedRoll(formula) {
const roll = await (new Roll(formula)).evaluate()
await this.#showDiceSoNice(roll)
return roll
}
static async #showDiceSoNice(roll) {
if (!game.modules.get("dice-so-nice")?.active) return
if (!game.dice3d?.showForRoll) return
const coreRollMode = game.settings.get("core", "rollMode")
let whisper = null
let blind = false
switch (coreRollMode) {
case "blindroll":
blind = true
case "gmroll":
whisper = ChatMessage.getWhisperRecipients("GM").map((user) => user.id)
break
case "selfroll":
whisper = [game.user.id]
break
case "publicroll":
case "roll":
default:
whisper = null
break
}
await game.dice3d.showForRoll(roll, game.user, true, whisper, blind)
}
static #needsSelection(dice) {
return new Set(dice.map((die) => die.type)).size > 1
}
@@ -1571,9 +1673,106 @@ export class LesOubliesRolls {
songesPoints: Number(context.system.songes?.points ?? 0),
cauchemarValue: Number(context.system.cauchemar?.value ?? 0),
cauchemarPoints: Number(context.system.cauchemar?.points ?? 0),
songesThreads: Number(context.system.reserves?.songesThreads ?? 0),
cauchemarThreads: Number(context.system.reserves?.cauchemarThreads ?? 0),
emptyGlobes: Number(context.system.reserves?.emptyGlobes ?? 0),
}
}
static #normalizeThreadReserveSource(source) {
return ["company", "compagnie"].includes(String(source || "").toLowerCase()) ? "company" : "actor"
}
static #getThreadReserveOwner(actor, source = "actor") {
return this.#normalizeThreadReserveSource(source) === "company"
? actor?.getCompagnie?.() ?? null
: actor
}
static #getThreadReserveLabel(actor, source = "actor") {
const normalized = this.#normalizeThreadReserveSource(source)
if (normalized === "actor") return "Réserve personnelle"
const company = this.#getThreadReserveOwner(actor, normalized)
return company ? `Réserve de compagnie — ${company.name}` : "Réserve de compagnie"
}
static #getThreadResourceKey(polarity) {
return polarity === "cauchemar" ? "cauchemarThreads" : "songesThreads"
}
static #getThreadReserveState(actor, source = "actor") {
const owner = this.#getThreadReserveOwner(actor, source)
return {
owner,
source: this.#normalizeThreadReserveSource(source),
label: this.#getThreadReserveLabel(actor, source),
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),
}
}
static #getThreadDialogState(actor) {
const actorReserve = this.#getThreadReserveState(actor, "actor")
const companyReserve = this.#getThreadReserveState(actor, "company")
const options = [
{ value: "actor", label: actorReserve.label },
]
if (companyReserve.owner) options.push({ value: "company", label: companyReserve.label })
return {
actor: actorReserve,
company: companyReserve,
options,
hasCompany: Boolean(companyReserve.owner),
}
}
static async #storeHarvestedThreads(actor, destinationSource, threadType, threadCount) {
const reserve = this.#getThreadReserveState(actor, destinationSource)
if (!reserve.owner || threadCount < 1) return false
const resourceKey = this.#getThreadResourceKey(threadType)
await reserve.owner.update({
[`system.reserves.${resourceKey}`]: Number(reserve[resourceKey] ?? 0) + threadCount,
"system.reserves.emptyGlobes": Math.max(Number(reserve.emptyGlobes ?? 0) - threadCount, 0),
})
return true
}
static #bindSpellPaymentSelection(dialog, { actor, spell, effectiveCost }) {
const root = this.#getDialogElement(dialog)
const form = root?.querySelector("form")
if (!form) return
const modeField = form.elements.namedItem("paymentMode")
const sourceField = form.elements.namedItem("paymentSource")
const effectiveCostField = root.querySelector("[data-effective-cost]")
const sourceWrapper = root.querySelector("[data-payment-source]")
const sourceHint = root.querySelector("[data-payment-source-hint]")
const update = () => {
const paymentMode = modeField instanceof HTMLSelectElement ? String(modeField.value || "points") : "points"
const paymentSource = sourceField instanceof HTMLSelectElement ? String(sourceField.value || "actor") : "actor"
const polarityLabel = spell.system.polarity === "cauchemar" ? "Cauchemar" : "Songes"
if (effectiveCostField instanceof HTMLInputElement) {
effectiveCostField.value = paymentMode === "points"
? `${effectiveCost} point${effectiveCost > 1 ? "s" : ""} de ${polarityLabel}`
: `${effectiveCost} fil${effectiveCost > 1 ? "s" : ""} de ${polarityLabel}`
}
if (sourceWrapper instanceof HTMLElement) {
sourceWrapper.hidden = paymentMode !== "fils"
}
if (sourceHint instanceof HTMLElement) {
sourceHint.textContent = paymentMode === "fils"
? `${this.#getThreadReserveLabel(actor, paymentSource)} utilisée. Les globes vidés y retournent automatiquement.`
: "La dépense se fait dans les points de Songes ou de Cauchemar du personnage."
}
}
if (modeField instanceof HTMLSelectElement) modeField.addEventListener("change", update)
if (sourceField instanceof HTMLSelectElement) sourceField.addEventListener("change", update)
update()
}
static #createSpentResource(extraDie) {
if (!extraDie) return null
return {
+6
View File
@@ -97,6 +97,12 @@ export class LesOubliesUtility {
return [...documents].sort((left, right) => left.name.localeCompare(right.name, "fr"))
}
static uniqueStrings(values = []) {
return [...new Set((Array.isArray(values) ? values : [])
.map((value) => String(value ?? "").trim())
.filter(Boolean))]
}
static async prepareEnrichedHtml(documentName, type, systemData) {
const htmlFields = game.system.documentTypes?.[documentName]?.[type]?.htmlFields ?? []
const enriched = {}
+5
View File
@@ -21,6 +21,11 @@ export default class CompagnieDataModel extends foundry.abstract.TypeDataModel {
label: new fields.StringField({ initial: "" }),
details: new fields.StringField({ initial: "" }),
}), { initial: [] }),
reserves: new fields.SchemaField({
songesThreads: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
cauchemarThreads: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
emptyGlobes: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
}),
}
}
}
+5
View File
@@ -56,6 +56,11 @@ export default class PersonnageDataModel extends foundry.abstract.TypeDataModel
money: new fields.SchemaField({
ecorces: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
}),
reserves: new fields.SchemaField({
songesThreads: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
cauchemarThreads: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
emptyGlobes: new fields.NumberField({ initial: 0, integer: true, min: 0 }),
}),
flagsNarratifs: new fields.SchemaField({
ombreDuTourment: new fields.BooleanField({ initial: false }),
isCaptain: new fields.BooleanField({ initial: false }),