Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 28d6143f87 | |||
| 7e5fb0baa7 | |||
| 5734f3a5e3 | |||
| 7675c47ff9 | |||
| 90fde23ebc | |||
| d17c9cba49 | |||
| bfbce9557b | |||
| 881cecf5b7 | |||
| de31c397fa | |||
| fe346f46bf | |||
| 96e5bd5b4d | |||
| 1ea1a4b4b7 | |||
| 25648aa2a3 | |||
| 41b1199704 | |||
| 3df46b5848 | |||
| 05c93f9475 | |||
| bb005ee9fc | |||
| fa5c4cc9ce | |||
| 3b0d4e032e | |||
| 539841c4ff | |||
| ffba37b59e | |||
| 1a7585e1f6 | |||
| b567c8bbea | |||
| 60b351f50d | |||
| ace726a1fc | |||
| 67499bc199 | |||
| 7eae95cbbd | |||
| 1b53bf9152 | |||
| 2570bf707e | |||
| cbeaaeec99 | |||
| 37badf2619 | |||
| 5839616863 | |||
| 89298490ef | |||
| bb42de19bd | |||
| 53f9c33419 | |||
| 06eba5f835 | |||
| 46fa2d15a3 | |||
| 8aae7bada0 | |||
| ceb62bca3f | |||
| 110ac65ba5 | |||
| 9b75fd4d96 | |||
| 141d6048e0 | |||
| ea7acf6bf8 | |||
| c20750caa7 | |||
| ce630feb51 | |||
| b35b684d50 | |||
| f6fb0b68b8 | |||
| e45edd60c4 | |||
| d389a85a9f | |||
| c217490a5b | |||
| 38eb1a8d3d | |||
| 4724cdf2bb | |||
| 6d06c8ddad | |||
| 2770774aa3 | |||
| e417b61625 | |||
| 9a8d580ef6 | |||
| 9ccb0f90f0 | |||
| 6cf0880ad3 | |||
| 96306623e5 | |||
| 7279cd752d | |||
| db3e8b5d35 | |||
| 54421e4a83 | |||
| ac44419b7a | |||
| a3fc0a42b9 | |||
| c8ce840e98 | |||
| 55a040062a | |||
| 1818a76499 | |||
| 55d1b41ca4 |
@@ -0,0 +1 @@
|
||||
packs/** filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
token: ${{ secrets.FOUNDRY_PUBLISH_KEY }}
|
||||
id: "fvtt-lethal-fantasy"
|
||||
version: ${{github.event.release.tag_name}}
|
||||
manifest: "https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/system.json"
|
||||
manifest: "https://www.uberwald.me/gitea/uberwald/fvtt-lethal-fantasy/releases/download/latest/system.json"
|
||||
notes: "https://www.uberwald.me/gitea/public/fvtt-lethal-fantasy/raw/branch/main/changelog.md"
|
||||
compatibility-minimum: "13"
|
||||
compatibility-verified: "13"
|
||||
compatibility-minimum: "14"
|
||||
compatibility-verified: "14"
|
||||
|
||||
@@ -8,4 +8,3 @@ node_modules/
|
||||
|
||||
.history
|
||||
.github/
|
||||
|
||||
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"biodata",
|
||||
"LETHALFANTASY"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
# 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. Fix monster defense mulligan reroll button missing cross-client.
|
||||
|
||||
## Accomplished
|
||||
|
||||
### Pass 1 — Critical Issues
|
||||
- **Telemetry removed** from system.json
|
||||
- **globalThis side effects** moved from top-level to `init` hook
|
||||
- **console.log → log()** helper guarded by setting
|
||||
- **Stale Tenebris refs** → LETHALFANTASY
|
||||
|
||||
### Pass 2 — V1/V2 Mixing, Fire-and-Forget
|
||||
- V1 sheet registrations, activateListeners/jQuery, FilePicker paths fixed
|
||||
- Fire-and-forget Promises now awaited
|
||||
- Misnamed `LethalFantasySkill` → `LethalFantasyWeapon`
|
||||
|
||||
### Pass 3 — Code Review Fixes
|
||||
- Duplicated dialogs fixed via `_onRender` bindings
|
||||
- renderChatMessage reverted to HTML hook
|
||||
- All review awaits confirmed
|
||||
|
||||
### Pass 4 — D30 Dialog Removal & Dead Code Audit
|
||||
- **D30 choice dialog removed** — auto-rolls bonus dice; special strike/defense reported as `specialEffect: "flag"` (informational)
|
||||
- **Spell calamity choice restored** — catch-all for non-standard choices uses `specialEffect: "flag"`
|
||||
- **Dead `specialEffect === "auto"` branches removed** from `chat-reaction.mjs` (×2), `combat.mjs` (×1), `reaction-message.hbs`
|
||||
- **Deleted `d30-special-choice.hbs` and `_buildSpecialLabel()`**
|
||||
- **Dead code audit** — 2 runtime bugs fixed, ~20 dead exports/methods, 33 unused i18n keys, 2 unused templates
|
||||
- **3 critical bugs fixed**: `SYSTEM.ROLL_TYPE`, `SYSTEM.EQUIPMENT_CATEGORIES`, missing imports in `applications/combat.mjs`
|
||||
- **`isPrimaryController` consolidated** to local function
|
||||
- **Aether/Grace deduction merged** via `_deductResourceOnCast()`
|
||||
- **`nextDefenseData` deduped** via `_storeNextDefenseData()`
|
||||
- **`buildDefenseReactionButtons` extracted** from `combat.mjs`; fixes stale Grit/Luck snapshots
|
||||
- **HP HUD toggling extracted** to `helpers.mjs`
|
||||
- **`node --check` passes all 55 `.mjs` files**
|
||||
|
||||
### Pass 5 — Live Verification
|
||||
- **D30=30 auto-roll verified** — Club attack shows D30=30 flag
|
||||
- **Defense request dialog verified** — Monster defense dialog with weapon dropdown
|
||||
- **Defense reaction dialog verified** — Luck spent, bonus die added, combat result correct
|
||||
- **AZA→Monster attack flow tested end-to-end**: Club attack (D20=16, D30=6) → Monster defense (D20=1) → defense reactions (Continue) → D30 attack bonus processed (+2, total 18)
|
||||
- **BUG FOUND & FIXED: `d30ChangedAttack` infinite loop** — `chat-reaction.mjs:452-455` do-while reset block missing `d30ChangedAttack = false`; added at line 456
|
||||
- **BUG FIX CONFIRMED**: Re-tested full flow — AZA Club attack (13, D30=12) → defense dialog → Monster defense (2, D30=24) → reaction dialog (only 1 show!) → Continue → "AZA hits Monster!" combat result → damage roll (1d6=2, total 3) → Apply Damage button. No infinite loop. Full E2E success.
|
||||
|
||||
### Pass 6 — Cross-Client Mulligan Reroll Fix
|
||||
- **BUG FIX: `handleAttackBoosted` hardcoded `canRerollDefense: false`** — `utils/combat.mjs:142` now computes `canRerollDefense` from `defenseD30message` via `hasD30Reroll()`. Also passes `d30message` to dialog template (was `null`).
|
||||
- **BUG FIX: Missing `defenseRerollContext` in socket data** — added `defenseRerollContext` to `attackBoosted` socket message at `chat-reaction.mjs:773`, and added `rerollDefense` handler in `handleAttackBoosted` at `utils/combat.mjs:203-240` so the mulligan reroll works cross-client.
|
||||
- **BUG FIX: Cross-client mulligan reroll now processes new D30 bonus dice** — after reroll, calls `processD30BonusDice` on the new D30 message to apply bonus dice, flags, and DR multipliers (was silently ignored).
|
||||
- **Import `hasD30Reroll` added** to `utils/combat.mjs`
|
||||
- **`bleed` top-level type handler added** to `processD30BonusDice` in `d30.mjs:79-81` — returns `specialEffect: "bleed"` same as combo path, so ranged attack bleed (values 5,10,15) creates reaction message and sets damage button bleed flag.
|
||||
|
||||
### Pass 7 — Cross-Client Attack D30 Pre-Processing
|
||||
- **BUG FIX: D30 attack bonus processed at source (defense request time) instead of in defense handler** — `chat-reaction.mjs:136-148` now calls `processD30BonusDice` in the defense request button click handler, sends the boosted `attackRoll` and full `d30AttackEffects` result in the defense request. The `createChatMessage` handler at `chat-reaction.mjs:622-626` uses the pre-computed values instead of re-processing (avoids double dice roll and ensures both clients agree on attack value).
|
||||
- **Fix ensures cross-client agreement**: Player and GM clients see the same boosted attack roll because it's computed once at attack time and sent via socket, not separately per client in the defense handler.
|
||||
- **BUG FIX: `d30AttackEffects` not propagated through `_storeNextDefenseData`** — added `d30AttackEffects` to `nextDefenseData` in `combat.mjs:353-355` so the `createChatMessage` handler finds pre-computed values for same-client path (was silently falling back to legacy dice-roll, causing double boost).
|
||||
- **BUG FIX: `d30AttackPrecomputedStale` flag for mulligan rerolls** — added `chat-reaction.mjs:464` flag and check at `chat-reaction.mjs:632-634`. After a mulligan reroll updates `attackD30message`, the stale pre-computed effects are bypassed and the new D30 message is processed fresh via `processD30BonusDice`.
|
||||
- **BUG FIX: `_dice-breakdown.hbs` partial never registered** — `loadTemplates([path])` registers partials with the full file path as the Handlebars ID, but the template references it by the short name `chat/dice-breakdown`. Fixed by passing an object `{"chat/dice-breakdown": "path"}` which explicitly sets the partial ID to match the template reference.
|
||||
- **SAFETY: do-while loop body wrapped in try/catch** — any exception from `createReactionMessage` calls now logs the error and continues safely instead of silently aborting the handler.
|
||||
|
||||
## Key Decisions
|
||||
- **Auto-roll bonus dice without dialog** — matches existing D30=27 (d6E) flow
|
||||
- **`buildDefenseReactionButtons` extracts only button-building** — defense while-loop structures differ between same-client and cross-client; merging loops risks behavioral divergence
|
||||
- **Inline grit/luck deduction uses live actor values**
|
||||
- **Aether/Grace helper uses `costFn` parameter**
|
||||
- **Cross-client mulligan reroll sends full `defenseRerollContext`** via socket so the defender can re-roll the same configured roll on their client
|
||||
|
||||
## Next Steps
|
||||
1. Test defense request dialogs (character/monster/save) — more variants
|
||||
2. Test all reaction message variants (shield block/fail, d30Bonus/Flag, grit, luck, etc.)
|
||||
3. Create Player user in Foundry for cross-client socket testing (includes mulligan reroll)
|
||||
4. Prune dead code: unused exports (~20), unused i18n keys (33), unused templates (2)
|
||||
|
||||
## Critical Context
|
||||
- **Chat buttons not interactive via DevTools snapshot** — need JS fallback: `document.querySelectorAll('button').forEach(b => { if (...) b.click(); })`
|
||||
- **Defense flow**: Attack card → target button (`.request-defense-btn`) → defense dialog → defense roll → defense reactions dialog → combat result card with Damage button → damage roll dialog → Apply Damage → HP application
|
||||
- **Clicking "Damage" directly bypasses defense** — rolls unapplied damage to chat
|
||||
- **Same-owner guard** (`chat-reaction.mjs:180-182`) — skips defense when GM owns both, unless `!defenderIsMonster`
|
||||
- **`d30ChangedAttack` infinite loop** — variable wasn't reset in do-while block; fix at `chat-reaction.mjs:456`
|
||||
- **Cross-client mulligan fix**: `handleAttackBoosted` now shows reroll button and handles the reroll action. Requires `defenseRerollContext` in socket data (added).
|
||||
- **Deserialized weapon object** — `weapon.name` works, `weapon.id` undefined, `weapon._id` works
|
||||
- **Fvtt server**: port 31000, foundrydata-dev
|
||||
- **No player user configured** — cannot test cross-client socket flow
|
||||
|
||||
## Relevant Files
|
||||
- `module/hooks/chat-reaction.mjs` — all 7 hook registrations; defense do-while loop; **d30ChangedAttack fix (line 456)**; socket data includes `defenseRerollContext` (line 773)
|
||||
- `module/utils/combat.mjs` — `buildDefenseReactionButtons`; **`handleAttackBoosted` mulligan fix (lines 142, 154, 227-249)**; imports `hasD30Reroll`
|
||||
- `module/utils/d30.mjs` — `processD30BonusDice`: auto-roll, flag reporting, no dialog; `hasD30Reroll` checks `type === "mulligan"`
|
||||
- `module/utils/helpers.mjs` — `_toggleHudWraps`/`_disableHudWraps`
|
||||
- `module/utils.mjs` — barrel re-exporting 23 static methods
|
||||
- `module/models/equipment.mjs` — `EQUIPMENT_CATEGORY` fix
|
||||
- `module/applications/combat.mjs` — added `import { SYSTEM }` and `import { log }`
|
||||
- `templates/chat/reaction-message.hbs` — `d30Flag` text changed; `d30Auto` branch removed
|
||||
- `templates/dialogs/d30-special-choice.hbs` — deleted
|
||||
- `lang/en.json` — 33 unused i18n keys remain
|
||||
+225
-32
@@ -529,16 +529,16 @@ i.lethalfantasy {
|
||||
}
|
||||
.lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .armor-hp {
|
||||
margin-right: 4px;
|
||||
min-width: 10rem;
|
||||
max-width: 10rem;
|
||||
min-width: 11rem;
|
||||
max-width: 11rem;
|
||||
}
|
||||
.lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .armor-hp .name {
|
||||
min-width: 6rem;
|
||||
max-width: 6rem;
|
||||
}
|
||||
.lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .armor-hp .input {
|
||||
min-width: 2.5rem;
|
||||
max-width: 2.5rem;
|
||||
min-width: 3.5rem;
|
||||
max-width: 3.5rem;
|
||||
}
|
||||
.lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .granted {
|
||||
min-width: 8rem;
|
||||
@@ -1178,6 +1178,45 @@ i.lethalfantasy {
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.lethalfantasy .tab.monster-combat .ranged-attacks {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.lethalfantasy .tab.monster-combat .ranged-attacks .attack .name {
|
||||
min-width: 10rem;
|
||||
max-width: 10rem;
|
||||
}
|
||||
.lethalfantasy .tab.monster-combat .ranged-attacks .attack .damage-dice {
|
||||
width: 5rem;
|
||||
max-width: 5rem;
|
||||
}
|
||||
.lethalfantasy .tab.monster-combat .ranged-attacks .ranged-weapon-range {
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid var(--color-border-light-tertiary, #ccc);
|
||||
padding-top: 6px;
|
||||
}
|
||||
.lethalfantasy .tab.monster-combat .ranged-attacks .ranged-weapon-range > label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
}
|
||||
.lethalfantasy .tab.monster-combat .ranged-attacks .ranged-weapon-range .range-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.lethalfantasy .tab.monster-combat .ranged-attacks .ranged-weapon-range .range-fields .range-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.lethalfantasy .tab.monster-combat .ranged-attacks .ranged-weapon-range .range-fields .range-field label {
|
||||
font-size: 0.7rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.lethalfantasy .tab.monster-combat .ranged-attacks .ranged-weapon-range .range-fields .range-field input {
|
||||
width: 3.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
.lethalfantasy .tab.monster-combat .armors {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
@@ -1988,9 +2027,12 @@ i.lethalfantasy {
|
||||
background: #4a4940 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.lethalfantasy .grit-luck-dialog {
|
||||
color: var(--color-text-dark-primary, #191813);
|
||||
}
|
||||
.lethalfantasy .grit-luck-dialog .combat-status {
|
||||
padding: 12px;
|
||||
background: linear-gradient(to bottom, rgba(42, 41, 32, 0.8) 0%, rgba(26, 25, 16, 0.9) 100%);
|
||||
background: linear-gradient(to bottom, rgba(42, 41, 32, 0.88) 0%, rgba(26, 25, 16, 0.95) 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.5);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
@@ -2010,11 +2052,25 @@ i.lethalfantasy {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.lethalfantasy .grit-luck-dialog .offer-text {
|
||||
color: #f0e6d2;
|
||||
color: var(--color-text-dark-primary, #191813);
|
||||
font-size: calc(var(--font-size-standard) * 1);
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
.lethalfantasy .grit-luck-dialog .shield-warning {
|
||||
color: #7a4000;
|
||||
background: rgba(255, 160, 0, 0.12);
|
||||
border: 1px solid rgba(255, 160, 0, 0.4);
|
||||
border-radius: 5px;
|
||||
font-size: calc(var(--font-size-standard) * 0.88);
|
||||
padding: 6px 10px;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.lethalfantasy .grit-luck-dialog .shield-warning i {
|
||||
color: #c07000;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.lethalfantasy .attack-result {
|
||||
padding: 16px;
|
||||
@@ -2105,31 +2161,66 @@ i.lethalfantasy {
|
||||
color: #d4af37;
|
||||
}
|
||||
.lethalfantasy .attack-result .attack-result-damage {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
.lethalfantasy .attack-result .attack-result-damage.single-btn {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.lethalfantasy .attack-result .attack-result-damage.spell-damage {
|
||||
grid-template-columns: 1fr;
|
||||
width: 100%;
|
||||
}
|
||||
.lethalfantasy .attack-result .attack-result-damage .roll-damage-btn {
|
||||
padding: 10px 16px;
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(to bottom, #8b0000 0%, #660000 100%);
|
||||
border: 1px solid #ff0000;
|
||||
border: 1px solid #4b0000;
|
||||
border-radius: 6px;
|
||||
color: #f0e6d2;
|
||||
font-weight: 600;
|
||||
font-size: calc(var(--font-size-standard) * 0.9);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.lethalfantasy .attack-result .attack-result-damage .roll-damage-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
.lethalfantasy .attack-result .attack-result-damage .roll-damage-btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
.lethalfantasy .attack-result .attack-result-damage .roll-damage-btn i {
|
||||
font-size: calc(var(--font-size-standard) * 1.1);
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.lethalfantasy .attack-result .attack-result-damage .roll-damage-btn:hover {
|
||||
background: linear-gradient(to bottom, #a00000 0%, #7b0000 100%);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
border-color: #5b0000;
|
||||
}
|
||||
.lethalfantasy .attack-result .attack-result-damage .roll-damage-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.lethalfantasy .attack-result .attack-result-damage .roll-damage-btn i {
|
||||
margin-right: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4), inset 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.lethalfantasy .equipment-content {
|
||||
font-family: var(--font-primary);
|
||||
@@ -2595,6 +2686,21 @@ i.lethalfantasy {
|
||||
max-width: 8rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.dialog-warning {
|
||||
margin: 0.4rem 0.2rem 0.2rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border-left: 3px solid #c8941a;
|
||||
background: rgba(200, 148, 26, 0.12);
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-secondary);
|
||||
font-size: calc(var(--font-size-standard) * 0.9);
|
||||
color: #7a5400;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.dialog-warning i {
|
||||
color: #c8941a;
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
.lethalfantasy.dice-roll,
|
||||
.fvtt-lethal-fantasy.dice-roll,
|
||||
.message.lethalfantasy.dice-roll,
|
||||
@@ -3174,16 +3280,10 @@ i.lethalfantasy {
|
||||
.message.lethalfantasy.dice-roll .damage-buttons .damage-buttons-grid,
|
||||
.message.fvtt-lethal-fantasy.dice-roll .damage-buttons .damage-buttons-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.lethalfantasy.dice-roll .damage-buttons .damage-buttons-grid.monster-damage,
|
||||
.fvtt-lethal-fantasy.dice-roll .damage-buttons .damage-buttons-grid.monster-damage,
|
||||
.message.lethalfantasy.dice-roll .damage-buttons .damage-buttons-grid.monster-damage,
|
||||
.message.fvtt-lethal-fantasy.dice-roll .damage-buttons .damage-buttons-grid.monster-damage {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 280px;
|
||||
margin: 0 auto;
|
||||
gap: 8px;
|
||||
}
|
||||
.lethalfantasy.dice-roll .damage-buttons .damage-buttons-grid .damage-roll-btn,
|
||||
.fvtt-lethal-fantasy.dice-roll .damage-buttons .damage-buttons-grid .damage-roll-btn,
|
||||
@@ -3443,6 +3543,7 @@ i.lethalfantasy {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
justify-items: center;
|
||||
}
|
||||
.lethalfantasy.dice-roll .d30-message,
|
||||
.fvtt-lethal-fantasy.dice-roll .d30-message,
|
||||
@@ -3889,35 +3990,74 @@ i.lethalfantasy {
|
||||
}
|
||||
.message .attack-result .attack-result-damage,
|
||||
.attack-result .attack-result-damage {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
.message .attack-result .attack-result-damage.single-btn,
|
||||
.attack-result .attack-result-damage.single-btn {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.message .attack-result .attack-result-damage.spell-damage,
|
||||
.attack-result .attack-result-damage.spell-damage {
|
||||
grid-template-columns: 1fr;
|
||||
width: 100%;
|
||||
}
|
||||
.message .attack-result .attack-result-damage .roll-damage-btn,
|
||||
.attack-result .attack-result-damage .roll-damage-btn {
|
||||
padding: 10px 16px;
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(to bottom, #8b0000 0%, #660000 100%);
|
||||
border: 1px solid #ff0000;
|
||||
border: 1px solid #4b0000;
|
||||
border-radius: 6px;
|
||||
color: #f0e6d2;
|
||||
font-weight: 600;
|
||||
font-size: calc(var(--font-size-standard) * 0.9);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.message .attack-result .attack-result-damage .roll-damage-btn::before,
|
||||
.attack-result .attack-result-damage .roll-damage-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
.message .attack-result .attack-result-damage .roll-damage-btn:hover::before,
|
||||
.attack-result .attack-result-damage .roll-damage-btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
.message .attack-result .attack-result-damage .roll-damage-btn i,
|
||||
.attack-result .attack-result-damage .roll-damage-btn i {
|
||||
font-size: calc(var(--font-size-standard) * 1.1);
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.message .attack-result .attack-result-damage .roll-damage-btn:hover,
|
||||
.attack-result .attack-result-damage .roll-damage-btn:hover {
|
||||
background: linear-gradient(to bottom, #a00000 0%, #7b0000 100%);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
border-color: #5b0000;
|
||||
}
|
||||
.message .attack-result .attack-result-damage .roll-damage-btn:active,
|
||||
.attack-result .attack-result-damage .roll-damage-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.message .attack-result .attack-result-damage .roll-damage-btn i,
|
||||
.attack-result .attack-result-damage .roll-damage-btn i {
|
||||
margin-right: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4), inset 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
#token-hud .hp-loss-wrap {
|
||||
position: absolute;
|
||||
@@ -3994,6 +4134,59 @@ i.lethalfantasy {
|
||||
padding-left: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
/* Luck/Grit Styles */
|
||||
#token-hud .luck-grit-wrap {
|
||||
position: absolute;
|
||||
left: 75px;
|
||||
display: none;
|
||||
top: 50%;
|
||||
width: 80px;
|
||||
text-align: start;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 1px solid rgba(139, 69, 19, 0.5);
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px;
|
||||
transform: translate(-100%, -50%);
|
||||
}
|
||||
#token-hud .luck-grit-hud-active {
|
||||
display: block;
|
||||
}
|
||||
#token-hud .luck-grit-hud-disabled {
|
||||
display: none;
|
||||
}
|
||||
#token-hud .luck-grit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
#token-hud .luck-grit-label {
|
||||
flex: 1;
|
||||
color: #c9b896;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
#token-hud .luck-grit-btn {
|
||||
width: 28px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
background: rgba(139, 69, 19, 0.25);
|
||||
border: 1px solid rgba(139, 69, 19, 0.4);
|
||||
border-radius: 3px;
|
||||
color: #d4c5a9;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#token-hud .luck-grit-btn:hover {
|
||||
background: rgba(139, 69, 19, 0.5);
|
||||
border-color: rgba(139, 69, 19, 0.7);
|
||||
}
|
||||
/* -------------------------------------------------- */
|
||||
/* Dice Tray — injected into the Foundry chat sidebar */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
+122
-8
@@ -1,8 +1,32 @@
|
||||
{
|
||||
"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}",
|
||||
"RoundNext": "Next second",
|
||||
"RoundPrev": "Previous second",
|
||||
"Rounds": "Seconds",
|
||||
"RoundNext": "Next second"
|
||||
"Settings": "Combat Settings",
|
||||
"ToggleDead": "Toggle Dead",
|
||||
"ToggleVis": "Toggle Visible",
|
||||
"TurnEnd": "End Turn",
|
||||
"TurnNext": "Next Turn",
|
||||
"TurnPrev": "Previous Turn"
|
||||
},
|
||||
"LETHALFANTASY": {
|
||||
"Armor": {
|
||||
@@ -382,7 +406,7 @@
|
||||
"rollProgressionCount": "Roll progression count",
|
||||
"rollProgressionDice": "Roll progression/Lethargy dice",
|
||||
"earned": "Earned",
|
||||
"divinityPoints": "Divinity points",
|
||||
"divinityPoints": "Grace",
|
||||
"aetherPoints": "Aether points",
|
||||
"attacks": "Attacks",
|
||||
"attackMode": "Attack Mode",
|
||||
@@ -462,8 +486,17 @@
|
||||
"notes": "Notes",
|
||||
"pc": "PC",
|
||||
"perception": "Perception",
|
||||
"pointBlank": "Point blank",
|
||||
"short": "Short",
|
||||
"medium": "Medium",
|
||||
"long": "Long",
|
||||
"extreme": "Extreme",
|
||||
"outOfSkill": "Out of skill",
|
||||
"range": "Range",
|
||||
"rangeDefenseDialog": "Ranged defense dialog",
|
||||
"rangeDefenseRoll": "Ranged defense roll",
|
||||
"rangeAttackDialog": "Ranged attack dialog",
|
||||
"rangeAttackRoll": "Ranged attack roll",
|
||||
"rangedAttackDefense": "Ranged attack defense",
|
||||
"resource": "Resource",
|
||||
"resources": "Resources",
|
||||
@@ -496,11 +529,42 @@
|
||||
"monster-defense": "Monster defense",
|
||||
"weapons": "Weapons",
|
||||
"wis": "WIS",
|
||||
"weapon-damage-medium": "Weapon damage medium",
|
||||
"weapon-damage-small": "Weapon damage small",
|
||||
"combatProgressionStart": "Combat start threshold",
|
||||
"miracle": "Miracle",
|
||||
"titleStandard": "Standard Roll"
|
||||
"titleStandard": "Standard Roll",
|
||||
"privateRoll": "Private Roll",
|
||||
"current": "Current",
|
||||
"max": "Max",
|
||||
"speed": "Speed",
|
||||
"bonuses": "Bonuses",
|
||||
"handToHandAttacks": "Hand To Hand Attacks",
|
||||
"beyondSkill": "Beyond Skill",
|
||||
"letItFly": "Let It Fly!",
|
||||
"class": "Class",
|
||||
"mortal": "Mortal",
|
||||
"alignment": "Alignment",
|
||||
"age": "Age",
|
||||
"height": "Height",
|
||||
"weight": "Weight",
|
||||
"eyes": "Eyes",
|
||||
"hair": "Hair",
|
||||
"magicUser": "Magic User",
|
||||
"clericUser": "Cleric User",
|
||||
"lastHdRoll": "Last HD roll",
|
||||
"naturalDR": "Natural DR",
|
||||
"magicalDR": "Magical DR",
|
||||
"saveBonus": "Save bonus (1/5 levels)",
|
||||
"spellBonus": "Spell bonus (1/5 levels)",
|
||||
"miracleBonus": "Miracle bonus (1/5 levels)",
|
||||
"devPointsTotal": "Dev. Points (Total)",
|
||||
"devPointsRem": "Dev. Points (Rem.)",
|
||||
"length": "Length",
|
||||
"vision": "Vision",
|
||||
"damageType": "Damage Type",
|
||||
"components": "Components",
|
||||
"coverRanged": "Cover vs ranged attacks",
|
||||
"standing": "Standing",
|
||||
"crouching": "Crouching"
|
||||
},
|
||||
"Miracle": {
|
||||
"FIELDS": {
|
||||
@@ -556,6 +620,15 @@
|
||||
},
|
||||
"savingThrow": {
|
||||
"label": "Saving throw"
|
||||
},
|
||||
"damageDiceOverpowered": {
|
||||
"label": "Overpowered Damage Dice"
|
||||
},
|
||||
"damageDiceOverpowered2": {
|
||||
"label": "Overpowered 2 Damage Dice"
|
||||
},
|
||||
"damageDice": {
|
||||
"label": "Damage Dice"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -575,7 +648,9 @@
|
||||
"messageLethargyKO": "{spellName} : Lethargy still ongoing ... ( dice result : {roll} )",
|
||||
"messageProgressionKO": "{name} can't attack this second.",
|
||||
"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": {
|
||||
"FIELDS": {}
|
||||
@@ -677,7 +752,8 @@
|
||||
"label": "Min"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoDestruction": "Auto-Destruction"
|
||||
},
|
||||
"Skill": {
|
||||
"Category": {
|
||||
@@ -776,6 +852,12 @@
|
||||
"cost": {
|
||||
"label": "Cost"
|
||||
},
|
||||
"costOverpowered": {
|
||||
"label": "Cost (Overpowered)"
|
||||
},
|
||||
"costOverpowered2": {
|
||||
"label": "Cost (Overpowered 2)"
|
||||
},
|
||||
"description": {
|
||||
"label": "Description"
|
||||
},
|
||||
@@ -802,6 +884,12 @@
|
||||
},
|
||||
"damageDice": {
|
||||
"label": "Damage dice"
|
||||
},
|
||||
"damageDiceOverpowered": {
|
||||
"label": "Overpowered Damage Dice"
|
||||
},
|
||||
"damageDiceOverpowered2": {
|
||||
"label": "Overpowered 2 Damage Dice"
|
||||
}
|
||||
},
|
||||
"Range": {
|
||||
@@ -836,7 +924,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Warning": {},
|
||||
"Warning": {
|
||||
"defenseShieldOrder": "To avoid a hit without using the shield, roll Grit or Luck first — then roll the shield."
|
||||
},
|
||||
"Weapon": {
|
||||
"FIELDS": {
|
||||
"isAgile": {
|
||||
@@ -981,6 +1071,30 @@
|
||||
"diceResult": "Dice result",
|
||||
"progressionCount": "Progression count:"
|
||||
},
|
||||
"Combat": {
|
||||
"RollMonsters": "Roll Monsters",
|
||||
"monstersNotRolledTitle": "Monsters Not Rolled",
|
||||
"monstersNotRolledMsg": "Monsters have not rolled this second. Proceed anyway?",
|
||||
"proceedYes": "Proceed",
|
||||
"proceedNo": "Cancel",
|
||||
"spellDRDialogTitle": "Spell Damage — Apply DR?",
|
||||
"spellDRDialogMsg": "Enter a damage reduction value to subtract, or click No DR to apply full damage.",
|
||||
"spellDRLabel": "DR:",
|
||||
"spellNoDR": "No DR",
|
||||
"spellApplyDR": "Apply DR",
|
||||
"chooseWeapon": "Choose a weapon for your reaction",
|
||||
"chooseSave": "Choose a save type",
|
||||
"chooseBonusDie": "Select a bonus die",
|
||||
"attackRoll": "Attack roll",
|
||||
"currentDefense": "Current defense",
|
||||
"shieldDice": "Shield dice",
|
||||
"shieldDR": "Shield DR",
|
||||
"attacker": "Attacker",
|
||||
"defender": "Defender"
|
||||
},
|
||||
"D30": {
|
||||
"chooseEffect": "Choose a D30 special effect to apply"
|
||||
},
|
||||
"EquipmentCategories": {
|
||||
"ClassKit": "Class Kit",
|
||||
"Clothing": "Clothing",
|
||||
|
||||
+107
-822
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { SYSTEM } from "./module/config/system.mjs"
|
||||
globalThis.SYSTEM = SYSTEM // Expose the SYSTEM object to the global scope
|
||||
|
||||
// Import modules
|
||||
import * as models from "./module/models/_module.mjs"
|
||||
@@ -14,15 +13,27 @@ import * as applications from "./module/applications/_module.mjs"
|
||||
import { LethalFantasyCombatTracker, LethalFantasyCombat } from "./module/applications/combat.mjs"
|
||||
import { Macros } from "./module/macros.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) } } }
|
||||
// Import chat reaction hooks (renderChatMessageHTML, preCreateChatMessage, defense/attack reactions, resource costing, auto-damage)
|
||||
import "./module/hooks/chat-reaction.mjs"
|
||||
|
||||
Hooks.once("init", function () {
|
||||
globalThis.SYSTEM = SYSTEM
|
||||
globalThis.pendingDefenses = new Map()
|
||||
console.info("Lethal Fantasy RPG | Initializing System")
|
||||
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.log = log
|
||||
game.system.CONST = SYSTEM
|
||||
|
||||
// Expose the system API
|
||||
@@ -54,12 +65,10 @@ Hooks.once("init", function () {
|
||||
miracle: models.LethalFantasyMiracle
|
||||
}
|
||||
|
||||
// Register sheet application classes
|
||||
foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet)
|
||||
// Register sheet application classes (V2)
|
||||
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.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.LethalFantasyGiftSheet, { types: ["gift"], makeDefault: true })
|
||||
foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasyVulnerabilitySheet, { types: ["vulnerability"], makeDefault: true })
|
||||
@@ -76,14 +85,6 @@ Hooks.once("init", function () {
|
||||
// Dice system configuration
|
||||
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
|
||||
game.socket.on(`system.${SYSTEM.id}`, LethalFantasyUtils.handleSocketEvent)
|
||||
|
||||
@@ -107,15 +108,22 @@ function preLocalizeConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
Hooks.once("ready", function () {
|
||||
Hooks.once("ready", async function () {
|
||||
console.info("LETHAL FANTASY | Ready")
|
||||
|
||||
// Initialiser la table des résultats D30
|
||||
documents.D30Roll.initialize()
|
||||
await documents.D30Roll.initialize()
|
||||
|
||||
if (!SYSTEM.DEV_MODE) {
|
||||
registerWorldCount("lethalFantasy")
|
||||
}
|
||||
// Register Handlebars partials used by chat templates.
|
||||
// The object key is the partial ID referenced in templates (e.g., {{> chat/dice-breakdown}}),
|
||||
// the value is the file path. Using an array would register with the full path as the ID,
|
||||
// which does NOT match the template reference.
|
||||
await foundry.applications.handlebars.loadTemplates({
|
||||
"chat/dice-breakdown": "systems/fvtt-lethal-fantasy/templates/chat/_dice-breakdown.hbs"
|
||||
})
|
||||
|
||||
// Saignement piloté par le combat tracker
|
||||
_registerBleedingHooks()
|
||||
|
||||
_showUserGuide()
|
||||
|
||||
@@ -129,805 +137,96 @@ Hooks.once("ready", function () {
|
||||
}
|
||||
})
|
||||
|
||||
// Test if version below 13
|
||||
let hookName = "renderChatMessage"
|
||||
if (foundry.utils.isNewerVersion(game.version, "12.0",)) {
|
||||
hookName = "renderChatMessageHTML"
|
||||
/**
|
||||
* 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(", "),
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
Hooks.on(hookName, (message, html, data) => {
|
||||
const typeMessage = data.message.flags.lethalFantasy?.typeMessage
|
||||
// Message de demande de jet de dés
|
||||
if (typeMessage === "askRoll") {
|
||||
// Affichage des boutons de jet de dés uniquement pour les joueurs
|
||||
if (game.user.isGM) {
|
||||
html.find(".ask-roll-dice").each((i, btn) => {
|
||||
btn.style.display = "none"
|
||||
})
|
||||
} else {
|
||||
html.find(".ask-roll-dice").click((event) => {
|
||||
const btn = $(event.currentTarget)
|
||||
const type = btn.data("type")
|
||||
const value = btn.data("value")
|
||||
const avantage = btn.data("avantage") ?? "="
|
||||
const character = game.user.character
|
||||
if (type === SYSTEM.ROLL_TYPE.RESOURCE) character.rollResource(value)
|
||||
else if (type === SYSTEM.ROLL_TYPE.SAVE) character.rollSave(value, avantage)
|
||||
})
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
// Gestion du survol et du clic sur les boutons de dégâts pour les GMs
|
||||
if (game.user.isGM) {
|
||||
// Show damage buttons only for GM
|
||||
$(html).find(".li-apply-wounds").each((i, btn) => {
|
||||
btn.style.display = "block"
|
||||
})
|
||||
|
||||
$(html).find(".apply-wounds-btn").hover(
|
||||
function (event) {
|
||||
// Mouse enter - select the token and pan to it
|
||||
let combatantId = $(this).data("combatant-id")
|
||||
if (combatantId && game.combat) {
|
||||
let combatant = game.combat.combatants.get(combatantId)
|
||||
if (combatant?.token) {
|
||||
let token = canvas.tokens.get(combatant.token.id)
|
||||
if (token) {
|
||||
token.control({ releaseOthers: true })
|
||||
canvas.animatePan(token.center)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
function (event) {
|
||||
// Mouse leave - release selection
|
||||
canvas.tokens.releaseAll()
|
||||
}
|
||||
)
|
||||
|
||||
$(html).find(".apply-wounds-btn").click((event) => {
|
||||
LethalFantasyUtils.applyDamage(message, event)
|
||||
})
|
||||
}
|
||||
|
||||
// Gestion du survol et du clic sur les boutons de défense
|
||||
$(html).find(".request-defense-btn").hover(
|
||||
function (event) {
|
||||
// Mouse enter - select the token and pan to it
|
||||
let tokenId = $(this).data("token-id")
|
||||
if (tokenId) {
|
||||
let token = canvas.tokens.get(tokenId)
|
||||
if (token) {
|
||||
token.control({ releaseOthers: true })
|
||||
canvas.animatePan(token.center)
|
||||
}
|
||||
}
|
||||
},
|
||||
function (event) {
|
||||
// Mouse leave - release selection
|
||||
canvas.tokens.releaseAll()
|
||||
}
|
||||
)
|
||||
|
||||
// Gestionnaire pour les boutons de demande de défense
|
||||
$(html).find(".request-defense-btn").off("click").on("click", (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const button = $(event.currentTarget)
|
||||
const combatantId = button.data("combatant-id")
|
||||
const tokenId = button.data("token-id")
|
||||
|
||||
// Récupérer le combattant soit du combat, soit directement du token
|
||||
let combatant = null
|
||||
let token = null
|
||||
|
||||
if (game.combat && combatantId) {
|
||||
combatant = game.combat.combatants.get(combatantId)
|
||||
}
|
||||
|
||||
// Si pas de combattant trouvé, chercher le token directement
|
||||
if (!combatant && tokenId) {
|
||||
token = canvas.tokens.get(tokenId)
|
||||
if (token) {
|
||||
// Créer un pseudo-combattant avec les infos du token
|
||||
combatant = {
|
||||
actor: token.actor,
|
||||
name: token.name,
|
||||
token: token,
|
||||
actorId: token.actorId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!combatant) return
|
||||
|
||||
// Récupérer les informations de l'attaquant depuis le message
|
||||
const attackerName = message.rolls[0]?.actorName || "Unknown"
|
||||
const attackerId = message.rolls[0]?.actorId
|
||||
const weaponName = message.rolls[0]?.rollName || "weapon"
|
||||
const attackRoll = message.rolls[0]?.rollTotal || 0
|
||||
const defenderName = combatant.name
|
||||
const attackWeaponId = message.rolls[0]?.rollTarget?.weapon?.id || message.rolls[0]?.rollTarget?.weapon?._id
|
||||
const attackRollType = message.rolls[0]?.type
|
||||
const attackRollKey = message.rolls[0]?.rollTarget?.rollKey
|
||||
console.log(`[LF] request-defense-btn | attackRollType=${attackRollType} defender=${defenderName} defenderType=${combatant.actor?.type}`)
|
||||
const attackD30result = message.rolls[0]?.options?.D30result || null
|
||||
const attackD30message = message.rolls[0]?.options?.D30message || null
|
||||
const attackRerollContext = {
|
||||
rollType: message.rolls[0]?.options?.rollType,
|
||||
rollTarget: foundry.utils.duplicate(message.rolls[0]?.options?.rollTarget ?? {}),
|
||||
actorId: message.rolls[0]?.options?.actorId,
|
||||
actorName: message.rolls[0]?.options?.actorName,
|
||||
actorImage: message.rolls[0]?.options?.actorImage,
|
||||
defenderId: combatant.actor?.id || null,
|
||||
defenderTokenId: tokenId || combatant.token?.id || null,
|
||||
rollContext: foundry.utils.duplicate(message.rolls[0]?.options?.rollData ?? {})
|
||||
}
|
||||
|
||||
// Préparer le message de demande de défense
|
||||
// Récupérer l'acteur attaquant pour vérifier le mode d'attaque
|
||||
const attacker = game.actors.get(attackerId)
|
||||
const isRangedAttack = attacker?.type === "monster" && attacker.system.attackMode === "ranged"
|
||||
|
||||
const defenseMsg = {
|
||||
type: "requestDefense",
|
||||
attackerName: attackerName,
|
||||
attackerId: attackerId,
|
||||
defenderName: defenderName,
|
||||
weaponName: weaponName,
|
||||
attackRoll: attackRoll,
|
||||
attackWeaponId: attackWeaponId,
|
||||
attackRollType: attackRollType,
|
||||
attackRollKey: attackRollKey,
|
||||
attackD30result: attackD30result,
|
||||
attackD30message: attackD30message,
|
||||
attackRerollContext: attackRerollContext,
|
||||
combatantId: combatantId,
|
||||
tokenId: tokenId,
|
||||
isRanged: isRangedAttack
|
||||
}
|
||||
|
||||
// Envoyer le message socket à l'utilisateur contrôlant le combatant
|
||||
// Only consider active (online) users; fall back to any active GM for unowned/GM monsters.
|
||||
let owners = game.users.filter(u =>
|
||||
u.active && combatant.actor.testUserPermission(u, "OWNER")
|
||||
)
|
||||
if (owners.length === 0) {
|
||||
owners = game.users.filter(u => u.active && u.isGM)
|
||||
}
|
||||
|
||||
// Récupérer l'acteur attaquant pour vérifier qui l'a lancé
|
||||
const attackerOwners = attacker ? game.users.filter(u => attacker.testUserPermission(u, "OWNER")).map(u => u.id) : []
|
||||
|
||||
// Monsters always need their owner (usually the GM) to roll a save/defense,
|
||||
// even if that owner also controls the attacker. Only skip for same-player PC-vs-PC.
|
||||
const defenderIsMonster = combatant.actor?.type === "monster"
|
||||
|
||||
let messageSent = false
|
||||
owners.forEach(owner => {
|
||||
// Don't let a player be both attacker and defender for their own PC, unless defending a monster.
|
||||
if (attackerOwners.includes(owner.id) && owner.id === game.user.id && !defenderIsMonster) {
|
||||
// Ne rien faire - on ne veut pas que l'attaquant se défende contre lui-même
|
||||
return
|
||||
}
|
||||
|
||||
if (owner.id === game.user.id) {
|
||||
// Si l'utilisateur actuel est le propriétaire du défenseur (mais pas l'attaquant), appeler directement
|
||||
LethalFantasyUtils.showDefenseRequest({ ...defenseMsg, userId: owner.id })
|
||||
messageSent = true
|
||||
} else {
|
||||
// Sinon, envoyer via socket
|
||||
game.socket.emit(`system.${SYSTEM.id}`, {
|
||||
...defenseMsg,
|
||||
userId: owner.id
|
||||
})
|
||||
messageSent = true
|
||||
}
|
||||
})
|
||||
|
||||
// Notification pour l'attaquant
|
||||
if (messageSent) {
|
||||
ui.notifications.info(`Defense request sent to ${defenderName}'s controller`)
|
||||
}
|
||||
if (!changed) return
|
||||
const currentHp = actor.system.hp.value ?? 0
|
||||
await actor.update({
|
||||
"system.hp.value": currentHp - hpLoss,
|
||||
"system.hp.wounds": wounds,
|
||||
})
|
||||
}
|
||||
|
||||
// Gestionnaire pour les boutons de jet de dégâts (armes et résultats de combat)
|
||||
$(html).find(".damage-roll-btn, .roll-damage-btn").off("click").on("click", async (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const button = $(event.currentTarget)
|
||||
const weaponId = button.data("weapon-id")
|
||||
const attackKey = button.data("attack-key")
|
||||
let attackerId = button.data("attacker-id")
|
||||
const defenderId = button.data("defender-id")
|
||||
const defenderTokenId = button.data("defender-token-id") || null
|
||||
const extraShieldDr = Number(button.data("extra-shield-dr") || 0)
|
||||
const damageType = button.data("damage-type")
|
||||
const damageFormula = button.data("damage-formula")
|
||||
const damageModifier = button.data("damage-modifier")
|
||||
const isMonster = button.data("is-monster")
|
||||
|
||||
// Récupérer l'acteur (soit depuis le message, soit depuis attackerId)
|
||||
let actor = attackerId ? game.actors.get(attackerId) : game.actors.get(message.rolls[0]?.actorId)
|
||||
if (!actor) {
|
||||
ui.notifications.error("Actor not found")
|
||||
return
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
// Pour les sorts, rouler les dés de dégâts avec option bypass DR
|
||||
if (damageType === "spell" && damageFormula) {
|
||||
const bypassArmor = await foundry.applications.api.DialogV2.confirm({
|
||||
window: { title: "Spell Damage" },
|
||||
classes: ["lethalfantasy"],
|
||||
content: "<p>Does this spell's damage bypass armor DR?</p>",
|
||||
yes: { label: "Yes (ignore armor)", icon: "fa-solid fa-wand-magic-sparkles" },
|
||||
no: { label: "No (apply armor DR)", icon: "fa-solid fa-shield" }
|
||||
})
|
||||
const rollOpts = {
|
||||
type: "spell-damage",
|
||||
rollType: "spell-damage",
|
||||
rollName: damageFormula,
|
||||
isDamage: true,
|
||||
rollData: { isDamage: true },
|
||||
bypassArmor: bypassArmor ?? false,
|
||||
defenderId,
|
||||
defenderTokenId,
|
||||
actorId: actor.id,
|
||||
actorName: actor.name,
|
||||
actorImage: actor.img
|
||||
}
|
||||
const roll = new LethalFantasyRoll(damageFormula, {}, rollOpts)
|
||||
await roll.evaluate()
|
||||
roll.options.rollTotal = roll.total
|
||||
if (game?.dice3d) await game.dice3d.showForRoll(roll, game.user, true)
|
||||
await roll.toMessage()
|
||||
return
|
||||
}
|
||||
|
||||
// Pour les boutons de résultat de combat (monster damage)
|
||||
if (damageType === "monster" && attackKey) {
|
||||
await actor.system.prepareMonsterRoll("monster-damage", attackKey, undefined, undefined, undefined, defenderId, defenderTokenId, extraShieldDr)
|
||||
return
|
||||
}
|
||||
|
||||
// Pour les monstres, utiliser prepareMonsterRoll
|
||||
if (isMonster || actor.type === "monster") {
|
||||
await actor.system.prepareMonsterRoll("monster-damage", weaponId, undefined, undefined, damageModifier)
|
||||
return
|
||||
}
|
||||
|
||||
// Pour les personnages, récupérer l'arme
|
||||
const weapon = actor.items.get(weaponId)
|
||||
if (!weapon) {
|
||||
ui.notifications.error("Weapon not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Lancer les dégâts avec la bonne méthode
|
||||
const rollType = damageType === "small" ? "weapon-damage-small" : "weapon-damage-medium"
|
||||
await actor.prepareRoll(rollType, weaponId, undefined, defenderId, defenderTokenId, extraShieldDr)
|
||||
})
|
||||
|
||||
// Masquer les boutons de dommages dans les messages de résultat de combat si l'utilisateur n'est pas l'attaquant
|
||||
$(html).find(".roll-damage-btn").each(function() {
|
||||
const button = $(this)
|
||||
const attackerId = button.data("attacker-id")
|
||||
|
||||
if (attackerId) {
|
||||
const attacker = game.actors.get(attackerId)
|
||||
// Masquer le bouton si l'utilisateur n'est pas GM et ne possède pas l'attaquant
|
||||
if (!game.user.isGM && !attacker?.testUserPermission(game.user, "OWNER")) {
|
||||
button.hide()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Hooks.on("getCombatTrackerEntryContext", (html, options) => {
|
||||
LethalFantasyUtils.pushCombatOptions(html, options);
|
||||
});
|
||||
|
||||
// Hook pour ajouter les données d'attaque au message de défense
|
||||
Hooks.on("preCreateChatMessage", (message) => {
|
||||
const rollType = message.rolls[0]?.options?.rollType
|
||||
|
||||
// Si c'est un message de défense et qu'on a des données en attente
|
||||
if ((rollType === "weapon-defense" || rollType === "monster-defense" || rollType === "save") && game.lethalFantasy?.nextDefenseData) {
|
||||
// Ajouter les données dans les flags du message
|
||||
message.updateSource({
|
||||
[`flags.${SYSTEM.id}.attackData`]: game.lethalFantasy.nextDefenseData
|
||||
})
|
||||
|
||||
console.log("Added attack data to defense message:", game.lethalFantasy.nextDefenseData)
|
||||
|
||||
// Nettoyer
|
||||
delete game.lethalFantasy.nextDefenseData
|
||||
}
|
||||
})
|
||||
|
||||
// Hook global pour gérer l'offre de Grit à l'attaquant après une défense
|
||||
Hooks.on("createChatMessage", async (message) => {
|
||||
const rollType = message.rolls[0]?.options?.rollType
|
||||
|
||||
console.log("Defense hook checking message, rollType:", rollType)
|
||||
|
||||
// Vérifier si c'est un message de défense
|
||||
if (rollType !== "weapon-defense" && rollType !== "monster-defense" && rollType !== "save") return
|
||||
|
||||
// Récupérer les données d'attaque depuis les flags
|
||||
const attackData = message.flags?.[SYSTEM.id]?.attackData
|
||||
|
||||
console.log("Defense message confirmed, attackData:", attackData)
|
||||
|
||||
if (!attackData) {
|
||||
console.log("No attack data found in message flags")
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
attackerId,
|
||||
attackRoll,
|
||||
attackerName,
|
||||
defenderName,
|
||||
attackWeaponId,
|
||||
attackRollType,
|
||||
attackRollKey,
|
||||
attackD30message,
|
||||
attackRerollContext,
|
||||
defenderId,
|
||||
defenderTokenId
|
||||
} = attackData
|
||||
let defenseRoll = message.rolls[0]?.options?.rollTotal || message.rolls[0]?.total || 0
|
||||
const defenseD30message = message.rolls[0]?.options?.D30message || null
|
||||
|
||||
console.log("Processing defense:", { attackRoll, defenseRoll, attackerId, defenderId })
|
||||
|
||||
// Attendre l'animation 3D
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.waitFor3DAnimationByMessageID(message.id)
|
||||
}
|
||||
|
||||
// Récupérer le défenseur et l'attaquant
|
||||
const defender = game.actors.get(defenderId)
|
||||
const attacker = game.actors.get(attackerId)
|
||||
const defenseRerollContext = {
|
||||
rollType: message.rolls[0]?.options?.rollType,
|
||||
rollTarget: foundry.utils.duplicate(message.rolls[0]?.options?.rollTarget ?? {}),
|
||||
actorId: message.rolls[0]?.options?.actorId,
|
||||
actorName: message.rolls[0]?.options?.actorName,
|
||||
actorImage: message.rolls[0]?.options?.actorImage,
|
||||
defenderId,
|
||||
defenderTokenId,
|
||||
rollContext: foundry.utils.duplicate(message.rolls[0]?.options?.rollData ?? {})
|
||||
}
|
||||
|
||||
const isPrimaryController = actor => {
|
||||
if (!actor) return false
|
||||
const activePlayerOwners = game.users.filter(u => u.active && !u.isGM && actor.testUserPermission(u, "OWNER"))
|
||||
if (activePlayerOwners.length > 0) {
|
||||
return activePlayerOwners[0].id === game.user.id
|
||||
}
|
||||
return game.user.isGM
|
||||
}
|
||||
|
||||
const createReactionMessage = async (actorDocument, content) => {
|
||||
await ChatMessage.create({
|
||||
content,
|
||||
speaker: ChatMessage.getSpeaker({ actor: actorDocument })
|
||||
})
|
||||
}
|
||||
|
||||
// Si le défenseur est un personnage qui perd, proposer Grit/Luck (seulement s'il a des points)
|
||||
// Seulement si l'utilisateur actuel est le propriétaire du défenseur
|
||||
let defenderHandledBonus = false
|
||||
let shieldReaction = null
|
||||
let shieldBlocked = false
|
||||
const isSpellOrMiracle = attackRollType === "spell-attack" || attackRollType === "miracle-attack"
|
||||
if (defender && defenseRoll < attackRoll && isPrimaryController(defender) && !isSpellOrMiracle) {
|
||||
const shieldData = LethalFantasyUtils.getShieldReactionData(defender)
|
||||
let canRerollDefense = LethalFantasyUtils.hasD30Reroll(defenseD30message)
|
||||
let canShieldReact = !!shieldData
|
||||
let canAdHocShield = !shieldData
|
||||
|
||||
while (defenseRoll < attackRoll) {
|
||||
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",
|
||||
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
||||
icon: "fa-solid fa-fist-raised",
|
||||
callback: () => "grit"
|
||||
})
|
||||
}
|
||||
|
||||
if (currentLuck > 0) {
|
||||
buttons.push({
|
||||
action: "luck",
|
||||
label: `Spend 1 Luck (+1D6) [${currentLuck} left]`,
|
||||
icon: "fa-solid fa-clover",
|
||||
callback: () => "luck"
|
||||
})
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
action: "bonusDie",
|
||||
label: "Add bonus die",
|
||||
icon: "fa-solid fa-dice",
|
||||
callback: () => "bonusDie"
|
||||
})
|
||||
|
||||
if (canRerollDefense) {
|
||||
buttons.push({
|
||||
action: "rerollDefense",
|
||||
label: "Re-roll defense",
|
||||
icon: "fa-solid fa-rotate-right",
|
||||
callback: () => "rerollDefense"
|
||||
})
|
||||
}
|
||||
|
||||
if (canShieldReact) {
|
||||
buttons.push({
|
||||
action: "shieldReact",
|
||||
label: `Roll shield (${shieldData.label})`,
|
||||
icon: "fa-solid fa-shield",
|
||||
callback: () => "shieldReact"
|
||||
})
|
||||
} else if (canAdHocShield) {
|
||||
// No pre-configured shield — offer ad-hoc shield option (useful for monsters)
|
||||
buttons.push({
|
||||
action: "adHocShield",
|
||||
label: "Roll ad-hoc shield (choose dice + DR)",
|
||||
icon: "fa-solid fa-shield-halved",
|
||||
callback: () => "adHocShield"
|
||||
})
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
action: "continue",
|
||||
label: "Continue (no defense bonus)",
|
||||
icon: "fa-solid fa-forward",
|
||||
callback: () => "continue"
|
||||
})
|
||||
|
||||
const choice = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Defense reactions" },
|
||||
classes: ["lethalfantasy"],
|
||||
content: `
|
||||
<div class="grit-luck-dialog">
|
||||
<div class="combat-status">
|
||||
<p><strong>${attackerName}</strong> rolled <strong>${attackRoll}</strong></p>
|
||||
<p><strong>${defenderName}</strong> currently has <strong>${defenseRoll}</strong></p>
|
||||
${defenseD30message ? `<p class="bonus-info">D30 special: ${defenseD30message.description}</p>` : ""}
|
||||
</div>
|
||||
<p class="offer-text">Choose how to improve the defense before resolving the hit.</p>
|
||||
</div>
|
||||
`,
|
||||
buttons,
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
if (!choice || choice === "continue") break
|
||||
|
||||
defenderHandledBonus = true
|
||||
|
||||
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>`)
|
||||
defenseRoll += bonusRoll
|
||||
await defender.update({ "system.grit.current": currentGrit - 1 })
|
||||
continue
|
||||
}
|
||||
|
||||
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>`)
|
||||
defenseRoll += bonusRoll
|
||||
await defender.update({ "system.luck.current": currentLuck - 1 })
|
||||
continue
|
||||
}
|
||||
|
||||
if (choice === "bonusDie") {
|
||||
const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(defenderName, "attack", defenseRoll, attackRoll)
|
||||
if (!bonusDie) continue
|
||||
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>`)
|
||||
defenseRoll += bonusRoll
|
||||
continue
|
||||
}
|
||||
|
||||
if (choice === "rerollDefense" && canRerollDefense) {
|
||||
const oldDefenseRoll = defenseRoll
|
||||
const reroll = await LethalFantasyUtils.rerollConfiguredRoll(defenseRerollContext)
|
||||
canRerollDefense = false
|
||||
if (!reroll) continue
|
||||
defenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
|
||||
await createReactionMessage(defender, `<p><strong>${defenderName}</strong> uses Mulligan and re-rolls defense: <strong>${oldDefenseRoll}</strong> → <strong>${defenseRoll}</strong>.</p>`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (choice === "shieldReact" && canShieldReact) {
|
||||
const shieldBonus = await LethalFantasyUtils.rollBonusDie(shieldData.formula, defender)
|
||||
const newDefenseTotal = defenseRoll + shieldBonus
|
||||
defenseRoll = newDefenseTotal
|
||||
canShieldReact = false
|
||||
|
||||
if (newDefenseTotal >= attackRoll) {
|
||||
shieldBlocked = true
|
||||
shieldReaction = {
|
||||
damageReduction: shieldData.damageReduction,
|
||||
label: shieldData.label,
|
||||
bonus: shieldBonus
|
||||
}
|
||||
await createReactionMessage(
|
||||
defender,
|
||||
`<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} ≥ ${attackRoll}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${shieldData.damageReduction}</strong> will apply to damage.</p>`
|
||||
)
|
||||
} else {
|
||||
shieldReaction = null
|
||||
await createReactionMessage(
|
||||
defender,
|
||||
`<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRoll}). Shield did not block — normal hit, armor DR only.</p>`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (choice === "adHocShield") {
|
||||
const adHoc = await LethalFantasyUtils.promptAdHocShield(defenderName, attackRoll, defenseRoll)
|
||||
if (!adHoc) continue
|
||||
const shieldBonus = await LethalFantasyUtils.rollBonusDie(adHoc.formula, defender)
|
||||
const newDefenseTotal = defenseRoll + shieldBonus
|
||||
defenseRoll = newDefenseTotal
|
||||
canShieldReact = false
|
||||
canAdHocShield = false
|
||||
|
||||
if (newDefenseTotal >= attackRoll) {
|
||||
shieldBlocked = true
|
||||
shieldReaction = {
|
||||
damageReduction: adHoc.damageReduction,
|
||||
label: `${adHoc.formula.toUpperCase()} shield`,
|
||||
bonus: shieldBonus
|
||||
}
|
||||
await createReactionMessage(
|
||||
defender,
|
||||
`<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} ≥ ${attackRoll}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${adHoc.damageReduction}</strong> will apply to damage.</p>`
|
||||
)
|
||||
} else {
|
||||
shieldReaction = null
|
||||
await createReactionMessage(
|
||||
defender,
|
||||
`<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRoll}). Shield did not block — normal hit, armor DR only.</p>`
|
||||
)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let attackRollFinal = attackRoll
|
||||
let attackerHandledBonus = false
|
||||
|
||||
// Si l'attaquant est un personnage qui perd et a du Grit
|
||||
// Seulement si l'utilisateur actuel est le propriétaire de l'attaquant (pas le MJ)
|
||||
if (!defenderHandledBonus && attacker && attackRollFinal <= defenseRoll && isPrimaryController(attacker)) {
|
||||
let canRerollAttack = LethalFantasyUtils.hasD30Reroll(attackD30message)
|
||||
|
||||
while (attackRollFinal <= defenseRoll) {
|
||||
const currentGrit = Number(attacker.system?.grit?.current) || 0
|
||||
const buttons = []
|
||||
|
||||
if (currentGrit > 0) {
|
||||
buttons.push({
|
||||
action: "grit",
|
||||
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
||||
icon: "fa-solid fa-fist-raised",
|
||||
callback: () => "grit"
|
||||
})
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
action: "bonusDie",
|
||||
label: "Add bonus die",
|
||||
icon: "fa-solid fa-dice",
|
||||
callback: () => "bonusDie"
|
||||
})
|
||||
|
||||
if (canRerollAttack && attackRerollContext) {
|
||||
buttons.push({
|
||||
action: "rerollAttack",
|
||||
label: "Re-roll attack",
|
||||
icon: "fa-solid fa-rotate-right",
|
||||
callback: () => "rerollAttack"
|
||||
})
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
action: "continue",
|
||||
label: "Continue (no attack bonus)",
|
||||
icon: "fa-solid fa-forward",
|
||||
callback: () => "continue"
|
||||
})
|
||||
|
||||
const choice = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Attack reactions" },
|
||||
classes: ["lethalfantasy"],
|
||||
content: `
|
||||
<div class="grit-luck-dialog">
|
||||
<div class="combat-status">
|
||||
<p><strong>${attackerName}</strong> currently has <strong>${attackRollFinal}</strong></p>
|
||||
<p><strong>${defenderName}</strong> rolled <strong>${defenseRoll}</strong></p>
|
||||
${attackD30message ? `<p class="bonus-info">D30 special: ${attackD30message.description}</p>` : ""}
|
||||
</div>
|
||||
<p class="offer-text">Choose how to improve the attack before resolving the combat result.</p>
|
||||
</div>
|
||||
`,
|
||||
buttons,
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
if (!choice || choice === "continue") break
|
||||
|
||||
attackerHandledBonus = true
|
||||
|
||||
if (choice === "grit") {
|
||||
const attackBonus = await LethalFantasyUtils.rollBonusDie("1d6", attacker, (total) => `<p><strong>${attackerName}</strong> spends 1 Grit and rolls <strong>${total}</strong> for attack.</p>`)
|
||||
attackRollFinal += attackBonus
|
||||
await attacker.update({ "system.grit.current": currentGrit - 1 })
|
||||
continue
|
||||
}
|
||||
|
||||
if (choice === "bonusDie") {
|
||||
const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(attackerName, "defense", attackRollFinal, defenseRoll)
|
||||
if (!bonusDie) continue
|
||||
const attackBonus = await LethalFantasyUtils.rollBonusDie(bonusDie, attacker, (total, formula) => `<p><strong>${attackerName}</strong> adds <strong>${formula.toUpperCase()}</strong> and rolls <strong>${total}</strong> for attack.</p>`)
|
||||
attackRollFinal += attackBonus
|
||||
continue
|
||||
}
|
||||
|
||||
if (choice === "rerollAttack" && canRerollAttack && attackRerollContext) {
|
||||
const oldAttackRoll = attackRollFinal
|
||||
const reroll = await LethalFantasyUtils.rerollConfiguredRoll(attackRerollContext)
|
||||
canRerollAttack = false
|
||||
if (!reroll) continue
|
||||
attackRollFinal = reroll.options?.rollTotal || reroll.total || oldAttackRoll
|
||||
await createReactionMessage(attacker, `<p><strong>${attackerName}</strong> uses Mulligan and re-rolls attack: <strong>${oldAttackRoll}</strong> → <strong>${attackRollFinal}</strong>.</p>`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shieldDamageReduction = shieldBlocked ? shieldReaction.damageReduction : 0
|
||||
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
|
||||
// Priorité: attaquant si il a géré le bonus, sinon défenseur si il a géré le bonus, sinon défenseur
|
||||
const shouldCreateMessage = attackerHandledBonus || (!attackerHandledBonus && defenderHandledBonus) || (!attackerHandledBonus && !defenderHandledBonus && isPrimaryController(defender))
|
||||
|
||||
if (shouldCreateMessage) {
|
||||
console.log("Creating comparison message", { attackerHandledBonus, defenderHandledBonus, isDefenderOwner: defender.isOwner })
|
||||
|
||||
await LethalFantasyUtils.compareAttackDefense({
|
||||
attackerName,
|
||||
attackerId,
|
||||
attackRoll: attackRollFinal,
|
||||
attackWeaponId,
|
||||
attackRollType,
|
||||
attackRollKey,
|
||||
defenderName,
|
||||
defenderId,
|
||||
defenderTokenId,
|
||||
defenseRoll,
|
||||
outcome,
|
||||
shieldDamageReduction
|
||||
})
|
||||
} else {
|
||||
console.log("Skipping message creation", { attackerHandledBonus, defenderHandledBonus })
|
||||
}
|
||||
})
|
||||
|
||||
// Hook pour appliquer automatiquement les dégâts si une cible est définie
|
||||
Hooks.on("createChatMessage", async (message) => {
|
||||
// Vérifier si c'est un message de dégâts avec un defenderId
|
||||
const defenderId = message.rolls[0]?.options?.defenderId
|
||||
const isDamage = message.rolls[0]?.options?.rollData?.isDamage
|
||||
|
||||
console.log("Auto-damage hook:", { defenderId, isDamage, rollType: message.rolls[0]?.options?.rollType })
|
||||
|
||||
if (!defenderId || !isDamage) return
|
||||
|
||||
// Récupérer l'attaquant depuis le roll
|
||||
const attackerId = message.rolls[0]?.options?.actorId
|
||||
const attacker = attackerId ? game.actors.get(attackerId) : null
|
||||
|
||||
// Déterminer qui doit appliquer les dégâts :
|
||||
// 1. Si l'attaquant a un propriétaire joueur, seul ce joueur applique
|
||||
// 2. Si l'attaquant n'a que le MJ comme propriétaire (monstre), seul le MJ applique
|
||||
const attackerOwners = attacker ? game.users.filter(u =>
|
||||
u.active && !u.isGM && attacker.testUserPermission(u, "OWNER")
|
||||
) : []
|
||||
|
||||
let shouldApplyDamage = false
|
||||
if (attackerOwners.length > 0) {
|
||||
// L'attaquant a des propriétaires joueurs, seul le premier propriétaire applique
|
||||
shouldApplyDamage = attackerOwners[0].id === game.user.id
|
||||
} else {
|
||||
// L'attaquant n'a que le MJ, seul le MJ applique
|
||||
shouldApplyDamage = game.user.isGM
|
||||
}
|
||||
|
||||
if (!shouldApplyDamage) {
|
||||
console.log("Auto-damage hook: Not responsible for applying damage, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Auto-damage hook: Applying damage as responsible user")
|
||||
|
||||
// Attendre l'animation 3D avant d'appliquer les dégâts
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.waitFor3DAnimationByMessageID(message.id)
|
||||
}
|
||||
|
||||
// Récupérer le défenseur
|
||||
const defender = game.actors.get(defenderId)
|
||||
if (!defender) {
|
||||
console.warn("Defender not found:", defenderId)
|
||||
return
|
||||
}
|
||||
|
||||
// Récupérer les dégâts (utiliser rollTotal qui contient le total calculé)
|
||||
const damageTotal = message.rolls[0]?.options?.rollTotal || message.rolls[0]?.total || 0
|
||||
const weaponName = message.rolls[0]?.options?.rollName || "Unknown Weapon"
|
||||
const attackerName = message.rolls[0]?.options?.actorName || "Unknown Attacker"
|
||||
|
||||
// Calculer les DR
|
||||
const bypassArmor = message.rolls[0]?.options?.bypassArmor || false
|
||||
const armorDR = bypassArmor ? 0 : (defender.computeDamageReduction() || 0)
|
||||
const extraShieldDr = Number(message.rolls[0]?.options?.extraShieldDr) || 0
|
||||
const totalDR = armorDR + extraShieldDr
|
||||
const finalDamage = Math.max(0, damageTotal - totalDR)
|
||||
|
||||
// Prefer the token ID stored in roll options (set at attack time when the exact token is known).
|
||||
// For unlinked tokens (default for monsters), this ensures we target the right instance even
|
||||
// when multiple unlinked copies of the same monster type are in combat.
|
||||
const rollDefenderTokenId = message.rolls[0]?.options?.defenderTokenId
|
||||
const defenderCombatant = game.combat?.combatants?.find(c => c.actorId === defender.id)
|
||||
const defenderTokenId = rollDefenderTokenId
|
||||
?? defenderCombatant?.token?.id
|
||||
?? canvas.tokens?.placeables?.find(t => t.actor?.id === defender.id)?.id
|
||||
?? null
|
||||
|
||||
// Apply damage. If the current user does not own the defender (e.g. player hitting a GM monster),
|
||||
// route the HP update to the GM via socket. The confirmation message is still created here
|
||||
// since all users can create chat messages.
|
||||
if (defender.isOwner) {
|
||||
// Resolve the token actor: prefer lookup by token ID (exact match for unlinked monsters),
|
||||
// fall back to combatant actor, then base world actor.
|
||||
const tokenActor = (defenderTokenId
|
||||
? canvas.tokens?.placeables?.find(t => t.id === defenderTokenId)?.actor
|
||||
: defenderCombatant?.actor) ?? defender
|
||||
await tokenActor.applyDamage(-finalDamage)
|
||||
} else {
|
||||
game.socket.emit(`system.${SYSTEM.id}`, { type: "applyDamage", actorId: defender.id, tokenId: defenderTokenId, damage: -finalDamage })
|
||||
}
|
||||
|
||||
// Créer un message de confirmation (visible to GM only)
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/damage-applied-message.hbs",
|
||||
{
|
||||
targetName: defender.name,
|
||||
damage: finalDamage,
|
||||
drText: bypassArmor ? "Armor DR bypassed (spell)" : (totalDR > 0 ? `Armor DR: ${armorDR}${extraShieldDr > 0 ? ` + Shield DR: ${extraShieldDr}` : ""}` : ""),
|
||||
weaponName: weaponName,
|
||||
attackerName: attackerName,
|
||||
rawDamage: damageTotal
|
||||
}
|
||||
)
|
||||
|
||||
await ChatMessage.create({
|
||||
content: messageContent,
|
||||
speaker: ChatMessage.getSpeaker({ actor: defender }),
|
||||
whisper: ChatMessage.getWhisperRecipients("GM")
|
||||
})
|
||||
})
|
||||
|
||||
return actors
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a macro when dropping an entity on the hotbar
|
||||
@@ -947,17 +246,3 @@ Hooks.on("hotbarDrop", (bar, data, slot) => {
|
||||
* Inject the Lethal Fantasy dice tray into the chat sidebar.
|
||||
*/
|
||||
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 ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
import { log } from "../utils.mjs"
|
||||
|
||||
/* -------------------------------------------- */
|
||||
export class LethalFantasyCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
|
||||
@@ -18,62 +20,44 @@ export class LethalFantasyCombatTracker extends foundry.applications.sidebar.tab
|
||||
actions: {
|
||||
initiativePlus: LethalFantasyCombatTracker.#initiativePlus,
|
||||
initiativeMinus: LethalFantasyCombatTracker.#initiativeMinus,
|
||||
rollMonsterProgression: LethalFantasyCombatTracker.#rollMonsterProgression,
|
||||
},
|
||||
});
|
||||
|
||||
async _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) {
|
||||
let c = game.combat.combatants.get(u.id);
|
||||
u.progressionCount = c.system.progressionCount
|
||||
u.isMonster = c.actor.type === "monster"
|
||||
}
|
||||
console.log("Combat Data", data);*/
|
||||
log("Combat Data", data);*/
|
||||
return data;
|
||||
}
|
||||
|
||||
static #initiativePlus(ev) {
|
||||
static async #initiativePlus(ev) {
|
||||
ev.preventDefault();
|
||||
let cId = ev.target.closest(".combatant").dataset.combatantId;
|
||||
let c = game.combat.combatants.get(cId);
|
||||
c.update({ 'initiative': c.initiative + 1 });
|
||||
console.log("Initiative Plus");
|
||||
await c.update({ 'initiative': c.initiative + 1 });
|
||||
}
|
||||
|
||||
static #initiativeMinus(ev) {
|
||||
static async #initiativeMinus(ev) {
|
||||
ev.preventDefault();
|
||||
let cId = ev.target.closest(".combatant").dataset.combatantId;
|
||||
let c = game.combat.combatants.get(cId);
|
||||
let newInit = Math.max(c.initiative - 1, 0);
|
||||
c.update({ 'initiative': newInit });
|
||||
await c.update({ 'initiative': newInit });
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
/**
|
||||
* Roll progression dice for all monster combatants that are eligible this round.
|
||||
* @param {Event} ev Click event.
|
||||
*/
|
||||
static async #rollMonsterProgression(ev) {
|
||||
ev.preventDefault();
|
||||
await game.combat.rollMonsterProgression();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +68,7 @@ export class LethalFantasyCombat extends Combat {
|
||||
* @returns {Combatant[]}
|
||||
*/
|
||||
setupTurns() {
|
||||
console?.log("Setup Turns....");
|
||||
log("Setup Turns....");
|
||||
this.turns ||= [];
|
||||
|
||||
// Determine the turn order and the current turn
|
||||
@@ -109,18 +93,12 @@ export class LethalFantasyCombat extends Combat {
|
||||
}
|
||||
|
||||
async rollInitiative(ids, options) {
|
||||
console.log("%%%%%%%%% Roll Initiative", ids, options);
|
||||
|
||||
ids = typeof ids === "string" ? [ids] : ids;
|
||||
let messages = [];
|
||||
let rollMode = game.settings.get("core", "rollMode");
|
||||
|
||||
let updates = [];
|
||||
for (let cId of ids) {
|
||||
const c = this.combatants.get(cId);
|
||||
const playerOwner = game.users.find(u => u.active && !u.isGM && u.character?.id === c.actor.id);
|
||||
if (game.user.isGM && playerOwner) {
|
||||
console.log("Rolling initiative for", c.actor.name);
|
||||
game.socket.emit(`system.${SYSTEM.id}`, { type: "rollInitiative", userId: playerOwner.id, actorId: c.actor.id, combatId: this.id, combatantId: c.id });
|
||||
} else {
|
||||
await c.actor.system.rollInitiative(this.id, c.id);
|
||||
@@ -130,19 +108,49 @@ export class LethalFantasyCombat extends Combat {
|
||||
return this;
|
||||
}
|
||||
|
||||
resetProgression(cId) {
|
||||
let c = this.combatants.get(cId);
|
||||
c.update({ 'system.progressionCount': 0 });
|
||||
/** Roll progression dice for all eligible monster combatants this round. Called manually by the GM. */
|
||||
async rollMonsterProgression() {
|
||||
const currentRound = this.round;
|
||||
const monsters = this.combatants.filter(c => c.actor?.type === "monster" && !c.isDefeated);
|
||||
|
||||
if (monsters.length === 0) {
|
||||
ui.notifications.warn("No monsters in combat.");
|
||||
return;
|
||||
}
|
||||
|
||||
let rolled = 0;
|
||||
for (let c of monsters) {
|
||||
if (c.initiative !== null && currentRound >= c.initiative) {
|
||||
await c.actor.system.rollProgressionDice(this.id, c.id);
|
||||
rolled++;
|
||||
}
|
||||
}
|
||||
|
||||
if (rolled === 0) {
|
||||
const earliest = monsters.reduce((min, c) => (c.initiative !== null && c.initiative < min) ? c.initiative : min, Infinity);
|
||||
if (earliest === Infinity) {
|
||||
ui.notifications.warn("Monsters have no initiative set. Roll initiative first.");
|
||||
} else {
|
||||
ui.notifications.info(`No monsters act yet — earliest monster initiative is ${earliest} (current round: ${currentRound}).`);
|
||||
}
|
||||
} else {
|
||||
this._monsterProgressionRolledRound = currentRound;
|
||||
}
|
||||
}
|
||||
|
||||
setCasting(cId) {
|
||||
async resetProgression(cId) {
|
||||
let c = this.combatants.get(cId);
|
||||
c.setFlag(SYSTEM.id, "casting", true);
|
||||
await c.update({ 'system.progressionCount': 0 });
|
||||
}
|
||||
|
||||
resetCasting(cId) {
|
||||
async setCasting(cId) {
|
||||
let c = this.combatants.get(cId);
|
||||
c.setFlag(SYSTEM.id, "casting", false);
|
||||
await c.setFlag(SYSTEM.id, "casting", true);
|
||||
}
|
||||
|
||||
async resetCasting(cId) {
|
||||
let c = this.combatants.get(cId);
|
||||
await c.setFlag(SYSTEM.id, "casting", false);
|
||||
}
|
||||
|
||||
isCasting(cId) {
|
||||
@@ -151,15 +159,12 @@ export class LethalFantasyCombat extends Combat {
|
||||
}
|
||||
|
||||
async nextTurn() {
|
||||
console.log("NEXT TURN");
|
||||
|
||||
let turn = this.turn ?? -1;
|
||||
let skipDefeated = this.settings.skipDefeated;
|
||||
|
||||
// Determine the next turn number
|
||||
let next = null;
|
||||
for (let [i, t] of this.turns.entries()) {
|
||||
console.log("Turn", t);
|
||||
if (i <= turn) continue;
|
||||
if (skipDefeated && t.isDefeated) continue;
|
||||
next = i;
|
||||
@@ -183,7 +188,6 @@ export class LethalFantasyCombat extends Combat {
|
||||
this.turnsDone = false
|
||||
|
||||
let turn = this.turn === null ? null : 0; // Preserve the fact that it's no-one's turn currently.
|
||||
console.log("ROUND", this);
|
||||
|
||||
let advanceTime = Math.max(this.turns.length - this.turn, 0) * CONFIG.time.turnTime;
|
||||
advanceTime += CONFIG.time.roundTime;
|
||||
@@ -201,8 +205,25 @@ export class LethalFantasyCombat extends Combat {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Warn if eligible monsters have not rolled progression dice this round
|
||||
const eligibleMonsters = this.combatants.filter(
|
||||
c => c.actor?.type === "monster" && !c.isDefeated && c.initiative !== null && this.round >= c.initiative
|
||||
);
|
||||
if (eligibleMonsters.length > 0 && this._monsterProgressionRolledRound !== this.round) {
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/combat-monsters-not-rolled.hbs", {message: game.i18n.localize("LETHALFANTASY.Combat.monstersNotRolledMsg")})
|
||||
const proceed = await foundry.applications.api.DialogV2.confirm({
|
||||
window: { title: game.i18n.localize("LETHALFANTASY.Combat.monstersNotRolledTitle") },
|
||||
content,
|
||||
yes: { label: game.i18n.localize("LETHALFANTASY.Combat.proceedYes") },
|
||||
no: { label: game.i18n.localize("LETHALFANTASY.Combat.proceedNo") },
|
||||
rejectClose: false,
|
||||
});
|
||||
if (!proceed) return this;
|
||||
}
|
||||
|
||||
for (let c of this.combatants) {
|
||||
if (nextRound >= c.initiative) {
|
||||
if (c.actor.type === "monster") continue; // Monsters roll manually via the "Roll Monsters" button
|
||||
const playerOwner = game.users.find(u => u.active && !u.isGM && u.character?.id === c.actor.id);
|
||||
if (game.user.isGM && playerOwner) {
|
||||
game.socket.emit(`system.${SYSTEM.id}`, { type: "rollProgressionDice", userId: playerOwner.id, progressionCount: c.system.progressionCount + 1, actorId: c.actor.id, combatId: this.id, combatantId: c.id });
|
||||
|
||||
@@ -16,7 +16,7 @@ const DICE_TYPES = ["d4", "d6", "d8", "d10", "d12", "d20", "d30"]
|
||||
* @param {Application} _chatLog
|
||||
* @param {HTMLElement|jQuery} html
|
||||
*/
|
||||
export function injectDiceTray(_chatLog, html) {
|
||||
export async function injectDiceTray(_chatLog, html) {
|
||||
const el = (html instanceof HTMLElement) ? html : (html[0] ?? html)
|
||||
if (!el?.querySelector) return
|
||||
if (el.querySelector(".lf-dice-tray")) return
|
||||
@@ -24,27 +24,14 @@ export function injectDiceTray(_chatLog, html) {
|
||||
const bar = document.createElement("div")
|
||||
bar.className = "lf-dice-tray"
|
||||
|
||||
const diceButtons = DICE_TYPES.map(d =>
|
||||
`<button type="button" class="lf-dt-die-btn" data-die="${d}" title="${d.toUpperCase()}">${d.toUpperCase()}</button>`
|
||||
).join("")
|
||||
|
||||
const countOptions = Array.from({ length: 9 }, (_, i) =>
|
||||
`<option value="${i + 1}">${i + 1}</option>`
|
||||
).join("")
|
||||
|
||||
bar.innerHTML = `
|
||||
<div class="lf-dt-row">
|
||||
<span class="lf-dt-label"><i class="fa-solid fa-dice"></i></span>
|
||||
<select class="lf-dt-count" title="${game.i18n.localize("LETHALFANTASY.DiceTray.CountTitle")}">
|
||||
${countOptions}
|
||||
</select>
|
||||
<div class="lf-dt-dice">${diceButtons}</div>
|
||||
<label class="lf-dt-explode-label" title="${game.i18n.localize("LETHALFANTASY.DiceTray.ExplodeTitle")}">
|
||||
<input type="checkbox" class="lf-dt-explode" />
|
||||
<i class="fa-solid fa-explosion"></i>
|
||||
</label>
|
||||
</div>
|
||||
`
|
||||
const diceButtons = DICE_TYPES.map(d => ({ value: d, label: d.toUpperCase() }))
|
||||
const countOptions = Array.from({ length: 9 }, (_, i) => i + 1)
|
||||
bar.innerHTML = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/ui/dice-tray.hbs", {
|
||||
countTitle: game.i18n.localize("LETHALFANTASY.DiceTray.CountTitle"),
|
||||
explodeTitle: game.i18n.localize("LETHALFANTASY.DiceTray.ExplodeTitle"),
|
||||
countOptions,
|
||||
diceButtons
|
||||
})
|
||||
|
||||
bar.addEventListener("click", async ev => {
|
||||
const btn = ev.target.closest(".lf-dt-die-btn")
|
||||
@@ -113,40 +100,29 @@ export async function rollFreeDie(dieType, count = 1, explode = false) {
|
||||
}
|
||||
}
|
||||
|
||||
const resultHtml = dieChips.map(chip => {
|
||||
const isMax = !chip.exploded && chip.value === sides
|
||||
const isMin = chip.value === 1
|
||||
const explodeIcon = chip.exploded ? `<i class="fa-solid fa-burst lf-dt-explode-icon"></i>` : ""
|
||||
const classes = ["lf-frc-die-chip", isMax ? "lf-frc-max" : "", isMin ? "lf-frc-min" : ""].filter(Boolean).join(" ")
|
||||
return `<div class="${classes}">
|
||||
<span class="lf-frc-die-type">${chip.label}</span>
|
||||
<span class="lf-frc-die-sep">→</span>
|
||||
<span class="lf-frc-die-val">${chip.value}${explodeIcon}</span>
|
||||
</div>`
|
||||
}).join("")
|
||||
const dieChipsWithClasses = dieChips.map(chip => ({
|
||||
...chip,
|
||||
classes: ["lf-frc-die-chip", !chip.exploded && chip.value === sides ? "lf-frc-max" : "", chip.value === 1 ? "lf-frc-min" : ""].filter(Boolean).join(" ")
|
||||
}))
|
||||
|
||||
const totalLabel = game.i18n.localize("LETHALFANTASY.Label.total").toUpperCase()
|
||||
const content = `
|
||||
<div class="lf-free-roll-card">
|
||||
<div class="lf-frc-header">
|
||||
<i class="fa-solid fa-dice"></i>
|
||||
<span class="lf-frc-title-text">${game.i18n.localize("LETHALFANTASY.DiceTray.ChatTitle")}</span>
|
||||
<span class="lf-frc-badge">${label}</span>
|
||||
</div>
|
||||
<div class="lf-frc-dice">${resultHtml}</div>
|
||||
<div class="lf-frc-total-bar">
|
||||
<span class="lf-frc-total-label">${totalLabel}</span>
|
||||
<span class="lf-frc-total-value">${total}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/free-roll-card.hbs", {
|
||||
titleText: game.i18n.localize("LETHALFANTASY.DiceTray.ChatTitle"),
|
||||
badge: label,
|
||||
dieChips: dieChipsWithClasses,
|
||||
totalLabel,
|
||||
total
|
||||
})
|
||||
|
||||
const rollMode = game.settings.get("core", "rollMode")
|
||||
// Normalize old-style rollMode keys (v12/v13) to new-style (v14), fallback to "public"
|
||||
const modeMap = { publicroll: "public", gmroll: "gm", blindroll: "blind", selfroll: "self" }
|
||||
const mode = modeMap[rollMode] ?? rollMode ?? "public"
|
||||
const msgData = {
|
||||
speaker: ChatMessage.getSpeaker(),
|
||||
content,
|
||||
sound: CONFIG.sounds.dice,
|
||||
mode,
|
||||
}
|
||||
ChatMessage.applyRollMode(msgData, rollMode)
|
||||
await ChatMessage.create(msgData)
|
||||
}
|
||||
|
||||
@@ -78,7 +78,6 @@ export default class LethalFantasyActorSheet extends HandlebarsApplicationMixin(
|
||||
/** @override */
|
||||
_onRender(context, options) {
|
||||
this.#dragDrop.forEach((d) => d.bind(this.element))
|
||||
// Add listeners to rollable elements
|
||||
const rollables = this.element.querySelectorAll(".rollable")
|
||||
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 current = foundry.utils.getProperty(this.document, attr)
|
||||
const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {}
|
||||
const fp = new FilePicker({
|
||||
const fp = new foundry.applications.ux.FilePicker.implementation({
|
||||
current,
|
||||
type: "image",
|
||||
redirectToRoot: img ? [img] : [],
|
||||
callback: (path) => {
|
||||
this.document.update({ [attr]: path })
|
||||
callback: async (path) => {
|
||||
await this.document.update({ [attr]: path })
|
||||
},
|
||||
top: this.position.top + 40,
|
||||
left: this.position.left + 10,
|
||||
@@ -261,7 +260,7 @@ export default class LethalFantasyActorSheet extends HandlebarsApplicationMixin(
|
||||
item = await fromUuid(uuid)
|
||||
if (!item) item = this.document.items.get(id)
|
||||
if (!item) return
|
||||
item.sheet.render(true)
|
||||
item.sheet.render({ force: true })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,8 +283,8 @@ export default class LethalFantasyActorSheet extends HandlebarsApplicationMixin(
|
||||
* @private
|
||||
* @static
|
||||
*/
|
||||
static #onCreateSpell(event, target) {
|
||||
const item = this.document.createEmbeddedDocuments("Item", [{ name: "Nouveau sortilège", type: "spell" }])
|
||||
static async #onCreateSpell(event, target) {
|
||||
await this.document.createEmbeddedDocuments("Item", [{ name: "Nouveau sortilège", type: "spell" }])
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
@@ -177,12 +177,12 @@ export default class LethalFantasyItemSheet extends HandlebarsApplicationMixin(f
|
||||
const attr = target.dataset.edit
|
||||
const current = foundry.utils.getProperty(this.document, attr)
|
||||
const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {}
|
||||
const fp = new FilePicker({
|
||||
const fp = new foundry.applications.ux.FilePicker.implementation({
|
||||
current,
|
||||
type: "image",
|
||||
redirectToRoot: img ? [img] : [],
|
||||
callback: (path) => {
|
||||
this.document.update({ [attr]: path })
|
||||
callback: async (path) => {
|
||||
await this.document.update({ [attr]: path })
|
||||
},
|
||||
top: this.position.top + 40,
|
||||
left: this.position.left + 10,
|
||||
|
||||
@@ -22,6 +22,7 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
|
||||
divinityPointsMinus: LethalFantasyCharacterSheet.#onDivinityPointsMinus,
|
||||
aetherPointsPlus: LethalFantasyCharacterSheet.#onAetherPointsPlus,
|
||||
aetherPointsMinus: LethalFantasyCharacterSheet.#onAetherPointsMinus,
|
||||
rollSpellDamage: LethalFantasyCharacterSheet.#onRollSpellDamage,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -70,10 +71,10 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
|
||||
biography: { id: "biography", group: "sheet", icon: "fa-solid fa-book", label: "LETHALFANTASY.Label.biography" },
|
||||
}
|
||||
if (this.actor.system.biodata.magicUser) {
|
||||
tabs.spells = { id: "spells", group: "sheet", icon: "fa-sharp-duotone fa-solid fa-wand-magic-sparkles", label: "LETHALFANTASY.Label.spells" }
|
||||
tabs.spells = { id: "spells", group: "sheet", icon: "fa-solid fa-wand-magic-sparkles", label: "LETHALFANTASY.Label.spells" }
|
||||
}
|
||||
if (this.actor.system.biodata.clericUser) {
|
||||
tabs.miracles = { id: "miracles", group: "sheet", icon: "fa-sharp-duotone fa-solid fa-hands-praying", label: "LETHALFANTASY.Label.miracles" }
|
||||
tabs.miracles = { id: "miracles", group: "sheet", icon: "fa-solid fa-hands-praying", label: "LETHALFANTASY.Label.miracles" }
|
||||
}
|
||||
for (const v of Object.values(tabs)) {
|
||||
v.active = this.tabGroups[v.group] === v.id
|
||||
@@ -172,61 +173,130 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
|
||||
})
|
||||
if (!roll) return null
|
||||
|
||||
await roll.toMessage({}, { rollMode: roll.options.rollMode })
|
||||
await roll.toMessage({}, { messageMode: roll.options.rollMode })
|
||||
}
|
||||
|
||||
static async #onRollInitiative(event, target) {
|
||||
await this.document.system.rollInitiative()
|
||||
}
|
||||
|
||||
static #onArmorHitPointsPlus(event, target) {
|
||||
static async #onArmorHitPointsPlus(event, target) {
|
||||
let armorHP = this.actor.system.combat.armorHitPoints
|
||||
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
|
||||
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
|
||||
points += 1
|
||||
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
|
||||
points -= 1
|
||||
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
|
||||
points += 1
|
||||
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
|
||||
points -= 1
|
||||
points = Math.max(points, 0)
|
||||
this.actor.update({ "system.aetherPoints.value": points })
|
||||
await this.actor.update({ "system.aetherPoints.value": points })
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles spell damage roll from the spell sheet tab.
|
||||
* Shows a DR dialog then rolls the appropriate damage formula.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onRollSpellDamage(event, target) {
|
||||
if (this.isEditMode) return
|
||||
const itemId = target.dataset.itemId
|
||||
const tier = target.dataset.damageTier
|
||||
const spell = this.actor.items.get(itemId)
|
||||
if (!spell) return
|
||||
|
||||
const formulaMap = {
|
||||
standard: spell.system.damageDice,
|
||||
overpowered: spell.system.damageDiceOverpowered,
|
||||
overpowered2: spell.system.damageDiceOverpowered2,
|
||||
}
|
||||
const formula = formulaMap[tier]
|
||||
if (!formula) return
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/spell-dr.hbs", {
|
||||
msg: game.i18n.localize("LETHALFANTASY.Combat.spellDRDialogMsg"),
|
||||
label: game.i18n.localize("LETHALFANTASY.Combat.spellDRLabel")
|
||||
})
|
||||
const manualDR = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: game.i18n.localize("LETHALFANTASY.Combat.spellDRDialogTitle") },
|
||||
classes: ["lethalfantasy"],
|
||||
position: { width: 320 },
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "noDR",
|
||||
type: "button",
|
||||
label: game.i18n.localize("LETHALFANTASY.Combat.spellNoDR"),
|
||||
icon: "fa-solid fa-wand-magic-sparkles",
|
||||
callback: () => 0
|
||||
},
|
||||
{
|
||||
action: "applyDR",
|
||||
type: "button",
|
||||
label: game.i18n.localize("LETHALFANTASY.Combat.spellApplyDR"),
|
||||
icon: "fa-solid fa-shield",
|
||||
callback: (event, button) => Number(button.form?.elements?.manualDr?.value) || 0
|
||||
},
|
||||
{
|
||||
action: "cancel",
|
||||
type: "button",
|
||||
label: game.i18n.localize("LETHALFANTASY.Combat.proceedNo"),
|
||||
callback: () => "cancel"
|
||||
}
|
||||
],
|
||||
rejectClose: false
|
||||
})
|
||||
if (manualDR === null || manualDR === "cancel") return
|
||||
|
||||
const rollOpts = {
|
||||
type: "spell-damage",
|
||||
rollType: "spell-damage",
|
||||
rollName: `${spell.name} — ${formula}`,
|
||||
isDamage: true,
|
||||
rollData: { isDamage: true },
|
||||
manualDR,
|
||||
actorId: this.actor.id,
|
||||
actorName: this.actor.name,
|
||||
actorImage: this.actor.img
|
||||
}
|
||||
await LethalFantasyRoll.rollSpellDamageToMessage(formula, rollOpts)
|
||||
}
|
||||
|
||||
static #onCreateEquipment(event, target) {
|
||||
}
|
||||
|
||||
_onRender(context, options) {
|
||||
// Inputs with class `item-quantity`
|
||||
const woundDescription = this.element.querySelectorAll('.wound-data')
|
||||
for (const input of woundDescription) {
|
||||
input.addEventListener("change", (e) => {
|
||||
input.addEventListener("change", async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
const newValue = e.currentTarget.value
|
||||
@@ -234,11 +304,11 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
|
||||
const fieldName = e.currentTarget.dataset.name
|
||||
let tab = foundry.utils.duplicate(this.actor.system.hp.wounds)
|
||||
tab[index][fieldName] = newValue
|
||||
console.log(tab, index, fieldName, newValue)
|
||||
this.actor.update({ "system.hp.wounds": tab });
|
||||
log(tab, index, fieldName, newValue)
|
||||
await this.actor.update({ "system.hp.wounds": tab });
|
||||
})
|
||||
}
|
||||
super._onRender();
|
||||
super._onRender(context, options);
|
||||
}
|
||||
|
||||
|
||||
@@ -259,9 +329,11 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
|
||||
|
||||
async _onRoll(event, target) {
|
||||
if (this.isEditMode) return
|
||||
const rollType = event.target.dataset.rollType
|
||||
let rollKey = event.target.dataset.rollKey;
|
||||
let rollDice = event.target.dataset?.rollDice;
|
||||
const el = event.currentTarget
|
||||
const rollType = el.dataset.rollType
|
||||
if (!rollType) return
|
||||
let rollKey = el.dataset.rollKey
|
||||
let rollDice = el.dataset.rollDice
|
||||
|
||||
this.actor.prepareRoll(rollType, rollKey, rollDice)
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export default class LethalFantasyMonsterSheet extends LethalFantasyActorSheet {
|
||||
*/
|
||||
async _onDrop(event) {
|
||||
if (!this.isEditable || !this.isEditMode) return
|
||||
const data = TextEditor.getDragEventData(event)
|
||||
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
|
||||
|
||||
// Handle different data types
|
||||
switch (data.type) {
|
||||
@@ -111,11 +111,13 @@ export default class LethalFantasyMonsterSheet extends LethalFantasyActorSheet {
|
||||
})
|
||||
if (!roll) return null
|
||||
|
||||
await roll.toMessage({}, { rollMode: roll.options.rollMode })
|
||||
await roll.toMessage({}, { messageMode: roll.options.rollMode })
|
||||
}
|
||||
|
||||
static async #onRollInitiative(event, target) {
|
||||
await this.document.system.rollInitiative(event, target)
|
||||
const combat = game.combat
|
||||
const combatant = combat?.combatants.find(c => c.actorId === this.document.id)
|
||||
await this.document.system.rollInitiative(combat?.id, combatant?.id)
|
||||
}
|
||||
|
||||
getBestWeaponClassSkill(skills, rollType, multiplier = 1.0) {
|
||||
|
||||
@@ -73,20 +73,6 @@
|
||||
],
|
||||
"description": "Possible Lethal or Vital Magical Strike or Add D20E to Spell Attack"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "choice",
|
||||
"choices": [
|
||||
{
|
||||
"type": "spell_calamity"
|
||||
},
|
||||
{
|
||||
"type": "bonus_dice",
|
||||
"dice": "D20E",
|
||||
"target": "spell_defense"
|
||||
}
|
||||
],
|
||||
"description": "Possible Spell Catastrophe or adds D20E to Spell Defense"
|
||||
},
|
||||
"skill_rolls": {
|
||||
"type": "skill_auto_success",
|
||||
"description": "Skill Succeeds Regardless of Opposing Roll"
|
||||
@@ -108,6 +94,20 @@
|
||||
}
|
||||
],
|
||||
"description": "Possible Flawless or Legendary Defense or Add D20E to Defense"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "choice",
|
||||
"choices": [
|
||||
{
|
||||
"type": "spell_calamity"
|
||||
},
|
||||
{
|
||||
"type": "bonus_dice",
|
||||
"dice": "D20E",
|
||||
"target": "spell_defense"
|
||||
}
|
||||
],
|
||||
"description": "Possible Spell Catastrophe or adds D20E to Spell Defense"
|
||||
}
|
||||
},
|
||||
"29": {
|
||||
@@ -131,11 +131,6 @@
|
||||
"amount": 1,
|
||||
"description": "Gain 1 Grit"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "gain_grit",
|
||||
"amount": 1,
|
||||
"description": "Gain 1 Grit"
|
||||
},
|
||||
"skill_rolls": {
|
||||
"type": "gain_grit",
|
||||
"amount": 1,
|
||||
@@ -145,6 +140,11 @@
|
||||
"type": "gain_grit",
|
||||
"amount": 1,
|
||||
"description": "Gain 1 Grit"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "gain_grit",
|
||||
"amount": 1,
|
||||
"description": "Gain 1 Grit"
|
||||
}
|
||||
},
|
||||
"28": {
|
||||
@@ -175,16 +175,16 @@
|
||||
"type": "no_lethargy",
|
||||
"description": "No Spell Lethargy the Aether Approves of Characters Efforts"
|
||||
},
|
||||
"ranged_defense": {
|
||||
"type": "luck_die",
|
||||
"scope": "combat",
|
||||
"description": "Granted 1 Luck dice for Use in This Combat Only"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "flash_of_pain",
|
||||
"duration_dice": "1D6E",
|
||||
"target": "caster",
|
||||
"description": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds"
|
||||
},
|
||||
"ranged_defense": {
|
||||
"type": "luck_die",
|
||||
"scope": "combat",
|
||||
"description": "Granted 1 Luck dice for Use in This Combat Only"
|
||||
}
|
||||
},
|
||||
"26": {
|
||||
@@ -224,12 +224,6 @@
|
||||
"target": "defender",
|
||||
"description": "Magical Damage inflicts Flash of pain 1D6E seconds"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "flash_of_pain",
|
||||
"duration_dice": "1D6E",
|
||||
"target": "caster",
|
||||
"description": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds"
|
||||
},
|
||||
"skill_rolls": {
|
||||
"type": "bonus_dice",
|
||||
"dice": "D6",
|
||||
@@ -239,6 +233,12 @@
|
||||
"ranged_defense": {
|
||||
"type": "recover_pain",
|
||||
"description": "Defender Recovers or ignores any flash of pain"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "flash_of_pain",
|
||||
"duration_dice": "1D6E",
|
||||
"target": "caster",
|
||||
"description": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds"
|
||||
}
|
||||
},
|
||||
"20": {
|
||||
@@ -310,23 +310,6 @@
|
||||
],
|
||||
"description": "Possible Vicious Application of a Magical Attack or add D12 to attack"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "choice",
|
||||
"choices": [
|
||||
{
|
||||
"type": "special_defense",
|
||||
"options": [
|
||||
"perfect_spell"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "bonus_dice",
|
||||
"dice": "D12",
|
||||
"target": "spell_defense"
|
||||
}
|
||||
],
|
||||
"description": "Possible 20/20 Spell defense that Saves Against Any Magical Attack Except a Lethal Magical Strike or add D12 to defense"
|
||||
},
|
||||
"skill_rolls": {
|
||||
"type": "bonus_flat",
|
||||
"amount": 20,
|
||||
@@ -349,6 +332,23 @@
|
||||
}
|
||||
],
|
||||
"description": "Possible 20/20 defense that avoids Any Attack Except a Lethal Strike or adds D12 to defense"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "choice",
|
||||
"choices": [
|
||||
{
|
||||
"type": "special_defense",
|
||||
"options": [
|
||||
"perfect_spell"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "bonus_dice",
|
||||
"dice": "D12",
|
||||
"target": "spell_defense"
|
||||
}
|
||||
],
|
||||
"description": "Possible 20/20 Spell defense that Saves Against Any Magical Attack Except a Lethal Magical Strike or add D12 to spell defense"
|
||||
}
|
||||
},
|
||||
"15": {
|
||||
@@ -359,10 +359,10 @@
|
||||
"type": "bleed"
|
||||
},
|
||||
{
|
||||
"type": "knockback"
|
||||
"type": "internal_injury"
|
||||
}
|
||||
],
|
||||
"description": "Bleed, Knock-back on Hit"
|
||||
"description": "Bleed, Internal Injury on Hit"
|
||||
},
|
||||
"ranged_attack": {
|
||||
"type": "bleed",
|
||||
@@ -372,10 +372,9 @@
|
||||
"type": "counter_attack",
|
||||
"options": [
|
||||
"kick",
|
||||
"punch",
|
||||
"shield_bash"
|
||||
"punch"
|
||||
],
|
||||
"description": "Kick, Punch or Shield Bash"
|
||||
"description": "Kick or Punch"
|
||||
},
|
||||
"skill_rolls": {
|
||||
"type": "bonus_flat",
|
||||
@@ -387,10 +386,9 @@
|
||||
"type": "counter_attack",
|
||||
"options": [
|
||||
"kick",
|
||||
"punch",
|
||||
"shield_bash"
|
||||
"punch"
|
||||
],
|
||||
"description": "Kick, Punch or Shield Bash"
|
||||
"description": "Kick or Punch"
|
||||
}
|
||||
},
|
||||
"13": {},
|
||||
@@ -398,7 +396,7 @@
|
||||
"melee_attack": {
|
||||
"type": "flurry",
|
||||
"condition": "hit_or_miss",
|
||||
"description": "Flurry Attack or Hit to Miss"
|
||||
"description": "Flurry Attack on Hit or Miss"
|
||||
},
|
||||
"ranged_attack": {
|
||||
"type": "double_damage_dice",
|
||||
@@ -413,10 +411,10 @@
|
||||
"type": "bleed"
|
||||
},
|
||||
{
|
||||
"type": "knockback"
|
||||
"type": "internal_injury"
|
||||
}
|
||||
],
|
||||
"description": "Bleed, Knock-back on Hit"
|
||||
"description": "Bleed, Internal Injury on Hit"
|
||||
},
|
||||
"ranged_attack": {
|
||||
"type": "bleed",
|
||||
@@ -426,10 +424,9 @@
|
||||
"type": "counter_attack",
|
||||
"options": [
|
||||
"kick",
|
||||
"punch",
|
||||
"shield_bash"
|
||||
"punch"
|
||||
],
|
||||
"description": "Kick, Punch or Shield Bash"
|
||||
"description": "Kick or Punch"
|
||||
},
|
||||
"skill_rolls": {
|
||||
"type": "bonus_flat",
|
||||
@@ -441,10 +438,9 @@
|
||||
"type": "counter_attack",
|
||||
"options": [
|
||||
"kick",
|
||||
"punch",
|
||||
"shield_bash"
|
||||
"punch"
|
||||
],
|
||||
"description": "Kick, Punch or Shield Bash"
|
||||
"description": "Kick or Punch"
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
@@ -464,10 +460,6 @@
|
||||
"type": "mulligan",
|
||||
"description": "Mulligan, Can Re-Roll This Spell Attack"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "mulligan",
|
||||
"description": "Mulligan, Can Re-Roll This Spell Defense"
|
||||
},
|
||||
"skill_rolls": {
|
||||
"type": "mulligan",
|
||||
"description": "Mulligan, Can Re-Roll This Skill roll"
|
||||
@@ -475,6 +467,10 @@
|
||||
"ranged_defense": {
|
||||
"type": "mulligan",
|
||||
"description": "Mulligan, Can Choose to Re-Roll This Defense"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "mulligan",
|
||||
"description": "Mulligan, Can Re-Roll This Spell Defense"
|
||||
}
|
||||
},
|
||||
"7": {
|
||||
@@ -496,10 +492,10 @@
|
||||
"type": "bleed"
|
||||
},
|
||||
{
|
||||
"type": "knockback"
|
||||
"type": "internal_injury"
|
||||
}
|
||||
],
|
||||
"description": "Bleed, Knock-back on Hit"
|
||||
"description": "Bleed, Internal Injury on Hit"
|
||||
},
|
||||
"ranged_attack": {
|
||||
"type": "bleed",
|
||||
@@ -509,10 +505,9 @@
|
||||
"type": "counter_attack",
|
||||
"options": [
|
||||
"kick",
|
||||
"punch",
|
||||
"shield_bash"
|
||||
"punch"
|
||||
],
|
||||
"description": "Kick, Punch, or Shield Bash"
|
||||
"description": "Kick or Punch"
|
||||
},
|
||||
"skill_rolls": {
|
||||
"type": "bonus_flat",
|
||||
@@ -524,10 +519,9 @@
|
||||
"type": "counter_attack",
|
||||
"options": [
|
||||
"kick",
|
||||
"punch",
|
||||
"shield_bash"
|
||||
"punch"
|
||||
],
|
||||
"description": "Kick, Punch, or Shield Bash"
|
||||
"description": "Kick or Punch"
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
@@ -552,17 +546,17 @@
|
||||
"multiplier": 3,
|
||||
"description": "Triple Damage on Spell Damage"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "bonus_dice",
|
||||
"dice": "D12",
|
||||
"target": "spell_defense",
|
||||
"description": "D12 Added to Spell Defense Modifier"
|
||||
},
|
||||
"ranged_defense": {
|
||||
"type": "dr_multiplier",
|
||||
"multiplier": 3,
|
||||
"includes_shield": true,
|
||||
"description": "DR Tripled including Shield"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "bonus_dice",
|
||||
"dice": "D12",
|
||||
"target": "spell_defense",
|
||||
"description": "D12 Added to Spell Defense Modifier"
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
@@ -587,17 +581,17 @@
|
||||
"multiplier": 2,
|
||||
"description": "Double Damage on Spell Damage"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "bonus_dice",
|
||||
"dice": "D6",
|
||||
"target": "spell_defense",
|
||||
"description": "D6 Added to Spell Defense Modifier"
|
||||
},
|
||||
"ranged_defense": {
|
||||
"type": "dr_multiplier",
|
||||
"multiplier": 2,
|
||||
"includes_shield": true,
|
||||
"description": "DR Doubled including Shield"
|
||||
},
|
||||
"arcane_spell_defense": {
|
||||
"type": "bonus_dice",
|
||||
"dice": "D6",
|
||||
"target": "spell_defense",
|
||||
"description": "D6 Added to Spell Defense Modifier"
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
@@ -608,7 +602,12 @@
|
||||
},
|
||||
"arcane_spell_attack": {
|
||||
"type": "spell_calamity",
|
||||
"description": "Possible Spell Calamity or Catastrophe"
|
||||
"description": "A possible spell calamity has occurred"
|
||||
},
|
||||
"melee_attack": {
|
||||
"type": "fumble",
|
||||
"detail": "melee_fumble",
|
||||
"description": "Possible Fumble"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -85,11 +85,18 @@ export const RANGE_CHOICES = {
|
||||
"pointblank": { label: "Point Blank (Special)", value: "pointblank" },
|
||||
"short": { label: "Short (+0)", value: "0" },
|
||||
"medium": { label: "Medium (Red +5)", value: "+5" },
|
||||
"long": { label: "Long (Purle +7)", value: "+7" },
|
||||
"long": { label: "Long (Purple +7)", value: "+7" },
|
||||
"extreme": { label: "Extreme (Grey +9)", value: "+9" },
|
||||
"beyondskill": { label: "Beyond Skill (Blue +11)", value: "beyondskill" }
|
||||
}
|
||||
|
||||
export const ATTACKER_MOVEMENT_CHOICES = {
|
||||
"none": { label: "None / Stationary (D20E Favor)", favor: true, value: "2D20kh" },
|
||||
"walk": { label: "Walk (D20E)", value: "D20" },
|
||||
"incombat": { label: "In Combat (D20E)", value: "D20" },
|
||||
"run": { label: "Jog/Run/Sprint (D20E Disfavor)", disfavor: true, value: "2D20kl" }
|
||||
}
|
||||
|
||||
export const ATTACKER_AIM_CHOICES = {
|
||||
"simple": { label: "Simple (+0)", value: "0" },
|
||||
"careful": { label: "Careful (Red +5)", value: "+4" },
|
||||
@@ -321,6 +328,7 @@ export const SYSTEM = {
|
||||
RANGE_CHOICES,
|
||||
FAVOR_CHOICES,
|
||||
ATTACKER_AIM_CHOICES,
|
||||
ATTACKER_MOVEMENT_CHOICES,
|
||||
MORTAL_CHOICES,
|
||||
SPELL_CRITICAL,
|
||||
MIRACLE_TYPES,
|
||||
|
||||
+75
-19
@@ -7,8 +7,10 @@ export default class LethalFantasyActor extends Actor {
|
||||
if (data instanceof Array) {
|
||||
return super.create(data, options);
|
||||
}
|
||||
// If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic
|
||||
if (data.items) {
|
||||
// If the created actor already has items (duplicated or imported) bypass the new actor creation logic.
|
||||
// Use .length check because Foundry V2 initializes data.items as an empty array by default
|
||||
// (truthy), which would incorrectly skip the layperson skill auto-population.
|
||||
if (data.items?.length > 0) {
|
||||
let actor = super.create(data, options);
|
||||
return actor;
|
||||
}
|
||||
@@ -72,23 +74,32 @@ export default class LethalFantasyActor extends Actor {
|
||||
/* *************************************************/
|
||||
async applyDamage(hpLoss) {
|
||||
let hp = this.system.hp.value + hpLoss
|
||||
if (hp < 0) {
|
||||
hp = 0
|
||||
await this.update({ "system.hp.value": hp })
|
||||
}
|
||||
|
||||
/* *************************************************/
|
||||
getNaturalDR() {
|
||||
if (this.type === "monster") {
|
||||
return Number(this.system.hp?.damageResistance) || 0
|
||||
}
|
||||
this.update({ "system.hp.value": hp })
|
||||
return Number(this.system.biodata?.naturalDR) || 0
|
||||
}
|
||||
|
||||
/* *************************************************/
|
||||
getMagicDR() {
|
||||
if (this.type === "monster") return 0
|
||||
return Number(this.system.biodata?.magicDR) || 0
|
||||
}
|
||||
|
||||
/* *************************************************/
|
||||
computeDamageReduction() {
|
||||
// Pour les monstres, utiliser hp.damageResistance et combat.damageReduction
|
||||
if (this.type === "monster") {
|
||||
let hpDR = Number(this.system.hp?.damageResistance) || 0
|
||||
let hpDR = this.getNaturalDR()
|
||||
let combatDR = Number(this.system.combat?.damageReduction) || 0
|
||||
return hpDR + combatDR
|
||||
}
|
||||
// Pour les personnages, utiliser biodata et items
|
||||
let naturalDR = Number(this.system.biodata?.naturalDR) || 0
|
||||
let magicDR = Number(this.system.biodata?.magicDR) || 0
|
||||
let naturalDR = this.getNaturalDR()
|
||||
let magicDR = this.getMagicDR()
|
||||
let armorDR = this.getArmorDR()
|
||||
return naturalDR + magicDR + armorDR
|
||||
}
|
||||
@@ -153,8 +164,8 @@ export default class LethalFantasyActor extends Actor {
|
||||
}
|
||||
|
||||
/* *************************************************/
|
||||
async prepareRoll(rollType, rollKey, rollDice, defenderId, defenderTokenId, extraShieldDr = 0) {
|
||||
console.log("Preparing roll", rollType, rollKey, rollDice, defenderId)
|
||||
async prepareRoll(rollType, rollKey, rollDice, defenderId, defenderTokenId, extraShieldDr = 0, d30Effects = {}) {
|
||||
log("Preparing roll", rollType, rollKey, rollDice, defenderId)
|
||||
let rollTarget
|
||||
switch (rollType) {
|
||||
case "granted":
|
||||
@@ -196,8 +207,53 @@ export default class LethalFantasyActor extends Actor {
|
||||
case "spell-power":
|
||||
case "miracle-attack":
|
||||
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
|
||||
// Read damage tier from combatant currentAction if available
|
||||
const activeCombatant = game.combat?.combatants?.find(c => c.actorId === this.id)
|
||||
const currentAction = activeCombatant?.getFlag(SYSTEM.id, "currentAction")
|
||||
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: await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/power-level.hbs", {itemName: rollTarget.name}),
|
||||
buttons,
|
||||
rejectClose: false,
|
||||
}) || "standard"
|
||||
} else {
|
||||
damageTier = "standard"
|
||||
}
|
||||
}
|
||||
rollTarget.damageTier = damageTier
|
||||
if (rollType === "spell-attack" || rollType === "spell-power") {
|
||||
const tierCostMap = { standard: "cost", overpowered: "costOverpowered", overpowered2: "costOverpowered2" }
|
||||
const costField = tierCostMap[damageTier] || "cost"
|
||||
const cost = Number(rollTarget.system?.[costField]) || 0
|
||||
const currentAether = Number(this.system.aetherPoints?.value) || 0
|
||||
if (cost > currentAether) {
|
||||
ui.notifications.warn(`${this.name} cannot cast ${rollTarget.name}: insufficient Aether (needs ${cost}, has ${currentAether}).`)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (rollType === "miracle-attack" || rollType === "miracle-power") {
|
||||
const cost = Number(rollTarget.system?.level) || 0
|
||||
const currentGrace = Number(this.system.divinityPoints?.value) || 0
|
||||
if (cost > currentGrace) {
|
||||
ui.notifications.warn(`${this.name} cannot invoke ${rollTarget.name}: insufficient Grace (needs ${cost}, has ${currentGrace}).`)
|
||||
return
|
||||
}
|
||||
}
|
||||
break
|
||||
case "shield-roll": {
|
||||
rollTarget = this.items.find((i) => i.type === "shield" && i.id === rollKey)
|
||||
@@ -206,8 +262,7 @@ export default class LethalFantasyActor extends Actor {
|
||||
rollTarget.rollKey = rollKey
|
||||
}
|
||||
break;
|
||||
case "weapon-damage-small":
|
||||
case "weapon-damage-medium":
|
||||
case "weapon-damage":
|
||||
case "weapon-attack":
|
||||
case "weapon-defense": {
|
||||
let weapon = this.items.find((i) => i.type === "weapon" && i.id === rollKey)
|
||||
@@ -245,9 +300,10 @@ export default class LethalFantasyActor extends Actor {
|
||||
weapon: weapon,
|
||||
weaponSkillModifier: skill.weaponSkillModifier,
|
||||
rollKey: rollKey,
|
||||
combat: foundry.utils.duplicate(this.system.combat)
|
||||
combat: foundry.utils.duplicate(this.system.combat),
|
||||
isRangedAttack: weapon.system.weaponType === "ranged"
|
||||
}
|
||||
if (rollType === "weapon-damage-small" || rollType === "weapon-damage-medium") {
|
||||
if (rollType === "weapon-damage") {
|
||||
rollTarget.grantedDice = this.system.granted.damageDice
|
||||
}
|
||||
if (rollType === "weapon-attack") {
|
||||
@@ -263,14 +319,14 @@ export default class LethalFantasyActor extends Actor {
|
||||
break
|
||||
default:
|
||||
ui.notifications.error(game.i18n.localize("LETHALFANTASY.Notifications.rollTypeNotFound") + String(rollType))
|
||||
break
|
||||
return
|
||||
}
|
||||
|
||||
// In all cases
|
||||
rollTarget.magicUser = this.system.biodata.magicUser
|
||||
rollTarget.actorModifiers = foundry.utils.duplicate(this.system.modifiers)
|
||||
rollTarget.actorLevel = this.system.biodata.level
|
||||
await this.system.roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr)
|
||||
await this.system.roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr, d30Effects)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export default class LethalFantasyChatMessage extends ChatMessage {
|
||||
messageData.isWhisper = false
|
||||
messageData.alias = this.user.name
|
||||
}
|
||||
data.content = `<section class="dice-rolls">${rollHTML}</section>`
|
||||
data.content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/dice-rolls.hbs", {rollHTML})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { log } from "../utils.mjs"
|
||||
|
||||
/**
|
||||
* Classe pour gérer les résultats du D30 dans Lethal Fantasy
|
||||
*/
|
||||
@@ -40,7 +42,7 @@ export default class D30Roll {
|
||||
this.resultsTable = data.d30_dice_results
|
||||
this.definitions = data.definitions
|
||||
|
||||
console.log("D30Roll | D30 results table loaded successfully")
|
||||
log("D30Roll | D30 results table loaded successfully")
|
||||
} catch (error) {
|
||||
console.error("D30Roll | Error loading D30 table:", error)
|
||||
ui.notifications.error("Unable to load D30 results table")
|
||||
@@ -111,17 +113,16 @@ export default class D30Roll {
|
||||
if (externalType === "weapon-attack") {
|
||||
if (!weapon) {
|
||||
console.warn("D30Roll | Weapon object required for weapon-attack type")
|
||||
return this.ROLL_TYPES.MELEE_ATTACK // Default to melee
|
||||
// Fall through to use options.isRanged if available, otherwise default melee
|
||||
}
|
||||
return weapon.system?.weaponType === "ranged"
|
||||
return (options.isRanged || weapon?.system?.weaponType === "ranged")
|
||||
? this.ROLL_TYPES.RANGED_ATTACK
|
||||
: this.ROLL_TYPES.MELEE_ATTACK
|
||||
}
|
||||
|
||||
// Monster attacks - default to melee
|
||||
// Monster attacks - check options.isRanged (set from rollTarget.attackMode) or weapon type
|
||||
if (externalType === "monster-attack") {
|
||||
// Check if weapon object has range information
|
||||
if (weapon?.system?.weaponType === "ranged") {
|
||||
if (options.isRanged || weapon?.system?.weaponType === "ranged") {
|
||||
return this.ROLL_TYPES.RANGED_ATTACK
|
||||
}
|
||||
return this.ROLL_TYPES.MELEE_ATTACK
|
||||
@@ -132,17 +133,22 @@ export default class D30Roll {
|
||||
return options.isRanged ? this.ROLL_TYPES.RANGED_DEFENSE : this.ROLL_TYPES.MELEE_DEFENSE
|
||||
}
|
||||
|
||||
// Spell types
|
||||
if (externalType === "spell-attack" || externalType === "spell" || externalType === "spell-power") {
|
||||
// Spell/Miracle types
|
||||
if (externalType === "spell-attack" || externalType === "spell" || externalType === "spell-power"
|
||||
|| externalType === "miracle-attack" || externalType === "miracle" || externalType === "miracle-power") {
|
||||
return this.ROLL_TYPES.ARCANE_SPELL_ATTACK
|
||||
}
|
||||
|
||||
// Skill types
|
||||
if (externalType === "skill" || externalType === "monster-skill" ||
|
||||
externalType === "save" || externalType === "challenge") {
|
||||
if (externalType === "skill" || externalType === "monster-skill" || externalType === "challenge") {
|
||||
return this.ROLL_TYPES.SKILL_ROLLS
|
||||
}
|
||||
|
||||
// Saving throw types
|
||||
if (externalType === "save") {
|
||||
return this.ROLL_TYPES.ARCANE_SPELL_DEFENSE
|
||||
}
|
||||
|
||||
// If no match, return null
|
||||
console.warn(`D30Roll | Unknown external roll type: ${externalType}`)
|
||||
return null
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
import { prompt } from "./roll-prompt.mjs"
|
||||
import { promptInitiative, promptCombatAction, promptRangedDefense, promptRangedAttack } from "./roll-combat.mjs"
|
||||
import { rollSpellDamageToMessage } from "./roll-damage.mjs"
|
||||
|
||||
export default class LethalFantasyRoll extends Roll {
|
||||
/**
|
||||
* The HTML template path used to render dice checks of this type
|
||||
* @type {string}
|
||||
*/
|
||||
static CHAT_TEMPLATE = "systems/fvtt-lethal-fantasy/templates/chat-message.hbs"
|
||||
|
||||
get type() {
|
||||
return this.options.type
|
||||
}
|
||||
|
||||
get titleFormula() {
|
||||
return this.options.titleFormula
|
||||
}
|
||||
|
||||
get rollName() {
|
||||
return this.options.rollName
|
||||
}
|
||||
|
||||
get target() {
|
||||
return this.options.target
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.options.value
|
||||
}
|
||||
|
||||
get treshold() {
|
||||
return this.options.treshold
|
||||
}
|
||||
|
||||
get actorId() {
|
||||
return this.options.actorId
|
||||
}
|
||||
|
||||
get actorName() {
|
||||
return this.options.actorName
|
||||
}
|
||||
|
||||
get actorImage() {
|
||||
return this.options.actorImage
|
||||
}
|
||||
|
||||
get modifier() {
|
||||
return this.options.modifier
|
||||
}
|
||||
|
||||
get resultType() {
|
||||
return this.options.resultType
|
||||
}
|
||||
|
||||
get isFailure() {
|
||||
return this.resultType === "failure"
|
||||
}
|
||||
|
||||
get hasTarget() {
|
||||
return this.options.hasTarget
|
||||
}
|
||||
|
||||
get targetName() {
|
||||
return this.options.targetName
|
||||
}
|
||||
|
||||
get targetArmor() {
|
||||
return this.options.targetArmor
|
||||
}
|
||||
|
||||
get targetMalus() {
|
||||
return this.options.targetMalus
|
||||
}
|
||||
|
||||
get realDamage() {
|
||||
return this.options.realDamage
|
||||
}
|
||||
|
||||
get rollTotal() {
|
||||
return this.options.rollTotal
|
||||
}
|
||||
|
||||
get diceResults() {
|
||||
return this.options.diceResults
|
||||
}
|
||||
|
||||
get rollTarget() {
|
||||
return this.options.rollTarget
|
||||
}
|
||||
|
||||
get D30result() {
|
||||
return this.options.D30result
|
||||
}
|
||||
|
||||
get D30message() {
|
||||
return this.options.D30message
|
||||
}
|
||||
|
||||
get badResult() {
|
||||
return this.options.badResult
|
||||
}
|
||||
|
||||
get rollData() {
|
||||
return this.options.rollData
|
||||
}
|
||||
|
||||
get defenderId() {
|
||||
return this.options.defenderId
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a title based on the given type.
|
||||
*
|
||||
* @param {string} type The type of the roll.
|
||||
* @param {string} target The target of the roll.
|
||||
* @returns {string} The generated title.
|
||||
*/
|
||||
static createTitle(type, target) {
|
||||
switch (type) {
|
||||
case "challenge":
|
||||
return `${game.i18n.localize("LETHALFANTASY.Label.titleChallenge")}`
|
||||
case "save":
|
||||
return `${game.i18n.localize("LETHALFANTASY.Label.titleSave")}`
|
||||
case "monster-skill":
|
||||
case "skill":
|
||||
return `${game.i18n.localize("LETHALFANTASY.Label.titleSkill")}`
|
||||
case "weapon-attack":
|
||||
return `${game.i18n.localize("LETHALFANTASY.Label.weapon-attack")}`
|
||||
case "weapon-defense":
|
||||
return `${game.i18n.localize("LETHALFANTASY.Label.weapon-defense")}`
|
||||
case "weapon-damage":
|
||||
return `${game.i18n.localize("LETHALFANTASY.Label.weapon-damage")}`
|
||||
case "spell":
|
||||
case "spell-attack":
|
||||
case "spell-power":
|
||||
return `${game.i18n.localize("LETHALFANTASY.Label.spell")}`
|
||||
case "miracle":
|
||||
case "miracle-attack":
|
||||
case "miracle-power":
|
||||
return `${game.i18n.localize("LETHALFANTASY.Label.miracle")}`
|
||||
default:
|
||||
return game.i18n.localize("LETHALFANTASY.Label.titleStandard")
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async render(chatOptions = {}) {
|
||||
let chatData = await this._getChatCardData(chatOptions.isPrivate)
|
||||
log("ChatData", chatData)
|
||||
return await foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, chatData)
|
||||
}
|
||||
|
||||
/*
|
||||
* Generates the data required for rendering a roll chat card.
|
||||
*/
|
||||
async _getChatCardData(isPrivate) {
|
||||
// Générer la liste des combatants de la scène
|
||||
let combatants = []
|
||||
let isAttack = this.type === "weapon-attack" || this.type === "monster-attack" || this.type === "spell-attack" || this.type === "miracle-attack"
|
||||
if (this.rollData?.isDamage || isAttack) {
|
||||
// D'abord, ajouter les combattants du combat actif
|
||||
if (game?.combat?.combatants) {
|
||||
for (let c of game.combat.combatants) {
|
||||
if (c.actorId !== this.actorId) {
|
||||
combatants.push({ id: c.id, name: c.name, tokenId: c.token.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensuite, ajouter tous les tokens de la scène active qui ne sont pas déjà dans la liste
|
||||
if (canvas?.scene?.tokens) {
|
||||
const existingTokenIds = new Set(combatants.map(c => c.tokenId))
|
||||
for (let token of canvas.scene.tokens) {
|
||||
if (token.actorId !== this.actorId && !existingTokenIds.has(token.id)) {
|
||||
combatants.push({
|
||||
id: token.id,
|
||||
name: token.name,
|
||||
tokenId: token.id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les informations de l'arme pour les attaques réussies
|
||||
let weaponDamageOptions = null
|
||||
log("Roll type:", this.type, "rollTarget:", this.rollTarget, "Has weapon:", !!this.rollTarget?.weapon)
|
||||
if (this.type === "weapon-attack" && this.rollTarget?.weapon) {
|
||||
const weapon = this.rollTarget.weapon
|
||||
weaponDamageOptions = {
|
||||
weaponId: weapon._id || weapon.id,
|
||||
weaponName: weapon.name,
|
||||
damageM: weapon.system?.damage?.damageM
|
||||
}
|
||||
log("Weapon damage options:", weaponDamageOptions)
|
||||
} else if (this.type === "monster-attack" && this.rollTarget) {
|
||||
weaponDamageOptions = {
|
||||
weaponId: this.rollTarget.rollKey,
|
||||
weaponName: this.rollTarget.name,
|
||||
damageFormula: this.rollTarget.damageDice,
|
||||
damageModifier: this.rollTarget.damageModifier,
|
||||
isMonster: true
|
||||
}
|
||||
log("Monster damage options:", weaponDamageOptions)
|
||||
}
|
||||
|
||||
const cardData = {
|
||||
css: [SYSTEM.id, "dice-roll"],
|
||||
data: this.data,
|
||||
diceTotal: this.dice.reduce((t, d) => t + d.total, 0),
|
||||
isGM: game.user.isGM,
|
||||
formula: this.formula,
|
||||
titleFormula: this.titleFormula,
|
||||
rollName: this.rollName,
|
||||
rollType: this.type,
|
||||
rollTarget: this.rollTarget,
|
||||
total: this.rollTotal,
|
||||
isFailure: this.isFailure,
|
||||
actorId: this.actorId,
|
||||
diceResults: this.diceResults,
|
||||
actingCharName: this.actorName,
|
||||
actingCharImg: this.actorImage,
|
||||
resultType: this.resultType,
|
||||
hasTarget: this.hasTarget,
|
||||
targetName: this.targetName,
|
||||
targetArmor: this.targetArmor,
|
||||
D30result: this.D30result,
|
||||
D30message: this.D30message,
|
||||
badResult: this.badResult,
|
||||
rollData: this.rollData,
|
||||
isPrivate: isPrivate,
|
||||
combatants: combatants,
|
||||
weaponDamageOptions: weaponDamageOptions,
|
||||
isAttack: isAttack,
|
||||
defenderId: this.defenderId,
|
||||
// Vérifier si l'utilisateur peut sélectionner une cible (est GM ou possède l'acteur)
|
||||
canSelectTarget: game.user.isGM || game.actors.get(this.actorId)?.testUserPermission(game.user, "OWNER")
|
||||
}
|
||||
cardData.cssClass = cardData.css.join(" ")
|
||||
cardData.tooltip = isPrivate ? "" : await this.getTooltip()
|
||||
return cardData
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the roll result to a chat message.
|
||||
*
|
||||
* @param {Object} [messageData={}] Additional data to include in the message.
|
||||
* @param {Object} options Options for message creation.
|
||||
* @param {string} options.messageMode The mode of the roll (e.g., public, private).
|
||||
* @param {boolean} [options.create=true] Whether to create the message.
|
||||
* @returns {Promise} - A promise that resolves when the message is created.
|
||||
*/
|
||||
async toMessage(messageData = {}, { messageMode, create = true } = {}) {
|
||||
return await super.toMessage(
|
||||
{
|
||||
isSave: this.isSave,
|
||||
isChallenge: this.isChallenge,
|
||||
isFailure: this.resultType === "failure",
|
||||
rollType: this.type,
|
||||
rollTarget: this.rollTarget,
|
||||
actingCharName: this.actorName,
|
||||
actingCharImg: this.actorImage,
|
||||
hasTarget: this.hasTarget,
|
||||
targetName: this.targetName,
|
||||
targetArmor: this.targetArmor,
|
||||
targetMalus: this.targetMalus,
|
||||
realDamage: this.realDamage,
|
||||
rollData: this.rollData,
|
||||
...messageData,
|
||||
},
|
||||
{ messageMode, create },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Attach imported prompt methods
|
||||
LethalFantasyRoll.prompt = prompt
|
||||
LethalFantasyRoll.promptInitiative = promptInitiative
|
||||
LethalFantasyRoll.promptCombatAction = promptCombatAction
|
||||
LethalFantasyRoll.promptRangedDefense = promptRangedDefense
|
||||
LethalFantasyRoll.promptRangedAttack = promptRangedAttack
|
||||
LethalFantasyRoll.rollSpellDamageToMessage = rollSpellDamageToMessage
|
||||
@@ -0,0 +1,714 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
import D30Roll from "./d30-roll.mjs"
|
||||
import LethalFantasyUtils from "../utils.mjs"
|
||||
|
||||
/* ***********************************************************/
|
||||
export async function promptInitiative(options = {}) {
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes); // v12 : Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)]))
|
||||
const fieldRollMode = new foundry.data.fields.StringField({
|
||||
choices: rollModes,
|
||||
blank: false,
|
||||
default: "public",
|
||||
})
|
||||
|
||||
if (SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass]) {
|
||||
options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass]
|
||||
} else {
|
||||
options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS["untrained"]
|
||||
}
|
||||
|
||||
let dialogContext = {
|
||||
actorClass: options.actorClass,
|
||||
initiativeDiceChoice: options.initiativeDiceChoice,
|
||||
initiativeDice: "1D20",
|
||||
maxInit: options.maxInit,
|
||||
fieldRollMode,
|
||||
rollModes
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/roll-initiative-dialog.hbs", dialogContext)
|
||||
|
||||
const label = game.i18n.localize("LETHALFANTASY.Label.initiative")
|
||||
const rollContext = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Initiative Roll" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "initiative",
|
||||
type: "button",
|
||||
label: label,
|
||||
callback: (event, button) => {
|
||||
const output = Array.from(button.form.elements).reduce((obj, input) => {
|
||||
if (input.name) obj[input.name] = input.value
|
||||
return obj
|
||||
}, {})
|
||||
return output
|
||||
},
|
||||
},
|
||||
],
|
||||
rejectClose: false // Click on Close button will not launch an error
|
||||
})
|
||||
|
||||
if (!rollContext) return
|
||||
|
||||
// When the value is a plain number (e.g. "1" for Declared Ready on Alert), wrapping it in
|
||||
// min(1, maxInit) produces a dice-less formula that FoundryVTT cannot evaluate to a valid
|
||||
// total. Use the constant directly; min() is only needed for actual dice expressions.
|
||||
const isDiceFormula = /[dD]/.test(rollContext.initiativeDice)
|
||||
const formula = isDiceFormula ? `min(${rollContext.initiativeDice}, ${options.maxInit})` : rollContext.initiativeDice
|
||||
|
||||
let initRoll = new Roll(formula, options.data)
|
||||
await initRoll.evaluate()
|
||||
let msg = await initRoll.toMessage({ flavor: `Initiative for ${options.actorName}` }, { messageMode: rollContext.visibility })
|
||||
if (game?.dice3d && initRoll.dice?.length) {
|
||||
await game.dice3d.waitFor3DAnimationByMessageID(msg.id)
|
||||
}
|
||||
|
||||
if (options.combatId && options.combatantId) {
|
||||
let combat = game.combats.get(options.combatId)
|
||||
await combat.updateEmbeddedDocuments("Combatant", [{ _id: options.combatantId, initiative: initRoll.total, 'system.progressionCount': 0, [`flags.${SYSTEM.id}.firstActionTaken`]: false }])
|
||||
}
|
||||
}
|
||||
|
||||
/* ***********************************************************/
|
||||
export async function promptCombatAction(options = {}) {
|
||||
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes); // v12 : Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)]))
|
||||
const fieldRollMode = new foundry.data.fields.StringField({
|
||||
choices: rollModes,
|
||||
blank: false,
|
||||
default: "public",
|
||||
})
|
||||
|
||||
let combatant = game.combats.get(options.combatId)?.combatants?.get(options.combatantId)
|
||||
if (!combatant) {
|
||||
console.error("No combatant found for this combat")
|
||||
return
|
||||
}
|
||||
let currentAction = combatant.getFlag(SYSTEM.id, "currentAction")
|
||||
|
||||
let position = game.user.getFlag(SYSTEM.id, "combat-action-dialog-pos") || { top: -1, left: -1 }
|
||||
|
||||
let dialogContext = {
|
||||
progressionDiceId: "",
|
||||
fieldRollMode,
|
||||
rollModes,
|
||||
currentAction,
|
||||
...options
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/combat-action-dialog.hbs", dialogContext)
|
||||
|
||||
let buttons = []
|
||||
if (currentAction) {
|
||||
if (currentAction.type === "weapon") {
|
||||
let weaponLabel = "Roll progression dice"
|
||||
if (currentAction.rangedMode) {
|
||||
// Compute loading count from the speed formula (e.g. "3+1d6" → load=3)
|
||||
const speedStr = currentAction.system?.speed?.[currentAction.rangedMode] ?? ""
|
||||
const rangedLoad = currentAction.rangedLoad ?? (Number(speedStr.split("+")[0]) || 0)
|
||||
if (rangedLoad > 0 && !currentAction.weaponLoaded) {
|
||||
weaponLabel = "Load weapon"
|
||||
}
|
||||
}
|
||||
buttons.push({
|
||||
action: "roll",
|
||||
type: "button",
|
||||
label: weaponLabel,
|
||||
callback: (event, button) => {
|
||||
let pos = $('#combat-action-dialog').position()
|
||||
game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos)
|
||||
return "rollProgressionDice"
|
||||
},
|
||||
})
|
||||
} else if (currentAction.type === "spell" || currentAction.type === "miracle") {
|
||||
let label = ""
|
||||
if (currentAction.spellStatus === "castingTime") {
|
||||
let pos = $('#combat-action-dialog').position()
|
||||
game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos)
|
||||
label = "Wait casting time"
|
||||
}
|
||||
if (currentAction.spellStatus === "toBeCasted") {
|
||||
let pos = $('#combat-action-dialog').position()
|
||||
game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos)
|
||||
label = "Cast spell/miracle"
|
||||
}
|
||||
if (currentAction.spellStatus === "lethargy") {
|
||||
let pos = $('#combat-action-dialog').position()
|
||||
game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos)
|
||||
label = "Roll lethargy dice"
|
||||
}
|
||||
buttons.push({
|
||||
action: "roll",
|
||||
type: "button",
|
||||
label: label,
|
||||
callback: (event, button) => {
|
||||
let pos = $('#combat-action-dialog').position()
|
||||
game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos))
|
||||
return "rollLethargyDice"
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
buttons.push({
|
||||
action: "roll",
|
||||
type: "button",
|
||||
label: "Select action",
|
||||
callback: (event, button) => {
|
||||
let pos = $('#combat-action-dialog').position()
|
||||
game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos))
|
||||
const output = Array.from(button.form.elements).reduce((obj, input) => {
|
||||
if (input.name) obj[input.name] = input.value
|
||||
return obj
|
||||
}, {})
|
||||
return output
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
buttons.push({
|
||||
action: "cancel",
|
||||
type: "button",
|
||||
label: "Other action, not listed here",
|
||||
callback: (event, button) => {
|
||||
let pos = $('#combat-action-dialog').position()
|
||||
game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos))
|
||||
return null;
|
||||
}
|
||||
})
|
||||
|
||||
let rollContext = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Combat Action Dialog" },
|
||||
id: "combat-action-dialog",
|
||||
classes: ["lethalfantasy"],
|
||||
position,
|
||||
content,
|
||||
buttons,
|
||||
rejectClose: false // Click on Close button will not launch an error
|
||||
})
|
||||
|
||||
log("RollContext", dialogContext, rollContext)
|
||||
// If action is cancelled, exit
|
||||
if (rollContext === null || rollContext === "cancel") {
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", "")
|
||||
let message = `${combatant.name} : Other action, progression reset`
|
||||
await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
return
|
||||
}
|
||||
|
||||
// Setup the current action
|
||||
if (!currentAction || currentAction === "") {
|
||||
// Get the item from the returned selectedChoice value
|
||||
let selectedChoice = rollContext.selectedChoice
|
||||
let rangedMode
|
||||
if (selectedChoice.match("simpleAim")) {
|
||||
selectedChoice = selectedChoice.replace("simpleAim", "")
|
||||
rangedMode = "simpleAim"
|
||||
}
|
||||
if (selectedChoice.match("carefulAim")) {
|
||||
selectedChoice = selectedChoice.replace("carefulAim", "")
|
||||
rangedMode = "carefulAim"
|
||||
}
|
||||
if (selectedChoice.match("focusedAim")) {
|
||||
selectedChoice = selectedChoice.replace("focusedAim", "")
|
||||
rangedMode = "focusedAim"
|
||||
}
|
||||
let selectedItem = combatant.actor.items.find(i => i.id === selectedChoice)
|
||||
// Setup flag for combat action usage
|
||||
let actionItem = foundry.utils.duplicate(selectedItem)
|
||||
// First action of this combat: use the class-based starting threshold;
|
||||
// all subsequent actions reset to 1 (normal progression).
|
||||
const firstActionTaken = combatant.getFlag(SYSTEM.id, "firstActionTaken")
|
||||
actionItem.progressionCount = firstActionTaken ? 1 : (combatant.actor.system.combat?.combatProgressionStart ?? 1)
|
||||
if (!firstActionTaken) await combatant.setFlag(SYSTEM.id, "firstActionTaken", true)
|
||||
actionItem.rangedMode = rangedMode
|
||||
// If this is a spell/miracle with multiple damage tiers, prompt tier choice
|
||||
if (actionItem.system?.damageDice) {
|
||||
const tiers = [
|
||||
{ id: "standard", label: "Standard", dice: actionItem.system.damageDice },
|
||||
{ id: "overpowered", label: "Overpowered", dice: actionItem.system.damageDiceOverpowered },
|
||||
{ id: "overpowered2", label: "Overpowered 2", dice: actionItem.system.damageDiceOverpowered2 },
|
||||
].filter(t => t.dice)
|
||||
if (tiers.length > 1) {
|
||||
const tierChoice = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Choose Damage Tier" },
|
||||
classes: ["lethalfantasy"],
|
||||
content: await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/damage-tier.hbs", {itemName: selectedItem.name}),
|
||||
buttons: tiers.map(t => ({
|
||||
action: t.id,
|
||||
type: "button",
|
||||
label: `${t.label} (${t.dice.toUpperCase()})`,
|
||||
icon: "fa-solid fa-wand-magic-sparkles",
|
||||
callback: () => t.id
|
||||
})),
|
||||
rejectClose: false
|
||||
})
|
||||
actionItem.damageTier = tierChoice || "standard"
|
||||
}
|
||||
}
|
||||
actionItem.castingTime = 1
|
||||
actionItem.spellStatus = "castingTime"
|
||||
// Set the flag on the combatant
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", actionItem)
|
||||
let message = `${combatant.name} action : ${selectedItem.name}, start rolling progression dice or casting time`
|
||||
await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
rollContext = (actionItem.type === "weapon") ? "rollProgressionDice" : "rollLethargyDice" // Set the roll context to rollProgressionDice
|
||||
currentAction = actionItem
|
||||
}
|
||||
|
||||
if (currentAction) {
|
||||
if (rollContext === "rollLethargyDice") {
|
||||
if (currentAction.spellStatus === "castingTime") {
|
||||
let time = currentAction.type === "spell" ? currentAction.system.castingTime : currentAction.system.prayerTime
|
||||
if (currentAction.castingTime < time) {
|
||||
let message = `Casting time : ${currentAction.name}, count : ${currentAction.castingTime}/${time}`
|
||||
await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
currentAction.castingTime += 1
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
|
||||
return
|
||||
} else {
|
||||
// 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 !`
|
||||
await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
currentAction.castingTime = 1
|
||||
currentAction.spellStatus = "toBeCasted"
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
|
||||
return
|
||||
}
|
||||
}
|
||||
if (currentAction.spellStatus === "toBeCasted") {
|
||||
combatant.actor.prepareRoll((currentAction.type === "spell") ? "spell-attack" : "miracle-attack", currentAction._id)
|
||||
if (currentAction.type === "spell") {
|
||||
currentAction.spellStatus = "lethargy"
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
|
||||
} else {
|
||||
// No lethargy for miracle
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", "")
|
||||
}
|
||||
return
|
||||
}
|
||||
if (currentAction.spellStatus === "lethargy") {
|
||||
// Roll lethargy dice
|
||||
let dice = LethalFantasyUtils.getLethargyDice(currentAction.system.level)
|
||||
let roll = new Roll(dice)
|
||||
await roll.evaluate()
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.showForRoll(roll)
|
||||
}
|
||||
let max = roll.dice[0].faces - 1
|
||||
let toCompare = Math.min(currentAction.progressionCount, max)
|
||||
if (roll.total <= toCompare) {
|
||||
// Notify that the player can act now with a chat message
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
|
||||
{
|
||||
success: true,
|
||||
actorName: combatant.actor.name,
|
||||
weaponName: currentAction.name,
|
||||
rollResult: roll.total,
|
||||
isLethargy: true
|
||||
}
|
||||
)
|
||||
await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
// Update the combatant progression count
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", "")
|
||||
// Display the action selection window again
|
||||
combatant.actor.system.rollProgressionDice(options.combatId, options.combatantId)
|
||||
} else {
|
||||
// Notify that the player cannot act now with a chat message
|
||||
currentAction.progressionCount += 1
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
|
||||
{
|
||||
success: false,
|
||||
actorName: combatant.actor.name,
|
||||
weaponName: currentAction.name,
|
||||
rollResult: roll.total,
|
||||
progressionCount: currentAction.progressionCount,
|
||||
isLethargy: true
|
||||
}
|
||||
)
|
||||
await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rollContext === "rollProgressionDice") {
|
||||
let formula = currentAction.system.combatProgressionDice
|
||||
if (currentAction?.rangedMode) {
|
||||
let toSplit = currentAction.system.speed[currentAction.rangedMode]
|
||||
let split = toSplit.split("+")
|
||||
currentAction.rangedLoad = Number(split[0]) || 0
|
||||
formula = split[1]
|
||||
log("Ranged Mode", currentAction.rangedMode, currentAction.rangedLoad, formula)
|
||||
}
|
||||
// Range weapon loading
|
||||
if (!currentAction.weaponLoaded && currentAction.rangedLoad) {
|
||||
if (currentAction.progressionCount < currentAction.rangedLoad) {
|
||||
let message = `Ranged weapon ${currentAction.name} is loading, loading count : ${currentAction.progressionCount}/${currentAction.rangedLoad}`
|
||||
await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
currentAction.progressionCount += 1
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
|
||||
} else {
|
||||
let message = `Ranged weapon ${currentAction.name} is loaded !`
|
||||
await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
currentAction.weaponLoaded = true
|
||||
currentAction.progressionCount = 1
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Melee mode
|
||||
let isMonster = combatant.actor.type === "monster"
|
||||
// Get the dice and roll it if
|
||||
let roll = new Roll(formula)
|
||||
await roll.evaluate()
|
||||
|
||||
let max = roll.dice[0].faces - 1
|
||||
max = Math.min(currentAction.progressionCount, max)
|
||||
let msg = await roll.toMessage({ flavor: `Progression Roll for ${currentAction.name}, progression count : ${currentAction.progressionCount}/${max}` }, { messageMode: rollContext.visibility })
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.waitFor3DAnimationByMessageID(msg.id)
|
||||
}
|
||||
|
||||
if (roll.total <= max) {
|
||||
// Notify that the player can act now with a chat message
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
|
||||
{
|
||||
success: true,
|
||||
actorName: combatant.actor.name,
|
||||
weaponName: currentAction.name,
|
||||
rollResult: roll.total
|
||||
}
|
||||
)
|
||||
await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", "")
|
||||
combatant.actor.prepareRoll(currentAction.type === "weapon" ? "weapon-attack" : "spell-attack", currentAction._id)
|
||||
} else {
|
||||
// Notify that the player cannot act now with a chat message
|
||||
currentAction.progressionCount += 1
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
|
||||
{
|
||||
success: false,
|
||||
actorName: combatant.actor.name,
|
||||
weaponName: currentAction.name,
|
||||
rollResult: roll.total,
|
||||
progressionCount: currentAction.progressionCount
|
||||
}
|
||||
)
|
||||
await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ***********************************************************/
|
||||
export async function promptRangedDefense(options = {}) {
|
||||
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes);
|
||||
const fieldRollMode = new foundry.data.fields.StringField({
|
||||
choices: rollModes,
|
||||
blank: false,
|
||||
default: "public",
|
||||
})
|
||||
|
||||
let dialogContext = {
|
||||
movementChoices: SYSTEM.MOVEMENT_CHOICES,
|
||||
moveDirectionChoices: SYSTEM.MOVE_DIRECTION_CHOICES,
|
||||
sizeChoices: SYSTEM.SIZE_CHOICES,
|
||||
rangeChoices: SYSTEM.RANGE_CHOICES,
|
||||
attackerAimChoices: SYSTEM.ATTACKER_AIM_CHOICES,
|
||||
movement: "none",
|
||||
moveDirection: "none",
|
||||
size: "+5",
|
||||
range: "short",
|
||||
attackerAim: "simple",
|
||||
fieldRollMode,
|
||||
rollModes
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/range-defense-dialog.hbs", dialogContext)
|
||||
|
||||
const label = game.i18n.localize("LETHALFANTASY.Label.rangeDefenseRoll")
|
||||
const rollContext = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Range Defense" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "rangeDefense",
|
||||
type: "button",
|
||||
label: label,
|
||||
callback: (event, button) => {
|
||||
const output = Array.from(button.form.elements).reduce((obj, input) => {
|
||||
if (input.name) obj[input.name] = input.value
|
||||
return obj
|
||||
}, {})
|
||||
return output
|
||||
},
|
||||
},
|
||||
],
|
||||
rejectClose: false // Click on Close button will not launch an error
|
||||
})
|
||||
|
||||
// If the user cancels the dialog, exit
|
||||
if (rollContext === null) return
|
||||
|
||||
log("RollContext", rollContext)
|
||||
// Add disfavor/favor option if point blank range
|
||||
if (rollContext.range === "pointblank") {
|
||||
rollContext.movement = rollContext.movement.replace("kh", "")
|
||||
rollContext.movement = rollContext.movement.replace("kl", "")
|
||||
rollContext.movement += "kl" // Add the kl to the movement (disfavor for point blank range)
|
||||
rollContext.range = "0"
|
||||
}
|
||||
if (rollContext.range === "beyondskill") {
|
||||
rollContext.movement = rollContext.movement.replace("kh", "")
|
||||
rollContext.movement = rollContext.movement.replace("kl", "")
|
||||
rollContext.movement += "kh" // Add the kl to the movement (favor for point blank range)
|
||||
rollContext.range = "+11"
|
||||
}
|
||||
|
||||
// Build the final modifier
|
||||
let fullModifier = Number(rollContext.moveDirection) +
|
||||
Number(rollContext.size) +
|
||||
Number(rollContext.range) +
|
||||
Number(rollContext?.attackerAim || 0)
|
||||
|
||||
let modifierFormula
|
||||
if (fullModifier === 0) {
|
||||
modifierFormula = "0"
|
||||
} else {
|
||||
let modAbs = Math.abs(fullModifier)
|
||||
modifierFormula = `D${modAbs + 1} -1`
|
||||
}
|
||||
|
||||
let rollData = { ...rollContext }
|
||||
// Merge rollContext object into options object
|
||||
options = { ...options, ...rollContext }
|
||||
options.rollName = "Ranged Defense"
|
||||
options.rollType = "weapon-defense"
|
||||
options.type = options.rollType // Required: this.type reads options.type
|
||||
options.rollMode = rollContext.visibility // Required: callers pass roll.options.rollMode to toMessage
|
||||
|
||||
const rollBase = new this(rollContext.movement, options.data, rollData)
|
||||
const rollModifier = new Roll(modifierFormula, options.data, rollData)
|
||||
await rollModifier.evaluate()
|
||||
await rollBase.evaluate()
|
||||
let rollD30 = await new Roll("1D30").evaluate()
|
||||
options.D30result = rollD30.total
|
||||
options.D30message = D30Roll.getResult(rollD30.total, options.rollType, options.rollTarget?.weapon, { isRanged: true })
|
||||
|
||||
let badResult = 0
|
||||
if (rollContext.movement.includes("kh")) {
|
||||
rollData.favor = "favor"
|
||||
badResult = Math.min(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 20)
|
||||
}
|
||||
if (rollContext.movement.includes("kl")) {
|
||||
rollData.favor = "disfavor"
|
||||
badResult = Math.max(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 1)
|
||||
}
|
||||
let dice = rollContext.movement
|
||||
let maxValue = 20 // As per latest changes (was : Number(dice.match(/\d+$/)[0])
|
||||
let rollTotal = -1
|
||||
let diceResults = []
|
||||
let resultType
|
||||
|
||||
let diceResult = rollBase.dice[0].results[0].result
|
||||
diceResults.push({ dice: `${dice.toUpperCase()}`, value: diceResult })
|
||||
let diceSum = diceResult
|
||||
while (diceResult === maxValue) {
|
||||
let r = await new Roll(dice).evaluate()
|
||||
diceResult = r.dice[0].results[0].result
|
||||
diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 })
|
||||
diceSum += (diceResult - 1)
|
||||
rollBase.dice[0].results.push({ result: diceResult, active: true })
|
||||
}
|
||||
if (fullModifier !== 0) {
|
||||
diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total })
|
||||
if (fullModifier < 0) {
|
||||
rollTotal = Math.max(diceSum - rollModifier.total, 0)
|
||||
} else {
|
||||
rollTotal = diceSum + rollModifier.total
|
||||
}
|
||||
} else {
|
||||
rollTotal = diceSum
|
||||
}
|
||||
rollBase.options = { ...rollBase.options, ...options }
|
||||
rollBase.options.resultType = resultType
|
||||
rollBase.options.rollTotal = rollTotal
|
||||
rollBase.options.diceResults = diceResults
|
||||
rollBase.options.rollTarget = options.rollTarget
|
||||
rollBase.options.titleFormula = `1D20E + ${modifierFormula}`
|
||||
rollBase.options.D30result = options.D30result
|
||||
rollBase.options.D30message = options.D30message
|
||||
rollBase.options.rollName = "Ranged Defense"
|
||||
rollBase.options.badResult = badResult
|
||||
rollBase.options.rollData = foundry.utils.duplicate(rollData)
|
||||
|
||||
return rollBase
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the GM for ranged attack context (movement, range, target size, aim) when a monster
|
||||
* attacks with a ranged weapon, then evaluates an exploding D20 attack roll with the resulting modifiers.
|
||||
*
|
||||
* @param {Object} options Options for the roll.
|
||||
* @param {string} options.actorId The attacker actor ID.
|
||||
* @param {string} options.actorName The attacker actor name.
|
||||
* @param {Object} options.rollTarget The rollTarget containing attackModifier and related data.
|
||||
* @returns {Promise<LethalFantasyRoll|null>} The resulting roll, or null if cancelled.
|
||||
*/
|
||||
export async function promptRangedAttack(options = {}) {
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes)
|
||||
const fieldRollMode = new foundry.data.fields.StringField({
|
||||
choices: rollModes,
|
||||
blank: false,
|
||||
default: "public",
|
||||
})
|
||||
|
||||
let dialogContext = {
|
||||
attackerMovementChoices: SYSTEM.ATTACKER_MOVEMENT_CHOICES,
|
||||
rangeChoices: SYSTEM.RANGE_CHOICES,
|
||||
sizeChoices: SYSTEM.SIZE_CHOICES,
|
||||
attackerAimChoices: SYSTEM.ATTACKER_AIM_CHOICES,
|
||||
movement: "none",
|
||||
range: "short",
|
||||
size: "+5",
|
||||
attackerAim: "simple",
|
||||
fieldRollMode,
|
||||
rollModes
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/range-attack-dialog.hbs", dialogContext)
|
||||
|
||||
const label = game.i18n.localize("LETHALFANTASY.Label.rangeAttackRoll")
|
||||
const rollContext = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Ranged Attack" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "rangedAttack",
|
||||
type: "button",
|
||||
label,
|
||||
callback: (event, button) => {
|
||||
const output = Array.from(button.form.elements).reduce((obj, input) => {
|
||||
if (input.name) obj[input.name] = input.value
|
||||
return obj
|
||||
}, {})
|
||||
return output
|
||||
},
|
||||
},
|
||||
],
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
if (rollContext === null) return null
|
||||
|
||||
// Handle pointblank: attacker at point blank gets favor (standing still easier to aim)
|
||||
if (rollContext.range === "pointblank") {
|
||||
rollContext.movement = rollContext.movement.replace("kh", "")
|
||||
rollContext.movement = rollContext.movement.replace("kl", "")
|
||||
rollContext.movement += "kh" // Favor for attacker at point blank
|
||||
rollContext.range = "0"
|
||||
}
|
||||
// Handle beyondskill: extreme range gives disfavor to attacker
|
||||
if (rollContext.range === "beyondskill") {
|
||||
rollContext.movement = rollContext.movement.replace("kh", "")
|
||||
rollContext.movement = rollContext.movement.replace("kl", "")
|
||||
rollContext.movement += "kl" // Disfavor for attacker beyond skill range
|
||||
rollContext.range = "+11"
|
||||
}
|
||||
|
||||
// Compute contextual penalty: range + target_size, reduced by aim bonus and attack modifier
|
||||
const attackModifier = options.rollTarget?.attackModifier ?? 0
|
||||
const contextualPenalty = Number(rollContext.range) + Number(rollContext.size)
|
||||
const aimBonus = Number(rollContext.attackerAim || 0)
|
||||
const fullModifier = contextualPenalty - aimBonus - attackModifier
|
||||
|
||||
let modifierFormula
|
||||
if (fullModifier === 0) {
|
||||
modifierFormula = "0"
|
||||
} else {
|
||||
const modAbs = Math.abs(fullModifier)
|
||||
modifierFormula = `D${modAbs + 1} -1`
|
||||
}
|
||||
|
||||
const rollData = { ...rollContext }
|
||||
options = { ...options, ...rollContext }
|
||||
options.rollName = "Ranged Attack"
|
||||
options.rollType = options.rollType || "monster-attack"
|
||||
options.type = options.rollType // Required: this.type reads options.type, used to build weaponDamageOptions in toHTML
|
||||
options.rollMode = rollContext.visibility // Required: callers pass roll.options.rollMode to toMessage
|
||||
options.isRangedAttack = true
|
||||
|
||||
const rollBase = new this(rollContext.movement, options.data, rollData)
|
||||
const rollModifier = new Roll(modifierFormula, options.data, rollData)
|
||||
await rollModifier.evaluate()
|
||||
await rollBase.evaluate()
|
||||
const rollD30 = await new Roll("1D30").evaluate()
|
||||
options.D30result = rollD30.total
|
||||
options.D30message = D30Roll.getResult(rollD30.total, options.rollType, undefined, { isRanged: true })
|
||||
|
||||
// Determine favor from dice formula
|
||||
let badResult = 0
|
||||
if (rollContext.movement.includes("kh")) {
|
||||
rollData.favor = "favor"
|
||||
badResult = Math.min(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 20)
|
||||
}
|
||||
if (rollContext.movement.includes("kl")) {
|
||||
rollData.favor = "disfavor"
|
||||
badResult = Math.max(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 1)
|
||||
}
|
||||
|
||||
const dice = rollContext.movement
|
||||
const maxValue = 20
|
||||
let rollTotal = -1
|
||||
let diceResults = []
|
||||
|
||||
let diceResult = rollBase.dice[0].results[0].result
|
||||
diceResults.push({ dice: `${dice.toUpperCase()}`, value: diceResult })
|
||||
let diceSum = diceResult
|
||||
// Exploding dice
|
||||
while (diceResult === maxValue) {
|
||||
const r = await new Roll(dice).evaluate()
|
||||
diceResult = r.dice[0].results[0].result
|
||||
diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 })
|
||||
diceSum += (diceResult - 1)
|
||||
rollBase.dice[0].results.push({ result: diceResult, active: true })
|
||||
}
|
||||
|
||||
if (fullModifier !== 0) {
|
||||
diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total })
|
||||
if (fullModifier > 0) {
|
||||
// Net penalty: subtract from roll
|
||||
rollTotal = Math.max(diceSum - rollModifier.total, 0)
|
||||
} else {
|
||||
// Net bonus: add to roll
|
||||
rollTotal = diceSum + rollModifier.total
|
||||
}
|
||||
} else {
|
||||
rollTotal = diceSum
|
||||
}
|
||||
|
||||
rollBase.options = { ...rollBase.options, ...options }
|
||||
rollBase.options.resultType = undefined
|
||||
rollBase.options.rollTotal = rollTotal
|
||||
rollBase.options.diceResults = diceResults
|
||||
rollBase.options.rollTarget = options.rollTarget
|
||||
rollBase.options.titleFormula = `1D20E + ${modifierFormula}`
|
||||
rollBase.options.D30result = options.D30result
|
||||
rollBase.options.D30message = options.D30message
|
||||
rollBase.options.rollName = "Ranged Attack"
|
||||
rollBase.options.badResult = badResult
|
||||
rollBase.options.rollData = foundry.utils.duplicate(rollData)
|
||||
|
||||
return rollBase
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Evaluate a spell/miracle damage formula with per-die explosion, then post to chat.
|
||||
* Explosion dice are shown manually via showForRoll; the main roll is shown automatically
|
||||
* by toMessage() (which triggers Dice So Nice via its createChatMessage hook).
|
||||
* Append "NE" to the formula to disable explosion.
|
||||
*
|
||||
* @param {string} formula Dice formula, e.g. "1d8", "2d6", "1d8NE"
|
||||
* @param {Object} rollOpts Options for LethalFantasyRoll (rollType, actorId, defenderId, etc.)
|
||||
* @returns {Promise<ChatMessage>}
|
||||
*/
|
||||
export async function rollSpellDamageToMessage(formula, rollOpts) {
|
||||
const roll = new this(formula, {}, rollOpts)
|
||||
await roll.evaluate()
|
||||
const shouldExplode = !/NE$/i.test(formula)
|
||||
const diceResults = []
|
||||
let diceSum = 0
|
||||
for (const term of roll.dice) {
|
||||
const singleDice = `1D${term.faces}`
|
||||
const termResults = Array.from(term.results)
|
||||
for (const r of termResults) {
|
||||
let diceResult = r.result
|
||||
diceResults.push({ dice: singleDice.toUpperCase(), value: diceResult })
|
||||
diceSum += diceResult
|
||||
if (shouldExplode && term.faces > 0) {
|
||||
while (diceResult === term.faces) {
|
||||
const xr = await new Roll(singleDice).evaluate()
|
||||
// Optional chaining guards against unexpected roll structure
|
||||
diceResult = xr.dice?.[0]?.results?.[0]?.result ?? (term.faces - 1)
|
||||
diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 })
|
||||
diceSum += (diceResult - 1)
|
||||
term.results.push({ result: diceResult, active: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
roll.options.diceResults = diceResults
|
||||
roll.options.rollTotal = diceSum
|
||||
return roll.toMessage()
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
import D30Roll from "./d30-roll.mjs"
|
||||
|
||||
/**
|
||||
* Prompt the user with a dialog to configure and execute a roll.
|
||||
*
|
||||
* @param {Object} options Configuration options for the roll.
|
||||
* @param {string} options.rollType The type of roll being performed (e.g., RESOURCE, DAMAGE, ATTACK, SAVE).
|
||||
* @param {string} options.rollValue The initial value or formula for the roll.
|
||||
* @param {string} options.rollTarget The target of the roll.
|
||||
* @param {"="|"+"|"++"|"-"|"--"} options.rollAdvantage If there is an avantage (+), a disadvantage (-), a double advantage (++), a double disadvantage (--) or a normal roll (=).
|
||||
* @param {string} options.actorId The ID of the actor performing the roll.
|
||||
* @param {string} options.actorName The name of the actor performing the roll.
|
||||
* @param {string} options.actorImage The image of the actor performing the roll.
|
||||
* @param {boolean} options.hasTarget Whether the roll has a target.
|
||||
* @param {Object} options.target The target of the roll, if any.
|
||||
* @param {Object} options.data Additional data for the roll.
|
||||
*
|
||||
* @returns {Promise<Object|null>} The roll result or null if the dialog was cancelled.
|
||||
*/
|
||||
export async function prompt(options = {}) {
|
||||
try {
|
||||
let dice = "1D20"
|
||||
let maxValue = 20
|
||||
let baseFormula = "1D20"
|
||||
let modifierFormula = "1D0"
|
||||
let hasModifier = true
|
||||
let hasChangeDice = false
|
||||
let hasD30 = false
|
||||
let hasFavor = false
|
||||
let hasMaxValue = true
|
||||
let hasGrantedDice = false
|
||||
let pointBlank = false
|
||||
let letItFly = false
|
||||
let saveSpell = game.lethalFantasy?.spellDefense ?? false
|
||||
let beyondSkill = false
|
||||
let hasStaticModifier = false
|
||||
let hasExplode = true
|
||||
let actor = game.actors.get(options.actorId)
|
||||
|
||||
if (options.rollType === "challenge" || options.rollType === "save") {
|
||||
options.rollName = game.i18n.localize(`LETHALFANTASY.Label.${options.rollTarget.rollKey}`)
|
||||
hasD30 = options.rollType === "save"
|
||||
if (options.rollTarget.rollKey === "dying") {
|
||||
dice = options.rollTarget.value
|
||||
hasModifier = false
|
||||
hasChangeDice = true
|
||||
hasFavor = true
|
||||
} else {
|
||||
dice = "1D20"
|
||||
hasFavor = true
|
||||
}
|
||||
|
||||
} else if (options.rollType === "granted") {
|
||||
hasD30 = false
|
||||
options.rollName = `Granted ${options.rollTarget.rollKey}`
|
||||
dice = options.rollTarget.formula
|
||||
baseFormula = options.rollTarget.formula
|
||||
hasModifier = false
|
||||
hasMaxValue = false
|
||||
hasChangeDice = false
|
||||
hasFavor = false
|
||||
|
||||
} else if (options.rollType === "monster-attack" || options.rollType === "monster-defense") {
|
||||
hasD30 = true
|
||||
options.rollName = options.rollTarget.name
|
||||
dice = "1D20"
|
||||
baseFormula = "D20"
|
||||
hasModifier = true
|
||||
hasChangeDice = false
|
||||
hasFavor = true
|
||||
if (options.rollType === "monster-attack") {
|
||||
options.rollTarget.value = options.rollTarget.attackModifier
|
||||
options.rollTarget.charModifier = 0
|
||||
} else {
|
||||
options.rollTarget.value = options.rollTarget.defenseModifier
|
||||
options.rollTarget.charModifier = 0
|
||||
options.isRangedDefense = options.rollTarget.isRangedDefense ?? false
|
||||
}
|
||||
|
||||
} else if (options.rollType === "monster-skill") {
|
||||
options.rollName = game.i18n.localize(`LETHALFANTASY.Label.${options.rollTarget.rollKey}`)
|
||||
dice = "1D20"
|
||||
baseFormula = "D20"
|
||||
hasModifier = true
|
||||
hasFavor = true
|
||||
hasChangeDice = false
|
||||
|
||||
} else if (options.rollType === "skill") {
|
||||
options.rollName = options.rollTarget.name
|
||||
hasD30 = true
|
||||
dice = "1D20"
|
||||
baseFormula = "D20"
|
||||
hasModifier = true
|
||||
hasFavor = true
|
||||
hasChangeDice = false
|
||||
options.rollTarget.value = Math.floor(options.rollTarget.system.skillTotal / 10)
|
||||
|
||||
} else if (options.rollType === "weapon-attack" || options.rollType === "weapon-defense") {
|
||||
hasD30 = true
|
||||
options.rollName = options.rollTarget.name
|
||||
dice = "1D20"
|
||||
baseFormula = "D20"
|
||||
hasModifier = true
|
||||
hasChangeDice = false
|
||||
hasFavor = true
|
||||
if (options.rollType === "weapon-attack") {
|
||||
if (options.rollTarget.weapon.system.weaponType === "melee") {
|
||||
options.rollTarget.value = options.rollTarget.combat.attackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus
|
||||
options.rollTarget.charModifier = options.rollTarget.combat.attackModifier
|
||||
} else {
|
||||
options.rollTarget.value = options.rollTarget.combat.rangedAttackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus
|
||||
options.rollTarget.charModifier = options.rollTarget.combat.rangedAttackModifier
|
||||
}
|
||||
} else {
|
||||
// For defense, check if it's a ranged defense
|
||||
const defenseModifier = options.rollTarget.isRangedDefense
|
||||
? options.rollTarget.combat.rangedDefenseModifier
|
||||
: options.rollTarget.combat.defenseModifier
|
||||
options.rollTarget.value = defenseModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.defenseBonus + options.rollTarget.armorDefense
|
||||
options.rollTarget.charModifier = defenseModifier
|
||||
// Store isRanged flag for D30 lookup
|
||||
options.isRangedDefense = options.rollTarget.isRangedDefense
|
||||
}
|
||||
|
||||
} else if (options.rollType === "spell" || options.rollType === "spell-attack" || options.rollType === "spell-power") {
|
||||
hasD30 = true
|
||||
options.rollName = options.rollTarget.name
|
||||
dice = "1D20"
|
||||
baseFormula = "D20"
|
||||
hasModifier = true
|
||||
hasChangeDice = false
|
||||
hasFavor = true
|
||||
options.rollTarget.value = options.rollTarget.actorModifiers.levelSpellModifier + options.rollTarget.actorModifiers.intSpellModifier
|
||||
options.rollTarget.charModifier = options.rollTarget.actorModifiers.intSpellModifier
|
||||
hasStaticModifier = options.rollType === "spell-power"
|
||||
//hasModifier = options.rollType !== "spell-attack"
|
||||
if (hasStaticModifier) {
|
||||
options.rollTarget.staticModifier = options.rollTarget.actorLevel
|
||||
} else {
|
||||
options.rollTarget.staticModifier = 0
|
||||
}
|
||||
|
||||
} else if (options.rollType === "miracle" || options.rollType === "miracle-attack" || options.rollType === "miracle-power") {
|
||||
hasD30 = true
|
||||
options.rollName = options.rollTarget.name
|
||||
dice = "1D20"
|
||||
baseFormula = "D20"
|
||||
hasChangeDice = false
|
||||
hasFavor = true
|
||||
options.rollTarget.value = options.rollTarget.actorModifiers.levelMiracleModifier + options.rollTarget.actorModifiers.chaMiracleModifier
|
||||
options.rollTarget.charModifier = options.rollTarget.actorModifiers.chaMiracleModifier
|
||||
hasStaticModifier = options.rollType === "miracle-power"
|
||||
//hasModifier = options.rollType !== "miracle-attack"
|
||||
if (hasStaticModifier) {
|
||||
options.rollTarget.staticModifier = options.rollTarget.actorLevel
|
||||
} else {
|
||||
options.rollTarget.staticModifier = 0
|
||||
}
|
||||
|
||||
} else if (options.rollType === "shield-roll") {
|
||||
hasD30 = false
|
||||
options.rollName = "Shield Defense"
|
||||
dice = options.rollTarget.system.defense.toUpperCase()
|
||||
baseFormula = dice
|
||||
hasModifier = true
|
||||
hasChangeDice = false
|
||||
hasMaxValue = false
|
||||
hasExplode = false
|
||||
hasFavor = true
|
||||
options.rollTarget.value = 0
|
||||
|
||||
} else if (options.rollType.includes("weapon-damage")) {
|
||||
options.rollName = options.rollTarget.name
|
||||
options.isDamage = true
|
||||
hasModifier = true
|
||||
hasChangeDice = false
|
||||
let damageBonus = (options.rollTarget.weapon.system.applyStrengthDamageBonus) ? options.rollTarget.combat.damageModifier : 0
|
||||
options.rollTarget.value = damageBonus + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.damageBonus
|
||||
options.rollTarget.charModifier = damageBonus
|
||||
dice = options.rollTarget.weapon.system.damage.damageM
|
||||
if (/NE$/i.test(dice)) {
|
||||
hasMaxValue = false
|
||||
hasExplode = false
|
||||
}
|
||||
dice = dice.replace(/NE$/i, "").replace("E", "")
|
||||
baseFormula = dice
|
||||
|
||||
} else if (options.rollType.includes("monster-damage")) {
|
||||
options.rollName = options.rollTarget.name
|
||||
options.isDamage = true
|
||||
hasModifier = true
|
||||
hasChangeDice = false
|
||||
options.rollTarget.value = options.rollTarget.damageModifier
|
||||
options.rollTarget.charModifier = 0
|
||||
dice = options.rollTarget.damageDice
|
||||
dice = dice.replace("E", "")
|
||||
baseFormula = dice
|
||||
if (options.rollTarget.noExplode) {
|
||||
hasMaxValue = false
|
||||
hasExplode = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (options.rollType === "save" && (options.rollTarget.rollKey === "pain" || options.rollTarget.rollKey === "paincourage")) {
|
||||
dice = options.rollTarget.rollDice
|
||||
baseFormula = options.rollTarget.rollDice
|
||||
hasModifier = false
|
||||
}
|
||||
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes);
|
||||
|
||||
|
||||
const fieldRollMode = new foundry.data.fields.StringField({
|
||||
choices: rollModes,
|
||||
blank: false,
|
||||
default: "public",
|
||||
})
|
||||
|
||||
const choiceModifier = SYSTEM.CHOICE_MODIFIERS
|
||||
const choiceDice = SYSTEM.CHOICE_DICE
|
||||
const choiceFavor = SYSTEM.FAVOR_CHOICES
|
||||
|
||||
let modifier = "+0"
|
||||
let targetName
|
||||
|
||||
// True for any ranged attack: PC weapon (ranged type) or monster attack (ranged mode)
|
||||
const isRangedAttack = (options.rollType === "weapon-attack" && options.rollTarget?.weapon?.system?.weaponType === "ranged")
|
||||
|| (options.rollType === "monster-attack" && options.rollTarget?.attackMode === "ranged")
|
||||
|
||||
let dialogContext = {
|
||||
rollType: options.rollType,
|
||||
rollTarget: options.rollTarget,
|
||||
rollName: options.rollName,
|
||||
actorName: options.actorName,
|
||||
rollModes,
|
||||
hasModifier,
|
||||
hasFavor,
|
||||
hasChangeDice,
|
||||
pointBlank,
|
||||
baseValue: options.rollTarget.value,
|
||||
attackerAimChoices: SYSTEM.ATTACKER_AIM_CHOICES,
|
||||
attackerAim: "0",
|
||||
changeDice: `${dice}`,
|
||||
fieldRollMode,
|
||||
choiceModifier,
|
||||
choiceDice,
|
||||
choiceFavor,
|
||||
baseFormula,
|
||||
dice,
|
||||
hasTarget: options.hasTarget,
|
||||
modifier,
|
||||
saveSpell,
|
||||
favor: "none",
|
||||
targetName,
|
||||
isRangedAttack
|
||||
}
|
||||
let rollContext
|
||||
if (options.rollContext) {
|
||||
rollContext = foundry.utils.duplicate(options.rollContext)
|
||||
hasGrantedDice = !!rollContext.hasGrantedDice
|
||||
pointBlank = !!rollContext.pointBlank
|
||||
beyondSkill = !!rollContext.beyondSkill
|
||||
letItFly = !!rollContext.letItFly
|
||||
saveSpell = !!rollContext.saveSpell
|
||||
const _rawMode = rollContext.rollMode || game.settings.get("core", "rollMode")
|
||||
const _modeMap = { publicroll: "public", gmroll: "gm", blindroll: "blind", selfroll: "self" }
|
||||
rollContext.visibility ||= _modeMap[_rawMode] ?? _rawMode ?? "public"
|
||||
rollContext.modifier ||= modifier
|
||||
rollContext.favor ||= "none"
|
||||
rollContext.changeDice ||= `${dice}`
|
||||
rollContext.attackerAim ||= "0"
|
||||
} else {
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/roll-dialog.hbs", dialogContext)
|
||||
|
||||
let position = game.user.getFlag(SYSTEM.id, "roll-dialog-pos") || { top: -1, left: -1 }
|
||||
const label = game.i18n.localize("LETHALFANTASY.Roll.roll")
|
||||
rollContext = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Roll dialog" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
position,
|
||||
buttons: [
|
||||
{
|
||||
action: "roll",
|
||||
type: "button",
|
||||
label: label,
|
||||
callback: (event, button, dialog) => {
|
||||
log("Roll context", event, button, dialog)
|
||||
let position = dialog?.position
|
||||
game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(position))
|
||||
const output = Array.from(button.form.elements).reduce((obj, input) => {
|
||||
if (input.name) obj[input.name] = input.value
|
||||
return obj
|
||||
}, {})
|
||||
return output
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: {
|
||||
"selectGranted": (event, button) => {
|
||||
hasGrantedDice = event.target.checked
|
||||
},
|
||||
"selectBeyondSkill": (event, button) => {
|
||||
beyondSkill = button.checked
|
||||
},
|
||||
"selectPointBlank": (event, button) => {
|
||||
pointBlank = button.checked
|
||||
},
|
||||
"selectLetItFly": (event, button) => {
|
||||
letItFly = button.checked
|
||||
},
|
||||
"saveSpellCheck": (event, button) => {
|
||||
saveSpell = button.checked
|
||||
},
|
||||
"gotoToken": (event, button) => {
|
||||
let tokenId = $(button).data("tokenId")
|
||||
let token = canvas.tokens?.get(tokenId)
|
||||
if (token) {
|
||||
canvas.animatePan({ x: token.x, y: token.y, duration: 200 })
|
||||
canvas.tokens.releaseAll()
|
||||
token.control({ releaseOthers: true })
|
||||
}
|
||||
}
|
||||
},
|
||||
rejectClose: false // Click on Close button will not launch an error
|
||||
})
|
||||
}
|
||||
|
||||
// If the user cancels the dialog, exit
|
||||
if (rollContext === null) return
|
||||
log("rollContext", rollContext, hasGrantedDice)
|
||||
rollContext.saveSpell = saveSpell // Update fucking flag
|
||||
|
||||
let fullModifier = 0
|
||||
let titleFormula = ""
|
||||
dice = rollContext.changeDice || dice
|
||||
if (hasModifier) {
|
||||
let bonus = Number(options.rollTarget.value)
|
||||
fullModifier = rollContext.modifier === "" ? 0 : parseInt(rollContext.modifier, 10) + bonus
|
||||
fullModifier += (rollContext.saveSpell) ? (options.rollTarget.actorModifiers?.saveModifier ?? 0) : 0
|
||||
if (Number(rollContext.attackerAim) > 0) {
|
||||
fullModifier += Number(rollContext.attackerAim)
|
||||
}
|
||||
|
||||
if (fullModifier === 0) {
|
||||
modifierFormula = "0"
|
||||
} else {
|
||||
let modAbs = Math.abs(fullModifier)
|
||||
modifierFormula = `D${modAbs + 1} - 1`
|
||||
}
|
||||
if (hasStaticModifier) {
|
||||
modifierFormula += ` + ${options.rollTarget.staticModifier}`
|
||||
}
|
||||
let sign = fullModifier < 0 ? "-" : "+"
|
||||
if (hasExplode) {
|
||||
titleFormula = `${dice}E ${sign} ${modifierFormula}`
|
||||
} else {
|
||||
titleFormula = `${dice} ${sign} ${modifierFormula}`
|
||||
}
|
||||
} else {
|
||||
modifierFormula = "0"
|
||||
fullModifier = 0
|
||||
baseFormula = `${dice}`
|
||||
if (hasExplode) {
|
||||
titleFormula = `${dice}E`
|
||||
} else {
|
||||
titleFormula = `${dice}`
|
||||
}
|
||||
}
|
||||
|
||||
// Latest addition : favor choice at point blank range
|
||||
if (pointBlank) {
|
||||
rollContext.favor = "favor"
|
||||
}
|
||||
if (beyondSkill) {
|
||||
rollContext.favor = "disfavor"
|
||||
}
|
||||
|
||||
// Specific pain case
|
||||
if (options.rollType === "save" && options.rollTarget.rollKey === "pain" || options.rollTarget.rollKey === "paincourage") {
|
||||
baseFormula = options.rollTarget.rollDice
|
||||
titleFormula = `${dice}`
|
||||
modifierFormula = "0"
|
||||
fullModifier = 0
|
||||
}
|
||||
|
||||
// Specific pain/poison/contagion case
|
||||
if (options.rollType === "save" && (options.rollTarget.rollKey === "poison" || options.rollTarget.rollKey === "contagion")) {
|
||||
hasD30 = false
|
||||
hasStaticModifier = true
|
||||
modifierFormula = ` + ${Math.abs(fullModifier)}`
|
||||
titleFormula = `${dice}E + ${Math.abs(fullModifier)}`
|
||||
}
|
||||
|
||||
if (letItFly) {
|
||||
baseFormula = "1D20"
|
||||
titleFormula = `1D20E`
|
||||
modifierFormula = "0"
|
||||
fullModifier = 0
|
||||
hasFavor = false
|
||||
hasExplode = true
|
||||
rollContext.favor = "none"
|
||||
}
|
||||
|
||||
const maxMatch = baseFormula ? baseFormula.match(/\d+$/) : null
|
||||
maxValue = maxMatch ? Number(maxMatch[0]) : 0
|
||||
|
||||
const rollData = {
|
||||
type: options.rollType,
|
||||
rollType: options.rollType,
|
||||
target: options.rollTarget,
|
||||
rollName: options.rollName,
|
||||
actorId: options.actorId,
|
||||
actorName: options.actorName,
|
||||
actorImage: options.actorImage,
|
||||
rollMode: rollContext.visibility,
|
||||
hasTarget: options.hasTarget,
|
||||
isDamage: options.isDamage,
|
||||
pointBlank,
|
||||
beyondSkill,
|
||||
letItFly,
|
||||
hasGrantedDice,
|
||||
titleFormula,
|
||||
targetName,
|
||||
...rollContext,
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook event that fires before the roll is made.
|
||||
* @function
|
||||
* @memberof hookEvents
|
||||
* @param {Object} options Options for the roll.
|
||||
* @param {Object} rollData All data related to the roll.
|
||||
* @returns {boolean} Explicitly return `false` to prevent roll to be made.
|
||||
*/
|
||||
if (Hooks.call("fvtt-lethal-fantasy.preRoll", options, rollData) === false) return
|
||||
|
||||
let rollBase = new this(baseFormula, options.data, rollData)
|
||||
const rollModifier = new Roll(modifierFormula, options.data, rollData)
|
||||
await rollModifier.evaluate()
|
||||
await rollBase.evaluate()
|
||||
|
||||
let rollFavor
|
||||
let badResult
|
||||
if (rollContext.favor === "favor") {
|
||||
rollFavor = new this(baseFormula, options.data, rollData)
|
||||
await rollFavor.evaluate()
|
||||
log("Favor dice", {
|
||||
rollBaseTotal: rollBase.total,
|
||||
rollFavorTotal: rollFavor.total,
|
||||
rollBaseResults: rollBase.dice.map(d => d.results.map(r => r.result)),
|
||||
rollFavorResults: rollFavor.dice.map(d => d.results.map(r => r.result)),
|
||||
baseFormula
|
||||
})
|
||||
if (game?.dice3d) {
|
||||
game.dice3d.showForRoll(rollFavor, game.user, true)
|
||||
}
|
||||
if (Number(rollFavor.result) > Number(rollBase.result)) {
|
||||
badResult = rollBase.result
|
||||
rollBase = rollFavor
|
||||
} else {
|
||||
badResult = rollFavor.result
|
||||
}
|
||||
rollFavor = null
|
||||
}
|
||||
|
||||
if (rollContext.favor === "disfavor") {
|
||||
rollFavor = new this(baseFormula, options.data, rollData)
|
||||
await rollFavor.evaluate()
|
||||
log("Disfavor dice", {
|
||||
rollBaseTotal: rollBase.total,
|
||||
rollFavorTotal: rollFavor.total,
|
||||
rollBaseResults: rollBase.dice.map(d => d.results.map(r => r.result)),
|
||||
rollFavorResults: rollFavor.dice.map(d => d.results.map(r => r.result)),
|
||||
baseFormula
|
||||
})
|
||||
if (game?.dice3d) {
|
||||
game.dice3d.showForRoll(rollFavor, game.user, true)
|
||||
}
|
||||
if (Number(rollFavor.result) < Number(rollBase.result)) {
|
||||
badResult = rollBase.result
|
||||
rollBase = rollFavor
|
||||
} else {
|
||||
badResult = rollFavor.result
|
||||
}
|
||||
rollFavor = null
|
||||
}
|
||||
|
||||
if (options.forceNoD30) {
|
||||
hasD30 = false
|
||||
}
|
||||
|
||||
if (hasD30) {
|
||||
let rollD30 = await new Roll("1D30").evaluate()
|
||||
if (game?.dice3d) {
|
||||
game.dice3d.showForRoll(rollD30, game.user, true)
|
||||
}
|
||||
options.D30result = rollD30.total
|
||||
|
||||
// Compute isRanged for D30: covers defense (isRangedDefense), monster ranged attacks (attackMode),
|
||||
// and PC weapon attacks (isRangedAttack or weaponType)
|
||||
const isRangedForD30 = options.isRangedDefense
|
||||
|| options.rollTarget?.attackMode === "ranged"
|
||||
|| options.rollTarget?.isRangedAttack === true
|
||||
|| options.rollTarget?.weapon?.system?.weaponType === "ranged"
|
||||
const d30Message = D30Roll.getResult(
|
||||
rollD30.total,
|
||||
options.rollType,
|
||||
options.rollTarget?.weapon,
|
||||
{ isRanged: isRangedForD30, isSpellSave: saveSpell }
|
||||
)
|
||||
options.D30message = d30Message
|
||||
}
|
||||
|
||||
let rollTotal = 0
|
||||
let diceResults = []
|
||||
let resultType
|
||||
let diceSum = 0
|
||||
|
||||
let singleDice = `1D${maxValue}`
|
||||
for (let i = 0; i < rollBase.dice.length; i++) {
|
||||
const dieResults = rollBase.dice[i].results
|
||||
const resultCount = dieResults.length
|
||||
for (let j = 0; j < resultCount; j++) {
|
||||
let diceResult = dieResults[j].result
|
||||
diceResults.push({ dice: `${singleDice.toUpperCase()}`, value: diceResult })
|
||||
diceSum += diceResult
|
||||
if (hasMaxValue) {
|
||||
while (diceResult === maxValue) {
|
||||
let r = await new Roll(baseFormula).evaluate()
|
||||
diceResult = r.dice[0].results[0].result
|
||||
diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 })
|
||||
diceSum += (diceResult - 1)
|
||||
// Add to DieTerm results so DSN/Foundry display shows explosion dice
|
||||
dieResults.push({ result: diceResult, active: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasGrantedDice && options.rollTarget.grantedDice && options.rollTarget.grantedDice !== "") {
|
||||
titleFormula += ` + ${options.rollTarget.grantedDice.toUpperCase()}`
|
||||
let grantedRoll = new Roll(options.rollTarget.grantedDice)
|
||||
await grantedRoll.evaluate()
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.showForRoll(grantedRoll, game.user, true)
|
||||
}
|
||||
diceResults.push({ dice: `${options.rollTarget.grantedDice.toUpperCase()}`, value: grantedRoll.total })
|
||||
rollTotal += grantedRoll.total
|
||||
}
|
||||
|
||||
if (fullModifier !== 0) {
|
||||
diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total })
|
||||
if (fullModifier < 0) {
|
||||
rollTotal += Math.max(diceSum - rollModifier.total, 0)
|
||||
} else {
|
||||
rollTotal += diceSum + rollModifier.total
|
||||
}
|
||||
} else {
|
||||
rollTotal += diceSum
|
||||
}
|
||||
|
||||
rollBase.options.resultType = resultType
|
||||
rollBase.options.rollTotal = rollTotal
|
||||
rollBase.options.diceResults = diceResults
|
||||
rollBase.options.rollTarget = options.rollTarget
|
||||
rollBase.options.titleFormula = titleFormula
|
||||
rollBase.options.D30result = options.D30result
|
||||
rollBase.options.D30message = options.D30message
|
||||
rollBase.options.badResult = badResult
|
||||
rollBase.options.rollType = options.rollType
|
||||
rollBase.options.actorId = options.actorId
|
||||
rollBase.options.actorName = options.actorName
|
||||
rollBase.options.actorImage = options.actorImage
|
||||
rollBase.options.rollData = foundry.utils.duplicate(rollData)
|
||||
rollBase.options.defenderId = options.defenderId
|
||||
rollBase.options.defenderTokenId = options.defenderTokenId
|
||||
rollBase.options.extraShieldDr = options.extraShieldDr || 0
|
||||
rollBase.options.damageTier = options.damageTier || "standard"
|
||||
rollBase.options.d30Bleed = options.d30Bleed || false
|
||||
rollBase.options.d30DamageMultiplier = options.d30DamageMultiplier || 1
|
||||
rollBase.options.d30DrMultiplier = options.d30DrMultiplier || 1
|
||||
|
||||
/**
|
||||
* A hook event that fires after the roll has been made.
|
||||
* @function
|
||||
* @memberof hookEvents
|
||||
* @param {Object} options Options for the roll.
|
||||
* @param {Object} rollData All data related to the roll.
|
||||
* @param {LethalFantasyRoll} roll The resulting roll.
|
||||
* @returns {boolean} Explicitly return `false` to prevent roll to be made.
|
||||
*/
|
||||
if (Hooks.call("fvtt-lethal-fantasy.Roll", options, rollData, rollBase) === false) return
|
||||
|
||||
return rollBase
|
||||
} finally {
|
||||
// Clear one-shot flag so it doesn't leak to subsequent non-spell saves
|
||||
if (game.lethalFantasy) game.lethalFantasy.spellDefense = false
|
||||
}
|
||||
}
|
||||
+1
-1351
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -32,20 +32,20 @@ export class Macros {
|
||||
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}');`
|
||||
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")
|
||||
break
|
||||
|
||||
case "rollDamage":
|
||||
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 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)
|
||||
break
|
||||
|
||||
case "rollAttack":
|
||||
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")
|
||||
break
|
||||
|
||||
@@ -57,7 +57,7 @@ export class Macros {
|
||||
|
||||
/**
|
||||
* 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 {*} name
|
||||
* @param {*} command
|
||||
@@ -72,7 +72,7 @@ export class Macros {
|
||||
type: "script",
|
||||
img: img,
|
||||
command: command,
|
||||
flags: { "tenebris.macro": true },
|
||||
flags: { "lethalFantasy.macro": true },
|
||||
},
|
||||
{ displaySheet: false },
|
||||
)
|
||||
|
||||
@@ -65,7 +65,7 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
|
||||
}
|
||||
|
||||
schema.hp = new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
|
||||
value: new fields.NumberField({ ...requiredInteger, initial: 1 }),
|
||||
max: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
|
||||
painDamage: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
|
||||
wounds: new fields.ArrayField(new fields.SchemaField(woundFieldSchema), {
|
||||
@@ -281,7 +281,7 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
|
||||
* @param {"="|"+"|"++"|"-"|"--"} rollAdvantage If there is an avantage (+), a disadvantage (-), a double advantage (++), a double disadvantage (--) or a normal roll (=).
|
||||
* @returns {Promise<null>} - A promise that resolves to null if the roll is cancelled.
|
||||
*/
|
||||
async roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr = 0) {
|
||||
async roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr = 0, d30Effects = {}) {
|
||||
const hasTarget = false
|
||||
let roll = await LethalFantasyRoll.prompt({
|
||||
rollType,
|
||||
@@ -293,11 +293,15 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
|
||||
target: false,
|
||||
defenderId,
|
||||
defenderTokenId,
|
||||
extraShieldDr
|
||||
extraShieldDr,
|
||||
damageTier: rollTarget.damageTier || "standard",
|
||||
d30Bleed: d30Effects.d30Bleed || false,
|
||||
d30DamageMultiplier: d30Effects.d30DamageMultiplier || 1,
|
||||
d30DrMultiplier: d30Effects.d30DrMultiplier || 1
|
||||
})
|
||||
if (!roll) return null
|
||||
|
||||
await roll.toMessage({}, { rollMode: roll.options.rollMode })
|
||||
await roll.toMessage({}, { messageMode: roll.options.rollMode })
|
||||
}
|
||||
|
||||
async rollInitiative(combatId = undefined, combatantId = undefined) {
|
||||
@@ -318,7 +322,7 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
|
||||
})
|
||||
if (!roll) return null
|
||||
|
||||
await roll.toMessage({}, { rollMode: roll.options.rollMode })
|
||||
await roll.toMessage({}, { messageMode: roll.options.rollMode })
|
||||
}
|
||||
|
||||
async rollProgressionDice(combatId, combatantId, rollProgressionCount) {
|
||||
@@ -356,6 +360,11 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
|
||||
}
|
||||
}
|
||||
|
||||
if (weaponsChoices.length === 0) {
|
||||
ui.notifications.warn(`${this.parent.name} has no weapons or spells available for combat. Add a weapon to the character sheet first.`)
|
||||
return
|
||||
}
|
||||
|
||||
let roll = await LethalFantasyRoll.promptCombatAction({
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
|
||||
@@ -7,7 +7,7 @@ export default class LethalFantasyEquipment extends foundry.abstract.TypeDataMod
|
||||
const requiredInteger = { required: true, nullable: false, integer: true }
|
||||
|
||||
schema.description = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.category = new fields.StringField({ required: true, initial: "tinbit", choices: SYSTEM.EQUIPMENT_CATEGORIES })
|
||||
schema.category = new fields.StringField({ required: true, initial: "tinbit", choices: SYSTEM.EQUIPMENT_CATEGORY })
|
||||
|
||||
schema.encLoad = new fields.NumberField({ required: true, initial: 0, min: 0 })
|
||||
schema.hi = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 })
|
||||
|
||||
@@ -35,6 +35,9 @@ export default class LethalFantasyMiracle extends foundry.abstract.TypeDataModel
|
||||
|
||||
schema.attackRoll = new fields.StringField({ required: true, initial: "" })
|
||||
schema.powerRoll = new fields.StringField({ required: true, initial: "" })
|
||||
schema.damageDice = new fields.StringField({ required: false, initial: "" })
|
||||
schema.damageDiceOverpowered = new fields.StringField({ required: false, initial: "" })
|
||||
schema.damageDiceOverpowered2 = new fields.StringField({ required: false, initial: "" })
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
+66
-19
@@ -56,15 +56,33 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
}, {}),
|
||||
)
|
||||
|
||||
const woundFieldSchema = {
|
||||
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
|
||||
duration: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
|
||||
description: new fields.StringField({ initial: "", required: false, nullable: true }),
|
||||
}
|
||||
|
||||
schema.hp = new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
|
||||
value: new fields.NumberField({ ...requiredInteger, initial: 1 }),
|
||||
average: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
|
||||
max: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
|
||||
wounds: new fields.ArrayField(new fields.SchemaField(woundFieldSchema), {
|
||||
initial: [
|
||||
{ description: "", value: 0, duration: 0 },
|
||||
{ description: "", value: 0, duration: 0 },
|
||||
{ description: "", value: 0, duration: 0 },
|
||||
{ description: "", value: 0, duration: 0 },
|
||||
{ description: "", value: 0, duration: 0 },
|
||||
{ description: "", value: 0, duration: 0 },
|
||||
{ description: "", value: 0, duration: 0 },
|
||||
{ description: "", value: 0, duration: 0 }
|
||||
], min: 8
|
||||
}),
|
||||
damageResistance: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
|
||||
painDamage: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
|
||||
})
|
||||
|
||||
const attackField = (label) => {
|
||||
const attackField = (label, initialNoExplode = false) => {
|
||||
const schema = {
|
||||
key: new fields.StringField({ required: true, nullable: false, initial: `attack${label}` }),
|
||||
name: new fields.StringField({ required: true, nullable: false, initial: `Attack ${label}` }),
|
||||
@@ -74,6 +92,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
damageDice: new fields.StringField({ required: true, nullable: false, initial: "1D6" }),
|
||||
damageModifier: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
|
||||
enabled: new fields.BooleanField({ initial: true, required: true, nullable: false }),
|
||||
noExplode: new fields.BooleanField({ initial: initialNoExplode, required: true, nullable: false }),
|
||||
}
|
||||
return new fields.SchemaField(schema, { label })
|
||||
}
|
||||
@@ -123,8 +142,8 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
shieldDefenseDice: new fields.StringField({ required: true, nullable: false, initial: "d4" })
|
||||
})
|
||||
schema.combatHTH = new fields.SchemaField({
|
||||
attack1: attackField("1"),
|
||||
attack2: attackField("2")
|
||||
attack1: attackField("1", true),
|
||||
attack2: attackField("2", true)
|
||||
})
|
||||
|
||||
schema.attackMode = new fields.StringField({
|
||||
@@ -141,6 +160,15 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
attack4: attackField("4")
|
||||
})
|
||||
|
||||
schema.rangedWeaponRange = new fields.SchemaField({
|
||||
pointBlank: new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }),
|
||||
short: new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }),
|
||||
medium: new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }),
|
||||
long: new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }),
|
||||
extreme: new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }),
|
||||
outOfSkill: new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 })
|
||||
})
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
@@ -154,8 +182,24 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
* @param {"="|"+"|"++"|"-"|"--"} rollAdvantage If there is an avantage (+), a disadvantage (-), a double advantage (++), a double disadvantage (--) or a normal roll (=).
|
||||
* @returns {Promise<null>} - A promise that resolves to null if the roll is cancelled.
|
||||
*/
|
||||
async roll(rollType, rollTarget, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0) {
|
||||
async roll(rollType, rollTarget, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0, d30Effects = {}) {
|
||||
const hasTarget = false
|
||||
|
||||
// Ranged monster defense uses the ranged defense dialog (movement, range, size modifiers)
|
||||
if (rollType === "monster-defense" && rollTarget?.isRangedDefense === true) {
|
||||
let roll = await LethalFantasyRoll.promptRangedDefense({
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
rollTarget,
|
||||
defenderId,
|
||||
defenderTokenId,
|
||||
})
|
||||
if (!roll) return null
|
||||
await roll.toMessage({}, { messageMode: roll.options.rollMode })
|
||||
return
|
||||
}
|
||||
|
||||
let roll = await LethalFantasyRoll.prompt({
|
||||
rollType,
|
||||
rollTarget,
|
||||
@@ -166,14 +210,18 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
target: false,
|
||||
defenderId,
|
||||
defenderTokenId,
|
||||
extraShieldDr
|
||||
extraShieldDr,
|
||||
damageTier: rollTarget.damageTier || "standard",
|
||||
d30Bleed: d30Effects.d30Bleed || false,
|
||||
d30DamageMultiplier: d30Effects.d30DamageMultiplier || 1,
|
||||
d30DrMultiplier: d30Effects.d30DrMultiplier || 1
|
||||
})
|
||||
if (!roll) return null
|
||||
|
||||
await roll.toMessage({}, { rollMode: roll.options.rollMode })
|
||||
await roll.toMessage({}, { messageMode: roll.options.rollMode })
|
||||
}
|
||||
|
||||
async prepareMonsterRoll(rollType, rollKey, rollDice = undefined, tokenId = undefined, damageModifier = undefined, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0) {
|
||||
async prepareMonsterRoll(rollType, rollKey, rollDice = undefined, tokenId = undefined, damageModifier = undefined, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0, d30Effects = {}) {
|
||||
let rollTarget
|
||||
switch (rollType) {
|
||||
case "monster-attack":
|
||||
@@ -182,6 +230,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
const attacksSet = this.attackMode === "ranged" ? this.rangedAttacks : this.attacks
|
||||
rollTarget = foundry.utils.duplicate(attacksSet[rollKey])
|
||||
rollTarget.rollKey = rollKey
|
||||
rollTarget.attackMode = this.attackMode
|
||||
if (rollType === "monster-defense") {
|
||||
rollTarget.isRangedDefense = game.lethalFantasy?.nextDefenseData?.isRanged ?? false
|
||||
}
|
||||
@@ -222,11 +271,10 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
await roll.toMessage({
|
||||
flavor,
|
||||
speaker: ChatMessage.getSpeaker({ actor: this.parent })
|
||||
})
|
||||
}, { messageMode: roll.options.rollMode ?? game.settings.get("core", "rollMode") })
|
||||
return
|
||||
}
|
||||
case "weapon-damage-small":
|
||||
case "weapon-damage-medium":
|
||||
case "weapon-damage":
|
||||
case "weapon-attack":
|
||||
case "weapon-defense": {
|
||||
let weapon = this.actor.items.find((i) => i.type === "weapon" && i.id === rollKey)
|
||||
@@ -259,7 +307,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
return
|
||||
}
|
||||
rollTarget = skill
|
||||
rollTarget.weapon = weapon
|
||||
rollTarget.weapon = foundry.utils.duplicate(weapon)
|
||||
rollTarget.weaponSkillModifier = skill.weaponSkillModifier
|
||||
rollTarget.rollKey = rollKey
|
||||
rollTarget.combat = foundry.utils.duplicate(this.combat)
|
||||
@@ -273,8 +321,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
// In all cases
|
||||
if (rollTarget) {
|
||||
rollTarget.tokenId = tokenId
|
||||
console.log(rollTarget)
|
||||
await this.roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr)
|
||||
await this.roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr, d30Effects)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +341,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
})
|
||||
if (!roll) return null
|
||||
|
||||
await roll.toMessage({}, { rollMode: roll.options.rollMode })
|
||||
await roll.toMessage({}, { messageMode: roll.options.rollMode })
|
||||
}
|
||||
|
||||
async rollProgressionDice(combatId, combatantId) {
|
||||
@@ -306,7 +353,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
return
|
||||
}
|
||||
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes)
|
||||
const fieldRollMode = new foundry.data.fields.StringField({
|
||||
choices: rollModes,
|
||||
blank: false,
|
||||
@@ -336,7 +383,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
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
|
||||
this.prepareMonsterRoll("monster-attack", key, undefined, token?.id)
|
||||
if (token?.object) {
|
||||
@@ -360,7 +407,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
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
|
||||
this.prepareMonsterRoll("monster-attack-hth", key, undefined, token?.id)
|
||||
if (token?.object) {
|
||||
@@ -379,7 +426,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
rollResult: roll.total
|
||||
}
|
||||
)
|
||||
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: this.parent }) })
|
||||
await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: this.parent }) })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export default class LethalFantasySpell extends foundry.abstract.TypeDataModel {
|
||||
})
|
||||
|
||||
schema.cost = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 })
|
||||
schema.costOverpowered = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 })
|
||||
schema.costOverpowered2 = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 })
|
||||
schema.memorized = new fields.BooleanField({ required: true, initial: false })
|
||||
|
||||
schema.components = new fields.SchemaField({
|
||||
@@ -40,6 +42,8 @@ export default class LethalFantasySpell extends foundry.abstract.TypeDataModel {
|
||||
schema.attackRoll = new fields.StringField({ required: true, initial: "" })
|
||||
schema.powerRoll = new fields.StringField({ required: true, initial: "" })
|
||||
schema.damageDice = new fields.StringField({ required: false, initial: "" })
|
||||
schema.damageDiceOverpowered = new fields.StringField({ required: false, initial: "" })
|
||||
schema.damageDiceOverpowered2 = new fields.StringField({ required: false, initial: "" })
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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() {
|
||||
const fields = foundry.data.fields
|
||||
const schema = {}
|
||||
@@ -45,9 +46,9 @@ export default class LethalFantasySkill extends foundry.abstract.TypeDataModel {
|
||||
})
|
||||
|
||||
schema.bonuses = new fields.SchemaField({
|
||||
attackBonus: new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }),
|
||||
damageBonus: new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }),
|
||||
defenseBonus: new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 })
|
||||
attackBonus: new fields.NumberField({ ...requiredInteger, required: true, initial: 0 }),
|
||||
damageBonus: new fields.NumberField({ ...requiredInteger, required: true, initial: 0 }),
|
||||
defenseBonus: new fields.NumberField({ ...requiredInteger, required: true, initial: 0 })
|
||||
})
|
||||
|
||||
schema.encLoad = new fields.NumberField({ required: true, initial: 0, min: 0 })
|
||||
@@ -62,7 +63,7 @@ export default class LethalFantasySkill extends foundry.abstract.TypeDataModel {
|
||||
static LOCALIZATION_PREFIXES = ["LETHALFANTASY.Weapon"]
|
||||
|
||||
get weaponCategory() {
|
||||
return game.i18n.localize(CATEGORY[this.weaponType].label)
|
||||
return game.i18n.localize(WEAPON_TYPE[this.weaponType] || this.weaponType)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
-1162
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,953 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
import { log } from "./helpers.mjs"
|
||||
import { processD30BonusDice, hasD30Reroll } from "./d30.mjs"
|
||||
|
||||
export async function handleSocketEvent(msg = {}) {
|
||||
log(`handleSocketEvent !`, msg)
|
||||
let actor
|
||||
switch (msg.type) {
|
||||
case "applyDamage":
|
||||
if (game.user.isGM) {
|
||||
// Prefer the specific token actor (correct for unlinked monsters); fall back to world actor.
|
||||
actor = msg.tokenId
|
||||
? canvas.tokens?.placeables?.find(t => t.id === msg.tokenId)?.actor
|
||||
: (game.combat?.combatants?.find(c => c.actorId === msg.actorId)?.actor
|
||||
?? game.actors.get(msg.actorId))
|
||||
if (actor) await actor.applyDamage(msg.damage)
|
||||
}
|
||||
break
|
||||
case "rollInitiative":
|
||||
if (msg.userId && msg.userId !== game.user.id) break
|
||||
actor = game.actors.get(msg.actorId)
|
||||
await actor.system.rollInitiative(msg.combatId, msg.combatantId)
|
||||
break
|
||||
case "rollProgressionDice":
|
||||
if (msg.userId && msg.userId !== game.user.id) break
|
||||
actor = game.actors.get(msg.actorId)
|
||||
await actor.system.rollProgressionDice(msg.combatId, msg.combatantId, msg.rollProgressionCount)
|
||||
break
|
||||
case "requestDefense":
|
||||
// Vérifier si le message est destiné à cet utilisateur
|
||||
if (msg.userId === game.user.id) {
|
||||
showDefenseRequest(msg)
|
||||
}
|
||||
break
|
||||
case "offerAttackerGrit":
|
||||
// Vérifier si le message est destiné à cet utilisateur
|
||||
if (msg.userId === game.user.id) {
|
||||
handleAttackerGritOffer(msg)
|
||||
}
|
||||
break
|
||||
case "applyBleeding":
|
||||
if (game.user.isGM) {
|
||||
actor = msg.tokenId
|
||||
? canvas.tokens?.placeables?.find(t => t.id === msg.tokenId)?.actor
|
||||
: game.actors.get(msg.actorId)
|
||||
if (actor && actor.system.hp?.wounds && msg.damage > 0) {
|
||||
const wounds = foundry.utils.duplicate(actor.system.hp.wounds)
|
||||
const slot = wounds.findIndex(w => !w.value && !w.duration)
|
||||
if (slot !== -1) {
|
||||
wounds[slot] = { value: msg.damage, duration: msg.damage, description: "Bleeding" }
|
||||
await actor.update({ "system.hp.wounds": wounds })
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
case "attackBoosted":
|
||||
if (msg.userId === game.user.id) {
|
||||
handleAttackBoosted(msg)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleAttackerGritOffer(msg) {
|
||||
const { attackerId, attackRoll, defenseRoll, attackerName, defenderName, attackWeaponId, attackRollType, attackRollKey, defenderId } = msg
|
||||
|
||||
const attacker = game.actors.get(attackerId)
|
||||
if (!attacker) {
|
||||
console.warn("Attacker not found:", attackerId)
|
||||
return
|
||||
}
|
||||
|
||||
const attackBonus = await offerAttackerGritBonus(
|
||||
attacker,
|
||||
attackRoll,
|
||||
defenseRoll,
|
||||
attackerName,
|
||||
defenderName
|
||||
)
|
||||
|
||||
const attackRollFinal = attackRoll + attackBonus
|
||||
|
||||
// Maintenant créer le message de comparaison
|
||||
await compareAttackDefense({
|
||||
attackerName,
|
||||
attackerId,
|
||||
attackRoll: attackRollFinal,
|
||||
attackWeaponId,
|
||||
attackRollType,
|
||||
attackRollKey,
|
||||
defenderName,
|
||||
defenderId,
|
||||
defenseRoll
|
||||
})
|
||||
}
|
||||
|
||||
export async function handleAttackBoosted(msg) {
|
||||
const {
|
||||
attackerName, attackerId, defenderName, defenderId, defenderTokenId,
|
||||
attackerHandledBonus, attackRollFinal, defenseRoll, attackWeaponId, attackRollType, attackRollKey,
|
||||
shieldDamageReduction: initialShieldDR,
|
||||
d30Bleed, d30DamageMultiplier, d30DrMultiplier,
|
||||
damageTier, attackD30message, defenseD30message, defenseRerollContext,
|
||||
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 processD30BonusDice(defenseD30message, "defense", null, defender, true)
|
||||
if (d30Result.modifier) {
|
||||
updatedDefenseRoll += d30Result.modifier
|
||||
if (d30Result.modifier > 0) {
|
||||
const msg = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"d30Bonus", actorName:defenderName, value:d30Result.modifier, side:"defense"})
|
||||
await ChatMessage.create({content: msg, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||
}
|
||||
}
|
||||
if (d30Result.specialEffect === "flag") {
|
||||
const msg = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"d30Flag", actorName:defenderName, specialName:d30Result.specialName || "Special Effect"})
|
||||
await ChatMessage.create({content: msg, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||
}
|
||||
if (d30Result.specialEffect === "drMultiplier") {
|
||||
defenseDrMultiplier = d30Result.multiplier
|
||||
const msg = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"d30DRMultiplier", actorName:defenderName, value:d30Result.multiplier})
|
||||
await ChatMessage.create({content: msg, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||
}
|
||||
}
|
||||
|
||||
// Show the defense reaction dialog — while-loop for multiple reactions
|
||||
if (defender) {
|
||||
while (updatedDefenseRoll < attackRollFinal) {
|
||||
const shieldData = canShieldReact ? { label: shieldLabel, formula: shieldFormula, damageReduction: shieldDr } : null
|
||||
const buttons = buildDefenseReactionButtons(defender, { canRerollDefense: hasD30Reroll(defenseD30message), shieldData, canShieldReact, canAdHocShield: canAdHoc })
|
||||
|
||||
const choice = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Defense reactions — attack boosted" },
|
||||
classes: ["lethalfantasy"],
|
||||
content: await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/defense-reaction.hbs", {
|
||||
attackerName,
|
||||
attackRoll: attackRollFinal,
|
||||
attackStatus: "boosted attack to",
|
||||
defenderName,
|
||||
defenseRoll: updatedDefenseRoll,
|
||||
defenseStatus: "currently has",
|
||||
d30message: defenseD30message || null,
|
||||
offerText: "The attack was boosted! Choose how to improve the defense."
|
||||
}),
|
||||
buttons,
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
if (!choice || choice === "continue") break
|
||||
|
||||
if (choice === "grit") {
|
||||
const bonusRoll = await rollBonusDie("1d6", defender)
|
||||
updatedDefenseRoll += bonusRoll
|
||||
await defender.update({ "system.grit.current": Math.max(0, (Number(defender.system?.grit?.current) || 0) - 1) })
|
||||
const gritRmContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"grit", actorName:defenderName, resource:"Grit", value:bonusRoll, side:"defense"})
|
||||
await ChatMessage.create({content: gritRmContent, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||
} else if (choice === "luck") {
|
||||
const bonusRoll = await rollBonusDie("1d6", defender)
|
||||
updatedDefenseRoll += bonusRoll
|
||||
await defender.update({ "system.luck.current": Math.max(0, (Number(defender.system?.luck?.current) || 0) - 1) })
|
||||
const luckRmContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"luck", actorName:defenderName, resource:"Luck", value:bonusRoll, side:"defense"})
|
||||
await ChatMessage.create({content: luckRmContent, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||
} else if (choice === "bonusDie") {
|
||||
const bonusDie = await promptCombatBonusDie(defenderName, "attack", updatedDefenseRoll, attackRollFinal)
|
||||
if (bonusDie) {
|
||||
const bonusRoll = await rollBonusDie(bonusDie, defender)
|
||||
updatedDefenseRoll += bonusRoll
|
||||
const bonusRmContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"bonusDie", actorName:defenderName, formula:bonusDie.toUpperCase(), value:bonusRoll, side:"defense"})
|
||||
await ChatMessage.create({content: bonusRmContent, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||
}
|
||||
} else if (choice === "shieldReact" && canShieldReact) {
|
||||
const shieldBonus = await rollBonusDie(shieldFormula, defender)
|
||||
const newDefenseTotal = updatedDefenseRoll + shieldBonus
|
||||
updatedDefenseRoll = newDefenseTotal
|
||||
canShieldReact = false
|
||||
if (newDefenseTotal >= attackRollFinal) {
|
||||
shieldBlocked = true
|
||||
shieldReaction = { damageReduction: shieldDr, label: shieldLabel, bonus: shieldBonus }
|
||||
const shieldBlockContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"shieldBlock", actorName:defenderName, shieldLabel, shieldBonus, newTotal:newDefenseTotal, opposingRoll:attackRollFinal, shieldDR:shieldDr})
|
||||
await ChatMessage.create({
|
||||
content: shieldBlockContent,
|
||||
speaker: ChatMessage.getSpeaker({ actor: defender })
|
||||
})
|
||||
} else {
|
||||
const shieldFailContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"shieldFail", actorName:defenderName, shieldLabel, shieldBonus, newTotal:newDefenseTotal, opposingRoll:attackRollFinal})
|
||||
await ChatMessage.create({
|
||||
content: shieldFailContent,
|
||||
speaker: ChatMessage.getSpeaker({ actor: defender })
|
||||
})
|
||||
}
|
||||
} else if (choice === "rerollDefense" && defenseRerollContext) {
|
||||
const oldDefenseRoll = updatedDefenseRoll
|
||||
const reroll = await rerollConfiguredRoll(defenseRerollContext)
|
||||
if (!reroll) continue
|
||||
updatedDefenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
|
||||
let newD30message = reroll.options?.D30message || null
|
||||
try {
|
||||
const mulliganContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {
|
||||
type: "mulligan",
|
||||
actorName: defenderName,
|
||||
side: "defense",
|
||||
oldRoll: oldDefenseRoll,
|
||||
newRoll: updatedDefenseRoll,
|
||||
diceResults: reroll.options?.diceResults || [],
|
||||
D30result: reroll.options?.D30result,
|
||||
D30message: newD30message
|
||||
})
|
||||
await ChatMessage.create({content: mulliganContent, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||
} catch(e) {
|
||||
console.error("Mulligan message creation failed (non-fatal):", e)
|
||||
}
|
||||
// Process new D30 bonus dice from the reroll
|
||||
if (newD30message) {
|
||||
defenseD30message = newD30message
|
||||
const d30Result = await processD30BonusDice(defenseD30message, "defense", null, defender, true)
|
||||
if (d30Result.modifier) {
|
||||
updatedDefenseRoll += d30Result.modifier
|
||||
if (d30Result.modifier > 0) {
|
||||
const rmContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"d30Bonus", actorName:defenderName, value:d30Result.modifier, side:"defense"})
|
||||
await ChatMessage.create({content: rmContent, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||
}
|
||||
}
|
||||
if (d30Result.specialEffect === "flag") {
|
||||
const rmContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"d30Flag", actorName:defenderName, specialName:d30Result.specialName || "Special Effect"})
|
||||
await ChatMessage.create({content: rmContent, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||
}
|
||||
if (d30Result.specialEffect === "drMultiplier") {
|
||||
const rmContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"d30DRMultiplier", actorName:defenderName, value:d30Result.multiplier})
|
||||
await ChatMessage.create({content: rmContent, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||
}
|
||||
}
|
||||
continue
|
||||
} else if (choice === "adHocShield" && canAdHoc) {
|
||||
const adHoc = await promptAdHocShield(defenderName, attackRollFinal, updatedDefenseRoll)
|
||||
if (adHoc) {
|
||||
const shieldBonus = await 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 }
|
||||
const adHocShieldBlockContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"shieldBlock", actorName:defenderName, shieldLabel: `${adHoc.formula.toUpperCase()} shield`, shieldBonus, newTotal:newDefenseTotal, opposingRoll:attackRollFinal, shieldDR:adHoc.damageReduction})
|
||||
await ChatMessage.create({
|
||||
content: adHocShieldBlockContent,
|
||||
speaker: ChatMessage.getSpeaker({ actor: defender })
|
||||
})
|
||||
} else {
|
||||
const adHocShieldFailContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"shieldFail", actorName:defenderName, shieldLabel: `${adHoc.formula.toUpperCase()} shield`, shieldBonus, newTotal:newDefenseTotal, opposingRoll:attackRollFinal})
|
||||
await ChatMessage.create({
|
||||
content: adHocShieldFailContent,
|
||||
speaker: ChatMessage.getSpeaker({ actor: defender })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finalShieldDR = shieldBlocked ? (shieldReaction?.damageReduction || 0) : 0
|
||||
const outcome = shieldBlocked ? "shielded-hit" : (updatedDefenseRoll >= attackRollFinal ? "miss" : "hit")
|
||||
|
||||
await 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
|
||||
})
|
||||
}
|
||||
|
||||
export async function showDefenseRequest(msg) {
|
||||
const attackerName = msg.attackerName
|
||||
const attackerId = msg.attackerId
|
||||
const defenderName = msg.defenderName
|
||||
const weaponName = msg.weaponName || "attack"
|
||||
const attackRoll = msg.attackRoll
|
||||
const attackWeaponId = msg.attackWeaponId
|
||||
const attackRollType = msg.attackRollType
|
||||
const attackRollKey = msg.attackRollKey
|
||||
const attackD30result = msg.attackD30result
|
||||
const attackD30message = msg.attackD30message
|
||||
const attackRerollContext = msg.attackRerollContext
|
||||
const d30AttackEffects = msg.d30AttackEffects
|
||||
const combatantId = msg.combatantId
|
||||
const tokenId = msg.tokenId
|
||||
|
||||
// Récupérer le défenseur - essayer d'abord depuis le combat, puis depuis le token
|
||||
let defender = null
|
||||
|
||||
if (game.combat && combatantId) {
|
||||
const combatant = game.combat.combatants.get(combatantId)
|
||||
if (combatant) {
|
||||
defender = combatant.actor
|
||||
}
|
||||
}
|
||||
|
||||
// Si pas trouvé dans le combat, chercher le token directement
|
||||
if (!defender && tokenId) {
|
||||
const token = canvas.tokens.get(tokenId)
|
||||
if (token) {
|
||||
defender = token.actor
|
||||
}
|
||||
}
|
||||
|
||||
if (!defender) {
|
||||
ui.notifications.error("Defender actor not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve the specific token ID now while we still have combatant/token context.
|
||||
// This is passed through to the damage roll so the GM-side socket handler can find the
|
||||
// correct synthetic actor for unlinked tokens (avoids wrong-instance damage with multiple
|
||||
// unlinked copies of the same monster).
|
||||
const defenderTokenId = (() => {
|
||||
if (game.combat && combatantId) {
|
||||
const cbt = game.combat.combatants.get(combatantId)
|
||||
if (cbt?.token?.id) return cbt.token.id
|
||||
}
|
||||
return tokenId ?? canvas.tokens?.placeables?.find(t => t.actor?.id === defender.id)?.id ?? null
|
||||
})()
|
||||
|
||||
const isMonster = defender.type === "monster"
|
||||
|
||||
const _storeNextDefenseData = (opts = {}) => {
|
||||
game.lethalFantasy = game.lethalFantasy || {}
|
||||
game.lethalFantasy.nextDefenseData = {
|
||||
attackerId, attackRoll, attackerName, defenderName,
|
||||
attackWeaponId, attackRollType, attackRollKey,
|
||||
attackD30result, attackD30message, attackRerollContext,
|
||||
damageTier: msg.damageTier,
|
||||
defenderId: defender.id, defenderTokenId,
|
||||
...(msg.attackNaturalRoll !== undefined && { attackNaturalRoll: msg.attackNaturalRoll }),
|
||||
...(d30AttackEffects !== undefined && { d30AttackEffects }),
|
||||
...(opts.isRanged !== undefined && { isRanged: opts.isRanged })
|
||||
}
|
||||
}
|
||||
|
||||
log(`[LF] showDefenseRequest | attackRollType=${attackRollType} isMonster=${isMonster} defender=${defender?.name}`)
|
||||
|
||||
// Spell/miracle attacks use saving throws instead of weapon defense
|
||||
const isSpellAttack = attackRollType === "spell-attack" || attackRollType === "miracle-attack"
|
||||
if (isSpellAttack) {
|
||||
const savesConfig = isMonster ? SYSTEM.MONSTER_SAVES : SYSTEM.SAVES
|
||||
const combatSaves = ["will", "dodge", "toughness"]
|
||||
const savesList = Object.values(savesConfig)
|
||||
.filter(s => combatSaves.includes(s.id))
|
||||
.map(s => ({id: s.id, label: game.i18n.localize(s.label)}))
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/defense-request-save.hbs", {
|
||||
attackerName, defenderName, weaponName, attackRoll, saves: savesList
|
||||
})
|
||||
|
||||
const result = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Saving Throw vs Spell" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "rollSave",
|
||||
type: "button",
|
||||
label: "Roll Save",
|
||||
icon: "fa-solid fa-person-running",
|
||||
callback: (event, button) => button.form.elements.saveKey.value,
|
||||
},
|
||||
],
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
if (result) {
|
||||
game.lethalFantasy = game.lethalFantasy || {}
|
||||
game.lethalFantasy.spellDefense = true // pré-cocher "Save against spell" dans le dialog
|
||||
_storeNextDefenseData()
|
||||
if (isMonster) {
|
||||
await defender.system.prepareMonsterRoll("save", result)
|
||||
} else {
|
||||
await defender.prepareRoll("save", result)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Pour les monstres, récupérer les attaques activées
|
||||
if (isMonster) {
|
||||
const attacksSet = defender.system.attackMode === "ranged" ? defender.system.rangedAttacks : defender.system.attacks
|
||||
const enabledAttacks = Object.entries(attacksSet).filter(([key, attack]) => attack.enabled)
|
||||
|
||||
if (enabledAttacks.length === 0) {
|
||||
ui.notifications.warn("No enabled attacks available for defense")
|
||||
return
|
||||
}
|
||||
|
||||
// Créer le contenu du dialogue pour monstre
|
||||
const attacksList = enabledAttacks.map(([key, attack]) => ({key, name: attack.name}))
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/defense-request-monster.hbs", {
|
||||
attackerName, defenderName, weaponName, attackRoll, attacks: attacksList
|
||||
})
|
||||
|
||||
// Afficher le dialogue
|
||||
const result = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: msg.isRanged ? "Ranged Defense Roll" : "Defense Roll" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "rangeDefense",
|
||||
type: "button",
|
||||
label: "Roll Defense",
|
||||
icon: "fa-solid fa-shield",
|
||||
callback: (event, button, dialog) => {
|
||||
const attackKey = button.form.elements.attackKey.value
|
||||
return attackKey
|
||||
},
|
||||
},
|
||||
],
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
// Si l'utilisateur a validé, lancer le jet de défense
|
||||
if (result) {
|
||||
_storeNextDefenseData({ isRanged: msg.isRanged })
|
||||
|
||||
await defender.system.prepareMonsterRoll("monster-defense", result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Pour les personnages, récupérer les armes équipées
|
||||
// Si l'attaque est une attaque à distance, utiliser le dialogue de défense à distance
|
||||
if (msg.isRanged) {
|
||||
const { default: LethalFantasyRoll } = await import("../documents/roll.mjs")
|
||||
const roll = await LethalFantasyRoll.promptRangedDefense({
|
||||
actorId: defender.id,
|
||||
actorName: defender.name,
|
||||
actorImage: defender.img,
|
||||
})
|
||||
if (roll) {
|
||||
_storeNextDefenseData({ isRanged: true })
|
||||
await roll.toMessage({}, { messageMode: roll.options.rollMode })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Pour les personnages, récupérer les armes équipées
|
||||
const equippedWeapons = defender.items.filter(i =>
|
||||
i.type === "weapon" && i.system.equipped === true
|
||||
)
|
||||
|
||||
if (equippedWeapons.length === 0) {
|
||||
ui.notifications.warn("No equipped weapons for defense")
|
||||
return
|
||||
}
|
||||
|
||||
// Créer le contenu du dialogue pour personnage
|
||||
const weaponsList = equippedWeapons.map(w => ({id: w.id, name: w.name}))
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/defense-request-character.hbs", {
|
||||
attackerName, defenderName, weaponName, attackRoll, weapons: weaponsList
|
||||
})
|
||||
|
||||
// Afficher le dialogue
|
||||
const result = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Defense Roll" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "defenseRoll",
|
||||
type: "button",
|
||||
label: "Roll Defense",
|
||||
icon: "fa-solid fa-shield",
|
||||
callback: (event, button, dialog) => {
|
||||
const weaponId = button.form.elements.weaponId.value
|
||||
return weaponId
|
||||
},
|
||||
},
|
||||
],
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
// Si l'utilisateur a validé, lancer le jet de défense
|
||||
if (result) {
|
||||
_storeNextDefenseData({ isRanged: msg.isRanged })
|
||||
|
||||
log("Storing defense data for character:", defender.id)
|
||||
|
||||
await defender.prepareRoll("weapon-defense", result)
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDefenseReactionButtons(defender, { canRerollDefense = false, shieldData = null, canShieldReact = false, canAdHocShield = false } = {}) {
|
||||
const currentGrit = Number(defender.system?.grit?.current) || 0
|
||||
const currentLuck = Number(defender.system?.luck?.current) || 0
|
||||
const buttons = []
|
||||
if (currentGrit > 0) {
|
||||
buttons.push({ action: "grit", type: "button", label: `Spend 1 Grit (+1D6) [${currentGrit} left]`, icon: "fa-solid fa-fist-raised", callback: () => "grit" })
|
||||
}
|
||||
if (currentLuck > 0) {
|
||||
buttons.push({ action: "luck", type: "button", label: `Spend 1 Luck (+1D6) [${currentLuck} left]`, icon: "fa-solid fa-clover", callback: () => "luck" })
|
||||
}
|
||||
buttons.push({ action: "bonusDie", type: "button", label: "Add bonus die", icon: "fa-solid fa-dice", callback: () => "bonusDie" })
|
||||
if (canRerollDefense) {
|
||||
buttons.push({ action: "rerollDefense", type: "button", label: "Re-roll defense (Mulligan)", icon: "fa-solid fa-rotate-right", callback: () => "rerollDefense" })
|
||||
}
|
||||
if (canShieldReact && shieldData) {
|
||||
buttons.push({ action: "shieldReact", type: "button", label: `Roll shield (${shieldData.label})`, icon: "fa-solid fa-shield", callback: () => "shieldReact" })
|
||||
} else if (canAdHocShield) {
|
||||
buttons.push({ action: "adHocShield", type: "button", label: "Roll ad-hoc shield (choose dice + DR)", icon: "fa-solid fa-shield-halved", callback: () => "adHocShield" })
|
||||
}
|
||||
buttons.push({ action: "continue", type: "button", label: "Continue (no defense bonus)", icon: "fa-solid fa-forward", callback: () => "continue" })
|
||||
return buttons
|
||||
}
|
||||
|
||||
export function getCombatBonusDiceChoices() {
|
||||
return ["1d4", "1d6", "1d8", "1d10", "1d12", "1d20", "1d20e"]
|
||||
}
|
||||
|
||||
export function getShieldReactionData(actor) {
|
||||
if (!actor) return null
|
||||
if (actor.type === "monster") {
|
||||
const formula = actor.system.combat?.shieldDefenseDice
|
||||
const damageReduction = actor.getShieldDR()
|
||||
if (!formula || damageReduction <= 0) return null
|
||||
return {
|
||||
label: game.i18n.localize("LETHALFANTASY.Label.shieldDefenseDice"),
|
||||
formula,
|
||||
damageReduction
|
||||
}
|
||||
}
|
||||
|
||||
const equippedShields = actor.items.filter(item => item.type === "shield" && item.system.equipped)
|
||||
if (equippedShields.length === 0) return null
|
||||
|
||||
const shield = equippedShields[0]
|
||||
return {
|
||||
label: shield.name,
|
||||
formula: shield.system.defense,
|
||||
damageReduction: actor.getShieldDR(),
|
||||
shieldId: shield.id
|
||||
}
|
||||
}
|
||||
|
||||
export async function promptCombatBonusDie(actorName, sideLabel, currentRoll, opposingRoll) {
|
||||
const choicesList = getCombatBonusDiceChoices()
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/bonus-die-select.hbs", {
|
||||
actorName, currentRoll, opposingRoll, sideLabel, choices: choicesList
|
||||
})
|
||||
|
||||
return await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Add Bonus Die" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "roll",
|
||||
type: "button",
|
||||
label: "Roll Bonus Die",
|
||||
icon: "fa-solid fa-dice",
|
||||
callback: (event, button) => {
|
||||
const sel = button.form?.elements?.bonusDie ?? button.closest("form")?.elements?.bonusDie
|
||||
return sel?.value ?? choicesList[0]
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "cancel",
|
||||
type: "button",
|
||||
label: "Cancel",
|
||||
icon: "fa-solid fa-xmark",
|
||||
callback: () => null
|
||||
}
|
||||
],
|
||||
rejectClose: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the GM or player to choose an ad-hoc shield dice and DR value.
|
||||
* Used when the defender has no pre-configured shield equipment.
|
||||
* @param {string} defenderName
|
||||
* @param {number} attackRoll
|
||||
* @param {number} defenseRoll
|
||||
* @returns {Promise<{formula: string, damageReduction: number}|null>}
|
||||
*/
|
||||
export async function promptAdHocShield(defenderName, attackRoll, defenseRoll) {
|
||||
const choicesList = getCombatBonusDiceChoices()
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/ad-hoc-shield.hbs", {
|
||||
defenderName, attackRoll, defenseRoll, choices: choicesList
|
||||
})
|
||||
|
||||
const raw = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Ad-hoc Shield Roll" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "roll",
|
||||
type: "button",
|
||||
label: "Roll Shield",
|
||||
icon: "fa-solid fa-shield",
|
||||
callback: (event, button) => {
|
||||
const shieldDice = button.form?.elements?.shieldDice ?? button.closest("form")?.elements?.shieldDice
|
||||
const shieldDR = button.form?.elements?.shieldDR ?? button.closest("form")?.elements?.shieldDR
|
||||
return {
|
||||
formula: shieldDice?.value ?? "1d6",
|
||||
damageReduction: Number(shieldDR?.value) || 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "cancel",
|
||||
type: "button",
|
||||
label: "Cancel",
|
||||
icon: "fa-solid fa-xmark",
|
||||
callback: () => null
|
||||
}
|
||||
],
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
return raw ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll a bonus die formula, optionally showing Dice So Nice animation.
|
||||
* @param {string} formula
|
||||
* @param {Actor} actor
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
export async function rollBonusDie(formula, actor) {
|
||||
const roll = new Roll(formula)
|
||||
await roll.evaluate()
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.showForRoll(roll, game.user, true)
|
||||
}
|
||||
return roll.total
|
||||
}
|
||||
|
||||
export async function rerollConfiguredRoll(rerollContext = {}) {
|
||||
const RollClass = CONFIG.Dice.rolls.find(r => r.name === "LethalFantasyRoll")
|
||||
if (typeof RollClass?.prompt !== "function") {
|
||||
ui.notifications.error("Lethal Fantasy roll class not available for reroll")
|
||||
return null
|
||||
}
|
||||
|
||||
return await RollClass.prompt({
|
||||
...foundry.utils.duplicate(rerollContext),
|
||||
rollContext: foundry.utils.duplicate(rerollContext.rollContext || {}),
|
||||
hasTarget: false,
|
||||
target: false
|
||||
})
|
||||
}
|
||||
|
||||
export async function offerAttackerGritBonus(attacker, currentAttackRoll, defenseRoll, attackerName, defenderName) {
|
||||
let totalBonus = 0
|
||||
let keepOffering = true
|
||||
|
||||
while (keepOffering && currentAttackRoll + totalBonus <= defenseRoll) {
|
||||
const currentGrit = attacker.system.grit.current
|
||||
|
||||
if (currentGrit <= 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
action: "grit",
|
||||
type: "button",
|
||||
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
||||
icon: "fa-solid fa-fist-raised",
|
||||
callback: () => "grit"
|
||||
},
|
||||
{
|
||||
action: "continue",
|
||||
type: "button",
|
||||
label: "Continue (no bonus)",
|
||||
icon: "fa-solid fa-forward",
|
||||
callback: () => "continue"
|
||||
}
|
||||
]
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/attack-grit.hbs", {
|
||||
attackerName,
|
||||
currentAttackRollWithBonus: currentAttackRoll + totalBonus,
|
||||
defenderName,
|
||||
defenseRoll,
|
||||
totalBonus
|
||||
})
|
||||
|
||||
const choice = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Attack with Grit" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons,
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
if (!choice || choice === "continue") {
|
||||
keepOffering = false
|
||||
break
|
||||
}
|
||||
|
||||
const bonusRoll = new Roll("1d6")
|
||||
await bonusRoll.evaluate()
|
||||
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.showForRoll(bonusRoll, game.user, true)
|
||||
}
|
||||
|
||||
totalBonus += bonusRoll.total
|
||||
|
||||
await attacker.update({ "system.grit.current": currentGrit - 1 })
|
||||
const gritRm = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"grit", actorName:attackerName, resource:"Grit", value:bonusRoll.total, side:"attack"})
|
||||
await ChatMessage.create({content: gritRm, speaker: ChatMessage.getSpeaker({actor: attacker})})
|
||||
}
|
||||
|
||||
return totalBonus
|
||||
}
|
||||
|
||||
export async function compareAttackDefense(data) {
|
||||
log("compareAttackDefense called with:", data)
|
||||
|
||||
// Compute D30 effects from the attack D30 message directly.
|
||||
// This is more reliable than depending on the caller-provided values, which are
|
||||
// computed per-client and may differ between clients due to cross-client processing order.
|
||||
const d30DamageMultiplier = data.attackD30message?.type === "damage_multiplier"
|
||||
? data.attackD30message.multiplier
|
||||
: (data.d30DamageMultiplier || 1)
|
||||
const d30Bleed = data.attackD30message?.type === "combo"
|
||||
? (data.attackD30message.effects?.some(e => e.type === "bleed" || e.type === "internal_injury") ? "true" : "")
|
||||
: data.attackD30message?.type === "bleed" ? "true" : (data.d30Bleed || "")
|
||||
const d30DrMultiplier = data.d30DrMultiplier || 1
|
||||
|
||||
const shieldDamageReduction = data.shieldDamageReduction || 0
|
||||
const outcome = data.outcome || (data.attackRoll > data.defenseRoll ? "hit" : "miss")
|
||||
const isAttackWin = outcome !== "miss"
|
||||
log("isAttackWin:", isAttackWin, "attackRoll:", data.attackRoll, "defenseRoll:", data.defenseRoll)
|
||||
|
||||
let damageButton = ""
|
||||
if (isAttackWin && (data.attackWeaponId || data.attackRollKey)) {
|
||||
log("Creating damage button. defenderId:", data.defenderId)
|
||||
// Déterminer le type de dégâts à lancer
|
||||
if (data.attackRollType === "weapon-attack") {
|
||||
damageButton = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/damage-button.hbs", {
|
||||
type: "weapon",
|
||||
attackerId: data.attackerId,
|
||||
defenderId: data.defenderId,
|
||||
defenderTokenId: data.defenderTokenId || "",
|
||||
shieldDamageReduction: shieldDamageReduction,
|
||||
attackWeaponId: data.attackWeaponId,
|
||||
d30Bleed,
|
||||
d30DamageMultiplier,
|
||||
d30DrMultiplier
|
||||
})
|
||||
} else if (data.attackRollType === "monster-attack") {
|
||||
damageButton = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/damage-button.hbs", {
|
||||
type: "monster",
|
||||
attackerId: data.attackerId,
|
||||
defenderId: data.defenderId,
|
||||
defenderTokenId: data.defenderTokenId || "",
|
||||
shieldDamageReduction: shieldDamageReduction,
|
||||
attackRollKey: data.attackRollKey,
|
||||
d30Bleed,
|
||||
d30DamageMultiplier,
|
||||
d30DrMultiplier
|
||||
})
|
||||
} else if (data.attackRollType === "spell-attack" || data.attackRollType === "miracle-attack") {
|
||||
const attacker = game.actors.get(data.attackerId)
|
||||
const spell = attacker?.items.get(data.attackWeaponId || data.attackRollKey)
|
||||
const chosenTier = data.damageTier || "standard"
|
||||
const allTiers = [
|
||||
{ id: "standard", formula: spell?.system?.damageDice, label: "Standard" },
|
||||
{ id: "overpowered", formula: spell?.system?.damageDiceOverpowered, label: "Overpowered" },
|
||||
{ id: "overpowered2", formula: spell?.system?.damageDiceOverpowered2, label: "Overpowered 2" },
|
||||
]
|
||||
const tierData = allTiers.filter(t => t.id === chosenTier && t.formula).map(t => ({
|
||||
formula: Handlebars.escapeExpression(t.formula),
|
||||
label: t.label
|
||||
}))
|
||||
if (tierData.length) {
|
||||
damageButton = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/damage-button.hbs", {
|
||||
type: "spell",
|
||||
attackerId: data.attackerId,
|
||||
defenderId: data.defenderId,
|
||||
defenderTokenId: data.defenderTokenId || "",
|
||||
tiers: tierData,
|
||||
d30Bleed,
|
||||
d30DamageMultiplier,
|
||||
d30DrMultiplier
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resultMessage = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/combat-result.hbs", {
|
||||
isAttackWin,
|
||||
outcome,
|
||||
attackerName: data.attackerName,
|
||||
defenderName: data.defenderName,
|
||||
attackRoll: data.attackRoll,
|
||||
defenseRoll: data.defenseRoll,
|
||||
shieldDamageReduction: shieldDamageReduction,
|
||||
damageButton
|
||||
})
|
||||
|
||||
log("Creating combat result message...")
|
||||
await ChatMessage.create({
|
||||
content: resultMessage,
|
||||
speaker: { alias: "Combat System" }
|
||||
})
|
||||
log("Combat result message created!")
|
||||
}
|
||||
|
||||
export async function applyDamage(message, event) {
|
||||
// Récupérer les données du message
|
||||
let combatantId = event.currentTarget.dataset.combatantId
|
||||
if (!combatantId) {
|
||||
ui.notifications.error("No combatant selected")
|
||||
return
|
||||
}
|
||||
|
||||
// Try to find the target: first as a combat combatant, then as a scene token
|
||||
let targetActor = null
|
||||
if (game.combat) {
|
||||
const combatant = game.combat.combatants.get(combatantId)
|
||||
if (combatant) {
|
||||
targetActor = combatant.token?.actor || game.actors.get(combatant.actorId)
|
||||
}
|
||||
}
|
||||
if (!targetActor) {
|
||||
// Fall back to scene token lookup (non-combat tokens use tokenId as their combatantId)
|
||||
const token = canvas.tokens?.placeables?.find(t => t.id === combatantId)
|
||||
targetActor = token?.actor
|
||||
}
|
||||
if (!targetActor) {
|
||||
ui.notifications.error("Target actor not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Récupérer les données de dégâts du message
|
||||
// Use options.rollTotal (includes weapon modifier bonus) rather than roll.total (dice formula only)
|
||||
let damageTotal = message.rolls[0]?.options?.rollTotal ?? message.rolls[0]?.total ?? 0
|
||||
let weaponName = message.rolls[0]?.options?.rollName || "Unknown Weapon"
|
||||
|
||||
// Calculer les DR
|
||||
let armorDR = targetActor.computeDamageReduction() || 0
|
||||
let shieldDR = targetActor.getShieldDR() || 0
|
||||
let totalDR = armorDR + shieldDR
|
||||
|
||||
// Créer le dialogue
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/apply-damage-dialog.hbs",
|
||||
{
|
||||
targetName: targetActor.name,
|
||||
weaponName: weaponName,
|
||||
damageTotal: damageTotal,
|
||||
armorDR: armorDR,
|
||||
shieldDR: shieldDR,
|
||||
totalDR: totalDR,
|
||||
damageNoDR: damageTotal,
|
||||
damageWithArmor: Math.max(0, damageTotal - armorDR),
|
||||
damageWithAll: Math.max(0, damageTotal - totalDR)
|
||||
}
|
||||
)
|
||||
|
||||
const result = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Apply Damage" },
|
||||
classes: ["lethalfantasy"],
|
||||
position: { width: 280 },
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "noDR",
|
||||
type: "button",
|
||||
label: "No DR",
|
||||
callback: () => ({ drType: "none", damage: damageTotal })
|
||||
},
|
||||
{
|
||||
action: "armorDR",
|
||||
type: "button",
|
||||
label: "With Armor DR",
|
||||
callback: () => ({ drType: "armor", damage: Math.max(0, damageTotal - armorDR) })
|
||||
},
|
||||
{
|
||||
action: "allDR",
|
||||
type: "button",
|
||||
label: "With Armor + Shield DR",
|
||||
callback: () => ({ drType: "all", damage: Math.max(0, damageTotal - totalDR) })
|
||||
},
|
||||
{
|
||||
action: "cancel",
|
||||
type: "button",
|
||||
label: "Cancel",
|
||||
callback: () => null
|
||||
}
|
||||
],
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
if (result && result.damage !== undefined) {
|
||||
await targetActor.applyDamage(-result.damage)
|
||||
|
||||
// Message de confirmation
|
||||
let drText = ""
|
||||
if (result.drType === "armor") {
|
||||
drText = `Armor DR: ${armorDR}`
|
||||
} else if (result.drType === "all") {
|
||||
drText = `Total DR: ${totalDR}`
|
||||
}
|
||||
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/damage-applied-message.hbs",
|
||||
{
|
||||
targetName: targetActor.name,
|
||||
damage: result.damage,
|
||||
drText: drText,
|
||||
weaponName: weaponName
|
||||
}
|
||||
)
|
||||
|
||||
await ChatMessage.create({
|
||||
user: game.user.id,
|
||||
speaker: { alias: targetActor.name },
|
||||
mode: "gm",
|
||||
content: messageContent
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
export { log } from "./helpers.mjs"
|
||||
|
||||
export function hasD30Reroll(d30Message) {
|
||||
return d30Message?.type === "mulligan"
|
||||
}
|
||||
|
||||
/**
|
||||
* Process D30 bonus dice for attack or defense.
|
||||
* Rolls and applies bonus dice BEFORE grit/luck/shield decisions.
|
||||
* For `choice` type results (D30=20, 30), shows dialog to choose between bonus dice and special effect.
|
||||
* For `bonus_dice` type results (D30=27, 2, 3), auto-rolls the dice.
|
||||
* @param {Object|null} d30Message The D30 result object
|
||||
* @param {"attack"|"defense"} side Whether processing the attack or defense side
|
||||
* @param {number|null} naturalRoll The natural D20 roll (for special strike type detection)
|
||||
* @param {Object} actor The actor (for dice3d display)
|
||||
* @returns {Promise<{modifier: number, specialEffect: string|null, specialName: string|null}>}
|
||||
*/
|
||||
export async function processD30BonusDice(d30Message, side, naturalRoll = null, actor = null, canDialog = true) {
|
||||
if (!d30Message) return { modifier: 0, specialEffect: null, specialName: null }
|
||||
|
||||
const validTargets = side === "attack" ? ["attack", "spell_attack"] : ["defense", "spell_defense"]
|
||||
|
||||
// ── Simple bonus_dice type ── auto-roll if target matches
|
||||
if (d30Message.type === "bonus_dice") {
|
||||
if (!validTargets.includes(d30Message.target)) return { modifier: 0, specialEffect: null, specialName: null }
|
||||
const modifier = await _rollD30BonusDie(d30Message.dice, actor, !canDialog)
|
||||
return { modifier, specialEffect: null, specialName: null }
|
||||
}
|
||||
|
||||
// ── Choice type ── auto-roll bonus dice, alert about special effects
|
||||
if (d30Message.type === "choice") {
|
||||
// Non-controlling client can't roll dice here — the controlling client
|
||||
// sends the updated values via socket.
|
||||
if (!canDialog) {
|
||||
return { modifier: 0, specialEffect: null, specialName: null }
|
||||
}
|
||||
|
||||
// Auto-roll bonus dice (like d6E on 27 — no dialog)
|
||||
const bonusChoice = d30Message.choices.find(c => c.type === "bonus_dice")
|
||||
let modifier = 0
|
||||
if (bonusChoice) {
|
||||
modifier = await _rollD30BonusDie(bonusChoice.dice, actor)
|
||||
}
|
||||
|
||||
// Inform about special strike/defense or other effects (informational only)
|
||||
const specialChoice = d30Message.choices.find(c => c.type === "special_strike" || c.type === "special_defense")
|
||||
if (specialChoice) {
|
||||
return { modifier, specialEffect: "flag", specialName: _buildSpecialName(specialChoice, naturalRoll) }
|
||||
}
|
||||
|
||||
// Non-standard choice (spell_calamity, etc.) — report it
|
||||
const nonStandardChoice = d30Message.choices.find(c => c.type !== "bonus_dice")
|
||||
if (nonStandardChoice) {
|
||||
return { modifier, specialEffect: "flag", specialName: _buildSpecialName(nonStandardChoice, naturalRoll) }
|
||||
}
|
||||
|
||||
return { modifier, specialEffect: null, specialName: null }
|
||||
}
|
||||
|
||||
// ── Combo type (bleed / internal injury) — flag for wound creation
|
||||
if (d30Message.type === "combo") {
|
||||
const hasBleed = d30Message.effects?.some(e => e.type === "bleed" || e.type === "internal_injury")
|
||||
if (hasBleed) {
|
||||
return { modifier: 0, specialEffect: "bleed", specialName: "Bleeding/Internal Injury" }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bleed type (ranged attacks) — flag for wound creation, same as combo bleed
|
||||
if (d30Message.type === "bleed") {
|
||||
return { modifier: 0, specialEffect: "bleed", specialName: "Bleeding" }
|
||||
}
|
||||
|
||||
// ── Damage multiplier type (2x/3x damage before DR)
|
||||
if (d30Message.type === "damage_multiplier") {
|
||||
return { modifier: 0, specialEffect: "damageMultiplier", specialName: `x${d30Message.multiplier} Damage`, multiplier: d30Message.multiplier }
|
||||
}
|
||||
|
||||
// ── DR multiplier type (2x/3x DR including shield)
|
||||
if (d30Message.type === "dr_multiplier") {
|
||||
return { modifier: 0, specialEffect: "drMultiplier", specialName: `x${d30Message.multiplier} DR`, multiplier: d30Message.multiplier }
|
||||
}
|
||||
|
||||
return { modifier: 0, specialEffect: null, specialName: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll a D30 bonus die and show with 3D dice if available.
|
||||
* @param {string} formula Dice formula (e.g. "D6", "D12", "D20E")
|
||||
* @param {Object} actor Actor for chat message speaker
|
||||
* @returns {Promise<number>} The roll total
|
||||
*/
|
||||
export async function _rollD30BonusDie(formula, actor, silent = false) {
|
||||
const cleaned = formula.replace(/NE$/i, "").replace("E", "")
|
||||
const roll = new Roll(cleaned)
|
||||
await roll.evaluate()
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.showForRoll(roll, game.user, true)
|
||||
}
|
||||
if (!silent) {
|
||||
await ChatMessage.create({
|
||||
content: await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"d30BonusRoll", formula: cleaned.toUpperCase(), value: roll.total}),
|
||||
speaker: ChatMessage.getSpeaker({ actor })
|
||||
})
|
||||
}
|
||||
return roll.total
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the special effect name based on the D30 result and natural roll.
|
||||
* @param {Object} specialChoice The choice object with type and options
|
||||
* @param {number|null} naturalRoll The natural D20 roll
|
||||
* @returns {string} The special effect name
|
||||
*/
|
||||
export function _buildSpecialName(specialChoice, naturalRoll) {
|
||||
if (specialChoice.type === "special_strike") {
|
||||
if (specialChoice.options.includes("lethal")) {
|
||||
if (naturalRoll === 20) return "Lethal Strike"
|
||||
if (naturalRoll >= 16 && naturalRoll <= 19) return "Vital Strike"
|
||||
return "Lethal/Vital Strike"
|
||||
}
|
||||
if (specialChoice.options.includes("vicious")) return "Vicious Strike"
|
||||
return "Special Strike"
|
||||
}
|
||||
if (specialChoice.type === "special_defense") {
|
||||
if (specialChoice.options.includes("perfect_spell")) return "Perfect Spell Defense"
|
||||
if (specialChoice.options.includes("flawless")) return "Flawless Defense"
|
||||
if (specialChoice.options.includes("legendary")) return "Legendary Defense"
|
||||
if (specialChoice.options.includes("perfect")) return "Perfect Defense"
|
||||
return "Special Defense"
|
||||
}
|
||||
return specialChoice.type.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase())
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
|
||||
export function log(...args) {
|
||||
if (game?.settings?.get(game.system.id, "debug")) {
|
||||
console.log(...args)
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadCompendiumData(compendium) {
|
||||
const pack = game.packs.get(compendium)
|
||||
return await pack?.getDocuments() ?? []
|
||||
}
|
||||
|
||||
export async function loadCompendium(compendium, filter = item => true) {
|
||||
let compendiumData = await loadCompendiumData(compendium)
|
||||
return compendiumData.filter(filter)
|
||||
}
|
||||
|
||||
export function pushCombatOptions(html, options) {
|
||||
options.push({ name: "Reset Progression", condition: true, icon: '<i class="fas fa-rotate-right"></i>', callback: target => { game.combat.resetProgression(target.data('combatant-id')); } })
|
||||
}
|
||||
|
||||
export function setHookListeners() {
|
||||
|
||||
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
|
||||
const _toggleHudWraps = (prefix) => {
|
||||
const enable = $(html).find(`.${prefix}-wrap`)[0].classList.contains(`${prefix}-hud-disabled`)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const w = $(html).find(`.${prefix}-wrap`)[i]
|
||||
w.classList.toggle(`${prefix}-hud-active`, enable)
|
||||
w.classList.toggle(`${prefix}-hud-disabled`, !enable)
|
||||
}
|
||||
}
|
||||
const _disableHudWraps = (prefix) => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const w = $(html).find(`.${prefix}-wrap`)[i]
|
||||
w.classList.remove(`${prefix}-hud-active`)
|
||||
w.classList.add(`${prefix}-hud-disabled`)
|
||||
}
|
||||
}
|
||||
|
||||
// HP Loss Button
|
||||
const lossHPButton = await foundry.applications.handlebars.renderTemplate('systems/fvtt-lethal-fantasy/templates/loss-hp-hud.hbs', {})
|
||||
$(html).find('div.left').append(lossHPButton);
|
||||
$(html).find('img.lethal-hp-loss-hud').click((event) => {
|
||||
event.preventDefault();
|
||||
_toggleHudWraps("hp-loss")
|
||||
})
|
||||
$(html).find('.loss-hp-hud-click').click(async (event) => {
|
||||
event.preventDefault();
|
||||
await hudActor.applyDamage(Number(event.currentTarget.dataset.hpValue));
|
||||
_disableHudWraps("hp-loss")
|
||||
})
|
||||
|
||||
// HP Gain Button
|
||||
const gainHPButton = await foundry.applications.handlebars.renderTemplate('systems/fvtt-lethal-fantasy/templates/gain-hp-hud.hbs', {})
|
||||
$(html).find('div.left').append(gainHPButton);
|
||||
$(html).find('img.lethal-hp-gain-hud').click((event) => {
|
||||
event.preventDefault();
|
||||
_toggleHudWraps("hp-gain")
|
||||
})
|
||||
$(html).find('.gain-hp-hud-click').click(async (event) => {
|
||||
event.preventDefault();
|
||||
await hudActor.applyDamage(Number(event.currentTarget.dataset.hpValue));
|
||||
// Clear bleeding wounds on heal
|
||||
const wounds = foundry.utils.duplicate(hudActor.system.hp.wounds || [])
|
||||
if (wounds.some(w => w.description === "Bleeding")) {
|
||||
await hudActor.update({
|
||||
"system.hp.wounds": wounds.map(w =>
|
||||
w.description === "Bleeding" ? { value: 0, duration: 0 } : w
|
||||
)
|
||||
})
|
||||
}
|
||||
_disableHudWraps("hp-gain")
|
||||
})
|
||||
|
||||
// Luck/Grit Buttons
|
||||
const luckGritButton = await foundry.applications.handlebars.renderTemplate('systems/fvtt-lethal-fantasy/templates/luck-grit-hud.hbs', {})
|
||||
$(html).find('div.left').append(luckGritButton);
|
||||
$(html).find('.lethal-luck-grit-hud').click((event) => {
|
||||
event.preventDefault();
|
||||
let wrap = $(html).find('.luck-grit-wrap')[0]
|
||||
if (wrap.classList.contains("luck-grit-hud-disabled")) {
|
||||
wrap.classList.add('luck-grit-hud-active');
|
||||
wrap.classList.remove('luck-grit-hud-disabled');
|
||||
} else {
|
||||
wrap.classList.remove('luck-grit-hud-active');
|
||||
wrap.classList.add('luck-grit-hud-disabled');
|
||||
}
|
||||
})
|
||||
$(html).find('.luck-grit-btn').click(async (event) => {
|
||||
event.preventDefault();
|
||||
const resource = event.currentTarget.dataset.resource;
|
||||
const amount = Number(event.currentTarget.dataset.amount);
|
||||
const current = Number(foundry.utils.getProperty(hudActor.system, `${resource}.current`)) || 0;
|
||||
const newValue = Math.max(0, current + amount);
|
||||
await hudActor.update({ [`system.${resource}.current`]: newValue });
|
||||
$(html).find('.luck-grit-wrap')[0].classList.remove('luck-grit-hud-active');
|
||||
$(html).find('.luck-grit-wrap')[0].classList.add('luck-grit-hud-disabled');
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function registerHandlebarsHelpers() {
|
||||
|
||||
Handlebars.registerHelper('isNull', function (val) {
|
||||
return val == null;
|
||||
});
|
||||
Handlebars.registerHelper('match', function (val, search) {
|
||||
if (val && search) {
|
||||
return val?.match(search);
|
||||
}
|
||||
return false
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('exists', function (val) {
|
||||
return val != null && val !== undefined;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('isEmpty', function (list) {
|
||||
if (list) return list.length === 0;
|
||||
else return false;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('notEmpty', function (list) {
|
||||
return list.length > 0;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('isNegativeOrNull', function (val) {
|
||||
return val <= 0;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('isNegative', function (val) {
|
||||
return val < 0;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('isPositive', function (val) {
|
||||
return val > 0;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('equals', function (val1, val2) {
|
||||
return val1 === val2;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('neq', function (val1, val2) {
|
||||
return val1 !== val2;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('gt', function (val1, val2) {
|
||||
return val1 > val2;
|
||||
})
|
||||
|
||||
Handlebars.registerHelper('lt', function (val1, val2) {
|
||||
return val1 < val2;
|
||||
})
|
||||
|
||||
Handlebars.registerHelper('gte', function (val1, val2) {
|
||||
return val1 >= val2;
|
||||
})
|
||||
|
||||
Handlebars.registerHelper('lte', function (val1, val2) {
|
||||
return val1 <= val2;
|
||||
})
|
||||
Handlebars.registerHelper('and', function (val1, val2) {
|
||||
return val1 && val2;
|
||||
})
|
||||
Handlebars.registerHelper('or', function (val1, val2) {
|
||||
return val1 || val2;
|
||||
})
|
||||
|
||||
Handlebars.registerHelper('or3', function (val1, val2, val3) {
|
||||
return val1 || val2 || val3;
|
||||
})
|
||||
|
||||
Handlebars.registerHelper('for', function (from, to, incr, block) {
|
||||
let accum = '';
|
||||
for (let i = from; i < to; i += incr)
|
||||
accum += block.fn(i);
|
||||
return accum;
|
||||
})
|
||||
|
||||
Handlebars.registerHelper('not', function (cond) {
|
||||
return !cond;
|
||||
})
|
||||
Handlebars.registerHelper('count', function (list) {
|
||||
return list.length;
|
||||
})
|
||||
Handlebars.registerHelper('countKeys', function (obj) {
|
||||
return Object.keys(obj).length;
|
||||
})
|
||||
|
||||
Handlebars.registerHelper('isEnabled', function (configKey) {
|
||||
return game.settings.get("bol", configKey);
|
||||
})
|
||||
Handlebars.registerHelper('split', function (str, separator, keep) {
|
||||
return str.split(separator)[keep];
|
||||
})
|
||||
|
||||
// If you need to add Handlebars helpers, here are a few useful examples:
|
||||
Handlebars.registerHelper('concat', function () {
|
||||
let outStr = '';
|
||||
for (let arg in arguments) {
|
||||
if (typeof arguments[arg] != 'object') {
|
||||
outStr += arguments[arg];
|
||||
}
|
||||
}
|
||||
return outStr;
|
||||
})
|
||||
|
||||
Handlebars.registerHelper('add', function (a, b) {
|
||||
return parseInt(a) + parseInt(b);
|
||||
});
|
||||
Handlebars.registerHelper('mul', function (a, b) {
|
||||
return parseInt(a) * parseInt(b);
|
||||
})
|
||||
Handlebars.registerHelper('sub', function (a, b) {
|
||||
return parseInt(a) - parseInt(b);
|
||||
})
|
||||
Handlebars.registerHelper('abbrev2', function (a) {
|
||||
return a.substring(0, 2);
|
||||
})
|
||||
Handlebars.registerHelper('abbrev3', function (a) {
|
||||
return a.substring(0, 3);
|
||||
})
|
||||
Handlebars.registerHelper('valueAtIndex', function (arr, idx) {
|
||||
return arr[idx];
|
||||
})
|
||||
Handlebars.registerHelper('includesKey', function (items, type, key) {
|
||||
return items.filter(i => i.type === type).map(i => i.system.key).includes(key);
|
||||
})
|
||||
Handlebars.registerHelper('includes', function (array, val) {
|
||||
return array.includes(val);
|
||||
})
|
||||
Handlebars.registerHelper('eval', function (expr) {
|
||||
return eval(expr);
|
||||
})
|
||||
Handlebars.registerHelper('isOwnerOrGM', function (actor) {
|
||||
log("Testing actor", actor.isOwner, game.userId)
|
||||
return actor.isOwner || game.isGM;
|
||||
})
|
||||
Handlebars.registerHelper('upperCase', function (text) {
|
||||
if (typeof text !== 'string') return text
|
||||
return text.toUpperCase()
|
||||
})
|
||||
Handlebars.registerHelper('upperFirst', function (text) {
|
||||
if (typeof text !== 'string') return text
|
||||
return text.charAt(0).toUpperCase() + text.slice(1)
|
||||
})
|
||||
Handlebars.registerHelper('upperFirstOnly', function (text) {
|
||||
if (typeof text !== 'string') return text
|
||||
return text.charAt(0).toUpperCase()
|
||||
})
|
||||
|
||||
// Handle v12 removal of this helper
|
||||
Handlebars.registerHelper('select', function (selected, options) {
|
||||
const escapedValue = RegExp.escape(Handlebars.escapeExpression(selected));
|
||||
const rgx = new RegExp(' value=[\"\']' + escapedValue + '[\"\']');
|
||||
const html = options.fn(this);
|
||||
return html.replace(rgx, "$& selected");
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export function getLethargyDice(level) {
|
||||
for (let s of SYSTEM.SPELL_LETHARGY_DICE) {
|
||||
if (Number(level) <= s.maxLevel) {
|
||||
return s.dice
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+394
-134
@@ -5,15 +5,16 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "fvtt-lethal-fantasy",
|
||||
"version": "1.0.0",
|
||||
"license": "UNLICENSED",
|
||||
"license": "MITs",
|
||||
"dependencies": {
|
||||
"gulp": "^5.0.0",
|
||||
"gulp-less": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@foundryvtt/foundryvtt-cli": "^1.0.2",
|
||||
"@foundryvtt/foundryvtt-cli": "^3.0.4",
|
||||
"commander": "^11.1.0",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
@@ -166,18 +167,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@foundryvtt/foundryvtt-cli": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@foundryvtt/foundryvtt-cli/-/foundryvtt-cli-1.0.3.tgz",
|
||||
"integrity": "sha512-M8NrMXFYpOEsLAbgRWfuMvUa9F6HwrROLtqhhBljqfVS1lgm0RJJY/7MObuXsTJOC6+Uu+QOPZTlw4k+hguG7w==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@foundryvtt/foundryvtt-cli/-/foundryvtt-cli-3.0.4.tgz",
|
||||
"integrity": "sha512-FAYqdxiQ4PZpQx0SaqMM19v907fW5KvbJ9OnT9kS8I0jaVh+PXTAuCDkoV31KrzGJIaGKEjCXNxnoU7qiFMRQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.2.0",
|
||||
"classic-level": "^1.2.0",
|
||||
"chalk": "^5.6.2",
|
||||
"classic-level": "^3.0.0",
|
||||
"esm": "^3.2.25",
|
||||
"js-yaml": "^4.1.0",
|
||||
"mkdirp": "^3.0.0",
|
||||
"nedb-promises": "^6.2.1",
|
||||
"yargs": "^17.7.1"
|
||||
"js-yaml": "^5.2.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
"nedb-promises": "^6.2.3",
|
||||
"yargs": "^18.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"fvtt": "fvtt.mjs"
|
||||
@@ -186,6 +188,29 @@
|
||||
"node": ">17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@foundryvtt/foundryvtt-cli/node_modules/js-yaml": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz",
|
||||
"integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@gulpjs/messages": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz",
|
||||
@@ -308,21 +333,21 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/abstract-level": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.4.tgz",
|
||||
"integrity": "sha512-eUP/6pbXBkMbXFdx4IH2fVgvB7M0JvR7/lIL33zcs0IBcwjdzSSl31TOJsaCzmKSSDF9h8QYSOJux4Nd4YJqFg==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-3.1.1.tgz",
|
||||
"integrity": "sha512-CW2gKbJFTuX1feMvOrvsVMmijAOgI9kg2Ie9Dq3gOcMt/dVVoVmqNlLcEUCT13NxHFMEajcUcVBIplbyDroDiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^6.0.3",
|
||||
"catering": "^2.1.0",
|
||||
"is-buffer": "^2.0.5",
|
||||
"level-supports": "^4.0.0",
|
||||
"level-supports": "^6.2.0",
|
||||
"level-transcoder": "^1.0.1",
|
||||
"module-error": "^1.0.1",
|
||||
"queue-microtask": "^1.2.3"
|
||||
"maybe-combine-errors": "^1.0.0",
|
||||
"module-error": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
@@ -646,20 +671,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/catering": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/catering/-/catering-2.1.1.tgz",
|
||||
"integrity": "sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
|
||||
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
@@ -702,34 +719,120 @@
|
||||
}
|
||||
},
|
||||
"node_modules/classic-level": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.4.1.tgz",
|
||||
"integrity": "sha512-qGx/KJl3bvtOHrGau2WklEZuXhS3zme+jf+fsu6Ej7W7IP/C49v7KNlWIsT1jZu0YnfzSIYDGcEWpCa1wKGWXQ==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/classic-level/-/classic-level-3.0.0.tgz",
|
||||
"integrity": "sha512-yGy8j8LjPbN0Bh3+ygmyYvrmskVita92pD/zCoalfcC9XxZj6iDtZTAnz+ot7GG8p9KLTG+MZ84tSA4AhkgVZQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abstract-level": "^1.0.2",
|
||||
"catering": "^2.1.0",
|
||||
"abstract-level": "^3.1.0",
|
||||
"module-error": "^1.0.1",
|
||||
"napi-macros": "^2.2.2",
|
||||
"node-gyp-build": "^4.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
|
||||
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"wrap-ansi": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/emoji-regex": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui/node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"string-width": "^7.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/clone": {
|
||||
@@ -1434,6 +1537,19 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-east-asian-width": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
|
||||
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
@@ -1934,6 +2050,7 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@@ -2213,12 +2330,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/level-supports": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/level-supports/-/level-supports-4.0.1.tgz",
|
||||
"integrity": "sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA==",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/level-supports/-/level-supports-6.2.0.tgz",
|
||||
"integrity": "sha512-QNxVXP0IRnBmMsJIh+sb2kwNCYcKciQZJEt+L1hPCHrKNELllXhvrlClVHXBYZVT+a7aTSM6StgNXdAldoab3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/level-transcoder": {
|
||||
@@ -2226,6 +2344,7 @@
|
||||
"resolved": "https://registry.npmjs.org/level-transcoder/-/level-transcoder-1.0.1.tgz",
|
||||
"integrity": "sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^6.0.3",
|
||||
"module-error": "^1.0.1"
|
||||
@@ -2333,6 +2452,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/maybe-combine-errors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/maybe-combine-errors/-/maybe-combine-errors-1.0.0.tgz",
|
||||
"integrity": "sha512-eefp6IduNPT6fVdwPp+1NgD0PML1NU5P6j1Mj5nz1nidX8/sWY7119WL8vTAHgqfsY74TzW0w1XPgdYEKkGZ5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -2389,6 +2518,7 @@
|
||||
"resolved": "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz",
|
||||
"integrity": "sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -2411,7 +2541,8 @@
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz",
|
||||
"integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
@@ -2449,6 +2580,7 @@
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
@@ -2752,26 +2884,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/queue-tick": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
|
||||
@@ -3456,30 +3568,85 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
|
||||
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"cliui": "^9.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"string-width": "^7.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
"yargs-parser": "^22.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
|
||||
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/emoji-regex": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yargs/node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
@@ -3594,18 +3761,29 @@
|
||||
}
|
||||
},
|
||||
"@foundryvtt/foundryvtt-cli": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@foundryvtt/foundryvtt-cli/-/foundryvtt-cli-1.0.3.tgz",
|
||||
"integrity": "sha512-M8NrMXFYpOEsLAbgRWfuMvUa9F6HwrROLtqhhBljqfVS1lgm0RJJY/7MObuXsTJOC6+Uu+QOPZTlw4k+hguG7w==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@foundryvtt/foundryvtt-cli/-/foundryvtt-cli-3.0.4.tgz",
|
||||
"integrity": "sha512-FAYqdxiQ4PZpQx0SaqMM19v907fW5KvbJ9OnT9kS8I0jaVh+PXTAuCDkoV31KrzGJIaGKEjCXNxnoU7qiFMRQQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^5.2.0",
|
||||
"classic-level": "^1.2.0",
|
||||
"chalk": "^5.6.2",
|
||||
"classic-level": "^3.0.0",
|
||||
"esm": "^3.2.25",
|
||||
"js-yaml": "^4.1.0",
|
||||
"mkdirp": "^3.0.0",
|
||||
"nedb-promises": "^6.2.1",
|
||||
"yargs": "^17.7.1"
|
||||
"js-yaml": "^5.2.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
"nedb-promises": "^6.2.3",
|
||||
"yargs": "^18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"js-yaml": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz",
|
||||
"integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"argparse": "^2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@gulpjs/messages": {
|
||||
@@ -3693,18 +3871,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"abstract-level": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.4.tgz",
|
||||
"integrity": "sha512-eUP/6pbXBkMbXFdx4IH2fVgvB7M0JvR7/lIL33zcs0IBcwjdzSSl31TOJsaCzmKSSDF9h8QYSOJux4Nd4YJqFg==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-3.1.1.tgz",
|
||||
"integrity": "sha512-CW2gKbJFTuX1feMvOrvsVMmijAOgI9kg2Ie9Dq3gOcMt/dVVoVmqNlLcEUCT13NxHFMEajcUcVBIplbyDroDiw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"buffer": "^6.0.3",
|
||||
"catering": "^2.1.0",
|
||||
"is-buffer": "^2.0.5",
|
||||
"level-supports": "^4.0.0",
|
||||
"level-supports": "^6.2.0",
|
||||
"level-transcoder": "^1.0.1",
|
||||
"module-error": "^1.0.1",
|
||||
"queue-microtask": "^1.2.3"
|
||||
"maybe-combine-errors": "^1.0.0",
|
||||
"module-error": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"acorn": {
|
||||
@@ -3919,16 +4096,10 @@
|
||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
||||
"dev": true
|
||||
},
|
||||
"catering": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/catering/-/catering-2.1.1.tgz",
|
||||
"integrity": "sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==",
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
|
||||
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"dev": true
|
||||
},
|
||||
"chokidar": {
|
||||
@@ -3957,27 +4128,77 @@
|
||||
}
|
||||
},
|
||||
"classic-level": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.4.1.tgz",
|
||||
"integrity": "sha512-qGx/KJl3bvtOHrGau2WklEZuXhS3zme+jf+fsu6Ej7W7IP/C49v7KNlWIsT1jZu0YnfzSIYDGcEWpCa1wKGWXQ==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/classic-level/-/classic-level-3.0.0.tgz",
|
||||
"integrity": "sha512-yGy8j8LjPbN0Bh3+ygmyYvrmskVita92pD/zCoalfcC9XxZj6iDtZTAnz+ot7GG8p9KLTG+MZ84tSA4AhkgVZQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"abstract-level": "^1.0.2",
|
||||
"catering": "^2.1.0",
|
||||
"abstract-level": "^3.1.0",
|
||||
"module-error": "^1.0.1",
|
||||
"napi-macros": "^2.2.2",
|
||||
"node-gyp-build": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
|
||||
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"wrap-ansi": "^9.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"dev": true
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
}
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"string-width": "^7.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"clone": {
|
||||
@@ -4483,6 +4704,12 @@
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
|
||||
},
|
||||
"get-east-asian-width": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
|
||||
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
|
||||
"dev": true
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
@@ -5019,9 +5246,9 @@
|
||||
}
|
||||
},
|
||||
"level-supports": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/level-supports/-/level-supports-4.0.1.tgz",
|
||||
"integrity": "sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA==",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/level-supports/-/level-supports-6.2.0.tgz",
|
||||
"integrity": "sha512-QNxVXP0IRnBmMsJIh+sb2kwNCYcKciQZJEt+L1hPCHrKNELllXhvrlClVHXBYZVT+a7aTSM6StgNXdAldoab3w==",
|
||||
"dev": true
|
||||
},
|
||||
"level-transcoder": {
|
||||
@@ -5114,6 +5341,12 @@
|
||||
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
|
||||
"integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg=="
|
||||
},
|
||||
"maybe-combine-errors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/maybe-combine-errors/-/maybe-combine-errors-1.0.0.tgz",
|
||||
"integrity": "sha512-eefp6IduNPT6fVdwPp+1NgD0PML1NU5P6j1Mj5nz1nidX8/sWY7119WL8vTAHgqfsY74TzW0w1XPgdYEKkGZ5A==",
|
||||
"dev": true
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -5405,12 +5638,6 @@
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true
|
||||
},
|
||||
"queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true
|
||||
},
|
||||
"queue-tick": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
|
||||
@@ -5931,24 +6158,57 @@
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
|
||||
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cliui": "^8.0.1",
|
||||
"cliui": "^9.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"string-width": "^7.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
"yargs-parser": "^22.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
|
||||
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
|
||||
"dev": true
|
||||
},
|
||||
"yocto-queue": {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@foundryvtt/foundryvtt-cli": "^1.0.2",
|
||||
"@foundryvtt/foundryvtt-cli": "^3.0.4",
|
||||
"commander": "^11.1.0",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
MANIFEST-000579
|
||||
MANIFEST-000693
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
2026/04/30-14:38:06.808992 7f3f35bff6c0 Recovering log #577
|
||||
2026/04/30-14:38:06.818757 7f3f35bff6c0 Delete type=3 #575
|
||||
2026/04/30-14:38:06.818831 7f3f35bff6c0 Delete type=0 #577
|
||||
2026/04/30-14:38:35.518816 7f3ee77fe6c0 Level-0 table #582: started
|
||||
2026/04/30-14:38:35.518836 7f3ee77fe6c0 Level-0 table #582: 0 bytes OK
|
||||
2026/04/30-14:38:35.525869 7f3ee77fe6c0 Delete type=0 #580
|
||||
2026/04/30-14:38:35.538258 7f3ee77fe6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
|
||||
2026/04/30-14:38:35.538288 7f3ee77fe6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
2026/04/29-20:13:54.005832 7fed937fe6c0 Recovering log #573
|
||||
2026/04/29-20:13:54.016172 7fed937fe6c0 Delete type=3 #571
|
||||
2026/04/29-20:13:54.016279 7fed937fe6c0 Delete type=0 #573
|
||||
2026/04/29-20:27:06.418200 7feb10fff6c0 Level-0 table #578: started
|
||||
2026/04/29-20:27:06.418231 7feb10fff6c0 Level-0 table #578: 0 bytes OK
|
||||
2026/04/29-20:27:06.424568 7feb10fff6c0 Delete type=0 #576
|
||||
2026/04/29-20:27:06.431809 7feb10fff6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
|
||||
2026/04/29-20:27:06.431871 7feb10fff6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,30 @@
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Safe Faller","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"1jI0I56YAbGNTKbm","system":{"description":"<p>The character has an unusual knack for avoiding damage when falling. The character takes no damage when falling from heights of 40 feet or less. Upon landing, the character will roll, ending in a crouched position ready for action. Beyond 40 feet, the character takes damage for each 10-foot increment, as if they were falling 10 feet. Safe fallers experience a 60-foot drop the same way others experience a 20-foot drop.</p>","cost":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Sense Curse or Possession","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"22CBQ8Fh6jUPX7fM","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">This character has an awareness of the supernatural and can sense if a mortal or object is <a name=\"_Int_jdnsMolB\">possessed</a> or cursed. This doesn’t enable them to break the curse or remove the possession. The character has the ability to detect if a possession or curse exists. The character should be told by the GM the specifics of the curse or who or what possesses the victim. Gauging the relative strength of the curse or possession should also be possible for the player. The GM should share relevant details about the scenario or adventure as necessary</span></p>","cost":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Voice Mimic","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"2fmKHYMqnQ2HPb6U","system":{"description":"<p>The character has the extraordinary ability to perfectly replicate the voices and speaking patterns of any mortal, making it impossible for even their loved ones to distinguish the imitation from the real person. It doesn’t reveal information about specific mortals, only the ability to mimic their speech patterns. This gift allows the character to impersonate any mortal whose speech they have heard, with no restrictions on the number of impersonations.</p>","cost":25},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Blind Fighting","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"3xsjiboRrciodQ58","system":{"description":"<p>The ability to confront an adversary veiled in darkness is one of the most sought-after and unusual abilities. This gift evokes an indescribable sensation, a phenomenon so rare it’s almost unheard of in our known world. Some believe it’s a divine gift, destined to help us fight the encroaching darkness. Others believe those who can do this are from an underground realm and are untrustworthy. Mortals often consider this gift more wondrous than the abilities to heal or create fire. The exceptionally gifted can fight effectively in complete darkness, whether blindfolded or permanently sightless, as if they had perfect vision. The gifted, despite their special ability, are rendered helpless in the dark, just like ordinary mortals, unless under attack, which highlights the absurdity of their gift. Their ability to fight blind doesn’t give them an edge over normal people in other activities in the dark. However, when attacked, they sense movements, attacks, and defenses just as if darkness did not blind them.</p><p class=\"MsoNormal\">Those who possess these gifts are unaffected by darkness and blindness in combat. This character is unrestricted in their ability to fight an unseen opponent. However, if the veiled or invisible attacker retreats, the gifted mortal lacks the means to follow or discern their direction of escape.</p><p class=\"MsoNormal\">This gift won’t prevent backstabs or surprise attacks, it only activates when the first attack happens, or if the character knows an attack is coming. A villain who blindfolds a character with this gift, expecting an easy victory, will be in for a surprise.</p><p class=\"MsoNormal\">This does not provide any ranged defense or allow for ranged attacks in darkness or while blind.</p>","cost":60},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Poison Immunity","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"5aTWPV1h6dOe9vc9","system":{"description":"<p>The character is immune to all poisons, regardless of their origin, be it natural, magical, or otherwise. <span style=\"mso-spacerun:yes\"> </span></p>","cost":40},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Thick Skin (noticeable)","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"BdLlTv5NY90BNl7g","system":{"description":"<p>The character gains 1 natural damage reduction because of their thicker, more resilient skin.</p><p class=\"MsoNormal\">If the skin is noticeably different, it will cost 20 development points. The character’s skin is thick and callused. A skin that doesn’t have a noticeable difference will cost 30 development points.</p>","cost":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Alert While Asleep","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"EoXHsO5ajOjDjHC3","system":{"description":"<p>Character <a name=\"_Int_T0InzPds\">has the ability to</a> meditate and gain the benefits of sleep but <a name=\"_Int_ZTvxAfT8\">remains</a> aware of the world around them. Although they do not have vision, they have an awareness that allows them to sense movements, sounds, smells, and temperature changes as if fully awake but does not improve over their normal perception abilities. Characters gain the advantage of never rolling for initiative as if they were asleep. <br />Cannot <a name=\"_Int_GBuziMyx\">be combined</a> with a reduced need to sleep.<br />Magic-users cannot purchase this gift because of their need for deep sleep while recovering aether points.</p>","cost":30},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Artistic","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"HEvDkOY00YFwBv4n","system":{"description":"","cost":10},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Increased Hit Points","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"IaciK9WGZi73o8nh","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">The character is remarkably tough and resistant to death. Provides a D4 boost in hit points. Multiple purchases are allowed. </span><span style=\"mso-spacerun:yes\"><span style=\"font-family:'Arial',sans-serif\"> </span></span></p>","cost":15},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Slayer","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"InSEGtiEAkhjL59G","system":{"description":"<p>Undead beware! This character deals extra damage against them, adding D4 to both their attack and damage rolls. Clerics and Paladins with this ability also receive the D4 when <a name=\"_Int_tMKd98b1\">attempting</a> to turn the undead.<span style=\"font-family:'Arial',sans-serif\"><br style=\"mso-special-character:line-break\" /><br style=\"mso-special-character:line-break\" /></span></p>","cost":30},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Fast Healer","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"JTMEDAezr2z5xzUV","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">The character gains one <a name=\"_Int_F753II27\">additional</a> healing point from a cycle of healing, be it natural healing, continued daily treatment or hospitalization. This is an additional point from what is normally granted for healing by your choice of mortal.<br /><br style=\"mso-special-character:line-break\" /><br style=\"mso-special-character:line-break\" /></span></p>","cost":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Sense Evil","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"KQ0vdJi2AjKnKt1Y","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">This gift grants the character the ability to sense the presence of evil in individuals or things. Contact with the person or item is essential for this sense to function. This gift operates passively, meaning the character doesn’t have to concentrate on it. This gift is unavoidable, requiring no dice roll and offering no chance for defense. The character needs to explain to the GM what it feels like to be near evil, so they can be alerted when their gift is active. Evil may manifest itself through symptoms such as a tingle on the back of your neck, a queasy feeling in your stomach, or a slight pain behind your eyes. The GM and players should collaboratively develop the specifics. </span><span style=\"mso-spacerun:yes\"><span style=\"font-family:'Arial',sans-serif\"> </span></span></p>","cost":25},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Musician","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"KnB9yQnJhLkGIfKv","system":{"description":"","cost":10},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Taunter","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"NTuH23RPaBNELgye","system":{"description":"<p>This character can taunt up to 3 enemies, compelling them to attack the taunter, ignoring allies and innocent victims. This remains true even if they’re already fighting or interacting with others. Once relentlessly mocked, the enemies will remain fixated on the taunter, refusing to flee, until either they or the taunter is eliminated. This skill is beneficial, but it’s crucial to remember its inherent dangers. The attacker, once taunted, will relentlessly pursue the character without stopping. If two players taunt, the first one to taunt will maintain the taunt effect. This ability is exclusive to players and cannot <a name=\"_Int_KHB0kFZD\">be used</a> to force player characters to act against their will.</p>","cost":30},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Thick Skin (Not noticeable)","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"PQEcP0b5MhXpdYoS","system":{"description":"<p>The character gains 1 natural damage reduction because of their thicker, more resilient skin.</p><p class=\"MsoNormal\">If the skin is noticeably different, it will cost 20 development points. The character’s skin is thick and callused. A skin that doesn’t have a noticeable difference will cost 30 development points.</p>","cost":30},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Animal Mimic","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"QtGB4xQkS0ddrt3g","system":{"description":"<p>With perfect accuracy, the character can mimic any animal’s noises, calls, and sound inflections, making it impossible for other animals to distinguish. Cannot provide information on what is being communicated, which may lead to an inappropriate response from other animals. This does not give the ability to understand or communicate with animals. The character can copy the sounds of animals with absolute precision. Any animal the character has heard communicating or making sounds can be impersonated, with no limit on the number of species.</p>","cost":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Fearless","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"ReoKLRqWoMhHvo1c","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">Character is immune to fear regardless of its source. Natural, magical, divine, dragon or other supernatural forces. </span><span style=\"mso-spacerun:yes\"><span style=\"font-family:'Arial',sans-serif\"> </span></span></p>","cost":30},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Sense Magic","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"TUJLQIMEkW3zLelZ","system":{"description":"<p>With this gift, the character can discern whether someone or something is infused with magic. The character needs to physically contact the person or item for this sense to function. This is a passive power, the character doesn’t need to actively focus on it. There’s no defense for this gift, so the player doesn’t need to roll. Describe to the GM what happens to your character when they encounter magic, so the GM can let you know when your gift activates. Magic can manifest in various ways, including goosebumps, a tingling chest, or a ringing in your ears. Player and GM should collaborate to fill in the details of this. <span style=\"mso-spacerun:yes\"> </span></p>","cost":25},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Reduced Need for Sleep","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"XLIy0LfuGZzMML0S","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">The character requires just 4 hours of sleep to regain health and aether points. A character can only sleep once a day to restore aether points, heal, or gain other benefits. This gift reduces rest time, giving the character more time to perform actions. </span><span style=\"mso-spacerun:yes\"><span style=\"font-family:'Arial',sans-serif\"> </span></span><span style=\"font-family:'Arial',sans-serif\"><br style=\"mso-special-character:line-break\" /><br style=\"mso-special-character:line-break\" /></span></p>","cost":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Hold Breath","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"dwMnrsOKU3TmHZHz","system":{"description":"<p>This character possesses the ability to hold their breath for an extended duration. The character, after 2 minutes of prep, can hold their breath and function as usual for 10 minutes. They can hold their breath for 15 minutes while stationary.</p><p class=\"MsoNormal\">If they have no time to prepare, they can hold their breath for 6 minutes during normal activities or 10 minutes while resting.</p><p class=\"MsoNormal\">When a character’s breath runs out, they’ll begin taking damage as normal. Once this gift’s time runs out, they will start to suffocate. <br style=\"mso-special-character:line-break\" /><br style=\"mso-special-character:line-break\" /></p>","cost":15},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Disease Immunity","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"dxZxAJ6Z8O1MJAAP","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">The character is immune to all diseases, regardless of their source, be it natural, magical, or otherwise. </span><span style=\"mso-spacerun:yes\"><span style=\"font-family:'Arial',sans-serif\"> </span></span></p>","cost":40},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Reduced Need for Food and Water","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"eJePqPw273udS0ME","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">This character can function at full capacity with only one ration of food and water every three days. Characters without access to food or water will only suffer half the normal effects of prolonged deprivation.</span></p>","cost":15},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"War Magician","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"jPQAVxHaJCSakQQy","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">This character excels in offensive spell casting, gaining an extra D4 on top of their combat casTting modifier. This effect doesn’t increase damage but makes the attack roll higher, making it harder for the target to dodge or resist the attack. Only spells that deal direct damage to the target qualify for this gift. <br style=\"mso-special-character:line-break\" /><br style=\"mso-special-character:line-break\" /></span></p>","cost":30},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Blood is Power","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"jaOah98vBdDRhwiv","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">This gift empowers magic-users to tap into their life’s blood for spell casting in desperate situations. A caster can exchange 1 hit point for 10 aether points, enabling them to cast a spell. Transferring health to aether wounds, the caster, leaving a black, bloody circular mark on their torso. Close examination reveals arcane runes within the wound, symbolizing life force fueling arcane power. The true meaning of the runes remains unclear, and this gift is extremely rare among those who practice magic.<br /><br style=\"mso-special-character:line-break\" /><br style=\"mso-special-character:line-break\" /></span></p>","cost":30},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Defend While Lethargic","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"k4rTrYDnmEFI0Oxk","system":{"description":"<p>Despite spell lethargy, the magic-user maintains their normal defensive capabilities. This doesn’t let them reduce the other effects of spell lethargy. The magic-user is limited to walking and lacks the ability to attack or cast spells. A flawless or legendary defense roll on the special results dice would lead to an attack, because it directly results from the defense roll.</p>","cost":30},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Silent Casting","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"ocSgJFS35odJV2Ie","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">Magic-users can cast spells silently, even if they normally require a verbal component. Even with this ability, magic-users cast spells at the same speed. The gift eliminates the need for verbalization during the spell, allowing the magic-user to cast it mentally. This would encompass scenarios where the magic-user is silenced, injured, or cannot speak. The need for somatic or material components persists. <br /><br style=\"mso-special-character:line-break\" /><br style=\"mso-special-character:line-break\" /></span></p>","cost":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Hard to Kill","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"tiSnIbFGcO3qlEk0","system":{"description":"<p>The character can sustain more negative hit points before dying. A character’s maximum hit point threshold is determined by adding +3 to their constitution score. This threshold dictates how far below zero their hit points can go before they die. The character’s stabilization roll is always one die higher than their constitution score allows. Maximum dice allowed is a D20.</p>","cost":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Combat Casting","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"u5iMDjYPhTrpBSp3","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">Magic-users can defend while casting a spell against a single attacker using their full normal defense. This lets them continue their spell casting. A hit on the magic-user still ruins the spell and costs 50 aether points. This gift is limited to defending against a single attacker. The caster faces a disfavored defense, while their attackers have favor against all other attackers. When there are multiple attackers, the Caster must indicate which attacker is being defended with this gift. The decision is subject to change if the designated attacker withdraws, is killed, or becomes incapacitated.<br /><br style=\"mso-special-character:line-break\" /><br style=\"mso-special-character:line-break\" /></span></p>","cost":30},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Distance Runner","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"uaFB5QtDfUCleMSd","system":{"description":"<p>The character can sprint or run for double the usual time. They can sprint for a number of seconds, equal to double their constitution, and run for double their constitution in minutes. <span style=\"mso-spacerun:yes\"> </span></p>","cost":10},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
{"folder":"yPWGvxHJbDNHVSnY","name":"Immune to Mind Control","type":"gift","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_gift.webp","_id":"x5gLtqlW4sdDmHTd","system":{"description":"<p>The character is immune to mind control and mental manipulation. All forms of magical persuasion or performance skills are included in this protection. This doesn’t make the character immune to fear or torture because those are emotional responses, not mind control.</p>","cost":40},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"exportSource":null,"coreVersion":"13.348","systemId":"fvtt-lethal-fantasy","systemVersion":"13.0.0","createdTime":1757441324033,"modifiedTime":1757441324033,"lastModifiedBy":"Ccm2SWj6Wb78Gkz7"}}
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-000576
|
||||
MANIFEST-000690
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
2026/04/30-14:38:06.823818 7f3f353fe6c0 Recovering log #574
|
||||
2026/04/30-14:38:06.834104 7f3f353fe6c0 Delete type=3 #572
|
||||
2026/04/30-14:38:06.834187 7f3f353fe6c0 Delete type=0 #574
|
||||
2026/04/30-14:38:35.525993 7f3ee77fe6c0 Level-0 table #579: started
|
||||
2026/04/30-14:38:35.526019 7f3ee77fe6c0 Level-0 table #579: 0 bytes OK
|
||||
2026/04/30-14:38:35.532067 7f3ee77fe6c0 Delete type=0 #577
|
||||
2026/04/30-14:38:35.538267 7f3ee77fe6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
|
||||
2026/04/30-14:38:35.538302 7f3ee77fe6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
2026/04/29-20:13:54.023030 7fed93fff6c0 Recovering log #570
|
||||
2026/04/29-20:13:54.032615 7fed93fff6c0 Delete type=3 #568
|
||||
2026/04/29-20:13:54.032677 7fed93fff6c0 Delete type=0 #570
|
||||
2026/04/29-20:27:06.286266 7feb10fff6c0 Level-0 table #575: started
|
||||
2026/04/29-20:27:06.286286 7feb10fff6c0 Level-0 table #575: 0 bytes OK
|
||||
2026/04/29-20:27:06.292048 7feb10fff6c0 Delete type=0 #573
|
||||
2026/04/29-20:27:06.299186 7feb10fff6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
|
||||
2026/04/29-20:27:06.299236 7feb10fff6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
MANIFEST-000581
|
||||
MANIFEST-000695
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2026/04/30-14:38:06.795186 7f3f353fe6c0 Recovering log #579
|
||||
2026/04/30-14:38:06.805935 7f3f353fe6c0 Delete type=3 #577
|
||||
2026/04/30-14:38:06.805994 7f3f353fe6c0 Delete type=0 #579
|
||||
2026/04/30-14:38:35.512680 7f3ee77fe6c0 Level-0 table #584: started
|
||||
2026/04/30-14:38:35.512729 7f3ee77fe6c0 Level-0 table #584: 0 bytes OK
|
||||
2026/04/30-14:38:35.518704 7f3ee77fe6c0 Delete type=0 #582
|
||||
2026/04/30-14:38:35.538247 7f3ee77fe6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
2026/04/30-14:38:35.538281 7f3ee77fe6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
2026/07/07-17:14:18.710797 7f677f7fe6c0 Recovering log #693
|
||||
2026/07/07-17:14:18.721726 7f677f7fe6c0 Delete type=3 #691
|
||||
2026/07/07-17:14:18.721797 7f677f7fe6c0 Delete type=0 #693
|
||||
2026/07/07-18:13:39.509293 7f677ebff6c0 Level-0 table #698: started
|
||||
2026/07/07-18:13:39.509377 7f677ebff6c0 Level-0 table #698: 0 bytes OK
|
||||
2026/07/07-18:13:39.515800 7f677ebff6c0 Delete type=0 #696
|
||||
2026/07/07-18:13:39.537358 7f677ebff6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
2026/07/07-18:13:39.537602 7f677ebff6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2026/04/29-20:13:53.990808 7fed927fc6c0 Recovering log #575
|
||||
2026/04/29-20:13:54.001559 7fed927fc6c0 Delete type=3 #573
|
||||
2026/04/29-20:13:54.001699 7fed927fc6c0 Delete type=0 #575
|
||||
2026/04/29-20:27:06.273802 7feb10fff6c0 Level-0 table #580: started
|
||||
2026/04/29-20:27:06.273877 7feb10fff6c0 Level-0 table #580: 0 bytes OK
|
||||
2026/04/29-20:27:06.280252 7feb10fff6c0 Delete type=0 #578
|
||||
2026/04/29-20:27:06.299161 7feb10fff6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
2026/04/29-20:27:06.299217 7feb10fff6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
2026/07/07-08:29:43.889413 7f677f7fe6c0 Recovering log #689
|
||||
2026/07/07-08:29:43.943359 7f677f7fe6c0 Delete type=3 #687
|
||||
2026/07/07-08:29:43.943451 7f677f7fe6c0 Delete type=0 #689
|
||||
2026/07/07-08:30:08.186974 7f677ebff6c0 Level-0 table #694: started
|
||||
2026/07/07-08:30:08.187025 7f677ebff6c0 Level-0 table #694: 0 bytes OK
|
||||
2026/07/07-08:30:08.193951 7f677ebff6c0 Delete type=0 #692
|
||||
2026/07/07-08:30:08.207875 7f677ebff6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
2026/07/07-08:30:08.208156 7f677ebff6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
MANIFEST-000276
|
||||
MANIFEST-000390
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2026/04/30-14:38:06.849252 7f3f353fe6c0 Recovering log #274
|
||||
2026/04/30-14:38:06.859072 7f3f353fe6c0 Delete type=3 #272
|
||||
2026/04/30-14:38:06.859126 7f3f353fe6c0 Delete type=0 #274
|
||||
2026/04/30-14:38:35.538433 7f3ee77fe6c0 Level-0 table #279: started
|
||||
2026/04/30-14:38:35.538452 7f3ee77fe6c0 Level-0 table #279: 0 bytes OK
|
||||
2026/04/30-14:38:35.544715 7f3ee77fe6c0 Delete type=0 #277
|
||||
2026/04/30-14:38:35.567736 7f3ee77fe6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
2026/04/30-14:38:35.567785 7f3ee77fe6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
2026/07/07-17:14:18.764463 7f677f7fe6c0 Recovering log #388
|
||||
2026/07/07-17:14:18.774743 7f677f7fe6c0 Delete type=3 #386
|
||||
2026/07/07-17:14:18.774813 7f677f7fe6c0 Delete type=0 #388
|
||||
2026/07/07-18:13:39.530110 7f677ebff6c0 Level-0 table #393: started
|
||||
2026/07/07-18:13:39.530142 7f677ebff6c0 Level-0 table #393: 0 bytes OK
|
||||
2026/07/07-18:13:39.537202 7f677ebff6c0 Delete type=0 #391
|
||||
2026/07/07-18:13:39.537544 7f677ebff6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
2026/07/07-18:13:39.537840 7f677ebff6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2026/04/29-20:13:54.051682 7fed937fe6c0 Recovering log #270
|
||||
2026/04/29-20:13:54.062010 7fed937fe6c0 Delete type=3 #268
|
||||
2026/04/29-20:13:54.062071 7fed937fe6c0 Delete type=0 #270
|
||||
2026/04/29-20:27:06.292128 7feb10fff6c0 Level-0 table #275: started
|
||||
2026/04/29-20:27:06.292152 7feb10fff6c0 Level-0 table #275: 0 bytes OK
|
||||
2026/04/29-20:27:06.299041 7feb10fff6c0 Delete type=0 #273
|
||||
2026/04/29-20:27:06.299199 7feb10fff6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
2026/04/29-20:27:06.299226 7feb10fff6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
2026/07/07-08:29:44.131849 7f677f7fe6c0 Recovering log #384
|
||||
2026/07/07-08:29:44.183800 7f677f7fe6c0 Delete type=3 #382
|
||||
2026/07/07-08:29:44.183891 7f677f7fe6c0 Delete type=0 #384
|
||||
2026/07/07-08:30:08.227162 7f677ebff6c0 Level-0 table #389: started
|
||||
2026/07/07-08:30:08.227201 7f677ebff6c0 Level-0 table #389: 0 bytes OK
|
||||
2026/07/07-08:30:08.234023 7f677ebff6c0 Delete type=0 #387
|
||||
2026/07/07-08:30:08.242020 7f677ebff6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
2026/07/07-08:30:08.254520 7f677ebff6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Dependent Ally","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"0pt2JD8zDsbZd9Oa","system":{"description":"<p>The character has a responsibility to protect or serve someone dependent or vulnerable. Parent, spouse, child, sister, and grandma are all examples. Dependent individuals often face dangerous circumstances and require financial support for healthcare, addiction, or other necessities. Players and the GM can collaborate to determine the exact details.</p><p class=\"MsoNormal\">The character’s primary duty is to prioritize the dependent ally’s needs and well-being over other tasks, or risk losing their support. The loss or death of a dependent ally results in the obligation to repay granted development points using current or future points.</p><p class=\"MsoNormal\">The GM can use the dependent ally to create difficult quests for the character. The GM should use this tool sparingly when it comes to their campaign narrative, but it’s a good option for filling gaps in gameplay or creating smaller quests. This vulnerability could surface at the worst possible time. Despite being badly wounded and needing time to rest, the character’s sister is kidnapped by a notorious bandit, forcing them into a daring rescue mission. The character’s father may have a fatal illness, prompting a search for a cleric to heal him. The potential is boundless.</p><p class=\"MsoNormal\"> </p>","cost":0,"gainedPoints":20},"effects":[],"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1746521463778,"modifiedTime":1746521463778,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null},"sort":0,"ownership":{"default":0,"l74y1UZmsu5KYmeR":3}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Delusional","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"3EKlIiVJwfQk3nai","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">This character is completely deluded, holding onto a false belief no matter what</span></p>","cost":0,"gainedPoints":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Clean Obsession","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"69pg6b2nYh3thR0o","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">The character has a powerful compulsion to maintain cleanliness for themselves and their equipment. They maintain a strict cleaning routine, meticulously cleaning all their items nightly and swiftly removing blood, dirt, or grime from their weapons and armor after each battle. Blood or dirt stains on their clothes cause them great anxiety, and they will become preoccupied with finding a way to remove them. Backup clothing, cleaning supplies, and other essentials should always be on their gear list. Characters camping by water are drawn to bathe, even if it’s not the most sensible choice in the circumstances. They find the clean water too tempting to ignore. While this doesn’t hold true for dirty water, <a name=\"_Int_AzixlEWi\">swamps</a>, or bogs, clean blue water will draw them in like a moth to a flame. Characters covered in dirt, blood, grime, or even worse, feces will <a name=\"_Int_Qb7IZGuK\">be distracted</a> and complain openly. The distraction will hinder their readiness, resulting in defending with disfavor until they clean themselves and their gear</span></p>","cost":0,"gainedPoints":10},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Paranoid","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"DJ37fv3mikulWe9j","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">The character has a sense of paranoia, believing someone is aiming to harm them. This feeling often stems from the belief that powerful people are scheming to ruin the character. This makes the character extremely wary of giving any information to authority figures. The character suspects almost everyone could be involved in the plot against them, making it incredibly difficult to trust anyone. The entity is blamed for all negative events, as if it intentionally caused them or is working tirelessly to destroy the character.</span></p>","cost":0,"gainedPoints":15},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Inappropriately Nosy","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"DVVr1Ga0wKDrXOvu","system":{"description":"<p>NOSY <br />(15) Development points.</p><p class=\"MsoNormal\">The character frequently asks inappropriate questions that are irrelevant to their journey and objectives. Their extreme curiosity about most mortals leads them to ask endless questions about anything that catches their interest. They might question the bartender about her experiences dating a half-orc, including details about their intimate life. They could question the big bad guy about his need for prisoners and the whereabouts of his parents. Are his parents aware of what he’s doing? Are there family members in his life? Where are they originally from?</p>","cost":0,"gainedPoints":15},"effects":[],"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1746521463778,"modifiedTime":1746521463778,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null},"sort":0,"ownership":{"default":0,"l74y1UZmsu5KYmeR":3}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Allergy","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"EEOS9oMqTLVrgmYI","system":{"description":"<p>The character suffers from a serious allergy triggering sneezing, coughing, blurry vision, and itchy welts and rashes. It’s tough to sneak and hide with this allergy, because they can’t stay still and make noise even when not moving. While sneaking under the influence of allergies, the character grants two circumstances that benefit the entity looking for them. Because they have blurry vision and a distorted sense of smell, perception rolls favor anyone sneaking or hiding from them by one circumstance. Their allergy causes them to attack with disfavor, and while affected, they can only move at a jogging pace. Upon removal from the allergen, the allergy will persist for either 1D6 real-time hours or 1 week of game time, whichever is shorter. <span style=\"mso-spacerun:yes\"> </span></p>","cost":0,"gainedPoints":10},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Awful Liar","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"EZUfYtvLdmtr2V5X","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">This character is a terrible liar, easily giving away their lies through physical and vocal cues. Expect the character to stutter, pause unexpectedly, or shake their head, contradicting their verbal response. Visible sweating, avoiding eye contact, looking down, fidgeting, or any other behavior that seems inconsistent with telling the truth could be signs they are lying. The player should act out the character’s inability to keep secrets, using this moment to lose the secret but gain grit by roleplaying the vulnerability</span></p>","cost":0,"gainedPoints":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Facial Scar","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"KSLK4GWd1yBswXDF","system":{"description":"<p>The character’s face is marred by a severe scar. Because of the scar, they face disfavor on all performance rolls. In role play, the character can choose to either hide or embrace the scar, but the penalty to performance persists. A character’s charisma is unaffected by being scared. Reduce the character’s appearance by 5.</p>","cost":0,"gainedPoints":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Superstitious","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"LmoNkyM88iK34NvK","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">The character is completely convinced that a particular item or act is essential for them to succeed. If the character lacks the item or fails to properly follow the superstition, their ability to succeed in any task will be compromised. A character lacking the “lucky item” or failing to complete the pre-task ritual will be at disfavor during any task. They roll with disfavor to both attack and defense when in combat. Challenges of strength or agility, as well as saving throws and arcane attacks, are among the rolls that must be made with disfavor.<br style=\"mso-special-character:line-break\" /><br style=\"mso-special-character:line-break\" /></span></p>","cost":0,"gainedPoints":30},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Albino","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"M8Tubc4rmXyg12Zz","system":{"description":"<p>This character, pale as snow, cannot withstand the sun’s heat. Under bright sunlight, they face disfavor on all rolls but do not experience any penalties at night. The character is without dark pigment, making their skin the color of pristine snow. Their skin, hair, eyes, nails and toenails are all pale and white. Many NPCs react poorly to their unconventional appearance, resulting in disfavor for all performance-based rolls. Wearing a dark cloak or robe that hides their face and skin allows the character to evade the negative effects. The effects activate during the day when the cloak or covering is lost, causing unfavorable rolls for combat, skills, and saving throws while exposed to sunlight.</p>","cost":0,"gainedPoints":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Easily Fooled","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"MTJReszg6Q4pG528","system":{"description":"<p>This character is quite naïve and gullible. They find credibility in the most improbable narratives and readily accept even the most absurd accounts of events. The character won’t negotiate on prices, accepting the value of any item for sale without debate. NPCs and others can easily deceive them with blatant lies, and they will believe even the most absurd tales. <span style=\"mso-spacerun:yes\"> </span></p>","cost":0,"gainedPoints":15},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Death Wish","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"QAIKh7qB3ImHnVCU","system":{"description":"<p>This character, haunted by past mistakes and pain, believes that only a heroic death in battle will allow them redemption. It’s crucial to understand that this isn’t suicide; they avoid deliberately ending their lives, as it would be seen as unfair. A character with a death wish won’t run from an enemy unless offered a greater glory or a more satisfying death. No matter how hopeless it seems, a worthy quest will always be pursued. They won’t abandon a situation where a glorious death is a probable outcome. A rational explanation is required for any decision that goes against their desire for a glorious death. The GM should explicitly state that characters’ actions could lead to their death if they are presented with such a scenario. If a character must choose between running and fighting, players should engage in roleplaying the decision. This is the pinnacle of role playing and should not <a name=\"_Int_J3ILFVXF\">be resolved</a> by chance. People who are not comfortable with role playing should steer clear of this vulnerability. Failing to fully capture the character’s desire, including their weaknesses and unique traits, will result in significant penalties in Grit. This would involve applying earned development points to eliminate the vulnerability. If the party convincingly argues for the greater good of escaping to fight again, they could earn additional Grit for their impressive roleplaying.<span style=\"mso-spacerun:yes\"> </span></p><p class=\"MsoNormal\">Role <a name=\"_Int_gMfqxeDW\">play</a> examples:</p><p class=\"MsoNormal\">If the character chooses to fight the monster, who will tell the king about the threat? The most noble act is to warn the citizens, allowing them to flee, and then for the character to die bravely defending them, ensuring their sacrifice is remembered by all. Dying here would be pointless, as no one would know or sing about the heroic deeds.</p><p class=\"MsoNormal\">Dying without saving the princess means she also dies, and a glorious death after a successful quest is far better than a failed quest and death.<br />Players should consider a variety of ideas instead of relying on the same one repeatedly to escape the consequences of the vulnerability.</p><p class=\"MsoNormal\">This vulnerability surpasses the “won’t back down vulnerability”. Beyond mere defiance, this character embraces risk, actively pursuing situations with a high chance of death rather than simply refusing to yield. <span style=\"mso-spacerun:yes\"> </span></p>","cost":0,"gainedPoints":40},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Chivalrous","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"SL8M9rYD36ITwmvG","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">Character is chivalrous to a fault. Opening doors or offering a blanket or coat to someone who is cold are often acts of chivalry. Showing politeness and respect to those around you. This courtesy is often extended to members of the opposite sex, even if they are a declared and proven mortal enemy. This is especially true for the fairer sex, who are always given a second chance. Despite not deserving it, they will <a name=\"_Int_BjZRw24T\">be treated</a> fairly. A chivalrous character would never attack a woman by striking first or attacking her from behind. They will not strike a woman who is unconscious or unable to defend herself. While this vulnerability is common among men, it can be modified for the specific gender dynamics of your world.<br />This vulnerability might be unsuitable for some backstories. Knights and Clerics codes may include most of these conditions, but they’re part of their class benefits and shouldn’t earn development points</span></p>","cost":0,"gainedPoints":10},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Illiterate","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"T5bR4EQ9bERMKPdR","system":{"description":"<p>It is assumed that all characters in LETHAL FANTASY possess literacy, being able to read and write in their native language, plus any additional languages they have learned. Illiteracy leaves individuals susceptible to scams and manipulation, as they cannot understand even the simplest of written warnings. Basic information, sign reading, and document review often come with a charge.</p>","cost":0,"gainedPoints":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Hyper Competitive","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"bC3WMMvFeWU2si6Y","system":{"description":"<p>Character is competitive, whether it’s something important or just a casual matter. Which combatant achieved the most enemy kills during the battle? Who can consume the most ale? Who will be the first to reach the dungeon? The player should embrace the role of someone who is intensely competitive. A reckless pursuit of an extra kill or a solo attempt to reach the treasure first could endanger both themselves and their allies. They don’t act recklessly or have a disregard for their safety, but they are driven to win and excel in any event, competition, or battle they face.</p>","cost":0,"gainedPoints":15},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Vigilante","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"dFQG6V5jpPCu3Yi1","system":{"description":"<p>Driven by an unyielding desire for justice, the character acts as a vigilante, seeking to rectify the world’s injustices. When someone avoids rightful consequences for their immoral actions, the character feels compelled to intervene. Only characters of good alignment are susceptible to this vulnerability because they are repulsed by acts of obvious malice and evil. We’re not talking about petty offenses, but serious crimes like murder, rape, and harming children - acts that truly violate our sense of morality. The character must confront the issue immediately, as they can’t simply let it pass. While this doesn’t mean abandoning friends in the middle of an adventure or quest, it does mean that the first time they lack purpose, the character must seek revenge. This could involve persuading authorities to act justly or taking matters into their own hands, even resorting to violence, including murder, to achieve retribution.</p>","cost":0,"gainedPoints":30},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Fear of Magic","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"dSQF1Lk9toVBUWJf","system":{"description":"<p>The character is staunchly opposed to magic, viewing it as dangerous, unclean, or simply wicked, and will not allow themselves to be enchanted. They won’t accept spells, even for healing or aid, on their own bodies. Both arcane and divine magic are considered unnatural by the character. The character is wary of magic users and clerics, remaining vigilant even though they don’t outright distrust fellow players. This heightened awareness stems from a fear that the magic might be turned against them. The player’s character should have a backstory that accounts for their fear. A charlatan might have promised a magical or clerical remedy, only for it to fail miserably. A magical event of catastrophic proportions took place. Someone close to the character was struck by a spell, inflicting devastating destruction or death. There are many ways the character’s fear can be portrayed. Let your imagination run wild. This could turn into a remarkable story of redemption.</p><p class=\"MsoNormal\">Unknown to the character, they could be healed or helped by magic or divine intervention. Any adventuring group will find this hard to resist. Using magic on unconscious or injured characters risks eroding trust in the caster. This offers an excellent opportunity for role-playing.</p><p class=\"MsoNormal\"> </p>","cost":0,"gainedPoints":35},"effects":[],"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1746521463778,"modifiedTime":1746521463778,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null},"sort":0,"ownership":{"default":0,"l74y1UZmsu5KYmeR":3}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Fear of Heights","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"eN3Qi0jCtreCujDn","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">The character’s crippling fear of heights restricts their movement to walking speed when they are on ledges, bridges, or pits over 20 feet above ground or water. The character cannot attack while they are dealing with this fear and they will defend with disfavor. Skills used near ledges, bridges, or pits are with disfavor. This reaction is triggered by anything the character fears they might fall into. If a character stumbles or trips and faces a serious risk of falling, they roll saving throws or other relevant rolls to avoid falling with disfavor</span></p>","cost":0,"gainedPoints":20},"effects":[],"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1746521463778,"modifiedTime":1746521463778,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null},"sort":0,"ownership":{"default":0,"l74y1UZmsu5KYmeR":3}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Honest to a Fault","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"fBkyX5erOJCF4btA","system":{"description":"<p>The character is incapable of lying. For the character, a lie is a moral offense that diminishes the world. This presents challenges in many contexts. The most effective action for the character is to remain silent in response to a question. Silence can be as revealing as saying the truth out loud. This vulnerability includes white lies. The character’s integrity is unwavering, meaning they won’t lie even to spare the feelings of someone close to them. A person who always speaks the truth, even about trivial matters, could find themselves alone at the campfire. Their high moral values might be the only thing that keeps them company. This is a fantastic opportunity to role play, but remember, being in character doesn’t excuse being cruel to others.</p>","cost":0,"gainedPoints":25},"effects":[],"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1746521463778,"modifiedTime":1746521463778,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null},"sort":0,"ownership":{"default":0,"l74y1UZmsu5KYmeR":3}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Bragger","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"iNSHcfW0RsEIJwj1","system":{"description":"<p>The character brags about their accomplishments in every aspect of their existence. This harkens back to their childhood and playground encounters where they were always the winner. Their youthful love affairs, early class training, anything they’ve ever achieved. Each achievement is an opportunity to boast and elaborate on it for all to hear. The character’s preoccupation is so blatant and repetitive that anyone who knows them will find the claims unconvincing. The character’s habit of overstating their successes creates an atmosphere of disbelief, even when they are truthful. This leads to delay in response if they need help or when they truly need to <a name=\"_Int_kzubRJoD\">be believed</a>.</p>","cost":0,"gainedPoints":10},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Paranoid of Death","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"jFAgsJQJA94Uxxy0","system":{"description":"<p>A premonition of their imminent demise has caused the character to develop an irrational fear of death. The possibilities are endless. It could be a fortune teller, a tarot card, a lifelike dream, or any other creative concept the character desires. They fear imminent death, leading them to second-guess their choices in a desperate attempt to cheat fate. Characters with this vulnerability will not take point on missions. Even when they win the initiative, they never attack first and will wait until they are attacked. This could cause losing an attack chance during the attack delay progression. Before <a name=\"_Int_g5TWGg8e\">taking action</a>, the character always deliberates carefully, avoiding impulsive behavior. The prophecy is probably not true. However, we’ll leave that to creative Game Magistrates and just speculate on where this prophecy might lead a group of brave adventurers.</p>","cost":0,"gainedPoints":25},"effects":[],"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1746521463778,"modifiedTime":1746521463778,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null},"sort":0,"ownership":{"default":0,"l74y1UZmsu5KYmeR":3}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Animal Hostility","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"oXV86z4CsRhUnKFg","system":{"description":"<p>This character faces hostility from a particular type of animal, which will growl, hiss, bark, or demonstrate its anger, possibly attacking the character on sight if the GM thinks it’s justified.</p>","cost":0,"gainedPoints":20},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Won't Back Down","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"sUOoazijnWvI0YZd","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">This character has unshakeable confidence in their skills and refuses to believe defeat is possible. They’ll fight until the very end, ignoring the fact that a reasonable person would give up. This overconfidence goes beyond combat and can encompass competitions of skill, intellect, or endurance. No matter how badly this character loses, they’ll find an excuse that points the finger at external factors rather than their own skills. The judges were bribed, the sun was in my eyes, I should have used my strong-arm, that was a lucky punch. Numerous excuses will spring to mind, effectively maintaining the character’s image of invincibility.</span></p>","cost":0,"gainedPoints":30},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Afraid of Animal","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"xin46ARJ0FPXMSTC","system":{"description":"<p>The character has an unreasonable fear of a creature that poses no real danger. Upon spotting the creature, the character will try to avoid it and absolutely will not touch it. Combat against the harmless creature is impossible, the character will defend with disfavor and is incapable of attacking it.</p><p class=\"MsoNormal\">The animal is likely to be seen in adventure areas, which will make the character afraid and unable to perform well because of fear of the animal or bad luck. The sight of the animal, even if it’s simply munching on grass, will cause the character to experience the “yips,” making them doubt their choices for the next hour. All rolls they make are with disfavor. This includes attack, defense, spell attacks, saving throws, and skill checks.</p>","cost":0,"gainedPoints":25},"effects":[],"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1746521463778,"modifiedTime":1746521463778,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null},"sort":0,"ownership":{"default":0,"l74y1UZmsu5KYmeR":3}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Looks Like a Criminal","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"xnvdjC0xMy0dfigw","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">The character is the spitting image of a real criminal who carried out a terrible deed. The character may share a resemblance to the images of wanted posters and drawings of the criminal. A reward offered by the authorities for the capture of the real criminal increases the likelihood of individuals in a large community misidentifying the character as the culprit. The character has a 1 in 10 chance of being mistaken for the criminal during any interaction with an NPC. Depending on the situation, the NPC might summon the guards, run away in fear, or shout for help. The GM needs to determine what’s <a name=\"_Int_XSbT5SAK\">appropriate</a>.</span></p>","cost":0,"gainedPoints":10},"effects":[],"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1746521463778,"modifiedTime":1746521463778,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null},"sort":0,"ownership":{"default":0,"l74y1UZmsu5KYmeR":3}}
|
||||
{"folder":"mnO9OzE7BEE2KDfh","name":"Conspiracy Theorist","type":"vulnerability","img":"systems/fvtt-lethal-fantasy/assets/icons/icon_vulnerability.webp","_id":"zkK6ixtCsCw3RH9X","system":{"description":"<p><span style=\"font-family:'Arial',sans-serif\">The character believes a secret plot is underway, orchestrated by influential figures, both known and unknown, who hold positions of power. Most of the world’s terrible events and disasters can be traced back to these mortals in power. The character is fiercely dedicated to revealing the conspiracy to their group and holds a deep distrust of all authority. <br />Players need to figure out how the secret plan connects to each adventure, making everything understandable</span></p>","cost":0,"gainedPoints":15},"effects":[],"sort":0,"ownership":{"default":0,"Ccm2SWj6Wb78Gkz7":3},"flags":{},"_stats":{"compendiumSource":null,"duplicateSource":null,"coreVersion":"13.342","systemId":"fvtt-lethal-fantasy","systemVersion":"12.0.35","createdTime":1736956548287,"modifiedTime":1746521463767,"lastModifiedBy":"l74y1UZmsu5KYmeR","exportSource":null}}
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-000575
|
||||
MANIFEST-000689
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2026/04/30-14:38:06.836968 7f3ee7fff6c0 Recovering log #573
|
||||
2026/04/30-14:38:06.846980 7f3ee7fff6c0 Delete type=3 #571
|
||||
2026/04/30-14:38:06.847050 7f3ee7fff6c0 Delete type=0 #573
|
||||
2026/04/30-14:38:35.532186 7f3ee77fe6c0 Level-0 table #578: started
|
||||
2026/04/30-14:38:35.532209 7f3ee77fe6c0 Level-0 table #578: 0 bytes OK
|
||||
2026/04/30-14:38:35.538132 7f3ee77fe6c0 Delete type=0 #576
|
||||
2026/04/30-14:38:35.538276 7f3ee77fe6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
2026/04/30-14:38:35.538295 7f3ee77fe6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
2026/07/07-17:14:18.752018 7f67ccbfe6c0 Recovering log #687
|
||||
2026/07/07-17:14:18.762282 7f67ccbfe6c0 Delete type=3 #685
|
||||
2026/07/07-17:14:18.762342 7f67ccbfe6c0 Delete type=0 #687
|
||||
2026/07/07-18:13:39.562602 7f677ebff6c0 Level-0 table #692: started
|
||||
2026/07/07-18:13:39.562663 7f677ebff6c0 Level-0 table #692: 0 bytes OK
|
||||
2026/07/07-18:13:39.568947 7f677ebff6c0 Delete type=0 #690
|
||||
2026/07/07-18:13:39.569296 7f677ebff6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
2026/07/07-18:13:39.569322 7f677ebff6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2026/04/29-20:13:54.036694 7fed92ffd6c0 Recovering log #569
|
||||
2026/04/29-20:13:54.047834 7fed92ffd6c0 Delete type=3 #567
|
||||
2026/04/29-20:13:54.047922 7fed92ffd6c0 Delete type=0 #569
|
||||
2026/04/29-20:27:06.280338 7feb10fff6c0 Level-0 table #574: started
|
||||
2026/04/29-20:27:06.280357 7feb10fff6c0 Level-0 table #574: 0 bytes OK
|
||||
2026/04/29-20:27:06.286179 7feb10fff6c0 Delete type=0 #572
|
||||
2026/04/29-20:27:06.299174 7feb10fff6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
2026/04/29-20:27:06.299209 7feb10fff6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
2026/07/07-08:29:44.074796 7f677ffff6c0 Recovering log #683
|
||||
2026/07/07-08:29:44.128067 7f677ffff6c0 Delete type=3 #681
|
||||
2026/07/07-08:29:44.128136 7f677ffff6c0 Delete type=0 #683
|
||||
2026/07/07-08:30:08.208175 7f677ebff6c0 Level-0 table #688: started
|
||||
2026/07/07-08:30:08.208206 7f677ebff6c0 Level-0 table #688: 0 bytes OK
|
||||
2026/07/07-08:30:08.214632 7f677ebff6c0 Delete type=0 #686
|
||||
2026/07/07-08:30:08.234270 7f677ebff6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
2026/07/07-08:30:08.242040 7f677ebff6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
|
||||
BIN
Binary file not shown.
@@ -387,11 +387,11 @@
|
||||
min-width: 6rem;
|
||||
max-width: 6rem;
|
||||
}
|
||||
min-width: 10rem;
|
||||
max-width: 10rem;
|
||||
min-width: 11rem;
|
||||
max-width: 11rem;
|
||||
.input {
|
||||
min-width: 2.5rem;
|
||||
max-width: 2.5rem;
|
||||
min-width: 3.5rem;
|
||||
max-width: 3.5rem;
|
||||
}
|
||||
}
|
||||
.granted {
|
||||
|
||||
+69
-12
@@ -145,9 +145,11 @@
|
||||
}
|
||||
|
||||
.grit-luck-dialog {
|
||||
color: var(--color-text-dark-primary, #191813);
|
||||
|
||||
.combat-status {
|
||||
padding: 12px;
|
||||
background: linear-gradient(to bottom, rgba(42, 41, 32, 0.8) 0%, rgba(26, 25, 16, 0.9) 100%);
|
||||
background: linear-gradient(to bottom, rgba(42, 41, 32, 0.88) 0%, rgba(26, 25, 16, 0.95) 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.5);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
@@ -171,11 +173,27 @@
|
||||
}
|
||||
|
||||
.offer-text {
|
||||
color: #f0e6d2;
|
||||
color: var(--color-text-dark-primary, #191813);
|
||||
font-size: calc(var(--font-size-standard) * 1);
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.shield-warning {
|
||||
color: #7a4000;
|
||||
background: rgba(255, 160, 0, 0.12);
|
||||
border: 1px solid rgba(255, 160, 0, 0.4);
|
||||
border-radius: 5px;
|
||||
font-size: calc(var(--font-size-standard) * 0.88);
|
||||
padding: 6px 10px;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
|
||||
i {
|
||||
color: #c07000;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,33 +301,72 @@
|
||||
}
|
||||
|
||||
.attack-result-damage {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
|
||||
&.single-btn {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&.spell-damage {
|
||||
grid-template-columns: 1fr;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roll-damage-btn {
|
||||
padding: 10px 16px;
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(to bottom, #8b0000 0%, #660000 100%);
|
||||
border: 1px solid #ff0000;
|
||||
border: 1px solid #4b0000;
|
||||
border-radius: 6px;
|
||||
color: #f0e6d2;
|
||||
font-weight: 600;
|
||||
font-size: calc(var(--font-size-standard) * 0.9);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: calc(var(--font-size-standard) * 1.1);
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(to bottom, #a00000 0%, #7b0000 100%);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
border-color: #5b0000;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4), inset 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,66 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Luck/Grit Styles */
|
||||
#token-hud .luck-grit-wrap {
|
||||
position: absolute;
|
||||
left: 75px;
|
||||
display: none;
|
||||
top: 50%;
|
||||
width: 80px;
|
||||
text-align: start;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 1px solid rgba(139, 69, 19, 0.5);
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px;
|
||||
transform: translate(-100%, -50%);
|
||||
}
|
||||
|
||||
#token-hud .luck-grit-hud-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#token-hud .luck-grit-hud-disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#token-hud .luck-grit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
#token-hud .luck-grit-label {
|
||||
flex: 1;
|
||||
color: #c9b896;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#token-hud .luck-grit-btn {
|
||||
width: 28px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
background: rgba(139, 69, 19, 0.25);
|
||||
border: 1px solid rgba(139, 69, 19, 0.4);
|
||||
border-radius: 3px;
|
||||
color: #d4c5a9;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: rgba(139, 69, 19, 0.5);
|
||||
border-color: rgba(139, 69, 19, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Dice Tray — injected into the Foundry chat sidebar */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
@@ -410,6 +410,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ranged-attacks {
|
||||
grid-template-columns: 1fr;
|
||||
.attack {
|
||||
.name {
|
||||
min-width: 10rem;
|
||||
max-width: 10rem;
|
||||
}
|
||||
.damage-dice {
|
||||
width: 5rem;
|
||||
max-width: 5rem;
|
||||
}
|
||||
}
|
||||
.ranged-weapon-range {
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid var(--color-border-light-tertiary, #ccc);
|
||||
padding-top: 6px;
|
||||
> label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
}
|
||||
.range-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
.range-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
label {
|
||||
font-size: 0.7rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
input {
|
||||
width: 3.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.armors {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
+69
-17
@@ -176,6 +176,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-warning {
|
||||
margin: 0.4rem 0.2rem 0.2rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border-left: 3px solid #c8941a;
|
||||
background: rgba(200, 148, 26, 0.12);
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-secondary);
|
||||
font-size: calc(var(--font-size-standard) * 0.9);
|
||||
color: #7a5400;
|
||||
line-height: 1.4;
|
||||
i {
|
||||
color: #c8941a;
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.lethalfantasy,
|
||||
.fvtt-lethal-fantasy,
|
||||
.message.lethalfantasy,
|
||||
@@ -631,15 +647,11 @@
|
||||
|
||||
.damage-buttons-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 280px;
|
||||
margin: 0 auto;
|
||||
gap: 8px;
|
||||
|
||||
&.monster-damage {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.damage-roll-btn {
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(to bottom, #8b4513 0%, #6b3410 100%);
|
||||
@@ -831,6 +843,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
justify-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1269,33 +1282,72 @@
|
||||
}
|
||||
|
||||
.attack-result-damage {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
|
||||
&.single-btn {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&.spell-damage {
|
||||
grid-template-columns: 1fr;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roll-damage-btn {
|
||||
padding: 10px 16px;
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(to bottom, #8b0000 0%, #660000 100%);
|
||||
border: 1px solid #ff0000;
|
||||
border: 1px solid #4b0000;
|
||||
border-radius: 6px;
|
||||
color: #f0e6d2;
|
||||
font-weight: 600;
|
||||
font-size: calc(var(--font-size-standard) * 0.9);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: calc(var(--font-size-standard) * 1.1);
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(to bottom, #a00000 0%, #7b0000 100%);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
border-color: #5b0000;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4), inset 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,95 +9,95 @@
|
||||
<legend>{{localize "LETHALFANTASY.Label.biodata"}}</legend>
|
||||
<div class="biodata">
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Class</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.class"}}</span>
|
||||
{{formInput
|
||||
systemFields.biodata.fields.class
|
||||
value=system.biodata.class
|
||||
}}
|
||||
</div>
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Level</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.level"}}</span>
|
||||
{{formInput
|
||||
systemFields.biodata.fields.level
|
||||
value=system.biodata.level
|
||||
}}
|
||||
</div>
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Mortal</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.mortal"}}</span>
|
||||
{{formInput
|
||||
systemFields.biodata.fields.mortal
|
||||
value=system.biodata.mortal
|
||||
}}
|
||||
</div>
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Alignment</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.alignment"}}</span>
|
||||
{{formInput
|
||||
systemFields.biodata.fields.alignment
|
||||
value=system.biodata.alignment
|
||||
}}
|
||||
</div>
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Age</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.age"}}</span>
|
||||
{{formInput systemFields.biodata.fields.age value=system.biodata.age}}
|
||||
</div>
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Height</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.height"}}</span>
|
||||
{{formInput
|
||||
systemFields.biodata.fields.height
|
||||
value=system.biodata.height
|
||||
}}
|
||||
</div>
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Weight</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.weight"}}</span>
|
||||
{{formInput
|
||||
systemFields.biodata.fields.weight
|
||||
value=system.biodata.weight
|
||||
}}
|
||||
</div>
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Eyes</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.eyes"}}</span>
|
||||
{{formInput
|
||||
systemFields.biodata.fields.eyes
|
||||
value=system.biodata.eyes
|
||||
}}
|
||||
</div>
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Hair</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.hair"}}</span>
|
||||
{{formInput
|
||||
systemFields.biodata.fields.hair
|
||||
value=system.biodata.hair
|
||||
}}
|
||||
</div>
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Dev. Points (Total)</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.devPointsTotal"}}</span>
|
||||
{{formInput
|
||||
systemFields.developmentPoints.fields.total
|
||||
value=system.developmentPoints.total
|
||||
}}
|
||||
</div>
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Dev. Points (Rem.)</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.devPointsRem"}}</span>
|
||||
{{formInput
|
||||
systemFields.developmentPoints.fields.remaining
|
||||
value=system.developmentPoints.remaining
|
||||
}}
|
||||
</div>
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Magic User</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.magicUser"}}</span>
|
||||
{{formInput
|
||||
systemFields.biodata.fields.magicUser
|
||||
value=system.biodata.magicUser
|
||||
}}
|
||||
</div>
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Cleric User</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.clericUser"}}</span>
|
||||
{{formInput
|
||||
systemFields.biodata.fields.clericUser
|
||||
value=system.biodata.clericUser
|
||||
}}
|
||||
</div>
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Save bonus (1/5levels)</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.saveBonus"}}</span>
|
||||
{{formInput
|
||||
systemFields.modifiers.fields.saveModifier
|
||||
value=system.modifiers.saveModifier
|
||||
@@ -107,7 +107,7 @@
|
||||
|
||||
{{#if system.biodata.magicUser}}
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Spell bonus (1/5levels)</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.spellBonus"}}</span>
|
||||
{{formInput
|
||||
systemFields.modifiers.fields.levelSpellModifier
|
||||
value=system.modifiers.levelSpellModifier
|
||||
@@ -117,7 +117,7 @@
|
||||
{{/if}}
|
||||
{{#if system.biodata.clericUser}}
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Miracle bonus (1/5levels)</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.miracleBonus"}}</span>
|
||||
{{formInput
|
||||
systemFields.modifiers.fields.levelMiracleModifier
|
||||
value=system.modifiers.levelMiracleModifier
|
||||
@@ -127,7 +127,7 @@
|
||||
{{/if}}
|
||||
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Last HD roll</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.lastHdRoll"}}</span>
|
||||
{{formInput
|
||||
systemFields.biodata.fields.hpPerLevel
|
||||
value=system.biodata.hpPerLevel
|
||||
@@ -136,7 +136,7 @@
|
||||
</div>
|
||||
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Natural DR</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.naturalDR"}}</span>
|
||||
{{formInput
|
||||
systemFields.biodata.fields.naturalDR
|
||||
value=system.biodata.naturalDR
|
||||
@@ -145,7 +145,7 @@
|
||||
</div>
|
||||
|
||||
<div class="biodata-elem">
|
||||
<span class="name">Magical DR</span>
|
||||
<span class="name">{{localize "LETHALFANTASY.Label.magicalDR"}}</span>
|
||||
{{formInput
|
||||
systemFields.biodata.fields.magicDR
|
||||
value=system.biodata.magicDR
|
||||
|
||||
@@ -87,16 +87,10 @@
|
||||
<i class="fa-solid fa-shield-halved" data-roll-type="weapon-defense" data-roll-key="{{item.id}}"></i>
|
||||
</a>
|
||||
|
||||
<a class="rollable" data-roll-type="weapon-damage-small" data-roll-key="{{item.id}}"
|
||||
data-tooltip="Roll Damage (Small)">
|
||||
<i class="fa-regular fa-face-head-bandage" data-roll-type="weapon-damage-small"
|
||||
data-roll-key="{{item.id}}"></i>S
|
||||
</a>
|
||||
|
||||
<a class="rollable" data-roll-type="weapon-damage-medium" data-roll-key="{{item.id}}"
|
||||
data-tooltip="Roll Damage (Medium)">
|
||||
<i class="fa-regular fa-face-head-bandage" data-roll-type="weapon-damage-medium"
|
||||
data-roll-key="{{item.id}}"></i>M
|
||||
<a class="rollable" data-roll-type="weapon-damage" data-roll-key="{{item.id}}"
|
||||
data-tooltip="Roll Damage">
|
||||
<i class="fa-regular fa-face-head-bandage" data-roll-type="weapon-damage"
|
||||
data-roll-key="{{item.id}}"></i>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
<legend>{{localize "LETHALFANTASY.Label.divinityPoints"}}</legend>
|
||||
<div class="miracle-details">
|
||||
<div class="miracle-detail">
|
||||
<span>Current</span>
|
||||
<span>{{localize "LETHALFANTASY.Label.current"}}</span>
|
||||
{{formField systemFields.divinityPoints.fields.value value=system.divinityPoints.value localize=true}}
|
||||
<a data-action="divinityPointsPlus"><i class="fa-solid fa-hexagon-plus"></i></a>
|
||||
<a data-action="divinityPointsMinus"><i class="fa-solid fa-hexagon-minus"></i></a>
|
||||
|
||||
<span>Max</span>
|
||||
<span>{{localize "LETHALFANTASY.Label.max"}}</span>
|
||||
{{formField systemFields.divinityPoints.fields.max value=system.divinityPoints.max localize=true
|
||||
disabled=isPlayMode}}
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
<legend>{{localize "LETHALFANTASY.Label.aetherPoints"}}</legend>
|
||||
<div class="spell-details">
|
||||
<div class="spell-detail">
|
||||
<span>Current</span>
|
||||
<span>{{localize "LETHALFANTASY.Label.current"}}</span>
|
||||
{{formField systemFields.aetherPoints.fields.value value=system.aetherPoints.value localize=true}}
|
||||
<a data-action="aetherPointsPlus"><i class="fa-solid fa-hexagon-plus"></i></a>
|
||||
<a data-action="aetherPointsMinus"><i class="fa-solid fa-hexagon-minus"></i></a>
|
||||
|
||||
<span>Max</span>
|
||||
<span>{{localize "LETHALFANTASY.Label.max"}}</span>
|
||||
{{formField systemFields.aetherPoints.fields.max value=system.aetherPoints.max localize=true
|
||||
disabled=isPlayMode}}
|
||||
</div>
|
||||
@@ -37,6 +37,25 @@
|
||||
<i class="fa-duotone fa-solid fa-stars" data-roll-type="spell-power" data-roll-key="{{item.id}}"></i>
|
||||
</a>
|
||||
|
||||
{{#if item.system.damageDice}}
|
||||
<a data-action="rollSpellDamage" data-item-id="{{item.id}}" data-damage-tier="standard"
|
||||
data-tooltip="Spell Damage (Standard)">
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i>S
|
||||
</a>
|
||||
{{/if}}
|
||||
{{#if item.system.damageDiceOverpowered}}
|
||||
<a data-action="rollSpellDamage" data-item-id="{{item.id}}" data-damage-tier="overpowered"
|
||||
data-tooltip="Spell Damage (Overpowered)">
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i>O
|
||||
</a>
|
||||
{{/if}}
|
||||
{{#if item.system.damageDiceOverpowered2}}
|
||||
<a data-action="rollSpellDamage" data-item-id="{{item.id}}" data-damage-tier="overpowered2"
|
||||
data-tooltip="Spell Damage (Overpowered 2)">
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i>O2
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
<div class="controls">
|
||||
<a data-tooltip="{{localize 'LETHALFANTASY.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
|
||||
|
||||
+10
-29
@@ -40,45 +40,39 @@
|
||||
{{#if (eq rollData.favor "favor")}}
|
||||
<div class="detail-badge favor-badge">
|
||||
<i class="fa-solid fa-sparkles"></i>
|
||||
<span>Favor</span>
|
||||
<span>{{localize "LETHALFANTASY.Label.favor"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (eq rollData.favor "disfavor")}}
|
||||
<div class="detail-badge disfavor-badge">
|
||||
<i class="fa-solid fa-skull"></i>
|
||||
<span>Disfavor</span>
|
||||
<span>{{localize "LETHALFANTASY.Label.disfavor"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if rollData.letItFly}}
|
||||
<div class="detail-badge special-badge">
|
||||
<i class="fa-solid fa-bow-arrow"></i>
|
||||
<span>Let It Fly!</span>
|
||||
<span>{{localize "LETHALFANTASY.Label.letItFly"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if rollData.pointBlank}}
|
||||
<div class="detail-badge special-badge">
|
||||
<i class="fa-solid fa-bullseye-arrow"></i>
|
||||
<span>Point Blank</span>
|
||||
<span>{{localize "LETHALFANTASY.Label.pointBlank"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if rollData.beyondSkill}}
|
||||
<div class="detail-badge special-badge">
|
||||
<i class="fa-solid fa-target-lock"></i>
|
||||
<span>Beyond Skill</span>
|
||||
<span>{{localize "LETHALFANTASY.Label.beyondSkill"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if rollData.damageSmall}}
|
||||
<div class="detail-badge damage-badge">
|
||||
<i class="fa-solid fa-dice-d6"></i>
|
||||
<span>{{localize "LETHALFANTASY.Label.weapon-damage-small"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if rollData.damageMedium}}
|
||||
{{#if rollData.isDamage}}
|
||||
<div class="detail-badge damage-badge">
|
||||
<i class="fa-solid fa-dice-d20"></i>
|
||||
<span>{{localize "LETHALFANTASY.Label.weapon-damage-medium"}}</span>
|
||||
<span>{{localize "LETHALFANTASY.Label.weapon-damage"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@@ -111,7 +105,7 @@
|
||||
{{#unless isPrivate}}
|
||||
<div class="result-section">
|
||||
<div class="main-result">
|
||||
<div class="result-label">Total</div>
|
||||
<div class="result-label">{{localize "LETHALFANTASY.Label.total"}}</div>
|
||||
<div class="result-value {{#if (eq resultType 'success')}}success{{else}}failure{{/if}}">
|
||||
{{total}}
|
||||
</div>
|
||||
@@ -177,7 +171,7 @@
|
||||
{{else}}
|
||||
<div class="private-result">
|
||||
<i class="fa-solid fa-eye-slash"></i>
|
||||
<span>Private Roll</span>
|
||||
<span>{{localize "LETHALFANTASY.Label.privateRoll"}}</span>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
@@ -202,19 +196,6 @@
|
||||
}}+{{weaponDamageOptions.damageModifier}}{{/if}}
|
||||
</button>
|
||||
{{else}}
|
||||
{{#if weaponDamageOptions.damageS}}
|
||||
<button
|
||||
class="damage-roll-btn"
|
||||
data-weapon-id="{{weaponDamageOptions.weaponId}}"
|
||||
data-damage-type="small"
|
||||
data-damage-formula="{{weaponDamageOptions.damageS}}"
|
||||
data-is-monster="false"
|
||||
title="{{localize 'LETHALFANTASY.Label.rollDamage'}}"
|
||||
>
|
||||
<i class="fa-solid fa-dice-d6"></i>
|
||||
Damage Small
|
||||
</button>
|
||||
{{/if}}
|
||||
{{#if weaponDamageOptions.damageM}}
|
||||
<button
|
||||
class="damage-roll-btn"
|
||||
@@ -225,7 +206,7 @@
|
||||
title="{{localize 'LETHALFANTASY.Label.rollDamage'}}"
|
||||
>
|
||||
<i class="fa-solid fa-dice-d20"></i>
|
||||
Damage Medium
|
||||
{{localize "LETHALFANTASY.Label.damage"}}
|
||||
</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="dice-breakdown">
|
||||
{{#each diceResults}}
|
||||
<span class="dice-item">
|
||||
<span class="dice-type">{{this.dice}}</span>
|
||||
<span class="dice-separator">→</span>
|
||||
<span class="dice-value">{{this.value}}</span>
|
||||
</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if D30message}}
|
||||
<div class="d30-result">
|
||||
<span class="d30-value">D30 → {{D30result}}</span> — {{D30message.description}}
|
||||
</div>
|
||||
{{/if}}
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="attack-result {{#if isAttackWin}}attack-success{{else}}attack-failure{{/if}}">
|
||||
<h3><i class="fa-solid {{#if isAttackWin}}fa-sword{{else}}fa-shield{{/if}}"></i> Combat Result</h3>
|
||||
<div class="combat-comparison">
|
||||
<div class="combat-side attacker {{#if isAttackWin}}winner{{else}}loser{{/if}}">
|
||||
<div class="side-label">{{localize "LETHALFANTASY.Combat.attacker"}}</div>
|
||||
<div class="side-info">
|
||||
<div class="side-name">{{attackerName}}</div>
|
||||
<div class="side-roll">{{attackRoll}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="combat-vs">VS</div>
|
||||
<div class="combat-side defender {{#if isAttackWin}}loser{{else}}winner{{/if}}">
|
||||
<div class="side-label">{{localize "LETHALFANTASY.Combat.defender"}}</div>
|
||||
<div class="side-info">
|
||||
<div class="side-name">{{defenderName}}</div>
|
||||
<div class="side-roll">{{defenseRoll}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="combat-result-text">
|
||||
{{#if (equals outcome "shielded-hit")}}
|
||||
<i class="fa-solid fa-shield"></i> <strong>{{defenderName}}</strong> has blocked with shield — apply armor DR + shield DR <strong>{{shieldDamageReduction}}</strong>.
|
||||
{{else if isAttackWin}}
|
||||
<i class="fa-solid fa-circle-check"></i> <strong>{{attackerName}}</strong> hits <strong>{{defenderName}}</strong>!
|
||||
{{else}}
|
||||
<i class="fa-solid fa-shield-halved"></i> <strong>{{defenderName}}</strong> avoided the attack!
|
||||
{{/if}}
|
||||
</div>
|
||||
{{{damageButton}}}
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
{{#if (equals type "weapon")}}
|
||||
<div class="attack-result-damage single-btn">
|
||||
<button class="roll-damage-btn"
|
||||
data-attacker-id="{{attackerId}}"
|
||||
data-defender-id="{{defenderId}}"
|
||||
data-defender-token-id="{{defenderTokenId}}"
|
||||
data-extra-shield-dr="{{shieldDamageReduction}}"
|
||||
data-weapon-id="{{attackWeaponId}}"
|
||||
data-damage-type="medium"
|
||||
data-d30-bleed="{{d30Bleed}}"
|
||||
data-d30-damage-mult="{{d30DamageMultiplier}}"
|
||||
data-d30-dr-mult="{{d30DrMultiplier}}">
|
||||
<i class="fa-solid fa-dice-d20"></i> Damage
|
||||
</button>
|
||||
</div>
|
||||
{{else if (equals type "monster")}}
|
||||
<div class="attack-result-damage single-btn">
|
||||
<button class="roll-damage-btn"
|
||||
data-attacker-id="{{attackerId}}"
|
||||
data-defender-id="{{defenderId}}"
|
||||
data-defender-token-id="{{defenderTokenId}}"
|
||||
data-extra-shield-dr="{{shieldDamageReduction}}"
|
||||
data-attack-key="{{attackRollKey}}"
|
||||
data-damage-type="monster"
|
||||
data-d30-bleed="{{d30Bleed}}"
|
||||
data-d30-damage-mult="{{d30DamageMultiplier}}"
|
||||
data-d30-dr-mult="{{d30DrMultiplier}}">
|
||||
<i class="fa-solid fa-burst"></i> Damage
|
||||
</button>
|
||||
</div>
|
||||
{{else if (equals type "spell")}}
|
||||
<div class="attack-result-damage spell-damage">
|
||||
{{#each tiers}}
|
||||
<button class="roll-damage-btn"
|
||||
data-attacker-id="{{../attackerId}}"
|
||||
data-defender-id="{{../defenderId}}"
|
||||
data-defender-token-id="{{../defenderTokenId}}"
|
||||
data-damage-type="spell"
|
||||
data-damage-formula="{{this.formula}}"
|
||||
data-d30-bleed="{{../d30Bleed}}"
|
||||
data-d30-damage-mult="{{../d30DamageMultiplier}}"
|
||||
data-d30-dr-mult="{{../d30DrMultiplier}}">
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i> {{this.label}} ({{this.formula}})
|
||||
</button>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
@@ -0,0 +1 @@
|
||||
<section class="dice-rolls">{{{rollHTML}}}</section>
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="lf-free-roll-card">
|
||||
<div class="lf-frc-header">
|
||||
<i class="fa-solid fa-dice"></i>
|
||||
<span class="lf-frc-title-text">{{titleText}}</span>
|
||||
<span class="lf-frc-badge">{{badge}}</span>
|
||||
</div>
|
||||
<div class="lf-frc-dice">
|
||||
{{#each dieChips}}
|
||||
<div class="{{this.classes}}">
|
||||
<span class="lf-frc-die-type">{{this.label}}</span>
|
||||
<span class="lf-frc-die-sep">→</span>
|
||||
<span class="lf-frc-die-val">{{this.value}}{{#if this.exploded}}<i class="fa-solid fa-burst lf-dt-explode-icon"></i>{{/if}}</span>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="lf-frc-total-bar">
|
||||
<span class="lf-frc-total-label">{{totalLabel}}</span>
|
||||
<span class="lf-frc-total-value">{{total}}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
{{#if (equals type "aetherSpend")}}
|
||||
<p>🔮 <strong>{{actorName}}</strong> casts <em>{{spellName}}{{#if tierLabel}}{{tierLabel}}{{/if}}</em> — spends <strong>{{value}}</strong> Aether <span style="color:#888;">({{oldValue}} → {{newValue}})</span>.</p>
|
||||
{{else if (equals type "graceSpend")}}
|
||||
<p>✨ <strong>{{actorName}}</strong> invokes <em>{{spellName}}</em> — spends <strong>{{value}}</strong> Grace <span style="color:#888;">({{oldValue}} → {{newValue}})</span>.</p>
|
||||
{{else if (equals type "bleedingNotice")}}
|
||||
<p><strong>Bleeding:</strong> Wound of {{value}} HP for {{value}} seconds.</p>
|
||||
{{else if (equals type "d30BonusRoll")}}
|
||||
<p>D30 bonus: rolled <strong>{{formula}}</strong> = <strong>{{value}}</strong></p>
|
||||
{{else if (equals type "mulligan")}}
|
||||
<p><strong>{{actorName}}</strong> uses Mulligan and re-rolls {{side}}: <strong>{{oldRoll}}</strong> → <strong>{{newRoll}}</strong>.</p>
|
||||
{{> chat/dice-breakdown diceResults=diceResults D30result=D30result D30message=D30message}}
|
||||
<p>Both sides may now react to the new numbers.</p>
|
||||
{{else}}
|
||||
<p>
|
||||
<strong>{{actorName}}</strong>
|
||||
{{#if (equals type "grit")}}
|
||||
spends 1 {{resource}} and rolls <strong>{{value}}</strong> for {{side}}.
|
||||
{{else if (equals type "luck")}}
|
||||
spends 1 {{resource}} and rolls <strong>{{value}}</strong> for {{side}}.
|
||||
{{else if (equals type "bonusDie")}}
|
||||
adds <strong>{{formula}}</strong> and rolls <strong>{{value}}</strong> for {{side}}.
|
||||
{{else if (equals type "d30Bonus")}}
|
||||
gains <strong>+{{value}}</strong> from D30 bonus die for {{side}}.
|
||||
{{else if (equals type "d30Flag")}}
|
||||
D30 — <strong>{{specialName}}</strong> possible for {{actorName}}!
|
||||
{{else if (equals type "d30Bleed")}}
|
||||
D30 — <strong>Bleeding/Internal Injury</strong> on hit! Damage past DR will cause a bleeding wound.
|
||||
{{else if (equals type "d30DamageMultiplier")}}
|
||||
D30 — <strong>x{{value}} damage</strong> before damage reduction!
|
||||
{{else if (equals type "d30DRMultiplier")}}
|
||||
D30 — Defense grants <strong>x{{value}} DR</strong> (choose which DR types to multiply when damage is applied).
|
||||
{{else if (equals type "shieldBlock")}}
|
||||
rolls <strong>{{shieldLabel}}</strong> and adds <strong>{{shieldBonus}}</strong> to defense ({{newTotal}} ≥ {{opposingRoll}}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>{{shieldDR}}</strong> will apply to damage.
|
||||
{{else if (equals type "shieldFail")}}
|
||||
rolls <strong>{{shieldLabel}}</strong> and adds <strong>{{shieldBonus}}</strong> to defense ({{newTotal}} < {{opposingRoll}}). Shield did not block — normal hit, armor DR only.
|
||||
{{else if (equals type "generic")}}
|
||||
{{{body}}}
|
||||
{{/if}}
|
||||
</p>
|
||||
{{/if}}
|
||||
@@ -13,6 +13,14 @@
|
||||
<span>{{ localize "COMBAT.End" }}</span>
|
||||
</button>
|
||||
|
||||
{{#if combat.round}}
|
||||
<button type="button" class="combat-control combat-control-lg" data-action="rollMonsterProgression"
|
||||
data-tooltip="{{ localize 'LETHALFANTASY.Combat.RollMonsters' }}">
|
||||
<i class="fa-solid fa-dragon" inert></i>
|
||||
<span>{{ localize "LETHALFANTASY.Combat.RollMonsters" }}</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
<!-- <button type="button" class="inline-control combat-control icon fa-solid fa-arrow-right" data-action="nextTurn"
|
||||
data-tooltip aria-label="{{ localize "COMBAT.TurnNext" }}"></button> -->
|
||||
<button type="button" class="inline-control combat-control icon fa-solid fa-forward-step" data-action="nextRound"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="grit-luck-dialog">
|
||||
<div class="combat-status">
|
||||
<p><strong>{{defenderName}}</strong> uses a shield (not equipped)</p>
|
||||
<p>{{localize "LETHALFANTASY.Combat.attackRoll"}}: <strong>{{attackRoll}}</strong> — {{localize "LETHALFANTASY.Combat.currentDefense"}}: <strong>{{defenseRoll}}</strong></p>
|
||||
</div>
|
||||
<div class="weapon-selection" style="margin-top:8px;">
|
||||
<label for="shield-dice">{{localize "LETHALFANTASY.Combat.shieldDice"}}:</label>
|
||||
<select id="shield-dice" name="shieldDice" style="width: 100%; margin-top: 4px;">
|
||||
{{#each choices}}
|
||||
<option value="{{this}}">{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="weapon-selection" style="margin-top:8px;">
|
||||
<label for="shield-dr">{{localize "LETHALFANTASY.Combat.shieldDR"}}:</label>
|
||||
<input id="shield-dr" name="shieldDR" type="number" min="0" value="0" style="width: 100%; margin-top: 4px;" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="grit-luck-dialog">
|
||||
<div class="combat-status">
|
||||
<p><strong>{{attackerName}}</strong> currently has <strong>{{currentAttackRollWithBonus}}</strong></p>
|
||||
<p><strong>{{defenderName}}</strong> rolled <strong>{{defenseRoll}}</strong></p>
|
||||
{{#if totalBonus}}<p class="bonus-info">Bonus already added: +{{totalBonus}}</p>{{/if}}
|
||||
</div>
|
||||
<p class="offer-text">You are losing! Spend Grit to add 1D6 to your attack?</p>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="grit-luck-dialog">
|
||||
<div class="combat-status">
|
||||
<p><strong>{{attackerName}}</strong> currently has <strong>{{attackRoll}}</strong></p>
|
||||
<p><strong>{{defenderName}}</strong> rolled <strong>{{defenseRoll}}</strong></p>
|
||||
{{#if d30message}}<p class="bonus-info">D30 special: {{d30message.description}}</p>{{/if}}
|
||||
</div>
|
||||
<p class="offer-text">{{offerText}}</p>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="grit-luck-dialog">
|
||||
<div class="combat-status">
|
||||
<p><strong>{{actorName}}</strong> currently has <strong>{{currentRoll}}</strong></p>
|
||||
<p>{{sideLabel}} opposing roll: <strong>{{opposingRoll}}</strong></p>
|
||||
</div>
|
||||
<div class="weapon-selection">
|
||||
<label for="bonus-die">{{localize "LETHALFANTASY.Combat.chooseBonusDie"}}:</label>
|
||||
<select id="bonus-die" name="bonusDie" style="width: 100%; margin-top: 8px;">
|
||||
{{#each choices}}
|
||||
<option value="{{this}}">{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<p>{{message}}</p>
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="grit-luck-dialog">
|
||||
<p><strong>D30 DR Multiplier ×{{multiplier}}</strong></p>
|
||||
<p>Choose which DR types to multiply:</p>
|
||||
<label style="display:block;margin:0.3rem 0">
|
||||
<input type="checkbox" id="d30-dr-base" {{#if canBase}}checked{{/if}} {{#unless baseEnabled}}disabled{{/unless}}>
|
||||
Base DR (Armor/Natural): {{baseDR}} → ×{{multiplier}} = {{baseDRMultiplied}}
|
||||
</label>
|
||||
<label style="display:block;margin:0.3rem 0">
|
||||
<input type="checkbox" id="d30-dr-shield" {{#if canShield}}checked{{/if}} {{#unless shieldEnabled}}disabled{{/unless}}>
|
||||
Shield DR: {{shieldDR}} → ×{{multiplier}} = {{shieldDRMultiplied}}
|
||||
</label>
|
||||
<label style="display:block;margin:0.3rem 0">
|
||||
<input type="checkbox" id="d30-dr-magic" {{#if canMagic}}checked{{/if}} {{#unless magicEnabled}}disabled{{/unless}}>
|
||||
Magic DR: {{magicDR}} → ×{{multiplier}} = {{magicDRMultiplied}}
|
||||
</label>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="grit-luck-dialog">
|
||||
<p><strong>{{itemName}}</strong> has multiple damage tiers.</p>
|
||||
<p>Choose which damage to use when the attack lands:</p>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="grit-luck-dialog">
|
||||
<div class="combat-status">
|
||||
<p><strong>{{attackerName}}</strong> {{attackStatus}} <strong>{{attackRoll}}</strong></p>
|
||||
<p><strong>{{defenderName}}</strong> {{defenseStatus}} <strong>{{defenseRoll}}</strong></p>
|
||||
{{#if d30message}}<p class="bonus-info">D30 special: {{d30message.description}}</p>{{/if}}
|
||||
</div>
|
||||
<p class="offer-text">{{offerText}}</p>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="defense-request-dialog">
|
||||
<div class="attack-info">
|
||||
<p><strong>{{attackerName}}</strong> attacks <strong>{{defenderName}}</strong> with <strong>{{weaponName}}</strong>!</p>
|
||||
<p>{{localize "LETHALFANTASY.Combat.attackRoll"}}: <strong>{{attackRoll}}</strong></p>
|
||||
</div>
|
||||
<div class="weapon-selection">
|
||||
<label for="defense-weapon">{{localize "LETHALFANTASY.Combat.chooseWeapon"}}:</label>
|
||||
<select id="defense-weapon" name="weaponId" style="width: 100%; margin-top: 8px;">
|
||||
{{#each weapons}}
|
||||
<option value="{{this.id}}">{{this.name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="defense-request-dialog">
|
||||
<div class="attack-info">
|
||||
<p><strong>{{attackerName}}</strong> attacks <strong>{{defenderName}}</strong> with <strong>{{weaponName}}</strong>!</p>
|
||||
<p>{{localize "LETHALFANTASY.Combat.attackRoll"}}: <strong>{{attackRoll}}</strong></p>
|
||||
</div>
|
||||
<div class="weapon-selection">
|
||||
<label for="defense-attack">{{localize "LETHALFANTASY.Combat.chooseWeapon"}}:</label>
|
||||
<select id="defense-attack" name="attackKey" style="width: 100%; margin-top: 8px;">
|
||||
{{#each attacks}}
|
||||
<option value="{{this.key}}">{{this.name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="defense-request-dialog">
|
||||
<div class="attack-info">
|
||||
<p><strong>{{attackerName}}</strong> targets <strong>{{defenderName}}</strong> with <strong>{{weaponName}}</strong>!</p>
|
||||
<p>{{localize "LETHALFANTASY.Combat.attackRoll"}}: <strong>{{attackRoll}}</strong></p>
|
||||
</div>
|
||||
<div class="weapon-selection">
|
||||
<label for="save-type">{{localize "LETHALFANTASY.Combat.chooseSave"}}:</label>
|
||||
<select id="save-type" name="saveKey" style="width: 100%; margin-top: 8px;">
|
||||
{{#each saves}}
|
||||
<option value="{{this.id}}">{{this.label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<p>Select the power level for <strong>{{itemName}}</strong>:</p>
|
||||
@@ -0,0 +1,7 @@
|
||||
<div style="padding:0.5rem 0">
|
||||
<p style="margin-bottom:0.6rem">{{msg}}</p>
|
||||
<div style="display:flex;align-items:center;gap:0.5rem">
|
||||
<label style="font-weight:bold">{{label}}</label>
|
||||
<input type="number" name="manualDr" value="0" min="0" style="width:5rem"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<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>
|
||||
|
||||
<div class="luck-grit-wrap luck-grit-hud-disabled">
|
||||
<div class="luck-grit-row">
|
||||
<span class="luck-grit-label">Luck</span>
|
||||
<button class="luck-grit-btn" data-resource="luck" data-amount="-1">−1</button>
|
||||
<button class="luck-grit-btn" data-resource="luck" data-amount="1">+1</button>
|
||||
</div>
|
||||
<div class="luck-grit-row">
|
||||
<span class="luck-grit-label">Grit</span>
|
||||
<button class="luck-grit-btn" data-resource="grit" data-amount="-1">−1</button>
|
||||
<button class="luck-grit-btn" data-resource="grit" data-amount="1">+1</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7,7 +7,7 @@
|
||||
{{formField systemFields.miracleType value=system.miracleType}}
|
||||
{{formField systemFields.level value=system.level}}
|
||||
|
||||
<label>Components</label>
|
||||
<label>{{localize "LETHALFANTASY.Label.components"}}</label>
|
||||
<div class="shift-right">
|
||||
{{formField systemFields.components.fields.verbal value=system.components.verbal}}
|
||||
{{formField systemFields.components.fields.somatic value=system.components.somatic}}
|
||||
@@ -27,6 +27,9 @@
|
||||
{{formField systemFields.areaAffected value=system.areaAffected}}
|
||||
{{formField systemFields.duration value=system.duration}}
|
||||
{{formField systemFields.savingThrow value=system.savingThrow}}
|
||||
{{formField systemFields.damageDice value=system.damageDice}}
|
||||
{{formField systemFields.damageDiceOverpowered value=system.damageDiceOverpowered}}
|
||||
{{formField systemFields.damageDiceOverpowered2 value=system.damageDiceOverpowered2}}
|
||||
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user