Last active
June 21, 2026 21:14
-
-
Save CodaBool/b7dbbffb4fd9b1454ee76c2f25e6922f to your computer and use it in GitHub Desktop.
relationship macro
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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