Note that this requires creating (or importing) non-compendium Critical Injury items manually, with valid min/max values Once you've created the critical injury items, you can create a rollable table using this Macro. You do not need to create the table, you need to create the items and then run this macro.
/* Description: Create a roll table for critical injuries from all critical injury items in the world Author: Unknown Release Notes v3 - 2024-06-29 - Updated for Foundry v12 compatability by Wrycu v2 - 2023-01-31 - Updated for Foundry v10 compatability by Wrycu v1 - 2021-05-31 - Initial release */ /*********************************** * DO NOT Modify values below here * ***********************************/ let critdamagevalues = game.items.filter(item => item.type === "criticalinjury"); let sorted = critdamagevalues.sort((a,b) => return a.system.min b.system.min ? -1 : a.system.min > b.system.min ? 1 : 0>); let rollresults = sorted.map(item => return type: 1, img: item.img, documentCollection: "Item", weight: 1, range: [item.system.min, item.system.max], resultId: item._id, text: item.name >>); RollTable.create( name: "Critical Injuries", results: rollresults, formula: "1d100" >);
You can do the same as above, but for critical hits:
/* Description: Create a roll table for critical damage from all critical damage items in the world Author: Unknown Release Notes v3 - 2024-06-29 - Updated for Foundry v12 compatability by Wrycu v2 - 2023-01-31 - Updated for Foundry v10 compatability by Wrycu v1 - 2021-05-31 - Initial release */ /*********************************** * DO NOT Modify values below here * ***********************************/ let critdamagevalues = game.items.filter(item => item.type === "criticaldamage"); let sorted = critdamagevalues.sort((a,b) => return a.system.min b.system.min ? -1 : a.system.min > b.system.min ? 1 : 0>); let rollresults = sorted.map(item => return type: 1, img: item.img, documentCollection: "Item", weight: 1, range: [item.system.min, item.system.max], resultId: item._id, text: item.name >>); RollTable.create( name: "Critical Damage", results: rollresults, formula: "1d100" >);
If you create a valid table with non-compendium critical injuries (described above), you can use this macro. Note that it requires both tables to contain the word "Critical" in order to have the dropdown display correctly. If using a different language or spelling, change the macro to reflect the correct table names.
/* Description: Roll for critical injury/damage Author: Unknown NOTE: Make sure the critical tables are set up first: https://github.com/StarWarsFoundryVTT/StarWarsFFG/wiki/Creating-Critical-Tables NOTE: Confirmed to work with Foundry v12 Release Notes v2.1 - 2023-02-17 - Adjusted linked tokens by Rysarian v2.0 - 2023-01-31 - Updated for Foundry v10 compatability by Wrycu v1.0 - - Initial release */ /*********************************** * DO NOT Modify values below here * ***********************************/ const tables = game.tables.map(table => if (table.name.includes("Critical")) if (actor != null && ((token.actor.type === "vehicle" && table.name === "Critical Damage") || (token.actor.type === "character" && table.name === "Critical Injuries"))) return ``; > else return ``; > > >) var modifier = 0; var durableRank = 0; //See if an actor is selected if (actor) if (token.document.actorLink) //Make sure we reference the real actor and not a copy of it var realActor = game.actors.get(actor.id); //Count the number of injuries the character already has modifier = realActor.items.filter(item => item.type === "criticalinjury" || item.type === "criticaldamage").length * 10; //check to see if the character has the Durable talent var durableTalent = realActor.talentList.filter(item => item.name.toLowerCase() === "durable"); //If the talent is found multiply it by 10 for the roll if (durableTalent.length > 0) durableRank = durableTalent[0].rank * 10; > > else var realActor = token.actor; //Count the number of injuries the token already has modifier = token.actor.items.filter(item => item.type === "criticalinjury" || item.type === "criticaldamage").length * 10; //check to see if the token has the Durable talent var durableTalent = token.actor.items.filter(item => item.name.toLowerCase() === "durable"); //If the talent is found multiply it by 10 for the roll if (durableTalent.length > 0) durableRank = durableTalent[0].system.ranks.current * 10; > > > let d = new Dialog( title: "Critical Roll", content: `Select table and modifier
Modifier: + modifier + `" value pl-c1">+ modifier + `" data-dtype="String" /> Durable: ` + durableRank + ` Table: `, buttons: one: icon: ' ', label: "Roll Critical", callback: (html) => let modifier; modifier = parseInt(html.find(".modifier").val(), 10); if (isNaN(modifier)) modifier = 0; > const table = html.find(".crittable :selected").val(); //Added in the Durable modifications as well as making sure it doesn't roll below 1 const critRoll = new Roll(`max(1d100 + $modifier> - $durableRank>, 1)`); const tableResult = game.tables.get(table).draw( roll: critRoll, displayChat: true >); //If we have an actor selected try to add the injury if (realActor) //Table roles are async so wait for it to return tableResult.then(function (value) //Ignore if we didn't draw a result if (value.results.length 0) return; > var firstResult = value.results[0]; var item = game.items.get(firstResult.documentId); if (item != null) //Add injury to the selected chracter realActor.createEmbeddedDocuments("Item", [item.toObject()]); ChatMessage.create( speaker: alias: realActor.name, token: realActor.id >, content: item.system.description >) > >); > > >, two: icon: ' ', label: "Cancel", callback: () => console.log("Chose Two") > >, default: "two", close: () => console.log("This always is logged no matter which option is chosen") >); d.render(true);
If done correctly, the macro should present a dialog that looks like this:
And the result should look like this:
As long as a token was selected, the Critical Injury or Critical Damage should have been automatically applied. If a token was not selected, the resulting injury can be dragged onto the character sheet to apply it:
This macro generates a random number of credits for your party. It will allow you to enter about how many credits you want to give each player and the number of players in your party (I set these at 25 and 4 by default, as I figure most mooks will have 100 credits or so on them), but of course you can change these values by editing line 3.
I then randomized that number between about half to 1.25 of what you entered just to give it a little bit of randomness - but ensures it'll always end up as a whole number. It then multiplies that number by however many players you have, lets them know how many total credits they found, and how many credits that is per player in the chat.
/* Description: Generate a random number of credits Author: Unknown Release Notes v2 - 2023-02-01 - Updated for Foundry v10 compatability by Wrycu v1 - - Initial release */ /*********************************** * DO NOT Modify values below here * ***********************************/ let d = new Dialog( title: "Random Credits", content:'Enter a rough number of credits per player, and the total number of players
Credits:Players:', buttons: one: icon: ' ', label: "Roll Credits", callback: (html) => let credits; credits =(parseInt(html.find(".credits").val(), 10)); if (isNaN(credits)) credits = 0;> let players; players = parseInt(html.find(".players").val(), 10); if (isNaN(players)) players = 1;> let lowcredits; lowcredits = credits * .5; let highcredits; highcredits = credits * 1.25; credits = (Math.floor(Math.random() * (highcredits, lowcredits+1)) + lowcredits); credits = Math.ceil(credits/players)*players; let creditstotal; creditstotal = credits * players; let message; let chatData; message = 'The players found ' + creditstotal + ' Credits! Thats ' + credits + ' for each of the ' + players + ' players.'; chatData = user:game.user.id, speaker:game.user, content:message >; ChatMessage.create(chatData, >); > >, two: icon: ' ', label: "Cancel", callback: () => console.log("Closed Credits") > >, default: "two", close: () => console.log("This always is logged no matter which option is chosen") >); d.render(true);
(this can be done automatically, without needing a macro, using the Enhancements module)
/* Description: Automatically renames combat slots to generic names Author: Wrycu Release Notes v2 - 2023-02-01 - Updated for Foundry v10 compatability by Wrycu v1 - - Initial release */ /*********************************** * DO NOT Modify values below here * ***********************************/ const cmbt = game.combat; function hasPlayer(c) if (c.token?.disposition === 1) return _id: c.id, img: "systems/starwarsffg/images/dice/starwars/lightside.png", name: "Friendly" >; > else if (c.token?.disposition === 0) return _id: c.id, img: "systems/starwarsffg/images/mod-all.png", name: "Neutral" >; > else return _id: c.id, img: "systems/starwarsffg/images/dice/starwars/darkside.png", name: "Hostile" >; > > let updates = >; updates = cmbt.combatants.map(c => return hasPlayer(c) >); cmbt.updateEmbeddedDocuments("Combatant", updates);
Note that you'll get a syntax error if you run the macro without an active combat.
This macro can be used to add a reminder for using boost dice acquired via advantage in combat. Works for PCs and NPCs. Token disposition MUST be set.
NOTE THAT THIS MACRO DOES NOT WORK BEYOND FOUNDRY 0.8.6 AS TURN ALERT HAS NOT BEEN UPDATED SINCE THEN
/* Description: Adds a boost die reminder to the next initiative slot with the same disposition as the current slot (e.g. adds a reminder for the next PC slot) Author: Wrycu Date: 2021-01-09 NOTE: This macro requires setting the disposition of tokens properly NOTE: This macro requires the "Turn Alert" module */ function find_applicable_slot(current_disposition) /* given a combat slot, find the next available slot with the same disposition */ var search_id = game.combat.current.turn; for (var i = search_id + 1; i game.combat.turns.length; i++) if (game.combat.turns[i].token.data.disposition === current_disposition) return [0, game.combat.turns[i].data._id]; > > for (var i = 0; i search_id; i++) if (game.combat.turns[i].token.data.disposition === current_disposition) return [1, game.combat.turns[i].data._id]; > > return [0, false]; > // validate the required module is loaded if (typeof TurnAlert === "undefined") ui.notifications.error("It appears TurnAlert is not installed - please do so before using this macro"); else if (!game.combat) ui.notifications.warn("You must have an active combat to use this macro") else // set up the type of die we'll be sending a reminder about var die_type = "bo"; // determine if the current slot is friendly, neutral, or enemy var current_disposition = game.combat.getCombatantByToken(game.combat.current.tokenId).token.data.disposition; // find the next slot with a disposition matching the current. this may be itself (in the next round) var data = find_applicable_slot(current_disposition); // check if we successfully found a token or not. it shouldn't be possible to fail to find one, but who knows. if (data[1] === false) ui.notifications.warn("Unable to find any actor with the correct disposition") else var message = "[Reminder] The active character has a [" + die_type + "]."; // create the alert object const alertData = round: data[0], roundAbsolute: false, // we want relative round numbers turnId: data[1], message: message, label: "Available die reminder", >; // create the alert itself TurnAlert.create(alertData); let disp_text = "Neutral"; if(current_disposition === 1) disp_text = "Friendly"; else if(current_disposition === -1) disp_text = "Hostile"; chat_content = `[$die_type>] has been passed to the next $disp_text> initiative slot`; ChatMessage.create( content: chat_content >); > >
The following macro can be used (by the GM or players) to set a boost or setback die status effect on the token targeted by them.
/* Description: Adds a boost or setback die targeted actor Author: Wrycu NOTE: This macro requires some configuration; see below NOTE: This macro requires an active target from the user running it Release Notes v2 - 2023-02-01 - Updated for Foundry v10 compatability by Wrycu v1 - 2021-01-09 - Initial release */ /********************** * Modify values here * **********************/ // select the base directory to your images (MUST CONTAIN TRAILING SLASH) var base_path = "icons/svg/"; // select your image names var images = boost: "skull.svg", setback: "unconscious.svg" > /*********************************** * DO NOT Modify values below here * ***********************************/ function set_token_status(path) game.user.targets.values().next()['value'].toggleEffect(path); > /* dialog stuff */ let d = new Dialog( title: "Critical Roll", content: `Select status to apply
`, buttons: one: icon: ' ', label: "Boost die", callback: (html) => set_token_status(base_path + images['boost']) > >, two: icon: ' ', label: "Setback die", callback: (html) => set_token_status(base_path + images['setback']) > >, three: icon: ' ', label: "Cancel", callback: () => console.log("Chose cancel") > >, default: "three", close: () => console.log("This always is logged no matter which option is chosen") >); d.render(true);
The following macro is to be used by the GM to set the default values for the bars on all the actors.
/* Description: This will set up the configuration for the Actors' tokens Author: Aljovin NOTE: Execute this macro before adding actors to the scene, it will update every actors at once NOTE: By design this will set the following: Minion: Top bar : quantity, bottom bar: stats.wounds Characters: top bar : stats.strain, bottom bar : stats.wounds vehicules: top bar : stats.systemStrain, bottom bar : stats.hullTrauma NOTE: The display will be "Always for Owner" Release Notes v3 - 2023-03-11 - Updated to fix bug with changes not actually updating the actors by LostestLocke v2 - 2023-02-01 - Updated for Foundry v10 compatability by Wrycu v1 - - Initial release */ /*********************************** * DO NOT Modify values below here * ***********************************/ async function applyChanges() for ( let actor of game.actors ) console.log("updating " + actor.name); // Update Token switch (actor.type) case "minion": console.log("minion"); await actor.update( "prototypeToken.bar1.attribute": "quantity", "prototypeToken.bar2.attribute": "stats.wounds", "prototypeToken.displayBars": 40 >); break; case "character": console.log("character"); await actor.update( "prototypeToken.bar1.attribute": "stats.strain", "prototypeToken.bar2.attribute": "stats.wounds", "prototypeToken.displayBars": 40 >); break; case "vehicle": console.log("vehicle"); await actor.update( "prototypeToken.bar1.attribute": "stats.strain", "prototypeToken.bar2.attribute": "stats.wounds", "prototypeToken.displayBars": 40 >); break; default: > > > new Dialog( title: `Standardize token Stats Bars`, content: ` `, buttons: yes: icon: " ", label: `Apply Changes`, callback: (html) => applyChanges() > >, no: icon: " ", label: `Cancel Changes` >, >, default: "yes", >).render(true)
This is to be used to identify the tokens that have the Adversary Talent and Defence and their ranks.
/* Description: This is to be used to identify the tokens that have the Adversary Talent and Defence and their ranks. Author: unknown Release Notes v2 - 2023-02-01 - Updated for Foundry v10 compatability by Wrycu v1 - - Initial release */ /*********************************** * DO NOT Modify values below here * ***********************************/ if (canvas.tokens.objects.children.length === 0) ui.notifications.error("There are no tokens on this scene"); > else let adversaryList = ""; for (let token of canvas.tokens.objects.children) if (token.actor != null) if (token.visible) let _adversaryType = ""; let _adversaryValue = 0, _defencemelee = 0, _defenceranged = 0, _soak = 0; for (let talents of token.actor.items) if (talents.name == "Adversary") _adversaryValue = talents.system.ranks.current; >; >; _adversaryType = token.actor.type.charAt(0).toUpperCase() + token.actor.type.substr(1).toLowerCase(); if (_adversaryType === "Character" || _adversaryType === "Minion") _defencemelee = token.actor.system.stats.defence.melee; _defenceranged = token.actor.system.stats.defence.ranged; _soak = parseInt(token.actor.system.stats.soak.value); > else if (_adversaryType === "Vehicle") //This needs to be adjusted if all four parts of the shield are going to be used. _defencemelee = _defenceranged = token.actor.system.stats.shields.fore; > adversaryList += ""; > > >; new Dialog( title: `Adversary levels`, content: ` td text-align: center; border: 1px black solid; > th text-align: center; border: 1px black solid; font-size: 18px; > " + token.actor.name + " "; adversaryList += _adversaryType; adversaryList += " "; if (_adversaryValue != 0) adversaryList += +_adversaryValue >; adversaryList += " "; if (_defencemelee != 0) adversaryList += _defencemelee; >; adversaryList += " "; if (_defenceranged != 0) adversaryList += _defenceranged; >; adversaryList += " "; if (_soak != 0) adversaryList += _soak; >; adversaryList += " Name Type Adv. Def-M Def-R Soak ` + adversaryList + ` `, buttons: Cancel: icon: " ", label: `Close` >, >, default: "Cancel", close: html => > >).render(true); >
The following macro can be used by the GM to remove items from an actor. Handy for items which aren't rendered in the inventory, such as utility arms.
/* Description: Remove all instances of an item from an actor Author: Wrycu NOTE: This MUST be run as GM NOTE: You MUST configure the values below Release Notes v2 - 2023-02-01 - Updated for Foundry v10 compatability by Wrycu v1 - 2021-05-31 - Initial release */ /********************** * Modify values here * **********************/ let actor_name = "arm man"; let item_name = "Utility Arm"; /*********************************** * DO NOT Modify values below here * ***********************************/ let my_actor = game.actors.filter(actor => actor.name === actor_name); if (my_actor.length === 0) ui.notifications.warn("Unable to find actor"); > else my_actor = my_actor[0]; let item = my_actor.items.filter(item => item.name === item_name); if (item.length === 0) ui.notifications.warn("Unable to find item"); > else ui.notifications.info("Found " + item.length + " instances of item; removing"); for (var x=0; x item.length; x++) let item_id = item[x]._id; my_actor.deleteEmbeddedDocuments("Item", [item_id]); > > >
Talents need to be dragged onto the sheet once and only change the rank (not the tier) if purchasing more. Then select a character and use the macro and it will display a pyramid in a window.
/* Author: Rysarian When using this macro: ** DO NOT drag multiple copies of talents on one character sheet ** DO NOT change the Tier on the character's talent when increasing the rank ** DO change the rank accordingly to how many ranks have been purchased ** This assumes the pyramid structure is being followed and will loop through based ** on how many Tier 1s the character has. */ let t1List = [], t2List = [], t3List = [], t4List = [], t5List = []; //Adds the talent to the list according to tier async function addToList(tier, talent) if(tier > 5) tier = 5; > switch (tier) case 1: t1List.push(talent); break; case 2: t2List.push(talent); break; case 3: t3List.push(talent); break; case 4: t4List.push(talent); break; case 5: t5List.push(talent); break; default: break; > > async function main() //Get all character talents let tList = token.actor.data.data.talentList; //Go through talent list to sort them into respective lists. for (let i = 0; i tList.length; i++) await addToList(parseInt(tList[i].tier), tList[i]); if (tList[i].isRanked) for (let j = 1; j tList[i].rank; j++) await addToList(parseInt(tList[i].tier) + j, tList[i]); > > > let talentPyramid = ""; //Add html text for dialog display for (let a = 0; a t1List.length; a++) let One, Two, Three, Four, Five; //Set any nulls to blank text if (t1List[a] != null) One = await t1List[a].name; > else One = ""; > if (t2List[a - 1] != null && a > 0) Two = await t2List[a - 1].name; > else Two = ""; > if (t3List[a - 2] != null) Three = await t3List[a - 2].name; > else Three = ""; > if (t4List[a - 3] != null) Four = await t4List[a - 3].name; > else Four = ""; > if (t5List[a - 4] != null) Five = await t5List[a - 4].name; > else Five = ""; > //Format the pyramid switch (a) case 0: talentPyramid += `$One> ` break; case 1: talentPyramid += `$One> $Two> ` break; case 2: talentPyramid += `$One> $Two> $Three> ` break; case 3: talentPyramid += `$One> $Two> $Three> $Four> ` break; default: talentPyramid += `$One> $Two> $Three> $Four> $Five> ` break; > > let d = new Dialog( title: "Talent Pyramid", content: ` .Empty background-color: black; border: 1px solid black; color: white; > td text-align: center; >Tier 1 Tier 2 Tier 3 Tier 4 Tier 5 $talentPyramid> `, buttons: close: icon: ' ', label: "Close", callback: () => console.log("Chose Two") > >, default: "two", close: () => console.log("This always is logged no matter which option is chosen") >); d.render(true); > if(canvas.tokens.controlled.length != 1 ) ui.notifications.warn("Please select a single token."); > else main(); >
Select a token. Dialog will provide a choice between Cool and Discipline, rolling the selected one and applying successes and advantages to healing strain.
/* Description: Macro for post encounter strain recovery. Select token, execute, select Cool or Discipline, and click "Recover." It should roll the pool and apply successes and advantages to removing strain on the selected actor. If there are leftover advantages, it reports that in chat. MANY MANY THANKS to Blaze for helping me do the preview correctly. Author: AdmiralDave NOTE: I've house ruled that Advantage can be spent to recover strain during this post-encounter recovery, as nothing in the description of the rule states that you can't and every other roll in the game suggests using advantage that way. If you disagree, set advantage_heals_strain below to false. Release Notes v3 - 2024-08-14 - Updated Macro to work on foundry v12 v2 - 2023-02-01 - Updated for Foundry v10 compatability by Wrycu v1 - 2022-08-30 - Initial release */ /********************** * Modify values here * **********************/ //set this to false if you don't want advantage to heal strain let advantage_heals_strain = true; /*********************************** * DO NOT Modify values below here * ***********************************/ main() async function main() //Is token selected? if (canvas.tokens.controlled.length != 1) ui.notifications.error(`A single token must be selected to recover $qlocal("SWFFG.Strain")>`); return; > let using_actor = canvas.tokens.controlled[0].actor; let current_strain = using_actor?.system?.stats?.strain?.value; if (current_strain == null || current_strain == undefined || current_strain == 0) ui.notifications.error(`$using_actor.name> has no $qlocal("SWFFG.Strain")> to heal`); return; > show_strain_recovery_popup(using_actor, current_strain); > function update_strain(using_actor, new_strain) using_actor.update( "system.stats.strain.value": new_strain >); > function show_strain_recovery_popup(using_actor, current_strain) let cool_skill = get_skill(using_actor, "Cool"); let discipline_skill = get_skill(using_actor, "Discipline"); let actor_sheet = using_actor.sheet.getData(); if (cool_skill == null || cool_skill == undefined) ui.notifications.error(`$using_actor.name> is missing $qlocal("SWFFG.SkillsNameCool")> skill object`); return; > if (discipline_skill == null || discipline_skill == undefined) ui.notifications.error(`$using_actor.name> is missing $qlocal("SWFFG.SkillsNameDiscipline")> skill object`); return; > let cool_char = get_characteristic(using_actor, cool_skill); if (cool_char == null || cool_char == undefined) let local_char = qlocal(`SWFFG.Characteristic$cool_skill.characteristic>`); ui.notifications.error(`$using_actor.name> is missing $local_char> characteristic object`); return; > let discipline_char = get_characteristic(using_actor, discipline_skill); if (discipline_char == null || discipline_char == undefined) let local_char = qlocal(`SWFFG.$discipline_skill.characteristic>`) ui.notifications.error(`$using_actor.name> is missing $local_char> characteristic object`); return; > let cool_pool = build_pool(cool_skill, cool_char); let discipline_pool = build_pool(discipline_skill, discipline_char); let cool_checked = ""; let discipline_checked = ""; if (is_skill_better(cool_pool, discipline_pool)) cool_checked = " checked"; else discipline_checked = " checked"; let popup_content = `Select Recovery Skill
$cool_checked> /> $cool_skill.label> $discipline_checked> /> $discipline_skill.label> `; let d = new Dialog( title: `Post-Encounter $qlocal("SWFFG.Strain")> Recovery`, content: popup_content, buttons: recover: icon: ' ', label: "Recover", callback: async (html) => let we_cool = html.find("#Cool")[0].checked; let rolled_pool = we_cool ? cool_pool : discipline_pool; //roll direct, update chat, heal strain let roll_result = new game.ffg.RollFFG(rolled_pool.renderDiceExpression()); roll_result.toMessage( speaker: alias: using_actor.name >, flavor: `$qlocal("SWFFG.Rolling")> to heal $qlocal("SWFFG.Strain")>. ` >) await new Promise((resolve) => setTimeout(resolve,2000)); let min_heal = roll_result.ffg.success; let max_heal = min_heal; let surplus_advantage = roll_result.ffg.advantage; if(advantage_heals_strain) max_heal += roll_result.ffg.advantage; surplus_advantage = 0; > let healed_strain = max_heal; if (current_strain max_heal) healed_strain = current_strain; if (min_heal current_strain) surplus_advantage = max_heal - healed_strain; else surplus_advantage = roll_result.ffg.advantage; > let message_content = `Healed $healed_strain> $qlocal("SWFFG.Strain")> `; if (surplus_advantage > 0) message_content += ` with $surplus_advantage> surplus $qlocal("SWFFG.Advantage")>`; message_content += `
`; ChatMessage.create( speaker: alias: using_actor.name >, content: message_content >); update_strain(using_actor, current_strain - healed_strain); > >, close: icon: ' ', label: "Cancel" > >, render: html => let cool_preview = html.find("#cool_preview")[0]; cool_pool.renderPreview(cool_preview); let discipline_preview = html.find("#discipline_preview")[0]; discipline_pool.renderPreview(discipline_preview); > >); d.render(true); > //helpers //================= function build_pool(skill, characteristic) let preview_dice_pool = new DicePoolFFG( ability: Math.max(characteristic?.value ? characteristic.value : 0, skill?.rank ? skill.rank : 0), boost: skill.boost, setback: skill.setback, remsetback: skill.remsetback, force: skill.force, advantage: skill.advantage, dark: skill.dark, light: skill.light, failure: skill.failure, threat: skill.threat, success: skill.success, triumph: skill?.triumph ? skill.triumph : 0, despair: skill?.despair ? skill.despair : 0, source: skill: skill?.ranksource?.length ? skill.ranksource : [], boost: skill?.boostsource?.length ? skill.boostsource : [], remsetback: skill?.remsetbacksource?.length ? skill.remsetbacksource : [], setback: skill?.setbacksource?.length ? skill.setbacksource : [], advantage: skill?.advantagesource?.length ? skill.advantagesource : [], dark: skill?.darksource?.length ? skill.darksource : [], light: skill?.lightsource?.length ? skill.lightsource : [], failure: skill?.failuresource?.length ? skill.failuresource : [], threat: skill?.threatsource?.length ? skill.threatsource : [], success: skill?.successsource?.length ? skill.successsource : [], >, >); preview_dice_pool.upgrade(Math.min(characteristic.value, skill.rank)); return preview_dice_pool; > function qlocal(key) return game.i18n.localize(key); > //get a given skill from the actor function get_skill(using_actor, skill_name) return using_actor?.system?.skills[skill_name]; > //get the paired characterstic for a skill from the actor function get_characteristic(using_actor, skill_obj) return using_actor?.system?.characteristics[skill_obj.characteristic]; > function is_skill_better(first_pool, second_pool) let first_total = first_pool.ability + first_pool.proficiency + first_pool.boost; let second_total = second_pool.ability + second_pool.proficiency + second_pool.boost; if(first_total > second_total) return true; if(first_total second_total) return false; if(first_pool.proficiency > second_pool.proficiency) return true; return false; >
This is a GM only macro due to the need to update the target. Select the token that did the attack. Target the one hit by the attack. Use the macro and it will apply wounds or strain accounting for soak, pierce, breach and stun damage.
/* Description: Auto calculates damage from the last successful attack from the attacker to the selected target. Author: Rysarian Release Notes v5 - 2023-12-30 - Prevent healing target if soak is greater than damage v4 - 2023-05-11 - Adjust Breach, Pierce and Stun Damage detection for current OggDude data; replace failing _roll property access with rolls[0]; adjust breach and pierce rank access for Foundry v10 by FloSchwalm v3 - 2023-03-11 - Updated to account for non-integer values for wounds or strain on the token sheet by LostestLocke v2 - 2023-02-01 - Updated for Foundry v10 compatability by Wrycu v1 - - Initial release */ /*********************************** * DO NOT Modify values below here * ***********************************/ async function main() if (canvas.tokens.controlled.length === 1) let attacker = token.actor; console.log(attacker); let result = game.messages.filter(m => m.speaker.actor === attacker.id); console.log(result); let messageNum = result.length - 1; let message = result[messageNum]; let weapon = message.rolls[0].data; if (message.rolls[0].ffg.success > 0) let targets = message.user.targets; if (targets.size > 0) let targetToken = await canvas.tokens.get(targets.ids[0]); let soak = parseInt(targetToken.actor.system.stats.soak.value); let oldWounds = Number.isInteger(parseInt(targetToken.actor.system.stats.wounds.value)) ? parseInt(targetToken.actor.system.stats.wounds.value) : 0; let oldStrain = Number.isInteger(parseInt(targetToken.actor.system.stats.strain.value)) ? parseInt(targetToken.actor.system.stats.strain.value) : 0; let pierce = 0, breach = 0; console.log(weapon) let pierceList = await weapon.system.itemmodifier.filter(w => w.name.toLowerCase().startsWith("pierce")); let breachList = await weapon.system.itemmodifier.filter(w => w.name.toLowerCase().startsWith("breach")); let isStrain = await weapon.system.itemmodifier.filter(w => w.name.toLowerCase().startsWith("stun damage")); if (pierceList.length > 0) pierce = pierceList[0].system.rank; > if (breachList.length > 0) breach = breachList[0].system.rank * 10; > let leftoverSoak = (soak - (pierce + breach)); leftoverSoak = (leftoverSoak 0) ? 0 : leftoverSoak; let baseDamage = (weapon.system.damage?.adjusted) ? weapon.system.damage.adjusted : weapon.system.damage.value; let extraDamage = parseInt(message.rolls[0].ffg.success); let totalDamage = parseInt(baseDamage + extraDamage); let damageTaken = 0; let damageType = ""; if (isStrain.length > 0) damageTaken = (oldStrain + (totalDamage - leftoverSoak)); damageType = 'system.stats.strain.value'; > else damageTaken = (oldWounds + (totalDamage - leftoverSoak)); damageType = 'system.stats.wounds.value'; > await targetToken.actor.update([damageType]: Math.max(0, parseInt(damageTaken))>); > else ui.notifications.info("No tokens targeted."); > > else ui.notifications.info("Attacker missed."); > > else ui.notifications.info("Please select a single token."); > > main();
This is a GM only macro due to the need to update the target. Select the token that did the attack. Target the one hit by the attack. Use the macro and it will calculate damage and reduction, accounting for soak, pierce, breach and stun damage, and prompt a window where you can edit damage, reduction, or damage type, depending of your needs. If, for example, you're shooter is a vehicle, and your target is a stormtrooper, then damage needs adjusting, as weapon deal more damage regarding of the scale. The UI lets you modify it, and applies damage (to wounds/hull trauma or strain/system strain) when all modification is done.
/* Description: Auto calculates damage from the last successful attack from the attacker to the selected target. Added Dialog to change values for ongoing attack before result is applied. Author: Rysarian (modified by Malven31) Release Notes v6 - 2024-07-01 - Add Dialog UI to override damage and reduction (soak or shield); handle vehicle attacks; add message iterating to avoid parsing too many messages (only the 15 latest by default, with the "messageIterationLimit" variable) v5 - 2023-12-30 - Prevent healing target if soak is greater than damage v4 - 2023-05-11 - Adjust Breach, Pierce and Stun Damage detection for current OggDude data; replace failing _roll property access with rolls[0]; adjust breach and pierce rank access for Foundry v10 by FloSchwalm v3 - 2023-03-11 - Updated to account for non-integer values for wounds or strain on the token sheet by LostestLocke v2 - 2023-02-01 - Updated for Foundry v10 compatability by Wrycu v1 - - Initial release */ /*********************************** * DO NOT Modify values below here * ***********************************/ let messageIterationLimit = 15; async function main() if (canvas.tokens.controlled.length !== 1) ui.notifications.info("Please select a single token."); return; > let attacker = token.actor; let messageSize = game.messages.size; let currentIndex = messageSize - 1; let minIndex = messageSize - messageIterationLimit; if (minIndex 0) minIndex = 0; > var messageIndex = -1; // Declare messageIndex outside of the loop var attackerType = ""; // Get message to access damages, weapons and attributes while (currentIndex >= minIndex) if (currentIndex 0) break; > const message = game.messages.contents[currentIndex]; if (message.rolls[0]) if (message.speaker.actor === attacker.id) messageIndex = currentIndex; attackerType = "ground"; break; > else if ( message.rolls.length && message.rolls[0].data.parent._id === attacker.id ) messageIndex = currentIndex; attackerType = "vehicle"; break; > > currentIndex--; > if (messageIndex === -1) ui.notifications.info("No Valid Damage Message Found."); return; > let message = game.messages.contents[messageIndex]; let weapon = message.rolls[0].data; if (message.rolls[0].ffg.success 0) ui.notifications.info("Attacker missed."); return; > let targets = message.user.targets; if (targets.size > 0) let targetToken = await canvas.tokens.get(targets.ids[0]); let targetType = targetToken.actor.system.stats["soak"] !== undefined ? "ground" : "vehicle"; var damage = 0; var strain = ""; var woundOrTrauma = ""; var isStun = false; var leftoverSoak = 0; var totalDamage = 0; var oldWounds = 0; var oldStrain = 0; // Handle cases where target is a ground actor or a vehicle actor if (targetType === "ground") // Ground damage handling let soak = parseInt(targetToken.actor.system.stats.soak.value); oldWounds = Number.isInteger( parseInt(targetToken.actor.system.stats.wounds.value) ) ? parseInt(targetToken.actor.system.stats.wounds.value) : 0; oldStrain = Number.isInteger( parseInt(targetToken.actor.system.stats.strain.value) ) ? parseInt(targetToken.actor.system.stats.strain.value) : 0; let pierce = 0, breach = 0; let pierceList = await weapon.system.itemmodifier.filter((w) => w.name.toLowerCase().startsWith("pierce") ); let breachList = await weapon.system.itemmodifier.filter((w) => w.name.toLowerCase().startsWith("breach") ); let isStrain = await weapon.system.itemmodifier.filter((w) => w.name.toLowerCase().startsWith("stun damage") ); isStrain += await weapon.system.itemmodifier.filter((w) => w.name.toLowerCase().startsWith("stun quality") ); if (pierceList.length > 0) pierce = pierceList[0].system.rank; > if (breachList.length > 0) breach = breachList[0].system.rank * 10; > leftoverSoak = soak - (pierce + breach); leftoverSoak = leftoverSoak 0 ? 0 : leftoverSoak; let baseDamage = weapon.system.damage?.adjusted ? weapon.system.damage.adjusted : weapon.system.damage.value; let extraDamage = parseInt(message.rolls[0].ffg.success); totalDamage = parseInt(baseDamage + extraDamage); strain = "system.stats.strain.value"; woundOrTrauma = "system.stats.wounds.value"; damage = Math.max(0, parseInt(totalDamage)); > else if (targetType === "vehicle") // Vehicle damage handling let armour = parseInt(targetToken.actor.system.stats.armour.value); oldWounds = Number.isInteger( parseInt(targetToken.actor.system.stats.hullTrauma.value) ) ? parseInt(targetToken.actor.system.stats.hullTrauma.value) : 0; oldStrain = Number.isInteger( parseInt(targetToken.actor.system.stats.systemStrain.value) ) ? parseInt(targetToken.actor.system.stats.systemStrain.value) : 0; leftoverSoak = armour; leftoverSoak = leftoverSoak 0 ? 0 : leftoverSoak; let baseDamage = weapon.system.damage?.adjusted ? weapon.system.damage.adjusted : weapon.system.damage.value; let extraDamage = parseInt(message.rolls[0].ffg.success); totalDamage = parseInt(baseDamage + extraDamage); strain = "system.stats.systemStrain.value"; woundOrTrauma = "system.stats.hullTrauma.value"; damage = Math.max(0, parseInt(totalDamage)); > // Confirmation dialog let d = new Dialog( title: "Damage Application", content: ` this width: 500px; > pi color: #232e24; > pt color: #060521; font-weight: bold; min-width: 350px; width: 350px; >Informations
background-color: #ebe8e478; margin: 1.2em 12px; padding: 1px 5px; margin-top: 0; margin-bottom: 0; margin-left: 5px; margin-right: 5px; "> This is a sum up of ongoing damage calculation. "Damage Done" : damage dealt (before reduction). "Damage Reduction" : reduce damage (soak or amour/shields). "Damage Type" : target damage type. Attacker Name : $attacker.name> Target Name : $targetToken.actor.name>Damage Modification
Damage Done : $damage>" data-dtype="Integer" /> Damage Reduction : $leftoverSoak>" data-dtype="Integer" /> Damage Type : isStun === false ? 'selected="selected"' : "" >" value="wounds">Wounds / Hull Trauma isStun === true ? 'selected="selected"' : "" >" value="strain">Strain / Mechanical Stress function change(param) switch (param) case "wounds": isStun = false; break; case "strain": isStun = true; break; > > `, buttons: one: icon: ' ', label: "Apply Damage", callback: (html) => // Get dialog fields, damage let damageChosen; damageChosen = parseInt( html.find(".damage").val(), 10 ); if (isNaN(damageChosen)) damageChosen = 0; > let leftoverSoakChosen; leftoverSoakChosen = parseInt( html.find(".soak").val(), 10 ); if (isNaN(damageChosen)) damageChosen = 0; > var damageTaken = 0; // Get select item, wounds / hull trauma or strain / mechanical strain var e = document.getElementById("woundsOrStrain"); var selectedValue = e.value; var damageTypeChosen = ""; if (selectedValue === "wounds") damageTypeChosen = woundOrTrauma; damageTaken = oldWounds + (totalDamage - leftoverSoakChosen); damage = Math.max(0, parseInt(damageTaken)); > else damageTypeChosen = strain; damageTaken = oldStrain + (totalDamage - leftoverSoakChosen); // Handle minion and rival case, no strain, so go wounds instead if ( targetType === "ground" && targetToken.actor.system.stats["strain"] !== undefined ) damageTypeChosen = woundOrTrauma; damageTaken = oldWounds + (totalDamage - leftoverSoakChosen); > > // Finally update target attribute targetToken.actor.update( [damageTypeChosen]: damageTaken, >); >, >, two: icon: ' ', label: "Cancel", callback: () => console.log("Closed"), >, >, default: "two", close: () => console.log( "This always is logged no matter which option is chosen" ), >, myDialogOptions ); d.render(true); > else ui.notifications.info("No tokens targeted."); > > const myDialogOptions = width: 440, >; main();
This is a GM only macro. You may have noticed that career data for specializations and signature abilities, as well as species data for talents becomes broken after reach system update. This macro re-links the data so purchasing with XP works again.
/* Description: Re-links career and species data after a system update. Author: Wrycu Release Notes v1 - 2024-08-28 - Initial release */ /*********************************** * DO NOT Modify values below here * ***********************************/ for (const actor of game.actors) if (actor.type === "character") let career = actor.items.find(i => i.type === "career"); if (career) let careerUpdateData = 'specializations': >, 'signatureabilities': >, >; for (const specialization of Object.values(career.system.specializations)) let foundSpecialization = fromUuidSync(specialization.source); if (!foundSpecialization) let updatedSpecialization = game.packs.get('starwarsffg.oggdudespecializations').index.find(i => i.name === specialization.name); if (updatedSpecialization) careerUpdateData['specializations'][updatedSpecialization._id] = name: updatedSpecialization.name, source: updatedSpecialization.uuid, id: updatedSpecialization._id, broken: false, >; careerUpdateData['specializations'][`-=$specialization.id>`] = null; > > > for (const signatureAbility of Object.values(career.system.signatureabilities)) let foundSignatureAbility = fromUuidSync(signatureAbility.source); if (!foundSignatureAbility) let updatedSignatureAbility = game.packs.get('starwarsffg.oggdudesignatureabilities').index.find(i => i.name === signatureAbility.name); if (updatedSignatureAbility) careerUpdateData['signatureabilities'][updatedSignatureAbility._id] = name: updatedSignatureAbility.name, source: updatedSignatureAbility.uuid, id: updatedSignatureAbility._id, broken: false, >; careerUpdateData['signatureabilities'][`-=$signatureAbility.id>`] = null; > > > career.update(system: careerUpdateData>); > let species = actor.items.find(i => i.type === "species"); if (species) let speciesUpdateData = 'talents': >, >; for (const talent of Object.values(species.system.talents)) let foundTalent = fromUuidSync(talent.source); if (!foundTalent) let updatedTalent = game.packs.get('starwarsffg.oggdudetalents').index.find(i => i.name === talent.name); if (updatedTalent) speciesUpdateData['talents'][updatedTalent._id] = name: updatedTalent.name, source: updatedTalent.uuid, id: updatedTalent._id, >; speciesUpdateData['talents'][`-=$talent.id>`] = null; > > > species.update(system: speciesUpdateData>); > > >