Merge pull request #30 from unrealkakeman89/Class-Skills-and-other-updates

Class skills and other updates
This commit is contained in:
CK 2020-10-08 08:36:09 -04:00 committed by GitHub
commit 324e201071
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
149 changed files with 1337 additions and 505 deletions

View file

@ -64,6 +64,7 @@
"SW5E.AlignmentNL": "Neutral Light",
"SW5E.AlignmentBN": "Balanced Neutral",
"SW5E.Archetypes": "Archetypes",
"SW5E.Appearance": "Appearance",
"SW5E.ArmorClass": "Armor Class",
"SW5E.AC": "AC",
"SW5E.ArmorProperties": "Armor Properties",
@ -72,6 +73,7 @@
"SW5E.ArmorProperAnchor": "Anchor",
"SW5E.ArmorProperAvoidant": "Avoidant",
"SW5E.ArmorProperBarbed": "Barbed",
"SW5E.ArmorProperBulky": "Bulky",
"SW5E.ArmorProperCharging": "Charging",
"SW5E.ArmorProperConcealing": "Concealing",
"SW5E.ArmorProperCumbersome": "Cumbersome",
@ -84,6 +86,7 @@
"SW5E.ArmorProperLightweight": "Lightweight",
"SW5E.ArmorProperMagnetic": "Magnetic",
"SW5E.ArmorProperObscured": "Obscured",
"SW5E.ArmorProperObtrusive": "Obtrusive",
"SW5E.ArmorProperPowered": "Powered",
"SW5E.ArmorProperReactive": "Reactive",
"SW5E.ArmorProperRegulated": "Regulated",
@ -93,6 +96,7 @@
"SW5E.ArmorProperSilent": "Silent",
"SW5E.ArmorProperSpiked": "Spiked",
"SW5E.ArmorProperSteadfast": "Steadfast",
"SW5E.ArmorProperStrength": "Strength Rqmt.",
"SW5E.ArmorProperVersatile": "Versatile",
"SW5E.Attack": "Attack",
"SW5E.AttackPl": "Attacks",
@ -101,6 +105,7 @@
"SW5E.Attuned": "Attuned",
"SW5E.Background": "Background",
"SW5E.Biography": "Biography",
"SW5E.Bonds": "Bonds",
"SW5E.BonusAbilityCheck": "Global Ability Check Bonus",
"SW5E.BonusAbilitySave": "Global Saving Throw Bonus",
"SW5E.BonusAbilitySkill": "Global Skill Check Bonus",
@ -173,6 +178,8 @@
"SW5E.ConsumableMedpac": "Medpac",
"SW5E.ConsumableTrinket": "Trinket",
"SW5E.ConsumableTechnology": "Technology",
"SW5E.ConsumableForce": "Force Points",
"SW5E.ConsumableTech": "Tech Points",
"SW5E.ConsumableUseWarnStart": "This consumable has",
"SW5E.ConsumableUseWarnEnd": "of the current unit",
"SW5E.ConsumableUnitWarn": "units remaining",
@ -194,7 +201,6 @@
"SW5E.DamRes": "Damage Resistances",
"SW5E.DamVuln": "Damage Vulnerabilities",
"SW5E.Damage": "Damage",
"SW5E.DamageRoll": "Damage Roll",
"SW5E.DamageAcid": "Acid",
"SW5E.DamageCold": "Cold",
"SW5E.DamageEnergy": "Energy",
@ -204,10 +210,10 @@
"SW5E.DamageKinetic": "Kinetic",
"SW5E.DamageLightning": "Lightning",
"SW5E.DamageNecrotic": "Necrotic",
"SW5E.DamagePhysical": "Physical",
"SW5E.DamagePoison": "Poison",
"SW5E.DamagePsychic": "Psychic",
"SW5E.DamageSonic": "Sonic",
"SW5E.DamageRoll": "Damage Roll",
"SW5E.Day": "Day",
"SW5E.DeathSave": "Death Saves",
"SW5E.DeathSaveCriticalSuccess": "{name} critically succeeded on a death saving throw and has regained 1 Hit Point!",
@ -227,6 +233,7 @@
"SW5E.DistSelf": "Self",
"SW5E.DistTouch": "Touch",
"SW5E.Duration": "Duration",
"SW5E.Effects": "Effects",
"SW5E.EquipmentBonus": "Magical Bonus",
"SW5E.EquipmentClothing": "Clothing",
"SW5E.EquipmentHeavy": "Heavy Armor",
@ -241,9 +248,13 @@
"SW5E.Exhaustion": "Exhaustion",
"SW5E.Expertise": "Expertise",
"SW5E.FeatureActionRecharge": "Action Recharge",
"SW5E.Flaws": "Flaws",
"SW5E.ItemTypeArchetype": "Archetype",
"SW5E.ItemTypeClass": "Class",
"SW5E.ItemTypeClassPl": "Class Levels",
"SW5E.ItemTypeClassFeat": "Class Feature",
"SW5E.ItemTypeClassFeats": "Class Features",
"SW5E.ItemTypeConsumable": "Consumable",
"SW5E.ItemTypeConsumablePl": "Consumables",
"SW5E.ItemTypeContainer": "Container",
@ -308,12 +319,15 @@
"SW5E.Healing": "Healing",
"SW5E.HealingTemp": "Healing (Temporary)",
"SW5E.Health": "Health",
"SW5E.HP": "Health",
"SW5E.HealthConditions": "Health Conditions",
"SW5E.HealthFormula": "Health Formula",
"SW5E.HPFormula": "Health Formula",
"SW5E.HitDice": "Hit Dice",
"SW5E.HitDiceRoll": "Roll Hit Dice",
"SW5E.HitDiceUsed": "Hit Dice Used",
"SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!",
"SW5E.Ideals": "Ideals",
"SW5E.Identified": "Identified",
"SW5E.Initiative": "Initiative",
"SW5E.Inspiration": "Inspiration",
@ -479,6 +493,9 @@
"SW5E.LongRestEpic": "Long Rest (1 hour)",
"SW5E.LongRestOvernight": "Long Rest (New Day)",
"SW5E.LongRestResult": "{name} takes a long rest and recovers {health} Hit Points and {dice} Hit Dice.",
"SW5E.LongRestResultHitDice": "{name} takes a long rest and recovers {dice} Hit Dice.",
"SW5E.LongRestResultHitPoints": "{name} takes a long rest and recovers {health} Hit Points.",
"SW5E.LongRestResultShort": "{name} takes a long rest.",
"SW5E.Max": "Max",
"SW5E.Modifier": "Modifier",
"SW5E.Name": "Character Name",
@ -489,6 +506,7 @@
"SW5E.OtherFormula": "Other Formula",
"SW5E.PactMagic": "Pact Magic",
"SW5E.Passive": "Passive",
"SW5E.PersonalityTraits": "Personality Traits",
"SW5E.PlaceTemplate": "Place Measured Template",
"SW5E.Polymorph": "Polymorph",
"SW5E.PolymorphAcceptSettings": "Custom Settings",
@ -511,7 +529,9 @@
"SW5E.PolymorphTokens": "Transform all linked tokens?",
"SW5E.PolymorphWarn": "You are not allowed to polymorph this actor!",
"SW5E.PolymorphWildShape": "Wild Shape",
"SW5E.Prepared": "Prepared",
"SW5E.Concentrate": "Concentrate",
"SW5E.Concentrated": "Concentrate",
"SW5E.Price": "Price",
"SW5E.Proficiency": "Proficiency",
"SW5E.Proficient": "Proficient",
@ -534,8 +554,14 @@
"SW5E.RollExample": "e.g. +1d4",
"SW5E.RollMode": "Roll Mode",
"SW5E.RollSituationalBonus": "Situational Bonus?",
"SW5E.Save": "Save",
"SW5E.Save": "Save",
"SW5E.SheetClassCharacter": "Default Character Sheet",
"SW5E.SheetClassNPC": "Default NPC Sheet",
"SW5E.SheetClassVehicle": "Default Vehicle Sheet",
"SW5E.SheetClassItem": "Default Item Sheet",
"SW5E.SavingThrow": "Saving Throw",
"SW5E.SaveDC": "DC {dc} {ability}",
"SW5E.SavePromptTitle": "{ability} Saving Throw",
"SW5E.ScalingFormula": "Scaling Formula",
"SW5E.SchoolLgt": "Light",
@ -556,6 +582,7 @@
"SW5E.ShortRestHint": "Take a short rest? On a short rest you may spend remaining Hit Dice and recover primary or secondary resources.",
"SW5E.ShortRestNoHD": "No Hit Dice remaining",
"SW5E.ShortRestResult": "{name} takes a short rest spending {dice} Hit Dice to recover {health} Hit Points.",
"SW5E.ShortRestResultShort": "{name} takes a short rest.",
"SW5E.ShortRestSelect": "Select Dice to Roll",
"SW5E.Size": "Size",
"SW5E.SizeGargantuan": "Gargantuan",
@ -624,6 +651,8 @@
"SW5E.PowerPrepInnate": "Innate Powercasting",
"SW5E.PowerPrepPrepared": "Prepared",
"SW5E.PowerPrepAlways": "Always Prepared",
"SW5E.PowerPreparationMode": "Power Preparation Mode",
"SW5E.PowerPrepared": "Prepared",
"SW5E.PowerConcentrationMode": "Power Concentration Mode",
"SW5E.PowerConcentrating": "Concentrating",
"SW5E.PowerProgArt": "Artificer",
@ -735,6 +764,7 @@
"SW5E.WeaponMartialB": "Martial Blaster",
"SW5E.WeaponMartialLW": "Martial Lightweapon",
"SW5E.WeaponNatural": "Natural",
"SW5E.WeaponSiege": "Siege",
"SW5E.WeaponPropertiesAmm": "Ammunition",
"SW5E.WeaponPropertiesAut": "Auto",
"SW5E.WeaponPropertiesBur": "Burst",
@ -754,6 +784,7 @@
"SW5E.WeaponPropertiesKen": "Keen",
"SW5E.WeaponPropertiesLgt": "Light",
"SW5E.WeaponPropertiesLum": "Luminous",
"SW5E.WeaponPropertiesMig": "Mighty",
"SW5E.WeaponPropertiesPic": "Piercing",
"SW5E.WeaponPropertiesRan": "Range",
"SW5E.WeaponPropertiesRap": "Rapid",

