Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a7585e1f6 | |||
| b567c8bbea | |||
| 60b351f50d | |||
| ace726a1fc | |||
| 67499bc199 | |||
| 7eae95cbbd | |||
| 1b53bf9152 | |||
| 2570bf707e |
@@ -0,0 +1,105 @@
|
|||||||
|
# Lethal Fantasy FoundryVTT System — Session Context
|
||||||
|
|
||||||
|
## Current Goal
|
||||||
|
Fix Grit/Luck defense reaction dialog UX (stacking dialogs, multiple clicks, revert on close) and cross-client sync of defense bonuses.
|
||||||
|
|
||||||
|
## Accomplished
|
||||||
|
|
||||||
|
### Pass 1 — Critical Issues
|
||||||
|
- **Telemetry removed**: `ClassCounter`, `registerWorldCount`, orphaned `worldKey` setting deleted from system.json
|
||||||
|
- **globalThis side effects**: `globalThis.SYSTEM`, `globalThis.pendingDefenses` moved from top-level to `init` hook
|
||||||
|
- **console.log → log()**: All runtime console.log replaced with `log()` helper guarded by `lethalFantasy.debug` setting
|
||||||
|
- **Stale Tenebris refs**: `macros.mjs` — `TENEBRIS.Label.jet` → `LETHALFANTASY.Label.jet`, `TENEBRIS.Manager.*` → `LETHALFANTASY.Label.*`, `tenebris.macro` flag → `lethalFantasy.macro`
|
||||||
|
|
||||||
|
### Pass 2 — V1/V2 Mixing, Fire-and-Forget
|
||||||
|
- **V1 sheet registrations removed**: `foundry.appv1.sheets.*` in system.json
|
||||||
|
- **V1 `activateListeners`/jQuery**: removed dead `defaultOptions`, V1 tab code from `combat.mjs`
|
||||||
|
- **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`
|
||||||
|
- **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
|
||||||
|
- **Duplicated dialogs**: Per-element `.rollable`/`.wound-data` bindings moved to `_onRender` (V2 destroys/recreates DOM each render); `_activateListeners` reverted
|
||||||
|
- **renderChatMessage reverted**: V2 hook `renderChatMessage` passes jQuery html, `querySelectorAll` fails; kept `renderChatMessageHTML`
|
||||||
|
- **Roll actions broken**: Fixed `async` base-actor-sheet methods; `_onRender` bindings for rollable elements restored
|
||||||
|
- **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
|
||||||
|
|
||||||
|
### Symptom (user process)
|
||||||
|
1. Monster (GM) attacks player — hits
|
||||||
|
2. Player uses Grit/Luck to boost defense
|
||||||
|
3. Defense now beats attack — reports new result
|
||||||
|
4. Dialog **stays open** — Grit/Luck/bonus dice options still visible
|
||||||
|
5. Closing dialog (Continue or X) causes "rolls vanish" — reverts to original result
|
||||||
|
|
||||||
|
### Root Cause Found — Duplicate cross-client processing (FIXED)
|
||||||
|
|
||||||
|
When monster (GM) attacks player, the `createChatMessage` hook fires on **both** clients:
|
||||||
|
|
||||||
|
```
|
||||||
|
Player's client: GM's client:
|
||||||
|
defense msg created defense msg synced
|
||||||
|
↓ ↓
|
||||||
|
hook fires (line 557) hook fires (line 557)
|
||||||
|
isPrimaryController(defender)=true isPrimaryController(defender)=false
|
||||||
|
↓ ↓
|
||||||
|
Defense dialog A shows Defense dialog skipped
|
||||||
|
Player spends Grit Cross-client code (line 1009):
|
||||||
|
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."
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
`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).
|
||||||
|
|
||||||
|
Before:
|
||||||
|
```js
|
||||||
|
if (defenderOwner && defenderOwner.id !== game.user.id) {
|
||||||
|
game.socket.emit(`system.${SYSTEM.id}`, { type: "attackBoosted", ... })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
+22
-4
@@ -873,6 +873,7 @@ Hooks.on("createChatMessage", async (message) => {
|
|||||||
|
|
||||||
// ── D30 bonus dice (attack) — resolved before grit/luck ────────────────
|
// ── D30 bonus dice (attack) — resolved before grit/luck ────────────────
|
||||||
if (attackD30message && !attackD30Processed) {
|
if (attackD30message && !attackD30Processed) {
|
||||||
|
const preD30AttackRoll = attackRollFinal
|
||||||
const canDialog = isPrimaryController(attacker)
|
const canDialog = isPrimaryController(attacker)
|
||||||
const d30Result = await LethalFantasyUtils.processD30BonusDice(attackD30message, "attack", attackNaturalRoll, attacker, canDialog)
|
const d30Result = await LethalFantasyUtils.processD30BonusDice(attackD30message, "attack", attackNaturalRoll, attacker, canDialog)
|
||||||
if (d30Result.modifier) {
|
if (d30Result.modifier) {
|
||||||
@@ -913,10 +914,17 @@ Hooks.on("createChatMessage", async (message) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
attackD30Processed = true
|
attackD30Processed = true
|
||||||
|
// If D30 boosted attack past defense, restart so defender can react.
|
||||||
|
// Only restart when D30 actually changed the outcome (pre-D30 defender was
|
||||||
|
// winning or tied, post-D30 defender is losing).
|
||||||
|
if (defender && preD30AttackRoll <= defenseRoll && defenseRoll < attackRollFinal) {
|
||||||
|
mulliganRestart = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Attack reaction loop ───────────────────────────────────────────────
|
// ── Attack reaction loop ───────────────────────────────────────────────
|
||||||
if (!defenderHandledBonus && attacker && attackRollFinal <= defenseRoll && isPrimaryController(attacker)) {
|
if (attacker && attackRollFinal <= defenseRoll && isPrimaryController(attacker)) {
|
||||||
while (attackRollFinal <= defenseRoll) {
|
while (attackRollFinal <= defenseRoll) {
|
||||||
const currentGrit = Number(attacker.system?.grit?.current) || 0
|
const currentGrit = Number(attacker.system?.grit?.current) || 0
|
||||||
const buttons = []
|
const buttons = []
|
||||||
@@ -1003,13 +1011,22 @@ Hooks.on("createChatMessage", async (message) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cross-client coordination: delegate the remaining reaction + message
|
// Cross-client coordination: only delegate to the defender's client
|
||||||
// to the defender's controller via socket. Only the attacker's owning
|
// when the attacker boosted past the defense. When no attacker boost
|
||||||
// client sends — preventing duplicate emissions from other clients.
|
// occurred, the defender's client already processed the defense via
|
||||||
|
// the createChatMessage hook and will create the correct comparison.
|
||||||
|
// Sending attackBoosted with stale (unboosted) values would cause
|
||||||
|
// the defender to see a duplicate dialog and overwrite the result.
|
||||||
if (defender && isPrimaryController(attacker)) {
|
if (defender && isPrimaryController(attacker)) {
|
||||||
const defenderOwner = game.users.find(u => u.active && !u.isGM && defender.testUserPermission(u, "OWNER"))
|
const defenderOwner = game.users.find(u => u.active && !u.isGM && defender.testUserPermission(u, "OWNER"))
|
||||||
|| game.users.find(u => u.active && u.isGM)
|
|| game.users.find(u => u.active && u.isGM)
|
||||||
if (defenderOwner && defenderOwner.id !== game.user.id) {
|
if (defenderOwner && defenderOwner.id !== game.user.id) {
|
||||||
|
// Send attackBoosted when the attacker actually boosted (so defender
|
||||||
|
// can respond to the new numbers), OR when the attacker has an active
|
||||||
|
// non-GM owner (PC-vs-PC cross-client) — the defender's hook-based
|
||||||
|
// processing is suppressed by attackerIsCrossClient, so the socket
|
||||||
|
// handler must show the defense dialog instead.
|
||||||
|
if (attackerHandledBonus || attackerHasNonGMOwner) {
|
||||||
const sData = LethalFantasyUtils.getShieldReactionData(defender)
|
const sData = LethalFantasyUtils.getShieldReactionData(defender)
|
||||||
game.socket.emit(`system.${SYSTEM.id}`, {
|
game.socket.emit(`system.${SYSTEM.id}`, {
|
||||||
type: "attackBoosted",
|
type: "attackBoosted",
|
||||||
@@ -1028,6 +1045,7 @@ Hooks.on("createChatMessage", async (message) => {
|
|||||||
shieldDr: sData?.damageReduction || 0,
|
shieldDr: sData?.damageReduction || 0,
|
||||||
canAdHocShield: !sData,
|
canAdHocShield: !sData,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Same client: restart for defender loop if attacker boosted past defense
|
// Same client: restart for defender loop if attacker boosted past defense
|
||||||
|
|||||||
@@ -255,18 +255,21 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
|
|||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
action: "noDR",
|
action: "noDR",
|
||||||
|
type: "button",
|
||||||
label: game.i18n.localize("LETHALFANTASY.Combat.spellNoDR"),
|
label: game.i18n.localize("LETHALFANTASY.Combat.spellNoDR"),
|
||||||
icon: "fa-solid fa-wand-magic-sparkles",
|
icon: "fa-solid fa-wand-magic-sparkles",
|
||||||
callback: () => 0
|
callback: () => 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "applyDR",
|
action: "applyDR",
|
||||||
|
type: "button",
|
||||||
label: game.i18n.localize("LETHALFANTASY.Combat.spellApplyDR"),
|
label: game.i18n.localize("LETHALFANTASY.Combat.spellApplyDR"),
|
||||||
icon: "fa-solid fa-shield",
|
icon: "fa-solid fa-shield",
|
||||||
callback: (event, button) => Number(button.form?.elements?.manualDr?.value) || 0
|
callback: (event, button) => Number(button.form?.elements?.manualDr?.value) || 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "cancel",
|
action: "cancel",
|
||||||
|
type: "button",
|
||||||
label: game.i18n.localize("LETHALFANTASY.Combat.proceedNo"),
|
label: game.i18n.localize("LETHALFANTASY.Combat.proceedNo"),
|
||||||
callback: () => "cancel"
|
callback: () => "cancel"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,20 +73,6 @@
|
|||||||
],
|
],
|
||||||
"description": "Possible Lethal or Vital Magical Strike or Add D20E to Spell Attack"
|
"description": "Possible Lethal or Vital Magical Strike or Add D20E to Spell Attack"
|
||||||
},
|
},
|
||||||
"arcane_spell_defense": {
|
|
||||||
"type": "choice",
|
|
||||||
"choices": [
|
|
||||||
{
|
|
||||||
"type": "spell_calamity"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "bonus_dice",
|
|
||||||
"dice": "D20E",
|
|
||||||
"target": "spell_defense"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Possible Spell Catastrophe or adds D20E to Spell Defense"
|
|
||||||
},
|
|
||||||
"skill_rolls": {
|
"skill_rolls": {
|
||||||
"type": "skill_auto_success",
|
"type": "skill_auto_success",
|
||||||
"description": "Skill Succeeds Regardless of Opposing Roll"
|
"description": "Skill Succeeds Regardless of Opposing Roll"
|
||||||
@@ -109,9 +95,19 @@
|
|||||||
],
|
],
|
||||||
"description": "Possible Flawless or Legendary Defense or Add D20E to Defense"
|
"description": "Possible Flawless or Legendary Defense or Add D20E to Defense"
|
||||||
},
|
},
|
||||||
"saving_throws": {
|
"arcane_spell_defense": {
|
||||||
"type": "save_auto_success",
|
"type": "choice",
|
||||||
"description": "Saving Throw Succeeds Regardless of Opposing Roll"
|
"choices": [
|
||||||
|
{
|
||||||
|
"type": "spell_calamity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bonus_dice",
|
||||||
|
"dice": "D20E",
|
||||||
|
"target": "spell_defense"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Possible Spell Catastrophe or adds D20E to Spell Defense"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"29": {
|
"29": {
|
||||||
@@ -135,11 +131,6 @@
|
|||||||
"amount": 1,
|
"amount": 1,
|
||||||
"description": "Gain 1 Grit"
|
"description": "Gain 1 Grit"
|
||||||
},
|
},
|
||||||
"arcane_spell_defense": {
|
|
||||||
"type": "gain_grit",
|
|
||||||
"amount": 1,
|
|
||||||
"description": "Gain 1 Grit"
|
|
||||||
},
|
|
||||||
"skill_rolls": {
|
"skill_rolls": {
|
||||||
"type": "gain_grit",
|
"type": "gain_grit",
|
||||||
"amount": 1,
|
"amount": 1,
|
||||||
@@ -150,7 +141,7 @@
|
|||||||
"amount": 1,
|
"amount": 1,
|
||||||
"description": "Gain 1 Grit"
|
"description": "Gain 1 Grit"
|
||||||
},
|
},
|
||||||
"saving_throws": {
|
"arcane_spell_defense": {
|
||||||
"type": "gain_grit",
|
"type": "gain_grit",
|
||||||
"amount": 1,
|
"amount": 1,
|
||||||
"description": "Gain 1 Grit"
|
"description": "Gain 1 Grit"
|
||||||
@@ -184,16 +175,16 @@
|
|||||||
"type": "no_lethargy",
|
"type": "no_lethargy",
|
||||||
"description": "No Spell Lethargy the Aether Approves of Characters Efforts"
|
"description": "No Spell Lethargy the Aether Approves of Characters Efforts"
|
||||||
},
|
},
|
||||||
|
"ranged_defense": {
|
||||||
|
"type": "luck_die",
|
||||||
|
"scope": "combat",
|
||||||
|
"description": "Granted 1 Luck dice for Use in This Combat Only"
|
||||||
|
},
|
||||||
"arcane_spell_defense": {
|
"arcane_spell_defense": {
|
||||||
"type": "flash_of_pain",
|
"type": "flash_of_pain",
|
||||||
"duration_dice": "1D6E",
|
"duration_dice": "1D6E",
|
||||||
"target": "caster",
|
"target": "caster",
|
||||||
"description": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds"
|
"description": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds"
|
||||||
},
|
|
||||||
"ranged_defense": {
|
|
||||||
"type": "luck_die",
|
|
||||||
"scope": "combat",
|
|
||||||
"description": "Granted 1 Luck dice for Use in This Combat Only"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"26": {
|
"26": {
|
||||||
@@ -208,12 +199,6 @@
|
|||||||
"amount": 1,
|
"amount": 1,
|
||||||
"target": "skill",
|
"target": "skill",
|
||||||
"description": "Add 1 to Skill Roll"
|
"description": "Add 1 to Skill Roll"
|
||||||
},
|
|
||||||
"saving_throws": {
|
|
||||||
"type": "bonus_flat",
|
|
||||||
"amount": 1,
|
|
||||||
"target": "save",
|
|
||||||
"description": "Add 1 to Saving Throw"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"21": {
|
"21": {
|
||||||
@@ -239,12 +224,6 @@
|
|||||||
"target": "defender",
|
"target": "defender",
|
||||||
"description": "Magical Damage inflicts Flash of pain 1D6E seconds"
|
"description": "Magical Damage inflicts Flash of pain 1D6E seconds"
|
||||||
},
|
},
|
||||||
"arcane_spell_defense": {
|
|
||||||
"type": "flash_of_pain",
|
|
||||||
"duration_dice": "1D6E",
|
|
||||||
"target": "caster",
|
|
||||||
"description": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds"
|
|
||||||
},
|
|
||||||
"skill_rolls": {
|
"skill_rolls": {
|
||||||
"type": "bonus_dice",
|
"type": "bonus_dice",
|
||||||
"dice": "D6",
|
"dice": "D6",
|
||||||
@@ -255,11 +234,11 @@
|
|||||||
"type": "recover_pain",
|
"type": "recover_pain",
|
||||||
"description": "Defender Recovers or ignores any flash of pain"
|
"description": "Defender Recovers or ignores any flash of pain"
|
||||||
},
|
},
|
||||||
"saving_throws": {
|
"arcane_spell_defense": {
|
||||||
"type": "bonus_dice",
|
"type": "flash_of_pain",
|
||||||
"dice": "D6",
|
"duration_dice": "1D6E",
|
||||||
"target": "save",
|
"target": "caster",
|
||||||
"description": "Granted D6 (1-6) Saving Throw Modifier for this Saving Throw Attempt"
|
"description": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"20": {
|
"20": {
|
||||||
@@ -331,23 +310,6 @@
|
|||||||
],
|
],
|
||||||
"description": "Possible Vicious Application of a Magical Attack or add D12 to attack"
|
"description": "Possible Vicious Application of a Magical Attack or add D12 to attack"
|
||||||
},
|
},
|
||||||
"arcane_spell_defense": {
|
|
||||||
"type": "choice",
|
|
||||||
"choices": [
|
|
||||||
{
|
|
||||||
"type": "special_defense",
|
|
||||||
"options": [
|
|
||||||
"perfect_spell"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "bonus_dice",
|
|
||||||
"dice": "D12",
|
|
||||||
"target": "spell_defense"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Possible 20/20 Spell defense that Saves Against Any Magical Attack Except a Lethal Magical Strike or add D12 to spell defense"
|
|
||||||
},
|
|
||||||
"skill_rolls": {
|
"skill_rolls": {
|
||||||
"type": "bonus_flat",
|
"type": "bonus_flat",
|
||||||
"amount": 20,
|
"amount": 20,
|
||||||
@@ -371,11 +333,22 @@
|
|||||||
],
|
],
|
||||||
"description": "Possible 20/20 defense that avoids Any Attack Except a Lethal Strike or adds D12 to defense"
|
"description": "Possible 20/20 defense that avoids Any Attack Except a Lethal Strike or adds D12 to defense"
|
||||||
},
|
},
|
||||||
"saving_throws": {
|
"arcane_spell_defense": {
|
||||||
"type": "bonus_flat",
|
"type": "choice",
|
||||||
"amount": 20,
|
"choices": [
|
||||||
"target": "save",
|
{
|
||||||
"description": "20 Added to Saving Throw"
|
"type": "special_defense",
|
||||||
|
"options": [
|
||||||
|
"perfect_spell"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bonus_dice",
|
||||||
|
"dice": "D12",
|
||||||
|
"target": "spell_defense"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Possible 20/20 Spell defense that Saves Against Any Magical Attack Except a Lethal Magical Strike or add D12 to spell defense"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"15": {
|
"15": {
|
||||||
@@ -416,12 +389,6 @@
|
|||||||
"punch"
|
"punch"
|
||||||
],
|
],
|
||||||
"description": "Kick or Punch"
|
"description": "Kick or Punch"
|
||||||
},
|
|
||||||
"saving_throws": {
|
|
||||||
"type": "bonus_flat",
|
|
||||||
"amount": 1,
|
|
||||||
"target": "save",
|
|
||||||
"description": "Add 1 to Saving Throw"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"13": {},
|
"13": {},
|
||||||
@@ -474,12 +441,6 @@
|
|||||||
"punch"
|
"punch"
|
||||||
],
|
],
|
||||||
"description": "Kick or Punch"
|
"description": "Kick or Punch"
|
||||||
},
|
|
||||||
"saving_throws": {
|
|
||||||
"type": "bonus_flat",
|
|
||||||
"amount": 1,
|
|
||||||
"target": "save",
|
|
||||||
"description": "Add 1 to Saving Throw"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"8": {
|
"8": {
|
||||||
@@ -499,10 +460,6 @@
|
|||||||
"type": "mulligan",
|
"type": "mulligan",
|
||||||
"description": "Mulligan, Can Re-Roll This Spell Attack"
|
"description": "Mulligan, Can Re-Roll This Spell Attack"
|
||||||
},
|
},
|
||||||
"arcane_spell_defense": {
|
|
||||||
"type": "mulligan",
|
|
||||||
"description": "Mulligan, Can Re-Roll This Spell Defense"
|
|
||||||
},
|
|
||||||
"skill_rolls": {
|
"skill_rolls": {
|
||||||
"type": "mulligan",
|
"type": "mulligan",
|
||||||
"description": "Mulligan, Can Re-Roll This Skill roll"
|
"description": "Mulligan, Can Re-Roll This Skill roll"
|
||||||
@@ -511,9 +468,9 @@
|
|||||||
"type": "mulligan",
|
"type": "mulligan",
|
||||||
"description": "Mulligan, Can Choose to Re-Roll This Defense"
|
"description": "Mulligan, Can Choose to Re-Roll This Defense"
|
||||||
},
|
},
|
||||||
"saving_throws": {
|
"arcane_spell_defense": {
|
||||||
"type": "mulligan",
|
"type": "mulligan",
|
||||||
"description": "Mulligan, Can Re-Roll This Saving Throw"
|
"description": "Mulligan, Can Re-Roll This Spell Defense"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"7": {
|
"7": {
|
||||||
@@ -565,12 +522,6 @@
|
|||||||
"punch"
|
"punch"
|
||||||
],
|
],
|
||||||
"description": "Kick or Punch"
|
"description": "Kick or Punch"
|
||||||
},
|
|
||||||
"saving_throws": {
|
|
||||||
"type": "bonus_flat",
|
|
||||||
"amount": 1,
|
|
||||||
"target": "save",
|
|
||||||
"description": "Add 1 to Saving Throw"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"3": {
|
"3": {
|
||||||
@@ -595,17 +546,17 @@
|
|||||||
"multiplier": 3,
|
"multiplier": 3,
|
||||||
"description": "Triple Damage on Spell Damage"
|
"description": "Triple Damage on Spell Damage"
|
||||||
},
|
},
|
||||||
"arcane_spell_defense": {
|
|
||||||
"type": "bonus_dice",
|
|
||||||
"dice": "D12",
|
|
||||||
"target": "spell_defense",
|
|
||||||
"description": "D12 Added to Spell Defense Modifier"
|
|
||||||
},
|
|
||||||
"ranged_defense": {
|
"ranged_defense": {
|
||||||
"type": "dr_multiplier",
|
"type": "dr_multiplier",
|
||||||
"multiplier": 3,
|
"multiplier": 3,
|
||||||
"includes_shield": true,
|
"includes_shield": true,
|
||||||
"description": "DR Tripled including Shield"
|
"description": "DR Tripled including Shield"
|
||||||
|
},
|
||||||
|
"arcane_spell_defense": {
|
||||||
|
"type": "bonus_dice",
|
||||||
|
"dice": "D12",
|
||||||
|
"target": "spell_defense",
|
||||||
|
"description": "D12 Added to Spell Defense Modifier"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"2": {
|
"2": {
|
||||||
@@ -630,17 +581,17 @@
|
|||||||
"multiplier": 2,
|
"multiplier": 2,
|
||||||
"description": "Double Damage on Spell Damage"
|
"description": "Double Damage on Spell Damage"
|
||||||
},
|
},
|
||||||
"arcane_spell_defense": {
|
|
||||||
"type": "bonus_dice",
|
|
||||||
"dice": "D6",
|
|
||||||
"target": "spell_defense",
|
|
||||||
"description": "D6 Added to Spell Defense Modifier"
|
|
||||||
},
|
|
||||||
"ranged_defense": {
|
"ranged_defense": {
|
||||||
"type": "dr_multiplier",
|
"type": "dr_multiplier",
|
||||||
"multiplier": 2,
|
"multiplier": 2,
|
||||||
"includes_shield": true,
|
"includes_shield": true,
|
||||||
"description": "DR Doubled including Shield"
|
"description": "DR Doubled including Shield"
|
||||||
|
},
|
||||||
|
"arcane_spell_defense": {
|
||||||
|
"type": "bonus_dice",
|
||||||
|
"dice": "D6",
|
||||||
|
"target": "spell_defense",
|
||||||
|
"description": "D6 Added to Spell Defense Modifier"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"1": {
|
"1": {
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ export default class LethalFantasyActor extends Actor {
|
|||||||
if (available.length > 1) {
|
if (available.length > 1) {
|
||||||
const buttons = available.map(([id]) => ({
|
const buttons = available.map(([id]) => ({
|
||||||
action: id,
|
action: id,
|
||||||
|
type: "button",
|
||||||
label: id.charAt(0).toUpperCase() + id.slice(1),
|
label: id.charAt(0).toUpperCase() + id.slice(1),
|
||||||
callback: () => id,
|
callback: () => id,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ export default class D30Roll {
|
|||||||
RANGED_DEFENSE: "ranged_defense",
|
RANGED_DEFENSE: "ranged_defense",
|
||||||
ARCANE_SPELL_ATTACK: "arcane_spell_attack",
|
ARCANE_SPELL_ATTACK: "arcane_spell_attack",
|
||||||
ARCANE_SPELL_DEFENSE: "arcane_spell_defense",
|
ARCANE_SPELL_DEFENSE: "arcane_spell_defense",
|
||||||
SKILL_ROLLS: "skill_rolls",
|
SKILL_ROLLS: "skill_rolls"
|
||||||
SAVING_THROWS: "saving_throws"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,8 +133,9 @@ export default class D30Roll {
|
|||||||
return options.isRanged ? this.ROLL_TYPES.RANGED_DEFENSE : this.ROLL_TYPES.MELEE_DEFENSE
|
return options.isRanged ? this.ROLL_TYPES.RANGED_DEFENSE : this.ROLL_TYPES.MELEE_DEFENSE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spell types
|
// Spell/Miracle types
|
||||||
if (externalType === "spell-attack" || externalType === "spell" || externalType === "spell-power") {
|
if (externalType === "spell-attack" || externalType === "spell" || externalType === "spell-power"
|
||||||
|
|| externalType === "miracle-attack" || externalType === "miracle" || externalType === "miracle-power") {
|
||||||
return this.ROLL_TYPES.ARCANE_SPELL_ATTACK
|
return this.ROLL_TYPES.ARCANE_SPELL_ATTACK
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ export default class D30Roll {
|
|||||||
|
|
||||||
// Saving throw types
|
// Saving throw types
|
||||||
if (externalType === "save") {
|
if (externalType === "save") {
|
||||||
return options.isSpellSave ? this.ROLL_TYPES.ARCANE_SPELL_DEFENSE : this.ROLL_TYPES.SAVING_THROWS
|
return this.ROLL_TYPES.ARCANE_SPELL_DEFENSE
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no match, return null
|
// If no match, return null
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
* @returns {Promise<Object|null>} The roll result or null if the dialog was cancelled.
|
* @returns {Promise<Object|null>} The roll result or null if the dialog was cancelled.
|
||||||
*/
|
*/
|
||||||
static async prompt(options = {}) {
|
static async prompt(options = {}) {
|
||||||
|
try {
|
||||||
let dice = "1D20"
|
let dice = "1D20"
|
||||||
let maxValue = 20
|
let maxValue = 20
|
||||||
let baseFormula = "1D20"
|
let baseFormula = "1D20"
|
||||||
@@ -139,7 +140,7 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
let hasGrantedDice = false
|
let hasGrantedDice = false
|
||||||
let pointBlank = false
|
let pointBlank = false
|
||||||
let letItFly = false
|
let letItFly = false
|
||||||
let saveSpell = false
|
let saveSpell = game.lethalFantasy?.spellDefense ?? false
|
||||||
let beyondSkill = false
|
let beyondSkill = false
|
||||||
let hasStaticModifier = false
|
let hasStaticModifier = false
|
||||||
let hasExplode = true
|
let hasExplode = true
|
||||||
@@ -358,7 +359,7 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
dice,
|
dice,
|
||||||
hasTarget: options.hasTarget,
|
hasTarget: options.hasTarget,
|
||||||
modifier,
|
modifier,
|
||||||
saveSpell: game.lethalFantasy?.spellDefense ?? false,
|
saveSpell,
|
||||||
favor: "none",
|
favor: "none",
|
||||||
targetName,
|
targetName,
|
||||||
isRangedAttack
|
isRangedAttack
|
||||||
@@ -390,6 +391,8 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
position,
|
position,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
|
action: "roll",
|
||||||
|
type: "button",
|
||||||
label: label,
|
label: label,
|
||||||
callback: (event, button, dialog) => {
|
callback: (event, button, dialog) => {
|
||||||
log("Roll context", event, button, dialog)
|
log("Roll context", event, button, dialog)
|
||||||
@@ -444,7 +447,7 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
if (hasModifier) {
|
if (hasModifier) {
|
||||||
let bonus = Number(options.rollTarget.value)
|
let bonus = Number(options.rollTarget.value)
|
||||||
fullModifier = rollContext.modifier === "" ? 0 : parseInt(rollContext.modifier, 10) + bonus
|
fullModifier = rollContext.modifier === "" ? 0 : parseInt(rollContext.modifier, 10) + bonus
|
||||||
fullModifier += (rollContext.saveSpell) ? options.rollTarget.actorModifiers.saveModifier : 0
|
fullModifier += (rollContext.saveSpell) ? (options.rollTarget.actorModifiers?.saveModifier ?? 0) : 0
|
||||||
if (Number(rollContext.attackerAim) > 0) {
|
if (Number(rollContext.attackerAim) > 0) {
|
||||||
fullModifier += Number(rollContext.attackerAim)
|
fullModifier += Number(rollContext.attackerAim)
|
||||||
}
|
}
|
||||||
@@ -679,6 +682,10 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
if (Hooks.call("fvtt-lethal-fantasy.Roll", options, rollData, rollBase) === false) return
|
if (Hooks.call("fvtt-lethal-fantasy.Roll", options, rollData, rollBase) === false) return
|
||||||
|
|
||||||
return rollBase
|
return rollBase
|
||||||
|
} finally {
|
||||||
|
// Clear one-shot flag so it doesn't leak to subsequent non-spell saves
|
||||||
|
if (game.lethalFantasy) game.lethalFantasy.spellDefense = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ***********************************************************/
|
/* ***********************************************************/
|
||||||
@@ -714,6 +721,8 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
content,
|
content,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
|
action: "initiative",
|
||||||
|
type: "button",
|
||||||
label: label,
|
label: label,
|
||||||
callback: (event, button) => {
|
callback: (event, button) => {
|
||||||
const output = Array.from(button.form.elements).reduce((obj, input) => {
|
const output = Array.from(button.form.elements).reduce((obj, input) => {
|
||||||
@@ -791,6 +800,7 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
}
|
}
|
||||||
buttons.push({
|
buttons.push({
|
||||||
action: "roll",
|
action: "roll",
|
||||||
|
type: "button",
|
||||||
label: weaponLabel,
|
label: weaponLabel,
|
||||||
callback: (event, button) => {
|
callback: (event, button) => {
|
||||||
let pos = $('#combat-action-dialog').position()
|
let pos = $('#combat-action-dialog').position()
|
||||||
@@ -817,6 +827,7 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
}
|
}
|
||||||
buttons.push({
|
buttons.push({
|
||||||
action: "roll",
|
action: "roll",
|
||||||
|
type: "button",
|
||||||
label: label,
|
label: label,
|
||||||
callback: (event, button) => {
|
callback: (event, button) => {
|
||||||
let pos = $('#combat-action-dialog').position()
|
let pos = $('#combat-action-dialog').position()
|
||||||
@@ -828,6 +839,7 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
} else {
|
} else {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
action: "roll",
|
action: "roll",
|
||||||
|
type: "button",
|
||||||
label: "Select action",
|
label: "Select action",
|
||||||
callback: (event, button) => {
|
callback: (event, button) => {
|
||||||
let pos = $('#combat-action-dialog').position()
|
let pos = $('#combat-action-dialog').position()
|
||||||
@@ -843,6 +855,7 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
}
|
}
|
||||||
buttons.push({
|
buttons.push({
|
||||||
action: "cancel",
|
action: "cancel",
|
||||||
|
type: "button",
|
||||||
label: "Other action, not listed here",
|
label: "Other action, not listed here",
|
||||||
callback: (event, button) => {
|
callback: (event, button) => {
|
||||||
let pos = $('#combat-action-dialog').position()
|
let pos = $('#combat-action-dialog').position()
|
||||||
@@ -910,6 +923,7 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
content: `<div class="grit-luck-dialog"><p><strong>${selectedItem.name}</strong> has multiple damage tiers.</p><p>Choose which damage to use when the attack lands:</p></div>`,
|
content: `<div class="grit-luck-dialog"><p><strong>${selectedItem.name}</strong> has multiple damage tiers.</p><p>Choose which damage to use when the attack lands:</p></div>`,
|
||||||
buttons: tiers.map(t => ({
|
buttons: tiers.map(t => ({
|
||||||
action: t.id,
|
action: t.id,
|
||||||
|
type: "button",
|
||||||
label: `${t.label} (${t.dice.toUpperCase()})`,
|
label: `${t.label} (${t.dice.toUpperCase()})`,
|
||||||
icon: "fa-solid fa-wand-magic-sparkles",
|
icon: "fa-solid fa-wand-magic-sparkles",
|
||||||
callback: () => t.id
|
callback: () => t.id
|
||||||
@@ -1114,6 +1128,8 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
content,
|
content,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
|
action: "rangeDefense",
|
||||||
|
type: "button",
|
||||||
label: label,
|
label: label,
|
||||||
callback: (event, button) => {
|
callback: (event, button) => {
|
||||||
const output = Array.from(button.form.elements).reduce((obj, input) => {
|
const output = Array.from(button.form.elements).reduce((obj, input) => {
|
||||||
@@ -1273,6 +1289,8 @@ export default class LethalFantasyRoll extends Roll {
|
|||||||
content,
|
content,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
|
action: "rangedAttack",
|
||||||
|
type: "button",
|
||||||
label,
|
label,
|
||||||
callback: (event, button) => {
|
callback: (event, button) => {
|
||||||
const output = Array.from(button.form.elements).reduce((obj, input) => {
|
const output = Array.from(button.form.elements).reduce((obj, input) => {
|
||||||
|
|||||||
+45
-19
@@ -28,8 +28,12 @@ export default class LethalFantasyUtils {
|
|||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
static setHookListeners() {
|
static setHookListeners() {
|
||||||
|
|
||||||
Hooks.on('renderTokenHUD', async (hud, html, token) => {
|
Hooks.on('renderTokenHUD', async (hud, html, data) => {
|
||||||
if (html.querySelector(".lethal-hp-loss-hud")) return
|
if (html.querySelector(".lethal-hp-loss-hud")) return
|
||||||
|
// The token/actor is on the HUD application instance, not the third param.
|
||||||
|
// hud.token / hud.object gives the Token (PlaceableObject), which has .actor.
|
||||||
|
const hudActor = hud.token?.actor ?? hud.object?.actor
|
||||||
|
if (!hudActor) return
|
||||||
// HP Loss Button (existing)
|
// HP Loss Button (existing)
|
||||||
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);
|
||||||
@@ -55,18 +59,13 @@ export default class LethalFantasyUtils {
|
|||||||
$(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;
|
let hpLoss = event.currentTarget.dataset.hpValue;
|
||||||
if (token) {
|
await hudActor.applyDamage(Number(hpLoss));
|
||||||
let tokenFull = canvas.tokens.placeables.find(t => t.id === token.id);
|
|
||||||
log(tokenFull, token)
|
|
||||||
let actor = tokenFull.actor;
|
|
||||||
await actor.applyDamage(Number(hpLoss));
|
|
||||||
$(html).find('.hp-loss-wrap')[0].classList.remove('hp-loss-hud-active');
|
$(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')[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.remove('hp-loss-hud-active');
|
||||||
$(html).find('.hp-loss-wrap')[1].classList.add('hp-loss-hud-disabled');
|
$(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.remove('hp-loss-hud-active');
|
||||||
$(html).find('.hp-loss-wrap')[2].classList.add('hp-loss-hud-disabled');
|
$(html).find('.hp-loss-wrap')[2].classList.add('hp-loss-hud-disabled');
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// HP Gain Button (new)
|
// HP Gain Button (new)
|
||||||
@@ -94,18 +93,24 @@ export default class LethalFantasyUtils {
|
|||||||
$(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;
|
let hpGain = event.currentTarget.dataset.hpValue;
|
||||||
if (token) {
|
await hudActor.applyDamage(Number(hpGain)); // Positive value to add HP
|
||||||
let tokenFull = canvas.tokens.placeables.find(t => t.id === token.id);
|
// Clear bleeding wounds on heal — regardless of heal amount, any
|
||||||
log(tokenFull, token)
|
// healing is enough to stop bleeding (field dressing / magic / rest).
|
||||||
let actor = tokenFull.actor;
|
const wounds = foundry.utils.duplicate(hudActor.system.hp.wounds || [])
|
||||||
await actor.applyDamage(Number(hpGain)); // Positive value to add HP
|
const hadBleeding = wounds.some(w => w.description === "Bleeding")
|
||||||
|
if (hadBleeding) {
|
||||||
|
await hudActor.update({
|
||||||
|
"system.hp.wounds": wounds.map(w =>
|
||||||
|
w.description === "Bleeding" ? { value: 0, duration: 0 } : w
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
$(html).find('.hp-gain-wrap')[0].classList.remove('hp-gain-hud-active');
|
$(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')[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.remove('hp-gain-hud-active');
|
||||||
$(html).find('.hp-gain-wrap')[1].classList.add('hp-gain-hud-disabled');
|
$(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.remove('hp-gain-hud-active');
|
||||||
$(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-disabled');
|
$(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-disabled');
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Luck/Grit Buttons
|
// Luck/Grit Buttons
|
||||||
@@ -124,17 +129,13 @@ export default class LethalFantasyUtils {
|
|||||||
})
|
})
|
||||||
$(html).find('.luck-grit-btn').click(async (event) => {
|
$(html).find('.luck-grit-btn').click(async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (token) {
|
|
||||||
let tokenFull = canvas.tokens.placeables.find(t => t.id === token.id);
|
|
||||||
let actor = tokenFull.actor;
|
|
||||||
const resource = event.currentTarget.dataset.resource;
|
const resource = event.currentTarget.dataset.resource;
|
||||||
const amount = Number(event.currentTarget.dataset.amount);
|
const amount = Number(event.currentTarget.dataset.amount);
|
||||||
const current = Number(foundry.utils.getProperty(actor.system, `${resource}.current`)) || 0;
|
const current = Number(foundry.utils.getProperty(hudActor.system, `${resource}.current`)) || 0;
|
||||||
const newValue = Math.max(0, current + amount);
|
const newValue = Math.max(0, current + amount);
|
||||||
await actor.update({ [`system.${resource}.current`]: newValue });
|
await hudActor.update({ [`system.${resource}.current`]: newValue });
|
||||||
$(html).find('.luck-grit-wrap')[0].classList.remove('luck-grit-hud-active');
|
$(html).find('.luck-grit-wrap')[0].classList.remove('luck-grit-hud-active');
|
||||||
$(html).find('.luck-grit-wrap')[0].classList.add('luck-grit-hud-disabled');
|
$(html).find('.luck-grit-wrap')[0].classList.add('luck-grit-hud-disabled');
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -298,6 +299,7 @@ export default class LethalFantasyUtils {
|
|||||||
if (currentGrit > 0) {
|
if (currentGrit > 0) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
action: "grit",
|
action: "grit",
|
||||||
|
type: "button",
|
||||||
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
||||||
icon: "fa-solid fa-fist-raised",
|
icon: "fa-solid fa-fist-raised",
|
||||||
callback: () => "grit"
|
callback: () => "grit"
|
||||||
@@ -307,6 +309,7 @@ export default class LethalFantasyUtils {
|
|||||||
if (currentLuck > 0) {
|
if (currentLuck > 0) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
action: "luck",
|
action: "luck",
|
||||||
|
type: "button",
|
||||||
label: `Spend 1 Luck (+1D6) [${currentLuck} left]`,
|
label: `Spend 1 Luck (+1D6) [${currentLuck} left]`,
|
||||||
icon: "fa-solid fa-clover",
|
icon: "fa-solid fa-clover",
|
||||||
callback: () => "luck"
|
callback: () => "luck"
|
||||||
@@ -315,6 +318,7 @@ export default class LethalFantasyUtils {
|
|||||||
|
|
||||||
buttons.push({
|
buttons.push({
|
||||||
action: "bonusDie",
|
action: "bonusDie",
|
||||||
|
type: "button",
|
||||||
label: "Add bonus die",
|
label: "Add bonus die",
|
||||||
icon: "fa-solid fa-dice",
|
icon: "fa-solid fa-dice",
|
||||||
callback: () => "bonusDie"
|
callback: () => "bonusDie"
|
||||||
@@ -323,6 +327,7 @@ export default class LethalFantasyUtils {
|
|||||||
if (canShieldReact) {
|
if (canShieldReact) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
action: "shieldReact",
|
action: "shieldReact",
|
||||||
|
type: "button",
|
||||||
label: `Roll shield (${shieldLabel})`,
|
label: `Roll shield (${shieldLabel})`,
|
||||||
icon: "fa-solid fa-shield",
|
icon: "fa-solid fa-shield",
|
||||||
callback: () => "shieldReact"
|
callback: () => "shieldReact"
|
||||||
@@ -330,6 +335,7 @@ export default class LethalFantasyUtils {
|
|||||||
} else if (canAdHoc) {
|
} else if (canAdHoc) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
action: "adHocShield",
|
action: "adHocShield",
|
||||||
|
type: "button",
|
||||||
label: "Roll ad-hoc shield (choose dice + DR)",
|
label: "Roll ad-hoc shield (choose dice + DR)",
|
||||||
icon: "fa-solid fa-shield-halved",
|
icon: "fa-solid fa-shield-halved",
|
||||||
callback: () => "adHocShield"
|
callback: () => "adHocShield"
|
||||||
@@ -338,6 +344,7 @@ export default class LethalFantasyUtils {
|
|||||||
|
|
||||||
buttons.push({
|
buttons.push({
|
||||||
action: "continue",
|
action: "continue",
|
||||||
|
type: "button",
|
||||||
label: "Continue (no defense bonus)",
|
label: "Continue (no defense bonus)",
|
||||||
icon: "fa-solid fa-forward",
|
icon: "fa-solid fa-forward",
|
||||||
callback: () => "continue"
|
callback: () => "continue"
|
||||||
@@ -533,6 +540,7 @@ export default class LethalFantasyUtils {
|
|||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
action: "rollSave",
|
action: "rollSave",
|
||||||
|
type: "button",
|
||||||
label: "Roll Save",
|
label: "Roll Save",
|
||||||
icon: "fa-solid fa-person-running",
|
icon: "fa-solid fa-person-running",
|
||||||
callback: (event, button) => button.form.elements.saveKey.value,
|
callback: (event, button) => button.form.elements.saveKey.value,
|
||||||
@@ -606,6 +614,8 @@ export default class LethalFantasyUtils {
|
|||||||
content,
|
content,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
|
action: "rangeDefense",
|
||||||
|
type: "button",
|
||||||
label: "Roll Defense",
|
label: "Roll Defense",
|
||||||
icon: "fa-solid fa-shield",
|
icon: "fa-solid fa-shield",
|
||||||
callback: (event, button, dialog) => {
|
callback: (event, button, dialog) => {
|
||||||
@@ -713,6 +723,8 @@ export default class LethalFantasyUtils {
|
|||||||
content,
|
content,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
|
action: "defenseRoll",
|
||||||
|
type: "button",
|
||||||
label: "Roll Defense",
|
label: "Roll Defense",
|
||||||
icon: "fa-solid fa-shield",
|
icon: "fa-solid fa-shield",
|
||||||
callback: (event, button, dialog) => {
|
callback: (event, button, dialog) => {
|
||||||
@@ -808,6 +820,7 @@ export default class LethalFantasyUtils {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
action: c.type,
|
action: c.type,
|
||||||
|
type: "button",
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
callback: () => c
|
callback: () => c
|
||||||
@@ -997,6 +1010,7 @@ export default class LethalFantasyUtils {
|
|||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
action: "roll",
|
action: "roll",
|
||||||
|
type: "button",
|
||||||
label: "Roll Bonus Die",
|
label: "Roll Bonus Die",
|
||||||
icon: "fa-solid fa-dice",
|
icon: "fa-solid fa-dice",
|
||||||
callback: (event, button) => {
|
callback: (event, button) => {
|
||||||
@@ -1006,6 +1020,7 @@ export default class LethalFantasyUtils {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "cancel",
|
action: "cancel",
|
||||||
|
type: "button",
|
||||||
label: "Cancel",
|
label: "Cancel",
|
||||||
icon: "fa-solid fa-xmark",
|
icon: "fa-solid fa-xmark",
|
||||||
callback: () => null
|
callback: () => null
|
||||||
@@ -1053,6 +1068,7 @@ export default class LethalFantasyUtils {
|
|||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
action: "roll",
|
action: "roll",
|
||||||
|
type: "button",
|
||||||
label: "Roll Shield",
|
label: "Roll Shield",
|
||||||
icon: "fa-solid fa-shield",
|
icon: "fa-solid fa-shield",
|
||||||
callback: (event, button) => {
|
callback: (event, button) => {
|
||||||
@@ -1066,6 +1082,7 @@ export default class LethalFantasyUtils {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "cancel",
|
action: "cancel",
|
||||||
|
type: "button",
|
||||||
label: "Cancel",
|
label: "Cancel",
|
||||||
icon: "fa-solid fa-xmark",
|
icon: "fa-solid fa-xmark",
|
||||||
callback: () => null
|
callback: () => null
|
||||||
@@ -1136,6 +1153,7 @@ export default class LethalFantasyUtils {
|
|||||||
if (currentGrit > 0) {
|
if (currentGrit > 0) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
action: "grit",
|
action: "grit",
|
||||||
|
type: "button",
|
||||||
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
||||||
icon: "fa-solid fa-fist-raised",
|
icon: "fa-solid fa-fist-raised",
|
||||||
callback: () => "grit"
|
callback: () => "grit"
|
||||||
@@ -1145,6 +1163,7 @@ export default class LethalFantasyUtils {
|
|||||||
if (currentLuck > 0) {
|
if (currentLuck > 0) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
action: "luck",
|
action: "luck",
|
||||||
|
type: "button",
|
||||||
label: `Spend 1 Luck (+1D6) [${currentLuck} left]`,
|
label: `Spend 1 Luck (+1D6) [${currentLuck} left]`,
|
||||||
icon: "fa-solid fa-clover",
|
icon: "fa-solid fa-clover",
|
||||||
callback: () => "luck"
|
callback: () => "luck"
|
||||||
@@ -1153,6 +1172,7 @@ export default class LethalFantasyUtils {
|
|||||||
|
|
||||||
buttons.push({
|
buttons.push({
|
||||||
action: "continue",
|
action: "continue",
|
||||||
|
type: "button",
|
||||||
label: "Continue (no bonus)",
|
label: "Continue (no bonus)",
|
||||||
icon: "fa-solid fa-forward",
|
icon: "fa-solid fa-forward",
|
||||||
callback: () => "continue"
|
callback: () => "continue"
|
||||||
@@ -1228,12 +1248,14 @@ export default class LethalFantasyUtils {
|
|||||||
const buttons = [
|
const buttons = [
|
||||||
{
|
{
|
||||||
action: "grit",
|
action: "grit",
|
||||||
|
type: "button",
|
||||||
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
||||||
icon: "fa-solid fa-fist-raised",
|
icon: "fa-solid fa-fist-raised",
|
||||||
callback: () => "grit"
|
callback: () => "grit"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "continue",
|
action: "continue",
|
||||||
|
type: "button",
|
||||||
label: "Continue (no bonus)",
|
label: "Continue (no bonus)",
|
||||||
icon: "fa-solid fa-forward",
|
icon: "fa-solid fa-forward",
|
||||||
callback: () => "continue"
|
callback: () => "continue"
|
||||||
@@ -1624,21 +1646,25 @@ export default class LethalFantasyUtils {
|
|||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
action: "noDR",
|
action: "noDR",
|
||||||
|
type: "button",
|
||||||
label: "No DR",
|
label: "No DR",
|
||||||
callback: () => ({ drType: "none", damage: damageTotal })
|
callback: () => ({ drType: "none", damage: damageTotal })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "armorDR",
|
action: "armorDR",
|
||||||
|
type: "button",
|
||||||
label: "With Armor DR",
|
label: "With Armor DR",
|
||||||
callback: () => ({ drType: "armor", damage: Math.max(0, damageTotal - armorDR) })
|
callback: () => ({ drType: "armor", damage: Math.max(0, damageTotal - armorDR) })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "allDR",
|
action: "allDR",
|
||||||
|
type: "button",
|
||||||
label: "With Armor + Shield DR",
|
label: "With Armor + Shield DR",
|
||||||
callback: () => ({ drType: "all", damage: Math.max(0, damageTotal - totalDR) })
|
callback: () => ({ drType: "all", damage: Math.max(0, damageTotal - totalDR) })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "cancel",
|
action: "cancel",
|
||||||
|
type: "button",
|
||||||
label: "Cancel",
|
label: "Cancel",
|
||||||
callback: () => null
|
callback: () => null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user