feat: implement core Foundry VTT system framework, including actor/item data models, spellcasting logic, dice mechanics, and automation scripts
This commit is contained in:
+18
-18
@@ -1,4 +1,4 @@
|
|||||||
# Changelog — hbm-rpg-v3
|
# Changelog - hbm-rpg-v3
|
||||||
|
|
||||||
All notable changes to the **HbM RPG v3** Foundry VTT system.
|
All notable changes to the **HbM RPG v3** Foundry VTT system.
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|||||||
- **Foundry v14 compatibility:** Migrated from deprecated `template.json` to `documentTypes` in `system.json`. The system now declares Actor and Item types directly in the manifest as required by Foundry v14+. `template.json` has been removed.
|
- **Foundry v14 compatibility:** Migrated from deprecated `template.json` to `documentTypes` in `system.json`. The system now declares Actor and Item types directly in the manifest as required by Foundry v14+. `template.json` has been removed.
|
||||||
- Updated `compatibility.verified` to `"14"` to support Foundry VTT v14 build 363.
|
- Updated `compatibility.verified` to `"14"` to support Foundry VTT v14 build 363.
|
||||||
|
|
||||||
## [1.2.0] — 2026-04-30
|
## [1.2.0] - 2026-04-30
|
||||||
|
|
||||||
Major mechanics + infrastructure release. Adds optional-rules sub-packs,
|
Major mechanics + infrastructure release. Adds optional-rules sub-packs,
|
||||||
ActiveEffects integration, automated tests, full localization for the new
|
ActiveEffects integration, automated tests, full localization for the new
|
||||||
@@ -19,8 +19,8 @@ features, and a v1.2.0 world migration.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Talent sub-packs** for optional rules:
|
- **Talent sub-packs** for optional rules:
|
||||||
- `talents-blood` — 5 talents from *Arcanum Sanguinis*
|
- `talents-blood` - 5 talents from *Arcanum Sanguinis*
|
||||||
- `talents-abyss` — 4 talents from *Klątwa Otchłani*
|
- `talents-abyss` - 4 talents from *Klątwa Otchłani*
|
||||||
- Each parsed talent is stamped with `flags['hbm-rpg-v3'].sourceModule`
|
- Each parsed talent is stamped with `flags['hbm-rpg-v3'].sourceModule`
|
||||||
so future module-aware UIs can filter or annotate them. *Note:* Foundry
|
so future module-aware UIs can filter or annotate them. *Note:* Foundry
|
||||||
has no native gating of compendium content by external modules; the flag
|
has no native gating of compendium content by external modules; the flag
|
||||||
@@ -38,7 +38,7 @@ features, and a v1.2.0 world migration.
|
|||||||
attribute, NPCs default to `2d6 + 0`.
|
attribute, NPCs default to `2d6 + 0`.
|
||||||
- **Per-turn Zeal regeneration is configurable.** The combat-turn handler
|
- **Per-turn Zeal regeneration is configurable.** The combat-turn handler
|
||||||
now reads `flags['hbm-rpg-v3'].zealRegenBonus` (default `0`) added to the
|
now reads `flags['hbm-rpg-v3'].zealRegenBonus` (default `0`) added to the
|
||||||
base `+1` regen — talents can grant additional regen via an ActiveEffect
|
base `+1` regen - talents can grant additional regen via an ActiveEffect
|
||||||
with `mode: ADD` targeting that flag.
|
with `mode: ADD` targeting that flag.
|
||||||
- **Long-rest cleanup.** `rest(actor, 'long')` now also deletes temporary
|
- **Long-rest cleanup.** `rest(actor, 'long')` now also deletes temporary
|
||||||
ActiveEffects on the actor (those with finite duration or with the
|
ActiveEffects on the actor (those with finite duration or with the
|
||||||
@@ -51,16 +51,16 @@ features, and a v1.2.0 world migration.
|
|||||||
activeEffect}` namespaces to both `lang/pl.json` and `lang/en.json`.
|
activeEffect}` namespaces to both `lang/pl.json` and `lang/en.json`.
|
||||||
- **Vitest test harness** (`vitest.config.ts`, `tests/setup.ts`). 5 test
|
- **Vitest test harness** (`vitest.config.ts`, `tests/setup.ts`). 5 test
|
||||||
files covering damage layers, rest mechanics, zeal-regen helper, talent
|
files covering damage layers, rest mechanics, zeal-regen helper, talent
|
||||||
parser, and the v1.2.0 migration — 18 tests, all passing.
|
parser, and the v1.2.0 migration - 18 tests, all passing.
|
||||||
|
|
||||||
### Notes on skipped items
|
### Notes on skipped items
|
||||||
- **Spell preparation** is intentionally not implemented — every spellcaster
|
- **Spell preparation** is intentionally not implemented - every spellcaster
|
||||||
can cast any known spell directly (per design).
|
can cast any known spell directly (per design).
|
||||||
- **Module gating of sub-pack talents** is not enforced because Foundry does
|
- **Module gating of sub-pack talents** is not enforced because Foundry does
|
||||||
not provide a hook to hide compendium documents based on external module
|
not provide a hook to hide compendium documents based on external module
|
||||||
presence. The `sourceModule` flag is preserved for future opt-in filtering.
|
presence. The `sourceModule` flag is preserved for future opt-in filtering.
|
||||||
|
|
||||||
## [1.1.2] — 2026-04-30
|
## [1.1.2] - 2026-04-30
|
||||||
|
|
||||||
Mechanics polish release. No content changes; focuses on the spell-cast UX, the
|
Mechanics polish release. No content changes; focuses on the spell-cast UX, the
|
||||||
NPC sheet, and the cast dialog.
|
NPC sheet, and the cast dialog.
|
||||||
@@ -87,14 +87,14 @@ NPC sheet, and the cast dialog.
|
|||||||
free-text actor-id field), so group casters can be picked visually.
|
free-text actor-id field), so group casters can be picked visually.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- `src/logic/spell-cast.ts` no longer posts separate damage / status messages —
|
- `src/logic/spell-cast.ts` no longer posts separate damage / status messages -
|
||||||
everything is consolidated into the new chat card via `renderTemplate`.
|
everything is consolidated into the new chat card via `renderTemplate`.
|
||||||
- `lang/{pl,en}.json` — added `HBM.spellCast.*` keys for the new card and
|
- `lang/{pl,en}.json` - added `HBM.spellCast.*` keys for the new card and
|
||||||
dialog widgets.
|
dialog widgets.
|
||||||
- `styles/hbm.css` — styling for the spell-cast card and the new dialog
|
- `styles/hbm.css` - styling for the spell-cast card and the new dialog
|
||||||
widgets (caster checkbox list, variant radio list).
|
widgets (caster checkbox list, variant radio list).
|
||||||
|
|
||||||
## [1.0.0] — 2026-04-29
|
## [1.0.0] - 2026-04-29
|
||||||
|
|
||||||
First production release. Adds the full magic and resource model from books I–VIII
|
First production release. Adds the full magic and resource model from books I–VIII
|
||||||
plus a build pipeline that generates compendium content from the source markdown
|
plus a build pipeline that generates compendium content from the source markdown
|
||||||
@@ -122,7 +122,7 @@ in `_books/`.
|
|||||||
- **Cast dialog** with mana/zeal/blood inputs, group-caster picker, GM bypass
|
- **Cast dialog** with mana/zeal/blood inputs, group-caster picker, GM bypass
|
||||||
checkboxes, validation feedback inline.
|
checkboxes, validation feedback inline.
|
||||||
- **New mechanics modules**: `blood-magic`, `abyss-magic`, `brewing`, `rest`,
|
- **New mechanics modules**: `blood-magic`, `abyss-magic`, `brewing`, `rest`,
|
||||||
`trade` — all exposed under `game.hbm.*`.
|
`trade` - all exposed under `game.hbm.*`.
|
||||||
- **Actor schema**: `attributes.blood`, `attributes.elixirTolerance`,
|
- **Actor schema**: `attributes.blood`, `attributes.elixirTolerance`,
|
||||||
`attributes.insanity` with derived clamps and resource cards on the sheet.
|
`attributes.insanity` with derived clamps and resource cards on the sheet.
|
||||||
- **Mental conditions**: paranoia, fobia, depresja, mania, schizofrenia
|
- **Mental conditions**: paranoia, fobia, depresja, mania, schizofrenia
|
||||||
@@ -144,19 +144,19 @@ in `_books/`.
|
|||||||
|
|
||||||
### Deferred
|
### Deferred
|
||||||
|
|
||||||
- `gear-*`, `actors-*`, `roll-tables-*` packs — source books lack stat-block data.
|
- `gear-*`, `actors-*`, `roll-tables-*` packs - source books lack stat-block data.
|
||||||
- Adventures package (`hbm-rpg-v3-adventures`).
|
- Adventures package (`hbm-rpg-v3-adventures`).
|
||||||
- Macro packs.
|
- Macro packs.
|
||||||
- Compendium browser premium-module integration.
|
- Compendium browser premium-module integration.
|
||||||
|
|
||||||
## [0.6.1] — 2026-04-29
|
## [0.6.1] - 2026-04-29
|
||||||
|
|
||||||
- Localization parity (`en.json` mirrors all `pl.json` namespaces).
|
- Localization parity (`en.json` mirrors all `pl.json` namespaces).
|
||||||
- CSS polish: badges, source pip, blood-pool tint, school details groups,
|
- CSS polish: badges, source pip, blood-pool tint, school details groups,
|
||||||
fieldset and array-row layouts.
|
fieldset and array-row layouts.
|
||||||
- Compendium browser metadata + `packFolders`.
|
- Compendium browser metadata + `packFolders`.
|
||||||
|
|
||||||
## [0.6.0] — 2026-04-29
|
## [0.6.0] - 2026-04-29
|
||||||
|
|
||||||
- Spell item-sheet rewrite (8 fieldsets, conditional rendering).
|
- Spell item-sheet rewrite (8 fieldsets, conditional rendering).
|
||||||
- Cast dialog (`askCastOptions`) with mana/zeal/blood/Hekate/group/bypass.
|
- Cast dialog (`askCastOptions`) with mana/zeal/blood/Hekate/group/bypass.
|
||||||
@@ -164,12 +164,12 @@ in `_books/`.
|
|||||||
- Resources: blood pool, elixir tolerance, insanity (conditional).
|
- Resources: blood pool, elixir tolerance, insanity (conditional).
|
||||||
- Spells tab grouped by school with badges.
|
- Spells tab grouped by school with badges.
|
||||||
|
|
||||||
## [0.5.0] — 2026-04-29
|
## [0.5.0] - 2026-04-29
|
||||||
|
|
||||||
- New mechanics modules under `game.hbm.*`: blood-magic, abyss-magic, brewing,
|
- New mechanics modules under `game.hbm.*`: blood-magic, abyss-magic, brewing,
|
||||||
rest, trade. Actor schema gains `elixirTolerance`, `insanity`.
|
rest, trade. Actor schema gains `elixirTolerance`, `insanity`.
|
||||||
|
|
||||||
## [0.4.0] — 2026-04-29
|
## [0.4.0] - 2026-04-29
|
||||||
|
|
||||||
- Phase 0 foundation: extended `constants`, `SpellData`, `CharacterData.attributes.blood`,
|
- Phase 0 foundation: extended `constants`, `SpellData`, `CharacterData.attributes.blood`,
|
||||||
migration runner, mental conditions.
|
migration runner, mental conditions.
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
# Foundry VTT System: Homebrew Magic RPG v3
|
# Foundry VTT System: Homebrew Magic RPG v3
|
||||||
|
|
||||||
A custom **Foundry Virtual Tabletop v13** system implementing *Homebrew Magic: Role Playing Game v3* — a Polish tabletop RPG with a **d6 success-pool (TS — Trudność:Sukcesy)** dice mechanic.
|
A custom **Foundry Virtual Tabletop v13** system implementing *Homebrew Magic: Role Playing Game v3* - a Polish tabletop RPG with a **d6 success-pool (TS - Trudność:Sukcesy)** dice mechanic.
|
||||||
|
|
||||||
> **Internal identifiers are English; user-facing labels are Polish (canonical) with English fallback.** The locale files in [`lang/`](./lang/) hold the translations.
|
> **Internal identifiers are English; user-facing labels are Polish (canonical) with English fallback.** The locale files in [`lang/`](./lang/) hold the translations.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**v1.0.0 — first production release.** Full magic and resource model from books I–VIII, 152 compendium docs across 11 packs, automated build pipeline that parses `_books/*.md` directly. See [`CHANGELOG.md`](./CHANGELOG.md) for the full history.
|
**v1.0.0 - first production release.** Full magic and resource model from books I–VIII, 152 compendium docs across 11 packs, automated build pipeline that parses `_books/*.md` directly. See [`CHANGELOG.md`](./CHANGELOG.md) for the full history.
|
||||||
|
|
||||||
A companion lore module ([`foundry-lore`](https://github.com/iwo-strzebonski/hbm-foundry-lore)) ships JournalEntry compendia for narrative chapters.
|
A companion lore module ([`foundry-lore`](https://github.com/iwo-strzebonski/hbm-foundry-lore)) ships JournalEntry compendia for narrative chapters.
|
||||||
|
|
||||||
## What's implemented
|
## What's implemented
|
||||||
|
|
||||||
- **Document types** — Actor: `character`, `npc`. Item: `spell`, `gear`, `ability`, `class`, `race`, `discipline`, `talent`.
|
- **Document types** - Actor: `character`, `npc`. Item: `spell`, `gear`, `ability`, `class`, `race`, `discipline`, `talent`.
|
||||||
- **TS d6 dice pool roller** (`HbmTSRoll`) — `1` auto-fail, `6` auto-success, configurable Threshold (T) and required Successes (Y), plus T-step / Y-step modifiers driven by Active Effects.
|
- **TS d6 dice pool roller** (`HbmTSRoll`) - `1` auto-fail, `6` auto-success, configurable Threshold (T) and required Successes (Y), plus T-step / Y-step modifiers driven by Active Effects.
|
||||||
- **Derived stats** — `health.max = 3 × (body + mind + soul)`, `zeal.max = ceil(soul/2)`, initiative pool, mana table lookup (Magic Power Level 0–X), blood pool (`body + soul`), elixir tolerance cap (`body + 1`), insanity counter.
|
- **Derived stats** - `health.max = 3 × (body + mind + soul)`, `zeal.max = ceil(soul/2)`, initiative pool, mana table lookup (Magic Power Level 0–X), blood pool (`body + soul`), elixir tolerance cap (`body + 1`), insanity counter.
|
||||||
- **4-layer damage model** — Magical Armor (with runic counter) → Magical Shield → Physical Armor (DR + condition) → Health. `Apply Damage` dialog with bypass flags; auto-applies `Oszołomiony` on heavy HP loss.
|
- **4-layer damage model** - Magical Armor (with runic counter) → Magical Shield → Physical Armor (DR + condition) → Health. `Apply Damage` dialog with bypass flags; auto-applies `Oszołomiony` on heavy HP loss.
|
||||||
- **Combat hooks** — Mana resets each round; Zeal +1 each turn; turns auto-skipped for Nieprzytomny / Obezwładniony; Podpalony ticks 1 environmental damage; Umierający prompts a Death Save.
|
- **Combat hooks** - Mana resets each round; Zeal +1 each turn; turns auto-skipped for Nieprzytomny / Obezwładniony; Podpalony ticks 1 environmental damage; Umierający prompts a Death Save.
|
||||||
- **Spell-cast workflow** — `validateCast()` gates on resources / race / talent / discipline / deity / group / inCombat / superspell / witch-symbol; dispatcher routes to standard / Sacred / Witch / Blood / Hekate hybrid; `rollSpellDamage()` parses formula tokens; reactive triggers (`killWithWeapon`, `targetCastsSpell`, `damageTaken`, `turnStart`).
|
- **Spell-cast workflow** - `validateCast()` gates on resources / race / talent / discipline / deity / group / inCombat / superspell / witch-symbol; dispatcher routes to standard / Sacred / Witch / Blood / Hekate hybrid; `rollSpellDamage()` parses formula tokens; reactive triggers (`killWithWeapon`, `targetCastsSpell`, `damageTaken`, `turnStart`).
|
||||||
- **Cast dialog** — mana / zeal / blood inputs, group-caster picker, Hekate toggle, GM bypass checkboxes (superspell, non-combat block), inline validation feedback.
|
- **Cast dialog** - mana / zeal / blood inputs, group-caster picker, Hekate toggle, GM bypass checkboxes (superspell, non-combat block), inline validation feedback.
|
||||||
- **Mechanics modules** (`game.hbm.*`) — `bloodMagic` (Self-Harm, Life Steal, talent integration), `abyssMagic` (Aspects vs Primal, Mistrz Losu penalty, `Dary Otchłani`, insanity gain), `brewing` (TS test + tolerance counter), `rest` (short / long), `trade` (founding company workflow, transactions, smuggling).
|
- **Mechanics modules** (`game.hbm.*`) - `bloodMagic` (Self-Harm, Life Steal, talent integration), `abyssMagic` (Aspects vs Primal, Mistrz Losu penalty, `Dary Otchłani`, insanity gain), `brewing` (TS test + tolerance counter), `rest` (short / long), `trade` (founding company workflow, transactions, smuggling).
|
||||||
- **Status conditions (Active Effects)** — 15 physical conditions (Aneks A) plus 5 mental conditions from Klątwa Otchłani VIII (paranoja, fobia, depresja, mania, schizofrenia).
|
- **Status conditions (Active Effects)** - 15 physical conditions (Aneks A) plus 5 mental conditions from Klątwa Otchłani VIII (paranoja, fobia, depresja, mania, schizofrenia).
|
||||||
- **Sheets** — ApplicationV2 + Handlebars for every document type. Character sheet: short-rest / long-rest header buttons, conditional resource cards (blood pool, elixir tolerance, insanity), spells tab grouped by school with badges (`★ super`, `⛧ group`, `☮ noncombat`, `🔒 race-lock`) and source-book pip.
|
- **Sheets** - ApplicationV2 + Handlebars for every document type. Character sheet: short-rest / long-rest header buttons, conditional resource cards (blood pool, elixir tolerance, insanity), spells tab grouped by school with badges (`★ super`, `⛧ group`, `☮ noncombat`, `🔒 race-lock`) and source-book pip.
|
||||||
- **Compendia** (11 packs, 152 docs) — `spells-general`, `spells-elements`, `spells-sacred`, `spells-academic`, `spells-superspells`, `spells-crimson` (93 spells); `talents`, `talents-npc` (34 talents); `races` (5), `disciplines`, `disciplines-forbidden` (20). Each pack carries `flags.hbm-rpg-v3.{category, sourceBook}` for compendium-browser; `packFolders` group them in the sidebar.
|
- **Compendia** (11 packs, 152 docs) - `spells-general`, `spells-elements`, `spells-sacred`, `spells-academic`, `spells-superspells`, `spells-crimson` (93 spells); `talents`, `talents-npc` (34 talents); `races` (5), `disciplines`, `disciplines-forbidden` (20). Each pack carries `flags.hbm-rpg-v3.{category, sourceBook}` for compendium-browser; `packFolders` group them in the sidebar.
|
||||||
- **Race / Class linking** — drag a Race or Class item onto a Character to auto-link `system.details.raceId` / `system.details.classIds[year]`. Feeds the Advancement panel (attribute / skill points available).
|
- **Race / Class linking** - drag a Race or Class item onto a Character to auto-link `system.details.raceId` / `system.details.classIds[year]`. Feeds the Advancement panel (attribute / skill points available).
|
||||||
- **Migration runner** — `src/migrations/` upgrades v0.2.x worlds (`complexityLevel` → `components.symbols.length`, `targets` regex → `areaOfEffect`, `overcasting` text → `overcastOptions[0].description`).
|
- **Migration runner** - `src/migrations/` upgrades v0.2.x worlds (`complexityLevel` → `components.symbols.length`, `targets` regex → `areaOfEffect`, `overcasting` text → `overcastOptions[0].description`).
|
||||||
|
|
||||||
## Project layout
|
## Project layout
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ Default `T = 4`, `Y = 1`. The roll dialog accepts both per-roll for harder tasks
|
|||||||
## Out of scope (v1.0.0)
|
## Out of scope (v1.0.0)
|
||||||
|
|
||||||
Deferred to follow-up versions:
|
Deferred to follow-up versions:
|
||||||
- Gear / artifact / actor / roll-table compendia — source books lack stat-block data.
|
- Gear / artifact / actor / roll-table compendia - source books lack stat-block data.
|
||||||
- Adventures package (`hbm-rpg-v3-adventures`).
|
- Adventures package (`hbm-rpg-v3-adventures`).
|
||||||
- Character creation wizard, advancement automation (PD spending).
|
- Character creation wizard, advancement automation (PD spending).
|
||||||
- Macro packs.
|
- Macro packs.
|
||||||
@@ -117,9 +117,9 @@ Deferred to follow-up versions:
|
|||||||
|
|
||||||
Game rules and content live in sibling folders of this repo:
|
Game rules and content live in sibling folders of this repo:
|
||||||
|
|
||||||
- [`../../_books/`](../../_books/) — synced PDFs of the rulebooks (read-only)
|
- [`../../_books/`](../../_books/) - synced PDFs of the rulebooks (read-only)
|
||||||
- [`../../spells/`](../../spells/), [`../../creatures/`](../../creatures/), [`../../items/`](../../items/) — structured content
|
- [`../../spells/`](../../spells/), [`../../creatures/`](../../creatures/), [`../../items/`](../../items/) - structured content
|
||||||
- [`../../rules/`](../../rules/), [`../../revisions/`](../../revisions/) — current and proposed mechanics
|
- [`../../rules/`](../../rules/), [`../../revisions/`](../../revisions/) - current and proposed mechanics
|
||||||
|
|
||||||
## Useful links
|
## Useful links
|
||||||
|
|
||||||
@@ -128,4 +128,4 @@ Game rules and content live in sibling folders of this repo:
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
All Rights Reserved — see [LICENSE.txt](./LICENSE.txt).
|
All Rights Reserved - see [LICENSE.txt](./LICENSE.txt).
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"label": "Forbidden Disciplines",
|
"label": "Forbidden Disciplines",
|
||||||
"entries": {
|
"entries": {
|
||||||
"Magia Otchłani — Magia Aspektów": {
|
"Magia Otchłani - Magia Aspektów": {
|
||||||
"name": "AbyssAspects"
|
"name": "AbyssAspects"
|
||||||
},
|
},
|
||||||
"Magia Otchłani — Pierwotna Magia": {
|
"Magia Otchłani - Pierwotna Magia": {
|
||||||
"name": "AbyssPrimal"
|
"name": "AbyssPrimal"
|
||||||
},
|
},
|
||||||
"Magia Krwi": {
|
"Magia Krwi": {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
"label": "Magic Disciplines",
|
"label": "Magic Disciplines",
|
||||||
"entries": {
|
"entries": {
|
||||||
"Alchemia - Warzenie Eliksirów": {
|
"Alchemia - Warzenie Eliksirów": {
|
||||||
"name": "Alchemy - Brewing of Elixirs"
|
"name": "Alchemy - Brewing of Elixirs",
|
||||||
|
"description": "[[Alchemia]]"
|
||||||
},
|
},
|
||||||
"Alchemia - Transmutacja": {
|
"Alchemia - Transmutacja": {
|
||||||
"name": "Alchemy - Transmutation"
|
"name": "Alchemy - Transmutation",
|
||||||
|
"description": "[[Alchemia]]"
|
||||||
},
|
},
|
||||||
"Rzemiosło Artefaktów": {
|
"Rzemiosło Artefaktów": {
|
||||||
"name": "Crafting of Artifacts",
|
"name": "Crafting of Artifacts",
|
||||||
@@ -48,7 +50,7 @@
|
|||||||
"Magia Sakralna": {
|
"Magia Sakralna": {
|
||||||
"name": "Sacred Magic"
|
"name": "Sacred Magic"
|
||||||
},
|
},
|
||||||
"Magia Sakralna — Egzorcyzmy": {
|
"Magia Sakralna - Egzorcyzmy": {
|
||||||
"name": "Sacred Magic - Exorcisms",
|
"name": "Sacred Magic - Exorcisms",
|
||||||
"description": "[[Magic Sakralna]]"
|
"description": "[[Magic Sakralna]]"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
},
|
},
|
||||||
"Krótki Odpoczynek": {
|
"Krótki Odpoczynek": {
|
||||||
"name": "Short Rest"
|
"name": "Short Rest"
|
||||||
|
},
|
||||||
|
"Chwila Wytchnienia": {
|
||||||
|
"name": "Take a Breather"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
{
|
|
||||||
"Celność": {
|
|
||||||
"name": "Accuracy",
|
|
||||||
"description": "Mag wykorzystuje swój talent magiczny, aby ułatwić sobie celowanie oraz żeby nieznacznie korygować swoje ataki dystansowe.\n\nAtaki dystansowe maga są o jeden stopień prostsze, a ponadto otrzymuje dwie dodatkowe kości do testów związanych z Umiejętnością Umiejętności Strzeleckie."
|
|
||||||
},
|
|
||||||
"Powietrzna Fala": {
|
|
||||||
"name": "Air Wave",
|
|
||||||
"description": "Mag wyrzuca z siebie potężną falę wiatru.\n\nKażda osoba w obszarze działania zaklęcia musi wykonać udany Test na Body (Siła) 4:S, gdzie liczba wymaganych sukcesów (S) jest równa liczbie wyrzuconych przez maga sukcesów. W wypadku niepowodzenia cel zostaje Powalony. Niezależnie od wyniku testu każda osoba w stożku otrzymuje tyle punktów obrażeń, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne maga."
|
|
||||||
},
|
|
||||||
"Gorejąca Skóra": {
|
|
||||||
"name": "Burning Skin",
|
|
||||||
"description": "Skóra i ubrania maga zaczynają się palić. Ogień ten nie niszczy ubrań maga ani nie zadaje mu obrażeń.\n\nWszyscy przeciwnicy, którzy zaatakują maga wręcz w trakcie działania tego zaklęcia otrzymują tyle punktów obrażeń, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne maga.\n\n---"
|
|
||||||
},
|
|
||||||
"Wyczucie Zagrożenia": {
|
|
||||||
"name": "Danger Sense",
|
|
||||||
"description": "Mag zaczyna skupiać się na delikatnych zmianach w otaczającym go powietrzu, co pomaga mu w unikaniu ataków.\n\nWszystkie ataki, których celem jest mag rzucający to zaklęcie, są trudniejsze o dwa stopnie (do maksymalnie Trudności (T) równej „6”), a ponadto mag otrzymuje dwie dodatkowe kości do rzutów związanych z Umiejętnością Refleks."
|
|
||||||
},
|
|
||||||
"Trzęsienie Ziemi": {
|
|
||||||
"name": "Earthquake",
|
|
||||||
"description": "W wybranym przez maga obszarze ziemia zaczyna się silnie trząść.\n\nWszystkie istoty w obszarze działania muszą wykonać udany Test na Body (Atletyka) 4:S, gdzie liczba wymaganych sukcesów (S) jest równa liczbie wyrzuconych przez maga sukcesów. W wypadku niepowodzenia postać zostaje Unieruchomiona i Powalona do początku swojej następnej tury. Efekt ten aktywuje się na końcu każdej tury maga i kończy się na początku trzeciej tury maga od rzucenia zaklęcia. Ponadto, jeśli postać nie zda tego testu trzykrotnie w trakcie działania tego zaklęcia, postać staje się Nieprzytomna do końca trwania zaklęcia."
|
|
||||||
},
|
|
||||||
"Ognista Powłoka": {
|
|
||||||
"name": "Fire Cloak",
|
|
||||||
"description": "Broń pokrywa się płomieniami, obniżając Trudność ataków wykonywanych tą bronią o 1 stopień oraz powodując, że broń dodatkowo zadaje 2 punkty obrażeń magicznych."
|
|
||||||
},
|
|
||||||
"Kula Ognia": {
|
|
||||||
"name": "Fireball",
|
|
||||||
"description": "Mag splata ogień w kulę, którą następnie ciska w wybrany cel.\n\nCel otrzymuje tyle punktów obrażeń, ile mag ma punktów w umiejętności Zdolności Magiczne, a następnie musi wykonać udany Test na Body (Refleks) 4:S, gdzie liczba wymaganych sukcesów (S) jest równa liczbie wyrzuconych przez maga sukcesów. W wypadku niepowodzenia Postać zostaje Podpalona na trzy rundy."
|
|
||||||
},
|
|
||||||
"Latanie": {
|
|
||||||
"name": "Flight",
|
|
||||||
"description": "Postać może latać przez 1 minutę."
|
|
||||||
},
|
|
||||||
"Żwirowa Zbroja": {
|
|
||||||
"name": "Gravel Armor",
|
|
||||||
"description": "Mag zamyka się w twardej, kamiennej zbroi.\n\nMag otrzymuje +1 punkt Pancerza, ale ze względu na to, że zbroja utrudnia ruchy, jego Prędkość zostaje zmniejszona o połowę."
|
|
||||||
},
|
|
||||||
"Gradobicie": {
|
|
||||||
"name": "Hailstorm",
|
|
||||||
"description": "Mag przywołuje nad dany obszar chmurę, z której zaczyna padać grad.\n\nKażda osoba w obszarze działania zaklęcia musi wykonać udany Test na Body (Refleks) 4:S, gdzie liczba wymaganych sukcesów (S) jest równa liczbie wyrzuconych przez maga sukcesów. W wypadku niepowodzenia postać otrzymuje tyle punktów obrażeń, ile wynosi wartość Umiejętności Zdolności Magiczne maga.\n\nDodatkowo obszar działania zaklęcia jest traktowany jak Trudny Teren (wszystkie istoty poruszają się w nim o połowę wolniej)."
|
|
||||||
},
|
|
||||||
"Lodowa Podłoga": {
|
|
||||||
"name": "Ice Floor",
|
|
||||||
"description": "Na obszarze o promieniu 2 m (mechanicznie reprezentowanym przez kwadrat o boku 5 m) pojawia się lód. Osoby w polu muszą wykonać udany Test na Body (Atletyka) 4:S, gdzie liczba wymaganych sukcesów (S) jest równa liczbie wyrzuconych przez maga sukcesów. W wypadku niepowodzenia postać zostaje Powalona."
|
|
||||||
},
|
|
||||||
"Podpalenie": {
|
|
||||||
"name": "Ignition",
|
|
||||||
"description": "Na obszarze o promieniu 2 m (mechanicznie reprezentowanym przez kwadrat o boku 5 m) pojawia się ogień. Każda osoba znajdująca się w tym obszarze otrzymuje 3 punkty obrażeń, a ponadto na początku swojej tury każda postać otrzymuje kolejne 3 punkty obrażeń, tak długo, jak będą się znajdować w obszarze tego zaklęcia."
|
|
||||||
},
|
|
||||||
"Wzniecanie Ognia": {
|
|
||||||
"name": "Kindle",
|
|
||||||
"description": "Postać podpala dotkniętą przez siebie rzecz dotykaną przez siebie."
|
|
||||||
},
|
|
||||||
"Mistyczna Mgła": {
|
|
||||||
"name": "Mystic Fog",
|
|
||||||
"description": "Mag przywołuje bardzo gęstą magiczną mgłę dookoła siebie.\n\nWszyscy przeciwnicy są Oślepieni, zaś wszyscy sojusznicy (w tym Mag) są traktowani jakby byli w pełni zasłonięci (przeciwnicy nie mogą atakować ich atakami dystansowymi ani wpływać na nich zaklęciami, które wymagają wybrania określonego celu, chyba że takie zaklęcie jest zaklęciem dotykowym).\n\nWszyscy sojusznicy (w tym mag) bez problemu widzą przez mgłę oraz widzą jej granicę. Mgła podąża za magiem.\n\n---"
|
|
||||||
},
|
|
||||||
"Ruchome Piaski": {
|
|
||||||
"name": "Quicksand",
|
|
||||||
"description": "Mag tymczasowo zmienia podłoże w ruchome piaski, utrudniając poruszanie się w obszarze działania zaklęcia.\n\nWszystkie istoty w tym obszarze mogą się poruszyć maksymalnie o zaokrągloną w górę połowę swojego Ruchu (czyli jeśli dana istota mogła się normalnie ruszać 10 m, tak w obszarze działania zaklęcia może się poruszyć o zaledwie 5 m)."
|
|
||||||
},
|
|
||||||
"Ukojenie": {
|
|
||||||
"name": "Solace",
|
|
||||||
"description": "Dookoła maga pojawia się kojąca rany delikatna mgiełka.\n\nWszyscy sojusznicy (w tym mag) w promieniu działania zaklęcia na początku następnej rundy maga odzyskują tyle Punktów Żywotności, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne maga."
|
|
||||||
},
|
|
||||||
"Skalista Tarcza": {
|
|
||||||
"name": "Stone Shield",
|
|
||||||
"description": "Postać tworzy w pobliżu siebie w wolnej przestrzeni ścianę z ziemi, piasku bądź żwiru.\n\nŚciana ta wytrzymuje ilość obrażeń równą wartości Umiejętności Zdolności Magiczne maga, a ponadto blokuje wizję oraz możliwość przejścia przez dany obszar istotom rozmiaru Średniego lub większego."
|
|
||||||
},
|
|
||||||
"Skalista Pułapka": {
|
|
||||||
"name": "Stone Trap",
|
|
||||||
"description": "Mag zaklina ziemię, piasek i żwir w danym obszarze, aby stworzyć pułapkę, która przebije kolcami pierwszą osobę, która wejdzie w dany obszar.\n\nKolce zadają tyle punktów obrażeń, ile mag ma punktów w Umiejętności Zdolności Magiczne, a następnie zaklęcie przestaje działać.\n\n---"
|
|
||||||
},
|
|
||||||
"Tworzenie i Kontrolowanie Wody": {
|
|
||||||
"name": "Water Create",
|
|
||||||
"description": "Postać tworzy i/lub zaczyna kontrolować do 1 litra wody. Gdy kontrolowana woda opuści zasięg 5 m, mag przestaje ją kontrolować. Zaklęcie to pozwala również na usunięcie lub wyparowanie do 1 litra wody."
|
|
||||||
},
|
|
||||||
"Szybki jak Wiatr": {
|
|
||||||
"name": "Wind Swift",
|
|
||||||
"description": "Mag splata wiatr, aby wspomóc się przy poruszaniu się.\n\nPrędkość maga zwiększa się dwukrotnie, a ponadto mag nie może zostać Powalony.\n\n---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
},
|
},
|
||||||
"Requiem": {
|
"Requiem": {
|
||||||
"name": "Requiem",
|
"name": "Requiem",
|
||||||
"description": "W dawnych kronikach Requiem symbolizowało narzędzie “ostatecznej separacji” - poprzez halabardę otwierano szczeliny w rzeczywistości, by elita Kultu mogła błyskawicznie przedrzeć się przez obronę, zasiewając zamęt i rozbicie wśród wrogów. Nikogo nie zabiera, nie drenuje życia, lecz wnosi do świata esencję Szkarłatu: rozprężenie struktur, dezorientację i przewagę dla wtajemniczonych.\n\nW ręce maga pojawia się utkana ze szkarłatnej energii halabarda, którą następnie może rzucić w wybrany przez siebie cel.\n\nGdy halabarda wbija się w wyznaczone miejsce, natychmiast zadaje wszystkim istotom w promieniu 10 metrów 3k6 obrażeń ignorujących Pancerz oraz wszystkie odporności, następnie mag (oraz do 2 sprzymierzeńców w promieniu 2 m) natychmiast teleportuje się do miejsca wbicia halabardy.\n\nPo wbiciu się ostrza, wokół (przez 1 godzinę na promieniu 50 m) gęstnieje szkarłatna aura. Każda istota (niebędąca członkiem Kultu) wchodząca lub przebywająca w strefie na początku swojej tury otrzymuje 2k6 obrażeń ignorujących Pancerz oraz wszystkie odporności oraz wykonuje Test na Soul (Determinacja) (T:S, 6:2) lub popada w panikę/dezorientację (opis narracyjny zależny od MG).\n\nW miejscu wbicia tworzy się trwały energetyczny obelisk (Szkarłatny Znak), działający jak fokus dla dalszych zaklęć Magii Szkarłatu (wszystkie zaklęcia Magii Szkarłatu rzucane przez członków Szkarłatnego Kultu są o jeden stopień prostsze, do minimalnej Trudności równej 3).\n\nUczestnicy rytuału podlegają “szkarłatnym snom” przez 7 dni, a ponadto wszystkie zaklęcia nienależące do dziedziny Magii Szkarłatu przez ten czas wymagają 3 Sukcesów więcej. “Szkarłatne sny” wpływają na zachowanie postaci (Mistrz Gry decyduje, czy da się uniknąć wykonywania poleceń “szeptów”).\n\nRequiem zawsze było zarówno narzędziem destrukcji, jak i rozłamu rzeczywistości — halabarda powstająca z esencji “Szkarłatnego Wymiaru” nie tylko umożliwia wtargnięcie w centrum bitwy, ale natychmiast wywołuje falę energii dezorganizującą, rozbijającą linie wroga i uderzającą w samą czasoprzestrzeń. Pozostaje symbolem siły Kultu i jego destruktywnego potencjału."
|
"description": "W dawnych kronikach Requiem symbolizowało narzędzie “ostatecznej separacji” - poprzez halabardę otwierano szczeliny w rzeczywistości, by elita Kultu mogła błyskawicznie przedrzeć się przez obronę, zasiewając zamęt i rozbicie wśród wrogów. Nikogo nie zabiera, nie drenuje życia, lecz wnosi do świata esencję Szkarłatu: rozprężenie struktur, dezorientację i przewagę dla wtajemniczonych.\n\nW ręce maga pojawia się utkana ze szkarłatnej energii halabarda, którą następnie może rzucić w wybrany przez siebie cel.\n\nGdy halabarda wbija się w wyznaczone miejsce, natychmiast zadaje wszystkim istotom w promieniu 10 metrów 3k6 obrażeń ignorujących Pancerz oraz wszystkie odporności, następnie mag (oraz do 2 sprzymierzeńców w promieniu 2 m) natychmiast teleportuje się do miejsca wbicia halabardy.\n\nPo wbiciu się ostrza, wokół (przez 1 godzinę na promieniu 50 m) gęstnieje szkarłatna aura. Każda istota (niebędąca członkiem Kultu) wchodząca lub przebywająca w strefie na początku swojej tury otrzymuje 2k6 obrażeń ignorujących Pancerz oraz wszystkie odporności oraz wykonuje Test na Soul (Determinacja) (T:S, 6:2) lub popada w panikę/dezorientację (opis narracyjny zależny od MG).\n\nW miejscu wbicia tworzy się trwały energetyczny obelisk (Szkarłatny Znak), działający jak fokus dla dalszych zaklęć Magii Szkarłatu (wszystkie zaklęcia Magii Szkarłatu rzucane przez członków Szkarłatnego Kultu są o jeden stopień prostsze, do minimalnej Trudności równej 3).\n\nUczestnicy rytuału podlegają “szkarłatnym snom” przez 7 dni, a ponadto wszystkie zaklęcia nienależące do dziedziny Magii Szkarłatu przez ten czas wymagają 3 Sukcesów więcej. “Szkarłatne sny” wpływają na zachowanie postaci (Mistrz Gry decyduje, czy da się uniknąć wykonywania poleceń “szeptów”).\n\nRequiem zawsze było zarówno narzędziem destrukcji, jak i rozłamu rzeczywistości - halabarda powstająca z esencji “Szkarłatnego Wymiaru” nie tylko umożliwia wtargnięcie w centrum bitwy, ale natychmiast wywołuje falę energii dezorganizującą, rozbijającą linie wroga i uderzającą w samą czasoprzestrzeń. Pozostaje symbolem siły Kultu i jego destruktywnego potencjału."
|
||||||
},
|
},
|
||||||
"Telekineza": {
|
"Telekineza": {
|
||||||
"name": "Telekinesis",
|
"name": "Telekinesis",
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"Superzaklęcie - Requiem": {
|
|
||||||
"name": "Superspell Requiem",
|
|
||||||
"description": "W dawnych kronikach Requiem symbolizowało narzędzie “ostatecznej separacji” - poprzez halabardę otwierano szczeliny w rzeczywistości, by elita Kultu mogła błyskawicznie przedrzeć się przez obronę, zasiewając zamęt i rozbicie wśród wrogów. Nikogo nie zabiera, nie drenuje życia, lecz wnosi do świata esencję Szkarłatu: rozprężenie struktur, dezorientację i przewagę dla wtajemniczonych.\n\nW ręce maga pojawia się utkana ze szkarłatnej energii halabarda, którą następnie może rzucić w wybrany przez siebie cel.\n\nGdy halabarda wbija się w wyznaczone miejsce, natychmiast zadaje wszystkim istotom w promieniu 10 metrów 3k6 obrażeń ignorujących Pancerz oraz wszystkie odporności, następnie mag (oraz do 2 sprzymierzeńców w promieniu 2 m) natychmiast teleportuje się do miejsca wbicia halabardy.\n\nPo wbiciu się ostrza, wokół (przez 1 godzinę na promieniu 50 m) gęstnieje szkarłatna aura. Każda istota (niebędąca członkiem Kultu) wchodząca lub przebywająca w strefie na początku swojej tury otrzymuje 2k6 obrażeń ignorujących Pancerz oraz wszystkie odporności oraz wykonuje Test na Soul (Determinacja) (T:S, 6:2) lub popada w panikę/dezorientację (opis narracyjny zależny od MG).\n\nW miejscu wbicia tworzy się trwały energetyczny obelisk (Szkarłatny Znak), działający jak fokus dla dalszych zaklęć Magii Szkarłatu (wszystkie zaklęcia Magii Szkarłatu rzucane przez członków Szkarłatnego Kultu są o jeden stopień prostsze, do minimalnej Trudności równej 3).\n\nUczestnicy rytuału podlegają “szkarłatnym snom” przez 7 dni, a ponadto wszystkie zaklęcia nienależące do dziedziny Magii Szkarłatu przez ten czas wymagają 3 Sukcesów więcej. “Szkarłatne sny” wpływają na zachowanie postaci (Mistrz Gry decyduje, czy da się uniknąć wykonywania poleceń “szeptów”).\n\nRequiem zawsze było zarówno narzędziem destrukcji, jak i rozłamu rzeczywistości — halabarda powstająca z esencji “Szkarłatnego Wymiaru” nie tylko umożliwia wtargnięcie w centrum bitwy, ale natychmiast wywołuje falę energii dezorganizującą, rozbijającą linie wroga i uderzającą w samą czasoprzestrzeń. Pozostaje symbolem siły Kultu i jego destruktywnego potencjału."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"label": "Talents - Curse of the Abyss",
|
|
||||||
"entries": {
|
|
||||||
"Swobodny Przepływ Magii": {
|
|
||||||
"name": "Free Flow Of Magic",
|
|
||||||
"description": "**Wymagania**: Dziedzina Magii (Magic Otchłani (Aspektów))\n\nAby móc otrzymać ten Talent, mag musi odrzucić swoją poprzednią dziedzinę i poświęcić się w pełni tej dziedzinie magii. Zmiana dziedziny magii jest możliwa wyłącznie jeden raz.\n\nMag może rzucać zaklęcia z innych Akademickich Dziedzin Magii niż jego pierwotna.\nMoże uczyć się zaklęć z Dziedzin innej niż swoja, ale są one o jeden punkt Many droższe, a ponadto do rzucania takich zaklęć wymagana jest esencja w formie płynnej. Mag natomiast nie może uczyć się ani rzucać modlitw. Ilość i potrzebne rodzaje esencji określa Mistrz Gry, natomiast zasada jest taka, że za każdy punkt Many, który kosztuje to zaklęcie, należy dodać kolejną fiolkę 50 ml Esencji. Fiolka może zostać podzielona na porcje - jedna fiolka zawiera pięć porcji Esencji.\n\nMagowie Aspektów mogą się ukrywać jako magowie swojej pierwotnej dziedziny"
|
|
||||||
},
|
|
||||||
"Dar Otchłani": {
|
|
||||||
"name": "Gift Of The Abyss",
|
|
||||||
"description": "**Wymagania**: Dziedzina Magii (Magic Otchłani (Pierwotna Magic)), Talent (Swobodny Przepływ Magii)\n\nAby móc otrzymać ten Talent, mag musi odrzucić swoją poprzednią dziedzinę i poświęcić się w pełni tej dziedzinie magii. Mag musi być obecnie Magiem Aspektów oraz musi być magiem poziomu co najmniej II oraz musi mieć talent Zakazana Wiedza (Otchłań). Zmiana Dziedziny magii jest możliwa wyłącznie jeden raz. Ten Talent zastępuje Talent “Swobodny Przepływ Magii”.\n\nMag może rzucać zaklęcia ze wszystkich Akademickich Dziedzin Magii oraz ich się uczyć.\nNie musi wykorzystywać many, ale za każdy punkt Many, który kosztuje zaklęcie, musi osiągnąć jeden sukces w Teście Magic (Zdolności Magiczne), gdzie Trudność (T) wynosi 5. W wypadku niepowodzenia zaklęcie się nie udaje, a mag musi rzucać tyloma kośćmi na tabelę Kary za Nieudolność, ile brakowało mu sukcesów do zdania Testu.\n\nMag Pierwotnej Magii traci wszelki związek ze swoją pierwotną dziedziną magii - jego ślad magiczny zmienia się nie do poznania."
|
|
||||||
},
|
|
||||||
"Metamagia": {
|
|
||||||
"name": "Metamagic",
|
|
||||||
"description": "*Ten Talent można wykupić wyłącznie podczas tworzenia postaci lub jeżeli Postać staje się Magiem Otchłani.*\n\n**Wymagania**: Dziedzina Magii (Wiedźmia Magic, Magic Alchemii (Transmutacji) lub Magic Otchłani)\n\nTego Talentu nie można wziąć jeżeli Postać ma już Talent Odporność na Magię lub Jedność z Magią.\n\nNiektórzy Magowie są w stanie zjednoczyć się z magią, jednak w przeciwieństwie do ras takich jak elfy, których magia jest naturą, jedność nie polega na umiejętności kierowania i rozkazywania magii, lecz na umiejętności zrozumienia i manipulacji magią w otoczeniu.\n\nTen Talent można brać wielokrotnie do maksymalnie dwóch dodatkowych razy, za każdym razem zwiększając maksymalną ilość Punktów Metamagii o 2.\n\nPostać może mieć jednocześnie tyle punktów Magii, ile wynosi zaokrąglona w górę połowa wartości Atrybutu Soul + 1. Każda z Akcji Metamagii może być wykonana wyłącznie raz w trakcie rundy oraz nie mogą wpływać na to samo zaklęcie, chyba że jest napisane inaczej.\n\nWszystkie Punkty Metamagii odnawiają się po Długim Odpoczynku.\n\nPostać z tym Talentem otrzymuje dostęp do następujących Akcji:\n#### Rozdwojone Zaklęcie\n**Koszt**: 3 Punkty Metamagii\n**Wymagania**: Zaklęcie, którego celem są pojedyncze cele i Nadczarowanie nie zwiększyło ilości celów.\n\nZaklęcie rzucane przez Postać działa na dodatkowy cel.\n\n#### Niezniszczalne Zaklęcie\n**Koszt**: 1 Punkt Metamagii\n\nPostać rzuca kością 1k3. Zaklęcie wymaga tyle dodatkowych Sukcesów do rozplecenia, ile wyniósł ten rzut.\n\n#### Rozproszenie Magii\n**Koszt**: 2 Punkty Metamagii, 1 Punkt Zapału\n**Wymagania**: Inna Postać rzucająca zaklęcie\n\nGdy wybrany cel rzuca zaklęcie, Postać może rzucić kością 1k3. Rzucane przez cel zaklęcie wymaga tyle dodatkowych Sukcesów, ile wyniósł ten rzut.\n\n#### Błyskawiczne Czarostwo\n**Koszt**: 2 Punkty Metamagii\n\nZaklęcie wymaga jednej tury mniej do rzucenia.\n\n---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,10 @@
|
|||||||
"name": "Ambidextrous",
|
"name": "Ambidextrous",
|
||||||
"description": "Sprawnie posługujesz się bronią w obydwu rękach.\n\n\nGdy wykonujesz ataki dwiema broniami, otrzymujesz dwie dodatkowe kości"
|
"description": "Sprawnie posługujesz się bronią w obydwu rękach.\n\n\nGdy wykonujesz ataki dwiema broniami, otrzymujesz dwie dodatkowe kości"
|
||||||
},
|
},
|
||||||
|
"Pasjonat Zwierząt": {
|
||||||
|
"name": "Animal Enthusiast",
|
||||||
|
"description": "Spowija cię aura, która naturalnie uspokaja zwierzęta w twojej obecności. Potrafisz mniej więcej zrozumieć, co zwierzę chce ci \"powiedzieć\". Niesprowokowane nie zaatakują cię. Podwajasz ilość kości wynikającą z Umiejętności Opieka nad Zwierzętami, jeżeli zwierzę nie jest wobec Ciebie agresywne."
|
||||||
|
},
|
||||||
"Arcymag (Dziedzina)": {
|
"Arcymag (Dziedzina)": {
|
||||||
"name": "Archmage",
|
"name": "Archmage",
|
||||||
"description": "Wszystkie Testy związane z daną Dziedziną Magii są prostsze o jeden stopień, a ponadto wszystkie zaklęcia są traktowane, jakby były nadczarowane o jeden stopień (ale nie zużywają dodatkowej many). Dodatkowo postać zna wszystkie zaklęcia z tej Dziedziny.\n\n\nTen Talent może zostać wybrane dla maksymalnie jednej Dziedziny - tej, w której jest Mistrzem Magii."
|
"description": "Wszystkie Testy związane z daną Dziedziną Magii są prostsze o jeden stopień, a ponadto wszystkie zaklęcia są traktowane, jakby były nadczarowane o jeden stopień (ale nie zużywają dodatkowej many). Dodatkowo postać zna wszystkie zaklęcia z tej Dziedziny.\n\n\nTen Talent może zostać wybrane dla maksymalnie jednej Dziedziny - tej, w której jest Mistrzem Magii."
|
||||||
@@ -17,6 +21,10 @@
|
|||||||
"name": "Armor Of Faith",
|
"name": "Armor Of Faith",
|
||||||
"description": "Możesz rzucać zaklęcia będąc w zbroi Średniej lub Ciężkiej bez kar."
|
"description": "Możesz rzucać zaklęcia będąc w zbroi Średniej lub Ciężkiej bez kar."
|
||||||
},
|
},
|
||||||
|
"Arytmetyczna Koncentracja": {
|
||||||
|
"name": "Arytmetyczna Koncentracja",
|
||||||
|
"description": "Ilość kości w Testach na utrzymanie Koncentracji jest zwiększona o połowę wartości Atrybutu Mind."
|
||||||
|
},
|
||||||
"Gadanina": {
|
"Gadanina": {
|
||||||
"name": "Babble",
|
"name": "Babble",
|
||||||
"description": "Podwajasz liczbę kości wynikających z ilości Punktów Umiejętności w Umiejętności Przekonywanie jeżeli chcesz kogoś zagadać lub odwrócić czyjąś uwagę swoją gadaniną."
|
"description": "Podwajasz liczbę kości wynikających z ilości Punktów Umiejętności w Umiejętności Przekonywanie jeżeli chcesz kogoś zagadać lub odwrócić czyjąś uwagę swoją gadaniną."
|
||||||
@@ -33,6 +41,10 @@
|
|||||||
"name": "Blessed Origin",
|
"name": "Blessed Origin",
|
||||||
"description": "Anioły od Boga-Jahwe-Jedynego dostały dwa dary - naturalną zdolność do latania oraz możliwość odrodzenia się po śmierci."
|
"description": "Anioły od Boga-Jahwe-Jedynego dostały dwa dary - naturalną zdolność do latania oraz możliwość odrodzenia się po śmierci."
|
||||||
},
|
},
|
||||||
|
"Błogosławieni Egzorcyści": {
|
||||||
|
"name": "Blogoslawieni Egzorcysci",
|
||||||
|
"description": "Zamiast pasywnej zdolności adeptów Magii Sakralnej, egzorcyści mają dostęp do egzorcyzmów oraz mogą wykupić i korzystać z talentu Zakazana Wiedza (Demonologia) bez żadnych kar."
|
||||||
|
},
|
||||||
"Sen na Jawie": {
|
"Sen na Jawie": {
|
||||||
"name": "Daydream",
|
"name": "Daydream",
|
||||||
"description": "Nie potrzebujesz snu.\n\n\nElfy w przeciwieństwie do innych ras nie śpią, ale nie wszystkie potrafią kontrolować swoje myśli w trakcie odpoczynku."
|
"description": "Nie potrzebujesz snu.\n\n\nElfy w przeciwieństwie do innych ras nie śpią, ale nie wszystkie potrafią kontrolować swoje myśli w trakcie odpoczynku."
|
||||||
@@ -49,6 +61,10 @@
|
|||||||
"name": "Feint",
|
"name": "Feint",
|
||||||
"description": "Postać wykonuje zwodniczy atak, którego celem jest zmylenie przeciwnika.\n\n\nNależy wykonać Test Przeciwstawny zgodnie z zasadami Akcji Atak, jednak w przypadku, gdy Postać, która zadeklarowała, że będzie wykorzystywała ten Talent wygra pojedynek, nie zadaje Obrażeń. W zamian cel Finty do końca następnej rundy atakującego bądź do następnego Ataku atakującego skierowanego w cel nie może unikać, odpierać ani parować ataku Postaci, która z powodzeniem użyła Finty.\n\n\nJeżeli atakujący atakuje oburącz, a Postać również trzyma dwie bronie, może sparować obydwa ataki, jednak musi podzielić pulę kości na obydwa parowania (podobnie jak przy walce oburącz)."
|
"description": "Postać wykonuje zwodniczy atak, którego celem jest zmylenie przeciwnika.\n\n\nNależy wykonać Test Przeciwstawny zgodnie z zasadami Akcji Atak, jednak w przypadku, gdy Postać, która zadeklarowała, że będzie wykorzystywała ten Talent wygra pojedynek, nie zadaje Obrażeń. W zamian cel Finty do końca następnej rundy atakującego bądź do następnego Ataku atakującego skierowanego w cel nie może unikać, odpierać ani parować ataku Postaci, która z powodzeniem użyła Finty.\n\n\nJeżeli atakujący atakuje oburącz, a Postać również trzyma dwie bronie, może sparować obydwa ataki, jednak musi podzielić pulę kości na obydwa parowania (podobnie jak przy walce oburącz)."
|
||||||
},
|
},
|
||||||
|
"Medyk Polowy": {
|
||||||
|
"name": "Field Medic",
|
||||||
|
"description": "Chirurgiem może i nie jesteś, ale równie dobrze potrafisz ludzi bandażować. Twoje testy na pomoc medyczną mają podwojoną ilość kości wynikającą z umiejętności. Dodatkowo, osoby które leczysz odnawiają 1k6 więcej Punktów Życia."
|
||||||
|
},
|
||||||
"Zakazana Wiedza (Dziedzina)": {
|
"Zakazana Wiedza (Dziedzina)": {
|
||||||
"name": "Forbidden Knowledge",
|
"name": "Forbidden Knowledge",
|
||||||
"description": "Liczba kości dla wybranej Umiejętności przy rzutach związanych z daną dziedziną jest podwajana. Jeśli pomimo tego Test się nie uda, musisz liczyć się z groźnymi konsekwencjami…"
|
"description": "Liczba kości dla wybranej Umiejętności przy rzutach związanych z daną dziedziną jest podwajana. Jeśli pomimo tego Test się nie uda, musisz liczyć się z groźnymi konsekwencjami…"
|
||||||
@@ -61,6 +77,10 @@
|
|||||||
"name": "Indestructible Spells",
|
"name": "Indestructible Spells",
|
||||||
"description": "Ilość Sukcesów wymaganych do rozplecenia zaklęcia rzuconego przez Ciebie jest zwiększona o zaokrągloną w górę połowę wartości Umiejętności Zdolności Magiczne."
|
"description": "Ilość Sukcesów wymaganych do rozplecenia zaklęcia rzuconego przez Ciebie jest zwiększona o zaokrągloną w górę połowę wartości Umiejętności Zdolności Magiczne."
|
||||||
},
|
},
|
||||||
|
"Jedność z Żywiołem": {
|
||||||
|
"name": "Jednosc Z Zywiolem",
|
||||||
|
"description": "Rzucasz jedną kością mniej (do minimum jednej) na kary związane z nieudolnym rzucaniem zaklęć, jeśli zaklęcie, które się nie powiodło, jest z Twojego żywiołu."
|
||||||
|
},
|
||||||
"Czuły Zmysł (Zmysł)": {
|
"Czuły Zmysł (Zmysł)": {
|
||||||
"name": "Keen Sense",
|
"name": "Keen Sense",
|
||||||
"description": "Jeden z Twoich zmysłów jest szczególnie rozwinięty. Wybierz jeden ze zmysłów (wzrok, słuch, dotyk, smak, węch). Jeśli wykonujesz Test na Spostrzegawczość z użyciem danego zmysłu, wartość Umiejętności Spostrzegawczość zostaje podwojona przy tym rzucie."
|
"description": "Jeden z Twoich zmysłów jest szczególnie rozwinięty. Wybierz jeden ze zmysłów (wzrok, słuch, dotyk, smak, węch). Jeśli wykonujesz Test na Spostrzegawczość z użyciem danego zmysłu, wartość Umiejętności Spostrzegawczość zostaje podwojona przy tym rzucie."
|
||||||
@@ -81,10 +101,38 @@
|
|||||||
"name": "Magical Sense",
|
"name": "Magical Sense",
|
||||||
"description": "Masz wrodzony dar do wykrywania i obrazowania otaczającej Cię magii, jej ukierunkowania oraz efektu. Testy na opieranie się efektowi zaklęć i na rozplatanie zaklęć wymagają jednego sukcesu mniej. Ponadto wybierasz, którym zmysłem \"widzisz\" magię (wzrok - wstęgi w powietrzu, dotyk - drżenie w palcach o różnym natężeniu i częstotliwości, słuch - różne dźwięki itd.)."
|
"description": "Masz wrodzony dar do wykrywania i obrazowania otaczającej Cię magii, jej ukierunkowania oraz efektu. Testy na opieranie się efektowi zaklęć i na rozplatanie zaklęć wymagają jednego sukcesu mniej. Ponadto wybierasz, którym zmysłem \"widzisz\" magię (wzrok - wstęgi w powietrzu, dotyk - drżenie w palcach o różnym natężeniu i częstotliwości, słuch - różne dźwięki itd.)."
|
||||||
},
|
},
|
||||||
|
"Magiczna Flora": {
|
||||||
|
"name": "Magiczna Flora",
|
||||||
|
"description": "PT testów związanych z rozpoznawaniem roślin jest niższy o jeden stopień, jeżeli Postać kiedykolwiek spotkała się z daną rośliną (nie musi o tym pamiętać)."
|
||||||
|
},
|
||||||
|
"Człowiek z Żelaza": {
|
||||||
|
"name": "Man Of Iron",
|
||||||
|
"description": "Twoja skóra jest niezwykle twarda i kiedy nie nosisz pancerza, daje ci 1 punkt pancerza."
|
||||||
|
},
|
||||||
"Mistrz Magii (Dziedzina)": {
|
"Mistrz Magii (Dziedzina)": {
|
||||||
"name": "Master Of Magic",
|
"name": "Master Of Magic",
|
||||||
"description": "Przy rzutach związanych z daną Dziedziną Magii liczba Kości otrzymywana we wszystkich Umiejętnościach zostaje podwojona. \n\n\nTen Talent może zostać wybrane dla maksymalnie jednej Dziedziny."
|
"description": "Przy rzutach związanych z daną Dziedziną Magii liczba Kości otrzymywana we wszystkich Umiejętnościach zostaje podwojona. \n\n\nTen Talent może zostać wybrane dla maksymalnie jednej Dziedziny."
|
||||||
},
|
},
|
||||||
|
"Miłosierdzie": {
|
||||||
|
"name": "Milosierdzie",
|
||||||
|
"description": "Efekty leczące i chroniące zaklęć Magii Sakralnej są o połowę mocniejsze."
|
||||||
|
},
|
||||||
|
"Nadzwyczajna Odporność": {
|
||||||
|
"name": "Nadzwyczajna Odpornosc",
|
||||||
|
"description": "Rzucasz dodatkową kością na testy związane z odpornością na trucizny. Ponadto Tolerancja na eliksiry odnawia się po Krótkim Odpoczynku, nie po Długim, jak normalnie."
|
||||||
|
},
|
||||||
|
"Naturalny Lot": {
|
||||||
|
"name": "Natural Flight",
|
||||||
|
"description": "Anioły i demony posiadają naturalne skrzydła, które pozwalają im latać.\n\n\nAby utrzymać się w powietrzu, postać musi poruszyć się o co najmniej 5 metrów w każdej turze walki. Jeżeli postać nie ruszy się o wymaganą odległość, spada na ziemię na koniec tury.\n\n\nPostać z tym Talentem może nabyć Talenty Zawisanie w Powietrzu oraz Powietrzne Uniki."
|
||||||
|
},
|
||||||
|
"Niezwykła Intuicja": {
|
||||||
|
"name": "Niezwykla Intuicja",
|
||||||
|
"description": "PT Testów na wykrywanie kłamstw i iluzji jest niższe o jeden stopień."
|
||||||
|
},
|
||||||
|
"Olimpijczyk": {
|
||||||
|
"name": "Olympian",
|
||||||
|
"description": "Jesteś bardzo wysportowany. Testy na Siłę, Atletykę lub inne testy związane z wysportowaniem dostają 2 dodatkowe kości."
|
||||||
|
},
|
||||||
"Oportunista": {
|
"Oportunista": {
|
||||||
"name": "Opportunist",
|
"name": "Opportunist",
|
||||||
"description": "Twoja zdolność do reagowania na wydarzenia w trakcie walki jest zadziwiająca.\n\n\nRaz na rundę gdy przeciwnik odchodzi od Ciebie i masz możliwość zaatakowania go, możesz podjąć próbę ataku."
|
"description": "Twoja zdolność do reagowania na wydarzenia w trakcie walki jest zadziwiająca.\n\n\nRaz na rundę gdy przeciwnik odchodzi od Ciebie i masz możliwość zaatakowania go, możesz podjąć próbę ataku."
|
||||||
@@ -97,6 +145,14 @@
|
|||||||
"name": "Personal Charm",
|
"name": "Personal Charm",
|
||||||
"description": "Wszystkie Testy związane z zauraczaniem kogoś lub wykorzystywaniem swojego wyglądu do zmanipulowania innej osoby są prostsze o jeden stopień."
|
"description": "Wszystkie Testy związane z zauraczaniem kogoś lub wykorzystywaniem swojego wyglądu do zmanipulowania innej osoby są prostsze o jeden stopień."
|
||||||
},
|
},
|
||||||
|
"Pierwotna Energia": {
|
||||||
|
"name": "Pierwotna Energia",
|
||||||
|
"description": "Nie możesz rzucać ani rozplatać zaklęć. Twój Maksymalny Koszt Zaklęcia oraz Ilość Many zostają dwukrotnie zwiększone.\n\n\nW ramach Akcji możesz pomóc wybranemu magowi w zasięgu 3 m w rzucaniu zaklęć. Do początku Twojej następnej tury, jeżeli wybrany mag się od Ciebie nie odsunie, gdy ten mag rzuca jakiekolwiek zaklęcie, możesz zadeklarować, że zużywasz swoją manę, aby wzmocnić zaklęcie wybranego maga (Księga Magii). Mana wykorzystana w celu wzmocnienia zaklęcia nie liczy się do wykonywanego przez maga testu na to, czy uda mu się kontrolować dużą ilość mocy, ale wzmacniając zaklęcie, nie możesz przekroczyć swojego maksymalnego kosztu zaklęcia.\n\n\nDodatkowo w ramach Akcji Natychmiastowej możesz przekazać część swojej many. Jeżeli chcesz to zrobić, wykonaj Test na Zdolności Magiczne (Magic). Trudność zależy od odległości od maga (4:S poniżej 1 m, 5:S poniżej 2 m, 6:S poniżej 3 m), a Ilość Wymaganych Sukcesów (S) jest równa ilości przekazywanej many."
|
||||||
|
},
|
||||||
|
"Podejrzliwość Wobec Zmian": {
|
||||||
|
"name": "Podejrzliwosc Wobec Zmian",
|
||||||
|
"description": "Rzucasz dodatkową kością na testy związane z uniknięciem efektu magicznego."
|
||||||
|
},
|
||||||
"Odporność na Trucizny": {
|
"Odporność na Trucizny": {
|
||||||
"name": "Poison Resistance",
|
"name": "Poison Resistance",
|
||||||
"description": "Postać do pewnego stopnia uodporniła się na zatrucia.\n\n\nGdy postać musi wykonać test na Wytrzymałość aby uniknąć zatrucia, podwój ilość kości wynikającą z poziomu Umiejętności Wytrzymałość."
|
"description": "Postać do pewnego stopnia uodporniła się na zatrucia.\n\n\nGdy postać musi wykonać test na Wytrzymałość aby uniknąć zatrucia, podwój ilość kości wynikającą z poziomu Umiejętności Wytrzymałość."
|
||||||
@@ -109,6 +165,10 @@
|
|||||||
"name": "Spell Shattering",
|
"name": "Spell Shattering",
|
||||||
"description": "Każdy mag posiadający ten Talent może raz na rundę za darmo dokonać próby rozplatania, jednak dane zaklęcie może być rozplatane wyłącznie raz.\n\n\nMagowie posiadający Zapał mogą próbować rozpleść więcej zaklęć - każda następna taka próba kosztuje jeden punkt Zapału. Magowie sakralni mogą potraktować rozplatanie zaklęcia jako modlitwę, zmieniając Atrybut swojego Testu na Duszę, ale jednocześnie zwiększając koszt wykorzystania tego Talentu o 1 punkt Zapału.\n\n\nJeżeli Postać widzi istotę rzucającą zaklęcie i jest od niej w zasięgu 20 m, może zadeklarować, że próbuje rozpleść rzucane zaklęcie. Postać wykonuje Test na Zdolności Magiczne (Magic lub Soul, jeśli jest magiem sakralnym), gdzie Trudność (T) jest równa trudności zaklęcia, a liczba wymaganych Sukcesów (S) jest o 1 większa od liczby sukcesów wyrzuconych przez maga rzucającego zaklęcie. Jeżeli wynik Testu Postaci będzie wyższy niż wynik Testu przeciwnika, zaklęcie zostaje rozplecione."
|
"description": "Każdy mag posiadający ten Talent może raz na rundę za darmo dokonać próby rozplatania, jednak dane zaklęcie może być rozplatane wyłącznie raz.\n\n\nMagowie posiadający Zapał mogą próbować rozpleść więcej zaklęć - każda następna taka próba kosztuje jeden punkt Zapału. Magowie sakralni mogą potraktować rozplatanie zaklęcia jako modlitwę, zmieniając Atrybut swojego Testu na Duszę, ale jednocześnie zwiększając koszt wykorzystania tego Talentu o 1 punkt Zapału.\n\n\nJeżeli Postać widzi istotę rzucającą zaklęcie i jest od niej w zasięgu 20 m, może zadeklarować, że próbuje rozpleść rzucane zaklęcie. Postać wykonuje Test na Zdolności Magiczne (Magic lub Soul, jeśli jest magiem sakralnym), gdzie Trudność (T) jest równa trudności zaklęcia, a liczba wymaganych Sukcesów (S) jest o 1 większa od liczby sukcesów wyrzuconych przez maga rzucającego zaklęcie. Jeżeli wynik Testu Postaci będzie wyższy niż wynik Testu przeciwnika, zaklęcie zostaje rozplecione."
|
||||||
},
|
},
|
||||||
|
"Tajemna Technologia": {
|
||||||
|
"name": "Tajemna Technologia",
|
||||||
|
"description": "PT Testów na rozpoznawanie artefaktów i rozumienie ich działania jest niższy o jeden stopień, jeżeli Postać miała styczność z czymś podobnym (nie musi o tym pamiętać)."
|
||||||
|
},
|
||||||
"Technologia Jest Wśród Nas": {
|
"Technologia Jest Wśród Nas": {
|
||||||
"name": "Technology Is Among Us",
|
"name": "Technology Is Among Us",
|
||||||
"description": "Jak inne rasy na co dzień mają styczność z magią, tak ludzie posługują się znacznie bardziej zaawansowaną technologią niż inne rasy - telefony, komputery, samochody… Długo by tu wymieniać. Dla innych ras ludzka technologia jest niezrozumiała, ale ludzie potrafią się nią bez problemu posługiwać.\n\n\nPodstawowa obsługa ludzkich urządzeń nie sprawia Ci kłopotu, a dodatkowo jeżeli masz instrukcję do danego urządzenia w języku zrozumiałym dla ciebie, to bez problemu możesz skorzystać z technologii Obcej cywilizacji, której poziom technologiczny jest porównywalny bądź niższy do poziomu cywilizacji ziemskiej."
|
"description": "Jak inne rasy na co dzień mają styczność z magią, tak ludzie posługują się znacznie bardziej zaawansowaną technologią niż inne rasy - telefony, komputery, samochody… Długo by tu wymieniać. Dla innych ras ludzka technologia jest niezrozumiała, ale ludzie potrafią się nią bez problemu posługiwać.\n\n\nPodstawowa obsługa ludzkich urządzeń nie sprawia Ci kłopotu, a dodatkowo jeżeli masz instrukcję do danego urządzenia w języku zrozumiałym dla ciebie, to bez problemu możesz skorzystać z technologii Obcej cywilizacji, której poziom technologiczny jest porównywalny bądź niższy do poziomu cywilizacji ziemskiej."
|
||||||
|
|||||||
+21
-11
@@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"HBM": {
|
"HBM": {
|
||||||
"system": { "name": "Homebrew Magic: RPG v3" },
|
"system": {
|
||||||
"actor": { "character": "Character", "npc": "NPC / Creature" },
|
"name": "Homebrew Magic: RPG v3"
|
||||||
|
},
|
||||||
|
"actor": {
|
||||||
|
"character": "Character",
|
||||||
|
"npc": "NPC / Creature"
|
||||||
|
},
|
||||||
"item": {
|
"item": {
|
||||||
"spell": "Spell",
|
"spell": "Spell",
|
||||||
"gear": "Gear",
|
"gear": "Gear",
|
||||||
@@ -155,7 +160,8 @@
|
|||||||
"sacred": "Sacred Magic",
|
"sacred": "Sacred Magic",
|
||||||
"witch": "Witch Magic",
|
"witch": "Witch Magic",
|
||||||
"blood": "Blood Magic",
|
"blood": "Blood Magic",
|
||||||
"hekate": "Hekate Hybrid"
|
"hekate": "Hekate Hybrid",
|
||||||
|
"prayer": "Prayer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"spellSchool": {
|
"spellSchool": {
|
||||||
@@ -173,14 +179,15 @@
|
|||||||
"manaSourceMage": "Mana Source",
|
"manaSourceMage": "Mana Source",
|
||||||
"illusion": "Illusion",
|
"illusion": "Illusion",
|
||||||
"sacred": "Sacred Magic",
|
"sacred": "Sacred Magic",
|
||||||
"sacredExorcism": "Sacred Magic — Exorcism",
|
"sacredExorcism": "Sacred Magic - Exorcism",
|
||||||
"witch": "Witch Magic",
|
"witch": "Witch Magic",
|
||||||
"necromancy": "Necromancy",
|
"necromancy": "Necromancy",
|
||||||
"blood": "Blood Magic",
|
"blood": "Blood Magic",
|
||||||
"crimson": "Crimson Magic",
|
"crimson": "Crimson Magic",
|
||||||
"abyssAspects": "Abyss — Aspects",
|
"abyssAspects": "Abyss - Aspects",
|
||||||
"abyssPrimal": "Abyss — Primal",
|
"abyssPrimal": "Abyss - Primal",
|
||||||
"wildWitch": "Wild Witch"
|
"wildWitch": "Wild Witch",
|
||||||
|
"eldritch": "Eldritch"
|
||||||
},
|
},
|
||||||
"deity": {
|
"deity": {
|
||||||
"common": "Common Prayers",
|
"common": "Common Prayers",
|
||||||
@@ -350,6 +357,7 @@
|
|||||||
"spellCast": {
|
"spellCast": {
|
||||||
"title": "Casting spell",
|
"title": "Casting spell",
|
||||||
"manaSpent": "Mana spent",
|
"manaSpent": "Mana spent",
|
||||||
|
"cannotBeUnraveled": "Cannot be unraveled",
|
||||||
"zealSpent": "Zeal spent",
|
"zealSpent": "Zeal spent",
|
||||||
"bloodSpent": "Blood spent",
|
"bloodSpent": "Blood spent",
|
||||||
"hekateMode": "Hekate mode (Sacred + Witch hybrid)",
|
"hekateMode": "Hekate mode (Sacred + Witch hybrid)",
|
||||||
@@ -401,12 +409,12 @@
|
|||||||
"physicalArmorReduced": "Physical Armor reduced: {n}",
|
"physicalArmorReduced": "Physical Armor reduced: {n}",
|
||||||
"healthLost": "Health lost: {n}",
|
"healthLost": "Health lost: {n}",
|
||||||
"armorBroken": "Armor broken!",
|
"armorBroken": "Armor broken!",
|
||||||
"runeTriggered": "Runes triggered — Magical Armor -1"
|
"runeTriggered": "Runes triggered - Magical Armor -1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"combat": {
|
"combat": {
|
||||||
"newRound": "New round — Mana reset",
|
"newRound": "New round - Mana reset",
|
||||||
"newTurn": "New turn — Zeal +1",
|
"newTurn": "New turn - Zeal +1",
|
||||||
"turnSkipped": "Turn skipped (condition)",
|
"turnSkipped": "Turn skipped (condition)",
|
||||||
"deathSavePrompt": "Make a Death Saving Throw (Body, T:4, Y:1)"
|
"deathSavePrompt": "Make a Death Saving Throw (Body, T:4, Y:1)"
|
||||||
},
|
},
|
||||||
@@ -457,6 +465,7 @@
|
|||||||
"itemAlreadyOwned": "'{name}' is already on this character sheet.",
|
"itemAlreadyOwned": "'{name}' is already on this character sheet.",
|
||||||
"dropTalentsHint": "Drag talents from the compendium onto this sheet.",
|
"dropTalentsHint": "Drag talents from the compendium onto this sheet.",
|
||||||
"physicalArmorBonus": "Armor bonus",
|
"physicalArmorBonus": "Armor bonus",
|
||||||
|
"initiativeBonus": "Initiative bonus",
|
||||||
"selectTalentParamTitle": "Talent Specialization",
|
"selectTalentParamTitle": "Talent Specialization",
|
||||||
"selectTalentParamDesc": "This talent requires a choice of specialization. Enter a value for ({param}):",
|
"selectTalentParamDesc": "This talent requires a choice of specialization. Enter a value for ({param}):",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
@@ -562,7 +571,8 @@
|
|||||||
"technologia-jest-wsrod-nas": "Technology is Among Us",
|
"technologia-jest-wsrod-nas": "Technology is Among Us",
|
||||||
"urok-osobisty": "Personal Charm",
|
"urok-osobisty": "Personal Charm",
|
||||||
"zakazana-wiedza-dziedzina": "Forbidden Knowledge (Discipline)",
|
"zakazana-wiedza-dziedzina": "Forbidden Knowledge (Discipline)",
|
||||||
"zawisanie-w-powietrzu": "Hovering"
|
"zawisanie-w-powietrzu": "Hovering",
|
||||||
|
"naturalny-lot": "Natural Flight"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+14
-9
@@ -160,7 +160,8 @@
|
|||||||
"sacred": "Magia Sakralna",
|
"sacred": "Magia Sakralna",
|
||||||
"witch": "Wiedźmia Magia",
|
"witch": "Wiedźmia Magia",
|
||||||
"blood": "Magia Krwi",
|
"blood": "Magia Krwi",
|
||||||
"hekate": "Hybryda Hekate"
|
"hekate": "Hybryda Hekate",
|
||||||
|
"prayer": "Modlitwa"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"spellSchool": {
|
"spellSchool": {
|
||||||
@@ -178,14 +179,15 @@
|
|||||||
"manaSourceMage": "Źródło Mocy",
|
"manaSourceMage": "Źródło Mocy",
|
||||||
"illusion": "Magia Iluzji",
|
"illusion": "Magia Iluzji",
|
||||||
"sacred": "Magia Sakralna",
|
"sacred": "Magia Sakralna",
|
||||||
"sacredExorcism": "Magia Sakralna — Egzorcyzmy",
|
"sacredExorcism": "Magia Sakralna - Egzorcyzmy",
|
||||||
"witch": "Wiedźmia Magia",
|
"witch": "Wiedźmia Magia",
|
||||||
"necromancy": "Nekromancja",
|
"necromancy": "Nekromancja",
|
||||||
"blood": "Magia Krwi",
|
"blood": "Magia Krwi",
|
||||||
"crimson": "Magia Szkarłatu",
|
"crimson": "Magia Szkarłatu",
|
||||||
"abyssAspects": "Magia Otchłani — Aspekty",
|
"abyssAspects": "Magia Otchłani - Aspekty",
|
||||||
"abyssPrimal": "Magia Otchłani — Pierwotna Magia",
|
"abyssPrimal": "Magia Otchłani - Pierwotna Magia",
|
||||||
"wildWitch": "Dzika Wiedźmia Magia"
|
"wildWitch": "Dzika Wiedźmia Magia",
|
||||||
|
"eldritch": "Magia Otchłani"
|
||||||
},
|
},
|
||||||
"deity": {
|
"deity": {
|
||||||
"common": "Modlitwy Powszechne",
|
"common": "Modlitwy Powszechne",
|
||||||
@@ -375,6 +377,7 @@
|
|||||||
"spellCast": {
|
"spellCast": {
|
||||||
"title": "Rzucanie zaklęcia",
|
"title": "Rzucanie zaklęcia",
|
||||||
"manaSpent": "Wydana mana",
|
"manaSpent": "Wydana mana",
|
||||||
|
"cannotBeUnraveled": "Nie da się rozpleść",
|
||||||
"cast": "Rzuć",
|
"cast": "Rzuć",
|
||||||
"notEnoughMana": "Niewystarczająca mana.",
|
"notEnoughMana": "Niewystarczająca mana.",
|
||||||
"notEnoughZeal": "Niewystarczający Zapał.",
|
"notEnoughZeal": "Niewystarczający Zapał.",
|
||||||
@@ -409,12 +412,12 @@
|
|||||||
"physicalArmorReduced": "Pancerz Fizyczny zredukował: {n}",
|
"physicalArmorReduced": "Pancerz Fizyczny zredukował: {n}",
|
||||||
"healthLost": "Utracona Żywotność: {n}",
|
"healthLost": "Utracona Żywotność: {n}",
|
||||||
"armorBroken": "Zbroja zniszczona!",
|
"armorBroken": "Zbroja zniszczona!",
|
||||||
"runeTriggered": "Runy zadziałały — Pancerz Magiczny -1"
|
"runeTriggered": "Runy zadziałały - Pancerz Magiczny -1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"combat": {
|
"combat": {
|
||||||
"newRound": "Nowa runda — Mana zresetowana",
|
"newRound": "Nowa runda - Mana zresetowana",
|
||||||
"newTurn": "Nowa tura — Zapał +1",
|
"newTurn": "Nowa tura - Zapał +1",
|
||||||
"turnSkipped": "Tura pominięta (stan)",
|
"turnSkipped": "Tura pominięta (stan)",
|
||||||
"deathSavePrompt": "Wykonaj Rzut Obronny przeciw Śmierci (Ciało, T:4, Y:1)"
|
"deathSavePrompt": "Wykonaj Rzut Obronny przeciw Śmierci (Ciało, T:4, Y:1)"
|
||||||
},
|
},
|
||||||
@@ -465,6 +468,7 @@
|
|||||||
"itemAlreadyOwned": "'{name}' jest już na tej karcie postaci.",
|
"itemAlreadyOwned": "'{name}' jest już na tej karcie postaci.",
|
||||||
"dropTalentsHint": "Przeciągnij talenty z kompendium na tę kartę.",
|
"dropTalentsHint": "Przeciągnij talenty z kompendium na tę kartę.",
|
||||||
"physicalArmorBonus": "Premia do pancerza",
|
"physicalArmorBonus": "Premia do pancerza",
|
||||||
|
"initiativeBonus": "Premia do inicjatywy",
|
||||||
"selectTalentParamTitle": "Specjalizacja Talentu",
|
"selectTalentParamTitle": "Specjalizacja Talentu",
|
||||||
"selectTalentParamDesc": "Ten talent wymaga określenia specjalizacji/parametru. Wpisz wartość dla parametru ({param}):",
|
"selectTalentParamDesc": "Ten talent wymaga określenia specjalizacji/parametru. Wpisz wartość dla parametru ({param}):",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
@@ -570,7 +574,8 @@
|
|||||||
"technologia-jest-wsrod-nas": "Technologia jest wśród nas",
|
"technologia-jest-wsrod-nas": "Technologia jest wśród nas",
|
||||||
"urok-osobisty": "Urok osobisty",
|
"urok-osobisty": "Urok osobisty",
|
||||||
"zakazana-wiedza-dziedzina": "Zakazana wiedza (Dziedzina)",
|
"zakazana-wiedza-dziedzina": "Zakazana wiedza (Dziedzina)",
|
||||||
"zawisanie-w-powietrzu": "Zawisanie w powietrzu"
|
"zawisanie-w-powietrzu": "Zawisanie w powietrzu",
|
||||||
|
"naturalny-lot": "Naturalny lot"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hbm-rpg-v3-foundry",
|
"name": "hbm-rpg-v3-foundry",
|
||||||
"version": "1.3.12",
|
"version": "1.3.30",
|
||||||
"description": "Foundry VTT system for Homebrew Magic: Role Playing Game v3",
|
"description": "Foundry VTT system for Homebrew Magic: Role Playing Game v3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ for (const pack of byPack.keys()) {
|
|||||||
console.log(` ✓ compiled ${pack}`);
|
console.log(` ✓ compiled ${pack}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[build-packs] done — ${byPack.size} packs in packs/`);
|
console.log(`[build-packs] done - ${byPack.size} packs in packs/`);
|
||||||
|
|
||||||
/** Convert a ParsedDoc into a Foundry document JSON object suitable for compilePack. */
|
/** Convert a ParsedDoc into a Foundry document JSON object suitable for compilePack. */
|
||||||
function toFoundryDoc(doc: ParsedDoc, folderId: string | null = null): Record<string, unknown> {
|
function toFoundryDoc(doc: ParsedDoc, folderId: string | null = null): Record<string, unknown> {
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ const nameTranslations: Record<string, string> = {
|
|||||||
"necromancy": "Necromancy",
|
"necromancy": "Necromancy",
|
||||||
"blood": "Blood Magic",
|
"blood": "Blood Magic",
|
||||||
"wildwitch": "Wild Witch Magic",
|
"wildwitch": "Wild Witch Magic",
|
||||||
|
"take-breather": "Take a Breather",
|
||||||
};
|
};
|
||||||
|
|
||||||
const folderNameTranslations: Record<string, string> = {
|
const folderNameTranslations: Record<string, string> = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* UUID reference linter — scans parsed docs (packs-src/*) for any
|
* UUID reference linter - scans parsed docs (packs-src/*) for any
|
||||||
* `@UUID[...]` references that don't resolve within the generated set.
|
* `@UUID[...]` references that don't resolve within the generated set.
|
||||||
*
|
*
|
||||||
* Run after build-packs.ts: `bun scripts/lint-uuid-refs.ts`
|
* Run after build-packs.ts: `bun scripts/lint-uuid-refs.ts`
|
||||||
@@ -37,14 +37,14 @@ function walk(dir: string): void {
|
|||||||
walk(packsSrcDir);
|
walk(packsSrcDir);
|
||||||
|
|
||||||
const broken = refs.filter((r) => {
|
const broken = refs.filter((r) => {
|
||||||
// UUID format: Compendium.system.pack.Item.<id> — only validate Item refs.
|
// UUID format: Compendium.system.pack.Item.<id> - only validate Item refs.
|
||||||
const m = r.uuid.match(/^Compendium\.[^.]+\.[^.]+\.Item\.(.+)$/);
|
const m = r.uuid.match(/^Compendium\.[^.]+\.[^.]+\.Item\.(.+)$/);
|
||||||
if (!m) return false;
|
if (!m) return false;
|
||||||
return !allIds.has(m[1]);
|
return !allIds.has(m[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (broken.length === 0) {
|
if (broken.length === 0) {
|
||||||
console.log(`[lint-uuid-refs] OK — ${refs.length} refs scanned, none broken`);
|
console.log(`[lint-uuid-refs] OK - ${refs.length} refs scanned, none broken`);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// HbM Apply Condition — pick a status effect and toggle it on targeted/selected tokens.
|
// HbM Apply Condition - pick a status effect and toggle it on targeted/selected tokens.
|
||||||
const targets = game.user.targets.size > 0 ? Array.from(game.user.targets) : canvas.tokens.controlled;
|
const targets = game.user.targets.size > 0 ? Array.from(game.user.targets) : canvas.tokens.controlled;
|
||||||
if (targets.length === 0) return ui.notifications.warn('Zaznacz lub naceluj token(y).');
|
if (targets.length === 0) return ui.notifications.warn('Zaznacz lub naceluj token(y).');
|
||||||
const effects = CONFIG.statusEffects.filter((e) => e.id?.startsWith('hbm.'));
|
const effects = CONFIG.statusEffects.filter((e) => e.id?.startsWith('hbm.'));
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// HbM Attribute Check — pick attribute on selected token's actor, roll d6 pool.
|
// HbM Attribute Check - pick attribute on selected token's actor, roll d6 pool.
|
||||||
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
||||||
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
||||||
const attrs = ['body', 'mind', 'soul', 'magic'];
|
const attrs = ['body', 'mind', 'soul', 'magic'];
|
||||||
const labels = { body: 'Ciało', mind: 'Umysł', soul: 'Dusza', magic: 'Magia' };
|
const labels = { body: 'Ciało', mind: 'Umysł', soul: 'Dusza', magic: 'Magia' };
|
||||||
const opts = attrs.map((a) => `<option value="${a}">${labels[a]} (${actor.system.attributes[a]?.value ?? 0})</option>`).join('');
|
const opts = attrs.map((a) => `<option value="${a}">${labels[a]} (${actor.system.attributes[a]?.value ?? 0})</option>`).join('');
|
||||||
const result = await Dialog.prompt({
|
const result = await Dialog.prompt({
|
||||||
title: `Test atrybutu — ${actor.name}`,
|
title: `Test atrybutu - ${actor.name}`,
|
||||||
content: `
|
content: `
|
||||||
<form>
|
<form>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -27,4 +27,4 @@ const result = await Dialog.prompt({
|
|||||||
if (!result) return;
|
if (!result) return;
|
||||||
const pool = actor.system.attributes[result.attr]?.value ?? 1;
|
const pool = actor.system.attributes[result.attr]?.value ?? 1;
|
||||||
const roll = await new Roll(`${pool}d6cs>=${result.threshold}`).evaluate();
|
const roll = await new Roll(`${pool}d6cs>=${result.threshold}`).evaluate();
|
||||||
await roll.toMessage({ flavor: `${actor.name} — test ${labels[result.attr]} (TS ${result.threshold})`, speaker: ChatMessage.getSpeaker({ actor }) });
|
await roll.toMessage({ flavor: `${actor.name} - test ${labels[result.attr]} (TS ${result.threshold})`, speaker: ChatMessage.getSpeaker({ actor }) });
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// HbM Group Cast Helper — sum mana of controlled token actors (potential co-casters).
|
// HbM Group Cast Helper - sum mana of controlled token actors (potential co-casters).
|
||||||
const tokens = canvas.tokens.controlled;
|
const tokens = canvas.tokens.controlled;
|
||||||
if (tokens.length < 2) return ui.notifications.warn('Zaznacz co najmniej 2 tokeny współrzucających.');
|
if (tokens.length < 2) return ui.notifications.warn('Zaznacz co najmniej 2 tokeny współrzucających.');
|
||||||
const lines = [];
|
const lines = [];
|
||||||
@@ -11,7 +11,7 @@ for (const t of tokens) {
|
|||||||
const magic = a.system.attributes?.magic?.value ?? 0;
|
const magic = a.system.attributes?.magic?.value ?? 0;
|
||||||
totalMana += mana;
|
totalMana += mana;
|
||||||
totalPool += magic;
|
totalPool += magic;
|
||||||
lines.push(`<li><strong>${a.name}</strong> — Mana ${mana}, Magia ${magic}</li>`);
|
lines.push(`<li><strong>${a.name}</strong> - Mana ${mana}, Magia ${magic}</li>`);
|
||||||
}
|
}
|
||||||
const html = `
|
const html = `
|
||||||
<div class="hbm-group-cast">
|
<div class="hbm-group-cast">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// HbM Long Rest — invoke game.hbm.rest(actor, 'long') on selected token's actor.
|
// HbM Long Rest - invoke game.hbm.rest(actor, 'long') on selected token's actor.
|
||||||
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
||||||
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
||||||
if (!game.hbm?.rest) return ui.notifications.error('Brak game.hbm.rest — system niezainicjowany.');
|
if (!game.hbm?.rest) return ui.notifications.error('Brak game.hbm.rest - system niezainicjowany.');
|
||||||
const result = await game.hbm.rest(actor, 'long');
|
const result = await game.hbm.rest(actor, 'long');
|
||||||
ui.notifications.info(`${actor.name}: długi odpoczynek — pełna regeneracja zasobów.`);
|
ui.notifications.info(`${actor.name}: długi odpoczynek - pełna regeneracja zasobów.`);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// HbM Pool Roll — prompts for pool size and threshold, then rolls a d6 pool.
|
// HbM Pool Roll - prompts for pool size and threshold, then rolls a d6 pool.
|
||||||
const result = await Dialog.prompt({
|
const result = await Dialog.prompt({
|
||||||
title: 'Rzut puli d6',
|
title: 'Rzut puli d6',
|
||||||
content: `
|
content: `
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// HbM Refill Zeal — bumps zeal by 1 on selected token's actor (debug helper).
|
// HbM Refill Zeal - bumps zeal by 1 on selected token's actor (debug helper).
|
||||||
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
||||||
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
||||||
const cur = actor.system.attributes?.zeal?.value ?? 0;
|
const cur = actor.system.attributes?.zeal?.value ?? 0;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// HbM Short Rest — invoke game.hbm.rest(actor, 'short') on selected token's actor.
|
// HbM Short Rest - invoke game.hbm.rest(actor, 'short') on selected token's actor.
|
||||||
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
||||||
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
||||||
if (!game.hbm?.rest) return ui.notifications.error('Brak game.hbm.rest — system niezainicjowany.');
|
if (!game.hbm?.rest) return ui.notifications.error('Brak game.hbm.rest - system niezainicjowany.');
|
||||||
const result = await game.hbm.rest(actor, 'short');
|
const result = await game.hbm.rest(actor, 'short');
|
||||||
ui.notifications.info(`${actor.name}: krótki odpoczynek — przywrócono ${result?.healed ?? 0} PW.`);
|
ui.notifications.info(`${actor.name}: krótki odpoczynek - przywrócono ${result?.hpRestored ?? 0} PW.`);
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// HbM Take a Breather - invoke game.hbm.rest(actor, 'breather') on selected token's actor.
|
||||||
|
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
||||||
|
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
||||||
|
if (!game.hbm?.rest) return ui.notifications.error('Brak game.hbm.rest - system niezainicjowany.');
|
||||||
|
const result = await game.hbm.rest(actor, 'breather');
|
||||||
|
ui.notifications.info(`${actor.name}: chwila wytchnienia - przywrócono ${result?.hpRestored ?? 0} PW i ${result?.manaRestored ?? 0} Many.`);
|
||||||
@@ -180,5 +180,6 @@
|
|||||||
"echo-zmierzchu": "twilight-echo",
|
"echo-zmierzchu": "twilight-echo",
|
||||||
"requiem": "requiem",
|
"requiem": "requiem",
|
||||||
"przywolanie-istoty-z-otchlani": "summon-eldritch-entity",
|
"przywolanie-istoty-z-otchlani": "summon-eldritch-entity",
|
||||||
"nic-przeznaczenia": "thread-of-destiny"
|
"nic-przeznaczenia": "thread-of-destiny",
|
||||||
|
"naturalny-lot": "natural-flight"
|
||||||
}
|
}
|
||||||
@@ -3,20 +3,20 @@
|
|||||||
"schools": {
|
"schools": {
|
||||||
"magia ogólna": "general",
|
"magia ogólna": "general",
|
||||||
"magia żywiołów": "elements",
|
"magia żywiołów": "elements",
|
||||||
"magia żywiołów — powietrze": "elementsAir",
|
"magia żywiołów - powietrze": "elementsAir",
|
||||||
"magia żywiołów (powietrze)": "elementsAir",
|
"magia żywiołów (powietrze)": "elementsAir",
|
||||||
"magia powietrza": "elementsAir",
|
"magia powietrza": "elementsAir",
|
||||||
"magia żywiołów — woda": "elementsWater",
|
"magia żywiołów - woda": "elementsWater",
|
||||||
"magia żywiołów (woda)": "elementsWater",
|
"magia żywiołów (woda)": "elementsWater",
|
||||||
"magia wody": "elementsWater",
|
"magia wody": "elementsWater",
|
||||||
"magia żywiołów — ogień": "elementsFire",
|
"magia żywiołów - ogień": "elementsFire",
|
||||||
"magia żywiołów (ogień)": "elementsFire",
|
"magia żywiołów (ogień)": "elementsFire",
|
||||||
"magia ognia": "elementsFire",
|
"magia ognia": "elementsFire",
|
||||||
"magia żywiołów — ziemia": "elementsEarth",
|
"magia żywiołów - ziemia": "elementsEarth",
|
||||||
"magia żywiołów (ziemia)": "elementsEarth",
|
"magia żywiołów (ziemia)": "elementsEarth",
|
||||||
"magia ziemi": "elementsEarth",
|
"magia ziemi": "elementsEarth",
|
||||||
"magia sakralna": "sacred",
|
"magia sakralna": "sacred",
|
||||||
"magia sakralna — egzorcyzmy": "sacredExorcism",
|
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
||||||
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
||||||
"egzorcyzmy": "sacredExorcism",
|
"egzorcyzmy": "sacredExorcism",
|
||||||
"wiedźmia magia": "witch",
|
"wiedźmia magia": "witch",
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
"nekromancja": "necromancy",
|
"nekromancja": "necromancy",
|
||||||
"magia krwi": "blood",
|
"magia krwi": "blood",
|
||||||
"magia szkarłatu": "crimson",
|
"magia szkarłatu": "crimson",
|
||||||
"magia otchłani — magia aspektów": "eldritchAspects",
|
"magia otchłani - magia aspektów": "eldritchAspects",
|
||||||
"magia otchłani — pierwotna magia": "eldritchPrimal",
|
"magia otchłani - pierwotna magia": "eldritchPrimal",
|
||||||
"dzika wiedźmia magia": "wildWitch"
|
"dzika wiedźmia magia": "wildWitch"
|
||||||
},
|
},
|
||||||
"disciplines": {
|
"disciplines": {
|
||||||
@@ -46,13 +46,13 @@
|
|||||||
"warzenie eliksirów": "alchemyBrewing",
|
"warzenie eliksirów": "alchemyBrewing",
|
||||||
"alchemia - warzenie eliksirów": "alchemyBrewing",
|
"alchemia - warzenie eliksirów": "alchemyBrewing",
|
||||||
"botanika": "botany",
|
"botanika": "botany",
|
||||||
"magia żywiołów — powietrze": "elementsAir",
|
"magia żywiołów - powietrze": "elementsAir",
|
||||||
"magia żywiołów (powietrze)": "elementsAir",
|
"magia żywiołów (powietrze)": "elementsAir",
|
||||||
"magia żywiołów — woda": "elementsWater",
|
"magia żywiołów - woda": "elementsWater",
|
||||||
"magia żywiołów (woda)": "elementsWater",
|
"magia żywiołów (woda)": "elementsWater",
|
||||||
"magia żywiołów — ogień": "elementsFire",
|
"magia żywiołów - ogień": "elementsFire",
|
||||||
"magia żywiołów (ogień)": "elementsFire",
|
"magia żywiołów (ogień)": "elementsFire",
|
||||||
"magia żywiołów — ziemia": "elementsEarth",
|
"magia żywiołów - ziemia": "elementsEarth",
|
||||||
"magia żywiołów (ziemia)": "elementsEarth",
|
"magia żywiołów (ziemia)": "elementsEarth",
|
||||||
"rzemiosło artefaktów": "artifice",
|
"rzemiosło artefaktów": "artifice",
|
||||||
"golemancja": "golemancy",
|
"golemancja": "golemancy",
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
"źródło mocy": "manaSourceMage",
|
"źródło mocy": "manaSourceMage",
|
||||||
"magia iluzji": "illusion",
|
"magia iluzji": "illusion",
|
||||||
"magia sakralna": "sacred",
|
"magia sakralna": "sacred",
|
||||||
"magia sakralna — egzorcyzmy": "sacredExorcism",
|
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
||||||
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
||||||
"egzorcyzmy": "sacredExorcism",
|
"egzorcyzmy": "sacredExorcism",
|
||||||
"wiedźmia magia": "witch",
|
"wiedźmia magia": "witch",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
export interface BookBlock {
|
export interface BookBlock {
|
||||||
/** The line directly preceding the bullet block — usually the item name. */
|
/** The line directly preceding the bullet block - usually the item name. */
|
||||||
name: string;
|
name: string;
|
||||||
/** Bullet keys/values, preserved in order. */
|
/** Bullet keys/values, preserved in order. */
|
||||||
fields: Array<{ key: string; value: string }>;
|
fields: Array<{ key: string; value: string }>;
|
||||||
@@ -45,7 +45,7 @@ const SEPARATOR = /^_{3,}$/;
|
|||||||
const BULLET = /^\*\s+([^:]+):\s*(.+)$/;
|
const BULLET = /^\*\s+([^:]+):\s*(.+)$/;
|
||||||
// Match any markdown heading level (1-6 `#` chars).
|
// Match any markdown heading level (1-6 `#` chars).
|
||||||
const HEADING = /^#{1,6}\s+(.+)$/;
|
const HEADING = /^#{1,6}\s+(.+)$/;
|
||||||
// Chapter headings are the two highest heading levels we encounter — heuristic:
|
// Chapter headings are the two highest heading levels we encounter - heuristic:
|
||||||
// treat ## headings as "chapter" and ### headings as "section".
|
// treat ## headings as "chapter" and ### headings as "section".
|
||||||
const CHAPTER_HEADING = /^#{1,2}\s+(.+)$/;
|
const CHAPTER_HEADING = /^#{1,2}\s+(.+)$/;
|
||||||
const SECTION_HEADING = /^#{3,4}\s+(.+)$/;
|
const SECTION_HEADING = /^#{3,4}\s+(.+)$/;
|
||||||
@@ -65,7 +65,7 @@ function skipFrontmatter(lines: string[]): number {
|
|||||||
for (let i = 1; i < lines.length; i++) {
|
for (let i = 1; i < lines.length; i++) {
|
||||||
if (FRONTMATTER_FENCE.test(lines[i].trim())) return i + 1;
|
if (FRONTMATTER_FENCE.test(lines[i].trim())) return i + 1;
|
||||||
}
|
}
|
||||||
return 0; // malformed — start from 0
|
return 0; // malformed - start from 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WalkOptions {
|
export interface WalkOptions {
|
||||||
@@ -92,7 +92,7 @@ export function walkBook(path: string, opts: WalkOptions = {}): BookBlock[] {
|
|||||||
for (let i = startIdx; i < lines.length; i++) {
|
for (let i = startIdx; i < lines.length; i++) {
|
||||||
const rawLine = lines[i].trimEnd();
|
const rawLine = lines[i].trimEnd();
|
||||||
|
|
||||||
// Skip Obsidian TOC bullets — they look like `- [[#Section|Label]]`.
|
// Skip Obsidian TOC bullets - they look like `- [[#Section|Label]]`.
|
||||||
if (OBSIDIAN_TOC_BULLET.test(rawLine.trim())) continue;
|
if (OBSIDIAN_TOC_BULLET.test(rawLine.trim())) continue;
|
||||||
|
|
||||||
// Chapter headings: ## or # level (the two highest we honour).
|
// Chapter headings: ## or # level (the two highest we honour).
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* with name, source book, and an empty description (filled in later).
|
* with name, source book, and an empty description (filled in later).
|
||||||
*
|
*
|
||||||
* Many disciplines also have a "passive ability" mentioned right after the
|
* Many disciplines also have a "passive ability" mentioned right after the
|
||||||
* discipline header in Podręcznik Gry — we capture the next non-empty line
|
* discipline header in Podręcznik Gry - we capture the next non-empty line
|
||||||
* if it looks like a short ability description.
|
* if it looks like a short ability description.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -34,13 +34,13 @@ const DISCIPLINE_SEEDS: DisciplineSeed[] = [
|
|||||||
{ id: 'manaSourceMage', polishName: 'Źródło Mocy', pack: 'disciplines', book: 'podrecznik-gry' },
|
{ id: 'manaSourceMage', polishName: 'Źródło Mocy', pack: 'disciplines', book: 'podrecznik-gry' },
|
||||||
{ id: 'illusion', polishName: 'Magia Iluzji', pack: 'disciplines', book: 'ksiega-magii' },
|
{ id: 'illusion', polishName: 'Magia Iluzji', pack: 'disciplines', book: 'ksiega-magii' },
|
||||||
{ id: 'sacred', polishName: 'Magia Sakralna', pack: 'disciplines', book: 'ksiega-magii' },
|
{ id: 'sacred', polishName: 'Magia Sakralna', pack: 'disciplines', book: 'ksiega-magii' },
|
||||||
{ id: 'sacredExorcism', polishName: 'Magia Sakralna — Egzorcyzmy', pack: 'disciplines', book: 'ksiega-magii' },
|
{ id: 'sacredExorcism', polishName: 'Magia Sakralna - Egzorcyzmy', pack: 'disciplines', book: 'ksiega-magii' },
|
||||||
{ id: 'witch', polishName: 'Wiedźmia Magia', pack: 'disciplines', book: 'ksiega-magii' },
|
{ id: 'witch', polishName: 'Wiedźmia Magia', pack: 'disciplines', book: 'ksiega-magii' },
|
||||||
{ id: 'necromancy', polishName: 'Nekromancja', pack: 'disciplines', book: 'ksiega-magii' },
|
{ id: 'necromancy', polishName: 'Nekromancja', pack: 'disciplines', book: 'ksiega-magii' },
|
||||||
{ id: 'blood', polishName: 'Magia Krwi', pack: 'disciplines-forbidden', book: 'arcanum-sanguinis' },
|
{ id: 'blood', polishName: 'Magia Krwi', pack: 'disciplines-forbidden', book: 'arcanum-sanguinis' },
|
||||||
{ id: 'crimson', polishName: 'Magia Szkarłatu', pack: 'disciplines-forbidden', book: 'crimson-cult' },
|
{ id: 'crimson', polishName: 'Magia Szkarłatu', pack: 'disciplines-forbidden', book: 'crimson-cult' },
|
||||||
{ id: 'abyssAspects', polishName: 'Magia Otchłani — Magia Aspektów', pack: 'disciplines-forbidden', book: 'klatwa-otchlani' },
|
{ id: 'abyssAspects', polishName: 'Magia Otchłani - Magia Aspektów', pack: 'disciplines-forbidden', book: 'klatwa-otchlani' },
|
||||||
{ id: 'abyssPrimal', polishName: 'Magia Otchłani — Pierwotna Magia', pack: 'disciplines-forbidden', book: 'klatwa-otchlani' },
|
{ id: 'abyssPrimal', polishName: 'Magia Otchłani - Pierwotna Magia', pack: 'disciplines-forbidden', book: 'klatwa-otchlani' },
|
||||||
{ id: 'wildWitch', polishName: 'Dzika Wiedźmia Magia', pack: 'disciplines-forbidden', book: 'klatwa-otchlani' },
|
{ id: 'wildWitch', polishName: 'Dzika Wiedźmia Magia', pack: 'disciplines-forbidden', book: 'klatwa-otchlani' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ function lookupDescription(repoRoot: string, polishName: string): string {
|
|||||||
resolve(repoRoot, 'ObsidianNotes/rules/02. Klątwa Otchłani.md'),
|
resolve(repoRoot, 'ObsidianNotes/rules/02. Klątwa Otchłani.md'),
|
||||||
resolve(repoRoot, 'ObsidianNotes/rules/00. Podręcznik Gry.md'),
|
resolve(repoRoot, 'ObsidianNotes/rules/00. Podręcznik Gry.md'),
|
||||||
];
|
];
|
||||||
const target = normalizeKey(polishName.split('—')[0]);
|
const target = normalizeKey(polishName.split('-')[0]);
|
||||||
|
|
||||||
for (const file of splitSources) {
|
for (const file of splitSources) {
|
||||||
let text = '';
|
let text = '';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Macro parser. Reads hand-coded macro source files from `scripts/macros/*.js`
|
* Macro parser. Reads hand-coded macro source files from `scripts/macros/*.js`
|
||||||
* and emits ParsedDoc[] for the build pipeline. Macros are not parsed from
|
* and emits ParsedDoc[] for the build pipeline. Macros are not parsed from
|
||||||
* books — they're a curated set of GM/player utilities.
|
* books - they're a curated set of GM/player utilities.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
@@ -23,6 +23,7 @@ const MACROS: MacroDef[] = [
|
|||||||
{ id: 'pool-roll', name: 'Rzut Puli d6', file: 'pool-roll.js', img: 'icons/svg/d20.svg' },
|
{ id: 'pool-roll', name: 'Rzut Puli d6', file: 'pool-roll.js', img: 'icons/svg/d20.svg' },
|
||||||
{ id: 'attribute-check', name: 'Test Atrybutu', file: 'attribute-check.js', img: 'icons/svg/dice-target.svg' },
|
{ id: 'attribute-check', name: 'Test Atrybutu', file: 'attribute-check.js', img: 'icons/svg/dice-target.svg' },
|
||||||
{ id: 'apply-condition', name: 'Nałóż Przypadłość', file: 'apply-condition.js', img: 'icons/svg/aura.svg' },
|
{ id: 'apply-condition', name: 'Nałóż Przypadłość', file: 'apply-condition.js', img: 'icons/svg/aura.svg' },
|
||||||
|
{ id: 'take-breather', name: 'Chwila Wytchnienia', file: 'take-breather.js', img: 'icons/svg/clockwork.svg' },
|
||||||
{ id: 'short-rest', name: 'Krótki Odpoczynek', file: 'short-rest.js', img: 'icons/svg/regen.svg' },
|
{ id: 'short-rest', name: 'Krótki Odpoczynek', file: 'short-rest.js', img: 'icons/svg/regen.svg' },
|
||||||
{ id: 'long-rest', name: 'Długi Odpoczynek', file: 'long-rest.js', img: 'icons/svg/sun.svg' },
|
{ id: 'long-rest', name: 'Długi Odpoczynek', file: 'long-rest.js', img: 'icons/svg/sun.svg' },
|
||||||
{ id: 'group-cast-helper', name: 'Pomocnik Rzucania Grupowego', file: 'group-cast-helper.js', img: 'icons/svg/upgrade.svg' },
|
{ id: 'group-cast-helper', name: 'Pomocnik Rzucania Grupowego', file: 'group-cast-helper.js', img: 'icons/svg/upgrade.svg' },
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { ParsedDoc, ParserContext, ParserFn, SourceBookId } from './types';
|
|||||||
import { slugify } from './helpers';
|
import { slugify } from './helpers';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
|
|
||||||
/** Looser zod schema — mirrors `SpellData` defaults; build pipeline converts to system payload. */
|
/** Looser zod schema - mirrors `SpellData` defaults; build pipeline converts to system payload. */
|
||||||
const SpellSystemSchema = z.object({
|
const SpellSystemSchema = z.object({
|
||||||
castingMode: z.enum(['standard', 'sacred', 'witch', 'blood']),
|
castingMode: z.enum(['standard', 'sacred', 'witch', 'blood']),
|
||||||
school: z.string(),
|
school: z.string(),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* - Podręcznik Gry, "Rozdział IV - Talenty"
|
* - Podręcznik Gry, "Rozdział IV - Talenty"
|
||||||
* - Bestiariusz, "Rozdział III - Talenty" (NPC-only talents)
|
* - Bestiariusz, "Rozdział III - Talenty" (NPC-only talents)
|
||||||
*
|
*
|
||||||
* The walker can't help us here — we segment by `________________`
|
* The walker can't help us here - we segment by `________________`
|
||||||
* separators within the talent chapter and look for a "Wymagania:" line.
|
* separators within the talent chapter and look for a "Wymagania:" line.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ const TALENT_SOURCES: TalentSource[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Talent chapters are noisy — split into segments by separator lines, then
|
* Talent chapters are noisy - split into segments by separator lines, then
|
||||||
* within each segment find consecutive talent blocks. A talent block is:
|
* within each segment find consecutive talent blocks. A talent block is:
|
||||||
* - first non-empty line = name (short, no trailing punctuation, no colon)
|
* - first non-empty line = name (short, no trailing punctuation, no colon)
|
||||||
* - optional `Wymagania: ...` line
|
* - optional `Wymagania: ...` line
|
||||||
@@ -109,7 +109,7 @@ export const parseTalents: ParserFn = async (ctx: ParserContext): Promise<Parsed
|
|||||||
if (i >= slice.length) break;
|
if (i >= slice.length) break;
|
||||||
|
|
||||||
const rawLine = slice[i].trim();
|
const rawLine = slice[i].trim();
|
||||||
// Stop heuristic — bail out of obvious chapter changes.
|
// Stop heuristic - bail out of obvious chapter changes.
|
||||||
if (/^Rozdział\b/.test(rawLine) || /^Aneks\b/.test(rawLine) || /^#{1,6}\s+(?:Rozdział|Aneks)\b/i.test(rawLine)) break;
|
if (/^Rozdział\b/.test(rawLine) || /^Aneks\b/.test(rawLine) || /^#{1,6}\s+(?:Rozdział|Aneks)\b/i.test(rawLine)) break;
|
||||||
|
|
||||||
let isName = false;
|
let isName = false;
|
||||||
@@ -127,7 +127,7 @@ export const parseTalents: ParserFn = async (ctx: ParserContext): Promise<Parsed
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isName) {
|
if (!isName) {
|
||||||
// Can't read this — skip the line and continue.
|
// Can't read this - skip the line and continue.
|
||||||
i++;
|
i++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -205,9 +205,104 @@ export const parseTalents: ParserFn = async (ctx: ParserContext): Promise<Parsed
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse discipline-specific talents from Wybór Dziedziny Magii
|
||||||
|
docs.push(...(await parseDisciplineTalents(ctx)));
|
||||||
|
|
||||||
return docs;
|
return docs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function parseDisciplineTalents(ctx: ParserContext): Promise<ParsedDoc[]> {
|
||||||
|
const docs: ParsedDoc[] = [];
|
||||||
|
const path = resolve(ctx.repoRoot, 'ObsidianNotes/rules/00. Podr\u0119cznik Gry.md');
|
||||||
|
let lines: string[] = [];
|
||||||
|
try {
|
||||||
|
lines = readFileSync(path, 'utf8').split(/\r?\n/);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIdx = lines.findIndex((l) => /^\s*Wyb[o\u00f3\u0143A3\ufffd]r Dziedziny Magii\s*$/i.test(l));
|
||||||
|
if (startIdx < 0) return [];
|
||||||
|
const endIdx = lines.findIndex((l, i) => i > startIdx && /^\s*Umiej[e\u0119\u0118\ufffd]tno[s\u015b\u015a\ufffd]ci i Talenty\s*$/i.test(l));
|
||||||
|
const slice = lines.slice(startIdx + 1, endIdx > 0 ? endIdx : lines.length);
|
||||||
|
|
||||||
|
const talentNames = [
|
||||||
|
'Podejrzliwo\u015b\u0107 Wobec Zmian',
|
||||||
|
'Nadzwyczajna Odporno\u015b\u0107',
|
||||||
|
'Magiczna Flora',
|
||||||
|
'Mi\u0142osierdzie',
|
||||||
|
'B\u0142ogos\u0142awieni Egzorcy\u015bci',
|
||||||
|
'Jedno\u015b\u0107 z \u017bywio\u0142em',
|
||||||
|
'Tajemna Technologia',
|
||||||
|
'Pierwotna Energia',
|
||||||
|
'Niezwyk\u0142a Intuicja',
|
||||||
|
'Arytmetyczna Koncentracja'
|
||||||
|
];
|
||||||
|
|
||||||
|
const cleanNames = talentNames.map(n => slugify(n));
|
||||||
|
|
||||||
|
for (let i = 0; i < slice.length; i++) {
|
||||||
|
const rawLine = slice[i].trim();
|
||||||
|
if (!rawLine) continue;
|
||||||
|
|
||||||
|
const slug = slugify(rawLine);
|
||||||
|
const talentIndex = cleanNames.indexOf(slug);
|
||||||
|
|
||||||
|
if (talentIndex !== -1) {
|
||||||
|
const name = talentNames[talentIndex];
|
||||||
|
const descLines: string[] = [];
|
||||||
|
let j = i + 1;
|
||||||
|
while (j < slice.length) {
|
||||||
|
const nextLine = slice[j].trim();
|
||||||
|
if (SEPARATOR.test(nextLine) || /^Rozdzia/i.test(nextLine) || /^___/.test(nextLine)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const nextSlug = slugify(nextLine);
|
||||||
|
if (cleanNames.includes(nextSlug)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Stop if we hit a main section like Alchemia, Botanika, Magia Sakralna etc.
|
||||||
|
const disciplineHeaders = [
|
||||||
|
'Alchemia', 'Transmutacja', 'Warzenie Eliksirów', 'Botanika',
|
||||||
|
'Magia Sakralna', 'Egzorcyzmy', 'Magia Żywiołów', 'Rzemiosło Artefaktów',
|
||||||
|
'Źródło Mocy', 'Magia Iluzji', 'Wiedźmia Magia'
|
||||||
|
];
|
||||||
|
if (disciplineHeaders.some(h => slugify(h) === nextSlug)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
descLines.push(slice[j]);
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
const description = descLines.join('\n').trim();
|
||||||
|
const baseSlug = slugify(name);
|
||||||
|
const id = ctx.idOverrides[baseSlug] ?? baseSlug;
|
||||||
|
|
||||||
|
docs.push({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
documentType: 'Item',
|
||||||
|
subType: 'talent',
|
||||||
|
pack: 'talents',
|
||||||
|
source: { book: 'podrecznik-gry', chapter: 'Wyb\u00f3r Dziedziny Magii', line: startIdx + i + 2 },
|
||||||
|
system: {
|
||||||
|
requirements: { race: '', attribute: '', skill: '', talent: '', title: '', discipline: '' },
|
||||||
|
multiSelect: false,
|
||||||
|
cost: '',
|
||||||
|
damageReductionBonus: 0,
|
||||||
|
description,
|
||||||
|
effect: '',
|
||||||
|
},
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
|
||||||
|
i = j - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
/** A talent name is short, doesn't end with `.`, no `:`, not a bullet, no digits-only. */
|
/** A talent name is short, doesn't end with `.`, no `:`, not a bullet, no digits-only. */
|
||||||
function isPlausibleName(line: string): boolean {
|
function isPlausibleName(line: string): boolean {
|
||||||
if (!line || line.length > 80) return false;
|
if (!line || line.length > 80) return false;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Shared parser types — what each parser emits before being handed off
|
* Shared parser types - what each parser emits before being handed off
|
||||||
* to the pack builder.
|
* to the pack builder.
|
||||||
*
|
*
|
||||||
* Every parsed document has a stable id (slug) and a sourceBook ref so
|
* Every parsed document has a stable id (slug) and a sourceBook ref so
|
||||||
@@ -21,7 +21,7 @@ export interface ParsedDoc {
|
|||||||
id: string;
|
id: string;
|
||||||
/** Display name (Polish, as in book). */
|
/** Display name (Polish, as in book). */
|
||||||
name: string;
|
name: string;
|
||||||
/** Foundry document type — `Item` or `Actor`. */
|
/** Foundry document type - `Item` or `Actor`. */
|
||||||
documentType: 'Item' | 'Actor';
|
documentType: 'Item' | 'Actor';
|
||||||
/** Foundry sub-type for the data model (e.g. `spell`, `talent`). */
|
/** Foundry sub-type for the data model (e.g. `spell`, `talent`). */
|
||||||
subType: string;
|
subType: string;
|
||||||
|
|||||||
+2
-1
@@ -133,6 +133,7 @@ export const SPELL_SCHOOLS = [
|
|||||||
'illusion', 'sacred', 'sacredExorcism', 'witch', 'necromancy',
|
'illusion', 'sacred', 'sacredExorcism', 'witch', 'necromancy',
|
||||||
// Forbidden / extra-academic
|
// Forbidden / extra-academic
|
||||||
'blood', 'crimson', 'abyssAspects', 'abyssPrimal', 'wildWitch',
|
'blood', 'crimson', 'abyssAspects', 'abyssPrimal', 'wildWitch',
|
||||||
|
'eldritch',
|
||||||
] as const;
|
] as const;
|
||||||
export type SpellSchool = (typeof SPELL_SCHOOLS)[number];
|
export type SpellSchool = (typeof SPELL_SCHOOLS)[number];
|
||||||
|
|
||||||
@@ -144,7 +145,7 @@ export const SACRED_DEITIES = [
|
|||||||
export type SacredDeity = (typeof SACRED_DEITIES)[number];
|
export type SacredDeity = (typeof SACRED_DEITIES)[number];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Witch magic symbols. EXTENSIBLE — additional content may add more.
|
* Witch magic symbols. EXTENSIBLE - additional content may add more.
|
||||||
* Used for autocomplete only; spell.components.symbols accepts arbitrary strings.
|
* Used for autocomplete only; spell.components.symbols accepts arbitrary strings.
|
||||||
*/
|
*/
|
||||||
export const WITCH_SYMBOLS: readonly string[] = Object.freeze([
|
export const WITCH_SYMBOLS: readonly string[] = Object.freeze([
|
||||||
|
|||||||
+16
-23
@@ -15,7 +15,7 @@ import { ATTRIBUTES, SKILL_KEYS, SKILLS, AttributeKey, getMagicPowerEntry } from
|
|||||||
* - attributes.health.max = 3 * (body + mind + soul)
|
* - attributes.health.max = 3 * (body + mind + soul)
|
||||||
* - attributes.zeal.max = ceil(soul / 2)
|
* - attributes.zeal.max = ceil(soul / 2)
|
||||||
* - attributes.initiative = mind + skills.reflex.value + skills.perception.value
|
* - attributes.initiative = mind + skills.reflex.value + skills.perception.value
|
||||||
* - attributes.mana.max / .maxPerSpell / magic.dicePool — from MAGIC_POWER_TABLE
|
* - attributes.mana.max / .maxPerSpell / magic.dicePool - from MAGIC_POWER_TABLE
|
||||||
*/
|
*/
|
||||||
export class CharacterData extends foundry.abstract.TypeDataModel {
|
export class CharacterData extends foundry.abstract.TypeDataModel {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
@@ -66,10 +66,10 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
|||||||
maxPerSpell: makeIntField(0, { min: 0 }),
|
maxPerSpell: makeIntField(0, { min: 0 }),
|
||||||
}),
|
}),
|
||||||
zeal: makeValueMaxField(0, 0),
|
zeal: makeValueMaxField(0, 0),
|
||||||
// Blood Pool (Arcanum Sanguinis Ch. III) — experimental until full spell list ships.
|
// Blood Pool (Arcanum Sanguinis Ch. III) - experimental until full spell list ships.
|
||||||
// Placeholder formula: max = body + soul.
|
// Placeholder formula: max = body + soul.
|
||||||
blood: makeValueMaxField(0, 0),
|
blood: makeValueMaxField(0, 0),
|
||||||
// Elixir tolerance (Podręcznik Gry — brewing). Each potion consumed adds 1;
|
// Elixir tolerance (Podręcznik Gry - brewing). Each potion consumed adds 1;
|
||||||
// recovers 1 per long rest. Soft cap = body + 1; over cap → poisoning.
|
// recovers 1 per long rest. Soft cap = body + 1; over cap → poisoning.
|
||||||
elixirTolerance: makeIntField(0, { min: 0 }),
|
elixirTolerance: makeIntField(0, { min: 0 }),
|
||||||
// Insanity points (Klątwa Otchłani VIII). Each Abyss exposure may add 1; rolls
|
// Insanity points (Klątwa Otchłani VIII). Each Abyss exposure may add 1; rolls
|
||||||
@@ -90,7 +90,10 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
|||||||
condition: makeIntField(0, { min: 0 }), // derived: sum of armor conditions
|
condition: makeIntField(0, { min: 0 }), // derived: sum of armor conditions
|
||||||
conditionMax: makeIntField(0, { min: 0 }), // derived: sum of armor conditionMax
|
conditionMax: makeIntField(0, { min: 0 }), // derived: sum of armor conditionMax
|
||||||
}),
|
}),
|
||||||
initiative: makeIntField(0, { min: 0 }),
|
initiative: new f.SchemaField({
|
||||||
|
value: makeIntField(0, { min: 0 }),
|
||||||
|
bonus: makeIntField(0, { min: 0 }),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
skills: new f.SchemaField(skillEntries),
|
skills: new f.SchemaField(skillEntries),
|
||||||
details: new f.SchemaField({
|
details: new f.SchemaField({
|
||||||
@@ -101,7 +104,7 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
|||||||
discipline: makeStringField('', { blank: true }),
|
discipline: makeStringField('', { blank: true }),
|
||||||
title: makeStringField('', { blank: true }),
|
title: makeStringField('', { blank: true }),
|
||||||
biography: makeHtmlField(''),
|
biography: makeHtmlField(''),
|
||||||
customEquipment: makeHtmlField(''), // legacy textarea — kept for migration
|
customEquipment: makeHtmlField(''), // legacy textarea - kept for migration
|
||||||
customItems: makeArrayField(new f.SchemaField({
|
customItems: makeArrayField(new f.SchemaField({
|
||||||
name: makeStringField('', { blank: true }),
|
name: makeStringField('', { blank: true }),
|
||||||
qty: makeIntField(1, { min: 0 }),
|
qty: makeIntField(1, { min: 0 }),
|
||||||
@@ -144,7 +147,7 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
|||||||
magicalArmor: { value: number; max: number; runicCounter: number };
|
magicalArmor: { value: number; max: number; runicCounter: number };
|
||||||
magicalShield: { value: number };
|
magicalShield: { value: number };
|
||||||
physicalArmor: { value: number; max: number; condition: number; conditionMax: number };
|
physicalArmor: { value: number; max: number; condition: number; conditionMax: number };
|
||||||
initiative: number;
|
initiative: { value: number; bonus: number };
|
||||||
};
|
};
|
||||||
skills: Record<string, { value: number; defaultAttribute: AttributeKey }>;
|
skills: Record<string, { value: number; defaultAttribute: AttributeKey }>;
|
||||||
details: { raceId?: string; classIds?: string[] };
|
details: { raceId?: string; classIds?: string[] };
|
||||||
@@ -214,11 +217,12 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.initiative =
|
a.initiative.value =
|
||||||
a.mind.value +
|
a.mind.value +
|
||||||
(sys.skills.reflex?.value ?? 0) +
|
(sys.skills.reflex?.value ?? 0) +
|
||||||
(sys.skills.perception?.value ?? 0) +
|
(sys.skills.perception?.value ?? 0) +
|
||||||
initiativeBonus;
|
initiativeBonus +
|
||||||
|
(a.initiative.bonus ?? 0);
|
||||||
|
|
||||||
// Clamp magical armor / shield
|
// Clamp magical armor / shield
|
||||||
if (a.magicalArmor.value > a.magicalArmor.max) a.magicalArmor.value = a.magicalArmor.max;
|
if (a.magicalArmor.value > a.magicalArmor.max) a.magicalArmor.value = a.magicalArmor.max;
|
||||||
@@ -268,21 +272,6 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
|||||||
// Blood Pool (placeholder formula): max = body + soul.
|
// Blood Pool (placeholder formula): max = body + soul.
|
||||||
a.blood.max = a.body.value + a.soul.value;
|
a.blood.max = a.body.value + a.soul.value;
|
||||||
if (a.blood.value > a.blood.max) a.blood.value = a.blood.max;
|
if (a.blood.value > a.blood.max) a.blood.value = a.blood.max;
|
||||||
|
|
||||||
// Advancement points (UUID-based race/class lookup; falls back to 0 if not set)
|
|
||||||
const raceItem = items.find((i) => i.type === 'race');
|
|
||||||
const classItems = items.filter((i) => i.type === 'class');
|
|
||||||
const startingAttr = Number(raceItem?.system?.attributePoints ?? 0);
|
|
||||||
const startingSkill = Number(raceItem?.system?.skillPoints ?? 0);
|
|
||||||
const startingFreeTalents = raceItem?.system?.freeTalents?.length ?? 0;
|
|
||||||
const classAttr = classItems.reduce((sum, c) => sum + Number(c.system?.attributePoints ?? 0), 0);
|
|
||||||
const classSkill = classItems.reduce((sum, c) => sum + Number(c.system?.skillPoints ?? 0), 0);
|
|
||||||
const spentAttr = (a.body.value - 1) + (a.mind.value - 1) + (a.soul.value - 1) + a.magic.value;
|
|
||||||
const spentSkill = Object.values(sys.skills).reduce((sum, s) => sum + (s.value ?? 0), 0);
|
|
||||||
const spentFreeTalents = items.filter((i) => i.type === 'talent').length;
|
|
||||||
sys.advancement.attributePointsAvailable = Math.max(0, startingAttr + classAttr + (sys.advancement.bonusAttributePoints ?? 0) - spentAttr);
|
|
||||||
sys.advancement.skillPointsAvailable = Math.max(0, startingSkill + classSkill + (sys.advancement.bonusSkillPoints ?? 0) - spentSkill);
|
|
||||||
sys.advancement.freeTalentsAvailable = Math.max(0, startingFreeTalents + (sys.advancement.bonusFreeTalents ?? 0) - spentFreeTalents);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -298,6 +287,10 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
|||||||
}
|
}
|
||||||
delete initialized.attributes.armor;
|
delete initialized.attributes.armor;
|
||||||
}
|
}
|
||||||
|
if (initialized?.attributes && typeof initialized.attributes.initiative === 'number') {
|
||||||
|
const legacy = initialized.attributes.initiative;
|
||||||
|
initialized.attributes.initiative = { value: legacy, bonus: 0 };
|
||||||
|
}
|
||||||
return initialized;
|
return initialized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-2
@@ -6,11 +6,11 @@ import {
|
|||||||
makeArrayField,
|
makeArrayField,
|
||||||
fields,
|
fields,
|
||||||
} from './fields';
|
} from './fields';
|
||||||
import { getMagicPowerEntry } from '../constants';
|
import { ATTRIBUTES, SKILL_KEYS, SKILLS, AttributeKey, getMagicPowerEntry } from '../constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NPC / Creature data model.
|
* NPC / Creature data model.
|
||||||
* Looser than character (no derived health formula — set directly).
|
* Looser than character (no derived health formula - set directly).
|
||||||
*/
|
*/
|
||||||
export class NpcData extends foundry.abstract.TypeDataModel {
|
export class NpcData extends foundry.abstract.TypeDataModel {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
@@ -29,6 +29,17 @@ export class NpcData extends foundry.abstract.TypeDataModel {
|
|||||||
description: makeStringField(''),
|
description: makeStringField(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const skillEntries: Record<string, foundry.data.fields.DataField.Any> = {};
|
||||||
|
for (const key of SKILL_KEYS) {
|
||||||
|
skillEntries[key] = new f.SchemaField({
|
||||||
|
value: makeIntField(0, { min: 0, max: 8 }),
|
||||||
|
defaultAttribute: makeStringField(SKILLS[key] as AttributeKey, {
|
||||||
|
blank: false,
|
||||||
|
choices: ATTRIBUTES as unknown as readonly string[],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attributes: new f.SchemaField({
|
attributes: new f.SchemaField({
|
||||||
body: new f.SchemaField({ value: makeIntField(1, { min: 1, max: 12 }) }),
|
body: new f.SchemaField({ value: makeIntField(1, { min: 1, max: 12 }) }),
|
||||||
@@ -49,8 +60,13 @@ export class NpcData extends foundry.abstract.TypeDataModel {
|
|||||||
magicalArmor: makeValueMaxField(0, 0),
|
magicalArmor: makeValueMaxField(0, 0),
|
||||||
magicalShield: makeValueMaxField(0, 0),
|
magicalShield: makeValueMaxField(0, 0),
|
||||||
physicalArmor: new f.SchemaField({ value: makeIntField(0, { min: 0 }) }),
|
physicalArmor: new f.SchemaField({ value: makeIntField(0, { min: 0 }) }),
|
||||||
|
initiative: new f.SchemaField({
|
||||||
|
value: makeIntField(0, { min: 0 }),
|
||||||
|
bonus: makeIntField(0, { min: 0 }),
|
||||||
|
}),
|
||||||
speed: makeStringField('5m'),
|
speed: makeStringField('5m'),
|
||||||
}),
|
}),
|
||||||
|
skills: new f.SchemaField(skillEntries),
|
||||||
details: new f.SchemaField({
|
details: new f.SchemaField({
|
||||||
type: makeStringField(''),
|
type: makeStringField(''),
|
||||||
size: makeStringField('medium'),
|
size: makeStringField('medium'),
|
||||||
@@ -88,11 +104,13 @@ export class NpcData extends foundry.abstract.TypeDataModel {
|
|||||||
const self = this as unknown as { parent?: { items?: Iterable<any> } } & Record<string, any>;
|
const self = this as unknown as { parent?: { items?: Iterable<any> } } & Record<string, any>;
|
||||||
const sys = this as unknown as {
|
const sys = this as unknown as {
|
||||||
attributes: {
|
attributes: {
|
||||||
|
mind: { value: number };
|
||||||
magic: { value: number; actual: number };
|
magic: { value: number; actual: number };
|
||||||
mana: { value: number; max: number; maxPerSpell: number };
|
mana: { value: number; max: number; maxPerSpell: number };
|
||||||
health: { value: number; max: number };
|
health: { value: number; max: number };
|
||||||
magicalArmor: { value: number; max: number };
|
magicalArmor: { value: number; max: number };
|
||||||
magicalShield: { value: number; max: number };
|
magicalShield: { value: number; max: number };
|
||||||
|
initiative: { value: number; bonus: number };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const a = sys.attributes;
|
const a = sys.attributes;
|
||||||
@@ -120,6 +138,25 @@ export class NpcData extends foundry.abstract.TypeDataModel {
|
|||||||
a.mana.maxPerSpell = mp.maxPerSpell;
|
a.mana.maxPerSpell = mp.maxPerSpell;
|
||||||
if (a.mana.value > a.mana.max) a.mana.value = a.mana.max;
|
if (a.mana.value > a.mana.max) a.mana.value = a.mana.max;
|
||||||
|
|
||||||
|
let initiativeBonus = 0;
|
||||||
|
for (const it of items) {
|
||||||
|
if (it.type === 'talent') {
|
||||||
|
const slug = it.flags?.['hbm-rpg-v3']?.slug || '';
|
||||||
|
const name = it.name?.toLowerCase() || '';
|
||||||
|
if (
|
||||||
|
slug === 'battle-readiness' || slug === 'gotowosc-do-walki' ||
|
||||||
|
name === 'gotowo\u015b\u0107 do walki' || name === 'battle readiness'
|
||||||
|
) {
|
||||||
|
initiativeBonus += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.initiative.value =
|
||||||
|
a.mind.value +
|
||||||
|
initiativeBonus +
|
||||||
|
(a.initiative.bonus ?? 0);
|
||||||
|
|
||||||
if (a.health.value > a.health.max) a.health.value = a.health.max;
|
if (a.health.value > a.health.max) a.health.value = a.health.max;
|
||||||
if (a.magicalArmor.value > a.magicalArmor.max) a.magicalArmor.value = a.magicalArmor.max;
|
if (a.magicalArmor.value > a.magicalArmor.max) a.magicalArmor.value = a.magicalArmor.max;
|
||||||
if (a.magicalShield.value > a.magicalShield.max) a.magicalShield.value = a.magicalShield.max;
|
if (a.magicalShield.value > a.magicalShield.max) a.magicalShield.value = a.magicalShield.max;
|
||||||
@@ -135,6 +172,10 @@ export class NpcData extends foundry.abstract.TypeDataModel {
|
|||||||
}
|
}
|
||||||
delete initialized.attributes.armor;
|
delete initialized.attributes.armor;
|
||||||
}
|
}
|
||||||
|
if (initialized?.attributes && typeof initialized.attributes.initiative === 'number') {
|
||||||
|
const legacy = initialized.attributes.initiative;
|
||||||
|
initialized.attributes.initiative = { value: legacy, bonus: 0 };
|
||||||
|
}
|
||||||
return initialized;
|
return initialized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { GEAR_CATEGORIES, DAMAGE_TYPES, ARMOR_TYPES, RARITIES } from '../constants';
|
import { GEAR_CATEGORIES, DAMAGE_TYPES, ARMOR_TYPES, RARITIES } from '../constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unified Gear item — discriminated by `category`.
|
* Unified Gear item - discriminated by `category`.
|
||||||
* Weapon/armor/equipment all share base fields; specialised fields
|
* Weapon/armor/equipment all share base fields; specialised fields
|
||||||
* are scoped under `weapon` and `armor` sub-schemas.
|
* are scoped under `weapon` and `armor` sub-schemas.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export class SpellData extends foundry.abstract.TypeDataModel {
|
|||||||
minCasters: makeIntField(1, { min: 1 }),
|
minCasters: makeIntField(1, { min: 1 }),
|
||||||
nonCombatOnly: makeBoolField(false),
|
nonCombatOnly: makeBoolField(false),
|
||||||
|
|
||||||
// Overcast — structured
|
// Overcast - structured
|
||||||
overcastOptions: makeArrayField(new f.SchemaField({
|
overcastOptions: makeArrayField(new f.SchemaField({
|
||||||
description: makeStringField(''),
|
description: makeStringField(''),
|
||||||
manaPerStep: makeIntField(1, { min: 0 }),
|
manaPerStep: makeIntField(1, { min: 0 }),
|
||||||
@@ -105,7 +105,7 @@ export class SpellData extends foundry.abstract.TypeDataModel {
|
|||||||
|
|
||||||
// Description
|
// Description
|
||||||
description: makeHtmlField(''),
|
description: makeHtmlField(''),
|
||||||
higherCircles: makeHtmlField(''), // legacy — superseded by overcastOptions
|
higherCircles: makeHtmlField(''), // legacy - superseded by overcastOptions
|
||||||
|
|
||||||
// Legacy fields (kept for migration; will be removed in a later release)
|
// Legacy fields (kept for migration; will be removed in a later release)
|
||||||
overcasting: makeStringField(''),
|
overcasting: makeStringField(''),
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ export async function askCastOptions(spell: SpellForDialog, actor: ActorForDialo
|
|||||||
<input type="number" name="bloodSpent" value="${baseBlood}" min="${baseBlood}" max="${max}"/>
|
<input type="number" name="bloodSpent" value="${baseBlood}" min="${baseBlood}" max="${max}"/>
|
||||||
</div>`);
|
</div>`);
|
||||||
}
|
}
|
||||||
|
if (mode !== 'sacred') {
|
||||||
|
fields.push(`
|
||||||
|
<div class="form-group">
|
||||||
|
<label><input type="checkbox" name="castAsPrayer"/> ${game.i18n.localize('HBM.spell.castingMode.prayer')} (+1 ${game.i18n.localize('HBM.resources.zealAbbr')}, ${game.i18n.localize('HBM.attributes.soul')} + ${game.i18n.localize('HBM.skills.devotion')})</label>
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
if (showHekate) {
|
if (showHekate) {
|
||||||
fields.push(`
|
fields.push(`
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -157,6 +163,7 @@ export async function askCastOptions(spell: SpellForDialog, actor: ActorForDialo
|
|||||||
if (fd.zealSpent != null) opts.zealSpent = Math.max(1, Number(fd.zealSpent) || 1);
|
if (fd.zealSpent != null) opts.zealSpent = Math.max(1, Number(fd.zealSpent) || 1);
|
||||||
if (fd.bloodSpent != null) opts.bloodSpent = Math.max(baseBlood, Number(fd.bloodSpent) || baseBlood);
|
if (fd.bloodSpent != null) opts.bloodSpent = Math.max(baseBlood, Number(fd.bloodSpent) || baseBlood);
|
||||||
if (fd.hekateMode) opts.hekateMode = 'witch';
|
if (fd.hekateMode) opts.hekateMode = 'witch';
|
||||||
|
if (fd.castAsPrayer) opts.castAsPrayer = true;
|
||||||
if (fd.bypassSuperspellWarning) opts.bypassSuperspellWarning = true;
|
if (fd.bypassSuperspellWarning) opts.bypassSuperspellWarning = true;
|
||||||
if (fd.bypassNonCombatBlock) opts.bypassNonCombatBlock = true;
|
if (fd.bypassNonCombatBlock) opts.bypassNonCombatBlock = true;
|
||||||
const groupRaw = String(fd.groupCasters ?? '').trim();
|
const groupRaw = String(fd.groupCasters ?? '').trim();
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export async function askApplyDamage(actor: ActorLike, presetAmount = 0): Promis
|
|||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
new foundry.appv1.api.Dialog({
|
new foundry.appv1.api.Dialog({
|
||||||
title: `${game.i18n.localize('HBM.damage.title')} — ${actor.name}`,
|
title: `${game.i18n.localize('HBM.damage.title')} - ${actor.name}`,
|
||||||
content: `
|
content: `
|
||||||
<form class="hbm-dialog">
|
<form class="hbm-dialog">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function collectRollModifiers(actor: any, tags: readonly SkillTag[]): Rol
|
|||||||
const hbm = ef?.flags?.hbm;
|
const hbm = ef?.flags?.hbm;
|
||||||
if (!hbm) continue;
|
if (!hbm) continue;
|
||||||
|
|
||||||
// Threshold step modifiers — keys may be tag names or 'all'
|
// Threshold step modifiers - keys may be tag names or 'all'
|
||||||
const ts = hbm.thresholdSteps as Record<string, number> | undefined;
|
const ts = hbm.thresholdSteps as Record<string, number> | undefined;
|
||||||
if (ts && typeof ts === 'object') {
|
if (ts && typeof ts === 'object') {
|
||||||
if (typeof ts['all'] === 'number') result.thresholdSteps += ts['all'];
|
if (typeof ts['all'] === 'number') result.thresholdSteps += ts['all'];
|
||||||
|
|||||||
+16
-3
@@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Roll helpers — convenience wrappers around HbmTSRoll bound to actor data.
|
* Roll helpers - convenience wrappers around HbmTSRoll bound to actor data.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HbmTSRoll } from './ts-roll';
|
import { HbmTSRoll } from './ts-roll';
|
||||||
import { collectRollModifiers, RollBlockedError } from './effect-mods';
|
import { collectRollModifiers, RollBlockedError } from './effect-mods';
|
||||||
import { ATTRIBUTES, AttributeKey, SKILLS, SKILL_TAGS, TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD } from '../constants';
|
import { ATTRIBUTES, AttributeKey, SKILLS, SKILL_TAGS, TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD, getMagicPowerEntry } from '../constants';
|
||||||
|
|
||||||
export interface SkillRollOptions {
|
export interface SkillRollOptions {
|
||||||
/** Override the skill's default attribute (GM allows another for a specific roll). */
|
/** Override the skill's default attribute (GM allows another for a specific roll). */
|
||||||
@@ -26,6 +26,13 @@ export interface ActorLike {
|
|||||||
|
|
||||||
function getAttributeValue(actor: ActorLike, key: AttributeKey): number {
|
function getAttributeValue(actor: ActorLike, key: AttributeKey): number {
|
||||||
const a = actor.system.attributes[key];
|
const a = actor.system.attributes[key];
|
||||||
|
if (key === 'magic' && a) {
|
||||||
|
if ('dicePool' in a && typeof a.dicePool === 'number') {
|
||||||
|
return a.dicePool;
|
||||||
|
}
|
||||||
|
const actual = (a as any).actual ?? a.value ?? 0;
|
||||||
|
return getMagicPowerEntry(actual).dicePool;
|
||||||
|
}
|
||||||
return a?.value ?? 0;
|
return a?.value ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,10 +98,16 @@ export async function rollAttribute(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function rollInitiative(actor: ActorLike): Promise<any> {
|
export async function rollInitiative(actor: ActorLike): Promise<any> {
|
||||||
|
const sysAttr = actor.system.attributes as any;
|
||||||
|
let mod = 0;
|
||||||
|
if (sysAttr.initiative && typeof sysAttr.initiative.value === 'number') {
|
||||||
|
mod = sysAttr.initiative.value;
|
||||||
|
} else {
|
||||||
const mind = getAttributeValue(actor, 'mind');
|
const mind = getAttributeValue(actor, 'mind');
|
||||||
const reflex = actor.system.skills?.reflex?.value ?? 0;
|
const reflex = actor.system.skills?.reflex?.value ?? 0;
|
||||||
const perception = actor.system.skills?.perception?.value ?? 0;
|
const perception = actor.system.skills?.perception?.value ?? 0;
|
||||||
const mod = mind + reflex + perception;
|
mod = mind + reflex + perception;
|
||||||
|
}
|
||||||
const flavor = game.i18n.localize('HBM.resources.initiative');
|
const flavor = game.i18n.localize('HBM.resources.initiative');
|
||||||
|
|
||||||
const roll = new Roll('2d6 + @mod', { mod });
|
const roll = new Roll('2d6 + @mod', { mod });
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface RollDialogResult {
|
|||||||
pool: number;
|
pool: number;
|
||||||
threshold: number;
|
threshold: number;
|
||||||
required: number;
|
required: number;
|
||||||
|
modifier: number;
|
||||||
flavor?: string;
|
flavor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +37,10 @@ export async function askRollParams(initial: {
|
|||||||
<label>${game.i18n.localize('HBM.roll.required')} (1–10)</label>
|
<label>${game.i18n.localize('HBM.roll.required')} (1–10)</label>
|
||||||
<input type="number" name="required" value="${y}" min="1" max="10"/>
|
<input type="number" name="required" value="${y}" min="1" max="10"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${game.i18n.localize('HBM.roll.modifier')}</label>
|
||||||
|
<input type="number" name="modifier" value="0"/>
|
||||||
|
</div>
|
||||||
</form>`,
|
</form>`,
|
||||||
buttons: {
|
buttons: {
|
||||||
roll: {
|
roll: {
|
||||||
@@ -48,6 +53,7 @@ export async function askRollParams(initial: {
|
|||||||
pool: initial.pool,
|
pool: initial.pool,
|
||||||
threshold: Math.min(6, Math.max(2, Number(fd['threshold']) || t)),
|
threshold: Math.min(6, Math.max(2, Number(fd['threshold']) || t)),
|
||||||
required: Math.min(10, Math.max(1, Number(fd['required']) || y)),
|
required: Math.min(10, Math.max(1, Number(fd['required']) || y)),
|
||||||
|
modifier: Number(fd['modifier']) || 0,
|
||||||
flavor: initial.flavor,
|
flavor: initial.flavor,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@
|
|||||||
* otherwise face >= T → success
|
* otherwise face >= T → success
|
||||||
* - Roll succeeds when total successes ≥ requiredSuccesses (Y).
|
* - Roll succeeds when total successes ≥ requiredSuccesses (Y).
|
||||||
*
|
*
|
||||||
* Formula syntax: `ts(N, T, Y)` — e.g. `/r ts(5, 4, 2)`.
|
* Formula syntax: `ts(N, T, Y)` - e.g. `/r ts(5, 4, 2)`.
|
||||||
* The N/T/Y parameters are also accepted via constructor data.
|
* The N/T/Y parameters are also accepted via constructor data.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ function computeTsResult(roll: Roll): HbmTsRollResult {
|
|||||||
// Collect raw die faces from all DiceTerm results in the roll.
|
// Collect raw die faces from all DiceTerm results in the roll.
|
||||||
const faces: number[] = [];
|
const faces: number[] = [];
|
||||||
for (const term of roll.terms) {
|
for (const term of roll.terms) {
|
||||||
// foundry.dice.terms.DiceTerm — has .results array with { result } entries.
|
// foundry.dice.terms.DiceTerm - has .results array with { result } entries.
|
||||||
const anyTerm = term as unknown as { results?: Array<{ result: number; active?: boolean; discarded?: boolean }> };
|
const anyTerm = term as unknown as { results?: Array<{ result: number; active?: boolean; discarded?: boolean }> };
|
||||||
if (Array.isArray(anyTerm.results)) {
|
if (Array.isArray(anyTerm.results)) {
|
||||||
for (const r of anyTerm.results) {
|
for (const r of anyTerm.results) {
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Homebrew Magic: RPG v3 — Foundry VTT system entry point.
|
* Homebrew Magic: RPG v3 - Foundry VTT system entry point.
|
||||||
*
|
*
|
||||||
* All internal identifiers are English. Polish is reserved for user-facing
|
* All internal identifiers are English. Polish is reserved for user-facing
|
||||||
* labels supplied via `lang/pl.json` (the canonical localisation).
|
* labels supplied via `lang/pl.json` (the canonical localisation).
|
||||||
@@ -111,7 +111,7 @@ Hooks.once('init', () => {
|
|||||||
|
|
||||||
// Override default initiative formula to HbM's 2d6 + initiative attribute.
|
// Override default initiative formula to HbM's 2d6 + initiative attribute.
|
||||||
CONFIG.Combat.initiative = {
|
CONFIG.Combat.initiative = {
|
||||||
formula: '2d6 + @attributes.initiative',
|
formula: '2d6 + @attributes.initiative.value',
|
||||||
decimals: 2,
|
decimals: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Magia Otchłani — Abyss Magic logic (Klątwa Otchłani Ch. III–VIII).
|
* Magia Otchłani - Abyss Magic logic (Klątwa Otchłani Ch. III–VIII).
|
||||||
*
|
*
|
||||||
* The Abyss splits into two disciplines:
|
* The Abyss splits into two disciplines:
|
||||||
* - Magia Aspektów ("aspects") — controlled, low-risk, predictable
|
* - Magia Aspektów ("aspects") - controlled, low-risk, predictable
|
||||||
* - Pierwotna Magia ("primal") — chaotic, high-power, requires `Dary Otchłani` rolls
|
* - Pierwotna Magia ("primal") - chaotic, high-power, requires `Dary Otchłani` rolls
|
||||||
*
|
*
|
||||||
* This module exposes:
|
* This module exposes:
|
||||||
* - dispatchAbyssCast(spell) — returns 'aspects' | 'primal' from spell.discipline.
|
* - dispatchAbyssCast(spell) - returns 'aspects' | 'primal' from spell.discipline.
|
||||||
* - rollAbyssGift(actor) — d100 against a roll table (resolved at runtime
|
* - rollAbyssGift(actor) - d100 against a roll table (resolved at runtime
|
||||||
* from the `roll-tables-abyss.dary-otchlani` pack
|
* from the `roll-tables-abyss.dary-otchlani` pack
|
||||||
* when present; otherwise falls back to a chat
|
* when present; otherwise falls back to a chat
|
||||||
* prompt for the GM).
|
* prompt for the GM).
|
||||||
* - rollMistrzLosuPenalty(actor) — d100 penalty roll (Klątwa Otchłani VII).
|
* - rollMistrzLosuPenalty(actor) - d100 penalty roll (Klątwa Otchłani VII).
|
||||||
* - addInsanity(actor, n) — increments actor.system.attributes.insanity;
|
* - addInsanity(actor, n) - increments actor.system.attributes.insanity;
|
||||||
* when crossing a threshold, fires a hook for
|
* when crossing a threshold, fires a hook for
|
||||||
* the GM to apply a mental condition.
|
* the GM to apply a mental condition.
|
||||||
*/
|
*/
|
||||||
@@ -69,7 +69,7 @@ async function rollAgainstAbyssTable(actor: CastableActor, slug: string, flavor:
|
|||||||
await roll.evaluate();
|
await roll.evaluate();
|
||||||
const speaker = ChatMessage.getSpeaker({ actor: actor as unknown as Actor });
|
const speaker = ChatMessage.getSpeaker({ actor: actor as unknown as Actor });
|
||||||
await roll.toMessage({
|
await roll.toMessage({
|
||||||
flavor: `${flavor} — ${actor.name} (${slug})`,
|
flavor: `${flavor} - ${actor.name} (${slug})`,
|
||||||
speaker,
|
speaker,
|
||||||
});
|
});
|
||||||
Hooks.callAll('hbm.abyssTableRoll', actor, slug, roll.total);
|
Hooks.callAll('hbm.abyssTableRoll', actor, slug, roll.total);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Magia Krwi — Blood Magic logic (Arcanum Sanguinis Ch. III).
|
* Magia Krwi - Blood Magic logic (Arcanum Sanguinis Ch. III).
|
||||||
*
|
*
|
||||||
* Three primitives:
|
* Three primitives:
|
||||||
* - spendBlood(actor, n) : deduct n from blood pool, fail if insufficient.
|
* - spendBlood(actor, n) : deduct n from blood pool, fail if insufficient.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Warzenie Eliksirów — Brewing logic (Podręcznik Gry, Alchemia / Aneks C).
|
* Warzenie Eliksirów - Brewing logic (Podręcznik Gry, Alchemia / Aneks C).
|
||||||
*
|
*
|
||||||
* Each character has `attributes.elixirTolerance` (default 0). Soft cap = body+1.
|
* Each character has `attributes.elixirTolerance` (default 0). Soft cap = body+1.
|
||||||
* Drinking a potion increments the counter; exceeding the cap triggers a
|
* Drinking a potion increments the counter; exceeding the cap triggers a
|
||||||
@@ -12,12 +12,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { HbmTSRoll } from '../dice/ts-roll';
|
import { HbmTSRoll } from '../dice/ts-roll';
|
||||||
import { TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD } from '../constants';
|
import { TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD, getMagicPowerEntry } from '../constants';
|
||||||
import type { CastableActor } from './spell-cast';
|
import type { CastableActor } from './spell-cast';
|
||||||
|
|
||||||
export interface ElixirRecipe {
|
export interface ElixirRecipe {
|
||||||
name: string;
|
name: string;
|
||||||
/** TS test target — { threshold, successes }. */
|
/** TS test target - { threshold, successes }. */
|
||||||
difficulty: { threshold: number; successes: number };
|
difficulty: { threshold: number; successes: number };
|
||||||
/** Ingredient names (free-form). */
|
/** Ingredient names (free-form). */
|
||||||
ingredients: string[];
|
ingredients: string[];
|
||||||
@@ -44,7 +44,11 @@ export async function consumeElixir(actor: CastableActor, recipe: Pick<ElixirRec
|
|||||||
export async function brewElixir(actor: CastableActor, recipe: ElixirRecipe): Promise<{ roll: HbmTSRoll; success: boolean }> {
|
export async function brewElixir(actor: CastableActor, recipe: ElixirRecipe): Promise<{ roll: HbmTSRoll; success: boolean }> {
|
||||||
const skillKey = recipe.skill ?? 'alchemyBrewing';
|
const skillKey = recipe.skill ?? 'alchemyBrewing';
|
||||||
const skillValue = actor.system.skills?.[skillKey]?.value ?? 0;
|
const skillValue = actor.system.skills?.[skillKey]?.value ?? 0;
|
||||||
const pool = actor.system.attributes.magic.actual + skillValue;
|
const magicAttr = actor.system.attributes.magic as any;
|
||||||
|
const magicDice = typeof magicAttr.dicePool === 'number'
|
||||||
|
? magicAttr.dicePool
|
||||||
|
: getMagicPowerEntry(magicAttr.actual ?? magicAttr.value ?? 0).dicePool;
|
||||||
|
const pool = magicDice + skillValue;
|
||||||
const roll = HbmTSRoll.fromParams({
|
const roll = HbmTSRoll.fromParams({
|
||||||
pool,
|
pool,
|
||||||
threshold: recipe.difficulty.threshold ?? TS_DEFAULT_THRESHOLD,
|
threshold: recipe.difficulty.threshold ?? TS_DEFAULT_THRESHOLD,
|
||||||
@@ -53,7 +57,7 @@ export async function brewElixir(actor: CastableActor, recipe: ElixirRecipe): Pr
|
|||||||
});
|
});
|
||||||
await roll.evaluate();
|
await roll.evaluate();
|
||||||
await roll.toMessage({
|
await roll.toMessage({
|
||||||
flavor: `Warzenie ${recipe.name} (Składniki: ${recipe.ingredients.join(', ') || '—'})`,
|
flavor: `Warzenie ${recipe.name} (Składniki: ${recipe.ingredients.join(', ') || '-'})`,
|
||||||
});
|
});
|
||||||
return { roll, success: !!roll.ts?.isSuccess };
|
return { roll, success: !!roll.ts?.isSuccess };
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Combat hooks — handle per-round Mana reset, per-turn Zeal regen,
|
* Combat hooks - handle per-round Mana reset, per-turn Zeal regen,
|
||||||
* and condition-driven turn behavior (skip / damage tick / death save).
|
* and condition-driven turn behavior (skip / damage tick / death save).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -50,14 +50,14 @@ export function registerCombatHooks(): void {
|
|||||||
await applyDamage(actor, { amount: 1, type: 'environmental', ignoreMagicalArmor: true, ignoreMagicalShield: true, ignorePhysicalArmor: true });
|
await applyDamage(actor, { amount: 1, type: 'environmental', ignoreMagicalArmor: true, ignoreMagicalShield: true, ignorePhysicalArmor: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dying: prompt death save (simplified — posts a chat reminder)
|
// Dying: prompt death save (simplified - posts a chat reminder)
|
||||||
if (hasStatus(actor, 'umierajacy')) {
|
if (hasStatus(actor, 'umierajacy')) {
|
||||||
ChatMessage.create({
|
ChatMessage.create({
|
||||||
content: `<strong>${actor.name}</strong>: ${game.i18n.localize('HBM.combat.deathSavePrompt')}`,
|
content: `<strong>${actor.name}</strong>: ${game.i18n.localize('HBM.combat.deathSavePrompt')}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-turn Zeal regen (characters) — base 1 + talent bonus from flag.
|
// Per-turn Zeal regen (characters) - base 1 + talent bonus from flag.
|
||||||
if (actor.type === 'character') {
|
if (actor.type === 'character') {
|
||||||
const sys = actor.system as { attributes: { zeal: { value: number; max: number } } };
|
const sys = actor.system as { attributes: { zeal: { value: number; max: number } } };
|
||||||
const regen = getZealRegen(actor);
|
const regen = getZealRegen(actor);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* HbM canonical status conditions (Aneks A — Podręcznik Gry).
|
* HbM canonical status conditions (Aneks A - Podręcznik Gry).
|
||||||
* 15 entries with Active-Effect change ops and `flags.hbm.*` metadata
|
* 15 entries with Active-Effect change ops and `flags.hbm.*` metadata
|
||||||
* consumed by the roll pipeline and combat hooks.
|
* consumed by the roll pipeline and combat hooks.
|
||||||
*/
|
*/
|
||||||
@@ -55,7 +55,7 @@ export const CONDITIONS: ConditionDef[] = [
|
|||||||
// Provisional: book description incomplete for Przerażony (Aneks A placeholder).
|
// Provisional: book description incomplete for Przerażony (Aneks A placeholder).
|
||||||
{ id: 'przerazony', i18nKey: 'HBM.conditions.przerazony', icon: 'icons/svg/terror.svg', flags: { hbm: { thresholdSteps: { all: 1 }, todoBookGap: true } } },
|
{ id: 'przerazony', i18nKey: 'HBM.conditions.przerazony', icon: 'icons/svg/terror.svg', flags: { hbm: { thresholdSteps: { all: 1 }, todoBookGap: true } } },
|
||||||
|
|
||||||
// Mental illnesses (Klątwa Otchłani Ch. VIII — book chapter currently a placeholder;
|
// Mental illnesses (Klątwa Otchłani Ch. VIII - book chapter currently a placeholder;
|
||||||
// these are stub registrations with `todoBookGap: true` until full mechanics drop).
|
// these are stub registrations with `todoBookGap: true` until full mechanics drop).
|
||||||
{ id: 'paranoja', i18nKey: 'HBM.conditions.paranoja', icon: 'icons/svg/eye.svg', flags: { hbm: { mental: true, todoBookGap: true } } },
|
{ id: 'paranoja', i18nKey: 'HBM.conditions.paranoja', icon: 'icons/svg/eye.svg', flags: { hbm: { mental: true, todoBookGap: true } } },
|
||||||
{ id: 'fobia', i18nKey: 'HBM.conditions.fobia', icon: 'icons/svg/silenced.svg', flags: { hbm: { mental: true, todoBookGap: true } } },
|
{ id: 'fobia', i18nKey: 'HBM.conditions.fobia', icon: 'icons/svg/silenced.svg', flags: { hbm: { mental: true, todoBookGap: true } } },
|
||||||
|
|||||||
+5
-5
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Damage application engine — flows through 4 layers:
|
* Damage application engine - flows through 4 layers:
|
||||||
* 1. Magical Armor (DR + every 5 dmg absorbed degrades value by 1 via runicCounter)
|
* 1. Magical Armor (DR + every 5 dmg absorbed degrades value by 1 via runicCounter)
|
||||||
* 2. Magical Shield (raw temp HP; cannot be healed; vanishes at 0)
|
* 2. Magical Shield (raw temp HP; cannot be healed; vanishes at 0)
|
||||||
* 3. Physical Armor (flat DR; optionally degrade `condition` by 1)
|
* 3. Physical Armor (flat DR; optionally degrade `condition` by 1)
|
||||||
@@ -54,7 +54,7 @@ export async function applyDamage(actor: ActorLike, opts: ApplyDamageOptions): P
|
|||||||
|
|
||||||
let remaining = total;
|
let remaining = total;
|
||||||
|
|
||||||
// Layer 1: Magical Armor — DR-based; runicCounter accumulates incoming damage.
|
// Layer 1: Magical Armor - DR-based; runicCounter accumulates incoming damage.
|
||||||
if (!opts.ignoreMagicalArmor && a.magicalArmor) {
|
if (!opts.ignoreMagicalArmor && a.magicalArmor) {
|
||||||
const dr = Math.max(0, Number(a.magicalArmor.value) || 0);
|
const dr = Math.max(0, Number(a.magicalArmor.value) || 0);
|
||||||
const absorbed = Math.min(dr, remaining);
|
const absorbed = Math.min(dr, remaining);
|
||||||
@@ -70,7 +70,7 @@ export async function applyDamage(actor: ActorLike, opts: ApplyDamageOptions): P
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layer 2: Magical Shield — pure temp HP (raw subtraction).
|
// Layer 2: Magical Shield - pure temp HP (raw subtraction).
|
||||||
if (remaining > 0 && !opts.ignoreMagicalShield && a.magicalShield) {
|
if (remaining > 0 && !opts.ignoreMagicalShield && a.magicalShield) {
|
||||||
const shield = Math.max(0, Number(a.magicalShield.value) || 0);
|
const shield = Math.max(0, Number(a.magicalShield.value) || 0);
|
||||||
const taken = Math.min(shield, remaining);
|
const taken = Math.min(shield, remaining);
|
||||||
@@ -83,7 +83,7 @@ export async function applyDamage(actor: ActorLike, opts: ApplyDamageOptions): P
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layer 3: Physical Armor — flat DR; optionally degrade condition.
|
// Layer 3: Physical Armor - flat DR; optionally degrade condition.
|
||||||
if (remaining > 0 && !opts.ignorePhysicalArmor && a.physicalArmor) {
|
if (remaining > 0 && !opts.ignorePhysicalArmor && a.physicalArmor) {
|
||||||
const dr = Math.max(0, Number(a.physicalArmor.value) || 0);
|
const dr = Math.max(0, Number(a.physicalArmor.value) || 0);
|
||||||
const absorbed = Math.min(dr, remaining);
|
const absorbed = Math.min(dr, remaining);
|
||||||
@@ -118,7 +118,7 @@ export async function applyDamage(actor: ActorLike, opts: ApplyDamageOptions): P
|
|||||||
|
|
||||||
if (opts.postChat ?? true) {
|
if (opts.postChat ?? true) {
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
`<strong>${actor.name}</strong> — ${game.i18n.localize('HBM.damage.report.title')}: <strong>${total}</strong>`,
|
`<strong>${actor.name}</strong> - ${game.i18n.localize('HBM.damage.report.title')}: <strong>${total}</strong>`,
|
||||||
];
|
];
|
||||||
if (report.absorbed.magicalArmor) lines.push(`${game.i18n.localize('HBM.resources.magicalArmor')}: −${report.absorbed.magicalArmor}`);
|
if (report.absorbed.magicalArmor) lines.push(`${game.i18n.localize('HBM.resources.magicalArmor')}: −${report.absorbed.magicalArmor}`);
|
||||||
if (report.absorbed.magicalShield) lines.push(`${game.i18n.localize('HBM.resources.magicalShield')}: −${report.absorbed.magicalShield}${report.shieldDropped ? ' ✦' : ''}`);
|
if (report.absorbed.magicalShield) lines.push(`${game.i18n.localize('HBM.resources.magicalShield')}: −${report.absorbed.magicalShield}${report.shieldDropped ? ' ✦' : ''}`);
|
||||||
|
|||||||
+3
-3
@@ -3,14 +3,14 @@
|
|||||||
*
|
*
|
||||||
* Short Rest (Krótki Odpoczynek):
|
* Short Rest (Krótki Odpoczynek):
|
||||||
* - Restore HP equal to actor.body.value (capped at max).
|
* - Restore HP equal to actor.body.value (capped at max).
|
||||||
* - Restore mana to max-per-spell? — no: short rest does NOT restore mana.
|
* - Restore mana to max-per-spell? - no: short rest does NOT restore mana.
|
||||||
* - Holders of `Nadzwyczajna Odporność` (alchemy passive) restore 1 elixir tolerance.
|
* - Holders of `Nadzwyczajna Odporność` (alchemy passive) restore 1 elixir tolerance.
|
||||||
*
|
*
|
||||||
* Long Rest (Długi Odpoczynek):
|
* Long Rest (Długi Odpoczynek):
|
||||||
* - Restore HP, mana, zeal to max.
|
* - Restore HP, mana, zeal to max.
|
||||||
* - Restore 1 elixir tolerance (or all, if `Nadzwyczajna Odporność`? — book says
|
* - Restore 1 elixir tolerance (or all, if `Nadzwyczajna Odporność`? - book says
|
||||||
* elixir tolerance recovers per long rest by default; tracked here as -1).
|
* elixir tolerance recovers per long rest by default; tracked here as -1).
|
||||||
* - Reset blood pool to max (assumption — refine when AS spell list lands).
|
* - Reset blood pool to max (assumption - refine when AS spell list lands).
|
||||||
* - Clear runic counter on magical armor.
|
* - Clear runic counter on magical armor.
|
||||||
*
|
*
|
||||||
* Hooks `hbm.beforeRest` and `hbm.afterRest` fire for module integration.
|
* Hooks `hbm.beforeRest` and `hbm.afterRest` fire for module integration.
|
||||||
|
|||||||
+63
-23
@@ -1,21 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* Spell casting workflow — handles standard, sacred (Magia Sakralna),
|
* Spell casting workflow - handles standard, sacred (Magia Sakralna),
|
||||||
* witch (Wiedźmia Magia), and blood (Magia Krwi) modes.
|
* witch (Wiedźmia Magia), and blood (Magia Krwi) modes.
|
||||||
*
|
*
|
||||||
* Pipeline per cast:
|
* Pipeline per cast:
|
||||||
* 1. validateCast() — gate on resources, race, talent, discipline,
|
* 1. validateCast() - gate on resources, race, talent, discipline,
|
||||||
* deity, group-cast, in-combat, witch symbols
|
* deity, group-cast, in-combat, witch symbols
|
||||||
* 2. Resource deduction — mana / zeal / blood
|
* 2. Resource deduction - mana / zeal / blood
|
||||||
* 3. TS test roll — pool depends on mode
|
* 3. TS test roll - pool depends on mode
|
||||||
* 4. (success) damage roll — parses spell.damageBase
|
* 4. (success) damage roll - parses spell.damageBase
|
||||||
* 5. Status effect auto-apply — from spell.statusEffects[]
|
* 5. Status effect auto-apply - from spell.statusEffects[]
|
||||||
* 6. Trigger registration — from spell.triggers[]
|
* 6. Trigger registration - from spell.triggers[]
|
||||||
*
|
*
|
||||||
* Bypasses for GM workflows: opts.bypassSuperspellWarning, opts.bypassNonCombatBlock.
|
* Bypasses for GM workflows: opts.bypassSuperspellWarning, opts.bypassNonCombatBlock.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HbmTSRoll } from '../dice/ts-roll';
|
import { HbmTSRoll } from '../dice/ts-roll';
|
||||||
import { TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD } from '../constants';
|
import { TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD, getMagicPowerEntry } from '../constants';
|
||||||
import { validateCast } from './spell-validation';
|
import { validateCast } from './spell-validation';
|
||||||
import type { ValidationResult } from './spell-validation';
|
import type { ValidationResult } from './spell-validation';
|
||||||
import { rollSpellDamage } from './spell-damage';
|
import { rollSpellDamage } from './spell-damage';
|
||||||
@@ -28,7 +28,7 @@ export interface CastableActor {
|
|||||||
type: 'character' | 'npc';
|
type: 'character' | 'npc';
|
||||||
system: {
|
system: {
|
||||||
attributes: {
|
attributes: {
|
||||||
magic: { value: number };
|
magic: { value: number; actual?: number; dicePool?: number };
|
||||||
soul: { value: number };
|
soul: { value: number };
|
||||||
mana: { value: number; max: number; maxPerSpell: number };
|
mana: { value: number; max: number; maxPerSpell: number };
|
||||||
zeal: { value: number; max: number };
|
zeal: { value: number; max: number };
|
||||||
@@ -71,6 +71,7 @@ export interface CastOptions {
|
|||||||
zealSpent?: number;
|
zealSpent?: number;
|
||||||
bloodSpent?: number;
|
bloodSpent?: number;
|
||||||
hekateMode?: 'sacred' | 'witch';
|
hekateMode?: 'sacred' | 'witch';
|
||||||
|
castAsPrayer?: boolean;
|
||||||
bypassSuperspellWarning?: boolean;
|
bypassSuperspellWarning?: boolean;
|
||||||
bypassNonCombatBlock?: boolean;
|
bypassNonCombatBlock?: boolean;
|
||||||
groupCasters?: string[];
|
groupCasters?: string[];
|
||||||
@@ -147,12 +148,16 @@ async function renderCastCard(
|
|||||||
triggersRegistered: number,
|
triggersRegistered: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const success = baseRoll?.ts?.isSuccess ?? false;
|
const success = baseRoll?.ts?.isSuccess ?? false;
|
||||||
const mode = spell.system.castingMode ?? 'standard';
|
const mode = opts.castAsPrayer ? 'prayer' : (spell.system.castingMode ?? 'standard');
|
||||||
|
|
||||||
// Costs ledger
|
// Costs ledger
|
||||||
const costs: Array<{ label: string; amount: number }> = [];
|
const costs: Array<{ label: string; amount: number }> = [];
|
||||||
if ((opts.manaSpent ?? 0) > 0) costs.push({ label: game.i18n.localize('HBM.spellCast.manaSpent'), amount: opts.manaSpent! });
|
if ((opts.manaSpent ?? 0) > 0) costs.push({ label: game.i18n.localize('HBM.spellCast.manaSpent'), amount: opts.manaSpent! });
|
||||||
if ((opts.zealSpent ?? 0) > 0) costs.push({ label: game.i18n.localize('HBM.spellCast.zealSpent'), amount: opts.zealSpent! });
|
if (opts.castAsPrayer) {
|
||||||
|
costs.push({ label: game.i18n.localize('HBM.spellCast.zealSpent'), amount: 1 });
|
||||||
|
} else if ((opts.zealSpent ?? 0) > 0) {
|
||||||
|
costs.push({ label: game.i18n.localize('HBM.spellCast.zealSpent'), amount: opts.zealSpent! });
|
||||||
|
}
|
||||||
if ((opts.bloodSpent ?? 0) > 0) costs.push({ label: game.i18n.localize('HBM.spellCast.bloodSpent'), amount: opts.bloodSpent! });
|
if ((opts.bloodSpent ?? 0) > 0) costs.push({ label: game.i18n.localize('HBM.spellCast.bloodSpent'), amount: opts.bloodSpent! });
|
||||||
|
|
||||||
// Damage block
|
// Damage block
|
||||||
@@ -220,10 +225,21 @@ async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOp
|
|||||||
const manaSpent = Math.max(baseCost, Math.floor(opts.manaSpent ?? baseCost));
|
const manaSpent = Math.max(baseCost, Math.floor(opts.manaSpent ?? baseCost));
|
||||||
const a = actor.system.attributes;
|
const a = actor.system.attributes;
|
||||||
|
|
||||||
await actor.update({ 'system.attributes.mana.value': a.mana.value - manaSpent });
|
const updates: Record<string, any> = { 'system.attributes.mana.value': a.mana.value - manaSpent };
|
||||||
|
if (opts.castAsPrayer) {
|
||||||
|
updates['system.attributes.zeal.value'] = a.zeal.value - 1;
|
||||||
|
}
|
||||||
|
await actor.update(updates);
|
||||||
|
|
||||||
const pool = a.magic.actual + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
const magicDice = typeof a.magic.dicePool === 'number'
|
||||||
const flavor = `${spell.name}`;
|
? a.magic.dicePool
|
||||||
|
: getMagicPowerEntry(a.magic.actual ?? a.magic.value ?? 0).dicePool;
|
||||||
|
const pool = opts.castAsPrayer
|
||||||
|
? a.soul.value + (actor.system.skills?.devotion?.value ?? 0)
|
||||||
|
: magicDice + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
||||||
|
const flavor = opts.castAsPrayer
|
||||||
|
? `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.prayer')}`
|
||||||
|
: `${spell.name}`;
|
||||||
const required = opts.requiredOverride ?? spell.system.difficulty.successes ?? TS_DEFAULT_REQUIRED;
|
const required = opts.requiredOverride ?? spell.system.difficulty.successes ?? TS_DEFAULT_REQUIRED;
|
||||||
const baseRoll = HbmTSRoll.fromParams({
|
const baseRoll = HbmTSRoll.fromParams({
|
||||||
pool,
|
pool,
|
||||||
@@ -234,7 +250,7 @@ async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOp
|
|||||||
await baseRoll.evaluate();
|
await baseRoll.evaluate();
|
||||||
await baseRoll.toMessage({ flavor, speaker: opts.speaker });
|
await baseRoll.toMessage({ flavor, speaker: opts.speaker });
|
||||||
|
|
||||||
// Overcast — extra TS test if manaSpent exceeds maxPerSpell.
|
// Overcast - extra TS test if manaSpent exceeds maxPerSpell.
|
||||||
if (manaSpent > a.mana.maxPerSpell && a.mana.maxPerSpell > 0) {
|
if (manaSpent > a.mana.maxPerSpell && a.mana.maxPerSpell > 0) {
|
||||||
const excess = manaSpent - a.mana.maxPerSpell;
|
const excess = manaSpent - a.mana.maxPerSpell;
|
||||||
const tThreshold = Math.min(6, Math.max(2, baseCost));
|
const tThreshold = Math.min(6, Math.max(2, baseCost));
|
||||||
@@ -259,7 +275,7 @@ async function castSacred(actor: CastableActor, spell: SpellLike, opts: CastOpti
|
|||||||
await actor.update({ 'system.attributes.zeal.value': a.zeal.value - zealCost });
|
await actor.update({ 'system.attributes.zeal.value': a.zeal.value - zealCost });
|
||||||
|
|
||||||
const pool = a.soul.value + (actor.system.skills?.devotion?.value ?? 0);
|
const pool = a.soul.value + (actor.system.skills?.devotion?.value ?? 0);
|
||||||
const flavor = `${spell.name} — ${game.i18n.localize('HBM.spell.castingMode.sacred')}`;
|
const flavor = `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.sacred')}`;
|
||||||
const required = opts.requiredOverride ?? spell.system.difficulty.successes ?? TS_DEFAULT_REQUIRED;
|
const required = opts.requiredOverride ?? spell.system.difficulty.successes ?? TS_DEFAULT_REQUIRED;
|
||||||
const roll = HbmTSRoll.fromParams({
|
const roll = HbmTSRoll.fromParams({
|
||||||
pool,
|
pool,
|
||||||
@@ -279,10 +295,21 @@ async function castWitch(actor: CastableActor, spell: SpellLike, opts: CastOptio
|
|||||||
|
|
||||||
// Witch magic still has a mana cost.
|
// Witch magic still has a mana cost.
|
||||||
const baseCost = Math.max(0, spell.system.manaCost ?? 0);
|
const baseCost = Math.max(0, spell.system.manaCost ?? 0);
|
||||||
await actor.update({ 'system.attributes.mana.value': a.mana.value - baseCost });
|
const updates: Record<string, any> = { 'system.attributes.mana.value': a.mana.value - baseCost };
|
||||||
|
if (opts.castAsPrayer) {
|
||||||
|
updates['system.attributes.zeal.value'] = a.zeal.value - 1;
|
||||||
|
}
|
||||||
|
await actor.update(updates);
|
||||||
|
|
||||||
const pool = a.magic.actual + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
const magicDice = typeof (a.magic as any).dicePool === 'number'
|
||||||
const flavor = `${spell.name} — ${game.i18n.localize('HBM.spell.castingMode.witch')} (${required}/${maxSymbols})`;
|
? (a.magic as any).dicePool
|
||||||
|
: getMagicPowerEntry((a.magic as any).actual ?? a.magic.value ?? 0).dicePool;
|
||||||
|
const pool = opts.castAsPrayer
|
||||||
|
? a.soul.value + (actor.system.skills?.devotion?.value ?? 0)
|
||||||
|
: magicDice + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
||||||
|
const flavor = opts.castAsPrayer
|
||||||
|
? `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.prayer')} (${required}/${maxSymbols})`
|
||||||
|
: `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.witch')} (${required}/${maxSymbols})`;
|
||||||
const reqSuccesses = opts.requiredOverride ?? spell.system.difficulty.successes ?? TS_DEFAULT_REQUIRED;
|
const reqSuccesses = opts.requiredOverride ?? spell.system.difficulty.successes ?? TS_DEFAULT_REQUIRED;
|
||||||
const roll = HbmTSRoll.fromParams({
|
const roll = HbmTSRoll.fromParams({
|
||||||
pool,
|
pool,
|
||||||
@@ -300,18 +327,31 @@ async function castBlood(actor: CastableActor, spell: SpellLike, opts: CastOptio
|
|||||||
const baseCost = Math.max(0, spell.system.bloodCost ?? 0);
|
const baseCost = Math.max(0, spell.system.bloodCost ?? 0);
|
||||||
const bloodSpent = Math.max(baseCost, Math.floor(opts.bloodSpent ?? baseCost));
|
const bloodSpent = Math.max(baseCost, Math.floor(opts.bloodSpent ?? baseCost));
|
||||||
const blood = a.blood;
|
const blood = a.blood;
|
||||||
|
|
||||||
|
const updates: Record<string, any> = {};
|
||||||
if (blood) {
|
if (blood) {
|
||||||
await actor.update({ 'system.attributes.blood.value': Math.max(0, blood.value - bloodSpent) });
|
updates['system.attributes.blood.value'] = Math.max(0, blood.value - bloodSpent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mana cost (some blood spells may also have one)
|
// Mana cost (some blood spells may also have one)
|
||||||
const manaCost = Math.max(0, spell.system.manaCost ?? 0);
|
const manaCost = Math.max(0, spell.system.manaCost ?? 0);
|
||||||
if (manaCost > 0) {
|
if (manaCost > 0) {
|
||||||
await actor.update({ 'system.attributes.mana.value': a.mana.value - manaCost });
|
updates['system.attributes.mana.value'] = a.mana.value - manaCost;
|
||||||
}
|
}
|
||||||
|
if (opts.castAsPrayer) {
|
||||||
|
updates['system.attributes.zeal.value'] = a.zeal.value - 1;
|
||||||
|
}
|
||||||
|
await actor.update(updates);
|
||||||
|
|
||||||
const pool = a.magic.actual + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
const magicDice = typeof (a.magic as any).dicePool === 'number'
|
||||||
const flavor = `${spell.name} — ${game.i18n.localize('HBM.spell.castingMode.blood')}`;
|
? (a.magic as any).dicePool
|
||||||
|
: getMagicPowerEntry((a.magic as any).actual ?? a.magic.value ?? 0).dicePool;
|
||||||
|
const pool = opts.castAsPrayer
|
||||||
|
? a.soul.value + (actor.system.skills?.devotion?.value ?? 0)
|
||||||
|
: magicDice + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
||||||
|
const flavor = opts.castAsPrayer
|
||||||
|
? `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.prayer')}`
|
||||||
|
: `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.blood')}`;
|
||||||
const required = opts.requiredOverride ?? spell.system.difficulty.successes ?? TS_DEFAULT_REQUIRED;
|
const required = opts.requiredOverride ?? spell.system.difficulty.successes ?? TS_DEFAULT_REQUIRED;
|
||||||
const roll = HbmTSRoll.fromParams({
|
const roll = HbmTSRoll.fromParams({
|
||||||
pool,
|
pool,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function buildDamageFormula(spellDamageBase: string, ctx: DamageContext):
|
|||||||
const tokenRe = /([a-zA-Z]+)(?:\s*([\/*])\s*(\d+))?/g;
|
const tokenRe = /([a-zA-Z]+)(?:\s*([\/*])\s*(\d+))?/g;
|
||||||
formula = formula.replace(tokenRe, (match, name: string, op: string | undefined, num: string | undefined) => {
|
formula = formula.replace(tokenRe, (match, name: string, op: string | undefined, num: string | undefined) => {
|
||||||
if (!ATTR_TOKENS.has(name)) {
|
if (!ATTR_TOKENS.has(name)) {
|
||||||
// Leave alone (likely dice notation like 1d6 — no, dice has digits before)
|
// Leave alone (likely dice notation like 1d6 - no, dice has digits before)
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
let v = resolveToken(name, ctx);
|
let v = resolveToken(name, ctx);
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
* Hooks check the matching event and prompt/resolve the trigger.
|
* Hooks check the matching event and prompt/resolve the trigger.
|
||||||
*
|
*
|
||||||
* Supported events:
|
* Supported events:
|
||||||
* - killWithWeapon (e.g. Szkarłatny Sztylet — free spell after kill)
|
* - killWithWeapon (e.g. Szkarłatny Sztylet - free spell after kill)
|
||||||
* - targetCastsSpell (e.g. Klątwa Szkarłatu — opposed save → unconscious)
|
* - targetCastsSpell (e.g. Klątwa Szkarłatu - opposed save → unconscious)
|
||||||
* - damageTaken (reactive shields)
|
* - damageTaken (reactive shields)
|
||||||
* - turnStart (per-turn drains)
|
* - turnStart (per-turn drains)
|
||||||
*
|
*
|
||||||
@@ -86,7 +86,7 @@ export async function fireTriggers(
|
|||||||
fired.push(t);
|
fired.push(t);
|
||||||
// Post chat card so GM can resolve the effect manually if no auto-handler.
|
// Post chat card so GM can resolve the effect manually if no auto-handler.
|
||||||
await ChatMessage.create({
|
await ChatMessage.create({
|
||||||
content: `<strong>${actor.name ?? ''}</strong> — Wyzwalacz: <em>${t.spellName}</em><br/>Efekt: ${t.effect}`,
|
content: `<strong>${actor.name ?? ''}</strong> - Wyzwalacz: <em>${t.spellName}</em><br/>Efekt: ${t.effect}`,
|
||||||
whisper: ChatMessage.getWhisperRecipients?.('GM') ?? [],
|
whisper: ChatMessage.getWhisperRecipients?.('GM') ?? [],
|
||||||
});
|
});
|
||||||
if (!t.oneShot) remaining.push(t);
|
if (!t.oneShot) remaining.push(t);
|
||||||
|
|||||||
@@ -41,18 +41,42 @@ function actorHasTalent(actor: CastableActor, talentId: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchDiscipline(disciplineId: string, targetId: string): boolean {
|
||||||
|
const d1 = disciplineId.toLowerCase().trim();
|
||||||
|
const d2 = targetId.toLowerCase().trim();
|
||||||
|
if (d1 === d2) return true;
|
||||||
|
|
||||||
|
const mappings: Record<string, string[]> = {
|
||||||
|
crimson: ['crimson', 'magia szkarłatu', 'magia szkarłatnego kultu', 'crimson magic', 'crimson cult magic'],
|
||||||
|
abyssaspects: ['abyssaspects', 'magia aspektów', 'aspects', 'aspect magic', 'abyss - aspects', 'magia otchłani - aspekty'],
|
||||||
|
abyssprimal: ['abyssprimal', 'magia otchłani', 'primal', 'eldritch', 'eldritch magic', 'abyss - primal', 'magia otchłani - pierwotna magia', 'pierwotna magia'],
|
||||||
|
witch: ['witch', 'wiedźmia magia', 'witch magic', 'dzika wiedźmia magia', 'wildwitch'],
|
||||||
|
blood: ['blood', 'magia krwi', 'blood magic'],
|
||||||
|
};
|
||||||
|
|
||||||
|
let key1: string | null = null;
|
||||||
|
let key2: string | null = null;
|
||||||
|
|
||||||
|
for (const [k, aliases] of Object.entries(mappings)) {
|
||||||
|
if (k === d1 || aliases.includes(d1)) key1 = k;
|
||||||
|
if (k === d2 || aliases.includes(d2)) key2 = k;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key1 && key2 && key1 === key2) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function actorHasDiscipline(actor: CastableActor, disciplineId: string): boolean {
|
function actorHasDiscipline(actor: CastableActor, disciplineId: string): boolean {
|
||||||
const id = disciplineId.toLowerCase();
|
|
||||||
const items = (actor as any).items as Iterable<any> | undefined;
|
const items = (actor as any).items as Iterable<any> | undefined;
|
||||||
if (!items) return false;
|
if (items) {
|
||||||
for (const it of items) {
|
for (const it of items) {
|
||||||
if (it.type !== 'discipline') continue;
|
if (it.type !== 'discipline') continue;
|
||||||
const d = String(it.system?.disciplineId ?? it.system?.id ?? it.name ?? '').toLowerCase();
|
const d = String(it.system?.disciplineId ?? it.system?.id ?? it.name ?? '');
|
||||||
if (d === id) return true;
|
if (matchDiscipline(d, disciplineId)) return true;
|
||||||
}
|
}
|
||||||
// Also accept characters whose details.discipline matches
|
}
|
||||||
const detail = String((actor as any).system?.details?.discipline ?? '').toLowerCase();
|
const detail = String((actor as any).system?.details?.discipline ?? '');
|
||||||
return detail === id;
|
return matchDiscipline(detail, disciplineId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function actorIsInCombat(actor: CastableActor): boolean {
|
function actorIsInCombat(actor: CastableActor): boolean {
|
||||||
@@ -79,7 +103,11 @@ export function validateCast(actor: CastableActor, spell: SpellLike, opts: CastO
|
|||||||
errors.push({ code: 'no-mana', message: 'Not enough mana', i18nKey: 'HBM.spellCast.notEnoughMana' });
|
errors.push({ code: 'no-mana', message: 'Not enough mana', i18nKey: 'HBM.spellCast.notEnoughMana' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mode === 'sacred' && opts.hekateMode !== 'witch') {
|
if (opts.castAsPrayer) {
|
||||||
|
if (1 > Number(a.zeal?.value ?? 0)) {
|
||||||
|
errors.push({ code: 'no-zeal', message: 'Not enough zeal', i18nKey: 'HBM.spellCast.notEnoughZeal' });
|
||||||
|
}
|
||||||
|
} else if (mode === 'sacred' && opts.hekateMode !== 'witch') {
|
||||||
const zealNeeded = Math.max(1, opts.zealSpent ?? 1);
|
const zealNeeded = Math.max(1, opts.zealSpent ?? 1);
|
||||||
if (zealNeeded > Number(a.zeal?.value ?? 0)) {
|
if (zealNeeded > Number(a.zeal?.value ?? 0)) {
|
||||||
errors.push({ code: 'no-zeal', message: 'Not enough zeal', i18nKey: 'HBM.spellCast.notEnoughZeal' });
|
errors.push({ code: 'no-zeal', message: 'Not enough zeal', i18nKey: 'HBM.spellCast.notEnoughZeal' });
|
||||||
@@ -122,7 +150,23 @@ export function validateCast(actor: CastableActor, spell: SpellLike, opts: CastO
|
|||||||
// Discipline gate (any-of)
|
// Discipline gate (any-of)
|
||||||
const discReq: string[] = Array.isArray(sys.requirements?.discipline) ? sys.requirements.discipline : [];
|
const discReq: string[] = Array.isArray(sys.requirements?.discipline) ? sys.requirements.discipline : [];
|
||||||
if (discReq.length > 0) {
|
if (discReq.length > 0) {
|
||||||
const ok = discReq.some((d) => actorHasDiscipline(actor, d));
|
const hasEldritchMagic = actorHasDiscipline(actor, 'abyssPrimal');
|
||||||
|
const hasAspectMagic = actorHasDiscipline(actor, 'abyssAspects');
|
||||||
|
const hasWitchMagic = actorHasDiscipline(actor, 'witch');
|
||||||
|
|
||||||
|
// Is the spell an Eldritch Spell?
|
||||||
|
const isEldritchSpell = discReq.some((d) => matchDiscipline(d, 'abyssPrimal')) ||
|
||||||
|
matchDiscipline(sys.school ?? '', 'abyssPrimal') ||
|
||||||
|
matchDiscipline(sys.discipline ?? '', 'abyssPrimal');
|
||||||
|
|
||||||
|
let bypassDisc = false;
|
||||||
|
if (hasEldritchMagic) {
|
||||||
|
bypassDisc = true; // Eldritch Magic allows casting ANY spells
|
||||||
|
} else if ((hasWitchMagic || hasAspectMagic) && !isEldritchSpell) {
|
||||||
|
bypassDisc = true; // Witch/Aspect Magic allows casting any spells EXCEPT Eldritch spells
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = bypassDisc || discReq.some((d) => actorHasDiscipline(actor, d));
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
const translatedDisciplines = discReq.map(d => {
|
const translatedDisciplines = discReq.map(d => {
|
||||||
return game.i18n.has(`HBM.spellSchool.${d}`) ? game.i18n.localize(`HBM.spellSchool.${d}`) : d;
|
return game.i18n.has(`HBM.spellSchool.${d}`) ? game.i18n.localize(`HBM.spellSchool.${d}`) : d;
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Trade logic — founding companies, transactions, smuggling.
|
* Trade logic - founding companies, transactions, smuggling.
|
||||||
* Source: Złoto Stal i Magia, Rozdział IX.
|
* Source: Złoto Stal i Magia, Rozdział IX.
|
||||||
*
|
*
|
||||||
* The book's exact difficulty formulas depend on commodity, route, and party
|
* The book's exact difficulty formulas depend on commodity, route, and party
|
||||||
@@ -45,7 +45,7 @@ function rollTrade(actor: CastableActor, label: string, params: TradeTestParams)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Founding a trading company — usually a single TS test plus capital. */
|
/** Founding a trading company - usually a single TS test plus capital. */
|
||||||
export function foundCompany(actor: CastableActor, params: TradeTestParams = {}): Promise<HbmTSRoll> {
|
export function foundCompany(actor: CastableActor, params: TradeTestParams = {}): Promise<HbmTSRoll> {
|
||||||
return rollTrade(actor, 'Założenie Firmy Handlowej', params);
|
return rollTrade(actor, 'Założenie Firmy Handlowej', params);
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ export function transaction(actor: CastableActor, params: TradeTestParams = {}):
|
|||||||
return rollTrade(actor, 'Transakcja', params);
|
return rollTrade(actor, 'Transakcja', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Smuggling — illegal transaction; failure should fire `hbm.smugglingFailed`. */
|
/** Smuggling - illegal transaction; failure should fire `hbm.smugglingFailed`. */
|
||||||
export async function smuggling(actor: CastableActor, params: TradeTestParams = {}): Promise<HbmTSRoll> {
|
export async function smuggling(actor: CastableActor, params: TradeTestParams = {}): Promise<HbmTSRoll> {
|
||||||
const roll = await rollTrade(actor, 'Przemyt', params);
|
const roll = await rollTrade(actor, 'Przemyt', params);
|
||||||
if (!roll.ts?.isSuccess) {
|
if (!roll.ts?.isSuccess) {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export async function runPendingMigrations(): Promise<void> {
|
|||||||
await step.run();
|
await step.run();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`${SYSTEM_ID} | Migration ${step.version} failed`, err);
|
console.error(`${SYSTEM_ID} | Migration ${step.version} failed`, err);
|
||||||
ui.notifications?.error(`HbM migration ${step.version} failed — see console.`);
|
ui.notifications?.error(`HbM migration ${step.version} failed - see console.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Character sheet — Foundry v13 ApplicationV2 + Handlebars.
|
* Character sheet - Foundry v13 ApplicationV2 + Handlebars.
|
||||||
* Features: tabs, roll-dialogs, drag-and-drop item creation.
|
* Features: tabs, roll-dialogs, drag-and-drop item creation.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ import { askCastOptions } from '../dice/cast-dialog';
|
|||||||
import { askApplyDamage } from '../dice/damage-dialog';
|
import { askApplyDamage } from '../dice/damage-dialog';
|
||||||
import { applyDamage } from '../logic/damage';
|
import { applyDamage } from '../logic/damage';
|
||||||
import { rest } from '../logic/rest';
|
import { rest } from '../logic/rest';
|
||||||
import { ATTRIBUTES, AttributeKey, SKILL_KEYS } from '../constants';
|
import { ATTRIBUTES, AttributeKey, SKILL_KEYS, getMagicPowerEntry } from '../constants';
|
||||||
|
|
||||||
const { ActorSheetV2 } = foundry.applications.sheets as unknown as {
|
const { ActorSheetV2 } = foundry.applications.sheets as unknown as {
|
||||||
ActorSheetV2: typeof foundry.applications.sheets.ActorSheetV2;
|
ActorSheetV2: typeof foundry.applications.sheets.ActorSheetV2;
|
||||||
@@ -55,7 +55,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
main: { template: 'systems/hbm-rpg-v3/templates/actor/character.hbs' },
|
main: { template: 'systems/hbm-rpg-v3/templates/actor/character.hbs' },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Active tab per group — persists across re-renders. */
|
/** Active tab per group - persists across re-renders. */
|
||||||
tabGroups: Record<string, string> = { primary: 'stats' };
|
tabGroups: Record<string, string> = { primary: 'stats' };
|
||||||
|
|
||||||
override async _prepareContext(options: unknown) {
|
override async _prepareContext(options: unknown) {
|
||||||
@@ -125,7 +125,8 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
'alchemyTransmutation', 'alchemyBrewing', 'botany',
|
'alchemyTransmutation', 'alchemyBrewing', 'botany',
|
||||||
'elementsAir', 'elementsWater', 'elementsFire', 'elementsEarth',
|
'elementsAir', 'elementsWater', 'elementsFire', 'elementsEarth',
|
||||||
'artifice', 'golemancy', 'runes', 'manaSourceMage',
|
'artifice', 'golemancy', 'runes', 'manaSourceMage',
|
||||||
'illusion', 'sacred', 'sacredExorcism', 'witch', 'necromancy', 'blood'
|
'illusion', 'sacred', 'sacredExorcism', 'witch', 'necromancy', 'blood',
|
||||||
|
'crimson', 'abyssAspects', 'abyssPrimal', 'eldritch'
|
||||||
].map(key => ({
|
].map(key => ({
|
||||||
key,
|
key,
|
||||||
label: game.i18n.localize(`HBM.spellSchool.${key}`)
|
label: game.i18n.localize(`HBM.spellSchool.${key}`)
|
||||||
@@ -136,7 +137,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
const armors = gear.filter((it: any) => it.system?.category === 'armor');
|
const armors = gear.filter((it: any) => it.system?.category === 'armor');
|
||||||
const equipment = gear.filter((it: any) => it.system?.category === 'equipment');
|
const equipment = gear.filter((it: any) => it.system?.category === 'equipment');
|
||||||
|
|
||||||
// Localized talents list no longer needed (dropdown removed — drag from compendium)
|
// Localized talents list no longer needed (dropdown removed - drag from compendium)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...ctx,
|
...ctx,
|
||||||
@@ -211,7 +212,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
const actor = (this as unknown as { actor: any }).actor;
|
const actor = (this as unknown as { actor: any }).actor;
|
||||||
const uuid = data['uuid'] as string;
|
const uuid = data['uuid'] as string;
|
||||||
|
|
||||||
// Auto-link Race / Class items by UUID — do NOT create embedded copies.
|
// Auto-link Race / Class items by UUID - do NOT create embedded copies.
|
||||||
if (item.type === 'race') {
|
if (item.type === 'race') {
|
||||||
await actor.update({ 'system.details.raceId': uuid });
|
await actor.update({ 'system.details.raceId': uuid });
|
||||||
return;
|
return;
|
||||||
@@ -258,8 +259,8 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
type: 'button', action: 'confirm',
|
type: 'button', action: 'confirm',
|
||||||
label: game.i18n.localize('HBM.ui.confirm') || 'Potwierd\u017a',
|
label: game.i18n.localize('HBM.ui.confirm') || 'Potwierd\u017a',
|
||||||
default: true,
|
default: true,
|
||||||
callback: (_ev: Event, _btn: any, html: HTMLElement) => {
|
callback: (_ev: Event, _btn: any, dialog: any) => {
|
||||||
const val = (html.querySelector('#hbm-talent-param-input') as HTMLInputElement)?.value?.trim();
|
const val = (dialog.element.querySelector('#hbm-talent-param-input') as HTMLInputElement)?.value?.trim();
|
||||||
specified = val || null;
|
specified = val || null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -292,7 +293,11 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
// Determine default pool so the dialog can show it
|
// Determine default pool so the dialog can show it
|
||||||
const skill = actor.system.skills?.[skillKey];
|
const skill = actor.system.skills?.[skillKey];
|
||||||
const attrKey = (skill?.defaultAttribute ?? 'body') as AttributeKey;
|
const attrKey = (skill?.defaultAttribute ?? 'body') as AttributeKey;
|
||||||
const pool = (actor.system.attributes[attrKey]?.value ?? 0) + (skill?.value ?? 0);
|
const attr = actor.system.attributes[attrKey];
|
||||||
|
const attrVal = (attrKey === 'magic' && attr)
|
||||||
|
? (attr.dicePool ?? getMagicPowerEntry(attr.actual ?? attr.value ?? 0).dicePool)
|
||||||
|
: (attr?.value ?? 0);
|
||||||
|
const pool = attrVal + (skill?.value ?? 0);
|
||||||
|
|
||||||
const params = await askRollParams({ pool, flavor: game.i18n.format('HBM.roll.rollSkill', { skill: game.i18n.localize(`HBM.skills.${skillKey}`) }) });
|
const params = await askRollParams({ pool, flavor: game.i18n.format('HBM.roll.rollSkill', { skill: game.i18n.localize(`HBM.skills.${skillKey}`) }) });
|
||||||
if (!params) return;
|
if (!params) return;
|
||||||
@@ -300,6 +305,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
await rollSkill(actor, skillKey, {
|
await rollSkill(actor, skillKey, {
|
||||||
threshold: params.threshold,
|
threshold: params.threshold,
|
||||||
required: params.required,
|
required: params.required,
|
||||||
|
modifier: params.modifier,
|
||||||
flavor: params.flavor,
|
flavor: params.flavor,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -309,13 +315,17 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
if (!attr) return;
|
if (!attr) return;
|
||||||
const actor = (this as unknown as { actor: any }).actor;
|
const actor = (this as unknown as { actor: any }).actor;
|
||||||
|
|
||||||
const pool = actor.system.attributes[attr]?.value ?? 0;
|
const a = actor.system.attributes[attr];
|
||||||
|
const pool = (attr === 'magic' && a)
|
||||||
|
? (a.dicePool ?? getMagicPowerEntry(a.actual ?? a.value ?? 0).dicePool)
|
||||||
|
: (a?.value ?? 0);
|
||||||
const params = await askRollParams({ pool, flavor: game.i18n.format('HBM.roll.rollAttribute', { attribute: game.i18n.localize(`HBM.attributes.${attr}`) }) });
|
const params = await askRollParams({ pool, flavor: game.i18n.format('HBM.roll.rollAttribute', { attribute: game.i18n.localize(`HBM.attributes.${attr}`) }) });
|
||||||
if (!params) return;
|
if (!params) return;
|
||||||
|
|
||||||
await rollAttribute(actor, attr, {
|
await rollAttribute(actor, attr, {
|
||||||
threshold: params.threshold,
|
threshold: params.threshold,
|
||||||
required: params.required,
|
required: params.required,
|
||||||
|
modifier: params.modifier,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +377,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
const actor = (this as unknown as { actor: any }).actor;
|
const actor = (this as unknown as { actor: any }).actor;
|
||||||
const r = await rest(actor, 'breather');
|
const r = await rest(actor, 'breather');
|
||||||
await ChatMessage.create({
|
await ChatMessage.create({
|
||||||
content: `<strong>${actor.name}</strong> — Chwila Wytchnienia: +${r.hpRestored} ŻYW · +${r.manaRestored} MN`,
|
content: `<strong>${actor.name}</strong> - Chwila Wytchnienia: +${r.hpRestored} ŻYW · +${r.manaRestored} MN`,
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -376,7 +386,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
const actor = (this as unknown as { actor: any }).actor;
|
const actor = (this as unknown as { actor: any }).actor;
|
||||||
const r = await rest(actor, 'short');
|
const r = await rest(actor, 'short');
|
||||||
await ChatMessage.create({
|
await ChatMessage.create({
|
||||||
content: `<strong>${actor.name}</strong> — Krótki Odpoczynek: +${r.hpRestored} ŻYW · +${r.manaRestored} MN${r.toleranceRecovered ? `, −${r.toleranceRecovered} tolerancja` : ''}`,
|
content: `<strong>${actor.name}</strong> - Krótki Odpoczynek: +${r.hpRestored} ŻYW · +${r.manaRestored} MN${r.toleranceRecovered ? `, −${r.toleranceRecovered} tolerancja` : ''}`,
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -385,7 +395,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
const actor = (this as unknown as { actor: any }).actor;
|
const actor = (this as unknown as { actor: any }).actor;
|
||||||
const r = await rest(actor, 'long');
|
const r = await rest(actor, 'long');
|
||||||
await ChatMessage.create({
|
await ChatMessage.create({
|
||||||
content: `<strong>${actor.name}</strong> — Długi Odpoczynek: +${r.hpRestored} ŻYW · +${r.manaRestored} MN · +${r.zealRestored} ZP · +${r.bloodRestored} krew · −${r.toleranceRecovered} tolerancja`,
|
content: `<strong>${actor.name}</strong> - Długi Odpoczynek: +${r.hpRestored} ŻYW · +${r.manaRestored} MN · +${r.zealRestored} ZP · +${r.bloodRestored} krew · −${r.toleranceRecovered} tolerancja`,
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -562,8 +572,8 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
</div>
|
</div>
|
||||||
<script>window._hbmRates=${ratesJson};</script>`;
|
<script>window._hbmRates=${ratesJson};</script>`;
|
||||||
|
|
||||||
await (foundry.applications.api as any).DialogV2.inform({
|
await (foundry.applications.api as any).DialogV2.prompt({
|
||||||
title: game.i18n.localize('HBM.ui.moneyConverterTitle'),
|
window: { title: game.i18n.localize('HBM.ui.moneyConverterTitle') },
|
||||||
content,
|
content,
|
||||||
rejectClose: false,
|
rejectClose: false,
|
||||||
});
|
});
|
||||||
|
|||||||
+52
-10
@@ -1,11 +1,11 @@
|
|||||||
import { rollAttribute, rollInitiative } from '../dice/macros';
|
import { rollSkill, rollAttribute, rollInitiative } from '../dice/macros';
|
||||||
import { askRollParams } from '../dice/roll-dialog';
|
import { askRollParams } from '../dice/roll-dialog';
|
||||||
import { askApplyDamage } from '../dice/damage-dialog';
|
import { askApplyDamage } from '../dice/damage-dialog';
|
||||||
import { askCastOptions } from '../dice/cast-dialog';
|
import { askCastOptions } from '../dice/cast-dialog';
|
||||||
import { applyDamage } from '../logic/damage';
|
import { applyDamage } from '../logic/damage';
|
||||||
import { castSpell } from '../logic/spell-cast';
|
import { castSpell } from '../logic/spell-cast';
|
||||||
import { HbmTSRoll } from '../dice/ts-roll';
|
import { HbmTSRoll } from '../dice/ts-roll';
|
||||||
import { ATTRIBUTES, AttributeKey, TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD } from '../constants';
|
import { ATTRIBUTES, AttributeKey, SKILL_KEYS, TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD, getMagicPowerEntry } from '../constants';
|
||||||
|
|
||||||
const { ActorSheetV2 } = foundry.applications.sheets as unknown as {
|
const { ActorSheetV2 } = foundry.applications.sheets as unknown as {
|
||||||
ActorSheetV2: typeof foundry.applications.sheets.ActorSheetV2;
|
ActorSheetV2: typeof foundry.applications.sheets.ActorSheetV2;
|
||||||
@@ -20,6 +20,7 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
position: { width: 640, height: 720 },
|
position: { width: 640, height: 720 },
|
||||||
window: { resizable: true, title: 'HBM.actor.npc' },
|
window: { resizable: true, title: 'HBM.actor.npc' },
|
||||||
actions: {
|
actions: {
|
||||||
|
rollSkill: NpcSheet._onRollSkill,
|
||||||
rollAttribute: NpcSheet._onRollAttribute,
|
rollAttribute: NpcSheet._onRollAttribute,
|
||||||
rollInitiative: NpcSheet._onRollInitiative,
|
rollInitiative: NpcSheet._onRollInitiative,
|
||||||
rollNpcAttack: NpcSheet._onRollNpcAttack,
|
rollNpcAttack: NpcSheet._onRollNpcAttack,
|
||||||
@@ -73,7 +74,13 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ...ctx, system: actor.system, attributes, spells, actorEffects };
|
const skills = SKILL_KEYS.map(key => ({
|
||||||
|
key,
|
||||||
|
value: sys.skills?.[key]?.value ?? 0,
|
||||||
|
label: game.i18n.localize(`HBM.skills.${key}`)
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { ...ctx, system: actor.system, attributes, skills, spells, actorEffects };
|
||||||
}
|
}
|
||||||
|
|
||||||
override _onRender(context: unknown, options: unknown) {
|
override _onRender(context: unknown, options: unknown) {
|
||||||
@@ -124,8 +131,8 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
type: 'button', action: 'confirm',
|
type: 'button', action: 'confirm',
|
||||||
label: game.i18n.localize('HBM.ui.confirm') || 'Potwierd\u017a',
|
label: game.i18n.localize('HBM.ui.confirm') || 'Potwierd\u017a',
|
||||||
default: true,
|
default: true,
|
||||||
callback: (_ev: Event, _btn: any, html: HTMLElement) => {
|
callback: (_ev: Event, _btn: any, dialog: any) => {
|
||||||
const val = (html.querySelector('#hbm-talent-param-input') as HTMLInputElement)?.value?.trim();
|
const val = (dialog.element.querySelector('#hbm-talent-param-input') as HTMLInputElement)?.value?.trim();
|
||||||
specified = val || null;
|
specified = val || null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -148,17 +155,52 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
await actor.createEmbeddedDocuments('Item', [item.toObject()]);
|
await actor.createEmbeddedDocuments('Item', [item.toObject()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async _onRollSkill(this: NpcSheet, _event: PointerEvent, target: HTMLElement) {
|
||||||
|
const skillKey = target.dataset.skill;
|
||||||
|
if (!skillKey) return;
|
||||||
|
const actor = (this as unknown as { actor: any }).actor;
|
||||||
|
|
||||||
|
// Determine default pool so the dialog can show it
|
||||||
|
const skill = actor.system.skills?.[skillKey];
|
||||||
|
const attrKey = (skill?.defaultAttribute ?? 'body') as AttributeKey;
|
||||||
|
const attr = actor.system.attributes[attrKey];
|
||||||
|
const attrVal = (attrKey === 'magic' && attr)
|
||||||
|
? ((attr as any).dicePool ?? getMagicPowerEntry((attr as any).actual ?? attr.value ?? 0).dicePool)
|
||||||
|
: (attr?.value ?? 0);
|
||||||
|
const pool = attrVal + (skill?.value ?? 0);
|
||||||
|
|
||||||
|
const params = await askRollParams({
|
||||||
|
pool,
|
||||||
|
flavor: game.i18n.format('HBM.roll.rollSkill', { skill: game.i18n.localize(`HBM.skills.${skillKey}`) })
|
||||||
|
});
|
||||||
|
if (!params) return;
|
||||||
|
|
||||||
|
await rollSkill(actor, skillKey, {
|
||||||
|
threshold: params.threshold,
|
||||||
|
required: params.required,
|
||||||
|
modifier: params.modifier,
|
||||||
|
flavor: params.flavor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static async _onRollAttribute(this: NpcSheet, _event: PointerEvent, target: HTMLElement) {
|
static async _onRollAttribute(this: NpcSheet, _event: PointerEvent, target: HTMLElement) {
|
||||||
const attr = target.dataset.attribute as AttributeKey | undefined;
|
const attr = target.dataset.attribute as AttributeKey | undefined;
|
||||||
if (!attr) return;
|
if (!attr) return;
|
||||||
const actor = (this as unknown as { actor: any }).actor;
|
const actor = (this as unknown as { actor: any }).actor;
|
||||||
const pool = actor.system.attributes[attr]?.value ?? 0;
|
const a = actor.system.attributes[attr];
|
||||||
|
const pool = (attr === 'magic' && a)
|
||||||
|
? ((a as any).dicePool ?? getMagicPowerEntry((a as any).actual ?? a.value ?? 0).dicePool)
|
||||||
|
: (a?.value ?? 0);
|
||||||
const params = await askRollParams({
|
const params = await askRollParams({
|
||||||
pool,
|
pool,
|
||||||
flavor: game.i18n.format('HBM.roll.rollAttribute', { attribute: game.i18n.localize(`HBM.attributes.${attr}`) }),
|
flavor: game.i18n.format('HBM.roll.rollAttribute', { attribute: game.i18n.localize(`HBM.attributes.${attr}`) }),
|
||||||
});
|
});
|
||||||
if (!params) return;
|
if (!params) return;
|
||||||
await rollAttribute(actor, attr, { threshold: params.threshold, required: params.required });
|
await rollAttribute(actor, attr, {
|
||||||
|
threshold: params.threshold,
|
||||||
|
required: params.required,
|
||||||
|
modifier: params.modifier,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static async _onRollInitiative(this: NpcSheet, _event: PointerEvent, _target: HTMLElement) {
|
static async _onRollInitiative(this: NpcSheet, _event: PointerEvent, _target: HTMLElement) {
|
||||||
@@ -177,7 +219,7 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
flavor: attack.name,
|
flavor: attack.name,
|
||||||
});
|
});
|
||||||
if (!params) return;
|
if (!params) return;
|
||||||
const flavor = `${attack.name}${attack.damage ? ` — ${attack.damage}` : ''}`;
|
const flavor = `${attack.name}${attack.damage ? ` - ${attack.damage}` : ''}`;
|
||||||
const roll = HbmTSRoll.fromParams({
|
const roll = HbmTSRoll.fromParams({
|
||||||
pool: params.pool,
|
pool: params.pool,
|
||||||
threshold: params.threshold ?? TS_DEFAULT_THRESHOLD,
|
threshold: params.threshold ?? TS_DEFAULT_THRESHOLD,
|
||||||
@@ -399,8 +441,8 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await (foundry.applications.api as any).DialogV2.inform({
|
await (foundry.applications.api as any).DialogV2.prompt({
|
||||||
title: game.i18n.localize('HBM.ui.moneyConverterTitle'),
|
window: { title: game.i18n.localize('HBM.ui.moneyConverterTitle') },
|
||||||
content: content,
|
content: content,
|
||||||
rejectClose: false,
|
rejectClose: false,
|
||||||
});
|
});
|
||||||
|
|||||||
+78
-5
@@ -1,5 +1,5 @@
|
|||||||
/* ═══════════════════════════════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════════════════════════════
|
||||||
Homebrew Magic: RPG v3 — Foundry VTT system styles
|
Homebrew Magic: RPG v3 - Foundry VTT system styles
|
||||||
═══════════════════════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
/* ── CSS custom properties ─────────────────────────────────────────────── */
|
/* ── CSS custom properties ─────────────────────────────────────────────── */
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme overrides */
|
/* Dark theme overrides */
|
||||||
.theme-dark .hbm,
|
.theme-dark .hbm:not(.theme-light),
|
||||||
.hbm.theme-dark {
|
.hbm.theme-dark:not(.theme-light) {
|
||||||
--hbm-fg: #e0d8c3;
|
--hbm-fg: #e0d8c3;
|
||||||
--hbm-bg: #1a1712;
|
--hbm-bg: #1a1712;
|
||||||
--hbm-bg-header: #0f0b07;
|
--hbm-bg-header: #0f0b07;
|
||||||
@@ -45,9 +45,12 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: 'Signika', sans-serif;
|
font-family: 'Signika', sans-serif;
|
||||||
color: var(--hbm-fg);
|
color: var(--hbm-fg);
|
||||||
background: var(--hbm-bg);
|
background: var(--hbm-bg) !important;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.hbm.sheet .window-content {
|
||||||
|
background: var(--hbm-bg) !important;
|
||||||
|
}
|
||||||
.hbm.sheet form,
|
.hbm.sheet form,
|
||||||
.hbm.sheet .sheet-wrapper {
|
.hbm.sheet .sheet-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -235,7 +238,54 @@
|
|||||||
}
|
}
|
||||||
.hbm .resource-value .sep { color: var(--hbm-border); }
|
.hbm .resource-value .sep { color: var(--hbm-border); }
|
||||||
.hbm .resource .static-value { font-size: 1.1rem; font-weight: 700; }
|
.hbm .resource .static-value { font-size: 1.1rem; font-weight: 700; }
|
||||||
.hbm .resource small { font-size: 0.7rem; color: #666; }
|
|
||||||
|
/* ── Advancement panel ─────────────────────────────────────────────────── */
|
||||||
|
.hbm .advancement-panel {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--hbm-card-bg);
|
||||||
|
border: 1px solid var(--hbm-border);
|
||||||
|
border-radius: var(--hbm-radius);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.hbm .advancement-panel .advancement-title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--hbm-accent);
|
||||||
|
border-bottom: 1px solid var(--hbm-border);
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
.hbm .advancement-panel .advancement-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1.5rem;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
.hbm .advancement-panel .advancement-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.hbm .advancement-panel .advancement-field label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.hbm .advancement-panel .advancement-field input[type='number'] {
|
||||||
|
width: 4rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--hbm-border);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--hbm-fg);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Skills grid ────────────────────────────────────────────────────────── */
|
/* ── Skills grid ────────────────────────────────────────────────────────── */
|
||||||
.hbm .skills-grid {
|
.hbm .skills-grid {
|
||||||
@@ -351,6 +401,29 @@
|
|||||||
background: var(--hbm-card-bg);
|
background: var(--hbm-card-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Item Sheet Textareas ───────────────────────────────────────────────── */
|
||||||
|
.hbm.item-body textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 12rem;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border: 1px solid var(--hbm-border);
|
||||||
|
border-radius: var(--hbm-radius);
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: var(--hbm-card-bg);
|
||||||
|
color: var(--hbm-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary/smaller textareas */
|
||||||
|
.hbm.item-body textarea[name="system.physicalDescription"],
|
||||||
|
.hbm.item-body textarea[name="system.mechanics"],
|
||||||
|
.hbm.item-body textarea[name="system.effect"],
|
||||||
|
.hbm.item-body textarea[name="system.higherCircles"],
|
||||||
|
.hbm.item-body textarea[name="system.passiveAbility"] {
|
||||||
|
min-height: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Dialog form ────────────────────────────────────────────────────────── */
|
/* ── Dialog form ────────────────────────────────────────────────────────── */
|
||||||
.hbm-dialog .form-group {
|
.hbm-dialog .form-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+5
-17
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"id": "hbm-rpg-v3",
|
"id": "hbm-rpg-v3",
|
||||||
"title": "Homebrew Magic: RPG v3",
|
"title": "Homebrew Magic: RPG v3",
|
||||||
"description": "System Foundry VTT dla Homebrew Magic: Role Playing Game v3 — autorska gra fabularna z mechaniką puli kości d6 (TS — Trudność:Sukcesy).",
|
"description": "System Foundry VTT dla Homebrew Magic: Role Playing Game v3 - autorska gra fabularna z mechaniką puli kości d6 (TS - Trudność:Sukcesy).",
|
||||||
"version": "1.3.12",
|
"version": "1.3.30",
|
||||||
"manifest": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3.json",
|
"manifest": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3.json",
|
||||||
"download": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3-v1.3.12.zip",
|
"download": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3-v1.3.30.zip",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "13",
|
"minimum": "13",
|
||||||
"verified": "14"
|
"verified": "14"
|
||||||
@@ -211,7 +211,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "talents-blood",
|
"name": "talents-blood",
|
||||||
"label": "Talenty — Magia Krwi",
|
"label": "Talenty - Magia Krwi",
|
||||||
"system": "hbm-rpg-v3",
|
"system": "hbm-rpg-v3",
|
||||||
"path": "packs/talents-blood",
|
"path": "packs/talents-blood",
|
||||||
"type": "Item",
|
"type": "Item",
|
||||||
@@ -229,19 +229,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "talents-eldritch",
|
"name": "talents-eldritch",
|
||||||
"label": "Talents - Eldritch",
|
"label": "Talenty - Klątwa Otchłani",
|
||||||
"system": "hbm",
|
|
||||||
"path": "packs/talents-eldritch",
|
|
||||||
"type": "Item",
|
|
||||||
"ownership": {
|
|
||||||
"PLAYER": "OBSERVER",
|
|
||||||
"ASSISTANT": "OWNER"
|
|
||||||
},
|
|
||||||
"flags": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "talents-eldritch",
|
|
||||||
"label": "Talenty — Klątwa Otchłani",
|
|
||||||
"system": "hbm-rpg-v3",
|
"system": "hbm-rpg-v3",
|
||||||
"path": "packs/talents-eldritch",
|
"path": "packs/talents-eldritch",
|
||||||
"type": "Item",
|
"type": "Item",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{{!-- Actor-level Active Effects section — included by character and NPC sheets. --}}
|
{{!-- Actor-level Active Effects section - included by character and NPC sheets. --}}
|
||||||
<fieldset class="hbm-effects actor-effects">
|
<fieldset class="hbm-effects actor-effects">
|
||||||
<legend>{{localize 'HBM.activeEffect.title'}}</legend>
|
<legend>{{localize 'HBM.activeEffect.title'}}</legend>
|
||||||
<button type="button" class="hbm-effect-add" data-action="actorEffectCreate">+ {{localize 'HBM.activeEffect.addEffect'}}</button>
|
<button type="button" class="hbm-effect-add" data-action="actorEffectCreate">+ {{localize 'HBM.activeEffect.addEffect'}}</button>
|
||||||
|
|||||||
+287
-161
@@ -1,13 +1,15 @@
|
|||||||
<div class="sheet-wrapper">
|
<div class="sheet-wrapper">
|
||||||
<!-- ═══ STICKY HEADER ═══════════════════════════════════════════════════ -->
|
<!-- ═══ STICKY HEADER ═══════════════════════════════════════════════════ -->
|
||||||
<header class="sheet-header">
|
<header class="sheet-header">
|
||||||
<input class="charname" type="text" name="name" value="{{document.name}}" placeholder="{{localize 'HBM.ui.name'}}" />
|
<input class="charname" type="text" name="name" value="{{document.name}}"
|
||||||
|
placeholder="{{localize 'HBM.ui.name'}}" />
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
<label>{{localize 'HBM.details.race'}}
|
<label>{{localize 'HBM.details.race'}}
|
||||||
<select name="system.details.race">
|
<select name="system.details.race">
|
||||||
<option value="">--</option>
|
<option value="">--</option>
|
||||||
{{#each races as |r|}}
|
{{#each races as |r|}}
|
||||||
<option value="{{r.key}}" {{#if (eq ../system.details.race r.key)}}selected{{/if}}>{{r.label}}</option>
|
<option value="{{r.key}}" {{#if (eq ../system.details.race r.key)}}selected{{/if}}>{{r.label}}
|
||||||
|
</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -15,16 +17,19 @@
|
|||||||
<input type="number" name="system.details.year" value="{{system.details.year}}" min="1" max="4" />
|
<input type="number" name="system.details.year" value="{{system.details.year}}" min="1" max="4" />
|
||||||
</label>
|
</label>
|
||||||
<label>{{localize 'HBM.details.startingYear'}}
|
<label>{{localize 'HBM.details.startingYear'}}
|
||||||
<input type="number" name="system.details.startingYear" value="{{system.details.startingYear}}" min="1900" />
|
<input type="number" name="system.details.startingYear" value="{{system.details.startingYear}}"
|
||||||
|
min="1900" />
|
||||||
</label>
|
</label>
|
||||||
<label>{{localize 'HBM.details.currentYear'}}
|
<label>{{localize 'HBM.details.currentYear'}}
|
||||||
<input type="number" name="system.details.currentYear" value="{{system.details.currentYear}}" min="1900" />
|
<input type="number" name="system.details.currentYear" value="{{system.details.currentYear}}"
|
||||||
|
min="1900" />
|
||||||
</label>
|
</label>
|
||||||
<label>{{localize 'HBM.details.discipline'}}
|
<label>{{localize 'HBM.details.discipline'}}
|
||||||
<select name="system.details.discipline">
|
<select name="system.details.discipline">
|
||||||
<option value="">--</option>
|
<option value="">--</option>
|
||||||
{{#each disciplines as |d|}}
|
{{#each disciplines as |d|}}
|
||||||
<option value="{{d.key}}" {{#if (eq ../system.details.discipline d.key)}}selected{{/if}}>{{d.label}}</option>
|
<option value="{{d.key}}" {{#if (eq ../system.details.discipline d.key)}}selected{{/if}}>{{d.label}}
|
||||||
|
</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -63,12 +68,13 @@
|
|||||||
<option value="9" {{#if (eq attr.value 9)}}selected{{/if}}>IX</option>
|
<option value="9" {{#if (eq attr.value 9)}}selected{{/if}}>IX</option>
|
||||||
<option value="10" {{#if (eq attr.value 10)}}selected{{/if}}>X</option>
|
<option value="10" {{#if (eq attr.value 10)}}selected{{/if}}>X</option>
|
||||||
</select>
|
</select>
|
||||||
<span class="actual-magic-badge" title="{{localize 'HBM.attributes.actualMagicHint'}}" style="font-size: 0.75rem; margin-top: 0.15rem; color: #a03030; font-weight: bold;">
|
<span class="actual-magic-badge" title="{{localize 'HBM.attributes.actualMagicHint'}}"
|
||||||
|
style="font-size: 0.75rem; margin-top: 0.15rem; color: #a03030; font-weight: bold;">
|
||||||
({{localize 'HBM.attributes.actual'}}: {{attr.actualLabel}})
|
({{localize 'HBM.attributes.actual'}}: {{attr.actualLabel}})
|
||||||
</span>
|
</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
<input type="number" name="system.attributes.{{attr.key}}.value" value="{{attr.value}}"
|
<input type="number" name="system.attributes.{{attr.key}}.value" value="{{attr.value}}" min="1"
|
||||||
min="1" max="8" />
|
max="9" />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<button type="button" class="roll-btn" data-action="rollAttribute" data-attribute="{{attr.key}}"
|
<button type="button" class="roll-btn" data-action="rollAttribute" data-attribute="{{attr.key}}"
|
||||||
title="{{localize 'HBM.roll.roll'}}">⚂</button>
|
title="{{localize 'HBM.roll.roll'}}">⚂</button>
|
||||||
@@ -80,15 +86,18 @@
|
|||||||
<div class="resource">
|
<div class="resource">
|
||||||
<label>{{localize 'HBM.resources.health'}}</label>
|
<label>{{localize 'HBM.resources.health'}}</label>
|
||||||
<span class="resource-value">
|
<span class="resource-value">
|
||||||
<input type="number" name="system.attributes.health.value" value="{{system.attributes.health.value}}" min="0" />
|
<input type="number" name="system.attributes.health.value"
|
||||||
|
value="{{system.attributes.health.value}}" min="0" />
|
||||||
<span class="sep">/</span>
|
<span class="sep">/</span>
|
||||||
<input type="number" name="system.attributes.health.max" value="{{system.attributes.health.max}}" min="0" />
|
<input type="number" name="system.attributes.health.max"
|
||||||
|
value="{{system.attributes.health.max}}" min="0" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="resource">
|
<div class="resource">
|
||||||
<label>{{localize 'HBM.resources.physicalArmor'}}</label>
|
<label>{{localize 'HBM.resources.physicalArmor'}}</label>
|
||||||
<span class="resource-value">
|
<span class="resource-value">
|
||||||
<span class="static-value" title="{{localize 'HBM.resources.physicalArmor'}}">{{system.attributes.physicalArmor.value}}</span>
|
<span class="static-value"
|
||||||
|
title="{{localize 'HBM.resources.physicalArmor'}}">{{system.attributes.physicalArmor.value}}</span>
|
||||||
</span>
|
</span>
|
||||||
<small>{{localize 'HBM.resources.max'}}: {{system.attributes.physicalArmor.max}}</small>
|
<small>{{localize 'HBM.resources.max'}}: {{system.attributes.physicalArmor.max}}</small>
|
||||||
<div style="display:flex; align-items:center; gap:0.3rem; margin-top:0.15rem;">
|
<div style="display:flex; align-items:center; gap:0.3rem; margin-top:0.15rem;">
|
||||||
@@ -111,42 +120,50 @@
|
|||||||
<input type="number" name="system.attributes.magicalArmor.value"
|
<input type="number" name="system.attributes.magicalArmor.value"
|
||||||
value="{{system.attributes.magicalArmor.value}}" min="0" />
|
value="{{system.attributes.magicalArmor.value}}" min="0" />
|
||||||
<span class="sep">/</span>
|
<span class="sep">/</span>
|
||||||
<input type="number" name="system.attributes.magicalArmor.max" value="{{system.attributes.magicalArmor.max}}"
|
<input type="number" name="system.attributes.magicalArmor.max"
|
||||||
min="0" />
|
value="{{system.attributes.magicalArmor.max}}" min="0" />
|
||||||
</span>
|
</span>
|
||||||
<small>{{localize 'HBM.resources.runicCounter'}}: {{system.attributes.magicalArmor.runicCounter}}/5</small>
|
<small>{{localize 'HBM.resources.runicCounter'}}:
|
||||||
|
{{system.attributes.magicalArmor.runicCounter}}/5</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="resource">
|
<div class="resource">
|
||||||
<label>{{localize 'HBM.resources.mana'}}</label>
|
<label>{{localize 'HBM.resources.mana'}}</label>
|
||||||
<span class="resource-value">
|
<span class="resource-value">
|
||||||
<input type="number" name="system.attributes.mana.value" value="{{system.attributes.mana.value}}" min="0" />
|
<input type="number" name="system.attributes.mana.value"
|
||||||
|
value="{{system.attributes.mana.value}}" min="0" />
|
||||||
<span class="sep">/</span>
|
<span class="sep">/</span>
|
||||||
<input type="number" name="system.attributes.mana.max" value="{{system.attributes.mana.max}}" min="0" />
|
<input type="number" name="system.attributes.mana.max" value="{{system.attributes.mana.max}}"
|
||||||
|
min="0" />
|
||||||
</span>
|
</span>
|
||||||
<small>{{localize 'HBM.resources.maxManaPerSpell'}}: {{system.attributes.mana.maxPerSpell}}</small>
|
<small>{{localize 'HBM.resources.maxManaPerSpell'}}: {{system.attributes.mana.maxPerSpell}}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="resource">
|
<div class="resource">
|
||||||
<label>{{localize 'HBM.resources.zeal'}}</label>
|
<label>{{localize 'HBM.resources.zeal'}}</label>
|
||||||
<span class="resource-value">
|
<span class="resource-value">
|
||||||
<input type="number" name="system.attributes.zeal.value" value="{{system.attributes.zeal.value}}" min="0" />
|
<input type="number" name="system.attributes.zeal.value"
|
||||||
|
value="{{system.attributes.zeal.value}}" min="0" />
|
||||||
<span class="sep">/</span>
|
<span class="sep">/</span>
|
||||||
<input type="number" name="system.attributes.zeal.max" value="{{system.attributes.zeal.max}}" min="0" />
|
<input type="number" name="system.attributes.zeal.max" value="{{system.attributes.zeal.max}}"
|
||||||
|
min="0" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{#if hasBloodMagic}}
|
{{#if hasBloodMagic}}
|
||||||
<div class="resource blood-pool">
|
<div class="resource blood-pool">
|
||||||
<label>{{localize 'HBM.resources.blood'}}</label>
|
<label>{{localize 'HBM.resources.blood'}}</label>
|
||||||
<span class="resource-value">
|
<span class="resource-value">
|
||||||
<input type="number" name="system.attributes.blood.value" value="{{system.attributes.blood.value}}" min="0" />
|
<input type="number" name="system.attributes.blood.value"
|
||||||
|
value="{{system.attributes.blood.value}}" min="0" />
|
||||||
<span class="sep">/</span>
|
<span class="sep">/</span>
|
||||||
<input type="number" name="system.attributes.blood.max" value="{{system.attributes.blood.max}}" min="0" />
|
<input type="number" name="system.attributes.blood.max" value="{{system.attributes.blood.max}}"
|
||||||
|
min="0" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div class="resource">
|
<div class="resource">
|
||||||
<label>{{localize 'HBM.resources.elixirTolerance'}}</label>
|
<label>{{localize 'HBM.resources.elixirTolerance'}}</label>
|
||||||
<span class="resource-value">
|
<span class="resource-value">
|
||||||
<input type="number" name="system.attributes.elixirTolerance" value="{{system.attributes.elixirTolerance}}" min="0" />
|
<input type="number" name="system.attributes.elixirTolerance"
|
||||||
|
value="{{system.attributes.elixirTolerance}}" min="0" />
|
||||||
</span>
|
</span>
|
||||||
<small>cap: {{elixirCap}}</small>
|
<small>cap: {{elixirCap}}</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,41 +171,44 @@
|
|||||||
<div class="resource warn">
|
<div class="resource warn">
|
||||||
<label>{{localize 'HBM.resources.insanity'}}</label>
|
<label>{{localize 'HBM.resources.insanity'}}</label>
|
||||||
<span class="resource-value">
|
<span class="resource-value">
|
||||||
<input type="number" name="system.attributes.insanity" value="{{system.attributes.insanity}}" min="0" />
|
<input type="number" name="system.attributes.insanity" value="{{system.attributes.insanity}}"
|
||||||
|
min="0" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div class="resource">
|
<div class="resource">
|
||||||
<label>{{localize 'HBM.resources.initiative'}}</label>
|
<label>{{localize 'HBM.resources.initiative'}}</label>
|
||||||
<span class="static-value">{{system.attributes.initiative}}</span>
|
<span class="resource-value">
|
||||||
|
<span class="static-value"
|
||||||
|
title="{{localize 'HBM.resources.initiative'}}">{{system.attributes.initiative.value}}</span>
|
||||||
|
</span>
|
||||||
|
<div style="display:flex; align-items:center; gap:0.3rem; margin-top:0.15rem;">
|
||||||
|
<small style="white-space:nowrap;">{{localize 'HBM.ui.initiativeBonus'}}:</small>
|
||||||
|
<input type="number" name="system.attributes.initiative.bonus"
|
||||||
|
value="{{system.attributes.initiative.bonus}}" min="0"
|
||||||
|
style="width:2.5rem; text-align:center; font-size:0.8rem; border:none; border-bottom:1px solid var(--hbm-border); background:transparent;" />
|
||||||
</div>
|
</div>
|
||||||
<div class="resource">
|
</div> </div>
|
||||||
|
|
||||||
|
<div class="advancement-panel">
|
||||||
|
<div class="advancement-title">{{localize 'HBM.ui.advancement'}}</div>
|
||||||
|
<div class="advancement-fields">
|
||||||
|
<div class="advancement-field">
|
||||||
<label>{{localize 'HBM.resources.experience'}}</label>
|
<label>{{localize 'HBM.resources.experience'}}</label>
|
||||||
<input type="number" name="system.details.experience" value="{{system.details.experience}}" min="0" />
|
<input type="number" name="system.details.experience" value="{{system.details.experience}}" min="0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="resource">
|
<div class="advancement-field">
|
||||||
<label>{{localize 'HBM.ui.attributePoints'}}</label>
|
<label>{{localize 'HBM.ui.attributePoints'}}</label>
|
||||||
<span class="resource-value">
|
<input type="number" name="system.advancement.attributePointsAvailable" value="{{system.advancement.attributePointsAvailable}}" min="0" />
|
||||||
<span class="static-value" title="{{localize 'HBM.ui.pointsAvailable'}}">{{system.advancement.attributePointsAvailable}}</span>
|
|
||||||
<span class="sep" title="Modyfikator MG">+</span>
|
|
||||||
<input type="number" name="system.advancement.bonusAttributePoints" value="{{system.advancement.bonusAttributePoints}}" min="0" style="width: 2.2rem; text-align: center; border: none; border-bottom: 1px solid var(--hbm-border); background: transparent;" />
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="resource">
|
<div class="advancement-field">
|
||||||
<label>{{localize 'HBM.ui.skillPoints'}}</label>
|
<label>{{localize 'HBM.ui.skillPoints'}}</label>
|
||||||
<span class="resource-value">
|
<input type="number" name="system.advancement.skillPointsAvailable" value="{{system.advancement.skillPointsAvailable}}" min="0" />
|
||||||
<span class="static-value" title="{{localize 'HBM.ui.pointsAvailable'}}">{{system.advancement.skillPointsAvailable}}</span>
|
|
||||||
<span class="sep" title="Modyfikator MG">+</span>
|
|
||||||
<input type="number" name="system.advancement.bonusSkillPoints" value="{{system.advancement.bonusSkillPoints}}" min="0" style="width: 2.2rem; text-align: center; border: none; border-bottom: 1px solid var(--hbm-border); background: transparent;" />
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="resource">
|
<div class="advancement-field">
|
||||||
<label>{{localize 'HBM.ui.freeTalents'}}</label>
|
<label>{{localize 'HBM.ui.freeTalents'}}</label>
|
||||||
<span class="resource-value">
|
<input type="number" name="system.advancement.freeTalentsAvailable" value="{{system.advancement.freeTalentsAvailable}}" min="0" />
|
||||||
<span class="static-value" title="{{localize 'HBM.ui.pointsAvailable'}}">{{system.advancement.freeTalentsAvailable}}</span>
|
</div>
|
||||||
<span class="sep" title="Modyfikator MG">+</span>
|
|
||||||
<input type="number" name="system.advancement.bonusFreeTalents" value="{{system.advancement.bonusFreeTalents}}" min="0" style="width: 2.2rem; text-align: center; border: none; border-bottom: 1px solid var(--hbm-border); background: transparent;" />
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -247,25 +267,30 @@
|
|||||||
<li class="item-row spell-row" data-school="{{spell.system.school}}">
|
<li class="item-row spell-row" data-school="{{spell.system.school}}">
|
||||||
<span class="item-name">{{spell.name}}
|
<span class="item-name">{{spell.name}}
|
||||||
{{#if spell.system.isSuperspell}}<span class="badge super" title="Superczar">★</span>{{/if}}
|
{{#if spell.system.isSuperspell}}<span class="badge super" title="Superczar">★</span>{{/if}}
|
||||||
{{#if spell.system.requiresGroupCast}}<span class="badge group" title="Czar grupowy">⛧</span>{{/if}}
|
{{#if spell.system.requiresGroupCast}}<span class="badge group"
|
||||||
{{#if spell.system.nonCombatOnly}}<span class="badge noncombat" title="Tylko poza walką">☮</span>{{/if}}
|
title="Czar grupowy">⛧</span>{{/if}}
|
||||||
{{#if spell.system.requirements.race.length}}<span class="badge race-lock" title="Wymagana rasa">🔒</span>{{/if}}
|
{{#if spell.system.nonCombatOnly}}<span class="badge noncombat"
|
||||||
|
title="Tylko poza walką">☮</span>{{/if}}
|
||||||
|
{{#if spell.system.requirements.race.length}}<span class="badge race-lock"
|
||||||
|
title="Wymagana rasa">🔒</span>{{/if}}
|
||||||
</span>
|
</span>
|
||||||
<span class="item-meta">
|
<span class="item-meta">
|
||||||
K{{spell.system.circle}}
|
K{{spell.system.circle}}
|
||||||
· {{localize (concat 'HBM.spell.castingMode.' spell.system.castingMode)}}
|
· {{localize (concat 'HBM.spell.castingMode.' spell.system.castingMode)}}
|
||||||
· {{localize 'HBM.resources.mana'}}: {{spell.system.manaCost}}
|
· {{localize 'HBM.resources.mana'}}: {{spell.system.manaCost}}
|
||||||
{{#if (eq spell.system.castingMode 'sacred')}}{{#if spell.system.deity}} · {{spell.system.deity}}{{/if}}{{/if}}
|
{{#if (eq spell.system.castingMode 'sacred')}}{{#if spell.system.deity}} ·
|
||||||
|
{{spell.system.deity}}{{/if}}{{/if}}
|
||||||
{{#if (eq spell.system.castingMode 'blood')}} · krew: {{spell.system.bloodCost}}{{/if}}
|
{{#if (eq spell.system.castingMode 'blood')}} · krew: {{spell.system.bloodCost}}{{/if}}
|
||||||
{{#if spell.system.sourceBook}} · <span class="source-pip">{{spell.system.sourceBook}}</span>{{/if}}
|
{{#if spell.system.sourceBook}} · <span
|
||||||
|
class="source-pip">{{spell.system.sourceBook}}</span>{{/if}}
|
||||||
</span>
|
</span>
|
||||||
<div class="item-controls">
|
<div class="item-controls">
|
||||||
<button type="button" class="cast-btn" data-action="castSpell" data-item-id="{{spell.id}}"
|
<button type="button" class="cast-btn" data-action="castSpell" data-item-id="{{spell.id}}"
|
||||||
title="{{localize 'HBM.spell.cast'}}">✦</button>
|
title="{{localize 'HBM.spell.cast'}}">✦</button>
|
||||||
<button type="button" class="edit-btn" data-action="editItem" data-item-id="{{spell.id}}"
|
<button type="button" class="edit-btn" data-action="editItem" data-item-id="{{spell.id}}"
|
||||||
title="{{localize 'HBM.ui.edit'}}">✎</button>
|
title="{{localize 'HBM.ui.edit'}}">✎</button>
|
||||||
<button type="button" class="delete-btn" data-action="deleteItem" data-item-id="{{spell.id}}"
|
<button type="button" class="delete-btn" data-action="deleteItem"
|
||||||
title="{{localize 'HBM.ui.delete'}}">✕</button>
|
data-item-id="{{spell.id}}" title="{{localize 'HBM.ui.delete'}}">✕</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
@@ -279,7 +304,8 @@
|
|||||||
|
|
||||||
<!-- ── TALENTS ────────────────────────────────────────────────────────── -->
|
<!-- ── TALENTS ────────────────────────────────────────────────────────── -->
|
||||||
<section class="tab-panel {{#if (eq tabGroups.primary 'talents')}}active{{/if}}" data-tab="talents">
|
<section class="tab-panel {{#if (eq tabGroups.primary 'talents')}}active{{/if}}" data-tab="talents">
|
||||||
<p class="empty-drop" style="font-size:0.8rem; color:#888; margin-bottom:10px;">{{localize 'HBM.ui.dropTalentsHint'}}</p>
|
<p class="empty-drop" style="font-size:0.8rem; color:#888; margin-bottom:10px;">{{localize
|
||||||
|
'HBM.ui.dropTalentsHint'}}</p>
|
||||||
{{#if talents.length}}
|
{{#if talents.length}}
|
||||||
<ul class="item-list">
|
<ul class="item-list">
|
||||||
{{#each talents as |item|}}
|
{{#each talents as |item|}}
|
||||||
@@ -302,14 +328,17 @@
|
|||||||
|
|
||||||
<!-- ── INVENTORY ──────────────────────────────────────────────────────── -->
|
<!-- ── INVENTORY ──────────────────────────────────────────────────────── -->
|
||||||
<section class="tab-panel {{#if (eq tabGroups.primary 'inventory')}}active{{/if}}" data-tab="inventory">
|
<section class="tab-panel {{#if (eq tabGroups.primary 'inventory')}}active{{/if}}" data-tab="inventory">
|
||||||
<div class="money-panel" style="display: flex; gap: 15px; align-items: center; background: var(--hbm-card-bg); border: 1px solid var(--hbm-border); border-radius: var(--hbm-radius); padding: 0.75rem; margin-bottom: 15px;">
|
<div class="money-panel"
|
||||||
|
style="display: flex; gap: 15px; align-items: center; background: var(--hbm-card-bg); border: 1px solid var(--hbm-border); border-radius: var(--hbm-radius); padding: 0.75rem; margin-bottom: 15px;">
|
||||||
<div style="flex: 1; display: flex; align-items: center; gap: 10px;">
|
<div style="flex: 1; display: flex; align-items: center; gap: 10px;">
|
||||||
<span class="icon" style="font-size: 1.5rem;">🪙</span>
|
<span class="icon" style="font-size: 1.5rem;">🪙</span>
|
||||||
<strong>{{localize 'HBM.ui.money'}}:</strong>
|
<strong>{{localize 'HBM.ui.money'}}:</strong>
|
||||||
<input type="number" name="system.details.money" value="{{system.details.money}}" min="0" style="width: 8rem; font-size: 1.1rem; text-align: center; border: none; border-bottom: 1px solid var(--hbm-border); background: transparent; color: var(--hbm-fg);" />
|
<input type="number" name="system.details.money" value="{{system.details.money}}" min="0"
|
||||||
|
style="width: 8rem; font-size: 1.1rem; text-align: center; border: none; border-bottom: 1px solid var(--hbm-border); background: transparent; color: var(--hbm-fg);" />
|
||||||
<span>PLN</span>
|
<span>PLN</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" data-action="recalculateMoney" style="padding: 4px 10px; font-weight: bold; background: var(--hbm-accent); color: #fff; border: none; border-radius: var(--hbm-radius); cursor: pointer;">
|
<button type="button" data-action="recalculateMoney"
|
||||||
|
style="padding: 4px 10px; font-weight: bold; background: var(--hbm-accent); color: #fff; border: none; border-radius: var(--hbm-radius); cursor: pointer;">
|
||||||
{{localize 'HBM.ui.recalculate'}}
|
{{localize 'HBM.ui.recalculate'}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -332,15 +361,19 @@
|
|||||||
{{#each weapons as |item|}}
|
{{#each weapons as |item|}}
|
||||||
<tr class="item-tr" data-item-id="{{item.id}}">
|
<tr class="item-tr" data-item-id="{{item.id}}">
|
||||||
<td><strong>{{item.name}}</strong></td>
|
<td><strong>{{item.name}}</strong></td>
|
||||||
<td style="text-align: center;">{{item.system.weapon.damage}} ({{localize (concat 'HBM.damageType.' item.system.weapon.damageType)}})</td>
|
<td style="text-align: center;">{{item.system.weapon.damage}} ({{localize (concat
|
||||||
|
'HBM.damageType.' item.system.weapon.damageType)}})</td>
|
||||||
<td style="text-align: center;">{{item.system.weight}}</td>
|
<td style="text-align: center;">{{item.system.weight}}</td>
|
||||||
<td style="text-align: center;">{{item.system.quantity}}</td>
|
<td style="text-align: center;">{{item.system.quantity}}</td>
|
||||||
<td style="text-align: center;">
|
<td style="text-align: center;">
|
||||||
<input type="checkbox" data-action="toggleEquipped" data-item-id="{{item.id}}" {{#if item.system.equipped}}checked{{/if}} style="cursor: pointer;" />
|
<input type="checkbox" data-action="toggleEquipped" data-item-id="{{item.id}}" {{#if
|
||||||
|
item.system.equipped}}checked{{/if}} style="cursor: pointer;" />
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: right; white-space: nowrap;">
|
<td style="text-align: right; white-space: nowrap;">
|
||||||
<button type="button" class="edit-btn" data-action="editItem" data-item-id="{{item.id}}" title="{{localize 'HBM.ui.edit'}}">✎</button>
|
<button type="button" class="edit-btn" data-action="editItem" data-item-id="{{item.id}}"
|
||||||
<button type="button" class="delete-btn" data-action="deleteItem" data-item-id="{{item.id}}" title="{{localize 'HBM.ui.delete'}}">✕</button>
|
title="{{localize 'HBM.ui.edit'}}">✎</button>
|
||||||
|
<button type="button" class="delete-btn" data-action="deleteItem"
|
||||||
|
data-item-id="{{item.id}}" title="{{localize 'HBM.ui.delete'}}">✕</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
@@ -369,15 +402,19 @@
|
|||||||
<tr class="item-tr" data-item-id="{{item.id}}">
|
<tr class="item-tr" data-item-id="{{item.id}}">
|
||||||
<td><strong>{{item.name}}</strong></td>
|
<td><strong>{{item.name}}</strong></td>
|
||||||
<td style="text-align: center;">{{item.system.armor.damageReduction}} DR</td>
|
<td style="text-align: center;">{{item.system.armor.damageReduction}} DR</td>
|
||||||
<td style="text-align: center;">{{item.system.armor.condition}} / {{item.system.armor.conditionMax}}</td>
|
<td style="text-align: center;">{{item.system.armor.condition}} /
|
||||||
|
{{item.system.armor.conditionMax}}</td>
|
||||||
<td style="text-align: center;">{{item.system.weight}}</td>
|
<td style="text-align: center;">{{item.system.weight}}</td>
|
||||||
<td style="text-align: center;">{{item.system.quantity}}</td>
|
<td style="text-align: center;">{{item.system.quantity}}</td>
|
||||||
<td style="text-align: center;">
|
<td style="text-align: center;">
|
||||||
<input type="checkbox" data-action="toggleEquipped" data-item-id="{{item.id}}" {{#if item.system.equipped}}checked{{/if}} style="cursor: pointer;" />
|
<input type="checkbox" data-action="toggleEquipped" data-item-id="{{item.id}}" {{#if
|
||||||
|
item.system.equipped}}checked{{/if}} style="cursor: pointer;" />
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: right; white-space: nowrap;">
|
<td style="text-align: right; white-space: nowrap;">
|
||||||
<button type="button" class="edit-btn" data-action="editItem" data-item-id="{{item.id}}" title="{{localize 'HBM.ui.edit'}}">✎</button>
|
<button type="button" class="edit-btn" data-action="editItem" data-item-id="{{item.id}}"
|
||||||
<button type="button" class="delete-btn" data-action="deleteItem" data-item-id="{{item.id}}" title="{{localize 'HBM.ui.delete'}}">✕</button>
|
title="{{localize 'HBM.ui.edit'}}">✎</button>
|
||||||
|
<button type="button" class="delete-btn" data-action="deleteItem"
|
||||||
|
data-item-id="{{item.id}}" title="{{localize 'HBM.ui.delete'}}">✕</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
@@ -405,8 +442,10 @@
|
|||||||
<td style="text-align: center;">{{item.system.weight}}</td>
|
<td style="text-align: center;">{{item.system.weight}}</td>
|
||||||
<td style="text-align: center;">{{item.system.quantity}}</td>
|
<td style="text-align: center;">{{item.system.quantity}}</td>
|
||||||
<td style="text-align: right; white-space: nowrap;">
|
<td style="text-align: right; white-space: nowrap;">
|
||||||
<button type="button" class="edit-btn" data-action="editItem" data-item-id="{{item.id}}" title="{{localize 'HBM.ui.edit'}}">✎</button>
|
<button type="button" class="edit-btn" data-action="editItem" data-item-id="{{item.id}}"
|
||||||
<button type="button" class="delete-btn" data-action="deleteItem" data-item-id="{{item.id}}" title="{{localize 'HBM.ui.delete'}}">✕</button>
|
title="{{localize 'HBM.ui.edit'}}">✎</button>
|
||||||
|
<button type="button" class="delete-btn" data-action="deleteItem"
|
||||||
|
data-item-id="{{item.id}}" title="{{localize 'HBM.ui.delete'}}">✕</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
@@ -423,8 +462,7 @@
|
|||||||
<div class="custom-equipment-section" style="margin-top: 20px;">
|
<div class="custom-equipment-section" style="margin-top: 20px;">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
|
||||||
<strong>{{localize 'HBM.ui.customEquipment'}}</strong>
|
<strong>{{localize 'HBM.ui.customEquipment'}}</strong>
|
||||||
<button type="button" data-action="addArrayEntry"
|
<button type="button" data-action="addArrayEntry" data-path="system.details.customItems"
|
||||||
data-path="system.details.customItems"
|
|
||||||
data-template="{"name":"","qty":1,"note":""}"
|
data-template="{"name":"","qty":1,"note":""}"
|
||||||
style="padding:2px 8px; font-size:0.8rem; background:var(--hbm-success); color:#fff; border:none; border-radius:var(--hbm-radius); cursor:pointer;">
|
style="padding:2px 8px; font-size:0.8rem; background:var(--hbm-success); color:#fff; border:none; border-radius:var(--hbm-radius); cursor:pointer;">
|
||||||
+ {{localize 'HBM.ui.add'}}
|
+ {{localize 'HBM.ui.add'}}
|
||||||
@@ -444,11 +482,15 @@
|
|||||||
{{#each system.details.customItems as |row i|}}
|
{{#each system.details.customItems as |row i|}}
|
||||||
<tr>
|
<tr>
|
||||||
<td><input type="text" name="system.details.customItems.{{i}}.name" value="{{row.name}}"
|
<td><input type="text" name="system.details.customItems.{{i}}.name" value="{{row.name}}"
|
||||||
style="width:100%; border:none; border-bottom:1px solid var(--hbm-border); background:transparent; color:var(--hbm-fg); font-size:0.85rem;" /></td>
|
style="width:100%; border:none; border-bottom:1px solid var(--hbm-border); background:transparent; color:var(--hbm-fg); font-size:0.85rem;" />
|
||||||
<td><input type="number" name="system.details.customItems.{{i}}.qty" value="{{row.qty}}" min="0"
|
</td>
|
||||||
style="width:3rem; text-align:center; border:none; border-bottom:1px solid var(--hbm-border); background:transparent; color:var(--hbm-fg); font-size:0.85rem;" /></td>
|
<td><input type="number" name="system.details.customItems.{{i}}.qty" value="{{row.qty}}"
|
||||||
|
min="0"
|
||||||
|
style="width:3rem; text-align:center; border:none; border-bottom:1px solid var(--hbm-border); background:transparent; color:var(--hbm-fg); font-size:0.85rem;" />
|
||||||
|
</td>
|
||||||
<td><input type="text" name="system.details.customItems.{{i}}.note" value="{{row.note}}"
|
<td><input type="text" name="system.details.customItems.{{i}}.note" value="{{row.note}}"
|
||||||
style="width:100%; border:none; border-bottom:1px solid var(--hbm-border); background:transparent; color:var(--hbm-fg); font-size:0.8rem; color:#777;" /></td>
|
style="width:100%; border:none; border-bottom:1px solid var(--hbm-border); background:transparent; color:var(--hbm-fg); font-size:0.8rem; color:#777;" />
|
||||||
|
</td>
|
||||||
<td style="text-align:right;">
|
<td style="text-align:right;">
|
||||||
<button type="button" class="delete-btn" data-action="removeArrayEntry"
|
<button type="button" class="delete-btn" data-action="removeArrayEntry"
|
||||||
data-path="system.details.customItems" data-index="{{i}}"
|
data-path="system.details.customItems" data-index="{{i}}"
|
||||||
@@ -473,15 +515,19 @@
|
|||||||
<section class="tab-panel {{#if (eq tabGroups.primary 'biography')}}active{{/if}}" data-tab="biography">
|
<section class="tab-panel {{#if (eq tabGroups.primary 'biography')}}active{{/if}}" data-tab="biography">
|
||||||
<div class="bio-grid" style="display: flex; gap: 20px; flex-direction: column;">
|
<div class="bio-grid" style="display: flex; gap: 20px; flex-direction: column;">
|
||||||
<div style="display: flex; gap: 20px;">
|
<div style="display: flex; gap: 20px;">
|
||||||
<div class="bio-image-container" style="display: flex; flex-direction: column; align-items: center; gap: 10px; flex-shrink: 0;">
|
<div class="bio-image-container"
|
||||||
<img class="profile-img" src="{{document.img}}" data-action="editImage" title="{{document.name}}" height="150" width="150" style="border: 2px solid #555; border-radius: 4px; cursor: pointer; background: #222;" />
|
style="display: flex; flex-direction: column; align-items: center; gap: 10px; flex-shrink: 0;">
|
||||||
|
<img class="profile-img" src="{{document.img}}" data-action="editImage"
|
||||||
|
title="{{document.name}}" height="150" width="150"
|
||||||
|
style="border: 2px solid #555; border-radius: 4px; cursor: pointer; background: #222;" />
|
||||||
<small style="color: #777;">{{localize 'HBM.ui.changeImageHint'}}</small>
|
<small style="color: #777;">{{localize 'HBM.ui.changeImageHint'}}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="bio-section" style="flex: 1; display: flex; flex-direction: column; gap: 10px;">
|
<div class="bio-section" style="flex: 1; display: flex; flex-direction: column; gap: 10px;">
|
||||||
<div style="overflow-x: auto; margin-top: 5px;">
|
<div style="overflow-x: auto; margin-top: 5px;">
|
||||||
<table class="schedule-table">
|
<table class="schedule-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style="background: rgba(0, 0, 0, 0.15); border-bottom: 2px solid var(--hbm-border);">
|
<tr
|
||||||
|
style="background: rgba(0, 0, 0, 0.15); border-bottom: 2px solid var(--hbm-border);">
|
||||||
<th style="width: 3%;">#</th>
|
<th style="width: 3%;">#</th>
|
||||||
<th style="width: 12%;">{{localize 'HBM.schedule.hours'}}</th>
|
<th style="width: 12%;">{{localize 'HBM.schedule.hours'}}</th>
|
||||||
<th style="width: 14%;">{{localize 'HBM.schedule.monday'}}</th>
|
<th style="width: 14%;">{{localize 'HBM.schedule.monday'}}</th>
|
||||||
@@ -496,159 +542,239 @@
|
|||||||
<!-- Row 1 -->
|
<!-- Row 1 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight: bold;">1</td>
|
<td style="font-weight: bold;">1</td>
|
||||||
<td><input type="text" name="system.details.schedule.row1.hours" value="{{system.details.schedule.row1.hours}}" /></td>
|
<td>7:30-8:30</td>
|
||||||
<td><input type="text" name="system.details.schedule.row1.mon" value="{{system.details.schedule.row1.mon}}" /></td>
|
<td><input type="text" name="system.details.schedule.row1.mon"
|
||||||
<td><input type="text" name="system.details.schedule.row1.tue" value="{{system.details.schedule.row1.tue}}" /></td>
|
value="{{system.details.schedule.row1.mon}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row1.wed" value="{{system.details.schedule.row1.wed}}" /></td>
|
<td><input type="text" name="system.details.schedule.row1.tue"
|
||||||
<td><input type="text" name="system.details.schedule.row1.thu" value="{{system.details.schedule.row1.thu}}" /></td>
|
value="{{system.details.schedule.row1.tue}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row1.fri" value="{{system.details.schedule.row1.fri}}" /></td>
|
<td><input type="text" name="system.details.schedule.row1.wed"
|
||||||
<td><input type="text" name="system.details.schedule.row1.sat" value="{{system.details.schedule.row1.sat}}" /></td>
|
value="{{system.details.schedule.row1.wed}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row1.thu"
|
||||||
|
value="{{system.details.schedule.row1.thu}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row1.fri"
|
||||||
|
value="{{system.details.schedule.row1.fri}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row1.sat"
|
||||||
|
value="{{system.details.schedule.row1.sat}}" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Row 2 -->
|
<!-- Row 2 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight: bold;">2</td>
|
<td style="font-weight: bold;">2</td>
|
||||||
<td><input type="text" name="system.details.schedule.row2.hours" value="{{system.details.schedule.row2.hours}}" /></td>
|
<td>8:35-9:35</td>
|
||||||
<td><input type="text" name="system.details.schedule.row2.mon" value="{{system.details.schedule.row2.mon}}" /></td>
|
<td><input type="text" name="system.details.schedule.row2.mon"
|
||||||
<td><input type="text" name="system.details.schedule.row2.tue" value="{{system.details.schedule.row2.tue}}" /></td>
|
value="{{system.details.schedule.row2.mon}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row2.wed" value="{{system.details.schedule.row2.wed}}" /></td>
|
<td><input type="text" name="system.details.schedule.row2.tue"
|
||||||
<td><input type="text" name="system.details.schedule.row2.thu" value="{{system.details.schedule.row2.thu}}" /></td>
|
value="{{system.details.schedule.row2.tue}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row2.fri" value="{{system.details.schedule.row2.fri}}" /></td>
|
<td><input type="text" name="system.details.schedule.row2.wed"
|
||||||
<td><input type="text" name="system.details.schedule.row2.sat" value="{{system.details.schedule.row2.sat}}" /></td>
|
value="{{system.details.schedule.row2.wed}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row2.thu"
|
||||||
|
value="{{system.details.schedule.row2.thu}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row2.fri"
|
||||||
|
value="{{system.details.schedule.row2.fri}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row2.sat"
|
||||||
|
value="{{system.details.schedule.row2.sat}}" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Row 3 -->
|
<!-- Row 3 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight: bold;">3</td>
|
<td style="font-weight: bold;">3</td>
|
||||||
<td><input type="text" name="system.details.schedule.row3.hours" value="{{system.details.schedule.row3.hours}}" /></td>
|
<td>9:40-10:40</td>
|
||||||
<td><input type="text" name="system.details.schedule.row3.mon" value="{{system.details.schedule.row3.mon}}" /></td>
|
<td><input type="text" name="system.details.schedule.row3.mon"
|
||||||
<td><input type="text" name="system.details.schedule.row3.tue" value="{{system.details.schedule.row3.tue}}" /></td>
|
value="{{system.details.schedule.row3.mon}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row3.wed" value="{{system.details.schedule.row3.wed}}" /></td>
|
<td><input type="text" name="system.details.schedule.row3.tue"
|
||||||
<td><input type="text" name="system.details.schedule.row3.thu" value="{{system.details.schedule.row3.thu}}" /></td>
|
value="{{system.details.schedule.row3.tue}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row3.fri" value="{{system.details.schedule.row3.fri}}" /></td>
|
<td><input type="text" name="system.details.schedule.row3.wed"
|
||||||
<td><input type="text" name="system.details.schedule.row3.sat" value="{{system.details.schedule.row3.sat}}" /></td>
|
value="{{system.details.schedule.row3.wed}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row3.thu"
|
||||||
|
value="{{system.details.schedule.row3.thu}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row3.fri"
|
||||||
|
value="{{system.details.schedule.row3.fri}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row3.sat"
|
||||||
|
value="{{system.details.schedule.row3.sat}}" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Break -->
|
<!-- Break -->
|
||||||
<tr style="background: rgba(0, 0, 0, 0.08); font-weight: bold;">
|
<tr style="background: rgba(0, 0, 0, 0.08); font-weight: bold;">
|
||||||
<td colspan="8" style="padding: 5px; text-align: center; letter-spacing: 0.1em; text-transform: uppercase; font-size: 0.75rem;">{{localize 'HBM.schedule.lunchBreak'}}</td>
|
<td colspan="8"
|
||||||
|
style="padding: 5px; text-align: center; letter-spacing: 0.1em; text-transform: uppercase; font-size: 0.75rem;">
|
||||||
|
{{localize 'HBM.schedule.lunchBreak'}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Row 4 -->
|
<!-- Row 4 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight: bold;">4</td>
|
<td style="font-weight: bold;">4</td>
|
||||||
<td><input type="text" name="system.details.schedule.row4.hours" value="{{system.details.schedule.row4.hours}}" /></td>
|
<td>11:10-12:10</td>
|
||||||
<td><input type="text" name="system.details.schedule.row4.mon" value="{{system.details.schedule.row4.mon}}" /></td>
|
<td><input type="text" name="system.details.schedule.row4.mon"
|
||||||
<td><input type="text" name="system.details.schedule.row4.tue" value="{{system.details.schedule.row4.tue}}" /></td>
|
value="{{system.details.schedule.row4.mon}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row4.wed" value="{{system.details.schedule.row4.wed}}" /></td>
|
<td><input type="text" name="system.details.schedule.row4.tue"
|
||||||
<td><input type="text" name="system.details.schedule.row4.thu" value="{{system.details.schedule.row4.thu}}" /></td>
|
value="{{system.details.schedule.row4.tue}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row4.fri" value="{{system.details.schedule.row4.fri}}" /></td>
|
<td><input type="text" name="system.details.schedule.row4.wed"
|
||||||
<td><input type="text" name="system.details.schedule.row4.sat" value="{{system.details.schedule.row4.sat}}" /></td>
|
value="{{system.details.schedule.row4.wed}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row4.thu"
|
||||||
|
value="{{system.details.schedule.row4.thu}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row4.fri"
|
||||||
|
value="{{system.details.schedule.row4.fri}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row4.sat"
|
||||||
|
value="{{system.details.schedule.row4.sat}}" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Row 5 -->
|
<!-- Row 5 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight: bold;">5</td>
|
<td style="font-weight: bold;">5</td>
|
||||||
<td><input type="text" name="system.details.schedule.row5.hours" value="{{system.details.schedule.row5.hours}}" /></td>
|
<td>12:15-13:15</td>
|
||||||
<td><input type="text" name="system.details.schedule.row5.mon" value="{{system.details.schedule.row5.mon}}" /></td>
|
<td><input type="text" name="system.details.schedule.row5.mon"
|
||||||
<td><input type="text" name="system.details.schedule.row5.tue" value="{{system.details.schedule.row5.tue}}" /></td>
|
value="{{system.details.schedule.row5.mon}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row5.wed" value="{{system.details.schedule.row5.wed}}" /></td>
|
<td><input type="text" name="system.details.schedule.row5.tue"
|
||||||
<td><input type="text" name="system.details.schedule.row5.thu" value="{{system.details.schedule.row5.thu}}" /></td>
|
value="{{system.details.schedule.row5.tue}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row5.fri" value="{{system.details.schedule.row5.fri}}" /></td>
|
<td><input type="text" name="system.details.schedule.row5.wed"
|
||||||
<td><input type="text" name="system.details.schedule.row5.sat" value="{{system.details.schedule.row5.sat}}" /></td>
|
value="{{system.details.schedule.row5.wed}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row5.thu"
|
||||||
|
value="{{system.details.schedule.row5.thu}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row5.fri"
|
||||||
|
value="{{system.details.schedule.row5.fri}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row5.sat"
|
||||||
|
value="{{system.details.schedule.row5.sat}}" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Row 6 -->
|
<!-- Row 6 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight: bold;">6</td>
|
<td style="font-weight: bold;">6</td>
|
||||||
<td><input type="text" name="system.details.schedule.row6.hours" value="{{system.details.schedule.row6.hours}}" /></td>
|
<td>13:20-14:20</td>
|
||||||
<td><input type="text" name="system.details.schedule.row6.mon" value="{{system.details.schedule.row6.mon}}" /></td>
|
<td><input type="text" name="system.details.schedule.row6.mon"
|
||||||
<td><input type="text" name="system.details.schedule.row6.tue" value="{{system.details.schedule.row6.tue}}" /></td>
|
value="{{system.details.schedule.row6.mon}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row6.wed" value="{{system.details.schedule.row6.wed}}" /></td>
|
<td><input type="text" name="system.details.schedule.row6.tue"
|
||||||
<td><input type="text" name="system.details.schedule.row6.thu" value="{{system.details.schedule.row6.thu}}" /></td>
|
value="{{system.details.schedule.row6.tue}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row6.fri" value="{{system.details.schedule.row6.fri}}" /></td>
|
<td><input type="text" name="system.details.schedule.row6.wed"
|
||||||
<td><input type="text" name="system.details.schedule.row6.sat" value="{{system.details.schedule.row6.sat}}" /></td>
|
value="{{system.details.schedule.row6.wed}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row6.thu"
|
||||||
|
value="{{system.details.schedule.row6.thu}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row6.fri"
|
||||||
|
value="{{system.details.schedule.row6.fri}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row6.sat"
|
||||||
|
value="{{system.details.schedule.row6.sat}}" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Row 7 -->
|
<!-- Row 7 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight: bold;">7</td>
|
<td style="font-weight: bold;">7</td>
|
||||||
<td><input type="text" name="system.details.schedule.row7.hours" value="{{system.details.schedule.row7.hours}}" /></td>
|
<td>14:25-15:25</td>
|
||||||
<td><input type="text" name="system.details.schedule.row7.mon" value="{{system.details.schedule.row7.mon}}" /></td>
|
<td><input type="text" name="system.details.schedule.row7.mon"
|
||||||
<td><input type="text" name="system.details.schedule.row7.tue" value="{{system.details.schedule.row7.tue}}" /></td>
|
value="{{system.details.schedule.row7.mon}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row7.wed" value="{{system.details.schedule.row7.wed}}" /></td>
|
<td><input type="text" name="system.details.schedule.row7.tue"
|
||||||
<td><input type="text" name="system.details.schedule.row7.thu" value="{{system.details.schedule.row7.thu}}" /></td>
|
value="{{system.details.schedule.row7.tue}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row7.fri" value="{{system.details.schedule.row7.fri}}" /></td>
|
<td><input type="text" name="system.details.schedule.row7.wed"
|
||||||
<td><input type="text" name="system.details.schedule.row7.sat" value="{{system.details.schedule.row7.sat}}" /></td>
|
value="{{system.details.schedule.row7.wed}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row7.thu"
|
||||||
|
value="{{system.details.schedule.row7.thu}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row7.fri"
|
||||||
|
value="{{system.details.schedule.row7.fri}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row7.sat"
|
||||||
|
value="{{system.details.schedule.row7.sat}}" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Break -->
|
<!-- Break -->
|
||||||
<tr style="background: rgba(0, 0, 0, 0.08); font-weight: bold;">
|
<tr style="background: rgba(0, 0, 0, 0.08); font-weight: bold;">
|
||||||
<td colspan="8" style="padding: 5px; text-align: center; letter-spacing: 0.1em; text-transform: uppercase; font-size: 0.75rem;">{{localize 'HBM.schedule.dinnerBreak'}}</td>
|
<td colspan="8"
|
||||||
|
style="padding: 5px; text-align: center; letter-spacing: 0.1em; text-transform: uppercase; font-size: 0.75rem;">
|
||||||
|
{{localize 'HBM.schedule.dinnerBreak'}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Row 8 -->
|
<!-- Row 8 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight: bold;">8</td>
|
<td style="font-weight: bold;">8</td>
|
||||||
<td><input type="text" name="system.details.schedule.row8.hours" value="{{system.details.schedule.row8.hours}}" /></td>
|
<td>16:30-17:30</td>
|
||||||
<td><input type="text" name="system.details.schedule.row8.mon" value="{{system.details.schedule.row8.mon}}" /></td>
|
<td><input type="text" name="system.details.schedule.row8.mon"
|
||||||
<td><input type="text" name="system.details.schedule.row8.tue" value="{{system.details.schedule.row8.tue}}" /></td>
|
value="{{system.details.schedule.row8.mon}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row8.wed" value="{{system.details.schedule.row8.wed}}" /></td>
|
<td><input type="text" name="system.details.schedule.row8.tue"
|
||||||
<td><input type="text" name="system.details.schedule.row8.thu" value="{{system.details.schedule.row8.thu}}" /></td>
|
value="{{system.details.schedule.row8.tue}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row8.fri" value="{{system.details.schedule.row8.fri}}" /></td>
|
<td><input type="text" name="system.details.schedule.row8.wed"
|
||||||
<td><input type="text" name="system.details.schedule.row8.sat" value="{{system.details.schedule.row8.sat}}" /></td>
|
value="{{system.details.schedule.row8.wed}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row8.thu"
|
||||||
|
value="{{system.details.schedule.row8.thu}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row8.fri"
|
||||||
|
value="{{system.details.schedule.row8.fri}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row8.sat"
|
||||||
|
value="{{system.details.schedule.row8.sat}}" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Row 9 -->
|
<!-- Row 9 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight: bold;">9</td>
|
<td style="font-weight: bold;">9</td>
|
||||||
<td><input type="text" name="system.details.schedule.row9.hours" value="{{system.details.schedule.row9.hours}}" /></td>
|
<td>17:30-18:30</td>
|
||||||
<td><input type="text" name="system.details.schedule.row9.mon" value="{{system.details.schedule.row9.mon}}" /></td>
|
<td><input type="text" name="system.details.schedule.row9.mon"
|
||||||
<td><input type="text" name="system.details.schedule.row9.tue" value="{{system.details.schedule.row9.tue}}" /></td>
|
value="{{system.details.schedule.row9.mon}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row9.wed" value="{{system.details.schedule.row9.wed}}" /></td>
|
<td><input type="text" name="system.details.schedule.row9.tue"
|
||||||
<td><input type="text" name="system.details.schedule.row9.thu" value="{{system.details.schedule.row9.thu}}" /></td>
|
value="{{system.details.schedule.row9.tue}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row9.fri" value="{{system.details.schedule.row9.fri}}" /></td>
|
<td><input type="text" name="system.details.schedule.row9.wed"
|
||||||
<td><input type="text" name="system.details.schedule.row9.sat" value="{{system.details.schedule.row9.sat}}" /></td>
|
value="{{system.details.schedule.row9.wed}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row9.thu"
|
||||||
|
value="{{system.details.schedule.row9.thu}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row9.fri"
|
||||||
|
value="{{system.details.schedule.row9.fri}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row9.sat"
|
||||||
|
value="{{system.details.schedule.row9.sat}}" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Break -->
|
<!-- Break -->
|
||||||
<tr style="background: rgba(0, 0, 0, 0.08); font-weight: bold;">
|
<tr style="background: rgba(0, 0, 0, 0.08); font-weight: bold;">
|
||||||
<td colspan="8" style="padding: 5px; text-align: center; letter-spacing: 0.1em; text-transform: uppercase; font-size: 0.75rem;">{{localize 'HBM.schedule.supperBreak'}}</td>
|
<td colspan="8"
|
||||||
|
style="padding: 5px; text-align: center; letter-spacing: 0.1em; text-transform: uppercase; font-size: 0.75rem;">
|
||||||
|
{{localize 'HBM.schedule.supperBreak'}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Row 10 -->
|
<!-- Row 10 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight: bold;">10</td>
|
<td style="font-weight: bold;">10</td>
|
||||||
<td><input type="text" name="system.details.schedule.row10.hours" value="{{system.details.schedule.row10.hours}}" /></td>
|
<td>19:00-20:00</td>
|
||||||
<td><input type="text" name="system.details.schedule.row10.mon" value="{{system.details.schedule.row10.mon}}" /></td>
|
<td><input type="text" name="system.details.schedule.row10.mon"
|
||||||
<td><input type="text" name="system.details.schedule.row10.tue" value="{{system.details.schedule.row10.tue}}" /></td>
|
value="{{system.details.schedule.row10.mon}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row10.wed" value="{{system.details.schedule.row10.wed}}" /></td>
|
<td><input type="text" name="system.details.schedule.row10.tue"
|
||||||
<td><input type="text" name="system.details.schedule.row10.thu" value="{{system.details.schedule.row10.thu}}" /></td>
|
value="{{system.details.schedule.row10.tue}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row10.fri" value="{{system.details.schedule.row10.fri}}" /></td>
|
<td><input type="text" name="system.details.schedule.row10.wed"
|
||||||
<td><input type="text" name="system.details.schedule.row10.sat" value="{{system.details.schedule.row10.sat}}" /></td>
|
value="{{system.details.schedule.row10.wed}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row10.thu"
|
||||||
|
value="{{system.details.schedule.row10.thu}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row10.fri"
|
||||||
|
value="{{system.details.schedule.row10.fri}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row10.sat"
|
||||||
|
value="{{system.details.schedule.row10.sat}}" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Row 11 -->
|
<!-- Row 11 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight: bold;">11</td>
|
<td style="font-weight: bold;">11</td>
|
||||||
<td><input type="text" name="system.details.schedule.row11.hours" value="{{system.details.schedule.row11.hours}}" /></td>
|
<td>20:05-21:05</td>
|
||||||
<td><input type="text" name="system.details.schedule.row11.mon" value="{{system.details.schedule.row11.mon}}" /></td>
|
<td><input type="text" name="system.details.schedule.row11.mon"
|
||||||
<td><input type="text" name="system.details.schedule.row11.tue" value="{{system.details.schedule.row11.tue}}" /></td>
|
value="{{system.details.schedule.row11.mon}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row11.wed" value="{{system.details.schedule.row11.wed}}" /></td>
|
<td><input type="text" name="system.details.schedule.row11.tue"
|
||||||
<td><input type="text" name="system.details.schedule.row11.thu" value="{{system.details.schedule.row11.thu}}" /></td>
|
value="{{system.details.schedule.row11.tue}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row11.fri" value="{{system.details.schedule.row11.fri}}" /></td>
|
<td><input type="text" name="system.details.schedule.row11.wed"
|
||||||
<td><input type="text" name="system.details.schedule.row11.sat" value="{{system.details.schedule.row11.sat}}" /></td>
|
value="{{system.details.schedule.row11.wed}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row11.thu"
|
||||||
|
value="{{system.details.schedule.row11.thu}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row11.fri"
|
||||||
|
value="{{system.details.schedule.row11.fri}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row11.sat"
|
||||||
|
value="{{system.details.schedule.row11.sat}}" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Row 12 -->
|
<!-- Row 12 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight: bold;">12</td>
|
<td style="font-weight: bold;">12</td>
|
||||||
<td><input type="text" name="system.details.schedule.row12.hours" value="{{system.details.schedule.row12.hours}}" /></td>
|
<td>21:10-22:10</td>
|
||||||
<td><input type="text" name="system.details.schedule.row12.mon" value="{{system.details.schedule.row12.mon}}" /></td>
|
<td><input type="text" name="system.details.schedule.row12.mon"
|
||||||
<td><input type="text" name="system.details.schedule.row12.tue" value="{{system.details.schedule.row12.tue}}" /></td>
|
value="{{system.details.schedule.row12.mon}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row12.wed" value="{{system.details.schedule.row12.wed}}" /></td>
|
<td><input type="text" name="system.details.schedule.row12.tue"
|
||||||
<td><input type="text" name="system.details.schedule.row12.thu" value="{{system.details.schedule.row12.thu}}" /></td>
|
value="{{system.details.schedule.row12.tue}}" /></td>
|
||||||
<td><input type="text" name="system.details.schedule.row12.fri" value="{{system.details.schedule.row12.fri}}" /></td>
|
<td><input type="text" name="system.details.schedule.row12.wed"
|
||||||
<td><input type="text" name="system.details.schedule.row12.sat" value="{{system.details.schedule.row12.sat}}" /></td>
|
value="{{system.details.schedule.row12.wed}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row12.thu"
|
||||||
|
value="{{system.details.schedule.row12.thu}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row12.fri"
|
||||||
|
value="{{system.details.schedule.row12.fri}}" /></td>
|
||||||
|
<td><input type="text" name="system.details.schedule.row12.sat"
|
||||||
|
value="{{system.details.schedule.row12.sat}}" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label><strong>{{localize 'HBM.details.personalDetails'}}</strong></label>
|
<label><strong>{{localize 'HBM.details.personalDetails'}}</strong></label>
|
||||||
<textarea name="system.details.personalDetails" rows="4" style="width: 100%; resize: vertical; border: 1px solid var(--hbm-border); border-radius: var(--hbm-radius); padding: 5px; background: var(--hbm-card-bg); color: var(--hbm-fg);">{{system.details.personalDetails}}</textarea>
|
<textarea name="system.details.personalDetails" rows="4"
|
||||||
|
style="width: 100%; resize: vertical; border: 1px solid var(--hbm-border); border-radius: var(--hbm-radius); padding: 5px; background: var(--hbm-card-bg); color: var(--hbm-fg);">{{system.details.personalDetails}}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bio-section">
|
<div class="bio-section">
|
||||||
<label><strong>{{localize 'HBM.ui.biography'}}</strong></label>
|
<label><strong>{{localize 'HBM.ui.biography'}}</strong></label>
|
||||||
<textarea name="system.details.biography" rows="8" style="width: 100%; resize: vertical; border: 1px solid var(--hbm-border); border-radius: var(--hbm-radius); padding: 5px; background: var(--hbm-card-bg); color: var(--hbm-fg);">{{system.details.biography}}</textarea>
|
<textarea name="system.details.biography" rows="8"
|
||||||
|
style="width: 100%; resize: vertical; border: 1px solid var(--hbm-border); border-radius: var(--hbm-radius); padding: 5px; background: var(--hbm-card-bg); color: var(--hbm-fg);">{{system.details.biography}}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -77,6 +77,19 @@
|
|||||||
<input type="number" name="system.attributes.magicalArmor.max" value="{{system.attributes.magicalArmor.max}}" min="0" />
|
<input type="number" name="system.attributes.magicalArmor.max" value="{{system.attributes.magicalArmor.max}}" min="0" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="resource">
|
||||||
|
<label>{{localize 'HBM.resources.initiative'}}</label>
|
||||||
|
<span class="resource-value">
|
||||||
|
<span class="static-value"
|
||||||
|
title="{{localize 'HBM.resources.initiative'}}">{{system.attributes.initiative.value}}</span>
|
||||||
|
</span>
|
||||||
|
<div style="display:flex; align-items:center; gap:0.3rem; margin-top:0.15rem;">
|
||||||
|
<small style="white-space:nowrap;">{{localize 'HBM.ui.initiativeBonus'}}:</small>
|
||||||
|
<input type="number" name="system.attributes.initiative.bonus"
|
||||||
|
value="{{system.attributes.initiative.bonus}}" min="0"
|
||||||
|
style="width:2.5rem; text-align:center; font-size:0.8rem; border:none; border-bottom:1px solid var(--hbm-border); background:transparent;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="resource">
|
<div class="resource">
|
||||||
<label>{{localize 'HBM.npc.speed'}}</label>
|
<label>{{localize 'HBM.npc.speed'}}</label>
|
||||||
<input type="text" name="system.attributes.speed" value="{{system.attributes.speed}}"/>
|
<input type="text" name="system.attributes.speed" value="{{system.attributes.speed}}"/>
|
||||||
@@ -94,6 +107,22 @@
|
|||||||
<label>{{localize 'HBM.npc.languages'}}: <input type="text" name="system.details.languages" value="{{system.details.languages}}"/></label>
|
<label>{{localize 'HBM.npc.languages'}}: <input type="text" name="system.details.languages" value="{{system.details.languages}}"/></label>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="npc-skills-section">
|
||||||
|
<fieldset>
|
||||||
|
<legend>{{localize 'HBM.ui.tabs.skills'}}</legend>
|
||||||
|
<div class="skills-grid">
|
||||||
|
{{#each skills as |sk|}}
|
||||||
|
<div class="skill">
|
||||||
|
<span class="skill-name">{{sk.label}}</span>
|
||||||
|
<input type="number" name="system.skills.{{sk.key}}.value" value="{{sk.value}}" min="0" max="8" />
|
||||||
|
<button type="button" class="roll-btn" data-action="rollSkill" data-skill="{{sk.key}}"
|
||||||
|
title="{{localize 'HBM.roll.roll'}}">⚂</button>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="combat-lists">
|
<section class="combat-lists">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{{localize 'HBM.npc.attacks'}}
|
<legend>{{localize 'HBM.npc.attacks'}}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
{{#if mode}}<span class="badge mode mode-{{mode}}">{{localize (concat 'HBM.spell.castingMode.' mode)}}</span>{{/if}}
|
{{#if mode}}<span class="badge mode mode-{{mode}}">{{localize (concat 'HBM.spell.castingMode.' mode)}}</span>{{/if}}
|
||||||
{{#if school}}<span class="badge school">{{localize (concat 'HBM.spellSchool.' school)}}</span>{{/if}}
|
{{#if school}}<span class="badge school">{{localize (concat 'HBM.spellSchool.' school)}}</span>{{/if}}
|
||||||
{{#if circle}}<span class="badge circle">{{localize 'HBM.spell.circle'}} {{circle}}</span>{{/if}}
|
{{#if circle}}<span class="badge circle">{{localize 'HBM.spell.circle'}} {{circle}}</span>{{/if}}
|
||||||
|
{{#if (or (eq mode "sacred") (eq mode "prayer"))}}
|
||||||
|
<span class="badge warning">{{localize 'HBM.spellCast.cannotBeUnraveled'}}</span>
|
||||||
|
{{/if}}
|
||||||
{{#if outcome.success}}
|
{{#if outcome.success}}
|
||||||
<span class="badge result success">✓ {{localize 'HBM.spellCast.success'}}</span>
|
<span class="badge result success">✓ {{localize 'HBM.spellCast.success'}}</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
@@ -72,7 +75,7 @@
|
|||||||
<h4>{{localize 'HBM.spellCast.triggers'}}</h4>
|
<h4>{{localize 'HBM.spellCast.triggers'}}</h4>
|
||||||
<ul>
|
<ul>
|
||||||
{{#each triggers as |t|}}
|
{{#each triggers as |t|}}
|
||||||
<li><span class="badge trigger-event">{{localize (concat 'HBM.triggerEvent.' t.event)}}</span> — {{t.effect}}</li>
|
<li><span class="badge trigger-event">{{localize (concat 'HBM.triggerEvent.' t.event)}}</span> - {{t.effect}}</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
{{localize "HBM.roll.successes"}}: <strong>{{ts.successes}}</strong>
|
{{localize "HBM.roll.successes"}}: <strong>{{ts.successes}}</strong>
|
||||||
/ {{localize "HBM.roll.required"}}: {{ts.required}}
|
/ {{localize "HBM.roll.required"}}: {{ts.required}}
|
||||||
({{localize "HBM.roll.threshold"}} {{ts.threshold}})
|
({{localize "HBM.roll.threshold"}} {{ts.threshold}})
|
||||||
— {{#if ts.isSuccess}}{{localize "HBM.roll.success"}}{{else}}{{localize "HBM.roll.failure"}}{{/if}}
|
- {{#if ts.isSuccess}}{{localize "HBM.roll.success"}}{{else}}{{localize "HBM.roll.failure"}}{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{#if ts.criticalSuccesses}}
|
{{#if ts.criticalSuccesses}}
|
||||||
<div class="crit">{{localize "HBM.roll.criticalSuccess"}}: {{ts.criticalSuccesses}}</div>
|
<div class="crit">{{localize "HBM.roll.criticalSuccess"}}: {{ts.criticalSuccesses}}</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{{!-- Active Effects partial — included by talent and gear sheets. --}}
|
{{!-- Active Effects partial - included by talent and gear sheets. --}}
|
||||||
{{#if supportsEffects}}
|
{{#if supportsEffects}}
|
||||||
<fieldset class="hbm-effects">
|
<fieldset class="hbm-effects">
|
||||||
<legend>{{localize 'HBM.activeEffect.title'}}</legend>
|
<legend>{{localize 'HBM.activeEffect.title'}}</legend>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<label>{{localize 'HBM.race.attributePoints'}}: <input type="number" name="system.attributePoints" value="{{system.attributePoints}}" min="0"/></label>
|
<label>{{localize 'HBM.race.attributePoints'}}: <input type="number" name="system.attributePoints" value="{{system.attributePoints}}" min="0"/></label>
|
||||||
<label>{{localize 'HBM.race.skillPoints'}}: <input type="number" name="system.skillPoints" value="{{system.skillPoints}}" min="0"/></label>
|
<label>{{localize 'HBM.race.skillPoints'}}: <input type="number" name="system.skillPoints" value="{{system.skillPoints}}" min="0"/></label>
|
||||||
</div>
|
</div>
|
||||||
<label>{{localize 'HBM.race.physicalDescription'}}<input type="text" name="system.physicalDescription" value="{{system.physicalDescription}}"/></label>
|
<label>{{localize 'HBM.race.physicalDescription'}}<textarea name="system.physicalDescription" rows="3">{{system.physicalDescription}}</textarea></label>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{{localize 'HBM.race.traits'}}
|
<legend>{{localize 'HBM.race.traits'}}
|
||||||
<button type="button" data-action="addArrayEntry" data-path="system.traits"
|
<button type="button" data-action="addArrayEntry" data-path="system.traits"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
{{#if (eq system.castingMode 'sacred')}}
|
{{#if (eq system.castingMode 'sacred')}}
|
||||||
<label>{{localize 'HBM.spell.deity'}}:
|
<label>{{localize 'HBM.spell.deity'}}:
|
||||||
<select name="system.deity">
|
<select name="system.deity">
|
||||||
<option value="">—</option>
|
<option value="">-</option>
|
||||||
{{#each @root.choices.deities as |label key|}}
|
{{#each @root.choices.deities as |label key|}}
|
||||||
<option value="{{key}}" {{#if (eq ../system.deity key)}}selected{{/if}}>{{label}}</option>
|
<option value="{{key}}" {{#if (eq ../system.deity key)}}selected{{/if}}>{{label}}</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
<label>{{localize 'HBM.spell.sourceBook'}}:
|
<label>{{localize 'HBM.spell.sourceBook'}}:
|
||||||
<select name="system.sourceBook">
|
<select name="system.sourceBook">
|
||||||
<option value="">—</option>
|
<option value="">-</option>
|
||||||
{{#each @root.choices.sourceBooks as |label key|}}
|
{{#each @root.choices.sourceBooks as |label key|}}
|
||||||
<option value="{{key}}" {{#if (eq ../system.sourceBook key)}}selected{{/if}}>{{label}}</option>
|
<option value="{{key}}" {{#if (eq ../system.sourceBook key)}}selected{{/if}}>{{label}}</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Vitest setup — stubs out Foundry globals that our modules touch on import.
|
* Vitest setup - stubs out Foundry globals that our modules touch on import.
|
||||||
* Each test can override these by reaching into globalThis directly.
|
* Each test can override these by reaching into globalThis directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ import { vi } from 'vitest';
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Roll stub — pool roll counting successes >= threshold
|
// Roll stub - pool roll counting successes >= threshold
|
||||||
class StubRoll {
|
class StubRoll {
|
||||||
formula: string;
|
formula: string;
|
||||||
data: any;
|
data: any;
|
||||||
|
|||||||
Reference in New Issue
Block a user