Compare commits

...

18 Commits

Author SHA1 Message Date
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
uberwald bb42de19bd REmove unused file 2026-06-11 23:05:55 +02:00
uberwald 53f9c33419 chore: gitignore LevelDB internal files, stop tracking auto-generated LDB bookkeeping
Release Creation / build (release) Successful in 46s
2026-06-11 23:00:45 +02:00
uberwald 06eba5f835 fix: show spell tier dialog on character sheet cast; duplicate rollTarget to prevent Item mutation 2026-06-11 22:56:54 +02:00
uberwald 46fa2d15a3 fix: allow defender to react when attacker boosts past defense via cross-client socket 2026-06-11 22:41:54 +02:00
uberwald 8aae7bada0 fix: pre-check 'Save against spell' checkbox when defense originates from spell attack 2026-06-11 22:17:37 +02:00
uberwald ceb62bca3f fix: add missing class lethal-luck-grit-hud to template so JS selector matches 2026-06-11 22:03:56 +02:00
uberwald 110ac65ba5 fix: replace hardcoded French bleeding notifications with i18n keys 2026-06-11 21:50:19 +02:00
uberwald 9b75fd4d96 feat: combat-tracker-driven bleeding (HP loss per wound per round) 2026-06-11 21:49:35 +02:00
55 changed files with 768 additions and 369 deletions
+1
View File
@@ -0,0 +1 @@
packs/** filter=lfs diff=lfs merge=lfs -text
+11
View File
@@ -9,3 +9,14 @@ node_modules/
.history .history
.github/ .github/
# LevelDB internals (auto-generated, churn on every open)
packs-system/**/*.log
packs-system/**/LOG
packs-system/**/LOG.old
packs-system/**/CURRENT
packs-system/**/LOCK
packs-system/**/MANIFEST-*
# YAML source for pack round-trip
packs_src/
+6
View File
@@ -0,0 +1,6 @@
{
"cSpell.words": [
"biodata",
"LETHALFANTASY"
]
}
+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
-62
View File
@@ -1,62 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with this repository.
## Overview
FoundryVTT v13+ game system for the **Lethal Fantasy RPG**. Entry point: `lethal-fantasy.mjs`.
## Commands
```bash
# Compile LESS styles (styles/ -> css/)
npx gulp css # one-shot
npx gulp # compile + watch
# Lint
npx eslint .
# Compendium pack management (LevelDB <-> YAML source)
npm run pushLDBtoYML # export packs-system/ LevelDB -> source YAML
npm run pullYMLtoLDB # import source YAML -> packs-system/ LevelDB
```
No test suite exists.
## Architecture
Four layers in `module/`, all wired in `lethal-fantasy.mjs` via the `init` hook:
| Layer | Path | Purpose |
|---|---|---|
| Config | `module/config/` | Game constants. `SYSTEM` is `globalThis.SYSTEM` — always use `SYSTEM.*` for enumerations. |
| Models | `module/models/` | `TypeDataModel` subclasses — data schemas per document type. |
| Documents | `module/documents/` | Actor/Item/Roll/ChatMessage subclasses — game logic, roll processing, hooks. |
| Applications | `module/applications/` | `ApplicationV2` sheets + custom combat tracker. |
**Actor types**: `character`, `monster`
**Item types**: `skill`, `gift`, `vulnerability`, `weapon`, `armor`, `shield`, `spell`, `miracle`, `equipment`
Each layer has an `_module.mjs` barrel file that re-exports all classes from that layer.
Templates (`.hbs`) live in `templates/`. Styles are authored in LESS under `styles/` and compiled to `css/`.
### Key Patterns
- **Sheets**: Extend `HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2)` — imported from `foundry.applications.api`. **Not** the legacy `ActorSheet`. Child sheets (e.g. `character-sheet.mjs`) extend `base-actor-sheet.mjs` and override `static PARTS` and `DEFAULT_OPTIONS.actions`. Template paths are prefixed `systems/fvtt-lethal-fantasy/templates/`. Actor sheets have a play/edit toggle via `_sheetMode` and `SHEET_MODES`.
- **Models**: `static defineSchema()` using `foundry.data.fields.*`. Field definitions derived from SYSTEM config objects.
- **Rolls**: `LethalFantasyRoll` extends `Roll` with rich metadata via `this.options`. `D30Roll` is a separate class for D30 result tables (initialized in the `ready` hook).
- **Socket**: Events use `game.socket.on(\`system.${SYSTEM.id}\`, ...)`. Multi-player attack-defense uses a global `pendingDefenses` Map.
- **i18n**: All user-visible strings are keys in `lang/en.json` as `LETHALFANTASY.Category.Key`. Always use `game.i18n.localize(key)`.
### Compendium Packs
Five LevelDB packs in `packs-system/`: skills, equipment, gifts, vulnerabilities, spells-miracles. Use the `tools/` scripts to export/import editable YAML.
## Code Style
- No semicolons, double quotes, 2-space indent
- JSDoc `/** */` required on all functions/classes
- Max line length 180 (strings/templates exempt)
- Arrow functions: omit parens for single param
- ESLint + Prettier config in `eslint.config.mjs`
+28 -2
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": {
@@ -624,7 +648,9 @@
"messageLethargyKO": "{spellName} : Lethargy still ongoing ... ( dice result : {roll} )", "messageLethargyKO": "{spellName} : Lethargy still ongoing ... ( dice result : {roll} )",
"messageProgressionKO": "{name} can't attack this second.", "messageProgressionKO": "{name} can't attack this second.",
"messageProgressionOKMonster": "{name} can attack this second with {weapon}.", "messageProgressionOKMonster": "{name} can attack this second with {weapon}.",
"messageProgressionKOMonster": "{name} can't attack this second (dice result {roll})." "messageProgressionKOMonster": "{name} can't attack this second (dice result {roll}).",
"bleedingCombatEnd": "Bleeding active out of combat: {names}",
"bleedingCombatStart": "Bleeding still active on: {names}"
}, },
"Opponent": { "Opponent": {
"FIELDS": {} "FIELDS": {}
+180 -48
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,9 +111,8 @@ 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) { // Saignement piloté par le combat tracker
registerWorldCount("lethalFantasy") _registerBleedingHooks()
}
_showUserGuide() _showUserGuide()
@@ -129,6 +126,97 @@ Hooks.once("ready", function () {
} }
}) })
/**
* Saignement piloté par le combat tracker.
* Chaque round = 1 seconde → les acteurs qui saignent perdent 1 HP/blessure.
* Hors combat, une notification prévient le MJ que des blessures saignent encore.
*/
function _registerBleedingHooks() {
if (!game.user.isGM) return
Hooks.on("combatRound", async (combat, previous, current) => {
if (previous === current) return
const processed = new Set()
for (const combatant of combat.combatants) {
const actor = combatant.actor
if (!actor || processed.has(actor.id)) continue
processed.add(actor.id)
await _applyBleedingTick(actor)
}
})
Hooks.on("combatEnd", async (combat) => {
const bleeding = _findBleedingActors()
if (bleeding.length) {
ui.notifications.warn(
game.i18n.format("LETHALFANTASY.Notifications.bleedingCombatEnd", {
names: bleeding.map(a => a.name).join(", "),
})
)
}
})
Hooks.on("combatStart", async (combat) => {
const bleeding = _findBleedingActors()
if (bleeding.length) {
ui.notifications.warn(
game.i18n.format("LETHALFANTASY.Notifications.bleedingCombatStart", {
names: bleeding.map(a => a.name).join(", "),
})
)
}
})
}
/**
* Appliquer 1 HP de dégât par blessure active, décrémenter la durée.
* @param {import("foundry/common/documents.mjs").Actor} actor
*/
async function _applyBleedingTick(actor) {
if (!actor?.system?.hp?.wounds) return
const wounds = foundry.utils.duplicate(actor.system.hp.wounds)
let hpLoss = 0
let changed = false
for (const wound of wounds) {
if (wound.duration > 0 && wound.value > 0) {
hpLoss += 1
wound.duration -= 1
if (wound.duration <= 0) {
wound.value = 0
wound.description = ""
}
changed = true
}
}
if (!changed) return
const currentHp = actor.system.hp.value ?? 0
await actor.update({
"system.hp.value": currentHp - hpLoss,
"system.hp.wounds": wounds,
})
}
/**
* Retourne les acteurs (monde + tokens) qui ont des blessures actives.
* @returns {import("foundry/common/documents.mjs").Actor[]}
*/
function _findBleedingActors() {
const actors = []
for (const actor of game.actors.values()) {
if (actor?.system?.hp?.wounds?.some(w => w.duration > 0 && w.value > 0)) {
actors.push(actor)
}
}
for (const token of canvas.tokens?.placeables ?? []) {
if (token.actor && !actors.includes(token.actor)) {
if (token.actor?.system?.hp?.wounds?.some(w => w.duration > 0 && w.value > 0)) {
actors.push(token.actor)
}
}
}
return actors
}
Hooks.on("renderChatMessageHTML", (message, html, data) => { Hooks.on("renderChatMessageHTML", (message, html, data) => {
const typeMessage = data.message.flags.lethalFantasy?.typeMessage const typeMessage = data.message.flags.lethalFantasy?.typeMessage
// Message de demande de jet de dés // Message de demande de jet de dés
@@ -239,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
@@ -458,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
@@ -469,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
@@ -477,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
} }
@@ -502,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) {
@@ -539,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
@@ -569,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
@@ -600,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
@@ -908,17 +1002,67 @@ Hooks.on("createChatMessage", async (message) => {
} }
} }
} }
// Cross-client coordination: only delegate to the defender's client
// 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"))
|| game.users.find(u => u.active && u.isGM)
if (defenderOwner && defenderOwner.id !== game.user.id) {
// Send attackBoosted when the attacker actually boosted (so defender
// can respond to the new numbers), OR when the attacker has an active
// non-GM owner (PC-vs-PC cross-client) — the defender's hook-based
// processing is suppressed by attackerIsCrossClient, so the socket
// handler must show the defense dialog instead.
if (attackerHandledBonus || attackerHasNonGMOwner) {
const sData = LethalFantasyUtils.getShieldReactionData(defender)
game.socket.emit(`system.${SYSTEM.id}`, {
type: "attackBoosted",
userId: defenderOwner.id,
attackerName, attackerId, defenderName, defenderId, defenderTokenId,
attackerHandledBonus, attackRollFinal, defenseRoll, attackWeaponId, attackRollType, attackRollKey,
shieldDamageReduction: shieldBlocked ? shieldReaction?.damageReduction ?? 0 : 0,
d30Bleed: d30Bleed ? "true" : "",
d30DamageMultiplier, d30DrMultiplier,
damageTier: damageTier || "standard",
attackD30message,
defenseD30message,
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
}
}
} while (mulliganRestart) } while (mulliganRestart)
const shieldDamageReduction = shieldBlocked ? shieldReaction.damageReduction : 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,
@@ -940,7 +1084,7 @@ Hooks.on("createChatMessage", async (message) => {
attackD30message attackD30message
}) })
} else { } else {
console.log("Skipping message creation", { attackerHandledBonus, defenderHandledBonus }) log("Skipping message creation", { attackerHandledBonus, defenderHandledBonus })
} }
}) })
@@ -1021,7 +1165,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
@@ -1046,11 +1190,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) {
@@ -1261,16 +1405,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) {
+5 -1
View File
@@ -429,7 +429,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",
@@ -653,6 +653,10 @@
"type": "spell_calamity", "type": "spell_calamity",
"description": "A possible spell calamity has occurred" "description": "A possible spell calamity has occurred"
}, },
"arcane_spell_defense": {
"type": "spell_calamity",
"description": "Possible Spell Calamity or Catastrophe"
},
"melee_attack": { "melee_attack": {
"type": "fumble", "type": "fumble",
"detail": "melee_fumble", "detail": "melee_fumble",
+26 -4
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":
@@ -205,12 +205,34 @@ export default class LethalFantasyActor extends Actor {
case "spell-power": case "spell-power":
case "miracle-attack": case "miracle-attack":
case "miracle-power": case "miracle-power":
rollTarget = this.items.find((i) => (i.type === "miracle" || i.type == "spell") && i.id === rollKey) rollTarget = foundry.utils.duplicate(this.items.find((i) => (i.type === "miracle" || i.type == "spell") && i.id === rollKey))
rollTarget.rollKey = rollKey rollTarget.rollKey = rollKey
// Read damage tier from combatant currentAction if available // Read damage tier from combatant currentAction if available
const activeCombatant = game.combat?.combatants?.find(c => c.actorId === this.id) const activeCombatant = game.combat?.combatants?.find(c => c.actorId === this.id)
const currentAction = activeCombatant?.getFlag(SYSTEM.id, "currentAction") const currentAction = activeCombatant?.getFlag(SYSTEM.id, "currentAction")
const damageTier = currentAction?.damageTier || "standard" let damageTier = currentAction?.damageTier
// No tier from combat action — prompt the user if multiple tiers exist
if (!damageTier) {
const tierMap = { standard: "damageDice", overpowered: "damageDiceOverpowered", overpowered2: "damageDiceOverpowered2" }
const available = Object.entries(tierMap).filter(([k, v]) => rollTarget.system?.[v])
if (available.length > 1) {
const buttons = available.map(([id]) => ({
action: id,
type: "button",
label: id.charAt(0).toUpperCase() + id.slice(1),
callback: () => id,
}))
damageTier = await foundry.applications.api.DialogV2.wait({
window: { title: "Choose spell tier" },
classes: ["lethalfantasy"],
content: `<p>Select the power level for <strong>${rollTarget.name}</strong>:</p>`,
buttons,
rejectClose: false,
}) || "standard"
} else {
damageTier = "standard"
}
}
rollTarget.damageTier = damageTier rollTarget.damageTier = damageTier
if (rollType === "spell-attack" || rollType === "spell-power") { if (rollType === "spell-attack" || rollType === "spell-power") {
const tierCostMap = { standard: "cost", overpowered: "costOverpowered", overpowered2: "costOverpowered2" } const tierCostMap = { standard: "cost", overpowered: "costOverpowered", overpowered2: "costOverpowered2" }
+3 -1
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
*/ */
@@ -41,7 +43,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")
+43 -25
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: 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)
} }
@@ -679,6 +682,10 @@ export default class LethalFantasyRoll extends Roll {
if (Hooks.call("fvtt-lethal-fantasy.Roll", options, rollData, rollBase) === false) return if (Hooks.call("fvtt-lethal-fantasy.Roll", options, rollData, rollBase) === false) return
return rollBase return rollBase
} finally {
// Clear one-shot flag so it doesn't leak to subsequent non-spell saves
if (game.lethalFantasy) game.lethalFantasy.spellDefense = false
}
} }
/* ***********************************************************/ /* ***********************************************************/
@@ -714,6 +721,8 @@ export default class LethalFantasyRoll extends Roll {
content, content,
buttons: [ buttons: [
{ {
action: "initiative",
type: "button",
label: label, label: label,
callback: (event, button) => { callback: (event, button) => {
const output = Array.from(button.form.elements).reduce((obj, input) => { const output = Array.from(button.form.elements).reduce((obj, input) => {
@@ -791,6 +800,7 @@ export default class LethalFantasyRoll extends Roll {
} }
buttons.push({ buttons.push({
action: "roll", action: "roll",
type: "button",
label: weaponLabel, label: weaponLabel,
callback: (event, button) => { callback: (event, button) => {
let pos = $('#combat-action-dialog').position() let pos = $('#combat-action-dialog').position()
@@ -817,6 +827,7 @@ export default class LethalFantasyRoll extends Roll {
} }
buttons.push({ buttons.push({
action: "roll", action: "roll",
type: "button",
label: label, label: label,
callback: (event, button) => { callback: (event, button) => {
let pos = $('#combat-action-dialog').position() let pos = $('#combat-action-dialog').position()
@@ -828,6 +839,7 @@ export default class LethalFantasyRoll extends Roll {
} else { } else {
buttons.push({ buttons.push({
action: "roll", action: "roll",
type: "button",
label: "Select action", label: "Select action",
callback: (event, button) => { callback: (event, button) => {
let pos = $('#combat-action-dialog').position() let pos = $('#combat-action-dialog').position()
@@ -843,6 +855,7 @@ export default class LethalFantasyRoll extends Roll {
} }
buttons.push({ buttons.push({
action: "cancel", action: "cancel",
type: "button",
label: "Other action, not listed here", label: "Other action, not listed here",
callback: (event, button) => { callback: (event, button) => {
let pos = $('#combat-action-dialog').position() let pos = $('#combat-action-dialog').position()
@@ -861,12 +874,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 +923,7 @@ export default class LethalFantasyRoll extends Roll {
content: `<div class="grit-luck-dialog"><p><strong>${selectedItem.name}</strong> has multiple damage tiers.</p><p>Choose which damage to use when the attack lands:</p></div>`, content: `<div class="grit-luck-dialog"><p><strong>${selectedItem.name}</strong> has multiple damage tiers.</p><p>Choose which damage to use when the attack lands:</p></div>`,
buttons: tiers.map(t => ({ buttons: tiers.map(t => ({
action: t.id, action: t.id,
type: "button",
label: `${t.label} (${t.dice.toUpperCase()})`, label: `${t.label} (${t.dice.toUpperCase()})`,
icon: "fa-solid fa-wand-magic-sparkles", icon: "fa-solid fa-wand-magic-sparkles",
callback: () => t.id callback: () => t.id
@@ -924,7 +938,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 +949,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 +996,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 +1016,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 +1028,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 +1071,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 +1088,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 +1128,8 @@ export default class LethalFantasyRoll extends Roll {
content, content,
buttons: [ buttons: [
{ {
action: "rangeDefense",
type: "button",
label: label, label: label,
callback: (event, button) => { callback: (event, button) => {
const output = Array.from(button.form.elements).reduce((obj, input) => { const output = Array.from(button.form.elements).reduce((obj, input) => {
@@ -1130,7 +1146,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", "")
@@ -1273,6 +1289,8 @@ export default class LethalFantasyRoll extends Roll {
content, content,
buttons: [ buttons: [
{ {
action: "rangedAttack",
type: "button",
label, label,
callback: (event, button) => { callback: (event, button) => {
const output = Array.from(button.form.elements).reduce((obj, input) => { const output = Array.from(button.form.elements).reduce((obj, input) => {
@@ -1426,7 +1444,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 +1482,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 +1499,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 = {
+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)
} }
} }
+300 -60
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,11 +187,16 @@ 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 })
} }
} }
} }
break break
case "attackBoosted":
if (msg.userId === game.user.id) {
LethalFantasyUtils.handleAttackBoosted(msg)
}
break
} }
} }
@@ -226,6 +234,225 @@ export default class LethalFantasyUtils {
}) })
} }
/* -------------------------------------------- */
static async handleAttackBoosted(msg) {
const {
attackerName, attackerId, defenderName, defenderId, defenderTokenId,
attackerHandledBonus, attackRollFinal, defenseRoll, attackWeaponId, attackRollType, attackRollKey,
shieldDamageReduction: initialShieldDR,
d30Bleed, d30DamageMultiplier, d30DrMultiplier,
damageTier, attackD30message, defenseD30message,
hasShield, shieldLabel, shieldFormula, shieldDr, canAdHocShield
} = msg
const defender = game.actors.get(defenderId)
if (!defender) return
let updatedDefenseRoll = defenseRoll
let shieldBlocked = false
let shieldReaction = null
let canShieldReact = hasShield
let canAdHoc = canAdHocShield
// ── D30 bonus dice (defense) — resolved before grit/luck/shield ───────
let defenseDrMultiplier = null
if (defenseD30message && defender) {
const d30Result = await LethalFantasyUtils.processD30BonusDice(defenseD30message, "defense", null, defender, true)
if (d30Result.modifier) {
updatedDefenseRoll += d30Result.modifier
if (d30Result.modifier > 0) {
await ChatMessage.create({
content: `<p><strong>${defenderName}</strong> gains <strong>+${d30Result.modifier}</strong> from D30 bonus die for defense.</p>`,
speaker: ChatMessage.getSpeaker({ actor: defender })
})
}
}
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") {
await ChatMessage.create({
content: `<p>D30 — <strong>${d30Result.specialName || "Special Effect"}</strong> triggered for ${defenderName}!</p>`,
speaker: ChatMessage.getSpeaker({ actor: defender })
})
}
if (d30Result.specialEffect === "drMultiplier") {
defenseDrMultiplier = d30Result.multiplier
await ChatMessage.create({
content: `<p>D30 — Defense grants <strong>x${d30Result.multiplier} DR</strong> (choose which DR types to multiply when damage is applied)</p>`,
speaker: ChatMessage.getSpeaker({ actor: defender })
})
}
}
// Show the defense reaction dialog — while-loop for multiple reactions
if (defender) {
while (updatedDefenseRoll < attackRollFinal) {
const currentGrit = Number(defender.system?.grit?.current) || 0
const currentLuck = Number(defender.system?.luck?.current) || 0
const buttons = []
if (currentGrit > 0) {
buttons.push({
action: "grit",
type: "button",
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
icon: "fa-solid fa-fist-raised",
callback: () => "grit"
})
}
if (currentLuck > 0) {
buttons.push({
action: "luck",
type: "button",
label: `Spend 1 Luck (+1D6) [${currentLuck} left]`,
icon: "fa-solid fa-clover",
callback: () => "luck"
})
}
buttons.push({
action: "bonusDie",
type: "button",
label: "Add bonus die",
icon: "fa-solid fa-dice",
callback: () => "bonusDie"
})
if (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>
`,
buttons,
rejectClose: false
})
if (!choice || choice === "continue") break
if (choice === "grit") {
const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender,
total => `<p><strong>${defenderName}</strong> spends 1 Grit and rolls <strong>${total}</strong> for defense.</p>`)
updatedDefenseRoll += bonusRoll
await defender.update({ "system.grit.current": currentGrit - 1 })
} 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
}
} else if (choice === "shieldReact" && canShieldReact) {
const shieldBonus = await LethalFantasyUtils.rollBonusDie(shieldFormula, defender)
const newDefenseTotal = updatedDefenseRoll + shieldBonus
updatedDefenseRoll = newDefenseTotal
canShieldReact = false
if (newDefenseTotal >= attackRollFinal) {
shieldBlocked = true
shieldReaction = { damageReduction: shieldDr, label: shieldLabel, bonus: shieldBonus }
await ChatMessage.create({
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 })
})
} else {
await ChatMessage.create({
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
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 outcome = shieldBlocked ? "shielded-hit" : (updatedDefenseRoll >= attackRollFinal ? "miss" : "hit")
await LethalFantasyUtils.compareAttackDefense({
attackerName,
attackerId,
attackRoll: attackRollFinal,
attackWeaponId,
attackRollType,
attackRollKey,
defenderName,
defenderId,
defenderTokenId,
defenseRoll: updatedDefenseRoll,
outcome,
shieldDamageReduction: finalShieldDR,
d30Bleed: d30Bleed || "",
d30DamageMultiplier: d30DamageMultiplier || 1,
d30DrMultiplier: (d30DrMultiplier != null && d30DrMultiplier !== 1) ? d30DrMultiplier : (defenseDrMultiplier ?? d30DrMultiplier ?? 1),
damageTier: damageTier || "standard",
attackD30message
})
}
/* -------------------------------------------- */ /* -------------------------------------------- */
static async showDefenseRequest(msg) { static async showDefenseRequest(msg) {
const attackerName = msg.attackerName const attackerName = msg.attackerName
@@ -279,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"
@@ -313,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,
@@ -323,6 +551,7 @@ export default class LethalFantasyUtils {
if (result) { if (result) {
game.lethalFantasy = game.lethalFantasy || {} game.lethalFantasy = game.lethalFantasy || {}
game.lethalFantasy.spellDefense = true // pré-cocher "Save against spell" dans le dialog
game.lethalFantasy.nextDefenseData = { game.lethalFantasy.nextDefenseData = {
attackerId, attackerId,
attackRoll, attackRoll,
@@ -340,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
@@ -385,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) => {
@@ -418,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
} }
@@ -492,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) => {
@@ -525,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)
} }
} }
@@ -562,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 => {
@@ -594,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
@@ -783,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) => {
@@ -792,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
@@ -839,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) => {
@@ -852,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
@@ -922,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"
@@ -931,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"
@@ -939,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"
@@ -1014,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"
@@ -1073,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
@@ -1088,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 = `
@@ -1173,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() {
@@ -1315,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) {
@@ -1410,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
} }
@@ -1453,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
@@ -1 +0,0 @@
MANIFEST-000631
View File
-8
View File
@@ -1,8 +0,0 @@
2026/06/11-20:25:36.058631 7f37427ed6c0 Recovering log #629
2026/06/11-20:25:36.068520 7f37427ed6c0 Delete type=3 #627
2026/06/11-20:25:36.068574 7f37427ed6c0 Delete type=0 #629
2026/06/11-20:41:45.938413 7f3740fff6c0 Level-0 table #634: started
2026/06/11-20:41:45.938512 7f3740fff6c0 Level-0 table #634: 0 bytes OK
2026/06/11-20:41:45.945312 7f3740fff6c0 Delete type=0 #632
2026/06/11-20:41:45.965586 7f3740fff6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
2026/06/11-20:41:45.965944 7f3740fff6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
-8
View File
@@ -1,8 +0,0 @@
2026/06/10-19:56:01.455200 7f301cbff6c0 Recovering log #625
2026/06/10-19:56:01.464323 7f301cbff6c0 Delete type=3 #623
2026/06/10-19:56:01.464355 7f301cbff6c0 Delete type=0 #625
2026/06/10-20:16:07.843239 7f2fce7fc6c0 Level-0 table #630: started
2026/06/10-20:16:07.843267 7f2fce7fc6c0 Level-0 table #630: 0 bytes OK
2026/06/10-20:16:07.849298 7f2fce7fc6c0 Delete type=0 #628
2026/06/10-20:16:07.849522 7f2fce7fc6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
2026/06/10-20:16:07.849556 7f2fce7fc6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
Binary file not shown.
View File
-1
View File
@@ -1 +0,0 @@
MANIFEST-000628
View File
-8
View File
@@ -1,8 +0,0 @@
2026/06/11-20:25:36.074598 7f37437ef6c0 Recovering log #626
2026/06/11-20:25:36.084983 7f37437ef6c0 Delete type=3 #624
2026/06/11-20:25:36.085075 7f37437ef6c0 Delete type=0 #626
2026/06/11-20:41:45.965954 7f3740fff6c0 Level-0 table #631: started
2026/06/11-20:41:45.965991 7f3740fff6c0 Level-0 table #631: 0 bytes OK
2026/06/11-20:41:45.972550 7f3740fff6c0 Delete type=0 #629
2026/06/11-20:41:45.992980 7f3740fff6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
2026/06/11-20:41:45.993323 7f3740fff6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
-8
View File
@@ -1,8 +0,0 @@
2026/06/10-19:56:01.470668 7f2fcffff6c0 Recovering log #622
2026/06/10-19:56:01.480292 7f2fcffff6c0 Delete type=3 #620
2026/06/10-19:56:01.480316 7f2fcffff6c0 Delete type=0 #622
2026/06/10-20:16:07.810643 7f2fce7fc6c0 Level-0 table #627: started
2026/06/10-20:16:07.810662 7f2fce7fc6c0 Level-0 table #627: 0 bytes OK
2026/06/10-20:16:07.816355 7f2fce7fc6c0 Delete type=0 #625
2026/06/10-20:16:07.823439 7f2fce7fc6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
2026/06/10-20:16:07.823453 7f2fce7fc6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
Binary file not shown.
View File
-1
View File
@@ -1 +0,0 @@
MANIFEST-000633
View File
-8
View File
@@ -1,8 +0,0 @@
2026/06/11-20:25:36.041244 7f3742fee6c0 Recovering log #631
2026/06/11-20:25:36.052153 7f3742fee6c0 Delete type=3 #629
2026/06/11-20:25:36.052218 7f3742fee6c0 Delete type=0 #631
2026/06/11-20:41:45.958653 7f3740fff6c0 Level-0 table #636: started
2026/06/11-20:41:45.958703 7f3740fff6c0 Level-0 table #636: 0 bytes OK
2026/06/11-20:41:45.965435 7f3740fff6c0 Delete type=0 #634
2026/06/11-20:41:45.965798 7f3740fff6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
2026/06/11-20:41:45.965918 7f3740fff6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
-8
View File
@@ -1,8 +0,0 @@
2026/06/10-19:56:01.439055 7f2fcffff6c0 Recovering log #627
2026/06/10-19:56:01.451210 7f2fcffff6c0 Delete type=3 #625
2026/06/10-19:56:01.451236 7f2fcffff6c0 Delete type=0 #627
2026/06/10-20:16:07.804529 7f2fce7fc6c0 Level-0 table #632: started
2026/06/10-20:16:07.804550 7f2fce7fc6c0 Level-0 table #632: 0 bytes OK
2026/06/10-20:16:07.810504 7f2fce7fc6c0 Delete type=0 #630
2026/06/10-20:16:07.823431 7f2fce7fc6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
2026/06/10-20:16:07.823457 7f2fce7fc6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
MANIFEST-000328
-8
View File
@@ -1,8 +0,0 @@
2026/06/11-20:25:36.103531 7f37437ef6c0 Recovering log #326
2026/06/11-20:25:36.114200 7f37437ef6c0 Delete type=3 #324
2026/06/11-20:25:36.114249 7f37437ef6c0 Delete type=0 #326
2026/06/11-20:41:45.972686 7f3740fff6c0 Level-0 table #331: started
2026/06/11-20:41:45.972724 7f3740fff6c0 Level-0 table #331: 0 bytes OK
2026/06/11-20:41:45.979068 7f3740fff6c0 Delete type=0 #329
2026/06/11-20:41:45.993007 7f3740fff6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
2026/06/11-20:41:45.993301 7f3740fff6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
-8
View File
@@ -1,8 +0,0 @@
2026/06/10-19:56:01.497944 7f301cbff6c0 Recovering log #322
2026/06/10-19:56:01.506991 7f301cbff6c0 Delete type=3 #320
2026/06/10-19:56:01.507034 7f301cbff6c0 Delete type=0 #322
2026/06/10-20:16:07.829554 7f2fce7fc6c0 Level-0 table #327: started
2026/06/10-20:16:07.829571 7f2fce7fc6c0 Level-0 table #327: 0 bytes OK
2026/06/10-20:16:07.836328 7f2fce7fc6c0 Delete type=0 #325
2026/06/10-20:16:07.849487 7f2fce7fc6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
2026/06/10-20:16:07.849533 7f2fce7fc6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
MANIFEST-000627
-8
View File
@@ -1,8 +0,0 @@
2026/06/11-20:25:36.088952 7f37427ed6c0 Recovering log #625
2026/06/11-20:25:36.100017 7f37427ed6c0 Delete type=3 #623
2026/06/11-20:25:36.100073 7f37427ed6c0 Delete type=0 #625
2026/06/11-20:41:45.945442 7f3740fff6c0 Level-0 table #630: started
2026/06/11-20:41:45.945486 7f3740fff6c0 Level-0 table #630: 0 bytes OK
2026/06/11-20:41:45.951778 7f3740fff6c0 Delete type=0 #628
2026/06/11-20:41:45.965603 7f3740fff6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
2026/06/11-20:41:45.965769 7f3740fff6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
-8
View File
@@ -1,8 +0,0 @@
2026/06/10-19:56:01.483620 7f2fceffd6c0 Recovering log #621
2026/06/10-19:56:01.493981 7f2fceffd6c0 Delete type=3 #619
2026/06/10-19:56:01.494004 7f2fceffd6c0 Delete type=0 #621
2026/06/10-20:16:07.797521 7f2fce7fc6c0 Level-0 table #626: started
2026/06/10-20:16:07.797566 7f2fce7fc6c0 Level-0 table #626: 0 bytes OK
2026/06/10-20:16:07.804440 7f2fce7fc6c0 Delete type=0 #624
2026/06/10-20:16:07.823416 7f2fce7fc6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
2026/06/10-20:16:07.823449 7f2fce7fc6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
Binary file not shown.
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="control-icon" data-action="lethal-luck-grit-hud"> <div class="control-icon lethal-luck-grit-hud" data-action="lethal-luck-grit-hud">
<i class="fa-solid fa-dice" title="Adjust Luck/Grit" style="width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:18px;cursor:pointer"></i> <i class="fa-solid fa-dice" title="Adjust Luck/Grit" style="width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:18px;cursor:pointer"></i>
<div class="luck-grit-wrap luck-grit-hud-disabled"> <div class="luck-grit-wrap luck-grit-hud-disabled">
+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');