Initial commit

This commit is contained in:
Octoturge
2026-06-09 22:06:56 +02:00
commit fb42c6e8cc
121 changed files with 14976 additions and 0 deletions
+303
View File
@@ -0,0 +1,303 @@
import {
makeIntField,
makeStringField,
makeHtmlField,
makeValueMaxField,
makeArrayField,
fields,
} from './fields';
import { ATTRIBUTES, SKILL_KEYS, SKILLS, AttributeKey, getMagicPowerEntry } from '../constants';
/**
* Player Character data model.
*
* Derived stats (computed in prepareDerivedData):
* - attributes.health.max = 3 * (body + mind + soul)
* - attributes.zeal.max = ceil(soul / 2)
* - attributes.initiative = mind + skills.reflex.value + skills.perception.value
* - attributes.mana.max / .maxPerSpell / magic.dicePool — from MAGIC_POWER_TABLE
*/
export class CharacterData extends foundry.abstract.TypeDataModel {
static defineSchema() {
const f = fields();
const attributeSchema = () => new f.SchemaField({
value: makeIntField(1, { min: 1, max: 8 }),
});
const skillEntries: Record<string, foundry.data.fields.DataField.Any> = {};
for (const key of SKILL_KEYS) {
skillEntries[key] = new f.SchemaField({
value: makeIntField(0, { min: 0, max: 8 }),
defaultAttribute: makeStringField(SKILLS[key] as AttributeKey, {
blank: false,
choices: ATTRIBUTES as unknown as readonly string[],
}),
});
}
const scheduleRows: Record<string, foundry.data.fields.DataField.Any> = {};
for (let i = 1; i <= 12; i++) {
scheduleRows[`row${i}`] = new f.SchemaField({
hours: makeStringField('', { blank: true }),
mon: makeStringField('', { blank: true }),
tue: makeStringField('', { blank: true }),
wed: makeStringField('', { blank: true }),
thu: makeStringField('', { blank: true }),
fri: makeStringField('', { blank: true }),
sat: makeStringField('', { blank: true }),
});
}
return {
attributes: new f.SchemaField({
body: attributeSchema(),
mind: attributeSchema(),
soul: attributeSchema(),
magic: new f.SchemaField({
value: makeIntField(0, { min: 0, max: 10 }),
actual: makeIntField(0, { min: 0, max: 10 }), // derived
dicePool: makeIntField(0, { min: 0 }), // derived
}),
health: makeValueMaxField(0, 0),
mana: new f.SchemaField({
value: makeIntField(0, { min: 0 }),
max: makeIntField(0, { min: 0 }),
maxPerSpell: makeIntField(0, { min: 0 }),
}),
zeal: makeValueMaxField(0, 0),
// Blood Pool (Arcanum Sanguinis Ch. III) — experimental until full spell list ships.
// Placeholder formula: max = body + soul.
blood: makeValueMaxField(0, 0),
// Elixir tolerance (Podręcznik Gry — brewing). Each potion consumed adds 1;
// recovers 1 per long rest. Soft cap = body + 1; over cap → poisoning.
elixirTolerance: makeIntField(0, { min: 0 }),
// Insanity points (Klątwa Otchłani VIII). Each Abyss exposure may add 1; rolls
// mental-condition table when reaching thresholds.
insanity: makeIntField(0, { min: 0 }),
magicalArmor: new f.SchemaField({
value: makeIntField(0, { min: 0 }),
max: makeIntField(0, { min: 0 }),
runicCounter: makeIntField(0, { min: 0 }),
}),
magicalShield: new f.SchemaField({
value: makeIntField(0, { min: 0 }),
}),
physicalArmor: new f.SchemaField({
value: makeIntField(0, { min: 0 }), // derived
max: makeIntField(0, { min: 0 }), // derived: base DR from equipment
bonus: makeIntField(0, { min: 0 }), // editable bonus (flat +DR)
condition: makeIntField(0, { min: 0 }), // derived: sum of armor conditions
conditionMax: makeIntField(0, { min: 0 }), // derived: sum of armor conditionMax
}),
initiative: makeIntField(0, { min: 0 }),
}),
skills: new f.SchemaField(skillEntries),
details: new f.SchemaField({
race: makeStringField('', { blank: true }),
raceId: makeStringField('', { blank: true }),
classIds: makeArrayField(new f.StringField({ blank: true })),
year: makeIntField(1, { min: 1, max: 4 }),
discipline: makeStringField('', { blank: true }),
title: makeStringField('', { blank: true }),
biography: makeHtmlField(''),
customEquipment: makeHtmlField(''), // legacy textarea — kept for migration
customItems: makeArrayField(new f.SchemaField({
name: makeStringField('', { blank: true }),
qty: makeIntField(1, { min: 0 }),
note: makeStringField('', { blank: true }),
})),
experience: makeIntField(0, { min: 0 }),
money: makeIntField(0, { min: 0 }),
classSchedule: makeStringField('', { blank: true }),
schedule: new f.SchemaField(scheduleRows),
personalDetails: makeHtmlField(''),
startingYear: makeIntField(2026, { min: 1900 }),
currentYear: makeIntField(2026, { min: 1900 }),
}),
advancement: new f.SchemaField({
attributePointsAvailable: makeIntField(0, { min: 0 }),
skillPointsAvailable: makeIntField(0, { min: 0 }),
freeTalentsAvailable: makeIntField(0, { min: 0 }), // derived
bonusAttributePoints: makeIntField(0, { min: 0 }),
bonusSkillPoints: makeIntField(0, { min: 0 }),
bonusFreeTalents: makeIntField(0, { min: 0 }),
}),
};
}
/** prepareDerivedData runs after source data loads; compute formulas here. */
override prepareDerivedData() {
const self = this as unknown as { parent?: { items?: Iterable<any> } } & Record<string, any>;
const sys = this as unknown as {
attributes: {
body: { value: number };
mind: { value: number };
soul: { value: number };
magic: { value: number; actual: number; dicePool: number };
health: { value: number; max: number };
mana: { value: number; max: number; maxPerSpell: number };
zeal: { value: number; max: number };
blood: { value: number; max: number };
elixirTolerance: number;
insanity: number;
magicalArmor: { value: number; max: number; runicCounter: number };
magicalShield: { value: number };
physicalArmor: { value: number; max: number; condition: number; conditionMax: number };
initiative: number;
};
skills: Record<string, { value: number; defaultAttribute: AttributeKey }>;
details: { raceId?: string; classIds?: string[] };
advancement: {
attributePointsAvailable: number;
skillPointsAvailable: number;
freeTalentsAvailable: number;
bonusAttributePoints: number;
bonusSkillPoints: number;
bonusFreeTalents: number;
};
};
const a = sys.attributes;
a.health.max = 3 * (a.body.value + a.mind.value + a.soul.value);
if (a.health.value > a.health.max) a.health.value = a.health.max;
a.zeal.max = Math.ceil(a.soul.value / 2);
if (a.zeal.value > a.zeal.max) a.zeal.value = a.zeal.max;
// Compute physical armor DR from equipped gear + talents
const items = (self.parent?.items ? Array.from(self.parent.items as Iterable<any>) : []) as any[];
// Calculate actual magic circle based on talents
let magicMod = 0;
for (const it of items) {
if (it.type === 'talent') {
const slug = it.flags?.['hbm-rpg-v3']?.slug || '';
const name = it.name?.toLowerCase() || '';
if (slug === 'jednosc-z-magia' || slug === 'unity-with-magic' || name === 'jedność z magią' || name === 'unity with magic') {
magicMod -= 1;
} else if (slug === 'odpornosc-na-magie' || slug === 'magic-resistance' || name === 'odporność na magię' || name === 'magic resistance') {
magicMod += 1;
}
}
}
const actualMagic = a.magic.value + magicMod;
a.magic.actual = Math.max(0, Math.min(10, actualMagic));
const mp = getMagicPowerEntry(a.magic.actual);
a.magic.dicePool = mp.dicePool;
a.mana.max = mp.manaPerRound;
a.mana.maxPerSpell = mp.maxPerSpell;
if (a.mana.value > a.mana.max) a.mana.value = a.mana.max;
// Count battle-readiness stacks for initiative bonus (+2 per copy)
let initiativeBonus = 0;
// Count Man of Iron copies for passive armor (only applies when no physical armor is worn)
let manOfIronStacks = 0;
for (const it of items) {
if (it.type === 'talent') {
const slug = it.flags?.['hbm-rpg-v3']?.slug || '';
const name = it.name?.toLowerCase() || '';
if (
slug === 'battle-readiness' || slug === 'gotowosc-do-walki' ||
name === 'gotowo\u015b\u0107 do walki' || name === 'battle readiness'
) {
initiativeBonus += 2;
}
if (
slug === 'man-of-iron' || slug === 'czlowiek-z-zelaza' ||
name === 'cz\u0142owiek z \u017celaza' || name === 'man of iron'
) {
manOfIronStacks += 1;
}
}
}
a.initiative =
a.mind.value +
(sys.skills.reflex?.value ?? 0) +
(sys.skills.perception?.value ?? 0) +
initiativeBonus;
// Clamp magical armor / shield
if (a.magicalArmor.value > a.magicalArmor.max) a.magicalArmor.value = a.magicalArmor.max;
let armorDR = 0;
let armorPieces = 0;
let totalArmorCondition = 0;
let totalArmorConditionMax = 0;
// Compute effective DR per piece using condition scaling
let effectiveDR = 0;
for (const it of items) {
if (it.type === 'gear') {
const g = it.system;
if (!g) continue;
if (g.equipped && g.category === 'armor') {
const baseDR = Number(g.armor?.damageReduction ?? 0);
const cond = Number(g.armor?.condition ?? 0);
const condMax = Number(g.armor?.conditionMax ?? 1);
armorDR += baseDR;
armorPieces += 1;
totalArmorCondition += cond;
totalArmorConditionMax += condMax;
// Condition-scaled DR: broken armor (cond=0) gives 0 DR
if (condMax > 0) {
effectiveDR += Math.floor(baseDR * (cond / condMax));
}
}
}
}
// physicalArmorBonus is stored/editable; add it to effective DR
const paBonus = (sys as any).attributes?.physicalArmor?.bonus ?? 0;
a.physicalArmor.max = Math.max(0, armorDR + paBonus);
a.physicalArmor.value = Math.max(0, effectiveDR + paBonus);
if (armorPieces > 0) {
a.physicalArmor.condition = totalArmorCondition;
a.physicalArmor.conditionMax = totalArmorConditionMax;
}
// Man of Iron: +1 passive armor per stack, only when wearing no physical armor
if (manOfIronStacks > 0 && armorPieces === 0) {
a.physicalArmor.max = Math.max(a.physicalArmor.max, manOfIronStacks);
a.physicalArmor.value = Math.max(a.physicalArmor.value, manOfIronStacks);
}
// Blood Pool (placeholder formula): max = body + soul.
a.blood.max = a.body.value + a.soul.value;
if (a.blood.value > a.blood.max) a.blood.value = a.blood.max;
// Advancement points (UUID-based race/class lookup; falls back to 0 if not set)
const raceItem = items.find((i) => i.type === 'race');
const classItems = items.filter((i) => i.type === 'class');
const startingAttr = Number(raceItem?.system?.attributePoints ?? 0);
const startingSkill = Number(raceItem?.system?.skillPoints ?? 0);
const startingFreeTalents = raceItem?.system?.freeTalents?.length ?? 0;
const classAttr = classItems.reduce((sum, c) => sum + Number(c.system?.attributePoints ?? 0), 0);
const classSkill = classItems.reduce((sum, c) => sum + Number(c.system?.skillPoints ?? 0), 0);
const spentAttr = (a.body.value - 1) + (a.mind.value - 1) + (a.soul.value - 1) + a.magic.value;
const spentSkill = Object.values(sys.skills).reduce((sum, s) => sum + (s.value ?? 0), 0);
const spentFreeTalents = items.filter((i) => i.type === 'talent').length;
sys.advancement.attributePointsAvailable = Math.max(0, startingAttr + classAttr + (sys.advancement.bonusAttributePoints ?? 0) - spentAttr);
sys.advancement.skillPointsAvailable = Math.max(0, startingSkill + classSkill + (sys.advancement.bonusSkillPoints ?? 0) - spentSkill);
sys.advancement.freeTalentsAvailable = Math.max(0, startingFreeTalents + (sys.advancement.bonusFreeTalents ?? 0) - spentFreeTalents);
}
/**
* Migration shim: map legacy `attributes.armor` (single number) to new
* `physicalArmor.value` so existing characters do not crash on load.
*/
_initializeSource(data: any, options: any): any {
const initialized = super._initializeSource(data, options);
if (initialized?.attributes && typeof initialized.attributes.armor === 'number') {
const legacy = initialized.attributes.armor;
if (!initialized.attributes.physicalArmor) {
initialized.attributes.physicalArmor = { value: legacy, condition: 1, conditionMax: 1 };
}
delete initialized.attributes.armor;
}
return initialized;
}
}
+140
View File
@@ -0,0 +1,140 @@
import {
makeIntField,
makeStringField,
makeHtmlField,
makeValueMaxField,
makeArrayField,
fields,
} from './fields';
import { getMagicPowerEntry } from '../constants';
/**
* NPC / Creature data model.
* Looser than character (no derived health formula — set directly).
*/
export class NpcData extends foundry.abstract.TypeDataModel {
static defineSchema() {
const f = fields();
const attackSchema = () => new f.SchemaField({
name: makeStringField(''),
description: makeStringField(''),
damage: makeStringField(''),
damageType: makeStringField('physical'),
bonus: makeIntField(0),
});
const namedEntry = () => new f.SchemaField({
name: makeStringField(''),
description: makeStringField(''),
});
return {
attributes: new f.SchemaField({
body: new f.SchemaField({ value: makeIntField(1, { min: 1, max: 12 }) }),
mind: new f.SchemaField({ value: makeIntField(1, { min: 1, max: 12 }) }),
soul: new f.SchemaField({ value: makeIntField(1, { min: 1, max: 12 }) }),
magic: new f.SchemaField({
value: makeIntField(0, { min: 0, max: 10 }),
actual: makeIntField(0, { min: 0, max: 10 }), // derived
}),
mana: new f.SchemaField({
value: makeIntField(0, { min: 0 }),
max: makeIntField(0, { min: 0 }),
maxPerSpell: makeIntField(0, { min: 0 }),
}),
zeal: makeValueMaxField(0, 0),
blood: makeValueMaxField(0, 0),
health: makeValueMaxField(1, 1),
magicalArmor: makeValueMaxField(0, 0),
magicalShield: makeValueMaxField(0, 0),
physicalArmor: new f.SchemaField({ value: makeIntField(0, { min: 0 }) }),
speed: makeStringField('5m'),
}),
details: new f.SchemaField({
type: makeStringField(''),
size: makeStringField('medium'),
alignment: makeStringField(''),
xp: makeIntField(0, { min: 0 }),
senses: makeStringField(''),
languages: makeStringField(''),
customEquipment: makeHtmlField(''),
money: makeIntField(0, { min: 0 }),
startingYear: makeIntField(2026, { min: 1900 }),
currentYear: makeIntField(2026, { min: 1900 }),
}),
combat: new f.SchemaField({
attacks: makeArrayField(attackSchema()),
specialAbilities: makeArrayField(namedEntry()),
reactions: makeArrayField(namedEntry()),
legendaryActions: makeArrayField(namedEntry()),
}),
defenses: new f.SchemaField({
savingThrows: makeArrayField(new f.StringField({ blank: false })),
damageResistances: makeArrayField(new f.StringField({ blank: false })),
damageImmunities: makeArrayField(new f.StringField({ blank: false })),
conditionImmunities: makeArrayField(new f.StringField({ blank: false })),
}),
lore: new f.SchemaField({
description: makeHtmlField(''),
habitat: makeStringField(''),
behavior: makeStringField(''),
history: makeStringField(''),
}),
};
}
override prepareDerivedData() {
const self = this as unknown as { parent?: { items?: Iterable<any> } } & Record<string, any>;
const sys = this as unknown as {
attributes: {
magic: { value: number; actual: number };
mana: { value: number; max: number; maxPerSpell: number };
health: { value: number; max: number };
magicalArmor: { value: number; max: number };
magicalShield: { value: number; max: number };
};
};
const a = sys.attributes;
// Compute magic.actual based on NPC talents
const items = (self.parent?.items ? Array.from(self.parent.items as Iterable<any>) : []) as any[];
let magicMod = 0;
for (const it of items) {
if (it.type === 'talent') {
const slug = it.flags?.['hbm-rpg-v3']?.slug || '';
const name = it.name?.toLowerCase() || '';
if (slug === 'jednosc-z-magia' || slug === 'unity-with-magic' || name === 'jedność z magią' || name === 'unity with magic') {
magicMod -= 1;
} else if (slug === 'odpornosc-na-magie' || slug === 'magic-resistance' || name === 'odporność na magię' || name === 'magic resistance') {
magicMod += 1;
}
}
}
const actualMagic = a.magic.value + magicMod;
a.magic.actual = Math.max(0, Math.min(10, actualMagic));
// Derive NPC mana from MAGIC_POWER_TABLE
const mp = getMagicPowerEntry(a.magic.actual);
a.mana.max = mp.manaPerRound;
a.mana.maxPerSpell = mp.maxPerSpell;
if (a.mana.value > a.mana.max) a.mana.value = a.mana.max;
if (a.health.value > a.health.max) a.health.value = a.health.max;
if (a.magicalArmor.value > a.magicalArmor.max) a.magicalArmor.value = a.magicalArmor.max;
if (a.magicalShield.value > a.magicalShield.max) a.magicalShield.value = a.magicalShield.max;
}
/** Migrate legacy `attributes.armor` → `physicalArmor.value` */
_initializeSource(data: any, options: any): any {
const initialized = super._initializeSource(data, options);
if (initialized?.attributes && typeof initialized.attributes.armor === 'number') {
const legacy = initialized.attributes.armor;
if (!initialized.attributes.physicalArmor) {
initialized.attributes.physicalArmor = { value: legacy };
}
delete initialized.attributes.armor;
}
return initialized;
}
}
+53
View File
@@ -0,0 +1,53 @@
/**
* Shared DataModel field helpers. Foundry's data model field constructors
* live under foundry.data.fields at runtime; we re-export local aliases
* to keep the data model files terse.
*/
// Foundry exposes fields globally; declared via fvtt-types.
export const fields = (): typeof foundry.data.fields => foundry.data.fields;
export function makeIntField(initial = 0, options: Partial<{ min: number; max: number; nullable: boolean }> = {}) {
const f = fields();
return new f.NumberField({
required: true,
nullable: options.nullable ?? false,
integer: true,
initial,
min: options.min,
max: options.max,
});
}
export function makeStringField(initial = '', options: Partial<{ blank: boolean; choices: readonly string[] }> = {}) {
const f = fields();
return new f.StringField({
required: true,
blank: options.blank ?? true,
initial,
choices: options.choices as string[] | undefined,
});
}
export function makeBoolField(initial = false) {
const f = fields();
return new f.BooleanField({ required: true, initial });
}
export function makeHtmlField(initial = '') {
const f = fields();
return new f.HTMLField({ required: true, blank: true, initial });
}
export function makeArrayField(element: foundry.data.fields.DataField.Any) {
const f = fields();
return new f.ArrayField(element);
}
export function makeValueMaxField(initial = 0, max = 0) {
const f = fields();
return new f.SchemaField({
value: makeIntField(initial, { min: 0 }),
max: makeIntField(max, { min: 0 }),
});
}
+19
View File
@@ -0,0 +1,19 @@
import { makeStringField, makeHtmlField, makeIntField } from './fields';
import { ABILITY_TYPES } from '../constants';
export class AbilityData extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
type: makeStringField('passive', {
blank: false,
choices: ABILITY_TYPES as unknown as readonly string[],
}),
prerequisite: makeStringField(''),
cost: makeStringField(''),
manaCost: makeIntField(0, { min: 0 }),
zealCost: makeIntField(0, { min: 0 }),
description: makeHtmlField(''),
mechanics: makeHtmlField(''),
};
}
}
+21
View File
@@ -0,0 +1,21 @@
import { makeIntField, makeStringField, makeHtmlField, makeArrayField, fields } from './fields';
export class ClassData extends foundry.abstract.TypeDataModel {
static defineSchema() {
const f = fields();
return {
year: makeIntField(1, { min: 1, max: 4 }),
attributePoints: makeIntField(0, { min: 0 }),
skillPoints: makeIntField(0, { min: 0 }),
talents: makeArrayField(new f.StringField({ blank: false })),
spells: makeArrayField(new f.StringField({ blank: false })),
features: makeArrayField(
new f.SchemaField({
name: makeStringField(''),
description: makeStringField(''),
}),
),
description: makeHtmlField(''),
};
}
}
+19
View File
@@ -0,0 +1,19 @@
import { makeStringField, makeHtmlField, makeArrayField, fields } from './fields';
import { ATTRIBUTES } from '../constants';
export class DisciplineData extends foundry.abstract.TypeDataModel {
static defineSchema() {
const f = fields();
return {
color: makeStringField(''),
suggestedAttribute: makeStringField('magic', {
blank: false,
choices: ATTRIBUTES as unknown as readonly string[],
}),
suggestedSkills: makeArrayField(new f.StringField({ blank: false })),
passiveAbility: makeHtmlField(''),
availableSpells: makeArrayField(new f.StringField({ blank: false })),
description: makeHtmlField(''),
};
}
}
+67
View File
@@ -0,0 +1,67 @@
import {
makeIntField,
makeStringField,
makeBoolField,
makeHtmlField,
makeArrayField,
fields,
} from './fields';
import { GEAR_CATEGORIES, DAMAGE_TYPES, ARMOR_TYPES, RARITIES } from '../constants';
/**
* Unified Gear item — discriminated by `category`.
* Weapon/armor/equipment all share base fields; specialised fields
* are scoped under `weapon` and `armor` sub-schemas.
*/
export class GearData extends foundry.abstract.TypeDataModel {
static defineSchema() {
const f = fields();
return {
category: makeStringField('equipment', {
blank: false,
choices: GEAR_CATEGORIES as unknown as readonly string[],
}),
rarity: makeStringField('common', {
blank: false,
choices: RARITIES as unknown as readonly string[],
}),
quantity: makeIntField(1, { min: 0 }),
weight: makeIntField(0, { min: 0 }),
value: makeIntField(0, { min: 0 }),
equipped: makeBoolField(false),
damageReductionBonus: makeIntField(0, { min: 0 }),
description: makeHtmlField(''),
weapon: new f.SchemaField({
damage: makeStringField(''),
damageType: makeStringField('physical', {
blank: false,
choices: DAMAGE_TYPES as unknown as readonly string[],
}),
properties: makeArrayField(new f.StringField({ blank: false })),
}),
armor: new f.SchemaField({
damageReduction: makeIntField(0, { min: 0 }),
condition: makeIntField(1, { min: 0 }),
conditionMax: makeIntField(1, { min: 0 }),
armorType: makeStringField('light', {
blank: false,
choices: ARMOR_TYPES as unknown as readonly string[],
}),
stealthDisadvantage: makeBoolField(false),
strengthRequirement: makeIntField(0, { min: 0 }),
}),
};
}
/** Migrate legacy `armor.armorClass` → `armor.damageReduction`. */
_initializeSource(data: any, options: any): any {
const initialized = super._initializeSource(data, options);
if (initialized?.armor && typeof initialized.armor.armorClass === 'number' && initialized.armor.damageReduction == null) {
initialized.armor.damageReduction = initialized.armor.armorClass;
delete initialized.armor.armorClass;
}
return initialized;
}
}
+21
View File
@@ -0,0 +1,21 @@
import { makeIntField, makeStringField, makeHtmlField, makeArrayField, fields } from './fields';
export class RaceData extends foundry.abstract.TypeDataModel {
static defineSchema() {
const f = fields();
return {
availableDisciplines: makeArrayField(new f.StringField({ blank: false })),
attributePoints: makeIntField(0, { min: 0 }),
skillPoints: makeIntField(0, { min: 0 }),
freeTalents: makeArrayField(new f.StringField({ blank: false })),
racialAbilities: makeArrayField(
new f.SchemaField({
name: makeStringField(''),
description: makeStringField(''),
}),
),
physicalDescription: makeStringField(''),
description: makeHtmlField(''),
};
}
}
+115
View File
@@ -0,0 +1,115 @@
import {
makeIntField,
makeStringField,
makeBoolField,
makeHtmlField,
makeArrayField,
fields,
} from './fields';
import {
CASTING_MODES,
SPELL_SCHOOLS,
SACRED_DEITIES,
SOURCE_BOOKS,
AOE_SHAPES,
SPELL_DAMAGE_TYPES,
TRIGGER_EVENTS,
TS_DEFAULT_THRESHOLD,
TS_DEFAULT_REQUIRED,
} from '../constants';
export class SpellData extends foundry.abstract.TypeDataModel {
static defineSchema() {
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[] }),
sourceBook: makeStringField('', { blank: true, choices: SOURCE_BOOKS as unknown as readonly string[] }),
// Casting mode + resources
castingMode: makeStringField('standard', { blank: false, choices: CASTING_MODES as unknown as readonly string[] }),
manaCost: makeIntField(1, { min: 0 }),
bloodCost: makeIntField(0, { min: 0 }),
// Difficulty
difficulty: new f.SchemaField({
threshold: makeIntField(TS_DEFAULT_THRESHOLD, { min: 2, max: 6 }),
successes: makeIntField(TS_DEFAULT_REQUIRED, { min: 1, max: 10 }),
}),
// Components
components: new f.SchemaField({
verbal: makeBoolField(false),
somatic: makeBoolField(false),
material: makeStringField(''),
symbols: makeArrayField(new f.StringField({ blank: false })),
}),
// Timing & geometry
castingTime: makeStringField('1 akcja'),
castingTimeRounds: makeIntField(0, { min: 0 }),
castingTimeMinutes: makeIntField(0, { min: 0 }),
range: makeStringField(''),
targets: makeStringField(''),
duration: makeStringField(''),
areaOfEffect: new f.SchemaField({
shape: makeStringField('point', { blank: false, choices: AOE_SHAPES as unknown as readonly string[] }),
x: makeIntField(0, { min: 0 }),
y: makeIntField(0, { min: 0 }),
unit: makeStringField('m'),
}),
// Damage
damageBase: makeStringField(''),
damageType: makeStringField('magical', { blank: false, choices: SPELL_DAMAGE_TYPES as unknown as readonly string[] }),
ignoresArmor: makeBoolField(false),
// Status effects on hit
statusEffects: makeArrayField(new f.StringField({ blank: false })),
saveAttribute: makeStringField(''),
saveSkill: makeStringField(''),
// Requirements
requirements: new f.SchemaField({
race: makeArrayField(new f.StringField({ blank: false })),
talent: makeArrayField(new f.StringField({ blank: false })),
discipline: makeArrayField(new f.StringField({ blank: false })),
}),
// Flags
isSuperspell: makeBoolField(false),
requiresGroupCast: makeBoolField(false),
minCasters: makeIntField(1, { min: 1 }),
nonCombatOnly: makeBoolField(false),
// Overcast — structured
overcastOptions: makeArrayField(new f.SchemaField({
description: makeStringField(''),
manaPerStep: makeIntField(1, { min: 0 }),
})),
// Reactive triggers
triggers: makeArrayField(new f.SchemaField({
event: makeStringField('killWithWeapon', { blank: false, choices: TRIGGER_EVENTS as unknown as readonly string[] }),
effect: makeStringField(''),
})),
// Variable success summons (Przywołanie Istoty z Otchłani)
variableSuccesses: makeArrayField(new f.SchemaField({
label: makeStringField(''),
successes: makeIntField(1, { min: 1, max: 10 }),
})),
// Description
description: makeHtmlField(''),
higherCircles: makeHtmlField(''), // legacy — superseded by overcastOptions
// Legacy fields (kept for migration; will be removed in a later release)
overcasting: makeStringField(''),
complexityLevel: makeIntField(0, { min: 0, max: 10 }),
};
}
}
+22
View File
@@ -0,0 +1,22 @@
import { makeIntField, makeStringField, makeHtmlField, makeBoolField, fields } from './fields';
export class TalentData extends foundry.abstract.TypeDataModel {
static defineSchema() {
const f = fields();
return {
requirements: new f.SchemaField({
race: makeStringField(''),
attribute: makeStringField(''),
skill: makeStringField(''),
talent: makeStringField(''),
title: makeStringField(''),
discipline: makeStringField(''),
}),
multiSelect: makeBoolField(false),
cost: makeStringField(''),
damageReductionBonus: makeIntField(0, { min: 0 }),
description: makeHtmlField(''),
effect: makeHtmlField(''),
};
}
}