- Profils raciaux appliqués automatiquement - DsN opératonnel - Gestion plus fine des fils/orbes
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user