-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathammo-tracker.js
165 lines (148 loc) · 5.14 KB
/
ammo-tracker.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
const FLAG_NAMESPACE = 'rc-spent-ammo';
export class GameAmmoTracker {
constructor(optActors) {
if (!optActors) {
optActors = game.users.players
.map(({data: {character}}) => game.actors.get(character));
}
this.trackers = optActors.map(actor => new ActorAmmoTracker(actor));
}
/** Records ammo at the start of a combat. */
async startCombat() {
console.log('Ammo Tracker starting combat');
return Promise.all(this.trackers.map(t => t.startCombat()));
}
/** Records ammo quantities at the end of a combat. */
async endCombat() {
console.log('Ammo Tracker ending combat');
return Promise.all(this.trackers.map(t => t.endCombat()));
}
/**
* Recover half of spent ammo (rounded down) for specified actor.
* @param {string} actorId
*/
async recoverAmmo(actorId) {
await this.trackers.find(({actor: {_id}}) => _id == actorId).recoverAmmo();
}
}
class ActorAmmoTracker {
constructor(actor) {
this.actor = actor;
this._ammoRecords = null;
}
/**
* Produces a list of all of the actor's ammo items.
* @return {Array<Item>}
*/
get ammoItems() {
return this.actor.items.filter(
item => item.data.data.consumableType == 'ammo'
);
}
/** Records ammo at the start of a combat. */
async startCombat() {
this._ammoRecords = {};
for (const item of this.ammoItems) {
// Record initial state
await item.setFlag(
FLAG_NAMESPACE,
'startQuantity',
item.data.data.quantity,
);
await item.setFlag(
FLAG_NAMESPACE,
'endQuantity',
// If the item is used up and removed from inventory, it won't be
// updated, so we set this to 0 and assume it'll be overwritten during
// `endCombat`
0,
);
this._ammoRecords[item._id] = item;
}
}
/** Records ammo quantities at the end of a combat. */
async endCombat() {
// Set the `endQuantity` flag on all remaining ammo items
for (const item of this.ammoItems) {
// TODO: This will not handle ammo that is fully spent and removed from
// inventory
if (item._id in this._ammoRecords) {
await item.setFlag(FLAG_NAMESPACE, 'endQuantity', item.data.data.quantity);
}
}
await this.notifySpentAmmo();
}
/**
* Lists ammo items that were consumed during combat.
* @return {Array<Item>}
*/
get spentAmmo() {
return Object.values(this._ammoRecords)
// Determine spent/recoverable ammo
.map(item => {
const startQuantity = item.getFlag(FLAG_NAMESPACE, 'startQuantity');
const endQuantity = item.getFlag(FLAG_NAMESPACE, 'endQuantity') || 0;
const spent = startQuantity - endQuantity;
const recoverable = Math.floor(spent / 2);
return {item, startQuantity, endQuantity, spent, recoverable};
})
// Ignore unspent ammo
.filter(({spent}) => spent > 0);
}
/** Recover half of spent ammo (rounded down). */
async recoverAmmo() {
const recoveredLines = [];
// Recover ammo where possible, updating quantity and recording message
for (const ammo of this.spentAmmo) {
const {item, recoverable} = ammo;
if (recoverable > 0) {
const newQuantity = item.data.data.quantity + recoverable;
await item.update({
data: {quantity: newQuantity}
});
// This prevents accidental double-clicks
await item.setFlag(FLAG_NAMESPACE, 'startQuantity', newQuantity);
await item.setFlag(FLAG_NAMESPACE, 'endQuantity', newQuantity);
recoveredLines.push(`${recoverable}x ${item.name} recovered`);
}
};
// Post a message if anything was recovered
if (recoveredLines.length) {
await ChatMessage.create({
content: [
`${this.actor.name} spends a minute recovering ammo`,
...recoveredLines,
].join('\n'),
speaker: ChatMessage.getSpeaker({actor: this.actor}),
});
} else {
await ChatMessage.create({
content: 'You already recovered this ammo!',
speaker: ChatMessage.getSpeaker({alias: "Ammo Tracker"}),
type: CHAT_MESSAGE_TYPES.WHISPER, // https://foundryvtt.com/api/foundry.js.html#line83
whisper: ChatMessage.getWhisperRecipients(this.actor.name)
});
}
}
/** Sends a whisper message about spent and recoverable ammo. */
async notifySpentAmmo() {
// Build a blurb for each type of ammo spent
const chatParts = this.spentAmmo.map(
({startQuantity, endQuantity, spent, recoverable, item}) => [
`${item.name}: ${startQuantity} -> ${endQuantity}`,
`<b>Spent:</b> ${spent}`,
`<b>Recoverable:</b> ${recoverable}`,
].join('\n'));
const recoverBtn = `<button data-actor-id='${
this.actor._id
}' class='rc-ammo-tracker-recover'>Recover Ammo</button>`;
if (chatParts.length) {
await ChatMessage.create({
content: [...chatParts, recoverBtn].join('<hr>'),
speaker: ChatMessage.getSpeaker({alias: "Ammo Tracker"}),
type: CHAT_MESSAGE_TYPES.WHISPER, // https://foundryvtt.com/api/foundry.js.html#line83
whisper: ChatMessage.getWhisperRecipients(this.actor.name)
});
}
}
}