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.
|
||||
|
||||
@@ -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.
|
||||
- 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,
|
||||
ActiveEffects integration, automated tests, full localization for the new
|
||||
@@ -19,8 +19,8 @@ features, and a v1.2.0 world migration.
|
||||
|
||||
### Added
|
||||
- **Talent sub-packs** for optional rules:
|
||||
- `talents-blood` — 5 talents from *Arcanum Sanguinis*
|
||||
- `talents-abyss` — 4 talents from *Klątwa Otchłani*
|
||||
- `talents-blood` - 5 talents from *Arcanum Sanguinis*
|
||||
- `talents-abyss` - 4 talents from *Klątwa Otchłani*
|
||||
- Each parsed talent is stamped with `flags['hbm-rpg-v3'].sourceModule`
|
||||
so future module-aware UIs can filter or annotate them. *Note:* Foundry
|
||||
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`.
|
||||
- **Per-turn Zeal regeneration is configurable.** The combat-turn handler
|
||||
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.
|
||||
- **Long-rest cleanup.** `rest(actor, 'long')` now also deletes temporary
|
||||
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`.
|
||||
- **Vitest test harness** (`vitest.config.ts`, `tests/setup.ts`). 5 test
|
||||
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
|
||||
- **Spell preparation** is intentionally not implemented — every spellcaster
|
||||
- **Spell preparation** is intentionally not implemented - every spellcaster
|
||||
can cast any known spell directly (per design).
|
||||
- **Module gating of sub-pack talents** is not enforced because Foundry does
|
||||
not provide a hook to hide compendium documents based on external module
|
||||
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
|
||||
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.
|
||||
|
||||
### 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`.
|
||||
- `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.
|
||||
- `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).
|
||||
|
||||
## [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
|
||||
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
|
||||
checkboxes, validation feedback inline.
|
||||
- **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`,
|
||||
`attributes.insanity` with derived clamps and resource cards on the sheet.
|
||||
- **Mental conditions**: paranoia, fobia, depresja, mania, schizofrenia
|
||||
@@ -144,19 +144,19 @@ in `_books/`.
|
||||
|
||||
### 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`).
|
||||
- Macro packs.
|
||||
- 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).
|
||||
- CSS polish: badges, source pip, blood-pool tint, school details groups,
|
||||
fieldset and array-row layouts.
|
||||
- Compendium browser metadata + `packFolders`.
|
||||
|
||||
## [0.6.0] — 2026-04-29
|
||||
## [0.6.0] - 2026-04-29
|
||||
|
||||
- Spell item-sheet rewrite (8 fieldsets, conditional rendering).
|
||||
- Cast dialog (`askCastOptions`) with mana/zeal/blood/Hekate/group/bypass.
|
||||
@@ -164,12 +164,12 @@ in `_books/`.
|
||||
- Resources: blood pool, elixir tolerance, insanity (conditional).
|
||||
- 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,
|
||||
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`,
|
||||
migration runner, mental conditions.
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
# 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## What's implemented
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
- **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`).
|
||||
- **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).
|
||||
- **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.
|
||||
- **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).
|
||||
- **Migration runner** — `src/migrations/` upgrades v0.2.x worlds (`complexityLevel` → `components.symbols.length`, `targets` regex → `areaOfEffect`, `overcasting` text → `overcastOptions[0].description`).
|
||||
- **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.
|
||||
- **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.
|
||||
- **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`).
|
||||
- **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).
|
||||
- **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.
|
||||
- **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).
|
||||
- **Migration runner** - `src/migrations/` upgrades v0.2.x worlds (`complexityLevel` → `components.symbols.length`, `targets` regex → `areaOfEffect`, `overcasting` text → `overcastOptions[0].description`).
|
||||
|
||||
## 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)
|
||||
|
||||
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`).
|
||||
- Character creation wizard, advancement automation (PD spending).
|
||||
- Macro packs.
|
||||
@@ -117,9 +117,9 @@ Deferred to follow-up versions:
|
||||
|
||||
Game rules and content live in sibling folders of this repo:
|
||||
|
||||
- [`../../_books/`](../../_books/) — synced PDFs of the rulebooks (read-only)
|
||||
- [`../../spells/`](../../spells/), [`../../creatures/`](../../creatures/), [`../../items/`](../../items/) — structured content
|
||||
- [`../../rules/`](../../rules/), [`../../revisions/`](../../revisions/) — current and proposed mechanics
|
||||
- [`../../_books/`](../../_books/) - synced PDFs of the rulebooks (read-only)
|
||||
- [`../../spells/`](../../spells/), [`../../creatures/`](../../creatures/), [`../../items/`](../../items/) - structured content
|
||||
- [`../../rules/`](../../rules/), [`../../revisions/`](../../revisions/) - current and proposed mechanics
|
||||
|
||||
## Useful links
|
||||
|
||||
@@ -128,4 +128,4 @@ Game rules and content live in sibling folders of this repo:
|
||||
|
||||
## License
|
||||
|
||||
All Rights Reserved — see [LICENSE.txt](./LICENSE.txt).
|
||||
All Rights Reserved - see [LICENSE.txt](./LICENSE.txt).
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"label": "Forbidden Disciplines",
|
||||
"entries": {
|
||||
"Magia Otchłani — Magia Aspektów": {
|
||||
"Magia Otchłani - Magia Aspektów": {
|
||||
"name": "AbyssAspects"
|
||||
},
|
||||
"Magia Otchłani — Pierwotna Magia": {
|
||||
"Magia Otchłani - Pierwotna Magia": {
|
||||
"name": "AbyssPrimal"
|
||||
},
|
||||
"Magia Krwi": {
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
"label": "Magic Disciplines",
|
||||
"entries": {
|
||||
"Alchemia - Warzenie Eliksirów": {
|
||||
"name": "Alchemy - Brewing of Elixirs"
|
||||
"name": "Alchemy - Brewing of Elixirs",
|
||||
"description": "[[Alchemia]]"
|
||||
},
|
||||
"Alchemia - Transmutacja": {
|
||||
"name": "Alchemy - Transmutation"
|
||||
"name": "Alchemy - Transmutation",
|
||||
"description": "[[Alchemia]]"
|
||||
},
|
||||
"Rzemiosło Artefaktów": {
|
||||
"name": "Crafting of Artifacts",
|
||||
@@ -48,7 +50,7 @@
|
||||
"Magia Sakralna": {
|
||||
"name": "Sacred Magic"
|
||||
},
|
||||
"Magia Sakralna — Egzorcyzmy": {
|
||||
"Magia Sakralna - Egzorcyzmy": {
|
||||
"name": "Sacred Magic - Exorcisms",
|
||||
"description": "[[Magic Sakralna]]"
|
||||
},
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
},
|
||||
"Krótki Odpoczynek": {
|
||||
"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": {
|
||||
"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": {
|
||||
"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",
|
||||
"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)": {
|
||||
"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."
|
||||
@@ -17,6 +21,10 @@
|
||||
"name": "Armor Of Faith",
|
||||
"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": {
|
||||
"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ą."
|
||||
@@ -33,6 +41,10 @@
|
||||
"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."
|
||||
},
|
||||
"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": {
|
||||
"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."
|
||||
@@ -49,6 +61,10 @@
|
||||
"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)."
|
||||
},
|
||||
"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)": {
|
||||
"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…"
|
||||
@@ -61,6 +77,10 @@
|
||||
"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."
|
||||
},
|
||||
"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ł)": {
|
||||
"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."
|
||||
@@ -81,10 +101,38 @@
|
||||
"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.)."
|
||||
},
|
||||
"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)": {
|
||||
"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."
|
||||
},
|
||||
"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": {
|
||||
"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."
|
||||
@@ -97,6 +145,14 @@
|
||||
"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ń."
|
||||
},
|
||||
"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": {
|
||||
"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ść."
|
||||
@@ -109,6 +165,10 @@
|
||||
"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."
|
||||
},
|
||||
"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": {
|
||||
"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."
|
||||
|
||||
+21
-11
@@ -1,7 +1,12 @@
|
||||
{
|
||||
"HBM": {
|
||||
"system": { "name": "Homebrew Magic: RPG v3" },
|
||||
"actor": { "character": "Character", "npc": "NPC / Creature" },
|
||||
"system": {
|
||||
"name": "Homebrew Magic: RPG v3"
|
||||
},
|
||||
"actor": {
|
||||
"character": "Character",
|
||||
"npc": "NPC / Creature"
|
||||
},
|
||||
"item": {
|
||||
"spell": "Spell",
|
||||
"gear": "Gear",
|
||||
@@ -155,7 +160,8 @@
|
||||
"sacred": "Sacred Magic",
|
||||
"witch": "Witch Magic",
|
||||
"blood": "Blood Magic",
|
||||
"hekate": "Hekate Hybrid"
|
||||
"hekate": "Hekate Hybrid",
|
||||
"prayer": "Prayer"
|
||||
}
|
||||
},
|
||||
"spellSchool": {
|
||||
@@ -173,14 +179,15 @@
|
||||
"manaSourceMage": "Mana Source",
|
||||
"illusion": "Illusion",
|
||||
"sacred": "Sacred Magic",
|
||||
"sacredExorcism": "Sacred Magic — Exorcism",
|
||||
"sacredExorcism": "Sacred Magic - Exorcism",
|
||||
"witch": "Witch Magic",
|
||||
"necromancy": "Necromancy",
|
||||
"blood": "Blood Magic",
|
||||
"crimson": "Crimson Magic",
|
||||
"abyssAspects": "Abyss — Aspects",
|
||||
"abyssPrimal": "Abyss — Primal",
|
||||
"wildWitch": "Wild Witch"
|
||||
"abyssAspects": "Abyss - Aspects",
|
||||
"abyssPrimal": "Abyss - Primal",
|
||||
"wildWitch": "Wild Witch",
|
||||
"eldritch": "Eldritch"
|
||||
},
|
||||
"deity": {
|
||||
"common": "Common Prayers",
|
||||
@@ -350,6 +357,7 @@
|
||||
"spellCast": {
|
||||
"title": "Casting spell",
|
||||
"manaSpent": "Mana spent",
|
||||
"cannotBeUnraveled": "Cannot be unraveled",
|
||||
"zealSpent": "Zeal spent",
|
||||
"bloodSpent": "Blood spent",
|
||||
"hekateMode": "Hekate mode (Sacred + Witch hybrid)",
|
||||
@@ -401,12 +409,12 @@
|
||||
"physicalArmorReduced": "Physical Armor reduced: {n}",
|
||||
"healthLost": "Health lost: {n}",
|
||||
"armorBroken": "Armor broken!",
|
||||
"runeTriggered": "Runes triggered — Magical Armor -1"
|
||||
"runeTriggered": "Runes triggered - Magical Armor -1"
|
||||
}
|
||||
},
|
||||
"combat": {
|
||||
"newRound": "New round — Mana reset",
|
||||
"newTurn": "New turn — Zeal +1",
|
||||
"newRound": "New round - Mana reset",
|
||||
"newTurn": "New turn - Zeal +1",
|
||||
"turnSkipped": "Turn skipped (condition)",
|
||||
"deathSavePrompt": "Make a Death Saving Throw (Body, T:4, Y:1)"
|
||||
},
|
||||
@@ -457,6 +465,7 @@
|
||||
"itemAlreadyOwned": "'{name}' is already on this character sheet.",
|
||||
"dropTalentsHint": "Drag talents from the compendium onto this sheet.",
|
||||
"physicalArmorBonus": "Armor bonus",
|
||||
"initiativeBonus": "Initiative bonus",
|
||||
"selectTalentParamTitle": "Talent Specialization",
|
||||
"selectTalentParamDesc": "This talent requires a choice of specialization. Enter a value for ({param}):",
|
||||
"tabs": {
|
||||
@@ -562,7 +571,8 @@
|
||||
"technologia-jest-wsrod-nas": "Technology is Among Us",
|
||||
"urok-osobisty": "Personal Charm",
|
||||
"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",
|
||||
"witch": "Wiedźmia Magia",
|
||||
"blood": "Magia Krwi",
|
||||
"hekate": "Hybryda Hekate"
|
||||
"hekate": "Hybryda Hekate",
|
||||
"prayer": "Modlitwa"
|
||||
}
|
||||
},
|
||||
"spellSchool": {
|
||||
@@ -178,14 +179,15 @@
|
||||
"manaSourceMage": "Źródło Mocy",
|
||||
"illusion": "Magia Iluzji",
|
||||
"sacred": "Magia Sakralna",
|
||||
"sacredExorcism": "Magia Sakralna — Egzorcyzmy",
|
||||
"sacredExorcism": "Magia Sakralna - Egzorcyzmy",
|
||||
"witch": "Wiedźmia Magia",
|
||||
"necromancy": "Nekromancja",
|
||||
"blood": "Magia Krwi",
|
||||
"crimson": "Magia Szkarłatu",
|
||||
"abyssAspects": "Magia Otchłani — Aspekty",
|
||||
"abyssPrimal": "Magia Otchłani — Pierwotna Magia",
|
||||
"wildWitch": "Dzika Wiedźmia Magia"
|
||||
"abyssAspects": "Magia Otchłani - Aspekty",
|
||||
"abyssPrimal": "Magia Otchłani - Pierwotna Magia",
|
||||
"wildWitch": "Dzika Wiedźmia Magia",
|
||||
"eldritch": "Magia Otchłani"
|
||||
},
|
||||
"deity": {
|
||||
"common": "Modlitwy Powszechne",
|
||||
@@ -375,6 +377,7 @@
|
||||
"spellCast": {
|
||||
"title": "Rzucanie zaklęcia",
|
||||
"manaSpent": "Wydana mana",
|
||||
"cannotBeUnraveled": "Nie da się rozpleść",
|
||||
"cast": "Rzuć",
|
||||
"notEnoughMana": "Niewystarczająca mana.",
|
||||
"notEnoughZeal": "Niewystarczający Zapał.",
|
||||
@@ -409,12 +412,12 @@
|
||||
"physicalArmorReduced": "Pancerz Fizyczny zredukował: {n}",
|
||||
"healthLost": "Utracona Żywotność: {n}",
|
||||
"armorBroken": "Zbroja zniszczona!",
|
||||
"runeTriggered": "Runy zadziałały — Pancerz Magiczny -1"
|
||||
"runeTriggered": "Runy zadziałały - Pancerz Magiczny -1"
|
||||
}
|
||||
},
|
||||
"combat": {
|
||||
"newRound": "Nowa runda — Mana zresetowana",
|
||||
"newTurn": "Nowa tura — Zapał +1",
|
||||
"newRound": "Nowa runda - Mana zresetowana",
|
||||
"newTurn": "Nowa tura - Zapał +1",
|
||||
"turnSkipped": "Tura pominięta (stan)",
|
||||
"deathSavePrompt": "Wykonaj Rzut Obronny przeciw Śmierci (Ciało, T:4, Y:1)"
|
||||
},
|
||||
@@ -465,6 +468,7 @@
|
||||
"itemAlreadyOwned": "'{name}' jest już na tej karcie postaci.",
|
||||
"dropTalentsHint": "Przeciągnij talenty z kompendium na tę kartę.",
|
||||
"physicalArmorBonus": "Premia do pancerza",
|
||||
"initiativeBonus": "Premia do inicjatywy",
|
||||
"selectTalentParamTitle": "Specjalizacja Talentu",
|
||||
"selectTalentParamDesc": "Ten talent wymaga określenia specjalizacji/parametru. Wpisz wartość dla parametru ({param}):",
|
||||
"tabs": {
|
||||
@@ -570,7 +574,8 @@
|
||||
"technologia-jest-wsrod-nas": "Technologia jest wśród nas",
|
||||
"urok-osobisty": "Urok osobisty",
|
||||
"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",
|
||||
"version": "1.3.12",
|
||||
"version": "1.3.30",
|
||||
"description": "Foundry VTT system for Homebrew Magic: Role Playing Game v3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -223,7 +223,7 @@ for (const pack of byPack.keys()) {
|
||||
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. */
|
||||
function toFoundryDoc(doc: ParsedDoc, folderId: string | null = null): Record<string, unknown> {
|
||||
|
||||
@@ -127,6 +127,7 @@ const nameTranslations: Record<string, string> = {
|
||||
"necromancy": "Necromancy",
|
||||
"blood": "Blood Magic",
|
||||
"wildwitch": "Wild Witch Magic",
|
||||
"take-breather": "Take a Breather",
|
||||
};
|
||||
|
||||
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.
|
||||
*
|
||||
* Run after build-packs.ts: `bun scripts/lint-uuid-refs.ts`
|
||||
@@ -37,14 +37,14 @@ function walk(dir: string): void {
|
||||
walk(packsSrcDir);
|
||||
|
||||
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\.(.+)$/);
|
||||
if (!m) return false;
|
||||
return !allIds.has(m[1]);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
if (targets.length === 0) return ui.notifications.warn('Zaznacz lub naceluj token(y).');
|
||||
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;
|
||||
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
||||
const attrs = ['body', 'mind', 'soul', 'magic'];
|
||||
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 result = await Dialog.prompt({
|
||||
title: `Test atrybutu — ${actor.name}`,
|
||||
title: `Test atrybutu - ${actor.name}`,
|
||||
content: `
|
||||
<form>
|
||||
<div class="form-group">
|
||||
@@ -27,4 +27,4 @@ const result = await Dialog.prompt({
|
||||
if (!result) return;
|
||||
const pool = actor.system.attributes[result.attr]?.value ?? 1;
|
||||
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;
|
||||
if (tokens.length < 2) return ui.notifications.warn('Zaznacz co najmniej 2 tokeny współrzucających.');
|
||||
const lines = [];
|
||||
@@ -11,7 +11,7 @@ for (const t of tokens) {
|
||||
const magic = a.system.attributes?.magic?.value ?? 0;
|
||||
totalMana += mana;
|
||||
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 = `
|
||||
<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;
|
||||
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');
|
||||
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({
|
||||
title: 'Rzut puli d6',
|
||||
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;
|
||||
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
||||
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;
|
||||
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');
|
||||
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",
|
||||
"requiem": "requiem",
|
||||
"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": {
|
||||
"magia ogólna": "general",
|
||||
"magia żywiołów": "elements",
|
||||
"magia żywiołów — powietrze": "elementsAir",
|
||||
"magia żywiołów - powietrze": "elementsAir",
|
||||
"magia żywiołów (powietrze)": "elementsAir",
|
||||
"magia powietrza": "elementsAir",
|
||||
"magia żywiołów — woda": "elementsWater",
|
||||
"magia żywiołów - woda": "elementsWater",
|
||||
"magia żywiołów (woda)": "elementsWater",
|
||||
"magia wody": "elementsWater",
|
||||
"magia żywiołów — ogień": "elementsFire",
|
||||
"magia żywiołów - ogień": "elementsFire",
|
||||
"magia żywiołów (ogień)": "elementsFire",
|
||||
"magia ognia": "elementsFire",
|
||||
"magia żywiołów — ziemia": "elementsEarth",
|
||||
"magia żywiołów - ziemia": "elementsEarth",
|
||||
"magia żywiołów (ziemia)": "elementsEarth",
|
||||
"magia ziemi": "elementsEarth",
|
||||
"magia sakralna": "sacred",
|
||||
"magia sakralna — egzorcyzmy": "sacredExorcism",
|
||||
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
||||
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
||||
"egzorcyzmy": "sacredExorcism",
|
||||
"wiedźmia magia": "witch",
|
||||
@@ -35,8 +35,8 @@
|
||||
"nekromancja": "necromancy",
|
||||
"magia krwi": "blood",
|
||||
"magia szkarłatu": "crimson",
|
||||
"magia otchłani — magia aspektów": "eldritchAspects",
|
||||
"magia otchłani — pierwotna magia": "eldritchPrimal",
|
||||
"magia otchłani - magia aspektów": "eldritchAspects",
|
||||
"magia otchłani - pierwotna magia": "eldritchPrimal",
|
||||
"dzika wiedźmia magia": "wildWitch"
|
||||
},
|
||||
"disciplines": {
|
||||
@@ -46,13 +46,13 @@
|
||||
"warzenie eliksirów": "alchemyBrewing",
|
||||
"alchemia - warzenie eliksirów": "alchemyBrewing",
|
||||
"botanika": "botany",
|
||||
"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 — 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",
|
||||
"rzemiosło artefaktów": "artifice",
|
||||
"golemancja": "golemancy",
|
||||
@@ -61,7 +61,7 @@
|
||||
"źródło mocy": "manaSourceMage",
|
||||
"magia iluzji": "illusion",
|
||||
"magia sakralna": "sacred",
|
||||
"magia sakralna — egzorcyzmy": "sacredExorcism",
|
||||
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
||||
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
||||
"egzorcyzmy": "sacredExorcism",
|
||||
"wiedźmia magia": "witch",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
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;
|
||||
/** Bullet keys/values, preserved in order. */
|
||||
fields: Array<{ key: string; value: string }>;
|
||||
@@ -45,7 +45,7 @@ const SEPARATOR = /^_{3,}$/;
|
||||
const BULLET = /^\*\s+([^:]+):\s*(.+)$/;
|
||||
// Match any markdown heading level (1-6 `#` chars).
|
||||
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".
|
||||
const CHAPTER_HEADING = /^#{1,2}\s+(.+)$/;
|
||||
const SECTION_HEADING = /^#{3,4}\s+(.+)$/;
|
||||
@@ -65,7 +65,7 @@ function skipFrontmatter(lines: string[]): number {
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
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 {
|
||||
@@ -92,7 +92,7 @@ export function walkBook(path: string, opts: WalkOptions = {}): BookBlock[] {
|
||||
for (let i = startIdx; i < lines.length; i++) {
|
||||
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;
|
||||
|
||||
// Chapter headings: ## or # level (the two highest we honour).
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* with name, source book, and an empty description (filled in later).
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@@ -34,13 +34,13 @@ const DISCIPLINE_SEEDS: DisciplineSeed[] = [
|
||||
{ id: 'manaSourceMage', polishName: 'Źródło Mocy', pack: 'disciplines', book: 'podrecznik-gry' },
|
||||
{ id: 'illusion', polishName: 'Magia Iluzji', 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: 'necromancy', polishName: 'Nekromancja', pack: 'disciplines', book: 'ksiega-magii' },
|
||||
{ id: 'blood', polishName: 'Magia Krwi', pack: 'disciplines-forbidden', book: 'arcanum-sanguinis' },
|
||||
{ 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: 'abyssPrimal', polishName: 'Magia Otchłani — Pierwotna Magia', 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: '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/00. Podręcznik Gry.md'),
|
||||
];
|
||||
const target = normalizeKey(polishName.split('—')[0]);
|
||||
const target = normalizeKey(polishName.split('-')[0]);
|
||||
|
||||
for (const file of splitSources) {
|
||||
let text = '';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Macro parser. Reads hand-coded macro source files from `scripts/macros/*.js`
|
||||
* 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';
|
||||
@@ -23,6 +23,7 @@ const MACROS: MacroDef[] = [
|
||||
{ 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: '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: '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' },
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ParsedDoc, ParserContext, ParserFn, SourceBookId } from './types';
|
||||
import { slugify } from './helpers';
|
||||
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({
|
||||
castingMode: z.enum(['standard', 'sacred', 'witch', 'blood']),
|
||||
school: z.string(),
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* - Podręcznik Gry, "Rozdział IV - Talenty"
|
||||
* - 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.
|
||||
*/
|
||||
|
||||
@@ -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:
|
||||
* - first non-empty line = name (short, no trailing punctuation, no colon)
|
||||
* - optional `Wymagania: ...` line
|
||||
@@ -109,7 +109,7 @@ export const parseTalents: ParserFn = async (ctx: ParserContext): Promise<Parsed
|
||||
if (i >= slice.length) break;
|
||||
|
||||
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;
|
||||
|
||||
let isName = false;
|
||||
@@ -127,7 +127,7 @@ export const parseTalents: ParserFn = async (ctx: ParserContext): Promise<Parsed
|
||||
}
|
||||
|
||||
if (!isName) {
|
||||
// Can't read this — skip the line and continue.
|
||||
// Can't read this - skip the line and continue.
|
||||
i++;
|
||||
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;
|
||||
};
|
||||
|
||||
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. */
|
||||
function isPlausibleName(line: string): boolean {
|
||||
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.
|
||||
*
|
||||
* Every parsed document has a stable id (slug) and a sourceBook ref so
|
||||
@@ -21,7 +21,7 @@ export interface ParsedDoc {
|
||||
id: string;
|
||||
/** Display name (Polish, as in book). */
|
||||
name: string;
|
||||
/** Foundry document type — `Item` or `Actor`. */
|
||||
/** Foundry document type - `Item` or `Actor`. */
|
||||
documentType: 'Item' | 'Actor';
|
||||
/** Foundry sub-type for the data model (e.g. `spell`, `talent`). */
|
||||
subType: string;
|
||||
|
||||
+36
-35
@@ -49,29 +49,29 @@ export const SKILLS: Readonly<Record<string, AttributeKey>> = Object.freeze({
|
||||
* `blocksTags` on a condition prevents rolls of any skill containing that tag.
|
||||
*/
|
||||
export const SKILL_TAGS: Readonly<Record<string, readonly SkillTag[]>> = Object.freeze({
|
||||
athletics: ['movement'],
|
||||
agility: ['movement'],
|
||||
strength: [],
|
||||
melee: ['attack'],
|
||||
ranged: ['attack', 'sight'],
|
||||
stealth: ['movement', 'sight'],
|
||||
endurance: [],
|
||||
reflex: [],
|
||||
perception: ['sight', 'hearing'],
|
||||
intuition: ['social'],
|
||||
craft: [],
|
||||
medicine: [],
|
||||
generalLore: [],
|
||||
natureLore: [],
|
||||
magicLore: [],
|
||||
theology: [],
|
||||
empathy: ['social', 'hearing'],
|
||||
persuasion: ['social', 'hearing'],
|
||||
intimidation: ['social'],
|
||||
determination: [],
|
||||
devotion: [],
|
||||
disguise: ['social'],
|
||||
animalHandling: ['social'],
|
||||
athletics: ['movement'],
|
||||
agility: ['movement'],
|
||||
strength: [],
|
||||
melee: ['attack'],
|
||||
ranged: ['attack', 'sight'],
|
||||
stealth: ['movement', 'sight'],
|
||||
endurance: [],
|
||||
reflex: [],
|
||||
perception: ['sight', 'hearing'],
|
||||
intuition: ['social'],
|
||||
craft: [],
|
||||
medicine: [],
|
||||
generalLore: [],
|
||||
natureLore: [],
|
||||
magicLore: [],
|
||||
theology: [],
|
||||
empathy: ['social', 'hearing'],
|
||||
persuasion: ['social', 'hearing'],
|
||||
intimidation: ['social'],
|
||||
determination: [],
|
||||
devotion: [],
|
||||
disguise: ['social'],
|
||||
animalHandling: ['social'],
|
||||
magicalAbilities: ['magic'],
|
||||
});
|
||||
|
||||
@@ -90,17 +90,17 @@ export interface MagicPowerEntry {
|
||||
}
|
||||
|
||||
export const MAGIC_POWER_TABLE: readonly MagicPowerEntry[] = Object.freeze([
|
||||
{ level: 0, label: '0', dicePool: 6, maxPerSpell: 10, manaPerRound: 20 },
|
||||
{ level: 1, label: 'I', dicePool: 4, maxPerSpell: 6, manaPerRound: 12 },
|
||||
{ level: 2, label: 'II', dicePool: 4, maxPerSpell: 5, manaPerRound: 10 },
|
||||
{ level: 3, label: 'III', dicePool: 3, maxPerSpell: 5, manaPerRound: 7 },
|
||||
{ level: 4, label: 'IV', dicePool: 3, maxPerSpell: 4, manaPerRound: 8 },
|
||||
{ level: 5, label: 'V', dicePool: 3, maxPerSpell: 4, manaPerRound: 6 },
|
||||
{ level: 6, label: 'VI', dicePool: 2, maxPerSpell: 3, manaPerRound: 6 },
|
||||
{ level: 7, label: 'VII', dicePool: 2, maxPerSpell: 3, manaPerRound: 4 },
|
||||
{ level: 8, label: 'VIII', dicePool: 1, maxPerSpell: 2, manaPerRound: 3 },
|
||||
{ level: 9, label: 'IX', dicePool: 1, maxPerSpell: 1, manaPerRound: 2 },
|
||||
{ level: 10, label: 'X', dicePool: 0, maxPerSpell: 1, manaPerRound: 1 },
|
||||
{ level: 0, label: '0', dicePool: 6, maxPerSpell: 10, manaPerRound: 20 },
|
||||
{ level: 1, label: 'I', dicePool: 4, maxPerSpell: 6, manaPerRound: 12 },
|
||||
{ level: 2, label: 'II', dicePool: 4, maxPerSpell: 5, manaPerRound: 10 },
|
||||
{ level: 3, label: 'III', dicePool: 3, maxPerSpell: 5, manaPerRound: 7 },
|
||||
{ level: 4, label: 'IV', dicePool: 3, maxPerSpell: 4, manaPerRound: 8 },
|
||||
{ level: 5, label: 'V', dicePool: 3, maxPerSpell: 4, manaPerRound: 6 },
|
||||
{ level: 6, label: 'VI', dicePool: 2, maxPerSpell: 3, manaPerRound: 6 },
|
||||
{ level: 7, label: 'VII', dicePool: 2, maxPerSpell: 3, manaPerRound: 4 },
|
||||
{ level: 8, label: 'VIII', dicePool: 1, maxPerSpell: 2, manaPerRound: 3 },
|
||||
{ level: 9, label: 'IX', dicePool: 1, maxPerSpell: 1, manaPerRound: 2 },
|
||||
{ level: 10, label: 'X', dicePool: 0, maxPerSpell: 1, manaPerRound: 1 },
|
||||
]);
|
||||
|
||||
export function getMagicPowerEntry(level: number): MagicPowerEntry {
|
||||
@@ -133,6 +133,7 @@ export const SPELL_SCHOOLS = [
|
||||
'illusion', 'sacred', 'sacredExorcism', 'witch', 'necromancy',
|
||||
// Forbidden / extra-academic
|
||||
'blood', 'crimson', 'abyssAspects', 'abyssPrimal', 'wildWitch',
|
||||
'eldritch',
|
||||
] as const;
|
||||
export type SpellSchool = (typeof SPELL_SCHOOLS)[number];
|
||||
|
||||
@@ -144,7 +145,7 @@ export const SACRED_DEITIES = [
|
||||
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.
|
||||
*/
|
||||
export const WITCH_SYMBOLS: readonly string[] = Object.freeze([
|
||||
|
||||
+17
-24
@@ -15,7 +15,7 @@ import { ATTRIBUTES, SKILL_KEYS, SKILLS, AttributeKey, getMagicPowerEntry } from
|
||||
* - attributes.health.max = 3 * (body + mind + soul)
|
||||
* - attributes.zeal.max = ceil(soul / 2)
|
||||
* - 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 {
|
||||
static defineSchema() {
|
||||
@@ -66,10 +66,10 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
||||
maxPerSpell: makeIntField(0, { min: 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.
|
||||
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.
|
||||
elixirTolerance: makeIntField(0, { min: 0 }),
|
||||
// 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
|
||||
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),
|
||||
details: new f.SchemaField({
|
||||
@@ -101,7 +104,7 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
||||
discipline: makeStringField('', { blank: true }),
|
||||
title: makeStringField('', { blank: true }),
|
||||
biography: makeHtmlField(''),
|
||||
customEquipment: makeHtmlField(''), // legacy textarea — kept for migration
|
||||
customEquipment: makeHtmlField(''), // legacy textarea - kept for migration
|
||||
customItems: makeArrayField(new f.SchemaField({
|
||||
name: makeStringField('', { blank: true }),
|
||||
qty: makeIntField(1, { min: 0 }),
|
||||
@@ -144,7 +147,7 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
||||
magicalArmor: { value: number; max: number; runicCounter: number };
|
||||
magicalShield: { value: number };
|
||||
physicalArmor: { value: number; max: number; condition: number; conditionMax: number };
|
||||
initiative: number;
|
||||
initiative: { value: number; bonus: number };
|
||||
};
|
||||
skills: Record<string, { value: number; defaultAttribute: AttributeKey }>;
|
||||
details: { raceId?: string; classIds?: string[] };
|
||||
@@ -214,11 +217,12 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
||||
}
|
||||
}
|
||||
|
||||
a.initiative =
|
||||
a.initiative.value =
|
||||
a.mind.value +
|
||||
(sys.skills.reflex?.value ?? 0) +
|
||||
(sys.skills.perception?.value ?? 0) +
|
||||
initiativeBonus;
|
||||
initiativeBonus +
|
||||
(a.initiative.bonus ?? 0);
|
||||
|
||||
// Clamp magical armor / shield
|
||||
if (a.magicalArmor.value > a.magicalArmor.max) a.magicalArmor.value = a.magicalArmor.max;
|
||||
@@ -261,28 +265,13 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
||||
|
||||
// Man of Iron: +1 passive armor per stack, only when wearing no physical armor
|
||||
if (manOfIronStacks > 0 && armorPieces === 0) {
|
||||
a.physicalArmor.max = Math.max(a.physicalArmor.max, manOfIronStacks);
|
||||
a.physicalArmor.max = Math.max(a.physicalArmor.max, manOfIronStacks);
|
||||
a.physicalArmor.value = Math.max(a.physicalArmor.value, manOfIronStacks);
|
||||
}
|
||||
|
||||
// Blood Pool (placeholder formula): max = body + soul.
|
||||
a.blood.max = a.body.value + a.soul.value;
|
||||
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;
|
||||
}
|
||||
if (initialized?.attributes && typeof initialized.attributes.initiative === 'number') {
|
||||
const legacy = initialized.attributes.initiative;
|
||||
initialized.attributes.initiative = { value: legacy, bonus: 0 };
|
||||
}
|
||||
return initialized;
|
||||
}
|
||||
}
|
||||
|
||||
+43
-2
@@ -6,11 +6,11 @@ import {
|
||||
makeArrayField,
|
||||
fields,
|
||||
} from './fields';
|
||||
import { getMagicPowerEntry } from '../constants';
|
||||
import { ATTRIBUTES, SKILL_KEYS, SKILLS, AttributeKey, getMagicPowerEntry } from '../constants';
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
static defineSchema() {
|
||||
@@ -29,6 +29,17 @@ export class NpcData extends foundry.abstract.TypeDataModel {
|
||||
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 {
|
||||
attributes: new f.SchemaField({
|
||||
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),
|
||||
magicalShield: makeValueMaxField(0, 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'),
|
||||
}),
|
||||
skills: new f.SchemaField(skillEntries),
|
||||
details: new f.SchemaField({
|
||||
type: makeStringField(''),
|
||||
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 sys = this as unknown as {
|
||||
attributes: {
|
||||
mind: { value: number };
|
||||
magic: { value: number; actual: number };
|
||||
mana: { value: number; max: number; maxPerSpell: number };
|
||||
health: { value: number; max: number };
|
||||
magicalArmor: { value: number; max: number };
|
||||
magicalShield: { value: number; max: number };
|
||||
initiative: { value: number; bonus: number };
|
||||
};
|
||||
};
|
||||
const a = sys.attributes;
|
||||
@@ -120,6 +138,25 @@ export class NpcData extends foundry.abstract.TypeDataModel {
|
||||
a.mana.maxPerSpell = mp.maxPerSpell;
|
||||
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.magicalArmor.value > a.magicalArmor.max) a.magicalArmor.value = a.magicalArmor.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;
|
||||
}
|
||||
if (initialized?.attributes && typeof initialized.attributes.initiative === 'number') {
|
||||
const legacy = initialized.attributes.initiative;
|
||||
initialized.attributes.initiative = { value: legacy, bonus: 0 };
|
||||
}
|
||||
return initialized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
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
|
||||
* are scoped under `weapon` and `armor` sub-schemas.
|
||||
*/
|
||||
|
||||
@@ -85,7 +85,7 @@ export class SpellData extends foundry.abstract.TypeDataModel {
|
||||
minCasters: makeIntField(1, { min: 1 }),
|
||||
nonCombatOnly: makeBoolField(false),
|
||||
|
||||
// Overcast — structured
|
||||
// Overcast - structured
|
||||
overcastOptions: makeArrayField(new f.SchemaField({
|
||||
description: makeStringField(''),
|
||||
manaPerStep: makeIntField(1, { min: 0 }),
|
||||
@@ -105,7 +105,7 @@ export class SpellData extends foundry.abstract.TypeDataModel {
|
||||
|
||||
// Description
|
||||
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)
|
||||
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}"/>
|
||||
</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) {
|
||||
fields.push(`
|
||||
<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.bloodSpent != null) opts.bloodSpent = Math.max(baseBlood, Number(fd.bloodSpent) || baseBlood);
|
||||
if (fd.hekateMode) opts.hekateMode = 'witch';
|
||||
if (fd.castAsPrayer) opts.castAsPrayer = true;
|
||||
if (fd.bypassSuperspellWarning) opts.bypassSuperspellWarning = true;
|
||||
if (fd.bypassNonCombatBlock) opts.bypassNonCombatBlock = true;
|
||||
const groupRaw = String(fd.groupCasters ?? '').trim();
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function askApplyDamage(actor: ActorLike, presetAmount = 0): Promis
|
||||
|
||||
return new Promise((resolve) => {
|
||||
new foundry.appv1.api.Dialog({
|
||||
title: `${game.i18n.localize('HBM.damage.title')} — ${actor.name}`,
|
||||
title: `${game.i18n.localize('HBM.damage.title')} - ${actor.name}`,
|
||||
content: `
|
||||
<form class="hbm-dialog">
|
||||
<div class="form-group">
|
||||
|
||||
@@ -21,7 +21,7 @@ export function collectRollModifiers(actor: any, tags: readonly SkillTag[]): Rol
|
||||
const hbm = ef?.flags?.hbm;
|
||||
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;
|
||||
if (ts && typeof ts === 'object') {
|
||||
if (typeof ts['all'] === 'number') result.thresholdSteps += ts['all'];
|
||||
|
||||
+19
-6
@@ -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 { 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 {
|
||||
/** 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 {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -91,10 +98,16 @@ export async function rollAttribute(
|
||||
}
|
||||
|
||||
export async function rollInitiative(actor: ActorLike): Promise<any> {
|
||||
const mind = getAttributeValue(actor, 'mind');
|
||||
const reflex = actor.system.skills?.reflex?.value ?? 0;
|
||||
const perception = actor.system.skills?.perception?.value ?? 0;
|
||||
const mod = mind + reflex + perception;
|
||||
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 reflex = actor.system.skills?.reflex?.value ?? 0;
|
||||
const perception = actor.system.skills?.perception?.value ?? 0;
|
||||
mod = mind + reflex + perception;
|
||||
}
|
||||
const flavor = game.i18n.localize('HBM.resources.initiative');
|
||||
|
||||
const roll = new Roll('2d6 + @mod', { mod });
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface RollDialogResult {
|
||||
pool: number;
|
||||
threshold: number;
|
||||
required: number;
|
||||
modifier: number;
|
||||
flavor?: string;
|
||||
}
|
||||
|
||||
@@ -36,6 +37,10 @@ export async function askRollParams(initial: {
|
||||
<label>${game.i18n.localize('HBM.roll.required')} (1–10)</label>
|
||||
<input type="number" name="required" value="${y}" min="1" max="10"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${game.i18n.localize('HBM.roll.modifier')}</label>
|
||||
<input type="number" name="modifier" value="0"/>
|
||||
</div>
|
||||
</form>`,
|
||||
buttons: {
|
||||
roll: {
|
||||
@@ -48,6 +53,7 @@ export async function askRollParams(initial: {
|
||||
pool: initial.pool,
|
||||
threshold: Math.min(6, Math.max(2, Number(fd['threshold']) || t)),
|
||||
required: Math.min(10, Math.max(1, Number(fd['required']) || y)),
|
||||
modifier: Number(fd['modifier']) || 0,
|
||||
flavor: initial.flavor,
|
||||
});
|
||||
},
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@
|
||||
* otherwise face >= T → success
|
||||
* - 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.
|
||||
*/
|
||||
|
||||
@@ -121,7 +121,7 @@ function computeTsResult(roll: Roll): HbmTsRollResult {
|
||||
// Collect raw die faces from all DiceTerm results in the roll.
|
||||
const faces: number[] = [];
|
||||
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 }> };
|
||||
if (Array.isArray(anyTerm.results)) {
|
||||
for (const r of anyTerm.results) {
|
||||
|
||||
+7
-7
@@ -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
|
||||
* labels supplied via `lang/pl.json` (the canonical localisation).
|
||||
@@ -111,16 +111,16 @@ Hooks.once('init', () => {
|
||||
|
||||
// Override default initiative formula to HbM's 2d6 + initiative attribute.
|
||||
CONFIG.Combat.initiative = {
|
||||
formula: '2d6 + @attributes.initiative',
|
||||
formula: '2d6 + @attributes.initiative.value',
|
||||
decimals: 2,
|
||||
};
|
||||
|
||||
// Handlebars helpers used in templates
|
||||
Handlebars.registerHelper('eq', (a: unknown, b: unknown) => a === b);
|
||||
Handlebars.registerHelper('gt', (a: number, b: number) => a > b);
|
||||
Handlebars.registerHelper('lt', (a: number, b: number) => a < b);
|
||||
Handlebars.registerHelper('or', (...args: unknown[]) => (args.slice(0, -1) as unknown[]).some(Boolean));
|
||||
Handlebars.registerHelper('and', (...args: unknown[]) => (args.slice(0, -1) as unknown[]).every(Boolean));
|
||||
Handlebars.registerHelper('eq', (a: unknown, b: unknown) => a === b);
|
||||
Handlebars.registerHelper('gt', (a: number, b: number) => a > b);
|
||||
Handlebars.registerHelper('lt', (a: number, b: number) => a < b);
|
||||
Handlebars.registerHelper('or', (...args: unknown[]) => (args.slice(0, -1) as unknown[]).some(Boolean));
|
||||
Handlebars.registerHelper('and', (...args: unknown[]) => (args.slice(0, -1) as unknown[]).every(Boolean));
|
||||
Handlebars.registerHelper('concat', (...args: unknown[]) => (args.slice(0, -1) as string[]).join(''));
|
||||
});
|
||||
|
||||
|
||||
@@ -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:
|
||||
* - Magia Aspektów ("aspects") — controlled, low-risk, predictable
|
||||
* - Pierwotna Magia ("primal") — chaotic, high-power, requires `Dary Otchłani` rolls
|
||||
* - Magia Aspektów ("aspects") - controlled, low-risk, predictable
|
||||
* - Pierwotna Magia ("primal") - chaotic, high-power, requires `Dary Otchłani` rolls
|
||||
*
|
||||
* This module exposes:
|
||||
* - dispatchAbyssCast(spell) — returns 'aspects' | 'primal' from spell.discipline.
|
||||
* - rollAbyssGift(actor) — d100 against a roll table (resolved at runtime
|
||||
* - dispatchAbyssCast(spell) - returns 'aspects' | 'primal' from spell.discipline.
|
||||
* - rollAbyssGift(actor) - d100 against a roll table (resolved at runtime
|
||||
* from the `roll-tables-abyss.dary-otchlani` pack
|
||||
* when present; otherwise falls back to a chat
|
||||
* prompt for the GM).
|
||||
* - rollMistrzLosuPenalty(actor) — d100 penalty roll (Klątwa Otchłani VII).
|
||||
* - addInsanity(actor, n) — increments actor.system.attributes.insanity;
|
||||
* - rollMistrzLosuPenalty(actor) - d100 penalty roll (Klątwa Otchłani VII).
|
||||
* - addInsanity(actor, n) - increments actor.system.attributes.insanity;
|
||||
* when crossing a threshold, fires a hook for
|
||||
* the GM to apply a mental condition.
|
||||
*/
|
||||
@@ -69,7 +69,7 @@ async function rollAgainstAbyssTable(actor: CastableActor, slug: string, flavor:
|
||||
await roll.evaluate();
|
||||
const speaker = ChatMessage.getSpeaker({ actor: actor as unknown as Actor });
|
||||
await roll.toMessage({
|
||||
flavor: `${flavor} — ${actor.name} (${slug})`,
|
||||
flavor: `${flavor} - ${actor.name} (${slug})`,
|
||||
speaker,
|
||||
});
|
||||
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:
|
||||
* - 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.
|
||||
* Drinking a potion increments the counter; exceeding the cap triggers a
|
||||
@@ -12,12 +12,12 @@
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
export interface ElixirRecipe {
|
||||
name: string;
|
||||
/** TS test target — { threshold, successes }. */
|
||||
/** TS test target - { threshold, successes }. */
|
||||
difficulty: { threshold: number; successes: number };
|
||||
/** Ingredient names (free-form). */
|
||||
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 }> {
|
||||
const skillKey = recipe.skill ?? 'alchemyBrewing';
|
||||
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({
|
||||
pool,
|
||||
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.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 };
|
||||
}
|
||||
|
||||
+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).
|
||||
*/
|
||||
|
||||
@@ -50,14 +50,14 @@ export function registerCombatHooks(): void {
|
||||
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')) {
|
||||
ChatMessage.create({
|
||||
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') {
|
||||
const sys = actor.system as { attributes: { zeal: { value: number; max: number } } };
|
||||
const regen = getZealRegen(actor);
|
||||
|
||||
+26
-26
@@ -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
|
||||
* consumed by the roll pipeline and combat hooks.
|
||||
*/
|
||||
@@ -17,51 +17,51 @@ export interface ConditionDef {
|
||||
const CHANGE_MODE_ADD = 2;
|
||||
|
||||
export const CONDITIONS: ConditionDef[] = [
|
||||
{ id: 'przewrocony', i18nKey: 'HBM.conditions.przewrocony', icon: 'icons/svg/falling.svg', flags: { hbm: { prone: true } } },
|
||||
{ id: 'unieruchomiony', i18nKey: 'HBM.conditions.unieruchomiony', icon: 'icons/svg/net.svg', flags: { hbm: { thresholdSteps: { attack: 1 }, defenseSteps: -1 } } },
|
||||
{ id: 'obezwladniony', i18nKey: 'HBM.conditions.obezwladniony', icon: 'icons/svg/blood.svg', flags: { hbm: { blocksTags: ['attack'], defenseT: 2 } } },
|
||||
{ id: 'nieprzytomny', i18nKey: 'HBM.conditions.nieprzytomny', icon: 'icons/svg/unconscious.svg', flags: { hbm: { skipTurn: true, blocksAllActions: true } } },
|
||||
{ id: 'umierajacy', i18nKey: 'HBM.conditions.umierajacy', icon: 'icons/svg/skull.svg', flags: { hbm: { dyingState: { failures: 0, hits: 0, mortalDamage: 0 } } } },
|
||||
{ id: 'ogluszony', i18nKey: 'HBM.conditions.ogluszony', icon: 'icons/svg/deaf.svg', flags: { hbm: { blocksTags: ['hearing'] } } },
|
||||
{ id: 'oslepiony', i18nKey: 'HBM.conditions.oslepiony', icon: 'icons/svg/blind.svg', flags: { hbm: { blocksTags: ['sight'] } } },
|
||||
{ id: 'oszolomiony', i18nKey: 'HBM.conditions.oszolomiony', icon: 'icons/svg/daze.svg', flags: { hbm: { maxActions: 1, blockZeal: true, halveSpeed: true } } },
|
||||
{ id: 'przewrocony', i18nKey: 'HBM.conditions.przewrocony', icon: 'icons/svg/falling.svg', flags: { hbm: { prone: true } } },
|
||||
{ id: 'unieruchomiony', i18nKey: 'HBM.conditions.unieruchomiony', icon: 'icons/svg/net.svg', flags: { hbm: { thresholdSteps: { attack: 1 }, defenseSteps: -1 } } },
|
||||
{ id: 'obezwladniony', i18nKey: 'HBM.conditions.obezwladniony', icon: 'icons/svg/blood.svg', flags: { hbm: { blocksTags: ['attack'], defenseT: 2 } } },
|
||||
{ id: 'nieprzytomny', i18nKey: 'HBM.conditions.nieprzytomny', icon: 'icons/svg/unconscious.svg', flags: { hbm: { skipTurn: true, blocksAllActions: true } } },
|
||||
{ id: 'umierajacy', i18nKey: 'HBM.conditions.umierajacy', icon: 'icons/svg/skull.svg', flags: { hbm: { dyingState: { failures: 0, hits: 0, mortalDamage: 0 } } } },
|
||||
{ id: 'ogluszony', i18nKey: 'HBM.conditions.ogluszony', icon: 'icons/svg/deaf.svg', flags: { hbm: { blocksTags: ['hearing'] } } },
|
||||
{ id: 'oslepiony', i18nKey: 'HBM.conditions.oslepiony', icon: 'icons/svg/blind.svg', flags: { hbm: { blocksTags: ['sight'] } } },
|
||||
{ id: 'oszolomiony', i18nKey: 'HBM.conditions.oszolomiony', icon: 'icons/svg/daze.svg', flags: { hbm: { maxActions: 1, blockZeal: true, halveSpeed: true } } },
|
||||
{
|
||||
id: 'zatruty',
|
||||
i18nKey: 'HBM.conditions.zatruty',
|
||||
icon: 'icons/svg/poison.svg',
|
||||
changes: [
|
||||
{ key: 'system.attributes.body.value', mode: CHANGE_MODE_ADD, value: '-1' },
|
||||
{ key: 'system.attributes.mind.value', mode: CHANGE_MODE_ADD, value: '-1' },
|
||||
{ key: 'system.attributes.soul.value', mode: CHANGE_MODE_ADD, value: '-1' },
|
||||
{ key: 'system.attributes.body.value', mode: CHANGE_MODE_ADD, value: '-1' },
|
||||
{ key: 'system.attributes.mind.value', mode: CHANGE_MODE_ADD, value: '-1' },
|
||||
{ key: 'system.attributes.soul.value', mode: CHANGE_MODE_ADD, value: '-1' },
|
||||
{ key: 'system.attributes.magic.value', mode: CHANGE_MODE_ADD, value: '1' },
|
||||
],
|
||||
},
|
||||
{ id: 'podpalony', i18nKey: 'HBM.conditions.podpalony', icon: 'icons/svg/fire.svg', flags: { hbm: { damagePerRound: { amount: 1, type: 'environmental' } } } },
|
||||
{ id: 'spowolniony', i18nKey: 'HBM.conditions.spowolniony', icon: 'icons/svg/clockwork.svg', flags: { hbm: { halveSpeed: true } } },
|
||||
{ id: 'podpalony', i18nKey: 'HBM.conditions.podpalony', icon: 'icons/svg/fire.svg', flags: { hbm: { damagePerRound: { amount: 1, type: 'environmental' } } } },
|
||||
{ id: 'spowolniony', i18nKey: 'HBM.conditions.spowolniony', icon: 'icons/svg/clockwork.svg', flags: { hbm: { halveSpeed: true } } },
|
||||
{
|
||||
id: 'przeklety',
|
||||
i18nKey: 'HBM.conditions.przeklety',
|
||||
icon: 'icons/svg/sun.svg',
|
||||
changes: [
|
||||
{ key: 'system.attributes.body.value', mode: CHANGE_MODE_ADD, value: '-1' },
|
||||
{ key: 'system.attributes.mind.value', mode: CHANGE_MODE_ADD, value: '-1' },
|
||||
{ key: 'system.attributes.soul.value', mode: CHANGE_MODE_ADD, value: '-1' },
|
||||
{ key: 'system.attributes.body.value', mode: CHANGE_MODE_ADD, value: '-1' },
|
||||
{ key: 'system.attributes.mind.value', mode: CHANGE_MODE_ADD, value: '-1' },
|
||||
{ key: 'system.attributes.soul.value', mode: CHANGE_MODE_ADD, value: '-1' },
|
||||
{ key: 'system.attributes.magic.value', mode: CHANGE_MODE_ADD, value: '1' },
|
||||
],
|
||||
flags: { hbm: { blocksRegen: true } },
|
||||
},
|
||||
{ id: 'zauroczony', i18nKey: 'HBM.conditions.zauroczony', icon: 'icons/svg/heal.svg', flags: { hbm: { thresholdSteps: { social: 1 }, charmedBy: '' } } },
|
||||
{ id: 'koncentracja', i18nKey: 'HBM.conditions.koncentracja', icon: 'icons/svg/aura.svg', flags: { hbm: { concentration: true, persistent: true } } },
|
||||
{ id: 'zauroczony', i18nKey: 'HBM.conditions.zauroczony', icon: 'icons/svg/heal.svg', flags: { hbm: { thresholdSteps: { social: 1 }, charmedBy: '' } } },
|
||||
{ id: 'koncentracja', i18nKey: 'HBM.conditions.koncentracja', icon: 'icons/svg/aura.svg', flags: { hbm: { concentration: true, persistent: true } } },
|
||||
// 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).
|
||||
{ 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: 'depresja', i18nKey: 'HBM.conditions.depresja', icon: 'icons/svg/sleep.svg', flags: { hbm: { mental: true, todoBookGap: true } } },
|
||||
{ id: 'mania', i18nKey: 'HBM.conditions.mania', icon: 'icons/svg/lightning.svg', flags: { hbm: { mental: true, todoBookGap: true } } },
|
||||
{ id: 'schizofrenia', i18nKey: 'HBM.conditions.schizofrenia', icon: 'icons/svg/stoned.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: 'depresja', i18nKey: 'HBM.conditions.depresja', icon: 'icons/svg/sleep.svg', flags: { hbm: { mental: true, todoBookGap: true } } },
|
||||
{ id: 'mania', i18nKey: 'HBM.conditions.mania', icon: 'icons/svg/lightning.svg', flags: { hbm: { mental: true, todoBookGap: true } } },
|
||||
{ id: 'schizofrenia', i18nKey: 'HBM.conditions.schizofrenia', icon: 'icons/svg/stoned.svg', flags: { hbm: { mental: true, todoBookGap: true } } },
|
||||
];
|
||||
|
||||
export function registerHbmConditions(): void {
|
||||
|
||||
+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)
|
||||
* 2. Magical Shield (raw temp HP; cannot be healed; vanishes at 0)
|
||||
* 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;
|
||||
|
||||
// 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) {
|
||||
const dr = Math.max(0, Number(a.magicalArmor.value) || 0);
|
||||
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) {
|
||||
const shield = Math.max(0, Number(a.magicalShield.value) || 0);
|
||||
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) {
|
||||
const dr = Math.max(0, Number(a.physicalArmor.value) || 0);
|
||||
const absorbed = Math.min(dr, remaining);
|
||||
@@ -118,7 +118,7 @@ export async function applyDamage(actor: ActorLike, opts: ApplyDamageOptions): P
|
||||
|
||||
if (opts.postChat ?? true) {
|
||||
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.magicalShield) lines.push(`${game.i18n.localize('HBM.resources.magicalShield')}: −${report.absorbed.magicalShield}${report.shieldDropped ? ' ✦' : ''}`);
|
||||
|
||||
+5
-5
@@ -3,14 +3,14 @@
|
||||
*
|
||||
* Short Rest (Krótki Odpoczynek):
|
||||
* - 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.
|
||||
*
|
||||
* Long Rest (Długi Odpoczynek):
|
||||
* - 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).
|
||||
* - 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.
|
||||
*
|
||||
* Hooks `hbm.beforeRest` and `hbm.afterRest` fire for module integration.
|
||||
@@ -134,8 +134,8 @@ export async function rest(actor: CastableActor, kind: RestKind): Promise<RestRe
|
||||
const conditionIds = new Set(CONDITIONS.map(c => c.id));
|
||||
for (const ef of actorAny.effects ?? []) {
|
||||
const isState = conditionIds.has(ef.id) ||
|
||||
(ef.statuses && [...ef.statuses].some(s => conditionIds.has(s))) ||
|
||||
conditionIds.has(ef.flags?.core?.statusId);
|
||||
(ef.statuses && [...ef.statuses].some(s => conditionIds.has(s))) ||
|
||||
conditionIds.has(ef.flags?.core?.statusId);
|
||||
|
||||
if (isState) {
|
||||
ids.push(ef.id);
|
||||
|
||||
+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.
|
||||
*
|
||||
* 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
|
||||
* 2. Resource deduction — mana / zeal / blood
|
||||
* 3. TS test roll — pool depends on mode
|
||||
* 4. (success) damage roll — parses spell.damageBase
|
||||
* 5. Status effect auto-apply — from spell.statusEffects[]
|
||||
* 6. Trigger registration — from spell.triggers[]
|
||||
* 2. Resource deduction - mana / zeal / blood
|
||||
* 3. TS test roll - pool depends on mode
|
||||
* 4. (success) damage roll - parses spell.damageBase
|
||||
* 5. Status effect auto-apply - from spell.statusEffects[]
|
||||
* 6. Trigger registration - from spell.triggers[]
|
||||
*
|
||||
* Bypasses for GM workflows: opts.bypassSuperspellWarning, opts.bypassNonCombatBlock.
|
||||
*/
|
||||
|
||||
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 type { ValidationResult } from './spell-validation';
|
||||
import { rollSpellDamage } from './spell-damage';
|
||||
@@ -28,7 +28,7 @@ export interface CastableActor {
|
||||
type: 'character' | 'npc';
|
||||
system: {
|
||||
attributes: {
|
||||
magic: { value: number };
|
||||
magic: { value: number; actual?: number; dicePool?: number };
|
||||
soul: { value: number };
|
||||
mana: { value: number; max: number; maxPerSpell: number };
|
||||
zeal: { value: number; max: number };
|
||||
@@ -71,6 +71,7 @@ export interface CastOptions {
|
||||
zealSpent?: number;
|
||||
bloodSpent?: number;
|
||||
hekateMode?: 'sacred' | 'witch';
|
||||
castAsPrayer?: boolean;
|
||||
bypassSuperspellWarning?: boolean;
|
||||
bypassNonCombatBlock?: boolean;
|
||||
groupCasters?: string[];
|
||||
@@ -147,12 +148,16 @@ async function renderCastCard(
|
||||
triggersRegistered: number,
|
||||
): Promise<void> {
|
||||
const success = baseRoll?.ts?.isSuccess ?? false;
|
||||
const mode = spell.system.castingMode ?? 'standard';
|
||||
const mode = opts.castAsPrayer ? 'prayer' : (spell.system.castingMode ?? 'standard');
|
||||
|
||||
// Costs ledger
|
||||
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.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! });
|
||||
|
||||
// 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 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 flavor = `${spell.name}`;
|
||||
const magicDice = typeof a.magic.dicePool === 'number'
|
||||
? 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 baseRoll = HbmTSRoll.fromParams({
|
||||
pool,
|
||||
@@ -234,7 +250,7 @@ async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOp
|
||||
await baseRoll.evaluate();
|
||||
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) {
|
||||
const excess = manaSpent - a.mana.maxPerSpell;
|
||||
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 });
|
||||
|
||||
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 roll = HbmTSRoll.fromParams({
|
||||
pool,
|
||||
@@ -279,10 +295,21 @@ async function castWitch(actor: CastableActor, spell: SpellLike, opts: CastOptio
|
||||
|
||||
// Witch magic still has a mana cost.
|
||||
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 flavor = `${spell.name} — ${game.i18n.localize('HBM.spell.castingMode.witch')} (${required}/${maxSymbols})`;
|
||||
const magicDice = typeof (a.magic as any).dicePool === 'number'
|
||||
? (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 roll = HbmTSRoll.fromParams({
|
||||
pool,
|
||||
@@ -300,18 +327,31 @@ async function castBlood(actor: CastableActor, spell: SpellLike, opts: CastOptio
|
||||
const baseCost = Math.max(0, spell.system.bloodCost ?? 0);
|
||||
const bloodSpent = Math.max(baseCost, Math.floor(opts.bloodSpent ?? baseCost));
|
||||
const blood = a.blood;
|
||||
|
||||
const updates: Record<string, any> = {};
|
||||
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)
|
||||
const manaCost = Math.max(0, spell.system.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 flavor = `${spell.name} — ${game.i18n.localize('HBM.spell.castingMode.blood')}`;
|
||||
const magicDice = typeof (a.magic as any).dicePool === 'number'
|
||||
? (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 roll = HbmTSRoll.fromParams({
|
||||
pool,
|
||||
|
||||
@@ -25,13 +25,13 @@ function resolveToken(token: string, ctx: DamageContext): number {
|
||||
const a = (ctx.actor as any).system?.attributes ?? {};
|
||||
const s = (ctx.actor as any).system?.skills ?? {};
|
||||
switch (token) {
|
||||
case 'body': return Number(a.body?.value ?? 0);
|
||||
case 'mind': return Number(a.mind?.value ?? 0);
|
||||
case 'soul': return Number(a.soul?.value ?? 0);
|
||||
case 'magic': return Number(a.magic?.value ?? 0);
|
||||
case 'body': return Number(a.body?.value ?? 0);
|
||||
case 'mind': return Number(a.mind?.value ?? 0);
|
||||
case 'soul': return Number(a.soul?.value ?? 0);
|
||||
case 'magic': return Number(a.magic?.value ?? 0);
|
||||
case 'magicalAbilities': return Number(s.magicalAbilities?.value ?? 0);
|
||||
case 'devotion': return Number(s.devotion?.value ?? 0);
|
||||
default: return 0;
|
||||
case 'devotion': return Number(s.devotion?.value ?? 0);
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function buildDamageFormula(spellDamageBase: string, ctx: DamageContext):
|
||||
const tokenRe = /([a-zA-Z]+)(?:\s*([\/*])\s*(\d+))?/g;
|
||||
formula = formula.replace(tokenRe, (match, name: string, op: string | undefined, num: string | undefined) => {
|
||||
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;
|
||||
}
|
||||
let v = resolveToken(name, ctx);
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
* Hooks check the matching event and prompt/resolve the trigger.
|
||||
*
|
||||
* Supported events:
|
||||
* - killWithWeapon (e.g. Szkarłatny Sztylet — free spell after kill)
|
||||
* - targetCastsSpell (e.g. Klątwa Szkarłatu — opposed save → unconscious)
|
||||
* - killWithWeapon (e.g. Szkarłatny Sztylet - free spell after kill)
|
||||
* - targetCastsSpell (e.g. Klątwa Szkarłatu - opposed save → unconscious)
|
||||
* - damageTaken (reactive shields)
|
||||
* - turnStart (per-turn drains)
|
||||
*
|
||||
@@ -86,7 +86,7 @@ export async function fireTriggers(
|
||||
fired.push(t);
|
||||
// Post chat card so GM can resolve the effect manually if no auto-handler.
|
||||
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') ?? [],
|
||||
});
|
||||
if (!t.oneShot) remaining.push(t);
|
||||
|
||||
@@ -41,18 +41,42 @@ function actorHasTalent(actor: CastableActor, talentId: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function actorHasDiscipline(actor: CastableActor, disciplineId: string): boolean {
|
||||
const id = disciplineId.toLowerCase();
|
||||
const items = (actor as any).items as Iterable<any> | undefined;
|
||||
if (!items) return false;
|
||||
for (const it of items) {
|
||||
if (it.type !== 'discipline') continue;
|
||||
const d = String(it.system?.disciplineId ?? it.system?.id ?? it.name ?? '').toLowerCase();
|
||||
if (d === id) return true;
|
||||
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;
|
||||
}
|
||||
// Also accept characters whose details.discipline matches
|
||||
const detail = String((actor as any).system?.details?.discipline ?? '').toLowerCase();
|
||||
return detail === id;
|
||||
|
||||
if (key1 && key2 && key1 === key2) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function actorHasDiscipline(actor: CastableActor, disciplineId: string): boolean {
|
||||
const items = (actor as any).items as Iterable<any> | undefined;
|
||||
if (items) {
|
||||
for (const it of items) {
|
||||
if (it.type !== 'discipline') continue;
|
||||
const d = String(it.system?.disciplineId ?? it.system?.id ?? it.name ?? '');
|
||||
if (matchDiscipline(d, disciplineId)) return true;
|
||||
}
|
||||
}
|
||||
const detail = String((actor as any).system?.details?.discipline ?? '');
|
||||
return matchDiscipline(detail, disciplineId);
|
||||
}
|
||||
|
||||
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' });
|
||||
}
|
||||
}
|
||||
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);
|
||||
if (zealNeeded > Number(a.zeal?.value ?? 0)) {
|
||||
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)
|
||||
const discReq: string[] = Array.isArray(sys.requirements?.discipline) ? sys.requirements.discipline : [];
|
||||
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) {
|
||||
const translatedDisciplines = discReq.map(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.
|
||||
*
|
||||
* 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> {
|
||||
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);
|
||||
}
|
||||
|
||||
/** 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> {
|
||||
const roll = await rollTrade(actor, 'Przemyt', params);
|
||||
if (!roll.ts?.isSuccess) {
|
||||
|
||||
@@ -50,7 +50,7 @@ export async function runPendingMigrations(): Promise<void> {
|
||||
await step.run();
|
||||
} catch (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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,7 @@ import { askCastOptions } from '../dice/cast-dialog';
|
||||
import { askApplyDamage } from '../dice/damage-dialog';
|
||||
import { applyDamage } from '../logic/damage';
|
||||
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 {
|
||||
ActorSheetV2: typeof foundry.applications.sheets.ActorSheetV2;
|
||||
@@ -25,29 +25,29 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
position: { width: 760, height: 720 },
|
||||
window: { resizable: true, title: 'HBM.actor.character' },
|
||||
actions: {
|
||||
rollSkill: CharacterSheet._onRollSkill,
|
||||
rollAttribute: CharacterSheet._onRollAttribute,
|
||||
rollInitiative: CharacterSheet._onRollInitiative,
|
||||
castSpell: CharacterSheet._onCastSpell,
|
||||
deleteItem: CharacterSheet._onDeleteItem,
|
||||
editItem: CharacterSheet._onEditItem,
|
||||
applyDamage: CharacterSheet._onApplyDamage,
|
||||
takeBreather: CharacterSheet._onTakeBreather,
|
||||
shortRest: CharacterSheet._onShortRest,
|
||||
longRest: CharacterSheet._onLongRest,
|
||||
actorEffectCreate: CharacterSheet._onActorEffectCreate,
|
||||
actorEffectToggle: CharacterSheet._onActorEffectToggle,
|
||||
actorEffectEdit: CharacterSheet._onActorEffectEdit,
|
||||
actorEffectDelete: CharacterSheet._onActorEffectDelete,
|
||||
addArrayEntry: CharacterSheet._onAddArrayEntry,
|
||||
removeArrayEntry: CharacterSheet._onRemoveArrayEntry,
|
||||
editImage: CharacterSheet._onEditImage,
|
||||
toggleEquipped: CharacterSheet._onToggleEquipped,
|
||||
recalculateMoney: CharacterSheet._onRecalculateMoney,
|
||||
rollSkill: CharacterSheet._onRollSkill,
|
||||
rollAttribute: CharacterSheet._onRollAttribute,
|
||||
rollInitiative: CharacterSheet._onRollInitiative,
|
||||
castSpell: CharacterSheet._onCastSpell,
|
||||
deleteItem: CharacterSheet._onDeleteItem,
|
||||
editItem: CharacterSheet._onEditItem,
|
||||
applyDamage: CharacterSheet._onApplyDamage,
|
||||
takeBreather: CharacterSheet._onTakeBreather,
|
||||
shortRest: CharacterSheet._onShortRest,
|
||||
longRest: CharacterSheet._onLongRest,
|
||||
actorEffectCreate: CharacterSheet._onActorEffectCreate,
|
||||
actorEffectToggle: CharacterSheet._onActorEffectToggle,
|
||||
actorEffectEdit: CharacterSheet._onActorEffectEdit,
|
||||
actorEffectDelete: CharacterSheet._onActorEffectDelete,
|
||||
addArrayEntry: CharacterSheet._onAddArrayEntry,
|
||||
removeArrayEntry: CharacterSheet._onRemoveArrayEntry,
|
||||
editImage: CharacterSheet._onEditImage,
|
||||
toggleEquipped: CharacterSheet._onToggleEquipped,
|
||||
recalculateMoney: CharacterSheet._onRecalculateMoney,
|
||||
},
|
||||
form: {
|
||||
submitOnChange: true,
|
||||
closeOnSubmit: false,
|
||||
closeOnSubmit: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -55,16 +55,16 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
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' };
|
||||
|
||||
override async _prepareContext(options: unknown) {
|
||||
const ctx = (await super._prepareContext(options)) as Record<string, unknown>;
|
||||
const actor = (this as unknown as { actor: { system: unknown; items: any[] } }).actor;
|
||||
const spells = actor.items.filter((it: any) => it.type === 'spell');
|
||||
const gear = actor.items.filter((it: any) => it.type === 'gear');
|
||||
const spells = actor.items.filter((it: any) => it.type === 'spell');
|
||||
const gear = actor.items.filter((it: any) => it.type === 'gear');
|
||||
const abilities = actor.items.filter((it: any) => it.type === 'ability');
|
||||
const talents = actor.items.filter((it: any) => it.type === 'talent');
|
||||
const talents = actor.items.filter((it: any) => it.type === 'talent');
|
||||
|
||||
// Group spells by school for sheet display
|
||||
const spellsBySchool = new Map<string, any[]>();
|
||||
@@ -125,7 +125,8 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
'alchemyTransmutation', 'alchemyBrewing', 'botany',
|
||||
'elementsAir', 'elementsWater', 'elementsFire', 'elementsEarth',
|
||||
'artifice', 'golemancy', 'runes', 'manaSourceMage',
|
||||
'illusion', 'sacred', 'sacredExorcism', 'witch', 'necromancy', 'blood'
|
||||
'illusion', 'sacred', 'sacredExorcism', 'witch', 'necromancy', 'blood',
|
||||
'crimson', 'abyssAspects', 'abyssPrimal', 'eldritch'
|
||||
].map(key => ({
|
||||
key,
|
||||
label: game.i18n.localize(`HBM.spellSchool.${key}`)
|
||||
@@ -136,18 +137,18 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
const armors = gear.filter((it: any) => it.system?.category === 'armor');
|
||||
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 {
|
||||
...ctx,
|
||||
system: actor.system,
|
||||
system: actor.system,
|
||||
attributes,
|
||||
skills,
|
||||
races,
|
||||
disciplines,
|
||||
tabGroups: this.tabGroups,
|
||||
attributeKeys: ATTRIBUTES,
|
||||
skillKeys: SKILL_KEYS,
|
||||
tabGroups: this.tabGroups,
|
||||
attributeKeys: ATTRIBUTES,
|
||||
skillKeys: SKILL_KEYS,
|
||||
spells,
|
||||
spellSchoolGroups,
|
||||
hasBloodMagic,
|
||||
@@ -159,7 +160,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
abilities,
|
||||
talents,
|
||||
actorEffects,
|
||||
tabs: this._prepareTabs(),
|
||||
tabs: this._prepareTabs(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -168,8 +169,8 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
const active = this.tabGroups['primary'] ?? 'stats';
|
||||
return ['stats', 'actions', 'skills', 'spells', 'talents', 'inventory', 'effects', 'biography'].map((id) => ({
|
||||
id,
|
||||
label: `HBM.ui.tabs.${id}`,
|
||||
active: active === id,
|
||||
label: `HBM.ui.tabs.${id}`,
|
||||
active: active === id,
|
||||
cssClass: active === id ? 'active' : '',
|
||||
}));
|
||||
}
|
||||
@@ -211,7 +212,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
const actor = (this as unknown as { actor: any }).actor;
|
||||
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') {
|
||||
await actor.update({ 'system.details.raceId': uuid });
|
||||
return;
|
||||
@@ -258,8 +259,8 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
type: 'button', action: 'confirm',
|
||||
label: game.i18n.localize('HBM.ui.confirm') || 'Potwierd\u017a',
|
||||
default: true,
|
||||
callback: (_ev: Event, _btn: any, html: HTMLElement) => {
|
||||
const val = (html.querySelector('#hbm-talent-param-input') as HTMLInputElement)?.value?.trim();
|
||||
callback: (_ev: Event, _btn: any, dialog: any) => {
|
||||
const val = (dialog.element.querySelector('#hbm-talent-param-input') as HTMLInputElement)?.value?.trim();
|
||||
specified = val || null;
|
||||
},
|
||||
},
|
||||
@@ -290,17 +291,22 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
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 skill = actor.system.skills?.[skillKey];
|
||||
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}`) }) });
|
||||
if (!params) return;
|
||||
|
||||
await rollSkill(actor, skillKey, {
|
||||
threshold: params.threshold,
|
||||
required: params.required,
|
||||
flavor: params.flavor,
|
||||
required: params.required,
|
||||
modifier: params.modifier,
|
||||
flavor: params.flavor,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -309,13 +315,17 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
if (!attr) return;
|
||||
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}`) }) });
|
||||
if (!params) return;
|
||||
|
||||
await rollAttribute(actor, attr, {
|
||||
threshold: params.threshold,
|
||||
required: params.required,
|
||||
required: params.required,
|
||||
modifier: params.modifier,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -323,7 +333,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
const itemId = target.dataset.itemId;
|
||||
if (!itemId) return;
|
||||
const actor = (this as unknown as { actor: any }).actor;
|
||||
const spell = actor.items.get(itemId);
|
||||
const spell = actor.items.get(itemId);
|
||||
if (!spell || spell.type !== 'spell') return;
|
||||
|
||||
const opts = await askCastOptions(spell, actor);
|
||||
@@ -337,7 +347,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
const itemId = target.dataset.itemId;
|
||||
if (!itemId) return;
|
||||
const actor = (this as unknown as { actor: any }).actor;
|
||||
const item = actor.items.get(itemId);
|
||||
const item = actor.items.get(itemId);
|
||||
if (!item) return;
|
||||
await item.delete();
|
||||
}
|
||||
@@ -367,7 +377,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
const actor = (this as unknown as { actor: any }).actor;
|
||||
const r = await rest(actor, 'breather');
|
||||
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 }),
|
||||
});
|
||||
}
|
||||
@@ -376,7 +386,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
const actor = (this as unknown as { actor: any }).actor;
|
||||
const r = await rest(actor, 'short');
|
||||
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 }),
|
||||
});
|
||||
}
|
||||
@@ -385,7 +395,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
const actor = (this as unknown as { actor: any }).actor;
|
||||
const r = await rest(actor, 'long');
|
||||
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 }),
|
||||
});
|
||||
}
|
||||
@@ -500,14 +510,14 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
const isLive = !!(eurData || usdData);
|
||||
|
||||
const CURRENCIES: Record<string, { label: string; toPln: number; note: string }> = {
|
||||
PLN: { label: 'PLN (Złoty)', toPln: 1, note: '' },
|
||||
EUR: { label: 'EUR (Euro)', toPln: eurRate, note: `NBP ${rateDate}` },
|
||||
USD: { label: 'USD (Dolar)', toPln: usdRate, note: `NBP ${rateDate}` },
|
||||
TH: { label: 'TH (Thrakka)', toPln: eurRate, note: '1:1 z EUR' },
|
||||
FC: { label: 'FC (Kredyt Federacji Sol-3)', toPln: 2.0, note: 'stały kurs 2 PLN = 1 FC' },
|
||||
ST: { label: 'ST (Srebrny Talent)', toPln: 240, note: '1 ST = 240 PLN' },
|
||||
ZK: { label: 'ZK (Złota Korona)', toPln: 2880, note: '1 ZK = 12 ST' },
|
||||
PL: { label: 'PL (Platynowy Lingot)', toPln: 34560, note: '1 PL = 12 ZK' },
|
||||
PLN: { label: 'PLN (Złoty)', toPln: 1, note: '' },
|
||||
EUR: { label: 'EUR (Euro)', toPln: eurRate, note: `NBP ${rateDate}` },
|
||||
USD: { label: 'USD (Dolar)', toPln: usdRate, note: `NBP ${rateDate}` },
|
||||
TH: { label: 'TH (Thrakka)', toPln: eurRate, note: '1:1 z EUR' },
|
||||
FC: { label: 'FC (Kredyt Federacji Sol-3)', toPln: 2.0, note: 'stały kurs 2 PLN = 1 FC' },
|
||||
ST: { label: 'ST (Srebrny Talent)', toPln: 240, note: '1 ST = 240 PLN' },
|
||||
ZK: { label: 'ZK (Złota Korona)', toPln: 2880, note: '1 ZK = 12 ST' },
|
||||
PL: { label: 'PL (Platynowy Lingot)', toPln: 34560, note: '1 PL = 12 ZK' },
|
||||
};
|
||||
const currKeys = Object.keys(CURRENCIES);
|
||||
const optHtml = (sel: string) => currKeys.map(k =>
|
||||
@@ -562,8 +572,8 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
</div>
|
||||
<script>window._hbmRates=${ratesJson};</script>`;
|
||||
|
||||
await (foundry.applications.api as any).DialogV2.inform({
|
||||
title: game.i18n.localize('HBM.ui.moneyConverterTitle'),
|
||||
await (foundry.applications.api as any).DialogV2.prompt({
|
||||
window: { title: game.i18n.localize('HBM.ui.moneyConverterTitle') },
|
||||
content,
|
||||
rejectClose: false,
|
||||
});
|
||||
|
||||
+67
-25
@@ -1,11 +1,11 @@
|
||||
import { rollAttribute, rollInitiative } from '../dice/macros';
|
||||
import { rollSkill, rollAttribute, rollInitiative } from '../dice/macros';
|
||||
import { askRollParams } from '../dice/roll-dialog';
|
||||
import { askApplyDamage } from '../dice/damage-dialog';
|
||||
import { askCastOptions } from '../dice/cast-dialog';
|
||||
import { applyDamage } from '../logic/damage';
|
||||
import { castSpell } from '../logic/spell-cast';
|
||||
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 {
|
||||
ActorSheetV2: typeof foundry.applications.sheets.ActorSheetV2;
|
||||
@@ -20,21 +20,22 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
position: { width: 640, height: 720 },
|
||||
window: { resizable: true, title: 'HBM.actor.npc' },
|
||||
actions: {
|
||||
rollAttribute: NpcSheet._onRollAttribute,
|
||||
rollInitiative: NpcSheet._onRollInitiative,
|
||||
rollNpcAttack: NpcSheet._onRollNpcAttack,
|
||||
applyDamage: NpcSheet._onApplyDamage,
|
||||
castSpell: NpcSheet._onCastSpell,
|
||||
deleteItem: NpcSheet._onDeleteItem,
|
||||
editItem: NpcSheet._onEditItem,
|
||||
actorEffectCreate: NpcSheet._onActorEffectCreate,
|
||||
actorEffectToggle: NpcSheet._onActorEffectToggle,
|
||||
actorEffectEdit: NpcSheet._onActorEffectEdit,
|
||||
actorEffectDelete: NpcSheet._onActorEffectDelete,
|
||||
addArrayEntry: NpcSheet._onAddArrayEntry,
|
||||
removeArrayEntry: NpcSheet._onRemoveArrayEntry,
|
||||
editImage: NpcSheet._onEditImage,
|
||||
recalculateMoney: NpcSheet._onRecalculateMoney,
|
||||
rollSkill: NpcSheet._onRollSkill,
|
||||
rollAttribute: NpcSheet._onRollAttribute,
|
||||
rollInitiative: NpcSheet._onRollInitiative,
|
||||
rollNpcAttack: NpcSheet._onRollNpcAttack,
|
||||
applyDamage: NpcSheet._onApplyDamage,
|
||||
castSpell: NpcSheet._onCastSpell,
|
||||
deleteItem: NpcSheet._onDeleteItem,
|
||||
editItem: NpcSheet._onEditItem,
|
||||
actorEffectCreate: NpcSheet._onActorEffectCreate,
|
||||
actorEffectToggle: NpcSheet._onActorEffectToggle,
|
||||
actorEffectEdit: NpcSheet._onActorEffectEdit,
|
||||
actorEffectDelete: NpcSheet._onActorEffectDelete,
|
||||
addArrayEntry: NpcSheet._onAddArrayEntry,
|
||||
removeArrayEntry: NpcSheet._onRemoveArrayEntry,
|
||||
editImage: NpcSheet._onEditImage,
|
||||
recalculateMoney: NpcSheet._onRecalculateMoney,
|
||||
},
|
||||
form: { submitOnChange: true, closeOnSubmit: false },
|
||||
};
|
||||
@@ -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) {
|
||||
@@ -124,8 +131,8 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
type: 'button', action: 'confirm',
|
||||
label: game.i18n.localize('HBM.ui.confirm') || 'Potwierd\u017a',
|
||||
default: true,
|
||||
callback: (_ev: Event, _btn: any, html: HTMLElement) => {
|
||||
const val = (html.querySelector('#hbm-talent-param-input') as HTMLInputElement)?.value?.trim();
|
||||
callback: (_ev: Event, _btn: any, dialog: any) => {
|
||||
const val = (dialog.element.querySelector('#hbm-talent-param-input') as HTMLInputElement)?.value?.trim();
|
||||
specified = val || null;
|
||||
},
|
||||
},
|
||||
@@ -148,17 +155,52 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
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) {
|
||||
const attr = target.dataset.attribute as AttributeKey | undefined;
|
||||
if (!attr) return;
|
||||
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({
|
||||
pool,
|
||||
flavor: game.i18n.format('HBM.roll.rollAttribute', { attribute: game.i18n.localize(`HBM.attributes.${attr}`) }),
|
||||
});
|
||||
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) {
|
||||
@@ -177,7 +219,7 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
flavor: attack.name,
|
||||
});
|
||||
if (!params) return;
|
||||
const flavor = `${attack.name}${attack.damage ? ` — ${attack.damage}` : ''}`;
|
||||
const flavor = `${attack.name}${attack.damage ? ` - ${attack.damage}` : ''}`;
|
||||
const roll = HbmTSRoll.fromParams({
|
||||
pool: params.pool,
|
||||
threshold: params.threshold ?? TS_DEFAULT_THRESHOLD,
|
||||
@@ -399,8 +441,8 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
await (foundry.applications.api as any).DialogV2.inform({
|
||||
title: game.i18n.localize('HBM.ui.moneyConverterTitle'),
|
||||
await (foundry.applications.api as any).DialogV2.prompt({
|
||||
window: { title: game.i18n.localize('HBM.ui.moneyConverterTitle') },
|
||||
content: content,
|
||||
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 ─────────────────────────────────────────────── */
|
||||
@@ -25,8 +25,8 @@
|
||||
}
|
||||
|
||||
/* Dark theme overrides */
|
||||
.theme-dark .hbm,
|
||||
.hbm.theme-dark {
|
||||
.theme-dark .hbm:not(.theme-light),
|
||||
.hbm.theme-dark:not(.theme-light) {
|
||||
--hbm-fg: #e0d8c3;
|
||||
--hbm-bg: #1a1712;
|
||||
--hbm-bg-header: #0f0b07;
|
||||
@@ -45,9 +45,12 @@
|
||||
height: 100%;
|
||||
font-family: 'Signika', sans-serif;
|
||||
color: var(--hbm-fg);
|
||||
background: var(--hbm-bg);
|
||||
background: var(--hbm-bg) !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hbm.sheet .window-content {
|
||||
background: var(--hbm-bg) !important;
|
||||
}
|
||||
.hbm.sheet form,
|
||||
.hbm.sheet .sheet-wrapper {
|
||||
display: flex;
|
||||
@@ -235,7 +238,54 @@
|
||||
}
|
||||
.hbm .resource-value .sep { color: var(--hbm-border); }
|
||||
.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 ────────────────────────────────────────────────────────── */
|
||||
.hbm .skills-grid {
|
||||
@@ -351,6 +401,29 @@
|
||||
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 ────────────────────────────────────────────────────────── */
|
||||
.hbm-dialog .form-group {
|
||||
display: flex;
|
||||
|
||||
+5
-17
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"id": "hbm-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).",
|
||||
"version": "1.3.12",
|
||||
"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.30",
|
||||
"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": {
|
||||
"minimum": "13",
|
||||
"verified": "14"
|
||||
@@ -211,7 +211,7 @@
|
||||
},
|
||||
{
|
||||
"name": "talents-blood",
|
||||
"label": "Talenty — Magia Krwi",
|
||||
"label": "Talenty - Magia Krwi",
|
||||
"system": "hbm-rpg-v3",
|
||||
"path": "packs/talents-blood",
|
||||
"type": "Item",
|
||||
@@ -229,19 +229,7 @@
|
||||
},
|
||||
{
|
||||
"name": "talents-eldritch",
|
||||
"label": "Talents - Eldritch",
|
||||
"system": "hbm",
|
||||
"path": "packs/talents-eldritch",
|
||||
"type": "Item",
|
||||
"ownership": {
|
||||
"PLAYER": "OBSERVER",
|
||||
"ASSISTANT": "OWNER"
|
||||
},
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"name": "talents-eldritch",
|
||||
"label": "Talenty — Klątwa Otchłani",
|
||||
"label": "Talenty - Klątwa Otchłani",
|
||||
"system": "hbm-rpg-v3",
|
||||
"path": "packs/talents-eldritch",
|
||||
"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">
|
||||
<legend>{{localize 'HBM.activeEffect.title'}}</legend>
|
||||
<button type="button" class="hbm-effect-add" data-action="actorEffectCreate">+ {{localize 'HBM.activeEffect.addEffect'}}</button>
|
||||
|
||||
+740
-614
File diff suppressed because it is too large
Load Diff
@@ -77,6 +77,19 @@
|
||||
<input type="number" name="system.attributes.magicalArmor.max" value="{{system.attributes.magicalArmor.max}}" min="0" />
|
||||
</span>
|
||||
</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">
|
||||
<label>{{localize 'HBM.npc.speed'}}</label>
|
||||
<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>
|
||||
</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">
|
||||
<fieldset>
|
||||
<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 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 (or (eq mode "sacred") (eq mode "prayer"))}}
|
||||
<span class="badge warning">{{localize 'HBM.spellCast.cannotBeUnraveled'}}</span>
|
||||
{{/if}}
|
||||
{{#if outcome.success}}
|
||||
<span class="badge result success">✓ {{localize 'HBM.spellCast.success'}}</span>
|
||||
{{else}}
|
||||
@@ -72,7 +75,7 @@
|
||||
<h4>{{localize 'HBM.spellCast.triggers'}}</h4>
|
||||
<ul>
|
||||
{{#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}}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{{localize "HBM.roll.successes"}}: <strong>{{ts.successes}}</strong>
|
||||
/ {{localize "HBM.roll.required"}}: {{ts.required}}
|
||||
({{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>
|
||||
{{#if ts.criticalSuccesses}}
|
||||
<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}}
|
||||
<fieldset class="hbm-effects">
|
||||
<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.skillPoints'}}: <input type="number" name="system.skillPoints" value="{{system.skillPoints}}" min="0"/></label>
|
||||
</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>
|
||||
<legend>{{localize 'HBM.race.traits'}}
|
||||
<button type="button" data-action="addArrayEntry" data-path="system.traits"
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
{{#if (eq system.castingMode 'sacred')}}
|
||||
<label>{{localize 'HBM.spell.deity'}}:
|
||||
<select name="system.deity">
|
||||
<option value="">—</option>
|
||||
<option value="">-</option>
|
||||
{{#each @root.choices.deities as |label key|}}
|
||||
<option value="{{key}}" {{#if (eq ../system.deity key)}}selected{{/if}}>{{label}}</option>
|
||||
{{/each}}
|
||||
@@ -39,7 +39,7 @@
|
||||
{{/if}}
|
||||
<label>{{localize 'HBM.spell.sourceBook'}}:
|
||||
<select name="system.sourceBook">
|
||||
<option value="">—</option>
|
||||
<option value="">-</option>
|
||||
{{#each @root.choices.sourceBooks as |label key|}}
|
||||
<option value="{{key}}" {{#if (eq ../system.sourceBook key)}}selected{{/if}}>{{label}}</option>
|
||||
{{/each}}
|
||||
|
||||
+5
-5
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -54,15 +54,15 @@ import { vi } from 'vitest';
|
||||
mergeObject: (a: any, b: any) => Object.assign(a, b),
|
||||
},
|
||||
abstract: {
|
||||
TypeDataModel: class {},
|
||||
TypeDataModel: class { },
|
||||
},
|
||||
applications: {
|
||||
api: {
|
||||
HandlebarsApplicationMixin: <T extends abstract new (...args: any[]) => any>(base: T) => base,
|
||||
},
|
||||
sheets: {
|
||||
ActorSheetV2: class {},
|
||||
ItemSheetV2: class {},
|
||||
ActorSheetV2: class { },
|
||||
ItemSheetV2: class { },
|
||||
},
|
||||
handlebars: {
|
||||
loadTemplates: vi.fn(async () => []),
|
||||
@@ -89,7 +89,7 @@ import { vi } from 'vitest';
|
||||
},
|
||||
};
|
||||
|
||||
// Roll stub — pool roll counting successes >= threshold
|
||||
// Roll stub - pool roll counting successes >= threshold
|
||||
class StubRoll {
|
||||
formula: string;
|
||||
data: any;
|
||||
|
||||
Reference in New Issue
Block a user