/** * Extend the base Actor document by defining a custom roll data structure which is ideal for the Simple system. * @extends {Actor} */ export class VermineActor extends Actor { /** @override */ prepareData() { // Prepare data for the actor. Calling the super version of this executes // the following, in order: data reset (to clear active effects), // prepareBaseData(), prepareEmbeddedDocuments() (including active effects), // prepareDerivedData(). // Initialize wound data to prevent undefined errors with active effects if (!this.system.minorWound) this.system.minorWound = { value: 0, min: 0, max: 5, threshold: 1 }; if (!this.system.majorWound) this.system.majorWound = { value: 0, min: 0, max: 4, threshold: 4 }; if (!this.system.deadlyWound) this.system.deadlyWound = { value: 0, min: 0, max: 2, threshold: 8 }; super.prepareData(); } /** @override */ prepareBaseData() { // Data modifications in this step occur before processing embedded // documents or derived data. if (this.type == 'character') { } } /** * @override * Augment the basic actor data with additional dynamic data. Typically, * you'll want to handle most of your calculated/derived data in this step. * Data calculated in this step should generally not exist in template.json * (such as ability modifiers rather than ability scores) and should be * available both inside and outside of character sheets (such as if an actor * is queried and has a roll executed directly from it). */ prepareDerivedData() { const actorData = this; const systemData = actorData.system; const flags = actorData.flags.vermine2047 || {}; // Make separate methods for each Actor type (character, npc, etc.) to keep // things organized. switch (this.type) { case "character": this._prepareCharacterData(actorData); break; case "npc": this._prepareNpcData(actorData); break; case "group": this._prepareGroupData(actorData); break; case "creature": this._prepareCreatureData(actorData); break; } } /** * Prepare Character type specific data */ _prepareCharacterData(actorData) { if (actorData.type !== 'character') return; this._setAgeType(); this._setCharacterEffort(); this._setCharacterSelfControl(); this._setCharacterThresholds(); // Make modifications to data here. For example: const systemData = actorData.system; // Loop through ability scores, and add their modifiers to our sheet output. for (let [key, ability] of Object.entries(systemData.abilities)) { // Calculate the modifier using d20 rules. ability.mod = Math.floor((ability.value - 10) / 2); } this.prepareCombatStatus(); } prepareCombatStatus() { // Ensure combatStatus exists (defined in base template) if (!this.system.combatStatus) return; //combat initiative reaction difficulty switch (parseInt(this.system.combatStatus.difficulty)) { case 5: this.system.combatStatus.label = "Offensif"; break; case 7: this.system.combatStatus.label = "Actif"; break; case 9: this.system.combatStatus.label = "Passif"; break; default: this.system.combatStatus.label = "Passif"; this.system.combatStatus.difficulty = "9"; break; } } /** * Prepare NPC type specific data. */ _prepareNpcData(actorData) { if (actorData.type !== 'npc') return; // Make modifications to data here. For example: const systemData = actorData.system; // Set wound thresholds based on threat level this._setNpcThresholds(); // Set reserve max values based on role this._setNpcAttributes(); this.prepareCombatStatus(); // Prepare abilities with labels for (let [k, v] of Object.entries(systemData.abilities)) { v.label = game.i18n.localize(CONFIG.VERMINE.abilities[k]) ?? k; } } /** * Set NPC wound thresholds based on threat level */ _setNpcThresholds() { const health = this.system.abilities?.health?.value || 1; const threatLevel = this.system.threat?.value || 1; const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {}; // Use threat-based wounds or fall back to health-based this.system.minorWound.threshold = threatConfig.minorWound || health; this.system.majorWound.threshold = threatConfig.majorWound || (health + 3); this.system.deadlyWound.threshold = threatConfig.deadlyWound || (health + 7 < 11 ? health + 7 : 10); // Set max wounds based on threat level this.system.minorWound.max = threatConfig.minorWound || 4; this.system.majorWound.max = threatConfig.majorWound || 3; this.system.deadlyWound.max = threatConfig.deadlyWound || 2; } /** * Set NPC attributes from role level */ _setNpcAttributes() { const roleLevel = this.system.role?.value || 1; const roleConfig = CONFIG.VERMINE.npcRoleLevels[roleLevel] || {}; // Set effort and self_control based on role this.system.attributes.effort.max = roleConfig.pools || 0; this.system.attributes.self_control.max = roleConfig.reaction_bonus || 0; } /** * Prepare Group type specific data. */ _prepareGroupData(actorData) { if (actorData.type !== 'group') return; this.prepareCombatStatus(); // Initialize group-specific data if not present this._initGroupData(); // Calculate reserve max based on group level this._calculateGroupReserve(); // Update morale level based on dice value this._updateGroupMorale(); } /** * Initialize group data with defaults */ _initGroupData() { if (this.type !== 'group') return; const system = this.system; // Initialize objectives if not present if (!system.objectives) { system.objectives = { major: [], minor: [] }; } // Initialize groupAbilities if not present if (!system.groupAbilities) { system.groupAbilities = []; } // Initialize reserve if not present if (!system.reserve) { system.reserve = { value: 0, min: 0, max: 10 }; } } /** * Calculate group reserve max based on level * Rules: Group level determines reserve size */ _calculateGroupReserve() { if (this.type !== 'group') return; const level = this.system.level?.value || 1; // Reserve max is based on group level (simplified: level * 1D for now) // Can be customized based on specific rules this.system.reserve.max = Math.min(10, level * 2); // Ensure value doesn't exceed max if (this.system.reserve.value > this.system.reserve.max) { this.system.reserve.value = this.system.reserve.max; } } /** * Update group morale level based on dice value * Rules: 7D+ = Haut, 6-3D = Normal, 2D- = Bas, 0D = Crise */ _updateGroupMorale() { if (this.type !== 'group') return; const moraleValue = this.system.morale?.value || 0; const moraleLevel = this.system.morale?.level; // If level is already explicitly set, keep it if (moraleLevel && moraleLevel !== "high") return; // Determine morale level based on dice value if (moraleValue >= 7) { this.system.morale.level = "high"; } else if (moraleValue >= 3) { this.system.morale.level = "normal"; } else if (moraleValue >= 1) { this.system.morale.level = "low"; } else { this.system.morale.level = "crisis"; } } /** * Prepare Creature type specific data. * Calculates computed values from pattern, size, role, and pack. */ _prepareCreatureData(actorData) { if (actorData.type !== 'creature') return; this.prepareCombatStatus(); // Calculate computed values from pattern, size, role, and pack this._calculateCreatureComputedValues(); // Set wound thresholds from creature characteristics this._calculateCreatureWoundThresholds(); } /** * Calculate creature computed values from pattern, size, role, and pack. * Rules: Attack = pattern + size + pack + role.reaction * Damage = pattern.damage + size.vigor + pack.damage * Reaction = role.reaction + role.reaction_bonus */ _calculateCreatureComputedValues() { if (this.type !== 'creature') return; const patternLevel = this.system.pattern?.value || 1; const sizeLevel = this.system.size?.value || 1; const roleLevel = this.system.role?.value || 1; const packLevel = this.system.pack?.value || 0; // Get config values const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}; const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}; const roleConfig = CONFIG.VERMINE.creatureRoleLevels[roleLevel] || {}; const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}; // Calculate computed values this.system.computed = this.system.computed || {}; // Attack: pattern + size + pack + role.reaction this.system.computed.attack = (patternConfig.attack || 0) + (sizeConfig.attack || 0) + (packConfig.attack || 0) + (roleConfig.reaction || 0); // Damage: pattern + size.vigor + pack this.system.computed.damage = (patternConfig.damage || 0) + (sizeConfig.vigor || 0) + (packConfig.damage || 0); // Vigor: size.vigor + pack.damage this.system.computed.vigor = (sizeConfig.vigor || 0) + (packConfig.damage || 0); // Reaction: role.reaction + role.reaction_bonus this.system.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0); this.system.computed.reactionBonus = roleConfig.reaction_bonus || 0; // Pools (reserves) this.system.computed.pools = roleConfig.pools || 0; // Gear and hindrance this.system.computed.gear = roleConfig.gear || 9; this.system.computed.gearHindrance = roleConfig.gear_hindrance || 0; // Protection this.system.computed.protection = roleConfig.protection || 1; } /** * Calculate creature wound thresholds from pattern, size, and pack. * Rules: Thresholds are sum of minorWound, majorWound, deadlyWound from all sources */ _calculateCreatureWoundThresholds() { if (this.type !== 'creature') return; const patternLevel = this.system.pattern?.value || 1; const sizeLevel = this.system.size?.value || 1; const packLevel = this.system.pack?.value || 0; const patternConfig = CONFIG.VERMINE.creaturePatternLevels[patternLevel] || {}; const sizeConfig = CONFIG.VERMINE.creatureSizeLevels[sizeLevel] || {}; const packConfig = CONFIG.VERMINE.creaturePackLevels[packLevel] || {}; // Calculate wound thresholds (sum of all sources) this.system.minorWound.threshold = (patternConfig.minorWound || 0) + (sizeConfig.minorWound || 0) + (packConfig.minorWound || 0); this.system.majorWound.threshold = (patternConfig.majorWound || 0) + (sizeConfig.majorWound || 0) + (packConfig.majorWound || 0); this.system.deadlyWound.threshold = (patternConfig.deadlyWound || 0) + (sizeConfig.deadlyWound || 0) + (packConfig.deadlyWound || 0); // Set max wounds this.system.minorWound.max = Math.min(5, this.system.minorWound.threshold + 2); this.system.majorWound.max = Math.min(4, this.system.majorWound.threshold + 1); this.system.deadlyWound.max = Math.min(2, this.system.deadlyWound.threshold); } /** * Override getRollData() that's supplied to rolls. */ getRollData() { const data = super.getRollData(); // Prepare character roll data. this._getCharacterRollData(data); this._getNpcRollData(data); return data; } /** * Prepare character roll data. */ _getCharacterRollData(data) { if (this.type !== 'character') return; // Copy the ability scores to the top level, so that rolls can use // formulas like `@str.mod + 4`. if (data.abilities) { for (let [k, v] of Object.entries(data.abilities)) { data[k] = foundry.utils.deepClone(v); } } // Add level for easier access, or fall back to 0. if (data.attributes.level) { data.lvl = data.attributes.level.value ?? 0; } } /** * Prepare NPC roll data. */ _getNpcRollData(data) { if (this.type !== 'npc') return; // Process additional NPC data here. } _setCharacterSelfControl() { this.system.attributes.self_control.max = 0 + Object.values(this.system.abilities).filter(i => i.category === "mental" || i.category === "social").map((i) => i.value).reduce((acc, curr) => acc + curr, 0) + this.modFromAgeSelfControl; } _setCharacterEffort() { this.system.attributes.effort.max = 0 + Object.values(this.system.abilities).filter(i => i.category === "physical" || i.category === "manual").map((i) => i.value).reduce((acc, curr) => acc + curr, 0) + this.modFromAgeEffort; } _setCharacterThresholds() { const health = this.system.abilities.health.value; this.system.minorWound.threshold = health; this.system.majorWound.threshold = health + 3; this.system.deadlyWound.threshold = (health + 7 < 11) ? health + 7 : 10; this.system.minorWound.max = 4 + this.modFromAgeWounds.l; this.system.majorWound.max = 3 + this.modFromAgeWounds.h; this.system.deadlyWound.max = 2 + this.modFromAgeWounds.d; } _setAgeType() { Object.keys(CONFIG.VERMINE.AgeTypes).forEach((type) => { if (this.system.identity.age >= parseInt(CONFIG.VERMINE.AgeTypes[type].beginning, 10)) { this.system.identity.ageType = type; } }); } get ageType() { return this.system.identity.ageType; } get modFromAgeSelfControl() { return this.ageType == 1 ? -1 : 0; } get modFromAgeEffort() { if (this.ageType == 1) return -1; if (this.ageType == 3) return -2; return 0; } get modFromAgeWounds() { if (this.ageType == 1) return { l: 0, h: 0, d: -1 }; if (this.ageType == 3) return { l: -1, h: -1, d: -1 }; return { l: 0, h: 0, d: 0 }; } }