193 lines
7.3 KiB
TypeScript
193 lines
7.3 KiB
TypeScript
/**
|
|
* Rest logic.
|
|
*
|
|
* 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.
|
|
* - Restore one third of the daily mana pool.
|
|
* - Holders of `Nadzwyczajna Odporność` (alchemy passive) restore 1 elixir tolerance.
|
|
*
|
|
* Long Rest (Długi Odpoczynek):
|
|
* - Restore HP, mana, zeal to max.
|
|
* - Restore 1 elixir tolerance (or all, if `Nadzwyczajna Odporność`? - book says
|
|
* elixir tolerance recovers per long rest by default; tracked here as -1).
|
|
* - Reset blood pool to max (assumption - refine when AS spell list lands).
|
|
* - Clear runic counter on magical armor.
|
|
*
|
|
* Hooks `hbm.beforeRest` and `hbm.afterRest` fire for module integration.
|
|
*/
|
|
|
|
import type { CastableActor } from './spell-cast';
|
|
import { CONDITIONS } from './conditions';
|
|
import { MAX_BREATHERS_BETWEEN_LONG_RESTS } from './rules';
|
|
|
|
export type RestKind = 'breather' | 'short' | 'long';
|
|
|
|
declare const Hooks: any;
|
|
declare const Roll: any;
|
|
|
|
interface ActorWithItems extends CastableActor {
|
|
items?: Iterable<{ type: string; system?: { slug?: string }; flags?: any }>;
|
|
}
|
|
|
|
function hasExtraordinaryResilience(actor: ActorWithItems): boolean {
|
|
if (!actor.items) return false;
|
|
for (const it of actor.items) {
|
|
const slug = (it as any).system?.slug ?? (it as any).flags?.['hbm-rpg-v3']?.slug ?? '';
|
|
if (slug === 'extraordinary-resilience' || slug === 'nadzwyczajna-odpornosc') return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export interface RestResult {
|
|
kind: RestKind;
|
|
hpRestored: number;
|
|
manaRestored: number;
|
|
zealRestored: number;
|
|
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> {
|
|
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;
|
|
const manaBefore = a.mana?.value ?? 0;
|
|
const zealBefore = a.zeal?.value ?? 0;
|
|
const bloodBefore = a.blood?.value ?? 0;
|
|
const toleranceBefore = a.elixirTolerance ?? 0;
|
|
|
|
let hpRestored = 0;
|
|
let manaRestored = 0;
|
|
let zealRestored = 0;
|
|
let bloodRestored = 0;
|
|
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();
|
|
const manaGain = Math.min(manaRoll.total, a.mana.max - manaBefore);
|
|
if (manaGain > 0) {
|
|
update['system.attributes.mana.value'] = manaBefore + manaGain;
|
|
manaRestored = manaGain;
|
|
}
|
|
}
|
|
const endurance = actor.system.skills?.endurance?.value ?? 0;
|
|
const roll = new Roll('1d6 + @endurance', { endurance });
|
|
await roll.evaluate();
|
|
const rollTotal = roll.total;
|
|
const heal = Math.min(rollTotal, (a.health?.max ?? 0) - hpBefore);
|
|
if (heal > 0) {
|
|
update['system.attributes.health.value'] = hpBefore + heal;
|
|
hpRestored = heal;
|
|
}
|
|
} else if (kind === 'short') {
|
|
if (a.mana?.max != null && manaBefore < a.mana.max) {
|
|
const manaGain = Math.min(Math.ceil(a.mana.max / 3), a.mana.max - manaBefore);
|
|
if (manaGain > 0) {
|
|
update['system.attributes.mana.value'] = manaBefore + manaGain;
|
|
manaRestored = manaGain;
|
|
}
|
|
}
|
|
const healAmount = Math.ceil((a.health?.max ?? 0) / 2);
|
|
const heal = Math.min(healAmount, (a.health?.max ?? 0) - hpBefore);
|
|
if (heal > 0) {
|
|
update['system.attributes.health.value'] = hpBefore + heal;
|
|
hpRestored = heal;
|
|
}
|
|
if (hasExtraordinaryResilience(actor as ActorWithItems) && toleranceBefore > 0) {
|
|
update['system.attributes.elixirTolerance'] = toleranceBefore - 1;
|
|
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;
|
|
}
|
|
if (a.mana?.max != null && manaBefore < a.mana.max) {
|
|
update['system.attributes.mana.value'] = a.mana.max;
|
|
manaRestored = a.mana.max - manaBefore;
|
|
}
|
|
if (a.zeal?.max != null && zealBefore < a.zeal.max) {
|
|
update['system.attributes.zeal.value'] = a.zeal.max;
|
|
zealRestored = a.zeal.max - zealBefore;
|
|
}
|
|
if (a.blood?.max != null && bloodBefore < a.blood.max) {
|
|
update['system.attributes.blood.value'] = a.blood.max;
|
|
bloodRestored = a.blood.max - bloodBefore;
|
|
}
|
|
if (toleranceBefore > 0) {
|
|
update['system.attributes.elixirTolerance'] = Math.max(0, toleranceBefore - 1);
|
|
toleranceRecovered = 1;
|
|
}
|
|
// Disabled Runiczny Pancerz points come back (only Rozerwanie Run is permanent).
|
|
if (a.magicalArmor && a.magicalArmor.value < a.magicalArmor.max) {
|
|
update['system.attributes.magicalArmor.value'] = a.magicalArmor.max;
|
|
}
|
|
if (a.greatSacrificeUsed) update['system.attributes.greatSacrificeUsed'] = 0;
|
|
if (a.bloodUsed) {
|
|
for (const k of ['selfHarm', 'lifeSteal', 'sacrifice', 'absorb']) update[`system.attributes.bloodUsed.${k}`] = 0;
|
|
}
|
|
}
|
|
|
|
if (Object.keys(update).length > 0) await actor.update(update);
|
|
|
|
let effectsCleared = 0;
|
|
const actorAny = actor as unknown as { effects?: any; deleteEmbeddedDocuments?: (t: string, ids: string[]) => Promise<unknown> };
|
|
const ids: string[] = [];
|
|
|
|
if (kind === 'short') {
|
|
for (const ef of actorAny.effects ?? []) {
|
|
const isUnconscious = ef.statuses?.has('nieprzytomny') || ef.flags?.core?.statusId === 'nieprzytomny' || ef.id === 'nieprzytomny';
|
|
if (isUnconscious) ids.push(ef.id);
|
|
}
|
|
} else if (kind === 'long') {
|
|
const conditionIds = new Set(CONDITIONS.map(c => c.id));
|
|
for (const ef of actorAny.effects ?? []) {
|
|
const isState = conditionIds.has(ef.id) ||
|
|
(ef.statuses && [...ef.statuses].some(s => conditionIds.has(s))) ||
|
|
conditionIds.has(ef.flags?.core?.statusId);
|
|
|
|
if (isState) {
|
|
ids.push(ef.id);
|
|
continue;
|
|
}
|
|
|
|
if (ef.origin) continue;
|
|
const dur = ef.duration ?? {};
|
|
const isTemporary = (dur.seconds ?? 0) > 0 || (dur.rounds ?? 0) > 0 || (dur.turns ?? 0) > 0 || ef.flags?.['hbm-rpg-v3']?.untilLongRest === true;
|
|
if (isTemporary) ids.push(ef.id);
|
|
}
|
|
}
|
|
|
|
if (ids.length > 0 && actorAny.deleteEmbeddedDocuments) {
|
|
await actorAny.deleteEmbeddedDocuments('ActiveEffect', ids);
|
|
effectsCleared = ids.length;
|
|
}
|
|
|
|
const result: RestResult = { kind, hpRestored, manaRestored, zealRestored, bloodRestored, toleranceRecovered, effectsCleared };
|
|
Hooks.callAll('hbm.afterRest', actor, result);
|
|
return result;
|
|
}
|