-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathwheels.js
262 lines (247 loc) · 10.9 KB
/
wheels.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
import { LegActions } from "./legActions.js";
// If adding a combatant that has a lair action, make a hidden temporary
// combatant at init 20 to remind.
Hooks.on("createCombatant", async (currToken, options, id) => {
if (!game.user.isGM) return;
await new Promise((r) => setTimeout(r, 200));
// Does this actor have lair actions?
const currCombat = currToken.parent.data;
if (
hasProperty(currToken, "actor.data.data.resources.lair.value") &&
currToken.actor.data.data.resources.lair.value
) {
// Have we already made a Lair Action Combatant?
if (!currCombat.combatants.find((combatant) => combatant.token.name === "Lair Action")) {
const lair = game.actors.getName("Lair Action");
let actor;
if (!lair) {
let actorData = await Actor.createDocuments([
{
name: "Lair Action",
type: "npc",
img: "icons/svg/mystery-man.svg",
},
]);
actor = actorData[0];
} else {
actor = lair;
}
const lairToken = canvas.scene.tokens.getName("Lair Action");
let token;
if (!lairToken) {
const tokenData = duplicate(actor.data.token);
tokenData.x = 0;
tokenData.y = 0;
tokenData.disposition = 0;
tokenData.img = "icons/svg/mystery-man.svg";
tokenData.actorId = actor.id;
tokenData.actorLink = true;
const tData = await canvas.scene.createEmbeddedDocuments("Token", [tokenData]);
token = tData[0];
} else {
token = lairToken;
}
const combatant = await game.combat.createEmbeddedDocuments("Combatant", [
{
tokenId: token.id,
hidden: true,
initiative: 20,
"flags.legendary-training-wheels.lairaction": true,
},
]);
}
}
});
// Keep track of when it's a player's turn and when it is a legendary
// creatures turn.
Hooks.on("updateCombat", async (currCombat, currOptions, isDiff, userID) => {
if (!game.user.isGM) return;
let turn = currOptions.turn;
// Find out where our non-legendary characters are in initiative.
// And find our NPCs with legendary actions.
let nonLegendTurns = [];
let legends = [];
let legUpdates = [];
currCombat.turns.forEach(async (combatant, pos) => {
if (getProperty(combatant, "actor.data.name") === "Lair Action") return;
const legMax =
getProperty(combatant, "token.actorData.data.resources.legact.max") ||
getProperty(combatant, "actor.data.data.resources.legact.max");
// Track legendary turns
if (legMax) {
// Reset legendary actions when we get to the start of next turn AFTER the legendary.
if (turn === pos + 1 || (turn === 0 && pos === currCombat.turns.length - 1)) {
legUpdates.push({ _id: combatant.token.id, "actorData.data.resources.legact.value": legMax });
}
// We can only use legendary actions at the END of ANOTHER creature's turn
// this means that it can't be the start of the creature's turn that follows ours :(
// because that is *actually* the trigger for the end of OUR turn
if (pos + 1 !== turn) {
// This is necessary to cover the circular edge case
if (turn === 0 && pos === currCombat.turns.length - 1) {
// Just ignore this
} else {
legends.push(combatant);
}
}
} else {
nonLegendTurns.push(pos);
}
});
if (legUpdates) {
// Update to reset leg actions
await canvas.scene.updateEmbeddedDocuments("Token", legUpdates);
}
if (!nonLegendTurns.length) return; // If no non-legendaries, don't prompt for legActions
if (!legends.length) return; // If no creatures with legendary actions, don't continue.
// Determine if any lairActions are present (there should only be 1, but whatever)
const lairActions = currCombat.turns
.filter((combatant) => combatant.data.flags["legendary-training-wheels"]?.lairaction)
.map((combatant) => {
return combatant.id;
});
// Find the id of the previous turn
let prevTurnId;
if (currCombat.turn === 0) {
prevTurnId = currCombat.turns[currCombat.turns.length - 1].id;
} else {
prevTurnId = currCombat.turns[currCombat.turn - 1].id;
}
// If it's our custom "Lair Action" token, then return early
if (lairActions.includes(prevTurnId)) return;
// An "Active" legend is any creature with legendary actions
// who CAN USE thier legendary actions.
// For this to be the case, it can NOT be the turn
// that immediately follows that legend's turn.
let activeLegends = legends.map((legendary) => {
// Remaining Legendary Actions
const rLA =
getProperty(legendary, "token.data.actorData.data.resources.legact.value") ||
getProperty(legendary, "actor.data.data.resources.legact.value");
// Maximum Legendary Actions
const mLA =
getProperty(legendary, "token.data.actorData.data.resources.legact.max") ||
getProperty(legendary, "actor.data.data.resources.legact.max");
// Legendary Items/Abilities/Features/etc.
const lItems =
getProperty(legendary, "token.data.actorData.data.items") || getProperty(legendary, "actor.data.items");
return {
name: getProperty(legendary, "name"),
remainingLegActions: rLA,
maxLegActions: mLA,
legendaryItems: lItems.filter((litem) => {
if (hasProperty(litem, "data.data.activation") && litem.data.data.activation.type === "legendary") {
return litem;
}
}),
img: getProperty(legendary, "token.data.img"),
_id: getProperty(legendary, "token.id"),
};
});
let myLegends = [];
for (const legend of activeLegends) {
if (parseInt(legend.remainingLegActions) !== 0) {
myLegends.push(legend);
}
}
const notifType = game.settings.get("legendary-training-wheels", "notificationType");
if (notifType === "dialog") {
let form = new LegActions(myLegends);
form.render(true);
} else if (notifType === "toasts") {
for (const myLeg of myLegends) {
ui.notifications.notify(
myLeg.name +
" has " +
myLeg.remainingLegActions +
"/" +
myLeg.maxLegActions +
" Legendary Actions remaining this round."
);
}
}
});
Hooks.on("createChatMessage", async (message, options, id) => {
if (!game.user.isGM) return;
if (message._roll) {
// BetterRolls 5e
const isBRSave = $(message.data.content).find("img")?.attr("title")?.toLowerCase()?.includes("save");
if (
(getProperty(message, "data.flavor") && getProperty(message, "data.flavor").includes("Saving Throw")) ||
isBRSave
) {
let legTok;
if (isBRSave) {
legTok = canvas.scene.tokens.getName(getProperty(message, "data.speaker.alias"));
} else {
legTok = canvas.scene.tokens.get(getProperty(message, "data.speaker.token"));
}
// Find legRes property. Either from the token first or from the actor
const legRes =
getProperty(legTok, "actorData.data.resources.legres.value") ||
getProperty(legTok, "actor.data.data.resources.legres.value");
if (legRes) {
// Do the same with finding the max
const maxRes =
getProperty(legTok, "actorData.data.resources.legres.max") ||
getProperty(legTok, "actor.data.data.resources.legres.max");
const notifType = game.settings.get("legendary-training-wheels", "notificationType");
if (notifType === "dialog") {
let use = false;
let d = new Dialog({
title: "Legendary Resistance",
content:
`A Saving Throw has been detected. Would you like to use Legendary Resistance to ignore it? You have ` +
legRes +
`/` +
maxRes +
` resistances remaining.`,
buttons: {
yes: {
icon: '<i class="fas fa-check"></i>',
label: "Yes",
callback: () => (use = true),
},
no: {
icon: '<i class="fas fa-close"></i>',
label: "No",
callback: () => (use = false),
},
},
close: async (html) => {
if (use) {
let legActor = game.actors.get(getProperty(message, "data.speaker.actor"));
ChatMessage.create({
user: game.user.id,
speaker: ChatMessage.getSpeaker({ legActor }),
content: "If the creature fails a saving throw, it can choose to succeed instead.",
flavor: "has used Legendary Resistance to succeed on the save!",
type: CONST.CHAT_MESSAGE_TYPES.IC,
});
await legTok.data.document.update({
"actorData.data.resources.legres.value": legRes - 1,
});
}
},
}).render(true);
} else if (notifType === "toasts") {
ui.notifications.notify(legTok.name + " still has Legendary Resistances. " + legRes + "/" + maxRes);
}
}
}
}
});
Hooks.once("init", () => {
game.settings.register("legendary-training-wheels", "notificationType", {
name: "Level of Notifications",
hint: "How often do you want to be bothered?",
scope: "world",
config: true,
type: String,
choices: {
dialog: "Dialog popups with buttons!",
toasts: "All messages will be toasts. No buttons.",
},
default: "dialog",
});
});