Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
601f154b75 | ||
|
|
c533ad8040 | ||
|
|
6b8d93f772 | ||
|
|
5f8e62ba02 | ||
|
|
ee6c1248ea | ||
|
|
522aac8678 | ||
|
|
2f3f0057b1 |
@@ -5,6 +5,25 @@ All notable changes to the **HbM RPG v3** Foundry VTT system.
|
||||
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
||||
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.4.0] - 2026-10-20
|
||||
|
||||
Vault sync ("Foundry Sync Brief 2026-10-20").
|
||||
|
||||
### Changed
|
||||
- Magic power levels III/IV: daily mana swapped to 24 / 21.
|
||||
- Wyczerpanie Many: casting beyond the daily pool is allowed; the shortfall is borrowed (cumulative counter `manaBorrowed`, reset by Długi Odpoczynek) and a N d6 tier roll is posted.
|
||||
- Over-limit overcast failure now rolls Kara za Nieudolność (N d6, tier 5-8 drains the pool by the base cost).
|
||||
- Odetchnięcie limited to twice between Długie Odpoczynki (`breathersTaken`); Długi Odpoczynek resets counters.
|
||||
- Oszołomiony applied automatically when one hit costs ≥ ½ max Żywotność; Koncentracja test 4:⌈⅓ lost⌉ reported; Oszołomiony/Przerażony expire at the end of the character's next turn.
|
||||
- Przewrócony and Przerażony condition definitions rewritten; Magiczny Pocisk deals at least 1 after Pancerz.
|
||||
- Armor-condition damage reduction rounds up.
|
||||
- "Reakcja" relabelled "Akcja Natychmiastowa" (per-round counter `immediateActionsUsed` reset each round).
|
||||
- Spell data recompiled from the vault (new costs, Aegis of Zeus slug `aegis-zeus`); spell `difficulty.successes` max raised to 30 (superspells); new `thaumaturgy` school.
|
||||
|
||||
### Added
|
||||
- `src/logic/rules.ts`: pure helpers for the new rules (+ tests), `absorbBlood`, `drinkManaElixir`, trade constants.
|
||||
- Migration 1.4.0.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"Powietrzna Fala": {
|
||||
"name": "Air Wave",
|
||||
"description": "Mag wyrzuca z siebie potężną falę wiatru.\r\n\r\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."
|
||||
"description": "Mag wyrzuca z siebie potężną falę wiatru.\r\n\r\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 Przewrócony. Niezależnie od wyniku testu każda osoba w obszarze działania zaklęcia otrzymuje tyle punktów obrażeń, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne maga."
|
||||
},
|
||||
"Ożywienie Szkieletu": {
|
||||
"name": "Animate Skeleton",
|
||||
@@ -27,11 +27,11 @@
|
||||
},
|
||||
"Woal Snów": {
|
||||
"name": "Dream Veil",
|
||||
"description": "Woal Snów przekształca obszar o promieniu 2 m (mechanicznie reprezentowanym przez kwadrat o boku 5 m) w fantasmagoryczny świat snów i koszmarów.\r\n\r\nWszyscy przeciwnicy w obszarze działania zaklęcia muszą wykonać Test na Soul (Determinacja) 4:S, gdzie liczba wymaganych sukcesów (S) jest równa liczbie wyrzuconych przez maga sukcesów na początku swojej tury lub po wejściu do obszaru.\r\n\r\nNiepowodzenie skutkuje otrzymaniem tylu obrażeń ignorujących Pancerz, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne maga rzucającego to zaklęcie, a ponadto Postać staje się Przerażona i wszystkie Testy związane z Umiejętnością Spostrzegawczość wymagają dwóch Sukcesów więcej.\r\n\r\nW przypadku zdania Testu Postać tak czy inaczej ma karę do Testów związanych z Umiejętnością Spostrzegawczość."
|
||||
"description": "Woal Snów przekształca obszar o promieniu 2 m (mechanicznie reprezentowanym przez kwadrat o boku 5 m) w fantasmagoryczny świat snów i koszmarów.\r\n\r\nWszyscy przeciwnicy w obszarze działania zaklęcia muszą wykonać Test na Soul (Determinacja) 4:S, gdzie liczba wymaganych sukcesów (S) jest równa liczbie wyrzuconych przez maga sukcesów na początku swojej tury lub po wejściu do obszaru.\r\n\r\nNiepowodzenie skutkuje otrzymaniem tylu obrażeń ignorujących Pancerz, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne maga rzucającego to zaklęcie, a ponadto Postać staje się [Przerażona](../../../rules/00.%20Podręcznik%20Gry/Aneks%20A%20-%20Stany%20Postaci/Przerażony.md) i wszystkie Testy związane z Umiejętnością Spostrzegawczość wymagają dwóch Sukcesów więcej.\r\n\r\nW przypadku zdania Testu Postać tak czy inaczej ma karę do Testów związanych z Umiejętnością Spostrzegawczość."
|
||||
},
|
||||
"Trzęsienie Ziemi": {
|
||||
"name": "Earthquake",
|
||||
"description": "W wybranym przez maga obszarze ziemia zaczyna się silnie trząść.\r\n\r\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."
|
||||
"description": "W wybranym przez maga obszarze ziemia zaczyna się silnie trząść.\r\n\r\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 Przewrócona 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."
|
||||
},
|
||||
"Eteryczny Przewodnik": {
|
||||
"name": "Ether Guide",
|
||||
@@ -63,11 +63,11 @@
|
||||
},
|
||||
"Tarcza Domu": {
|
||||
"name": "House Shield",
|
||||
"description": "Mag otacza wybrany cel gęstą, wielowarstwową osłoną, znacznie mocniejszą niż standardowa Magiczna Tarcza.\r\n\r\nTarcza ma wytrzymałość równą wartości Umiejętności Zdolności Magiczne maga + 3. Gdy cel ma otrzymać obrażenia, są one zmniejszane maksymalnie o obecną wytrzymałość Tarczy, która spada o tyle, ile obrażeń pochłonęła. Gdy wytrzymałość spadnie do 0, zaklęcie przestaje działać.\r\n\r\nDopóki cel jest chroniony tą Tarczą, następne zaklęcie, które rzuci do końca swojej najbliższej Tury, otrzymuje dodatkową kość do puli Testu.\r\n\r\nZa dodatkowe 2 punkty Many, mag może rzucić to zaklęcie na samego siebie zamiast na inny cel."
|
||||
"description": "Mag otacza wybrany cel gęstą, wielowarstwową osłoną, znacznie mocniejszą niż standardowa Magiczna Tarcza.\n\nTarcza ma wytrzymałość równą wartości Umiejętności Zdolności Magiczne maga + 3. Gdy cel ma otrzymać obrażenia, są one zmniejszane maksymalnie o obecną wytrzymałość Tarczy, która spada o tyle, ile obrażeń pochłonęła. Gdy wytrzymałość spadnie do 0, zaklęcie przestaje działać.\n\nDopóki cel jest chroniony tą Tarczą, następne zaklęcie, które rzuci do końca swojej najbliższej Tury, otrzymuje dodatkową kość do puli Testu.\n\nZa dodatkowe 2 punkty Many, mag może rzucić to zaklęcie na samego siebie zamiast na inny cel."
|
||||
},
|
||||
"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."
|
||||
"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 Przewrócona."
|
||||
},
|
||||
"Podpalenie": {
|
||||
"name": "Ignition",
|
||||
@@ -95,7 +95,7 @@
|
||||
},
|
||||
"Magnetyzm": {
|
||||
"name": "Magnetism",
|
||||
"description": "Wykorzystujesz swoje zdolności i wiedzę dotyczącą przemian metali w celu wytworzenia pola magnetycznego o promieniu 2 m (mechanicznie reprezentowanym przez kwadrat o boku 5 m) centrowanego na Tobie.\r\n\r\nGdy rzucisz to zaklęcie, wszystkie istoty trzymające metalową broń lub noszący metalowy ekwipunek muszą 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 wszelkie metalowe przedmioty są wyrywane z rąk tej istoty i upadają 1 m od Ciebie.\r\n\r\nPonadto, jeśli dana istota nosi metalową zbroję i nie zda tego Testu, zostaje Powalona oraz przesunięta w Twoją stronę o 2 m (do 1 m od Ciebie).\r\n\r\nPonadto wszystkie istoty w Twoim pobliżu otrzymują -1 kość do rzutów na Walkę Wręcz oraz Umiejętności Strzeleckie.\r\n\r\n---"
|
||||
"description": "Wykorzystujesz swoje zdolności i wiedzę dotyczącą przemian metali w celu wytworzenia pola magnetycznego o promieniu 2 m (mechanicznie reprezentowanym przez kwadrat o boku 5 m) centrowanego na Tobie.\r\n\r\nGdy rzucisz to zaklęcie, wszystkie istoty trzymające metalową broń lub noszący metalowy ekwipunek muszą 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 wszelkie metalowe przedmioty są wyrywane z rąk tej istoty i upadają 1 m od Ciebie.\r\n\r\nPonadto, jeśli dana istota nosi metalową zbroję i nie zda tego Testu, zostaje Przewrócona oraz przesunięta w Twoją stronę o 2 m (do 1 m od Ciebie).\r\n\r\nPonadto do końca trwania zaklęcia wszystkie istoty w Twoim pobliżu otrzymują -1 kość do rzutów na Walkę Wręcz oraz Umiejętności Strzeleckie.\r\n\r\n---"
|
||||
},
|
||||
"Labirynt Umysłu": {
|
||||
"name": "Mind Labyrinth",
|
||||
@@ -119,7 +119,7 @@
|
||||
},
|
||||
"Osłona Świty": {
|
||||
"name": "Retinue Guard",
|
||||
"description": "Mag rozdziela swoją moc pomiędzy do czterech celów w zasięgu, otaczając każdego z nich osobną, słabszą Tarczą.\r\n\r\nKażda z Tarcz ma niezależną wytrzymałość równą połowie wartości Umiejętności Zdolności Magiczne maga (zaokrąglonej w górę) + 1. Gdy dany cel ma otrzymać obrażenia, są one zmniejszane maksymalnie o wytrzymałość jego własnej Tarczy, która spada o tyle, ile obrażeń pochłonęła. Gdy wytrzymałość danej Tarczy spadnie do 0, znika tylko ta jedna Tarcza - pozostałe działają nadal.\r\n\r\nMag może objąć tym zaklęciem samego siebie jako jeden z celów bez dodatkowego kosztu."
|
||||
"description": "Mag rozdziela swoją moc pomiędzy do czterech celów w zasięgu, otaczając każdego z nich osobną, słabszą Tarczą.\n\nKażda z Tarcz ma niezależną wytrzymałość równą połowie wartości Umiejętności Zdolności Magiczne maga (zaokrąglonej w górę) + 1. Gdy dany cel ma otrzymać obrażenia, są one zmniejszane maksymalnie o wytrzymałość jego własnej Tarczy, która spada o tyle, ile obrażeń pochłonęła. Gdy wytrzymałość danej Tarczy spadnie do 0, znika tylko ta jedna Tarcza - pozostałe działają nadal.\n\nMag może objąć tym zaklęciem samego siebie jako jeden z celów bez dodatkowego kosztu."
|
||||
},
|
||||
"Rozsypanie": {
|
||||
"name": "Shatter",
|
||||
@@ -127,7 +127,7 @@
|
||||
},
|
||||
"Bańka Ochronna": {
|
||||
"name": "Shield Bubble",
|
||||
"description": "Mag wyczarowuje kopulastą osłonę o promieniu 4 m (mechanicznie kwadrat o boku 10 m), obejmującą wszystkich sojuszników wewnątrz.\r\n\r\nBańka ma jedną, wspólną wytrzymałość równą wartości Umiejętności Zdolności Magiczne maga + 5. Każdy atak wymierzony w istotę osłoniętą Bańką, który przychodzi z zewnątrz, jest zmniejszany o aktualną wytrzymałość Bańki, która spada o tyle, ile obrażeń pochłonęła - dopóki nie spadnie do 0, po czym zaklęcie przestaje działać. Istoty wewnątrz Bańki mogą swobodnie ją opuszczać i wracać.\r\n\r\nDopóki Bańka trwa, każdy sojusznik wewnątrz niej, który rzuci zaklęcie, otrzymuje dodatkową kość do puli tego Testu.\r\n\r\nZa dodatkowe 3 punkty Many, mag może umieścić środek Bańki na sobie zamiast w innym punkcie w zasięgu."
|
||||
"description": "Mag wyczarowuje kopulastą osłonę o promieniu 4 m (mechanicznie kwadrat o boku 10 m), obejmującą wszystkich sojuszników wewnątrz.\n\nBańka ma jedną, wspólną wytrzymałość równą wartości Umiejętności Zdolności Magiczne maga + 3. Każdy atak wymierzony w istotę osłoniętą Bańką, który przychodzi z zewnątrz, jest zmniejszany o aktualną wytrzymałość Bańki, która spada o tyle, ile obrażeń pochłonęła - dopóki nie spadnie do 0, po czym zaklęcie przestaje działać. Istoty wewnątrz Bańki mogą swobodnie ją opuszczać i wracać.\n\nDopóki Bańka trwa, każdy sojusznik wewnątrz niej, który rzuci zaklęcie, otrzymuje dodatkową kość do puli tego Testu.\n\nZa dodatkowe 3 punkty Many, mag może umieścić środek Bańki na sobie zamiast w innym punkcie w zasięgu."
|
||||
},
|
||||
"Ukojenie": {
|
||||
"name": "Solace",
|
||||
@@ -151,7 +151,7 @@
|
||||
},
|
||||
"Szybki jak Wiatr": {
|
||||
"name": "Wind Swift",
|
||||
"description": "Mag splata wiatr, aby wspomóc się przy poruszaniu się.\r\n\r\nPrędkość maga zwiększa się dwukrotnie, a ponadto mag nie może zostać Powalony.\r\n\r\n---"
|
||||
"description": "Mag splata wiatr, aby wspomóc się przy poruszaniu się.\r\n\r\nPrędkość maga zwiększa się dwukrotnie, a ponadto mag nie może zostać Przewrócony.\r\n\r\n---"
|
||||
},
|
||||
"Alchemia": {
|
||||
"name": "Alchemy"
|
||||
@@ -162,12 +162,12 @@
|
||||
"Magia Ognia": {
|
||||
"name": "Fire Magic"
|
||||
},
|
||||
"Transmutacja": {
|
||||
"name": "Transmutation"
|
||||
},
|
||||
"Magia Żywiołów": {
|
||||
"name": "Elemental Magic"
|
||||
},
|
||||
"Transmutacja": {
|
||||
"name": "Transmutation"
|
||||
},
|
||||
"Magia Powietrza": {
|
||||
"name": "Air Magic"
|
||||
},
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
},
|
||||
"Klątwa Szkarłatu": {
|
||||
"name": "Crimson Curse",
|
||||
"description": "Cel zostaje spowity ciemną mgłą, która, gdy cel spróbuje rzucić zaklęcie, wchłonie w siebie część użytej magii i odda ją magowi, który chce rzucić zaklęcie.\r\n\r\nGdy cel tego zaklęcia rzuci z powodzeniem zaklęcie, musi wykonać udany Test na Determinację (Soul) 4:S, gdzie liczba wymaganych sukcesów (S) jest równa liczbie wyrzuconych przez maga sukcesów. W wypadku niepowodzenia postać staje się Nieprzytomna. Niezależnie od wyniku testu cel otrzymuje tyle punktów obrażeń, ile wynosi suma wartości Umiejętności Zdolności Magiczne maga i celu."
|
||||
"description": "Cel zostaje spowity ciemną mgłą, która, gdy cel spróbuje rzucić zaklęcie, wchłonie w siebie część użytej magii i odda ją magowi, który chce rzucić zaklęcie.\r\n\r\nGdy cel tego zaklęcia rzuci z powodzeniem zaklęcie, musi wykonać udany Test na Determinację (Soul) 4:S, gdzie liczba wymaganych sukcesów (S) jest równa liczbie wyrzuconych przez maga sukcesów. W wypadku niepowodzenia postać staje się Nieprzytomna i otrzymuje tyle punktów obrażeń, ile wynosi suma wartości Umiejętności Zdolności Magiczne maga i celu. Jeżeli cel zda Test, otrzymuje jedynie połowę tych obrażeń (zaokrągloną w górę) i nie traci przytomności."
|
||||
},
|
||||
"Szkarłatny Sztylet": {
|
||||
"name": "Crimson Dagger",
|
||||
"description": "W dłoni maga materializuje się zakrzywiony szkarłatny sztylet utkany z czerwonej energii, lub wybrana trzymana broń biała zostaje pokryta szkarłatną aurą. Broń zadaje dodatkowe 2 punkty obrażeń magicznych\r\n\r\nJeśli mag zabije przeciwnika bronią naznaczoną tym zaklęciem, może natychmiast wykonać Akcję Natychmiastową, aby rzucić dowolne znane zaklęcie Magii Szkarłatu bez wydawania Many. Efekt ten może być aktywowany wyłącznie raz na rundę.\r\n\r\nSzkarłatny Sztylet to jedno z pierwszych zaklęć, które poznają młodsi członkowie Kultu. Broń ta żywi się krwią wrogów, przekazując część ich siły życiowej swojemu władcy. Starożytne kroniki Kultu opisują, jak Pierwszy Patriarcha podczas Wielkiej Wojny z Otchłanią używał podobnego zaklęcia, by jego wojownicy mogli walczyć bez przerwy, czerpiąc moc z pokonanych wrogów."
|
||||
"description": "W dłoni maga materializuje się zakrzywiony szkarłatny sztylet utkany z czerwonej energii, lub wybrana trzymana broń biała zostaje pokryta szkarłatną aurą. Broń zadaje dodatkowe 2 punkty obrażeń magicznych.\r\n\r\nJeśli mag zabije przeciwnika bronią naznaczoną tym zaklęciem, może natychmiast wykonać Akcję Natychmiastową, aby rzucić bez wydawania Many dowolne znane zaklęcie Magii Szkarłatu, którego bazowy koszt many nie przekracza 3. Zaklęcia rzuconego w ten sposób nie można nadczarować. Efekt ten może być aktywowany wyłącznie raz na rundę.\r\n\r\nSzkarłatny Sztylet to jedno z pierwszych zaklęć, które poznają młodsi członkowie Kultu. Broń ta żywi się krwią wrogów, przekazując część ich siły życiowej swojemu władcy. Starożytne kroniki Kultu opisują, jak Pierwszy Patriarcha podczas Wielkiej Wojny z Otchłanią używał podobnego zaklęcia, by jego wojownicy mogli walczyć bez przerwy, czerpiąc moc z pokonanych wrogów."
|
||||
},
|
||||
"Szkarłatne Płomienie": {
|
||||
"name": "Crimson Flames",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"Szkarłatna Sprawiedliwość": {
|
||||
"name": "Crimson Justice",
|
||||
"description": "Na wybrany cel spada z nieba (bądź sklepienia) szkarłatna błyskawica.\r\n\r\nCel otrzymuje obrażenia równe dwukrotności wartości Umiejętności Zdolności maga rzucającego to zaklęcie, a ponadto wszystkie osoby w promieniu 1 m od celu (włącznie z celem) muszą wykonać udany Test na Refleks (Body) 4:S, gdzie S to liczba wyrzuconych przez maga Sukcesów. W wypadku niepowodzenia postać zostaje Ogłuszona i Oślepiona."
|
||||
"description": "Na wybrany cel spada z nieba (bądź sklepienia) szkarłatna błyskawica.\r\n\r\nCel otrzymuje obrażenia równe dwukrotności wartości Umiejętności Zdolności Magiczne maga rzucającego to zaklęcie, a ponadto wszystkie osoby w promieniu 1 m od celu (włącznie z celem) muszą wykonać udany Test na Refleks (Body) 4:S, gdzie S to liczba wyrzuconych przez maga Sukcesów. W wypadku niepowodzenia postać zostaje Ogłuszona i Oślepiona."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"Tajemniczy Strumień": {
|
||||
"name": "Mysterious Stream",
|
||||
"description": "Z ręki maga wystrzeliwuje błyskawica utkana z mroczno-fioletowej energii.\r\n\r\nTajemniczy Strumień zadaje celowi zadaje tyle punktów obrażeń, ile mag ma punktów w Umiejętności Zdolności Magiczne. Każde dwa (zamiast pięciu) punkty tych obrażeń wyłączają jeden punkt Runicznego Pancerza celu, a przeciwko Magicznej Osłonie celu (np. Magicznej Tarczy) te same obrażenia liczą się podwójnie.\r\n\r\nJeżeli cel nie posiadał magicznej osłony, musi on wykonać udany Test na Determinacja (Soul) 6:S, gdzie liczba wymaganych Sukcesów (S) jest równa liczbie wyrzuconych przez maga sukcesów. Jeżeli trafienie przełamało magiczną osłonę celu, wymagana liczba Sukcesów wynosi zamiast tego 2×S - poświata pękającej osłony rozdziera umysł celu na Otchłań. W obu wypadkach, w razie niepowodzenia testu, postać otrzymuje 1k3 Punktów Obłędu. Jeżeli cel posiadał magiczną osłonę, która nie została przełamana, ten efekt nie występuje."
|
||||
"description": "Z ręki maga wystrzeliwuje błyskawica utkana z mroczno-fioletowej energii.\r\n\r\nTajemniczy Strumień zadaje celowi tyle punktów obrażeń, ile mag ma punktów w Umiejętności Zdolności Magiczne. Każde dwa (zamiast pięciu) punkty tych obrażeń wyłączają jeden punkt Runicznego Pancerza celu, a przeciwko Magicznej Osłonie celu (np. Magicznej Tarczy) te same obrażenia liczą się podwójnie.\r\n\r\nJeżeli cel nie posiadał magicznej osłony, musi on wykonać udany Test na Determinacja (Soul) 6:S, gdzie liczba wymaganych Sukcesów (S) jest równa liczbie wyrzuconych przez maga sukcesów. Jeżeli trafienie przełamało magiczną osłonę celu, wymagana liczba Sukcesów wynosi zamiast tego 2×S - poświata pękającej osłony rozdziera umysł celu na Otchłań. W obu wypadkach, w razie niepowodzenia testu, postać otrzymuje 1k3 Punktów Obłędu. Jeżeli cel posiadał magiczną osłonę, która nie została przełamana, ten efekt nie występuje."
|
||||
},
|
||||
"Neutralizacja": {
|
||||
"name": "Neutralization",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"Pierwotna Kula": {
|
||||
"name": "Primal Orb",
|
||||
"description": "Mag wyczarowywuje kulę pierwotnej energii, którą następnie ciska w wybranego przeciwnika.\r\n\r\nZaklęcie zadaje celowi zadaje tyle punktów obrażeń, ile mag wynosi suma wartości Umiejętności Zdolności Magiczne oraz rzutu jedną kością sześciościenną (1k6).\r\n\r\nPierwotna Kula porusza się bardzo chaotycznie. Jeżeli w promieniu 2 m od celu, ale w zasięgu zaklęcia, jest jeszcze co najmniej jedna wroga Istota, należy wykonać rzut kością sześciościenną. Jeżeli wynik będzie niższy niż 3, to kula zamiast w wybrany cel trafia w najbliższą do celu Istotę."
|
||||
"description": "Mag wyczarowywuje kulę pierwotnej energii, którą następnie ciska w wybranego przeciwnika.\r\n\r\nZaklęcie zadaje celowi tyle punktów obrażeń, ile wynosi suma wartości Umiejętności Zdolności Magiczne oraz rzutu jedną kością sześciościenną (1k6).\r\n\r\nPierwotna Kula porusza się bardzo chaotycznie. Jeżeli w promieniu 2 m od celu, ale w zasięgu zaklęcia, jest jeszcze co najmniej jedna wroga Istota, należy wykonać rzut kością sześciościenną. Jeżeli wynik będzie niższy niż 3, to kula zamiast w wybrany cel trafia w najbliższą do celu Istotę."
|
||||
},
|
||||
"Przywołanie Istoty z Otchłani": {
|
||||
"name": "Summon Eldritch Entity",
|
||||
|
||||
@@ -19,15 +19,15 @@
|
||||
},
|
||||
"Zagłada": {
|
||||
"name": "Doom",
|
||||
"description": "**Wymagania:** Mistrzostwo (dowolna Dziedzina), co najmniej 3 dodatkowych magów biorących udział w rytuale (wszyscy muszą znać Zagładę) - ponadto żaden mag biorący udział w rytuale nie może być Źródłem Mocy\r\n\r\nZagłada jest ostatecznym superzaklęciem unicestwienia, stworzonym jako rytuał, który nie tylko zabija, ale też wymazuje wszelką nadzieję na odwrócenie skutków. W przeciwieństwie do Destrukcji, która „tylko” eksploduje nagromadzoną manę, Zagłada celowo splata ją z samą strukturą życia istot w zasięgu.\r\n\r\nZaklęcie wymaga wielogodzinnego rytuału, podczas którego uczestniczący magowie tworzą skomplikowaną sieć połączeń magicznych, zakończoną spięciem w głównym magu. W momencie zakończenia rytuału cała zgromadzona moc zostaje uwolniona w postaci bezgłośnej, szkarłatno‑czarnej fali, która w jednej chwili „wymazuje” życie w obrębie sfery.\r\n\r\nPo udanym rzuceniu zaklęcia emituje ono czerwoną falę światła oraz dźwięk dzwonu, który słyszą w głowie wszystkie istoty w zasięgu zaklęcia, a następnie wszystkie istoty żywe znajdujące się w sferze o średnicy 50 m, wycentrowanej na głównym magu, giną natychmiast, ignorując Pancerz, magiczne osłony, odporności, talenty, Aegis i inne superzaklęcia ochronne. Nie wykonuje się żadnych testów obronnych; śmierć następuje automatycznie. Jedynymi wyjątkami są główny mag prowadzący rytuał oraz istoty, których główny mag świadomie lub podświadomie nie chce zabić.\r\n\r\nW praktyce oznacza to, że MG, na podstawie relacji i emocji głównego maga, ustala listę postaci, które są instynktownie oszczędzane (np. najbliżsi sojusznicy, ukochane osoby). Każda z tych istot jest traktowana jako indywidualny wyjątek: fala Zagłady sprawdza je osobno i nie zadaje im żadnych obrażeń, choć wszystko wokół może zostać unicestwione - nie powstaje żadna „strefa ochronna”, w której inni mogliby się ukryć, ani nie dochodzi do jakiegokolwiek „oczyszczania” z chorób czy pasożytów.\r\n\r\nZagłada ma jednak straszliwą cenę: wszyscy pozostali magowie uczestniczący w rytuale giną, niezależnie od tego, gdzie się znajdują. Ich siły życiowe są świadomie „wypalane” w ramach zaklęcia – śmierć współrzucających nie jest efektem, którego można uniknąć zasięgiem czy osłoną, lecz integralnym kosztem rytuału.\r\n\r\nIstoty zabite przez Zagładę nie mogą zostać wskrzeszone żadną znaną magią - nie działają na nie Reanimacja, nekromancja, superzaklęcia ani boskie modlitwy wskrzeszające. Mechanicznie traktuje się je jak cele Prawdziwej Dezintegracji, z tą różnicą, że choć ciała mogą pozostać (lub nie), dusze są „wymazane” z obiegu – powrót do życia wymagałby wyłącznie wyjątkowej, fabularnej interwencji Mistrza Gry.\r\n\r\nW miejscu rzucenia Zagłady pozostaje obszar magicznie skażony: choć brak tu „odpadów alchemicznych” jak przy Destrukcji, sama tkanka rzeczywistości jest nadpalona. MG może zdecydować, że każda istota przebywająca dłużej w tym obszarze musi wykonywać okresowe testy Duszy, ryzykując trwałe urazy psychiczne, klątwy lub inne długofalowe konsekwencje - nie jest to jednak integralna część efektu zaklęcia, a raczej fabularne piętno użycia tak nienaturalnej mocy.\r\n\r\n---"
|
||||
"description": "**Wymagania:** Mistrzostwo (dowolna Dziedzina), co najmniej 3 dodatkowych magów biorących udział w rytuale (wszyscy muszą znać Zagładę) - ponadto żaden mag biorący udział w rytuale nie może być Źródłem Mocy\r\n\r\nZagłada jest ostatecznym superzaklęciem unicestwienia, stworzonym jako rytuał, który nie tylko zabija, ale też wymazuje wszelką nadzieję na odwrócenie skutków. W przeciwieństwie do Destrukcji, która „tylko” eksploduje nagromadzoną manę, Zagłada celowo splata ją z samą strukturą życia istot w zasięgu.\r\n\r\nZaklęcie wymaga wielogodzinnego rytuału, podczas którego uczestniczący magowie tworzą skomplikowaną sieć połączeń magicznych, zakończoną spięciem w głównym magu. W momencie zakończenia rytuału cała zgromadzona moc zostaje uwolniona w postaci bezgłośnej, szkarłatno-czarnej fali, która w jednej chwili „wymazuje” życie w obrębie sfery.\r\n\r\nPo udanym rzuceniu zaklęcia emituje ono czerwoną falę światła oraz dźwięk dzwonu, który słyszą w głowie wszystkie istoty w zasięgu zaklęcia, a następnie wszystkie istoty żywe znajdujące się w sferze o średnicy 50 m, wycentrowanej na głównym magu, giną natychmiast, ignorując Pancerz, magiczne osłony, odporności, talenty, Aegis i inne superzaklęcia ochronne. Nie wykonuje się żadnych testów obronnych; śmierć następuje automatycznie. Jedynymi wyjątkami są główny mag prowadzący rytuał oraz istoty, których główny mag świadomie lub podświadomie nie chce zabić.\r\n\r\nW praktyce oznacza to, że MG, na podstawie relacji i emocji głównego maga, ustala listę postaci, które są instynktownie oszczędzane (np. najbliżsi sojusznicy, ukochane osoby). Każda z tych istot jest traktowana jako indywidualny wyjątek: fala Zagłady sprawdza je osobno i nie zadaje im żadnych obrażeń, choć wszystko wokół może zostać unicestwione - nie powstaje żadna „strefa ochronna”, w której inni mogliby się ukryć, ani nie dochodzi do jakiegokolwiek „oczyszczania” z chorób czy pasożytów.\r\n\r\nZagłada ma jednak straszliwą cenę: wszyscy pozostali magowie uczestniczący w rytuale giną, niezależnie od tego, gdzie się znajdują. Ich siły życiowe są świadomie „wypalane” w ramach zaklęcia - śmierć współrzucających nie jest efektem, którego można uniknąć zasięgiem czy osłoną, lecz integralnym kosztem rytuału.\r\n\r\nIstoty zabite przez Zagładę nie mogą zostać wskrzeszone żadną znaną magią - nie działają na nie Reanimacja, nekromancja, superzaklęcia ani boskie modlitwy wskrzeszające. Mechanicznie traktuje się je jak cele Prawdziwej Dezintegracji, z tą różnicą, że choć ciała mogą pozostać (lub nie), dusze są „wymazane” z obiegu - powrót do życia wymagałby wyłącznie wyjątkowej, fabularnej interwencji Mistrza Gry.\r\n\r\nW miejscu rzucenia Zagłady pozostaje obszar magicznie skażony: choć brak tu „odpadów alchemicznych” jak przy Destrukcji, sama tkanka rzeczywistości jest nadpalona. MG może zdecydować, że każda istota przebywająca dłużej w tym obszarze musi wykonywać okresowe testy Duszy, ryzykując trwałe urazy psychiczne, klątwy lub inne długofalowe konsekwencje - nie jest to jednak integralna część efektu zaklęcia, a raczej fabularne piętno użycia tak nienaturalnej mocy.\r\n\r\n---"
|
||||
},
|
||||
"Fala Energii": {
|
||||
"name": "Energy Wave",
|
||||
"description": "Mag wyrzuca z siebie potężną falę półwidzialnej energii.\r\n\r\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 cel zostaje Oszołomiony. 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. Każde dwa (zamiast pięciu) punkty tych obrażeń wyłączają jeden punkt Runicznego Pancerza każdego trafionego celu z osobna, a przeciwko Magicznej Osłonie (np. Magicznej Tarczy) tych celów te same obrażenia liczą się podwójnie."
|
||||
"description": "Mag wyrzuca z siebie potężną falę półwidzialnej energii.\r\n\r\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 cel zostaje Oszołomiony. Niezależnie od wyniku testu każda osoba w obszarze działania zaklęcia otrzymuje tyle punktów obrażeń, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne maga. Każde dwa (zamiast pięciu) punkty tych obrażeń wyłączają jeden punkt Runicznego Pancerza każdego trafionego celu z osobna, a przeciwko Magicznej Osłonie (np. Magicznej Tarczy) tych celów te same obrażenia liczą się podwójnie."
|
||||
},
|
||||
"Przebicie Eteru": {
|
||||
"name": "Ether Pierce",
|
||||
"description": "Mag skupia swoją siłę, naginając rzeczywistość wokół celu, raniąc go pomimo osłon.\r\n\r\nCel otrzymuje tyle punktów Obrażeń ignorujących Pancerz, magiczne osłony i wszystkie odporności, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne maga.\r\n\r\nKażde dwa (zamiast pięciu) punkty obrażeń wyłączają jeden punkt Runicznego Pancerza, a przeciwko Magicznej Osłonie (np. Magicznym Tarczom) te same obrażenia liczą się podwójnie."
|
||||
"description": "Mag skupia swoją siłę, naginając rzeczywistość wokół celu, raniąc go pomimo osłon.\r\n\r\nCel otrzymuje tyle punktów Obrażeń ignorujących Pancerz oraz wszystkie odporności, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne maga.\r\n\r\nKażde dwa (zamiast pięciu) punkty obrażeń wyłączają jeden punkt Runicznego Pancerza, a przeciwko Magicznej Osłonie (np. Magicznym Tarczom) te same obrażenia liczą się podwójnie."
|
||||
},
|
||||
"Poblask": {
|
||||
"name": "Gleam",
|
||||
@@ -39,15 +39,15 @@
|
||||
},
|
||||
"Magiczny Pocisk": {
|
||||
"name": "Magic Missile",
|
||||
"description": "Mag wyczarowuje pocisk, którego wygląd jest zależny od wybranej przez siebie Dziedziny Magii.\r\n\r\nPocisk ten zadaje tyle punktów obrażeń, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne maga."
|
||||
"description": "Mag wyczarowuje pocisk, którego wygląd jest zależny od wybranej przez siebie Dziedziny Magii.\n\nPocisk ten zadaje tyle punktów obrażeń, ile wynosi zaokrąglona w górę połowa wartości Umiejętności Zdolności Magiczne maga.\n\nZwykły Pancerz nie może obniżyć obrażeń Magicznego Pocisku poniżej 1 (Runiczny Pancerz i Magiczna Osłona działają normalnie)."
|
||||
},
|
||||
"Magiczna Plaga": {
|
||||
"name": "Magic Plague",
|
||||
"description": "**Wymagania:** Mistrzostwo (Botanika, Magic Wiedźm, Alchemia (Warzenie Eliksirów) lub Magic Otchłani)\r\n\r\nZakazana Wiedza nie bez powodu jest nazywana w ten sposób. Są po prostu rzeczy, które nigdy nie powinny ujrzeć światła dziennego, rzeczy, które sprawiają, że Jeźdźcy Apokalipsy zdają się być dziećmi w przebraniach - jednym z zaklęć powstałych dzięki tej wiedzy jest superzaklęcie Magicznej Plagi.\r\n\r\nW trakcie nowiu spotkała się wiedźma, botanik i alchemik, cała trójka studiująca Otchłań. Chcąc zasiać zniszczenie w mieście, które ich wygnało, przez całą noc opracowywali zemstę - coś, co zniszczy chlubę tego miasta, czyli magiczną akademię. Stworzyli zarazę, która bardzo szybko się rozprzestrzeniła, zabijając najpierw wszystkich uczniów, następnie pozostałych magów, a na końcu ich samych.\r\n\r\nZaklęcie uwalnia zakaźną plagę magiczną, która rozprzestrzenia się w promieniu 10 km od punktu rytuału. Plaga atakuje zdolności magiczne - dotknięte nią istoty tracą 1 punkt Magii dziennie, zaś Mana odnawia się dopiero po Krótkim Odpoczynku, nie na koniec tury. Nieleczone chore istoty umierają, gdy ich Magic spadnie do 0. Plaga przenosi się przez kontakt z zakażonymi istotami.\r\n\r\nIstoty niemagiczne i niemagowie są odporni na chorobę, jednak dalej mogą być jej nosicielami.\r\n\r\nIstoty magiczne, np. anioły, elfy czy demony są na nią szczególnie wrażliwe.\r\n\r\nLeczenie wymaga specjalnego antidotum znanego tylko rzucającym. Każdy uczestnik staje się naturalnym nosicielem, i choć zna antidotum, istnieje ok. 70% szans na to, że antidotum w przypadku rzucającego to zaklęcie nie zadziała. W przeciwieństwie do następnych zakażonych uczestnicy rytuału tracą jeden punkt Magii tygodniowo.\r\n\r\n---"
|
||||
"description": "**Wymagania:** Mistrzostwo (Botanika, Magic Wiedźm, Alchemia (Warzenie Eliksirów) lub Magic Otchłani)\r\n\r\nZakazana Wiedza nie bez powodu jest nazywana w ten sposób. Są po prostu rzeczy, które nigdy nie powinny ujrzeć światła dziennego, rzeczy, które sprawiają, że Jeźdźcy Apokalipsy zdają się być dziećmi w przebraniach - jednym z zaklęć powstałych dzięki tej wiedzy jest superzaklęcie Magicznej Plagi.\r\n\r\nW trakcie nowiu spotkała się wiedźma, botanik i alchemik, cała trójka studiująca Otchłań. Chcąc zasiać zniszczenie w mieście, które ich wygnało, przez całą noc opracowywali zemstę - coś, co zniszczy chlubę tego miasta, czyli magiczną akademię. Stworzyli zarazę, która bardzo szybko się rozprzestrzeniła, zabijając najpierw wszystkich uczniów, następnie pozostałych magów, a na końcu ich samych.\r\n\r\nZaklęcie uwalnia zakaźną plagę magiczną, która rozprzestrzenia się w promieniu 10 km od punktu rytuału. Plaga atakuje zdolności magiczne - dotknięte nią istoty tracą 1 punkt Magii dziennie, a ich Mana nie odnawia się podczas Odetchnięcia ani Krótkiego Odpoczynku - przywraca ją wyłącznie Długi Odpoczynek, i to tylko do połowy dziennej puli. Nieleczone chore istoty umierają, gdy ich Magic spadnie do 0. Plaga przenosi się przez kontakt z zakażonymi istotami.\r\n\r\nIstoty niemagiczne i niemagowie są odporni na chorobę, jednak dalej mogą być jej nosicielami.\r\n\r\nIstoty magiczne, np. anioły, elfy czy demony są na nią szczególnie wrażliwe.\r\n\r\nLeczenie wymaga specjalnego antidotum znanego tylko rzucającym. Każdy uczestnik staje się naturalnym nosicielem, i choć zna antidotum, istnieje ok. 70% szans na to, że antidotum w przypadku rzucającego to zaklęcie nie zadziała. W przeciwieństwie do następnych zakażonych uczestnicy rytuału tracą jeden punkt Magii tygodniowo.\r\n\r\n---"
|
||||
},
|
||||
"Magiczna Tarcza": {
|
||||
"name": "Magic Shield",
|
||||
"description": "Mag wyczarowuje magiczną osłonę, która chroni wybrany przez niego cel przed atakami.\r\n\r\nMagiczna Tarcza ma wytrzymałość na ataki równą połowie wartości Umiejętności Zdolności Magiczne + 1. Gdy cel ma otrzymać obrażenia, to są one zmniejszane maksymalnie o obecną wytrzymałość Magicznej Tarczy. Każdy atak zmniejsza wytrzymałość Magicznej Tarczy o ilość obrażeń, które zadaje. Jeżeli wytrzymałość Magicznej Tarczy będzie równa 0, zaklęcie przestaje działać."
|
||||
"description": "Mag wyczarowuje magiczną osłonę, która chroni wybrany przez niego cel przed atakami.\r\n\r\nMagiczna Tarcza ma wytrzymałość na ataki równą zaokrąglonej w górę połowie wartości Umiejętności Zdolności Magiczne + 1. Gdy cel ma otrzymać obrażenia, to są one zmniejszane maksymalnie o obecną wytrzymałość Magicznej Tarczy. Każdy atak zmniejsza wytrzymałość Magicznej Tarczy o ilość obrażeń, które zadaje. Jeżeli wytrzymałość Magicznej Tarczy będzie równa 0, zaklęcie przestaje działać."
|
||||
},
|
||||
"Szybka Myśl": {
|
||||
"name": "Quick Thought",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"description": "Dzięki doświadczonej dłoni Demeter-Ceres wskazane uprawy błyskawicznie dojrzewają. Rośliny można zebrać od razu - zaklęcie zużywa potencjał plonu “z przyszłości”, co powoduje, że dane rośliny nie dadzą już w danym roku owoców.\r\n\r\nKażde kolejne siewy będą mniej obfite, chyba że pole zostanie ponownie poświęcone.\r\n\r\nTo zaklęcie może zostać rzucone tylko raz na dzień, raz w sezonie na konkretny obszar."
|
||||
},
|
||||
"Egida": {
|
||||
"name": "Aegis",
|
||||
"name": "Aegis Zeus",
|
||||
"description": "Zeus-Jowisz otacza wybrany cel swoją ochroną.\r\n\r\nCel otrzymuje +1 Pancerza, a ponadto wszystkie ataki kierowanie w cel są trudniejsze o jeden stopień."
|
||||
},
|
||||
"Tajemne Poznanie": {
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"Ostrze Dawida": {
|
||||
"name": "Blade Of David",
|
||||
"description": "Dzięki świętej inkantacji, rzucający może zamienić (oddane dobrowolnie lub własne) pióro anioła w ostrze o tej samej długości (od sztyletu po krótki miecz).\r\n\r\nOstrze jest bronią Tnącą Magiczną zadającą S+1 obrażeń i utrzymuje się przez 3 tury. Po tym czasie ostrze traci swoje właściwości i zamienia się w pozbawione magii pióro. Takiego pióra nie można użyć ponownie jako składnika tego zaklęcia."
|
||||
"description": "Dzięki świętej inkantacji, rzucający może zamienić (oddane dobrowolnie lub własne) pióro anioła w ostrze o tej samej długości (od sztyletu po krótki miecz).\r\n\r\nOstrze jest bronią Tnącą Magiczną zadającą S+1 obrażeń i utrzymuje się przez 3 rundy. Po tym czasie ostrze traci swoje właściwości i zamienia się w pozbawione magii pióro. Takiego pióra nie można użyć ponownie jako składnika tego zaklęcia."
|
||||
},
|
||||
"Więź Serc": {
|
||||
"name": "Bond Of Hearts",
|
||||
@@ -43,7 +43,7 @@
|
||||
},
|
||||
"Urok Niewinności": {
|
||||
"name": "Charm Of Innocence",
|
||||
"description": "Cel wydaje się wszystkim w otoczeniu urzekający i nieszkodliwy – agresywne działania przeciwników wobec celu wymagają dwóch Sukcesów więcej, a postronne osoby przyjmują wobec niego postawę przyjazną."
|
||||
"description": "Cel wydaje się wszystkim w otoczeniu urzekający i nieszkodliwy - agresywne działania przeciwników wobec celu wymagają dwóch Sukcesów więcej, a postronne osoby przyjmują wobec niego postawę przyjazną."
|
||||
},
|
||||
"Strzała Kupidyna": {
|
||||
"name": "Cupids Arrow",
|
||||
@@ -115,7 +115,7 @@
|
||||
},
|
||||
"Kradzież Many": {
|
||||
"name": "Mana Drain",
|
||||
"description": "Hekate spogląda krzywym wzrokiem na wskazanego przez swojego wyznawcę niewiernego, przekazując część mocy magicznej celu magowi rzucającego to zaklęcie.\r\n\r\nDo końca trwania zaklęcia, mag rzucający to zaklęcie zwiększa swoją Manę o 2 oraz Maksymalny Koszt Zaklęcia o 1, zaś cel zmniejsza swój Maksymalny Koszt Zaklęcia o 2."
|
||||
"description": "Hekate spogląda krzywym wzrokiem na wskazanego przez swojego wyznawcę niewiernego, przekazując część mocy magicznej celu magowi rzucającego to zaklęcie.\r\n\r\nMag rzucający to zaklęcie natychmiast odzyskuje 1k6 punktów Many - jego Mana nie może jednak przekroczyć dziennej puli. Ponadto do końca trwania zaklęcia Maksymalny Koszt Zaklęcia maga zwiększa się o 1, zaś Maksymalny Koszt Zaklęcia celu zmniejsza się o 2."
|
||||
},
|
||||
"Modlitwa o Opiekę": {
|
||||
"name": "Prayer For Protection",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"Większe Wchłanianie Krwi": {
|
||||
"name": "Wieksze Wchlanianie Krwi",
|
||||
"description": "Ta Akcja zastępuje Akcję \"Wchłanianie Krwi\"\nKoszt: Akcja Natychmiastowa, min. 1 punkt Krwi\n\nMag wchłania w siebie zebraną energię życiową wrogów. Za każdy poświęcany 1 punkt Krwi, mag odzyskuje 5 punktów Żywotności.\n\nZamiast leczenia, mag za każde pięć punktów Krwi może odzyskać 1 punkt Zapału."
|
||||
"description": "Ta Akcja zastępuje Akcję \"Wchłanianie Krwi\"\nKoszt: Akcja Natychmiastowa, min. 1 punkt Krwi\n\nMag wchłania w siebie zebraną energię życiową wrogów. Za każdy poświęcany 1 punkt Krwi, mag odzyskuje 3 punkty Żywotności.\n\nZamiast leczenia, mag za każde pięć punktów Krwi może odzyskać 1 punkt Zapału."
|
||||
},
|
||||
"Wielka Ofiara": {
|
||||
"name": "Wielka Ofiara",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"Metamagia": {
|
||||
"name": "Metamagic",
|
||||
"description": "*Ten Talent można wykupić wyłącznie podczas tworzenia postaci lub jeżeli Postać staje się Magiem 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](Rozdwojone%20Zaklęcie.md)\n- [Niezniszczalne Zaklęcie](Niezniszczalne%20Zaklęcie.md)\n- [Rozproszenie Magii](Rozproszenie%20Magii.md)\n- [Błyskawiczne Czarostwo](Błyskawiczne%20Czarostwo.md)"
|
||||
"description": "*Ten Talent można wykupić wyłącznie podczas tworzenia postaci lub jeżeli Postać staje się Magiem 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 Metamagii, 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](Rozdwojone%20Zaklęcie.md)\n- [Niezniszczalne Zaklęcie](Niezniszczalne%20Zaklęcie.md)\n- [Rozproszenie Magii](Rozproszenie%20Magii.md)\n- [Błyskawiczne Czarostwo](Błyskawiczne%20Czarostwo.md)"
|
||||
},
|
||||
"Niezniszczalne Zaklęcie": {
|
||||
"name": "Niezniszczalne Zaklecie",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"Dawniej Cały, Wkrótce Cały": {
|
||||
"name": "Dawniej Caly Wkrotce Caly",
|
||||
"description": "Wielki Taumaturg utracił ciało, a jego dusza została rozdarta na strzępy - jednak dzięki działaniom Szkarłatnego Kultu z dnia na dzień odzyskuje coraz więcej swoich mocy.\n\nWszystkie zaklęcia z Magii Szkarłatnej wymagają dwóch sukcesów więcej, ale Postać może rzucić jedno dowolne znane zaklęcie w dowolnym momencie rundy."
|
||||
"description": "Wielki Taumaturg utracił ciało, a jego dusza została rozdarta na strzępy - jednak dzięki działaniom Szkarłatnego Kultu z dnia na dzień odzyskuje coraz więcej swoich mocy.\n\nWszystkie zaklęcia z Magii Szkarłatnej wymagają dwóch sukcesów więcej, ale Postać może rzucić jedno dowolne znane zaklęcie w dowolnym momencie rundy (jako Akcję Natychmiastową)."
|
||||
},
|
||||
"Głos Rozkazu": {
|
||||
"name": "Glos Rozkazu",
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"Księżniczka Piekieł": {
|
||||
"name": "Ksiezniczka Piekiel",
|
||||
"description": "Tytuł \"księżniczka demonów\" nie jest u Narcyzy kurtuazją - to uznany przez inne demony stopień Autorytetu, który nosi od urodzenia, niezależnie od tego, ile ze swojej mocy akurat ukrywa pod studenckimi szatami.\n\nTa Postać posiada Autorytet na poziomie IV (patrz Stan: [Autorytet](../../00.%20Podręcznik%20Gry/Aneks%20A%20-%20Stany%20Postaci/Autorytet.md)) oraz odnawia dodatkowy 1 punkt Zapału na początku każdej swojej Tury, ponad standardową regenerację."
|
||||
"description": "Tytuł \"księżniczka demonów\" nie jest u Narcyzy kurtuazją - to uznany przez inne demony stopień Autorytetu, który nosi od urodzenia, niezależnie od tego, ile ze swojej mocy akurat ukrywa pod studenckimi szatami.\n\nTa Postać posiada Autorytet na poziomie IV (patrz Stan: [Autorytet](../../00.%20Podręcznik%20Gry/Aneks%20A%20-%20Stany%20Postaci/Autorytet.md)) oraz odnawia dodatkowy 1 punkt Zapału na początku każdej swojej Tury, ponad standardową regenerację.\n\nWszystko to ma swoją cenę: klątwa Pana Demonów ciągle wysysa z Narcyzy siły. Dodatkowy punkt Zapału z tego Talentu nie odnawia się na początku Tury, jeżeli w poprzedniej Turze użyła [Rozkazu Księżniczki](Rozkaz%20Księżniczki.md). Poza tym na początku każdej swojej Tury, w której jakikolwiek jej Rozkaz wciąż obowiązuje, traci 1 punkt Żywotności, którego nie obniża żaden Pancerz, Runiczny Pancerz ani Magiczna Osłona."
|
||||
},
|
||||
"Mackowate Więzy Otchłani": {
|
||||
"name": "Mackowate Wiezy Otchlani",
|
||||
@@ -39,7 +39,7 @@
|
||||
},
|
||||
"Oko, Które Widziało Wszystko": {
|
||||
"name": "Oko Ktore Widzialo Wszystko",
|
||||
"description": "Wieki spędzone jako naczynie Gilgamesza nauczyły Pelagiusa Caudexa jednej rzeczy: w walce niemal nie istnieje ruch, cios czy zaklęcie, którego nie widziałby już wcześniej, w innym ciele, w innym starciu, wieki temu.\n\nRaz na rundę, gdy Postać jest celem Ataku, Testu Przeciwstawnego lub zaklęcia, może zadeklarować Unik lub Parowanie bez poświęcania na to Akcji ani Akcji Natychmiastowej, a do wykonywanego w ten sposób Testu dodaje kości w liczbie równej wartości Umiejętności Intuicja.\n\nJeżeli Postać w ten sposób unika lub paruje Atak, którego wcześniej (w tym starciu) już raz uniknęła lub sparowała w identyczny sposób, to zamiast Testu automatycznie odnosi sukces równy liczbie wymaganych Sukcesów - Pelagius po prostu przestaje dawać się zaskakiwać tym samym sztuczkom dwa razy."
|
||||
"description": "Wieki spędzone jako naczynie Gilgamesza nauczyły Pelagiusa Caudexa jednej rzeczy: w walce niemal nie istnieje ruch, cios czy zaklęcie, którego nie widziałby już wcześniej, w innym ciele, w innym starciu, wieki temu.\n\nRaz na rundę, gdy Postać jest celem Ataku, Testu Przeciwstawnego lub zaklęcia, może zadeklarować Unik lub Parowanie (jako Akcję Natychmiastową, zgodnie z zasadami Tury), a do wykonywanego w ten sposób Testu dodaje kości w liczbie równej wartości Umiejętności Intuicja.\n\nJeżeli Postać w ten sposób unika lub paruje Atak, którego wcześniej (w tym starciu) już raz uniknęła lub sparowała w identyczny sposób, to zamiast Testu automatycznie odnosi sukces równy liczbie wymaganych Sukcesów - Pelagius po prostu przestaje dawać się zaskakiwać tym samym sztuczkom dwa razy."
|
||||
},
|
||||
"Otoczony": {
|
||||
"name": "Otoczony",
|
||||
@@ -55,7 +55,7 @@
|
||||
},
|
||||
"Pierwszy Patriarcha": {
|
||||
"name": "Pierwszy Patriarcha",
|
||||
"description": "Szkarłatny Kult jest wypaczeniem nauk Wielkiego Taumaturga, jednak to dzięki niemu ten potężny mag odzyskał na tyle sił, aby być w stanie wpływać na losy świata, zyskując przy okazji nowe zdolności wynikające bezpośrednio ze Szkarłatnej Magii - będącej Dziedziną, która przekształciła jego nauki w narzędzie wojny.\n\nZa każdego aktywnie walczącego z Pierwszym Patriarchą Maga Otchłani, Pierwszy Patriarcha może rzucić dodatkowe zaklęcie za darmo. Ponadto Postać odzyskuje dodatkowy punkt Zapału na początku swojej Tury."
|
||||
"description": "Szkarłatny Kult jest wypaczeniem nauk Wielkiego Taumaturga, jednak to dzięki niemu ten potężny mag odzyskał na tyle sił, aby być w stanie wpływać na losy świata, zyskując przy okazji nowe zdolności wynikające bezpośrednio ze Szkarłatnej Magii - będącej Dziedziną, która przekształciła jego nauki w narzędzie wojny.\n\nZa każdego aktywnie walczącego z Pierwszym Patriarchą Maga Otchłani, Pierwszy Patriarcha może rzucić dodatkowe zaklęcie za darmo. Ponadto Postać odzyskuje dodatkowy punkt Zapału na początku swojej Tury.\n\n> [!note] Świadomy wyjątek\n> Pierwszy Patriarcha jest w praktyce pomniejszym bóstwem, więc ten Talent celowo łamie ogólne zasady (limit darmowych akcji, regenerację Zapału). Istoty na podobnym poziomie mocy mogą mieć zdolności wykraczające poza zasady śmiertelników - zawsze opisane wprost w ich Talentach."
|
||||
},
|
||||
"Pochłonięcie": {
|
||||
"name": "Pochloniecie",
|
||||
@@ -63,7 +63,7 @@
|
||||
},
|
||||
"Pożeranie Zaklęć": {
|
||||
"name": "Pozeranie Zaklec",
|
||||
"description": "Gdy zaklęcie trafia w tę Postać, nie zawsze robi to, co powinno - czasem po prostu znika, wchłonięte, zanim zdąży zadziałać.\n\nGdy ta Postać staje się celem zaklęcia, może, jako Reakcję, spróbować je pochłonąć zamiast pozwolić mu zadziałać. Jeśli forma tej Postaci nie ma określonej inaczej szansy powodzenia (patrz jej karta), wymaga to Testu Magic (Zdolności Magiczne) 4:1. W przypadku sukcesu zaklęcie nie ma żadnego efektu, a ta Postać wybiera jedno z poniższych:\n\n- **Kontratak** - natychmiast wykonuje darmowy Atak przeciwko rzucającemu, korzystając z zasad swojego podstawowego Ataku (bez dodatkowego kosztu Akcji).\n- **Efekt** - zamiast atakować, nakłada na rzucającego jedną dowolną właściwość z [Zamiaru](Zamiar.md) (np. wymuszony Test Obłędu, zniszczenie punktu Runicznego Pancerza czy Magicznej Osłony) bez wykonywania Ataku ani Testu - pochłonięta magia po prostu obraca się przeciwko rzucającemu.\n\n> [!gm]+ Tylko dla MG\n> W formie kostki śluzu ta Postać pochłania zaklęcia niemal odruchowo - Test wykonuje automatycznie z sukcesem, bez rzutu, chyba że zaklęcie jest wyjątkowo potężne lub fabularnie ważne, by przeszło."
|
||||
"description": "Gdy zaklęcie trafia w tę Postać, nie zawsze robi to, co powinno - czasem po prostu znika, wchłonięte, zanim zdąży zadziałać.\n\nGdy ta Postać staje się celem zaklęcia, może, jako Akcję Natychmiastową, spróbować je pochłonąć zamiast pozwolić mu zadziałać. Jeśli forma tej Postaci nie ma określonej inaczej szansy powodzenia (patrz jej karta), wymaga to Testu Magic (Zdolności Magiczne) 4:1. W przypadku sukcesu zaklęcie nie ma żadnego efektu, a ta Postać wybiera jedno z poniższych:\n\n- **Kontratak** - natychmiast wykonuje darmowy Atak przeciwko rzucającemu, korzystając z zasad swojego podstawowego Ataku (bez dodatkowego kosztu Akcji).\n- **Efekt** - zamiast atakować, nakłada na rzucającego jedną dowolną właściwość z [Zamiaru](Zamiar.md) (np. wymuszony Test Obłędu, zniszczenie punktu Runicznego Pancerza czy Magicznej Osłony) bez wykonywania Ataku ani Testu - pochłonięta magia po prostu obraca się przeciwko rzucającemu.\n\n> [!gm]+ Tylko dla MG\n> W formie kostki śluzu ta Postać pochłania zaklęcia niemal odruchowo - Test wykonuje automatycznie z sukcesem, bez rzutu, chyba że zaklęcie jest wyjątkowo potężne lub fabularnie ważne, by przeszło."
|
||||
},
|
||||
"Przywilej Baby Jagi": {
|
||||
"name": "Przywilej Baby Jagi",
|
||||
@@ -71,11 +71,11 @@
|
||||
},
|
||||
"Rozkaz Księżniczki": {
|
||||
"name": "Rozkaz Ksiezniczki",
|
||||
"description": "Narcyzie nie trzeba niczego wymuszać siłą - wystarczy, że każe.\n\nJako Akcję Natychmiastową, kosztem 1 punktu Zapału, ta Postać wybiera jeden cel w zasięgu 15 m, który jest w stanie zrozumieć jej polecenia, i wykonuje Test Soul (Przekonywanie) 4:S. Rozstrzygnięcie następuje zgodnie ze Stanem [Autorytet](../../00.%20Podręcznik%20Gry/Aneks%20A%20-%20Stany%20Postaci/Autorytet.md): jeżeli Autorytet celu jest o dwa stopnie niższy niż Autorytet tej Postaci (zwykle IV), cel nie jest w stanie przeciwstawić się efektowi; w innym wypadku cel wykonuje Test o Trudności wynikającej z różnicy wartości Atrybutu Soul pomiędzy nim a tą Postacią (patrz tabela w Stanie Autorytet), gdzie wymagana liczba Sukcesów (S) jest równa liczbie Sukcesów wyrzuconych przez tę Postać w powyższym Teście.\n\nW wypadku niepowodzenia cel musi wykonywać rozkazy tej Postaci aż do końca jej następnej Tury - łącznie z rozkazami wymierzonymi w jego sojuszników, o ile nie są one wprost samobójcze.\n\nPonieważ jest to Akcja Natychmiastowa, nie koliduje ze zwykłą Akcją tej Postaci (Atakiem, rzuceniem zaklęcia czy ruchem) - w ramach limitu Akcji Natychmiastowych przysługującego jej w danej rundzie zgodnie ze standardowymi zasadami."
|
||||
"description": "Narcyzie nie trzeba niczego wymuszać siłą - wystarczy, że każe.\n\nJako Akcję Natychmiastową, kosztem 1 punktu Zapału, ta Postać wybiera jeden cel w zasięgu 15 m, który jest w stanie zrozumieć jej polecenia, i wykonuje Test Soul (Przekonywanie) 4:S. Rozstrzygnięcie następuje zgodnie ze Stanem [Autorytet](../../00.%20Podręcznik%20Gry/Aneks%20A%20-%20Stany%20Postaci/Autorytet.md): jeżeli Autorytet celu jest o dwa stopnie niższy niż Autorytet tej Postaci (zwykle IV), cel nie jest w stanie przeciwstawić się efektowi; w innym wypadku cel wykonuje Test o Trudności wynikającej z różnicy wartości Atrybutu Soul pomiędzy nim a tą Postacią (patrz tabela w Stanie Autorytet), gdzie wymagana liczba Sukcesów (S) jest równa liczbie Sukcesów wyrzuconych przez tę Postać w powyższym Teście.\n\nW wypadku niepowodzenia cel musi wykonywać rozkazy tej Postaci aż do końca jej następnej Tury - łącznie z rozkazami wymierzonymi w jego sojuszników, o ile nie są one wprost samobójcze.\n\nPonieważ jest to Akcja Natychmiastowa, nie koliduje ze zwykłą Akcją tej Postaci (Atakiem, rzuceniem zaklęcia czy ruchem). Pierwsze użycie w danej rundzie rozlicza się w ramach standardowego limitu Akcji Natychmiastowych przysługującego jej w tej rundzie. Każde kolejne użycie w tej samej rundzie to wciąż Akcja Natychmiastowa, ale nie zużywa już tego limitu - jej jedynym kosztem, tak jak za pierwszym razem, pozostaje 1 punkt Zapału."
|
||||
},
|
||||
"Skarbiec Królów": {
|
||||
"name": "Skarbiec Krolow",
|
||||
"description": "Nikt na Ziemi nie wie, ile artefaktów naprawdę spoczywa w Bramie Babilonu - a Pelagius Caudex nie ma zamiaru nikomu tego zdradzać. Wystarczy, że uchyli jej wrota, a dziesiątki złotych kręgów rozkwitają w powietrzu wokół niego, każdy gotowy wypluć z siebie inny wytwór tysięcy lat rzemiosła.\n\nJako Akcję, Postać może otworzyć liczbę Złotych Kręgów równą wartości Umiejętności Rzemiosło - każdy Krąg wykonuje osobny Atak Dystansowy (Magic (Umiejętności Strzeleckie)) S+3 Punktów Obrażeń, zasięg 15 m, wymierzony w dowolne, niezależnie wybrane cele w zasięgu. Pula kości Ataku dzieli się między wszystkie otwarte Kręgi tak, jak przy walce oburącz z wieloma przeciwnikami.\n\nRaz na starcie Postać może poświęcić 2 punkty Zapału, aby jeden z Kręgów przywołał broń na tyle potężną, że Test Ataku wykonywany przez ten Krąg otrzymuje dodatkowe 2 kości, a trafienie zadaje dodatkowo 1k6 Obrażeń ignorujących Pancerz.\n\nPelagius niemal nigdy nie sięga po broń ze Skarbca osobiście - woli, by to jego przeciwnicy musieli zmierzyć się z całym jego arsenałem naraz, zamiast z człowiekiem, który akurat trzyma w rękach tylko jeden miecz."
|
||||
"description": "Nikt na Ziemi nie wie, ile artefaktów naprawdę spoczywa w Bramie Babilonu - a Pelagius Caudex nie ma zamiaru nikomu tego zdradzać. Wystarczy, że uchyli jej wrota, a dziesiątki złotych kręgów rozkwitają w powietrzu wokół niego, każdy gotowy wypluć z siebie inny wytwór tysięcy lat rzemiosła.\n\nJako Akcję, Postać może otworzyć liczbę Złotych Kręgów równą wartości Umiejętności Rzemiosło - każdy Krąg wykonuje osobny Atak Dystansowy (Magic (Umiejętności Strzeleckie)) S+3 Punktów Obrażeń, zasięg 15 m, wymierzony w dowolne, niezależnie wybrane cele w zasięgu. Pula kości Ataku dzieli się między wszystkie otwarte Kręgi tak, jak przy walce oburącz z wieloma przeciwnikami.\n\nRaz na starcie Postać może poświęcić 2 punkty Zapału, aby jeden z Kręgów przywołał broń na tyle potężną, że Test Ataku wykonywany przez ten Krąg otrzymuje dodatkowe 2 kości, a trafienie zadaje dodatkowo 1k6 Obrażeń ignorujących Pancerz.\n\n> [!info] Artefakty noszone na stałe\n> Poza wytworami, które Pelagius wyrzuca w przeciwników, Brama Babilonu zawiera przedmioty, które zakłada na siebie, zanim zacznie się jakiekolwiek starcie. Każdy z nich daje jedną, ściśle określoną przewagę:\n> - **Diadem Szamasza** - pełna odporność na efekty wpływające na umysł (Zauroczenie, rozkazy i Autorytet, Przerażenie, Złowróżbny cień). To dlatego sztuczki Narcyzy nie mają na niego wpływu.\n> - **Pieczęć Enlila** - +3 do Zdolności Magicznych.\n> - **Sandały Etany** - Szybkość 15 m i ignorowanie Terenu Trudnego.\n>\n> Arsenał jest dużo większy, niż Pelagius kiedykolwiek pokazał, więc MG może dodać Pelagiusowi kolejne przedmioty z Bramy, o ile każdy z nich daje wyłącznie jedną przewagę i da się go zdjąć lub zniszczyć.\n\nPelagius niemal nigdy nie sięga po broń ze Skarbca osobiście - woli, by to jego przeciwnicy musieli zmierzyć się z całym jego arsenałem naraz, zamiast z człowiekiem, który akurat trzyma w rękach tylko jeden miecz."
|
||||
},
|
||||
"Skaza z Xivell": {
|
||||
"name": "Skaza Z Xivell",
|
||||
@@ -123,7 +123,7 @@
|
||||
},
|
||||
"Zapora Posłańca": {
|
||||
"name": "Zapora Poslanca",
|
||||
"description": "Coś w Dianie - to samo coś, co pozwoliło jej niegdyś przeżyć ponad pół godziny w środku rozpadającego się rytuału, który powinien był ją zabić - wciąż czuwa, gdy zbliża się cudza magia.\n\nJako Akcję Natychmiastową, gdy Postać lub sojusznik w promieniu 10 m ma zostać trafiony zaklęciem, Postać może interweniować na dwa sposoby (do wyboru):\n- **Osłona:** cel otrzymuje punkty Magicznej Osłony w liczbie równej wartości Atrybutu Magic Postaci.\n- **Rozproszenie:** Postać wykonuje Test Magic (Zdolności Magiczne) T:5, gdzie liczba wymaganych Sukcesów jest o 1 większa niż liczba Sukcesów rzucającego zaklęcie - w przypadku powodzenia zaklęcie zostaje rozplecione, tak jak przy Talencie Rozplatanie Zaklęć.\n\nTen Talent nie ma ograniczenia użyć na rundę ani na starcie - jego jedynym kosztem jest Akcja Natychmiastowa, których Postać wciąż może wykonać tylko ograniczoną liczbę na rundę zgodnie z normalnymi zasadami."
|
||||
"description": "Coś w Dianie - to samo coś, co pozwoliło jej niegdyś przeżyć ponad pół godziny w środku rozpadającego się rytuału, który powinien był ją zabić - wciąż czuwa, gdy zbliża się zagrożenie.\n\nJako Akcję Natychmiastową, gdy Postać lub sojusznik w promieniu 10 m ma zostać trafiony zaklęciem, Postać może interweniować na dwa sposoby (do wyboru):\n- **Osłona:** cel otrzymuje punkty Magicznej Osłony w liczbie równej wartości Atrybutu Magic Postaci.\n- **Rozproszenie:** Postać wykonuje Test Magic (Zdolności Magiczne) T:5, gdzie liczba wymaganych Sukcesów jest ustalana jak przy Talencie Rozplatanie Zaklęć (Zasada Potężniejszych Zaklęć) - w przypadku powodzenia zaklęcie zostaje rozplecione, tak jak przy Talencie Rozplatanie Zaklęć.\n\nTen Talent nie ma ograniczenia użyć na starcie. Raz na rundę Postać może użyć go za darmo, jako Akcję Natychmiastową w ramach standardowego limitu Akcji Natychmiastowych. Każde kolejne użycie w tej samej rundzie to wciąż Akcja Natychmiastowa, ale nie zużywa już tego limitu - kosztuje wtedy dodatkowo 1 punkt Zapału, tak jak kolejne próby rozplatania w Talencie [Rozplatanie Zaklęć](../../00.%20Podręcznik%20Gry/Rozdział%20IV%20-%20Talenty/Rozplatanie%20Zaklęć.md)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"Arytmetyczna Koncentracja": {
|
||||
"name": "Arytmetyczna Koncentracja",
|
||||
"description": "Ilość kości w Testach na utrzymanie Koncentracji jest zwiększona o połowę wartości Atrybutu Mind."
|
||||
"description": "Ilość kości w Testach na utrzymanie Koncentracji jest zwiększona o zaokrągloną w górę połowę wartości Atrybutu Mind."
|
||||
},
|
||||
"Gadanina": {
|
||||
"name": "Babble",
|
||||
@@ -63,7 +63,7 @@
|
||||
},
|
||||
"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."
|
||||
"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 Żywotności."
|
||||
},
|
||||
"Zakazana Wiedza (Dziedzina)": {
|
||||
"name": "Forbidden Knowledge",
|
||||
@@ -115,7 +115,7 @@
|
||||
},
|
||||
"Miłosierdzie": {
|
||||
"name": "Milosierdzie",
|
||||
"description": "Efekty leczące i chroniące zaklęć Magii Sakralnej są o połowę mocniejsze."
|
||||
"description": "Efekty leczące i chroniące zaklęć Magii Sakralnej są o połowę mocniejsze (wartość po zwiększeniu zaokrąglamy w górę)."
|
||||
},
|
||||
"Nadzwyczajna Odporność": {
|
||||
"name": "Nadzwyczajna Odpornosc",
|
||||
@@ -135,11 +135,11 @@
|
||||
},
|
||||
"Oportunista": {
|
||||
"name": "Opportunist",
|
||||
"description": "Twoja zdolność do reagowania na wydarzenia w trakcie walki jest zadziwiająca.\n\nRaz na rundę gdy przeciwnik odchodzi od Ciebie i masz możliwość zaatakowania go, możesz podjąć próbę ataku."
|
||||
"description": "Twoja zdolność do reagowania na wydarzenia w trakcie walki jest zadziwiająca.\n\nGdy przeciwnik odchodzi od Ciebie i masz możliwość zaatakowania go, możesz podjąć próbę ataku jako Akcja Natychmiastowa. Każde użycie zużywa Akcję Natychmiastową (patrz: [Tura](../Rozdział%20V%20-%20Walka%20i%20Magic/Tura.md)), więc liczba ataków w rundzie jest ograniczona wyłącznie liczbą Akcji Natychmiastowych, jakie możesz wykonać."
|
||||
},
|
||||
"Parowanie": {
|
||||
"name": "Parry",
|
||||
"description": "Zamiast unikać bądź kontrować ataków, raz na rundę Postać może sparować jeden atak wręcz. \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).\n\nPostać wykonuje Test Przeciwstawny (jak w przypadku Odpierania Ataku), jednak Test jest prostszy o jeden stopień, a ponadto ilość wymaganych Sukcesów należy zmniejszyć o zaokrągloną w górę połowę wartości Umiejętności Walka Wręcz atakowanego."
|
||||
"description": "Zamiast unikać bądź kontrować ataków, raz na rundę (jako Akcja Natychmiastowa) Postać może sparować jeden atak wręcz. \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).\n\nPostać wykonuje Test Przeciwstawny (jak w przypadku Odpierania Ataku), jednak Test jest prostszy o jeden Stopień. Parowanie się udaje, jeżeli Postać uzyska co najmniej tyle Sukcesów, ile atakujący, pomniejszone o zaokrągloną w górę połowę wartości swojej Umiejętności Walka Wręcz (czyli remis i niewielka przegrana również są sukcesem) - atak zostaje wtedy Sparowany i nie zadaje obrażeń. W przeciwnym razie atak trafia i zadaje obrażenia na zwykłych zasadach."
|
||||
},
|
||||
"Urok Osobisty": {
|
||||
"name": "Personal Charm",
|
||||
@@ -147,7 +147,7 @@
|
||||
},
|
||||
"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\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\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."
|
||||
"description": "Nie możesz rzucać ani rozplatać zaklęć. Twój Maksymalny Koszt Zaklęcia oraz Ilość Many zostają dwukrotnie zwiększone.\n\nW ramach Akcji [Pomoc](../Rozdział%20V%20-%20Walka%20i%20Magic/Pomoc.md) możesz pomóc wybranemu magowi w zasięgu 3 m (zamiast zwykłego 1 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\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",
|
||||
@@ -163,7 +163,11 @@
|
||||
},
|
||||
"Rozplatanie Zaklęć": {
|
||||
"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\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\nJeżeli Postać widzi istotę rzucającą zaklęcie i jest od niej w zasięgu 20 m, może zadeklarować, że próbuje rozpleść rzucane zaklęcie. Postać wykonuje Test na Zdolności Magiczne (Magic lub Soul, jeśli jest magiem sakralnym), gdzie Trudność (T) jest równa trudności zaklęcia, a liczba wymaganych Sukcesów (S) jest o 1 większa od liczby sukcesów wyrzuconych przez maga rzucającego zaklęcie. Jeżeli wynik Testu Postaci będzie wyższy niż wynik Testu przeciwnika, zaklęcie zostaje rozplecione."
|
||||
"description": "Każdy mag posiadający ten Talent może raz na rundę dokonać próby rozplatania w ramach darmowej Akcji Natychmiastowej (o ile nie wykorzystał jej już w tej rundzie), jednak dane zaklęcie może być rozplatane wyłącznie raz.\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\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) wynika z [Zasady Potężniejszych Zaklęć](../../01.%20Księga%20Magii/Rozdział%20II%20-%20Teoria%20Rzucania%20Zaklęć/Zasada%20Potężniejszych%20Zaklęć.md) (2 + nadwyżka Sukcesów rzucającego ponad wymagane przez zaklęcie, plus bonusy z zaklęć Niezniszczalnych). Jeżeli Postać uzyska wymaganą liczbę Sukcesów, zaklęcie zostaje rozplecione."
|
||||
},
|
||||
"Sztuki Walki": {
|
||||
"name": "Sztuki Walki",
|
||||
"description": "Wieloletni trening sprawił, że Twoje ciało samo w sobie jest bronią.\n\nNie stosujesz kar do Ataków bez Broni (patrz: [Atak bez Broni](../Rozdział%20V%20-%20Walka%20i%20Magic/Atak%20bronią.md)). Twoje pięści, kopnięcia, uderzenia głowicą i ogonem liczą się jako broń biała o Obrażeniach równych 1: możesz nimi Odpierać ataki wręcz, a także korzystać z Talentów wymagających broni białej (np. Parowanie, Finta, Riposta).\n\nTen Talent obejmuje również walkę przedmiotami, które nie są bronią (taborety, narzędzia itp.; tarcza nadal nie jest bronią) - nie stosujesz wtedy kar do Ataków Improwizowanych.\n\nWykonując Powalenie lub Rozbrojenie otrzymujesz dwie dodatkowe kości do Testu Przeciwstawnego."
|
||||
},
|
||||
"Tajemna Technologia": {
|
||||
"name": "Tajemna Technologia",
|
||||
|
||||
+16
-13
@@ -105,7 +105,6 @@
|
||||
"spell": {
|
||||
"label": "Spell",
|
||||
"cast": "Cast spell",
|
||||
"circle": "Circle",
|
||||
"school": "School",
|
||||
"discipline": "Discipline",
|
||||
"difficulty": "Spell Threshold",
|
||||
@@ -187,7 +186,8 @@
|
||||
"abyssAspects": "Abyss - Aspects",
|
||||
"abyssPrimal": "Abyss - Primal",
|
||||
"wildWitch": "Wild Witch",
|
||||
"eldritch": "Eldritch"
|
||||
"eldritch": "Eldritch",
|
||||
"thaumaturgy": "Thaumaturgy"
|
||||
},
|
||||
"deity": {
|
||||
"common": "Common Prayers",
|
||||
@@ -200,14 +200,14 @@
|
||||
"eros": "Eros"
|
||||
},
|
||||
"sourceBook": {
|
||||
"core-rules": "PG",
|
||||
"magic-book": "KM",
|
||||
"arcanum-sanguinis": "AS",
|
||||
"crimson-cult": "CS",
|
||||
"abyss-curse": "KO",
|
||||
"humanity-guide": "PLM",
|
||||
"bestiary": "B",
|
||||
"gold-steel-magic": "ZSM"
|
||||
"podrecznik-gry": "Podręcznik Gry",
|
||||
"ksiega-magii": "Księga Magii",
|
||||
"arcanum-sanguinis": "Arcanum Sanguinis",
|
||||
"crimson-cult": "Szkarłatny Kult",
|
||||
"klatwa-otchlani": "Klątwa Otchłani",
|
||||
"przewodnik": "Przewodnik",
|
||||
"bestiariusz": "Bestiariusz",
|
||||
"zlote-stal-magia": "Złoto i Stal: Magia"
|
||||
},
|
||||
"aoeShape": {
|
||||
"point": "Point",
|
||||
@@ -264,7 +264,7 @@
|
||||
"type": "Type",
|
||||
"passive": "Passive",
|
||||
"active": "Active",
|
||||
"reaction": "Reaction",
|
||||
"reaction": "Immediate Action",
|
||||
"freeAction": "Free Action",
|
||||
"prerequisite": "Prerequisite",
|
||||
"description": "Description",
|
||||
@@ -324,7 +324,7 @@
|
||||
"languages": "Languages",
|
||||
"attacks": "Attacks",
|
||||
"specialAbilities": "Special Abilities",
|
||||
"reactions": "Reactions",
|
||||
"reactions": "Immediate Actions",
|
||||
"legendaryActions": "Legendary Actions",
|
||||
"savingThrows": "Saving Throws",
|
||||
"damageResistances": "Damage Resistances",
|
||||
@@ -366,6 +366,9 @@
|
||||
"bypassNonCombatBlock": "Bypass non-combat block",
|
||||
"cast": "Cast",
|
||||
"notEnoughMana": "Not enough mana.",
|
||||
"borrowMana": "Not enough mana - the shortfall is borrowed (Mana Exhaustion).",
|
||||
"manaExhaustion": "Mana Exhaustion",
|
||||
"incompetencePenalty": "Penalty for Incompetence",
|
||||
"notEnoughZeal": "Not enough zeal.",
|
||||
"notEnoughBlood": "Not enough blood.",
|
||||
"raceLocked": "Spell requires race: {races}.",
|
||||
@@ -451,7 +454,7 @@
|
||||
"spend": "Spend",
|
||||
"attacks": "Attacks",
|
||||
"specialAbilities": "Special Abilities",
|
||||
"reactions": "Reactions",
|
||||
"reactions": "Immediate Actions",
|
||||
"legendaryActions": "Legendary Actions",
|
||||
"actionsTitle": "Quick Actions",
|
||||
"customEquipment": "Custom Equipment",
|
||||
|
||||
+16
-13
@@ -105,7 +105,6 @@
|
||||
"spell": {
|
||||
"label": "Zaklęcie",
|
||||
"cast": "Rzuć zaklęcie",
|
||||
"circle": "Krąg",
|
||||
"school": "Szkoła",
|
||||
"discipline": "Dziedzina",
|
||||
"difficulty": "Próg zaklęcia",
|
||||
@@ -187,7 +186,8 @@
|
||||
"abyssAspects": "Magia Otchłani - Aspekty",
|
||||
"abyssPrimal": "Magia Otchłani - Pierwotna Magia",
|
||||
"wildWitch": "Dzika Wiedźmia Magia",
|
||||
"eldritch": "Magia Otchłani"
|
||||
"eldritch": "Magia Otchłani",
|
||||
"thaumaturgy": "Taumaturgia"
|
||||
},
|
||||
"deity": {
|
||||
"common": "Modlitwy Powszechne",
|
||||
@@ -200,14 +200,14 @@
|
||||
"eros": "Eros"
|
||||
},
|
||||
"sourceBook": {
|
||||
"core-rules": "PG",
|
||||
"magic-book": "KM",
|
||||
"arcanum-sanguinis": "AS",
|
||||
"crimson-cult": "CS",
|
||||
"abyss-curse": "KO",
|
||||
"humanity-guide": "PLM",
|
||||
"bestiary": "B",
|
||||
"gold-steel-magic": "ZSM"
|
||||
"podrecznik-gry": "Podręcznik Gry",
|
||||
"ksiega-magii": "Księga Magii",
|
||||
"arcanum-sanguinis": "Arcanum Sanguinis",
|
||||
"crimson-cult": "Szkarłatny Kult",
|
||||
"klatwa-otchlani": "Klątwa Otchłani",
|
||||
"przewodnik": "Przewodnik",
|
||||
"bestiariusz": "Bestiariusz",
|
||||
"zlote-stal-magia": "Złoto i Stal: Magia"
|
||||
},
|
||||
"aoeShape": {
|
||||
"point": "Punkt",
|
||||
@@ -284,7 +284,7 @@
|
||||
"type": "Typ",
|
||||
"passive": "Pasywna",
|
||||
"active": "Aktywna",
|
||||
"reaction": "Reakcja",
|
||||
"reaction": "Akcja Natychmiastowa",
|
||||
"freeAction": "Akcja Darmowa",
|
||||
"prerequisite": "Wymóg",
|
||||
"description": "Opis",
|
||||
@@ -344,7 +344,7 @@
|
||||
"languages": "Języki",
|
||||
"attacks": "Ataki",
|
||||
"specialAbilities": "Zdolności Specjalne",
|
||||
"reactions": "Reakcje",
|
||||
"reactions": "Akcje Natychmiastowe",
|
||||
"legendaryActions": "Akcje Legendarne",
|
||||
"savingThrows": "Rzuty Obronne",
|
||||
"damageResistances": "Odporności na Obrażenia",
|
||||
@@ -380,6 +380,9 @@
|
||||
"cannotBeUnraveled": "Nie da się rozpleść",
|
||||
"cast": "Rzuć",
|
||||
"notEnoughMana": "Niewystarczająca mana.",
|
||||
"borrowMana": "Brakuje many - różnica zostanie pożyczona (Wyczerpanie Many).",
|
||||
"manaExhaustion": "Wyczerpanie Many",
|
||||
"incompetencePenalty": "Kara za Nieudolność",
|
||||
"notEnoughZeal": "Niewystarczający Zapał.",
|
||||
"notEnoughBlood": "Niewystarczająca Krew.",
|
||||
"raceLocked": "Zaklęcie wymaga rasy: {races}.",
|
||||
@@ -454,7 +457,7 @@
|
||||
"spend": "Wydaj",
|
||||
"attacks": "Ataki",
|
||||
"specialAbilities": "Zdolności Specjalne",
|
||||
"reactions": "Reakcje",
|
||||
"reactions": "Akcje Natychmiastowe",
|
||||
"legendaryActions": "Akcje Legendarne",
|
||||
"actionsTitle": "Szybkie Akcje",
|
||||
"customEquipment": "Własny ekwipunek",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hbm-rpg-v3-foundry",
|
||||
"version": "1.3.37",
|
||||
"version": "1.4.0",
|
||||
"description": "Foundry VTT system for Homebrew Magic: Role Playing Game v3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -54,18 +54,20 @@ allDocs.push(...(await parseGear(ctx)));
|
||||
|
||||
console.log(`[build-packs] parsed ${allDocs.length} documents total`);
|
||||
|
||||
// 2b. Assign signature spells: every discipline lists the spells of its own school.
|
||||
// Sacred Magic lists only the common prayers (the rest are deity-specific).
|
||||
// 2b. signatureSpells = every spell belonging to the discipline (own school or
|
||||
// listed in the spell's requirements.discipline).
|
||||
// Spells with school `eldritch` belong to Abyss - Primal Magic.
|
||||
const SCHOOL_TO_DISCIPLINE: Record<string, string> = { eldritch: 'abyssPrimal' };
|
||||
for (const disc of allDocs) {
|
||||
if (disc.subType !== 'discipline') continue;
|
||||
const sys = disc.system as Record<string, any>;
|
||||
const spells = allDocs.filter((d) => {
|
||||
sys.signatureSpells = allDocs
|
||||
.filter((d) => {
|
||||
if (d.subType !== 'spell') return false;
|
||||
const sp = d.system as Record<string, any>;
|
||||
if (sp.school !== disc.id || sp.isSuperspell) return false;
|
||||
return disc.id !== 'sacred' || sp.deity === 'common';
|
||||
});
|
||||
sys.signatureSpells = spells.map((d) => d.name);
|
||||
return (SCHOOL_TO_DISCIPLINE[sp.school] ?? sp.school) === disc.id || (sp.requirements?.discipline ?? []).includes(disc.id);
|
||||
})
|
||||
.map((d) => d.name);
|
||||
}
|
||||
|
||||
// 3. Group by pack and emit one JSON per doc into packs-src/<pack>/.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// DEPRECATED: one-off extractor that writes the old nested spell frontmatter. The vault now uses the flat format read by scripts/parsers/spell-parser.ts - do not run this over ObsidianNotes/spells.
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
@@ -3,4 +3,4 @@ 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, '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 (liczniki Odetchnięć i pożyczonej Many wyzerowane).`);
|
||||
|
||||
@@ -3,4 +3,5 @@ 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');
|
||||
if (result?.blocked) return ui.notifications.warn(`${actor.name}: limit Odetchnięć (2 między Długimi Odpoczynkami) wyczerpany.`);
|
||||
ui.notifications.info(`${actor.name}: chwila wytchnienia - przywrócono ${result?.hpRestored ?? 0} PW i ${result?.manaRestored ?? 0} Many.`);
|
||||
|
||||
@@ -5,59 +5,109 @@ 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. */
|
||||
const SpellSystemSchema = z.object({
|
||||
/**
|
||||
* Flat, human-editable vault frontmatter for a spell. Nothing is nested;
|
||||
* optional keys can simply be left out. `toSystem()` expands it into the
|
||||
* (partly nested) Foundry `SpellData` shape.
|
||||
*/
|
||||
const SpellNoteSchema = z.object({
|
||||
castingMode: z.enum(['standard', 'sacred', 'witch', 'blood']),
|
||||
school: z.string(),
|
||||
deity: z.string().default(''),
|
||||
sourceBook: z.string(),
|
||||
manaCost: z.number().int().min(0),
|
||||
bloodCost: z.number().int().min(0).default(0),
|
||||
threshold: z.number().int().min(1).max(6),
|
||||
successes: z.number().int().min(1),
|
||||
complexityLevel: z.number().int().min(0).default(0),
|
||||
isSuperspell: z.boolean().default(false),
|
||||
requiresGroupCast: z.boolean().default(false),
|
||||
minCasters: z.number().int().min(1).default(1),
|
||||
nonCombatOnly: z.boolean().default(false),
|
||||
damageBase: z.string().default(''),
|
||||
damageType: z.string().default(''),
|
||||
verbal: z.boolean().default(false),
|
||||
somatic: z.boolean().default(false),
|
||||
material: z.coerce.string().default(''),
|
||||
symbols: z.array(z.string()).default([]),
|
||||
requiresRace: z.array(z.string()).default([]),
|
||||
requiresTalent: z.array(z.string()).default([]),
|
||||
requiresDiscipline: z.array(z.string()).default([]),
|
||||
castingTime: z.coerce.string().default('Instant'),
|
||||
range: z.coerce.string().default(''),
|
||||
targets: z.coerce.string().default(''),
|
||||
duration: z.coerce.string().default(''),
|
||||
damageBase: z.coerce.string().default(''),
|
||||
damageType: z.string().default('magical'),
|
||||
ignoresArmor: z.boolean().default(false),
|
||||
statusEffects: z.array(z.string()).default([]),
|
||||
saveAttribute: z.string().default(''),
|
||||
saveSkill: z.string().default(''),
|
||||
triggers: z.array(z.object({ event: z.string(), effect: z.string() })).default([]),
|
||||
components: z.object({
|
||||
verbal: z.boolean().default(false),
|
||||
somatic: z.boolean().default(false),
|
||||
material: z.string().default(''),
|
||||
symbols: z.array(z.string()).default([]),
|
||||
}),
|
||||
requirements: z.object({
|
||||
race: z.array(z.string()).default([]),
|
||||
talent: z.array(z.string()).default([]),
|
||||
discipline: z.array(z.string()).default([]),
|
||||
}),
|
||||
difficulty: z.object({
|
||||
threshold: z.number().int().min(1).max(6),
|
||||
successes: z.number().int().min(1),
|
||||
}),
|
||||
range: z.object({
|
||||
kind: z.string(),
|
||||
value: z.number().optional(),
|
||||
unit: z.string().optional(),
|
||||
text: z.string().optional(),
|
||||
}),
|
||||
duration: z.string().default(''),
|
||||
castingTime: z.string().default(''),
|
||||
target: z.string().default(''),
|
||||
areaOfEffect: z
|
||||
.object({ shape: z.string(), x: z.number().optional(), y: z.number().optional(), unit: z.string().optional() })
|
||||
.nullable()
|
||||
.default(null),
|
||||
overcastOptions: z
|
||||
.array(z.object({ text: z.string(), repeatable: z.boolean().default(true), cost: z.number().int().default(0) }))
|
||||
.default([]),
|
||||
description: z.string().default(''),
|
||||
overcast: z.array(z.string()).default([]),
|
||||
});
|
||||
type SpellNote = z.infer<typeof SpellNoteSchema>;
|
||||
|
||||
type Aoe = { shape: string; x: number; y: number; unit: string };
|
||||
|
||||
/** Derive the structured area of effect from the free-text `targets` line. */
|
||||
export function deriveAreaOfEffect(targets: string): Aoe {
|
||||
const t = targets.replace(/\s+/g, ' ');
|
||||
let m = t.match(/kwadrat o boku (\d+)\s*m/i);
|
||||
if (m) return { shape: 'square', x: Number(m[1]), y: Number(m[1]), unit: 'm' };
|
||||
m = t.match(/prostokąt (\d+)\s*m\s*[x×]\s*(\d+)\s*m/i);
|
||||
if (m) return { shape: 'rectangle', x: Number(m[1]), y: Number(m[2]), unit: 'm' };
|
||||
m = t.match(/(?:sfera|kula) o średnicy (\d+)\s*m/i);
|
||||
if (m) return { shape: 'sphere', x: Number(m[1]) / 2, y: 0, unit: 'm' };
|
||||
m = t.match(/(?:sfera|kula|kopuła) o promieniu (\d+)\s*m/i);
|
||||
if (m) return { shape: 'sphere', x: Number(m[1]), y: 0, unit: 'm' };
|
||||
if (/^linia/i.test(t)) return { shape: 'line', x: 0, y: 0, unit: 'm' };
|
||||
return { shape: 'point', x: 0, y: 0, unit: 'm' };
|
||||
}
|
||||
|
||||
/** Casting time in whole rounds / minutes, for the sheet's numeric fields. */
|
||||
export function deriveCastingTime(text: string): { rounds: number; minutes: number } {
|
||||
let m = text.match(/(\d+)\s*godzin/i);
|
||||
if (m) return { rounds: 0, minutes: Number(m[1]) * 60 };
|
||||
m = text.match(/(\d+)\s*minut/i);
|
||||
if (m) return { rounds: 0, minutes: Number(m[1]) };
|
||||
m = text.match(/(\d+)\s*rund/i);
|
||||
if (m) return { rounds: Number(m[1]), minutes: 0 };
|
||||
return { rounds: 0, minutes: 0 };
|
||||
}
|
||||
|
||||
function toSystem(n: SpellNote): Record<string, unknown> {
|
||||
const time = deriveCastingTime(n.castingTime);
|
||||
return {
|
||||
school: n.school,
|
||||
deity: n.deity,
|
||||
sourceBook: n.sourceBook,
|
||||
castingMode: n.castingMode,
|
||||
manaCost: n.manaCost,
|
||||
bloodCost: n.bloodCost,
|
||||
difficulty: { threshold: n.threshold, successes: n.successes },
|
||||
components: { verbal: n.verbal, somatic: n.somatic, material: n.material, symbols: n.symbols },
|
||||
castingTime: n.castingTime,
|
||||
castingTimeRounds: time.rounds,
|
||||
castingTimeMinutes: time.minutes,
|
||||
range: n.range,
|
||||
targets: n.targets,
|
||||
duration: n.duration,
|
||||
areaOfEffect: deriveAreaOfEffect(n.targets),
|
||||
damageBase: n.damageBase,
|
||||
damageType: n.damageType || 'magical',
|
||||
ignoresArmor: n.ignoresArmor,
|
||||
statusEffects: n.statusEffects,
|
||||
saveAttribute: n.saveAttribute,
|
||||
saveSkill: n.saveSkill,
|
||||
requirements: { race: n.requiresRace, talent: n.requiresTalent, discipline: n.requiresDiscipline },
|
||||
isSuperspell: n.isSuperspell,
|
||||
requiresGroupCast: n.requiresGroupCast,
|
||||
minCasters: n.minCasters,
|
||||
nonCombatOnly: n.nonCombatOnly,
|
||||
overcastOptions: n.overcast.map((description) => ({ description, manaPerStep: 1 })),
|
||||
triggers: n.triggers,
|
||||
complexityLevel: n.complexityLevel,
|
||||
};
|
||||
}
|
||||
|
||||
function getAllFiles(dirPath: string, arrayOfFiles: string[] = []) {
|
||||
const files = readdirSync(dirPath);
|
||||
@@ -100,13 +150,12 @@ export const parseSpells: ParserFn = async (ctx: ParserContext): Promise<ParsedD
|
||||
continue;
|
||||
}
|
||||
|
||||
const payload = { ...frontmatter, description: bodyRaw.trim() };
|
||||
|
||||
const nameMatch = payload.description.match(/^#\s+(.+)$/m);
|
||||
const body = bodyRaw.trim();
|
||||
const nameMatch = body.match(/^#\s+(.+)$/m);
|
||||
const spellName = nameMatch ? nameMatch[1].trim() : basename(file, '.md');
|
||||
payload.description = payload.description.replace(/^#\s+.+$/m, '').trim();
|
||||
const description = body.replace(/^#\s+.+$/m, '').trim();
|
||||
|
||||
const parsed = SpellSystemSchema.safeParse(payload);
|
||||
const parsed = SpellNoteSchema.safeParse(frontmatter);
|
||||
if (!parsed.success) {
|
||||
if (ctx.strict) throw new Error(`[spell-parser] Validation failed for "${file}": ${parsed.error.message}`);
|
||||
continue;
|
||||
@@ -129,8 +178,8 @@ export const parseSpells: ParserFn = async (ctx: ParserContext): Promise<ParsedD
|
||||
subType: 'spell',
|
||||
pack,
|
||||
source: { book: parsed.data.sourceBook, chapter: 'Spells', line: 1 },
|
||||
system: parsed.data,
|
||||
description: payload.description,
|
||||
system: { ...toSystem(parsed.data), description },
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+11
-5
@@ -91,12 +91,16 @@ export interface MagicPowerEntry {
|
||||
manaPerDay: number; // daily mana pool ("Ilość Many")
|
||||
}
|
||||
|
||||
declare const __SYSTEM_ID__: string;
|
||||
/** System id; lives here (not in hbm.ts) so logic modules and tests don't pull in the sheets. */
|
||||
export const SYSTEM_ID: string = typeof __SYSTEM_ID__ !== 'undefined' ? __SYSTEM_ID__ : 'hbm-rpg-v3';
|
||||
|
||||
export const MAGIC_POWER_TABLE: readonly MagicPowerEntry[] = Object.freeze([
|
||||
{ level: 0, label: '0', dicePool: 6, maxPerSpell: 10, manaPerDay: 60 },
|
||||
{ level: 1, label: 'I', dicePool: 4, maxPerSpell: 6, manaPerDay: 36 },
|
||||
{ level: 2, label: 'II', dicePool: 4, maxPerSpell: 5, manaPerDay: 30 },
|
||||
{ level: 3, label: 'III', dicePool: 3, maxPerSpell: 5, manaPerDay: 21 },
|
||||
{ level: 4, label: 'IV', dicePool: 3, maxPerSpell: 4, manaPerDay: 24 },
|
||||
{ level: 3, label: 'III', dicePool: 3, maxPerSpell: 5, manaPerDay: 24 },
|
||||
{ level: 4, label: 'IV', dicePool: 3, maxPerSpell: 4, manaPerDay: 21 },
|
||||
{ level: 5, label: 'V', dicePool: 3, maxPerSpell: 4, manaPerDay: 18 },
|
||||
{ level: 6, label: 'VI', dicePool: 2, maxPerSpell: 3, manaPerDay: 18 },
|
||||
{ level: 7, label: 'VII', dicePool: 2, maxPerSpell: 3, manaPerDay: 12 },
|
||||
@@ -136,6 +140,8 @@ export const SPELL_SCHOOLS = [
|
||||
// Forbidden / extra-academic
|
||||
'blood', 'crimson', 'abyssAspects', 'abyssPrimal', 'wildWitch',
|
||||
'eldritch',
|
||||
// Malferian thaumaturgy (house-shield, retinue-guard, shield-bubble)
|
||||
'thaumaturgy',
|
||||
] as const;
|
||||
export type SpellSchool = (typeof SPELL_SCHOOLS)[number];
|
||||
|
||||
@@ -160,9 +166,9 @@ export const WITCH_SYMBOLS: readonly string[] = Object.freeze([
|
||||
|
||||
/** Source-book provenance for compendium content. */
|
||||
export const SOURCE_BOOKS = [
|
||||
'core-rules', 'magic-book', 'arcanum-sanguinis',
|
||||
'crimson-cult', 'abyss-curse', 'humanity-guide',
|
||||
'bestiary', 'gold-steel-magic',
|
||||
'podrecznik-gry', 'ksiega-magii', 'arcanum-sanguinis',
|
||||
'crimson-cult', 'klatwa-otchlani', 'przewodnik',
|
||||
'bestiariusz', 'zlote-stal-magia',
|
||||
] as const;
|
||||
export type SourceBook = (typeof SOURCE_BOOKS)[number];
|
||||
|
||||
|
||||
@@ -72,6 +72,12 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
||||
// 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 }),
|
||||
// Mana borrowed beyond the daily pool since the last Długi Odpoczynek (Wyczerpanie Many).
|
||||
manaBorrowed: makeIntField(0, { min: 0 }),
|
||||
// Odetchnięcia taken since the last Długi Odpoczynek (max 2).
|
||||
breathersTaken: makeIntField(0, { min: 0 }),
|
||||
// Akcje Natychmiastowe used in the current round (1 free, Zapał buys more).
|
||||
immediateActionsUsed: makeIntField(0, { min: 0 }),
|
||||
// Insanity points (Klątwa Otchłani VIII). Each Abyss exposure may add 1; rolls
|
||||
// mental-condition table when reaching thresholds.
|
||||
insanity: makeIntField(0, { min: 0 }),
|
||||
@@ -143,6 +149,9 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
||||
zeal: { value: number; max: number };
|
||||
blood: { value: number; max: number };
|
||||
elixirTolerance: number;
|
||||
manaBorrowed: number;
|
||||
breathersTaken: number;
|
||||
immediateActionsUsed: number;
|
||||
insanity: number;
|
||||
magicalArmor: { value: number; max: number; runicCounter: number };
|
||||
magicalShield: { value: number };
|
||||
@@ -247,7 +256,7 @@ export class CharacterData extends foundry.abstract.TypeDataModel {
|
||||
totalArmorConditionMax += condMax;
|
||||
// Condition-scaled DR: broken armor (cond=0) gives 0 DR
|
||||
if (condMax > 0) {
|
||||
effectiveDR += Math.floor(baseDR * (cond / condMax));
|
||||
effectiveDR += Math.ceil(baseDR * (cond / condMax));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ export class NpcData extends foundry.abstract.TypeDataModel {
|
||||
bonus: makeIntField(0, { min: 0 }),
|
||||
}),
|
||||
speed: makeStringField('5m'),
|
||||
manaBorrowed: makeIntField(0, { min: 0 }),
|
||||
immediateActionsUsed: makeIntField(0, { min: 0 }),
|
||||
}),
|
||||
skills: new f.SchemaField(skillEntries),
|
||||
details: new f.SchemaField({
|
||||
|
||||
@@ -23,7 +23,6 @@ export class SpellData extends foundry.abstract.TypeDataModel {
|
||||
const f = fields();
|
||||
return {
|
||||
// Identification
|
||||
circle: makeIntField(1, { min: 0, max: 9 }),
|
||||
school: makeStringField('', { blank: true, choices: SPELL_SCHOOLS as unknown as readonly string[] }),
|
||||
discipline: makeStringField(''),
|
||||
deity: makeStringField('', { blank: true, choices: SACRED_DEITIES as unknown as readonly string[] }),
|
||||
@@ -37,7 +36,7 @@ export class SpellData extends foundry.abstract.TypeDataModel {
|
||||
// Difficulty
|
||||
difficulty: new f.SchemaField({
|
||||
threshold: makeIntField(TS_DEFAULT_THRESHOLD, { min: 2, max: 6 }),
|
||||
successes: makeIntField(TS_DEFAULT_REQUIRED, { min: 1, max: 10 }),
|
||||
successes: makeIntField(TS_DEFAULT_REQUIRED, { min: 1, max: 30 }),
|
||||
}),
|
||||
|
||||
// Components
|
||||
@@ -100,7 +99,7 @@ export class SpellData extends foundry.abstract.TypeDataModel {
|
||||
// Variable success summons (Przywołanie Istoty z Otchłani)
|
||||
variableSuccesses: makeArrayField(new f.SchemaField({
|
||||
label: makeStringField(''),
|
||||
successes: makeIntField(1, { min: 1, max: 10 }),
|
||||
successes: makeIntField(1, { min: 1, max: 30 }),
|
||||
})),
|
||||
|
||||
// Description
|
||||
|
||||
+2
-1
@@ -33,7 +33,8 @@ import * as brewing from './logic/brewing';
|
||||
import { rest } from './logic/rest';
|
||||
import * as trade from './logic/trade';
|
||||
|
||||
export const SYSTEM_ID = __SYSTEM_ID__;
|
||||
import { SYSTEM_ID } from './constants';
|
||||
export { SYSTEM_ID };
|
||||
|
||||
Hooks.once('init', () => {
|
||||
console.log(`${SYSTEM_ID} | Initialising Homebrew Magic: RPG v3 system`);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
*/
|
||||
|
||||
import type { CastableActor } from './spell-cast';
|
||||
import { bloodAbsorbHp, bloodToZeal } from './rules';
|
||||
|
||||
const SELF_HARM_RATIO = 2; // HP per 1 Blood
|
||||
const RESPECT_FOR_LIFE_PENALTY = 3; // multiplier when talent present
|
||||
@@ -76,6 +77,32 @@ export async function selfHarm(actor: CastableActor, hpSpent: number): Promise<{
|
||||
return { ok: true, bloodGained: newBlood - blood.value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Wchłanianie Krwi: spend Krew to heal Żywotność – 2 HP per Krew (Akcja, once per round);
|
||||
* Większe Wchłanianie Krwi: 3 HP per Krew (Akcja Natychmiastowa), or 1 Zapał per 5 Krew.
|
||||
*/
|
||||
export async function absorbBlood(
|
||||
actor: CastableActor,
|
||||
bloodSpent: number,
|
||||
opts: { greater?: boolean; asZeal?: boolean } = {},
|
||||
): Promise<{ ok: boolean; hpGained: number; zealGained: number; reason?: string }> {
|
||||
const a = actor.system.attributes as any;
|
||||
if (bloodSpent <= 0) return { ok: true, hpGained: 0, zealGained: 0 };
|
||||
if (!a.blood || a.blood.value < bloodSpent) return { ok: false, hpGained: 0, zealGained: 0, reason: 'insufficient-blood' };
|
||||
const update: Record<string, number> = { 'system.attributes.blood.value': a.blood.value - bloodSpent };
|
||||
let hpGained = 0;
|
||||
let zealGained = 0;
|
||||
if (opts.greater && opts.asZeal) {
|
||||
zealGained = Math.min(bloodToZeal(bloodSpent), Math.max(0, (a.zeal?.max ?? 0) - (a.zeal?.value ?? 0)));
|
||||
update['system.attributes.zeal.value'] = (a.zeal?.value ?? 0) + zealGained;
|
||||
} else {
|
||||
hpGained = Math.min(bloodAbsorbHp(bloodSpent, !!opts.greater), Math.max(0, (a.health?.max ?? 0) - (a.health?.value ?? 0)));
|
||||
update['system.attributes.health.value'] = (a.health?.value ?? 0) + hpGained;
|
||||
}
|
||||
await actor.update(update);
|
||||
return { ok: true, hpGained, zealGained };
|
||||
}
|
||||
|
||||
/** Life Steal: invoked from the damage-application hook when the source is a blood spell. */
|
||||
export async function lifeStealOnDamage(actor: CastableActor, damageDealt: number): Promise<number> {
|
||||
if (damageDealt <= 0) return 0;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { HbmTSRoll } from '../dice/ts-roll';
|
||||
import { TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD, getMagicPowerEntry } from '../constants';
|
||||
import type { CastableActor } from './spell-cast';
|
||||
import { MANA_ELIXIR_NORMAL, MANA_ELIXIR_QUICK_DIE, manaElixirGain } from './rules';
|
||||
|
||||
export interface ElixirRecipe {
|
||||
name: string;
|
||||
@@ -61,3 +62,21 @@ export async function brewElixir(actor: CastableActor, recipe: ElixirRecipe): Pr
|
||||
});
|
||||
return { roll, success: !!roll.ts?.isSuccess };
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliksir Odnowienia Many: normal use restores 3 mana (capped at the daily pool) and
|
||||
* raises Maksymalny Koszt Zaklęcia by 1 until the end of the drinker's next turn;
|
||||
* quick use (Akcja Natychmiastowa) restores 1d3 mana only.
|
||||
*/
|
||||
export async function drinkManaElixir(actor: CastableActor, quick = false): Promise<{ gained: number }> {
|
||||
const a = actor.system.attributes as any;
|
||||
let amount = MANA_ELIXIR_NORMAL;
|
||||
if (quick) {
|
||||
const roll = new Roll(`1d${MANA_ELIXIR_QUICK_DIE}`);
|
||||
await roll.evaluate();
|
||||
amount = roll.total ?? 1;
|
||||
}
|
||||
const gained = manaElixirGain(a.mana.value, a.mana.max, amount);
|
||||
await actor.update({ 'system.attributes.mana.value': a.mana.value + gained });
|
||||
return { gained };
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ async function onApplyDamage(btn: HTMLElement): Promise<void> {
|
||||
ignoreMagicalArmor: ignoresArmor,
|
||||
ignoreMagicalShield: ignoresArmor,
|
||||
ignorePhysicalArmor: ignoresArmor,
|
||||
minDamageAfterArmor: btn.dataset.minAfterArmor === 'true',
|
||||
} as any);
|
||||
}
|
||||
|
||||
|
||||
+41
-1
@@ -20,8 +20,42 @@ function hasStatus(actor: any, id: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Statuses lasting "until the end of this character's next turn" (see conditions.ts `expires`). */
|
||||
export const END_OF_NEXT_TURN_STATUSES = ['oszolomiony', 'przerazony'] as const;
|
||||
|
||||
const FLAG_SCOPE = 'hbm-rpg-v3';
|
||||
|
||||
function expiringEffects(actor: any): any[] {
|
||||
const out: any[] = [];
|
||||
for (const ef of actor?.effects ?? []) {
|
||||
const ids: string[] = ef?.statuses ? [...ef.statuses] : [];
|
||||
if (ids.some((s) => (END_OF_NEXT_TURN_STATUSES as readonly string[]).includes(s))) out.push(ef);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Start of the actor's turn: arm effects so they end with this turn. */
|
||||
async function armExpiring(actor: any): Promise<void> {
|
||||
for (const ef of expiringEffects(actor)) {
|
||||
if (!ef.getFlag?.(FLAG_SCOPE, 'turnStarted')) await ef.setFlag?.(FLAG_SCOPE, 'turnStarted', true);
|
||||
}
|
||||
}
|
||||
|
||||
/** End of the actor's turn: drop armed effects. */
|
||||
async function expireArmed(actor: any): Promise<void> {
|
||||
const ids = expiringEffects(actor).filter((ef) => ef.getFlag?.(FLAG_SCOPE, 'turnStarted')).map((ef) => ef.id);
|
||||
if (ids.length) await actor.deleteEmbeddedDocuments?.('ActiveEffect', ids);
|
||||
}
|
||||
|
||||
export function registerCombatHooks(): void {
|
||||
Hooks.on('combatRound', async (_combat: Combat, _updateData: unknown, _options: { advanceTime?: number; direction?: number }) => {
|
||||
Hooks.on('combatRound', async (combat: Combat, _updateData: unknown, _options: { advanceTime?: number; direction?: number }) => {
|
||||
// One free Akcja Natychmiastowa per round – reset the per-round counter.
|
||||
for (const c of (combat as any)?.combatants ?? []) {
|
||||
const actor = c.actor;
|
||||
if (actor?.system?.attributes?.immediateActionsUsed) {
|
||||
await actor.update({ 'system.attributes.immediateActionsUsed': 0 });
|
||||
}
|
||||
}
|
||||
ChatMessage.create({
|
||||
content: `<em>${game.i18n.localize('HBM.combat.newRound')}</em>`,
|
||||
whisper: ChatMessage.getWhisperRecipients('GM'),
|
||||
@@ -29,9 +63,15 @@ export function registerCombatHooks(): void {
|
||||
});
|
||||
|
||||
Hooks.on('combatTurn', async (combat: Combat) => {
|
||||
// The previous combatant's turn just ended: expire their timed Stany.
|
||||
const prevId = (combat as any).previous?.combatantId;
|
||||
const prevActor = prevId ? (combat as any).combatants?.get?.(prevId)?.actor : null;
|
||||
if (prevActor) await expireArmed(prevActor);
|
||||
|
||||
const combatant = combat.combatant;
|
||||
const actor = combatant?.actor;
|
||||
if (!actor) return;
|
||||
await armExpiring(actor);
|
||||
|
||||
// Skip turn for unconscious / restrained-to-incapacitation
|
||||
if (hasStatus(actor, 'nieprzytomny') || hasStatus(actor, 'obezwladniony')) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* consumed by the roll pipeline and combat hooks.
|
||||
*/
|
||||
|
||||
import { SYSTEM_ID } from '../hbm';
|
||||
import { SYSTEM_ID } from '../constants';
|
||||
|
||||
export interface ConditionDef {
|
||||
id: string;
|
||||
@@ -17,14 +17,14 @@ 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: 'przewrocony', i18nKey: 'HBM.conditions.przewrocony', icon: 'icons/svg/falling.svg', flags: { hbm: { prone: true, attacksWithinMeters: 3, thresholdStepsAgainst: -2, onlyDefense: 'dodge', standUpCost: 'halfMoveCeil' } } },
|
||||
{ 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: 'oszolomiony', i18nKey: 'HBM.conditions.oszolomiony', icon: 'icons/svg/daze.svg', flags: { hbm: { maxActions: 1, blockZeal: true, halveSpeed: true, expires: 'endOfNextTurn' } } },
|
||||
{
|
||||
id: 'zatruty',
|
||||
i18nKey: 'HBM.conditions.zatruty',
|
||||
@@ -53,7 +53,7 @@ export const CONDITIONS: ConditionDef[] = [
|
||||
{ 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: { fearSource: '', thresholdStepsVsSource: 1, noCloserToSource: true, expires: 'endOfNextTurn', refreshOnReapply: true } } },
|
||||
|
||||
// Mental illnesses (Klątwa Otchłani Ch. VIII - book chapter currently a placeholder;
|
||||
// these are stub registrations with `todoBookGap: true` until full mechanics drop).
|
||||
|
||||
+22
-2
@@ -7,11 +7,14 @@
|
||||
*
|
||||
* Bypass flags allow effects to skip individual layers.
|
||||
*
|
||||
* After resolving, if HP loss meets the Oszołomiony threshold ⌈(C+U+D)/3⌉
|
||||
* the effect is auto-applied to character actors.
|
||||
* After resolving, if HP loss (after reductions) is at least half of max
|
||||
* Żywotność (= C+U+D) the Oszołomiony status is applied (lasts until the end
|
||||
* of that character's next turn). Żywotność lost also yields a Koncentracja
|
||||
* test 4:⌈⅓ lost⌉ (min 1).
|
||||
*/
|
||||
|
||||
import { DamageType } from '../constants';
|
||||
import { triggersStun, concentrationTest } from './rules';
|
||||
|
||||
export interface ApplyDamageOptions {
|
||||
amount: number;
|
||||
@@ -20,6 +23,8 @@ export interface ApplyDamageOptions {
|
||||
ignoreMagicalShield?: boolean;
|
||||
ignorePhysicalArmor?: boolean;
|
||||
damageArmor?: boolean;
|
||||
/** Magiczny Pocisk: at least 1 damage gets through Pancerz to Żywotność. */
|
||||
minDamageAfterArmor?: boolean;
|
||||
postChat?: boolean;
|
||||
}
|
||||
|
||||
@@ -29,6 +34,8 @@ export interface DamageReport {
|
||||
hpDamage: number;
|
||||
shieldDropped: boolean;
|
||||
oszolomionyApplied: boolean;
|
||||
/** Dusza (Determinacja) 4:S for a concentrating target; absent when no Żywotność was lost. */
|
||||
concentrationTest?: { threshold: number; successes: number };
|
||||
}
|
||||
|
||||
interface ActorLike {
|
||||
@@ -90,6 +97,10 @@ export async function applyDamage(actor: ActorLike, opts: ApplyDamageOptions): P
|
||||
if (absorbed > 0) {
|
||||
report.absorbed.physicalArmor = absorbed;
|
||||
remaining -= absorbed;
|
||||
if (opts.minDamageAfterArmor && remaining === 0 && absorbed > 0) {
|
||||
remaining = 1;
|
||||
report.absorbed.physicalArmor = absorbed - 1;
|
||||
}
|
||||
}
|
||||
if (opts.damageArmor && actor.type === 'character' && a.physicalArmor.condition != null) {
|
||||
// Decrement durability of the first equipped armor piece (character only)
|
||||
@@ -116,6 +127,15 @@ export async function applyDamage(actor: ActorLike, opts: ApplyDamageOptions): P
|
||||
|
||||
if (Object.keys(updates).length > 0) await actor.update(updates);
|
||||
|
||||
// Oszołomiony: a single hit that costs ≥ half max Żywotność after all reductions.
|
||||
if (triggersStun(report.hpDamage, Number(a.health?.max) || 0) && actor.toggleStatusEffect) {
|
||||
const already = actor.effects?.find((e: any) => e.statuses?.has?.('oszolomiony'));
|
||||
if (!already) await actor.toggleStatusEffect('oszolomiony', { active: true });
|
||||
report.oszolomionyApplied = true;
|
||||
}
|
||||
const concentration = concentrationTest(report.hpDamage);
|
||||
if (concentration) report.concentrationTest = concentration;
|
||||
|
||||
if (opts.postChat ?? true) {
|
||||
const lines: string[] = [
|
||||
`<strong>${actor.name}</strong> - ${game.i18n.localize('HBM.damage.report.title')}: <strong>${total}</strong>`,
|
||||
|
||||
+16
-1
@@ -4,6 +4,7 @@
|
||||
* Breather (Odetchnięcie, 10 min):
|
||||
* - Restore 1d6 + Wytrzymałość skill value HP (capped at max).
|
||||
* - Restore 1d6 mana (capped at daily pool max).
|
||||
* - At most twice between two Długie Odpoczynki (counter attributes.breathersTaken).
|
||||
*
|
||||
* Short Rest (Krótki Odpoczynek, 1h+):
|
||||
* - Restore half of max HP.
|
||||
@@ -22,6 +23,7 @@
|
||||
|
||||
import type { CastableActor } from './spell-cast';
|
||||
import { CONDITIONS } from './conditions';
|
||||
import { MAX_BREATHERS_BETWEEN_LONG_RESTS } from './rules';
|
||||
|
||||
export type RestKind = 'breather' | 'short' | 'long';
|
||||
|
||||
@@ -49,11 +51,19 @@ export interface RestResult {
|
||||
bloodRestored: number;
|
||||
toleranceRecovered: number;
|
||||
effectsCleared: number;
|
||||
/** True when an Odetchnięcie was refused (already 2 since the last Długi Odpoczynek). */
|
||||
blocked?: boolean;
|
||||
}
|
||||
|
||||
export async function rest(actor: CastableActor, kind: RestKind): Promise<RestResult> {
|
||||
Hooks.callAll('hbm.beforeRest', actor, kind);
|
||||
const a = actor.system.attributes as any;
|
||||
if (kind === 'breather' && (a.breathersTaken ?? 0) >= MAX_BREATHERS_BETWEEN_LONG_RESTS) {
|
||||
return {
|
||||
kind, hpRestored: 0, manaRestored: 0, zealRestored: 0, bloodRestored: 0,
|
||||
toleranceRecovered: 0, effectsCleared: 0, blocked: true,
|
||||
};
|
||||
}
|
||||
Hooks.callAll('hbm.beforeRest', actor, kind);
|
||||
const update: Record<string, unknown> = {};
|
||||
|
||||
const hpBefore = a.health?.value ?? 0;
|
||||
@@ -69,6 +79,7 @@ export async function rest(actor: CastableActor, kind: RestKind): Promise<RestRe
|
||||
let toleranceRecovered = 0;
|
||||
|
||||
if (kind === 'breather') {
|
||||
update['system.attributes.breathersTaken'] = (a.breathersTaken ?? 0) + 1;
|
||||
if (a.mana?.max != null && manaBefore < a.mana.max) {
|
||||
const manaRoll = new Roll('1d6');
|
||||
await manaRoll.evaluate();
|
||||
@@ -106,6 +117,10 @@ export async function rest(actor: CastableActor, kind: RestKind): Promise<RestRe
|
||||
toleranceRecovered = 1;
|
||||
}
|
||||
} else {
|
||||
// Długi Odpoczynek resets the per-rest counters.
|
||||
update['system.attributes.breathersTaken'] = 0;
|
||||
update['system.attributes.manaBorrowed'] = 0;
|
||||
update['system.attributes.immediateActionsUsed'] = 0;
|
||||
if (a.health?.max != null && hpBefore < a.health.max) {
|
||||
update['system.attributes.health.value'] = a.health.max;
|
||||
hpRestored = a.health.max - hpBefore;
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Pure rule helpers from the 2026-10-20 vault sync (Foundry Sync Brief).
|
||||
* No Foundry globals here, so everything is unit-testable.
|
||||
*
|
||||
* Rounding convention: everything fractional rounds UP, except
|
||||
* - new-spell mana cost (nearest, min 1),
|
||||
* - Nadczarowanie bonus from level 3 (halved, DOWN, min 1),
|
||||
* - fire damage with Hartowany w Ogniu (halved, DOWN).
|
||||
*/
|
||||
|
||||
/** Odetchnięcie: at most twice between two Długie Odpoczynki. */
|
||||
export const MAX_BREATHERS_BETWEEN_LONG_RESTS = 2;
|
||||
|
||||
/** Pożyczanie Many (Wyczerpanie Many) – tier table, N d6 summed. */
|
||||
export const MANA_EXHAUSTION_TIERS = [
|
||||
{ min: 1, max: 4, id: 'zdyszanie', label: 'Zdyszanie' },
|
||||
{ min: 5, max: 8, id: 'zmeczenie', label: 'Zmęczenie' },
|
||||
{ min: 9, max: 12, id: 'wyczerpanie', label: 'Wyczerpanie' },
|
||||
{ min: 13, max: 16, id: 'zalamanie-sil', label: 'Załamanie Sił' },
|
||||
{ min: 17, max: 20, id: 'zapasc', label: 'Zapaść' },
|
||||
{ min: 21, max: 24, id: 'ciezka-zapasc', label: 'Ciężka Zapaść' },
|
||||
{ min: 25, max: Infinity, id: 'wypalenie', label: 'Wypalenie' },
|
||||
] as const;
|
||||
|
||||
export function manaExhaustionTier(total: number) {
|
||||
return MANA_EXHAUSTION_TIERS.find((t) => total >= t.min && total <= t.max) ?? MANA_EXHAUSTION_TIERS[0];
|
||||
}
|
||||
|
||||
/** Mana beyond the remaining pool that has to be "borrowed" for a cast. */
|
||||
export function manaToBorrow(cost: number, poolRemaining: number): number {
|
||||
return Math.max(0, cost - Math.max(0, poolRemaining));
|
||||
}
|
||||
|
||||
/** Number of d6 for the Wyczerpanie Many roll: cumulative borrowed mana incl. this cast. */
|
||||
export function exhaustionDice(borrowedSoFar: number, borrowedNow: number): number {
|
||||
return Math.max(0, borrowedSoFar) + Math.max(0, borrowedNow);
|
||||
}
|
||||
|
||||
/** Over-limit overcast: T by base cost, S = E(E+1)/2. */
|
||||
export function overLimitTest(baseCost: number, excess: number): { threshold: number; successes: number } {
|
||||
const threshold = baseCost <= 2 ? 4 : baseCost <= 4 ? 5 : 6;
|
||||
return { threshold, successes: (excess * (excess + 1)) / 2 };
|
||||
}
|
||||
|
||||
/** Nadczarowanie bonus multiplier: levels 1–2 full, level 3+ halved DOWN, min 1. */
|
||||
export function overcastBonus(level: number, perLevel: number): number {
|
||||
if (level <= 0) return 0;
|
||||
if (level <= 2) return perLevel * level;
|
||||
const full = perLevel * 2;
|
||||
const tapered = Math.max(1, Math.floor(perLevel / 2));
|
||||
return full + tapered * (level - 2);
|
||||
}
|
||||
|
||||
/** Halve a rolled dice-bonus result for level 3+ overcast (DOWN, min 1). */
|
||||
export function taperedDiceBonus(rolled: number): number {
|
||||
return Math.max(1, Math.floor(rolled / 2));
|
||||
}
|
||||
|
||||
/** Zasada Potężniejszych Zaklęć: nadwyżka = caster's Sukcesy − S (min 0). */
|
||||
export function spellSurplus(casterSuccesses: number, required: number): number {
|
||||
return Math.max(0, casterSuccesses - required);
|
||||
}
|
||||
export const defenderSuccessesNeeded = (surplus: number) => 1 + surplus;
|
||||
export const dispelSuccessesNeeded = (surplus: number, indestructible = false) =>
|
||||
2 + surplus + (indestructible ? 1 : 0);
|
||||
|
||||
/** Oszołomiony trigger: one hit costs at least half max Żywotność (after reductions). */
|
||||
export function triggersStun(healthLost: number, maxHealth: number): boolean {
|
||||
return maxHealth > 0 && healthLost > 0 && healthLost * 2 >= maxHealth;
|
||||
}
|
||||
|
||||
/** Koncentracja test: Dusza (Determinacja) 4:S, S = ⌈⅓ lost⌉, min 1. None if nothing lost. */
|
||||
export function concentrationTest(healthLost: number): { threshold: number; successes: number } | null {
|
||||
if (healthLost <= 0) return null;
|
||||
return { threshold: 4, successes: Math.max(1, Math.ceil(healthLost / 3)) };
|
||||
}
|
||||
|
||||
/** Unik: positive difference means dodge failed; damage uses difference + 2. */
|
||||
export function dodgeResult(attackerSuccesses: number, defenderSuccesses: number) {
|
||||
const diff = attackerSuccesses - defenderSuccesses;
|
||||
return { hit: diff > 0, damageSuccesses: diff > 0 ? diff + 2 : 0 };
|
||||
}
|
||||
|
||||
/** Parowanie (T−1): parried when parrier ≥ attacker − ⌈½ Walka Wręcz⌉. */
|
||||
export function parrySucceeds(parrierSuccesses: number, attackerSuccesses: number, melee: number): boolean {
|
||||
return parrierSuccesses >= attackerSuccesses - Math.ceil(melee / 2);
|
||||
}
|
||||
|
||||
/** Przewrócony: standing up costs half Ruch (rounded up). */
|
||||
export function standUpCost(movement: number): number {
|
||||
return Math.ceil(movement / 2);
|
||||
}
|
||||
|
||||
/** Spowolniony / Oszołomiony: Ruch halved, rounded up. */
|
||||
export function halvedMovement(movement: number): number {
|
||||
return Math.ceil(movement / 2);
|
||||
}
|
||||
|
||||
/** Szarża requires a straight Ruch of ≥ 4 m started outside the target's reach. */
|
||||
export const CHARGE_MIN_DISTANCE_M = 4;
|
||||
|
||||
/** Immediate Actions: 1 free per round; 1 Zapał buys two extra. Returns Zapał cost of the n-th action (1-based). */
|
||||
export function immediateActionZealCost(nth: number): number {
|
||||
if (nth <= 1) return 0;
|
||||
const extra = nth - 1;
|
||||
return Math.ceil(extra / 2);
|
||||
}
|
||||
|
||||
/** Total Zapał for n Immediate Actions in one round. */
|
||||
export function immediateActionsZealTotal(n: number): number {
|
||||
return n <= 1 ? 0 : Math.ceil((n - 1) / 2);
|
||||
}
|
||||
|
||||
/** Źródło Mocy mana-passing test: T by distance 4/5/6 for <1/<2/<3 m. */
|
||||
export function manaPassThreshold(distanceM: number): number | null {
|
||||
if (distanceM < 1) return 4;
|
||||
if (distanceM < 2) return 5;
|
||||
if (distanceM < 3) return 6;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Pomoc: regular mage contributes at most ⌈½ own Maksymalny Koszt⌉. */
|
||||
export function helpContributionCap(maxPerSpell: number, manaSource = false): number {
|
||||
return manaSource ? maxPerSpell : Math.ceil(maxPerSpell / 2);
|
||||
}
|
||||
|
||||
/** Wchłanianie Krwi: HP per Krew (2 base, 3 for Większe). */
|
||||
export function bloodAbsorbHp(blood: number, greater = false): number {
|
||||
return Math.max(0, blood) * (greater ? 3 : 2);
|
||||
}
|
||||
/** Większe Wchłanianie Krwi: 1 Zapał per 5 Krew. */
|
||||
export function bloodToZeal(blood: number): number {
|
||||
return Math.floor(Math.max(0, blood) / 5);
|
||||
}
|
||||
|
||||
/** Eliksir Odnowienia Many: +3 mana (capped by the pool); quick use = 1d3 as Immediate Action. */
|
||||
export const MANA_ELIXIR_NORMAL = 3;
|
||||
export const MANA_ELIXIR_QUICK_DIE = 3;
|
||||
export function manaElixirGain(current: number, max: number, amount: number): number {
|
||||
return Math.max(0, Math.min(amount, max - current));
|
||||
}
|
||||
|
||||
/** Magiczny Pocisk (and similar): damage that reaches Żywotność after Pancerz is at least 1. */
|
||||
export function minOneAfterArmor(damage: number, armor: number): number {
|
||||
return Math.max(1, damage - armor);
|
||||
}
|
||||
|
||||
/** Trade (Złoto, Stal i Magia). */
|
||||
export const TRADE = {
|
||||
companyFee: 5000,
|
||||
companyTest: { threshold: 4, successes: 3 },
|
||||
bribeTiers: [
|
||||
{ rate: 0.02, discount: 0.1 },
|
||||
{ rate: 0.05, discount: 0.2 },
|
||||
{ rate: 0.08, discount: 0.3 },
|
||||
],
|
||||
minBribe: 500,
|
||||
thresholdFloor: 2,
|
||||
negotiationSuccessesPerStep: 4,
|
||||
minigameFailureCost: 0.1,
|
||||
/** Unconfirmed (brief §16.3) – keep configurable. */
|
||||
eurRate: 4.36,
|
||||
} as const;
|
||||
|
||||
/** Negotiation bands for the 4:1 test: successes → step (1–2, 3–4, 5–6, 7+). */
|
||||
export function negotiationBand(successes: number): number {
|
||||
if (successes <= 0) return 0;
|
||||
return Math.min(4, Math.ceil(successes / 2));
|
||||
}
|
||||
|
||||
/** Przerażony: modifier on tests aimed at the fear source (T+1), 0 otherwise. */
|
||||
export function fearThresholdMod(sourceId: string | undefined, targetId: string | undefined): number {
|
||||
return sourceId && targetId && sourceId === targetId ? 1 : 0;
|
||||
}
|
||||
+43
-4
@@ -16,6 +16,7 @@
|
||||
|
||||
import { HbmTSRoll } from '../dice/ts-roll';
|
||||
import { TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD, getMagicPowerEntry } from '../constants';
|
||||
import { manaToBorrow, exhaustionDice, manaExhaustionTier, overLimitTest } from './rules';
|
||||
import { validateCast } from './spell-validation';
|
||||
import type { ValidationResult } from './spell-validation';
|
||||
import { rollSpellDamage } from './spell-damage';
|
||||
@@ -197,6 +198,8 @@ async function renderCastCard(
|
||||
})),
|
||||
type: spell.system.damageType ?? 'magical',
|
||||
ignoresArmor: !!spell.system.ignoresArmor,
|
||||
// Magiczny Pocisk: at least 1 damage reaches Żywotność after Pancerz.
|
||||
minAfterArmor: ((spell as any).flags?.['hbm-rpg-v3']?.slug ?? (spell.system as any).slug) === 'magic-missile',
|
||||
targets: targets.length > 0 ? targets : null,
|
||||
};
|
||||
}
|
||||
@@ -230,7 +233,6 @@ async function renderCastCard(
|
||||
rolls: rollsHtml,
|
||||
mode,
|
||||
school: spell.system.school ?? '',
|
||||
circle: (spell.system as any).circle ?? 0,
|
||||
outcome: { success },
|
||||
costs,
|
||||
damage,
|
||||
@@ -254,11 +256,17 @@ async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOp
|
||||
const manaSpent = Math.max(baseCost, Math.floor(opts.manaSpent ?? baseCost));
|
||||
const a = actor.system.attributes;
|
||||
|
||||
const updates: Record<string, any> = { 'system.attributes.mana.value': a.mana.value - manaSpent };
|
||||
// Casting beyond the remaining daily pool is allowed: the missing mana is
|
||||
// "borrowed" and triggers a Wyczerpanie Many roll (N d6, N = cumulative
|
||||
// borrowed mana since the last Długi Odpoczynek, incl. this cast).
|
||||
const borrowed = manaToBorrow(manaSpent, a.mana.value);
|
||||
const updates: Record<string, any> = { 'system.attributes.mana.value': Math.max(0, a.mana.value - manaSpent) };
|
||||
if (borrowed > 0) updates['system.attributes.manaBorrowed'] = (a.manaBorrowed ?? 0) + borrowed;
|
||||
if (opts.castAsPrayer) {
|
||||
updates['system.attributes.zeal.value'] = a.zeal.value - 1;
|
||||
}
|
||||
await actor.update(updates);
|
||||
if (borrowed > 0) await rollManaExhaustion(actor, exhaustionDice(a.manaBorrowed ?? 0, borrowed));
|
||||
|
||||
const magicDice = typeof a.magic.dicePool === 'number'
|
||||
? a.magic.dicePool
|
||||
@@ -284,8 +292,7 @@ async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOp
|
||||
// (E) over maxPerSpell: S = E * (E + 1) / 2 (Zasada Nadczarowywania Zaklęć).
|
||||
if (manaSpent > a.mana.maxPerSpell && a.mana.maxPerSpell > 0) {
|
||||
const excess = manaSpent - a.mana.maxPerSpell;
|
||||
const tThreshold = baseCost <= 2 ? 4 : baseCost <= 4 ? 5 : 6;
|
||||
const ySuccesses = (excess * (excess + 1)) / 2;
|
||||
const { threshold: tThreshold, successes: ySuccesses } = overLimitTest(baseCost, excess);
|
||||
const overcastFlavor = `${game.i18n.localize('HBM.spellCast.overcastTest')}: ${spell.name}`;
|
||||
const overcastRoll = HbmTSRoll.fromParams({
|
||||
pool,
|
||||
@@ -295,11 +302,43 @@ async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOp
|
||||
});
|
||||
await overcastRoll.evaluate();
|
||||
await recordRoll(rolls, overcastRoll, overcastFlavor);
|
||||
// Failure → Kara za Nieudolność: N d6, N = missing Sukcesy.
|
||||
const got = Number((overcastRoll as any).ts?.successes ?? 0);
|
||||
if (got < ySuccesses) await rollIncompetencePenalty(actor, ySuccesses - got, baseCost);
|
||||
}
|
||||
|
||||
return baseRoll;
|
||||
}
|
||||
|
||||
/** Wyczerpanie Many: roll N d6 and post the tier (tiers in Wyczerpanie Many.md). */
|
||||
async function rollManaExhaustion(actor: CastableActor, dice: number): Promise<void> {
|
||||
if (dice <= 0) return;
|
||||
const roll = new Roll(`${dice}d6`);
|
||||
await roll.evaluate();
|
||||
const tier = manaExhaustionTier(roll.total ?? 0);
|
||||
await ChatMessage.create({
|
||||
content: `<strong>${(actor as any).name}</strong>: ${game.i18n.localize('HBM.spellCast.manaExhaustion')} (${dice}k6 = ${roll.total}) → <strong>${tier.label}</strong>`,
|
||||
rolls: [roll.toJSON()],
|
||||
speaker: ChatMessage.getSpeaker({ actor: actor as any }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Kara za Nieudolność: N d6 where N = missing Sukcesy; tier 5–8 also drains the daily pool by the base cost. */
|
||||
async function rollIncompetencePenalty(actor: CastableActor, missing: number, baseCost: number): Promise<void> {
|
||||
const roll = new Roll(`${missing}d6`);
|
||||
await roll.evaluate();
|
||||
const total = roll.total ?? 0;
|
||||
if (total >= 5 && total <= 8) {
|
||||
const a = actor.system.attributes;
|
||||
await actor.update({ 'system.attributes.mana.value': Math.max(0, a.mana.value - baseCost) });
|
||||
}
|
||||
await ChatMessage.create({
|
||||
content: `<strong>${(actor as any).name}</strong>: ${game.i18n.localize('HBM.spellCast.incompetencePenalty')} (${missing}k6 = ${total})`,
|
||||
rolls: [roll.toJSON()],
|
||||
speaker: ChatMessage.getSpeaker({ actor: actor as any }),
|
||||
});
|
||||
}
|
||||
|
||||
async function castSacred(actor: CastableActor, spell: SpellLike, opts: CastOptions, rolls: CastRollEntry[]): Promise<HbmTSRoll | null> {
|
||||
const a = actor.system.attributes;
|
||||
const zealCost = Math.max(1, opts.zealSpent ?? 1);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* unless they map to a known machine effect ID.
|
||||
*/
|
||||
|
||||
import { SYSTEM_ID } from '../hbm';
|
||||
import { SYSTEM_ID } from '../constants';
|
||||
|
||||
export interface SpellTrigger {
|
||||
event: string;
|
||||
|
||||
@@ -100,7 +100,8 @@ export function validateCast(actor: CastableActor, spell: SpellLike, opts: CastO
|
||||
|
||||
if (mode === 'standard' || mode === 'witch' || (mode === 'sacred' && opts.hekateMode === 'witch')) {
|
||||
if (manaSpent > Number(a.mana?.value ?? 0)) {
|
||||
errors.push({ code: 'no-mana', message: 'Not enough mana', i18nKey: 'HBM.spellCast.notEnoughMana' });
|
||||
// Allowed since the 2026-10-20 sync: the shortfall is borrowed (Wyczerpanie Many).
|
||||
warnings.push({ code: 'borrow-mana', message: 'Not enough mana - the shortfall will be borrowed (Wyczerpanie Many)', i18nKey: 'HBM.spellCast.borrowMana' });
|
||||
}
|
||||
}
|
||||
if (opts.castAsPrayer) {
|
||||
|
||||
@@ -63,3 +63,4 @@ export async function smuggling(actor: CastableActor, params: TradeTestParams =
|
||||
}
|
||||
return roll;
|
||||
}
|
||||
export { TRADE, negotiationBand } from './rules';
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* (DataModel default usually handles this; explicit safety net).
|
||||
*/
|
||||
|
||||
import { SYSTEM_ID } from '../hbm';
|
||||
import { SYSTEM_ID } from '../constants';
|
||||
import type { MigrationStep } from './index';
|
||||
|
||||
interface LegacySpellSystem {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Migration to v1.4.0 (vault sync 2026-10-20).
|
||||
*
|
||||
* Operations:
|
||||
* - Backfill the new per-rest / per-round counters
|
||||
* (`manaBorrowed`, `breathersTaken`, `immediateActionsUsed`) on characters and NPCs.
|
||||
* - Rename the slug flag of embedded Zeus spells "aegis" → "aegis-zeus"
|
||||
* (the general Aegis spell keeps "aegis").
|
||||
*
|
||||
* Magic power levels III/IV swapped their daily mana (24/21) in constants only;
|
||||
* stored levels are unchanged and mana.max is derived, so no data change is needed.
|
||||
*/
|
||||
|
||||
import type { MigrationStep } from './index';
|
||||
|
||||
const FLAG_SCOPE = 'hbm-rpg-v3';
|
||||
|
||||
async function backfillCounters(): Promise<void> {
|
||||
for (const a of (game.actors?.contents ?? []) as any[]) {
|
||||
const attrs = a.system?.attributes;
|
||||
if (!attrs) continue;
|
||||
const update: Record<string, unknown> = {};
|
||||
if (attrs.manaBorrowed == null) update['system.attributes.manaBorrowed'] = 0;
|
||||
if (attrs.immediateActionsUsed == null) update['system.attributes.immediateActionsUsed'] = 0;
|
||||
if (a.type === 'character' && attrs.breathersTaken == null) update['system.attributes.breathersTaken'] = 0;
|
||||
if (Object.keys(update).length > 0) await a.update(update);
|
||||
}
|
||||
}
|
||||
|
||||
function isZeusAegis(item: any): boolean {
|
||||
return item?.type === 'spell'
|
||||
&& item.flags?.[FLAG_SCOPE]?.slug === 'aegis'
|
||||
&& item.system?.deity === 'zeus';
|
||||
}
|
||||
|
||||
async function renameZeusAegis(): Promise<void> {
|
||||
const fix = async (item: any) => {
|
||||
if (isZeusAegis(item)) await item.update({ [`flags.${FLAG_SCOPE}.slug`]: 'aegis-zeus' });
|
||||
};
|
||||
for (const it of (game.items?.contents ?? []) as any[]) await fix(it);
|
||||
for (const a of (game.actors?.contents ?? []) as any[]) {
|
||||
for (const it of a.items ?? []) await fix(it);
|
||||
}
|
||||
}
|
||||
|
||||
export const migration_1_4_0: MigrationStep = {
|
||||
version: '1.4.0',
|
||||
description: 'v1.4.0: borrowed-mana / breather / immediate-action counters, Zeus Aegis slug',
|
||||
async run() {
|
||||
await backfillCounters();
|
||||
await renameZeusAegis();
|
||||
},
|
||||
};
|
||||
@@ -6,9 +6,10 @@
|
||||
* (semver-style string). Migrations are idempotent.
|
||||
*/
|
||||
|
||||
import { SYSTEM_ID } from '../hbm';
|
||||
import { SYSTEM_ID } from '../constants';
|
||||
import { migration_0_2_0 } from './0.2.0';
|
||||
import { migration_1_2_0 } from './1.2.0';
|
||||
import { migration_1_4_0 } from './1.4.0';
|
||||
|
||||
export interface MigrationStep {
|
||||
version: string; // version this migration brings the world TO
|
||||
@@ -19,6 +20,7 @@ export interface MigrationStep {
|
||||
const MIGRATIONS: MigrationStep[] = [
|
||||
migration_0_2_0,
|
||||
migration_1_2_0,
|
||||
migration_1_4_0,
|
||||
];
|
||||
|
||||
function compareSemver(a: string, b: string): number {
|
||||
|
||||
@@ -78,7 +78,7 @@ export class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
.map(([school, items]) => ({
|
||||
school,
|
||||
label: game.i18n.localize(`HBM.spellSchool.${school}`),
|
||||
items: items.sort((a: any, b: any) => (a.system?.circle ?? 0) - (b.system?.circle ?? 0)),
|
||||
items: items.sort((a: any, b: any) => a.name.localeCompare(b.name, 'pl')),
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, 'pl'));
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
const ctx = (await super._prepareContext(options)) as Record<string, unknown>;
|
||||
const actor = (this as unknown as { actor: { system: any; items: any[] } }).actor;
|
||||
const spells = actor.items.filter((it: any) => it.type === 'spell')
|
||||
.sort((a: any, b: any) => (a.system?.circle ?? 0) - (b.system?.circle ?? 0) || a.name.localeCompare(b.name, 'pl'));
|
||||
.sort((a: any, b: any) => a.name.localeCompare(b.name, 'pl'));
|
||||
const actorEffects = [...((actor as any).effects ?? [])].map((ef: any) => ({
|
||||
id: ef.id,
|
||||
name: ef.name,
|
||||
|
||||
+2
-2
@@ -2,9 +2,9 @@
|
||||
"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.37",
|
||||
"version": "1.4.0",
|
||||
"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.37.zip",
|
||||
"download": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3-v1.4.0.zip",
|
||||
"compatibility": {
|
||||
"minimum": "13",
|
||||
"verified": "14"
|
||||
|
||||
@@ -275,14 +275,13 @@
|
||||
title="Wymagana rasa">🔒</span>{{/if}}
|
||||
</span>
|
||||
<span class="item-meta">
|
||||
K{{spell.system.circle}}
|
||||
· {{localize (concat 'HBM.spell.castingMode.' spell.system.castingMode)}}
|
||||
{{localize (concat 'HBM.spell.castingMode.' spell.system.castingMode)}}
|
||||
· {{localize 'HBM.resources.mana'}}: {{spell.system.manaCost}}
|
||||
{{#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}} · <span
|
||||
class="source-pip">{{spell.system.sourceBook}}</span>{{/if}}
|
||||
class="source-pip">{{localize (concat 'HBM.sourceBook.' spell.system.sourceBook)}}</span>{{/if}}
|
||||
</span>
|
||||
<div class="item-controls">
|
||||
<button type="button" class="cast-btn" data-action="castSpell" data-item-id="{{spell.id}}"
|
||||
|
||||
@@ -206,8 +206,7 @@
|
||||
<div class="row spell-row">
|
||||
<span class="badge mode mode-{{sp.system.castingMode}}"
|
||||
title="{{localize (concat 'HBM.spell.castingMode.' sp.system.castingMode)}}">{{sp.system.castingMode}}</span>
|
||||
<span class="spell-name"><strong>{{sp.name}}</strong>{{#if sp.system.circle}}
|
||||
<small>K{{sp.system.circle}}</small>{{/if}}</span>
|
||||
<span class="spell-name"><strong>{{sp.name}}</strong></span>
|
||||
<button type="button" data-action="castSpell" data-item-id="{{sp.id}}"
|
||||
title="{{localize 'HBM.spell.cast'}}">✦ {{localize 'HBM.spell.cast'}}</button>
|
||||
<button type="button" data-action="editItem" data-item-id="{{sp.id}}"
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<div class="hbm-card-meta">
|
||||
{{#if mode}}<span class="badge mode mode-{{mode}}">{{localize (concat 'HBM.spell.castingMode.' mode)}}</span>{{/if}}
|
||||
{{#if school}}<span class="badge school">{{localize (concat 'HBM.spellSchool.' school)}}</span>{{/if}}
|
||||
{{#if circle}}<span class="badge circle">{{localize 'HBM.spell.circle'}} {{circle}}</span>{{/if}}
|
||||
{{#if (or (eq mode "sacred") (eq mode "prayer"))}}
|
||||
<span class="badge warning">{{localize 'HBM.spellCast.cannotBeUnraveled'}}</span>
|
||||
{{/if}}
|
||||
@@ -52,7 +51,7 @@
|
||||
{{#if damage.targets}}
|
||||
<div class="hbm-card-targets">
|
||||
{{#each damage.targets as |t|}}
|
||||
<button type="button" class="hbm-action" data-action="hbm-apply-damage" data-target-uuid="{{t.uuid}}" data-amount="{{../damage.total}}" data-type="{{../damage.type}}" data-ignores-armor="{{../damage.ignoresArmor}}">
|
||||
<button type="button" class="hbm-action" data-action="hbm-apply-damage" data-target-uuid="{{t.uuid}}" data-amount="{{../damage.total}}" data-type="{{../damage.type}}" data-ignores-armor="{{../damage.ignoresArmor}}" data-min-after-armor="{{../damage.minAfterArmor}}">
|
||||
⚔ {{t.name}}
|
||||
</button>
|
||||
{{/each}}
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
<fieldset>
|
||||
<legend>{{localize 'HBM.spell.label'}}</legend>
|
||||
<div class="row">
|
||||
<label>{{localize 'HBM.spell.circle'}}:
|
||||
<input type="number" name="system.circle" value="{{system.circle}}" min="0" max="9" />
|
||||
</label>
|
||||
<label>{{localize 'HBM.spell.school'}}:
|
||||
<select name="system.school">
|
||||
{{#each @root.choices.schools as |label key|}}
|
||||
|
||||
@@ -30,7 +30,7 @@ function makeActor(overrides: Partial<any> = {}) {
|
||||
}
|
||||
|
||||
describe('rest', () => {
|
||||
it('take a breather restores all mana and 1d6 + endurance HP, leaves zeal/blood alone', async () => {
|
||||
it('take a breather restores 1d6 mana and 1d6 + endurance HP, leaves zeal/blood alone', async () => {
|
||||
const { actor, updates } = makeActor({
|
||||
attributes: {
|
||||
health: { value: 10, max: 30 },
|
||||
@@ -43,13 +43,21 @@ describe('rest', () => {
|
||||
const r = await rest(actor, 'breather');
|
||||
expect(r.kind).toBe('breather');
|
||||
expect(r.hpRestored).toBe(6); // 4 (stub roll default) + 2 (endurance) = 6
|
||||
expect(r.manaRestored).toBe(19);
|
||||
expect(r.manaRestored).toBe(4); // stub 1d6 = 4
|
||||
expect(updates['system.attributes.health.value']).toBe(16);
|
||||
expect(updates['system.attributes.mana.value']).toBe(20);
|
||||
expect(updates['system.attributes.mana.value']).toBe(5);
|
||||
expect(updates['system.attributes.zeal.value']).toBeUndefined();
|
||||
expect(updates['system.attributes.breathersTaken']).toBe(1);
|
||||
});
|
||||
|
||||
it('short rest restores all mana, 1/3 HP, and removes unconscious state', async () => {
|
||||
it('refuses a third breather between two long rests', async () => {
|
||||
const { actor, updates } = makeActor({ attributes: { breathersTaken: 2 } });
|
||||
const r = await rest(actor, 'breather');
|
||||
expect(r.blocked).toBe(true);
|
||||
expect(updates['system.attributes.health.value']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('short rest restores ⌈⅓⌉ of the mana pool, ⌈½⌉ HP, and removes unconscious state', async () => {
|
||||
const effects = [
|
||||
{ id: 'nieprzytomny', statuses: new Set(['nieprzytomny']) },
|
||||
{ id: 'przewrocony', statuses: new Set(['przewrocony']) },
|
||||
@@ -63,10 +71,10 @@ describe('rest', () => {
|
||||
});
|
||||
const r = await rest(actor, 'short');
|
||||
expect(r.kind).toBe('short');
|
||||
expect(r.hpRestored).toBe(10); // 30 / 3 = 10
|
||||
expect(r.manaRestored).toBe(19);
|
||||
expect(updates['system.attributes.health.value']).toBe(20);
|
||||
expect(updates['system.attributes.mana.value']).toBe(20);
|
||||
expect(r.hpRestored).toBe(15); // ⌈30 / 2⌉
|
||||
expect(r.manaRestored).toBe(7); // ⌈20 / 3⌉
|
||||
expect(updates['system.attributes.health.value']).toBe(25);
|
||||
expect(updates['system.attributes.mana.value']).toBe(8);
|
||||
expect(deletedEffects).toContain('nieprzytomny');
|
||||
expect(deletedEffects).not.toContain('przewrocony');
|
||||
expect(r.effectsCleared).toBe(1);
|
||||
@@ -97,6 +105,8 @@ describe('rest', () => {
|
||||
expect(updates['system.attributes.blood.value']).toBe(6);
|
||||
expect(updates['system.attributes.magicalArmor.runicCounter']).toBe(0);
|
||||
expect(r.toleranceRecovered).toBe(1);
|
||||
expect(updates['system.attributes.breathersTaken']).toBe(0);
|
||||
expect(updates['system.attributes.manaBorrowed']).toBe(0);
|
||||
|
||||
expect(deletedEffects).toContain('nieprzytomny');
|
||||
expect(deletedEffects).toContain('przewrocony');
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
manaExhaustionTier, manaToBorrow, exhaustionDice, overLimitTest, overcastBonus, taperedDiceBonus,
|
||||
spellSurplus, dispelSuccessesNeeded, triggersStun, concentrationTest, dodgeResult, parrySucceeds,
|
||||
standUpCost, immediateActionsZealTotal, manaPassThreshold, helpContributionCap, bloodAbsorbHp,
|
||||
bloodToZeal, manaElixirGain, negotiationBand,
|
||||
} from '../../src/logic/rules';
|
||||
import { getMagicPowerEntry } from '../../src/constants';
|
||||
|
||||
describe('magic power table', () => {
|
||||
it('has III=24 and IV=21 mana per day', () => {
|
||||
expect(getMagicPowerEntry(3).manaPerDay).toBe(24);
|
||||
expect(getMagicPowerEntry(4).manaPerDay).toBe(21);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Wyczerpanie Many', () => {
|
||||
it('maps tiers', () => {
|
||||
expect(manaExhaustionTier(3).id).toBe('zdyszanie');
|
||||
expect(manaExhaustionTier(8).id).toBe('zmeczenie');
|
||||
expect(manaExhaustionTier(12).id).toBe('wyczerpanie');
|
||||
expect(manaExhaustionTier(16).id).toBe('zalamanie-sil');
|
||||
expect(manaExhaustionTier(20).id).toBe('zapasc');
|
||||
expect(manaExhaustionTier(24).id).toBe('ciezka-zapasc');
|
||||
expect(manaExhaustionTier(25).id).toBe('wypalenie');
|
||||
});
|
||||
it('borrows only the shortfall and accumulates dice', () => {
|
||||
expect(manaToBorrow(5, 2)).toBe(3);
|
||||
expect(manaToBorrow(2, 5)).toBe(0);
|
||||
expect(exhaustionDice(4, 3)).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('overcast', () => {
|
||||
it('over-limit test T by base cost, S triangular', () => {
|
||||
expect(overLimitTest(2, 3)).toEqual({ threshold: 4, successes: 6 });
|
||||
expect(overLimitTest(4, 1)).toEqual({ threshold: 5, successes: 1 });
|
||||
expect(overLimitTest(6, 5)).toEqual({ threshold: 6, successes: 15 });
|
||||
});
|
||||
it('tapers from level 3 (halved down, min 1)', () => {
|
||||
expect(overcastBonus(2, 4)).toBe(8);
|
||||
expect(overcastBonus(3, 4)).toBe(10);
|
||||
expect(overcastBonus(3, 1)).toBe(3);
|
||||
expect(taperedDiceBonus(5)).toBe(2);
|
||||
expect(taperedDiceBonus(1)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Zasada Potężniejszych Zaklęć', () => {
|
||||
it('computes surplus and dispel needs', () => {
|
||||
expect(spellSurplus(5, 3)).toBe(2);
|
||||
expect(spellSurplus(2, 3)).toBe(0);
|
||||
expect(dispelSuccessesNeeded(2, true)).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('combat rules', () => {
|
||||
it('Oszołomiony at half max Żywotność', () => {
|
||||
expect(triggersStun(5, 10)).toBe(true);
|
||||
expect(triggersStun(4, 10)).toBe(false);
|
||||
expect(triggersStun(0, 0)).toBe(false);
|
||||
});
|
||||
it('Koncentracja S = ceil(lost/3), min 1', () => {
|
||||
expect(concentrationTest(0)).toBeNull();
|
||||
expect(concentrationTest(1)).toEqual({ threshold: 4, successes: 1 });
|
||||
expect(concentrationTest(7)).toEqual({ threshold: 4, successes: 3 });
|
||||
});
|
||||
it('Unik adds 2 to a positive difference', () => {
|
||||
expect(dodgeResult(3, 3)).toEqual({ hit: false, damageSuccesses: 0 });
|
||||
expect(dodgeResult(5, 2)).toEqual({ hit: true, damageSuccesses: 5 });
|
||||
});
|
||||
it('Parowanie tolerance is ceil(½ Walka Wręcz)', () => {
|
||||
expect(parrySucceeds(3, 5, 3)).toBe(true);
|
||||
expect(parrySucceeds(2, 5, 3)).toBe(false);
|
||||
});
|
||||
it('rounds movement up', () => {
|
||||
expect(standUpCost(5)).toBe(3);
|
||||
});
|
||||
it('1 Zapał buys two extra Immediate Actions', () => {
|
||||
expect([1, 2, 3, 4, 5].map(immediateActionsZealTotal)).toEqual([0, 1, 1, 2, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pomoc / Źródło Mocy', () => {
|
||||
it('caps contribution and sets pass threshold', () => {
|
||||
expect(helpContributionCap(5)).toBe(3);
|
||||
expect(helpContributionCap(5, true)).toBe(5);
|
||||
expect(manaPassThreshold(0.5)).toBe(4);
|
||||
expect(manaPassThreshold(2.5)).toBe(6);
|
||||
expect(manaPassThreshold(3)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('blood, elixirs, trade', () => {
|
||||
it('Wchłanianie Krwi 2 HP/Krew, Większe 3, 5:1 Zapał', () => {
|
||||
expect(bloodAbsorbHp(4)).toBe(8);
|
||||
expect(bloodAbsorbHp(4, true)).toBe(12);
|
||||
expect(bloodToZeal(12)).toBe(2);
|
||||
});
|
||||
it('mana elixir is capped by the pool', () => {
|
||||
expect(manaElixirGain(8, 10, 3)).toBe(2);
|
||||
});
|
||||
it('negotiation bands', () => {
|
||||
expect([0, 1, 2, 3, 4, 5, 6, 7, 12].map(negotiationBand)).toEqual([0, 1, 1, 2, 2, 3, 3, 4, 4]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { resolve } from 'node:path';
|
||||
import { parseSpells, deriveAreaOfEffect, deriveCastingTime } from '../../scripts/parsers/spell-parser';
|
||||
import { SOURCE_BOOKS } from '../../src/constants';
|
||||
|
||||
describe('spell-parser', () => {
|
||||
it('expands the flat vault frontmatter into a Foundry-loadable system payload', async () => {
|
||||
const ctx = { repoRoot: resolve(__dirname, '../../../..'), idOverrides: {}, strict: true } as any;
|
||||
const docs = await parseSpells(ctx);
|
||||
expect(docs.length).toBeGreaterThan(100);
|
||||
|
||||
const mm = docs.find((d) => d.id === 'magic-missile')!;
|
||||
const s = mm.system as any;
|
||||
expect(mm.name).toBe('Magiczny Pocisk');
|
||||
expect(s.sourceBook).toBe('ksiega-magii');
|
||||
expect(s.castingTime).toBe('Instant');
|
||||
expect(s.range).toBe('15 m');
|
||||
expect(s.targets).toBe('1');
|
||||
expect(s.damageBase).toBe('magicalAbilities/2');
|
||||
expect(s.damageType).toBe('magical');
|
||||
expect(s.difficulty).toEqual({ threshold: 4, successes: 1 });
|
||||
expect(s.overcastOptions).toEqual([{ description: '+1 punkt Obrażeń (najwyżej dwa Poziomy Nadczarowania)', manaPerStep: 1 }]);
|
||||
expect(s).not.toHaveProperty('circle');
|
||||
|
||||
for (const d of docs) {
|
||||
const sys = d.system as any;
|
||||
// No object leaks into string fields (the "[object Object]" bug).
|
||||
for (const k of ['range', 'targets', 'duration', 'castingTime', 'damageBase']) {
|
||||
expect(typeof sys[k], `${d.id}.${k}`).toBe('string');
|
||||
}
|
||||
for (const o of sys.overcastOptions) expect(o.description, d.id).toBeTruthy();
|
||||
expect(SOURCE_BOOKS as readonly string[], d.id).toContain(sys.sourceBook);
|
||||
}
|
||||
});
|
||||
|
||||
it('derives area of effect from the targets line', () => {
|
||||
expect(deriveAreaOfEffect('Kwadrat o boku 5 m')).toMatchObject({ shape: 'square', x: 5, y: 5 });
|
||||
expect(deriveAreaOfEffect('Prostokąt 3 m x 8 m. Dłuższy bok')).toMatchObject({ shape: 'rectangle', x: 3, y: 8 });
|
||||
expect(deriveAreaOfEffect('Sfera o średnicy 100 m')).toMatchObject({ shape: 'sphere', x: 50 });
|
||||
expect(deriveAreaOfEffect('1')).toMatchObject({ shape: 'point', x: 0 });
|
||||
});
|
||||
|
||||
it('derives casting time numbers', () => {
|
||||
expect(deriveCastingTime('Instant')).toEqual({ rounds: 0, minutes: 0 });
|
||||
expect(deriveCastingTime('2 godziny')).toEqual({ rounds: 0, minutes: 120 });
|
||||
expect(deriveCastingTime('10 minut')).toEqual({ rounds: 0, minutes: 10 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user