View file

@ -302,14 +302,14 @@
}
}
input[type="text"] {
input[type="text"],
input[type="number"] {
height: 20px;
max-width: 20px;
margin: 0;
padding: 0;
text-align: center;
}
input[type="checkbox"] {
position: relative;
width: 16px;
@ -317,7 +317,6 @@
margin: 0;
top: 4px;
}
span.sep {
font-size: 12px;
}
@ -421,6 +420,7 @@
padding: 0 5px;
overflow-y: auto;
scrollbar-width: thin;
color: @colorTan;
// Inventory Item
.item {
@ -448,10 +448,10 @@
}
&.rollable:hover .item-image {
background-image: url("/icons/svg/d20-grey.svg") !important;
background-image: url("../../icons/svg/d20-grey.svg") !important;
}
&.rollable .item-image:hover {
background-image: url("/icons/svg/d20-black.svg") !important;
background-image: url("../../icons/svg/d20-black.svg") !important;
}
i.attuned {
@ -484,6 +484,7 @@
.inventory-header {
margin: 2px 0;
padding: 0;
align-items: center;
background: rgba(0, 0, 0, 0.05);
border: @borderGroove;
font-weight: bold;
@ -501,6 +502,11 @@
}
}
// Item names
.item-name {
color: @colorDark;
}
// Item Detail Sections
.item-detail {
flex: 0 0 70px;
@ -688,6 +694,44 @@
// Empty powerbook controls
.powerbook-empty .item-controls { flex: 1; }
/* ----------------------------------------- */
/* Active Effects */
/* ----------------------------------------- */
.effects {
.effect-name{
flex: 2;
align-items: center;
color: @colorDark;
h4 { margin: 0; }
}
.effect-icon {
flex: 0 0 30px;
height: 30px;
margin-right: 5px;
border: none;
}
.effect-source,
.effect-duration {
text-align: center;
border-left: 1px solid @colorFaint;
border-right: 1px solid @colorFaint;
}
.effect-controls {
flex: 0 0 60px;
text-align: right;
}
.effect {
align-items: center;
border-bottom: 1px solid @colorFaint;
&:last-child { border-bottom: none; }
}
}
/* ----------------------------------------- */
/* TinyMCE */
/* ----------------------------------------- */

View file

@ -20,7 +20,9 @@
/* ----------------------------------------- */
// Item Sheet form fields
input[type="text"], select {
input[type="text"],
input[type="number"],
select {
height: calc(100% - 2px);
border: 1px solid @colorTan;
background: rgba(0, 0, 0, 0.05);
@ -28,7 +30,8 @@
}
// Hovered Fields
input[type="text"] {
input[type="text"],
input[type="number"] {
&:hover,
&:focus {
border: 1px solid #111;
@ -157,7 +160,8 @@
/* ----------------------------------------- */
// Input Fields
input[type="text"] {
input[type="text"],
input[type="number"] {
background: none;
border: 1px solid transparent;
&:hover,

View file

@ -149,4 +149,27 @@
}
}
}
/* ----------------------------------------- */
/* Biography */
/* ----------------------------------------- */
.characteristics {
flex: 0 0 180px;
height: 100%;
padding: 0 3px 3px;
label {
flex: 0 0 20px;
.bungeeInline();
font-size: 16px;
font-weight: normal;
line-height: 20px;
text-align: center;
}
textarea {
.openSans();
border: 1px solid @colorFaint;
resize: none;
}
}
}

View file

@ -88,7 +88,9 @@
.details {
// Item Sheet form fields
input[type="text"], select {
input[type="text"],
input[type="number"],
select {
height: 24px;
border: 1px solid @colorTan;
background: rgba(0, 0, 0, 0.05);

View file

@ -6,3 +6,33 @@
@import "character.less";
@import "npc.less";
@import "vehicle.less";
// TODO: Remove number styling after 0.7.x
input[type="number"] {
width: calc(100% - 2px);
min-width: 20px;
height: 26px;
background: rgba(0, 0, 0, 0.05);
padding: 1px 3px;
margin: 0;
color: #191813;
font-family: inherit;
font-size: inherit;
text-align: inherit;
line-height: inherit;
border: 1px solid #7a7971;
border-radius: 3px;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
-moz-appearance: textfield;
&:focus {
box-shadow: 0 0 5px red;
}
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}

View file

@ -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 + data.attributes.prof + dcBonus;
// If we merged saves when transforming, take the highest bonus here.
if (originalSaves && abl.proficient) {
@ -131,11 +126,11 @@ export default class Actor5e extends Actor {
if ( joat ) init.prof = Math.floor(0.5 * data.attributes.prof);
else if ( athlete ) init.prof = Math.ceil(0.5 * data.attributes.prof);
else init.prof = 0;
init.bonus = init.value + (flags.initiativeAlert ? 5 : 0);
init.bonus = Number(init.value + (flags.initiativeAlert ? 5 : 0));
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;
}
}

View file

@ -19,7 +19,8 @@ export default class ActorSheet5e extends ActorSheet {
this._filters = {
inventory: new Set(),
powerbook: new Set(),
features: new Set()
features: new Set(),
effects: new Set()
};
}
@ -31,7 +32,8 @@ export default class ActorSheet5e extends ActorSheet {
scrollY: [
".inventory .inventory-list",
".features .inventory-list",
".powerbook .inventory-list"
".powerbook .inventory-list",
".effects .inventory-list"
],
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
});
@ -82,7 +84,7 @@ export default class ActorSheet5e extends ActorSheet {
abl.label = CONFIG.SW5E.abilities[a];
}
// Update skill labels
// Skills
if (data.actor.data.skills) {
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
@ -98,12 +100,21 @@ export default class ActorSheet5e extends ActorSheet {
// Prepare owned items
this._prepareItems(data);
// Prepare active effects
// TODO Disabled until 0.7.5 release
// this._prepareEffects(data);
// Return data to the sheet
return data
}
/* -------------------------------------------- */
/**
* Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies
* @param {object} traits The raw traits data object from the actor data
* @private
*/
_prepareTraits(traits) {
const map = {
"dr": CONFIG.SW5E.damageResistanceTypes,
@ -137,6 +148,43 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Prepare the data structure for Active Effects which are currently applied to the Actor.
* @param {object} data The object of rendering data which is being prepared
* @private
*/
_prepareEffects(data) {
// Define effect header categories
const categories = {
temporary: {
label: "Temporary Effects",
effects: []
},
passive: {
label: "Passive Effects",
effects: []
},
inactive: {
label: "Inactive Effects",
effects: []
}
};
// Iterate over active effects, classifying them into categories
for ( let e of this.actor.effects ) {
e._getSourceName(); // Trigger a lookup for the source name
if ( e.data.disabled ) categories.inactive.effects.push(e);
else if ( e.isTemporary ) categories.temporary.effects.push(e);
else categories.inactive.push(e);
}
// Add the prepared categories of effects to the rendering data
return data.effects = categories;
}
/* -------------------------------------------- */
/**
* Insert a power into the powerbook object when rendering the character sheet
* @param {Object} data The Actor data being prepared
@ -163,18 +211,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 +235,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 +243,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 +263,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
@ -328,6 +392,10 @@ export default class ActorSheet5e extends ActorSheet {
html.find('.item-delete').click(this._onItemDelete.bind(this));
html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
// Active Effect management
html.find(".effect-control").click(this._onManageActiveEffect.bind(this));
}
// Owner Only Listeners
@ -508,13 +576,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);
@ -671,6 +732,28 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Manage Active Effect instances through the Actor Sheet via effect control buttons.
* @param {MouseEvent} event The left-click event on the effect control
* @private
*/
_onManageActiveEffect(event) {
event.preventDefault();
const a = event.currentTarget;
const li = a.closest(".effect");
const effect = this.actor.effects.get(li.dataset.effectId);
switch ( a.dataset.action ) {
case "edit":
return new ActiveEffectConfig(effect).render(true);
case "delete":
return effect.delete();
case "toggle":
return effect.update({disabled: !effect.data.disabled});
}
}
/* -------------------------------------------- */
/**
* Handle rolling an Ability check, either a test or a saving throw
* @param {Event} event The originating click event
@ -843,4 +926,4 @@ export default class ActorSheet5e extends ActorSheet {
// Return a copy of the extracted data
return duplicate(itemData);
}
}
}

View file

@ -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.
@ -69,7 +70,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
};
// Partition items by category
let [items, powers, feats, classes, species] = data.items.reduce((arr, item) => {
let [items, powers, feats, classes, species, archetypes, classfeatures] = data.items.reduce((arr, item) => {
// Item details
item.img = item.img || DEFAULT_TOKEN;
@ -88,10 +89,12 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
if ( item.type === "power" ) arr[1].push(item);
else if ( item.type === "feat" ) arr[2].push(item);
else if ( item.type === "class" ) arr[3].push(item);
else if ( item.type === "species" ) arr[4].push(item);
else if ( item.type === "species" ) arr[4].push(item);
else if ( item.type === "archetype" ) arr[5].push(item);
else if ( item.type === "classfeature" ) arr[6].push(item);
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
return arr;
}, [[], [], [], [], []]);
}, [[], [], [], [], [], [], []]);
// Apply active item filters
items = this._filterItems(items, this._filters.inventory);
@ -115,6 +118,8 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// Organize Features
const features = {
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: false, dataset: {type: "classfeature"}, isClassfeature: true},
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true},
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
@ -125,6 +130,8 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
}
classes.sort((a, b) => b.levels - a.levels);
features.classes.items = classes;
features.classfeatures.items = classfeatures;
features.archetype.items = archetypes;
features.species.items = species;
// Assign and return
@ -253,4 +260,41 @@ 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
// then add new features as long as level increases
if ( classWasAlreadyPresent ) {
const lvl = cls.data.data.levels;
const newLvl = Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level);
if ( !(lvl === newLvl) ) {
cls.update({"data.levels": newLvl});
itemData.data.levels = newLvl;
Actor5e.getClassFeatures(itemData).then(features => {
this.actor.createEmbeddedEntity("OwnedItem", features);
});
}
return
}
}
super._onDropItemCreate(itemData);
}
}

View file

@ -51,8 +51,8 @@ export default class AbilityUseDialog extends Dialog {
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
// Create the Dialog and return as a Promise
const icon = data.hasPowerSlots ? "fa-magic" : "fa-fist-raised";
const label = game.i18n.localize("SW5E.AbilityUse" + (data.hasPowerSlots ? "Cast" : "Use"));
const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
return new Promise((resolve) => {
const dlg = new this(item, {
title: `${item.name}: Usage Configuration`,
@ -85,6 +85,12 @@ export default class AbilityUseDialog extends Dialog {
const lvl = itemData.level;
const canUpcast = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
// If can't upcast, return early and don't bother calculating available power slots
if (!canUpcast) {
data = mergeObject(data, { isPower: true, canUpcast });
return;
}
// Determine the levels which are feasible
let lmax = 0;
const powerLevels = Array.fromRange(10).reduce((arr, i) => {
@ -97,7 +103,7 @@ export default class AbilityUseDialog extends Dialog {
arr.push({
level: i,
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
canCast: canUpcast && (max > 0),
canCast: max > 0,
hasSlots: slots > 0
});
return arr;
@ -109,14 +115,14 @@ export default class AbilityUseDialog extends Dialog {
powerLevels.push({
level: 'pact',
label: `${game.i18n.format('SW5E.PowerLevelPact', {level: pact.level, n: pact.value})}`,
canCast: canUpcast,
canCast: true,
hasSlots: pact.value > 0
});
}
const canCast = powerLevels.some(l => l.hasSlots);
// Return merged data
data = mergeObject(data, { hasPowerSlots: true, canUpcast, powerLevels });
data = mergeObject(data, { isPower: true, canUpcast, powerLevels });
if ( !canCast ) data.errors.push("SW5E.PowerCastNoSlots");
}

View file

@ -120,7 +120,7 @@ export default class ActorSheetFlags extends BaseEntitySheet {
// Diff the data against any applied overrides and apply
// TODO: Remove this logical gate once 0.7.x is release channel
if ( !isNewerVersion("0.7.1", game.data.version) ){
updateData.data = diffObject(this.object.overrides, updateData.data);
updateData = diffObject(this.object.data, updateData);
}
await actor.update(updateData, {diff: false});
}

View file

@ -1,102 +0,0 @@
/**
* A specialized Dialog subclass for casting a power item at a certain level
* @type {Dialog}
*/
export class PowerCastDialog extends Dialog {
constructor(actor, item, dialogData={}, options={}) {
super(dialogData, options);
this.options.classes = ["sw5e", "dialog"];
/**
* Store a reference to the Actor entity which is casting the power
* @type {Actor5e}
*/
this.actor = actor;
/**
* Store a reference to the Item entity which is the power being cast
* @type {Item5e}
*/
this.item = item;
}
/* -------------------------------------------- */
/* Rendering */
/* -------------------------------------------- */
/**
* A constructor function which displays the Power Cast Dialog app for a given Actor and Item.
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
* @param {Actor5e} actor
* @param {Item5e} item
* @return {Promise}
*/
static async create(actor, item) {
const ad = actor.data.data;
const id = item.data.data;
// Determine whether the power may be upcast
const lvl = id.level;
const canUpcast = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode);
// Determine the levels which are feasible
let lmax = 0;
const powerLevels = Array.fromRange(10).reduce((arr, i) => {
if ( i < lvl ) return arr;
const l = ad.powers["power"+i] || {max: 0, override: null};
let max = parseInt(l.override || l.max || 0);
let slots = Math.clamped(parseInt(l.value || 0), 0, max);
if ( max > 0 ) lmax = i;
arr.push({
level: i,
label: i > 0 ? `${CONFIG.SW5E.powerLevels[i]} (${slots} Slots)` : CONFIG.SW5E.powerLevels[i],
canCast: canUpcast && (max > 0),
hasSlots: slots > 0
});
return arr;
}, []).filter(sl => sl.level <= lmax);
const pact = ad.powers.pact;
if (pact.level >= lvl) {
// If this character has pact slots, present them as an option for
// casting the power.
powerLevels.push({
level: 'pact',
label: game.i18n.localize('SW5E.PowerLevelPact')
+ ` (${game.i18n.localize('SW5E.Level')} ${pact.level}) `
+ `(${pact.value} ${game.i18n.localize('SW5E.Slots')})`,
canCast: canUpcast,
hasSlots: pact.value > 0
});
}
const canCast = powerLevels.some(l => l.hasSlots);
// Render the Power casting template
const html = await renderTemplate("systems/sw5e/templates/apps/power-cast.html", {
item: item.data,
canCast: canCast,
canUpcast: canUpcast,
powerLevels,
hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget
});
// Create the Dialog and return as a Promise
return new Promise((resolve, reject) => {
const dlg = new this(actor, item, {
title: `${item.name}: Power Configuration`,
content: html,
buttons: {
cast: {
icon: '<i class="fas fa-magic"></i>',
label: "Cast",
callback: html => resolve(new FormData(html[0].querySelector("#power-config-form")))
}
},
default: "cast",
close: reject
});
dlg.render(true);
});
}
}

View file

@ -33,6 +33,7 @@ export const highlightCriticalSuccessFailure = function(message, html, data) {
export const displayChatActionButtons = function(message, html, data) {
const chatCard = html.find(".sw5e.chat-card");
if ( chatCard.length > 0 ) {
html.find(".flavor-text").remove();
// If the user is the message author or the actor owner, proceed
let actor = game.actors.get(data.message.speaker.actor);

35
module/classFeatures.js Normal file
View file

@ -0,0 +1,35 @@
export const ClassFeatures = {
"berserker": {
"archetypes": {
"addicted-approach": {
"label": "Addicted Approach",
"source": "PHB",
"features": {
"3": ["Compendium.sw5e.archetypes.PCwepUZqHYlxr4T3", "Compendium.sw5e.classfeatures.efOA0nrvUqKJOOeP", "Compendium.sw5e.classfeatures.nT6AfpQXSZ4IeChO"],
"6": ["Compendium.sw5e.classfeatures.GbJDWzoTKWL7sEpR"],
"10": ["Compendium.sw5e.classfeatures.3jqPPd5qJBBnonPw"],
"14": ["Compendium.sw5e.classfeatures.xzRNHB2M2HdOZzr7"]
}
}
},
"features": {
"1": ["Compendium.sw5e.classfeatures.IDt6duVrBzL8euRc", "Compendium.sw5e.classfeatures.rPOLy96fW96N2UPg"],
"2": ["Compendium.sw5e.classfeatures.DlYiCiG39R0goG9u", "Compendium.sw5e.classfeatures.FbSpxpXm1xONn0na", "Compendium.sw5e.classfeatures.KDiQ8O2evV2Z1YTo", "Compendium.sw5e.classfeatures.Q1JyHnVs9iIEBs91", "Compendium.sw5e.classfeatures.ROdICoWR82v6A2Rf", "Compendium.sw5e.classfeatures.cdCx5Hvq2rYRMzRj", "Compendium.sw5e.classfeatures.dTdbL8dypa6BAdnP", "Compendium.sw5e.classfeatures.h1uDhP1tEOuvjRw6", "Compendium.sw5e.classfeatures.hMiA075EKBBOL2cv", "Compendium.sw5e.classfeatures.sgJdISZMtwv08WPJ", "Compendium.sw5e.classfeatures.v4CZJ8LBMl5PYZCO"],
"3": ["Compendium.sw5e.classfeatures.kzwSN9SabKgWZZvU"],
"4": ["Compendium.sw5e.classfeatures.9oyy0MMqEws2qoil"],
"5": ["Compendium.sw5e.classfeatures.dPWmHiWmpnhHTsgd"],
"7": ["Compendium.sw5e.classfeatures.Cid5ujSdnooH0vMm", "Compendium.sw5e.classfeatures.WTBhKJgkArQI3Tgv", "Compendium.sw5e.classfeatures.oiT3TJxzRWPKAX9E", "Compendium.sw5e.classfeatures.pMEmIt3NWThbee8k", "Compendium.sw5e.classfeatures.qWV5YogZcpZ3Y3xj"],
"9": ["Compendium.sw5e.classfeatures.bi8G8H5Ur9B3BAyM"],
"11": ["Compendium.sw5e.classfeatures.eWbTifdXJvvXT4CV"],
"13": ["Compendium.sw5e.classfeatures.Hg8zYh1iXL0DGUVq", "Compendium.sw5e.classfeatures.QRnYiJmRk18ekE9v", "Compendium.sw5e.classfeatures.sfEr8ZBFVddlfLeF", "Compendium.sw5e.classfeatures.yGC9VzT840qQWxca"],
"15": ["Compendium.sw5e.classfeatures.YHPUv9lN3nCapAgP"],
"18": ["Compendium.sw5e.classfeatures.fFKNqUAWh0ZOhvRc"],
"20": ["Compendium.sw5e.classfeatures.IWTDawTUf79eWbEV"]
}
},
"consular": {
"features": {
"20": ["Compendium.sw5e.classfeatures.gSGeitc98ItAwhfF"]
}
}
};

View file

@ -9,8 +9,17 @@ export const _getInitiativeFormula = function(combatant) {
const actor = combatant.actor;
if ( !actor ) return "1d20";
const init = actor.data.data.attributes.init;
const parts = ["1d20", init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
if ( actor.getFlag("sw5e", "initiativeAdv") ) parts[0] = "2d20kh";
let nd = 1;
let mods = "";
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r=1";
if (actor.getFlag("sw5e", "initiativeAdv")) {
nd = 2;
mods += "kh";
}
const parts = [`${nd}d20${mods}`, init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
// Optionally apply Dexterity tiebreaker
const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker");

View file

@ -1,3 +1,5 @@
import {ClassFeatures} from "./classFeatures.js"
// Namespace SW5e Configuration Values
export const SW5E = {};
@ -250,7 +252,9 @@ SW5E.consumableTypes = {
"medpac": "SW5E.ConsumableMedpac",
"technology": "SW5E.ConsumableTechnology",
"ammunition": "SW5E.ConsumableAmmunition",
"trinket": "SW5E.ConsumableTrinket"
"trinket": "SW5E.ConsumableTrinket",
"force": "SW5E.ConsumableForce",
"tech": "SW5E.ConsumableTech"
};
/* -------------------------------------------- */
@ -294,6 +298,7 @@ SW5E.armorPropertiesTypes = {
"Anchor": "SW5E.ArmorProperAnchor",
"Avoidant": "SW5E.ArmorProperAvoidant",
"Barbed": "SW5E.ArmorProperBarbed",
"Bulky": "SW5E.ArmorProperBulky",
"Charging": "SW5E.ArmorProperCharging",
"Concealing": "SW5E.ArmorProperConcealing",
"Cumbersome": "SW5E.ArmorProperCumbersome",
@ -306,6 +311,7 @@ SW5E.armorPropertiesTypes = {
"Lightweight": "SW5E.ArmorProperLightweight",
"Magnetic": "SW5E.ArmorProperMagnetic",
"Obscured": "SW5E.ArmorProperObscured",
"Obtrusive": "SW5E.ArmorProperObtrusive",
"Powered": "SW5E.ArmorProperPowered",
"Reactive": "SW5E.ArmorProperReactive",
"Regulated": "SW5E.ArmorProperRegulated",
@ -314,6 +320,7 @@ SW5E.armorPropertiesTypes = {
"Rigid": "SW5E.ArmorProperRigid",
"Silent": "SW5E.ArmorProperSilent",
"Spiked": "SW5E.ArmorProperSpiked",
"Strength": "SW5E.ArmorProperStrength",
"Steadfast": "SW5E.ArmorProperSteadfast",
"Versatile": "SW5E.ArmorProperVersatile"
};
@ -483,7 +490,11 @@ SW5E.powerScalingModes = {
/* -------------------------------------------- */
// Weapon Types
/**
* Define the set of types which a weapon item can take
* @type {Object}
*/
SW5E.weaponTypes = {
"simpleVW": "SW5E.WeaponSimpleVW",
"simpleB": "SW5E.WeaponSimpleB",
@ -493,7 +504,8 @@ SW5E.weaponTypes = {
"martialLW": "SW5E.WeaponMartialLW",
"natural": "SW5E.WeaponNatural",
"improv": "SW5E.WeaponImprov",
"ammo": "SW5E.WeaponAmmo"
"ammo": "SW5E.WeaponAmmo",
"siege": "SW5E.WeaponSiege"
};
@ -515,14 +527,15 @@ SW5E.weaponProperties = {
"dis": "SW5E.WeaponPropertiesDis",
"dpt": "SW5E.WeaponPropertiesDpt",
"dou": "SW5E.WeaponPropertiesDou",
"hvy": "SW5E.WeaponPropertiesHvy",
"hid": "SW5E.WeaponPropertiesHid",
"fin": "SW5E.WeaponPropertiesFin",
"fix": "SW5E.WeaponPropertiesFix",
"foc": "SW5E.WeaponPropertiesFoc",
"hvy": "SW5E.WeaponPropertiesHvy",
"hid": "SW5E.WeaponPropertiesHid",
"ken": "SW5E.WeaponPropertiesKen",
"lgt": "SW5E.WeaponPropertiesLgt",
"lum": "SW5E.WeaponPropertiesLum",
"mig": "SW5E.WeaponPropertiesMig",
"pic": "SW5E.WeaponPropertiesPic",
"rap": "SW5E.WeaponPropertiesRap",
"rch": "SW5E.WeaponPropertiesRch",
@ -555,7 +568,6 @@ SW5E.powerSchools = {
"enh": "SW5E.SchoolEnh"
};
// Power Levels
SW5E.powerLevels = {
0: "SW5E.PowerLevel0",
@ -642,7 +654,6 @@ SW5E.cover = {
.5: 'SW5E.CoverHalf',
.75: 'SW5E.CoverThreeQuarters',
1: 'SW5E.CoverTotal'
};
/* -------------------------------------------- */
@ -665,6 +676,7 @@ SW5E.conditionTypes = {
"prone": "SW5E.ConProne",
"restrained": "SW5E.ConRestrained",
"shocked": "SW5E.ConShocked",
"slowed": "SW5E.ConSlowed",
"stunned": "SW5E.ConStunned",
"unconscious": "SW5E.ConUnconscious"
};
@ -785,6 +797,9 @@ SW5E.CR_EXP_LEVELS = [
20000, 22000, 25000, 33000, 41000, 50000, 62000, 75000, 90000, 105000, 120000, 135000, 155000
];
// Character Features Per Class And Level
SW5E.classFeatures = ClassFeatures;
// Configure Optional Character Flags
SW5E.characterFlags = {
"detailOriented": {
@ -866,7 +881,13 @@ SW5E.characterFlags = {
section: "Feats",
type: Boolean
},
"remarkableAthlete": {
"reliableTalent": {
name: "SW5E.FlagsReliableTalent",
hint: "SW5E.FlagsReliableTalentHint",
section: "Feats",
type: Boolean
},
"remarkableAthlete": {
name: "SW5E.FlagsRemarkableAthlete",
hint: "SW5E.FlagsRemarkableAthleteHint",
abilities: ['str','dex','con'],
@ -881,3 +902,8 @@ SW5E.characterFlags = {
placeholder: 20
}
};
// Configure allowed status flags
SW5E.allowedActorFlags = [
"isPolymorphed", "originalActor"
].concat(Object.keys(SW5E.characterFlags));

View file

@ -150,7 +150,6 @@ export default class Item5e extends Item {
// Get the Item's data
const itemData = this.data;
const actorData = this.actor ? this.actor.data : {};
const data = itemData.data;
const C = CONFIG.SW5E;
const labels = {};
@ -183,8 +182,16 @@ export default class Item5e extends Item {
// Species Items
else if ( itemData.type === "species" ) {
//labels.species = C.species[data.species];
}
// labels.species = C.species[data.species];
}
// Archetype Items
else if ( itemData.type === "archetype" ) {
// labels.archetype = C.archetype[data.archetype];
}
// Class Feature Items
else if ( itemData.type === "classfeature" ) {
}
// Equipment Items
else if ( itemData.type === "equipment" ) {
@ -228,16 +235,12 @@ export default class Item5e extends Item {
// Item Actions
if ( data.hasOwnProperty("actionType") ) {
// Save DC
let save = data.save || {};
if ( !save.ability ) save.dc = null;
else if ( this.isOwned ) { // Actor owned items
if ( save.scaling === "power" ) save.dc = actorData.data.attributes.powerdc;
else if ( save.scaling !== "flat" ) save.dc = this.actor.getPowerDC(save.scaling);
} else { // Un-owned items
// Saving throws for unowned items
const save = data.save;
if ( save?.ability && !this.isOwned ) {
if ( save.scaling !== "flat" ) save.dc = null;
labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability: C.abilities[save.ability]});
}
labels.save = save.ability ? `${game.i18n.localize("SW5E.AbbreviationDC")} ${save.dc || ""} ${C.abilities[save.ability]}` : "";
// Damage
let dam = data.damage || {};
@ -303,13 +306,20 @@ export default class Item5e extends Item {
user: game.user._id,
type: CONST.CHAT_MESSAGE_TYPES.OTHER,
content: html,
flavor: this.name,
speaker: {
actor: this.actor._id,
token: this.actor.token,
alias: this.actor.name
}
},
flags: {"core.canPopout": true}
};
// If the consumable was destroyed in the process - embed the item data in the surviving message
if ( (this.data.type === "consumable") && !this.actor.items.has(this.id) ) {
chatData.flags["sw5e.itemData"] = this.data;
}
// Toggle default roll mode
rollMode = rollMode || game.settings.get("core", "rollMode");
if ( ["gmroll", "blindroll"].includes(rollMode) ) chatData["whisper"] = ChatMessage.getWhisperRecipients("GM");
@ -443,7 +453,7 @@ export default class Item5e extends Item {
// Maybe initiate template placement workflow
if ( this.hasAreaTarget && placeTemplate ) {
const template = AbilityTemplate.fromItem(this);
if ( template ) template.drawPreview(event);
if ( template ) template.drawPreview();
if ( this.owner && this.owner.sheet ) this.owner.sheet.minimize();
}
return true;
@ -481,7 +491,7 @@ export default class Item5e extends Item {
// Ability activation properties
if ( data.hasOwnProperty("activation") ) {
props.push(
labels.activation + (data.activation.condition ? `(${data.activation.condition})` : ""),
labels.activation + (data.activation?.condition ? ` (${data.activation.condition})` : ""),
labels.target,
labels.range,
labels.duration
@ -617,17 +627,15 @@ export default class Item5e extends Item {
rollData["atk"] = [itemData.attackBonus, actorBonus.attack].filterJoin(" + ");
}
// Ammunition Bonus
delete this._ammo;
const consume = itemData.consume;
if ( consume?.type === "ammo" ) {
if ( !consume.target ) {
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name}));
return false;
}
const ammo = this.actor.items.get(consume.target);
// Ammunition Bonus
delete this._ammo;
const consume = itemData.consume;
if ( consume?.type === "ammo" ) {
const ammo = this.actor.items.get(consume.target);
if(ammo?.data){
const q = ammo.data.data.quantity;
if ( q && (q - consume.amount >= 0) ) {
const consumeAmount = consume.amount ?? 0;
if ( q && (q - consumeAmount >= 0) ) {
let ammoBonus = ammo.data.data.attackBonus;
if ( ammoBonus ) {
parts.push("@ammo");
@ -636,7 +644,10 @@ export default class Item5e extends Item {
this._ammo = ammo;
}
}
//}else{
// ui.notifications.error(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}));
}
}
// Compose roll options
const rollConfig = mergeObject({
@ -856,7 +867,7 @@ export default class Item5e extends Item {
* Place an attack roll using an item (weapon, feat, power, or equipment)
* Rely upon the d20Roll logic for the core implementation
*
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance
* @return {Promise<Roll>} A Promise which resolves to the created Roll instance
*/
async rollFormula(options={}) {
if ( !this.data.data.formula ) {
@ -941,7 +952,7 @@ export default class Item5e extends Item {
// Maybe initiate template placement workflow
if ( this.hasAreaTarget && placeTemplate ) {
const template = AbilityTemplate.fromItem(this);
if ( template ) template.drawPreview(event);
if ( template ) template.drawPreview();
if ( this.owner && this.owner.sheet ) this.owner.sheet.minimize();
}
return true;
@ -1065,48 +1076,41 @@ export default class Item5e extends Item {
const isTargetted = action === "save";
if ( !( isTargetted || game.user.isGM || message.isAuthor ) ) return;
// Get the Actor from a synthetic Token
// Recover the actor for the chat card
const actor = this._getChatCardActor(card);
if ( !actor ) return;
// Get the Item
const item = actor.getOwnedItem(card.dataset.itemId);
// Get the Item from stored flag data or by the item ID on the Actor
const storedData = message.getFlag("sw5e", "itemData");
const item = storedData ? this.createOwned(storedData, actor) : actor.getOwnedItem(card.dataset.itemId);
if ( !item ) {
return ui.notifications.error(game.i18n.format("SW5E.ActionWarningNoItem", {item: card.dataset.itemId, name: actor.name}))
}
const powerLevel = parseInt(card.dataset.powerLevel) || null;
// Get card targets
let targets = [];
if ( isTargetted ) {
targets = this._getChatCardTargets(card);
if ( !targets.length ) {
ui.notifications.warn(game.i18n.localize("SW5E.ActionWarningNoToken"));
return button.disabled = false;
}
}
// Attack and Damage Rolls
if ( action === "attack" ) await item.rollAttack({event});
else if ( action === "damage" ) await item.rollDamage({event, powerLevel});
else if ( action === "versatile" ) await item.rollDamage({event, powerLevel, versatile: true});
else if ( action === "formula" ) await item.rollFormula({event, powerLevel});
// Saving Throws for card targets
else if ( action === "save" ) {
for ( let a of targets ) {
const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: a.token});
await a.rollAbilitySave(button.dataset.ability, { event, speaker });
}
}
// Tool usage
else if ( action === "toolCheck" ) await item.rollToolCheck({event});
// Power Template Creation
else if ( action === "placeTemplate") {
const template = AbilityTemplate.fromItem(item);
if ( template ) template.drawPreview(event);
// Handle different actions
switch ( action ) {
case "attack":
await item.rollAttack({event}); break;
case "damage":
await item.rollDamage({event, powerLevel}); break;
case "versatile":
await item.rollDamage({event, powerLevel, versatile: true}); break;
case "formula":
await item.rollFormula({event, powerLevel}); break;
case "save":
const targets = this._getChatCardTargets(card);
for ( let token of targets ) {
const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: token});
await token.actor.rollAbilitySave(button.dataset.ability, { event, speaker });
}
break;
case "toolCheck":
await item.rollToolCheck({event}); break;
case "placeTemplate":
const template = AbilityTemplate.fromItem(item);
if ( template ) template.drawPreview();
break;
}
// Re-enable the button
@ -1164,10 +1168,9 @@ export default class Item5e extends Item {
* @private
*/
static _getChatCardTargets(card) {
const character = game.user.character;
const controlled = canvas.tokens.controlled;
const targets = controlled.reduce((arr, t) => t.actor ? arr.concat([t.actor]) : arr, []);
if ( character && (controlled.length === 0) ) targets.push(character);
let targets = canvas.tokens.controlled.filter(t => !!t.actor);
if ( !targets.length && game.user.character ) targets = targets.concat(game.user.character.getActiveTokens());
if ( !targets.length ) ui.notifications.warn(game.i18n.localize("SW5E.ActionWarningNoToken"));
return targets;
}

View file

@ -8,9 +8,7 @@ export default class ItemSheet5e extends ItemSheet {
constructor(...args) {
super(...args);
if ( this.object.data.type === "class" ) {
this.options.resizable = true;
this.options.width = 600;
this.options.height = 640;
}
}
@ -20,7 +18,7 @@ export default class ItemSheet5e extends ItemSheet {
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
width: 560,
height: 420,
height: "auto",
classes: ["sw5e", "sheet", "item"],
resizable: true,
scrollY: [".tab.details"],
@ -182,7 +180,14 @@ export default class ItemSheet5e extends ItemSheet {
else if ( item.type === "species" ) {
}
else if ( item.type === "archetype" ) {
}
else if ( item.type === "classfeature" ) {
}
// Action type
if ( item.data.actionType ) {
props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
@ -220,7 +225,9 @@ export default class ItemSheet5e extends ItemSheet {
/** @override */
setPosition(position={}) {
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
if ( !this._minimized ) {
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
}
return super.setPosition(position);
}
@ -249,13 +256,6 @@ export default class ItemSheet5e extends ItemSheet {
super.activateListeners(html);
html.find(".damage-control").click(this._onDamageControl.bind(this));
html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this));
// Armor properties
// html.find(".armorproperties-control").click(this._onarmorpropertiesControl.bind(this));
// Weapon properties
// html.find(".weaponproperties-control").click(this._onweaponpropertiesControl.bind(this));
}
/* -------------------------------------------- */

View file

@ -136,6 +136,34 @@ export const migrateActorData = function(actor) {
return updateData;
};
/* -------------------------------------------- */
/**
* Scrub an Actor's system data, removing all keys which are not explicitly defined in the system template
* @param {Object} actorData The data object for an Actor
* @return {Object} The scrubbed Actor data
*/
function cleanActorData(actorData) {
// Scrub system data
const model = game.system.model.Actor[actorData.type];
actorData.data = filterObject(actorData.data, model);
// Scrub system flags
const allowedFlags = CONFIG.SW5E.allowedActorFlags.reduce((obj, f) => {
obj[f] = null;
return obj;
}, {});
if ( actorData.flags.sw5e ) {
actorData.flags.sw5e = filterObject(actorData.flags.sw5e, allowedFlags);
}
// Return the scrubbed data
return actorData;
}
/* -------------------------------------------- */
/**
@ -222,3 +250,33 @@ const _migrateRemoveDeprecated = function(ent, updateData) {
updateData[`data.${parts.join(".")}`] = null;
}
};
/* -------------------------------------------- */
/**
* A general tool to purge flags from all entities in a Compendium pack.
* @param {Compendium} pack The compendium pack to clean
* @private
*/
export async function purgeFlags(pack) {
const cleanFlags = (flags) => {
const flags5e = flags.sw5e || null;
return flags5e ? {sw5e: flags5e} : {};
};
await pack.configure({locked: false});
const content = await pack.getContent();
for ( let entity of content ) {
const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)};
if ( pack.entity === "Actor" ) {
update.items = entity.data.items.map(i => {
i.flags = cleanFlags(i.flags);
return i;
})
}
await pack.updateEntity(update, {recursive: false});
console.log(`Purged flags from ${entity.name}`);
}
await pack.configure({locked: true});
}

View file

@ -52,9 +52,8 @@ export default class AbilityTemplate extends MeasuredTemplate {
/**
* Creates a preview of the power template
* @param {Event} event The initiating click event
*/
drawPreview(event) {
drawPreview() {
const initialLayer = canvas.activeLayer;
this.draw();
this.layer.activate();

View file

@ -13,6 +13,7 @@ export const preloadHandlebarsTemplates = async function() {
"systems/sw5e/templates/actors/parts/actor-inventory.html",
"systems/sw5e/templates/actors/parts/actor-features.html",
"systems/sw5e/templates/actors/parts/actor-powerbook.html",
"systems/sw5e/templates/actors/parts/actor-effects.html",
// Item Sheet Partials
"systems/sw5e/templates/items/parts/item-action.html",

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Some files were not shown because too many files have changed in this diff Show more