refactor: remove D30 choice dialog, extract defense reaction buttons, fix bugs
Release Creation / build (release) Successful in 45s

- Remove D30 choice dialog — auto-roll bonus dice, flag special effects
- Fix d30ChangedAttack infinite loop in defense do-while (missing reset)
- Fix chat button dataset attributes (rollType/rollTarget/rollAvantage)
- Extract buildDefenseReactionButtons from both defense loops
- Merge Aether/Grace deduction via _deductResourceOnCast helper
- Extract HP HUD toggling (_toggleHudWraps/_disableHudWraps)
- Fix SYSTEM.EQUIPMENT_CATEGORIES typo in equipment model
- Add missing imports to combat.mjs
- Remove dead d30Auto branches, _buildSpecialLabel, d30-special-choice.hbs
This commit is contained in:
2026-06-29 11:44:46 +02:00
parent 41b1199704
commit 25648aa2a3
10 changed files with 202 additions and 517 deletions
+60 -87
View File
@@ -6,100 +6,73 @@ Fix Grit/Luck defense reaction dialog UX (stacking dialogs, multiple clicks, rev
## Accomplished ## Accomplished
### Pass 1 — Critical Issues ### Pass 1 — Critical Issues
- **Telemetry removed**: `ClassCounter`, `registerWorldCount`, orphaned `worldKey` setting deleted from system.json - **Telemetry removed** from system.json
- **globalThis side effects**: `globalThis.SYSTEM`, `globalThis.pendingDefenses` moved from top-level to `init` hook - **globalThis side effects** moved from top-level to `init` hook
- **console.log → log()**: All runtime console.log replaced with `log()` helper guarded by `lethalFantasy.debug` setting - **console.log → log()** helper guarded by setting
- **Stale Tenebris refs**: `macros.mjs``TENEBRIS.Label.jet``LETHALFANTASY.Label.jet`, `TENEBRIS.Manager.*``LETHALFANTASY.Label.*`, `tenebris.macro` flag → `lethalFantasy.macro` - **Stale Tenebris refs** → LETHALFANTASY
### Pass 2 — V1/V2 Mixing, Fire-and-Forget ### Pass 2 — V1/V2 Mixing, Fire-and-Forget
- **V1 sheet registrations removed**: `foundry.appv1.sheets.*` in system.json - V1 sheet registrations, activateListeners/jQuery, FilePicker paths fixed
- **V1 `activateListeners`/jQuery**: removed dead `defaultOptions`, V1 tab code from `combat.mjs` - Fire-and-forget Promises now awaited
- **V2 API paths**: `FilePicker` → V2, `TextEditor.getDragEventData` → V2, `item.sheet.render(true)``render({force:true})`, `super._onRender()``super._onRender(context, options)`, `token._id``token.id` - Misnamed `LethalFantasySkill``LethalFantasyWeapon`
- **Fire-and-forget Promises**: All `actor.update()`, `ChatMessage.create()`, `prepareRoll()`, `prepareMonsterRoll()`, socket handler calls now awaited
- **Misnamed class**: `LethalFantasySkill``LethalFantasyWeapon`; added missing `WEAPON_TYPE` import; fixed `weaponCategory`
### Pass 3 — Code Review Fixes ### Pass 3 — Code Review Fixes
- **Duplicated dialogs**: Per-element `.rollable`/`.wound-data` bindings moved to `_onRender` (V2 destroys/recreates DOM each render); `_activateListeners` reverted - Duplicated dialogs fixed via `_onRender` bindings
- **renderChatMessage reverted**: V2 hook `renderChatMessage` passes jQuery html, `querySelectorAll` fails; kept `renderChatMessageHTML` - renderChatMessage reverted to HTML hook
- **Roll actions broken**: Fixed `async` base-actor-sheet methods; `_onRender` bindings for rollable elements restored - All review awaits confirmed
- **Token HUD guard**: `html.querySelector()``html.find().length` (html is jQuery object)
- **All review awaits confirmed**: `showDefenseRequest`/`socket` handlers all awaited
## Defense Dialog Investigation — Status ### Pass 4 — D30 Dialog Removal & Dead Code Audit
- **D30 choice dialog removed** — auto-rolls bonus dice; special strike/defense reported as `specialEffect: "flag"` (informational)
- **Spell calamity choice restored** — catch-all for non-standard choices uses `specialEffect: "flag"`
- **Dead `specialEffect === "auto"` branches removed** from chat-reaction.mjs (×2), combat.mjs (×1), reaction-message.hbs
- **Deleted `d30-special-choice.hbs` and `_buildSpecialLabel()`**
- **Dead code audit** — 2 runtime bugs fixed, ~20 dead exports/methods, 33 unused i18n keys, 2 unused templates
- **3 critical bugs fixed**: SYSTEM.ROLL_TYPE, SYSTEM.EQUIPMENT_CATEGORIES, missing imports in combat.mjs
- **`isPrimaryController` consolidated** to local function
- **Aether/Grace deduction merged** via `_deductResourceOnCast()`
- **`nextDefenseData` deduped** via `_storeNextDefenseData()`
- **`buildDefenseReactionButtons` extracted** from combat.mjs; fixes stale Grit/Luck snapshots
- **HP HUD toggling extracted** to helpers.mjs
- **`node --check` passes all 55 `.mjs` files**
### Symptom (user process) ### Pass 5 — Live Verification
1. Monster (GM) attacks player — hits - **D30=30 auto-roll verified** — Club attack shows D30=30 flag
2. Player uses Grit/Luck to boost defense - **Defense request dialog verified** — Monster defense dialog with weapon dropdown
3. Defense now beats attack — reports new result - **Defense reaction dialog verified** — Luck spent, bonus die added, combat result correct
4. Dialog **stays open** — Grit/Luck/bonus dice options still visible - **AZA→Monster attack flow tested end-to-end**: Club attack (D20=16, D30=6) → Monster defense (D20=1) → defense reactions (Continue) → D30 attack bonus processed (+2, total 18)
5. Closing dialog (Continue or X) causes "rolls vanish" — reverts to original result - **BUG FOUND & FIXED: `d30ChangedAttack` infinite loop** — chat-reaction.mjs:452-455 do-while reset block missing `d30ChangedAttack = false`; added at line 456
- **BUG FIX CONFIRMED**: Re-tested full flow — AZA Club attack (13, D30=12) → defense dialog → Monster defense (2, D30=24) → reaction dialog (only 1 show!) → Continue → "AZA hits Monster!" combat result → damage roll (1d6=2, total 3) → Apply Damage button. No infinite loop. Full E2E success.
### Root Cause Found — Duplicate cross-client processing (FIXED) ## Key Decisions
- **Auto-roll bonus dice without dialog** — matches existing D30=27 (d6E) flow
- **`buildDefenseReactionButtons` extracts only button-building** — defense while-loop structures differ between same-client and cross-client; merging loops risks behavioral divergence
- **Inline grit/luck deduction uses live actor values**
- **Aether/Grace helper uses `costFn` parameter**
When monster (GM) attacks player, the `createChatMessage` hook fires on **both** clients: ## Next Steps
1. Test defense request dialogs (character/monster/save) — more variants
2. Test all reaction message variants (shield block/fail, d30Bonus/Flag, grit, luck, etc.)
3. Create Player user in Foundry for cross-client socket testing
4. Prune dead code: unused exports (~20), unused i18n keys (33), unused templates (2)
``` ## Critical Context
Player's client: GM's client: - **Chat buttons not interactive via DevTools snapshot** — need JS fallback: `document.querySelectorAll('button').forEach(b => { if (...) b.click(); })`
defense msg created defense msg synced - **Defense flow**: Attack card → target button (`.request-defense-btn`) → defense dialog → defense roll → defense reactions dialog → combat result card with Damage button → damage roll dialog → Apply Damage → HP application
↓ ↓ - **Clicking "Damage" directly bypasses defense** — rolls unapplied damage to chat
hook fires (line 557) hook fires (line 557) - **Same-owner guard** (`chat-reaction.mjs:180-182`) — skips defense when GM owns both, unless `!defenderIsMonster`
isPrimaryController(defender)=true isPrimaryController(defender)=false - **`d30ChangedAttack` infinite loop** — variable wasn't reset in do-while block; fix at `chat-reaction.mjs:456`
↓ ↓ - **Deserialized weapon object** — `weapon.name` works, `weapon.id` undefined, `weapon._id` works
Defense dialog A shows Defense dialog skipped - **Fvtt server**: port 31000, foundrydata-dev
Player spends Grit Cross-client code (line 1009): - **No player user configured** — cannot test cross-client socket flow
defenseRoll=10→16 isPrimaryController(attacker)=true
While loop exits defenderOwner=player (≠GM)
Comparison: "miss" ↓
**Sends attackBoosted with ORIGINAL
defenseRoll=10 (stale!)**
Player receives socket → handleAttackBoosted
→ Defense dialog B shows with OLD values
→ When closed, comparison: "hit" (overwrites!)
```
Player sees **two** dialogs (A then B). Dialog B uses unboosted values, so closing/ignoring it produces a stale "hit" result that overwrites the correct "miss." ## Relevant Files
- `module/hooks/chat-reaction.mjs` — all 7 hook registrations; defense do-while loop; **d30ChangedAttack fix (line 456)**
### Fix - `module/utils/combat.mjs``buildDefenseReactionButtons`, `_storeNextDefenseData`
`lethal-fantasy.mjs:1016` — only send `attackBoosted` socket when `attackerHandledBonus || attackerHasNonGMOwner`. Guards against stale-socket overwrite for GM→player combat (where hook-based processing works without socket), while preserving socket delegation for PC→PC cross-client (where `attackerIsCrossClient` suppresses the hook-based processing on the defender's client). - `module/utils/d30.mjs``processD30BonusDice`: auto-roll, flag reporting, no dialog
- `module/utils/helpers.mjs``_toggleHudWraps`/`_disableHudWraps`
Before: - `module/utils.mjs` — barrel re-exporting 23 static methods
```js - `module/models/equipment.mjs` — EQUIPMENT_CATEGORY fix
if (defenderOwner && defenderOwner.id !== game.user.id) { - `module/applications/combat.mjs` — added imports
game.socket.emit(`system.${SYSTEM.id}`, { type: "attackBoosted", ... }) - `templates/chat/reaction-message.hbs` — d30 removed
return - `templates/dialogs/d30-special-choice.hbs` — deleted
} - `lang/en.json` — 33 unused i18n keys remain
```
After:
```js
if (defenderOwner && defenderOwner.id !== game.user.id) {
if (attackerHandledBonus || attackerHasNonGMOwner) {
game.socket.emit(`system.${SYSTEM.id}`, { type: "attackBoosted", ... })
}
return
}
```
### Same-Client Path
Code pattern is identical between attack and defense dialogs — both use `await DialogV2.wait({rejectClose:false})` in a while loop. Same-client defense works correctly because no duplicate socket messages arrive.
### Other Findings
- `offerGritLuckBonus` (`utils.mjs:1121`) is dead code — never called
- `promptCombatBonusDie` (`utils.mjs:975`) is correct — DialogV2 resolves to callback return value, not `action`
- Cross-client `handleAttackBoosted` (`utils.mjs:291`) still uses `else if` chain without `continue` — functionally correct but differs from same-client pattern
### Code Paths
| Flow | File | Line |
|------|------|------|
| Same-client attack | `lethal-fantasy.mjs` | 918-1004 |
| Same-client defense | `lethal-fantasy.mjs` | 697-870 |
| Cross-client defense | `module/utils.mjs` | 291-445 |
| Cross-client socket guard | `lethal-fantasy.mjs` | 1006-1037 |
| Attack Grit offer | `module/utils.mjs` | 1210-1290 |
### Key Files
- `lethal-fantasy.mjs` — Main system hooks, same-client attack/defense reactions
- `module/utils.mjs` — Cross-client defense flow, bonus dialogs, compareAttackDefense
- `module/documents/actor.mjs``prepareRoll()` entry point
- `module/documents/roll.mjs` — Roll resolution pipeline
+2
View File
@@ -1,3 +1,5 @@
import { SYSTEM } from "../config/system.mjs"
import { log } from "../utils.mjs"
/* -------------------------------------------- */ /* -------------------------------------------- */
export class LethalFantasyCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { export class LethalFantasyCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
+39 -149
View File
@@ -14,12 +14,12 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => {
} else { } else {
for (const btn of html.querySelectorAll(".ask-roll-dice")) { for (const btn of html.querySelectorAll(".ask-roll-dice")) {
btn.addEventListener("click", () => { btn.addEventListener("click", () => {
const type = btn.dataset.type const type = btn.dataset.rollType
const value = btn.dataset.value const value = btn.dataset.rollTarget
const avantage = btn.dataset.avantage ?? "=" const avantage = btn.dataset.rollAvantage ?? "normal"
const character = game.user.character const character = game.user.character
if (type === SYSTEM.ROLL_TYPE.RESOURCE) character.rollResource(value) if (type === "resource") character.rollResource(value)
else if (type === SYSTEM.ROLL_TYPE.SAVE) character.rollSave(value, avantage) else if (type === "save") character.rollSave(value, avantage)
}) })
} }
} }
@@ -453,6 +453,7 @@ Hooks.on("createChatMessage", async (message) => {
mulliganRestart = false mulliganRestart = false
defenderHandledBonus = false defenderHandledBonus = false
attackerHandledBonus = false attackerHandledBonus = false
d30ChangedAttack = false
// ── D30 bonus dice (defense) — resolved before grit/luck/shield ─────── // ── D30 bonus dice (defense) — resolved before grit/luck/shield ───────
if (defenseD30message && !defenseD30Processed && isPrimaryController(defender) && !attackerIsCrossClient) { if (defenseD30message && !defenseD30Processed && isPrimaryController(defender) && !attackerIsCrossClient) {
@@ -463,10 +464,6 @@ Hooks.on("createChatMessage", async (message) => {
await createReactionMessage(defender, {type:"d30Bonus", actorName:defenderName, value:d30Result.modifier, side:"defense"}) await createReactionMessage(defender, {type:"d30Bonus", actorName:defenderName, value:d30Result.modifier, side:"defense"})
} }
} }
if (d30Result.specialEffect === "auto") {
defenseRoll = attackRollFinal + 1 // auto-block
await createReactionMessage(defender, {type:"d30Auto", actorName:defenderName, specialName:d30Result.specialName||"Special Defense", side:"defense"})
}
if (d30Result.specialEffect === "flag") { if (d30Result.specialEffect === "flag") {
await createReactionMessage(defender, {type:"d30Flag", actorName:defenderName, specialName:d30Result.specialName||"Special Effect"}) await createReactionMessage(defender, {type:"d30Flag", actorName:defenderName, specialName:d30Result.specialName||"Special Effect"})
} }
@@ -483,66 +480,7 @@ Hooks.on("createChatMessage", async (message) => {
// create the comparison message with the updated attack roll. // create the comparison message with the updated attack roll.
if (defender && defenseRoll < attackRollFinal && isPrimaryController(defender) && !isSpellOrMiracle && !attackerIsCrossClient && !d30PendingFromGM) { if (defender && defenseRoll < attackRollFinal && isPrimaryController(defender) && !isSpellOrMiracle && !attackerIsCrossClient && !d30PendingFromGM) {
while (defenseRoll < attackRollFinal) { while (defenseRoll < attackRollFinal) {
const currentGrit = Number(defender.system?.grit?.current) || 0 const buttons = LethalFantasyUtils.buildDefenseReactionButtons(defender, { canRerollDefense, shieldData, canShieldReact, canAdHocShield })
const currentLuck = Number(defender.system?.luck?.current) || 0
const buttons = []
if (currentGrit > 0) {
buttons.push({
action: "grit",
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
icon: "fa-solid fa-fist-raised",
callback: () => "grit"
})
}
if (currentLuck > 0) {
buttons.push({
action: "luck",
label: `Spend 1 Luck (+1D6) [${currentLuck} left]`,
icon: "fa-solid fa-clover",
callback: () => "luck"
})
}
buttons.push({
action: "bonusDie",
label: "Add bonus die",
icon: "fa-solid fa-dice",
callback: () => "bonusDie"
})
if (canRerollDefense) {
buttons.push({
action: "rerollDefense",
label: "Re-roll defense (Mulligan)",
icon: "fa-solid fa-rotate-right",
callback: () => "rerollDefense"
})
}
if (canShieldReact) {
buttons.push({
action: "shieldReact",
label: `Roll shield (${shieldData.label})`,
icon: "fa-solid fa-shield",
callback: () => "shieldReact"
})
} else if (canAdHocShield) {
buttons.push({
action: "adHocShield",
label: "Roll ad-hoc shield (choose dice + DR)",
icon: "fa-solid fa-shield-halved",
callback: () => "adHocShield"
})
}
buttons.push({
action: "continue",
label: "Continue (no defense bonus)",
icon: "fa-solid fa-forward",
callback: () => "continue"
})
const dialogContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/defense-reaction.hbs", { const dialogContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/defense-reaction.hbs", {
attackerName, attackerName,
@@ -569,7 +507,7 @@ Hooks.on("createChatMessage", async (message) => {
if (choice === "grit") { if (choice === "grit") {
const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender) const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender)
defenseRoll += bonusRoll defenseRoll += bonusRoll
await defender.update({ "system.grit.current": currentGrit - 1 }) await defender.update({ "system.grit.current": Math.max(0, (Number(defender.system?.grit?.current) || 0) - 1) })
await createReactionMessage(defender, {type:"grit", actorName:defenderName, resource:"Grit", value:bonusRoll, side:"defense"}) await createReactionMessage(defender, {type:"grit", actorName:defenderName, resource:"Grit", value:bonusRoll, side:"defense"})
continue continue
} }
@@ -577,7 +515,7 @@ Hooks.on("createChatMessage", async (message) => {
if (choice === "luck") { if (choice === "luck") {
const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender) const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender)
defenseRoll += bonusRoll defenseRoll += bonusRoll
await defender.update({ "system.luck.current": currentLuck - 1 }) await defender.update({ "system.luck.current": Math.max(0, (Number(defender.system?.luck?.current) || 0) - 1) })
await createReactionMessage(defender, {type:"luck", actorName:defenderName, resource:"Luck", value:bonusRoll, side:"defense"}) await createReactionMessage(defender, {type:"luck", actorName:defenderName, resource:"Luck", value:bonusRoll, side:"defense"})
continue continue
} }
@@ -675,12 +613,6 @@ Hooks.on("createChatMessage", async (message) => {
await createReactionMessage(attacker, {type:"d30Bonus", actorName:attackerName, value:d30Result.modifier, side:"attack"}) await createReactionMessage(attacker, {type:"d30Bonus", actorName:attackerName, value:d30Result.modifier, side:"attack"})
} }
} }
if (d30Result.specialEffect === "auto") {
attackRollFinal = defenseRoll + 1 // auto-hit
if (canDialog) {
await createReactionMessage(attacker, {type:"d30Auto", actorName:attackerName, specialName:d30Result.specialName||"Special Strike", side:"attack"})
}
}
if (d30Result.specialEffect === "flag" && canDialog) { if (d30Result.specialEffect === "flag" && canDialog) {
await createReactionMessage(attacker, {type:"d30Flag", actorName:attackerName, specialName:d30Result.specialName||"Special Effect"}) await createReactionMessage(attacker, {type:"d30Flag", actorName:attackerName, specialName:d30Result.specialName||"Special Effect"})
} }
@@ -901,93 +833,51 @@ Hooks.on("createChatMessage", async (message) => {
} }
}) })
// Hook: deduct aether when a spell-attack or spell-power roll is posted to chat async function _deductResourceOnCast(message, rollTypes, itemType, costFn, resourceField, templateType) {
Hooks.on("createChatMessage", async (message) => { if (!rollTypes.includes(message.rolls[0]?.options?.rollType)) return
if (!["spell-attack", "spell-power"].includes(message.rolls[0]?.options?.rollType)) return
const actorId = message.rolls[0]?.options?.actorId const actorId = message.rolls[0]?.options?.actorId
if (!actorId) return if (!actorId) return
const actor = game.actors.get(actorId) const actor = game.actors.get(actorId)
if (!actor) return if (!actor) return
if (!isPrimaryController(actor)) return
// Only the primary controller (player owner or GM) handles this
const activePlayerOwners = game.users.filter(u => u.active && !u.isGM && actor.testUserPermission(u, "OWNER"))
const isPrimary = activePlayerOwners.length > 0
? activePlayerOwners[0].id === game.user.id
: game.user.isGM
if (!isPrimary) return
const rollTarget = message.rolls[0]?.options?.rollTarget const rollTarget = message.rolls[0]?.options?.rollTarget
const spellId = rollTarget?.id || rollTarget?._id const itemId = rollTarget?.id || rollTarget?._id
const spell = spellId ? actor.items.get(spellId) : null const item = itemId ? actor.items.get(itemId) : null
if (!spell || spell.type !== "spell") return if (!item || item.type !== itemType) return
const damageTier = message.rolls[0]?.options?.damageTier || "standard" const damageTier = message.rolls[0]?.options?.damageTier || "standard"
const tierCostMap = { standard: "cost", overpowered: "costOverpowered", overpowered2: "costOverpowered2" } const cost = costFn(item, damageTier)
const costField = tierCostMap[damageTier] || "cost"
const cost = Number(spell.system?.[costField]) || 0
if (cost <= 0) return if (cost <= 0) return
const currentAether = Number(actor.system.aetherPoints?.value) || 0 const current = Number(foundry.utils.getProperty(actor.system, resourceField)) || 0
const newAether = Math.max(0, currentAether - cost) const newValue = Math.max(0, current - cost)
await actor.update({ "system.aetherPoints.value": newAether }) await actor.update({ [`system.${resourceField}`]: newValue })
const tierLabel = damageTier === "standard" ? "" : ` (${damageTier})` const tierLabel = damageTier === "standard" ? "" : ` (${damageTier})`
const aetherContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", { const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {
type: "aetherSpend", type: templateType, actorName: actor.name, spellName: item.name, tierLabel,
actorName: actor.name, value: cost, oldValue: current, newValue
spellName: spell.name,
tierLabel,
value: cost,
oldValue: currentAether,
newValue: newAether
})
await ChatMessage.create({
content: aetherContent,
speaker: ChatMessage.getSpeaker({ actor })
})
}) })
await ChatMessage.create({ content, speaker: ChatMessage.getSpeaker({ actor }) })
}
// Hook: deduct aether when a spell-attack or spell-power roll is posted to chat
Hooks.on("createChatMessage", (message) => _deductResourceOnCast(message,
["spell-attack", "spell-power"], "spell",
(item, tier) => {
const m = { standard: "cost", overpowered: "costOverpowered", overpowered2: "costOverpowered2" }
return Number(item.system?.[m[tier] || "cost"]) || 0
},
"aetherPoints.value", "aetherSpend"
))
// Hook: deduct grace when a miracle-attack or miracle-power roll is posted to chat // Hook: deduct grace when a miracle-attack or miracle-power roll is posted to chat
Hooks.on("createChatMessage", async (message) => { Hooks.on("createChatMessage", (message) => _deductResourceOnCast(message,
if (!["miracle-attack", "miracle-power"].includes(message.rolls[0]?.options?.rollType)) return ["miracle-attack", "miracle-power"], "miracle",
(item) => Number(item.system?.level) || 0,
const actorId = message.rolls[0]?.options?.actorId "divinityPoints.value", "graceSpend"
if (!actorId) return ))
const actor = game.actors.get(actorId)
if (!actor) return
const activePlayerOwners = game.users.filter(u => u.active && !u.isGM && actor.testUserPermission(u, "OWNER"))
const isPrimary = activePlayerOwners.length > 0
? activePlayerOwners[0].id === game.user.id
: game.user.isGM
if (!isPrimary) return
const rollTarget = message.rolls[0]?.options?.rollTarget
const miracleId = rollTarget?.id || rollTarget?._id
const miracle = miracleId ? actor.items.get(miracleId) : null
if (!miracle || miracle.type !== "miracle") return
const cost = Number(miracle.system?.level) || 0
if (cost <= 0) return
const currentGrace = Number(actor.system.divinityPoints?.value) || 0
const newGrace = Math.max(0, currentGrace - cost)
await actor.update({ "system.divinityPoints.value": newGrace })
const graceContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {
type: "graceSpend",
actorName: actor.name,
spellName: miracle.name,
value: cost,
oldValue: currentGrace,
newValue: newGrace
})
await ChatMessage.create({
content: graceContent,
speaker: ChatMessage.getSpeaker({ actor })
})
})
// Hook pour appliquer automatiquement les dégâts si une cible est définie // Hook pour appliquer automatiquement les dégâts si une cible est définie
Hooks.on("createChatMessage", async (message) => { Hooks.on("createChatMessage", async (message) => {
+1 -1
View File
@@ -7,7 +7,7 @@ export default class LethalFantasyEquipment extends foundry.abstract.TypeDataMod
const requiredInteger = { required: true, nullable: false, integer: true } const requiredInteger = { required: true, nullable: false, integer: true }
schema.description = new fields.HTMLField({ required: true, textSearch: true }) schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.category = new fields.StringField({ required: true, initial: "tinbit", choices: SYSTEM.EQUIPMENT_CATEGORIES }) schema.category = new fields.StringField({ required: true, initial: "tinbit", choices: SYSTEM.EQUIPMENT_CATEGORY })
schema.encLoad = new fields.NumberField({ required: true, initial: 0, min: 0 }) schema.encLoad = new fields.NumberField({ required: true, initial: 0, min: 0 })
schema.hi = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }) schema.hi = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 })
+3 -3
View File
@@ -1,6 +1,6 @@
import { log, loadCompendiumData, loadCompendium, pushCombatOptions, setHookListeners, registerHandlebarsHelpers, getLethargyDice } from "./utils/helpers.mjs" import { log, loadCompendiumData, loadCompendium, pushCombatOptions, setHookListeners, registerHandlebarsHelpers, getLethargyDice } from "./utils/helpers.mjs"
import { hasD30Reroll, processD30BonusDice, _rollD30BonusDie, _buildSpecialLabel, _buildSpecialName } from "./utils/d30.mjs" import { hasD30Reroll, processD30BonusDice, _rollD30BonusDie, _buildSpecialName } from "./utils/d30.mjs"
import { handleSocketEvent, handleAttackerGritOffer, handleAttackBoosted, showDefenseRequest, compareAttackDefense, applyDamage, offerAttackerGritBonus, getCombatBonusDiceChoices, getShieldReactionData, promptCombatBonusDie, promptAdHocShield, rollBonusDie, rerollConfiguredRoll } from "./utils/combat.mjs" import { handleSocketEvent, handleAttackerGritOffer, handleAttackBoosted, showDefenseRequest, compareAttackDefense, applyDamage, offerAttackerGritBonus, getCombatBonusDiceChoices, getShieldReactionData, buildDefenseReactionButtons, promptCombatBonusDie, promptAdHocShield, rollBonusDie, rerollConfiguredRoll } from "./utils/combat.mjs"
export { log } export { log }
@@ -14,7 +14,6 @@ export default class LethalFantasyUtils {
static hasD30Reroll = hasD30Reroll static hasD30Reroll = hasD30Reroll
static processD30BonusDice = processD30BonusDice static processD30BonusDice = processD30BonusDice
static _rollD30BonusDie = _rollD30BonusDie static _rollD30BonusDie = _rollD30BonusDie
static _buildSpecialLabel = _buildSpecialLabel
static _buildSpecialName = _buildSpecialName static _buildSpecialName = _buildSpecialName
static handleSocketEvent = handleSocketEvent static handleSocketEvent = handleSocketEvent
static handleAttackerGritOffer = handleAttackerGritOffer static handleAttackerGritOffer = handleAttackerGritOffer
@@ -25,6 +24,7 @@ export default class LethalFantasyUtils {
static offerAttackerGritBonus = offerAttackerGritBonus static offerAttackerGritBonus = offerAttackerGritBonus
static getCombatBonusDiceChoices = getCombatBonusDiceChoices static getCombatBonusDiceChoices = getCombatBonusDiceChoices
static getShieldReactionData = getShieldReactionData static getShieldReactionData = getShieldReactionData
static buildDefenseReactionButtons = buildDefenseReactionButtons
static promptCombatBonusDie = promptCombatBonusDie static promptCombatBonusDie = promptCombatBonusDie
static promptAdHocShield = promptAdHocShield static promptAdHocShield = promptAdHocShield
static rollBonusDie = rollBonusDie static rollBonusDie = rollBonusDie
+44 -135
View File
@@ -124,11 +124,6 @@ export async function handleAttackBoosted(msg) {
await ChatMessage.create({content: msg, speaker: ChatMessage.getSpeaker({actor: defender})}) await ChatMessage.create({content: msg, speaker: ChatMessage.getSpeaker({actor: defender})})
} }
} }
if (d30Result.specialEffect === "auto") {
updatedDefenseRoll = attackRollFinal + 1
const msg = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"d30Auto", actorName:defenderName, specialName:d30Result.specialName || "Special Defense", side:"defense"})
await ChatMessage.create({content: msg, speaker: ChatMessage.getSpeaker({actor: defender})})
}
if (d30Result.specialEffect === "flag") { if (d30Result.specialEffect === "flag") {
const msg = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"d30Flag", actorName:defenderName, specialName:d30Result.specialName || "Special Effect"}) const msg = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"d30Flag", actorName:defenderName, specialName:d30Result.specialName || "Special Effect"})
await ChatMessage.create({content: msg, speaker: ChatMessage.getSpeaker({actor: defender})}) await ChatMessage.create({content: msg, speaker: ChatMessage.getSpeaker({actor: defender})})
@@ -143,63 +138,8 @@ export async function handleAttackBoosted(msg) {
// Show the defense reaction dialog — while-loop for multiple reactions // Show the defense reaction dialog — while-loop for multiple reactions
if (defender) { if (defender) {
while (updatedDefenseRoll < attackRollFinal) { while (updatedDefenseRoll < attackRollFinal) {
const currentGrit = Number(defender.system?.grit?.current) || 0 const shieldData = canShieldReact ? { label: shieldLabel, formula: shieldFormula, damageReduction: shieldDr } : null
const currentLuck = Number(defender.system?.luck?.current) || 0 const buttons = buildDefenseReactionButtons(defender, { canRerollDefense: false, shieldData, canShieldReact, canAdHocShield: canAdHoc })
const buttons = []
if (currentGrit > 0) {
buttons.push({
action: "grit",
type: "button",
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
icon: "fa-solid fa-fist-raised",
callback: () => "grit"
})
}
if (currentLuck > 0) {
buttons.push({
action: "luck",
type: "button",
label: `Spend 1 Luck (+1D6) [${currentLuck} left]`,
icon: "fa-solid fa-clover",
callback: () => "luck"
})
}
buttons.push({
action: "bonusDie",
type: "button",
label: "Add bonus die",
icon: "fa-solid fa-dice",
callback: () => "bonusDie"
})
if (canShieldReact) {
buttons.push({
action: "shieldReact",
type: "button",
label: `Roll shield (${shieldLabel})`,
icon: "fa-solid fa-shield",
callback: () => "shieldReact"
})
} else if (canAdHoc) {
buttons.push({
action: "adHocShield",
type: "button",
label: "Roll ad-hoc shield (choose dice + DR)",
icon: "fa-solid fa-shield-halved",
callback: () => "adHocShield"
})
}
buttons.push({
action: "continue",
type: "button",
label: "Continue (no defense bonus)",
icon: "fa-solid fa-forward",
callback: () => "continue"
})
const choice = await foundry.applications.api.DialogV2.wait({ const choice = await foundry.applications.api.DialogV2.wait({
window: { title: "Defense reactions — attack boosted" }, window: { title: "Defense reactions — attack boosted" },
@@ -223,13 +163,13 @@ export async function handleAttackBoosted(msg) {
if (choice === "grit") { if (choice === "grit") {
const bonusRoll = await rollBonusDie("1d6", defender) const bonusRoll = await rollBonusDie("1d6", defender)
updatedDefenseRoll += bonusRoll updatedDefenseRoll += bonusRoll
await defender.update({ "system.grit.current": currentGrit - 1 }) await defender.update({ "system.grit.current": Math.max(0, (Number(defender.system?.grit?.current) || 0) - 1) })
const gritRmContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"grit", actorName:defenderName, resource:"Grit", value:bonusRoll, side:"defense"}) const gritRmContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"grit", actorName:defenderName, resource:"Grit", value:bonusRoll, side:"defense"})
await ChatMessage.create({content: gritRmContent, speaker: ChatMessage.getSpeaker({actor: defender})}) await ChatMessage.create({content: gritRmContent, speaker: ChatMessage.getSpeaker({actor: defender})})
} else if (choice === "luck") { } else if (choice === "luck") {
const bonusRoll = await rollBonusDie("1d6", defender) const bonusRoll = await rollBonusDie("1d6", defender)
updatedDefenseRoll += bonusRoll updatedDefenseRoll += bonusRoll
await defender.update({ "system.luck.current": currentLuck - 1 }) await defender.update({ "system.luck.current": Math.max(0, (Number(defender.system?.luck?.current) || 0) - 1) })
const luckRmContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"luck", actorName:defenderName, resource:"Luck", value:bonusRoll, side:"defense"}) const luckRmContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"luck", actorName:defenderName, resource:"Luck", value:bonusRoll, side:"defense"})
await ChatMessage.create({content: luckRmContent, speaker: ChatMessage.getSpeaker({actor: defender})}) await ChatMessage.create({content: luckRmContent, speaker: ChatMessage.getSpeaker({actor: defender})})
} else if (choice === "bonusDie") { } else if (choice === "bonusDie") {
@@ -364,6 +304,19 @@ export async function showDefenseRequest(msg) {
const isMonster = defender.type === "monster" const isMonster = defender.type === "monster"
const _storeNextDefenseData = (opts = {}) => {
game.lethalFantasy = game.lethalFantasy || {}
game.lethalFantasy.nextDefenseData = {
attackerId, attackRoll, attackerName, defenderName,
attackWeaponId, attackRollType, attackRollKey,
attackD30result, attackD30message, attackRerollContext,
damageTier: msg.damageTier,
defenderId: defender.id, defenderTokenId,
...(msg.attackNaturalRoll !== undefined && { attackNaturalRoll: msg.attackNaturalRoll }),
...(opts.isRanged !== undefined && { isRanged: opts.isRanged })
}
}
log(`[LF] showDefenseRequest | attackRollType=${attackRollType} isMonster=${isMonster} defender=${defender?.name}`) log(`[LF] showDefenseRequest | attackRollType=${attackRollType} isMonster=${isMonster} defender=${defender?.name}`)
// Spell/miracle attacks use saving throws instead of weapon defense // Spell/miracle attacks use saving throws instead of weapon defense
@@ -398,22 +351,7 @@ export async function showDefenseRequest(msg) {
if (result) { if (result) {
game.lethalFantasy = game.lethalFantasy || {} game.lethalFantasy = game.lethalFantasy || {}
game.lethalFantasy.spellDefense = true // pré-cocher "Save against spell" dans le dialog game.lethalFantasy.spellDefense = true // pré-cocher "Save against spell" dans le dialog
game.lethalFantasy.nextDefenseData = { _storeNextDefenseData()
attackerId,
attackRoll,
attackerName,
defenderName,
attackWeaponId,
attackRollType,
attackRollKey,
attackD30result,
attackD30message,
attackRerollContext,
attackNaturalRoll: msg.attackNaturalRoll,
damageTier: msg.damageTier,
defenderId: defender.id,
defenderTokenId
}
if (isMonster) { if (isMonster) {
await defender.system.prepareMonsterRoll("save", result) await defender.system.prepareMonsterRoll("save", result)
} else { } else {
@@ -462,25 +400,7 @@ export async function showDefenseRequest(msg) {
// Si l'utilisateur a validé, lancer le jet de défense // Si l'utilisateur a validé, lancer le jet de défense
if (result) { if (result) {
// Stocker temporairement les données pour le hook preCreateChatMessage _storeNextDefenseData({ isRanged: msg.isRanged })
game.lethalFantasy = game.lethalFantasy || {}
game.lethalFantasy.nextDefenseData = {
attackerId,
attackRoll,
attackerName,
defenderName,
attackWeaponId,
attackRollType,
attackRollKey,
attackD30result,
attackD30message,
attackRerollContext,
attackNaturalRoll: msg.attackNaturalRoll,
damageTier: msg.damageTier,
defenderId: defender.id,
defenderTokenId,
isRanged: msg.isRanged
}
await defender.system.prepareMonsterRoll("monster-defense", result) await defender.system.prepareMonsterRoll("monster-defense", result)
} }
@@ -497,23 +417,7 @@ export async function showDefenseRequest(msg) {
actorImage: defender.img, actorImage: defender.img,
}) })
if (roll) { if (roll) {
game.lethalFantasy = game.lethalFantasy || {} _storeNextDefenseData({ isRanged: true })
game.lethalFantasy.nextDefenseData = {
attackerId,
attackRoll,
attackerName,
defenderName,
attackWeaponId,
attackRollType,
attackRollKey,
attackD30result,
attackD30message,
attackRerollContext,
damageTier: msg.damageTier,
defenderId: defender.id,
defenderTokenId,
isRanged: true
}
await roll.toMessage({}, { messageMode: roll.options.rollMode }) await roll.toMessage({}, { messageMode: roll.options.rollMode })
} }
return return
@@ -558,25 +462,7 @@ export async function showDefenseRequest(msg) {
// Si l'utilisateur a validé, lancer le jet de défense // Si l'utilisateur a validé, lancer le jet de défense
if (result) { if (result) {
// Stocker temporairement les données pour le hook preCreateChatMessage _storeNextDefenseData({ isRanged: msg.isRanged })
game.lethalFantasy = game.lethalFantasy || {}
game.lethalFantasy.nextDefenseData = {
attackerId,
attackRoll,
attackerName,
defenderName,
attackWeaponId,
attackRollType,
attackRollKey,
attackD30result,
attackD30message,
attackRerollContext,
attackNaturalRoll: msg.attackNaturalRoll,
damageTier: msg.damageTier,
defenderId: defender.id,
defenderTokenId,
isRanged: msg.isRanged
}
log("Storing defense data for character:", defender.id) log("Storing defense data for character:", defender.id)
@@ -584,6 +470,29 @@ export async function showDefenseRequest(msg) {
} }
} }
export function buildDefenseReactionButtons(defender, { canRerollDefense = false, shieldData = null, canShieldReact = false, canAdHocShield = false } = {}) {
const currentGrit = Number(defender.system?.grit?.current) || 0
const currentLuck = Number(defender.system?.luck?.current) || 0
const buttons = []
if (currentGrit > 0) {
buttons.push({ action: "grit", type: "button", label: `Spend 1 Grit (+1D6) [${currentGrit} left]`, icon: "fa-solid fa-fist-raised", callback: () => "grit" })
}
if (currentLuck > 0) {
buttons.push({ action: "luck", type: "button", label: `Spend 1 Luck (+1D6) [${currentLuck} left]`, icon: "fa-solid fa-clover", callback: () => "luck" })
}
buttons.push({ action: "bonusDie", type: "button", label: "Add bonus die", icon: "fa-solid fa-dice", callback: () => "bonusDie" })
if (canRerollDefense) {
buttons.push({ action: "rerollDefense", type: "button", label: "Re-roll defense (Mulligan)", icon: "fa-solid fa-rotate-right", callback: () => "rerollDefense" })
}
if (canShieldReact && shieldData) {
buttons.push({ action: "shieldReact", type: "button", label: `Roll shield (${shieldData.label})`, icon: "fa-solid fa-shield", callback: () => "shieldReact" })
} else if (canAdHocShield) {
buttons.push({ action: "adHocShield", type: "button", label: "Roll ad-hoc shield (choose dice + DR)", icon: "fa-solid fa-shield-halved", callback: () => "adHocShield" })
}
buttons.push({ action: "continue", type: "button", label: "Continue (no defense bonus)", icon: "fa-solid fa-forward", callback: () => "continue" })
return buttons
}
export function getCombatBonusDiceChoices() { export function getCombatBonusDiceChoices() {
return ["1d4", "1d6", "1d8", "1d10", "1d12", "1d20", "1d20e"] return ["1d4", "1d6", "1d8", "1d10", "1d12", "1d20", "1d20e"]
} }
+19 -74
View File
@@ -28,63 +28,34 @@ export async function processD30BonusDice(d30Message, side, naturalRoll = null,
return { modifier, specialEffect: null, specialName: null } return { modifier, specialEffect: null, specialName: null }
} }
// ── Choice type ── present all options to the player // ── Choice type ── auto-roll bonus dice, alert about special effects
if (d30Message.type === "choice") { if (d30Message.type === "choice") {
// If we can't show dialogs (wrong client), skip — the primary client // Non-controlling client can't roll dice here — the controlling client
// will communicate its choice result via socket. Auto-rolling here // sends the updated values via socket.
// would give a different modifier on each client, causing divergence.
if (!canDialog) { if (!canDialog) {
return { modifier: 0, specialEffect: null, specialName: null } return { modifier: 0, specialEffect: null, specialName: null }
} }
const buttons = d30Message.choices.map(c => { // Auto-roll bonus dice (like d6E on 27 — no dialog)
let label const bonusChoice = d30Message.choices.find(c => c.type === "bonus_dice")
let icon let modifier = 0
if (c.type === "bonus_dice") { if (bonusChoice) {
label = `Roll ${c.dice.toUpperCase()} and add to ${side}` modifier = await _rollD30BonusDie(bonusChoice.dice, actor)
icon = "fa-solid fa-dice"
} else if (c.type === "special_strike") {
label = _buildSpecialLabel(c, naturalRoll)
icon = "fa-solid fa-star"
} else if (c.type === "special_defense") {
label = _buildSpecialLabel(c, naturalRoll)
icon = "fa-solid fa-shield-halved"
} else {
label = c.type.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase())
icon = "fa-solid fa-question"
}
return {
action: c.type,
type: "button",
label,
icon,
callback: () => c
}
})
const choice = await foundry.applications.api.DialogV2.wait({
window: { title: "D30 Special — Choose Effect" },
classes: ["lethalfantasy"],
content: await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/d30-special-choice.hbs", {
description: d30Message.description
}),
buttons,
rejectClose: false
})
if (!choice) return { modifier: 0, specialEffect: null, specialName: null }
if (choice.type === "bonus_dice") {
const modifier = await _rollD30BonusDie(choice.dice, actor)
return { modifier, specialEffect: null, specialName: null }
} }
if (choice.type === "special_strike" || choice.type === "special_defense") { // Inform about special strike/defense or other effects (informational only)
return { modifier: 0, specialEffect: "auto", specialName: _buildSpecialName(choice, naturalRoll) } const specialChoice = d30Message.choices.find(c => c.type === "special_strike" || c.type === "special_defense")
if (specialChoice) {
return { modifier, specialEffect: "flag", specialName: _buildSpecialName(specialChoice, naturalRoll) }
} }
// Non-standard choice (spell_calamity, etc.) — report it // Non-standard choice (spell_calamity, etc.) — report it
return { modifier: 0, specialEffect: "flag", specialName: choice.type } const nonStandardChoice = d30Message.choices.find(c => c.type !== "bonus_dice")
if (nonStandardChoice) {
return { modifier, specialEffect: "flag", specialName: _buildSpecialName(nonStandardChoice, naturalRoll) }
}
return { modifier, specialEffect: null, specialName: null }
} }
// ── Combo type (bleed / internal injury) — flag for wound creation // ── Combo type (bleed / internal injury) — flag for wound creation
@@ -130,32 +101,6 @@ export async function _rollD30BonusDie(formula, actor, silent = false) {
return roll.total return roll.total
} }
/**
* Build a human-readable label for a special strike/defense choice in the D30 prompt.
* @param {Object} specialChoice The choice object with type and options
* @param {number|null} naturalRoll The natural D20 roll
* @returns {string} Display label
*/
export function _buildSpecialLabel(specialChoice, naturalRoll) {
if (specialChoice.type === "special_strike") {
if (specialChoice.options.includes("lethal")) {
if (naturalRoll === 20) return "Lethal Strike (auto-hit)"
if (naturalRoll >= 16 && naturalRoll <= 19) return "Vital Strike (auto-hit)"
return "Lethal/Vital Strike (auto-hit)"
}
if (specialChoice.options.includes("vicious")) return "Vicious Strike (auto-hit)"
return "Special Strike (auto-hit)"
}
if (specialChoice.type === "special_defense") {
if (specialChoice.options.includes("perfect_spell")) return "Perfect Spell Defense (auto-block)"
if (specialChoice.options.includes("flawless")) return "Flawless Defense (auto-block)"
if (specialChoice.options.includes("legendary")) return "Legendary Defense (auto-block)"
if (specialChoice.options.includes("perfect")) return "Perfect Defense (auto-block)"
return "Special Defense (auto-block)"
}
return "Special Effect"
}
/** /**
* Build the special effect name based on the D30 result and natural roll. * Build the special effect name based on the D30 result and natural roll.
* @param {Object} specialChoice The choice object with type and options * @param {Object} specialChoice The choice object with type and options
@@ -179,5 +124,5 @@ export function _buildSpecialName(specialChoice, naturalRoll) {
if (specialChoice.options.includes("perfect")) return "Perfect Defense" if (specialChoice.options.includes("perfect")) return "Perfect Defense"
return "Special Defense" return "Special Defense"
} }
return "Special Effect" return specialChoice.type.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase())
} }
+26 -54
View File
@@ -28,83 +28,55 @@ export function setHookListeners() {
// hud.token / hud.object gives the Token (PlaceableObject), which has .actor. // hud.token / hud.object gives the Token (PlaceableObject), which has .actor.
const hudActor = hud.token?.actor ?? hud.object?.actor const hudActor = hud.token?.actor ?? hud.object?.actor
if (!hudActor) return if (!hudActor) return
// HP Loss Button (existing) const _toggleHudWraps = (prefix) => {
const enable = $(html).find(`.${prefix}-wrap`)[0].classList.contains(`${prefix}-hud-disabled`)
for (let i = 0; i < 3; i++) {
const w = $(html).find(`.${prefix}-wrap`)[i]
w.classList.toggle(`${prefix}-hud-active`, enable)
w.classList.toggle(`${prefix}-hud-disabled`, !enable)
}
}
const _disableHudWraps = (prefix) => {
for (let i = 0; i < 3; i++) {
const w = $(html).find(`.${prefix}-wrap`)[i]
w.classList.remove(`${prefix}-hud-active`)
w.classList.add(`${prefix}-hud-disabled`)
}
}
// HP Loss Button
const lossHPButton = await foundry.applications.handlebars.renderTemplate('systems/fvtt-lethal-fantasy/templates/loss-hp-hud.hbs', {}) const lossHPButton = await foundry.applications.handlebars.renderTemplate('systems/fvtt-lethal-fantasy/templates/loss-hp-hud.hbs', {})
$(html).find('div.left').append(lossHPButton); $(html).find('div.left').append(lossHPButton);
$(html).find('img.lethal-hp-loss-hud').click((event) => { $(html).find('img.lethal-hp-loss-hud').click((event) => {
event.preventDefault(); event.preventDefault();
let hpMenu = $(html).find('.hp-loss-wrap')[0] _toggleHudWraps("hp-loss")
if (hpMenu.classList.contains("hp-loss-hud-disabled")) {
$(html).find('.hp-loss-wrap')[0].classList.add('hp-loss-hud-active');
$(html).find('.hp-loss-wrap')[0].classList.remove('hp-loss-hud-disabled');
$(html).find('.hp-loss-wrap')[1].classList.add('hp-loss-hud-active');
$(html).find('.hp-loss-wrap')[1].classList.remove('hp-loss-hud-disabled');
$(html).find('.hp-loss-wrap')[2].classList.add('hp-loss-hud-active');
$(html).find('.hp-loss-wrap')[2].classList.remove('hp-loss-hud-disabled');
} else {
$(html).find('.hp-loss-wrap')[0].classList.remove('hp-loss-hud-active');
$(html).find('.hp-loss-wrap')[0].classList.add('hp-loss-hud-disabled');
$(html).find('.hp-loss-wrap')[1].classList.remove('hp-loss-hud-active');
$(html).find('.hp-loss-wrap')[1].classList.add('hp-loss-hud-disabled');
$(html).find('.hp-loss-wrap')[2].classList.remove('hp-loss-hud-active');
$(html).find('.hp-loss-wrap')[2].classList.add('hp-loss-hud-disabled');
}
}) })
$(html).find('.loss-hp-hud-click').click(async (event) => { $(html).find('.loss-hp-hud-click').click(async (event) => {
event.preventDefault(); event.preventDefault();
let hpLoss = event.currentTarget.dataset.hpValue; await hudActor.applyDamage(Number(event.currentTarget.dataset.hpValue));
await hudActor.applyDamage(Number(hpLoss)); _disableHudWraps("hp-loss")
$(html).find('.hp-loss-wrap')[0].classList.remove('hp-loss-hud-active');
$(html).find('.hp-loss-wrap')[0].classList.add('hp-loss-hud-disabled');
$(html).find('.hp-loss-wrap')[1].classList.remove('hp-loss-hud-active');
$(html).find('.hp-loss-wrap')[1].classList.add('hp-loss-hud-disabled');
$(html).find('.hp-loss-wrap')[2].classList.remove('hp-loss-hud-active');
$(html).find('.hp-loss-wrap')[2].classList.add('hp-loss-hud-disabled');
}) })
// HP Gain Button (new) // HP Gain Button
const gainHPButton = await foundry.applications.handlebars.renderTemplate('systems/fvtt-lethal-fantasy/templates/gain-hp-hud.hbs', {}) const gainHPButton = await foundry.applications.handlebars.renderTemplate('systems/fvtt-lethal-fantasy/templates/gain-hp-hud.hbs', {})
$(html).find('div.left').append(gainHPButton); $(html).find('div.left').append(gainHPButton);
$(html).find('img.lethal-hp-gain-hud').click((event) => { $(html).find('img.lethal-hp-gain-hud').click((event) => {
event.preventDefault(); event.preventDefault();
let hpMenu = $(html).find('.hp-gain-wrap')[0] _toggleHudWraps("hp-gain")
if (hpMenu.classList.contains("hp-gain-hud-disabled")) {
$(html).find('.hp-gain-wrap')[0].classList.add('hp-gain-hud-active');
$(html).find('.hp-gain-wrap')[0].classList.remove('hp-gain-hud-disabled');
$(html).find('.hp-gain-wrap')[1].classList.add('hp-gain-hud-active');
$(html).find('.hp-gain-wrap')[1].classList.remove('hp-gain-hud-disabled');
$(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-active');
$(html).find('.hp-gain-wrap')[2].classList.remove('hp-gain-hud-disabled');
} else {
$(html).find('.hp-gain-wrap')[0].classList.remove('hp-gain-hud-active');
$(html).find('.hp-gain-wrap')[0].classList.add('hp-gain-hud-disabled');
$(html).find('.hp-gain-wrap')[1].classList.remove('hp-gain-hud-active');
$(html).find('.hp-gain-wrap')[1].classList.add('hp-gain-hud-disabled');
$(html).find('.hp-gain-wrap')[2].classList.remove('hp-gain-hud-active');
$(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-disabled');
}
}) })
$(html).find('.gain-hp-hud-click').click(async (event) => { $(html).find('.gain-hp-hud-click').click(async (event) => {
event.preventDefault(); event.preventDefault();
let hpGain = event.currentTarget.dataset.hpValue; await hudActor.applyDamage(Number(event.currentTarget.dataset.hpValue));
await hudActor.applyDamage(Number(hpGain)); // Positive value to add HP // Clear bleeding wounds on heal
// Clear bleeding wounds on heal — regardless of heal amount, any
// healing is enough to stop bleeding (field dressing / magic / rest).
const wounds = foundry.utils.duplicate(hudActor.system.hp.wounds || []) const wounds = foundry.utils.duplicate(hudActor.system.hp.wounds || [])
const hadBleeding = wounds.some(w => w.description === "Bleeding") if (wounds.some(w => w.description === "Bleeding")) {
if (hadBleeding) {
await hudActor.update({ await hudActor.update({
"system.hp.wounds": wounds.map(w => "system.hp.wounds": wounds.map(w =>
w.description === "Bleeding" ? { value: 0, duration: 0 } : w w.description === "Bleeding" ? { value: 0, duration: 0 } : w
) )
}) })
} }
$(html).find('.hp-gain-wrap')[0].classList.remove('hp-gain-hud-active'); _disableHudWraps("hp-gain")
$(html).find('.hp-gain-wrap')[0].classList.add('hp-gain-hud-disabled');
$(html).find('.hp-gain-wrap')[1].classList.remove('hp-gain-hud-active');
$(html).find('.hp-gain-wrap')[1].classList.add('hp-gain-hud-disabled');
$(html).find('.hp-gain-wrap')[2].classList.remove('hp-gain-hud-active');
$(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-disabled');
}) })
// Luck/Grit Buttons // Luck/Grit Buttons
+1 -3
View File
@@ -21,10 +21,8 @@
adds <strong>{{formula}}</strong> and rolls <strong>{{value}}</strong> for {{side}}. adds <strong>{{formula}}</strong> and rolls <strong>{{value}}</strong> for {{side}}.
{{else if (equals type "d30Bonus")}} {{else if (equals type "d30Bonus")}}
gains <strong>+{{value}}</strong> from D30 bonus die for {{side}}. gains <strong>+{{value}}</strong> from D30 bonus die for {{side}}.
{{else if (equals type "d30Auto")}}
uses <strong>{{specialName}}</strong> from D30 — {{#if (equals side "defense")}}defense automatically succeeds!{{else}}attack automatically hits!{{/if}}
{{else if (equals type "d30Flag")}} {{else if (equals type "d30Flag")}}
D30 — <strong>{{specialName}}</strong> triggered for {{actorName}}! D30 — <strong>{{specialName}}</strong> possible for {{actorName}}!
{{else if (equals type "d30Bleed")}} {{else if (equals type "d30Bleed")}}
D30 — <strong>Bleeding/Internal Injury</strong> on hit! Damage past DR will cause a bleeding wound. D30 — <strong>Bleeding/Internal Injury</strong> on hit! Damage past DR will cause a bleeding wound.
{{else if (equals type "d30DamageMultiplier")}} {{else if (equals type "d30DamageMultiplier")}}
-4
View File
@@ -1,4 +0,0 @@
<div class="grit-luck-dialog">
<p><strong>D30 result:</strong> {{description}}</p>
<p>{{localize "LETHALFANTASY.D30.chooseEffect"}}</p>
</div>