/
pc.js
221 lines (199 loc) · 10.4 KB
/
pc.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
import ZweihanderBaseActor from "./base-actor";
import * as ZweihanderUtils from "../../utils";
import ZweihanderActorConfig from "../../apps/actor-config";
export default class ZweihanderPC extends ZweihanderBaseActor {
prepareDerivedData(actorData) {
const noWarn = CONFIG.ZWEI.NO_WARN || actorData._id === null;
const configOptions = ZweihanderActorConfig.getConfig(actorData);
// set up utility variables
const data = actorData.data;
data.tier = CONFIG.ZWEI.tiers[actorData.items.filter(i => i.type === 'profession').length];
// calculate primary attribute bonuses (first digit)
Object.values(data.stats.primaryAttributes).forEach(a => a.bonus = Math.floor(a.value / 10));
// add ancestral modifiers to the primary attribute bonuses
const ancestry = actorData.items.find(i => i.type === 'ancestry');
const applyBonusModifiers = (list, mod, source) => list?.forEach?.(a => {
const attr = ZweihanderUtils.primaryAttributeMapping[a.slice(1,2)];
//TODO should be safe to remove this after migration of existing data
if (!attr) {
ui?.notifications?.warn(`"${a.trim()}" is not a valid primary attribute bonus abbreviation in ${source}!`);
return;
}
data.stats.primaryAttributes[attr].bonus += mod;
})
// ancestral bonus advances
if (ancestry) {
applyBonusModifiers(ancestry.data.data.ancestralModifiers.positive, +1, `ancestry ${ancestry.name} of actor ${actorData.name}`);
applyBonusModifiers(ancestry.data.data.ancestralModifiers.negative, -1, `ancestry ${ancestry.name} of actor ${actorData.name}`);
}
// professional bonus advances
actorData.items.filter(i => i.type === 'profession').forEach(p => {
const advancesList = p.data.data.bonusAdvances?.filter?.(a => a.purchased)?.map?.(a => a.name) ?? [];
applyBonusModifiers(advancesList, +1, `profession ${p.name} of actor ${actorData.name}`);
})
// assign inital peril & damage
const sa = data.stats.secondaryAttributes;
sa.perilThreshold = {};
sa.damageThreshold = {};
sa.perilThreshold.value = data.stats.primaryAttributes[configOptions.pthAttribute].bonus + 3;
// get equipped armor
const equippedArmor = actorData.items
.filter(a => a.type === 'armor' && a.data.data.equipped);
// calculate total damage threshold modifier from armor
// according to the rule book, this doesn't stack, so we choose the maximium!
// to account for shields with "maker's mark" quality, we need to implement active effects
const maxEquippedArmor = equippedArmor?.[
ZweihanderUtils.argMax(equippedArmor.map(a => a.data.data.damageThresholdModifier))
];
const damageModifier = maxEquippedArmor?.data?.data?.damageThresholdModifier ?? 0;
sa.damageThreshold.value = data.stats.primaryAttributes[configOptions.dthAttribute].bonus + damageModifier;;
// active effects tracking Proof of Concept
sa.damageThreshold.base = data.stats.primaryAttributes[configOptions.dthAttribute].bonus;
sa.damageThreshold.mods = [];
if (maxEquippedArmor && damageModifier > 0) {
sa.damageThreshold.mods.push(
{ source: `${maxEquippedArmor.name} DTM`, type: 'add', argument: damageModifier }
);
}
// get peril malus
const basePerilCurrent = data.stats.secondaryAttributes.perilCurrent.value
const effectivePerilCurrent = this.getEffectivePerilLadderValue(basePerilCurrent, configOptions.isIgnoredPerilLadderValue);
data.stats.secondaryAttributes.perilCurrent.effectiveValue = effectivePerilCurrent;
const perilMalus = this.getPerilMalus(effectivePerilCurrent);
// calculate special actions underlying values
const calcSecondayAttributeSpecialActionValue = (secAttr, name) => {
const skill = actorData.items.find(item =>
item.type === 'skill' && item.name === secAttr.associatedSkill
);
if (skill) {
const primAttr = skill.data.data.associatedPrimaryAttribute.toLowerCase();
secAttr.value = data.stats.primaryAttributes[primAttr].value + Math.max(0, skill.data.data.bonus - perilMalus);
} else {
noWarn || ui?.notifications?.warn(`Can't find associated skill ${secAttr.associatedSkill} for secondary attribute ${name}!`);
}
}
//calculate parry
calcSecondayAttributeSpecialActionValue(data.stats.secondaryAttributes.parry, "Parry");
//calculate parry
calcSecondayAttributeSpecialActionValue(data.stats.secondaryAttributes.dodge, "Dodge");
//calculate parry
calcSecondayAttributeSpecialActionValue(data.stats.secondaryAttributes.magick, "Magick");
// encumbrance calculations...
// assign encumbrance from equipped trappings
const carriedTrappings = actorData.items
.filter(i => ['trapping', 'armor', 'weapon'].includes(i.type) && i.data.data.carried);
const nine4one = game.settings.get("zweihander", "encumbranceNineForOne");
const smallTrappingsEnc = !nine4one ? 0 : Math.floor(
carriedTrappings
.filter(t => t.data.data.encumbrance === 0)
.map(t => t.data.data.quantity || 0)
.reduce((a, b) => a + b, 0) / 9
);
const normalTrappingsEnc = carriedTrappings
.filter(t => t.data.data.encumbrance !== 0)
.map(t => t.data.data.encumbrance * (t.data.data.quantity ?? 1))
.reduce((a, b) => a + b, 0);
// assign encumbrance from currency
const currencyEnc = Math.floor(
Object.values(data.currency).reduce((a, b) => a + b, 0) / 1000
);
const enc = data.stats.secondaryAttributes.encumbrance = {};
// assign initial encumbrance threshold
enc.value = data.stats.primaryAttributes.brawn.bonus + 3 + configOptions.encumbranceModifier;
// assign current encumbrance
enc.current = smallTrappingsEnc + normalTrappingsEnc + currencyEnc;
// assign overage
enc.overage = Math.max(0, enc.current - enc.value)
// calculate initiative
const ini = data.stats.secondaryAttributes.initiative = {};
ini.value = data.stats.primaryAttributes[configOptions.intAttribute].bonus + 3 + configOptions.initiativeModifier;
ini.overage = enc.overage;
ini.current = Math.max(0, ini.value - ini.overage);
// calculate movement
const mov = data.stats.secondaryAttributes.movement = {};
mov.value = data.stats.primaryAttributes[configOptions.movAttribute].bonus + 3 + configOptions.movementModifier;
mov.overage = enc.overage;
mov.current = Math.max(0, mov.value - mov.overage);
}
async _preCreate(actorData, options, user, that) {
// roll primary attributes for new pc
await super._preCreate(actorData, options, user, that);
const pas = actorData.data.stats.primaryAttributes;
const update = {};
if (CONFIG.ZWEI.primaryAttributes.every(pa => pas[pa].value === 0)) {
for (let pa of CONFIG.ZWEI.primaryAttributes) {
const roll = await (new Roll('2d10+35')).evaluate();
update[`data.stats.primaryAttributes.${pa}.value`] = roll.total;
}
}
if (!update.token) update.token = {}
update.token.actorLink = true;
await actorData.update(update);
}
async _preUpdate(changed, options, user, actor) {
const actorData = actor.data;
const oldDamage = actorData.data.stats.secondaryAttributes.damageCurrent.value;
const newDamage = changed.data?.stats?.secondaryAttributes?.damageCurrent?.value;
const injurySettingEnabled = game.settings.get("zweihander", "injuryPrompt");
if (injurySettingEnabled && (newDamage !== undefined) && (newDamage < oldDamage) && ((newDamage > 0) && (newDamage <=3))) {
let injuryToRoll = newDamage == 3 ? "Moderate" : newDamage == 2 ? "Serious" : "Grievous";
await Dialog.confirm({
title: `${actor.name}: Injury Configuration`,
content: `<h4>You are ${injuryToRoll}ly Wounded. Roll for Injury?</h4>`,
yes: () => this._rollInjury(injuryToRoll, actor),
defaultYes: false
});
}
}
//@todo: refactor into another class, possibly dice.js
async _rollInjury(injuryToRoll, actor) {
const injuryChaosRoll = new Roll(`${injuryToRoll === 'Moderate' ? 1 : injuryToRoll === 'Serious' ? 2 : 3}d6`, actor.data.data);
const rollResult = await injuryChaosRoll.evaluate();
await rollResult.toMessage({ speaker: ChatMessage.getSpeaker({ actor: actor }), content: injuryChaosRoll.total, flavor: `Attempts to avoid Injury...` });
const injurySustained = rollResult.terms[0].results.some(die => die.result === 6);
if (!injurySustained) return;
const tablesPack = game.packs.get("zweihander.zh-gm-tables");
const tablesIndex = await tablesPack.getIndex();
const injuryTableEntry = tablesIndex.find(table => ZweihanderUtils.normalizedIncludes(table.name, injuryToRoll));
const injuryTable = await tablesPack.getDocument(injuryTableEntry._id);
const diceRoll = await injuryTable.roll();
const finalResult = await injuryTable.draw({ "roll": diceRoll })
}
async createEmbeddedDocuments(embeddedName, data, context, actor) {
if (embeddedName === "Item") {
const filteredData = [];
let ancestryAttached = actor.data.items.some(i => i.type === 'ancestry');
const actorProfessions = actor.data.items.filter(i => i.type === 'profession');
let numberOfProfessionsAttached = actorProfessions.length;
for (let item of data) {
if (item.type === "profession") {
const previousTiersCompleted = actorProfessions
.map(profession => profession.data.data.completed)
.every(value => value === true);
const allTiersAssigned = numberOfProfessionsAttached == 3;
const dragDroppedOwnProfession = actorProfessions.some(p => p._id === item._id);
if (allTiersAssigned && !dragDroppedOwnProfession) {
ui.notifications.error("A character may not enter more than 3 Professions.");
} else if (!previousTiersCompleted && !dragDroppedOwnProfession) {
ui.notifications.error("A character must complete the previous Tier before entering a new Profession.");
}
if (!allTiersAssigned && previousTiersCompleted) {
filteredData.push(item);
numberOfProfessionsAttached++;
}
} else if (item.type === "ancestry") {
if (ancestryAttached) {
ui.notifications.error("A character may not possess more than 1 Ancestry.");
} else {
filteredData.push(item);
ancestryAttached = true;
}
} else {
filteredData.push(item);
}
}
return filteredData;
}
return data;
}
}