Merge pull request #30 from unrealkakeman89/Class-Skills-and-other-updates
Class skills and other updates
37
lang/en.json
|
@ -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",
|
||||
|
|
|
@ -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 */
|
||||
/* ----------------------------------------- */
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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"]
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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");
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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",
|
||||
|
|
BIN
packs/Icons/Archetypes/Acquisitions Practice.webp
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
packs/Icons/Archetypes/Addicted Approach.webp
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
packs/Icons/Archetypes/Adept Specialist.webp
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
packs/Icons/Archetypes/Aing-Tii Order.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Archetypes/Armormech Engineering.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Armstech Engineering.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Archetypes/Artificer Engineering.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Artillerist Technique.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Archetypes/Assault Specialist.webp
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
packs/Icons/Archetypes/Astrotech Engineering.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Archetypes/Ataru Form.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Archetypes/Audiotech Engineering.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Ballistic Approach.webp
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
packs/Icons/Archetypes/Beguiler Practice.webp
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
packs/Icons/Archetypes/Biochem Engineering.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Archetypes/Blademaster Specialist.webp
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
packs/Icons/Archetypes/Bloodstorm Approach.webp
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
packs/Icons/Archetypes/Brawling Approach.webp
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
packs/Icons/Archetypes/Bulwark Technique.webp
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
packs/Icons/Archetypes/Chef Pursuit.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Crimson Order.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Archetypes/Cybertech Engineering.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Cyclone Approach.webp
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
packs/Icons/Archetypes/Deadeye Technique.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Archetypes/Demolitions Specialist.webp
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
packs/Icons/Archetypes/Disabling Practice.webp
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
packs/Icons/Archetypes/Doctor Pursuit.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Echani Order.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Archetypes/Enhancement Specialist.webp
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
packs/Icons/Archetypes/Explorer Pursuit.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Frenzied Approach.webp
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
packs/Icons/Archetypes/Gadgeteer Engineering.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Gambler Pursuit.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Geneticist Pursuit.webp
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
packs/Icons/Archetypes/Gunslinger Practice.webp
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
packs/Icons/Archetypes/Heavy Weapons Specialist.webp
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
packs/Icons/Archetypes/Hunter Technique.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Archetypes/Illusionist Technique.webp
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
packs/Icons/Archetypes/Industrial Approach.webp
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
packs/Icons/Archetypes/Inquisitor Technique.webp
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
packs/Icons/Archetypes/Jal Shey Order.webp
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
packs/Icons/Archetypes/Jar'Kai Form.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Juggernaut Approach.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Archetypes/Juyo Vapaad Form.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Kage Order.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Archetypes/Kyuzo Order.webp
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
packs/Icons/Archetypes/Lethality Practice.webp
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
packs/Icons/Archetypes/Makashi Form.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Marauder Approach.webp
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
packs/Icons/Archetypes/Mastermind Technique.webp
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
packs/Icons/Archetypes/Matukai Order.webp
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
packs/Icons/Archetypes/Mounted Specialist.webp
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
packs/Icons/Archetypes/Nightsister Order.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Archetypes/Niman Form.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Archetypes/Path of Aggression.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Archetypes/Path of Communion.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Archetypes/Path of Etherealness.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Path of Focus.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Archetypes/Path of Shadows.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Path of Synthesis.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Path of Tenacity.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Archetypes/Path of Witchcraft.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Archetypes/Path of the Corsair.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Path of the Forceblade.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Performance Practice.webp
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
packs/Icons/Archetypes/Politician Pursuit.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Archetypes/Praetorian Specialist.webp
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
packs/Icons/Archetypes/Precision Approach.webp
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
packs/Icons/Archetypes/Predator Technique.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Archetypes/Ruffian Practice.webp
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
packs/Icons/Archetypes/Saboteur Practice.webp
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
packs/Icons/Archetypes/Sawbones Practice.webp
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
packs/Icons/Archetypes/Scrapper Practice.webp
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
packs/Icons/Archetypes/Sharpshooter Practice.webp
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
packs/Icons/Archetypes/Shield Specialist.webp
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
packs/Icons/Archetypes/Shien Djem So Form.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Archetypes/Shii-Cho Form.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Archetypes/Slayer Technique.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Archetypes/Slicer Pursuit.webp
Normal file
After Width: | Height: | Size: 11 KiB |