diff --git a/css/fvtt-cthulhu-eternal.css b/css/fvtt-cthulhu-eternal.css
index 199c097..334fccb 100644
--- a/css/fvtt-cthulhu-eternal.css
+++ b/css/fvtt-cthulhu-eternal.css
@@ -3167,8 +3167,16 @@ i.fvtt-cthulhu-eternal {
font-size: calc(var(--font-size-standard) * 1.2);
text-shadow: 0 0 10px var(--color-shadow-primary);
}
-.dice-roll .chat-actions {
- display: flex;
+.dice-roll .skill-progress-notice {
+ margin: 0.3rem 0 0;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.8rem;
+ font-style: italic;
+ color: var(--color-level-warning);
+ border-left: 3px solid var(--color-level-warning);
+ background: rgba(var(--color-level-warning-rgb, 180, 130, 0), 0.08);
+}
+.dice-roll .chat-actions { display: flex;
flex-wrap: wrap;
gap: 0.375rem;
padding: 0.5rem;
@@ -3210,9 +3218,23 @@ i.fvtt-cthulhu-eternal {
.dice-roll .chat-actions .nudge-roll,
.dice-roll .chat-actions .damage-roll,
.dice-roll .chat-actions .healing-roll,
-.dice-roll .chat-actions .opposed-roll {
+.dice-roll .chat-actions .opposed-roll,
+.dice-roll .chat-actions .nudge-to-success {
display: none;
}
+.dice-roll .chat-actions .nudge-to-success {
+ width: auto;
+ padding: 0 0.5rem !important;
+ gap: 0.3rem;
+ font-size: 0.75rem;
+ font-weight: bold;
+ background: var(--color-level-success);
+ color: var(--color-light-1);
+ border-color: var(--color-level-success);
+}
+.dice-roll .chat-actions .nudge-to-success:hover {
+ filter: brightness(1.15);
+}
.opposed-roll-result {
padding: 0.5rem;
background: rgba(0, 0, 0, 0.05);
diff --git a/cthulhu-eternal.mjs b/cthulhu-eternal.mjs
index e89c61b..04c728d 100644
--- a/cthulhu-eternal.mjs
+++ b/cthulhu-eternal.mjs
@@ -159,11 +159,42 @@ Hooks.on('tokenActionHudCoreApiReady', async () => {
})
Hooks.on("renderChatMessageHTML", (message, html, data) => {
+ const alreadyUsedLabel = game.i18n.localize("CTHULHUETERNAL.Label.alreadyUsed")
+
+ // Helper: disable all action buttons in the message
+ function disableChatActions(container) {
+ $(container).find(".chat-action-button").each((i, btn) => {
+ btn.setAttribute("disabled", "disabled")
+ btn.setAttribute("data-tooltip", alreadyUsedLabel)
+ btn.style.opacity = "0.5"
+ btn.style.cursor = "not-allowed"
+ btn.style.pointerEvents = "none"
+ })
+ }
+
+ // If an action was already used on this message, disable all buttons immediately
+ if (message.getFlag("fvtt-cthulhu-eternal", "actionUsed")) {
+ disableChatActions(html)
+ }
+
+ // Wrapper: click any action button → disable all + persist flag
+ function withActionLock(handler) {
+ return async (event) => {
+ const container = $(event.currentTarget).closest(".chat-message, .message-content, div[class]")[0] || html
+ disableChatActions(container)
+ message.setFlag("fvtt-cthulhu-eternal", "actionUsed", true).catch(() => { })
+ await handler(event)
+ }
+ }
+
// Affichage des boutons de jet de dés uniquement pour les joueurs
if (message.author.id === game.user.id || game.user.isGM) {
$(html).find(".nudge-roll").each((i, btn) => {
btn.style.display = "inline"
})
+ $(html).find(".nudge-to-success").each((i, btn) => {
+ btn.style.display = "inline-flex"
+ })
$(html).find(".damage-roll").each((i, btn) => {
btn.style.display = "inline"
})
@@ -178,47 +209,55 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => {
btn.style.display = "inline"
})
}
- $(html).find(".nudge-roll").click((event) => {
+ $(html).find(".nudge-roll").click(withActionLock(() => {
CthulhuEternalUtils.nudgeRoll(message)
- })
- $(html).find(".damage-roll").click((event) => {
+ }))
+ $(html).find(".nudge-to-success").click(withActionLock(() => {
+ CthulhuEternalUtils.nudgeToSuccess(message)
+ }))
+ $(html).find(".damage-roll").click(withActionLock((event) => {
let formula = $(event.currentTarget).data("roll-value")
CthulhuEternalUtils.damageRoll(message, formula)
- })
- $(html).find(".healing-roll").click((event) => {
+ }))
+ $(html).find(".healing-roll").click(withActionLock(() => {
CthulhuEternalUtils.healingRoll(message)
- })
- $(html).find(".worn-weapon-check").click((event) => {
+ }))
+ $(html).find(".worn-weapon-check").click(withActionLock(() => {
CthulhuEternalUtils.wornWeaponCheck(message)
- })
- $(html).find(".san-loose").click((event) => {
+ }))
+ $(html).find(".san-loose").click(withActionLock((event) => {
CthulhuEternalUtils.applySANLoss(message, event)
- })
- $(html).find(".san-type").click((event) => {
+ }))
+ $(html).find(".san-type").click(withActionLock((event) => {
CthulhuEternalUtils.applySANType(message, event)
- })
- $(html).find(".opposed-roll").click((event) => {
+ }))
+ $(html).find(".opposed-roll").click(withActionLock((event) => {
CthulhuEternalUtils.opposedRollManagement(message, event)
- })
+ }))
}
if (game.user.isGM) {
$(html).find(".li-apply-wounds").each((i, btn) => {
btn.style.display = "block"
})
- $(html).find(".apply-wounds-btn").click((event) => {
+ $(html).find(".apply-wounds-btn").click(withActionLock((event) => {
CthulhuEternalUtils.applyWounds(message, event)
- })
+ }))
$(html).find(".apply-wounds-btn").hover(
function (event) {
- // Mouse enter - select the token
- let combatantId = $(this).data("combatant-id")
+ // Mouse enter - highlight the token on the canvas
+ const tokenId = $(this).data("token-id")
+ if (tokenId) {
+ const token = canvas.tokens.get(tokenId)
+ if (token) token.control({ releaseOthers: true })
+ return
+ }
+ // Legacy: resolve via combatant
+ const combatantId = $(this).data("combatant-id")
if (combatantId && game.combat) {
- let combatant = game.combat.combatants.get(combatantId)
+ const combatant = game.combat.combatants.get(combatantId)
if (combatant?.token) {
- let token = canvas.tokens.get(combatant.token.id)
- if (token) {
- token.control({ releaseOthers: true })
- }
+ const token = canvas.tokens.get(combatant.token.id)
+ if (token) token.control({ releaseOthers: true })
}
}
},
diff --git a/lang/en.json b/lang/en.json
index 0f84a18..b169b8d 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -24,6 +24,9 @@
"Settings": {
"era": "Select the era of your game",
"eraHint": "Select the era of your game",
+ "nudge": "Allow roll modification (WP spend)",
+ "nudgeHint": "When enabled, players can spend Willpower Points to modify failed rolls. Disable to remove this optional rule.",
+ "nudgeDisabled": "Roll modification via Willpower Points is currently disabled.",
"Common": "Common",
"Classical": "Classical",
"Medieval": "Medieval",
@@ -622,6 +625,7 @@
"Weapon": "Weapon",
"ZeroWP": "Zero WP : Automatic failure (ie 0%)",
"LowWP": "Low WP",
+ "notEnoughWP": "Not enough WP to perform this action",
"Exhausted": "Exhausted",
"wornWeaponWarning": "Worn weapon: Don't forget to make the luck roll after the attack!",
"creature": "Creature",
@@ -773,6 +777,7 @@
"lethalityLethal": "Lethal !!",
"lethalityNotLethal": "Non-Lethal",
"WPSpent": "WP Spent",
+ "alreadyUsed": "Already used",
"targetMove": "Target Move",
"attackerState": "Attacker State",
"targetSize": "Target Size",
@@ -808,7 +813,8 @@
"roll": "Roll",
"applyNudge": "Apply",
"cancel": "Cancel",
- "nudgeRoll": "Nudge Roll"
+ "nudgeRoll": "Nudge Roll",
+ "nudgeToSuccess": "Succeed at {score}% for {wp} WP"
},
"Tooltip": {
"sanBP": ">5 SAN lost in one roll, temporary insanity. If SAN less reaches BP = a Disorder unconscious Breaking and AND reset BP.",
diff --git a/lang/fr.json b/lang/fr.json
index 2e7941b..5161f87 100644
--- a/lang/fr.json
+++ b/lang/fr.json
@@ -24,6 +24,9 @@
"Settings": {
"era": "Sélectionnez l'époque de votre jeu",
"eraHint": "L'époque détermine les compétences, les armes et les équipements disponibles pour votre protagoniste.",
+ "nudge": "Autoriser la modification des jets (dépense de PVO)",
+ "nudgeHint": "Si activé, les joueurs peuvent dépenser des Points de Volonté pour modifier leurs jets ratés. Désactivez pour supprimer cette règle optionnelle.",
+ "nudgeDisabled": "La modification des jets via les Points de Volonté est actuellement désactivée.",
"Common": "Commun",
"Classical": "Classique",
"Medieval": "Médiéval",
@@ -616,6 +619,7 @@
"Weapon": "Arme",
"ZeroWP": "PVO à 0 : Echec automatique (ie 0%)",
"LowWP": "PVO faibles",
+ "notEnoughWP": "PVO insuffisants pour effectuer cette action",
"Exhausted": "Epuisé", "wornWeaponWarning": "Arme usée : N'oubliez pas de faire le jet de chance après l'attaque !", "creature": "Créature",
"Rituals": "Rituels",
"Tomes": "Ouvrages",
@@ -764,6 +768,7 @@
"lethalityLethal": "Létal !!",
"lethalityNotLethal": "Non létal",
"WPSpent": "PVO dépensés",
+ "alreadyUsed": "Déjà utilisé",
"targetMove": "Mouvement de la cible",
"attackerState": "Etat de l'attaquant",
"targetSize": "Taille de la cible",
@@ -800,7 +805,8 @@
"roll": "Jet",
"applyNudge": "Lancer",
"cancel": "Annuler",
- "nudgeRoll": "Modifier le jet"
+ "nudgeRoll": "Modifier le jet",
+ "nudgeToSuccess": "Réussir à {score}% pour {wp} PVO"
},
"Tooltip": {
"sanBP": "Perte de 5+ SAN en 1 jet : folie temporaire. SI la SAN atteint le PR : trouble mental, perte de conscience et reset du PR.",
diff --git a/module/applications/sheets/creature-sheet.mjs b/module/applications/sheets/creature-sheet.mjs
index a2cd980..e961817 100644
--- a/module/applications/sheets/creature-sheet.mjs
+++ b/module/applications/sheets/creature-sheet.mjs
@@ -162,7 +162,7 @@ export default class CthulhuEternalCreatureSheet extends CthulhuEternalActorShee
async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return
- const data = TextEditor.getDragEventData(event)
+ const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
// Handle different data types
switch (data.type) {
diff --git a/module/applications/sheets/protagonist-sheet.mjs b/module/applications/sheets/protagonist-sheet.mjs
index eb548cc..10297c2 100644
--- a/module/applications/sheets/protagonist-sheet.mjs
+++ b/module/applications/sheets/protagonist-sheet.mjs
@@ -264,7 +264,7 @@ export default class CthulhuEternalProtagonistSheet extends CthulhuEternalActorS
async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return
- const data = TextEditor.getDragEventData(event)
+ const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
// Handle different data types
switch (data.type) {
diff --git a/module/applications/sheets/vehicle-sheet.mjs b/module/applications/sheets/vehicle-sheet.mjs
index 3183e15..1cf010b 100644
--- a/module/applications/sheets/vehicle-sheet.mjs
+++ b/module/applications/sheets/vehicle-sheet.mjs
@@ -81,8 +81,8 @@ export default class CthulhuEternalVehicleSheet extends CthulhuEternalActorSheet
break
case "description":
context.tab = context.tabs.description
- context.enrichedDescription = await TextEditor.enrichHTML(doc.system.description, { async: true })
- context.enrichedNotes = await TextEditor.enrichHTML(doc.system.notes, { async: true })
+ context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.description, { async: true })
+ context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.notes, { async: true })
break
}
return context
@@ -104,7 +104,7 @@ export default class CthulhuEternalVehicleSheet extends CthulhuEternalActorSheet
async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return
- const data = TextEditor.getDragEventData(event)
+ const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
// Handle different data types
switch (data.type) {
diff --git a/module/documents/roll.mjs b/module/documents/roll.mjs
index 34ab54c..648e4c2 100644
--- a/module/documents/roll.mjs
+++ b/module/documents/roll.mjs
@@ -139,11 +139,19 @@ export default class CthulhuEternalRoll extends Roll {
weapon.system.killRadius = choice.killRadius // Override kill radius
}
- let combatants = []
+ let targets = []
if (game?.combat?.combatants) {
for (let c of game.combat.combatants) {
- if (c.actorid !== actor.id) {
- combatants.push({ id: c.id, name: c.name })
+ if (c.actorId !== actor.id) {
+ targets.push({ id: c.id, name: c.name, actorId: c.actorId, tokenId: c.token?.id })
+ }
+ }
+ }
+ // Fall back to scene tokens if not in combat
+ if (targets.length === 0 && canvas?.scene) {
+ for (let tokenDoc of canvas.scene.tokens) {
+ if (tokenDoc.actorId !== actor.id) {
+ targets.push({ name: tokenDoc.name, actorId: tokenDoc.actorId, tokenId: tokenDoc.id })
}
}
}
@@ -162,12 +170,11 @@ export default class CthulhuEternalRoll extends Roll {
isLethal,
ammoUsed: weapon?.ammoUsed || 0,
rollResult: lethalityRoll.total,
- combatants: combatants
+ combatants: targets
}
- let flavor = await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/chat-lethal-damage.hbs", msgData)
let msg = await ChatMessage.create({
user: game.user.id,
- content: flavor,
+ content: await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/chat-lethal-damage.hbs", msgData),
speaker: ChatMessage.getSpeaker({ actor: actor }),
}, { rollMode: options.rollMode, create: true })
await msg.setFlag("fvtt-cthulhu-eternal", "woundData", msgData)
@@ -191,12 +198,11 @@ export default class CthulhuEternalRoll extends Roll {
formula,
ammoUsed: weapon?.ammoUsed || 0,
rollResult: damageRoll.total,
- combatants: combatants
+ combatants: targets
}
- let flavor = await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/chat-regular-damage.hbs", msgData)
let msg = await ChatMessage.create({
user: game.user.id,
- content: flavor,
+ content: await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/chat-regular-damage.hbs", msgData),
speaker: ChatMessage.getSpeaker({ actor: actor }),
}, { rollMode: options.rollMode, create: true })
await msg.setFlag("fvtt-cthulhu-eternal", "woundData", msgData)
@@ -254,7 +260,8 @@ export default class CthulhuEternalRoll extends Roll {
let formula = "1d100"
let hasModifier = true
let hasMultiplier = false
- options.isNudge = true
+ const nudgeEnabled = game.settings.get("fvtt-cthulhu-eternal", "settings-nudge")
+ options.isNudge = nudgeEnabled
let actor = game.actors.get(options.actorId)
let target = CthulhuEternalUtils.getTarget()
@@ -271,7 +278,7 @@ export default class CthulhuEternalRoll extends Roll {
case "san":
case "char":
options.initialScore = options.rollItem.targetScore
- options.isNudge = (options.rollType !== "san")
+ options.isNudge = nudgeEnabled && (options.rollType !== "san")
break
case "resource":
hasModifier = false
@@ -322,7 +329,7 @@ export default class CthulhuEternalRoll extends Roll {
break
}
- const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); //Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)]))
+ const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes ?? CONFIG.Dice.rollModes)
const fieldRollMode = new foundry.data.fields.StringField({
choices: rollModes,
blank: false,
@@ -526,6 +533,16 @@ export default class CthulhuEternalRoll extends Roll {
}
rollData.resultType = resultType
+ // Compute WP cost to nudge a failure into a success (minimum roll needed = targetScore)
+ if (this.options.isFailure && options.isNudge && !this.options.isNudgedRoll && rollData.targetScore > 0 && this.total > rollData.targetScore) {
+ const actor = game.actors.get(options.actorId)
+ const wpAvailable = actor?.system?.wp?.value ?? 0
+ const wpCostToSucceed = Math.ceil((this.total - rollData.targetScore) / 5)
+ this.options.wpCostToSucceed = (wpCostToSucceed <= wpAvailable) ? wpCostToSucceed : 0
+ } else {
+ this.options.wpCostToSucceed = 0
+ }
+
this.options.isLowWP = rollData.isLowWP
this.options.isZeroWP = rollData.isZeroWP
this.options.isExhausted = rollData.isExhausted
@@ -685,7 +702,19 @@ export default class CthulhuEternalRoll extends Roll {
* @param {boolean} [options.create=true] Whether to create the message.
* @returns {Promise} - A promise that resolves when the message is created.
*/
- async toMessage(messageData = {}, { rollMode, create = true } = {}) {
+ async toMessage(messageData = {}, { rollMode, messageMode, create = true } = {}) {
+ let rollData = this.options.rollData || this.options
+ let rollItem = this.options.rollItem
+
+ // Determine skill progression before calling super.toMessage so _getChatCardData picks it up
+ let skillMarkedForProgress = false
+ if (rollData.resultType?.includes("failure") && rollItem?.type === "skill") {
+ if (rollItem.system.diceEvolved && !rollItem.system.rollFailed && !rollItem.system.isAdversary) {
+ skillMarkedForProgress = true
+ }
+ }
+ this.options.skillMarkedForProgress = skillMarkedForProgress
+
let rollMsg = await super.toMessage(
{
isFailure: this.resultType === "failure",
@@ -695,34 +724,17 @@ export default class CthulhuEternalRoll extends Roll {
realDamage: this.realDamage,
...messageData,
},
- { rollMode: rollMode },
+ { messageMode: messageMode ?? rollMode },
)
- let rollData = this.options.rollData || this.options
- let rollItem = this.options.rollItem
await rollMsg.setFlag("fvtt-cthulhu-eternal", "rollData", rollData)
- // Manage the skill evolution if the roll is a failure
- if (rollData.resultType.includes("failure") && rollItem.type === "skill") {
- // Is the skill able to progress
- if (rollItem.system.diceEvolved && !rollItem.system.rollFailed) {
- // If the skill is not adversary, we can evolve it
- if (!rollItem.system.isAdversary) {
- rollItem.system.rollFailed = true
- // Get the actor and update the skill
- const actor = game.actors.get(rollData.actorId)
- await actor.updateEmbeddedDocuments("Item", [{
- _id: rollItem._id,
- "system.rollFailed": true
- }])
- // Create a chat message to inform the user
- const flavor = `${rollItem.name} - ${game.i18n.localize("CTHULHUETERNAL.Label.skillFailed")}`
- await ChatMessage.create({
- user: game.user.id,
- content: `
`,
- speaker: ChatMessage.getSpeaker({ actor: rollData.actor }),
- }, { rollMode: rollData.rollMode, create: true })
- }
- }
+ // Apply skill rollFailed flag after the message is created
+ if (skillMarkedForProgress) {
+ const actor = game.actors.get(rollData.actorId)
+ await actor.updateEmbeddedDocuments("Item", [{
+ _id: rollItem._id,
+ "system.rollFailed": true
+ }])
}
// If the roll is a SAN roll, we propose to select the SAN loss
diff --git a/module/models/creature.mjs b/module/models/creature.mjs
index 836d77d..c61c686 100644
--- a/module/models/creature.mjs
+++ b/module/models/creature.mjs
@@ -74,7 +74,7 @@ export default class CthulhuEternalCreature extends foundry.abstract.TypeDataMod
})
if (!roll) return null
- await roll.toMessage({}, { rollMode: roll.options.rollMode })
+ await roll.toMessage({}, { messageMode: roll.options.rollMode })
}
}
diff --git a/module/models/protagonist.mjs b/module/models/protagonist.mjs
index df76af8..346f696 100644
--- a/module/models/protagonist.mjs
+++ b/module/models/protagonist.mjs
@@ -401,7 +401,7 @@ export default class CthulhuEternalProtagonist extends foundry.abstract.TypeData
})
if (!roll) return null
- await roll.toMessage({}, { rollMode: roll.options.rollMode })
+ await roll.toMessage({}, { messageMode: roll.options.rollMode })
}
}
diff --git a/module/utils.mjs b/module/utils.mjs
index b84a8b4..d2a9141 100644
--- a/module/utils.mjs
+++ b/module/utils.mjs
@@ -16,6 +16,14 @@ export default class CthulhuEternalUtils {
config: true,
onChange: _ => window.location.reload()
});
+ game.settings.register("fvtt-cthulhu-eternal", "settings-nudge", {
+ name: game.i18n.localize("CTHULHUETERNAL.Settings.nudge"),
+ hint: game.i18n.localize("CTHULHUETERNAL.Settings.nudgeHint"),
+ default: true,
+ scope: "world",
+ type: Boolean,
+ config: true
+ });
game.settings.register("fvtt-cthulhu-eternal", "roll-opposed-store", {
name: "Roll Opposed Store",
hint: "Whether to store opposed roll results for later use",
@@ -536,17 +544,63 @@ export default class CthulhuEternalUtils {
})
}
+ static async nudgeToSuccess(rollMessage) {
+ if (!game.settings.get("fvtt-cthulhu-eternal", "settings-nudge")) {
+ ui.notifications.warn(game.i18n.localize("CTHULHUETERNAL.Settings.nudgeDisabled"))
+ return
+ }
+ const rollOptions = rollMessage.rolls[0]?.options
+ if (!rollOptions) return
+ const actor = game.actors.get(rollOptions.actorId)
+ if (!actor) return
+ const rollTotal = rollMessage.rolls[0].total
+ const targetScore = rollOptions.rollData?.targetScore ?? rollOptions.targetScore
+ const wpCostToSucceed = Math.ceil((rollTotal - targetScore) / 5)
+
+ if (actor.system.wp.value < wpCostToSucceed) {
+ ui.notifications.warn(game.i18n.localize("CTHULHUETERNAL.Label.notEnoughWP"))
+ return
+ }
+
+ const dialogContext = foundry.utils.duplicate(rollOptions)
+ dialogContext.nudgedValue = targetScore
+ dialogContext.wpCost = wpCostToSucceed
+ dialogContext.isNudgedRoll = true
+ dialogContext.isNudge = false
+
+ const roll = new CthulhuEternalRoll(String(targetScore))
+ await roll.evaluate()
+ roll.options = dialogContext
+ roll.displayRollResult(roll, dialogContext, dialogContext.rollData)
+ roll.toMessage()
+
+ actor.system.modifyWP(-wpCostToSucceed)
+
+ // Original roll was a failure with skill progression — nudge succeeded, so unmark progression
+ if (rollOptions.skillMarkedForProgress && rollOptions.rollItem?._id) {
+ const skillItem = actor.items.get(rollOptions.rollItem._id)
+ if (skillItem) await skillItem.update({ "system.rollFailed": false })
+ }
+
+ await rollMessage.delete()
+ }
+
static async nudgeRoll(rollMessage) {
+ if (!game.settings.get("fvtt-cthulhu-eternal", "settings-nudge")) {
+ ui.notifications.warn(game.i18n.localize("CTHULHUETERNAL.Settings.nudgeDisabled"))
+ return
+ }
let dialogContext = rollMessage.rolls[0]?.options
let actor = game.actors.get(dialogContext.actorId)
+ const rollTotal = rollMessage.rolls[0].total
dialogContext.wpValue = actor.system.wp.value
- dialogContext.rollResultIndex = rollMessage.rolls[0].total - 1
- dialogContext.minValue = Math.max(rollMessage.rolls[0].total - (dialogContext.wpValue * 5), 1)
- dialogContext.maxValue = Math.min(rollMessage.rolls[0].total + (dialogContext.wpValue * 5), 100)
+ // Rule: 1 WP can decrease the roll by 1–5. Only decrease is allowed.
+ dialogContext.minValue = Math.max(rollTotal - (dialogContext.wpValue * 5), 1)
+ dialogContext.maxValue = rollTotal
+ dialogContext.rollResultIndex = rollTotal - dialogContext.minValue // index of current value in nudgeOptions
dialogContext.wpCost = 0
-
- // Build options table for the select operator between minValue and maxValue
+ dialogContext.nudgedValue = rollTotal // default: no change for the select: values from minValue to maxValue (current roll)
dialogContext.nudgeOptions = Array.from({ length: dialogContext.maxValue - dialogContext.minValue + 1 }, (_, i) => dialogContext.minValue + i)
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/nudge-dialog.hbs", dialogContext)
@@ -565,6 +619,9 @@ export default class CthulhuEternalUtils {
if (input.name) obj[input.name] = input.value
return obj
}, {})
+ // Compute nudgedValue from the selected index (selectOptions uses 0-based indices for arrays)
+ dialogContext.nudgedValue = dialogContext.minValue + Number(output.modifiedValue)
+ dialogContext.wpCost = Math.ceil(Math.abs(rollTotal - dialogContext.nudgedValue) / 5)
return output
},
},
@@ -579,7 +636,7 @@ export default class CthulhuEternalUtils {
rejectClose: false, // Click on Close button will not launch an error
render: (event, dialog) => {
$(".nudged-score-select").change(event => {
- dialogContext.nudgedValue = Number(event.target.value) + 1
+ dialogContext.nudgedValue = dialogContext.minValue + Number(event.target.value)
dialogContext.wpCost = Math.ceil(Math.abs(rollMessage.rolls[0].total - dialogContext.nudgedValue) / 5)
$("#nudged-wp-cost").val(dialogContext.wpCost)
})
@@ -591,6 +648,11 @@ export default class CthulhuEternalUtils {
return
}
+ if (actor.system.wp.value < dialogContext.wpCost) {
+ ui.notifications.warn(game.i18n.localize("CTHULHUETERNAL.Label.notEnoughWP"))
+ return
+ }
+
const roll = new CthulhuEternalRoll(String(dialogContext.nudgedValue))
await roll.evaluate()
roll.options = dialogContext
@@ -601,6 +663,13 @@ export default class CthulhuEternalUtils {
actor.system.modifyWP(-dialogContext.wpCost)
+ // If original roll had skill progression and nudge converts it to success, unmark progression
+ const nudgedTargetScore = dialogContext.rollData?.targetScore ?? dialogContext.targetScore
+ if (dialogContext.skillMarkedForProgress && dialogContext.nudgedValue <= nudgedTargetScore && dialogContext.rollItem?._id) {
+ const skillItem = actor.items.get(dialogContext.rollItem._id)
+ if (skillItem) await skillItem.update({ "system.rollFailed": false })
+ }
+
// Delete the initial roll message
await rollMessage.delete()
@@ -642,15 +711,29 @@ export default class CthulhuEternalUtils {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noActorFound"))
return
}
- // Remove the chat message
this.removeChatMessageId(message.id)
- // Get the targetted actorId from the button's data attribute
- let targetCombatantId = event.currentTarget.dataset.combatantId
- let combatant = game.combat.combatants.get(targetCombatantId)
- let targetActor = combatant.token?.actor || game.actors.get(combatant.actorId)
+ // Resolve the target actor: prefer canvas token (works for both combat and scene tokens)
+ const targetTokenId = event.currentTarget.dataset.tokenId
+ const targetActorId = event.currentTarget.dataset.actorId
+ let targetActor
+ if (targetTokenId) {
+ const token = canvas.tokens.get(targetTokenId)
+ targetActor = token?.actor
+ }
+ if (!targetActor && targetActorId) {
+ targetActor = game.actors.get(targetActorId)
+ }
+ // Legacy fallback: combatant lookup
if (!targetActor) {
- ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noTargetActorFound") + targetCombatantId)
+ const combatantId = event.currentTarget.dataset.combatantId
+ if (combatantId && game.combat) {
+ const combatant = game.combat.combatants.get(combatantId)
+ targetActor = combatant?.token?.actor || game.actors.get(combatant?.actorId)
+ }
+ }
+ if (!targetActor) {
+ ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noTargetActorFound"))
return
}
targetActor.applyWounds(woundData)
diff --git a/packs-system/rituals/000295.log b/packs-system/rituals/000308.log
similarity index 100%
rename from packs-system/rituals/000295.log
rename to packs-system/rituals/000308.log
diff --git a/packs-system/rituals/CURRENT b/packs-system/rituals/CURRENT
index 7fe694c..759a159 100644
--- a/packs-system/rituals/CURRENT
+++ b/packs-system/rituals/CURRENT
@@ -1 +1 @@
-MANIFEST-000293
+MANIFEST-000306
diff --git a/packs-system/rituals/LOG b/packs-system/rituals/LOG
index 84f4f93..5c6bf14 100644
--- a/packs-system/rituals/LOG
+++ b/packs-system/rituals/LOG
@@ -1,11 +1,7 @@
-2026/04/28-23:36:37.192093 7f5797fff6c0 Delete type=3 #1
-2026/04/28-23:37:29.068330 7f57977fe6c0 Level-0 table #296: started
-2026/04/28-23:37:29.068358 7f57977fe6c0 Level-0 table #296: 0 bytes OK
-2026/04/28-23:37:29.074817 7f57977fe6c0 Delete type=0 #294
-2026/04/28-23:37:29.106904 7f57977fe6c0 Manual compaction at level-0 from '!items!4oyPRBWPBWAChrJP' @ 72057594037927935 : 1 .. '!items!zVFfp3o0G0Zg3Ia4' @ 0 : 0; will stop at '!items!zVFfp3o0G0Zg3Ia4' @ 52 : 1
-2026/04/28-23:37:29.106913 7f57977fe6c0 Compacting 1@0 + 0@1 files
-2026/04/28-23:37:29.111066 7f57977fe6c0 Generated table #297@0: 26 keys, 60964 bytes
-2026/04/28-23:37:29.111082 7f57977fe6c0 Compacted 1@0 + 0@1 files => 60964 bytes
-2026/04/28-23:37:29.117095 7f57977fe6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
-2026/04/28-23:37:29.117212 7f57977fe6c0 Delete type=2 #248
-2026/04/28-23:37:29.117383 7f57977fe6c0 Manual compaction at level-0 from '!items!zVFfp3o0G0Zg3Ia4' @ 52 : 1 .. '!items!zVFfp3o0G0Zg3Ia4' @ 0 : 0; will stop at (end)
+2026/05/13-20:38:21.073323 7ff670fed6c0 Recovering log #304
+2026/05/13-20:38:21.083446 7ff670fed6c0 Delete type=3 #302
+2026/05/13-20:38:21.083514 7ff670fed6c0 Delete type=0 #304
+2026/05/13-21:29:49.379063 7ff6637fe6c0 Level-0 table #309: started
+2026/05/13-21:29:49.379189 7ff6637fe6c0 Level-0 table #309: 0 bytes OK
+2026/05/13-21:29:49.386620 7ff6637fe6c0 Delete type=0 #307
+2026/05/13-21:29:49.393683 7ff6637fe6c0 Manual compaction at level-0 from '!items!4oyPRBWPBWAChrJP' @ 72057594037927935 : 1 .. '!items!zVFfp3o0G0Zg3Ia4' @ 0 : 0; will stop at (end)
diff --git a/packs-system/rituals/LOG.old b/packs-system/rituals/LOG.old
index 740ac98..aa269f8 100644
--- a/packs-system/rituals/LOG.old
+++ b/packs-system/rituals/LOG.old
@@ -1,4 +1,7 @@
-2026/04/28-23:36:37.170404 7f5797fff6c0 Log #291: 0 ops saved to Table #292 OK
-2026/04/28-23:36:37.170516 7f5797fff6c0 Archiving /home/morr/foundry/foundrydata-dev/Data/systems/fvtt-cthulhu-eternal/packs-system/rituals/000291.log: OK
-2026/04/28-23:36:37.171708 7f5797fff6c0 Table #248: 26 entries OK
-2026/04/28-23:36:37.175280 7f5797fff6c0 **** Repaired leveldb /home/morr/foundry/foundrydata-dev/Data/systems/fvtt-cthulhu-eternal/packs-system/rituals; recovered 1 files; 60964 bytes. Some data may have been lost. ****
+2026/05/13-13:05:01.465748 7ff6717ee6c0 Recovering log #300
+2026/05/13-13:05:01.518261 7ff6717ee6c0 Delete type=3 #298
+2026/05/13-13:05:01.518318 7ff6717ee6c0 Delete type=0 #300
+2026/05/13-13:57:22.815510 7ff6637fe6c0 Level-0 table #305: started
+2026/05/13-13:57:22.815533 7ff6637fe6c0 Level-0 table #305: 0 bytes OK
+2026/05/13-13:57:22.821566 7ff6637fe6c0 Delete type=0 #303
+2026/05/13-13:57:22.821711 7ff6637fe6c0 Manual compaction at level-0 from '!items!4oyPRBWPBWAChrJP' @ 72057594037927935 : 1 .. '!items!zVFfp3o0G0Zg3Ia4' @ 0 : 0; will stop at (end)
diff --git a/packs-system/rituals/MANIFEST-000293 b/packs-system/rituals/MANIFEST-000293
deleted file mode 100644
index 5a6db06..0000000
Binary files a/packs-system/rituals/MANIFEST-000293 and /dev/null differ
diff --git a/packs-system/rituals/MANIFEST-000306 b/packs-system/rituals/MANIFEST-000306
new file mode 100644
index 0000000..43b294d
Binary files /dev/null and b/packs-system/rituals/MANIFEST-000306 differ
diff --git a/packs-system/skills/000464.log b/packs-system/skills/000477.log
similarity index 100%
rename from packs-system/skills/000464.log
rename to packs-system/skills/000477.log
diff --git a/packs-system/skills/CURRENT b/packs-system/skills/CURRENT
index dd4dce9..b79af71 100644
--- a/packs-system/skills/CURRENT
+++ b/packs-system/skills/CURRENT
@@ -1 +1 @@
-MANIFEST-000462
+MANIFEST-000475
diff --git a/packs-system/skills/LOG b/packs-system/skills/LOG
index a88f6a4..5e7bcea 100644
--- a/packs-system/skills/LOG
+++ b/packs-system/skills/LOG
@@ -1,11 +1,7 @@
-2026/04/28-23:36:37.131149 7f57a57ee6c0 Delete type=3 #1
-2026/04/28-23:37:29.054894 7f57977fe6c0 Level-0 table #465: started
-2026/04/28-23:37:29.054961 7f57977fe6c0 Level-0 table #465: 0 bytes OK
-2026/04/28-23:37:29.061529 7f57977fe6c0 Delete type=0 #463
-2026/04/28-23:37:29.081738 7f57977fe6c0 Manual compaction at level-0 from '!folders!5PrT9QmN1cFPzDFP' @ 72057594037927935 : 1 .. '!items!zvoUByzWSWZ87fxA' @ 0 : 0; will stop at '!items!zvoUByzWSWZ87fxA' @ 1281 : 1
-2026/04/28-23:37:29.081748 7f57977fe6c0 Compacting 1@0 + 0@1 files
-2026/04/28-23:37:29.088487 7f57977fe6c0 Generated table #466@0: 556 keys, 320457 bytes
-2026/04/28-23:37:29.088505 7f57977fe6c0 Compacted 1@0 + 0@1 files => 320457 bytes
-2026/04/28-23:37:29.094678 7f57977fe6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
-2026/04/28-23:37:29.094831 7f57977fe6c0 Delete type=2 #417
-2026/04/28-23:37:29.117360 7f57977fe6c0 Manual compaction at level-0 from '!items!zvoUByzWSWZ87fxA' @ 1281 : 1 .. '!items!zvoUByzWSWZ87fxA' @ 0 : 0; will stop at (end)
+2026/05/13-20:38:21.045516 7ff6717ee6c0 Recovering log #473
+2026/05/13-20:38:21.056349 7ff6717ee6c0 Delete type=3 #471
+2026/05/13-20:38:21.056423 7ff6717ee6c0 Delete type=0 #473
+2026/05/13-21:29:49.443482 7ff6637fe6c0 Level-0 table #478: started
+2026/05/13-21:29:49.443606 7ff6637fe6c0 Level-0 table #478: 0 bytes OK
+2026/05/13-21:29:49.450275 7ff6637fe6c0 Delete type=0 #476
+2026/05/13-21:29:49.450420 7ff6637fe6c0 Manual compaction at level-0 from '!folders!5PrT9QmN1cFPzDFP' @ 72057594037927935 : 1 .. '!items!zvoUByzWSWZ87fxA' @ 0 : 0; will stop at (end)
diff --git a/packs-system/skills/LOG.old b/packs-system/skills/LOG.old
index 2d8ad3c..6bb4978 100644
--- a/packs-system/skills/LOG.old
+++ b/packs-system/skills/LOG.old
@@ -1,4 +1,7 @@
-2026/04/28-23:36:36.935963 7f57a57ee6c0 Log #460: 0 ops saved to Table #461 OK
-2026/04/28-23:36:36.936025 7f57a57ee6c0 Archiving /home/morr/foundry/foundrydata-dev/Data/systems/fvtt-cthulhu-eternal/packs-system/skills/000460.log: OK
-2026/04/28-23:36:36.942723 7f57a57ee6c0 Table #417: 556 entries OK
-2026/04/28-23:36:36.946588 7f57a57ee6c0 **** Repaired leveldb /home/morr/foundry/foundrydata-dev/Data/systems/fvtt-cthulhu-eternal/packs-system/skills; recovered 1 files; 320457 bytes. Some data may have been lost. ****
+2026/05/13-13:05:01.363329 7ff663fff6c0 Recovering log #469
+2026/05/13-13:05:01.408343 7ff663fff6c0 Delete type=3 #467
+2026/05/13-13:05:01.408405 7ff663fff6c0 Delete type=0 #469
+2026/05/13-13:57:22.802421 7ff6637fe6c0 Level-0 table #474: started
+2026/05/13-13:57:22.802455 7ff6637fe6c0 Level-0 table #474: 0 bytes OK
+2026/05/13-13:57:22.809135 7ff6637fe6c0 Delete type=0 #472
+2026/05/13-13:57:22.821691 7ff6637fe6c0 Manual compaction at level-0 from '!folders!5PrT9QmN1cFPzDFP' @ 72057594037927935 : 1 .. '!items!zvoUByzWSWZ87fxA' @ 0 : 0; will stop at (end)
diff --git a/packs-system/skills/MANIFEST-000462 b/packs-system/skills/MANIFEST-000462
deleted file mode 100644
index ca3e25c..0000000
Binary files a/packs-system/skills/MANIFEST-000462 and /dev/null differ
diff --git a/packs-system/skills/MANIFEST-000475 b/packs-system/skills/MANIFEST-000475
new file mode 100644
index 0000000..298f6d2
Binary files /dev/null and b/packs-system/skills/MANIFEST-000475 differ
diff --git a/packs-system/weapons/000110.log b/packs-system/weapons/000123.log
similarity index 100%
rename from packs-system/weapons/000110.log
rename to packs-system/weapons/000123.log
diff --git a/packs-system/weapons/CURRENT b/packs-system/weapons/CURRENT
index db6fa61..2b465ed 100644
--- a/packs-system/weapons/CURRENT
+++ b/packs-system/weapons/CURRENT
@@ -1 +1 @@
-MANIFEST-000108
+MANIFEST-000121
diff --git a/packs-system/weapons/LOG b/packs-system/weapons/LOG
index f62f6d5..bf270b5 100644
--- a/packs-system/weapons/LOG
+++ b/packs-system/weapons/LOG
@@ -1,11 +1,7 @@
-2026/04/28-23:36:37.166060 7f57a4fed6c0 Delete type=3 #1
-2026/04/28-23:37:29.061681 7f57977fe6c0 Level-0 table #111: started
-2026/04/28-23:37:29.061722 7f57977fe6c0 Level-0 table #111: 0 bytes OK
-2026/04/28-23:37:29.068229 7f57977fe6c0 Delete type=0 #109
-2026/04/28-23:37:29.095030 7f57977fe6c0 Manual compaction at level-0 from '!folders!0DI3T2jve3nsmsfZ' @ 72057594037927935 : 1 .. '!items!zyxA9DhO36t5OBDv' @ 0 : 0; will stop at '!items!zyxA9DhO36t5OBDv' @ 55 : 1
-2026/04/28-23:37:29.095044 7f57977fe6c0 Compacting 1@0 + 0@1 files
-2026/04/28-23:37:29.100652 7f57977fe6c0 Generated table #112@0: 362 keys, 93592 bytes
-2026/04/28-23:37:29.100675 7f57977fe6c0 Compacted 1@0 + 0@1 files => 93592 bytes
-2026/04/28-23:37:29.106685 7f57977fe6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
-2026/04/28-23:37:29.106785 7f57977fe6c0 Delete type=2 #63
-2026/04/28-23:37:29.117373 7f57977fe6c0 Manual compaction at level-0 from '!items!zyxA9DhO36t5OBDv' @ 55 : 1 .. '!items!zyxA9DhO36t5OBDv' @ 0 : 0; will stop at (end)
+2026/05/13-20:38:21.061357 7ff663fff6c0 Recovering log #119
+2026/05/13-20:38:21.070703 7ff663fff6c0 Delete type=3 #117
+2026/05/13-20:38:21.070750 7ff663fff6c0 Delete type=0 #119
+2026/05/13-21:29:49.386758 7ff6637fe6c0 Level-0 table #124: started
+2026/05/13-21:29:49.386971 7ff6637fe6c0 Level-0 table #124: 0 bytes OK
+2026/05/13-21:29:49.393349 7ff6637fe6c0 Delete type=0 #122
+2026/05/13-21:29:49.393700 7ff6637fe6c0 Manual compaction at level-0 from '!folders!0DI3T2jve3nsmsfZ' @ 72057594037927935 : 1 .. '!items!zyxA9DhO36t5OBDv' @ 0 : 0; will stop at (end)
diff --git a/packs-system/weapons/LOG.old b/packs-system/weapons/LOG.old
index 5a90874..12177b0 100644
--- a/packs-system/weapons/LOG.old
+++ b/packs-system/weapons/LOG.old
@@ -1,4 +1,7 @@
-2026/04/28-23:36:37.136506 7f57a4fed6c0 Log #106: 0 ops saved to Table #107 OK
-2026/04/28-23:36:37.136625 7f57a4fed6c0 Archiving /home/morr/foundry/foundrydata-dev/Data/systems/fvtt-cthulhu-eternal/packs-system/weapons/000106.log: OK
-2026/04/28-23:36:37.141991 7f57a4fed6c0 Table #63: 362 entries OK
-2026/04/28-23:36:37.145196 7f57a4fed6c0 **** Repaired leveldb /home/morr/foundry/foundrydata-dev/Data/systems/fvtt-cthulhu-eternal/packs-system/weapons; recovered 1 files; 93592 bytes. Some data may have been lost. ****
+2026/05/13-13:05:01.411955 7ff671fef6c0 Recovering log #115
+2026/05/13-13:05:01.463033 7ff671fef6c0 Delete type=3 #113
+2026/05/13-13:05:01.463084 7ff671fef6c0 Delete type=0 #115
+2026/05/13-13:57:22.809226 7ff6637fe6c0 Level-0 table #120: started
+2026/05/13-13:57:22.809250 7ff6637fe6c0 Level-0 table #120: 0 bytes OK
+2026/05/13-13:57:22.815441 7ff6637fe6c0 Delete type=0 #118
+2026/05/13-13:57:22.821701 7ff6637fe6c0 Manual compaction at level-0 from '!folders!0DI3T2jve3nsmsfZ' @ 72057594037927935 : 1 .. '!items!zyxA9DhO36t5OBDv' @ 0 : 0; will stop at (end)
diff --git a/packs-system/weapons/MANIFEST-000108 b/packs-system/weapons/MANIFEST-000108
deleted file mode 100644
index 372c91e..0000000
Binary files a/packs-system/weapons/MANIFEST-000108 and /dev/null differ
diff --git a/packs-system/weapons/MANIFEST-000121 b/packs-system/weapons/MANIFEST-000121
new file mode 100644
index 0000000..9498b0d
Binary files /dev/null and b/packs-system/weapons/MANIFEST-000121 differ
diff --git a/templates/chat-lethal-damage.hbs b/templates/chat-lethal-damage.hbs
index 24a436c..4f99c53 100644
--- a/templates/chat-lethal-damage.hbs
+++ b/templates/chat-lethal-damage.hbs
@@ -22,10 +22,20 @@
{{/if}}
-
-
+ {{localize "CTHULHUETERNAL.Label.applyWounds"}}
+
+ {{#each combatants}}
+
+ {{/each}}
+
{{#if isLethal}}
diff --git a/templates/chat-message.hbs b/templates/chat-message.hbs
index 8e89c8d..2cd5496 100644
--- a/templates/chat-message.hbs
+++ b/templates/chat-message.hbs
@@ -150,6 +150,13 @@
{{/unless}}
+ {{#if skillMarkedForProgress}}
+
+
+ {{rollItem.name}} — {{localize "CTHULHUETERNAL.Label.skillFailed"}}
+
+ {{/if}}
+
{{! Zone d'actions regroupées }}
{{#if isSuccess}}
@@ -233,6 +240,15 @@
>
+ {{#if wpCostToSucceed}}
+
+
+ {{localize "CTHULHUETERNAL.Roll.nudgeToSuccess" score=targetScore wp=wpCostToSucceed}}
+
+ {{/if}}
{{/if}}
{{/if}}
diff --git a/templates/chat-regular-damage.hbs b/templates/chat-regular-damage.hbs
index 8f38121..d670ac2 100644
--- a/templates/chat-regular-damage.hbs
+++ b/templates/chat-regular-damage.hbs
@@ -39,8 +39,10 @@
{{#each combatants}}