Compare commits

...

15 Commits

Author SHA1 Message Date
uberwald 3b0d4e032e fix: cache results.length before explosion loop to prevent double-count
Release Creation / build (release) Successful in 50s
Pushing explosion dice to DieTerm.results made the for loop
condition (j < results.length) grow mid-iteration, re-processing
the explosion result as a normal die. This produced spurious
entries like `1D6 → 4` alongside the correct `1D6-1 → 3`.

Fixes prompt() (for loop) and rollSpellDamageToMessage()
(for...of) by caching result count / snapshotting the array
before iterating.
2026-06-16 19:34:13 +02:00
uberwald 539841c4ff fix: push explosion dice to DieTerm results so DSN displays them
Explosion rolls were evaluated as separate Roll instances but never
added to the original DieTerm's results array. Dice So Nice reads
DieTerm.results to render 3D dice, so explosions were invisible.

Now each explosion result is pushed into the DieTerm's results array
({result, active:true}), letting DSN render explosion dice in the
correct chronological order alongside the main die.

Applies to prompt(), promptRangedDefense(), promptRangedAttack(),
and rollSpellDamageToMessage().
2026-06-16 19:29:07 +02:00
uberwald ffba37b59e Fix D30 management, again 2026-06-14 23:00:39 +02:00
uberwald 1a7585e1f6 fix: merge saving_throws D30 table into arcane_spell_defense
Release Creation / build (release) Successful in 49s
saving_throws was redundant — all saves in this system are
vs spells. Removed SAVING_THROWS constant; all save rollType
lookups use ARCANE_SPELL_DEFENSE. D30=1 arcane_spell_defense
blank (no special result). Added miracle types to ARCANE_SPELL_ATTACK
mapping so they get D30 results instead of null.
2026-06-14 22:57:41 +02:00
uberwald b567c8bbea Fix D30 management, again
Release Creation / build (release) Successful in 48s
2026-06-13 23:15:22 +02:00
uberwald 60b351f50d Fix spell save/defense again
Release Creation / build (release) Successful in 45s
2026-06-13 21:06:18 +02:00
uberwald ace726a1fc fix: use try/finally for spellDefense cleanup instead of delete
Release Creation / build (release) Successful in 47s
delete on game.lethalFantasy.spellDefense was breaking roll flow
on Foundry's proxied game object.  Use try/finally with
assignment to false instead, which is safe on any object type.

Initialize saveSpell local var from one-shot flag so D30
chart lookup correctly uses arcane_spell_defense for spell saves
without requiring user to click the pre-checked checkbox.
2026-06-13 16:51:52 +02:00
uberwald 67499bc199 fix: add missing arcane_spell_defense entry for D30=1
Release Creation / build (release) Successful in 49s
User spec lists D30=1 as 'Possible Spell Calamity or Catastrophe'
for Arcane Spell Defense but it was missing from the table.
2026-06-13 16:29:54 +02:00
uberwald 7eae95cbbd fix: use arcane spell defense D30 chart for spell saves
Release Creation / build (release) Successful in 43s
Bug: saveSpell local var initialized to false (line 142), while
dialog checkbox was pre-checked via dialogContext.saveSpell =
game.lethalFantasy.spellDefense.  If user didn't click the checkbox,
D30 call used SAVING_THROWS chart instead of ARCANE_SPELL_DEFENSE.

Also: game.lethalFantasy.spellDefense was set true before spell
defense rolls but never cleared, leaking to subsequent non-spell saves.

Fix: initialize saveSpell from the one-shot flag and delete it
immediately.  Dialog context now uses the local saveSpell variable
instead of re-reading the deleted flag.
2026-06-13 15:22:34 +02:00
uberwald 1b53bf9152 fix: resolve hud actor from hud.token not render context param
Release Creation / build (release) Successful in 41s
V2 renderTokenHUD passes (hud, html, data) where data is a render
context object — not a TokenDocument.  data.actor was undefined,
and data?.token?.actor was also undefined.  Use hud.token.actor
(or hud.object.actor) instead, which is the real PlaceableObject
with proper actor resolution.

Also fix html.find() → html.querySelector() for V2 HTMLElement.
2026-06-12 19:01:54 +02:00
uberwald 2570bf707e fix: prevent duplicate cross-client defense dialog, clear bleed on heal
- Only send attackBoosted socket when attackerHandledBonus || attackerHasNonGMOwner
  (GM→player: hook handles it, no socket needed; PC→PC: socket needed)
