Skip to content

Instantly share code, notes, and snippets.

@CodaBool
Last active June 21, 2026 21:14
Show Gist options
  • Select an option

  • Save CodaBool/b7dbbffb4fd9b1454ee76c2f25e6922f to your computer and use it in GitHub Desktop.

Select an option

Save CodaBool/b7dbbffb4fd9b1454ee76c2f25e6922f to your computer and use it in GitHub Desktop.
relationship macro
// source Eddie Dover
// https://github.com/EddieDover/mothership-crew-relationships/blob/master/src/lang/en.json
const OWNER = CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
const RELATIONSHIPS = {
categories: {
1: "Companions",
2: "Professional",
3: "Romantic",
4: "Obligation & Debt",
5: "Rivalry",
6: "Ambivalent",
7: "Shared Trauma",
8: "Unlikely Bond",
},
results: {
1: {
0: "They have been through hell together before.",
1: "They are close friends and confidants.",
2: "They have similar hobbies or interests.",
3: "They have an unspoken code of mutual protection.",
4: "One is trying to corrupt the other.",
5: "They trust each other with their lives.",
6: "They serve as a moral compass for the other.",
7: "They are thick as thieves.",
8: "One reminds the other of a lost friend or family member.",
9: "They have a pact to get back somewhere together, no matter what.",
},
2: {
0: "They worked together at the same company.",
1: "One is a mentor to the other.",
2: "They cover for the other's mistakes.",
3: "One got the job the other wanted.",
4: "They are professional rivals in direct competition.",
5: "One is clearly better at the job, and it's a sore point.",
6: "They share complaints about management or the current assignment.",
7: "They collaborated on a successful—and profitable—project.",
8: "Their methods and philosophies clash constantly.",
9: "They have a shared history of failing together.",
},
3: {
0: "They are secretly involved with each other.",
1: "They are in a relationship together.",
2: "They are former lovers, and it's still awkward.",
3: "They had a bad breakup in their past.",
4: "There is an unspoken attraction between them.",
5: "They have a friends with benefits arrangement.",
6: "They're stuck in a will-they-won't-they dynamic.",
7: "They hooked up once, and it's complicated.",
8: "One is trying to win the other back.",
9: "They are trying to keep things strictly professional.",
},
4: {
0: "One saved the other's life, creating a life debt.",
1: "One knows a deeply damaging secret about the other.",
2: "One is blackmailing the other.",
3: "They owe the same dangerous people or corporation money.",
4: "One took the fall for the other's mistake or crime.",
5: "There is an unresolved debt between them.",
6: "They are bound by a past betrayal.",
7: "One helped the other escape the law.",
8: "They are caught in the same conspiracy.",
9: "There is a major unspoken obligation between them.",
},
5: {
0: "They have a begrudging mutual respect for each other.",
1: "They are bitter enemies with a shared history.",
2: "They are former friends turned rivals.",
3: "They are in a friendly competition with each other.",
4: "One is a one-sided rival, but the other is oblivious.",
5: "They want what the other has.",
6: "They compete over nearly everything.",
7: "They are from opposing clique.",
8: "They are forced to work together despite their animosity.",
9: "One blames the other for a personal loss.",
},
6: {
0: "They have never really talked much, but are now stuck together.",
1: "There is mutual indifference toward them.",
2: "They run in different social circles.",
3: "They know of each other, but that's it.",
4: "One is actively avoiding the other.",
5: "Something unspoken makes them awkward around each other.",
6: "Their communication is stilted and formal.",
7: "One is a minor celebrity to the other, but they don't know it.",
8: "They have nothing in common to bond over.",
9: "They get the feeling they've met before, but can't place where.",
},
7: {
0: "They are the only survivors of a past disaster.",
1: "They both witnessed something they shouldn't have.",
2: "One saved the other from a horrific monster.",
3: "They survived the same medical experiment.",
4: "They share the memory of friend's death.",
5: "They were both part of an accident with a high body count.",
6: "They are bonded by a terrifying encounter from their past.",
7: "They both suffer from a recurring nightmare from a shared event.",
8: "One knows the other's most shameful secret.",
9: "They are both running from the same past mistake.",
},
8: {
0: "One comes from privilege, the other from poverty.",
1: "They are two vastly different people who found a mutual interest.",
2: "One is the other's hero, but they don't know it.",
3: "They are both hiding a secret.",
4: "They constantly get on each other's nerves, but they work well together.",
5: "They were rivals, but have since gained respect for one another.",
6: "One is a jaded veteran, the other a naive rookie.",
7: "They are united by a common enemy.",
8: "They are the only ones who can perform a specific task together.",
9: "One is the other's confidant for a secret they are both keeping.",
},
},
};
const playerUsers = game.users.filter(u => !u.isGM);
const ownedActors = game.actors
.filter(actor =>
playerUsers.some(user => actor.testUserPermission(user, OWNER))
)
.sort((a, b) => a.name.localeCompare(b.name));
if (!ownedActors.length) {
ui.notifications.warn("No player-owned actors found.");
return;
}
const actorRows = ownedActors.map(actor => {
const owners = playerUsers
.filter(user => actor.testUserPermission(user, OWNER))
.map(user => user.name)
.join(", ");
const img = actor.img
? `<img src="${actor.img}" width="40" height="40" style="border-radius:50%; object-fit:cover;">`
: "";
return `
<tr style="border-bottom: 1px solid rgba(255,255,255,0.1); background: black; color: white;">
<td style="padding: 6px; vertical-align: top;">
<input type="checkbox" name="actors" value="${actor.id}">
</td>
<td style="padding: 6px; display:flex; align-items:center; gap:10px;">
${img}
<div>
<strong>${actor.name}</strong>
<div style="opacity: .7; font-size: 0.85em;">
Owner${owners.includes(",") ? "s" : ""}: ${owners}
</div>
</div>
</td>
</tr>
`;
}).join("");
const getSelectedActors = button => {
const form = button.form;
const selectedActorIds = Array.from(
form.querySelectorAll(`input[name="actors"]:checked`)
).map(input => input.value);
return selectedActorIds
.map(id => game.actors.get(id))
.filter(Boolean);
};
const buttons = Object.entries(RELATIONSHIPS.categories).map(([key, name]) => ({
action: key,
label: `${key}. ${name}`,
callback: async (event, button, dialog) => {
const actors = getSelectedActors(button);
if (!actors.length) {
ui.notifications.warn("No actors selected.");
return false;
}
const names = actors.map(actor => actor.name);
const roll = Math.floor(Math.random() * 10);
const result = RELATIONSHIPS.results[key][roll];
await ChatMessage.create({
speaker: ChatMessage.getSpeaker(),
content: `
<h2>${name}</h2>
<p style="font-size: 1.2em" class="hint">${names.join(" & ")}</p>
<p style="font-size: 1.4em">${result}</p>
`,
});
},
}));
await foundry.applications.api.DialogV2.wait({
window: {
title: "Relationship Table",
},
content: `
<div>
<p style="margin-bottom: 0.5rem;">
Select the player-owned actors involved in this relationship.
</p>
<div class="form-group" style="max-height: 400px; overflow-y: auto; padding-right: 4px;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th style="text-align:left; padding: 6px; width: 80px;">Use?</th>
<th style="text-align:left; padding: 6px;">Actor</th>
</tr>
</thead>
<tbody>
${actorRows}
</tbody>
</table>
</div>
<p style="text-align: center; opacity: .7; margin-top: 0.75rem;">
Then select a relationship category.
</p>
</div>
`,
buttons,
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment