Files
roll20-character-sheets/Without Numbers/Source/ts/calculations.ts
T
2024-04-14 16:40:46 +00:00

633 lines
19 KiB
TypeScript

/* global getAttrs, setAttrs, getSectionIDs, generateRowID, on, removeRepeatingRow, _, getTranslationByKey */
/* Calculations */
const calculateSaves = () => {
getAttrs(
[
...attributes.map((attr) => `${attr}_mod`),
"level",
"luck_save",
"save_physical",
"save_mental",
"save_evasion",
"save_luck",
],
(v) => {
const base = 16 - (parseInt(v.level) || 1);
const setting: { [key: string]: any } = {
save_physical:
base -
(Math.max(parseInt(v.strength_mod), parseInt(v.constitution_mod)) ||
0),
save_mental:
base -
(Math.max(parseInt(v.charisma_mod), parseInt(v.wisdom_mod)) || 0),
save_evasion:
base -
(Math.max(parseInt(v.intelligence_mod), parseInt(v.dexterity_mod)) ||
0),
};
if (v.luck_save === "1") setting.save_luck = base;
mySetAttrs(setting, v);
}
);
};
const calculateEffort = () => {
getSectionIDs("repeating_psychic-skills", (idArray) => {
getAttrs(
[
...effortAttributes,
"psionics_total_effort",
...idArray.map((id) => `repeating_psychic-skills_${id}_skill`),
],
(v) => {
const attrBonus =
Math.max(parseInt(v.wisdom_mod), parseInt(v.constitution_mod)) || 0,
skillBonus = Math.max(
...skills.psionic.map((x) => parseInt(v[`skill_${x}`]) || 0),
...idArray.map(
(id) => parseInt(v[`repeating_psychic-skills_${id}_skill`]) || 0
)
);
const psionics_total_effort =
Math.max(1 + attrBonus + skillBonus, 1) +
parseInt(v.psionics_extra_effort) -
parseInt(v.psionics_committed_effort_current) -
parseInt(v.psionics_committed_effort_scene) -
parseInt(v.psionics_committed_effort_day) || 0;
mySetAttrs(
{
psionics_total_effort,
},
v
);
}
);
});
};
const calculateMagicEffort = () => {
getAttrs(
[
"magic_total_effort",
"magic_committed_effort_current",
"magic_committed_effort_scene",
"magic_committed_effort_day",
"magic_uncommitted_effort",
],
(v) => {
const magic_uncommitted_effort =
parseInt(v.magic_total_effort) -
(parseInt(v.magic_committed_effort_current) +
parseInt(v.magic_committed_effort_scene) +
parseInt(v.magic_committed_effort_day)) || 0;
mySetAttrs(
{
magic_uncommitted_effort,
},
v
);
}
);
};
const calculateProcessing = () => {
getSectionIDs("repeating_processing-nodes", (idArray) => {
const sourceAttrs = [
...idArray.map((id) => `repeating_processing-nodes_${id}_node_value`),
...idArray.map((id) => `repeating_processing-nodes_${id}_node_connected`),
"wisdom_mod",
"intelligence_mod",
"ai_extra_processing",
"ai_committed_processing_current",
"ai_committed_processing_scene",
"ai_committed_processing_day",
"ai_total_processing",
];
getAttrs(sourceAttrs, (v) => {
const maxProcessing =
(1 + Math.max(parseInt(v.wisdom_mod), parseInt(v.intelligence_mod)) ||
0) +
Math.max(
...idArray
.filter(
(id) =>
v[`repeating_processing-nodes_${id}_node_connected`] === "1"
)
.map(
(id) =>
parseInt(v[`repeating_processing-nodes_${id}_node_value`]) || 0
),
0
);
const ai_total_processing =
maxProcessing +
parseInt(v.ai_extra_processing) -
parseInt(v.ai_committed_processing_current) -
parseInt(v.ai_committed_processing_scene) -
parseInt(v.ai_committed_processing_day) || 0;
mySetAttrs(
{
ai_total_processing,
},
v
);
});
});
};
const calculateAC = () => {
getSectionIDs("repeating_armor", (idArray) => {
const sourceAttrs = [
...idArray.map((id) => `repeating_armor_${id}_armor_ac`),
...idArray.map((id) => `repeating_armor_${id}_armor_ranged_ac`),
...idArray.map((id) => `repeating_armor_${id}_armor_melee_ac`),
...idArray.map((id) => `repeating_armor_${id}_armor_active`),
...idArray.map((id) => `repeating_armor_${id}_armor_type`),
"system",
"npc",
"AC",
"innate_ac",
"dexterity_mod",
];
getAttrs(sourceAttrs, (v) => {
if (v.npc === "1") return;
const activeIDs = idArray.filter(
(id) => v[`repeating_armor_${id}_armor_active`] === "1"
);
const stackingType = (type: string) =>
type === "SHIELD" || type === "ACCESSORY";
const calculateArmorClass = (acType: string): number => {
return Math.max(
parseInt(v.innate_ac) || 0,
...activeIDs
.filter(
(id) => !stackingType(v[`repeating_armor_${id}_armor_type`])
)
.map((id) => parseInt(v[`repeating_armor_${id}_${acType}_ac`]) || 0)
);
};
const calculateStackedArmor = (acType: string): number => {
return Math.max(
0,
...activeIDs
.filter((id) => stackingType(v[`repeating_armor_${id}_armor_type`]))
.map((id) => parseInt(v[`repeating_armor_${id}_${acType}_ac`]) || 0)
);
};
const armor = calculateArmorClass("armor");
const shieldAC = calculateStackedArmor("armor");
const withShield = armor >= shieldAC ? armor + 1 : shieldAC;
const ac = shieldAC > 0 ? withShield : armor;
const AC = ac + (parseInt(v.dexterity_mod) || 0);
let update: { [key: string]: number } = { AC };
if (v.system === "cwn") {
const rangedBase = calculateArmorClass("armor_ranged");
const rangedStack = calculateStackedArmor("armor_ranged");
const armor_ranged_ac = rangedBase + rangedStack;
const meleeBase = calculateArmorClass("armor_melee");
const meleeStack = calculateStackedArmor("armor_melee");
const armor_melee_ac = meleeBase + meleeStack;
update = {
...update,
armor_ranged_ac,
armor_melee_ac,
};
}
mySetAttrs(update, v);
});
});
};
const calculateMaxStrain = () => {
getAttrs(["constitution", "strain_max"], (v) => {
mySetAttrs(
{
strain_max: parseInt(v.constitution) || 0,
},
v
);
});
};
const calculatePermanentStrain = () => {
getAttrs(
["cyberware_strain_total", "strain_permanent_extra", "strain_permanent"],
(v) => {
const permStrain =
parseInt(v.cyberware_strain_total) +
parseInt(v.strain_permanent_extra) || 0;
mySetAttrs(
{
strain_permanent: permStrain,
},
v
);
}
);
};
const calculateCyberwareStrain = () => {
//Loop through the cyberware and add up the strain.
getSectionIDs("repeating_cyberware", (idArray) => {
const sourceAttrs = [
...idArray.map((id) => `repeating_cyberware_${id}_cyberware_strain`),
"cyberware_strain_total",
];
getAttrs(sourceAttrs, (v) => {
const cyberwareStrain = idArray.reduce((m, id) => {
m += parseInt(v[`repeating_cyberware_${id}_cyberware_strain`]) || 0;
return m;
}, 0);
mySetAttrs(
{
cyberware_strain_total: cyberwareStrain,
},
v
);
});
});
};
const calculateStrain = () => {
//Add up all the strain
getAttrs(
[
"strain_permanent",
"strain_extra",
"strain",
"strain_max",
"strain_above_max",
],
(v) => {
const strain =
(parseInt(v.strain_permanent) || 0) + (parseInt(v.strain_extra) || 0);
const strain_above_max = strain > parseInt(v.strain_max) ? 1 : 0;
mySetAttrs(
{
strain: strain,
strain_above_max: strain_above_max,
},
v
);
}
);
};
const calculateAttr = (attr: string) => {
getAttrs([attr, `${attr}_base`, `${attr}_boosts`], (v) => {
const setting = {
[`${attr}`]: `${
(parseInt(v[`${attr}_base`]) || 10) +
(parseInt(v[`${attr}_boosts`]) || 0)
}`,
};
mySetAttrs(setting, v, null, () => {
calculateMod(attr);
});
});
};
const calculateMod = (attr: string) => {
getAttrs([attr, `${attr}_bonus`, `${attr}_mod`], (v) => {
const mod = ((value) => {
if (value >= 18) return 2;
else if (value >= 14) return 1;
else if (value >= 8) return 0;
else if (value >= 4) return -1;
else return -2;
})(parseInt(v[attr]) || 10);
const setting = {
[`${attr}_mod`]: `${mod + (parseInt(v[`${attr}_bonus`]) || 0)}`,
};
mySetAttrs(setting, v, null, () => {
calculateSaves();
generateWeaponDisplay();
calculateStrDexMod();
if (attr === "dexterity") calculateAC();
});
});
};
const calculateStrDexMod = () => {
getAttrs(["str_dex_mod", "strength_mod", "dexterity_mod"], (v) => {
const str_dex_mod = Math.max(
parseInt(v.strength_mod) || 0,
parseInt(v.dexterity_mod) || 0
);
mySetAttrs(
{
str_dex_mod,
},
v
);
});
};
const calculateShellAttrs = () => {
const physicalAttrs = ["strength", "dexterity", "constitution"];
getSectionIDs("repeating_shells", (idArray) => {
const sourceAttrs = [
...idArray.map((id) => `repeating_shells_${id}_shell_active`),
...idArray.map((id) => `repeating_shells_${id}_shell_strength`),
...idArray.map((id) => `repeating_shells_${id}_shell_dexterity`),
...idArray.map((id) => `repeating_shells_${id}_shell_constitution`),
...physicalAttrs,
"setting_transhuman_enable",
"setting_ai_enable",
];
getAttrs(sourceAttrs, (v) => {
if (
v.setting_transhuman_enable === "transhuman" ||
v.setting_ai_enable === "ai"
) {
let attributes: { [key: string]: string } = {};
physicalAttrs.forEach(
(attr) =>
(attributes[attr] =
idArray
.filter(
(id) => v[`repeating_shells_${id}_shell_active`] === "1"
)
.map((id) => v[`repeating_shells_${id}_shell_${attr}`])[0] ||
"None")
);
mySetAttrs(attributes, v, null, () => {
physicalAttrs
.filter((attr) => attributes[attr] !== "None")
.forEach((attr) => calculateMod(attr));
});
} else {
physicalAttrs.forEach((attr) => calculateAttr(attr));
}
});
});
};
const calculateNextLevelXP = () => {
const xp = [0, 3, 6, 12, 18, 27, 39, 54, 72, 93];
getAttrs(["level", "setting_xp_scheme"], (v) => {
const level = parseInt(v.level);
if (v.setting_xp_scheme === "xp") {
if (level < 10) {
setAttrs({
xp_next: xp[level],
});
} else {
setAttrs({
xp_next: 93 + (level - 9) * 24,
});
}
} else if (v.setting_xp_scheme === "money") {
setAttrs({
xp_next: 2500 * (2 * level),
});
}
});
};
const calculateGearReadiedStowed = () => {
const doCalc = (
gearIDs: string[],
weaponIDs: string[],
armorIDs: string[]
) => {
const attrs = [
...gearIDs.map((id) => `repeating_gear_${id}_gear_amount`),
...gearIDs.map((id) => `repeating_gear_${id}_gear_encumbrance`),
...gearIDs.map((id) => `repeating_gear_${id}_gear_status`),
...gearIDs.map((id) => `repeating_gear_${id}_gear_bundled`),
...armorIDs.map((id) => `repeating_armor_${id}_armor_encumbrance`),
...armorIDs.map((id) => `repeating_armor_${id}_armor_encumbrance_bonus`),
...armorIDs.map((id) => `repeating_armor_${id}_armor_status`),
...weaponIDs.map((id) => `repeating_weapons_${id}_weapon_encumbrance`),
...weaponIDs.map((id) => `repeating_weapons_${id}_weapon_status`),
"gear_readied",
"gear_stowed",
"strength",
"gear_readied_max",
"gear_readied_over",
"gear_stowed_over",
"gear_stowed_max",
];
getAttrs(attrs, (v) => {
const [gear_readied, gear_stowed] = armorIDs.reduce(
(m, id) => {
if (v[`repeating_armor_${id}_armor_status`] === "READIED") {
m[0] += parseInt(v[`repeating_armor_${id}_armor_encumbrance`]) || 0;
} else if (v[`repeating_armor_${id}_armor_status`] === "STOWED")
m[1] += parseInt(v[`repeating_armor_${id}_armor_encumbrance`]) || 0;
return m;
},
weaponIDs.reduce(
(m, id) => {
if (v[`repeating_weapons_${id}_weapon_status`] === "READIED")
m[0] +=
parseInt(v[`repeating_weapons_${id}_weapon_encumbrance`]) || 0;
else if (v[`repeating_weapons_${id}_weapon_status`] === "STOWED")
m[1] +=
parseInt(v[`repeating_weapons_${id}_weapon_encumbrance`]) || 0;
return m;
},
gearIDs.reduce(
(m, id) => {
const amount =
parseInt(v[`repeating_gear_${id}_gear_amount`]) || 0;
let packingFactor = 1;
if (v[`repeating_gear_${id}_gear_bundled`] === "on") {
packingFactor = 3;
}
if (v[`repeating_gear_${id}_gear_status`] === "READIED")
m[0] +=
Math.ceil(
(amount *
parseFloat(v[`repeating_gear_${id}_gear_encumbrance`])) /
packingFactor
) || 0;
else if (v[`repeating_gear_${id}_gear_status`] === "STOWED")
m[1] +=
Math.ceil(
(amount *
parseFloat(v[`repeating_gear_${id}_gear_encumbrance`])) /
packingFactor
) || 0;
return m;
},
[0, 0]
)
)
);
const armor_encumbrance_bonus = Math.max(
0,
...armorIDs
.filter((id) => v[`repeating_armor_${id}_armor_status`] === "READIED")
.map(
(id) =>
parseInt(v[`repeating_armor_${id}_armor_encumbrance_bonus`]) || 0
)
);
const gear_stowed_max =
parseInt(v.strength) + armor_encumbrance_bonus || 0;
const gear_readied_max = Math.floor(gear_stowed_max / 2);
const setting = {
gear_readied,
gear_stowed,
gear_readied_max,
gear_stowed_max,
gear_readied_over:
parseInt(v.gear_readied) > gear_readied_max ? "1" : "0",
gear_stowed_over: parseInt(v.gear_stowed) > gear_stowed_max ? "1" : "0",
};
mySetAttrs(setting, v, {
silent: true,
});
});
};
getSectionIDs("repeating_gear", (gearIDs) => {
getSectionIDs("repeating_weapons", (weaponIDs) => {
getSectionIDs("repeating_armor", (armorIDs) =>
doCalc(gearIDs, weaponIDs, armorIDs)
);
});
});
};
const generateWeaponDisplay = () => {
// Generates the weapon menu and sets the display of weapons
// in display mode in one go.
getSectionIDs("repeating_weapons", (idArray) => {
const prefixes = idArray.map((id) => `repeating_weapons_${id}`);
const sourceAttrs = [
...prefixes.map((prefix) => `${prefix}_weapon_attack`),
...prefixes.map((prefix) => `${prefix}_weapon_name`),
...prefixes.map((prefix) => `${prefix}_weapon_skill_bonus`),
...prefixes.map((prefix) => `${prefix}_weapon_attribute_mod`),
...prefixes.map((prefix) => `${prefix}_weapon_damage`),
...prefixes.map((prefix) => `${prefix}_weapon_shock`),
...prefixes.map((prefix) => `${prefix}_weapon_shock_damage`),
...prefixes.map((prefix) => `${prefix}_weapon_shock_ac`),
...prefixes.map((prefix) => `${prefix}_weapon_skill_to_damage`),
...prefixes.map((prefix) => `${prefix}_weapon_attack_display`),
...prefixes.map((prefix) => `${prefix}_weapon_damage_display`),
...attributes.map((attr) => `${attr}_mod`),
...weaponSkills,
"attack_bonus",
"str_dex_mod",
"macro_weapons",
];
getAttrs(sourceAttrs, (v) => {
const setting: { [key: string]: string } = {};
const baseAttackBonus = parseInt(v.attack_bonus) || 0;
prefixes.forEach((prefix) => {
const attrBonus =
parseInt(
v[(v[`${prefix}_weapon_attribute_mod`] || "").slice(2, -1)]
) || 0;
const skillBonus =
parseInt(v[(v[`${prefix}_weapon_skill_bonus`] || "").slice(2, -1)]) ||
parseInt(v[`${prefix}_weapon_skill_bonus`]) ||
0;
const damageBonus =
attrBonus +
(v[`${prefix}_weapon_skill_to_damage`] === "@{weapon_skill_bonus}"
? skillBonus
: 0);
const weaponDamage =
v[`${prefix}_weapon_damage`] === "0"
? ""
: v[`${prefix}_weapon_damage`];
const shockString =
v[`${prefix}_weapon_shock`] !== "0"
? `, ${
(parseInt(v[`${prefix}_weapon_shock_damage`]) || 0) +
damageBonus
}\xa0${translate("SHOCK").toString().toLowerCase()}${
v[`${prefix}_weapon_shock_ac`]
? ` ${translate("VS_AC_LEQ")} ${
v[`${prefix}_weapon_shock_ac`]
}`
: ""
}`
: "";
const attack =
baseAttackBonus +
(parseInt(v[`${prefix}_weapon_attack`]) || 0) +
(skillBonus === -1 ? -2 : skillBonus) +
attrBonus;
const damage =
weaponDamage +
(weaponDamage
? damageBonus === 0
? ""
: damageBonus > 0
? ` + ${damageBonus}`
: ` - ${-damageBonus}`
: damageBonus);
setting[`${prefix}_weapon_attack_display`] =
attack >= 0 ? `+${attack}` : attack.toString();
setting[`${prefix}_weapon_damage_display`] = `${
damage || 0
}\xa0${translate("DAMAGE").toString().toLowerCase()}${shockString}`;
});
setting.macro_weapons = prefixes
.map((prefix, index) => {
const label = `${v[`${prefix}_weapon_name`]} (${
setting[`${prefix}_weapon_attack_display`]
})`;
return buildLink(
label,
`${prefix}_attack`,
index === prefixes.length - 1
);
})
.join(" ");
mySetAttrs(setting, v, {
silent: true,
});
});
});
};
const handleAmmoAPI = (sName: string) => {
const formula = sName === "weapons" ? "[[-1 - @{weapon_burst}]]" : "-1";
getSectionIDs(`repeating_${sName}`, (idArray) => {
getAttrs(
[
"setting_use_ammo",
...idArray.map((id) => `repeating_${sName}_${id}_weapon_use_ammo`),
...idArray.map((id) => `repeating_${sName}_${id}_weapon_api`),
],
(v) => {
const setting = idArray.reduce((m: { [k: string]: string }, id) => {
m[`repeating_${sName}_${id}_weapon_api`] =
v.setting_use_ammo === "1" &&
v[`repeating_${sName}_${id}_weapon_use_ammo`] !== "0"
? `\n!modattr --mute --charid @{character_id} --repeating_${sName}_${id}_weapon_ammo|${formula}`
: "";
return m;
}, {});
mySetAttrs(setting, v, {
silent: true,
});
}
);
});
};