Skill Modifier Total Formula
-
+
Template Formula
@@ -5278,7 +5413,7 @@
@@ -8201,6 +8336,16 @@ const repeatingSum = (destinations, section, fields) => {
});
});
};
+
+on("sheet:opened", function() {/*
+ getAttrs(['version_number'], function(values) {
+ if(values['version_number']==0) {
+ AddBuffsToSkills();
+ setAttrs("version_number":1)
+ }
+ });*/
+});
+
// ====== PC /NPC Sheet ====== \\
on("change:level", function() {
ArmorUpdate();
@@ -9303,6 +9448,228 @@ var EncumbranceUpdate = function() {
}
});
};
+
+// Buff Stacking System
+// Handles complex bonus stacking rules with type checking
+// See about implementing some of this for the rest of the sheet to help with code cleanup.
+
+// Configuration: Define all skills
+const SKILLSLIST = [
+ 'Acrobatics', 'Athletics', 'Climb', 'Deception', 'Endurance', 'GatherInformation',
+ 'Initiative', 'Jump', 'Knowledge-Bureaucracy', 'Knowledge-GalacticLore', 'Knowledge-LifeSciences',
+ 'Knowledge-PhysicalScience', 'Knowledge-SocialScience', 'Knowledge-Tactics',
+ 'Knowledge-Technology', 'Mechanics', 'Perception', 'Persuasion', 'Pilot', 'Ride',
+ 'Stealth', 'Swim', 'Survival', 'TreatInjury', 'UseComputer', 'UsetheForce'
+];
+
+const DEFENSES = ['reflex', 'flatfooted', 'fort', 'will'];
+
+// Bonus types that always stack (even with themselves)
+const ALWAYS_STACK_TYPES = ['Untyped', 'Penalty', 'Circumstance', 'Dodge'];
+
+// Main calculation function
+var CalculateBonuses = function() {
+ getSectionIDs('repeating_bonuses', (idArray) => {
+ // Build array of all attributes we need to get
+ const getArr = [];
+
+ idArray.forEach(id => {
+ getArr.push(`repeating_bonuses_${id}_bonus-applied`);
+ getArr.push(`repeating_bonuses_${id}_bonus-type`);
+ getArr.push(`repeating_bonuses_${id}_bonus-all-attacks`);
+ getArr.push(`repeating_bonuses_${id}_bonus-all-damage`);
+ getArr.push(`repeating_bonuses_${id}_bonus-all-defenses`);
+ getArr.push(`repeating_bonuses_${id}_bonus-reflex`);
+ getArr.push(`repeating_bonuses_${id}_bonus-flatfooted`);
+ getArr.push(`repeating_bonuses_${id}_bonus-fort`);
+ getArr.push(`repeating_bonuses_${id}_bonus-will`);
+ getArr.push(`repeating_bonuses_${id}_bonus-all-skills`);
+
+ // Add all individual skill attributes
+ SKILLSLIST.forEach(skill => {
+ getArr.push(`repeating_bonuses_${id}_bonuses-${skill}`);
+ });
+ });
+
+ getAttrs(getArr, (values) => {
+ // Data structures to hold bonuses by category and type
+ const attackBonuses = [];
+ const damageBonuses = [];
+ const defenseBonuses = {
+ reflex: [],
+ flatfooted: [],
+ fort: [],
+ will: []
+ };
+ const skillBonuses = {};
+ SKILLSLIST.forEach(skill => {
+ skillBonuses[skill] = [];
+ });
+
+ // Process each buff entry
+ idArray.forEach(id => {
+ const applied = parseInt(values[`repeating_bonuses_${id}_bonus-applied`]) || 0;
+
+ // Skip if buff is not applied
+ if (applied !== 1) return;
+
+ const bonusType = values[`repeating_bonuses_${id}_bonus-type`] || 'Untyped';
+
+ // Process Attacks
+ const attackValue = parseInt(values[`repeating_bonuses_${id}_bonus-all-attacks`]) || 0;
+ if (attackValue !== 0) {
+ attackBonuses.push({ value: attackValue, type: bonusType });
+ }
+
+ // Process Damage
+ const damageValue = parseInt(values[`repeating_bonuses_${id}_bonus-all-damage`]) || 0;
+ if (damageValue !== 0) {
+ damageBonuses.push({ value: damageValue, type: bonusType });
+ }
+
+ // Process Defenses
+ const allDefensesValue = parseInt(values[`repeating_bonuses_${id}_bonus-all-defenses`]) || 0;
+
+ DEFENSES.forEach(defense => {
+ const individualValue = parseInt(values[`repeating_bonuses_${id}_bonus-${defense}`]) || 0;
+
+ // If all-defenses has a value, apply it to this defense
+ if (allDefensesValue !== 0) {
+ defenseBonuses[defense].push({ value: allDefensesValue, type: bonusType });
+ }
+
+ // If this specific defense has a value, apply it
+ if (individualValue !== 0) {
+ defenseBonuses[defense].push({ value: individualValue, type: bonusType });
+ }
+ });
+
+ // Process Skills
+ const allSkillsValue = parseInt(values[`repeating_bonuses_${id}_bonus-all-skills`]) || 0;
+
+ SKILLSLIST.forEach(skill => {
+ const individualValue = parseInt(values[`repeating_bonuses_${id}_bonuses-${skill}`]) || 0;
+
+ // If all-skills has a value, apply it to this skill
+ if (allSkillsValue !== 0) {
+ skillBonuses[skill].push({ value: allSkillsValue, type: bonusType });
+ }
+
+ // If this specific skill has a value, apply it
+ if (individualValue !== 0) {
+ skillBonuses[skill].push({ value: individualValue, type: bonusType });
+ }
+ });
+ });
+
+ // Calculate totals using stacking rules
+ const totalAttack = calculateStackedBonus(attackBonuses);
+ const totalDamage = calculateStackedBonus(damageBonuses);
+
+ const totalDefenses = {};
+ DEFENSES.forEach(defense => {
+ totalDefenses[defense] = calculateStackedBonus(defenseBonuses[defense]);
+ });
+
+ const totalSkills = {};
+ SKILLSLIST.forEach(skill => {
+ totalSkills[skill] = calculateStackedBonus(skillBonuses[skill]);
+ });
+
+ // Build the setAttrs object
+ const setObj = {
+ 'total_bonus_attack': totalAttack,
+ 'total_bonus_damage': totalDamage,
+ 'total_bonus_reflex': totalDefenses.reflex,
+ 'total_bonus_flatfooted': totalDefenses.flatfooted,
+ 'total_bonus_fort': totalDefenses.fort,
+ 'total_bonus_will': totalDefenses.will
+ };
+
+ // Add all skill totals
+ SKILLSLIST.forEach(skill => {
+ setObj[`total_bonus_${skill}`] = totalSkills[skill];
+ });
+
+ // Set all calculated values
+ setAttrs(setObj);
+ });
+ });
+};
+
+// Function to calculate stacked bonus given an array of {value, type} objects
+function calculateStackedBonus(bonusArray) {
+ if (bonusArray.length === 0) return 0;
+ // Separate bonuses by type
+ const bonusesByType = {};
+
+ bonusArray.forEach(bonus => {
+ const type = bonus.type || 'Untyped';
+ if (!bonusesByType[type]) {
+ bonusesByType[type] = [];
+ }
+ bonusesByType[type].push(bonus.value);
+ });
+
+ let total = 0;
+
+ // For each bonus type, apply stacking rules
+ for (const type in bonusesByType) {
+ const values = bonusesByType[type];
+
+ if (ALWAYS_STACK_TYPES.includes(type)) {
+ // These types always stack - sum all values
+ total += values.reduce((sum, val) => sum + val, 0);
+ } else {
+ // For typed bonuses, only the highest applies
+ total += Math.max(...values);
+ }
+ }
+
+ return total;
+}
+on("change:repeating_bonuses remove:repeating_bonuses", CalculateBonuses);
+
+var AddBuffsToSkills = function() {
+ // Build array of all formula attributes we need to get
+ const getArr = SKILLSLIST.map(skill => `${skill}Formula`);
+
+ getAttrs(getArr, (values) => {
+ const setObj = {};
+
+ // Process each skill
+ SKILLSLIST.forEach(skillName => {
+ const formulaAttr = `${skillName}Formula`;
+ let formula = values[formulaAttr] || '';
+
+ // Check if the buff reference is already in the formula
+ const buffReference = `@{total_bonus_${skillName}}[Buffs]`;
+
+ if (!formula.includes(buffReference)) {
+ // Add the buff reference to the end of the formula
+ if (formula.length > 0 && !formula.endsWith('+')) {
+ formula += '+';
+ }
+ formula += buffReference;
+
+ // Add to our update object
+ setObj[formulaAttr] = formula;
+ }
+ });
+
+ // Only call setAttrs if we have changes to make
+ if (Object.keys(setObj).length > 0) {
+ setAttrs(setObj);
+ console.log(`Updated ${Object.keys(setObj).length} skill formulas with buff references.`);
+ } else {
+ console.log('All skill formulas already contain buff references.');
+ }
+ });
+};
+
+// Event listener
+on("clicked:AddBuffsToSkills", AddBuffsToSkills );
+
var VehicleSizeUpdate = function() {
getAttrs(["vehicle-size","vehicle-CapitalGrapple","vehicle-alternateStealth"], function(v) {
console.log("Size = " + v["vehicle-size"])
diff --git a/Star Wars Saga Edition/sheet.json b/Star Wars Saga Edition/sheet.json
index 640d0c2333..4b55194b67 100644
--- a/Star Wars Saga Edition/sheet.json
+++ b/Star Wars Saga Edition/sheet.json
@@ -4,7 +4,7 @@
"authors": "Alicia G (original author), Stephen C. (maintainer), Iain J (contributor)",
"roll20userid": "2889,436906,2196054",
"preview": "StarWarsSagaEditionpreview.png",
- "instructions": "## Character Sheet \n\nInspired by the Saga Edition sheet by Mad Irishman Productions. This sheet calculates statistics using the rules as written, with options for various house rules. If you find a mistake or wish to request a feature, please contact the authors (links to profiles in the wiki). \n\n **Last Updated** 25 January, 2025\n\n View the wiki for the change log and more information on the sheet: https://wiki.roll20.net/Star_Wars_Saga_Edition_Character_Sheet",
+ "instructions": "## Character Sheet \n\nInspired by the Saga Edition sheet by Mad Irishman Productions. This sheet calculates statistics using the rules as written, with options for various house rules. If you find a mistake or wish to request a feature, please contact the authors (links to profiles in the wiki). \n\n **Last Updated** 1 February, 2025\n\n View the wiki for the change log and more information on the sheet: https://wiki.roll20.net/Star_Wars_Saga_Edition_Character_Sheet",
"useroptions": [
{
"attribute": "yellow-logo",