- Clear bleeding wounds when HP restored via token HUD heal buttons
2026-06-12 17:23:39 +02:00
uberwald cbeaaeec99 Final fixes and code review checks
Release Creation / build (release) Successful in 39s
2026-06-12 08:19:42 +02:00
uberwald 37badf2619 fix: attack/defense cross-client reaction flow
- C1: Stop D30 auto-roll on non-primary clients (caused divergence)
- C2: defenderOwner fallback to GM for monster defenders
- C3: Fix tie outcome in handleAttackBoosted (>= not >)
- C5: Convert handleAttackBoosted to while-loop (multi-reaction)
- C4/C6: shouldCreateMessage cross-client guard
- M2: Coordinate main flow defender dialog vs socket handler
- M3: Fresh grit/luck reads each socket handler iteration
- M4: Include defenseD30message in socket payload + re-process
- M5: Communicate attackerHandledBonus in socket payload
- i18n: Add missing COMBAT.* keys, fix weapon.hbs label localize
- d30_results_tables: Fix string typo
2026-06-12 02:51:59 +02:00
uberwald 5839616863 chore: add pushLDBtoYML/pullYMLtoLDB scripts, gitignore packs_src 2026-06-12 01:56:19 +02:00
uberwald 89298490ef fix: pre-check 'Save against spell' checkbox in template when saveSpell is true 2026-06-12 01:56:03 +02:00
22 changed files with 629 additions and 460 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ jobs:
token: ${{ secrets.FOUNDRY_PUBLISH_KEY }} token: ${{ secrets.FOUNDRY_PUBLISH_KEY }}
id: "fvtt-lethal-fantasy" id: "fvtt-lethal-fantasy"
version: ${{github.event.release.tag_name}} version: ${{github.event.release.tag_name}}
manifest: "https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/system.json" manifest: "https://www.uberwald.me/gitea/uberwald/fvtt-lethal-fantasy/releases/download/latest/system.json"
notes: "https://www.uberwald.me/gitea/public/fvtt-lethal-fantasy/raw/branch/main/changelog.md" notes: "https://www.uberwald.me/gitea/public/fvtt-lethal-fantasy/raw/branch/main/changelog.md"
compatibility-minimum: "14" compatibility-minimum: "14"
compatibility-verified: "14" compatibility-verified: "14"
+3
View File
@@ -17,3 +17,6 @@ packs-system/**/CURRENT
packs-system/**/LOCK packs-system/**/LOCK
packs-system/**/MANIFEST-* packs-system/**/MANIFEST-*
# YAML source for pack round-trip
packs_src/
+105
View File
@@ -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
+25 -1
View File
@@ -1,8 +1,32 @@
{ {
"COMBAT": { "COMBAT": {
"Begin": "Begin Combat",
"Create": "Create Encounter",
"Delete": "Delete Encounter",
"Encounter": "Encounter",
"EncounterNext": "Next Encounter",
"EncounterPrevious": "Previous Encounter",
"End": "End Combat",
"InitiativeReset": "Reset Initiative",
"InitiativeRoll": "Roll Initiative",
"InitiativeScore": "Initiative Score",
"NavLabel": "Combat Tracker Navigation",
"None": "None",
"NotStarted": "Not Started",
"PanToCombatant": "Pan to Combatant",
"PingCombatant": "Ping Combatant",
"RollAll": "Roll All",
"RollNPC": "Roll NPCs",
"Round": "Second {round}", "Round": "Second {round}",
"RoundNext": "Next second",
"RoundPrev": "Previous second",
"Rounds": "Seconds", "Rounds": "Seconds",
"RoundNext": "Next second" "Settings": "Combat Settings",
"ToggleDead": "Toggle Dead",
"ToggleVis": "Toggle Visible",
"TurnEnd": "End Turn",
"TurnNext": "Next Turn",
"TurnPrev": "Previous Turn"
}, },
"LETHALFANTASY": { "LETHALFANTASY": {
"Armor": { "Armor": {
+90 -72
View File
@@ -4,7 +4,6 @@
*/ */
import { SYSTEM } from "./module/config/system.mjs" import { SYSTEM } from "./module/config/system.mjs"
globalThis.SYSTEM = SYSTEM // Expose the SYSTEM object to the global scope
// Import modules // Import modules
import * as models from "./module/models/_module.mjs" import * as models from "./module/models/_module.mjs"
@@ -14,15 +13,24 @@ import * as applications from "./module/applications/_module.mjs"
import { LethalFantasyCombatTracker, LethalFantasyCombat } from "./module/applications/combat.mjs" import { LethalFantasyCombatTracker, LethalFantasyCombat } from "./module/applications/combat.mjs"
import { Macros } from "./module/macros.mjs" import { Macros } from "./module/macros.mjs"
import { setupTextEnrichers } from "./module/enrichers.mjs" import { setupTextEnrichers } from "./module/enrichers.mjs"
import { default as LethalFantasyUtils } from "./module/utils.mjs" import LethalFantasyUtils, { log } from "./module/utils.mjs"
export class ClassCounter { static printHello() { console.log("Hello") } static sendJsonPostRequest(e, s) { const t = { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify(s) }; return fetch(e, t).then((e => { if (!e.ok) throw new Error("La requête a échoué avec le statut " + e.status); return e.json() })).catch((e => { throw console.error("Erreur envoi de la requête:", e), e })) } static registerUsageCount(e = game.system.id, s = {}) { if (game.user.isGM) { game.settings.register(e, "world-key", { name: "Unique world key", scope: "world", config: !1, default: "", type: String }); let t = game.settings.get(e, "world-key"); null != t && "" != t && "NONE" != t && "none" != t.toLowerCase() || (t = foundry.utils.randomID(32), game.settings.set(e, "world-key", t)); let a = { name: e, system: game.system.id, worldKey: t, version: game.system.version, language: game.settings.get("core", "language"), remoteAddr: game.data.addresses.remote, nbInstalledModules: game.modules.size, nbActiveModules: game.modules.filter((e => e.active)).length, nbPacks: game.world.packs.size, nbUsers: game.users.size, nbScenes: game.scenes.size, nbActors: game.actors.size, nbPlaylist: game.playlists.size, nbTables: game.tables.size, nbCards: game.cards.size, optionsData: s, foundryVersion: `${game.release.generation}.${game.release.build}` }; this.sendJsonPostRequest("https://www.uberwald.me/fvtt_appcount/count_post.php", a) } } }
Hooks.once("init", function () { Hooks.once("init", function () {
globalThis.SYSTEM = SYSTEM
globalThis.pendingDefenses = new Map()
console.info("Lethal Fantasy RPG | Initializing System") console.info("Lethal Fantasy RPG | Initializing System")
console.info(SYSTEM.ASCII) console.info(SYSTEM.ASCII)
game.settings.register(game.system.id, "debug", {
name: "Debug logging",
scope: "client",
config: true,
default: false,
type: Boolean,
})
globalThis.lethalFantasy = game.system globalThis.lethalFantasy = game.system
globalThis.log = log
game.system.CONST = SYSTEM game.system.CONST = SYSTEM
// Expose the system API // Expose the system API
@@ -54,12 +62,10 @@ Hooks.once("init", function () {
miracle: models.LethalFantasyMiracle miracle: models.LethalFantasyMiracle
} }
// Register sheet application classes // Register sheet application classes (V2)
foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet)
foundry.documents.collections.Actors.registerSheet("lethalFantasy", applications.LethalFantasyCharacterSheet, { types: ["character"], makeDefault: true }) foundry.documents.collections.Actors.registerSheet("lethalFantasy", applications.LethalFantasyCharacterSheet, { types: ["character"], makeDefault: true })
foundry.documents.collections.Actors.registerSheet("lethalFantasy", applications.LethalFantasyMonsterSheet, { types: ["monster"], makeDefault: true }) foundry.documents.collections.Actors.registerSheet("lethalFantasy", applications.LethalFantasyMonsterSheet, { types: ["monster"], makeDefault: true })
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ActorSheet)
foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasySkillSheet, { types: ["skill"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasySkillSheet, { types: ["skill"], makeDefault: true })
foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasyGiftSheet, { types: ["gift"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasyGiftSheet, { types: ["gift"], makeDefault: true })
foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasyVulnerabilitySheet, { types: ["vulnerability"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasyVulnerabilitySheet, { types: ["vulnerability"], makeDefault: true })
@@ -76,14 +82,6 @@ Hooks.once("init", function () {
// Dice system configuration // Dice system configuration
CONFIG.Dice.rolls.push(documents.LethalFantasyRoll) CONFIG.Dice.rolls.push(documents.LethalFantasyRoll)
game.settings.register("lethalFantasy", "worldKey", {
name: "Unique world key",
scope: "world",
config: false,
type: String,
default: "",
})
// Activate socket handler // Activate socket handler
game.socket.on(`system.${SYSTEM.id}`, LethalFantasyUtils.handleSocketEvent) game.socket.on(`system.${SYSTEM.id}`, LethalFantasyUtils.handleSocketEvent)
@@ -113,10 +111,6 @@ Hooks.once("ready", function () {
// Initialiser la table des résultats D30 // Initialiser la table des résultats D30
documents.D30Roll.initialize() documents.D30Roll.initialize()
if (!SYSTEM.DEV_MODE) {
registerWorldCount("lethalFantasy")
}
// Saignement piloté par le combat tracker // Saignement piloté par le combat tracker
_registerBleedingHooks() _registerBleedingHooks()
@@ -333,7 +327,7 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => {
? (rollTargetData?._id || rollTargetData?.id) ? (rollTargetData?._id || rollTargetData?.id)
: (rollTargetData?.weapon?.id || rollTargetData?.weapon?._id) : (rollTargetData?.weapon?.id || rollTargetData?.weapon?._id)
const attackRollKey = rollTargetData?.rollKey const attackRollKey = rollTargetData?.rollKey
console.log(`[LF] request-defense-btn | attackRollType=${attackRollType} defender=${defenderName} defenderType=${combatant.actor?.type}`) log(`[LF] request-defense-btn | attackRollType=${attackRollType} defender=${defenderName} defenderType=${combatant.actor?.type}`)
const attackD30result = message.rolls[0]?.options?.D30result || null const attackD30result = message.rolls[0]?.options?.D30result || null
const attackD30message = message.rolls[0]?.options?.D30message || null const attackD30message = message.rolls[0]?.options?.D30message || null
const attackDiceResults = message.rolls[0]?.options?.diceResults || null const attackDiceResults = message.rolls[0]?.options?.diceResults || null
@@ -552,7 +546,7 @@ Hooks.on("preCreateChatMessage", (message) => {
[`flags.${SYSTEM.id}.attackData`]: game.lethalFantasy.nextDefenseData [`flags.${SYSTEM.id}.attackData`]: game.lethalFantasy.nextDefenseData
}) })
console.log("Added attack data to defense message:", game.lethalFantasy.nextDefenseData) log("Added attack data to defense message:", game.lethalFantasy.nextDefenseData)
// Nettoyer // Nettoyer
delete game.lethalFantasy.nextDefenseData delete game.lethalFantasy.nextDefenseData
@@ -563,7 +557,7 @@ Hooks.on("preCreateChatMessage", (message) => {
Hooks.on("createChatMessage", async (message) => { Hooks.on("createChatMessage", async (message) => {
const rollType = message.rolls[0]?.options?.rollType const rollType = message.rolls[0]?.options?.rollType
console.log("Defense hook checking message, rollType:", rollType) log("Defense hook checking message, rollType:", rollType)
// Vérifier si c'est un message de défense // Vérifier si c'est un message de défense
if (rollType !== "weapon-defense" && rollType !== "monster-defense" && rollType !== "save") return if (rollType !== "weapon-defense" && rollType !== "monster-defense" && rollType !== "save") return
@@ -571,10 +565,10 @@ Hooks.on("createChatMessage", async (message) => {
// Récupérer les données d'attaque depuis les flags // Récupérer les données d'attaque depuis les flags
const attackData = message.flags?.[SYSTEM.id]?.attackData const attackData = message.flags?.[SYSTEM.id]?.attackData
console.log("Defense message confirmed, attackData:", attackData) log("Defense message confirmed, attackData:", attackData)
if (!attackData) { if (!attackData) {
console.log("No attack data found in message flags") log("No attack data found in message flags")
return return
} }
@@ -596,7 +590,7 @@ Hooks.on("createChatMessage", async (message) => {
let defenseRoll = message.rolls[0]?.options?.rollTotal || message.rolls[0]?.total || 0 let defenseRoll = message.rolls[0]?.options?.rollTotal || message.rolls[0]?.total || 0
const defenseD30message = message.rolls[0]?.options?.D30message || null const defenseD30message = message.rolls[0]?.options?.D30message || null
console.log("Processing defense:", { attackRoll, defenseRoll, attackerId, defenderId }) log("Processing defense:", { attackRoll, defenseRoll, attackerId, defenderId })
// Attendre l'animation 3D // Attendre l'animation 3D
if (game?.dice3d) { if (game?.dice3d) {
@@ -633,6 +627,10 @@ Hooks.on("createChatMessage", async (message) => {
}) })
} }
// Detect cross-client scenario: attacker has an active non-GM owner on another client
const attackerHasNonGMOwner = attacker && game.users.some(u => u.active && !u.isGM && attacker.testUserPermission(u, "OWNER"))
const attackerIsCrossClient = attackerHasNonGMOwner && !isPrimaryController(attacker)
// Reaction phase — both sides may use grit/luck/shield/mulligan before the outcome is resolved. // Reaction phase — both sides may use grit/luck/shield/mulligan before the outcome is resolved.
// After a mulligan reroll (either side), the comparison restarts so both sides can react to the new numbers. // After a mulligan reroll (either side), the comparison restarts so both sides can react to the new numbers.
let defenderHandledBonus = false let defenderHandledBonus = false
@@ -663,7 +661,7 @@ Hooks.on("createChatMessage", async (message) => {
attackerHandledBonus = false attackerHandledBonus = false
// ── D30 bonus dice (defense) — resolved before grit/luck/shield ─────── // ── D30 bonus dice (defense) — resolved before grit/luck/shield ───────
if (defenseD30message && !defenseD30Processed && isPrimaryController(defender)) { if (defenseD30message && !defenseD30Processed && isPrimaryController(defender) && !attackerIsCrossClient) {
const d30Result = await LethalFantasyUtils.processD30BonusDice(defenseD30message, "defense", null, defender, true) const d30Result = await LethalFantasyUtils.processD30BonusDice(defenseD30message, "defense", null, defender, true)
if (d30Result.modifier) { if (d30Result.modifier) {
defenseRoll += d30Result.modifier defenseRoll += d30Result.modifier
@@ -694,7 +692,9 @@ Hooks.on("createChatMessage", async (message) => {
} }
// ── Defense reaction loop ────────────────────────────────────────────── // ── Defense reaction loop ──────────────────────────────────────────────
if (defender && defenseRoll < attackRollFinal && isPrimaryController(defender) && !isSpellOrMiracle) { // Skip when attacker is cross-client — the socket handler (handleAttackBoosted)
// will show the defense dialog and create the comparison message.
if (defender && defenseRoll < attackRollFinal && isPrimaryController(defender) && !isSpellOrMiracle && !attackerIsCrossClient) {
while (defenseRoll < attackRollFinal) { while (defenseRoll < attackRollFinal) {
const currentGrit = Number(defender.system?.grit?.current) || 0 const currentGrit = Number(defender.system?.grit?.current) || 0
const currentLuck = Number(defender.system?.luck?.current) || 0 const currentLuck = Number(defender.system?.luck?.current) || 0
@@ -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,44 +1011,66 @@ Hooks.on("createChatMessage", async (message) => {
} }
} }
// If attacker boosted past defense, let the defender react // Cross-client coordination: only delegate to the defender's client
if (attackerHandledBonus && defenseRoll < attackRollFinal && defender) { // when the attacker boosted past the defense. When no attacker boost
// 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)) {
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)
if (defenderOwner && defenderOwner.id !== game.user.id) { if (defenderOwner && defenderOwner.id !== game.user.id) {
// Cross-client: send socket to defender's client // Send attackBoosted when the attacker actually boosted (so defender
const sData = LethalFantasyUtils.getShieldReactionData(defender) // can respond to the new numbers), OR when the attacker has an active
game.socket.emit(`system.${SYSTEM.id}`, { // non-GM owner (PC-vs-PC cross-client) — the defender's hook-based
type: "attackBoosted", // processing is suppressed by attackerIsCrossClient, so the socket
userId: defenderOwner.id, // handler must show the defense dialog instead.
attackerName, attackerId, defenderName, defenderId, defenderTokenId, if (attackerHandledBonus || attackerHasNonGMOwner) {
attackRollFinal, defenseRoll, attackWeaponId, attackRollType, attackRollKey, const sData = LethalFantasyUtils.getShieldReactionData(defender)
shieldDamageReduction: shieldBlocked ? shieldReaction?.damageReduction ?? 0 : 0, game.socket.emit(`system.${SYSTEM.id}`, {
d30Bleed: d30Bleed ? "true" : "", type: "attackBoosted",
d30DamageMultiplier, d30DrMultiplier, userId: defenderOwner.id,
damageTier: damageTier || "standard", attackerName, attackerId, defenderName, defenderId, defenderTokenId,
attackD30message, attackerHandledBonus, attackRollFinal, defenseRoll, attackWeaponId, attackRollType, attackRollKey,
hasShield: !!sData, shieldDamageReduction: shieldBlocked ? shieldReaction?.damageReduction ?? 0 : 0,
shieldLabel: sData?.label || "", d30Bleed: d30Bleed ? "true" : "",
shieldFormula: sData?.formula || "", d30DamageMultiplier, d30DrMultiplier,
shieldDr: sData?.damageReduction || 0, damageTier: damageTier || "standard",
canAdHocShield: !sData, attackD30message,
}) defenseD30message,
return // Comparison message created by defender's client hasShield: !!sData,
shieldLabel: sData?.label || "",
shieldFormula: sData?.formula || "",
shieldDr: sData?.damageReduction || 0,
canAdHocShield: !sData,
})
}
return
}
// Same client: restart for defender loop if attacker boosted past defense
if (defenseRoll < attackRollFinal && attackerHandledBonus) {
mulliganRestart = true
} }
// Single-client (GM controls both): restart so defender loop can run
mulliganRestart = true
} }
} while (mulliganRestart) } while (mulliganRestart)
const shieldDamageReduction = shieldBlocked ? shieldReaction?.damageReduction ?? 0 : 0 const shieldDamageReduction = shieldBlocked ? shieldReaction?.damageReduction ?? 0 : 0
const outcome = shieldBlocked ? "shielded-hit" : (attackRollFinal > defenseRoll ? "hit" : "miss") const outcome = shieldBlocked ? "shielded-hit" : (attackRollFinal > defenseRoll ? "hit" : "miss")
// Créer le message de comparaison - uniquement par le client qui a géré le dernier bonus // Only one client should create the comparison message:
// Priorité: attaquant si il a géré le bonus, sinon défenseur si il a géré le bonus, sinon défenseur // 1. Attacker boosted → attacker's client creates (or socket handler for cross-client)
const shouldCreateMessage = attackerHandledBonus || (!attackerHandledBonus && defenderHandledBonus) || (!attackerHandledBonus && !defenderHandledBonus && isPrimaryController(defender)) // 2. Defender boosted → defender's client creates
// 3. Nobody boosted → attacker's client (preferred) or defender's client (fallback if same-client)
const shouldCreateMessage = attackerHandledBonus
|| (!attackerHandledBonus && defenderHandledBonus)
|| (!attackerHandledBonus && !defenderHandledBonus && (
(isPrimaryController(defender) && !attackerIsCrossClient)
|| isPrimaryController(attacker)
))
if (shouldCreateMessage) { if (shouldCreateMessage) {
console.log("Creating comparison message", { attackerHandledBonus, defenderHandledBonus, isDefenderOwner: defender.isOwner }) log("Creating comparison message", { attackerHandledBonus, defenderHandledBonus, isDefenderOwner: defender.isOwner })
await LethalFantasyUtils.compareAttackDefense({ await LethalFantasyUtils.compareAttackDefense({
attackerName, attackerName,
@@ -1062,7 +1092,7 @@ Hooks.on("createChatMessage", async (message) => {
attackD30message attackD30message
}) })
} else { } else {
console.log("Skipping message creation", { attackerHandledBonus, defenderHandledBonus }) log("Skipping message creation", { attackerHandledBonus, defenderHandledBonus })
} }
}) })
@@ -1143,7 +1173,7 @@ Hooks.on("createChatMessage", async (message) => {
const defenderId = message.rolls[0]?.options?.defenderId const defenderId = message.rolls[0]?.options?.defenderId
const isDamage = message.rolls[0]?.options?.rollData?.isDamage const isDamage = message.rolls[0]?.options?.rollData?.isDamage
console.log("Auto-damage hook:", { defenderId, isDamage, rollType: message.rolls[0]?.options?.rollType }) log("Auto-damage hook:", { defenderId, isDamage, rollType: message.rolls[0]?.options?.rollType })
if (!defenderId || !isDamage) return if (!defenderId || !isDamage) return
@@ -1168,11 +1198,11 @@ Hooks.on("createChatMessage", async (message) => {
} }
if (!shouldApplyDamage) { if (!shouldApplyDamage) {
console.log("Auto-damage hook: Not responsible for applying damage, skipping") log("Auto-damage hook: Not responsible for applying damage, skipping")
return return
} }
console.log("Auto-damage hook: Applying damage as responsible user") log("Auto-damage hook: Applying damage as responsible user")
// Attendre l'animation 3D avant d'appliquer les dégâts // Attendre l'animation 3D avant d'appliquer les dégâts
if (game?.dice3d) { if (game?.dice3d) {
@@ -1383,16 +1413,4 @@ Hooks.on("hotbarDrop", (bar, data, slot) => {
*/ */
Hooks.on("renderChatLog", (_chatLog, html) => applications.injectDiceTray(_chatLog, html)) Hooks.on("renderChatLog", (_chatLog, html) => applications.injectDiceTray(_chatLog, html))
/**
* Register world usage statistics
* @param {string} registerKey
*/
async function registerWorldCount(registerKey) {
if (game.user.isGM) {
try {
ClassCounter.registerUsageCount(game.system.id, {})
} catch {
console.log("No usage log ")
}
}
}
+13 -41
View File
@@ -24,30 +24,29 @@ export class LethalFantasyCombatTracker extends foundry.applications.sidebar.tab
async _prepareContext(options) { async _prepareContext(options) {
let data = await super._prepareContext(options); let data = await super._prepareContext(options);
console?.log("Combat Tracker Data", data); log("Combat Tracker Data", data);
/*for (let u of data.turns) { /*for (let u of data.turns) {
let c = game.combat.combatants.get(u.id); let c = game.combat.combatants.get(u.id);
u.progressionCount = c.system.progressionCount u.progressionCount = c.system.progressionCount
u.isMonster = c.actor.type === "monster" u.isMonster = c.actor.type === "monster"
} }
console.log("Combat Data", data);*/ log("Combat Data", data);*/
return data; return data;
} }
static #initiativePlus(ev) { static async #initiativePlus(ev) {
ev.preventDefault(); ev.preventDefault();
let cId = ev.target.closest(".combatant").dataset.combatantId; let cId = ev.target.closest(".combatant").dataset.combatantId;
let c = game.combat.combatants.get(cId); let c = game.combat.combatants.get(cId);
c.update({ 'initiative': c.initiative + 1 }); await c.update({ 'initiative': c.initiative + 1 });
console.log("Initiative Plus");
} }
static #initiativeMinus(ev) { static async #initiativeMinus(ev) {
ev.preventDefault(); ev.preventDefault();
let cId = ev.target.closest(".combatant").dataset.combatantId; let cId = ev.target.closest(".combatant").dataset.combatantId;
let c = game.combat.combatants.get(cId); let c = game.combat.combatants.get(cId);
let newInit = Math.max(c.initiative - 1, 0); let newInit = Math.max(c.initiative - 1, 0);
c.update({ 'initiative': newInit }); await c.update({ 'initiative': newInit });
} }
/** /**
@@ -58,33 +57,6 @@ export class LethalFantasyCombatTracker extends foundry.applications.sidebar.tab
ev.preventDefault(); ev.preventDefault();
await game.combat.rollMonsterProgression(); await game.combat.rollMonsterProgression();
} }
activateListeners(html) {
super.activateListeners(html);
// Display Combat settings
html.find(".initiative-plus").click(ev => {
ev.preventDefault();
let cId = ev.currentTarget.closest(".combatant").dataset.combatantId;
let c = game.combat.combatants.get(cId);
c.update({ 'initiative': c.initiative + 1 });
});
html.find(".initiative-minus").click(ev => {
ev.preventDefault();
let cId = ev.currentTarget.closest(".combatant").dataset.combatantId;
let c = game.combat.combatants.get(cId);
c.update({ 'initiative': c.initiative - 1 });
console.log("Initiative Minus");
});
}
/* -------------------------------------------- */
static get defaultOptions() {
let path = "systems/fvtt-lethal-fantasy/templates/combat-tracker.hbs";
return foundry.utils.mergeObject(super.defaultOptions, {
template: path,
});
}
} }
export class LethalFantasyCombat extends Combat { export class LethalFantasyCombat extends Combat {
@@ -94,7 +66,7 @@ export class LethalFantasyCombat extends Combat {
* @returns {Combatant[]} * @returns {Combatant[]}
*/ */
setupTurns() { setupTurns() {
console?.log("Setup Turns...."); log("Setup Turns....");
this.turns ||= []; this.turns ||= [];
// Determine the turn order and the current turn // Determine the turn order and the current turn
@@ -164,19 +136,19 @@ export class LethalFantasyCombat extends Combat {
} }
} }
resetProgression(cId) { async resetProgression(cId) {
let c = this.combatants.get(cId); let c = this.combatants.get(cId);
c.update({ 'system.progressionCount': 0 }); await c.update({ 'system.progressionCount': 0 });
} }
setCasting(cId) { async setCasting(cId) {
let c = this.combatants.get(cId); let c = this.combatants.get(cId);
c.setFlag(SYSTEM.id, "casting", true); await c.setFlag(SYSTEM.id, "casting", true);
} }
resetCasting(cId) { async resetCasting(cId) {
let c = this.combatants.get(cId); let c = this.combatants.get(cId);
c.setFlag(SYSTEM.id, "casting", false); await c.setFlag(SYSTEM.id, "casting", false);
} }
isCasting(cId) { isCasting(cId) {
@@ -78,7 +78,6 @@ export default class LethalFantasyActorSheet extends HandlebarsApplicationMixin(
/** @override */ /** @override */
_onRender(context, options) { _onRender(context, options) {
this.#dragDrop.forEach((d) => d.bind(this.element)) this.#dragDrop.forEach((d) => d.bind(this.element))
// Add listeners to rollable elements
const rollables = this.element.querySelectorAll(".rollable") const rollables = this.element.querySelectorAll(".rollable")
rollables.forEach((d) => d.addEventListener("click", this._onRoll.bind(this))) rollables.forEach((d) => d.addEventListener("click", this._onRoll.bind(this)))
} }
@@ -234,12 +233,12 @@ export default class LethalFantasyActorSheet extends HandlebarsApplicationMixin(
const attr = target.dataset.edit const attr = target.dataset.edit
const current = foundry.utils.getProperty(this.document, attr) const current = foundry.utils.getProperty(this.document, attr)
const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {} const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {}
const fp = new FilePicker({ const fp = new foundry.applications.ux.FilePicker.implementation({
current, current,
type: "image", type: "image",
redirectToRoot: img ? [img] : [], redirectToRoot: img ? [img] : [],
callback: (path) => { callback: async (path) => {
this.document.update({ [attr]: path }) await this.document.update({ [attr]: path })
}, },
top: this.position.top + 40, top: this.position.top + 40,
left: this.position.left + 10, left: this.position.left + 10,
@@ -261,7 +260,7 @@ export default class LethalFantasyActorSheet extends HandlebarsApplicationMixin(
item = await fromUuid(uuid) item = await fromUuid(uuid)
if (!item) item = this.document.items.get(id) if (!item) item = this.document.items.get(id)
if (!item) return if (!item) return
item.sheet.render(true) item.sheet.render({ force: true })
} }
/** /**
@@ -284,8 +283,8 @@ export default class LethalFantasyActorSheet extends HandlebarsApplicationMixin(
* @private * @private
* @static * @static
*/ */
static #onCreateSpell(event, target) { static async #onCreateSpell(event, target) {
const item = this.document.createEmbeddedDocuments("Item", [{ name: "Nouveau sortilège", type: "spell" }]) await this.document.createEmbeddedDocuments("Item", [{ name: "Nouveau sortilège", type: "spell" }])
} }
// #endregion // #endregion
@@ -177,12 +177,12 @@ export default class LethalFantasyItemSheet extends HandlebarsApplicationMixin(f
const attr = target.dataset.edit const attr = target.dataset.edit
const current = foundry.utils.getProperty(this.document, attr) const current = foundry.utils.getProperty(this.document, attr)
const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {} const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {}
const fp = new FilePicker({ const fp = new foundry.applications.ux.FilePicker.implementation({
current, current,
type: "image", type: "image",
redirectToRoot: img ? [img] : [], redirectToRoot: img ? [img] : [],
callback: (path) => { callback: async (path) => {
this.document.update({ [attr]: path }) await this.document.update({ [attr]: path })
}, },
top: this.position.top + 40, top: this.position.top + 40,
left: this.position.left + 10, left: this.position.left + 10,
+19 -17
View File
@@ -180,44 +180,44 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
await this.document.system.rollInitiative() await this.document.system.rollInitiative()
} }
static #onArmorHitPointsPlus(event, target) { static async #onArmorHitPointsPlus(event, target) {
let armorHP = this.actor.system.combat.armorHitPoints let armorHP = this.actor.system.combat.armorHitPoints
armorHP += 1 armorHP += 1
this.actor.update({ "system.combat.armorHitPoints": armorHP }) await this.actor.update({ "system.combat.armorHitPoints": armorHP })
} }
static #onArmorHitPointsMinus(event, target) { static async #onArmorHitPointsMinus(event, target) {
let armorHP = this.actor.system.combat.armorHitPoints let armorHP = this.actor.system.combat.armorHitPoints
armorHP -= 1 armorHP -= 1
this.actor.update({ "system.combat.armorHitPoints": Math.max(armorHP, 0) }) await this.actor.update({ "system.combat.armorHitPoints": Math.max(armorHP, 0) })
} }
static #onDivinityPointsPlus(event, target) { static async #onDivinityPointsPlus(event, target) {
let points = this.actor.system.divinityPoints.value let points = this.actor.system.divinityPoints.value
points += 1 points += 1
points = Math.min(points, this.actor.system.divinityPoints.max) points = Math.min(points, this.actor.system.divinityPoints.max)
this.actor.update({ "system.divinityPoints.value": points }) await this.actor.update({ "system.divinityPoints.value": points })
} }
static #onDivinityPointsMinus(event, target) { static async #onDivinityPointsMinus(event, target) {
let points = this.actor.system.divinityPoints.value let points = this.actor.system.divinityPoints.value
points -= 1 points -= 1
points = Math.max(points, 0) points = Math.max(points, 0)
this.actor.update({ "system.divinityPoints.value": points }) await this.actor.update({ "system.divinityPoints.value": points })
} }
static #onAetherPointsPlus(event, target) { static async #onAetherPointsPlus(event, target) {
let points = this.actor.system.aetherPoints.value let points = this.actor.system.aetherPoints.value
points += 1 points += 1
points = Math.min(points, this.actor.system.aetherPoints.max) points = Math.min(points, this.actor.system.aetherPoints.max)
this.actor.update({ "system.aetherPoints.value": points }) await this.actor.update({ "system.aetherPoints.value": points })
} }
static #onAetherPointsMinus(event, target) { static async #onAetherPointsMinus(event, target) {
let points = this.actor.system.aetherPoints.value let points = this.actor.system.aetherPoints.value
points -= 1 points -= 1
points = Math.max(points, 0) points = Math.max(points, 0)
this.actor.update({ "system.aetherPoints.value": points }) await this.actor.update({ "system.aetherPoints.value": points })
} }
/** /**
@@ -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"
} }
@@ -293,10 +296,9 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
} }
_onRender(context, options) { _onRender(context, options) {
// Inputs with class `item-quantity`
const woundDescription = this.element.querySelectorAll('.wound-data') const woundDescription = this.element.querySelectorAll('.wound-data')
for (const input of woundDescription) { for (const input of woundDescription) {
input.addEventListener("change", (e) => { input.addEventListener("change", async (e) => {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
const newValue = e.currentTarget.value const newValue = e.currentTarget.value
@@ -304,11 +306,11 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
const fieldName = e.currentTarget.dataset.name const fieldName = e.currentTarget.dataset.name
let tab = foundry.utils.duplicate(this.actor.system.hp.wounds) let tab = foundry.utils.duplicate(this.actor.system.hp.wounds)
tab[index][fieldName] = newValue tab[index][fieldName] = newValue
console.log(tab, index, fieldName, newValue) log(tab, index, fieldName, newValue)
this.actor.update({ "system.hp.wounds": tab }); await this.actor.update({ "system.hp.wounds": tab });
}) })
} }
super._onRender(); super._onRender(context, options);
} }
+1 -1
View File
@@ -91,7 +91,7 @@ export default class LethalFantasyMonsterSheet extends LethalFantasyActorSheet {
*/ */
async _onDrop(event) { async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return if (!this.isEditable || !this.isEditMode) return
const data = TextEditor.getDragEventData(event) const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
// Handle different data types // Handle different data types
switch (data.type) { switch (data.type) {
+55 -104
View File
@@ -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": {},
@@ -429,7 +396,7 @@
"melee_attack": { "melee_attack": {
"type": "flurry", "type": "flurry",
"condition": "hit_or_miss", "condition": "hit_or_miss",
"description": "Flurry Attack or Hit to Miss" "description": "Flurry Attack on Hit or Miss"
}, },
"ranged_attack": { "ranged_attack": {
"type": "double_damage_dice", "type": "double_damage_dice",
@@ -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": {
+3 -2
View File
@@ -72,7 +72,7 @@ export default class LethalFantasyActor extends Actor {
/* *************************************************/ /* *************************************************/
async applyDamage(hpLoss) { async applyDamage(hpLoss) {
let hp = this.system.hp.value + hpLoss let hp = this.system.hp.value + hpLoss
this.update({ "system.hp.value": hp }) await this.update({ "system.hp.value": hp })
} }
/* *************************************************/ /* *************************************************/
@@ -163,7 +163,7 @@ export default class LethalFantasyActor extends Actor {
/* *************************************************/ /* *************************************************/
async prepareRoll(rollType, rollKey, rollDice, defenderId, defenderTokenId, extraShieldDr = 0, d30Effects = {}) { async prepareRoll(rollType, rollKey, rollDice, defenderId, defenderTokenId, extraShieldDr = 0, d30Effects = {}) {
console.log("Preparing roll", rollType, rollKey, rollDice, defenderId) log("Preparing roll", rollType, rollKey, rollDice, defenderId)
let rollTarget let rollTarget
switch (rollType) { switch (rollType) {
case "granted": case "granted":
@@ -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,
})) }))
+8 -6
View File
@@ -1,3 +1,5 @@
import { log } from "../utils.mjs"
/** /**
* Classe pour gérer les résultats du D30 dans Lethal Fantasy * Classe pour gérer les résultats du D30 dans Lethal Fantasy
*/ */
@@ -25,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"
} }
/** /**
@@ -41,7 +42,7 @@ export default class D30Roll {
this.resultsTable = data.d30_dice_results this.resultsTable = data.d30_dice_results
this.definitions = data.definitions this.definitions = data.definitions
console.log("D30Roll | D30 results table loaded successfully") log("D30Roll | D30 results table loaded successfully")
} catch (error) { } catch (error) {
console.error("D30Roll | Error loading D30 table:", error) console.error("D30Roll | Error loading D30 table:", error)
ui.notifications.error("Unable to load D30 results table") ui.notifications.error("Unable to load D30 results table")
@@ -132,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
} }
@@ -144,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
+54 -28
View File
@@ -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,9 +391,11 @@ 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) => {
console.log("Roll context", event, button, dialog) log("Roll context", event, button, dialog)
let position = dialog?.position let position = dialog?.position
game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(position)) game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(position))
const output = Array.from(button.form.elements).reduce((obj, input) => { const output = Array.from(button.form.elements).reduce((obj, input) => {
@@ -435,7 +438,7 @@ export default class LethalFantasyRoll extends Roll {
// If the user cancels the dialog, exit // If the user cancels the dialog, exit
if (rollContext === null) return if (rollContext === null) return
console.log("rollContext", rollContext, hasGrantedDice) log("rollContext", rollContext, hasGrantedDice)
rollContext.saveSpell = saveSpell // Update fucking flag rollContext.saveSpell = saveSpell // Update fucking flag
let fullModifier = 0 let fullModifier = 0
@@ -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)
} }
@@ -552,7 +555,7 @@ export default class LethalFantasyRoll extends Roll {
if (rollContext.favor === "favor") { if (rollContext.favor === "favor") {
rollFavor = new this(baseFormula, options.data, rollData) rollFavor = new this(baseFormula, options.data, rollData)
await rollFavor.evaluate() await rollFavor.evaluate()
console.log("Rolling with favor", rollFavor) log("Rolling with favor", rollFavor)
if (game?.dice3d) { if (game?.dice3d) {
game.dice3d.showForRoll(rollFavor, game.user, true) game.dice3d.showForRoll(rollFavor, game.user, true)
} }
@@ -613,8 +616,10 @@ export default class LethalFantasyRoll extends Roll {
let singleDice = `1D${maxValue}` let singleDice = `1D${maxValue}`
for (let i = 0; i < rollBase.dice.length; i++) { for (let i = 0; i < rollBase.dice.length; i++) {
for (let j = 0; j < rollBase.dice[i].results.length; j++) { const dieResults = rollBase.dice[i].results
let diceResult = rollBase.dice[i].results[j].result const resultCount = dieResults.length
for (let j = 0; j < resultCount; j++) {
let diceResult = dieResults[j].result
diceResults.push({ dice: `${singleDice.toUpperCase()}`, value: diceResult }) diceResults.push({ dice: `${singleDice.toUpperCase()}`, value: diceResult })
diceSum += diceResult diceSum += diceResult
if (hasMaxValue) { if (hasMaxValue) {
@@ -623,6 +628,8 @@ export default class LethalFantasyRoll extends Roll {
diceResult = r.dice[0].results[0].result diceResult = r.dice[0].results[0].result
diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 }) diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1) diceSum += (diceResult - 1)
// Add to DieTerm results so DSN/Foundry display shows explosion dice
dieResults.push({ result: diceResult, active: true })
} }
} }
} }
@@ -679,6 +686,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 +725,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 +804,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 +831,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 +843,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 +859,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()
@@ -861,12 +878,12 @@ export default class LethalFantasyRoll extends Roll {
rejectClose: false // Click on Close button will not launch an error rejectClose: false // Click on Close button will not launch an error
}) })
console.log("RollContext", dialogContext, rollContext) log("RollContext", dialogContext, rollContext)
// If action is cancelled, exit // If action is cancelled, exit
if (rollContext === null || rollContext === "cancel") { if (rollContext === null || rollContext === "cancel") {
await combatant.setFlag(SYSTEM.id, "currentAction", "") await combatant.setFlag(SYSTEM.id, "currentAction", "")
let message = `${combatant.name} : Other action, progression reset` let message = `${combatant.name} : Other action, progression reset`
ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
return return
} }
@@ -910,6 +927,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
@@ -924,7 +942,7 @@ export default class LethalFantasyRoll extends Roll {
// Set the flag on the combatant // Set the flag on the combatant
await combatant.setFlag(SYSTEM.id, "currentAction", actionItem) await combatant.setFlag(SYSTEM.id, "currentAction", actionItem)
let message = `${combatant.name} action : ${selectedItem.name}, start rolling progression dice or casting time` let message = `${combatant.name} action : ${selectedItem.name}, start rolling progression dice or casting time`
ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
rollContext = (actionItem.type === "weapon") ? "rollProgressionDice" : "rollLethargyDice" // Set the roll context to rollProgressionDice rollContext = (actionItem.type === "weapon") ? "rollProgressionDice" : "rollLethargyDice" // Set the roll context to rollProgressionDice
currentAction = actionItem currentAction = actionItem
} }
@@ -935,14 +953,14 @@ export default class LethalFantasyRoll extends Roll {
let time = currentAction.type === "spell" ? currentAction.system.castingTime : currentAction.system.prayerTime let time = currentAction.type === "spell" ? currentAction.system.castingTime : currentAction.system.prayerTime
if (currentAction.castingTime < time) { if (currentAction.castingTime < time) {
let message = `Casting time : ${currentAction.name}, count : ${currentAction.castingTime}/${time}` let message = `Casting time : ${currentAction.name}, count : ${currentAction.castingTime}/${time}`
ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
currentAction.castingTime += 1 currentAction.castingTime += 1
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
return return
} else { } else {
// Last counting second — announce ready and transition immediately (no extra second consumed) // Last counting second — announce ready and transition immediately (no extra second consumed)
let message = `Casting time : ${currentAction.name}, count : ${time}/${time} — ready to cast next second !` let message = `Casting time : ${currentAction.name}, count : ${time}/${time} — ready to cast next second !`
ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
currentAction.castingTime = 1 currentAction.castingTime = 1
currentAction.spellStatus = "toBeCasted" currentAction.spellStatus = "toBeCasted"
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
@@ -982,7 +1000,7 @@ export default class LethalFantasyRoll extends Roll {
isLethargy: true isLethargy: true
} }
) )
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
// Update the combatant progression count // Update the combatant progression count
await combatant.setFlag(SYSTEM.id, "currentAction", "") await combatant.setFlag(SYSTEM.id, "currentAction", "")
// Display the action selection window again // Display the action selection window again
@@ -1002,7 +1020,7 @@ export default class LethalFantasyRoll extends Roll {
isLethargy: true isLethargy: true
} }
) )
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
} }
} }
} }
@@ -1014,18 +1032,18 @@ export default class LethalFantasyRoll extends Roll {
let split = toSplit.split("+") let split = toSplit.split("+")
currentAction.rangedLoad = Number(split[0]) || 0 currentAction.rangedLoad = Number(split[0]) || 0
formula = split[1] formula = split[1]
console.log("Ranged Mode", currentAction.rangedMode, currentAction.rangedLoad, formula) log("Ranged Mode", currentAction.rangedMode, currentAction.rangedLoad, formula)
} }
// Range weapon loading // Range weapon loading
if (!currentAction.weaponLoaded && currentAction.rangedLoad) { if (!currentAction.weaponLoaded && currentAction.rangedLoad) {
if (currentAction.progressionCount < currentAction.rangedLoad) { if (currentAction.progressionCount < currentAction.rangedLoad) {
let message = `Ranged weapon ${currentAction.name} is loading, loading count : ${currentAction.progressionCount}/${currentAction.rangedLoad}` let message = `Ranged weapon ${currentAction.name} is loading, loading count : ${currentAction.progressionCount}/${currentAction.rangedLoad}`
ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
currentAction.progressionCount += 1 currentAction.progressionCount += 1
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
} else { } else {
let message = `Ranged weapon ${currentAction.name} is loaded !` let message = `Ranged weapon ${currentAction.name} is loaded !`
ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
currentAction.weaponLoaded = true currentAction.weaponLoaded = true
currentAction.progressionCount = 1 currentAction.progressionCount = 1
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
@@ -1057,13 +1075,13 @@ export default class LethalFantasyRoll extends Roll {
rollResult: roll.total rollResult: roll.total
} }
) )
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
await combatant.setFlag(SYSTEM.id, "currentAction", "") await combatant.setFlag(SYSTEM.id, "currentAction", "")
combatant.actor.prepareRoll(currentAction.type === "weapon" ? "weapon-attack" : "spell-attack", currentAction._id) combatant.actor.prepareRoll(currentAction.type === "weapon" ? "weapon-attack" : "spell-attack", currentAction._id)
} else { } else {
// Notify that the player cannot act now with a chat message // Notify that the player cannot act now with a chat message
currentAction.progressionCount += 1 currentAction.progressionCount += 1
combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
const messageContent = await foundry.applications.handlebars.renderTemplate( const messageContent = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-lethal-fantasy/templates/progression-message.hbs", "systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
{ {
@@ -1074,7 +1092,7 @@ export default class LethalFantasyRoll extends Roll {
progressionCount: currentAction.progressionCount progressionCount: currentAction.progressionCount
} }
) )
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
} }
} }
} }
@@ -1114,6 +1132,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) => {
@@ -1130,7 +1150,7 @@ export default class LethalFantasyRoll extends Roll {
// If the user cancels the dialog, exit // If the user cancels the dialog, exit
if (rollContext === null) return if (rollContext === null) return
console.log("RollContext", rollContext) log("RollContext", rollContext)
// Add disfavor/favor option if point blank range // Add disfavor/favor option if point blank range
if (rollContext.range === "pointblank") { if (rollContext.range === "pointblank") {
rollContext.movement = rollContext.movement.replace("kh", "") rollContext.movement = rollContext.movement.replace("kh", "")
@@ -1198,6 +1218,7 @@ export default class LethalFantasyRoll extends Roll {
diceResult = r.dice[0].results[0].result diceResult = r.dice[0].results[0].result
diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 }) diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1) diceSum += (diceResult - 1)
rollBase.dice[0].results.push({ result: diceResult, active: true })
} }
if (fullModifier !== 0) { if (fullModifier !== 0) {
diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total }) diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total })
@@ -1273,6 +1294,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) => {
@@ -1358,6 +1381,7 @@ export default class LethalFantasyRoll extends Roll {
diceResult = r.dice[0].results[0].result diceResult = r.dice[0].results[0].result
diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 }) diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1) diceSum += (diceResult - 1)
rollBase.dice[0].results.push({ result: diceResult, active: true })
} }
if (fullModifier !== 0) { if (fullModifier !== 0) {
@@ -1426,7 +1450,7 @@ export default class LethalFantasyRoll extends Roll {
/** @override */ /** @override */
async render(chatOptions = {}) { async render(chatOptions = {}) {
let chatData = await this._getChatCardData(chatOptions.isPrivate) let chatData = await this._getChatCardData(chatOptions.isPrivate)
console.log("ChatData", chatData) log("ChatData", chatData)
return await foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, chatData) return await foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, chatData)
} }
@@ -1464,15 +1488,15 @@ export default class LethalFantasyRoll extends Roll {
// Récupérer les informations de l'arme pour les attaques réussies // Récupérer les informations de l'arme pour les attaques réussies
let weaponDamageOptions = null let weaponDamageOptions = null
console.log("Roll type:", this.type, "rollTarget:", this.rollTarget, "Has weapon:", !!this.rollTarget?.weapon) log("Roll type:", this.type, "rollTarget:", this.rollTarget, "Has weapon:", !!this.rollTarget?.weapon)
if (this.type === "weapon-attack" && this.rollTarget?.weapon) { if (this.type === "weapon-attack" && this.rollTarget?.weapon) {
const weapon = this.rollTarget.weapon const weapon = this.rollTarget.weapon
weaponDamageOptions = { weaponDamageOptions = {
weaponId: weapon._id || weapon.id, weaponId: weapon.id,
weaponName: weapon.name, weaponName: weapon.name,
damageM: weapon.system?.damage?.damageM damageM: weapon.system?.damage?.damageM
} }
console.log("Weapon damage options:", weaponDamageOptions) log("Weapon damage options:", weaponDamageOptions)
} else if (this.type === "monster-attack" && this.rollTarget) { } else if (this.type === "monster-attack" && this.rollTarget) {
weaponDamageOptions = { weaponDamageOptions = {
weaponId: this.rollTarget.rollKey, weaponId: this.rollTarget.rollKey,
@@ -1481,7 +1505,7 @@ export default class LethalFantasyRoll extends Roll {
damageModifier: this.rollTarget.damageModifier, damageModifier: this.rollTarget.damageModifier,
isMonster: true isMonster: true
} }
console.log("Monster damage options:", weaponDamageOptions) log("Monster damage options:", weaponDamageOptions)
} }
const cardData = { const cardData = {
@@ -1570,7 +1594,8 @@ export default class LethalFantasyRoll extends Roll {
let diceSum = 0 let diceSum = 0
for (const term of roll.dice) { for (const term of roll.dice) {
const singleDice = `1D${term.faces}` const singleDice = `1D${term.faces}`
for (const r of term.results) { const termResults = Array.from(term.results)
for (const r of termResults) {
let diceResult = r.result let diceResult = r.result
diceResults.push({ dice: singleDice.toUpperCase(), value: diceResult }) diceResults.push({ dice: singleDice.toUpperCase(), value: diceResult })
diceSum += diceResult diceSum += diceResult
@@ -1581,6 +1606,7 @@ export default class LethalFantasyRoll extends Roll {
diceResult = xr.dice?.[0]?.results?.[0]?.result ?? (term.faces - 1) diceResult = xr.dice?.[0]?.results?.[0]?.result ?? (term.faces - 1)
diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 }) diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1) diceSum += (diceResult - 1)
term.results.push({ result: diceResult, active: true })
} }
} }
} }
+5 -5
View File
@@ -32,20 +32,20 @@ export class Macros {
dropData.rollType === "save" dropData.rollType === "save"
? `game.actors.get('${dropData.actorId}').system.roll('${dropData.rollType}', '${dropData.rollTarget}', '=');` ? `game.actors.get('${dropData.actorId}').system.roll('${dropData.rollType}', '${dropData.rollTarget}', '=');`
: `game.actors.get('${dropData.actorId}').system.roll('${dropData.rollType}', '${dropData.rollTarget}');` : `game.actors.get('${dropData.actorId}').system.roll('${dropData.rollType}', '${dropData.rollTarget}');`
const rollName = `${game.i18n.localize("TENEBRIS.Label.jet")} ${game.i18n.localize(`TENEBRIS.Manager.${dropData.rollTarget}`)}` const rollName = `${game.i18n.localize("LETHALFANTASY.Label.jet")} ${game.i18n.localize(`LETHALFANTASY.Label.${dropData.rollTarget}`)}`
this.createMacro(slot, rollName, rollCommand, "icons/svg/d20-grey.svg") this.createMacro(slot, rollName, rollCommand, "icons/svg/d20-grey.svg")
break break
case "rollDamage": case "rollDamage":
const weapon = game.actors.get(dropData.actorId).items.get(dropData.rollTarget) const weapon = game.actors.get(dropData.actorId).items.get(dropData.rollTarget)
const rollDamageCommand = `game.actors.get('${dropData.actorId}').system.roll('${dropData.rollType}', '${dropData.rollTarget}');` const rollDamageCommand = `game.actors.get('${dropData.actorId}').system.roll('${dropData.rollType}', '${dropData.rollTarget}');`
const rollDamageName = `${game.i18n.localize("TENEBRIS.Label.jet")} ${weapon.name}` const rollDamageName = `${game.i18n.localize("LETHALFANTASY.Label.jet")} ${weapon.name}`
this.createMacro(slot, rollDamageName, rollDamageCommand, weapon.img) this.createMacro(slot, rollDamageName, rollDamageCommand, weapon.img)
break break
case "rollAttack": case "rollAttack":
const rollAttackCommand = `game.actors.get('${dropData.actorId}').system.roll('${dropData.rollValue}', '${dropData.rollTarget}');` const rollAttackCommand = `game.actors.get('${dropData.actorId}').system.roll('${dropData.rollValue}', '${dropData.rollTarget}');`
const rollAttackName = `${game.i18n.localize("TENEBRIS.Label.jet")} ${dropData.rollTarget}` const rollAttackName = `${game.i18n.localize("LETHALFANTASY.Label.jet")} ${dropData.rollTarget}`
this.createMacro(slot, rollAttackName, rollAttackCommand, "icons/svg/d20-grey.svg") this.createMacro(slot, rollAttackName, rollAttackCommand, "icons/svg/d20-grey.svg")
break break
@@ -57,7 +57,7 @@ export class Macros {
/** /**
* Create a macro * Create a macro
* All macros are flaged with a tenebris.macro flag at true * All macros are flaged with a lethalFantasy.macro flag at true
* @param {*} slot * @param {*} slot
* @param {*} name * @param {*} name
* @param {*} command * @param {*} command
@@ -72,7 +72,7 @@ export class Macros {
type: "script", type: "script",
img: img, img: img,
command: command, command: command,
flags: { "tenebris.macro": true }, flags: { "lethalFantasy.macro": true },
}, },
{ displaySheet: false }, { displaySheet: false },
) )
+3 -3
View File
@@ -383,7 +383,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
rollResult: roll.total rollResult: roll.total
} }
) )
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: this.parent }) }) await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: this.parent }) })
let token = combatant?.token let token = combatant?.token
this.prepareMonsterRoll("monster-attack", key, undefined, token?.id) this.prepareMonsterRoll("monster-attack", key, undefined, token?.id)
if (token?.object) { if (token?.object) {
@@ -407,7 +407,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
rollResult: roll.total rollResult: roll.total
} }
) )
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: this.parent }) }) await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: this.parent }) })
let token = combatant?.token let token = combatant?.token
this.prepareMonsterRoll("monster-attack-hth", key, undefined, token?.id) this.prepareMonsterRoll("monster-attack-hth", key, undefined, token?.id)
if (token?.object) { if (token?.object) {
@@ -426,7 +426,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
rollResult: roll.total rollResult: roll.total
} }
) )
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: this.parent }) }) await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: this.parent }) })
} }
} }
+3 -2
View File
@@ -1,6 +1,7 @@
import { SYSTEM } from "../config/system.mjs" import { SYSTEM } from "../config/system.mjs"
import { WEAPON_TYPE } from "../config/weapon.mjs"
export default class LethalFantasySkill extends foundry.abstract.TypeDataModel { export default class LethalFantasyWeapon extends foundry.abstract.TypeDataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields const fields = foundry.data.fields
const schema = {} const schema = {}
@@ -62,7 +63,7 @@ export default class LethalFantasySkill extends foundry.abstract.TypeDataModel {
static LOCALIZATION_PREFIXES = ["LETHALFANTASY.Weapon"] static LOCALIZATION_PREFIXES = ["LETHALFANTASY.Weapon"]
get weaponCategory() { get weaponCategory() {
return game.i18n.localize(CATEGORY[this.weaponType].label) return game.i18n.localize(WEAPON_TYPE[this.weaponType] || this.weaponType)
} }
} }
+226 -166
View File
@@ -1,8 +1,9 @@
import { SYSTEM } from "./config/system.mjs" import { SYSTEM } from "./config/system.mjs"
// Map temporaire pour stocker les données d'attaque en attente de défense export function log(...args) {
if (!globalThis.pendingDefenses) { if (game?.settings?.get(game.system.id, "debug")) {
globalThis.pendingDefenses = new Map() console.log(...args)
}
} }
export default class LethalFantasyUtils { export default class LethalFantasyUtils {
@@ -27,7 +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
// 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);
@@ -50,21 +56,16 @@ export default class LethalFantasyUtils {
$(html).find('.hp-loss-wrap')[2].classList.add('hp-loss-hud-disabled'); $(html).find('.hp-loss-wrap')[2].classList.add('hp-loss-hud-disabled');
} }
}) })
$(html).find('.loss-hp-hud-click').click((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);
console.log(tokenFull, token)
let actor = tokenFull.actor;
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)
@@ -89,21 +90,27 @@ export default class LethalFantasyUtils {
$(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-disabled'); $(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-disabled');
} }
}) })
$(html).find('.gain-hp-hud-click').click((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
console.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 || [])
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
@@ -120,26 +127,22 @@ export default class LethalFantasyUtils {
wrap.classList.add('luck-grit-hud-disabled'); wrap.classList.add('luck-grit-hud-disabled');
} }
}) })
$(html).find('.luck-grit-btn').click((event) => { $(html).find('.luck-grit-btn').click(async (event) => {
event.preventDefault(); event.preventDefault();
if (token) { const resource = event.currentTarget.dataset.resource;
let tokenFull = canvas.tokens.placeables.find(t => t.id === token._id); const amount = Number(event.currentTarget.dataset.amount);
let actor = tokenFull.actor; const current = Number(foundry.utils.getProperty(hudActor.system, `${resource}.current`)) || 0;
const resource = event.currentTarget.dataset.resource; const newValue = Math.max(0, current + amount);
const amount = Number(event.currentTarget.dataset.amount); await hudActor.update({ [`system.${resource}.current`]: newValue });
const current = Number(foundry.utils.getProperty(actor.system, `${resource}.current`)) || 0; $(html).find('.luck-grit-wrap')[0].classList.remove('luck-grit-hud-active');
const newValue = Math.max(0, current + amount); $(html).find('.luck-grit-wrap')[0].classList.add('luck-grit-hud-disabled');
actor.update({ [`system.${resource}.current`]: newValue });
$(html).find('.luck-grit-wrap')[0].classList.remove('luck-grit-hud-active');
$(html).find('.luck-grit-wrap')[0].classList.add('luck-grit-hud-disabled');
}
}) })
}) })
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
static handleSocketEvent(msg = {}) { static async handleSocketEvent(msg = {}) {
console.log(`handleSocketEvent !`, msg) log(`handleSocketEvent !`, msg)
let actor let actor
switch (msg.type) { switch (msg.type) {
case "applyDamage": case "applyDamage":
@@ -149,18 +152,18 @@ export default class LethalFantasyUtils {
? canvas.tokens?.placeables?.find(t => t.id === msg.tokenId)?.actor ? canvas.tokens?.placeables?.find(t => t.id === msg.tokenId)?.actor
: (game.combat?.combatants?.find(c => c.actorId === msg.actorId)?.actor : (game.combat?.combatants?.find(c => c.actorId === msg.actorId)?.actor
?? game.actors.get(msg.actorId)) ?? game.actors.get(msg.actorId))
if (actor) actor.applyDamage(msg.damage) if (actor) await actor.applyDamage(msg.damage)
} }
break break
case "rollInitiative": case "rollInitiative":
if (msg.userId && msg.userId !== game.user.id) break if (msg.userId && msg.userId !== game.user.id) break
actor = game.actors.get(msg.actorId) actor = game.actors.get(msg.actorId)
actor.system.rollInitiative(msg.combatId, msg.combatantId) await actor.system.rollInitiative(msg.combatId, msg.combatantId)
break break
case "rollProgressionDice": case "rollProgressionDice":
if (msg.userId && msg.userId !== game.user.id) break if (msg.userId && msg.userId !== game.user.id) break
actor = game.actors.get(msg.actorId) actor = game.actors.get(msg.actorId)
actor.system.rollProgressionDice(msg.combatId, msg.combatantId, msg.rollProgressionCount) await actor.system.rollProgressionDice(msg.combatId, msg.combatantId, msg.rollProgressionCount)
break break
case "requestDefense": case "requestDefense":
// Vérifier si le message est destiné à cet utilisateur // Vérifier si le message est destiné à cet utilisateur
@@ -184,7 +187,7 @@ export default class LethalFantasyUtils {
const slot = wounds.findIndex(w => !w.value && !w.duration) const slot = wounds.findIndex(w => !w.value && !w.duration)
if (slot !== -1) { if (slot !== -1) {
wounds[slot] = { value: msg.damage, duration: msg.damage, description: "Bleeding" } wounds[slot] = { value: msg.damage, duration: msg.damage, description: "Bleeding" }
actor.update({ "system.hp.wounds": wounds }) await actor.update({ "system.hp.wounds": wounds })
} }
} }
} }
@@ -235,10 +238,10 @@ export default class LethalFantasyUtils {
static async handleAttackBoosted(msg) { static async handleAttackBoosted(msg) {
const { const {
attackerName, attackerId, defenderName, defenderId, defenderTokenId, attackerName, attackerId, defenderName, defenderId, defenderTokenId,
attackRollFinal, defenseRoll, attackWeaponId, attackRollType, attackRollKey, attackerHandledBonus, attackRollFinal, defenseRoll, attackWeaponId, attackRollType, attackRollKey,
shieldDamageReduction: initialShieldDR, shieldDamageReduction: initialShieldDR,
d30Bleed, d30DamageMultiplier, d30DrMultiplier, d30Bleed, d30DamageMultiplier, d30DrMultiplier,
damageTier, attackD30message, damageTier, attackD30message, defenseD30message,
hasShield, shieldLabel, shieldFormula, shieldDr, canAdHocShield hasShield, shieldLabel, shieldFormula, shieldDr, canAdHocShield
} = msg } = msg
@@ -251,138 +254,183 @@ export default class LethalFantasyUtils {
let canShieldReact = hasShield let canShieldReact = hasShield
let canAdHoc = canAdHocShield let canAdHoc = canAdHocShield
// Show the defense reaction dialog // ── D30 bonus dice (defense) — resolved before grit/luck/shield ───────
if (defender && updatedDefenseRoll < attackRollFinal) { let defenseDrMultiplier = null
const currentGrit = Number(defender.system?.grit?.current) || 0 if (defenseD30message && defender) {
const currentLuck = Number(defender.system?.luck?.current) || 0 const d30Result = await LethalFantasyUtils.processD30BonusDice(defenseD30message, "defense", null, defender, true)
const buttons = [] if (d30Result.modifier) {
updatedDefenseRoll += d30Result.modifier
if (currentGrit > 0) { if (d30Result.modifier > 0) {
buttons.push({ await ChatMessage.create({
action: "grit", content: `<p><strong>${defenderName}</strong> gains <strong>+${d30Result.modifier}</strong> from D30 bonus die for defense.</p>`,
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`, speaker: ChatMessage.getSpeaker({ actor: defender })
icon: "fa-solid fa-fist-raised", })
callback: () => "grit" }
}
if (d30Result.specialEffect === "auto") {
updatedDefenseRoll = attackRollFinal + 1
await ChatMessage.create({
content: `<p><strong>${defenderName}</strong> uses <strong>${d30Result.specialName || "Special Defense"}</strong> from D30 — defense automatically succeeds!</p>`,
speaker: ChatMessage.getSpeaker({ actor: defender })
}) })
} }
if (d30Result.specialEffect === "flag") {
if (currentLuck > 0) { await ChatMessage.create({
buttons.push({ content: `<p>D30 — <strong>${d30Result.specialName || "Special Effect"}</strong> triggered for ${defenderName}!</p>`,
action: "luck", speaker: ChatMessage.getSpeaker({ actor: defender })
label: `Spend 1 Luck (+1D6) [${currentLuck} left]`,
icon: "fa-solid fa-clover",
callback: () => "luck"
}) })
} }
if (d30Result.specialEffect === "drMultiplier") {
buttons.push({ defenseDrMultiplier = d30Result.multiplier
action: "bonusDie", await ChatMessage.create({
label: "Add bonus die", content: `<p>D30 — Defense grants <strong>x${d30Result.multiplier} DR</strong> (choose which DR types to multiply when damage is applied)</p>`,
icon: "fa-solid fa-dice", speaker: ChatMessage.getSpeaker({ actor: defender })
callback: () => "bonusDie"
})
if (canShieldReact) {
buttons.push({
action: "shieldReact",
label: `Roll shield (${shieldLabel})`,
icon: "fa-solid fa-shield",
callback: () => "shieldReact"
})
} else if (canAdHoc) {
buttons.push({
action: "adHocShield",
label: "Roll ad-hoc shield (choose dice + DR)",
icon: "fa-solid fa-shield-halved",
callback: () => "adHocShield"
}) })
} }
}
buttons.push({ // Show the defense reaction dialog — while-loop for multiple reactions
action: "continue", if (defender) {
label: "Continue (no defense bonus)", while (updatedDefenseRoll < attackRollFinal) {
icon: "fa-solid fa-forward", const currentGrit = Number(defender.system?.grit?.current) || 0
callback: () => "continue" const currentLuck = Number(defender.system?.luck?.current) || 0
}) const buttons = []
const choice = await foundry.applications.api.DialogV2.wait({ if (currentGrit > 0) {
window: { title: "Defense reactions — attack boosted" }, buttons.push({
classes: ["lethalfantasy"], action: "grit",
content: ` type: "button",
<div class="grit-luck-dialog"> label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
<div class="combat-status"> icon: "fa-solid fa-fist-raised",
<p><strong>${attackerName}</strong> boosted attack to <strong>${attackRollFinal}</strong></p> callback: () => "grit"
<p><strong>${defenderName}</strong> currently has <strong>${updatedDefenseRoll}</strong></p> })
}
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({
window: { title: "Defense reactions — attack boosted" },
classes: ["lethalfantasy"],
content: `
<div class="grit-luck-dialog">
<div class="combat-status">
<p><strong>${attackerName}</strong> boosted attack to <strong>${attackRollFinal}</strong></p>
<p><strong>${defenderName}</strong> currently has <strong>${updatedDefenseRoll}</strong></p>
</div>
<p class="offer-text">The attack was boosted! Choose how to improve the defense.</p>
</div> </div>
<p class="offer-text">The attack was boosted! Choose how to improve the defense.</p> `,
</div> buttons,
`, rejectClose: false
buttons, })
rejectClose: false
})
if (choice === "grit") { if (!choice || choice === "continue") break
const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender,
total => `<p><strong>${defenderName}</strong> spends 1 Grit and rolls <strong>${total}</strong> for defense.</p>`) if (choice === "grit") {
updatedDefenseRoll += bonusRoll const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender,
await defender.update({ "system.grit.current": currentGrit - 1 }) total => `<p><strong>${defenderName}</strong> spends 1 Grit and rolls <strong>${total}</strong> for defense.</p>`)
} else if (choice === "luck") {
const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender,
total => `<p><strong>${defenderName}</strong> spends 1 Luck and rolls <strong>${total}</strong> for defense.</p>`)
updatedDefenseRoll += bonusRoll
await defender.update({ "system.luck.current": currentLuck - 1 })
} else if (choice === "bonusDie") {
const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(defenderName, "attack", updatedDefenseRoll, attackRollFinal)
if (bonusDie) {
const bonusRoll = await LethalFantasyUtils.rollBonusDie(bonusDie, defender,
(total, formula) => `<p><strong>${defenderName}</strong> adds <strong>${formula.toUpperCase()}</strong> and rolls <strong>${total}</strong> for defense.</p>`)
updatedDefenseRoll += bonusRoll updatedDefenseRoll += bonusRoll
} await defender.update({ "system.grit.current": currentGrit - 1 })
} else if (choice === "shieldReact" && canShieldReact) { } else if (choice === "luck") {
const shieldBonus = await LethalFantasyUtils.rollBonusDie(shieldFormula, defender) const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender,
const newDefenseTotal = updatedDefenseRoll + shieldBonus total => `<p><strong>${defenderName}</strong> spends 1 Luck and rolls <strong>${total}</strong> for defense.</p>`)
updatedDefenseRoll = newDefenseTotal updatedDefenseRoll += bonusRoll
canShieldReact = false await defender.update({ "system.luck.current": currentLuck - 1 })
if (newDefenseTotal >= attackRollFinal) { } else if (choice === "bonusDie") {
shieldBlocked = true const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(defenderName, "attack", updatedDefenseRoll, attackRollFinal)
shieldReaction = { damageReduction: shieldDr, label: shieldLabel, bonus: shieldBonus } if (bonusDie) {
await ChatMessage.create({ const bonusRoll = await LethalFantasyUtils.rollBonusDie(bonusDie, defender,
content: `<p><strong>${defenderName}</strong> rolls <strong>${shieldLabel}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal}${attackRollFinal}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${shieldDr}</strong> will apply to damage.</p>`, (total, formula) => `<p><strong>${defenderName}</strong> adds <strong>${formula.toUpperCase()}</strong> and rolls <strong>${total}</strong> for defense.</p>`)
speaker: ChatMessage.getSpeaker({ actor: defender }) updatedDefenseRoll += bonusRoll
}) }
} else { } else if (choice === "shieldReact" && canShieldReact) {
await ChatMessage.create({ const shieldBonus = await LethalFantasyUtils.rollBonusDie(shieldFormula, defender)
content: `<p><strong>${defenderName}</strong> rolls <strong>${shieldLabel}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRollFinal}). Shield did not block — normal hit, armor DR only.</p>`,
speaker: ChatMessage.getSpeaker({ actor: defender })
})
}
} else if (choice === "adHocShield" && canAdHoc) {
const adHoc = await LethalFantasyUtils.promptAdHocShield(defenderName, attackRollFinal, updatedDefenseRoll)
if (adHoc) {
const shieldBonus = await LethalFantasyUtils.rollBonusDie(adHoc.formula, defender)
const newDefenseTotal = updatedDefenseRoll + shieldBonus const newDefenseTotal = updatedDefenseRoll + shieldBonus
updatedDefenseRoll = newDefenseTotal updatedDefenseRoll = newDefenseTotal
canShieldReact = false canShieldReact = false
canAdHoc = false
if (newDefenseTotal >= attackRollFinal) { if (newDefenseTotal >= attackRollFinal) {
shieldBlocked = true shieldBlocked = true
shieldReaction = { damageReduction: adHoc.damageReduction, label: `${adHoc.formula.toUpperCase()} shield`, bonus: shieldBonus } shieldReaction = { damageReduction: shieldDr, label: shieldLabel, bonus: shieldBonus }
await ChatMessage.create({ await ChatMessage.create({
content: `<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal}${attackRollFinal}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${adHoc.damageReduction}</strong> will apply to damage.</p>`, content: `<p><strong>${defenderName}</strong> rolls <strong>${shieldLabel}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal}${attackRollFinal}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${shieldDr}</strong> will apply to damage.</p>`,
speaker: ChatMessage.getSpeaker({ actor: defender }) speaker: ChatMessage.getSpeaker({ actor: defender })
}) })
} else { } else {
await ChatMessage.create({ await ChatMessage.create({
content: `<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRollFinal}). Shield did not block — normal hit, armor DR only.</p>`, content: `<p><strong>${defenderName}</strong> rolls <strong>${shieldLabel}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRollFinal}). Shield did not block — normal hit, armor DR only.</p>`,
speaker: ChatMessage.getSpeaker({ actor: defender }) speaker: ChatMessage.getSpeaker({ actor: defender })
}) })
} }
} else if (choice === "adHocShield" && canAdHoc) {
const adHoc = await LethalFantasyUtils.promptAdHocShield(defenderName, attackRollFinal, updatedDefenseRoll)
if (adHoc) {
const shieldBonus = await LethalFantasyUtils.rollBonusDie(adHoc.formula, defender)
const newDefenseTotal = updatedDefenseRoll + shieldBonus
updatedDefenseRoll = newDefenseTotal
canShieldReact = false
canAdHoc = false
if (newDefenseTotal >= attackRollFinal) {
shieldBlocked = true
shieldReaction = { damageReduction: adHoc.damageReduction, label: `${adHoc.formula.toUpperCase()} shield`, bonus: shieldBonus }
await ChatMessage.create({
content: `<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal}${attackRollFinal}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${adHoc.damageReduction}</strong> will apply to damage.</p>`,
speaker: ChatMessage.getSpeaker({ actor: defender })
})
} else {
await ChatMessage.create({
content: `<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRollFinal}). Shield did not block — normal hit, armor DR only.</p>`,
speaker: ChatMessage.getSpeaker({ actor: defender })
})
}
}
} }
} }
} }
const finalShieldDR = shieldBlocked ? (shieldReaction?.damageReduction || 0) : 0 const finalShieldDR = shieldBlocked ? (shieldReaction?.damageReduction || 0) : 0
const outcome = shieldBlocked ? "shielded-hit" : (updatedDefenseRoll > attackRollFinal ? "miss" : "hit") const outcome = shieldBlocked ? "shielded-hit" : (updatedDefenseRoll >= attackRollFinal ? "miss" : "hit")
await LethalFantasyUtils.compareAttackDefense({ await LethalFantasyUtils.compareAttackDefense({
attackerName, attackerName,
@@ -399,7 +447,7 @@ export default class LethalFantasyUtils {
shieldDamageReduction: finalShieldDR, shieldDamageReduction: finalShieldDR,
d30Bleed: d30Bleed || "", d30Bleed: d30Bleed || "",
d30DamageMultiplier: d30DamageMultiplier || 1, d30DamageMultiplier: d30DamageMultiplier || 1,
d30DrMultiplier: d30DrMultiplier || 1, d30DrMultiplier: (d30DrMultiplier != null && d30DrMultiplier !== 1) ? d30DrMultiplier : (defenseDrMultiplier ?? d30DrMultiplier ?? 1),
damageTier: damageTier || "standard", damageTier: damageTier || "standard",
attackD30message attackD30message
}) })
@@ -458,7 +506,7 @@ export default class LethalFantasyUtils {
const isMonster = defender.type === "monster" const isMonster = defender.type === "monster"
console.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
const isSpellAttack = attackRollType === "spell-attack" || attackRollType === "miracle-attack" const isSpellAttack = attackRollType === "spell-attack" || attackRollType === "miracle-attack"
@@ -492,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,
@@ -520,9 +569,9 @@ export default class LethalFantasyUtils {
defenderTokenId defenderTokenId
} }
if (isMonster) { if (isMonster) {
defender.system.prepareMonsterRoll("save", result) await defender.system.prepareMonsterRoll("save", result)
} else { } else {
defender.prepareRoll("save", result) await defender.prepareRoll("save", result)
} }
} }
return return
@@ -565,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) => {
@@ -598,7 +649,7 @@ export default class LethalFantasyUtils {
isRanged: msg.isRanged isRanged: msg.isRanged
} }
defender.system.prepareMonsterRoll("monster-defense", result) await defender.system.prepareMonsterRoll("monster-defense", result)
} }
return return
} }
@@ -672,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) => {
@@ -705,9 +758,9 @@ export default class LethalFantasyUtils {
isRanged: msg.isRanged isRanged: msg.isRanged
} }
console.log("Storing defense data for character:", defender.id) log("Storing defense data for character:", defender.id)
defender.prepareRoll("weapon-defense", result) await defender.prepareRoll("weapon-defense", result)
} }
} }
@@ -742,18 +795,11 @@ export default class LethalFantasyUtils {
// ── Choice type ── present all options to the player // ── Choice type ── present all options to the player
if (d30Message.type === "choice") { if (d30Message.type === "choice") {
// Try to find a bonus_dice option matching this side // If we can't show dialogs (wrong client), skip — the primary client
const autoBonus = d30Message.choices.find(c => c.type === "bonus_dice" && validTargets.includes(c.target)) // will communicate its choice result via socket. Auto-rolling here
// would give a different modifier on each client, causing divergence.
// If we can't show dialogs (wrong client), auto-roll bonus dice if available
if (!canDialog) { if (!canDialog) {
if (autoBonus) { return { modifier: 0, specialEffect: null, specialName: null }
const modifier = await this._rollD30BonusDie(autoBonus.dice, actor, true)
return { modifier, specialEffect: null, specialName: null }
}
// No bonus dice available on this side — just report as flag
const first = d30Message.choices[0]
return { modifier: 0, specialEffect: "flag", specialName: first?.type || "choice" }
} }
const buttons = d30Message.choices.map(c => { const buttons = d30Message.choices.map(c => {
@@ -774,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
@@ -963,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) => {
@@ -972,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
@@ -1019,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) => {
@@ -1032,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
@@ -1102,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"
@@ -1111,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"
@@ -1119,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"
@@ -1194,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"
@@ -1253,7 +1309,7 @@ export default class LethalFantasyUtils {
/* -------------------------------------------- */ /* -------------------------------------------- */
static async compareAttackDefense(data) { static async compareAttackDefense(data) {
console.log("compareAttackDefense called with:", data) log("compareAttackDefense called with:", data)
// Compute D30 effects from the attack D30 message directly. // Compute D30 effects from the attack D30 message directly.
// This is more reliable than depending on the caller-provided values, which are // This is more reliable than depending on the caller-provided values, which are
@@ -1268,11 +1324,11 @@ export default class LethalFantasyUtils {
const outcome = data.outcome || (data.attackRoll > data.defenseRoll ? "hit" : "miss") const outcome = data.outcome || (data.attackRoll > data.defenseRoll ? "hit" : "miss")
const isAttackWin = outcome !== "miss" const isAttackWin = outcome !== "miss"
console.log("isAttackWin:", isAttackWin, "attackRoll:", data.attackRoll, "defenseRoll:", data.defenseRoll) log("isAttackWin:", isAttackWin, "attackRoll:", data.attackRoll, "defenseRoll:", data.defenseRoll)
let damageButton = "" let damageButton = ""
if (isAttackWin && (data.attackWeaponId || data.attackRollKey)) { if (isAttackWin && (data.attackWeaponId || data.attackRollKey)) {
console.log("Creating damage button. defenderId:", data.defenderId) log("Creating damage button. defenderId:", data.defenderId)
// Déterminer le type de dégâts à lancer // Déterminer le type de dégâts à lancer
if (data.attackRollType === "weapon-attack") { if (data.attackRollType === "weapon-attack") {
damageButton = ` damageButton = `
@@ -1353,12 +1409,12 @@ export default class LethalFantasyUtils {
</div> </div>
` `
console.log("Creating combat result message...") log("Creating combat result message...")
await ChatMessage.create({ await ChatMessage.create({
content: resultMessage, content: resultMessage,
speaker: { alias: "Combat System" } speaker: { alias: "Combat System" }
}) })
console.log("Combat result message created!") log("Combat result message created!")
} }
static registerHandlebarsHelpers() { static registerHandlebarsHelpers() {
@@ -1495,7 +1551,7 @@ export default class LethalFantasyUtils {
return eval(expr); return eval(expr);
}) })
Handlebars.registerHelper('isOwnerOrGM', function (actor) { Handlebars.registerHelper('isOwnerOrGM', function (actor) {
console.log("Testing actor", actor.isOwner, game.userId) log("Testing actor", actor.isOwner, game.userId)
return actor.isOwner || game.isGM; return actor.isOwner || game.isGM;
}) })
Handlebars.registerHelper('upperCase', function (text) { Handlebars.registerHelper('upperCase', function (text) {
@@ -1590,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
} }
@@ -1633,7 +1693,7 @@ export default class LethalFantasyUtils {
} }
) )
ChatMessage.create({ await ChatMessage.create({
user: game.user.id, user: game.user.id,
speaker: { alias: targetActor.name }, speaker: { alias: targetActor.name },
mode: "gm", mode: "gm",
+1
View File
@@ -130,6 +130,7 @@
type="checkbox" type="checkbox"
name="saveSpellCheck" name="saveSpellCheck"
data-action="saveSpellCheck" data-action="saveSpellCheck"
{{#if saveSpell}}checked{{/if}}
/> />
</div> </div>
{{/if}} {{/if}}
+1 -1
View File
@@ -17,7 +17,7 @@
{{formField systemFields.damageType.fields.typeS value=system.damageType.typeS}} {{formField systemFields.damageType.fields.typeS value=system.damageType.typeS}}
</div> </div>
{{formField systemFields.damage.fields.damageM value=system.damage.damageM label="LETHALFANTASY.Label.damage"}} {{formField systemFields.damage.fields.damageM value=system.damage.damageM label=(localize "LETHALFANTASY.Label.damage")}}
{{formField systemFields.applyStrengthDamageBonus value=system.applyStrengthDamageBonus localize=true}} {{formField systemFields.applyStrengthDamageBonus value=system.applyStrengthDamageBonus localize=true}}
+2
View File
@@ -0,0 +1,2 @@
import { CompendiumsManager } from './CompendiumsManager.mjs';
CompendiumsManager.packToDistDir('packs_src', 'packs-system');
+2
View File
@@ -0,0 +1,2 @@
import { CompendiumsManager } from './CompendiumsManager.mjs';
CompendiumsManager.unpackToSrcDir('packs_src', 'packs-system');