diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c9726d..dd7bcc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index cc4ff22..b93b72d 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/lang/compendium/en/disciplines-forbidden.json b/lang/compendium/en/disciplines-forbidden.json index a78032e..a4a05c6 100644 --- a/lang/compendium/en/disciplines-forbidden.json +++ b/lang/compendium/en/disciplines-forbidden.json @@ -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": { diff --git a/lang/compendium/en/disciplines.json b/lang/compendium/en/disciplines.json index 7c4d996..e0bbda0 100644 --- a/lang/compendium/en/disciplines.json +++ b/lang/compendium/en/disciplines.json @@ -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]]" }, diff --git a/lang/compendium/en/hbm-macros.json b/lang/compendium/en/hbm-macros.json index d52e9b4..1fbe234 100644 --- a/lang/compendium/en/hbm-macros.json +++ b/lang/compendium/en/hbm-macros.json @@ -21,6 +21,9 @@ }, "Krótki Odpoczynek": { "name": "Short Rest" + }, + "Chwila Wytchnienia": { + "name": "Take a Breather" } } } diff --git a/lang/compendium/en/spells-elements.json b/lang/compendium/en/spells-elements.json deleted file mode 100644 index de2c509..0000000 --- a/lang/compendium/en/spells-elements.json +++ /dev/null @@ -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---" - } -} diff --git a/lang/compendium/en/spells-general.json b/lang/compendium/en/spells-general.json index 69b77cf..14427f4 100644 --- a/lang/compendium/en/spells-general.json +++ b/lang/compendium/en/spells-general.json @@ -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", diff --git a/lang/compendium/en/spells-superspells.json b/lang/compendium/en/spells-superspells.json deleted file mode 100644 index 1c1c810..0000000 --- a/lang/compendium/en/spells-superspells.json +++ /dev/null @@ -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." - } -} diff --git a/lang/compendium/en/talents-abyss.json b/lang/compendium/en/talents-abyss.json deleted file mode 100644 index 4dd9de7..0000000 --- a/lang/compendium/en/talents-abyss.json +++ /dev/null @@ -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---" - } - } -} diff --git a/lang/compendium/en/talents.json b/lang/compendium/en/talents.json index 7635e3d..1262acf 100644 --- a/lang/compendium/en/talents.json +++ b/lang/compendium/en/talents.json @@ -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." diff --git a/lang/en.json b/lang/en.json index ec32e17..a30ce3d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -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" } } -} +} \ No newline at end of file diff --git a/lang/pl.json b/lang/pl.json index cd72c1a..8a80eae 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -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" } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 6aa8818..9ae336c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/build-packs.ts b/scripts/build-packs.ts index 83b3cac..9520042 100644 --- a/scripts/build-packs.ts +++ b/scripts/build-packs.ts @@ -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 { diff --git a/scripts/generate-babele-translations.ts b/scripts/generate-babele-translations.ts index f9d969c..bc9a797 100644 --- a/scripts/generate-babele-translations.ts +++ b/scripts/generate-babele-translations.ts @@ -127,6 +127,7 @@ const nameTranslations: Record = { "necromancy": "Necromancy", "blood": "Blood Magic", "wildwitch": "Wild Witch Magic", + "take-breather": "Take a Breather", }; const folderNameTranslations: Record = { diff --git a/scripts/lint-uuid-refs.ts b/scripts/lint-uuid-refs.ts index 29f9468..bb9ca76 100644 --- a/scripts/lint-uuid-refs.ts +++ b/scripts/lint-uuid-refs.ts @@ -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. — only validate Item refs. + // UUID format: Compendium.system.pack.Item. - 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); } diff --git a/scripts/macros/apply-condition.js b/scripts/macros/apply-condition.js index 733031e..5dd3add 100644 --- a/scripts/macros/apply-condition.js +++ b/scripts/macros/apply-condition.js @@ -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.')); diff --git a/scripts/macros/attribute-check.js b/scripts/macros/attribute-check.js index 08cede7..1b1e669 100644 --- a/scripts/macros/attribute-check.js +++ b/scripts/macros/attribute-check.js @@ -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) => ``).join(''); const result = await Dialog.prompt({ - title: `Test atrybutu — ${actor.name}`, + title: `Test atrybutu - ${actor.name}`, content: `
@@ -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 }) }); diff --git a/scripts/macros/group-cast-helper.js b/scripts/macros/group-cast-helper.js index 342dd2d..e95167c 100644 --- a/scripts/macros/group-cast-helper.js +++ b/scripts/macros/group-cast-helper.js @@ -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(`
  • ${a.name} — Mana ${mana}, Magia ${magic}
  • `); + lines.push(`
  • ${a.name} - Mana ${mana}, Magia ${magic}
  • `); } const html = `
    diff --git a/scripts/macros/long-rest.js b/scripts/macros/long-rest.js index ef10e77..32a9171 100644 --- a/scripts/macros/long-rest.js +++ b/scripts/macros/long-rest.js @@ -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.`); diff --git a/scripts/macros/pool-roll.js b/scripts/macros/pool-roll.js index a63ab0d..4b3dce0 100644 --- a/scripts/macros/pool-roll.js +++ b/scripts/macros/pool-roll.js @@ -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: ` diff --git a/scripts/macros/refill-zeal.js b/scripts/macros/refill-zeal.js index 5af03a8..e7a9fe8 100644 --- a/scripts/macros/refill-zeal.js +++ b/scripts/macros/refill-zeal.js @@ -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; diff --git a/scripts/macros/short-rest.js b/scripts/macros/short-rest.js index 2f9c196..66db232 100644 --- a/scripts/macros/short-rest.js +++ b/scripts/macros/short-rest.js @@ -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.`); diff --git a/scripts/macros/take-breather.js b/scripts/macros/take-breather.js new file mode 100644 index 0000000..3c308ce --- /dev/null +++ b/scripts/macros/take-breather.js @@ -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.`); diff --git a/scripts/parsers/_id-overrides.json b/scripts/parsers/_id-overrides.json index 27405f0..e9c866f 100644 --- a/scripts/parsers/_id-overrides.json +++ b/scripts/parsers/_id-overrides.json @@ -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" +} \ No newline at end of file diff --git a/scripts/parsers/_label-mappings.json b/scripts/parsers/_label-mappings.json index 77a3108..d4debe5 100644 --- a/scripts/parsers/_label-mappings.json +++ b/scripts/parsers/_label-mappings.json @@ -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", @@ -126,4 +126,4 @@ "zdolności magiczne": "magicalAbilities", "oddanie": "devotion" } -} +} \ No newline at end of file diff --git a/scripts/parsers/book-walker.ts b/scripts/parsers/book-walker.ts index a883d6c..a788288 100644 --- a/scripts/parsers/book-walker.ts +++ b/scripts/parsers/book-walker.ts @@ -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). diff --git a/scripts/parsers/discipline-parser.ts b/scripts/parsers/discipline-parser.ts index b071b2f..efae68e 100644 --- a/scripts/parsers/discipline-parser.ts +++ b/scripts/parsers/discipline-parser.ts @@ -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 = ''; diff --git a/scripts/parsers/macro-parser.ts b/scripts/parsers/macro-parser.ts index 43ca1da..c9dddd5 100644 --- a/scripts/parsers/macro-parser.ts +++ b/scripts/parsers/macro-parser.ts @@ -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' }, diff --git a/scripts/parsers/spell-parser.ts b/scripts/parsers/spell-parser.ts index 2665c2c..5a22728 100644 --- a/scripts/parsers/spell-parser.ts +++ b/scripts/parsers/spell-parser.ts @@ -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(), @@ -101,7 +101,7 @@ export const parseSpells: ParserFn = async (ctx: ParserContext): Promise= 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 { + 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; diff --git a/scripts/parsers/types.ts b/scripts/parsers/types.ts index aa7678d..51f48ed 100644 --- a/scripts/parsers/types.ts +++ b/scripts/parsers/types.ts @@ -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; diff --git a/src/constants.ts b/src/constants.ts index 696a242..4406e5b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -49,29 +49,29 @@ export const SKILLS: Readonly> = Object.freeze({ * `blocksTags` on a condition prevents rolls of any skill containing that tag. */ export const SKILL_TAGS: Readonly> = 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([ diff --git a/src/data/actor-character.ts b/src/data/actor-character.ts index fd4f044..41a6ef3 100644 --- a/src/data/actor-character.ts +++ b/src/data/actor-character.ts @@ -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; 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; } } diff --git a/src/data/actor-npc.ts b/src/data/actor-npc.ts index a1f38b9..6d8c7d5 100644 --- a/src/data/actor-npc.ts +++ b/src/data/actor-npc.ts @@ -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 = {}; + 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 } } & Record; 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; } } diff --git a/src/data/item-gear.ts b/src/data/item-gear.ts index 27355e9..00f10db 100644 --- a/src/data/item-gear.ts +++ b/src/data/item-gear.ts @@ -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. */ diff --git a/src/data/item-spell.ts b/src/data/item-spell.ts index eafe482..84ce247 100644 --- a/src/data/item-spell.ts +++ b/src/data/item-spell.ts @@ -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(''), diff --git a/src/dice/cast-dialog.ts b/src/dice/cast-dialog.ts index 90bb917..2ecdab4 100644 --- a/src/dice/cast-dialog.ts +++ b/src/dice/cast-dialog.ts @@ -76,6 +76,12 @@ export async function askCastOptions(spell: SpellForDialog, actor: ActorForDialo
    `); } + if (mode !== 'sacred') { + fields.push(` +
    + +
    `); + } if (showHekate) { fields.push(`
    @@ -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(); diff --git a/src/dice/damage-dialog.ts b/src/dice/damage-dialog.ts index aed4907..357310a 100644 --- a/src/dice/damage-dialog.ts +++ b/src/dice/damage-dialog.ts @@ -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: `
    diff --git a/src/dice/effect-mods.ts b/src/dice/effect-mods.ts index 87e5f9f..7c5645d 100644 --- a/src/dice/effect-mods.ts +++ b/src/dice/effect-mods.ts @@ -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 | undefined; if (ts && typeof ts === 'object') { if (typeof ts['all'] === 'number') result.thresholdSteps += ts['all']; diff --git a/src/dice/macros.ts b/src/dice/macros.ts index 47792f2..9122ba3 100644 --- a/src/dice/macros.ts +++ b/src/dice/macros.ts @@ -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,12 +98,18 @@ export async function rollAttribute( } export async function rollInitiative(actor: ActorLike): Promise { - 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 }); await roll.evaluate(); await roll.toMessage({ diff --git a/src/dice/roll-dialog.ts b/src/dice/roll-dialog.ts index 10f71ff..bb0aef4 100644 --- a/src/dice/roll-dialog.ts +++ b/src/dice/roll-dialog.ts @@ -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: {
    +
    + + +
    `, 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, }); }, diff --git a/src/dice/ts-roll.ts b/src/dice/ts-roll.ts index cb065ed..4cdd3bf 100644 --- a/src/dice/ts-roll.ts +++ b/src/dice/ts-roll.ts @@ -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) { diff --git a/src/hbm.ts b/src/hbm.ts index b0571a1..81ad063 100644 --- a/src/hbm.ts +++ b/src/hbm.ts @@ -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('')); }); diff --git a/src/logic/abyss-magic.ts b/src/logic/abyss-magic.ts index 6594db2..1df4da1 100644 --- a/src/logic/abyss-magic.ts +++ b/src/logic/abyss-magic.ts @@ -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); diff --git a/src/logic/blood-magic.ts b/src/logic/blood-magic.ts index 890a38c..68e1661 100644 --- a/src/logic/blood-magic.ts +++ b/src/logic/blood-magic.ts @@ -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. diff --git a/src/logic/brewing.ts b/src/logic/brewing.ts index 25b550c..4780eea 100644 --- a/src/logic/brewing.ts +++ b/src/logic/brewing.ts @@ -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 { 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 }; } diff --git a/src/logic/combat.ts b/src/logic/combat.ts index 6d52453..ee3e84d 100644 --- a/src/logic/combat.ts +++ b/src/logic/combat.ts @@ -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: `${actor.name}: ${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); diff --git a/src/logic/conditions.ts b/src/logic/conditions.ts index 71c9e20..b0a3fde 100644 --- a/src/logic/conditions.ts +++ b/src/logic/conditions.ts @@ -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 { diff --git a/src/logic/damage.ts b/src/logic/damage.ts index bce1dbd..9add5e0 100644 --- a/src/logic/damage.ts +++ b/src/logic/damage.ts @@ -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[] = [ - `${actor.name} — ${game.i18n.localize('HBM.damage.report.title')}: ${total}`, + `${actor.name} - ${game.i18n.localize('HBM.damage.report.title')}: ${total}`, ]; 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 ? ' ✦' : ''}`); diff --git a/src/logic/rest.ts b/src/logic/rest.ts index 74e0626..1b2b4a5 100644 --- a/src/logic/rest.ts +++ b/src/logic/rest.ts @@ -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,9 +134,9 @@ export async function rest(actor: CastableActor, kind: RestKind): Promise 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); continue; diff --git a/src/logic/spell-cast.ts b/src/logic/spell-cast.ts index f9493b2..b2a3fa5 100644 --- a/src/logic/spell-cast.ts +++ b/src/logic/spell-cast.ts @@ -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 { 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 = { '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 = { '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 = {}; 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, diff --git a/src/logic/spell-damage.ts b/src/logic/spell-damage.ts index 8f54afa..287936c 100644 --- a/src/logic/spell-damage.ts +++ b/src/logic/spell-damage.ts @@ -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); diff --git a/src/logic/spell-triggers.ts b/src/logic/spell-triggers.ts index 8fe24cb..dd65737 100644 --- a/src/logic/spell-triggers.ts +++ b/src/logic/spell-triggers.ts @@ -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: `${actor.name ?? ''} — Wyzwalacz: ${t.spellName}
    Efekt: ${t.effect}`, + content: `${actor.name ?? ''} - Wyzwalacz: ${t.spellName}
    Efekt: ${t.effect}`, whisper: ChatMessage.getWhisperRecipients?.('GM') ?? [], }); if (!t.oneShot) remaining.push(t); diff --git a/src/logic/spell-validation.ts b/src/logic/spell-validation.ts index e26e759..73589b6 100644 --- a/src/logic/spell-validation.ts +++ b/src/logic/spell-validation.ts @@ -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 | 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 = { + 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 | 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; diff --git a/src/logic/trade.ts b/src/logic/trade.ts index c1a5f3e..baeaeae 100644 --- a/src/logic/trade.ts +++ b/src/logic/trade.ts @@ -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 { 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 { const roll = await rollTrade(actor, 'Przemyt', params); if (!roll.ts?.isSuccess) { diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 52d8e3c..6c57682 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -50,7 +50,7 @@ export async function runPendingMigrations(): Promise { 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; } } diff --git a/src/sheets/character-sheet.ts b/src/sheets/character-sheet.ts index f989067..58f10e9 100644 --- a/src/sheets/character-sheet.ts +++ b/src/sheets/character-sheet.ts @@ -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 = { primary: 'stats' }; override async _prepareContext(options: unknown) { const ctx = (await super._prepareContext(options)) as Record; 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(); @@ -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: `${actor.name} — Chwila Wytchnienia: +${r.hpRestored} ŻYW · +${r.manaRestored} MN`, + content: `${actor.name} - 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: `${actor.name} — Krótki Odpoczynek: +${r.hpRestored} ŻYW · +${r.manaRestored} MN${r.toleranceRecovered ? `, −${r.toleranceRecovered} tolerancja` : ''}`, + content: `${actor.name} - 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: `${actor.name} — Długi Odpoczynek: +${r.hpRestored} ŻYW · +${r.manaRestored} MN · +${r.zealRestored} ZP · +${r.bloodRestored} krew · −${r.toleranceRecovered} tolerancja`, + content: `${actor.name} - 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 = { - 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) {
    `; - 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, }); diff --git a/src/sheets/npc-sheet.ts b/src/sheets/npc-sheet.ts index 7276b1d..31d17c2 100644 --- a/src/sheets/npc-sheet.ts +++ b/src/sheets/npc-sheet.ts @@ -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) { @@ -110,7 +117,7 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) { const paramType = match[1]; const title = game.i18n.localize('HBM.ui.selectTalentParamTitle') || 'Wybierz parametr talentu'; const labelText = game.i18n.format('HBM.ui.selectTalentParamDesc', { param: paramType }) || `Wprowadź wartość dla parametru (${paramType}):`; - + let specified: string | null = null; await (foundry.applications.api as any).DialogV2.wait({ title, @@ -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, @@ -326,7 +368,7 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) { } } } - + // Fallback to latest rate try { const response = await fetch(`https://api.nbp.pl/api/exchangerates/rates/a/${currency}/?format=json`); @@ -399,8 +441,8 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
    `; - 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, }); diff --git a/styles/hbm.css b/styles/hbm.css index b94ada2..546697e 100644 --- a/styles/hbm.css +++ b/styles/hbm.css @@ -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; diff --git a/system.json b/system.json index 4de37f5..722acee 100644 --- a/system.json +++ b/system.json @@ -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", diff --git a/templates/actor/_actor-effects.hbs b/templates/actor/_actor-effects.hbs index 35c349d..3d7460d 100644 --- a/templates/actor/_actor-effects.hbs +++ b/templates/actor/_actor-effects.hbs @@ -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. --}}
    {{localize 'HBM.activeEffect.title'}} diff --git a/templates/actor/character.hbs b/templates/actor/character.hbs index 33468e5..1528270 100644 --- a/templates/actor/character.hbs +++ b/templates/actor/character.hbs @@ -1,285 +1,311 @@
    - -
    - -
    - - - - - -
    - - -
    + +
    + +
    + + + + + +
    + + +
    - -
    + +
    - -
    + +
    -
    - {{#each attributes as |attr|}} -
    - - {{#if (eq attr.key 'magic')}} - - - ({{localize 'HBM.attributes.actual'}}: {{attr.actualLabel}}) - - {{else}} - - {{/if}} - -
    - {{/each}} -
    - -
    -
    - - - - / - - -
    -
    - - - {{system.attributes.physicalArmor.value}} - - {{localize 'HBM.resources.max'}}: {{system.attributes.physicalArmor.max}} -
    - {{localize 'HBM.ui.physicalArmorBonus'}}: - -
    -
    -
    - - - - -
    -
    - - - - / - - - {{localize 'HBM.resources.runicCounter'}}: {{system.attributes.magicalArmor.runicCounter}}/5 -
    -
    - - - - / - - - {{localize 'HBM.resources.maxManaPerSpell'}}: {{system.attributes.mana.maxPerSpell}} -
    -
    - - - - / - - -
    - {{#if hasBloodMagic}} -
    - - - - / - - -
    - {{/if}} -
    - - - - - cap: {{elixirCap}} -
    - {{#if (gt system.attributes.insanity 0)}} -
    - - - - -
    - {{/if}} -
    - - {{system.attributes.initiative}} -
    -
    - - -
    -
    - - - {{system.advancement.attributePointsAvailable}} - + - - -
    -
    - - - {{system.advancement.skillPointsAvailable}} - + - - -
    -
    - - - {{system.advancement.freeTalentsAvailable}} - + - - -
    -
    -
    - - -
    -
    -

    {{localize 'HBM.ui.actionsTitle'}}

    -
    - - - - - -
    -
    -
    - - -
    -
    - {{#each skills as |sk|}} -
    - {{sk.label}} - - -
    - {{/each}} -
    -
    - - -
    - {{#if spells.length}} - {{#each spellSchoolGroups as |group|}} -
    - {{group.label}} ({{group.items.length}}) -
      - {{#each group.items as |spell|}} -
    • - {{spell.name}} - {{#if spell.system.isSuperspell}}{{/if}} - {{#if spell.system.requiresGroupCast}}{{/if}} - {{#if spell.system.nonCombatOnly}}{{/if}} - {{#if spell.system.requirements.race.length}}🔒{{/if}} +
      + {{#each attributes as |attr|}} +
      + + {{#if (eq attr.key 'magic')}} + + + ({{localize 'HBM.attributes.actual'}}: {{attr.actualLabel}}) - - K{{spell.system.circle}} - · {{localize (concat 'HBM.spell.castingMode.' spell.system.castingMode)}} - · {{localize 'HBM.resources.mana'}}: {{spell.system.manaCost}} - {{#if (eq spell.system.castingMode 'sacred')}}{{#if spell.system.deity}} · {{spell.system.deity}}{{/if}}{{/if}} - {{#if (eq spell.system.castingMode 'blood')}} · krew: {{spell.system.bloodCost}}{{/if}} - {{#if spell.system.sourceBook}} · {{spell.system.sourceBook}}{{/if}} + {{else}} + + {{/if}} + +
      + {{/each}} +
      + +
      +
      + + + + / + -
      - - - +
      +
      + + + {{system.attributes.physicalArmor.value}} + + {{localize 'HBM.resources.max'}}: {{system.attributes.physicalArmor.max}} +
      + {{localize 'HBM.ui.physicalArmorBonus'}}: +
      -
    • +
    +
    + + + + +
    +
    + + + + / + + + {{localize 'HBM.resources.runicCounter'}}: + {{system.attributes.magicalArmor.runicCounter}}/5 +
    +
    + + + + / + + + {{localize 'HBM.resources.maxManaPerSpell'}}: {{system.attributes.mana.maxPerSpell}} +
    +
    + + + + / + + +
    + {{#if hasBloodMagic}} +
    + + + + / + + +
    + {{/if}} +
    + + + + + cap: {{elixirCap}} +
    + {{#if (gt system.attributes.insanity 0)}} +
    + + + + +
    + {{/if}} +
    + + + {{system.attributes.initiative.value}} + +
    + {{localize 'HBM.ui.initiativeBonus'}}: + +
    +
    + +
    +
    {{localize 'HBM.ui.advancement'}}
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + + +
    +
    +

    {{localize 'HBM.ui.actionsTitle'}}

    +
    + + + + + +
    +
    +
    + + +
    +
    + {{#each skills as |sk|}} +
    + {{sk.label}} + + +
    {{/each}} - - - {{/each}} - {{else}} -

    {{localize 'HBM.ui.dropItems'}}

    - {{/if}} +
    +
    + + +
    + {{#if spells.length}} + {{#each spellSchoolGroups as |group|}} +
    + {{group.label}} ({{group.items.length}}) +
      + {{#each group.items as |spell|}} +
    • + {{spell.name}} + {{#if spell.system.isSuperspell}}{{/if}} + {{#if spell.system.requiresGroupCast}}{{/if}} + {{#if spell.system.nonCombatOnly}}{{/if}} + {{#if spell.system.requirements.race.length}}🔒{{/if}} + + + K{{spell.system.circle}} + · {{localize (concat 'HBM.spell.castingMode.' spell.system.castingMode)}} + · {{localize 'HBM.resources.mana'}}: {{spell.system.manaCost}} + {{#if (eq spell.system.castingMode 'sacred')}}{{#if spell.system.deity}} · + {{spell.system.deity}}{{/if}}{{/if}} + {{#if (eq spell.system.castingMode 'blood')}} · krew: {{spell.system.bloodCost}}{{/if}} + {{#if spell.system.sourceBook}} · {{spell.system.sourceBook}}{{/if}} + +
      + + + +
      +
    • + {{/each}} +
    +
    + {{/each}} + {{else}} +

    {{localize 'HBM.ui.dropItems'}}

    + {{/if}}
    -

    {{localize 'HBM.ui.dropTalentsHint'}}

    +

    {{localize + 'HBM.ui.dropTalentsHint'}}

    {{#if talents.length}}
      {{#each talents as |item|}} @@ -299,358 +325,458 @@

      {{localize 'HBM.ui.dropItems'}}

      {{/if}}
    - - -
    -
    -
    - 🪙 - {{localize 'HBM.ui.money'}}: - - PLN -
    - +
    + + {{#if weapons.length}} +
    +

    {{localize 'HBM.gear.weapon'}}

    + + + + + + + + + + + + + {{#each weapons as |item|}} + + + + + + + + + {{/each}} + +
    {{localize 'HBM.ui.name'}}{{localize 'HBM.gear.damage'}}{{localize 'HBM.gear.weight'}}{{localize 'HBM.gear.quantity'}}{{localize 'HBM.gear.equipped'}}
    {{item.name}}{{item.system.weapon.damage}} ({{localize (concat + 'HBM.damageType.' item.system.weapon.damageType)}}){{item.system.weight}}{{item.system.quantity}} + + + + +
    +
    + {{/if}} + + {{#if armors.length}} +
    +

    {{localize 'HBM.gear.armor'}}

    + + + + + + + + + + + + + + {{#each armors as |item|}} + + + + + + + + + + {{/each}} + +
    {{localize 'HBM.ui.name'}}{{localize 'HBM.gear.damageReduction'}}{{localize 'HBM.gear.condition'}}{{localize 'HBM.gear.weight'}}{{localize 'HBM.gear.quantity'}}{{localize 'HBM.gear.equipped'}}
    {{item.name}}{{item.system.armor.damageReduction}} DR{{item.system.armor.condition}} / + {{item.system.armor.conditionMax}}{{item.system.weight}}{{item.system.quantity}} + + + + +
    +
    + {{/if}} + + {{#if equipment.length}} +
    +

    {{localize 'HBM.gear.equipment'}}

    + + + + + + + + + + + {{#each equipment as |item|}} + + + + + + + {{/each}} + +
    {{localize 'HBM.ui.name'}}{{localize 'HBM.gear.weight'}}{{localize 'HBM.gear.quantity'}}
    {{item.name}}{{item.system.weight}}{{item.system.quantity}} + + +
    +
    + {{/if}} + + {{#if (eq gear.length 0)}} +

    {{localize 'HBM.ui.dropItems'}}

    + {{/if}} + + +
    +
    + {{localize 'HBM.ui.customEquipment'}} +
    - - {{#if weapons.length}} -
    -

    {{localize 'HBM.gear.weapon'}}

    - - - - - - - - - - - - - {{#each weapons as |item|}} - - - - - - - - - {{/each}} - -
    {{localize 'HBM.ui.name'}}{{localize 'HBM.gear.damage'}}{{localize 'HBM.gear.weight'}}{{localize 'HBM.gear.quantity'}}{{localize 'HBM.gear.equipped'}}
    {{item.name}}{{item.system.weapon.damage}} ({{localize (concat 'HBM.damageType.' item.system.weapon.damageType)}}){{item.system.weight}}{{item.system.quantity}} - - - - -
    -
    + {{#if system.details.customItems.length}} + + + + + + + + + + + {{#each system.details.customItems as |row i|}} + + + + + + + {{/each}} + +
    {{localize 'HBM.ui.name'}}{{localize 'HBM.gear.quantity'}}{{localize 'HBM.ui.notes'}}
    + + + + +
    + {{else}} +

    {{localize 'HBM.ui.dropItems'}}

    {{/if}} +
    +
    - {{#if armors.length}} -
    -

    {{localize 'HBM.gear.armor'}}

    - - - - - - - - - - - - - - {{#each armors as |item|}} - - - - - - - - - - {{/each}} - -
    {{localize 'HBM.ui.name'}}{{localize 'HBM.gear.damageReduction'}}{{localize 'HBM.gear.condition'}}{{localize 'HBM.gear.weight'}}{{localize 'HBM.gear.quantity'}}{{localize 'HBM.gear.equipped'}}
    {{item.name}}{{item.system.armor.damageReduction}} DR{{item.system.armor.condition}} / {{item.system.armor.conditionMax}}{{item.system.weight}}{{item.system.quantity}} - - - - -
    -
    - {{/if}} + +
    + {{> 'systems/hbm-rpg-v3/templates/actor/_actor-effects.hbs'}} +
    - {{#if equipment.length}} -
    -

    {{localize 'HBM.gear.equipment'}}

    - - - - - - - - - - - {{#each equipment as |item|}} - - - - - - - {{/each}} - -
    {{localize 'HBM.ui.name'}}{{localize 'HBM.gear.weight'}}{{localize 'HBM.gear.quantity'}}
    {{item.name}}{{item.system.weight}}{{item.system.quantity}} - - -
    -
    - {{/if}} - - {{#if (eq gear.length 0)}} -

    {{localize 'HBM.ui.dropItems'}}

    - {{/if}} - - -
    -
    - {{localize 'HBM.ui.customEquipment'}} - + +
    +
    +
    +
    + + {{localize 'HBM.ui.changeImageHint'}}
    - {{#if system.details.customItems.length}} - - - - - - - - - - - {{#each system.details.customItems as |row i|}} - - - - - - - {{/each}} - -
    {{localize 'HBM.ui.name'}}{{localize 'HBM.gear.quantity'}}{{localize 'HBM.ui.notes'}}
    - -
    - {{else}} -

    {{localize 'HBM.ui.dropItems'}}

    - {{/if}} -
    -
    - - -
    - {{> 'systems/hbm-rpg-v3/templates/actor/_actor-effects.hbs'}} -
    - - -
    -
    -
    -
    - - {{localize 'HBM.ui.changeImageHint'}} -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #{{localize 'HBM.schedule.hours'}}{{localize 'HBM.schedule.monday'}}{{localize 'HBM.schedule.tuesday'}}{{localize 'HBM.schedule.wednesday'}}{{localize 'HBM.schedule.thursday'}}{{localize 'HBM.schedule.friday'}}{{localize 'HBM.schedule.saturday'}}
    1
    2
    3
    {{localize 'HBM.schedule.lunchBreak'}}
    4
    5
    6
    7
    {{localize 'HBM.schedule.dinnerBreak'}}
    8
    9
    {{localize 'HBM.schedule.supperBreak'}}
    10
    11
    12
    -
    -
    - - +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    #{{localize 'HBM.schedule.hours'}}{{localize 'HBM.schedule.monday'}}{{localize 'HBM.schedule.tuesday'}}{{localize 'HBM.schedule.wednesday'}}{{localize 'HBM.schedule.thursday'}}{{localize 'HBM.schedule.friday'}}{{localize 'HBM.schedule.saturday'}}
    17:30-8:30
    28:35-9:35
    39:40-10:40
    + {{localize 'HBM.schedule.lunchBreak'}}
    411:10-12:10
    512:15-13:15
    613:20-14:20
    714:25-15:25
    + {{localize 'HBM.schedule.dinnerBreak'}}
    816:30-17:30
    917:30-18:30
    + {{localize 'HBM.schedule.supperBreak'}}
    1019:00-20:00
    1120:05-21:05
    1221:10-22:10
    +
    +
    + + +
    +
    + + +
    -
    - - -
    -
    -
    -
    -
    + +
    + \ No newline at end of file diff --git a/templates/actor/npc.hbs b/templates/actor/npc.hbs index 2a77523..58d71a8 100644 --- a/templates/actor/npc.hbs +++ b/templates/actor/npc.hbs @@ -77,6 +77,19 @@ +
    + + + {{system.attributes.initiative.value}} + +
    + {{localize 'HBM.ui.initiativeBonus'}}: + +
    +
    @@ -93,6 +106,22 @@ + +
    +
    + {{localize 'HBM.ui.tabs.skills'}} +
    + {{#each skills as |sk|}} +
    + {{sk.label}} + + +
    + {{/each}} +
    +
    +
    diff --git a/templates/chat/spell-cast.hbs b/templates/chat/spell-cast.hbs index ed05a07..66bee4f 100644 --- a/templates/chat/spell-cast.hbs +++ b/templates/chat/spell-cast.hbs @@ -5,6 +5,9 @@ {{#if mode}}{{localize (concat 'HBM.spell.castingMode.' mode)}}{{/if}} {{#if school}}{{localize (concat 'HBM.spellSchool.' school)}}{{/if}} {{#if circle}}{{localize 'HBM.spell.circle'}} {{circle}}{{/if}} + {{#if (or (eq mode "sacred") (eq mode "prayer"))}} + {{localize 'HBM.spellCast.cannotBeUnraveled'}} + {{/if}} {{#if outcome.success}} ✓ {{localize 'HBM.spellCast.success'}} {{else}} @@ -72,7 +75,7 @@

    {{localize 'HBM.spellCast.triggers'}}

      {{#each triggers as |t|}} -
    • {{localize (concat 'HBM.triggerEvent.' t.event)}} — {{t.effect}}
    • +
    • {{localize (concat 'HBM.triggerEvent.' t.event)}} - {{t.effect}}
    • {{/each}}
    diff --git a/templates/chat/ts-roll.hbs b/templates/chat/ts-roll.hbs index 0344d9b..ab7ecec 100644 --- a/templates/chat/ts-roll.hbs +++ b/templates/chat/ts-roll.hbs @@ -10,7 +10,7 @@ {{localize "HBM.roll.successes"}}: {{ts.successes}} / {{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}}
    {{#if ts.criticalSuccesses}}
    {{localize "HBM.roll.criticalSuccess"}}: {{ts.criticalSuccesses}}
    diff --git a/templates/item/_effects.hbs b/templates/item/_effects.hbs index ec0dc73..0e48803 100644 --- a/templates/item/_effects.hbs +++ b/templates/item/_effects.hbs @@ -1,4 +1,4 @@ -{{!-- Active Effects partial — included by talent and gear sheets. --}} +{{!-- Active Effects partial - included by talent and gear sheets. --}} {{#if supportsEffects}}
    {{localize 'HBM.activeEffect.title'}} diff --git a/templates/item/race.hbs b/templates/item/race.hbs index 0758fb3..9385cdd 100644 --- a/templates/item/race.hbs +++ b/templates/item/race.hbs @@ -3,7 +3,7 @@ - +
    {{localize 'HBM.race.traits'}}