6fbc7e7864
The previous solution used a flag, but it didn't persist across preparation cycles. This solution checks this.effects.applicationPhase directly. If phase is 'initial' or 'final', it means effects are already being applied, so we skip the super.prepareEmbeddedDocuments() call to avoid the 'phase has already completed' error. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
464 lines
15 KiB
JavaScript
464 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() {
|
|
// Check if effects are already being applied in this preparation cycle
|
|
// In Foundry V11+, the parent prepareEmbeddedDocuments() calls applyActiveEffects()
|
|
// If this is called multiple times in the same cycle, we get the "phase already completed" error
|
|
const phase = this.effects?.applicationPhase;
|
|
|
|
// If we're already in the middle of applying effects (initial or final phase),
|
|
// don't call super as it would try to apply effects again
|
|
if (phase === "initial" || phase === "final") {
|
|
return;
|
|
}
|
|
|
|
// If effects haven't been applied yet, proceed normally
|
|
super.prepareEmbeddedDocuments();
|
|
}
|
|
|
|
/**
|
|
* @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 };
|
|
}
|
|
|
|
} |