428 lines
14 KiB
JavaScript
428 lines
14 KiB
JavaScript
|
|
/**
|
|
* Extend the base Actor document by defining a custom roll data structure which is ideal for the Simple system.
|
|
* @extends {Actor}
|
|
*/
|
|
export default class VermineActor extends Actor {
|
|
|
|
/**
|
|
* @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() {
|
|
super.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 };
|
|
}
|
|
|
|
} |