forked from GitHub-Mirrors/foundry-sw5e
Updating to Push Class skills on level up
Update from DND5E beta for Class Skills on level up and other various fixups
This commit is contained in:
parent
67ba5b2d2d
commit
53d7284596
36 changed files with 1537 additions and 440 deletions
|
@ -64,13 +64,6 @@ export default class Actor5e extends Actor {
|
|||
|
||||
/** @override */
|
||||
prepareBaseData() {
|
||||
|
||||
// Compute initial ability score modifiers in base data since these may be referenced
|
||||
for (let abl of Object.values(this.data.data.abilities)) {
|
||||
abl.mod = Math.floor((abl.value - 10) / 2);
|
||||
}
|
||||
|
||||
// Type-specific base data preparation
|
||||
switch ( this.data.type ) {
|
||||
case "character":
|
||||
return this._prepareCharacterData(this.data);
|
||||
|
@ -107,6 +100,7 @@ export default class Actor5e extends Actor {
|
|||
}
|
||||
|
||||
// Ability modifiers and saves
|
||||
const dcBonus = Number.isNumeric(data.bonuses.power.dc) ? parseInt(data.bonuses.power.dc) : 0;
|
||||
const saveBonus = Number.isNumeric(bonuses.save) ? parseInt(bonuses.save) : 0;
|
||||
const checkBonus = Number.isNumeric(bonuses.check) ? parseInt(bonuses.check) : 0;
|
||||
for (let [id, abl] of Object.entries(data.abilities)) {
|
||||
|
@ -115,6 +109,7 @@ export default class Actor5e extends Actor {
|
|||
abl.saveBonus = saveBonus;
|
||||
abl.checkBonus = checkBonus;
|
||||
abl.save = abl.mod + abl.prof + abl.saveBonus;
|
||||
abl.dc = 8 + abl.mod + abl.prof + dcBonus;
|
||||
|
||||
// If we merged saves when transforming, take the highest bonus here.
|
||||
if (originalSaves && abl.proficient) {
|
||||
|
@ -135,7 +130,7 @@ export default class Actor5e extends Actor {
|
|||
init.total = init.mod + init.prof + init.bonus;
|
||||
|
||||
// Prepare power-casting data
|
||||
data.attributes.powerdc = this.getPowerDC(data.attributes.powercasting);
|
||||
this._computePowercastingDC(this.data);
|
||||
this._computePowercastingProgression(this.data);
|
||||
}
|
||||
|
||||
|
@ -165,22 +160,6 @@ export default class Actor5e extends Actor {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return the power DC for this actor using a certain ability score
|
||||
* @param {string} ability The ability score, i.e. "str"
|
||||
* @return {number} The power DC
|
||||
*/
|
||||
getPowerDC(ability) {
|
||||
const actorData = this.data.data;
|
||||
let bonus = getProperty(actorData, "bonuses.power.dc");
|
||||
bonus = Number.isNumeric(bonus) ? parseInt(bonus) : 0;
|
||||
ability = actorData.abilities[ability];
|
||||
const prof = actorData.attributes.prof;
|
||||
return 8 + (ability ? ability.mod : 0) + prof + bonus;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getRollData() {
|
||||
const data = super.getRollData();
|
||||
|
@ -194,6 +173,101 @@ export default class Actor5e extends Actor {
|
|||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return the features which a character is awarded for each class level
|
||||
* @param cls {Object} Data object for class, equivalent to Item5e.data or raw compendium entry
|
||||
* @return {Promise<Item5e[]>} Array of Item5e entities
|
||||
*/
|
||||
static async getClassFeatures(cls) {
|
||||
const level = cls.data.levels;
|
||||
const className = cls.name.toLowerCase();
|
||||
|
||||
// Get the configuration of features which may be added
|
||||
const clsConfig = CONFIG.SW5E.classFeatures[className];
|
||||
let featureIDs = clsConfig["features"][level] || [];
|
||||
const subclassName = cls.data.subclass.toLowerCase().slugify();
|
||||
|
||||
// Identify subclass features
|
||||
if ( subclassName !== "" ) {
|
||||
const subclassConfig = clsConfig["subclasses"][subclassName];
|
||||
if ( subclassConfig !== undefined ) {
|
||||
const subclassFeatureIDs = subclassConfig["features"][level];
|
||||
if ( subclassFeatureIDs ) {
|
||||
featureIDs = featureIDs.concat(subclassFeatureIDs);
|
||||
}
|
||||
}
|
||||
else console.warn("Invalid subclass: " + subclassName);
|
||||
}
|
||||
|
||||
// Load item data for all identified features
|
||||
const features = await Promise.all(featureIDs.map(id => fromUuid(id)));
|
||||
|
||||
// Class powers should always be prepared
|
||||
for ( const feature of features ) {
|
||||
if ( feature.type === "power" ) {
|
||||
const preparation = feature.data.data.preparation;
|
||||
preparation.mode = "always";
|
||||
preparation.prepared = true;
|
||||
}
|
||||
}
|
||||
return features;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async updateEmbeddedEntity(embeddedName, data, options={}) {
|
||||
const createItems = embeddedName === "OwnedItem" ? await this._createClassFeatures(data) : [];
|
||||
let updated = await super.updateEmbeddedEntity(embeddedName, data, options);
|
||||
if ( createItems.length ) await this.createEmbeddedEntity("OwnedItem", createItems);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create additional class features in the Actor when a class item is updated.
|
||||
* @private
|
||||
*/
|
||||
async _createClassFeatures(updated) {
|
||||
let toCreate = [];
|
||||
for (let u of updated instanceof Array ? updated : [updated]) {
|
||||
const item = this.items.get(u._id);
|
||||
if (!item || (item.data.type !== "class")) continue;
|
||||
const classData = duplicate(item.data);
|
||||
let changed = false;
|
||||
|
||||
// Get and create features for an increased class level
|
||||
const newLevels = getProperty(u, "data.levels");
|
||||
if (newLevels && (newLevels > item.data.data.levels)) {
|
||||
classData.data.levels = newLevels;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Get features for a newly changed subclass
|
||||
const newSubclass = getProperty(u, "data.subclass");
|
||||
if (newSubclass && (newSubclass !== item.data.data.subclass)) {
|
||||
classData.data.subclass = newSubclass;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Get the new features
|
||||
if ( changed ) {
|
||||
const features = await Actor5e.getClassFeatures(classData);
|
||||
if ( features.length ) toCreate.push(...features);
|
||||
}
|
||||
}
|
||||
|
||||
// De-dupe created items with ones that already exist (by name)
|
||||
if ( toCreate.length ) {
|
||||
const existing = new Set(this.items.map(i => i.name));
|
||||
toCreate = toCreate.filter(c => !existing.has(c.name));
|
||||
}
|
||||
return toCreate
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Data Preparation Helpers */
|
||||
/* -------------------------------------------- */
|
||||
|
@ -313,6 +387,31 @@ export default class Actor5e extends Actor {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Compute the powercasting DC for all item abilities which use power DC scaling
|
||||
* @param {object} actorData The actor data being prepared
|
||||
* @private
|
||||
*/
|
||||
_computePowercastingDC(actorData) {
|
||||
|
||||
// Compute the powercasting DC
|
||||
const data = actorData.data;
|
||||
data.attributes.powerdc = data.attributes.powercasting ? data.abilities[data.attributes.powercasting].dc : 10;
|
||||
|
||||
// Apply powercasting DC to any power items which use it
|
||||
for ( let i of this.items ) {
|
||||
const save = i.data.data.save;
|
||||
if ( save?.ability ) {
|
||||
if ( save.scaling === "power" ) save.dc = data.attributes.powerdc;
|
||||
else if ( save.scaling !== "flat" ) save.dc = data.abilities[save.scaling]?.dc ?? 10;
|
||||
const ability = CONFIG.SW5E.abilities[save.ability];
|
||||
i.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare data related to the power-casting capabilities of the Actor
|
||||
* @private
|
||||
|
@ -419,7 +518,7 @@ export default class Actor5e extends Actor {
|
|||
// [Optional] add Currency Weight
|
||||
if ( game.settings.get("sw5e", "currencyWeight") ) {
|
||||
const currency = actorData.data.currency;
|
||||
const numCoins = Object.values(currency).reduce((val, denom) => val += denom, 0);
|
||||
const numCoins = Object.values(currency).reduce((val, denom) => val += Math.max(denom, 0), 0);
|
||||
weight += Math.round((numCoins * 10) / CONFIG.SW5E.encumbrance.currencyPerWeight) / 10;
|
||||
}
|
||||
|
||||
|
@ -591,12 +690,12 @@ export default class Actor5e extends Actor {
|
|||
|
||||
// Update Actor data
|
||||
if ( usesSlots && consumeSlot && (lvl > 0) ) {
|
||||
const slots = parseInt(this.data.data.powers[consumeSlot].value);
|
||||
const slots = parseInt(this.data.data.powers[consumeSlot]?.value);
|
||||
if ( slots === 0 || Number.isNaN(slots) ) {
|
||||
return ui.notifications.error(game.i18n.localize("SW5E.PowerCastNoSlots"));
|
||||
}
|
||||
await this.update({
|
||||
[`data.powers.${consumeSlot}.value`]: Math.max(parseInt(this.data.data.powers[consumeSlot].value) - 1, 0)
|
||||
[`data.powers.${consumeSlot}.value`]: Math.max(slots - 1, 0)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -610,7 +709,7 @@ export default class Actor5e extends Actor {
|
|||
// Initiate ability template placement workflow if selected
|
||||
if ( placeTemplate && item.hasAreaTarget ) {
|
||||
const template = AbilityTemplate.fromItem(item);
|
||||
if ( template ) template.drawPreview(event);
|
||||
if ( template ) template.drawPreview();
|
||||
if ( this.sheet.rendered ) this.sheet.minimize();
|
||||
}
|
||||
|
||||
|
@ -1004,19 +1103,26 @@ export default class Actor5e extends Actor {
|
|||
await this.updateEmbeddedEntity("OwnedItem", updateItems);
|
||||
|
||||
// Display a Chat Message summarizing the rest effects
|
||||
let restFlavor;
|
||||
switch (game.settings.get("sw5e", "restVariant")) {
|
||||
case 'normal': restFlavor = game.i18n.localize("SW5E.ShortRestNormal"); break;
|
||||
case 'gritty': restFlavor = game.i18n.localize(newDay ? "SW5E.ShortRestOvernight" : "SW5E.ShortRestGritty"); break;
|
||||
case 'epic': restFlavor = game.i18n.localize("SW5E.ShortRestEpic"); break;
|
||||
}
|
||||
|
||||
if ( chat ) {
|
||||
|
||||
// Summarize the rest duration
|
||||
let restFlavor;
|
||||
switch (game.settings.get("sw5e", "restVariant")) {
|
||||
case 'normal': restFlavor = game.i18n.localize("SW5E.ShortRestNormal"); break;
|
||||
case 'gritty': restFlavor = game.i18n.localize(newDay ? "SW5E.ShortRestOvernight" : "SW5E.ShortRestGritty"); break;
|
||||
case 'epic': restFlavor = game.i18n.localize("SW5E.ShortRestEpic"); break;
|
||||
}
|
||||
|
||||
// Summarize the health effects
|
||||
let srMessage = "SW5E.ShortRestResultShort";
|
||||
if ((dhd !== 0) && (dhp !== 0)) srMessage = "SW5E.ShortRestResult";
|
||||
|
||||
// Create a chat message
|
||||
ChatMessage.create({
|
||||
user: game.user._id,
|
||||
speaker: {actor: this, alias: this.name},
|
||||
flavor: restFlavor,
|
||||
content: game.i18n.format("SW5E.ShortRestResult", {name: this.name, dice: -dhd, health: dhp})
|
||||
content: game.i18n.format(srMessage, {name: this.name, dice: -dhd, health: dhp})
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1036,13 +1142,13 @@ export default class Actor5e extends Actor {
|
|||
* Take a long rest, recovering HP, HD, resources, and power slots
|
||||
* @param {boolean} dialog Present a confirmation dialog window whether or not to take a long rest
|
||||
* @param {boolean} chat Summarize the results of the rest workflow as a chat message
|
||||
* @param {boolean} newDay Whether the long rest carries over to a new day
|
||||
* @return {Promise} A Promise which resolves once the long rest workflow has completed
|
||||
*/
|
||||
async longRest({dialog=true, chat=true}={}) {
|
||||
async longRest({dialog=true, chat=true, newDay=true}={}) {
|
||||
const data = this.data.data;
|
||||
|
||||
// Maybe present a confirmation dialog
|
||||
let newDay = false;
|
||||
if ( dialog ) {
|
||||
try {
|
||||
newDay = await LongRestDialog.longRestDialog({actor: this});
|
||||
|
@ -1120,12 +1226,17 @@ export default class Actor5e extends Actor {
|
|||
case 'epic': restFlavor = game.i18n.localize("SW5E.LongRestEpic"); break;
|
||||
}
|
||||
|
||||
// Determine the chat message to display
|
||||
if ( chat ) {
|
||||
let lrMessage = "SW5E.LongRestResultShort";
|
||||
if((dhp !== 0) && (dhd !== 0)) lrMessage = "SW5E.LongRestResult";
|
||||
else if ((dhp !== 0) && (dhd === 0)) lrMessage = "SW5E.LongRestResultHitPoints";
|
||||
else if ((dhp === 0) && (dhd !== 0)) lrMessage = "SW5E.LongRestResultHitDice";
|
||||
ChatMessage.create({
|
||||
user: game.user._id,
|
||||
speaker: {actor: this, alias: this.name},
|
||||
flavor: restFlavor,
|
||||
content: game.i18n.format("SW5E.LongRestResult", {name: this.name, health: dhp, dice: dhd})
|
||||
content: game.i18n.format(lrMessage, {name: this.name, health: dhp, dice: dhd})
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1356,4 +1467,16 @@ export default class Actor5e extends Actor {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* DEPRECATED METHODS */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since sw5e 0.97
|
||||
*/
|
||||
getPowerDC(ability) {
|
||||
console.warn(`The Actor5e#getPowerDC(ability) method has been deprecated in favor of Actor5e#data.data.abilities[ability].dc`);
|
||||
return this.data.data.abilities[ability]?.dc;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -163,18 +163,18 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
};
|
||||
|
||||
// Format a powerbook entry for a certain indexed level
|
||||
const registerSection = (sl, i, label, level={}) => {
|
||||
const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
|
||||
powerbook[i] = {
|
||||
order: i,
|
||||
label: label,
|
||||
usesSlots: i > 0,
|
||||
canCreate: owner && (i >= 1),
|
||||
canCreate: owner,
|
||||
canPrepare: (data.actor.type === "character") && (i >= 1),
|
||||
powers: [],
|
||||
uses: useLabels[i] || level.value || 0,
|
||||
slots: useLabels[i] || level.max || 0,
|
||||
override: level.override || 0,
|
||||
dataset: {"type": "power", "level": i},
|
||||
uses: useLabels[i] || value || 0,
|
||||
slots: useLabels[i] || max || 0,
|
||||
override: override || 0,
|
||||
dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode},
|
||||
prop: sl
|
||||
};
|
||||
};
|
||||
|
@ -187,7 +187,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
return max;
|
||||
}, 0);
|
||||
|
||||
// Structure the powerbook for every level up to the maximum which has a slot
|
||||
// Level-based powercasters have cantrips and leveled slots
|
||||
if ( maxLevel > 0 ) {
|
||||
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
for (let lvl = 1; lvl <= maxLevel; lvl++) {
|
||||
|
@ -195,9 +195,18 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
|
||||
}
|
||||
}
|
||||
|
||||
// Pact magic users have cantrips and a pact magic section
|
||||
if ( levels.pact && levels.pact.max ) {
|
||||
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
registerSection("pact", sections.pact, CONFIG.SW5E.powerPreparationModes.pact, levels.pact);
|
||||
if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
const l = levels.pact;
|
||||
const config = CONFIG.SW5E.powerPreparationModes.pact;
|
||||
registerSection("pact", sections.pact, config, {
|
||||
prepMode: "pact",
|
||||
value: l.value,
|
||||
max: l.max,
|
||||
override: l.override
|
||||
});
|
||||
}
|
||||
|
||||
// Iterate over every power item, adding powers to the powerbook by section
|
||||
|
@ -206,17 +215,24 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
let s = power.data.level || 0;
|
||||
const sl = `power${s}`;
|
||||
|
||||
// Powercasting mode specific headings
|
||||
// Specialized powercasting modes (if they exist)
|
||||
if ( mode in sections ) {
|
||||
s = sections[mode];
|
||||
if ( !powerbook[s] ){
|
||||
registerSection(mode, s, CONFIG.SW5E.powerPreparationModes[mode], levels[mode]);
|
||||
const l = levels[mode] || {};
|
||||
const config = CONFIG.SW5E.powerPreparationModes[mode];
|
||||
registerSection(mode, s, config, {
|
||||
prepMode: mode,
|
||||
value: l.value,
|
||||
max: l.max,
|
||||
override: l.override
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Higher-level power headings
|
||||
// Sections for higher-level powers which the caster "should not" have, but power items exist for
|
||||
else if ( !powerbook[s] ) {
|
||||
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], levels[sl]);
|
||||
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]});
|
||||
}
|
||||
|
||||
// Add the power to the relevant heading
|
||||
|
@ -508,13 +524,6 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
itemData = scroll.data;
|
||||
}
|
||||
|
||||
// Upgrade the number of class levels a character has
|
||||
if ( (itemData.type === "class") && ( this.actor.itemTypes.class.find(c => c.name === itemData.name)) ) {
|
||||
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
||||
const lvl = cls.data.data.levels;
|
||||
return cls.update({"data.levels": Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level)})
|
||||
}
|
||||
|
||||
// Create the owned item as normal
|
||||
// TODO remove conditional logic in 0.7.x
|
||||
if (isNewerVersion(game.data.version, "0.6.9")) return super._onDropItemCreate(itemData);
|
||||
|
@ -843,4 +852,4 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
// Return a copy of the extracted data
|
||||
return duplicate(itemData);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import ActorSheet5e from "./base.js";
|
||||
import Actor5e from "../entity.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for player character type actors in the SW5E system.
|
||||
|
@ -253,4 +254,32 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
yes: () => this.actor.convertCurrency()
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
|
||||
// Upgrade the number of class levels a character has
|
||||
// and add features
|
||||
if ( itemData.type === "class" ) {
|
||||
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
||||
const classWasAlreadyPresent = !!cls;
|
||||
|
||||
// Add new features for class level
|
||||
if ( !classWasAlreadyPresent ) {
|
||||
Actor5e.getClassFeatures(itemData).then(features => {
|
||||
this.actor.createEmbeddedEntity("OwnedItem", features);
|
||||
});
|
||||
}
|
||||
|
||||
// If the actor already has the class, increment the level instead of creating a new item
|
||||
if ( classWasAlreadyPresent ) {
|
||||
const lvl = cls.data.data.levels;
|
||||
return cls.update({"data.levels": Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level)})
|
||||
}
|
||||
}
|
||||
|
||||
super._onDropItemCreate(itemData);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue