-
Notifications
You must be signed in to change notification settings - Fork 79
Helpful macros
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.
Code
/*
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:
Code
/*
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 & damage (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.
To use it, select a token and run the macro. It will default to Critical Injuries or Critical Damage depending on the type of token selected, but you can override it in the popup.
Code & Details
/*
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 - <unknown> - 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 `<option value="${table._id}" selected>${table.name}</option>`;
} else {
return `<option value="${table._id}">${table.name}</option>`;
}
}
})
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: `<p>Select table and modifier</p>
<div class="grid grid-3col">
<div>Modifier:
<input name="modifier" class="modifier" style="width:50%" type="text" placeholder="` + modifier + `" value="` + modifier + `" data-dtype="String" />
</div>
<div>Durable: ` + durableRank + `
</div>
<div>
Table: <select class="crittable">${tables.join("")}</select>
</div>
</div>`,
buttons: {
one: {
icon: '<i class="fas fa-check"></i>',
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: '<i class="fas fa-times"></i>',
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:
Code
Taken from https://github.com/saethone/SWFFGSMacros.
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 - <unknown> - Initial release
*/
/***********************************
* DO NOT Modify values below here *
***********************************/
let d = new Dialog({
title: "Random Credits",
content:'<p>Enter a rough number of credits per player, and the total number of players</p><div>Credits: <input name="credits" class="credits" style="width:40%" type="text" value="25" data-dtype="String" /></div><div>Players: <input name="players" class="players" style="width:40%" type="text" value="4" data-dtype="String" /></div>',
buttons: {
one: {
icon: '<i class="fas fa-check"></i>',
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 = '<div>The players found <span style="font-weight:bold; background:#FFFF99">' + creditstotal + '</span> Credits! Thats <span style="font-weight:bold; background:#FFFF99">' + credits + '</span> for each of the <span style="font-weight:bold; background:#FFFF99">' + players + '</span> players.</div>';
chatData = {
user:game.user.id,
speaker:game.user,
content:message
};
ChatMessage.create(chatData, {});
}
},
two: {
icon: '<i class="fas fa-times"></i>',
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)
Code
- Create the macro included below, being careful to update the paths to your image files
- BE SURE TO NAME THIS MACRO
COMBAT
- BE SURE TO NAME THIS MACRO
- Install the VTT Hook Macros module if it isn't already installed
- Create a journal entry called
Hook Macros
(this is case sensitive) - Set the contents of the journal entry to
@Hook[renderCombatTracker] @Macro[Combat]
- Your combat tracker will now auto-update names and icons
/*
Description: Automatically renames combat slots to generic names
Author: Wrycu
Release Notes
v2 - 2023-02-01 - Updated for Foundry v10 compatability by Wrycu
v1 - <unknown> - 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
Code
/*
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.
Code
/*
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: `<p>Select status to apply</p>`,
buttons: {
one: {
icon: '<i class="fas fa-arrow-up"></i>',
label: "Boost die",
callback: (html) => {
set_token_status(base_path + images['boost'])
}
},
two: {
icon: '<i class="fas fa-arrow-down"></i>',
label: "Setback die",
callback: (html) => {
set_token_status(base_path + images['setback'])
}
},
three: {
icon: '<i class="fas fa-times"></i>',
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.
Code
/*
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 - <unknown> - 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: `
<form>
<div class="form-group">
<label>This will update the actor's token to update bars to the standard version.</label>
</div>
</form>
`,
buttons: {
yes: {
icon: "<i class='fas fa-check'></i>",
label: `Apply Changes`,
callback: (html) => {
applyChanges()
}
},
no: {
icon: "<i class='fas fa-times'></i>",
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.
Code
/*
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 - <unknown> - 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 += "<tr><td><strong>" + token.actor.name + "</strong></td><td>";
adversaryList += _adversaryType;
adversaryList += "</td><td>";
if (_adversaryValue != 0) { adversaryList += +_adversaryValue };
adversaryList += "</td><td>";
if (_defencemelee != 0) { adversaryList += _defencemelee; };
adversaryList += "</td><td>";
if (_defenceranged != 0) { adversaryList += _defenceranged; };
adversaryList += "</td><td>";
if (_soak != 0) { adversaryList += _soak; };
adversaryList += "</td></tr>";
}
}
};
new Dialog({
title: `Adversary levels`,
content: `
<style>
td {
text-align: center;
border: 1px black solid;
}
th {
text-align: center;
border: 1px black solid;
font-size: 18px;
}
</style>
<form>
<div class="form-group">
<label>Those have Adversary talent:</label>
</div>
<div class="form-group">
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Adv.</th>
<th>Def-M</th>
<th>Def-R</th>
<th>Soak</th>
</tr>
` + adversaryList + `
</table>
</div>
</form>
`,
buttons: {
Cancel: {
icon: "<i class='fas fa-times'></i>",
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.
Code
/*
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.
Code
/*
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 += `<tr>
<td>${One}</td>
<td class="Empty"> </td>
<td class="Empty"> </td>
<td class="Empty"> </td>
<td class="Empty"> </td>
</tr>`
break;
case 1:
talentPyramid += `<tr>
<td>${One}</td>
<td>${Two} </td>
<td class="Empty"> </td>
<td class="Empty"> </td>
<td class="Empty"> </td>
</tr>`
break;
case 2:
talentPyramid += `<tr>
<td>${One}</td>
<td>${Two} </td>
<td>${Three} </td>
<td class="Empty"> </td>
<td class="Empty"> </td>
</tr>`
break;
case 3:
talentPyramid += `<tr>
<td>${One}</td>
<td>${Two} </td>
<td>${Three} </td>
<td>${Four} </td>
<td class="Empty"> </td>
</tr>`
break;
default:
talentPyramid += `<tr>
<td>${One}</td>
<td>${Two} </td>
<td>${Three} </td>
<td>${Four} </td>
<td>${Five} </td>
</tr>`
break;
}
}
let d = new Dialog({
title: "Talent Pyramid",
content: `<style type="text/css">
.Empty {
background-color: black;
border: 1px solid black;
color: white;
}
td {
text-align: center;
}
</style>
<table border="1">
<tr>
<th>Tier 1</th>
<th>Tier 2</th>
<th>Tier 3</th>
<th>Tier 4</th>
<th>Tier 5</th>
</tr>
${talentPyramid}
</table>`,
buttons: {
close: {
icon: '<i class="fas fa-times"></i>',
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.
Code
/*
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
v4 - 2024-11-25 - Check if actor has Balance talent and add force pool max to the skill check
Change speaker alias to speaker actor for chat message
Improve render for the dialog: Remove source of skill and display them in grid
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 balance_talent = get_talent(using_actor, "balance");
let skillForce = 0;
if (balance_talent.length != 0) {
skillForce = get_force(using_actor);
}
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, skillForce);
let discipline_pool = build_pool(discipline_skill, discipline_char, skillForce);
let cool_checked = "";
let discipline_checked = "";
if (is_skill_better(cool_pool, discipline_pool))
cool_checked = " checked";
else
discipline_checked = " checked";
let popup_content = `
<h2>Select Recovery Skill</h2>
<div class="grid grid-2col">
<div class="form-group">
<label for="Cool">
<input type="radio" id="Cool" name="selected_skill" value="Cool"${cool_checked} />
${cool_skill.label}
<div id="cool_preview"></div>
</label>
</div>
<div class="form-group">
<label for="Discipline">
<input type="radio" id="Discipline" name="selected_skill" value="Discipline"${discipline_checked} />
${discipline_skill.label}
<div id="discipline_preview"></div>
</label>
</div>
</div>
`;
let d = new Dialog({
title: `Post-Encounter ${qlocal("SWFFG.Strain")} Recovery`,
content: popup_content,
buttons: {
recover: {
icon: '<i class="fas fa-check"></i>',
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: {
actor: using_actor
},
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 = `
<p>Healed ${healed_strain} ${qlocal("SWFFG.Strain")}
`;
if (surplus_advantage > 0)
message_content += ` with ${surplus_advantage} surplus ${qlocal("SWFFG.Advantage")}`;
message_content += `</p>`;
ChatMessage.create({
speaker: {
alias: using_actor.name
},
content: message_content
});
update_strain(using_actor, current_strain - healed_strain);
}
},
close: {
icon: '<i class="fas fa-times"></i>',
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, skillForce)
{
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 + skillForce,
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,
});
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;
}
//get a given talent from the actor
function get_talent(using_actor, talent_name) {
return using_actor?.talentList.filter(item => item.name.toLowerCase() === talent_name);
}
function get_force(using_actor) {
return using_actor?.system?.stats.forcePool.max;
}
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.
Code
/*
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 - <unknown> - 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.
Code
/*
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 - <unknown> - 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: `
<style>
this {
width: 500px;
}
pi {
color: #232e24;
}
pt {
color: #060521;
font-weight: bold;
min-width: 350px;
width: 350px;
}
</style>
<h3>Informations</h3>
<blockquote style="
background-color: #ebe8e478;
margin: 1.2em 12px;
padding: 1px 5px;
margin-top: 0;
margin-bottom: 0;
margin-left: 5px;
margin-right: 5px;
">
<div>
<pi>
This is a sum up of ongoing damage calculation.<br>
<b>"Damage Done"</b> : damage dealt (before reduction).<br>
<b>"Damage Reduction"</b> : reduce damage (soak or amour/shields).<br>
<b>"Damage Type"</b> : target damage type.
</pi>
</div>
</blockquote>
<hr>
<div>
<pt style="margin: 1.2em 12px;margin-top: 20px;min-width: 200px">
Attacker Name : </pt>
${attacker.name}
</div>
<div>
<pt style="margin: 1.2em 12px;margin-top: 20px;min-width: 200px">
Target Name : </pt>
${targetToken.actor.name}
</div>
<hr>
<h3>Damage Modification</h3>
<div>
<pt style="margin: 1.2em 12px;margin-top: 20px;min-width: 200px">
Damage Done : </pt>
<input name="damage" id="damage" class="damage" style="width:40%" input type="number" value="${damage}" data-dtype="Integer" />
</div>
<div>
<pt style="margin: 1.2em 12px;margin-top: 20px;min-width: 200px">
Damage Reduction : </pt>
<input name="soak" id="soak" class="soak" style="width:40%" input type="number" value="${leftoverSoak}" data-dtype="Integer" />
</div>
<div>
<div>
<pt style="margin: 1.2em 12px;margin-top: 20px;min-width: 200px">
Damage Type : </pt>
<select id="woundsOrStrain" name="selection" onChange="change(this.value);">
<option ${
isStun === false ? 'selected="selected"' : ""
}" value="wounds">Wounds / Hull Trauma</option>
<option ${
isStun === true ? 'selected="selected"' : ""
}" value="strain">Strain / Mechanical Stress</option>
</select>
</div>
</div>
<hr>
<script>
function change(param) {
switch (param) {
case "wounds":
isStun = false;
break;
case "strain":
isStun = true;
break;
}
}
</script>
`,
buttons: {
one: {
icon: '<i class="fas fa-check"></i>',
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: '<i class="fas fa-times"></i>',
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.
Code
/*
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});
}
}
}
- Getting started
- Helpful resources
- Frequently Asked Questions
- New feature walkthrough
- Genesys