81adfb7ffd
- Move wound data initialization to prepareBaseData() (before effects are applied) - Initialize combatStatus in prepareBaseData() to prevent undefined errors - Add protection against recursive effect application in prepareEmbeddedDocuments() - This prevents the 'ActiveEffect application phase has already completed' error The error occurred because modify data in prepareDerivedData() (like combatStatus) could trigger observers that try to re-apply effects during the same cycle. By initializing all required data in prepareBaseData() and protecting prepareEmbeddedDocuments() from recursive calls, we ensure effects are applied exactly once per preparation cycle. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
471 lines
15 KiB
JavaScript
471 lines
15 KiB
JavaScript
|
|
/**
|
|
* 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 */
|
|
prepareBaseData() {
|
|
// Data modifications in this step occur before processing embedded
|
|
// documents or derived data.
|
|
|
|
// 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 };
|
|
|
|
// Initialize combatStatus to prevent errors
|
|
if (!this.system.combatStatus) {
|
|
this.system.combatStatus = { difficulty: "9", label: "Passif" };
|
|
}
|
|
|
|
if (this.type == 'character') {
|
|
|
|
}
|
|
}
|
|
|
|
/** @override */
|
|
prepareEmbeddedDocuments() {
|
|
// Prevent recursive effect application
|
|
// Foundry V11+ can sometimes call this multiple times in a preparation cycle
|
|
if (this._preparingEffects) return;
|
|
this._preparingEffects = true;
|
|
|
|
try {
|
|
super.prepareEmbeddedDocuments();
|
|
} finally {
|
|
delete this._preparingEffects;
|
|
}
|
|
}
|
|
|
|
/** @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) {
|
|
this.system.combatStatus = { difficulty: "9", label: "Passif" };
|
|
return;
|
|
}
|
|
|
|
// Ensure difficulty exists
|
|
if (!this.system.combatStatus.difficulty) {
|
|
this.system.combatStatus.difficulty = "9";
|
|
}
|
|
|
|
//combat initiative reaction difficulty
|
|
const difficulty = parseInt(this.system.combatStatus.difficulty) || 9;
|
|
|
|
// Only update if values are different to avoid triggering unnecessary updates
|
|
const currentLabel = this.system.combatStatus.label;
|
|
let newLabel = "Passif";
|
|
|
|
switch (difficulty) {
|
|
case 5: newLabel = "Offensif"; break;
|
|
case 7: newLabel = "Actif"; break;
|
|
case 9: newLabel = "Passif"; break;
|
|
}
|
|
|
|
// Only update if label changed
|
|
if (currentLabel !== newLabel) {
|
|
this.system.combatStatus.label = newLabel;
|
|
}
|
|
|
|
// Only update difficulty if it was undefined or invalid
|
|
if (!this.system.combatStatus.difficulty || isNaN(parseInt(this.system.combatStatus.difficulty))) {
|
|
this.system.combatStatus.difficulty = "9";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 };
|
|
}
|
|
|
|
} |