diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..4ca0a625 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,14 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": false, + "quoteProps": "consistent", + "jsxSingleQuote": false, + "trailingComma": "none", + "bracketSpacing": false, + "jsxBracketSameLine": false, + "arrowParens": "always", + "endOfLine": "lf" +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a73a41b..cac0e10e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,2 +1,3 @@ { + "editor.formatOnSave": true } \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index dabc0757..d18fc7d3 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -8,19 +8,19 @@ const less = require("gulp-less"); const SW5E_LESS = ["less/**/*.less"]; function compileLESS() { - return gulp.src("less/original/sw5e.less").pipe(less()).pipe(gulp.dest("./")); + return gulp.src("less/original/sw5e.less").pipe(less()).pipe(gulp.dest("./")); } function compileGlobalLess() { - return gulp.src("less/update/sw5e-global.less").pipe(less()).pipe(gulp.dest("./")); + return gulp.src("less/update/sw5e-global.less").pipe(less()).pipe(gulp.dest("./")); } function compileLightLess() { - return gulp.src("less/update/sw5e-light.less").pipe(less()).pipe(gulp.dest("./")); + return gulp.src("less/update/sw5e-light.less").pipe(less()).pipe(gulp.dest("./")); } function compileDarkLess() { - return gulp.src("less/update/sw5e-dark.less").pipe(less()).pipe(gulp.dest("./")); + return gulp.src("less/update/sw5e-dark.less").pipe(less()).pipe(gulp.dest("./")); } const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compileDarkLess); @@ -30,7 +30,7 @@ const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compil /* ----------------------------------------- */ function watchUpdates() { - gulp.watch(SW5E_LESS, css); + gulp.watch(SW5E_LESS, css); } /* ----------------------------------------- */ diff --git a/lang/en.json b/lang/en.json index 1e8118cd..1decdafc 100644 --- a/lang/en.json +++ b/lang/en.json @@ -46,10 +46,10 @@ "SETTINGS.5eReset": "Reset", "SETTINGS.5eRestEpic": "Epic Heroism (LR: 1 hour, SR: 1 min)", "SETTINGS.5eRestGritty": "Gritty Realism (LR: 7 days, SR: 8 hours)", - "SETTINGS.5eUndoChanges": "Undo Changes", "SETTINGS.5eRestL": "Configure which rest variant should be used for games within this system.", "SETTINGS.5eRestN": "Rest Variant", "SETTINGS.5eRestPHB": "Player's Handbook (LR: 8 hours, SR: 1 hour)", + "SETTINGS.5eUndoChanges": "Undo Changes", "SETTINGS.SWColorDark": "Dark Theme", "SETTINGS.SWColorL": "Set the color theme of the game", "SETTINGS.SWColorLight": "Light Theme", @@ -80,6 +80,7 @@ "SW5E.AbilityUseCast": "Cast Power", "SW5E.AbilityUseChargedHint": "This {type} is charged and ready to use!", "SW5E.AbilityUseChargesLabel": "{value} Charges", + "SW5E.AbilityUseConfig": "Usage Configuration", "SW5E.AbilityUseConsumableChargeHint": "Using this {type} will consume 1 charge of {value} remaining.", "SW5E.AbilityUseConsumableDestroyHint": "Using this {type} will consume its final charge and it will be destroyed.", "SW5E.AbilityUseConsumableLabel": "{max} per {per}", @@ -109,7 +110,6 @@ "SW5E.ActorWarningInvalidItem": "{itemType} items cannot be added to a {actorType}.", "SW5E.Add": "Add", "SW5E.AddEmbeddedItemPromptHint": "Do you want to add these items to your character sheet?", - "SW5E.SelectItemsPromptTitle": "Select Items", "SW5E.AdditionalNotes": "Additional Notes", "SW5E.Advantage": "Advantage", "SW5E.Alignment": "Alignment", @@ -193,7 +193,7 @@ "SW5E.BonusSaveForm": "Update Bonuses", "SW5E.BonusTechPowerDC": "Global Tech Power DC Bonus", "SW5E.BonusTitle": "Configure Actor Bonuses", - "SW5E.BurnFuel": "Burn", + "SW5E.BurnFuel": "Burn", "SW5E.CapacityMultiplier": "Capacity Multiplier", "SW5E.CentStorageCapacity": "Central Storage Capacity", "SW5E.ChallengeRating": "Challenge Rating", @@ -267,6 +267,10 @@ "SW5E.ConUnconscious": "Unconscious", "SW5E.Core": "Core", "SW5E.CostGP": "Cost (CR)", + "SW5E.Cover": "Cover", + "SW5E.CoverHalf": "Half", + "SW5E.CoverThreeQuarters": "Three Quarters", + "SW5E.CoverTotal": "Total", "SW5E.CreatureAberration": "Aberration", "SW5E.CreatureAberrationPl": "Aberrations", "SW5E.CreatureBeast": "Beast", @@ -281,25 +285,20 @@ "SW5E.CreatureHumanoidPl": "Humanoids", "SW5E.CreaturePlant": "Plant", "SW5E.CreaturePlantPl": "Plants", - "SW5E.CreatureUndead": "Undead", - "SW5E.CreatureUndeadPl": "Undead", - "SW5E.CreatureType": "Creature Type", - "SW5E.CreatureTypeTitle": "Configure Creature Type", "SW5E.CreatureSwarm": "Swarm", - "SW5E.CreatureSwarmSize": "Swarm Size", "SW5E.CreatureSwarmPhrase": "Swarm of {size} {type}", + "SW5E.CreatureSwarmSize": "Swarm Size", + "SW5E.CreatureType": "Creature Type", "SW5E.CreatureTypeConfig": "Configure Creature Type", "SW5E.CreatureTypeSelectorCustom": "Custom Type", "SW5E.CreatureTypeSelectorSubtype": "Subtype", - "SW5E.Crewed": "Crewed", - "SW5E.Cover": "Cover", - "SW5E.CoverHalf": "Half", - "SW5E.CoverThreeQuarters": "Three Quarters", - "SW5E.CoverTotal": "Total", + "SW5E.CreatureTypeTitle": "Configure Creature Type", + "SW5E.CreatureUndead": "Undead", + "SW5E.CreatureUndeadPl": "Undead", "SW5E.CrewCap": "Crew Capacity", + "SW5E.Crewed": "Crewed", "SW5E.Critical": "Critical", "SW5E.CriticalHit": "Critical Hit", - "SW5E.PowerfulCritical": "Powerful Critical", "SW5E.Currency": "Currency", "SW5E.CurrencyConvert": "Convert All Currency", "SW5E.CurrencyConvertHint": "Convert all carried currency to the highest possible denomination to reduce the amount of coinage carried by the character. Be wary, this action cannot be undone.", @@ -319,7 +318,6 @@ "SW5E.DamageRoll": "Damage Roll", "SW5E.DamageSonic": "Sonic", "SW5E.DamImm": "Damage Immunities", - "SW5E.DmgRed": "Damage Reduction", "SW5E.DamRes": "Damage Resistances", "SW5E.DamVuln": "Damage Vulnerabilities", "SW5E.DarkPowerDC": "Dark Power DC", @@ -345,24 +343,29 @@ "SW5E.DistMi": "Miles", "SW5E.DistSelf": "Self", "SW5E.DistTouch": "Touch", + "SW5E.DmgRed": "Damage Reduction", "SW5E.Duration": "Duration", - "SW5E.EffectsCategoryTemporary": "Temporary Effects", - "SW5E.EffectsCategoryPassive": "Passive Effects", - "SW5E.EffectsCategoryInactive": "Inactive Effects", "SW5E.EffectCreate": "Create Effect", "SW5E.EffectDelete": "Delete Effect", "SW5E.EffectEdit": "Edit Effect", + "SW5E.EffectInactive": "Inactive Effects", + "SW5E.EffectNew": "New Effect", + "SW5E.EffectPassive": "Passive Effects", "SW5E.Effects": "Effects", + "SW5E.EffectTemporary": "Temporary Effects", + "SW5E.EffectsCategoryInactive": "Inactive Effects", + "SW5E.EffectsCategoryPassive": "Passive Effects", + "SW5E.EffectsCategoryTemporary": "Temporary Effects", "SW5E.EffectToggle": "Toggle Effect", "SW5E.Engine": "Engine", "SW5E.EnginePl": "Engines", "SW5E.EquipmentBonus": "Magical Bonus", "SW5E.EquipmentClothing": "Clothing", "SW5E.EquipmentHeavy": "Heavy Armor", + "SW5E.EquipmentHyperdrive": "Hyperdrive", "SW5E.EquipmentLight": "Light Armor", "SW5E.EquipmentMedium": "Medium Armor", "SW5E.EquipmentNatural": "Natural Armor", - "SW5E.EquipmentHyperdrive": "Hyperdrive", "SW5E.EquipmentPowerCoupling": "Power Coupling", "SW5E.EquipmentReactor": "Reactor", "SW5E.EquipmentShield": "Shield", @@ -377,6 +380,7 @@ "SW5E.Expertise": "Expertise", "SW5E.Favorites": "Favorites", "SW5E.FavoritesAndNotes": "Favorites & Notes", + "SW5E.Feats": "Feats", "SW5E.FeatureActionRecharge": "Action Recharge", "SW5E.FeatureActive": "Active Abilities", "SW5E.FeatureAdd": "Create Feature", @@ -387,8 +391,8 @@ "SW5E.FeatureRechargeOn": "Recharge On", "SW5E.FeatureRechargeResult": "1d6 Result", "SW5E.Features": "Features", - "SW5E.FeatureUsage": "Feature Usage", "SW5E.FeatureType": "Feature Type", + "SW5E.FeatureUsage": "Feature Usage", "SW5E.FeetAbbr": "ft.", "SW5E.Filter": "Filter", "SW5E.FilterNoPowers": "No powers found for this set of filters.", @@ -517,9 +521,9 @@ "SW5E.Flaws": "Flaws", "SW5E.ForcePowerbook": "Force Powers", "SW5E.Formula": "Formula", - "SW5E.FuelCapacity": "Fuel Capacity", + "SW5E.FuelCapacity": "Fuel Capacity", + "SW5E.FuelCostPerUnit": "Fuel Cost per Unit", "SW5E.FuelCostsMod": "Fuel Costs Modifier", - "SW5E.FuelCostPerUnit": "Fuel Cost per Unit", "SW5E.GrantedAbilities": "Granted Abilities", "SW5E.HalfProficient": "Half Proficient", "SW5E.HardpointSizeMod": "Hardpoint Size Modifier", @@ -585,6 +589,7 @@ "SW5E.ItemTypeArchetype": "Archetype", "SW5E.ItemTypeBackground": "Background", "Sw5E.ItemTypeBackgroundPl": "Backgrounds", + "SW5E.ItemTypeBackpack": "Container", "SW5E.ItemTypeClass": "Class", "SW5E.ItemTypeClassFeat": "Class Feature", "SW5E.ItemTypeClassFeats": "Class Features", @@ -594,9 +599,9 @@ "SW5E.ItemTypeContainer": "Container", "SW5E.ItemTypeContainerPl": "Containers", "SW5E.ItemTypeDeployment": "Deployment", - "SW5E.ItemTypeDeploymentPl": "Deployments", "SW5E.ItemTypeDeploymentFeature": "Deployment Feature", "SW5E.ItemTypeDeploymentFeaturePl": "Deployment Features", + "SW5E.ItemTypeDeploymentPl": "Deployments", "SW5E.ItemTypeEquipment": "Equipment", "SW5E.ItemTypeEquipmentPl": "Equipment", "SW5E.ItemTypeFeat": "Feat", @@ -751,6 +756,7 @@ "SW5E.LongRest": "Long Rest", "SW5E.LongRestEpic": "Long Rest (1 hour)", "SW5E.LongRestGritty": "Long Rest (7 days)", + "SW5E.LongRestHint": "Take a long rest? On a long rest you will recover hit points, half your maximum hit dice, class resources, limited use item charges, and power points.", "SW5E.LongRestNormal": "Long Rest (8 hours)", "SW5E.LongRestOvernight": "Long Rest (New Day)", "SW5E.LongRestResult": "{name} takes a long rest.", @@ -770,8 +776,8 @@ "SW5E.LongRestResultTP": "{name} takes a long rest and recovers {tech} Tech Points.", "SW5E.LongRestResultTPHD": "{name} takes a long rest and recovers {tech} Tech Points and {dice} Hit Dice.", "SW5E.Max": "Max", - "SW5E.Modifier": "Modifier", "SW5E.ModCap": "Modification Capacity", + "SW5E.Modifier": "Modifier", "SW5E.Movement": "Movement", "SW5E.MovementBurrow": "Burrow", "SW5E.MovementClimb": "Climb", @@ -780,13 +786,15 @@ "SW5E.MovementCrawl": "Crawl", "SW5E.MovementFly": "Fly", "SW5E.MovementHover": "Hover", - "SW5E.MovementRoll": "Roll", - "SW5E.MovementSpace": "Space Flight", + "SW5E.MovementRoll": "Roll", + "SW5E.MovementSpace": "Space Flight", "SW5E.MovementSwim": "Swim", "SW5E.MovementTurn": "Turning", "SW5E.MovementUnits": "Units", "SW5E.MovementWalk": "Walk", "SW5E.Name": "Character Name", + "SW5E.NewDay": "Is New Day?", + "SW5E.NewDayHint": "Recover limited use abilities which recharge \"per day\"?", "SW5E.NoCharges": "No Charges", "SW5E.None": "None", "SW5E.NoPowerLevels": "This character has no powercaster levels, but you may add powers manually.", @@ -839,11 +847,12 @@ "SW5E.PowerCreate": "Create Power", "SW5E.PowerDC": "Power DC", "SW5E.PowerDetails": "Power Details", - "SW5E.PowerDie": "Power Die", - "SW5E.PowerDiePl": "Power Dice", - "SW5E.PowerDieAlloc": "Power Die Allocation", "SW5E.PowerDiceRecovery": "Power Dice Recovery", + "SW5E.PowerDie": "Power Die", + "SW5E.PowerDieAlloc": "Power Die Allocation", + "SW5E.PowerDiePl": "Power Dice", "SW5E.PowerEffects": "Power Effects", + "SW5E.PowerfulCritical": "Powerful Critical", "SW5E.PowerLevel": "Power Level", "SW5E.PowerLevel0": "At-Will", "SW5E.PowerLevel1": "1st Level", @@ -873,9 +882,9 @@ "SW5E.PowerProgression": "Power Progression", "SW5E.PowerProgSct": "Scout", "SW5E.PowerProgSnt": "Sentinel", + "SW5E.PowerRouting": "Power Routing", "SW5E.PowerSchool": "Power School", "SW5E.PowersKnown": "Powers Known", - "SW5E.PowerRouting": "Power Routing", "SW5E.PowerTarget": "Power Target", "SW5E.PowerUnprepared": "Unprepared", "SW5E.PowerUsage": "Power Usage", @@ -891,15 +900,16 @@ "SW5E.Reaction": "Reaction", "SW5E.ReactionPl": "Reactions", "SW5E.Recharge": "Recharge", - "SW5E.Refitting": "Refitting", - "SW5E.Refuel": "Refuel", + "SW5E.Refitting": "Refitting", + "SW5E.Refuel": "Refuel", "SW5E.RegenerationRateCoefficient": "Regeneration Rate Coefficient", "SW5E.RequiredMaterials": "Required Materials", "SW5E.Requirements": "Requirements", - "SW5E.ResourcesAndTraits": "Resources & Traits", "SW5E.ResourcePrimary": "Resource 1", + "SW5E.ResourcesAndTraits": "Resources & Traits", "SW5E.ResourceSecondary": "Resource 2", "SW5E.ResourceTertiary": "Resource 3", + "SW5E.Rest": "Rest", "SW5E.RestL": "L. Rest", "SW5E.RestS": "S. Rest", "SW5E.Ritual": "Ritual", @@ -919,6 +929,7 @@ "SW5E.SchoolLgt": "Light", "SW5E.SchoolTec": "Tech", "SW5E.SchoolUni": "Universal", + "SW5E.SelectItemsPromptTitle": "Select Items", "SW5E.SenseBlindsight": "Blindsight", "SW5E.SenseBS": "Blindsight", "SW5E.SenseDarkvision": "Darkvision", @@ -937,10 +948,10 @@ "SW5E.SheetClassNPC": "Default NPC Sheet", "SW5E.SheetClassNPCOld": "Old NPC Sheet", "SW5E.SheetClassVehicle": "Default Vehicle Sheet", - "SW5E.ShieldDice": "Shield Dice", - "SW5E.ShieldPoints": "Shield Points", - "SW5E.ShieldPointsFormula": "Shield Points Formula", - "SW5E.ShieldRegen": "Regen", + "SW5E.ShieldDice": "Shield Dice", + "SW5E.ShieldPoints": "Shield Points", + "SW5E.ShieldPointsFormula": "Shield Points Formula", + "SW5E.ShieldRegen": "Regen", "SW5E.ShortRest": "Short Rest", "SW5E.ShortRestEpic": "Short Rest (5 minutes)", "SW5E.ShortRestGritty": "Short Rest (8 hours)", @@ -975,12 +986,12 @@ "SW5E.SkillPrc": "Perception", "SW5E.SkillPrf": "Performance", "SW5E.SkillPromptTitle": "{skill} Skill Check", - "SW5E.Skip": "Skip", "SW5E.Skills": "Skills", "SW5E.SkillSlt": "Sleight of Hand", "SW5E.SkillSte": "Stealth", "SW5E.SkillSur": "Survival", "SW5E.SkillTec": "Technology", + "SW5E.Skip": "Skip", "SW5E.Slots": "Slots", "SW5E.Source": "Source", "SW5E.Special": "Special", @@ -991,8 +1002,8 @@ "SW5E.Speed": "Speed", "SW5E.SpeedSpecial": "Special Movement", "SW5E.StarshipAmbassador": "Ambassador", - "SW5E.StarshipArmorandShields": "Starship Armor and Shields", "SW5E.StarshipArmorandShieldProps": "Starship Armor & Shield Properties", + "SW5E.StarshipArmorandShields": "Starship Armor and Shields", "SW5E.StarshipBattleship": "Battleship", "SW5E.StarshipBlockadeShip": "Blockade Ship", "SW5E.StarshipBomber": "Bomber", @@ -1117,6 +1128,7 @@ "SW5E.TraitToolProf": "Tool Proficiencies", "SW5E.TraitWeaponProf": "Weapon Proficiencies", "SW5E.Type": "Type", + "SW5E.Uncrewed": "Uncrewed", "SW5E.Unequipped": "Unequipped", "SW5E.UniversalPowerDC": "Universal Power DC", "SW5E.Unlimited": "Unlimited", @@ -1141,16 +1153,27 @@ "SW5E.VersatileDamage": "Versatile Damage", "SW5E.VsDC": "vs DC.", "SW5E.WeaponAmmo": "Ammunition", + "SW5E.WeaponBlasterPistolProficiency": "Blaster Pistol", + "SW5E.WeaponChakramProficiency": "Chakrams", + "SW5E.WeaponDoubleBladeProficiency": "Doubleblade", + "SW5E.WeaponDoubleSaberProficiency": "Doublesaber", + "SW5E.WeaponDoubleShotoProficiency": "Doubleshoto", + "SW5E.WeaponDoubleSwordProficiency": "Doublesword", + "SW5E.WeaponHiddenBladeProficiency": "Hidden Blade", "SW5E.WeaponImprov": "Improvised", + "SW5E.WeaponImprovisedProficiency": "Improvised Weapons", + "SW5E.WeaponLightFoilProficiency": "Lightfoil", + "SW5E.WeaponLightRingProficiency": "Light Ring", "SW5E.WeaponMartialB": "Martial Blaster", + "SW5E.WeaponMartialBlasterProficiency": "Martial Blasters", + "SW5E.WeaponMartialLightweaponProficiency": "Martial Lightweapons", "SW5E.WeaponMartialLW": "Martial Lightweapon", - "SW5E.WeaponPrimarySW": "Primary (Starship)", - "SW5E.WeaponSecondarySW": "Secondary (Starship)", - "SW5E.WeaponTertiarySW": "Tertiary (Starship)", - "SW5E.WeaponQuaternarySW": "Quaternary (Starship)", "SW5E.WeaponMartialProficiency": "Martial Weapons", + "SW5E.WeaponMartialVibroweaponProficiency": "Martial Vibroweapons", "SW5E.WeaponMartialVW": "Martial Vibroweapon", "SW5E.WeaponNatural": "Natural", + "SW5E.WeaponNaturalProficiency": "Natural Weapons", + "SW5E.WeaponPrimarySW": "Primary (Starship)", "SW5E.WeaponPropertiesAmm": "Ammunition", "SW5E.WeaponPropertiesAut": "Auto", "SW5E.WeaponPropertiesBur": "Burst", @@ -1168,14 +1191,14 @@ "SW5E.WeaponPropertiesFix": "Fixed", "SW5E.WeaponPropertiesFoc": "Focus", "SW5E.WeaponPropertiesHid": "Hidden", - "SW5E.WeaponPropertiesHvy": "Heavy", "SW5E.WeaponPropertiesHom": "Homing", + "SW5E.WeaponPropertiesHvy": "Heavy", "SW5E.WeaponPropertiesIon": "Ionizing", "SW5E.WeaponPropertiesKen": "Keen", "SW5E.WeaponPropertiesLgt": "Light", "SW5E.WeaponPropertiesLum": "Luminous", - "SW5E.WeaponPropertiesMlt": "Melt", "SW5E.WeaponPropertiesMig": "Mighty", + "SW5E.WeaponPropertiesMlt": "Melt", "SW5E.WeaponPropertiesOvr": "Overheat", "SW5E.WeaponPropertiesPic": "Piercing", "SW5E.WeaponPropertiesPow": "Power", @@ -1194,32 +1217,21 @@ "SW5E.WeaponPropertiesVer": "Versatile", "SW5E.WeaponPropertiesVic": "Vicious", "SW5E.WeaponPropertiesZon": "Zone", + "SW5E.WeaponQuaternarySW": "Quaternary (Starship)", + "SW5E.WeaponSaberWhipProficiency": "Saberwhip", + "SW5E.WeaponSecondarySW": "Secondary (Starship)", "SW5E.WeaponSiege": "Siege", "SW5E.WeaponSimpleB": "Simple Blaster", + "SW5E.WeaponSimpleBlasterProficiency": "Simple Blasters", + "SW5E.WeaponSimpleLightweaponProficiency": "Simple Lightweapons", "SW5E.WeaponSimpleLW": "Simple Lightweapon", - "SW5E.WeaponBlasterPistolProficiency": "Blaster Pistol", - "SW5E.WeaponChakramProficiency": "Chakrams", - "SW5E.WeaponDoubleBladeProficiency": "Doubleblade", - "SW5E.WeaponDoubleSaberProficiency": "Doublesaber", - "SW5E.WeaponDoubleShotoProficiency": "Doubleshoto", - "SW5E.WeaponDoubleSwordProficiency": "Doublesword", - "SW5E.WeaponHiddenBladeProficiency": "Hidden Blade", - "SW5E.WeaponImprovisedProficiency": "Improvised Weapons", "SW5E.WeaponSimpleProficiency": "Simple Weapons", - "SW5E.WeaponLightFoilProficiency": "Lightfoil", - "SW5E.WeaponLightRingProficiency": "Light Ring", - "SW5E.WeaponMartialBlasterProficiency": "Martial Blasters", - "SW5E.WeaponMartialLightweaponProficiency": "Martial Lightweapons", - "SW5E.WeaponMartialVibroweaponProficiency": "Martial Vibroweapons", - "SW5E.WeaponNaturalProficiency": "Natural Weapons", - "SW5E.WeaponSaberWhipProficiency": "Saberwhip", - "SW5E.WeaponSimpleBlasterProficiency": "Simple Blasters", - "SW5E.WeaponSimpleLightweaponProficiency": "Simple Lightweapons", - "SW5E.WeaponSimpleVibroweaponProficiency": "Simple Vibroweapons", + "SW5E.WeaponSimpleVibroweaponProficiency": "Simple Vibroweapons", "SW5E.WeaponSimpleVW": "Simple Vibroweapon", - "SW5E.WeaponTechbladeProficiency": "Techblades", - "SW5E.WeaponVibrorapierProficiency": "Vibrorapier", - "SW5E.WeaponVibrowhipProficiency": "Vibrowhip", "SW5E.WeaponSizeAbb": "Size", + "SW5E.WeaponTechbladeProficiency": "Techblades", + "SW5E.WeaponTertiarySW": "Tertiary (Starship)", + "SW5E.WeaponVibrorapierProficiency": "Vibrorapier", + "SW5E.WeaponVibrowhipProficiency": "Vibrowhip", "SW5E.Weight": "Weight" } \ No newline at end of file diff --git a/module/actor/entity.js b/module/actor/entity.js index 228b08d4..0fe01203 100644 --- a/module/actor/entity.js +++ b/module/actor/entity.js @@ -1,8 +1,8 @@ -import { d20Roll, damageRoll } from "../dice.js"; +import {d20Roll, damageRoll} from "../dice.js"; import SelectItemsPrompt from "../apps/select-items-prompt.js"; import ShortRestDialog from "../apps/short-rest.js"; import LongRestDialog from "../apps/long-rest.js"; -import {SW5E} from '../config.js'; +import {SW5E} from "../config.js"; import Item5e from "../item/entity.js"; /** @@ -10,1775 +10,1875 @@ import Item5e from "../item/entity.js"; * @extends {Actor} */ export default class Actor5e extends Actor { + /** + * The data source for Actor5e.classes allowing it to be lazily computed. + * @type {Object} + * @private + */ + _classes = undefined; - /** - * The data source for Actor5e.classes allowing it to be lazily computed. - * @type {Object} - * @private - */ - _classes = undefined; + /* -------------------------------------------- */ + /* Properties */ + /* -------------------------------------------- */ - /* -------------------------------------------- */ - /* Properties */ - /* -------------------------------------------- */ - - /** - * A mapping of classes belonging to this Actor. - * @type {Object} - */ - get classes() { - if ( this._classes !== undefined ) return this._classes; - if ( this.data.type !== "character" ) return this._classes = {}; - return this._classes = this.items.filter((item) => item.type === "class").reduce((obj, cls) => { - obj[cls.name.slugify({strict: true})] = cls; - return obj; - }, {}); - } - - /* -------------------------------------------- */ - - /** - * Is this Actor currently polymorphed into some other creature? - * @type {boolean} - */ - get isPolymorphed() { - return this.getFlag("sw5e", "isPolymorphed") || false; - } - - /* -------------------------------------------- */ - /* Methods */ - /* -------------------------------------------- */ - - /** @override */ - prepareData() { - super.prepareData(); - - // iterate over owned items and recompute attributes that depend on prepared actor data - this.items.forEach(item => item.prepareFinalAttributes()); - } - - /* -------------------------------------------- */ - - /** @override */ - prepareBaseData() { - switch ( this.data.type ) { - case "character": - return this._prepareCharacterData(this.data); - case "npc": - return this._prepareNPCData(this.data); - case "starship": - return this._prepareStarshipData(this.data); - case "vehicle": - return this._prepareVehicleData(this.data); + /** + * A mapping of classes belonging to this Actor. + * @type {Object} + */ + get classes() { + if (this._classes !== undefined) return this._classes; + if (this.data.type !== "character") return (this._classes = {}); + return (this._classes = this.items + .filter((item) => item.type === "class") + .reduce((obj, cls) => { + obj[cls.name.slugify({strict: true})] = cls; + return obj; + }, {})); } - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - prepareDerivedData() { - const actorData = this.data; - const data = actorData.data; - const flags = actorData.flags.sw5e || {}; - const bonuses = getProperty(data, "bonuses.abilities") || {}; + /** + * Is this Actor currently polymorphed into some other creature? + * @type {boolean} + */ + get isPolymorphed() { + return this.getFlag("sw5e", "isPolymorphed") || false; + } - // Retrieve data for polymorphed actors - let originalSaves = null; - let originalSkills = null; - if (this.isPolymorphed) { - const transformOptions = this.getFlag('sw5e', 'transformOptions'); - const original = game.actors?.get(this.getFlag('sw5e', 'originalActor')); - if (original) { - if (transformOptions.mergeSaves) { - originalSaves = original.data.data.abilities; + /* -------------------------------------------- */ + /* Methods */ + /* -------------------------------------------- */ + + /** @override */ + prepareData() { + super.prepareData(); + + // iterate over owned items and recompute attributes that depend on prepared actor data + this.items.forEach((item) => item.prepareFinalAttributes()); + } + + /* -------------------------------------------- */ + + /** @override */ + prepareBaseData() { + switch (this.data.type) { + case "character": + return this._prepareCharacterData(this.data); + case "npc": + return this._prepareNPCData(this.data); + case "starship": + return this._prepareStarshipData(this.data); + case "vehicle": + return this._prepareVehicleData(this.data); } - if (transformOptions.mergeSkills) { - originalSkills = original.data.data.skills; + } + + /* -------------------------------------------- */ + + /** @override */ + prepareDerivedData() { + const actorData = this.data; + const data = actorData.data; + const flags = actorData.flags.sw5e || {}; + const bonuses = getProperty(data, "bonuses.abilities") || {}; + + // Retrieve data for polymorphed actors + let originalSaves = null; + let originalSkills = null; + if (this.isPolymorphed) { + const transformOptions = this.getFlag("sw5e", "transformOptions"); + const original = game.actors?.get(this.getFlag("sw5e", "originalActor")); + if (original) { + if (transformOptions.mergeSaves) { + originalSaves = original.data.data.abilities; + } + if (transformOptions.mergeSkills) { + originalSkills = original.data.data.skills; + } + } } - } - } - // 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)) { - abl.mod = Math.floor((abl.value - 10) / 2); - abl.prof = (abl.proficient || 0) * data.attributes.prof; - 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) { - abl.save = Math.max(abl.save, originalSaves[id].save); - } - } - - // Inventory encumbrance - data.attributes.encumbrance = this._computeEncumbrance(actorData); - - // Prepare skills - this._prepareSkills(actorData, bonuses, checkBonus, originalSkills); - - // Reset class store to ensure it is updated with any changes - this._classes = undefined; - - // Determine Initiative Modifier - const init = data.attributes.init; - const athlete = flags.remarkableAthlete; - const joat = flags.jackOfAllTrades; - init.mod = data.abilities.dex.mod; - 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.value = init.value ?? 0; - init.bonus = init.value + (flags.initiativeAlert ? 5 : 0); - init.total = init.mod + init.prof + init.bonus; - - // Cache labels - this.labels = {}; - if ( this.type === "npc" ) { - this.labels["creatureType"] = this.constructor.formatCreatureType(data.details.type); - } - - // Prepare power-casting data - this._computePowercastingProgression(this.data); - } - - /* -------------------------------------------- */ - - /** - * Return the amount of experience required to gain a certain character level. - * @param level {Number} The desired level - * @return {Number} The XP required - */ - getLevelExp(level) { - const levels = CONFIG.SW5E.CHARACTER_EXP_LEVELS; - return levels[Math.min(level, levels.length - 1)]; - } - - /* -------------------------------------------- */ - - /** - * Return the amount of experience granted by killing a creature of a certain CR. - * @param cr {Number} The creature's challenge rating - * @return {Number} The amount of experience granted per kill - */ - getCRExp(cr) { - if (cr < 1.0) return Math.max(200 * cr, 10); - return CONFIG.SW5E.CR_EXP_LEVELS[cr]; - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - getRollData() { - const data = super.getRollData(); - data.prof = this.data.data.attributes.prof || 0; - data.classes = Object.entries(this.classes).reduce((obj, e) => { - const [slug, cls] = e; - obj[slug] = cls.data.data; - return obj; - }, {}); - return data; - } - - /* -------------------------------------------- */ - - /** - * Given a list of items to add to the Actor, optionally prompt the - * user for which they would like to add. - * @param {Array.} items - The items being added to the Actor. - * @param {boolean} [prompt=true] - Whether or not to prompt the user. - * @returns {Promise} - */ - async addEmbeddedItems(items, prompt=true) { - let itemsToAdd = items; - if ( !items.length ) return []; - - // Obtain the array of item creation data - let toCreate = []; - if (prompt) { - const itemIdsToAdd = await SelectItemsPrompt.create(items, { - hint: game.i18n.localize('SW5E.AddEmbeddedItemPromptHint') - }); - for (let item of items) { - if (itemIdsToAdd.includes(item.id)) toCreate.push(item.toObject()); - } - } else { - toCreate = items.map(item => item.toObject()); - } - - // Create the requested items - if (itemsToAdd.length === 0) return []; - return Item5e.createDocuments(toCreate, {parent: this}); - } - - /* -------------------------------------------- */ - - /** - * Get a list of features to add to the Actor when a class item is updated. - * Optionally prompt the user for which they would like to add. - */ - async getClassFeatures({className, archetypeName, level}={}) { - const existing = new Set(this.items.map(i => i.name)); - const features = await Actor5e.loadClassFeatures({className, archetypeName, level}); - return features.filter(f => !existing.has(f.name)) || []; - } - - /* -------------------------------------------- */ - - /** - * Return the features which a character is awarded for each class level - * @param {string} className The class name being added - * @param {string} archetypeName The archetype of the class being added, if any - * @param {number} level The number of levels in the added class - * @param {number} priorLevel The previous level of the added class - * @return {Promise} Array of Item5e entities - */ - static async loadClassFeatures({className="", archetypeName="", level=1, priorLevel=0}={}) { - className = className.toLowerCase(); - archetypeName = archetypeName.slugify(); - - // Get the configuration of features which may be added - const clsConfig = CONFIG.SW5E.classFeatures[className]; - if (!clsConfig) return []; - - // Acquire class features - let ids = []; - for ( let [l, f] of Object.entries(clsConfig.features || {}) ) { - l = parseInt(l); - if ( (l <= level) && (l > priorLevel) ) ids = ids.concat(f); - } - - // Acquire archetype features - const archConfig = clsConfig.archetypes[archetypeName] || {}; - for ( let [l, f] of Object.entries(archConfig.features || {}) ) { - l = parseInt(l); - if ( (l <= level) && (l > priorLevel) ) ids = ids.concat(f); - } - - // Load item data for all identified features - const features = []; - for ( let id of ids ) { - features.push(await 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; - } - - /* -------------------------------------------- */ - /* Data Preparation Helpers */ - /* -------------------------------------------- */ - - /** - * Prepare Character type specific data - */ - _prepareCharacterData(actorData) { - const data = actorData.data; - - // Determine character level and available hit dice based on owned Class items - const [level, hd] = this.items.reduce((arr, item) => { - if ( item.type === "class" ) { - const classLevels = parseInt(item.data.data.levels) || 1; - arr[0] += classLevels; - arr[1] += classLevels - (parseInt(item.data.data.hitDiceUsed) || 0); - } - return arr; - }, [0, 0]); - data.details.level = level; - data.attributes.hd = hd; - - // Character proficiency bonus - data.attributes.prof = Math.floor((level + 7) / 4); - - // Experience required for next level - const xp = data.details.xp; - xp.max = this.getLevelExp(level || 1); - const prior = this.getLevelExp(level - 1 || 0); - const required = xp.max - prior; - const pct = Math.round((xp.value - prior) * 100 / required); - xp.pct = Math.clamped(pct, 0, 100); - } - - /* -------------------------------------------- */ - - /** - * Prepare NPC type specific data - */ - _prepareNPCData(actorData) { - const data = actorData.data; - - // Kill Experience - data.details.xp.value = this.getCRExp(data.details.cr); - - // Proficiency - data.attributes.prof = Math.floor((Math.max(data.details.cr, 1) + 7) / 4); - - // Powercaster Level - if ( data.attributes.powercasting && !Number.isNumeric(data.details.powerLevel) ) { - data.details.powerLevel = Math.max(data.details.cr, 1); - } - } - - /* -------------------------------------------- */ - - /** - * Prepare vehicle type-specific data - * @param actorData - * @private - */ - _prepareVehicleData(actorData) {} - - /* -------------------------------------------- */ - - /** - * Prepare starship type-specific data - * @param actorData - * @private - */ - _prepareStarshipData(actorData) { - const data = actorData.data; - - // Proficiency - data.attributes.prof = Math.floor((Math.max(data.details.tier, 1) + 7) / 4); - - // Link hull to hp and shields to temp hp - data.attributes.hull.value = data.attributes.hp.value; - data.attributes.hull.max = data.attributes.hp.max; - data.attributes.shld.value = data.attributes.hp.temp; - data.attributes.shld.max = data.attributes.hp.tempmax; - } - - /* -------------------------------------------- */ - - /** - * Prepare skill checks. - * @param actorData - * @param bonuses Global bonus data. - * @param checkBonus Ability check specific bonus. - * @param originalSkills A transformed actor's original actor's skills. - * @private - */ - _prepareSkills(actorData, bonuses, checkBonus, originalSkills) { - if (actorData.type === 'vehicle') return; - - const data = actorData.data; - const flags = actorData.flags.sw5e || {}; - - // Skill modifiers - const feats = SW5E.characterFlags; - const athlete = flags.remarkableAthlete; - const joat = flags.jackOfAllTrades; - const observant = flags.observantFeat; - const skillBonus = Number.isNumeric(bonuses.skill) ? parseInt(bonuses.skill) : 0; - for (let [id, skl] of Object.entries(data.skills)) { - skl.value = Math.clamped(Number(skl.value).toNearest(0.5), 0, 2) ?? 0; - let round = Math.floor; - - // Remarkable - if ( athlete && (skl.value < 0.5) && feats.remarkableAthlete.abilities.includes(skl.ability) ) { - skl.value = 0.5; - round = Math.ceil; - } - - // Jack of All Trades - if ( joat && (skl.value < 0.5) ) { - skl.value = 0.5; - } - - // Polymorph Skill Proficiencies - if ( originalSkills ) { - skl.value = Math.max(skl.value, originalSkills[id].value); - } - - // Compute modifier - skl.bonus = checkBonus + skillBonus; - skl.mod = data.abilities[skl.ability].mod; - skl.prof = round(skl.value * data.attributes.prof); - skl.total = skl.mod + skl.prof + skl.bonus; - - // Compute passive bonus - const passive = observant && (feats.observantFeat.skills.includes(id)) ? 5 : 0; - skl.passive = 10 + skl.total + passive; - } - } - - /* -------------------------------------------- */ - - /** - * Prepare data related to the power-casting capabilities of the Actor - * @private - */ - _computePowercastingProgression (actorData) { - if (actorData.type === 'vehicle' || actorData.type === 'starship') return; - const ad = actorData.data; - const powers = ad.powers; - const isNPC = actorData.type === 'npc'; - - // Powercasting DC - // TODO: Consider an option for using the variant rule of all powers use the same value - ad.attributes.powerForceLightDC = 8 + ad.abilities.wis.mod + ad.attributes.prof ?? 10; - ad.attributes.powerForceDarkDC = 8 + ad.abilities.cha.mod + ad.attributes.prof ?? 10; - ad.attributes.powerForceUnivDC = Math.max(ad.attributes.powerForceLightDC,ad.attributes.powerForceDarkDC) ?? 10 - ad.attributes.powerTechDC = 8 + ad.abilities.int.mod + ad.attributes.prof ?? 10; - - // Translate the list of classes into force and tech power-casting progression - const forceProgression = { - classes: 0, - levels: 0, - multi: 0, - maxClass: "none", - maxClassPriority: 0, - maxClassLevels: 0, - maxClassPowerLevel: 0, - powersKnown: 0, - points: 0 - }; - const techProgression = { - classes: 0, - levels: 0, - multi: 0, - maxClass: "none", - maxClassPriority: 0, - maxClassLevels: 0, - maxClassPowerLevel: 0, - powersKnown: 0, - points: 0 - }; - - // Tabulate the total power-casting progression - const classes = this.data.items.filter(i => i.type === "class"); - let priority = 0; - for ( let cls of classes ) { - const d = cls.data.data; - if ( d.powercasting.progression === "none" ) continue; - const levels = d.levels; - const prog = d.powercasting.progression; - // TODO: Consider a more dynamic system - switch (prog) { - case 'consular': - priority = 3; - forceProgression.levels += levels; - forceProgression.multi += (SW5E.powerMaxLevel['consular'][19]/9)*levels; - forceProgression.classes++; - // see if class controls high level forcecasting - if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){ - forceProgression.maxClass = 'consular'; - forceProgression.maxClassLevels = levels; - forceProgression.maxClassPriority = priority; - forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['consular'][Math.clamped((levels - 1), 0, 20)]; - } - // calculate points and powers known - forceProgression.powersKnown += SW5E.powersKnown['consular'][Math.clamped((levels - 1), 0, 20)]; - forceProgression.points += SW5E.powerPoints['consular'][Math.clamped((levels - 1), 0, 20)]; - break; - case 'engineer': - priority = 2 - techProgression.levels += levels; - techProgression.multi += (SW5E.powerMaxLevel['engineer'][19]/9)*levels; - techProgression.classes++; - // see if class controls high level techcasting - if ((levels >= techProgression.maxClassLevels) && (priority > techProgression.maxClassPriority)){ - techProgression.maxClass = 'engineer'; - techProgression.maxClassLevels = levels; - techProgression.maxClassPriority = priority; - techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['engineer'][Math.clamped((levels - 1), 0, 20)]; - } - techProgression.powersKnown += SW5E.powersKnown['engineer'][Math.clamped((levels - 1), 0, 20)]; - techProgression.points += SW5E.powerPoints['engineer'][Math.clamped((levels - 1), 0, 20)]; - break; - case 'guardian': - priority = 1; - forceProgression.levels += levels; - forceProgression.multi += (SW5E.powerMaxLevel['guardian'][19]/9)*levels; - forceProgression.classes++; - // see if class controls high level forcecasting - if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){ - forceProgression.maxClass = 'guardian'; - forceProgression.maxClassLevels = levels; - forceProgression.maxClassPriority = priority; - forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['guardian'][Math.clamped((levels - 1), 0, 20)]; - } - forceProgression.powersKnown += SW5E.powersKnown['guardian'][Math.clamped((levels - 1), 0, 20)]; - forceProgression.points += SW5E.powerPoints['guardian'][Math.clamped((levels - 1), 0, 20)]; - break; - case 'scout': - priority = 1; - techProgression.levels += levels; - techProgression.multi += (SW5E.powerMaxLevel['scout'][19]/9)*levels; - techProgression.classes++; - // see if class controls high level techcasting - if ((levels >= techProgression.maxClassLevels) && (priority > techProgression.maxClassPriority)){ - techProgression.maxClass = 'scout'; - techProgression.maxClassLevels = levels; - techProgression.maxClassPriority = priority; - techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['scout'][Math.clamped((levels - 1), 0, 20)]; - } - techProgression.powersKnown += SW5E.powersKnown['scout'][Math.clamped((levels - 1), 0, 20)]; - techProgression.points += SW5E.powerPoints['scout'][Math.clamped((levels - 1), 0, 20)]; - break; - case 'sentinel': - priority = 2; - forceProgression.levels += levels; - forceProgression.multi += (SW5E.powerMaxLevel['sentinel'][19]/9)*levels; - forceProgression.classes++; - // see if class controls high level forcecasting - if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){ - forceProgression.maxClass = 'sentinel'; - forceProgression.maxClassLevels = levels; - forceProgression.maxClassPriority = priority; - forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['sentinel'][Math.clamped((levels - 1), 0, 20)]; - } - forceProgression.powersKnown += SW5E.powersKnown['sentinel'][Math.clamped((levels - 1), 0, 20)]; - forceProgression.points += SW5E.powerPoints['sentinel'][Math.clamped((levels - 1), 0, 20)]; - break; } - } - - if (isNPC) { - // EXCEPTION: NPC with an explicit power-caster level - if (ad.details.powerForceLevel) { - forceProgression.levels = ad.details.powerForceLevel; - ad.attributes.force.level = forceProgression.levels; - forceProgression.maxClass = ad.attributes.powercasting; - forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel[forceProgression.maxClass][Math.clamped((forceProgression.levels - 1), 0, 20)]; - } - if (ad.details.powerTechLevel) { - techProgression.levels = ad.details.powerTechLevel; - ad.attributes.tech.level = techProgression.levels; - techProgression.maxClass = ad.attributes.powercasting; - techProgression.maxClassPowerLevel = SW5E.powerMaxLevel[techProgression.maxClass][Math.clamped((techProgression.levels - 1), 0, 20)]; - } - } else { - // EXCEPTION: multi-classed progression uses multi rounded down rather than levels - if (forceProgression.classes > 1) { - forceProgression.levels = Math.floor(forceProgression.multi); - forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['multi'][forceProgression.levels - 1]; - } - if (techProgression.classes > 1) { - techProgression.levels = Math.floor(techProgression.multi); - techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['multi'][techProgression.levels - 1]; - } - } - - - // Look up the number of slots per level from the powerLimit table - let forcePowerLimit = Array.from(SW5E.powerLimit['none']); - for (let i = 0; i < (forceProgression.maxClassPowerLevel); i++) { - forcePowerLimit[i] = SW5E.powerLimit[forceProgression.maxClass][i]; - } - - for ( let [n, lvl] of Object.entries(powers) ) { - let i = parseInt(n.slice(-1)); - if ( Number.isNaN(i) ) continue; - if ( Number.isNumeric(lvl.foverride) ) lvl.fmax = Math.max(parseInt(lvl.foverride), 0); - else lvl.fmax = forcePowerLimit[i-1] || 0; - if (isNPC){ - lvl.fvalue = lvl.fmax; - }else{ - lvl.fvalue = Math.min(parseInt(lvl.fvalue || lvl.value || lvl.fmax),lvl.fmax); - } - } - - let techPowerLimit = Array.from(SW5E.powerLimit['none']); - for (let i = 0; i < (techProgression.maxClassPowerLevel); i++) { - techPowerLimit[i] = SW5E.powerLimit[techProgression.maxClass][i]; - } - - for ( let [n, lvl] of Object.entries(powers) ) { - let i = parseInt(n.slice(-1)); - if ( Number.isNaN(i) ) continue; - if ( Number.isNumeric(lvl.toverride) ) lvl.tmax = Math.max(parseInt(lvl.toverride), 0); - else lvl.tmax = techPowerLimit[i-1] || 0; - if (isNPC){ - lvl.tvalue = lvl.tmax; - }else{ - lvl.tvalue = Math.min(parseInt(lvl.tvalue || lvl.value || lvl.tmax),lvl.tmax); - } - } - - // Set Force and tech power for PC Actors - if (!isNPC) { - if (forceProgression.levels) { - ad.attributes.force.known.max = forceProgression.powersKnown; - ad.attributes.force.points.max = forceProgression.points + Math.max(ad.abilities.wis.mod, ad.abilities.cha.mod); - ad.attributes.force.level = forceProgression.levels; - } - if (techProgression.levels){ - ad.attributes.tech.known.max = techProgression.powersKnown; - ad.attributes.tech.points.max = techProgression.points + ad.abilities.int.mod; - ad.attributes.tech.level = techProgression.levels; - } - } - - - // Tally Powers Known and check for migration first to avoid errors - let hasKnownPowers = actorData?.data?.attributes?.force?.known?.value !== undefined; - if ( hasKnownPowers ) { - const knownPowers = this.data.items.filter(i => i.type === "power"); - let knownForcePowers = 0; - let knownTechPowers = 0; - for ( let knownPower of knownPowers ) { - const d = knownPower.data; - switch (knownPower.data.school){ - case "lgt": - case "uni": - case "drk":{ - knownForcePowers++; - break; - } - case "tec":{ - knownTechPowers++; - break; - } + // 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)) { + abl.mod = Math.floor((abl.value - 10) / 2); + abl.prof = (abl.proficient || 0) * data.attributes.prof; + 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) { + abl.save = Math.max(abl.save, originalSaves[id].save); + } } - } - ad.attributes.force.known.value = knownForcePowers; - ad.attributes.tech.known.value = knownTechPowers; - } - } - /* -------------------------------------------- */ + // Inventory encumbrance + data.attributes.encumbrance = this._computeEncumbrance(actorData); - /** - * Compute the level and percentage of encumbrance for an Actor. - * - * Optionally include the weight of carried currency across all denominations by applying the standard rule - * from the PHB pg. 143 - * @param {Object} actorData The data object for the Actor being rendered - * @returns {{max: number, value: number, pct: number}} An object describing the character's encumbrance level - * @private - */ - _computeEncumbrance(actorData) { - // TODO: Maybe add an option for variant encumbrance - // Get the total weight from items - const physicalItems = ["weapon", "equipment", "consumable", "tool", "backpack", "loot"]; - let weight = actorData.items.reduce((weight, i) => { - if ( !physicalItems.includes(i.type) ) return weight; - const q = i.data.data.quantity || 0; - const w = i.data.data.weight || 0; - return weight + (q * w); - }, 0); + // Prepare skills + this._prepareSkills(actorData, bonuses, checkBonus, originalSkills); - // [Optional] add Currency Weight (for non-transformed actors) - if ( game.settings.get("sw5e", "currencyWeight") && actorData.data.currency ) { - const currency = actorData.data.currency; - const numCoins = Object.values(currency).reduce((val, denom) => val += Math.max(denom, 0), 0); - weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; - } + // Reset class store to ensure it is updated with any changes + this._classes = undefined; - // Determine the encumbrance size class - let mod = { - tiny: 0.5, - sm: 1, - med: 1, - lg: 2, - huge: 4, - grg: 8 - }[actorData.data.traits.size] || 1; - if ( this.getFlag("sw5e", "powerfulBuild") ) mod = Math.min(mod * 2, 8); + // Determine Initiative Modifier + const init = data.attributes.init; + const athlete = flags.remarkableAthlete; + const joat = flags.jackOfAllTrades; + init.mod = data.abilities.dex.mod; + 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.value = init.value ?? 0; + init.bonus = init.value + (flags.initiativeAlert ? 5 : 0); + init.total = init.mod + init.prof + init.bonus; - // Compute Encumbrance percentage - weight = weight.toNearest(0.1); - const max = actorData.data.abilities.str.value * CONFIG.SW5E.encumbrance.strMultiplier * mod; - const pct = Math.clamped((weight * 100) / max, 0, 100); - return { value: weight.toNearest(0.1), max, pct, encumbered: pct > (2/3) }; - } - - /* -------------------------------------------- */ - /* Event Handlers - /* -------------------------------------------- */ - - /** @inheritDoc */ - async _preCreate(data, options, user) { - await super._preCreate(data, options, user); - - // Token size category - const s = CONFIG.SW5E.tokenSizes[this.data.data.traits.size || "med"]; - this.data.token.update({width: s, height: s}); - - // Player character configuration - if ( this.type === "character" ) { - this.data.token.update({vision: true, actorLink: true, disposition: 1}); - } - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - async _preUpdate(changed, options, user) { - await super._preUpdate(changed, options, user); - - // Apply changes in Actor size to Token width/height - const newSize = foundry.utils.getProperty(changed, "data.traits.size"); - if ( newSize && (newSize !== foundry.utils.getProperty(this.data, "data.traits.size")) ) { - let size = CONFIG.SW5E.tokenSizes[newSize]; - if ( !foundry.utils.hasProperty(changed, "token.width") ) { - changed.token = changed.token || {}; - changed.token.height = size; - changed.token.width = size; - } - } - - // Reset death save counters - const isDead = this.data.data.attributes.hp.value <= 0; - if ( isDead && (foundry.utils.getProperty(changed, "data.attributes.hp.value") > 0) ) { - foundry.utils.setProperty(changed, "data.attributes.death.success", 0); - foundry.utils.setProperty(changed, "data.attributes.death.failure", 0); - } - } - - /* -------------------------------------------- */ - - /** - * Assign a class item as the original class for the Actor based on which class has the most levels - * @protected - */ - _assignPrimaryClass() { - const classes = this.itemTypes.class.sort((a, b) => b.data.data.levels - a.data.data.levels); - const newPC = classes[0]?.id || ""; - return this.update({"data.details.originalClass": newPC}); - } - - /* -------------------------------------------- */ - /* Gameplay Mechanics */ - /* -------------------------------------------- */ - - /** @override */ - async modifyTokenAttribute(attribute, value, isDelta, isBar) { - if ( attribute === "attributes.hp" ) { - const hp = getProperty(this.data.data, attribute); - const delta = isDelta ? (-1 * value) : (hp.value + hp.temp) - value; - return this.applyDamage(delta); - } - return super.modifyTokenAttribute(attribute, value, isDelta, isBar); - } - - /* -------------------------------------------- */ - - /** - * Apply a certain amount of damage or healing to the health pool for Actor - * @param {number} amount An amount of damage (positive) or healing (negative) to sustain - * @param {number} multiplier A multiplier which allows for resistance, vulnerability, or healing - * @return {Promise} A Promise which resolves once the damage has been applied - */ - async applyDamage(amount=0, multiplier=1) { - amount = Math.floor(parseInt(amount) * multiplier); - const hp = this.data.data.attributes.hp; - - // Deduct damage from temp HP first - const tmp = parseInt(hp.temp) || 0; - const dt = amount > 0 ? Math.min(tmp, amount) : 0; - - // Remaining goes to health - const tmpMax = parseInt(hp.tempmax) || 0; - const dh = Math.clamped(hp.value - (amount - dt), 0, hp.max + tmpMax); - - // Update the Actor - const updates = { - "data.attributes.hp.temp": tmp - dt, - "data.attributes.hp.value": dh - }; - - // Delegate damage application to a hook - // TODO replace this in the future with a better modifyTokenAttribute function in the core - const allowed = Hooks.call("modifyTokenAttribute", { - attribute: "attributes.hp", - value: amount, - isDelta: false, - isBar: true - }, updates); - return allowed !== false ? this.update(updates) : this; - } - - /* -------------------------------------------- */ - - /** - * Roll a Skill Check - * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus - * @param {string} skillId The skill id (e.g. "ins") - * @param {Object} options Options which configure how the skill check is rolled - * @return {Promise} A Promise which resolves to the created Roll instance - */ - rollSkill(skillId, options={}) { - const skl = this.data.data.skills[skillId]; - const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; - - // Compose roll parts and data - const parts = ["@mod"]; - const data = {mod: skl.mod + skl.prof}; - - // Ability test bonus - if ( bonuses.check ) { - data["checkBonus"] = bonuses.check; - parts.push("@checkBonus"); - } - - // Skill check bonus - if ( bonuses.skill ) { - data["skillBonus"] = bonuses.skill; - parts.push("@skillBonus"); - } - - // Add provided extra roll parts now because they will get clobbered by mergeObject below - if (options.parts?.length > 0) { - parts.push(...options.parts); - } - - // Reliable Talent applies to any skill check we have full or better proficiency in - const reliableTalent = (skl.value >= 1 && this.getFlag("sw5e", "reliableTalent")); - - // Roll and return - const rollData = foundry.utils.mergeObject(options, { - parts: parts, - data: data, - title: game.i18n.format("SW5E.SkillPromptTitle", {skill: CONFIG.SW5E.skills[skillId] || CONFIG.SW5E.starshipSkills[skillId]}), - halflingLucky: this.getFlag("sw5e", "halflingLucky"), - reliableTalent: reliableTalent, - messageData: { - speaker: options.speaker || ChatMessage.getSpeaker({actor: this}), - "flags.sw5e.roll": {type: "skill", skillId } - } - }); - return d20Roll(rollData); - } - - /* -------------------------------------------- */ - - /** - * Roll a generic ability test or saving throw. - * Prompt the user for input on which variety of roll they want to do. - * @param {String}abilityId The ability id (e.g. "str") - * @param {Object} options Options which configure how ability tests or saving throws are rolled - */ - rollAbility(abilityId, options={}) { - const label = CONFIG.SW5E.abilities[abilityId]; - new Dialog({ - title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}), - content: `

${game.i18n.format("SW5E.AbilityPromptText", {ability: label})}

`, - buttons: { - test: { - label: game.i18n.localize("SW5E.ActionAbil"), - callback: () => this.rollAbilityTest(abilityId, options) - }, - save: { - label: game.i18n.localize("SW5E.ActionSave"), - callback: () => this.rollAbilitySave(abilityId, options) + // Cache labels + this.labels = {}; + if (this.type === "npc") { + this.labels["creatureType"] = this.constructor.formatCreatureType(data.details.type); } - } - }).render(true); - } - /* -------------------------------------------- */ - - /** - * Roll an Ability Test - * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus - * @param {String} abilityId The ability ID (e.g. "str") - * @param {Object} options Options which configure how ability tests are rolled - * @return {Promise} A Promise which resolves to the created Roll instance - */ - rollAbilityTest(abilityId, options={}) { - const label = CONFIG.SW5E.abilities[abilityId]; - const abl = this.data.data.abilities[abilityId]; - - // Construct parts - const parts = ["@mod"]; - const data = {mod: abl.mod}; - - // Add feat-related proficiency bonuses - const feats = this.data.flags.sw5e || {}; - if ( feats.remarkableAthlete && SW5E.characterFlags.remarkableAthlete.abilities.includes(abilityId) ) { - parts.push("@proficiency"); - data.proficiency = Math.ceil(0.5 * this.data.data.attributes.prof); - } - else if ( feats.jackOfAllTrades ) { - parts.push("@proficiency"); - data.proficiency = Math.floor(0.5 * this.data.data.attributes.prof); + // Prepare power-casting data + this._computeDerivedPowercasting(this.data); } - // Add global actor bonus - const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; - if ( bonuses.check ) { - parts.push("@checkBonus"); - data.checkBonus = bonuses.check; + /* -------------------------------------------- */ + + /** + * Return the amount of experience required to gain a certain character level. + * @param level {Number} The desired level + * @return {Number} The XP required + */ + getLevelExp(level) { + const levels = CONFIG.SW5E.CHARACTER_EXP_LEVELS; + return levels[Math.min(level, levels.length - 1)]; } - // Add provided extra roll parts now because they will get clobbered by mergeObject below - if (options.parts?.length > 0) { - parts.push(...options.parts); + /* -------------------------------------------- */ + + /** + * Return the amount of experience granted by killing a creature of a certain CR. + * @param cr {Number} The creature's challenge rating + * @return {Number} The amount of experience granted per kill + */ + getCRExp(cr) { + if (cr < 1.0) return Math.max(200 * cr, 10); + return CONFIG.SW5E.CR_EXP_LEVELS[cr]; } - // Roll and return - const rollData = foundry.utils.mergeObject(options, { - parts: parts, - data: data, - title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}), - halflingLucky: feats.halflingLucky, - messageData: { - speaker: options.speaker || ChatMessage.getSpeaker({actor: this}), - "flags.sw5e.roll": {type: "ability", abilityId } - } - }); - return d20Roll(rollData); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ - - /** - * Roll an Ability Saving Throw - * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus - * @param {String} abilityId The ability ID (e.g. "str") - * @param {Object} options Options which configure how ability tests are rolled - * @return {Promise} A Promise which resolves to the created Roll instance - */ - rollAbilitySave(abilityId, options={}) { - const label = CONFIG.SW5E.abilities[abilityId]; - const abl = this.data.data.abilities[abilityId]; - - // Construct parts - const parts = ["@mod"]; - const data = {mod: abl.mod}; - - // Include proficiency bonus - if ( abl.prof > 0 ) { - parts.push("@prof"); - data.prof = abl.prof; + /** @inheritdoc */ + getRollData() { + const data = super.getRollData(); + data.prof = this.data.data.attributes.prof || 0; + data.classes = Object.entries(this.classes).reduce((obj, e) => { + const [slug, cls] = e; + obj[slug] = cls.data.data; + return obj; + }, {}); + return data; } - // Include a global actor ability save bonus - const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; - if ( bonuses.save ) { - parts.push("@saveBonus"); - data.saveBonus = bonuses.save; + /* -------------------------------------------- */ + + /** + * Given a list of items to add to the Actor, optionally prompt the + * user for which they would like to add. + * @param {Array.} items - The items being added to the Actor. + * @param {boolean} [prompt=true] - Whether or not to prompt the user. + * @returns {Promise} + */ + async addEmbeddedItems(items, prompt = true) { + let itemsToAdd = items; + if (!items.length) return []; + + // Obtain the array of item creation data + let toCreate = []; + if (prompt) { + const itemIdsToAdd = await SelectItemsPrompt.create(items, { + hint: game.i18n.localize("SW5E.AddEmbeddedItemPromptHint") + }); + for (let item of items) { + if (itemIdsToAdd.includes(item.id)) toCreate.push(item.toObject()); + } + } else { + toCreate = items.map((item) => item.toObject()); + } + + // Create the requested items + if (itemsToAdd.length === 0) return []; + return Item5e.createDocuments(toCreate, {parent: this}); } - // Add provided extra roll parts now because they will get clobbered by mergeObject below - if (options.parts?.length > 0) { - parts.push(...options.parts); + /* -------------------------------------------- */ + + /** + * Get a list of features to add to the Actor when a class item is updated. + * Optionally prompt the user for which they would like to add. + */ + async getClassFeatures({className, archetypeName, level} = {}) { + const existing = new Set(this.items.map((i) => i.name)); + const features = await Actor5e.loadClassFeatures({className, archetypeName, level}); + return features.filter((f) => !existing.has(f.name)) || []; } - // Roll and return - const rollData = foundry.utils.mergeObject(options, { - parts: parts, - data: data, - title: game.i18n.format("SW5E.SavePromptTitle", {ability: label}), - halflingLucky: this.getFlag("sw5e", "halflingLucky"), - messageData: { - speaker: options.speaker || ChatMessage.getSpeaker({actor: this}), - "flags.sw5e.roll": {type: "save", abilityId } - } - }); - return d20Roll(rollData); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Return the features which a character is awarded for each class level + * @param {string} className The class name being added + * @param {string} archetypeName The archetype of the class being added, if any + * @param {number} level The number of levels in the added class + * @param {number} priorLevel The previous level of the added class + * @return {Promise} Array of Item5e entities + */ + static async loadClassFeatures({className = "", archetypeName = "", level = 1, priorLevel = 0} = {}) { + className = className.toLowerCase(); + archetypeName = archetypeName.slugify(); - /** - * Perform a death saving throw, rolling a d20 plus any global save bonuses - * @param {Object} options Additional options which modify the roll - * @return {Promise} A Promise which resolves to the Roll instance - */ - async rollDeathSave(options={}) { + // Get the configuration of features which may be added + const clsConfig = CONFIG.SW5E.classFeatures[className]; + if (!clsConfig) return []; - // Display a warning if we are not at zero HP or if we already have reached 3 - const death = this.data.data.attributes.death; - if ( (this.data.data.attributes.hp.value > 0) || (death.failure >= 3) || (death.success >= 3)) { - ui.notifications.warn(game.i18n.localize("SW5E.DeathSaveUnnecessary")); - return null; + // Acquire class features + let ids = []; + for (let [l, f] of Object.entries(clsConfig.features || {})) { + l = parseInt(l); + if (l <= level && l > priorLevel) ids = ids.concat(f); + } + + // Acquire archetype features + const archConfig = clsConfig.archetypes[archetypeName] || {}; + for (let [l, f] of Object.entries(archConfig.features || {})) { + l = parseInt(l); + if (l <= level && l > priorLevel) ids = ids.concat(f); + } + + // Load item data for all identified features + const features = []; + for (let id of ids) { + features.push(await 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; } - // Evaluate a global saving throw bonus - const parts = []; - const data = {}; + /* -------------------------------------------- */ + /* Data Preparation Helpers */ + /* -------------------------------------------- */ - // Include a global actor ability save bonus - const bonuses = foundry.utils.getProperty(this.data.data, "bonuses.abilities") || {}; - if ( bonuses.save ) { - parts.push("@saveBonus"); - data.saveBonus = bonuses.save; + /** + * Prepare Character type specific data + */ + _prepareCharacterData(actorData) { + const data = actorData.data; + + // Determine character level and available hit dice based on owned Class items + const [level, hd] = this.items.reduce( + (arr, item) => { + if (item.type === "class") { + const classLevels = parseInt(item.data.data.levels) || 1; + arr[0] += classLevels; + arr[1] += classLevels - (parseInt(item.data.data.hitDiceUsed) || 0); + } + return arr; + }, + [0, 0] + ); + data.details.level = level; + data.attributes.hd = hd; + + // Character proficiency bonus + data.attributes.prof = Math.floor((level + 7) / 4); + + // Experience required for next level + const xp = data.details.xp; + xp.max = this.getLevelExp(level || 1); + const prior = this.getLevelExp(level - 1 || 0); + const required = xp.max - prior; + const pct = Math.round(((xp.value - prior) * 100) / required); + xp.pct = Math.clamped(pct, 0, 100); + + // Add base Powercasting attributes + this._computeBasePowercasting(actorData); } - // Evaluate the roll - const rollData = foundry.utils.mergeObject(options, { - parts: parts, - data: data, - title: game.i18n.localize("SW5E.DeathSavingThrow"), - halflingLucky: this.getFlag("sw5e", "halflingLucky"), - targetValue: 10, - messageData: { - speaker: options.speaker || ChatMessage.getSpeaker({actor: this}), - "flags.sw5e.roll": {type: "death"} - } - }); - const roll = await d20Roll(rollData); - if ( !roll ) return null; + /* -------------------------------------------- */ - // Take action depending on the result - const success = roll.total >= 10; - const d20 = roll.dice[0].total; + /** + * Prepare NPC type specific data + */ + _prepareNPCData(actorData) { + const data = actorData.data; - let chatString; + // Kill Experience + data.details.xp.value = this.getCRExp(data.details.cr); - // Save success - if ( success ) { - let successes = (death.success || 0) + 1; + // Proficiency + data.attributes.prof = Math.floor((Math.max(data.details.cr, 1) + 7) / 4); - // Critical Success = revive with 1hp - if ( d20 === 20 ) { - await this.update({ - "data.attributes.death.success": 0, - "data.attributes.death.failure": 0, - "data.attributes.hp.value": 1 + this._computeBasePowercasting(actorData); + + // Powercaster Level + if (data.attributes.powercasting && !Number.isNumeric(data.details.powerLevel)) { + data.details.powerLevel = Math.max(data.details.cr, 1); + } + } + + /* -------------------------------------------- */ + + /** + * Prepare vehicle type-specific data + * @param actorData + * @private + */ + _prepareVehicleData(actorData) {} + + /* -------------------------------------------- */ + + /** + * Prepare starship type-specific data + * @param actorData + * @private + */ + _prepareStarshipData(actorData) { + const data = actorData.data; + + // Proficiency + data.attributes.prof = Math.floor((Math.max(data.details.tier, 1) + 7) / 4); + + // Link hull to hp and shields to temp hp + data.attributes.hull.value = data.attributes.hp.value; + data.attributes.hull.max = data.attributes.hp.max; + data.attributes.shld.value = data.attributes.hp.temp; + data.attributes.shld.max = data.attributes.hp.tempmax; + } + + /* -------------------------------------------- */ + + /** + * Prepare skill checks. + * @param actorData + * @param bonuses Global bonus data. + * @param checkBonus Ability check specific bonus. + * @param originalSkills A transformed actor's original actor's skills. + * @private + */ + _prepareSkills(actorData, bonuses, checkBonus, originalSkills) { + if (actorData.type === "vehicle") return; + + const data = actorData.data; + const flags = actorData.flags.sw5e || {}; + + // Skill modifiers + const feats = SW5E.characterFlags; + const athlete = flags.remarkableAthlete; + const joat = flags.jackOfAllTrades; + const observant = flags.observantFeat; + const skillBonus = Number.isNumeric(bonuses.skill) ? parseInt(bonuses.skill) : 0; + for (let [id, skl] of Object.entries(data.skills)) { + skl.value = Math.clamped(Number(skl.value).toNearest(0.5), 0, 2) ?? 0; + let round = Math.floor; + + // Remarkable + if (athlete && skl.value < 0.5 && feats.remarkableAthlete.abilities.includes(skl.ability)) { + skl.value = 0.5; + round = Math.ceil; + } + + // Jack of All Trades + if (joat && skl.value < 0.5) { + skl.value = 0.5; + } + + // Polymorph Skill Proficiencies + if (originalSkills) { + skl.value = Math.max(skl.value, originalSkills[id].value); + } + + // Compute modifier + skl.bonus = checkBonus + skillBonus; + skl.mod = data.abilities[skl.ability].mod; + skl.prof = round(skl.value * data.attributes.prof); + skl.total = skl.mod + skl.prof + skl.bonus; + + // Compute passive bonus + const passive = observant && feats.observantFeat.skills.includes(id) ? 5 : 0; + skl.passive = 10 + skl.total + passive; + } + } + + /* -------------------------------------------- */ + + /** + * Prepare data related to the power-casting capabilities of the Actor + * @private + */ + _computeBasePowercasting(actorData) { + if (actorData.type === "vehicle" || actorData.type === "starship") return; + const ad = actorData.data; + const powers = ad.powers; + const isNPC = actorData.type === "npc"; + + // Translate the list of classes into force and tech power-casting progression + const forceProgression = { + classes: 0, + levels: 0, + multi: 0, + maxClass: "none", + maxClassPriority: 0, + maxClassLevels: 0, + maxClassPowerLevel: 0, + powersKnown: 0, + points: 0 + }; + const techProgression = { + classes: 0, + levels: 0, + multi: 0, + maxClass: "none", + maxClassPriority: 0, + maxClassLevels: 0, + maxClassPowerLevel: 0, + powersKnown: 0, + points: 0 + }; + + // Tabulate the total power-casting progression + const classes = this.data.items.filter((i) => i.type === "class"); + let priority = 0; + for (let cls of classes) { + const d = cls.data.data; + if (d.powercasting.progression === "none") continue; + const levels = d.levels; + const prog = d.powercasting.progression; + // TODO: Consider a more dynamic system + switch (prog) { + case "consular": + priority = 3; + forceProgression.levels += levels; + forceProgression.multi += (SW5E.powerMaxLevel["consular"][19] / 9) * levels; + forceProgression.classes++; + // see if class controls high level forcecasting + if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) { + forceProgression.maxClass = "consular"; + forceProgression.maxClassLevels = levels; + forceProgression.maxClassPriority = priority; + forceProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["consular"][Math.clamped(levels - 1, 0, 20)]; + } + // calculate points and powers known + forceProgression.powersKnown += SW5E.powersKnown["consular"][Math.clamped(levels - 1, 0, 20)]; + forceProgression.points += SW5E.powerPoints["consular"][Math.clamped(levels - 1, 0, 20)]; + break; + case "engineer": + priority = 2; + techProgression.levels += levels; + techProgression.multi += (SW5E.powerMaxLevel["engineer"][19] / 9) * levels; + techProgression.classes++; + // see if class controls high level techcasting + if (levels >= techProgression.maxClassLevels && priority > techProgression.maxClassPriority) { + techProgression.maxClass = "engineer"; + techProgression.maxClassLevels = levels; + techProgression.maxClassPriority = priority; + techProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["engineer"][Math.clamped(levels - 1, 0, 20)]; + } + techProgression.powersKnown += SW5E.powersKnown["engineer"][Math.clamped(levels - 1, 0, 20)]; + techProgression.points += SW5E.powerPoints["engineer"][Math.clamped(levels - 1, 0, 20)]; + break; + case "guardian": + priority = 1; + forceProgression.levels += levels; + forceProgression.multi += (SW5E.powerMaxLevel["guardian"][19] / 9) * levels; + forceProgression.classes++; + // see if class controls high level forcecasting + if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) { + forceProgression.maxClass = "guardian"; + forceProgression.maxClassLevels = levels; + forceProgression.maxClassPriority = priority; + forceProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["guardian"][Math.clamped(levels - 1, 0, 20)]; + } + forceProgression.powersKnown += SW5E.powersKnown["guardian"][Math.clamped(levels - 1, 0, 20)]; + forceProgression.points += SW5E.powerPoints["guardian"][Math.clamped(levels - 1, 0, 20)]; + break; + case "scout": + priority = 1; + techProgression.levels += levels; + techProgression.multi += (SW5E.powerMaxLevel["scout"][19] / 9) * levels; + techProgression.classes++; + // see if class controls high level techcasting + if (levels >= techProgression.maxClassLevels && priority > techProgression.maxClassPriority) { + techProgression.maxClass = "scout"; + techProgression.maxClassLevels = levels; + techProgression.maxClassPriority = priority; + techProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["scout"][Math.clamped(levels - 1, 0, 20)]; + } + techProgression.powersKnown += SW5E.powersKnown["scout"][Math.clamped(levels - 1, 0, 20)]; + techProgression.points += SW5E.powerPoints["scout"][Math.clamped(levels - 1, 0, 20)]; + break; + case "sentinel": + priority = 2; + forceProgression.levels += levels; + forceProgression.multi += (SW5E.powerMaxLevel["sentinel"][19] / 9) * levels; + forceProgression.classes++; + // see if class controls high level forcecasting + if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) { + forceProgression.maxClass = "sentinel"; + forceProgression.maxClassLevels = levels; + forceProgression.maxClassPriority = priority; + forceProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["sentinel"][Math.clamped(levels - 1, 0, 20)]; + } + forceProgression.powersKnown += SW5E.powersKnown["sentinel"][Math.clamped(levels - 1, 0, 20)]; + forceProgression.points += SW5E.powerPoints["sentinel"][Math.clamped(levels - 1, 0, 20)]; + break; + } + } + + if (isNPC) { + // EXCEPTION: NPC with an explicit power-caster level + if (ad.details.powerForceLevel) { + forceProgression.levels = ad.details.powerForceLevel; + ad.attributes.force.level = forceProgression.levels; + forceProgression.maxClass = ad.attributes.powercasting; + forceProgression.maxClassPowerLevel = + SW5E.powerMaxLevel[forceProgression.maxClass][Math.clamped(forceProgression.levels - 1, 0, 20)]; + } + if (ad.details.powerTechLevel) { + techProgression.levels = ad.details.powerTechLevel; + ad.attributes.tech.level = techProgression.levels; + techProgression.maxClass = ad.attributes.powercasting; + techProgression.maxClassPowerLevel = + SW5E.powerMaxLevel[techProgression.maxClass][Math.clamped(techProgression.levels - 1, 0, 20)]; + } + } else { + // EXCEPTION: multi-classed progression uses multi rounded down rather than levels + if (forceProgression.classes > 1) { + forceProgression.levels = Math.floor(forceProgression.multi); + forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel["multi"][forceProgression.levels - 1]; + } + if (techProgression.classes > 1) { + techProgression.levels = Math.floor(techProgression.multi); + techProgression.maxClassPowerLevel = SW5E.powerMaxLevel["multi"][techProgression.levels - 1]; + } + } + + // Look up the number of slots per level from the powerLimit table + let forcePowerLimit = Array.from(SW5E.powerLimit["none"]); + for (let i = 0; i < forceProgression.maxClassPowerLevel; i++) { + forcePowerLimit[i] = SW5E.powerLimit[forceProgression.maxClass][i]; + } + + for (let [n, lvl] of Object.entries(powers)) { + let i = parseInt(n.slice(-1)); + if (Number.isNaN(i)) continue; + if (Number.isNumeric(lvl.foverride)) lvl.fmax = Math.max(parseInt(lvl.foverride), 0); + else lvl.fmax = forcePowerLimit[i - 1] || 0; + if (isNPC) { + lvl.fvalue = lvl.fmax; + } else { + lvl.fvalue = Math.min(parseInt(lvl.fvalue || lvl.value || lvl.fmax), lvl.fmax); + } + } + + let techPowerLimit = Array.from(SW5E.powerLimit["none"]); + for (let i = 0; i < techProgression.maxClassPowerLevel; i++) { + techPowerLimit[i] = SW5E.powerLimit[techProgression.maxClass][i]; + } + + for (let [n, lvl] of Object.entries(powers)) { + let i = parseInt(n.slice(-1)); + if (Number.isNaN(i)) continue; + if (Number.isNumeric(lvl.toverride)) lvl.tmax = Math.max(parseInt(lvl.toverride), 0); + else lvl.tmax = techPowerLimit[i - 1] || 0; + if (isNPC) { + lvl.tvalue = lvl.tmax; + } else { + lvl.tvalue = Math.min(parseInt(lvl.tvalue || lvl.value || lvl.tmax), lvl.tmax); + } + } + + // Set Force and tech power for PC Actors + if (!isNPC) { + if (forceProgression.levels) { + ad.attributes.force.known.max = forceProgression.powersKnown; + ad.attributes.force.points.max = forceProgression.points; + ad.attributes.force.level = forceProgression.levels; + } + if (techProgression.levels) { + ad.attributes.tech.known.max = techProgression.powersKnown; + ad.attributes.tech.points.max = techProgression.points; + ad.attributes.tech.level = techProgression.levels; + } + } + + // Tally Powers Known and check for migration first to avoid errors + let hasKnownPowers = actorData?.data?.attributes?.force?.known?.value !== undefined; + if (hasKnownPowers) { + const knownPowers = this.data.items.filter((i) => i.type === "power"); + let knownForcePowers = 0; + let knownTechPowers = 0; + for (let knownPower of knownPowers) { + const d = knownPower.data; + switch (d.data.school) { + case "lgt": + case "uni": + case "drk": { + knownForcePowers++; + break; + } + case "tec": { + knownTechPowers++; + break; + } + } + } + ad.attributes.force.known.value = knownForcePowers; + ad.attributes.tech.known.value = knownTechPowers; + } + } + + /* -------------------------------------------- */ + + /** + * Prepare data related to the power-casting capabilities of the Actor + * @private + */ + _computeDerivedPowercasting(actorData) { + if (!(actorData.type === "character" || actorData.type === "npc")) return; + + const ad = actorData.data; + + // Powercasting DC for Actors and NPCs + // TODO: Consider an option for using the variant rule of all powers use the same value + ad.attributes.powerForceLightDC = 8 + ad.abilities.wis.mod + ad.attributes.prof ?? 10; + ad.attributes.powerForceDarkDC = 8 + ad.abilities.cha.mod + ad.attributes.prof ?? 10; + ad.attributes.powerForceUnivDC = + Math.max(ad.attributes.powerForceLightDC, ad.attributes.powerForceDarkDC) ?? 10; + ad.attributes.powerTechDC = 8 + ad.abilities.int.mod + ad.attributes.prof ?? 10; + + if (actorData.type !== "character") return; + + // Set Force and tech bonus points for PC Actors + if (!!ad.attributes.force.level) { + ad.attributes.force.points.max += Math.max(ad.abilities.wis.mod, ad.abilities.cha.mod); + } + if (!!ad.attributes.tech.level) { + ad.attributes.tech.points.max += ad.abilities.int.mod; + } + } + + /* -------------------------------------------- */ + + /** + * Compute the level and percentage of encumbrance for an Actor. + * + * Optionally include the weight of carried currency across all denominations by applying the standard rule + * from the PHB pg. 143 + * @param {Object} actorData The data object for the Actor being rendered + * @returns {{max: number, value: number, pct: number}} An object describing the character's encumbrance level + * @private + */ + _computeEncumbrance(actorData) { + // TODO: Maybe add an option for variant encumbrance + // Get the total weight from items + const physicalItems = ["weapon", "equipment", "consumable", "tool", "backpack", "loot"]; + let weight = actorData.items.reduce((weight, i) => { + if (!physicalItems.includes(i.type)) return weight; + const q = i.data.data.quantity || 0; + const w = i.data.data.weight || 0; + return weight + q * w; + }, 0); + + // [Optional] add Currency Weight (for non-transformed actors) + if (game.settings.get("sw5e", "currencyWeight") && actorData.data.currency) { + const currency = actorData.data.currency; + const numCoins = Object.values(currency).reduce((val, denom) => (val += Math.max(denom, 0)), 0); + weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; + } + + // Determine the encumbrance size class + let mod = + { + tiny: 0.5, + sm: 1, + med: 1, + lg: 2, + huge: 4, + grg: 8 + }[actorData.data.traits.size] || 1; + if (this.getFlag("sw5e", "powerfulBuild")) mod = Math.min(mod * 2, 8); + + // Compute Encumbrance percentage + weight = weight.toNearest(0.1); + const max = actorData.data.abilities.str.value * CONFIG.SW5E.encumbrance.strMultiplier * mod; + const pct = Math.clamped((weight * 100) / max, 0, 100); + return {value: weight.toNearest(0.1), max, pct, encumbered: pct > 2 / 3}; + } + + /* -------------------------------------------- */ + /* Event Handlers */ + /* -------------------------------------------- */ + + /** @inheritdoc */ + async _preCreate(data, options, user) { + await super._preCreate(data, options, user); + + // Token size category + const s = CONFIG.SW5E.tokenSizes[this.data.data.traits.size || "med"]; + this.data.token.update({width: s, height: s}); + + // Player character configuration + if (this.type === "character") { + this.data.token.update({vision: true, actorLink: true, disposition: 1}); + } + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + async _preUpdate(changed, options, user) { + await super._preUpdate(changed, options, user); + + // Apply changes in Actor size to Token width/height + const newSize = foundry.utils.getProperty(changed, "data.traits.size"); + if (newSize && newSize !== foundry.utils.getProperty(this.data, "data.traits.size")) { + let size = CONFIG.SW5E.tokenSizes[newSize]; + if (!foundry.utils.hasProperty(changed, "token.width")) { + changed.token = changed.token || {}; + changed.token.height = size; + changed.token.width = size; + } + } + + // Reset death save counters + const isDead = this.data.data.attributes.hp.value <= 0; + if (isDead && foundry.utils.getProperty(changed, "data.attributes.hp.value") > 0) { + foundry.utils.setProperty(changed, "data.attributes.death.success", 0); + foundry.utils.setProperty(changed, "data.attributes.death.failure", 0); + } + } + + /* -------------------------------------------- */ + + /** + * Assign a class item as the original class for the Actor based on which class has the most levels + * @protected + */ + _assignPrimaryClass() { + const classes = this.itemTypes.class.sort((a, b) => b.data.data.levels - a.data.data.levels); + const newPC = classes[0]?.id || ""; + return this.update({"data.details.originalClass": newPC}); + } + + /* -------------------------------------------- */ + /* Gameplay Mechanics */ + /* -------------------------------------------- */ + + /** @override */ + async modifyTokenAttribute(attribute, value, isDelta, isBar) { + if (attribute === "attributes.hp") { + const hp = getProperty(this.data.data, attribute); + const delta = isDelta ? -1 * value : hp.value + hp.temp - value; + return this.applyDamage(delta); + } + return super.modifyTokenAttribute(attribute, value, isDelta, isBar); + } + + /* -------------------------------------------- */ + + /** + * Apply a certain amount of damage or healing to the health pool for Actor + * @param {number} amount An amount of damage (positive) or healing (negative) to sustain + * @param {number} multiplier A multiplier which allows for resistance, vulnerability, or healing + * @return {Promise} A Promise which resolves once the damage has been applied + */ + async applyDamage(amount = 0, multiplier = 1) { + amount = Math.floor(parseInt(amount) * multiplier); + const hp = this.data.data.attributes.hp; + + // Deduct damage from temp HP first + const tmp = parseInt(hp.temp) || 0; + const dt = amount > 0 ? Math.min(tmp, amount) : 0; + + // Remaining goes to health + const tmpMax = parseInt(hp.tempmax) || 0; + const dh = Math.clamped(hp.value - (amount - dt), 0, hp.max + tmpMax); + + // Update the Actor + const updates = { + "data.attributes.hp.temp": tmp - dt, + "data.attributes.hp.value": dh + }; + + // Delegate damage application to a hook + // TODO replace this in the future with a better modifyTokenAttribute function in the core + const allowed = Hooks.call( + "modifyTokenAttribute", + { + attribute: "attributes.hp", + value: amount, + isDelta: false, + isBar: true + }, + updates + ); + return allowed !== false ? this.update(updates) : this; + } + + /* -------------------------------------------- */ + + /** + * Roll a Skill Check + * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus + * @param {string} skillId The skill id (e.g. "ins") + * @param {Object} options Options which configure how the skill check is rolled + * @return {Promise} A Promise which resolves to the created Roll instance + */ + rollSkill(skillId, options = {}) { + const skl = this.data.data.skills[skillId]; + const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; + + // Compose roll parts and data + const parts = ["@mod"]; + const data = {mod: skl.mod + skl.prof}; + + // Ability test bonus + if (bonuses.check) { + data["checkBonus"] = bonuses.check; + parts.push("@checkBonus"); + } + + // Skill check bonus + if (bonuses.skill) { + data["skillBonus"] = bonuses.skill; + parts.push("@skillBonus"); + } + + // Add provided extra roll parts now because they will get clobbered by mergeObject below + if (options.parts?.length > 0) { + parts.push(...options.parts); + } + + // Reliable Talent applies to any skill check we have full or better proficiency in + const reliableTalent = skl.value >= 1 && this.getFlag("sw5e", "reliableTalent"); + + // Roll and return + const rollData = foundry.utils.mergeObject(options, { + parts: parts, + data: data, + title: game.i18n.format("SW5E.SkillPromptTitle", { + skill: CONFIG.SW5E.skills[skillId] || CONFIG.SW5E.starshipSkills[skillId] + }), + halflingLucky: this.getFlag("sw5e", "halflingLucky"), + reliableTalent: reliableTalent, + messageData: { + "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}), + "flags.sw5e.roll": {type: "skill", skillId} + } }); - chatString = "SW5E.DeathSaveCriticalSuccess"; - } + return d20Roll(rollData); + } - // 3 Successes = survive and reset checks - else if ( successes === 3 ) { - await this.update({ - "data.attributes.death.success": 0, - "data.attributes.death.failure": 0 + /* -------------------------------------------- */ + + /** + * Roll a generic ability test or saving throw. + * Prompt the user for input on which variety of roll they want to do. + * @param {String}abilityId The ability id (e.g. "str") + * @param {Object} options Options which configure how ability tests or saving throws are rolled + */ + rollAbility(abilityId, options = {}) { + const label = CONFIG.SW5E.abilities[abilityId]; + new Dialog({ + title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}), + content: `

${game.i18n.format("SW5E.AbilityPromptText", {ability: label})}

`, + buttons: { + test: { + label: game.i18n.localize("SW5E.ActionAbil"), + callback: () => this.rollAbilityTest(abilityId, options) + }, + save: { + label: game.i18n.localize("SW5E.ActionSave"), + callback: () => this.rollAbilitySave(abilityId, options) + } + } + }).render(true); + } + + /* -------------------------------------------- */ + + /** + * Roll an Ability Test + * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus + * @param {String} abilityId The ability ID (e.g. "str") + * @param {Object} options Options which configure how ability tests are rolled + * @return {Promise} A Promise which resolves to the created Roll instance + */ + rollAbilityTest(abilityId, options = {}) { + const label = CONFIG.SW5E.abilities[abilityId]; + const abl = this.data.data.abilities[abilityId]; + + // Construct parts + const parts = ["@mod"]; + const data = {mod: abl.mod}; + + // Add feat-related proficiency bonuses + const feats = this.data.flags.sw5e || {}; + if (feats.remarkableAthlete && SW5E.characterFlags.remarkableAthlete.abilities.includes(abilityId)) { + parts.push("@proficiency"); + data.proficiency = Math.ceil(0.5 * this.data.data.attributes.prof); + } else if (feats.jackOfAllTrades) { + parts.push("@proficiency"); + data.proficiency = Math.floor(0.5 * this.data.data.attributes.prof); + } + + // Add global actor bonus + const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; + if (bonuses.check) { + parts.push("@checkBonus"); + data.checkBonus = bonuses.check; + } + + // Add provided extra roll parts now because they will get clobbered by mergeObject below + if (options.parts?.length > 0) { + parts.push(...options.parts); + } + + // Roll and return + const rollData = foundry.utils.mergeObject(options, { + parts: parts, + data: data, + title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}), + halflingLucky: feats.halflingLucky, + messageData: { + "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}), + "flags.sw5e.roll": {type: "ability", abilityId} + } }); - chatString = "SW5E.DeathSaveSuccess"; - } - - // Increment successes - else await this.update({"data.attributes.death.success": Math.clamped(successes, 0, 3)}); + return d20Roll(rollData); } - // Save failure - else { - let failures = (death.failure || 0) + (d20 === 1 ? 2 : 1); - await this.update({"data.attributes.death.failure": Math.clamped(failures, 0, 3)}); - if ( failures >= 3 ) { // 3 Failures = death - chatString = "SW5E.DeathSaveFailure"; - } - } + /* -------------------------------------------- */ - // Display success/failure chat message - if ( chatString ) { - let chatData = { content: game.i18n.format(chatString, {name: this.name}), speaker }; - ChatMessage.applyRollMode(chatData, roll.options.rollMode); - await ChatMessage.create(chatData); - } + /** + * Roll an Ability Saving Throw + * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus + * @param {String} abilityId The ability ID (e.g. "str") + * @param {Object} options Options which configure how ability tests are rolled + * @return {Promise} A Promise which resolves to the created Roll instance + */ + rollAbilitySave(abilityId, options = {}) { + const label = CONFIG.SW5E.abilities[abilityId]; + const abl = this.data.data.abilities[abilityId]; - // Return the rolled result - return roll; - } + // Construct parts + const parts = ["@mod"]; + const data = {mod: abl.mod}; - /* -------------------------------------------- */ - - /** - * Roll a hit die of the appropriate type, gaining hit points equal to the die roll plus your CON modifier - * @param {string} [denomination] The hit denomination of hit die to roll. Example "d8". - * If no denomination is provided, the first available HD will be used - * @param {boolean} [dialog] Show a dialog prompt for configuring the hit die roll? - * @return {Promise} The created Roll instance, or null if no hit die was rolled - */ - async rollHitDie(denomination, {dialog=true}={}) { - - // If no denomination was provided, choose the first available - let cls = null; - if ( !denomination ) { - cls = this.itemTypes.class.find(c => c.data.data.hitDiceUsed < c.data.data.levels); - if ( !cls ) return null; - denomination = cls.data.data.hitDice; - } - - // Otherwise locate a class (if any) which has an available hit die of the requested denomination - else { - cls = this.items.find(i => { - const d = i.data.data; - return (d.hitDice === denomination) && ((d.hitDiceUsed || 0) < (d.levels || 1)); - }); - } - - // If no class is available, display an error notification - if ( !cls ) { - ui.notifications.error(game.i18n.format("SW5E.HitDiceWarn", {name: this.name, formula: denomination})); - return null; - } - - // Prepare roll data - const parts = [`1${denomination}`, "@abilities.con.mod"]; - const title = game.i18n.localize("SW5E.HitDiceRoll"); - const rollData = foundry.utils.deepClone(this.data.data); - - // Call the roll helper utility - const roll = await damageRoll({ - event: new Event("hitDie"), - parts: parts, - data: rollData, - title: title, - allowCritical: false, - fastForward: !dialog, - dialogOptions: {width: 350}, - messageData: { - speaker: ChatMessage.getSpeaker({actor: this}), - "flags.sw5e.roll": {type: "hitDie"} - } - }); - if ( !roll ) return null; - - // Adjust actor data - await cls.update({"data.hitDiceUsed": cls.data.data.hitDiceUsed + 1}); - const hp = this.data.data.attributes.hp; - const dhp = Math.min(hp.max + (hp.tempmax ?? 0) - hp.value, roll.total); - await this.update({"data.attributes.hp.value": hp.value + dhp}); - return roll; - } - - /* -------------------------------------------- */ - - /** - * Results from a rest operation. - * - * @typedef {object} RestResult - * @property {number} dhp Hit points recovered during the rest. - * @property {number} dhd Hit dice recovered or spent during the rest. - * @property {object} updateData Updates applied to the actor. - * @property {Array.} updateItems Updates applied to actor's items. - * @property {boolean} newDay Whether a new day occurred during the rest. - */ - - /* -------------------------------------------- */ - - /** - * Take a short rest, possibly spending hit dice and recovering resources, item uses, and tech slots & points. - * - * @param {object} [options] - * @param {boolean} [options.dialog=true] Present a dialog window which allows for rolling hit dice as part - * of the Short Rest and selecting whether a new day has occurred. - * @param {boolean} [options.chat=true] Summarize the results of the rest workflow as a chat message. - * @param {boolean} [options.autoHD=false] Automatically spend Hit Dice if you are missing 3 or more hit points. - * @param {boolean} [options.autoHDThreshold=3] A number of missing hit points which would trigger an automatic HD roll. - * @return {Promise.} A Promise which resolves once the short rest workflow has completed. - */ - async shortRest({dialog=true, chat=true, autoHD=false, autoHDThreshold=3}={}) { - - // Take note of the initial hit points and number of hit dice the Actor has - const hd0 = this.data.data.attributes.hd; - const hp0 = this.data.data.attributes.hp.value; - let newDay = false; - - // Display a Dialog for rolling hit dice - if ( dialog ) { - try { - newDay = await ShortRestDialog.shortRestDialog({actor: this, canRoll: hd0 > 0}); - } catch(err) { - return; - } - } - - // Automatically spend hit dice - else if ( autoHD ) { - await this.autoSpendHitDice({ threshold: autoHDThreshold }); - } - - return this._rest(chat, newDay, false, this.data.data.attributes.hd - hd0, this.data.data.attributes.hp.value - hp0, this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value); - } - - /* -------------------------------------------- */ - - /** - * Take a long rest, recovering hit points, hit dice, resources, item uses, and tech & force power points & slots. - * - * @param {object} [options] - * @param {boolean} [options.dialog=true] Present a confirmation dialog window whether or not to take a long rest. - * @param {boolean} [options.chat=true] Summarize the results of the rest workflow as a chat message. - * @param {boolean} [options.newDay=true] 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, newDay=true}={}) { - // Maybe present a confirmation dialog - if ( dialog ) { - try { - newDay = await LongRestDialog.longRestDialog({actor: this}); - } catch(err) { - return; - } - } - - return this._rest(chat, newDay, true, 0, 0, this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value, this.data.data.attributes.force.points.max - this.data.data.attributes.force.points.value); - } - - /* -------------------------------------------- */ - - /** - * Perform all of the changes needed for a short or long rest. - * - * @param {boolean} chat Summarize the results of the rest workflow as a chat message. - * @param {boolean} newDay Has a new day occurred during this rest? - * @param {boolean} longRest Is this a long rest? - * @param {number} [dhd=0] Number of hit dice spent during so far during the rest. - * @param {number} [dhp=0] Number of hit points recovered so far during the rest. - * @param {number} [dtp=0] Number of tech points recovered so far during the rest. - * @param {number} [dfp=0] Number of force points recovered so far during the rest. - * @return {Promise.} Consolidated results of the rest workflow. - * @private - */ - async _rest(chat, newDay, longRest, dhd=0, dhp=0, dtp=0, dfp=0) { - // TODO: Turn gritty realism into the SW5e longer rests variant rule https://sw5e.com/rules/variantRules/Longer%20Rests - let hitPointsRecovered = 0; - let hitPointUpdates = {}; - let hitDiceRecovered = 0; - let hitDiceUpdates = []; - - // Recover hit points & hit dice on long rest - if ( longRest ) { - ({ updates: hitPointUpdates, hitPointsRecovered } = this._getRestHitPointRecovery()); - ({ updates: hitDiceUpdates, hitDiceRecovered } = this._getRestHitDiceRecovery()); - } - - // Figure out the rest of the changes - const result = { - dhd: dhd + hitDiceRecovered, - dhp: dhp + hitPointsRecovered, - dtp: dtp, - dfp: dfp, - updateData: { - ...hitPointUpdates, - ...this._getRestResourceRecovery({ recoverShortRestResources: !longRest, recoverLongRestResources: longRest }), - ...this._getRestPowerRecovery({ recoverForcePowers: longRest }) - }, - updateItems: [ - ...hitDiceUpdates, - ...this._getRestItemUsesRecovery({ recoverLongRestUses: longRest, recoverDailyUses: newDay }) - ], - newDay: newDay - } - - // Perform updates - await this.update(result.updateData); - await this.updateEmbeddedDocuments("Item", result.updateItems); - - // Display a Chat Message summarizing the rest effects - if ( chat ) await this._displayRestResultMessage(result, longRest); - - // Return data summarizing the rest effects - return result; - } - - /* -------------------------------------------- */ - - /** - * Display a chat message with the result of a rest. - * - * @param {RestResult} result Result of the rest operation. - * @param {boolean} [longRest=false] Is this a long rest? - * @return {Promise.} Chat message that was created. - * @protected - */ - async _displayRestResultMessage(result, longRest=false) { - const { dhd, dhp, dtp, dfp, newDay } = result; - const diceRestored = dhd !== 0; - const healthRestored = dhp !== 0; - const length = longRest ? "Long" : "Short"; - - let restFlavor, message; - - // Summarize the rest duration - switch (game.settings.get("sw5e", "restVariant")) { - case 'normal': restFlavor = (longRest && newDay) ? "SW5E.LongRestOvernight" : `SW5E.${length}RestNormal`; break; - case 'gritty': restFlavor = (!longRest && newDay) ? "SW5E.ShortRestOvernight" : `SW5E.${length}RestGritty`; break; - case 'epic': restFlavor = `SW5E.${length}RestEpic`; break; - } - - // Determine the chat message to display - if (longRest) { - message = "SW5E.LongRestResult"; - if (dhp !== 0) message += "HP"; - if (dfp !== 0) message += "FP"; - if (dtp !== 0) message += "TP"; - if (dhd !== 0) message += "HD"; - } else { - message = "SW5E.ShortRestResultShort"; - if ((dhd !== 0) && (dhp !== 0)){ - if (dtp !== 0){ - message = "SW5E.ShortRestResultWithTech"; - }else{ - message = "SW5E.ShortRestResult"; + // Include proficiency bonus + if (abl.prof > 0) { + parts.push("@prof"); + data.prof = abl.prof; } - }else{ - if (dtp !== 0){ - message = "SW5E.ShortRestResultOnlyTech"; + + // Include a global actor ability save bonus + const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; + if (bonuses.save) { + parts.push("@saveBonus"); + data.saveBonus = bonuses.save; } - } + + // Add provided extra roll parts now because they will get clobbered by mergeObject below + if (options.parts?.length > 0) { + parts.push(...options.parts); + } + + // Roll and return + const rollData = foundry.utils.mergeObject(options, { + parts: parts, + data: data, + title: game.i18n.format("SW5E.SavePromptTitle", {ability: label}), + halflingLucky: this.getFlag("sw5e", "halflingLucky"), + messageData: { + "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}), + "flags.sw5e.roll": {type: "save", abilityId} + } + }); + return d20Roll(rollData); } - // Create a chat message - let chatData = { - user: game.user.id, - speaker: {actor: this, alias: this.name}, - flavor: game.i18n.localize(restFlavor), - content: game.i18n.format(message, { - name: this.name, - dice: longRest ? dhd : -dhd, - health: dhp, - tech: dtp, - force: dfp - }) - }; - ChatMessage.applyRollMode(chatData, game.settings.get("core", "rollMode")); - return ChatMessage.create(chatData); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Perform a death saving throw, rolling a d20 plus any global save bonuses + * @param {Object} options Additional options which modify the roll + * @return {Promise} A Promise which resolves to the Roll instance + */ + async rollDeathSave(options = {}) { + // Display a warning if we are not at zero HP or if we already have reached 3 + const death = this.data.data.attributes.death; + if (this.data.data.attributes.hp.value > 0 || death.failure >= 3 || death.success >= 3) { + ui.notifications.warn(game.i18n.localize("SW5E.DeathSaveUnnecessary")); + return null; + } - /** - * Automatically spend hit dice to recover hit points up to a certain threshold. - * - * @param {object} [options] - * @param {number} [options.threshold=3] A number of missing hit points which would trigger an automatic HD roll. - * @return {Promise.} Number of hit dice spent. - */ - async autoSpendHitDice({ threshold=3 }={}) { - const max = this.data.data.attributes.hp.max + this.data.data.attributes.hp.tempmax; + // Evaluate a global saving throw bonus + const parts = []; + const data = {}; - let diceRolled = 0; - while ( (this.data.data.attributes.hp.value + threshold) <= max ) { - const r = await this.rollHitDie(undefined, {dialog: false}); - if ( r === null ) break; - diceRolled += 1; + // Include a global actor ability save bonus + const bonuses = foundry.utils.getProperty(this.data.data, "bonuses.abilities") || {}; + if (bonuses.save) { + parts.push("@saveBonus"); + data.saveBonus = bonuses.save; + } + + // Evaluate the roll + const rollData = foundry.utils.mergeObject(options, { + parts: parts, + data: data, + title: game.i18n.localize("SW5E.DeathSavingThrow"), + halflingLucky: this.getFlag("sw5e", "halflingLucky"), + targetValue: 10, + messageData: { + "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}), + "flags.sw5e.roll": {type: "death"} + } + }); + const roll = await d20Roll(rollData); + if (!roll) return null; + + // Take action depending on the result + const success = roll.total >= 10; + const d20 = roll.dice[0].total; + + let chatString; + + // Save success + if (success) { + let successes = (death.success || 0) + 1; + + // Critical Success = revive with 1hp + if (d20 === 20) { + await this.update({ + "data.attributes.death.success": 0, + "data.attributes.death.failure": 0, + "data.attributes.hp.value": 1 + }); + chatString = "SW5E.DeathSaveCriticalSuccess"; + } + + // 3 Successes = survive and reset checks + else if (successes === 3) { + await this.update({ + "data.attributes.death.success": 0, + "data.attributes.death.failure": 0 + }); + chatString = "SW5E.DeathSaveSuccess"; + } + + // Increment successes + else await this.update({"data.attributes.death.success": Math.clamped(successes, 0, 3)}); + } + + // Save failure + else { + let failures = (death.failure || 0) + (d20 === 1 ? 2 : 1); + await this.update({"data.attributes.death.failure": Math.clamped(failures, 0, 3)}); + if (failures >= 3) { + // 3 Failures = death + chatString = "SW5E.DeathSaveFailure"; + } + } + + // Display success/failure chat message + if (chatString) { + let chatData = {content: game.i18n.format(chatString, {name: this.name}), speaker}; + ChatMessage.applyRollMode(chatData, roll.options.rollMode); + await ChatMessage.create(chatData); + } + + // Return the rolled result + return roll; } - return diceRolled; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Roll a hit die of the appropriate type, gaining hit points equal to the die roll plus your CON modifier + * @param {string} [denomination] The hit denomination of hit die to roll. Example "d8". + * If no denomination is provided, the first available HD will be used + * @param {boolean} [dialog] Show a dialog prompt for configuring the hit die roll? + * @return {Promise} The created Roll instance, or null if no hit die was rolled + */ + async rollHitDie(denomination, {dialog = true} = {}) { + // If no denomination was provided, choose the first available + let cls = null; + if (!denomination) { + cls = this.itemTypes.class.find((c) => c.data.data.hitDiceUsed < c.data.data.levels); + if (!cls) return null; + denomination = cls.data.data.hitDice; + } - /** - * Recovers actor hit points and eliminates any temp HP. - * - * @param {object} [options] - * @param {boolean} [options.recoverTemp=true] Reset temp HP to zero. - * @param {boolean} [options.recoverTempMax=true] Reset temp max HP to zero. - * @return {object} Updates to the actor and change in hit points. - * @protected - */ - _getRestHitPointRecovery({ recoverTemp=true, recoverTempMax=true }={}) { - const data = this.data.data; - let updates = {}; - let max = data.attributes.hp.max; + // Otherwise locate a class (if any) which has an available hit die of the requested denomination + else { + cls = this.items.find((i) => { + const d = i.data.data; + return d.hitDice === denomination && (d.hitDiceUsed || 0) < (d.levels || 1); + }); + } - if ( recoverTempMax ) { - updates["data.attributes.hp.tempmax"] = 0; - } else { - max += data.attributes.hp.tempmax; - } - updates["data.attributes.hp.value"] = max; - if ( recoverTemp ) { - updates["data.attributes.hp.temp"] = 0; + // If no class is available, display an error notification + if (!cls) { + ui.notifications.error(game.i18n.format("SW5E.HitDiceWarn", {name: this.name, formula: denomination})); + return null; + } + + // Prepare roll data + const parts = [`1${denomination}`, "@abilities.con.mod"]; + const title = game.i18n.localize("SW5E.HitDiceRoll"); + const rollData = foundry.utils.deepClone(this.data.data); + + // Call the roll helper utility + const roll = await damageRoll({ + event: new Event("hitDie"), + parts: parts, + data: rollData, + title: title, + allowCritical: false, + fastForward: !dialog, + dialogOptions: {width: 350}, + messageData: { + "speaker": ChatMessage.getSpeaker({actor: this}), + "flags.sw5e.roll": {type: "hitDie"} + } + }); + if (!roll) return null; + + // Adjust actor data + await cls.update({"data.hitDiceUsed": cls.data.data.hitDiceUsed + 1}); + const hp = this.data.data.attributes.hp; + const dhp = Math.min(hp.max + (hp.tempmax ?? 0) - hp.value, roll.total); + await this.update({"data.attributes.hp.value": hp.value + dhp}); + return roll; } - return { updates, hitPointsRecovered: max - data.attributes.hp.value }; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Results from a rest operation. + * + * @typedef {object} RestResult + * @property {number} dhp Hit points recovered during the rest. + * @property {number} dhd Hit dice recovered or spent during the rest. + * @property {object} updateData Updates applied to the actor. + * @property {Array.} updateItems Updates applied to actor's items. + * @property {boolean} newDay Whether a new day occurred during the rest. + */ - /** - * Recovers actor resources. - * @param {object} [options] - * @param {boolean} [options.recoverShortRestResources=true] Recover resources that recharge on a short rest. - * @param {boolean} [options.recoverLongRestResources=true] Recover resources that recharge on a long rest. - * @return {object} Updates to the actor. - * @protected - */ - _getRestResourceRecovery({recoverShortRestResources=true, recoverLongRestResources=true}={}) { - let updates = {}; - for ( let [k, r] of Object.entries(this.data.data.resources) ) { - if ( Number.isNumeric(r.max) && ((recoverShortRestResources && r.sr) || (recoverLongRestResources && r.lr)) ) { - updates[`data.resources.${k}.value`] = Number(r.max); - } - } - return updates; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Take a short rest, possibly spending hit dice and recovering resources, item uses, and tech slots & points. + * + * @param {object} [options] + * @param {boolean} [options.dialog=true] Present a dialog window which allows for rolling hit dice as part + * of the Short Rest and selecting whether a new day has occurred. + * @param {boolean} [options.chat=true] Summarize the results of the rest workflow as a chat message. + * @param {boolean} [options.autoHD=false] Automatically spend Hit Dice if you are missing 3 or more hit points. + * @param {boolean} [options.autoHDThreshold=3] A number of missing hit points which would trigger an automatic HD roll. + * @return {Promise.} A Promise which resolves once the short rest workflow has completed. + */ + async shortRest({dialog = true, chat = true, autoHD = false, autoHDThreshold = 3} = {}) { + // Take note of the initial hit points and number of hit dice the Actor has + const hd0 = this.data.data.attributes.hd; + const hp0 = this.data.data.attributes.hp.value; + let newDay = false; - /** - * Recovers power slots. - * - * @param longRest = true It's a long rest - * @return {object} Updates to the actor. - * @protected - */ - _getRestPowerRecovery({ recoverTechPowers=true, recoverForcePowers=true }={}) { - let updates = {}; + // Display a Dialog for rolling hit dice + if (dialog) { + try { + newDay = await ShortRestDialog.shortRestDialog({actor: this, canRoll: hd0 > 0}); + } catch (err) { + return; + } + } - if (recoverTechPowers) { - updates["data.attributes.tech.points.value"] = this.data.data.attributes.tech.points.max; - updates["data.attributes.tech.points.temp"] = 0; - updates["data.attributes.tech.points.tempmax"] = 0; + // Automatically spend hit dice + else if (autoHD) { + await this.autoSpendHitDice({threshold: autoHDThreshold}); + } - for (let [k, v] of Object.entries(this.data.data.powers)) { - updates[`data.powers.${k}.tvalue`] = Number.isNumeric(v.toverride) ? v.toverride : (v.tmax ?? 0); - } + return this._rest( + chat, + newDay, + false, + this.data.data.attributes.hd - hd0, + this.data.data.attributes.hp.value - hp0, + this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value + ); } - if (recoverForcePowers) { - updates["data.attributes.force.points.value"] = this.data.data.attributes.force.points.max; - updates["data.attributes.force.points.temp"] = 0; - updates["data.attributes.force.points.tempmax"] = 0; + /* -------------------------------------------- */ - for ( let [k, v] of Object.entries(this.data.data.powers) ) { - updates[`data.powers.${k}.fvalue`] = Number.isNumeric(v.foverride) ? v.foverride : (v.fmax ?? 0); - } + /** + * Take a long rest, recovering hit points, hit dice, resources, item uses, and tech & force power points & slots. + * + * @param {object} [options] + * @param {boolean} [options.dialog=true] Present a confirmation dialog window whether or not to take a long rest. + * @param {boolean} [options.chat=true] Summarize the results of the rest workflow as a chat message. + * @param {boolean} [options.newDay=true] 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, newDay = true} = {}) { + // Maybe present a confirmation dialog + if (dialog) { + try { + newDay = await LongRestDialog.longRestDialog({actor: this}); + } catch (err) { + return; + } + } + + return this._rest( + chat, + newDay, + true, + 0, + 0, + this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value, + this.data.data.attributes.force.points.max - this.data.data.attributes.force.points.value + ); } - return updates; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Perform all of the changes needed for a short or long rest. + * + * @param {boolean} chat Summarize the results of the rest workflow as a chat message. + * @param {boolean} newDay Has a new day occurred during this rest? + * @param {boolean} longRest Is this a long rest? + * @param {number} [dhd=0] Number of hit dice spent during so far during the rest. + * @param {number} [dhp=0] Number of hit points recovered so far during the rest. + * @param {number} [dtp=0] Number of tech points recovered so far during the rest. + * @param {number} [dfp=0] Number of force points recovered so far during the rest. + * @return {Promise.} Consolidated results of the rest workflow. + * @private + */ + async _rest(chat, newDay, longRest, dhd = 0, dhp = 0, dtp = 0, dfp = 0) { + // TODO: Turn gritty realism into the SW5e longer rests variant rule https://sw5e.com/rules/variantRules/Longer%20Rests + let hitPointsRecovered = 0; + let hitPointUpdates = {}; + let hitDiceRecovered = 0; + let hitDiceUpdates = []; - /** - * Recovers class hit dice during a long rest. - * - * @param {object} [options] - * @param {number} [options.maxHitDice] Maximum number of hit dice to recover. - * @return {object} Array of item updates and number of hit dice recovered. - * @protected - */ - _getRestHitDiceRecovery({ maxHitDice=undefined }={}) { - // Determine the number of hit dice which may be recovered - if ( maxHitDice === undefined ) { - maxHitDice = Math.max(Math.floor(this.data.data.details.level / 2), 1); + // Recover hit points & hit dice on long rest + if (longRest) { + ({updates: hitPointUpdates, hitPointsRecovered} = this._getRestHitPointRecovery()); + ({updates: hitDiceUpdates, hitDiceRecovered} = this._getRestHitDiceRecovery()); + } + + // Figure out the rest of the changes + const result = { + dhd: dhd + hitDiceRecovered, + dhp: dhp + hitPointsRecovered, + dtp: dtp, + dfp: dfp, + updateData: { + ...hitPointUpdates, + ...this._getRestResourceRecovery({ + recoverShortRestResources: !longRest, + recoverLongRestResources: longRest + }), + ...this._getRestPowerRecovery({recoverForcePowers: longRest}) + }, + updateItems: [ + ...hitDiceUpdates, + ...this._getRestItemUsesRecovery({recoverLongRestUses: longRest, recoverDailyUses: newDay}) + ], + newDay: newDay + }; + + // Perform updates + await this.update(result.updateData); + await this.updateEmbeddedDocuments("Item", result.updateItems); + + // Display a Chat Message summarizing the rest effects + if (chat) await this._displayRestResultMessage(result, longRest); + + // Return data summarizing the rest effects + return result; } - // Sort classes which can recover HD, assuming players prefer recovering larger HD first. - const sortedClasses = Object.values(this.classes).sort((a, b) => { - return (parseInt(b.data.data.hitDice.slice(1)) || 0) - (parseInt(a.data.data.hitDice.slice(1)) || 0); - }); + /* -------------------------------------------- */ - let updates = []; - let hitDiceRecovered = 0; - for ( let item of sortedClasses ) { - const d = item.data.data; - if ( (hitDiceRecovered < maxHitDice) && (d.hitDiceUsed > 0) ) { - let delta = Math.min(d.hitDiceUsed || 0, maxHitDice - hitDiceRecovered); - hitDiceRecovered += delta; - updates.push({_id: item.id, "data.hitDiceUsed": d.hitDiceUsed - delta}); - } + /** + * Display a chat message with the result of a rest. + * + * @param {RestResult} result Result of the rest operation. + * @param {boolean} [longRest=false] Is this a long rest? + * @return {Promise.} Chat message that was created. + * @protected + */ + async _displayRestResultMessage(result, longRest = false) { + const {dhd, dhp, dtp, dfp, newDay} = result; + const diceRestored = dhd !== 0; + const healthRestored = dhp !== 0; + const length = longRest ? "Long" : "Short"; + + let restFlavor, message; + + // Summarize the rest duration + switch (game.settings.get("sw5e", "restVariant")) { + case "normal": + restFlavor = longRest && newDay ? "SW5E.LongRestOvernight" : `SW5E.${length}RestNormal`; + break; + case "gritty": + restFlavor = !longRest && newDay ? "SW5E.ShortRestOvernight" : `SW5E.${length}RestGritty`; + break; + case "epic": + restFlavor = `SW5E.${length}RestEpic`; + break; + } + + // Determine the chat message to display + if (longRest) { + message = "SW5E.LongRestResult"; + if (dhp !== 0) message += "HP"; + if (dfp !== 0) message += "FP"; + if (dtp !== 0) message += "TP"; + if (dhd !== 0) message += "HD"; + } else { + message = "SW5E.ShortRestResultShort"; + if (dhd !== 0 && dhp !== 0) { + if (dtp !== 0) { + message = "SW5E.ShortRestResultWithTech"; + } else { + message = "SW5E.ShortRestResult"; + } + } else { + if (dtp !== 0) { + message = "SW5E.ShortRestResultOnlyTech"; + } + } + } + + // Create a chat message + let chatData = { + user: game.user.id, + speaker: {actor: this, alias: this.name}, + flavor: game.i18n.localize(restFlavor), + content: game.i18n.format(message, { + name: this.name, + dice: longRest ? dhd : -dhd, + health: dhp, + tech: dtp, + force: dfp + }) + }; + ChatMessage.applyRollMode(chatData, game.settings.get("core", "rollMode")); + return ChatMessage.create(chatData); } - return { updates, hitDiceRecovered }; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Automatically spend hit dice to recover hit points up to a certain threshold. + * + * @param {object} [options] + * @param {number} [options.threshold=3] A number of missing hit points which would trigger an automatic HD roll. + * @return {Promise.} Number of hit dice spent. + */ + async autoSpendHitDice({threshold = 3} = {}) { + const max = this.data.data.attributes.hp.max + this.data.data.attributes.hp.tempmax; - /** - * Recovers item uses during short or long rests. - * - * @param {object} [options] - * @param {boolean} [options.recoverShortRestUses=true] Recover uses for items that recharge after a short rest. - * @param {boolean} [options.recoverLongRestUses=true] Recover uses for items that recharge after a long rest. - * @param {boolean} [options.recoverDailyUses=true] Recover uses for items that recharge on a new day. - * @return {Array.} Array of item updates. - * @protected - */ - _getRestItemUsesRecovery({ recoverShortRestUses=true, recoverLongRestUses=true, recoverDailyUses=true }={}) { - let recovery = []; - if ( recoverShortRestUses ) recovery.push("sr"); - if ( recoverLongRestUses ) recovery.push("lr"); - if ( recoverDailyUses ) recovery.push("day"); + let diceRolled = 0; + while (this.data.data.attributes.hp.value + threshold <= max) { + const r = await this.rollHitDie(undefined, {dialog: false}); + if (r === null) break; + diceRolled += 1; + } - let updates = []; - for ( let item of this.items ) { - const d = item.data.data; - if ( d.uses && recovery.includes(d.uses.per) ) { - updates.push({_id: item.id, "data.uses.value": d.uses.max}); - } - if ( recoverLongRestUses && d.recharge && d.recharge.value ) { - updates.push({_id: item.id, "data.recharge.charged": true}); - } + return diceRolled; } - return updates; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Recovers actor hit points and eliminates any temp HP. + * + * @param {object} [options] + * @param {boolean} [options.recoverTemp=true] Reset temp HP to zero. + * @param {boolean} [options.recoverTempMax=true] Reset temp max HP to zero. + * @return {object} Updates to the actor and change in hit points. + * @protected + */ + _getRestHitPointRecovery({recoverTemp = true, recoverTempMax = true} = {}) { + const data = this.data.data; + let updates = {}; + let max = data.attributes.hp.max; - /** - * Transform this Actor into another one. - * - * @param {Actor} target The target Actor. - * @param {boolean} [keepPhysical] Keep physical abilities (str, dex, con) - * @param {boolean} [keepMental] Keep mental abilities (int, wis, cha) - * @param {boolean} [keepSaves] Keep saving throw proficiencies - * @param {boolean} [keepSkills] Keep skill proficiencies - * @param {boolean} [mergeSaves] Take the maximum of the save proficiencies - * @param {boolean} [mergeSkills] Take the maximum of the skill proficiencies - * @param {boolean} [keepClass] Keep proficiency bonus - * @param {boolean} [keepFeats] Keep features - * @param {boolean} [keepPowers] Keep powers - * @param {boolean} [keepItems] Keep items - * @param {boolean} [keepBio] Keep biography - * @param {boolean} [keepVision] Keep vision - * @param {boolean} [transformTokens] Transform linked tokens too - */ - async transformInto(target, { keepPhysical=false, keepMental=false, keepSaves=false, keepSkills=false, - mergeSaves=false, mergeSkills=false, keepClass=false, keepFeats=false, keepPowers=false, - keepItems=false, keepBio=false, keepVision=false, transformTokens=true}={}) { + if (recoverTempMax) { + updates["data.attributes.hp.tempmax"] = 0; + } else { + max += data.attributes.hp.tempmax; + } + updates["data.attributes.hp.value"] = max; + if (recoverTemp) { + updates["data.attributes.hp.temp"] = 0; + } - // Ensure the player is allowed to polymorph - const allowed = game.settings.get("sw5e", "allowPolymorphing"); - if ( !allowed && !game.user.isGM ) { - return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphWarn")); + return {updates, hitPointsRecovered: max - data.attributes.hp.value}; } - // Get the original Actor data and the new source data - const o = this.toJSON(); - o.flags.sw5e = o.flags.sw5e || {}; - o.flags.sw5e.transformOptions = {mergeSkills, mergeSaves}; - const source = target.toJSON(); + /* -------------------------------------------- */ - // Prepare new data to merge from the source - const d = { - type: o.type, // Remain the same actor type - name: `${o.name} (${source.name})`, // Append the new shape to your old name - data: source.data, // Get the data model of your new form - items: source.items, // Get the items of your new form - effects: o.effects.concat(source.effects), // Combine active effects from both forms - img: source.img, // New appearance - permission: o.permission, // Use the original actor permissions - folder: o.folder, // Be displayed in the same sidebar folder - flags: o.flags // Use the original actor flags - }; - - // Specifically delete some data attributes - delete d.data.resources; // Don't change your resource pools - delete d.data.currency; // Don't lose currency - delete d.data.bonuses; // Don't lose global bonuses - - // Specific additional adjustments - d.data.details.alignment = o.data.details.alignment; // Don't change alignment - d.data.attributes.exhaustion = o.data.attributes.exhaustion; // Keep your prior exhaustion level - d.data.attributes.inspiration = o.data.attributes.inspiration; // Keep inspiration - d.data.powers = o.data.powers; // Keep power slots - - // Token appearance updates - d.token = {name: d.name}; - for ( let k of ["width", "height", "scale", "img", "mirrorX", "mirrorY", "tint", "alpha", "lockRotation"] ) { - d.token[k] = source.token[k]; - } - if ( !keepVision ) { - for ( let k of ['dimSight', 'brightSight', 'dimLight', 'brightLight', 'vision', 'sightAngle'] ) { - d.token[k] = source.token[k]; - } - } - if ( source.token.randomImg ) { - const images = await target.getTokenImages(); - d.token.img = images[Math.floor(Math.random() * images.length)]; + /** + * Recovers actor resources. + * @param {object} [options] + * @param {boolean} [options.recoverShortRestResources=true] Recover resources that recharge on a short rest. + * @param {boolean} [options.recoverLongRestResources=true] Recover resources that recharge on a long rest. + * @return {object} Updates to the actor. + * @protected + */ + _getRestResourceRecovery({recoverShortRestResources = true, recoverLongRestResources = true} = {}) { + let updates = {}; + for (let [k, r] of Object.entries(this.data.data.resources)) { + if ( + Number.isNumeric(r.max) && + ((recoverShortRestResources && r.sr) || (recoverLongRestResources && r.lr)) + ) { + updates[`data.resources.${k}.value`] = Number(r.max); + } + } + return updates; } - // Transfer ability scores - const abilities = d.data.abilities; - for ( let k of Object.keys(abilities) ) { - const oa = o.data.abilities[k]; - const prof = abilities[k].proficient; - if ( keepPhysical && ["str", "dex", "con"].includes(k) ) abilities[k] = oa; - else if ( keepMental && ["int", "wis", "cha"].includes(k) ) abilities[k] = oa; - if ( keepSaves ) abilities[k].proficient = oa.proficient; - else if ( mergeSaves ) abilities[k].proficient = Math.max(prof, oa.proficient); + /* -------------------------------------------- */ + + /** + * Recovers power slots. + * + * @param longRest = true It's a long rest + * @return {object} Updates to the actor. + * @protected + */ + _getRestPowerRecovery({recoverTechPowers = true, recoverForcePowers = true} = {}) { + let updates = {}; + + if (recoverTechPowers) { + updates["data.attributes.tech.points.value"] = this.data.data.attributes.tech.points.max; + updates["data.attributes.tech.points.temp"] = 0; + updates["data.attributes.tech.points.tempmax"] = 0; + + for (let [k, v] of Object.entries(this.data.data.powers)) { + updates[`data.powers.${k}.tvalue`] = Number.isNumeric(v.toverride) ? v.toverride : v.tmax ?? 0; + } + } + + if (recoverForcePowers) { + updates["data.attributes.force.points.value"] = this.data.data.attributes.force.points.max; + updates["data.attributes.force.points.temp"] = 0; + updates["data.attributes.force.points.tempmax"] = 0; + + for (let [k, v] of Object.entries(this.data.data.powers)) { + updates[`data.powers.${k}.fvalue`] = Number.isNumeric(v.foverride) ? v.foverride : v.fmax ?? 0; + } + } + + return updates; } - // Transfer skills - if ( keepSkills ) d.data.skills = o.data.skills; - else if ( mergeSkills ) { - for ( let [k, s] of Object.entries(d.data.skills) ) { - s.value = Math.max(s.value, o.data.skills[k].value); - } + /* -------------------------------------------- */ + + /** + * Recovers class hit dice during a long rest. + * + * @param {object} [options] + * @param {number} [options.maxHitDice] Maximum number of hit dice to recover. + * @return {object} Array of item updates and number of hit dice recovered. + * @protected + */ + _getRestHitDiceRecovery({maxHitDice = undefined} = {}) { + // Determine the number of hit dice which may be recovered + if (maxHitDice === undefined) { + maxHitDice = Math.max(Math.floor(this.data.data.details.level / 2), 1); + } + + // Sort classes which can recover HD, assuming players prefer recovering larger HD first. + const sortedClasses = Object.values(this.classes).sort((a, b) => { + return (parseInt(b.data.data.hitDice.slice(1)) || 0) - (parseInt(a.data.data.hitDice.slice(1)) || 0); + }); + + let updates = []; + let hitDiceRecovered = 0; + for (let item of sortedClasses) { + const d = item.data.data; + if (hitDiceRecovered < maxHitDice && d.hitDiceUsed > 0) { + let delta = Math.min(d.hitDiceUsed || 0, maxHitDice - hitDiceRecovered); + hitDiceRecovered += delta; + updates.push({"_id": item.id, "data.hitDiceUsed": d.hitDiceUsed - delta}); + } + } + + return {updates, hitDiceRecovered}; } - // Keep specific items from the original data - d.items = d.items.concat(o.items.filter(i => { - if ( i.type === "class" ) return keepClass; - else if ( i.type === "feat" ) return keepFeats; - else if ( i.type === "power" ) return keepPowers; - else return keepItems; - })); + /* -------------------------------------------- */ - // Transfer classes for NPCs - if (!keepClass && d.data.details.cr) { - d.items.push({ - type: 'class', - name: game.i18n.localize('SW5E.PolymorphTmpClass'), - data: { levels: d.data.details.cr } - }); + /** + * Recovers item uses during short or long rests. + * + * @param {object} [options] + * @param {boolean} [options.recoverShortRestUses=true] Recover uses for items that recharge after a short rest. + * @param {boolean} [options.recoverLongRestUses=true] Recover uses for items that recharge after a long rest. + * @param {boolean} [options.recoverDailyUses=true] Recover uses for items that recharge on a new day. + * @return {Array.} Array of item updates. + * @protected + */ + _getRestItemUsesRecovery({recoverShortRestUses = true, recoverLongRestUses = true, recoverDailyUses = true} = {}) { + let recovery = []; + if (recoverShortRestUses) recovery.push("sr"); + if (recoverLongRestUses) recovery.push("lr"); + if (recoverDailyUses) recovery.push("day"); + + let updates = []; + for (let item of this.items) { + const d = item.data.data; + if (d.uses && recovery.includes(d.uses.per)) { + updates.push({"_id": item.id, "data.uses.value": d.uses.max}); + } + if (recoverLongRestUses && d.recharge && d.recharge.value) { + updates.push({"_id": item.id, "data.recharge.charged": true}); + } + } + + return updates; } - // Keep biography - if (keepBio) d.data.details.biography = o.data.details.biography; + /* -------------------------------------------- */ - // Keep senses - if (keepVision) d.data.traits.senses = o.data.traits.senses; - - // Set new data flags - if ( !this.isPolymorphed || !d.flags.sw5e.originalActor ) d.flags.sw5e.originalActor = this.id; - d.flags.sw5e.isPolymorphed = true; - - // Update unlinked Tokens in place since they can simply be re-dropped from the base actor - if (this.isToken) { - const tokenData = d.token; - tokenData.actorData = d; - delete tokenData.actorData.token; - return this.token.update(tokenData); - } - - // Update regular Actors by creating a new Actor with the Polymorphed data - await this.sheet.close(); - Hooks.callAll('sw5e.transformActor', this, target, d, { - keepPhysical, keepMental, keepSaves, keepSkills, mergeSaves, mergeSkills, - keepClass, keepFeats, keepPowers, keepItems, keepBio, keepVision, transformTokens - }); - const newActor = await this.constructor.create(d, {renderSheet: true}); - - // Update placed Token instances - if ( !transformTokens ) return; - const tokens = this.getActiveTokens(true); - const updates = tokens.map(t => { - const newTokenData = foundry.utils.deepClone(d.token); - if ( !t.data.actorLink ) newTokenData.actorData = newActor.data; - newTokenData._id = t.data._id; - newTokenData.actorId = newActor.id; - return newTokenData; - }); - return canvas.scene?.updateEmbeddedDocuments("Token", updates); - } - - /* -------------------------------------------- */ - - /** - * If this actor was transformed with transformTokens enabled, then its - * active tokens need to be returned to their original state. If not, then - * we can safely just delete this actor. - */ - async revertOriginalForm() { - if ( !this.isPolymorphed ) return; - if ( !this.isOwner ) { - return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphRevertWarn")); - } - - // If we are reverting an unlinked token, simply replace it with the base actor prototype - if ( this.isToken ) { - const baseActor = game.actors.get(this.token.data.actorId); - const prototypeTokenData = await baseActor.getTokenData(); - const tokenUpdate = {actorData: {}}; - for ( let k of ["width", "height", "scale", "img", "mirrorX", "mirrorY", "tint", "alpha", "lockRotation"] ) { - tokenUpdate[k] = prototypeTokenData[k]; - } - return this.token.update(tokenUpdate, {recursive: false}); - } - - // Obtain a reference to the original actor - const original = game.actors.get(this.getFlag('sw5e', 'originalActor')); - if ( !original ) return; - - // Get the Tokens which represent this actor - if ( canvas.ready ) { - const tokens = this.getActiveTokens(true); - const tokenUpdates = tokens.map(t => { - const tokenData = original.data.token.toJSON(); - tokenData._id = t.id; - tokenData.actorId = original.id; - return tokenData; - }); - canvas.scene.updateEmbeddedDocuments("Token", tokenUpdates); - } - - // Delete the polymorphed version of the actor, if possible - const isRendered = this.sheet.rendered; - if ( game.user.isGM ) await this.delete(); - else if ( isRendered ) this.sheet.close(); - if ( isRendered ) original.sheet.render(isRendered); - return original; - } - - /* -------------------------------------------- */ - - /** - * Add additional system-specific sidebar directory context menu options for SW5e Actor entities - * @param {jQuery} html The sidebar HTML - * @param {Array} entryOptions The default array of context menu options - */ - static addDirectoryContextOptions(html, entryOptions) { - entryOptions.push({ - name: 'SW5E.PolymorphRestoreTransformation', - icon: '', - callback: li => { - const actor = game.actors.get(li.data('entityId')); - return actor.revertOriginalForm(); - }, - condition: li => { + /** + * Transform this Actor into another one. + * + * @param {Actor} target The target Actor. + * @param {boolean} [keepPhysical] Keep physical abilities (str, dex, con) + * @param {boolean} [keepMental] Keep mental abilities (int, wis, cha) + * @param {boolean} [keepSaves] Keep saving throw proficiencies + * @param {boolean} [keepSkills] Keep skill proficiencies + * @param {boolean} [mergeSaves] Take the maximum of the save proficiencies + * @param {boolean} [mergeSkills] Take the maximum of the skill proficiencies + * @param {boolean} [keepClass] Keep proficiency bonus + * @param {boolean} [keepFeats] Keep features + * @param {boolean} [keepPowers] Keep powers + * @param {boolean} [keepItems] Keep items + * @param {boolean} [keepBio] Keep biography + * @param {boolean} [keepVision] Keep vision + * @param {boolean} [transformTokens] Transform linked tokens too + */ + async transformInto( + target, + { + keepPhysical = false, + keepMental = false, + keepSaves = false, + keepSkills = false, + mergeSaves = false, + mergeSkills = false, + keepClass = false, + keepFeats = false, + keepPowers = false, + keepItems = false, + keepBio = false, + keepVision = false, + transformTokens = true + } = {} + ) { + // Ensure the player is allowed to polymorph const allowed = game.settings.get("sw5e", "allowPolymorphing"); - if ( !allowed && !game.user.isGM ) return false; - const actor = game.actors.get(li.data('entityId')); - return actor && actor.isPolymorphed; - } - }); - } + if (!allowed && !game.user.isGM) { + return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphWarn")); + } - /* -------------------------------------------- */ + // Get the original Actor data and the new source data + const o = this.toJSON(); + o.flags.sw5e = o.flags.sw5e || {}; + o.flags.sw5e.transformOptions = {mergeSkills, mergeSaves}; + const source = target.toJSON(); - /** - * Format a type object into a string. - * @param {object} typeData The type data to convert to a string. - * @returns {string} - */ - static formatCreatureType(typeData) { - if ( typeof typeData === "string" ) return typeData; // backwards compatibility - let localizedType; - if ( typeData.value === "custom" ) { - localizedType = typeData.custom; - } else { - let code = CONFIG.SW5E.creatureTypes[typeData.value]; - localizedType = game.i18n.localize(!!typeData.swarm ? `${code}Pl` : code); + // Prepare new data to merge from the source + const d = { + type: o.type, // Remain the same actor type + name: `${o.name} (${source.name})`, // Append the new shape to your old name + data: source.data, // Get the data model of your new form + items: source.items, // Get the items of your new form + effects: o.effects.concat(source.effects), // Combine active effects from both forms + img: source.img, // New appearance + permission: o.permission, // Use the original actor permissions + folder: o.folder, // Be displayed in the same sidebar folder + flags: o.flags // Use the original actor flags + }; + + // Specifically delete some data attributes + delete d.data.resources; // Don't change your resource pools + delete d.data.currency; // Don't lose currency + delete d.data.bonuses; // Don't lose global bonuses + + // Specific additional adjustments + d.data.details.alignment = o.data.details.alignment; // Don't change alignment + d.data.attributes.exhaustion = o.data.attributes.exhaustion; // Keep your prior exhaustion level + d.data.attributes.inspiration = o.data.attributes.inspiration; // Keep inspiration + d.data.powers = o.data.powers; // Keep power slots + + // Token appearance updates + d.token = {name: d.name}; + for (let k of ["width", "height", "scale", "img", "mirrorX", "mirrorY", "tint", "alpha", "lockRotation"]) { + d.token[k] = source.token[k]; + } + if (!keepVision) { + for (let k of ["dimSight", "brightSight", "dimLight", "brightLight", "vision", "sightAngle"]) { + d.token[k] = source.token[k]; + } + } + if (source.token.randomImg) { + const images = await target.getTokenImages(); + d.token.img = images[Math.floor(Math.random() * images.length)]; + } + + // Transfer ability scores + const abilities = d.data.abilities; + for (let k of Object.keys(abilities)) { + const oa = o.data.abilities[k]; + const prof = abilities[k].proficient; + if (keepPhysical && ["str", "dex", "con"].includes(k)) abilities[k] = oa; + else if (keepMental && ["int", "wis", "cha"].includes(k)) abilities[k] = oa; + if (keepSaves) abilities[k].proficient = oa.proficient; + else if (mergeSaves) abilities[k].proficient = Math.max(prof, oa.proficient); + } + + // Transfer skills + if (keepSkills) d.data.skills = o.data.skills; + else if (mergeSkills) { + for (let [k, s] of Object.entries(d.data.skills)) { + s.value = Math.max(s.value, o.data.skills[k].value); + } + } + + // Keep specific items from the original data + d.items = d.items.concat( + o.items.filter((i) => { + if (i.type === "class") return keepClass; + else if (i.type === "feat") return keepFeats; + else if (i.type === "power") return keepPowers; + else return keepItems; + }) + ); + + // Transfer classes for NPCs + if (!keepClass && d.data.details.cr) { + d.items.push({ + type: "class", + name: game.i18n.localize("SW5E.PolymorphTmpClass"), + data: {levels: d.data.details.cr} + }); + } + + // Keep biography + if (keepBio) d.data.details.biography = o.data.details.biography; + + // Keep senses + if (keepVision) d.data.traits.senses = o.data.traits.senses; + + // Set new data flags + if (!this.isPolymorphed || !d.flags.sw5e.originalActor) d.flags.sw5e.originalActor = this.id; + d.flags.sw5e.isPolymorphed = true; + + // Update unlinked Tokens in place since they can simply be re-dropped from the base actor + if (this.isToken) { + const tokenData = d.token; + tokenData.actorData = d; + delete tokenData.actorData.token; + return this.token.update(tokenData); + } + + // Update regular Actors by creating a new Actor with the Polymorphed data + await this.sheet.close(); + Hooks.callAll("sw5e.transformActor", this, target, d, { + keepPhysical, + keepMental, + keepSaves, + keepSkills, + mergeSaves, + mergeSkills, + keepClass, + keepFeats, + keepPowers, + keepItems, + keepBio, + keepVision, + transformTokens + }); + const newActor = await this.constructor.create(d, {renderSheet: true}); + + // Update placed Token instances + if (!transformTokens) return; + const tokens = this.getActiveTokens(true); + const updates = tokens.map((t) => { + const newTokenData = foundry.utils.deepClone(d.token); + if (!t.data.actorLink) newTokenData.actorData = newActor.data; + newTokenData._id = t.data._id; + newTokenData.actorId = newActor.id; + return newTokenData; + }); + return canvas.scene?.updateEmbeddedDocuments("Token", updates); } - let type = localizedType; - if ( !!typeData.swarm ) { - type = game.i18n.format('SW5E.CreatureSwarmPhrase', { - size: game.i18n.localize(CONFIG.SW5E.actorSizes[typeData.swarm]), - type: localizedType - }); + + /* -------------------------------------------- */ + + /** + * If this actor was transformed with transformTokens enabled, then its + * active tokens need to be returned to their original state. If not, then + * we can safely just delete this actor. + */ + async revertOriginalForm() { + if (!this.isPolymorphed) return; + if (!this.isOwner) { + return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphRevertWarn")); + } + + // If we are reverting an unlinked token, simply replace it with the base actor prototype + if (this.isToken) { + const baseActor = game.actors.get(this.token.data.actorId); + const prototypeTokenData = await baseActor.getTokenData(); + const tokenUpdate = {actorData: {}}; + for (let k of ["width", "height", "scale", "img", "mirrorX", "mirrorY", "tint", "alpha", "lockRotation"]) { + tokenUpdate[k] = prototypeTokenData[k]; + } + return this.token.update(tokenUpdate, {recursive: false}); + } + + // Obtain a reference to the original actor + const original = game.actors.get(this.getFlag("sw5e", "originalActor")); + if (!original) return; + + // Get the Tokens which represent this actor + if (canvas.ready) { + const tokens = this.getActiveTokens(true); + const tokenData = await original.getTokenData(); + const tokenUpdates = tokens.map((t) => { + const update = duplicate(tokenData); + update._id = t.id; + delete update.x; + delete update.y; + return update; + }); + canvas.scene.updateEmbeddedDocuments("Token", tokenUpdates); + } + + // Delete the polymorphed version of the actor, if possible + const isRendered = this.sheet.rendered; + if (game.user.isGM) await this.delete(); + else if (isRendered) this.sheet.close(); + if (isRendered) original.sheet.render(isRendered); + return original; } - if (typeData.subtype) type = `${type} (${typeData.subtype})`; - return type; - } - /* -------------------------------------------- */ - /* 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; - } + /** + * Add additional system-specific sidebar directory context menu options for SW5e Actor entities + * @param {jQuery} html The sidebar HTML + * @param {Array} entryOptions The default array of context menu options + */ + static addDirectoryContextOptions(html, entryOptions) { + entryOptions.push({ + name: "SW5E.PolymorphRestoreTransformation", + icon: '', + callback: (li) => { + const actor = game.actors.get(li.data("entityId")); + return actor.revertOriginalForm(); + }, + condition: (li) => { + const allowed = game.settings.get("sw5e", "allowPolymorphing"); + if (!allowed && !game.user.isGM) return false; + const actor = game.actors.get(li.data("entityId")); + return actor && actor.isPolymorphed; + } + }); + } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Cast a Power, consuming a power slot of a certain level - * @param {Item5e} item The power being cast by the actor - * @param {Event} event The originating user interaction which triggered the cast - * @deprecated since sw5e 1.2.0 - */ - async usePower(item, {configureDialog=true}={}) { - console.warn(`The Actor5e#usePower method has been deprecated in favor of Item5e#roll`); - if ( item.data.type !== "power" ) throw new Error("Wrong Item type"); - return item.roll(); - } -} \ No newline at end of file + /** + * Format a type object into a string. + * @param {object} typeData The type data to convert to a string. + * @returns {string} + */ + static formatCreatureType(typeData) { + if (typeof typeData === "string") return typeData; // backwards compatibility + let localizedType; + if (typeData.value === "custom") { + localizedType = typeData.custom; + } else { + let code = CONFIG.SW5E.creatureTypes[typeData.value]; + localizedType = game.i18n.localize(!!typeData.swarm ? `${code}Pl` : code); + } + let type = localizedType; + if (!!typeData.swarm) { + type = game.i18n.format("SW5E.CreatureSwarmPhrase", { + size: game.i18n.localize(CONFIG.SW5E.actorSizes[typeData.swarm]), + type: localizedType + }); + } + if (typeData.subtype) type = `${type} (${typeData.subtype})`; + return type; + } + + /* -------------------------------------------- */ + /* 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; + } + + /* -------------------------------------------- */ + + /** + * Cast a Power, consuming a power slot of a certain level + * @param {Item5e} item The power being cast by the actor + * @param {Event} event The originating user interaction which triggered the cast + * @deprecated since sw5e 1.2.0 + */ + async usePower(item, {configureDialog = true} = {}) { + console.warn(`The Actor5e#usePower method has been deprecated in favor of Item5e#roll`); + if (item.data.type !== "power") throw new Error("Wrong Item type"); + return item.roll(); + } +} diff --git a/module/actor/old_entity.js b/module/actor/old_entity.js new file mode 100644 index 00000000..50b9b895 --- /dev/null +++ b/module/actor/old_entity.js @@ -0,0 +1,2126 @@ +import {d20Roll, damageRoll} from "../dice.js"; +import ShortRestDialog from "../apps/short-rest.js"; +import LongRestDialog from "../apps/long-rest.js"; +import {SW5E} from "../config.js"; + +/** + * Extend the base Actor class to implement additional system-specific logic for SW5e. + */ +export default class Actor5e extends Actor { + /** + * Is this Actor currently polymorphed into some other creature? + * @return {boolean} + */ + get isPolymorphed() { + return this.getFlag("sw5e", "isPolymorphed") || false; + } + + /* -------------------------------------------- */ + + /** @override */ + prepareBaseData() { + switch (this.data.type) { + case "character": + return this._prepareCharacterData(this.data); + case "npc": + return this._prepareNPCData(this.data); + case "starship": + return this._prepareStarshipData(this.data); + case "vehicle": + return this._prepareVehicleData(this.data); + } + } + + /* -------------------------------------------- */ + + /** @override */ + prepareDerivedData() { + const actorData = this.data; + const data = actorData.data; + const flags = actorData.flags.sw5e || {}; + const bonuses = getProperty(data, "bonuses.abilities") || {}; + + // Retrieve data for polymorphed actors + let originalSaves = null; + let originalSkills = null; + if (this.isPolymorphed) { + const transformOptions = this.getFlag("sw5e", "transformOptions"); + const original = game.actors?.get(this.getFlag("sw5e", "originalActor")); + if (original) { + if (transformOptions.mergeSaves) { + originalSaves = original.data.data.abilities; + } + if (transformOptions.mergeSkills) { + originalSkills = original.data.data.skills; + } + } + } + + // 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)) { + abl.mod = Math.floor((abl.value - 10) / 2); + abl.prof = (abl.proficient || 0) * data.attributes.prof; + 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) { + abl.save = Math.max(abl.save, originalSaves[id].save); + } + } + + // Inventory encumbrance + data.attributes.encumbrance = this._computeEncumbrance(actorData); + + if (actorData.type === "starship") { + // Calculate AC + data.attributes.ac.value += Math.min(data.abilities.dex.mod, data.attributes.equip.armor.maxDex); + + // Set Power Die Storage + data.attributes.power.central.max += data.attributes.equip.powerCoupling.centralCap; + data.attributes.power.comms.max += data.attributes.equip.powerCoupling.systemCap; + data.attributes.power.engines.max += data.attributes.equip.powerCoupling.systemCap; + data.attributes.power.shields.max += data.attributes.equip.powerCoupling.systemCap; + data.attributes.power.sensors.max += data.attributes.equip.powerCoupling.systemCap; + data.attributes.power.weapons.max += data.attributes.equip.powerCoupling.systemCap; + + // Find Size info of Starship + const size = actorData.items.filter((i) => i.type === "starship"); + if (size.length === 0) return; + const sizeData = size[0].data; + + // Prepare Hull Points + data.attributes.hp.max = + sizeData.hullDiceRolled.reduce((a, b) => a + b, 0) + + data.abilities.con.mod * data.attributes.hull.dicemax; + if (data.attributes.hp.value === null) data.attributes.hp.value = data.attributes.hp.max; + + // Prepare Shield Points + data.attributes.hp.tempmax = + (sizeData.shldDiceRolled.reduce((a, b) => a + b, 0) + + data.abilities.str.mod * data.attributes.shld.dicemax) * + data.attributes.equip.shields.capMult; + if (data.attributes.hp.temp === null) data.attributes.hp.temp = data.attributes.hp.tempmax; + + // Prepare Speeds + data.attributes.movement.space = + sizeData.baseSpaceSpeed + 50 * (data.abilities.str.mod - data.abilities.con.mod); + data.attributes.movement.turn = Math.min( + data.attributes.movement.space, + Math.max(50, sizeData.baseTurnSpeed - 50 * (data.abilities.dex.mod - data.abilities.con.mod)) + ); + + // Prepare Max Suites + data.attributes.mods.suites.max = + sizeData.modMaxSuitesBase + sizeData.modMaxSuitesMult * data.abilities.con.mod; + + // Prepare Hardpoints + data.attributes.mods.hardpoints.max = sizeData.hardpointMult * Math.max(1, data.abilities.str.mod); + + //Prepare Fuel + data.attributes.fuel = this._computeFuel(actorData); + } + + // Prepare skills + this._prepareSkills(actorData, bonuses, checkBonus, originalSkills); + + // Determine Initiative Modifier + const init = data.attributes.init; + const athlete = flags.remarkableAthlete; + const joat = flags.jackOfAllTrades; + init.mod = data.abilities.dex.mod; + 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.value = init.value ?? 0; + init.bonus = init.value + (flags.initiativeAlert ? 5 : 0); + init.total = init.mod + init.prof + init.bonus; + + // Prepare power-casting data + data.attributes.powerForceLightDC = 8 + data.abilities.wis.mod + data.attributes.prof ?? 10; + data.attributes.powerForceDarkDC = 8 + data.abilities.cha.mod + data.attributes.prof ?? 10; + data.attributes.powerForceUnivDC = + Math.max(data.attributes.powerForceLightDC, data.attributes.powerForceDarkDC) ?? 10; + data.attributes.powerTechDC = 8 + data.abilities.int.mod + data.attributes.prof ?? 10; + this._computeDerivedPowercasting(this.data); + + // Compute owned item attributes which depend on prepared Actor data + this.items.forEach((item) => { + item.getSaveDC(); + item.getAttackToHit(); + }); + } + + /* -------------------------------------------- */ + + /** + * Return the amount of experience required to gain a certain character level. + * @param level {Number} The desired level + * @return {Number} The XP required + */ + getLevelExp(level) { + const levels = CONFIG.SW5E.CHARACTER_EXP_LEVELS; + return levels[Math.min(level, levels.length - 1)]; + } + + /* -------------------------------------------- */ + + /** + * Return the amount of experience granted by killing a creature of a certain CR. + * @param cr {Number} The creature's challenge rating + * @return {Number} The amount of experience granted per kill + */ + getCRExp(cr) { + if (cr < 1.0) return Math.max(200 * cr, 10); + return CONFIG.SW5E.CR_EXP_LEVELS[cr]; + } + + /* -------------------------------------------- */ + + /** @override */ + getRollData() { + const data = super.getRollData(); + data.classes = this.data.items.reduce((obj, i) => { + if (i.type === "class") { + obj[i.name.slugify({strict: true})] = i.data; + } + return obj; + }, {}); + data.prof = this.data.data.attributes.prof || 0; + return data; + } + + /* -------------------------------------------- */ + + /** + * Return the features which a character is awarded for each class level + * @param {string} className The class name being added + * @param {string} archetypeName The archetype of the class being added, if any + * @param {number} level The number of levels in the added class + * @param {number} priorLevel The previous level of the added class + * @return {Promise} Array of Item5e entities + */ + static async getClassFeatures({className = "", archetypeName = "", level = 1, priorLevel = 0} = {}) { + className = className.toLowerCase(); + archetypeName = archetypeName.slugify(); + + // Get the configuration of features which may be added + const clsConfig = CONFIG.SW5E.classFeatures[className]; + if (!clsConfig) return []; + + // Acquire class features + let ids = []; + for (let [l, f] of Object.entries(clsConfig.features || {})) { + l = parseInt(l); + if (l <= level && l > priorLevel) ids = ids.concat(f); + } + + // Acquire archetype features + const archConfig = clsConfig.archetypes[archetypeName] || {}; + for (let [l, f] of Object.entries(archConfig.features || {})) { + l = parseInt(l); + if (l <= level && l > priorLevel) ids = ids.concat(f); + } + + // Load item data for all identified features + const features = []; + for (let id of ids) { + features.push(await 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 updateData = expandObject(u); + const config = { + className: updateData.name || item.data.name, + archetypeName: getProperty(updateData, "data.archetype") || item.data.data.archetype, + level: getProperty(updateData, "data.levels"), + priorLevel: item ? item.data.data.levels : 0 + }; + + // Get and create features for an increased class level + let changed = false; + if (config.level && config.level > config.priorLevel) changed = true; + if (config.archetypeName !== item.data.data.archetype) changed = true; + + // Get features to create + if (changed) { + const existing = new Set(this.items.map((i) => i.name)); + const features = await Actor5e.getClassFeatures(config); + for (let f of features) { + if (!existing.has(f.name)) toCreate.push(f); + } + } + } + return toCreate; + } + + /* -------------------------------------------- */ + /* Data Preparation Helpers */ + /* -------------------------------------------- */ + + /** + * Prepare Character type specific data + */ + _prepareCharacterData(actorData) { + const data = actorData.data; + + // Determine character level and available hit dice based on owned Class items + const [level, hd] = actorData.items.reduce( + (arr, item) => { + if (item.type === "class") { + const classLevels = parseInt(item.data.levels) || 1; + arr[0] += classLevels; + arr[1] += classLevels - (parseInt(item.data.hitDiceUsed) || 0); + } + return arr; + }, + [0, 0] + ); + data.details.level = level; + data.attributes.hd = hd; + + // Character proficiency bonus + data.attributes.prof = Math.floor((level + 7) / 4); + + // Experience required for next level + const xp = data.details.xp; + xp.max = this.getLevelExp(level || 1); + const prior = this.getLevelExp(level - 1 || 0); + const required = xp.max - prior; + const pct = Math.round(((xp.value - prior) * 100) / required); + xp.pct = Math.clamped(pct, 0, 100); + + // Add base Powercasting attributes + this._computeBasePowercasting(actorData); + } + + /* -------------------------------------------- */ + + /** + * Prepare NPC type specific data + */ + _prepareNPCData(actorData) { + const data = actorData.data; + + // Kill Experience + data.details.xp.value = this.getCRExp(data.details.cr); + + // Proficiency + data.attributes.prof = Math.floor((Math.max(data.details.cr, 1) + 7) / 4); + + this._computeBasePowercasting(actorData); + + // Powercaster Level + if (data.attributes.powercasting && !Number.isNumeric(data.details.powerLevel)) { + data.details.powerLevel = Math.max(data.details.cr, 1); + } + } + + /* -------------------------------------------- */ + + /** + * Prepare vehicle type-specific data + * @param actorData + * @private + */ + _prepareVehicleData(actorData) {} + + /* -------------------------------------------- */ + + /* -------------------------------------------- */ + + /** + * Prepare starship type-specific data + * @param actorData + * @private + */ + _prepareStarshipData(actorData) { + const data = actorData.data; + data.attributes.prof = 0; + // Determine Starship size-based properties based on owned Starship item + const size = actorData.items.filter((i) => i.type === "starship"); + if (size.length !== 0) { + const sizeData = size[0].data; + const tiers = parseInt(sizeData.tier) || 0; + data.traits.size = sizeData.size; // needs to be the short code + data.details.tier = tiers; + data.attributes.ac.value = 10 + Math.max(tiers - 1, 0); + data.attributes.hull.die = sizeData.hullDice; + data.attributes.hull.dicemax = sizeData.hullDiceStart + tiers; + data.attributes.hull.dice = sizeData.hullDiceStart + tiers - (parseInt(sizeData.hullDiceUsed) || 0); + data.attributes.shld.die = sizeData.shldDice; + data.attributes.shld.dicemax = sizeData.shldDiceStart + tiers; + data.attributes.shld.dice = sizeData.shldDiceStart + tiers - (parseInt(sizeData.shldDiceUsed) || 0); + sizeData.pwrDice = SW5E.powerDieTypes[tiers]; + data.attributes.power.die = sizeData.pwrDice; + data.attributes.cost.baseBuild = sizeData.buildBaseCost; + data.attributes.workforce.minBuild = sizeData.buildMinWorkforce; + data.attributes.workforce.max = data.attributes.workforce.minBuild * 5; + data.attributes.cost.baseUpgrade = SW5E.baseUpgradeCost[tiers]; + data.attributes.cost.multUpgrade = sizeData.upgrdCostMult; + data.attributes.workforce.minUpgrade = sizeData.upgrdMinWorkforce; + data.attributes.equip.size.crewMinWorkforce = parseInt(sizeData.crewMinWorkforce) || 1; + data.attributes.mods.capLimit = sizeData.modBaseCap; + data.attributes.mods.suites.cap = sizeData.modMaxSuiteCap; + data.attributes.cost.multModification = sizeData.modCostMult; + data.attributes.workforce.minModification = sizeData.modMinWorkforce; + data.attributes.cost.multEquip = sizeData.equipCostMult; + data.attributes.workforce.minEquip = sizeData.equipMinWorkforce; + data.attributes.equip.size.cargoCap = sizeData.cargoCap; + data.attributes.fuel.cost = sizeData.fuelCost; + data.attributes.fuel.cap = sizeData.fuelCap; + data.attributes.equip.size.foodCap = sizeData.foodCap; + } + + // Determine Starship armor-based properties based on owned Starship item + const armor = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "ssarmor"); // && (i.data.equipped === true))); + if (armor.length !== 0) { + const armorData = armor[0].data; + data.attributes.equip.armor.dr = parseInt(armorData.dmgred.value) || 0; + data.attributes.equip.armor.maxDex = armorData.armor.dex; + data.attributes.equip.armor.stealthDisadv = armorData.stealth; + } + + // Determine Starship hyperdrive-based properties based on owned Starship item + const hyperdrive = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "hyper"); // && (i.data.equipped === true))); + if (hyperdrive.length !== 0) { + const hdData = hyperdrive[0].data; + data.attributes.equip.hyperdrive.class = parseFloat(hdData.hdclass.value) || null; + } + + // Determine Starship power coupling-based properties based on owned Starship item + const pwrcpl = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "powerc"); // && (i.data.equipped === true))); + if (pwrcpl.length !== 0) { + const pwrcplData = pwrcpl[0].data; + data.attributes.equip.powerCoupling.centralCap = parseInt(pwrcplData.cscap.value) || 0; + data.attributes.equip.powerCoupling.systemCap = parseInt(pwrcplData.sscap.value) || 0; + data.attributes.power.central.max = 0; + data.attributes.power.comms.max = 0; + data.attributes.power.engines.max = 0; + data.attributes.power.shields.max = 0; + data.attributes.power.sensors.max = 0; + data.attributes.power.weapons.max = 0; + } + + // Determine Starship reactor-based properties based on owned Starship item + const reactor = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "reactor"); // && (i.data.equipped === true))); + if (reactor.length !== 0) { + const reactorData = reactor[0].data; + data.attributes.equip.reactor.fuelMult = parseFloat(reactorData.fuelcostsmod.value) || 0; + data.attributes.equip.reactor.powerRecDie = reactorData.powdicerec.value; + } + + // Determine Starship shield-based properties based on owned Starship item + const shields = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "ssshield"); // && (i.data.equipped === true))); + if (shields.length !== 0) { + const shieldsData = shields[0].data; + data.attributes.equip.shields.capMult = parseFloat(shieldsData.capx.value) || 1; + data.attributes.equip.shields.regenRateMult = parseFloat(shieldsData.regrateco.value) || 1; + } + } + + /* -------------------------------------------- */ + + /** + * Prepare skill checks. + * @param actorData + * @param bonuses Global bonus data. + * @param checkBonus Ability check specific bonus. + * @param originalSkills A transformed actor's original actor's skills. + * @private + */ + _prepareSkills(actorData, bonuses, checkBonus, originalSkills) { + if (actorData.type === "vehicle") return; + + const data = actorData.data; + const flags = actorData.flags.sw5e || {}; + + // Skill modifiers + const feats = SW5E.characterFlags; + const athlete = flags.remarkableAthlete; + const joat = flags.jackOfAllTrades; + const observant = flags.observantFeat; + const skillBonus = Number.isNumeric(bonuses.skill) ? parseInt(bonuses.skill) : 0; + for (let [id, skl] of Object.entries(data.skills)) { + skl.value = Math.clamped(Number(skl.value).toNearest(0.5), 0, 2) ?? 0; + let round = Math.floor; + + // Remarkable + if (athlete && skl.value < 0.5 && feats.remarkableAthlete.abilities.includes(skl.ability)) { + skl.value = 0.5; + round = Math.ceil; + } + + // Jack of All Trades + if (joat && skl.value < 0.5) { + skl.value = 0.5; + } + + // Polymorph Skill Proficiencies + if (originalSkills) { + skl.value = Math.max(skl.value, originalSkills[id].value); + } + + // Compute modifier + skl.bonus = checkBonus + skillBonus; + skl.mod = data.abilities[skl.ability].mod; + skl.prof = round(skl.value * data.attributes.prof); + skl.total = skl.mod + skl.prof + skl.bonus; + + // Compute passive bonus + const passive = observant && feats.observantFeat.skills.includes(id) ? 5 : 0; + skl.passive = 10 + skl.total + passive; + } + } + + /* -------------------------------------------- */ + + /** + * Prepare data related to the power-casting capabilities of the Actor + * @private + */ + _computeBasePowercasting(actorData) { + if (actorData.type === "vehicle" || actorData.type === "starship") return; + const powers = actorData.data.powers; + const isNPC = actorData.type === "npc"; + + // Translate the list of classes into force and tech power-casting progression + const forceProgression = { + classes: 0, + levels: 0, + multi: 0, + maxClass: "none", + maxClassPriority: 0, + maxClassLevels: 0, + maxClassPowerLevel: 0, + powersKnown: 0, + points: 0 + }; + const techProgression = { + classes: 0, + levels: 0, + multi: 0, + maxClass: "none", + maxClassPriority: 0, + maxClassLevels: 0, + maxClassPowerLevel: 0, + powersKnown: 0, + points: 0 + }; + + // Tabulate the total power-casting progression + const classes = this.data.items.filter((i) => i.type === "class"); + let priority = 0; + for (let cls of classes) { + const d = cls.data; + if (d.powercasting === "none") continue; + const levels = d.levels; + const prog = d.powercasting; + + switch (prog) { + case "consular": + priority = 3; + forceProgression.levels += levels; + forceProgression.multi += (SW5E.powerMaxLevel["consular"][19] / 9) * levels; + forceProgression.classes++; + // see if class controls high level forcecasting + if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) { + forceProgression.maxClass = "consular"; + forceProgression.maxClassLevels = levels; + forceProgression.maxClassPriority = priority; + forceProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["consular"][Math.clamped(levels - 1, 0, 20)]; + } + // calculate points and powers known + forceProgression.powersKnown += SW5E.powersKnown["consular"][Math.clamped(levels - 1, 0, 20)]; + forceProgression.points += SW5E.powerPoints["consular"][Math.clamped(levels - 1, 0, 20)]; + break; + case "engineer": + priority = 2; + techProgression.levels += levels; + techProgression.multi += (SW5E.powerMaxLevel["engineer"][19] / 9) * levels; + techProgression.classes++; + // see if class controls high level techcasting + if (levels >= techProgression.maxClassLevels && priority > techProgression.maxClassPriority) { + techProgression.maxClass = "engineer"; + techProgression.maxClassLevels = levels; + techProgression.maxClassPriority = priority; + techProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["engineer"][Math.clamped(levels - 1, 0, 20)]; + } + techProgression.powersKnown += SW5E.powersKnown["engineer"][Math.clamped(levels - 1, 0, 20)]; + techProgression.points += SW5E.powerPoints["engineer"][Math.clamped(levels - 1, 0, 20)]; + break; + case "guardian": + priority = 1; + forceProgression.levels += levels; + forceProgression.multi += (SW5E.powerMaxLevel["guardian"][19] / 9) * levels; + forceProgression.classes++; + // see if class controls high level forcecasting + if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) { + forceProgression.maxClass = "guardian"; + forceProgression.maxClassLevels = levels; + forceProgression.maxClassPriority = priority; + forceProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["guardian"][Math.clamped(levels - 1, 0, 20)]; + } + forceProgression.powersKnown += SW5E.powersKnown["guardian"][Math.clamped(levels - 1, 0, 20)]; + forceProgression.points += SW5E.powerPoints["guardian"][Math.clamped(levels - 1, 0, 20)]; + break; + case "scout": + priority = 1; + techProgression.levels += levels; + techProgression.multi += (SW5E.powerMaxLevel["scout"][19] / 9) * levels; + techProgression.classes++; + // see if class controls high level techcasting + if (levels >= techProgression.maxClassLevels && priority > techProgression.maxClassPriority) { + techProgression.maxClass = "scout"; + techProgression.maxClassLevels = levels; + techProgression.maxClassPriority = priority; + techProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["scout"][Math.clamped(levels - 1, 0, 20)]; + } + techProgression.powersKnown += SW5E.powersKnown["scout"][Math.clamped(levels - 1, 0, 20)]; + techProgression.points += SW5E.powerPoints["scout"][Math.clamped(levels - 1, 0, 20)]; + break; + case "sentinel": + priority = 2; + forceProgression.levels += levels; + forceProgression.multi += (SW5E.powerMaxLevel["sentinel"][19] / 9) * levels; + forceProgression.classes++; + // see if class controls high level forcecasting + if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) { + forceProgression.maxClass = "sentinel"; + forceProgression.maxClassLevels = levels; + forceProgression.maxClassPriority = priority; + forceProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["sentinel"][Math.clamped(levels - 1, 0, 20)]; + } + forceProgression.powersKnown += SW5E.powersKnown["sentinel"][Math.clamped(levels - 1, 0, 20)]; + forceProgression.points += SW5E.powerPoints["sentinel"][Math.clamped(levels - 1, 0, 20)]; + break; + } + } + + // EXCEPTION: multi-classed progression uses multi rounded down rather than levels + if (!isNPC && forceProgression.classes > 1) { + forceProgression.levels = Math.floor(forceProgression.multi); + forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel["multi"][forceProgression.levels - 1]; + } + if (!isNPC && techProgression.classes > 1) { + techProgression.levels = Math.floor(techProgression.multi); + techProgression.maxClassPowerLevel = SW5E.powerMaxLevel["multi"][techProgression.levels - 1]; + } + + // EXCEPTION: NPC with an explicit power-caster level + if (isNPC && actorData.data.details.powerForceLevel) { + forceProgression.levels = actorData.data.details.powerForceLevel; + actorData.data.attributes.force.level = forceProgression.levels; + forceProgression.maxClass = actorData.data.attributes.powercasting; + forceProgression.maxClassPowerLevel = + SW5E.powerMaxLevel[forceProgression.maxClass][Math.clamped(forceProgression.levels - 1, 0, 20)]; + } + if (isNPC && actorData.data.details.powerTechLevel) { + techProgression.levels = actorData.data.details.powerTechLevel; + actorData.data.attributes.tech.level = techProgression.levels; + techProgression.maxClass = actorData.data.attributes.powercasting; + techProgression.maxClassPowerLevel = + SW5E.powerMaxLevel[techProgression.maxClass][Math.clamped(techProgression.levels - 1, 0, 20)]; + } + + // Look up the number of slots per level from the powerLimit table + let forcePowerLimit = Array.from(SW5E.powerLimit["none"]); + for (let i = 0; i < forceProgression.maxClassPowerLevel; i++) { + forcePowerLimit[i] = SW5E.powerLimit[forceProgression.maxClass][i]; + } + + for (let [n, lvl] of Object.entries(powers)) { + let i = parseInt(n.slice(-1)); + if (Number.isNaN(i)) continue; + if (Number.isNumeric(lvl.foverride)) lvl.fmax = Math.max(parseInt(lvl.foverride), 0); + else lvl.fmax = forcePowerLimit[i - 1] || 0; + if (isNPC) { + lvl.fvalue = lvl.fmax; + } else { + lvl.fvalue = Math.min(parseInt(lvl.fvalue || lvl.value || lvl.fmax), lvl.fmax); + } + } + + let techPowerLimit = Array.from(SW5E.powerLimit["none"]); + for (let i = 0; i < techProgression.maxClassPowerLevel; i++) { + techPowerLimit[i] = SW5E.powerLimit[techProgression.maxClass][i]; + } + + for (let [n, lvl] of Object.entries(powers)) { + let i = parseInt(n.slice(-1)); + if (Number.isNaN(i)) continue; + if (Number.isNumeric(lvl.toverride)) lvl.tmax = Math.max(parseInt(lvl.toverride), 0); + else lvl.tmax = techPowerLimit[i - 1] || 0; + if (isNPC) { + lvl.tvalue = lvl.tmax; + } else { + lvl.tvalue = Math.min(parseInt(lvl.tvalue || lvl.value || lvl.tmax), lvl.tmax); + } + } + + // Set Force and tech power for PC Actors + if (!isNPC && forceProgression.levels) { + actorData.data.attributes.force.known.max = forceProgression.powersKnown; + actorData.data.attributes.force.points.max = forceProgression.points; // + Math.max(actorData.data.abilities.wis.mod,actorData.data.abilities.cha.mod); + actorData.data.attributes.force.level = forceProgression.levels; + } + if (!isNPC && techProgression.levels) { + actorData.data.attributes.tech.known.max = techProgression.powersKnown; + actorData.data.attributes.tech.points.max = techProgression.points; // + actorData.data.abilities.int.mod; + actorData.data.attributes.tech.level = techProgression.levels; + } + + // Tally Powers Known and check for migration first to avoid errors + let hasKnownPowers = actorData?.data?.attributes?.force?.known?.value !== undefined; + if (hasKnownPowers) { + const knownPowers = this.data.items.filter((i) => i.type === "power"); + let knownForcePowers = 0; + let knownTechPowers = 0; + for (let knownPower of knownPowers) { + const d = knownPower.data; + switch (knownPower.data.school) { + case "lgt": + case "uni": + case "drk": { + knownForcePowers++; + break; + } + case "tec": { + knownTechPowers++; + break; + } + } + continue; + } + actorData.data.attributes.force.known.value = knownForcePowers; + actorData.data.attributes.tech.known.value = knownTechPowers; + } + } + + /* -------------------------------------------- */ + + /** + * Prepare data related to the power-casting capabilities of the Actor + * @private + */ + _computeDerivedPowercasting(actorData) { + if (actorData.type !== "actor") return; + + // Set Force and tech power for PC Actors + if (!!actorData.data.attributes.force.level) { + actorData.data.attributes.force.points.max += Math.max( + actorData.data.abilities.wis.mod, + actorData.data.abilities.cha.mod + ); + } + if (!!actorData.data.attributes.tech.level) { + actorData.data.attributes.tech.points.max += actorData.data.abilities.int.mod; + } + } + + /* -------------------------------------------- */ + + /** + * Compute the level and percentage of encumbrance for an Actor. + * + * Optionally include the weight of carried currency across all denominations by applying the standard rule + * from the PHB pg. 143 + * @param {Object} actorData The data object for the Actor being rendered + * @returns {{max: number, value: number, pct: number}} An object describing the character's encumbrance level + * @private + */ + _computeEncumbrance(actorData) { + // Get the total weight from items + const physicalItems = ["weapon", "equipment", "consumable", "tool", "backpack", "loot"]; + let weight = actorData.items.reduce((weight, i) => { + if (!physicalItems.includes(i.type)) return weight; + const q = i.data.quantity || 0; + const w = i.data.weight || 0; + return weight + q * w; + }, 0); + + // [Optional] add Currency Weight (for non-transformed actors) + if (game.settings.get("sw5e", "currencyWeight") && actorData.data.currency) { + const currency = actorData.data.currency; + const numCoins = Object.values(currency).reduce((val, denom) => (val += Math.max(denom, 0)), 0); + weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; + } + + // Determine the encumbrance size class + let mod = + { + tiny: 0.5, + sm: 1, + med: 1, + lg: 2, + huge: 4, + grg: 8 + }[actorData.data.traits.size] || 1; + if (this.getFlag("sw5e", "powerfulBuild")) mod = Math.min(mod * 2, 8); + + // Compute Encumbrance percentage + weight = weight.toNearest(0.1); + const max = actorData.data.abilities.str.value * CONFIG.SW5E.encumbrance.strMultiplier * mod; + const pct = Math.clamped((weight * 100) / max, 0, 100); + return {value: weight.toNearest(0.1), max, pct, encumbered: pct > 2 / 3}; + } + + _computeFuel(actorData) { + const fuel = actorData.data.attributes.fuel; + // Compute Fuel percentage + const pct = Math.clamped((fuel.value.toNearest(0.1) * 100) / fuel.cap, 0, 100); + return {...fuel, pct, fueled: pct > 0}; + } + + /* -------------------------------------------- */ + /* Socket Listeners and Handlers + /* -------------------------------------------- */ + + /** @override */ + static async create(data, options = {}) { + data.token = data.token || {}; + if (data.type === "character") { + mergeObject( + data.token, + { + vision: true, + dimSight: 30, + brightSight: 0, + actorLink: true, + disposition: 1 + }, + {overwrite: false} + ); + } + return super.create(data, options); + } + + /* -------------------------------------------- */ + + /** @override */ + async update(data, options = {}) { + // Apply changes in Actor size to Token width/height + const newSize = getProperty(data, "data.traits.size"); + if (newSize && newSize !== getProperty(this.data, "data.traits.size")) { + let size = CONFIG.SW5E.tokenSizes[newSize]; + if (this.isToken) this.token.update({height: size, width: size}); + else if (!data["token.width"] && !hasProperty(data, "token.width")) { + data["token.height"] = size; + data["token.width"] = size; + } + } + + // Reset death save counters + if (this.data.data.attributes.hp.value <= 0 && getProperty(data, "data.attributes.hp.value") > 0) { + setProperty(data, "data.attributes.death.success", 0); + setProperty(data, "data.attributes.death.failure", 0); + } + + // Perform the update + return super.update(data, options); + } + + /* -------------------------------------------- */ + + /** @override */ + async createEmbeddedEntity(embeddedName, itemData, options = {}) { + // Pre-creation steps for owned items + if (embeddedName === "OwnedItem") this._preCreateOwnedItem(itemData, options); + + // Standard embedded entity creation + return super.createEmbeddedEntity(embeddedName, itemData, options); + } + + /* -------------------------------------------- */ + + /** + * A temporary shim function which will eventually (in core fvtt version 0.8.0+) be migrated to the new abstraction layer + * @param itemData + * @param options + * @private + */ + _preCreateOwnedItem(itemData, options) { + if (this.data.type === "vehicle") return; + const isNPC = this.data.type === "npc"; + let initial = {}; + switch (itemData.type) { + case "weapon": + if (getProperty(itemData, "data.equipped") === undefined) { + initial["data.equipped"] = isNPC; // NPCs automatically equip weapons + } + if (getProperty(itemData, "data.proficient") === undefined) { + if (isNPC) { + initial["data.proficient"] = true; // NPCs automatically have equipment proficiency + } else { + const weaponProf = { + natural: true, + simpleVW: "sim", + simpleB: "sim", + simpleLW: "sim", + martialVW: "mar", + martialB: "mar", + martialLW: "mar" + }[itemData.data?.weaponType]; // Player characters check proficiency + const actorWeaponProfs = this.data.data.traits?.weaponProf?.value || []; + const hasWeaponProf = weaponProf === true || actorWeaponProfs.includes(weaponProf); + initial["data.proficient"] = hasWeaponProf; + } + } + break; + + case "equipment": + if (getProperty(itemData, "data.equipped") === undefined) { + initial["data.equipped"] = isNPC; // NPCs automatically equip equipment + } + if (getProperty(itemData, "data.proficient") === undefined) { + if (isNPC) { + initial["data.proficient"] = true; // NPCs automatically have equipment proficiency + } else { + const armorProf = { + natural: true, + clothing: true, + light: "lgt", + medium: "med", + heavy: "hvy", + shield: "shl" + }[itemData.data?.armor?.type]; // Player characters check proficiency + const actorArmorProfs = this.data.data.traits?.armorProf?.value || []; + const hasEquipmentProf = armorProf === true || actorArmorProfs.includes(armorProf); + initial["data.proficient"] = hasEquipmentProf; + } + } + break; + + case "power": + initial["data.prepared"] = true; // automatically prepare powers for everyone + break; + } + mergeObject(itemData, initial); + } + + /* -------------------------------------------- */ + /* Gameplay Mechanics */ + /* -------------------------------------------- */ + + /** @override */ + async modifyTokenAttribute(attribute, value, isDelta, isBar) { + if (attribute === "attributes.hp") { + const hp = getProperty(this.data.data, attribute); + const delta = isDelta ? -1 * value : hp.value + hp.temp - value; + return this.applyDamage(delta); + } + return super.modifyTokenAttribute(attribute, value, isDelta, isBar); + } + + /* -------------------------------------------- */ + + /** + * Apply a certain amount of damage or healing to the health pool for Actor + * @param {number} amount An amount of damage (positive) or healing (negative) to sustain + * @param {number} multiplier A multiplier which allows for resistance, vulnerability, or healing + * @return {Promise} A Promise which resolves once the damage has been applied + */ + async applyDamage(amount = 0, multiplier = 1) { + amount = Math.floor(parseInt(amount) * multiplier); + const hp = this.data.data.attributes.hp; + + // Deduct damage from temp HP first + const tmp = parseInt(hp.temp) || 0; + const dt = amount > 0 ? Math.min(tmp, amount) : 0; + + // Remaining goes to health + const tmpMax = parseInt(hp.tempmax) || 0; + const dh = Math.clamped(hp.value - (amount - dt), 0, hp.max + tmpMax); + + // Update the Actor + const updates = { + "data.attributes.hp.temp": tmp - dt, + "data.attributes.hp.value": dh + }; + + // Delegate damage application to a hook + // TODO replace this in the future with a better modifyTokenAttribute function in the core + const allowed = Hooks.call( + "modifyTokenAttribute", + { + attribute: "attributes.hp", + value: amount, + isDelta: false, + isBar: true + }, + updates + ); + return allowed !== false ? this.update(updates) : this; + } + + /* -------------------------------------------- */ + + /** + * Roll a Skill Check + * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus + * @param {string} skillId The skill id (e.g. "ins") + * @param {Object} options Options which configure how the skill check is rolled + * @return {Promise} A Promise which resolves to the created Roll instance + */ + rollSkill(skillId, options = {}) { + const skl = this.data.data.skills[skillId]; + const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; + + // Compose roll parts and data + const parts = ["@mod"]; + const data = {mod: skl.mod + skl.prof}; + + // Ability test bonus + if (bonuses.check) { + data["checkBonus"] = bonuses.check; + parts.push("@checkBonus"); + } + + // Skill check bonus + if (bonuses.skill) { + data["skillBonus"] = bonuses.skill; + parts.push("@skillBonus"); + } + + // Add provided extra roll parts now because they will get clobbered by mergeObject below + if (options.parts?.length > 0) { + parts.push(...options.parts); + } + + // Reliable Talent applies to any skill check we have full or better proficiency in + const reliableTalent = skl.value >= 1 && this.getFlag("sw5e", "reliableTalent"); + + // Roll and return + const rollData = mergeObject(options, { + parts: parts, + data: data, + title: game.i18n.format("SW5E.SkillPromptTitle", { + skill: CONFIG.SW5E.skills[skillId] || CONFIG.SW5E.starshipSkills[skillId] + }), + halflingLucky: this.getFlag("sw5e", "halflingLucky"), + reliableTalent: reliableTalent, + messageData: {"flags.sw5e.roll": {type: "skill", skillId}} + }); + rollData.speaker = options.speaker || ChatMessage.getSpeaker({actor: this}); + return d20Roll(rollData); + } + + /* -------------------------------------------- */ + + /** + * Roll a generic ability test or saving throw. + * Prompt the user for input on which variety of roll they want to do. + * @param {String}abilityId The ability id (e.g. "str") + * @param {Object} options Options which configure how ability tests or saving throws are rolled + */ + rollAbility(abilityId, options = {}) { + const label = CONFIG.SW5E.abilities[abilityId]; + new Dialog({ + title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}), + content: `

${game.i18n.format("SW5E.AbilityPromptText", {ability: label})}

`, + buttons: { + test: { + label: game.i18n.localize("SW5E.ActionAbil"), + callback: () => this.rollAbilityTest(abilityId, options) + }, + save: { + label: game.i18n.localize("SW5E.ActionSave"), + callback: () => this.rollAbilitySave(abilityId, options) + } + } + }).render(true); + } + + /* -------------------------------------------- */ + + /** + * Roll an Ability Test + * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus + * @param {String} abilityId The ability ID (e.g. "str") + * @param {Object} options Options which configure how ability tests are rolled + * @return {Promise} A Promise which resolves to the created Roll instance + */ + rollAbilityTest(abilityId, options = {}) { + const label = CONFIG.SW5E.abilities[abilityId]; + const abl = this.data.data.abilities[abilityId]; + + // Construct parts + const parts = ["@mod"]; + const data = {mod: abl.mod}; + + // Add feat-related proficiency bonuses + const feats = this.data.flags.sw5e || {}; + if (feats.remarkableAthlete && SW5E.characterFlags.remarkableAthlete.abilities.includes(abilityId)) { + parts.push("@proficiency"); + data.proficiency = Math.ceil(0.5 * this.data.data.attributes.prof); + } else if (feats.jackOfAllTrades) { + parts.push("@proficiency"); + data.proficiency = Math.floor(0.5 * this.data.data.attributes.prof); + } + + // Add global actor bonus + const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; + if (bonuses.check) { + parts.push("@checkBonus"); + data.checkBonus = bonuses.check; + } + + // Add provided extra roll parts now because they will get clobbered by mergeObject below + if (options.parts?.length > 0) { + parts.push(...options.parts); + } + + // Roll and return + const rollData = mergeObject(options, { + parts: parts, + data: data, + title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}), + halflingLucky: feats.halflingLucky, + messageData: {"flags.sw5e.roll": {type: "ability", abilityId}} + }); + rollData.speaker = options.speaker || ChatMessage.getSpeaker({actor: this}); + return d20Roll(rollData); + } + + /* -------------------------------------------- */ + + /** + * Roll an Ability Saving Throw + * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus + * @param {String} abilityId The ability ID (e.g. "str") + * @param {Object} options Options which configure how ability tests are rolled + * @return {Promise} A Promise which resolves to the created Roll instance + */ + rollAbilitySave(abilityId, options = {}) { + const label = CONFIG.SW5E.abilities[abilityId]; + const abl = this.data.data.abilities[abilityId]; + + // Construct parts + const parts = ["@mod"]; + const data = {mod: abl.mod}; + + // Include proficiency bonus + if (abl.prof > 0) { + parts.push("@prof"); + data.prof = abl.prof; + } + + // Include a global actor ability save bonus + const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; + if (bonuses.save) { + parts.push("@saveBonus"); + data.saveBonus = bonuses.save; + } + + // Add provided extra roll parts now because they will get clobbered by mergeObject below + if (options.parts?.length > 0) { + parts.push(...options.parts); + } + + // Roll and return + const rollData = mergeObject(options, { + parts: parts, + data: data, + title: game.i18n.format("SW5E.SavePromptTitle", {ability: label}), + halflingLucky: this.getFlag("sw5e", "halflingLucky"), + messageData: {"flags.sw5e.roll": {type: "save", abilityId}} + }); + rollData.speaker = options.speaker || ChatMessage.getSpeaker({actor: this}); + return d20Roll(rollData); + } + + /* -------------------------------------------- */ + + /** + * Perform a death saving throw, rolling a d20 plus any global save bonuses + * @param {Object} options Additional options which modify the roll + * @return {Promise} A Promise which resolves to the Roll instance + */ + async rollDeathSave(options = {}) { + // Display a warning if we are not at zero HP or if we already have reached 3 + const death = this.data.data.attributes.death; + if (this.data.data.attributes.hp.value > 0 || death.failure >= 3 || death.success >= 3) { + ui.notifications.warn(game.i18n.localize("SW5E.DeathSaveUnnecessary")); + return null; + } + + // Evaluate a global saving throw bonus + const parts = []; + const data = {}; + const speaker = options.speaker || ChatMessage.getSpeaker({actor: this}); + + // Include a global actor ability save bonus + const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; + if (bonuses.save) { + parts.push("@saveBonus"); + data.saveBonus = bonuses.save; + } + + // Evaluate the roll + const rollData = mergeObject(options, { + parts: parts, + data: data, + title: game.i18n.localize("SW5E.DeathSavingThrow"), + speaker: speaker, + halflingLucky: this.getFlag("sw5e", "halflingLucky"), + targetValue: 10, + messageData: {"flags.sw5e.roll": {type: "death"}} + }); + rollData.speaker = speaker; + const roll = await d20Roll(rollData); + if (!roll) return null; + + // Take action depending on the result + const success = roll.total >= 10; + const d20 = roll.dice[0].total; + + // Save success + if (success) { + let successes = (death.success || 0) + 1; + + // Critical Success = revive with 1hp + if (d20 === 20) { + await this.update({ + "data.attributes.death.success": 0, + "data.attributes.death.failure": 0, + "data.attributes.hp.value": 1 + }); + await ChatMessage.create({ + content: game.i18n.format("SW5E.DeathSaveCriticalSuccess", {name: this.name}), + speaker + }); + } + + // 3 Successes = survive and reset checks + else if (successes === 3) { + await this.update({ + "data.attributes.death.success": 0, + "data.attributes.death.failure": 0 + }); + await ChatMessage.create({ + content: game.i18n.format("SW5E.DeathSaveSuccess", {name: this.name}), + speaker + }); + } + + // Increment successes + else await this.update({"data.attributes.death.success": Math.clamped(successes, 0, 3)}); + } + + // Save failure + else { + let failures = (death.failure || 0) + (d20 === 1 ? 2 : 1); + await this.update({"data.attributes.death.failure": Math.clamped(failures, 0, 3)}); + if (failures >= 3) { + // 3 Failures = death + await ChatMessage.create({ + content: game.i18n.format("SW5E.DeathSaveFailure", {name: this.name}), + speaker + }); + } + } + + // Return the rolled result + return roll; + } + + /* -------------------------------------------- */ + + /** + * Roll a hit die of the appropriate type, gaining hit points equal to the die roll plus your CON modifier + * @param {string} [denomination] The hit denomination of hit die to roll. Example "d8". + * If no denomination is provided, the first available HD will be used + * @param {boolean} [dialog] Show a dialog prompt for configuring the hit die roll? + * @return {Promise} The created Roll instance, or null if no hit die was rolled + */ + async rollHitDie(denomination, {dialog = true} = {}) { + // If no denomination was provided, choose the first available + let cls = null; + if (!denomination) { + cls = this.itemTypes.class.find((c) => c.data.data.hitDiceUsed < c.data.data.levels); + if (!cls) return null; + denomination = cls.data.data.hitDice; + } + + // Otherwise locate a class (if any) which has an available hit die of the requested denomination + else { + cls = this.items.find((i) => { + const d = i.data.data; + return d.hitDice === denomination && (d.hitDiceUsed || 0) < (d.levels || 1); + }); + } + + // If no class is available, display an error notification + if (!cls) { + ui.notifications.error(game.i18n.format("SW5E.HitDiceWarn", {name: this.name, formula: denomination})); + return null; + } + + // Prepare roll data + const parts = [`1${denomination}`, "@abilities.con.mod"]; + const title = game.i18n.localize("SW5E.HitDiceRoll"); + const rollData = duplicate(this.data.data); + + // Call the roll helper utility + const roll = await damageRoll({ + event: new Event("hitDie"), + parts: parts, + data: rollData, + title: title, + speaker: ChatMessage.getSpeaker({actor: this}), + allowcritical: false, + fastForward: !dialog, + dialogOptions: {width: 350}, + messageData: {"flags.sw5e.roll": {type: "hitDie"}} + }); + if (!roll) return null; + + // Adjust actor data + await cls.update({"data.hitDiceUsed": cls.data.data.hitDiceUsed + 1}); + const hp = this.data.data.attributes.hp; + const dhp = Math.min(hp.max + (hp.tempmax ?? 0) - hp.value, roll.total); + await this.update({"data.attributes.hp.value": hp.value + dhp}); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Roll a hull die of the appropriate type, gaining hull points equal to the die roll plus your CON modifier + * @param {string} [denomination] The hit denomination of hull die to roll. Example "d8". + * If no denomination is provided, the first available HD will be used + * @param {string} [numDice] How many damage dice to roll? + * @param {string} [keep] Which dice to keep? Example "kh1". + * @param {boolean} [dialog] Show a dialog prompt for configuring the hull die roll? + * @return {Promise} The created Roll instance, or null if no hull die was rolled + */ + async rollHullDie(denomination, numDice = "1", keep = "", {dialog = true} = {}) { + // If no denomination was provided, choose the first available + let sship = null; + if (!denomination) { + sship = this.itemTypes.class.find( + (s) => s.data.data.hullDiceUsed < s.data.data.tier + s.data.data.hullDiceStart + ); + if (!sship) return null; + denomination = sship.data.data.hullDice; + } + + // Otherwise locate a starship (if any) which has an available hit die of the requested denomination + else { + sship = this.items.find((i) => { + const d = i.data.data; + return d.hullDice === denomination && (d.hitDiceUsed || 0) < (d.tier || 0) + d.hullDiceStart; + }); + } + + // If no class is available, display an error notification + if (!sship) { + ui.notifications.error(game.i18n.format("SW5E.HullDiceWarn", {name: this.name, formula: denomination})); + return null; + } + + // Prepare roll data + const parts = [`${numDice}${denomination}${keep}`, "@abilities.con.mod"]; + const title = game.i18n.localize("SW5E.HullDiceRoll"); + const rollData = duplicate(this.data.data); + + // Call the roll helper utility + const roll = await damageRoll({ + event: new Event("hitDie"), + parts: parts, + data: rollData, + title: title, + speaker: ChatMessage.getSpeaker({actor: this}), + allowcritical: false, + fastForward: !dialog, + dialogOptions: {width: 350}, + messageData: {"flags.sw5e.roll": {type: "hullDie"}} + }); + if (!roll) return null; + + // Adjust actor data + await sship.update({"data.hullDiceUsed": sship.data.data.hullDiceUsed + 1}); + const hp = this.data.data.attributes.hp; + const dhp = Math.min(hp.max - hp.value, roll.total); + await this.update({"data.attributes.hp.value": hp.value + dhp}); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Roll a hull die of the appropriate type, gaining hull points equal to the die roll plus your CON modifier + * @return {Promise} The created Roll instance, or null if no hull die was rolled + */ + async rollHullDieCheck() { + // If no denomination was provided, choose the first available + let sship = null; + if (!denomination) { + sship = this.itemTypes.class.find( + (s) => s.data.data.hullDiceUsed < s.data.data.tier + s.data.data.hullDiceStart + ); + if (!sship) return null; + denomination = sship.data.data.hullDice; + } + + // Otherwise locate a starship (if any) which has an available hit die of the requested denomination + else { + sship = this.items.find((i) => { + const d = i.data.data; + return d.hullDice === denomination && (d.hitDiceUsed || 0) < (d.tier || 0) + d.hullDiceStart; + }); + } + + // If no class is available, display an error notification + if (!sship) { + ui.notifications.error(game.i18n.format("SW5E.HullDiceWarn", {name: this.name, formula: denomination})); + return null; + } + + // Prepare roll data + const parts = [`${numDice}${denomination}${keep}`, "@abilities.con.mod"]; + const title = game.i18n.localize("SW5E.HullDiceRoll"); + const rollData = duplicate(this.data.data); + + // Call the roll helper utility + const roll = await damageRoll({ + event: new Event("hitDie"), + parts: parts, + data: rollData, + title: title, + speaker: ChatMessage.getSpeaker({actor: this}), + allowcritical: false, + fastForward: !dialog, + dialogOptions: {width: 350}, + messageData: {"flags.sw5e.roll": {type: "hullDie"}} + }); + if (!roll) return null; + + // Adjust actor data + await sship.update({"data.hullDiceUsed": sship.data.data.hullDiceUsed + 1}); + const hp = this.data.data.attributes.hp; + const dhp = Math.min(hp.max - hp.value, roll.total); + await this.update({"data.attributes.hp.value": hp.value + dhp}); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Roll a shield die of the appropriate type, gaining shield points equal to the die roll + * multiplied by the shield regeneration coefficient + * @param {string} [denomination] The denomination of shield die to roll. Example "d8". + * If no denomination is provided, the first available SD will be used + * @param {boolean} [natural] Natural ship shield regeneration (true) or user action (false)? + * @param {string} [numDice] How many damage dice to roll? + * @param {string} [keep] Which dice to keep? Example "kh1". + * @param {boolean} [dialog] Show a dialog prompt for configuring the shield die roll? + * @return {Promise} The created Roll instance, or null if no shield die was rolled + */ + async rollShieldDie(denomination, natural = false, numDice = "1", keep = "", {dialog = true} = {}) { + // If no denomination was provided, choose the first available + let sship = null; + if (!denomination) { + sship = this.itemTypes.class.find( + (s) => s.data.data.shldDiceUsed < s.data.data.tier + s.data.data.shldDiceStart + ); + if (!sship) return null; + denomination = sship.data.data.shldDice; + } + + // Otherwise locate a starship (if any) which has an available hit die of the requested denomination + else { + sship = this.items.find((i) => { + const d = i.data.data; + return d.shldDice === denomination && (d.shldDiceUsed || 0) < (d.tier || 0) + d.shldDiceStart; + }); + } + + // If no starship is available, display an error notification + if (!sship) { + ui.notifications.error(game.i18n.format("SW5E.ShldDiceWarn", {name: this.name, formula: denomination})); + return null; + } + + // if natural regeneration roll max + if (natural) { + numdice = denomination.substring(1); + denomination = ""; + keep = ""; + } + + // Prepare roll data + const parts = [`${numDice}${denomination}${keep} * @attributes.regenRate`]; + const title = game.i18n.localize("SW5E.ShieldDiceRoll"); + const rollData = duplicate(this.data.data); + + // Call the roll helper utility + roll = await damageRoll({ + event: new Event("shldDie"), + parts: parts, + data: rollData, + title: title, + speaker: ChatMessage.getSpeaker({actor: this}), + allowcritical: false, + fastForward: !dialog, + dialogOptions: {width: 350}, + messageData: {"flags.sw5e.roll": {type: "shldDie"}} + }); + if (!roll) return null; + + // Adjust actor data + await sship.update({"data.shldDiceUsed": sship.data.data.shldDiceUsed + 1}); + const hp = this.data.data.attributes.hp; + const dhp = Math.min(hp.tempmax - hp.temp, roll.total); + await this.update({"data.attributes.hp.temp": hp.temp + dhp}); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Cause this Actor to take a Short Rest and regain all Tech Points + * During a Short Rest resources and limited item uses may be recovered + * @param {boolean} dialog Present a dialog window which allows for rolling hit dice as part of the Short Rest + * @param {boolean} chat Summarize the results of the rest workflow as a chat message + * @param {boolean} autoHD Automatically spend Hit Dice if you are missing 3 or more hit points + * @param {boolean} autoHDThreshold A number of missing hit points which would trigger an automatic HD roll + * @return {Promise} A Promise which resolves once the short rest workflow has completed + */ + async shortRest({dialog = true, chat = true, autoHD = false, autoHDThreshold = 3} = {}) { + // Take note of the initial hit points and number of hit dice the Actor has + const hp = this.data.data.attributes.hp; + const hd0 = this.data.data.attributes.hd; + const hp0 = hp.value; + let newDay = false; + + // Display a Dialog for rolling hit dice + if (dialog) { + try { + newDay = await ShortRestDialog.shortRestDialog({actor: this, canRoll: hd0 > 0}); + } catch (err) { + return; + } + } + + // Automatically spend hit dice + else if (autoHD) { + while (hp.value + autoHDThreshold <= hp.max) { + const r = await this.rollHitDie(undefined, {dialog: false}); + if (r === null) break; + } + } + + // Note the change in HP and HD and TP which occurred + const dhd = this.data.data.attributes.hd - hd0; + const dhp = this.data.data.attributes.hp.value - hp0; + const dtp = this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value; + + // Automatically Retore Tech Points + this.update({"data.attributes.tech.points.value": this.data.data.attributes.tech.points.max}); + + // Recover character resources + const updateData = {}; + for (let [k, r] of Object.entries(this.data.data.resources)) { + if (r.max && r.sr) { + updateData[`data.resources.${k}.value`] = r.max; + } + } + + // Recover item uses + const recovery = newDay ? ["sr", "day"] : ["sr"]; + const items = this.items.filter((item) => item.data.data.uses && recovery.includes(item.data.data.uses.per)); + const updateItems = items.map((item) => { + return { + "_id": item._id, + "data.uses.value": item.data.data.uses.max + }; + }); + await this.updateEmbeddedEntity("OwnedItem", updateItems); + + // Display a Chat Message summarizing the rest effects + 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) { + if (dtp !== 0) { + srMessage = "SW5E.ShortRestResultWithTech"; + } else { + srMessage = "SW5E.ShortRestResult"; + } + } else { + if (dtp !== 0) { + srMessage = "SW5E.ShortRestResultOnlyTech"; + } + } + + // Create a chat message + ChatMessage.create({ + user: game.user._id, + speaker: {actor: this, alias: this.name}, + flavor: restFlavor, + content: game.i18n.format(srMessage, {name: this.name, dice: -dhd, health: dhp, tech: dtp}) + }); + } + + // Return data summarizing the rest effects + return { + dhd: dhd, + dhp: dhp, + dtp: dtp, + updateData: updateData, + updateItems: updateItems, + newDay: newDay + }; + } + + /* -------------------------------------------- */ + + /** + * Take a long rest, recovering HP, HD, resources, Force and Power points 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, newDay = true} = {}) { + const data = this.data.data; + + // Maybe present a confirmation dialog + if (dialog) { + try { + newDay = await LongRestDialog.longRestDialog({actor: this}); + } catch (err) { + return; + } + } + + // Recover hit, tech, and force points to full, and eliminate any existing temporary HP, TP, and FP + const dhp = data.attributes.hp.max - data.attributes.hp.value; + const dtp = data.attributes.tech.points.max - data.attributes.tech.points.value; + const dfp = data.attributes.force.points.max - data.attributes.force.points.value; + const updateData = { + "data.attributes.hp.value": data.attributes.hp.max, + "data.attributes.hp.temp": 0, + "data.attributes.hp.tempmax": 0, + "data.attributes.tech.points.value": data.attributes.tech.points.max, + "data.attributes.tech.points.temp": 0, + "data.attributes.tech.points.tempmax": 0, + "data.attributes.force.points.value": data.attributes.force.points.max, + "data.attributes.force.points.temp": 0, + "data.attributes.force.points.tempmax": 0 + }; + + // Recover character resources + for (let [k, r] of Object.entries(data.resources)) { + if (r.max && (r.sr || r.lr)) { + updateData[`data.resources.${k}.value`] = r.max; + } + } + + // Recover power slots + for (let [k, v] of Object.entries(data.powers)) { + updateData[`data.powers.${k}.fvalue`] = Number.isNumeric(v.foverride) ? v.foverride : v.fmax ?? 0; + } + for (let [k, v] of Object.entries(data.powers)) { + updateData[`data.powers.${k}.tvalue`] = Number.isNumeric(v.toverride) ? v.toverride : v.tmax ?? 0; + } + // Determine the number of hit dice which may be recovered + let recoverHD = Math.max(Math.floor(data.details.level / 2), 1); + let dhd = 0; + + // Sort classes which can recover HD, assuming players prefer recovering larger HD first. + const updateItems = this.items + .filter((item) => item.data.type === "class") + .sort((a, b) => { + let da = parseInt(a.data.data.hitDice.slice(1)) || 0; + let db = parseInt(b.data.data.hitDice.slice(1)) || 0; + return db - da; + }) + .reduce((updates, item) => { + const d = item.data.data; + if (recoverHD > 0 && d.hitDiceUsed > 0) { + let delta = Math.min(d.hitDiceUsed || 0, recoverHD); + recoverHD -= delta; + dhd += delta; + updates.push({"_id": item.id, "data.hitDiceUsed": d.hitDiceUsed - delta}); + } + return updates; + }, []); + + // Iterate over owned items, restoring uses per day and recovering Hit Dice + const recovery = newDay ? ["sr", "lr", "day"] : ["sr", "lr"]; + for (let item of this.items) { + const d = item.data.data; + if (d.uses && recovery.includes(d.uses.per)) { + updateItems.push({"_id": item.id, "data.uses.value": d.uses.max}); + } else if (d.recharge && d.recharge.value) { + updateItems.push({"_id": item.id, "data.recharge.charged": true}); + } + } + + // Perform the updates + await this.update(updateData); + if (updateItems.length) 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(newDay ? "SW5E.LongRestOvernight" : "SW5E.LongRestNormal"); + break; + case "gritty": + restFlavor = game.i18n.localize("SW5E.LongRestGritty"); + break; + case "epic": + restFlavor = game.i18n.localize("SW5E.LongRestEpic"); + break; + } + + // Determine the chat message to display + if (chat) { + let lrMessage = "SW5E.LongRestResult"; + if (dhp !== 0) lrMessage += "HP"; + if (dfp !== 0) lrMessage += "FP"; + if (dtp !== 0) lrMessage += "TP"; + if (dhd !== 0) lrMessage += "HD"; + ChatMessage.create({ + user: game.user._id, + speaker: {actor: this, alias: this.name}, + flavor: restFlavor, + content: game.i18n.format(lrMessage, {name: this.name, health: dhp, tech: dtp, force: dfp, dice: dhd}) + }); + } + + // Return data summarizing the rest effects + return { + dhd: dhd, + dhp: dhp, + dtp: dtp, + dfp: dfp, + updateData: updateData, + updateItems: updateItems, + newDay: newDay + }; + } + + /* -------------------------------------------- */ + + /** + * Deploy an Actor into this one. + * + * @param {Actor} target The Actor to be deployed. + * @param {boolean} [coord] Deploy as Coordinator + * @param {boolean} [gunner] Deploy as Gunner + * @param {boolean} [mech] Deploy as Mechanic + * @param {boolean} [oper] Deploy as Operator + * @param {boolean} [pilot] Deploy as Pilot + * @param {boolean} [tech] Deploy as Technician + * @param {boolean} [crew] Deploy as Crew + * @param {boolean} [pass] Deploy as Passenger + */ + async deployInto( + target, + { + coord = false, + gunner = false, + mech = false, + oper = false, + pilot = false, + tech = false, + crew = false, + pass = false + } = {} + ) { + // Get the starship Actor data and the new char data + const sship = duplicate(this.toJSON()); + const ssDeploy = sship.data.attributes.deployment; + const char = target; + const charUUID = char.uuid; + const charName = char.data.name; + const charRank = char.data.data.attributes.rank; + let charProf = 0; + if (charRank.total > 0) { + charProf = char.data.data.attributes.prof; + } + + if (coord) { + ssDeploy.coord.uuid = charUUID; + ssDeploy.coord.name = charName; + ssDeploy.coord.rank = charRank.coord; + ssDeploy.coord.prof = charProf; + } + + if (gunner) { + ssDeploy.gunner.uuid = charUUID; + ssDeploy.gunner.name = charName; + ssDeploy.gunner.rank = charRank.gunner; + ssDeploy.gunner.prof = charProf; + } + + if (mech) { + ssDeploy.mechanic.uuid = charUUID; + ssDeploy.mechanic.name = charName; + ssDeploy.mechanic.rank = charRank.mechanic; + ssDeploy.mechanic.prof = charProf; + } + + if (oper) { + ssDeploy.operator.uuid = charUUID; + ssDeploy.operator.name = charName; + ssDeploy.operator.rank = charRank.operator; + ssDeploy.operator.prof = charProf; + } + + if (pilot) { + ssDeploy.pilot.uuid = charUUID; + ssDeploy.pilot.name = charName; + ssDeploy.pilot.rank = charRank.pilot; + ssDeploy.pilot.prof = charProf; + } + + if (tech) { + ssDeploy.technician.uuid = charUUID; + ssDeploy.technician.name = charName; + ssDeploy.technician.rank = charRank.technician; + ssDeploy.technician.prof = charProf; + } + + if (crew) { + ssDeploy.crew.push({uuid: charUUID, name: charName, rank: charRank, prof: charProf}); + } + + if (pass) { + ssDeploy.passenger.push({uuid: charUUID, name: charName, rank: charRank, prof: charProf}); + } + this.update({"data.attributes.deployment": ssDeploy}); + } + + /** + * Transform this Actor into another one. + * + * @param {Actor} target The target Actor. + * @param {boolean} [keepPhysical] Keep physical abilities (str, dex, con) + * @param {boolean} [keepMental] Keep mental abilities (int, wis, cha) + * @param {boolean} [keepSaves] Keep saving throw proficiencies + * @param {boolean} [keepSkills] Keep skill proficiencies + * @param {boolean} [mergeSaves] Take the maximum of the save proficiencies + * @param {boolean} [mergeSkills] Take the maximum of the skill proficiencies + * @param {boolean} [keepClass] Keep proficiency bonus + * @param {boolean} [keepFeats] Keep features + * @param {boolean} [keepPowers] Keep powers + * @param {boolean} [keepItems] Keep items + * @param {boolean} [keepBio] Keep biography + * @param {boolean} [keepVision] Keep vision + * @param {boolean} [transformTokens] Transform linked tokens too + */ + async transformInto( + target, + { + keepPhysical = false, + keepMental = false, + keepSaves = false, + keepSkills = false, + mergeSaves = false, + mergeSkills = false, + keepClass = false, + keepFeats = false, + keepPowers = false, + keepItems = false, + keepBio = false, + keepVision = false, + transformTokens = true + } = {} + ) { + // Ensure the player is allowed to polymorph + const allowed = game.settings.get("sw5e", "allowPolymorphing"); + if (!allowed && !game.user.isGM) { + return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphWarn")); + } + + // Get the original Actor data and the new source data + const o = duplicate(this.toJSON()); + o.flags.sw5e = o.flags.sw5e || {}; + o.flags.sw5e.transformOptions = {mergeSkills, mergeSaves}; + const source = duplicate(target.toJSON()); + + // Prepare new data to merge from the source + const d = { + type: o.type, // Remain the same actor type + name: `${o.name} (${source.name})`, // Append the new shape to your old name + data: source.data, // Get the data model of your new form + items: source.items, // Get the items of your new form + effects: o.effects.concat(source.effects), // Combine active effects from both forms + token: source.token, // New token configuration + img: source.img, // New appearance + permission: o.permission, // Use the original actor permissions + folder: o.folder, // Be displayed in the same sidebar folder + flags: o.flags // Use the original actor flags + }; + + // Additional adjustments + delete d.data.resources; // Don't change your resource pools + delete d.data.currency; // Don't lose currency + delete d.data.bonuses; // Don't lose global bonuses + delete d.token.actorId; // Don't reference the old actor ID + d.token.actorLink = o.token.actorLink; // Keep your actor link + d.token.name = d.name; // Token name same as actor name + d.data.details.alignment = o.data.details.alignment; // Don't change alignment + d.data.attributes.exhaustion = o.data.attributes.exhaustion; // Keep your prior exhaustion level + d.data.attributes.inspiration = o.data.attributes.inspiration; // Keep inspiration + d.data.powers = o.data.powers; // Keep power slots + + // Handle wildcard + if (source.token.randomImg) { + const images = await target.getTokenImages(); + d.token.img = images[Math.floor(Math.random() * images.length)]; + } + + // Keep Token configurations + const tokenConfig = ["displayName", "vision", "actorLink", "disposition", "displayBars", "bar1", "bar2"]; + if (keepVision) { + tokenConfig.push(...["dimSight", "brightSight", "dimLight", "brightLight", "vision", "sightAngle"]); + } + for (let c of tokenConfig) { + d.token[c] = o.token[c]; + } + + // Transfer ability scores + const abilities = d.data.abilities; + for (let k of Object.keys(abilities)) { + const oa = o.data.abilities[k]; + const prof = abilities[k].proficient; + if (keepPhysical && ["str", "dex", "con"].includes(k)) abilities[k] = oa; + else if (keepMental && ["int", "wis", "cha"].includes(k)) abilities[k] = oa; + if (keepSaves) abilities[k].proficient = oa.proficient; + else if (mergeSaves) abilities[k].proficient = Math.max(prof, oa.proficient); + } + + // Transfer skills + if (keepSkills) d.data.skills = o.data.skills; + else if (mergeSkills) { + for (let [k, s] of Object.entries(d.data.skills)) { + s.value = Math.max(s.value, o.data.skills[k].value); + } + } + + // Keep specific items from the original data + d.items = d.items.concat( + o.items.filter((i) => { + if (i.type === "class") return keepClass; + else if (i.type === "feat") return keepFeats; + else if (i.type === "power") return keepPowers; + else return keepItems; + }) + ); + + // Transfer classes for NPCs + if (!keepClass && d.data.details.cr) { + d.items.push({ + type: "class", + name: game.i18n.localize("SW5E.PolymorphTmpClass"), + data: {levels: d.data.details.cr} + }); + } + + // Keep biography + if (keepBio) d.data.details.biography = o.data.details.biography; + + // Keep senses + if (keepVision) d.data.traits.senses = o.data.traits.senses; + + // Set new data flags + if (!this.isPolymorphed || !d.flags.sw5e.originalActor) d.flags.sw5e.originalActor = this.id; + d.flags.sw5e.isPolymorphed = true; + + // Update unlinked Tokens in place since they can simply be re-dropped from the base actor + if (this.isToken) { + const tokenData = d.token; + tokenData.actorData = d; + delete tokenData.actorData.token; + return this.token.update(tokenData); + } + + // Update regular Actors by creating a new Actor with the Polymorphed data + await this.sheet.close(); + Hooks.callAll("sw5e.transformActor", this, target, d, { + keepPhysical, + keepMental, + keepSaves, + keepSkills, + mergeSaves, + mergeSkills, + keepClass, + keepFeats, + keepPowers, + keepItems, + keepBio, + keepVision, + transformTokens + }); + const newActor = await this.constructor.create(d, {renderSheet: true}); + + // Update placed Token instances + if (!transformTokens) return; + const tokens = this.getActiveTokens(true); + const updates = tokens.map((t) => { + const newTokenData = duplicate(d.token); + if (!t.data.actorLink) newTokenData.actorData = newActor.data; + newTokenData._id = t.data._id; + newTokenData.actorId = newActor.id; + return newTokenData; + }); + return canvas.scene?.updateEmbeddedEntity("Token", updates); + } + + /* -------------------------------------------- */ + + /** + * If this actor was transformed with transformTokens enabled, then its + * active tokens need to be returned to their original state. If not, then + * we can safely just delete this actor. + */ + async revertOriginalForm() { + if (!this.isPolymorphed) return; + if (!this.owner) { + return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphRevertWarn")); + } + + // If we are reverting an unlinked token, simply replace it with the base actor prototype + if (this.isToken) { + const baseActor = game.actors.get(this.token.data.actorId); + const prototypeTokenData = duplicate(baseActor.token); + prototypeTokenData.actorData = null; + return this.token.update(prototypeTokenData); + } + + // Obtain a reference to the original actor + const original = game.actors.get(this.getFlag("sw5e", "originalActor")); + if (!original) return; + + // Get the Tokens which represent this actor + if (canvas.ready) { + const tokens = this.getActiveTokens(true); + const tokenUpdates = tokens.map((t) => { + const tokenData = duplicate(original.data.token); + tokenData._id = t.id; + tokenData.actorId = original.id; + return tokenData; + }); + canvas.scene.updateEmbeddedEntity("Token", tokenUpdates); + } + + // Delete the polymorphed Actor and maybe re-render the original sheet + const isRendered = this.sheet.rendered; + if (game.user.isGM) await this.delete(); + original.sheet.render(isRendered); + return original; + } + + /* -------------------------------------------- */ + + /** + * Add additional system-specific sidebar directory context menu options for SW5e Actor entities + * @param {jQuery} html The sidebar HTML + * @param {Array} entryOptions The default array of context menu options + */ + static addDirectoryContextOptions(html, entryOptions) { + entryOptions.push({ + name: "SW5E.PolymorphRestoreTransformation", + icon: '', + callback: (li) => { + const actor = game.actors.get(li.data("entityId")); + return actor.revertOriginalForm(); + }, + condition: (li) => { + const allowed = game.settings.get("sw5e", "allowPolymorphing"); + if (!allowed && !game.user.isGM) return false; + const actor = game.actors.get(li.data("entityId")); + return actor && actor.isPolymorphed; + } + }); + } + + /* -------------------------------------------- */ + /* 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; + } + + /* -------------------------------------------- */ + + /** + * Cast a Power, consuming a power slot of a certain level + * @param {Item5e} item The power being cast by the actor + * @param {Event} event The originating user interaction which triggered the cast + * @deprecated since sw5e 1.2.0 + */ + async usePower(item, {configureDialog = true} = {}) { + console.warn(`The Actor5e#usePower method has been deprecated in favor of Item5e#roll`); + if (item.data.type !== "power") throw new Error("Wrong Item type"); + return item.roll(); + } +} diff --git a/module/actor/sheets/newSheet/base.js b/module/actor/sheets/newSheet/base.js index 938601b7..0b2136bc 100644 --- a/module/actor/sheets/newSheet/base.js +++ b/module/actor/sheets/newSheet/base.js @@ -5,7 +5,7 @@ import ActorHitDiceConfig from "../../../apps/hit-dice-config.js"; import ActorMovementConfig from "../../../apps/movement-config.js"; import ActorSensesConfig from "../../../apps/senses-config.js"; import ActorTypeConfig from "../../../apps/actor-type.js"; -import {SW5E} from '../../../config.js'; +import {SW5E} from "../../../config.js"; import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js"; /** @@ -14,963 +14,976 @@ import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effe * @extends {ActorSheet} */ export default class ActorSheet5e extends ActorSheet { - constructor(...args) { - super(...args); + constructor(...args) { + super(...args); + + /** + * Track the set of item filters which are applied + * @type {Set} + */ + this._filters = { + inventory: new Set(), + forcePowerbook: new Set(), + techPowerbook: new Set(), + features: new Set(), + effects: new Set() + }; + } + + /* -------------------------------------------- */ + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + scrollY: [ + ".inventory .group-list", + ".features .group-list", + ".force-powerbook .group-list", + ".tech-powerbook .group-list", + ".effects .effects-list" + ], + tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}] + }); + } + + /* -------------------------------------------- */ /** - * Track the set of item filters which are applied - * @type {Set} + * A set of item types that should be prevented from being dropped on this type of actor sheet. + * @type {Set} */ - this._filters = { - inventory: new Set(), - forcePowerbook: new Set(), - techPowerbook: new Set(), - features: new Set(), - effects: new Set() - }; - } + static unsupportedItemTypes = new Set(); - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - scrollY: [ - ".inventory .group-list", - ".features .group-list", - ".force-powerbook .group-list", - ".tech-powerbook .group-list", - ".effects .effects-list" - ], - tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}] - }); - } - - /* -------------------------------------------- */ - - /** - * A set of item types that should be prevented from being dropped on this type of actor sheet. - * @type {Set} - */ - static unsupportedItemTypes = new Set(); - - /* -------------------------------------------- */ - - /** @override */ - get template() { - if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html"; - return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`; - } - - /* -------------------------------------------- */ - - /** @override */ - getData(options) { - - // Basic data - let isOwner = this.actor.isOwner; - const data = { - owner: isOwner, - limited: this.actor.limited, - options: this.options, - editable: this.isEditable, - cssClass: isOwner ? "editable" : "locked", - isCharacter: this.actor.data.type === "character", - isNPC: this.actor.data.type === "npc", - isStarship: this.actor.data.type === "starship", - isVehicle: this.actor.data.type === 'vehicle', - config: CONFIG.SW5E, - rollData: this.actor.getRollData.bind(this.actor) - }; - - // The Actor's data - const actorData = this.actor.data.toObject(false); - data.actor = actorData; - data.data = actorData.data; - - // Owned Items - data.items = actorData.items; - for ( let i of data.items ) { - const item = this.actor.items.get(i._id); - i.labels = item.labels; - } - data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0)); - - // Labels and filters - data.labels = this.actor.labels || {}; - data.filters = this._filters; - - // Ability Scores - for ( let [a, abl] of Object.entries(actorData.data.abilities)) { - abl.icon = this._getProficiencyIcon(abl.proficient); - abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient]; - abl.label = CONFIG.SW5E.abilities[a]; + /** @override */ + get template() { + if (!game.user.isGM && this.actor.limited) + return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html"; + return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`; } - // Skills - if (actorData.data.skills) { - for ( let [s, skl] of Object.entries(actorData.data.skills)) { - skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability]; - skl.icon = this._getProficiencyIcon(skl.value); - skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value]; - if (data.actor.type === "starship") { - skl.label = CONFIG.SW5E.starshipSkills[s]; - }else{ - skl.label = CONFIG.SW5E.skills[s]; + /* -------------------------------------------- */ + + /** @override */ + getData(options) { + // Basic data + let isOwner = this.actor.isOwner; + const data = { + owner: isOwner, + limited: this.actor.limited, + options: this.options, + editable: this.isEditable, + cssClass: isOwner ? "editable" : "locked", + isCharacter: this.actor.type === "character", + isNPC: this.actor.type === "npc", + isStarship: this.actor.type === "starship", + isVehicle: this.actor.type === "vehicle", + config: CONFIG.SW5E, + rollData: this.actor.getRollData.bind(this.actor) + }; + + // The Actor's data + const actorData = this.actor.data.toObject(false); + data.actor = actorData; + data.data = actorData.data; + + // Owned Items + data.items = actorData.items; + for (let i of data.items) { + const item = this.actor.items.get(i._id); + i.labels = item.labels; } - } - } + data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0)); - // Movement speeds - data.movement = this._getMovementSpeed(actorData); + // Labels and filters + data.labels = this.actor.labels || {}; + data.filters = this._filters; - // Senses - data.senses = this._getSenses(actorData); - - // Update traits - this._prepareTraits(actorData.data.traits); - - // Prepare owned items - this._prepareItems(data); - - // Prepare active effects - data.effects = prepareActiveEffectCategories(this.actor.effects); - - // Return data to the sheet - return data - } - - /* -------------------------------------------- */ - - /** - * Prepare the display of movement speed data for the Actor* - * @param {object} actorData The Actor data being prepared. - * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk" - * @returns {{primary: string, special: string}} - * @private - */ - _getMovementSpeed(actorData, largestPrimary=false) { - const movement = actorData.data.attributes.movement || {}; - - // Prepare an array of available movement speeds - let speeds = [ - [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`], - [movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`], - [movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")], - [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`] - ] - if ( largestPrimary ) { - speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]); - } - - // Filter and sort speeds on their values - speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]); - - // Case 1: Largest as primary - if ( largestPrimary ) { - let primary = speeds.shift(); - return { - primary: `${primary ? primary[1] : "0"} ${movement.units}`, - special: speeds.map(s => s[1]).join(", ") - } - } - - // Case 2: Walk as primary - else { - return { - primary: `${movement.walk || 0} ${movement.units}`, - special: speeds.length ? speeds.map(s => s[1]).join(", ") : "" - } - } - } - - /* -------------------------------------------- */ - - _getSenses(actorData) { - const senses = actorData.data.attributes.senses || {}; - const tags = {}; - for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) { - const v = senses[k] ?? 0 - if ( v === 0 ) continue; - tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`; - } - if ( !!senses.special ) tags["special"] = senses.special; - return tags; - } - - /* -------------------------------------------- */ - - /** - * 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, - "di": CONFIG.SW5E.damageResistanceTypes, - "dv": CONFIG.SW5E.damageResistanceTypes, - "ci": CONFIG.SW5E.conditionTypes, - "languages": CONFIG.SW5E.languages, - "armorProf": CONFIG.SW5E.armorProficiencies, - "weaponProf": CONFIG.SW5E.weaponProficiencies, - "toolProf": CONFIG.SW5E.toolProficiencies - }; - for ( let [t, choices] of Object.entries(map) ) { - const trait = traits[t]; - if ( !trait ) continue; - let values = []; - if ( trait.value ) { - values = trait.value instanceof Array ? trait.value : [trait.value]; - } - trait.selected = values.reduce((obj, t) => { - obj[t] = choices[t]; - return obj; - }, {}); - - // Add custom entry - if ( trait.custom ) { - trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim()); - } - trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; - } - } - - /* -------------------------------------------- */ - - /** - * Insert a power into the powerbook object when rendering the character sheet - * @param {Object} data The Actor data being prepared - * @param {Array} powers The power data being prepared - * @param {string} school The school of the powerbook being prepared - * @private - */ - _preparePowerbook(data, powers, school) { - const owner = this.actor.isOwner; - const levels = data.data.powers; - const powerbook = {}; - - // Define some mappings - const sections = { - "atwill": -20, - "innate": -10, - }; - - // Label power slot uses headers - const useLabels = { - "-20": "-", - "-10": "-", - "0": "∞" - }; - - // Format a powerbook entry for a certain indexed level - const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => { - powerbook[i] = { - order: i, - label: label, - usesSlots: i > 0, - canCreate: owner, - canPrepare: (data.actor.type === "character") && (i >= 1), - powers: [], - 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, "school": school}, - prop: sl - }; - }; - - // Determine the maximum power level which has a slot - const maxLevel = Array.fromRange(10).reduce((max, i) => { - if ( i === 0 ) return max; - const level = levels[`power${i}`]; - if ( (level.max || level.override ) && ( i > max ) ) max = i; - return max; - }, 0); - - // 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++) { - const sl = `power${lvl}`; - registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]); - } - } - - // Iterate over every power item, adding powers to the powerbook by section - powers.forEach(power => { - const mode = power.data.preparation.mode || "prepared"; - let s = power.data.level || 0; - const sl = `power${s}`; - - // Specialized powercasting modes (if they exist) - if ( mode in sections ) { - s = sections[mode]; - if ( !powerbook[s] ){ - 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 - }); + // Ability Scores + for (let [a, abl] of Object.entries(actorData.data.abilities)) { + abl.icon = this._getProficiencyIcon(abl.proficient); + abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient]; + abl.label = CONFIG.SW5E.abilities[a]; } - } - // 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: levels[sl]}); - } - - // Add the power to the relevant heading - powerbook[s].powers.push(power); - }); - - // Sort the powerbook by section level - const sorted = Object.values(powerbook); - sorted.sort((a, b) => a.order - b.order); - return sorted; - } - - /* -------------------------------------------- */ - - /** - * Determine whether an Owned Item will be shown based on the current set of filters - * @return {boolean} - * @private - */ - _filterItems(items, filters) { - return items.filter(item => { - const data = item.data; - - // Action usage - for ( let f of ["action", "bonus", "reaction"] ) { - if ( filters.has(f) ) { - if ((data.activation && (data.activation.type !== f))) return false; + // Skills + if (actorData.data.skills) { + for (let [s, skl] of Object.entries(actorData.data.skills)) { + skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability]; + skl.icon = this._getProficiencyIcon(skl.value); + skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value]; + if (data.actor.type === "starship") { + skl.label = CONFIG.SW5E.starshipSkills[s]; + } else { + skl.label = CONFIG.SW5E.skills[s]; + } + } } - } - // Power-specific filters - if ( filters.has("ritual") ) { - if (data.components.ritual !== true) return false; - } - if ( filters.has("concentration") ) { - if (data.components.concentration !== true) return false; - } - if ( filters.has("prepared") ) { - if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true; - if ( this.actor.data.type === "npc" ) return true; - if ( this.actor.data.type === "starship" ) return true; - return data.preparation.prepared; - } + // Movement speeds + data.movement = this._getMovementSpeed(actorData); - // Equipment-specific filters - if ( filters.has("equipped") ) { - if ( data.equipped !== true ) return false; - } - return true; - }); - } + // Senses + data.senses = this._getSenses(actorData); - /* -------------------------------------------- */ + // Update traits + this._prepareTraits(actorData.data.traits); - /** - * Get the font-awesome icon used to display a certain level of skill proficiency - * @private - */ - _getProficiencyIcon(level) { - const icons = { - 0: '', - 0.5: '', - 1: '', - 2: '' - }; - return icons[level] || icons[0]; - } + // Prepare owned items + this._prepareItems(data); - /* -------------------------------------------- */ - /* Event Listeners and Handlers - /* -------------------------------------------- */ + // Prepare active effects + data.effects = prepareActiveEffectCategories(this.actor.effects); - /** @inheritdoc */ - activateListeners(html) { - - // Activate Item Filters - const filterLists = html.find(".filter-list"); - filterLists.each(this._initializeFilterItemList.bind(this)); - filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this)); - - // Item summaries - html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event)); - - // View Item Sheets - html.find('.item-edit').click(this._onItemEdit.bind(this)); - - // Editable Only Listeners - if ( this.isEditable ) { - - // Input focus and update - const inputs = html.find("input"); - inputs.focus(ev => ev.currentTarget.select()); - inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)); - - // Ability Proficiency - html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this)); - - // Toggle Skill Proficiency - html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this)); - - // Trait Selector - html.find('.trait-selector').click(this._onTraitSelector.bind(this)); - - // Configure Special Flags - html.find('.config-button').click(this._onConfigMenu.bind(this)); - - // Owned Item management - html.find('.item-create').click(this._onItemCreate.bind(this)); - html.find('.item-delete').click(this._onItemDelete.bind(this)); - html.find('.item-collapse').click(this._onItemCollapse.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)); - html.find('.increment-class-level').click(this._onIncrementClassLevel.bind(this)); - html.find('.decrement-class-level').click(this._onDecrementClassLevel.bind(this)); - - // Active Effect management - html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor)); + // Return data to the sheet + return data; } - // Owner Only Listeners - if ( this.actor.isOwner ) { + /* -------------------------------------------- */ - // Ability Checks - html.find('.ability-name').click(this._onRollAbilityTest.bind(this)); + /** + * Prepare the display of movement speed data for the Actor* + * @param {object} actorData The Actor data being prepared. + * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk" + * @returns {{primary: string, special: string}} + * @private + */ + _getMovementSpeed(actorData, largestPrimary = false) { + const movement = actorData.data.attributes.movement || {}; - - // Roll Skill Checks - html.find('.skill-name').click(this._onRollSkillCheck.bind(this)); - - // Item Rolling - html.find('.item .item-image').click(event => this._onItemRoll(event)); - html.find('.item .item-recharge').click(event => this._onItemRecharge(event)); - } - - // Otherwise remove rollable classes - else { - html.find(".rollable").each((i, el) => el.classList.remove("rollable")); - } - - // Handle default listeners last so system listeners are triggered first - super.activateListeners(html); - } - - /* -------------------------------------------- */ - - /** - * Iinitialize Item list filters by activating the set of filters which are currently applied - * @private - */ - _initializeFilterItemList(i, ul) { - const set = this._filters[ul.dataset.filter]; - const filters = ul.querySelectorAll(".filter-item"); - for ( let li of filters ) { - if ( set.has(li.dataset.filter) ) li.classList.add("active"); - } - } - - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ - - /** - * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs - * @param event - * @private - */ - _onChangeInputDelta(event) { - const input = event.target; - const value = input.value; - if ( ["+", "-"].includes(value[0]) ) { - let delta = parseFloat(value); - input.value = getProperty(this.actor.data, input.name) + delta; - } else if ( value[0] === "=" ) { - input.value = value.slice(1); - } - } - - /* -------------------------------------------- */ - - /** - * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options - * @param {Event} event The click event which originated the selection - * @private - */ - _onConfigMenu(event) { - event.preventDefault(); - const button = event.currentTarget; - let app; - switch ( button.dataset.action ) { - case "hit-dice": - app = new ActorHitDiceConfig(this.object); - break; - case "movement": - app = new ActorMovementConfig(this.object); - break; - case "flags": - app = new ActorSheetFlags(this.object); - break; - case "senses": - app = new ActorSensesConfig(this.object); - break; - case "type": - new ActorTypeConfig(this.object).render(true); - break; - } - app?.render(true); - } - - /* -------------------------------------------- */ - - /** - * Handle cycling proficiency in a Skill - * @param {Event} event A click or contextmenu event which triggered the handler - * @private - */ - _onCycleSkillProficiency(event) { - event.preventDefault(); - const field = $(event.currentTarget).siblings('input[type="hidden"]'); - - // Get the current level and the array of levels - const level = parseFloat(field.val()); - const levels = [0, 1, 0.5, 2]; - let idx = levels.indexOf(level); - - // Toggle next level - forward on click, backwards on right - if ( event.type === "click" ) { - field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]); - } else if ( event.type === "contextmenu" ) { - field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]); - } - - // Update the field value and save the form - this._onSubmit(event); - } - - /* -------------------------------------------- */ - - /** @override */ - async _onDropActor(event, data) { - const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get('sw5e', 'allowPolymorphing')); - if ( !canPolymorph ) return false; - - // Get the target actor - let sourceActor = null; - if (data.pack) { - const pack = game.packs.find(p => p.collection === data.pack); - sourceActor = await pack.getEntity(data.id); - } else { - sourceActor = game.actors.get(data.id); - } - if ( !sourceActor ) return; - - // Define a function to record polymorph settings for future use - const rememberOptions = html => { - const options = {}; - html.find('input').each((i, el) => { - options[el.name] = el.checked; - }); - const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options); - game.settings.set('sw5e', 'polymorphSettings', settings); - return settings; - }; - - // Create and render the Dialog - return new Dialog({ - title: game.i18n.localize('SW5E.PolymorphPromptTitle'), - content: { - options: game.settings.get('sw5e', 'polymorphSettings'), - i18n: SW5E.polymorphSettings, - isToken: this.actor.isToken - }, - default: 'accept', - buttons: { - accept: { - icon: '', - label: game.i18n.localize('SW5E.PolymorphAcceptSettings'), - callback: html => this.actor.transformInto(sourceActor, rememberOptions(html)) - }, - wildshape: { - icon: '', - label: game.i18n.localize('SW5E.PolymorphWildShape'), - callback: html => this.actor.transformInto(sourceActor, { - keepBio: true, - keepClass: true, - keepMental: true, - mergeSaves: true, - mergeSkills: true, - transformTokens: rememberOptions(html).transformTokens - }) - }, - polymorph: { - icon: '', - label: game.i18n.localize('SW5E.Polymorph'), - callback: html => this.actor.transformInto(sourceActor, { - transformTokens: rememberOptions(html).transformTokens - }) - }, - cancel: { - icon: '', - label: game.i18n.localize('Cancel') + // Prepare an array of available movement speeds + let speeds = [ + [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`], + [movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`], + [ + movement.fly, + `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "") + ], + [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`] + ]; + if (largestPrimary) { + speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]); } - } - }, { - classes: ['dialog', 'sw5e'], - width: 600, - template: 'systems/sw5e/templates/apps/polymorph-prompt.html' - }).render(true); - } - /* -------------------------------------------- */ + // Filter and sort speeds on their values + speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]); - /** @override */ - async _onDropItemCreate(itemData) { + // Case 1: Largest as primary + if (largestPrimary) { + let primary = speeds.shift(); + return { + primary: `${primary ? primary[1] : "0"} ${movement.units}`, + special: speeds.map((s) => s[1]).join(", ") + }; + } - // Check to make sure items of this type are allowed on this actor - if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) { - return ui.notifications.warn(game.i18n.format("SW5E.ActorWarningInvalidItem", { - itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]), - actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type]) - })); + // Case 2: Walk as primary + else { + return { + primary: `${movement.walk || 0} ${movement.units}`, + special: speeds.length ? speeds.map((s) => s[1]).join(", ") : "" + }; + } } - // Create a Consumable power scroll on the Inventory tab - if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) { - const scroll = await Item5e.createScrollFromPower(itemData); - itemData = scroll.data; + /* -------------------------------------------- */ + + _getSenses(actorData) { + const senses = actorData.data.attributes.senses || {}; + const tags = {}; + for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) { + const v = senses[k] ?? 0; + if (v === 0) continue; + tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`; + } + if (!!senses.special) tags["special"] = senses.special; + return tags; } - if ( itemData.data ) { - // Ignore certain statuses - ["equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]); + /* -------------------------------------------- */ - // Downgrade ATTUNED to REQUIRED - itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED); + /** + * 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, + di: CONFIG.SW5E.damageResistanceTypes, + dv: CONFIG.SW5E.damageResistanceTypes, + ci: CONFIG.SW5E.conditionTypes, + languages: CONFIG.SW5E.languages, + armorProf: CONFIG.SW5E.armorProficiencies, + weaponProf: CONFIG.SW5E.weaponProficiencies, + toolProf: CONFIG.SW5E.toolProficiencies + }; + for (let [t, choices] of Object.entries(map)) { + const trait = traits[t]; + if (!trait) continue; + let values = []; + if (trait.value) { + values = trait.value instanceof Array ? trait.value : [trait.value]; + } + trait.selected = values.reduce((obj, t) => { + obj[t] = choices[t]; + return obj; + }, {}); + + // Add custom entry + if (trait.custom) { + trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim())); + } + trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; + } } - // Stack identical consumables - if ( itemData.type === "consumable" && itemData.flags.core?.sourceId ) { - const similarItem = this.actor.items.find(i => { - const sourceId = i.getFlag("core", "sourceId"); - return sourceId && (sourceId === itemData.flags.core?.sourceId) && - (i.type === "consumable"); - }); - if ( similarItem ) { - return similarItem.update({ - 'data.quantity': similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1) + /* -------------------------------------------- */ + + /** + * Insert a power into the powerbook object when rendering the character sheet + * @param {Object} data The Actor data being prepared + * @param {Array} powers The power data being prepared + * @param {string} school The school of the powerbook being prepared + * @private + */ + _preparePowerbook(data, powers, school) { + const owner = this.actor.isOwner; + const levels = data.data.powers; + const powerbook = {}; + + // Define some mappings + const sections = { + atwill: -20, + innate: -10 + }; + + // Label power slot uses headers + const useLabels = { + "-20": "-", + "-10": "-", + "0": "∞" + }; + + // Format a powerbook entry for a certain indexed level + const registerSection = (sl, i, label, {prepMode = "prepared", value, max, override} = {}) => { + powerbook[i] = { + order: i, + label: label, + usesSlots: i > 0, + canCreate: owner, + canPrepare: data.actor.type === "character" && i >= 1, + powers: [], + 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, + "school": school + }, + prop: sl + }; + }; + + // Determine the maximum power level which has a slot + const maxLevel = Array.fromRange(10).reduce((max, i) => { + if (i === 0) return max; + const level = levels[`power${i}`]; + if ((level.max || level.override) && i > max) max = i; + return max; + }, 0); + + // 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++) { + const sl = `power${lvl}`; + registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]); + } + } + + // Iterate over every power item, adding powers to the powerbook by section + powers.forEach((power) => { + const mode = power.data.preparation.mode || "prepared"; + let s = power.data.level || 0; + const sl = `power${s}`; + + // Specialized powercasting modes (if they exist) + if (mode in sections) { + s = sections[mode]; + if (!powerbook[s]) { + 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 + }); + } + } + + // 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: levels[sl]}); + } + + // Add the power to the relevant heading + powerbook[s].powers.push(power); }); - } + + // Sort the powerbook by section level + const sorted = Object.values(powerbook); + sorted.sort((a, b) => a.order - b.order); + return sorted; } - // Create the owned item as normal - return super._onDropItemCreate(itemData); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Determine whether an Owned Item will be shown based on the current set of filters + * @return {boolean} + * @private + */ + _filterItems(items, filters) { + return items.filter((item) => { + const data = item.data; - /** - * Handle enabling editing for a power slot override value - * @param {MouseEvent} event The originating click event - * @private - */ - async _onPowerSlotOverride (event) { - const span = event.currentTarget.parentElement; - const level = span.dataset.level; - const override = this.actor.data.data.powers[level].override || span.dataset.slots; - const input = document.createElement("INPUT"); - input.type = "text"; - input.name = `data.powers.${level}.override`; - input.value = override; - input.placeholder = span.dataset.slots; - input.dataset.dtype = "Number"; + // Action usage + for (let f of ["action", "bonus", "reaction"]) { + if (filters.has(f)) { + if (data.activation && data.activation.type !== f) return false; + } + } - // Replace the HTML - const parent = span.parentElement; - parent.removeChild(span); - parent.appendChild(input); - } + // Power-specific filters + if (filters.has("ritual")) { + if (data.components.ritual !== true) return false; + } + if (filters.has("concentration")) { + if (data.components.concentration !== true) return false; + } + if (filters.has("prepared")) { + if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true; + if (this.actor.data.type === "npc") return true; + if (this.actor.data.type === "starship") return true; + return data.preparation.prepared; + } - /* -------------------------------------------- */ - - /** - * Change the uses amount of an Owned Item within the Actor - * @param {Event} event The triggering click event - * @private - */ - async _onUsesChange(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max); - event.target.value = uses; - return item.update({ 'data.uses.value': uses }); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method - * @private - */ - _onItemRoll(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - return item.roll(); - } - - /* -------------------------------------------- */ - - /** - * Handle attempting to recharge an item usage by rolling a recharge check - * @param {Event} event The originating click event - * @private - */ - _onItemRecharge(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - return item.rollRecharge(); - }; - - /* -------------------------------------------- */ - - /** - * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method - * @private - */ - _onItemSummary(event) { - event.preventDefault(); - let li = $(event.currentTarget).parents(".item"), - item = this.actor.items.get(li.data("item-id")), - chatData = item.getChatData({secrets: this.actor.isOwner}); - - // Toggle summary - if ( li.hasClass("expanded") ) { - let summary = li.children(".item-summary"); - summary.slideUp(200, () => summary.remove()); - } else { - let div = $(`
${chatData.description.value}
`); - let props = $(`
`); - chatData.properties.forEach(p => props.append(`${p}`)); - div.append(props); - li.append(div.hide()); - div.slideDown(200); + // Equipment-specific filters + if (filters.has("equipped")) { + if (data.equipped !== true) return false; + } + return true; + }); } - li.toggleClass("expanded"); - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset - * @param {Event} event The originating click event - * @private - */ - _onItemCreate(event) { - event.preventDefault(); - const header = event.currentTarget; - const type = header.dataset.type; - const itemData = { - name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}), - type: type, - data: foundry.utils.deepClone(header.dataset) - }; - delete itemData.data["type"]; - return this.actor.createEmbeddedDocuments("Item", [itemData]); - } - - /* -------------------------------------------- */ - - /** - * Handle editing an existing Owned Item for the Actor - * @param {Event} event The originating click event - * @private - */ - _onItemEdit(event) { - event.preventDefault(); - const li = event.currentTarget.closest(".item"); - const item = this.actor.items.get(li.dataset.itemId); - return item.sheet.render(true); - } - - /* -------------------------------------------- */ - - /** - * Handle deleting an existing Owned Item for the Actor - * @param {Event} event The originating click event - * @private - */ - _onItemDelete(event) { - event.preventDefault(); - const li = event.currentTarget.closest(".item"); - const item = this.actor.items.get(li.dataset.itemId); - if ( item ) return item.delete(); - } - - /** - * Handle collapsing a Feature row on the actor sheet - * @param {Event} event The originating click event - * @private - */ - -_onItemCollapse(event) { - event.preventDefault(); - - event.currentTarget.classList.toggle("active"); - - const li = event.currentTarget.closest("li"); - const content = li.querySelector(".content"); - - if (content.style.display === "none") { - content.style.display = "block"; - } else { - content.style.display = "none"; + /** + * Get the font-awesome icon used to display a certain level of skill proficiency + * @private + */ + _getProficiencyIcon(level) { + const icons = { + 0: '', + 0.5: '', + 1: '', + 2: '' + }; + return icons[level] || icons[0]; } - } -/** - * Handle incrementing class level on the actor sheet - * @param {Event} event The originating click event - * @private - */ - - _onIncrementClassLevel(event) { - event.preventDefault(); + /* -------------------------------------------- */ + /* Event Listeners and Handlers + /* -------------------------------------------- */ - const div = event.currentTarget.closest(".character") - const li = event.currentTarget.closest("li"); + /** @inheritdoc */ + activateListeners(html) { + // Activate Item Filters + const filterLists = html.find(".filter-list"); + filterLists.each(this._initializeFilterItemList.bind(this)); + filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this)); - const actorId = div.id.split("-")[1]; - const itemId = li.dataset.itemId; + // Item summaries + html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event)); - const actor = game.actors.get(actorId); - const item = actor.items.get(itemId); + // View Item Sheets + html.find(".item-edit").click(this._onItemEdit.bind(this)); - let levels = item.data.data.levels; - const update = {_id: item.data._id, data: {levels: (levels + 1) }}; + // Editable Only Listeners + if (this.isEditable) { + // Input focus and update + const inputs = html.find("input"); + inputs.focus((ev) => ev.currentTarget.select()); + inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)); - actor.updateEmbeddedDocuments("Item", [update]); -} - -/** - * Handle decrementing class level on the actor sheet - * @param {Event} event The originating click event - * @private - */ - - _onDecrementClassLevel(event) { - event.preventDefault(); - - const div = event.currentTarget.closest(".character") - const li = event.currentTarget.closest("li"); - - const actorId = div.id.split("-")[1]; - const itemId = li.dataset.itemId; - - const actor = game.actors.get(actorId); - const item = actor.items.get(itemId); - - let levels = item.data.data.levels; - const update = {_id: item.data._id, data: {levels: (levels - 1) }}; - - actor.updateEmbeddedDocuments("Item", [update]); -} - - /* -------------------------------------------- */ - - /** - * Handle rolling an Ability check, either a test or a saving throw - * @param {Event} event The originating click event - * @private - */ - _onRollAbilityTest(event) { - event.preventDefault(); - let ability = event.currentTarget.parentElement.dataset.ability; - return this.actor.rollAbility(ability, {event: event}); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling a Skill check - * @param {Event} event The originating click event - * @private - */ - _onRollSkillCheck(event) { - event.preventDefault(); - const skill = event.currentTarget.parentElement.dataset.skill; - return this.actor.rollSkill(skill, {event: event}); - } - - /* -------------------------------------------- */ - - /** - * Handle toggling Ability score proficiency level - * @param {Event} event The originating click event - * @private - */ - _onToggleAbilityProficiency(event) { - event.preventDefault(); - const field = event.currentTarget.previousElementSibling; - return this.actor.update({[field.name]: 1 - parseInt(field.value)}); - } - - /* -------------------------------------------- */ - - /** - * Handle toggling of filters to display a different set of owned items - * @param {Event} event The click event which triggered the toggle - * @private - */ - _onToggleFilter(event) { - event.preventDefault(); - const li = event.currentTarget; - const set = this._filters[li.parentElement.dataset.filter]; - const filter = li.dataset.filter; - if ( set.has(filter) ) set.delete(filter); - else set.add(filter); - return this.render(); - } - - /* -------------------------------------------- */ - - /** - * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options - * @param {Event} event The click event which originated the selection - * @private - */ - _onTraitSelector(event) { - event.preventDefault(); - const a = event.currentTarget; - const label = a.parentElement.querySelector("label"); - const choices = CONFIG.SW5E[a.dataset.options]; - const options = { name: a.dataset.target, title: label.innerText, choices }; - return new TraitSelector(this.actor, options).render(true) - } - - /* -------------------------------------------- */ - - /** @override */ - _getHeaderButtons() { - let buttons = super._getHeaderButtons(); - if (this.actor.isPolymorphed) { - buttons.unshift({ - label: 'SW5E.PolymorphRestoreTransformation', - class: "restore-transformation", - icon: "fas fa-backward", - onclick: () => this.actor.revertOriginalForm() - }); - } - return buttons; - } + // Ability Proficiency + html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this)); + + // Toggle Skill Proficiency + html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this)); + + // Trait Selector + html.find(".trait-selector").click(this._onTraitSelector.bind(this)); + + // Configure Special Flags + html.find(".config-button").click(this._onConfigMenu.bind(this)); + + // Owned Item management + html.find(".item-create").click(this._onItemCreate.bind(this)); + html.find(".item-delete").click(this._onItemDelete.bind(this)); + html.find(".item-collapse").click(this._onItemCollapse.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)); + html.find(".increment-class-level").click(this._onIncrementClassLevel.bind(this)); + html.find(".decrement-class-level").click(this._onDecrementClassLevel.bind(this)); + + // Active Effect management + html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor)); + } + + // Owner Only Listeners + if (this.actor.isOwner) { + // Ability Checks + html.find(".ability-name").click(this._onRollAbilityTest.bind(this)); + + // Roll Skill Checks + html.find(".skill-name").click(this._onRollSkillCheck.bind(this)); + + // Item Rolling + html.find(".item .item-image").click((event) => this._onItemRoll(event)); + html.find(".item .item-recharge").click((event) => this._onItemRecharge(event)); + } + + // Otherwise remove rollable classes + else { + html.find(".rollable").each((i, el) => el.classList.remove("rollable")); + } + + // Handle default listeners last so system listeners are triggered first + super.activateListeners(html); + } + + /* -------------------------------------------- */ + + /** + * Iinitialize Item list filters by activating the set of filters which are currently applied + * @private + */ + _initializeFilterItemList(i, ul) { + const set = this._filters[ul.dataset.filter]; + const filters = ul.querySelectorAll(".filter-item"); + for (let li of filters) { + if (set.has(li.dataset.filter)) li.classList.add("active"); + } + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** + * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs + * @param event + * @private + */ + _onChangeInputDelta(event) { + const input = event.target; + const value = input.value; + if (["+", "-"].includes(value[0])) { + let delta = parseFloat(value); + input.value = getProperty(this.actor.data, input.name) + delta; + } else if (value[0] === "=") { + input.value = value.slice(1); + } + } + + /* -------------------------------------------- */ + + /** + * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options + * @param {Event} event The click event which originated the selection + * @private + */ + _onConfigMenu(event) { + event.preventDefault(); + const button = event.currentTarget; + let app; + switch (button.dataset.action) { + case "hit-dice": + app = new ActorHitDiceConfig(this.object); + break; + case "movement": + app = new ActorMovementConfig(this.object); + break; + case "flags": + app = new ActorSheetFlags(this.object); + break; + case "senses": + app = new ActorSensesConfig(this.object); + break; + case "type": + new ActorTypeConfig(this.object).render(true); + break; + } + app?.render(true); + } + + /* -------------------------------------------- */ + + /** + * Handle cycling proficiency in a Skill + * @param {Event} event A click or contextmenu event which triggered the handler + * @private + */ + _onCycleSkillProficiency(event) { + event.preventDefault(); + const field = $(event.currentTarget).siblings('input[type="hidden"]'); + + // Get the current level and the array of levels + const level = parseFloat(field.val()); + const levels = [0, 1, 0.5, 2]; + let idx = levels.indexOf(level); + + // Toggle next level - forward on click, backwards on right + if (event.type === "click") { + field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]); + } else if (event.type === "contextmenu") { + field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]); + } + + // Update the field value and save the form + this._onSubmit(event); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropActor(event, data) { + const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing")); + if (!canPolymorph) return false; + + // Get the target actor + let sourceActor = null; + if (data.pack) { + const pack = game.packs.find((p) => p.collection === data.pack); + sourceActor = await pack.getEntity(data.id); + } else { + sourceActor = game.actors.get(data.id); + } + if (!sourceActor) return; + + // Define a function to record polymorph settings for future use + const rememberOptions = (html) => { + const options = {}; + html.find("input").each((i, el) => { + options[el.name] = el.checked; + }); + const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options); + game.settings.set("sw5e", "polymorphSettings", settings); + return settings; + }; + + // Create and render the Dialog + return new Dialog( + { + title: game.i18n.localize("SW5E.PolymorphPromptTitle"), + content: { + options: game.settings.get("sw5e", "polymorphSettings"), + i18n: SW5E.polymorphSettings, + isToken: this.actor.isToken + }, + default: "accept", + buttons: { + accept: { + icon: '', + label: game.i18n.localize("SW5E.PolymorphAcceptSettings"), + callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html)) + }, + wildshape: { + icon: '', + label: game.i18n.localize("SW5E.PolymorphWildShape"), + callback: (html) => + this.actor.transformInto(sourceActor, { + keepBio: true, + keepClass: true, + keepMental: true, + mergeSaves: true, + mergeSkills: true, + transformTokens: rememberOptions(html).transformTokens + }) + }, + polymorph: { + icon: '', + label: game.i18n.localize("SW5E.Polymorph"), + callback: (html) => + this.actor.transformInto(sourceActor, { + transformTokens: rememberOptions(html).transformTokens + }) + }, + cancel: { + icon: '', + label: game.i18n.localize("Cancel") + } + } + }, + { + classes: ["dialog", "sw5e"], + width: 600, + template: "systems/sw5e/templates/apps/polymorph-prompt.html" + } + ).render(true); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropItemCreate(itemData) { + // Check to make sure items of this type are allowed on this actor + if (this.constructor.unsupportedItemTypes.has(itemData.type)) { + return ui.notifications.warn( + game.i18n.format("SW5E.ActorWarningInvalidItem", { + itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]), + actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type]) + }) + ); + } + + // Create a Consumable power scroll on the Inventory tab + if (itemData.type === "power" && this._tabs[0].active === "inventory") { + const scroll = await Item5e.createScrollFromPower(itemData); + itemData = scroll.data; + } + + if (itemData.data) { + // Ignore certain statuses + ["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]); + + // Downgrade ATTUNED to REQUIRED + itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED); + } + + // Stack identical consumables + if (itemData.type === "consumable" && itemData.flags.core?.sourceId) { + const similarItem = this.actor.items.find((i) => { + const sourceId = i.getFlag("core", "sourceId"); + return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable"; + }); + if (similarItem && itemData.name !== "Power Cell") { + // Always create a new powercell instead of increasing quantity + return similarItem.update({ + "data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1) + }); + } + } + + // Create the owned item as normal + return super._onDropItemCreate(itemData); + } + + /* -------------------------------------------- */ + + /** + * Handle enabling editing for a power slot override value + * @param {MouseEvent} event The originating click event + * @private + */ + async _onPowerSlotOverride(event) { + const span = event.currentTarget.parentElement; + const level = span.dataset.level; + const override = this.actor.data.data.powers[level].override || span.dataset.slots; + const input = document.createElement("INPUT"); + input.type = "text"; + input.name = `data.powers.${level}.override`; + input.value = override; + input.placeholder = span.dataset.slots; + input.dataset.dtype = "Number"; + + // Replace the HTML + const parent = span.parentElement; + parent.removeChild(span); + parent.appendChild(input); + } + + /* -------------------------------------------- */ + + /** + * Change the uses amount of an Owned Item within the Actor + * @param {Event} event The triggering click event + * @private + */ + async _onUsesChange(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max); + event.target.value = uses; + return item.update({"data.uses.value": uses}); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method + * @private + */ + _onItemRoll(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + return item.roll(); + } + + /* -------------------------------------------- */ + + /** + * Handle attempting to recharge an item usage by rolling a recharge check + * @param {Event} event The originating click event + * @private + */ + _onItemRecharge(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + return item.rollRecharge(); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method + * @private + */ + _onItemSummary(event) { + event.preventDefault(); + let li = $(event.currentTarget).parents(".item"), + item = this.actor.items.get(li.data("item-id")), + chatData = item.getChatData({secrets: this.actor.isOwner}); + + // Toggle summary + if (li.hasClass("expanded")) { + let summary = li.children(".item-summary"); + summary.slideUp(200, () => summary.remove()); + } else { + let div = $(`
${chatData.description.value}
`); + let props = $(`
`); + chatData.properties.forEach((p) => props.append(`${p}`)); + div.append(props); + li.append(div.hide()); + div.slideDown(200); + } + li.toggleClass("expanded"); + } + + /* -------------------------------------------- */ + + /** + * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset + * @param {Event} event The originating click event + * @private + */ + _onItemCreate(event) { + event.preventDefault(); + const header = event.currentTarget; + const type = header.dataset.type; + const itemData = { + name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}), + type: type, + data: foundry.utils.deepClone(header.dataset) + }; + delete itemData.data["type"]; + return this.actor.createEmbeddedDocuments("Item", [itemData]); + } + + /* -------------------------------------------- */ + + /** + * Handle editing an existing Owned Item for the Actor + * @param {Event} event The originating click event + * @private + */ + _onItemEdit(event) { + event.preventDefault(); + const li = event.currentTarget.closest(".item"); + const item = this.actor.items.get(li.dataset.itemId); + return item.sheet.render(true); + } + + /* -------------------------------------------- */ + + /** + * Handle deleting an existing Owned Item for the Actor + * @param {Event} event The originating click event + * @private + */ + _onItemDelete(event) { + event.preventDefault(); + const li = event.currentTarget.closest(".item"); + const item = this.actor.items.get(li.dataset.itemId); + if (item) return item.delete(); + } + + /** + * Handle collapsing a Feature row on the actor sheet + * @param {Event} event The originating click event + * @private + */ + + _onItemCollapse(event) { + event.preventDefault(); + + event.currentTarget.classList.toggle("active"); + + const li = event.currentTarget.closest("li"); + const content = li.querySelector(".content"); + + if (content.style.display === "none") { + content.style.display = "block"; + } else { + content.style.display = "none"; + } + } + + /** + * Handle incrementing class level on the actor sheet + * @param {Event} event The originating click event + * @private + */ + + _onIncrementClassLevel(event) { + event.preventDefault(); + + const div = event.currentTarget.closest(".character"); + const li = event.currentTarget.closest("li"); + + const actorId = div.id.split("-")[1]; + const itemId = li.dataset.itemId; + + const actor = game.actors.get(actorId); + const item = actor.items.get(itemId); + + let levels = item.data.data.levels; + const update = {_id: item.data._id, data: {levels: levels + 1}}; + + actor.updateEmbeddedDocuments("Item", [update]); + } + + /** + * Handle decrementing class level on the actor sheet + * @param {Event} event The originating click event + * @private + */ + + _onDecrementClassLevel(event) { + event.preventDefault(); + + const div = event.currentTarget.closest(".character"); + const li = event.currentTarget.closest("li"); + + const actorId = div.id.split("-")[1]; + const itemId = li.dataset.itemId; + + const actor = game.actors.get(actorId); + const item = actor.items.get(itemId); + + let levels = item.data.data.levels; + const update = {_id: item.data._id, data: {levels: levels - 1}}; + + actor.updateEmbeddedDocuments("Item", [update]); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling an Ability check, either a test or a saving throw + * @param {Event} event The originating click event + * @private + */ + _onRollAbilityTest(event) { + event.preventDefault(); + let ability = event.currentTarget.parentElement.dataset.ability; + return this.actor.rollAbility(ability, {event: event}); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling a Skill check + * @param {Event} event The originating click event + * @private + */ + _onRollSkillCheck(event) { + event.preventDefault(); + const skill = event.currentTarget.parentElement.dataset.skill; + return this.actor.rollSkill(skill, {event: event}); + } + + /* -------------------------------------------- */ + + /** + * Handle toggling Ability score proficiency level + * @param {Event} event The originating click event + * @private + */ + _onToggleAbilityProficiency(event) { + event.preventDefault(); + const field = event.currentTarget.previousElementSibling; + return this.actor.update({[field.name]: 1 - parseInt(field.value)}); + } + + /* -------------------------------------------- */ + + /** + * Handle toggling of filters to display a different set of owned items + * @param {Event} event The click event which triggered the toggle + * @private + */ + _onToggleFilter(event) { + event.preventDefault(); + const li = event.currentTarget; + const set = this._filters[li.parentElement.dataset.filter]; + const filter = li.dataset.filter; + if (set.has(filter)) set.delete(filter); + else set.add(filter); + return this.render(); + } + + /* -------------------------------------------- */ + + /** + * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options + * @param {Event} event The click event which originated the selection + * @private + */ + _onTraitSelector(event) { + event.preventDefault(); + const a = event.currentTarget; + const label = a.parentElement.querySelector("label"); + const choices = CONFIG.SW5E[a.dataset.options]; + const options = {name: a.dataset.target, title: label.innerText, choices}; + return new TraitSelector(this.actor, options).render(true); + } + + /* -------------------------------------------- */ + + /** @override */ + _getHeaderButtons() { + let buttons = super._getHeaderButtons(); + if (this.actor.isPolymorphed) { + buttons.unshift({ + label: "SW5E.PolymorphRestoreTransformation", + class: "restore-transformation", + icon: "fas fa-backward", + onclick: () => this.actor.revertOriginalForm() + }); + } + return buttons; + } } diff --git a/module/actor/sheets/newSheet/character.js b/module/actor/sheets/newSheet/character.js index 58176a54..91c68b1d 100644 --- a/module/actor/sheets/newSheet/character.js +++ b/module/actor/sheets/newSheet/character.js @@ -7,246 +7,339 @@ import Actor5e from "../../entity.js"; * @type {ActorSheet5e} */ export default class ActorSheet5eCharacterNew extends ActorSheet5e { + get template() { + if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; + return "systems/sw5e/templates/actors/newActor/character-sheet.html"; + } + /** + * Define default rendering options for the NPC sheet + * @return {Object} + */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["swalt", "sw5e", "sheet", "actor", "character"], + blockFavTab: true, + subTabs: null, + width: 800, + tabs: [ + { + navSelector: ".root-tabs", + contentSelector: ".sheet-body", + initial: "attributes" + } + ] + }); + } - get template() { - if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; - return "systems/sw5e/templates/actors/newActor/character-sheet.html"; - } - /** - * Define default rendering options for the NPC sheet - * @return {Object} - */ - static get defaultOptions() { + /* -------------------------------------------- */ - return mergeObject(super.defaultOptions, { - classes: ["swalt", "sw5e", "sheet", "actor", "character"], - blockFavTab: true, - subTabs: null, - width: 800, - tabs: [{ - navSelector: ".root-tabs", - contentSelector: ".sheet-body", - initial: "attributes" - }], - }); - } + /** + * Add some extra data when rendering the sheet to reduce the amount of logic required within the template. + */ + getData() { + const sheetData = super.getData(); - /* -------------------------------------------- */ + // Temporary HP + let hp = sheetData.data.attributes.hp; + if (hp.temp === 0) delete hp.temp; + if (hp.tempmax === 0) delete hp.tempmax; - /** - * Add some extra data when rendering the sheet to reduce the amount of logic required within the template. - */ - getData() { - const sheetData = super.getData(); + // Resources + sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => { + const res = sheetData.data.resources[r] || {}; + res.name = r; + res.placeholder = game.i18n.localize("SW5E.Resource" + r.titleCase()); + if (res && res.value === 0) delete res.value; + if (res && res.max === 0) delete res.max; + return arr.concat([res]); + }, []); - // Temporary HP - let hp = sheetData.data.attributes.hp; - if (hp.temp === 0) delete hp.temp; - if (hp.tempmax === 0) delete hp.tempmax; + // Experience Tracking + sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); + sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", "); + sheetData["multiclassLabels"] = this.actor.itemTypes.class + .map((c) => { + return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" "); + }) + .join(", "); - // Resources - sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => { - const res = sheetData.data.resources[r] || {}; - res.name = r; - res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase()); - if (res && res.value === 0) delete res.value; - if (res && res.max === 0) delete res.max; - return arr.concat([res]); - }, []); + // Return data for rendering + return sheetData; + } - // Experience Tracking - sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); - sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", "); - sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => { - return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ') - }).join(', '); + /* -------------------------------------------- */ - // Return data for rendering - return sheetData; - } + /** + * Organize and classify Owned Items for Character sheets + * @private + */ + _prepareItems(data) { + // Categorize items as inventory, powerbook, features, and classes + const inventory = { + weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}}, + equipment: {label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"}}, + consumable: {label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"}}, + tool: {label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"}}, + backpack: {label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"}}, + loot: {label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"}} + }; - /* -------------------------------------------- */ + // Partition items by category + let [ + items, + forcepowers, + techpowers, + feats, + classes, + deployments, + deploymentfeatures, + ventures, + species, + archetypes, + classfeatures, + backgrounds, + fightingstyles, + fightingmasteries, + lightsaberforms + ] = data.items.reduce( + (arr, item) => { + // Item details + item.img = item.img || CONST.DEFAULT_TOKEN; + item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; + item.attunement = { + [CONFIG.SW5E.attunementTypes.REQUIRED]: { + icon: "fa-sun", + cls: "not-attuned", + title: "SW5E.AttunementRequired" + }, + [CONFIG.SW5E.attunementTypes.ATTUNED]: { + icon: "fa-sun", + cls: "attuned", + title: "SW5E.AttunementAttuned" + } + }[item.data.attunement]; - /** - * Organize and classify Owned Items for Character sheets - * @private - */ - _prepareItems(data) { + // Item usage + item.hasUses = item.data.uses && item.data.uses.max > 0; + item.isOnCooldown = + item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; + item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; + item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type); - // Categorize items as inventory, powerbook, features, and classes - const inventory = { - weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} }, - equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} }, - consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} }, - tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} }, - backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} }, - loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} } - }; + // Item toggle state + this._prepareItemToggleState(item); - // Partition items by category - let [items, forcepowers, techpowers, feats, classes, deployments, deploymentfeatures, ventures, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => { + // Primary Class + if (item.type === "class") + item.isOriginalClass = item._id === this.actor.data.data.details.originalClass; - // Item details - item.img = item.img || CONST.DEFAULT_TOKEN; - item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); - item.attunement = { - [CONFIG.SW5E.attunementTypes.REQUIRED]: { - icon: "fa-sun", - cls: "not-attuned", - title: "SW5E.AttunementRequired" - }, - [CONFIG.SW5E.attunementTypes.ATTUNED]: { - icon: "fa-sun", - cls: "attuned", - title: "SW5E.AttunementAttuned" + // Classify items into types + if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[1].push(item); + else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[2].push(item); + else if (item.type === "feat") arr[3].push(item); + else if (item.type === "class") arr[4].push(item); + else if (item.type === "deployment") arr[5].push(item); + else if (item.type === "deploymentfeature") arr[6].push(item); + else if (item.type === "venture") arr[7].push(item); + else if (item.type === "species") arr[8].push(item); + else if (item.type === "archetype") arr[9].push(item); + else if (item.type === "classfeature") arr[10].push(item); + else if (item.type === "background") arr[11].push(item); + else if (item.type === "fightingstyle") arr[12].push(item); + else if (item.type === "fightingmastery") arr[13].push(item); + else if (item.type === "lightsaberform") arr[14].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); + forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); + techpowers = this._filterItems(techpowers, this._filters.techPowerbook); + feats = this._filterItems(feats, this._filters.features); + + // Organize items + for (let i of items) { + i.data.quantity = i.data.quantity || 0; + i.data.weight = i.data.weight || 0; + i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1); + inventory[i.type].items.push(i); } - }[item.data.attunement]; - // Item usage - item.hasUses = item.data.uses && (item.data.uses.max > 0); - item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); - item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); - item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); + // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) + const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); + const techPowerbook = this._preparePowerbook(data, techpowers, "tec"); - // Item toggle state - this._prepareItemToggleState(item); + // Organize Features + const features = { + classes: { + label: "SW5E.ItemTypeClassPl", + items: [], + hasActions: false, + dataset: {type: "class"}, + isClass: true + }, + classfeatures: { + label: "SW5E.ItemTypeClassFeats", + items: [], + hasActions: true, + dataset: {type: "classfeature"}, + isClassfeature: true + }, + archetype: { + label: "SW5E.ItemTypeArchetype", + items: [], + hasActions: false, + dataset: {type: "archetype"}, + isArchetype: true + }, + deployments: { + label: "SW5E.ItemTypeDeploymentPl", + items: [], + hasActions: false, + dataset: {type: "deployment"}, + isDeployment: true + }, + deploymentfeatures: { + label: "SW5E.ItemTypeDeploymentFeaturePl", + items: [], + hasActions: true, + dataset: {type: "deploymentfeature"}, + isDeploymentfeature: true + }, + ventures: { + label: "SW5E.ItemTypeVenturePl", + items: [], + hasActions: false, + dataset: {type: "venture"}, + isVenture: true + }, + species: { + label: "SW5E.ItemTypeSpecies", + items: [], + hasActions: false, + dataset: {type: "species"}, + isSpecies: true + }, + background: { + label: "SW5E.ItemTypeBackground", + items: [], + hasActions: false, + dataset: {type: "background"}, + isBackground: true + }, + fightingstyles: { + label: "SW5E.ItemTypeFightingStylePl", + items: [], + hasActions: false, + dataset: {type: "fightingstyle"}, + isFightingstyle: true + }, + fightingmasteries: { + label: "SW5E.ItemTypeFightingMasteryPl", + items: [], + hasActions: false, + dataset: {type: "fightingmastery"}, + isFightingmastery: true + }, + lightsaberforms: { + label: "SW5E.ItemTypeLightsaberFormPl", + items: [], + hasActions: false, + dataset: {type: "lightsaberform"}, + isLightsaberform: true + }, + active: { + label: "SW5E.FeatureActive", + items: [], + hasActions: true, + dataset: {"type": "feat", "activation.type": "action"} + }, + passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}} + }; + for (let f of feats) { + if (f.data.activation.type) features.active.items.push(f); + else features.passive.items.push(f); + } + classes.sort((a, b) => b.data.levels - a.data.levels); + features.classes.items = classes; + features.classfeatures.items = classfeatures; + features.archetype.items = archetypes; + features.deployments.items = deployments; + features.deploymentfeatures.items = deploymentfeatures; + features.ventures.items = ventures; + features.species.items = species; + features.background.items = backgrounds; + features.fightingstyles.items = fightingstyles; + features.fightingmasteries.items = fightingmasteries; + features.lightsaberforms.items = lightsaberforms; - // Primary Class - if ( item.type === "class" ) item.isOriginalClass = ( item.data._id === this.actor.data.data.details.originalClass ); - - // Classify items into types - if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[1].push(item); - else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[2].push(item); - else if ( item.type === "feat" ) arr[3].push(item); - else if ( item.type === "class" ) arr[4].push(item); - else if ( item.type === "deployment" ) arr[5].push(item); - else if ( item.type === "deploymentfeature" ) arr[6].push(item); - else if ( item.type === "venture" ) arr[7].push(item); - else if ( item.type === "species" ) arr[8].push(item); - else if ( item.type === "archetype" ) arr[9].push(item); - else if ( item.type === "classfeature" ) arr[10].push(item); - else if ( item.type === "background" ) arr[11].push(item); - else if ( item.type === "fightingstyle" ) arr[12].push(item); - else if ( item.type === "fightingmastery" ) arr[13].push(item); - else if ( item.type === "lightsaberform" ) arr[14].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); - forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); - techpowers = this._filterItems(techpowers, this._filters.techPowerbook); - feats = this._filterItems(feats, this._filters.features); - - // Organize items - for ( let i of items ) { - i.data.quantity = i.data.quantity || 0; - i.data.weight = i.data.weight || 0; - i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10; - inventory[i.type].items.push(i); + // Assign and return + data.inventory = Object.values(inventory); + data.forcePowerbook = forcePowerbook; + data.techPowerbook = techPowerbook; + data.features = Object.values(features); } - // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) - const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); - const techPowerbook = this._preparePowerbook(data, techpowers, "tec"); + /* -------------------------------------------- */ - // Organize Features - const features = { - classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true }, - classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true }, - archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true }, - deployments: { label: "SW5E.ItemTypeDeploymentPl", items: [], hasActions: false, dataset: {type: "deployment"}, isDeployment: true }, - deploymentfeatures: { label: "SW5E.ItemTypeDeploymentFeaturePl", items: [], hasActions: true, dataset: {type: "deploymentfeature"}, isDeploymentfeature: true }, - ventures: { label: "SW5E.ItemTypeVenturePl", items: [], hasActions: false, dataset: {type: "venture"}, isVenture: true }, - species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true }, - background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true }, - fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true }, - fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true }, - lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true }, - active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, - passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} } - }; - for ( let f of feats ) { - if ( f.data.activation.type ) features.active.items.push(f); - else features.passive.items.push(f); + /** + * A helper method to establish the displayed preparation state for an item + * @param {Item} item + * @private + */ + _prepareItemToggleState(item) { + if (item.type === "power") { + const isAlways = getProperty(item.data, "preparation.mode") === "always"; + const isPrepared = getProperty(item.data, "preparation.prepared"); + item.toggleClass = isPrepared ? "active" : ""; + if (isAlways) item.toggleClass = "fixed"; + if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; + else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; + else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); + } else { + const isActive = getProperty(item.data, "equipped"); + item.toggleClass = isActive ? "active" : ""; + item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); + } } - classes.sort((a, b) => b.data.levels - a.data.levels); - features.classes.items = classes; - features.classfeatures.items = classfeatures; - features.archetype.items = archetypes; - features.deployments.items = deployments; - features.deploymentfeatures.items = deploymentfeatures; - features.ventures.items = ventures; - features.species.items = species; - features.background.items = backgrounds; - features.fightingstyles.items = fightingstyles; - features.fightingmasteries.items = fightingmasteries; - features.lightsaberforms.items = lightsaberforms; - - // Assign and return - data.inventory = Object.values(inventory); - data.forcePowerbook = forcePowerbook; - data.techPowerbook = techPowerbook; - data.features = Object.values(features); - } + /* -------------------------------------------- */ + /* Event Listeners and Handlers /* -------------------------------------------- */ - /** - * A helper method to establish the displayed preparation state for an item - * @param {Item} item - * @private - */ - _prepareItemToggleState(item) { - if (item.type === "power") { - const isAlways = getProperty(item.data, "preparation.mode") === "always"; - const isPrepared = getProperty(item.data, "preparation.prepared"); - item.toggleClass = isPrepared ? "active" : ""; - if ( isAlways ) item.toggleClass = "fixed"; - if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; - else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; - else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); - } - else { - const isActive = getProperty(item.data, "equipped"); - item.toggleClass = isActive ? "active" : ""; - item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); - } - } + /** + * Activate event listeners using the prepared sheet HTML + * @param html {jQuery} The prepared HTML object ready to be rendered into the DOM + */ + activateListeners(html) { + super.activateListeners(html); + if (!this.isEditable) return; - /* -------------------------------------------- */ - /* Event Listeners and Handlers - /* -------------------------------------------- */ + // Inventory Functions + // html.find(".currency-convert").click(this._onConvertCurrency.bind(this)); - /** - * Activate event listeners using the prepared sheet HTML - * @param html {jQuery} The prepared HTML object ready to be rendered into the DOM - */ - activateListeners(html) { - super.activateListeners(html); - if ( !this.isEditable ) return; + // Item State Toggling + html.find(".item-toggle").click(this._onToggleItem.bind(this)); - // Inventory Functions - // html.find(".currency-convert").click(this._onConvertCurrency.bind(this)); + // Short and Long Rest + html.find(".short-rest").click(this._onShortRest.bind(this)); + html.find(".long-rest").click(this._onLongRest.bind(this)); - // Item State Toggling - html.find('.item-toggle').click(this._onToggleItem.bind(this)); + // Rollable sheet actions + html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); - // Short and Long Rest - html.find('.short-rest').click(this._onShortRest.bind(this)); - html.find('.long-rest').click(this._onLongRest.bind(this)); - - // Rollable sheet actions - html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); - - // Send Languages to Chat onClick - html.find('[data-options="share-languages"]').click(event => { - event.preventDefault(); - let langs = this.actor.data.data.traits.languages.value.map(l => CONFIG.SW5E.languages[l] || l).join(", "); - let custom = this.actor.data.data.traits.languages.custom; - if (custom) langs += ", " + custom.replace(/;/g, ","); - let content = ` + // Send Languages to Chat onClick + html.find('[data-options="share-languages"]').click((event) => { + event.preventDefault(); + let langs = this.actor.data.data.traits.languages.value + .map((l) => CONFIG.SW5E.languages[l] || l) + .join(", "); + let custom = this.actor.data.data.traits.languages.custom; + if (custom) langs += ", " + custom.replace(/;/g, ","); + let content = `
@@ -256,404 +349,404 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
`; - // Send to Chat - let rollBlind = false; - let rollMode = game.settings.get("core", "rollMode"); - if (rollMode === "blindroll") rollBlind = true; - let data = { - user: game.user.data._id, - content: content, - blind: rollBlind, - speaker: { - actor: this.actor.data._id, - token: this.actor.token, - alias: this.actor.name - }, - type: CONST.CHAT_MESSAGE_TYPES.OTHER - }; + // Send to Chat + let rollBlind = false; + let rollMode = game.settings.get("core", "rollMode"); + if (rollMode === "blindroll") rollBlind = true; + let data = { + user: game.user.data._id, + content: content, + blind: rollBlind, + speaker: { + actor: this.actor.data._id, + token: this.actor.token, + alias: this.actor.name + }, + type: CONST.CHAT_MESSAGE_TYPES.OTHER + }; - if (["gmroll", "blindroll"].includes(rollMode)) data["whisper"] = ChatMessage.getWhisperRecipients("GM"); - else if (rollMode === "selfroll") data["whisper"] = [game.users.get(game.user.data._id)]; + if (["gmroll", "blindroll"].includes(rollMode)) data["whisper"] = ChatMessage.getWhisperRecipients("GM"); + else if (rollMode === "selfroll") data["whisper"] = [game.users.get(game.user.data._id)]; - ChatMessage.create(data); - }); + ChatMessage.create(data); + }); - // Item Delete Confirmation - html.find('.item-delete').off("click"); - html.find('.item-delete').click(event => { - let li = $(event.currentTarget).parents('.item'); - let itemId = li.attr("data-item-id"); - let item = this.actor.items.get(itemId); - new Dialog({ - title: `Deleting ${item.data.name}`, - content: `

Are you sure you want to delete ${item.data.name}?

`, - buttons: { - Yes: { - icon: '', - label: 'Yes', - callback: dlg => { - this.actor.deleteOwnedItem(itemId); - } - }, - cancel: { - icon: '', - label: 'No' - }, - }, - default: 'cancel' - }).render(true); - }); - } - - /* -------------------------------------------- */ - - /** - * Handle mouse click events for character sheet actions - * @param {MouseEvent} event The originating click event - * @private - */ - _onSheetAction(event) { - event.preventDefault(); - const button = event.currentTarget; - switch( button.dataset.action ) { - case "rollDeathSave": - return this.actor.rollDeathSave({event: event}); - case "rollInitiative": - return this.actor.rollInitiative({createCombatants: true}); + // Item Delete Confirmation + html.find(".item-delete").off("click"); + html.find(".item-delete").click((event) => { + let li = $(event.currentTarget).parents(".item"); + let itemId = li.attr("data-item-id"); + let item = this.actor.items.get(itemId); + new Dialog({ + title: `Deleting ${item.data.name}`, + content: `

Are you sure you want to delete ${item.data.name}?

`, + buttons: { + Yes: { + icon: '', + label: "Yes", + callback: (dlg) => { + this.actor.deleteOwnedItem(itemId); + } + }, + cancel: { + icon: '', + label: "No" + } + }, + default: "cancel" + }).render(true); + }); } - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - - /** - * Handle toggling the state of an Owned Item within the Actor - * @param {Event} event The triggering click event - * @private - */ - _onToggleItem(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; - return item.update({[attr]: !getProperty(item.data, attr)}); - } - - /* -------------------------------------------- */ - - /** - * Take a short rest, calling the relevant function on the Actor instance - * @param {Event} event The triggering click event - * @private - */ - async _onShortRest(event) { - event.preventDefault(); - await this._onSubmit(event); - return this.actor.shortRest(); - } - - /* -------------------------------------------- */ - - /** - * Take a long rest, calling the relevant function on the Actor instance - * @param {Event} event The triggering click event - * @private - */ - async _onLongRest(event) { - event.preventDefault(); - await this._onSubmit(event); - return this.actor.longRest(); - } - - /* -------------------------------------------- */ - - /** @override */ - async _onDropItemCreate(itemData) { - - // Increment the number of class levels of a character instead of creating a new item - if ( itemData.type === "class" ) { - const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name); - let priorLevel = cls?.data.data.levels ?? 0; - if ( !!cls ) { - const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); - if ( next > priorLevel ) { - itemData.levels = next; - return cls.update({"data.levels": next}); + /** + * Handle mouse click events for character sheet actions + * @param {MouseEvent} event The originating click event + * @private + */ + _onSheetAction(event) { + event.preventDefault(); + const button = event.currentTarget; + switch (button.dataset.action) { + case "rollDeathSave": + return this.actor.rollDeathSave({event: event}); + case "rollInitiative": + return this.actor.rollInitiative({createCombatants: true}); } - } } - // Increment the number of deployment ranks of a character instead of creating a new item - // else if ( itemData.type === "deployment" ) { - // const rnk = this.actor.itemTypes.deployment.find(c => c.name === itemData.name); - // let priorRank = rnk?.data.data.ranks ?? 0; - // if ( !!rnk ) { - // const next = Math.min(priorLevel + 1, 5 + priorRank - this.actor.data.data.details.rank); - // if ( next > priorRank ) { - // itemData.ranks = next; - // return rnk.update({"data.ranks": next}); - // } - // } - // } + /* -------------------------------------------- */ - // Default drop handling if levels were not added - return super._onDropItemCreate(itemData); - } + /** + * Handle toggling the state of an Owned Item within the Actor + * @param {Event} event The triggering click event + * @private + */ + _onToggleItem(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; + return item.update({[attr]: !getProperty(item.data, attr)}); + } + + /* -------------------------------------------- */ + + /** + * Take a short rest, calling the relevant function on the Actor instance + * @param {Event} event The triggering click event + * @private + */ + async _onShortRest(event) { + event.preventDefault(); + await this._onSubmit(event); + return this.actor.shortRest(); + } + + /* -------------------------------------------- */ + + /** + * Take a long rest, calling the relevant function on the Actor instance + * @param {Event} event The triggering click event + * @private + */ + async _onLongRest(event) { + event.preventDefault(); + await this._onSubmit(event); + return this.actor.longRest(); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropItemCreate(itemData) { + // Increment the number of class levels of a character instead of creating a new item + if (itemData.type === "class") { + const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name); + let priorLevel = cls?.data.data.levels ?? 0; + if (!!cls) { + const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); + if (next > priorLevel) { + itemData.levels = next; + return cls.update({"data.levels": next}); + } + } + } + + // Increment the number of deployment ranks of a character instead of creating a new item + // else if ( itemData.type === "deployment" ) { + // const rnk = this.actor.itemTypes.deployment.find(c => c.name === itemData.name); + // let priorRank = rnk?.data.data.ranks ?? 0; + // if ( !!rnk ) { + // const next = Math.min(priorLevel + 1, 5 + priorRank - this.actor.data.data.details.rank); + // if ( next > priorRank ) { + // itemData.ranks = next; + // return rnk.update({"data.ranks": next}); + // } + // } + // } + + // Default drop handling if levels were not added + return super._onDropItemCreate(itemData); + } } async function addFavorites(app, html, data) { - // Thisfunction is adapted for the SwaltSheet from the Favorites Item - // Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord). - // It is licensed under a Creative Commons Attribution 4.0 International License - // and can be found at https://github.com/syl3r86/favtab. - let favItems = []; - let favFeats = []; - let favPowers = { - 0: { - isCantrip: true, - powers: [] - }, - 1: { - powers: [], - value: data.actor.data.powers.power1.value, - max: data.actor.data.powers.power1.max - }, - 2: { - powers: [], - value: data.actor.data.powers.power2.value, - max: data.actor.data.powers.power2.max - }, - 3: { - powers: [], - value: data.actor.data.powers.power3.value, - max: data.actor.data.powers.power3.max - }, - 4: { - powers: [], - value: data.actor.data.powers.power4.value, - max: data.actor.data.powers.power4.max - }, - 5: { - powers: [], - value: data.actor.data.powers.power5.value, - max: data.actor.data.powers.power5.max - }, - 6: { - powers: [], - value: data.actor.data.powers.power6.value, - max: data.actor.data.powers.power6.max - }, - 7: { - powers: [], - value: data.actor.data.powers.power7.value, - max: data.actor.data.powers.power7.max - }, - 8: { - powers: [], - value: data.actor.data.powers.power8.value, - max: data.actor.data.powers.power8.max - }, - 9: { - powers: [], - value: data.actor.data.powers.power9.value, - max: data.actor.data.powers.power9.max - } - } + // Thisfunction is adapted for the SwaltSheet from the Favorites Item + // Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord). + // It is licensed under a Creative Commons Attribution 4.0 International License + // and can be found at https://github.com/syl3r86/favtab. + let favItems = []; + let favFeats = []; + let favPowers = { + 0: { + isCantrip: true, + powers: [] + }, + 1: { + powers: [], + value: data.actor.data.powers.power1.value, + max: data.actor.data.powers.power1.max + }, + 2: { + powers: [], + value: data.actor.data.powers.power2.value, + max: data.actor.data.powers.power2.max + }, + 3: { + powers: [], + value: data.actor.data.powers.power3.value, + max: data.actor.data.powers.power3.max + }, + 4: { + powers: [], + value: data.actor.data.powers.power4.value, + max: data.actor.data.powers.power4.max + }, + 5: { + powers: [], + value: data.actor.data.powers.power5.value, + max: data.actor.data.powers.power5.max + }, + 6: { + powers: [], + value: data.actor.data.powers.power6.value, + max: data.actor.data.powers.power6.max + }, + 7: { + powers: [], + value: data.actor.data.powers.power7.value, + max: data.actor.data.powers.power7.max + }, + 8: { + powers: [], + value: data.actor.data.powers.power8.value, + max: data.actor.data.powers.power8.max + }, + 9: { + powers: [], + value: data.actor.data.powers.power9.value, + max: data.actor.data.powers.power9.max + } + }; - let powerCount = 0 - let items = data.actor.items; - for (let item of items) { - if (item.type == "class") continue; - if (item.flags.favtab === undefined || item.flags.favtab.isFavourite === undefined) { - item.flags.favtab = { - isFavourite: false - }; + let powerCount = 0; + let items = data.actor.items; + for (let item of items) { + if (item.type == "class") continue; + if (item.flags.favtab === undefined || item.flags.favtab.isFavourite === undefined) { + item.flags.favtab = { + isFavourite: false + }; + } + let isFav = item.flags.favtab.isFavourite; + if (app.options.editable) { + let favBtn = $( + `` + ); + favBtn.click((ev) => { + app.actor.items.get(item.data._id).update({ + "flags.favtab.isFavourite": !item.flags.favtab.isFavourite + }); + }); + html.find(`.item[data-item-id="${item.data._id}"]`).find(".item-controls").prepend(favBtn); + } + + if (isFav) { + item.powerComps = ""; + if (item.data.components) { + let comps = item.data.components; + let v = comps.vocal ? "V" : ""; + let s = comps.somatic ? "S" : ""; + let m = comps.material ? "M" : ""; + let c = !!comps.concentration; + let r = !!comps.ritual; + item.powerComps = `${v}${s}${m}`; + item.powerCon = c; + item.powerRit = r; + } + + item.editable = app.options.editable; + switch (item.type) { + case "feat": + if (item.flags.favtab.sort === undefined) { + item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present + } + favFeats.push(item); + break; + case "power": + if (item.data.preparation.mode) { + item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})`; + } + if (item.data.level) { + favPowers[item.data.level].powers.push(item); + } else { + favPowers[0].powers.push(item); + } + powerCount++; + break; + default: + if (item.flags.favtab.sort === undefined) { + item.flags.favtab.sort = (favItems.count + 1) * 100000; // initial sort key if not present + } + favItems.push(item); + break; + } + } } - let isFav = item.flags.favtab.isFavourite; + + // Alter core CSS to fit new button + // if (app.options.editable) { + // html.find('.powerbook .item-controls').css('flex', '0 0 88px'); + // html.find('.inventory .item-controls, .features .item-controls').css('flex', '0 0 90px'); + // html.find('.favourite .item-controls').css('flex', '0 0 22px'); + // } + + let tabContainer = html.find(".favtabtarget"); + data.favItems = favItems.length > 0 ? favItems.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false; + data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false; + data.favPowers = powerCount > 0 ? favPowers : false; + data.editable = app.options.editable; + + await loadTemplates(["systems/sw5e/templates/actors/newActor/item.hbs"]); + let favtabHtml = $(await renderTemplate("systems/sw5e/templates/actors/newActor/template.hbs", data)); + favtabHtml.find(".item-name h4").click((event) => app._onItemSummary(event)); + if (app.options.editable) { - let favBtn = $(``); - favBtn.click(ev => { - app.actor.items.get(item.data._id).update({ - "flags.favtab.isFavourite": !item.flags.favtab.isFavourite + favtabHtml.find(".item-image").click((ev) => app._onItemRoll(ev)); + let handler = (ev) => app._onDragStart(ev); + favtabHtml.find(".item").each((i, li) => { + if (li.classList.contains("inventory-header")) return; + li.setAttribute("draggable", true); + li.addEventListener("dragstart", handler, false); + }); + //favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event)); + favtabHtml.find(".item-edit").click((ev) => { + let itemId = $(ev.target).parents(".item")[0].dataset.itemId; + app.actor.items.get(itemId).sheet.render(true); + }); + favtabHtml.find(".item-fav").click((ev) => { + let itemId = $(ev.target).parents(".item")[0].dataset.itemId; + let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite; + app.actor.items.get(itemId).update({ + "flags.favtab.isFavourite": val + }); + }); + + // Sorting + favtabHtml.find(".item").on("drop", (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData("text/plain")); + // if (dropData.actorId !== app.actor.id || dropData.data.type === 'power') return; + if (dropData.actorId !== app.actor.id) return; + let list = null; + if (dropData.data.type === "feat") list = favFeats; + else list = favItems; + let dragSource = list.find((i) => i.data._id === dropData.data._id); + let siblings = list.filter((i) => i.data._id !== dropData.data._id); + let targetId = ev.target.closest(".item").dataset.itemId; + let dragTarget = siblings.find((s) => s.data._id === targetId); + + if (dragTarget === undefined) return; + const sortUpdates = SortingHelpers.performIntegerSort(dragSource, { + target: dragTarget, + siblings: siblings, + sortKey: "flags.favtab.sort" + }); + const updateData = sortUpdates.map((u) => { + const update = u.update; + update._id = u.target.data._id; + return update; + }); + app.actor.updateEmbeddedEntity("OwnedItem", updateData); }); - }); - html.find(`.item[data-item-id="${item.data._id}"]`).find('.item-controls').prepend(favBtn); } - - if (isFav) { - item.powerComps = ""; - if (item.data.components) { - let comps = item.data.components; - let v = (comps.vocal) ? "V" : ""; - let s = (comps.somatic) ? "S" : ""; - let m = (comps.material) ? "M" : ""; - let c = !!(comps.concentration); - let r = !!(comps.ritual); - item.powerComps = `${v}${s}${m}`; - item.powerCon = c; - item.powerRit = r; - } - - item.editable = app.options.editable; - switch (item.type) { - case 'feat': - if (item.flags.favtab.sort === undefined) { - item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present - } - favFeats.push(item); - break; - case 'power': - if (item.data.preparation.mode) { - item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})` - } - if (item.data.level) { - favPowers[item.data.level].powers.push(item); - } else { - favPowers[0].powers.push(item); - } - powerCount++; - break; - default: - if (item.flags.favtab.sort === undefined) { - item.flags.favtab.sort = (favItems.count + 1) * 100000; // initial sort key if not present - } - favItems.push(item); - break; - } - } - } - - // Alter core CSS to fit new button - // if (app.options.editable) { - // html.find('.powerbook .item-controls').css('flex', '0 0 88px'); - // html.find('.inventory .item-controls, .features .item-controls').css('flex', '0 0 90px'); - // html.find('.favourite .item-controls').css('flex', '0 0 22px'); - // } - - let tabContainer = html.find('.favtabtarget'); - data.favItems = favItems.length > 0 ? favItems.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false; - data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false; - data.favPowers = powerCount > 0 ? favPowers : false; - data.editable = app.options.editable; - - await loadTemplates(['systems/sw5e/templates/actors/newActor/item.hbs']); - let favtabHtml = $(await renderTemplate('systems/sw5e/templates/actors/newActor/template.hbs', data)); - favtabHtml.find('.item-name h4').click(event => app._onItemSummary(event)); - - if (app.options.editable) { - favtabHtml.find('.item-image').click(ev => app._onItemRoll(ev)); - let handler = ev => app._onDragStart(ev); - favtabHtml.find('.item').each((i, li) => { - if (li.classList.contains("inventory-header")) return; - li.setAttribute("draggable", true); - li.addEventListener("dragstart", handler, false); - }); - //favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event)); - favtabHtml.find('.item-edit').click(ev => { - let itemId = $(ev.target).parents('.item')[0].dataset.itemId; - app.actor.items.get(itemId).sheet.render(true); - }); - favtabHtml.find('.item-fav').click(ev => { - let itemId = $(ev.target).parents('.item')[0].dataset.itemId; - let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite - app.actor.items.get(itemId).update({ - "flags.favtab.isFavourite": val - }); - }); - - // Sorting - favtabHtml.find('.item').on('drop', ev => { - ev.preventDefault(); - ev.stopPropagation(); - - let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData('text/plain')); - // if (dropData.actorId !== app.actor.id || dropData.data.type === 'power') return; - if (dropData.actorId !== app.actor.id) return; - let list = null; - if (dropData.data.type === 'feat') list = favFeats; - else list = favItems; - let dragSource = list.find(i => i.data._id === dropData.data._id); - let siblings = list.filter(i => i.data._id !== dropData.data._id); - let targetId = ev.target.closest('.item').dataset.itemId; - let dragTarget = siblings.find(s => s.data._id === targetId); - - if (dragTarget === undefined) return; - const sortUpdates = SortingHelpers.performIntegerSort(dragSource, { - target: dragTarget, - siblings: siblings, - sortKey: 'flags.favtab.sort' - }); - const updateData = sortUpdates.map(u => { - const update = u.update; - update._id = u.target.data._id; - return update; - }); - app.actor.updateEmbeddedEntity("OwnedItem", updateData); - }); - } - tabContainer.append(favtabHtml); - // if(app.options.editable) { - // let handler = ev => app._onDragItemStart(ev); - // tabContainer.find('.item').each((i, li) => { - // if (li.classList.contains("inventory-header")) return; - // li.setAttribute("draggable", true); - // li.addEventListener("dragstart", handler, false); - // }); - //} - // try { - // if (game.modules.get("betterrolls5e") && game.modules.get("betterrolls5e").active) BetterRolls.addItemContent(app.object, favtabHtml, ".item .item-name h4", ".item-properties", ".item > .rollable div"); - // } - // catch (err) { - // // Better Rolls not found! - // } - Hooks.callAll("renderedSwaltSheet", app, html, data); + tabContainer.append(favtabHtml); + // if(app.options.editable) { + // let handler = ev => app._onDragItemStart(ev); + // tabContainer.find('.item').each((i, li) => { + // if (li.classList.contains("inventory-header")) return; + // li.setAttribute("draggable", true); + // li.addEventListener("dragstart", handler, false); + // }); + //} + // try { + // if (game.modules.get("betterrolls5e") && game.modules.get("betterrolls5e").active) BetterRolls.addItemContent(app.object, favtabHtml, ".item .item-name h4", ".item-properties", ".item > .rollable div"); + // } + // catch (err) { + // // Better Rolls not found! + // } + Hooks.callAll("renderedSwaltSheet", app, html, data); } async function addSubTabs(app, html, data) { - if(data.options.subTabs == null) { - //let subTabs = []; //{subgroup: '', target: '', active: false} - data.options.subTabs = {}; - html.find('[data-subgroup-selection] [data-subgroup]').each((idx, el) => { - let subgroup = el.getAttribute('data-subgroup'); - let target = el.getAttribute('data-target'); - let targetObj = {target: target, active: el.classList.contains("active")} - if(data.options.subTabs.hasOwnProperty(subgroup)) { - data.options.subTabs[subgroup].push(targetObj); - } else { - data.options.subTabs[subgroup] = []; - data.options.subTabs[subgroup].push(targetObj); - } - }) - } - - for(const group in data.options.subTabs) { - data.options.subTabs[group].forEach(tab => { - if(tab.active) { - html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass('active'); - } else { - html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass('active'); - } - }) - } - - html.find('[data-subgroup-selection]').children().on('click', event => { - let subgroup = event.target.closest('[data-subgroup]').getAttribute('data-subgroup'); - let target = event.target.closest('[data-target]').getAttribute('data-target'); - html.find(`[data-subgroup=${subgroup}]`).removeClass('active'); - html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass('active'); - let tabId = data.options.subTabs[subgroup].find(tab => { - return tab.target == target - }); - data.options.subTabs[subgroup].map(el => { - el.active = el.target == target; - return el; - }) - - }) - + if (data.options.subTabs == null) { + //let subTabs = []; //{subgroup: '', target: '', active: false} + data.options.subTabs = {}; + html.find("[data-subgroup-selection] [data-subgroup]").each((idx, el) => { + let subgroup = el.getAttribute("data-subgroup"); + let target = el.getAttribute("data-target"); + let targetObj = {target: target, active: el.classList.contains("active")}; + if (data.options.subTabs.hasOwnProperty(subgroup)) { + data.options.subTabs[subgroup].push(targetObj); + } else { + data.options.subTabs[subgroup] = []; + data.options.subTabs[subgroup].push(targetObj); + } + }); + } + for (const group in data.options.subTabs) { + data.options.subTabs[group].forEach((tab) => { + if (tab.active) { + html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass("active"); + } else { + html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass("active"); + } + }); + } + html.find("[data-subgroup-selection]") + .children() + .on("click", (event) => { + let subgroup = event.target.closest("[data-subgroup]").getAttribute("data-subgroup"); + let target = event.target.closest("[data-target]").getAttribute("data-target"); + html.find(`[data-subgroup=${subgroup}]`).removeClass("active"); + html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass("active"); + let tabId = data.options.subTabs[subgroup].find((tab) => { + return tab.target == target; + }); + data.options.subTabs[subgroup].map((el) => { + el.active = el.target == target; + return el; + }); + }); } Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => { - addFavorites(app, html, data); - addSubTabs(app, html, data); -}); \ No newline at end of file + addFavorites(app, html, data); + addSubTabs(app, html, data); +}); diff --git a/module/actor/sheets/newSheet/npc.js b/module/actor/sheets/newSheet/npc.js index dea59e8f..699005ea 100644 --- a/module/actor/sheets/newSheet/npc.js +++ b/module/actor/sheets/newSheet/npc.js @@ -1,4 +1,3 @@ -import Actor5e from "../../entity.js"; import ActorSheet5e from "./base.js"; /** @@ -7,143 +6,154 @@ import ActorSheet5e from "./base.js"; * @extends {ActorSheet5e} */ export default class ActorSheet5eNPCNew extends ActorSheet5e { - - /** @override */ - get template() { - if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; - return `systems/sw5e/templates/actors/newActor/npc-sheet.html`; - } - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - classes: ["sw5e", "sheet", "actor", "npc"], - width: 800, - tabs: [{ - navSelector: ".root-tabs", - contentSelector: ".sheet-body", - initial: "attributes" - }], - }); - } - - /* -------------------------------------------- */ - - /** @override */ - static unsupportedItemTypes = new Set(["class"]); - - /* -------------------------------------------- */ - - /** - * Organize Owned Items for rendering the NPC sheet - * @private - */ - _prepareItems(data) { - - // Categorize Items as Features and Powers - const features = { - weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} }, - actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, - passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} }, - equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} - }; - - // Start by classifying items into groups for rendering - let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => { - item.img = item.img || CONST.DEFAULT_TOKEN; - item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); - item.hasUses = item.data.uses && (item.data.uses.max > 0); - item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); - item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); - item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); - if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item); - else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item); - else arr[2].push(item); - return arr; - }, [[], [], []]); - - // Apply item filters - forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); - techpowers = this._filterItems(techpowers, this._filters.techPowerbook); - other = this._filterItems(other, this._filters.features); - - // Organize Powerbook - const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); - const techPowerbook = this._preparePowerbook(data, techpowers, "tec"); - - // Organize Features - for ( let item of other ) { - if ( item.type === "weapon" ) features.weapons.items.push(item); - else if ( item.type === "feat" ) { - if ( item.data.activation.type ) features.actions.items.push(item); - else features.passive.items.push(item); - } - else features.equipment.items.push(item); + /** @override */ + get template() { + if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; + return `systems/sw5e/templates/actors/newActor/npc-sheet.html`; + } + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["sw5e", "sheet", "actor", "npc"], + width: 800, + tabs: [ + { + navSelector: ".root-tabs", + contentSelector: ".sheet-body", + initial: "attributes" + } + ] + }); } - // Assign and return - data.features = Object.values(features); - data.forcePowerbook = forcePowerbook; - data.techPowerbook = techPowerbook; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @override */ + static unsupportedItemTypes = new Set(["class"]); - /** @override */ - getData(options) { - const data = super.getData(options); + /* -------------------------------------------- */ - // Challenge Rating - const cr = parseFloat(data.data.details.cr || 0); - const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; - data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; + /** + * Organize Owned Items for rendering the NPC sheet + * @private + */ + _prepareItems(data) { + // Categorize Items as Features and Powers + const features = { + weapons: { + label: game.i18n.localize("SW5E.AttackPl"), + items: [], + hasActions: true, + dataset: {"type": "weapon", "weapon-type": "natural"} + }, + actions: { + label: game.i18n.localize("SW5E.ActionPl"), + items: [], + hasActions: true, + dataset: {"type": "feat", "activation.type": "action"} + }, + passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}}, + equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} + }; - // Creature Type - data.labels["type"] = this.actor.labels.creatureType; - return data; - } + // Start by classifying items into groups for rendering + let [forcepowers, techpowers, other] = data.items.reduce( + (arr, item) => { + item.img = item.img || CONST.DEFAULT_TOKEN; + item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; + item.hasUses = item.data.uses && item.data.uses.max > 0; + item.isOnCooldown = + item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; + item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; + item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type); + if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item); + else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item); + else arr[2].push(item); + return arr; + }, + [[], [], []] + ); - /* -------------------------------------------- */ - /* Object Updates */ - /* -------------------------------------------- */ + // Apply item filters + forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); + techpowers = this._filterItems(techpowers, this._filters.techPowerbook); + other = this._filterItems(other, this._filters.features); - /** @override */ - async _updateObject(event, formData) { + // Organize Powerbook + const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); + const techPowerbook = this._preparePowerbook(data, techpowers, "tec"); - // Format NPC Challenge Rating - const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; - let crv = "data.details.cr"; - let cr = formData[crv]; - cr = crs[cr] || parseFloat(cr); - if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); + // Organize Features + for (let item of other) { + if (item.type === "weapon") features.weapons.items.push(item); + else if (item.type === "feat") { + if (item.data.activation.type) features.actions.items.push(item); + else features.passive.items.push(item); + } else features.equipment.items.push(item); + } - // Parent ActorSheet update steps - return super._updateObject(event, formData); - } + // Assign and return + data.features = Object.values(features); + data.forcePowerbook = forcePowerbook; + data.techPowerbook = techPowerbook; + } - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - activateListeners(html) { - super.activateListeners(html); - html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); - } + /** @inheritdoc */ + getData(options) { + const data = super.getData(options); - /* -------------------------------------------- */ + // Challenge Rating + const cr = parseFloat(data.data.details.cr || 0); + const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; + data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; - /** - * Handle rolling NPC health values using the provided formula - * @param {Event} event The original click event - * @private - */ - _onRollHPFormula(event) { - event.preventDefault(); - const formula = this.actor.data.data.attributes.hp.formula; - if ( !formula ) return; - const hp = new Roll(formula).roll().total; - AudioHelper.play({src: CONFIG.sounds.dice}); - this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); - } + // Creature Type + data.labels["type"] = this.actor.labels.creatureType; + return data; + } + + /* -------------------------------------------- */ + /* Object Updates */ + /* -------------------------------------------- */ + + /** @override */ + async _updateObject(event, formData) { + // Format NPC Challenge Rating + const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; + let crv = "data.details.cr"; + let cr = formData[crv]; + cr = crs[cr] || parseFloat(cr); + if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr); + + // Parent ActorSheet update steps + return super._updateObject(event, formData); + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling NPC health values using the provided formula + * @param {Event} event The original click event + * @private + */ + _onRollHPFormula(event) { + event.preventDefault(); + const formula = this.actor.data.data.attributes.hp.formula; + if (!formula) return; + const hp = new Roll(formula).roll().total; + AudioHelper.play({src: CONFIG.sounds.dice}); + this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); + } } - diff --git a/module/actor/sheets/newSheet/starship.js b/module/actor/sheets/newSheet/starship.js index e27354d6..bf7e397e 100644 --- a/module/actor/sheets/newSheet/starship.js +++ b/module/actor/sheets/newSheet/starship.js @@ -6,150 +6,164 @@ import ActorSheet5e from "./base.js"; * @extends {ActorSheet5e} */ export default class ActorSheet5eStarship extends ActorSheet5e { - - /** @override */ - get template() { - if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; - return `systems/sw5e/templates/actors/newActor/starship.html`; - } - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - classes: ["sw5e", "sheet", "actor", "starship"], - width: 800, - tabs: [{ - navSelector: ".root-tabs", - contentSelector: ".sheet-body", - initial: "attributes" - }], - }); - } - - /* -------------------------------------------- */ - - /** - * Organize Owned Items for rendering the starship sheet - * @private - */ - _prepareItems(data) { - - // Categorize Items as Features and Powers - const features = { - weapons: { label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), items: [], hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} }, - passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} }, - equipment: { label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}}, - starshipfeatures: { label: game.i18n.localize("SW5E.StarshipfeaturePl"), items: [], hasActions: true, dataset: {type: "starshipfeature"} }, - starshipmods: { label: game.i18n.localize("SW5E.StarshipmodPl"), items: [], hasActions: false, dataset: {type: "starshipmod"} } - }; - - // Start by classifying items into groups for rendering - let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => { - item.img = item.img || CONST.DEFAULT_TOKEN; - item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); - item.hasUses = item.data.uses && (item.data.uses.max > 0); - item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); - item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); - item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); - if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item); - else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item); - else arr[2].push(item); - return arr; - }, [[], [], []]); - - // Apply item filters - forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); - techpowers = this._filterItems(techpowers, this._filters.techPowerbook); - other = this._filterItems(other, this._filters.features); - - // Organize Powerbook -// const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); -// const techPowerbook = this._preparePowerbook(data, techpowers, "tec"); - - // Organize Features - for ( let item of other ) { - if ( item.type === "weapon" ) features.weapons.items.push(item); - else if ( item.type === "feat" ) { - if ( item.data.activation.type ) features.actions.items.push(item); - else features.passive.items.push(item); - } - else if ( item.type === "starshipfeature" ) { - features.starshipfeatures.items.push(item); - } - else if ( item.type === "starshipmod" ) { - features.starshipmods.items.push(item); - } - else features.equipment.items.push(item); + /** @override */ + get template() { + if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; + return `systems/sw5e/templates/actors/newActor/starship.html`; + } + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["sw5e", "sheet", "actor", "starship"], + width: 800, + tabs: [ + { + navSelector: ".root-tabs", + contentSelector: ".sheet-body", + initial: "attributes" + } + ] + }); } - // Assign and return - data.features = Object.values(features); -// data.forcePowerbook = forcePowerbook; -// data.techPowerbook = techPowerbook; - } + /* -------------------------------------------- */ + /** + * Organize Owned Items for rendering the starship sheet + * @private + */ + _prepareItems(data) { + // Categorize Items as Features and Powers + const features = { + weapons: { + label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), + items: [], + hasActions: true, + dataset: {"type": "weapon", "weapon-type": "natural"} + }, + passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}}, + equipment: {label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}}, + starshipfeatures: { + label: game.i18n.localize("SW5E.StarshipfeaturePl"), + items: [], + hasActions: true, + dataset: {type: "starshipfeature"} + }, + starshipmods: { + label: game.i18n.localize("SW5E.StarshipmodPl"), + items: [], + hasActions: false, + dataset: {type: "starshipmod"} + } + }; - /* -------------------------------------------- */ + // Start by classifying items into groups for rendering + let [forcepowers, techpowers, other] = data.items.reduce( + (arr, item) => { + item.img = item.img || CONST.DEFAULT_TOKEN; + item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; + item.hasUses = item.data.uses && item.data.uses.max > 0; + item.isOnCooldown = + item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; + item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; + item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type); + if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item); + else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item); + else arr[2].push(item); + return arr; + }, + [[], [], []] + ); - /** @override */ - getData(options) { - const data = super.getData(options); + // Apply item filters + forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); + techpowers = this._filterItems(techpowers, this._filters.techPowerbook); + other = this._filterItems(other, this._filters.features); - // Add Size info - data.isTiny = data.actor.data.traits.size === "tiny"; - data.isSmall = data.actor.data.traits.size === "sm"; - data.isMedium = data.actor.data.traits.size === "med"; - data.isLarge = data.actor.data.traits.size === "lg"; - data.isHuge = data.actor.data.traits.size === "huge"; - data.isGargantuan = data.actor.data.traits.size === "grg"; + // Organize Powerbook + // const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); + // const techPowerbook = this._preparePowerbook(data, techpowers, "tec"); - // Challenge Rating - const cr = parseFloat(data.data.details.cr || 0); - const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; - data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; - return data; - } + // Organize Features + for (let item of other) { + if (item.type === "weapon") features.weapons.items.push(item); + else if (item.type === "feat") { + if (item.data.activation.type) features.actions.items.push(item); + else features.passive.items.push(item); + } else if (item.type === "starshipfeature") { + features.starshipfeatures.items.push(item); + } else if (item.type === "starshipmod") { + features.starshipmods.items.push(item); + } else features.equipment.items.push(item); + } - /* -------------------------------------------- */ - /* Object Updates */ - /* -------------------------------------------- */ + // Assign and return + data.features = Object.values(features); + // data.forcePowerbook = forcePowerbook; + // data.techPowerbook = techPowerbook; + } - /** @override */ - async _updateObject(event, formData) { + /* -------------------------------------------- */ - // Format NPC Challenge Rating - const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; - let crv = "data.details.cr"; - let cr = formData[crv]; - cr = crs[cr] || parseFloat(cr); - if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); + /** @override */ + getData(options) { + const data = super.getData(options); - // Parent ActorSheet update steps - return super._updateObject(event, formData); - } + // Add Size info + data.isTiny = data.actor.data.traits.size === "tiny"; + data.isSmall = data.actor.data.traits.size === "sm"; + data.isMedium = data.actor.data.traits.size === "med"; + data.isLarge = data.actor.data.traits.size === "lg"; + data.isHuge = data.actor.data.traits.size === "huge"; + data.isGargantuan = data.actor.data.traits.size === "grg"; - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ + // Challenge Rating + const cr = parseFloat(data.data.details.cr || 0); + const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; + data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; + return data; + } - /** @override */ - activateListeners(html) { - super.activateListeners(html); - html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); - } + /* -------------------------------------------- */ + /* Object Updates */ + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @override */ + async _updateObject(event, formData) { + // Format NPC Challenge Rating + const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; + let crv = "data.details.cr"; + let cr = formData[crv]; + cr = crs[cr] || parseFloat(cr); + if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr); - /** - * Handle rolling NPC health values using the provided formula - * @param {Event} event The original click event - * @private - */ - _onRollHPFormula(event) { - event.preventDefault(); - const formula = this.actor.data.data.attributes.hp.formula; - if ( !formula ) return; - const hp = new Roll(formula).roll().total; - AudioHelper.play({src: CONFIG.sounds.dice}); - this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); - } + // Parent ActorSheet update steps + return super._updateObject(event, formData); + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling NPC health values using the provided formula + * @param {Event} event The original click event + * @private + */ + _onRollHPFormula(event) { + event.preventDefault(); + const formula = this.actor.data.data.attributes.hp.formula; + if (!formula) return; + const hp = new Roll(formula).roll().total; + AudioHelper.play({src: CONFIG.sounds.dice}); + this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); + } } diff --git a/module/actor/sheets/newSheet/vehicle.js b/module/actor/sheets/newSheet/vehicle.js index aea7f402..a5c6e2ea 100644 --- a/module/actor/sheets/newSheet/vehicle.js +++ b/module/actor/sheets/newSheet/vehicle.js @@ -6,409 +6,427 @@ import ActorSheet5e from "./base.js"; * @type {ActorSheet5e} */ export default class ActorSheet5eVehicle extends ActorSheet5e { - /** - * Define default rendering options for the Vehicle sheet. - * @returns {Object} - */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - classes: ["sw5e", "sheet", "actor", "vehicle"], - width: 605, - height: 680 - }); - } - - /* -------------------------------------------- */ - - /** @override */ - static unsupportedItemTypes = new Set(["class"]); - - /* -------------------------------------------- */ - - - /** - * Creates a new cargo entry for a vehicle Actor. - */ - static get newCargo() { - return { - name: '', - quantity: 1 - }; - } - - /* -------------------------------------------- */ - - /** - * Compute the total weight of the vehicle's cargo. - * @param {Number} totalWeight The cumulative item weight from inventory items - * @param {Object} actorData The data object for the Actor being rendered - * @returns {{max: number, value: number, pct: number}} - * @private - */ - _computeEncumbrance(totalWeight, actorData) { - - // Compute currency weight - const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); - totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; - - // Vehicle weights are an order of magnitude greater. - totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier; - - // Compute overall encumbrance - const max = actorData.data.attributes.capacity.cargo; - const pct = Math.clamped((totalWeight * 100) / max, 0, 100); - return {value: totalWeight.toNearest(0.1), max, pct}; - } - - /* -------------------------------------------- */ - - /** @override */ - _getMovementSpeed(actorData, largestPrimary=true) { - return super._getMovementSpeed(actorData, largestPrimary); - } - - /* -------------------------------------------- */ - - /** - * Prepare items that are mounted to a vehicle and require one or more crew - * to operate. - * @private - */ - _prepareCrewedItem(item) { - - // Determine crewed status - const isCrewed = item.data.crewed; - item.toggleClass = isCrewed ? 'active' : ''; - item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`); - - // Handle crew actions - if (item.type === 'feat' && item.data.activation.type === 'crew') { - item.crew = item.data.activation.cost; - item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`); - if (item.data.cover === .5) item.cover = '½'; - else if (item.data.cover === .75) item.cover = '¾'; - else if (item.data.cover === null) item.cover = '—'; - if (item.crew < 1 || item.crew === null) item.crew = '—'; + /** + * Define default rendering options for the Vehicle sheet. + * @returns {Object} + */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["sw5e", "sheet", "actor", "vehicle"], + width: 605, + height: 680 + }); } - // Prepare vehicle weapons - if (item.type === 'equipment' || item.type === 'weapon') { - item.threshold = item.data.hp.dt ? item.data.hp.dt : '—'; - } - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @override */ + static unsupportedItemTypes = new Set(["class"]); - /** - * Organize Owned Items for rendering the Vehicle sheet. - * @private - */ - _prepareItems(data) { - const cargoColumns = [{ - label: game.i18n.localize('SW5E.Quantity'), - css: 'item-qty', - property: 'quantity', - editable: 'Number' - }]; + /* -------------------------------------------- */ - const equipmentColumns = [{ - label: game.i18n.localize('SW5E.Quantity'), - css: 'item-qty', - property: 'data.quantity' - }, { - label: game.i18n.localize('SW5E.AC'), - css: 'item-ac', - property: 'data.armor.value' - }, { - label: game.i18n.localize('SW5E.HP'), - css: 'item-hp', - property: 'data.hp.value', - editable: 'Number' - }, { - label: game.i18n.localize('SW5E.Threshold'), - css: 'item-threshold', - property: 'threshold' - }]; - - const features = { - actions: { - label: game.i18n.localize('SW5E.ActionPl'), - items: [], - crewable: true, - dataset: {type: 'feat', 'activation.type': 'crew'}, - columns: [{ - label: game.i18n.localize('SW5E.VehicleCrew'), - css: 'item-crew', - property: 'crew' - }, { - label: game.i18n.localize('SW5E.Cover'), - css: 'item-cover', - property: 'cover' - }] - }, - equipment: { - label: game.i18n.localize('SW5E.ItemTypeEquipment'), - items: [], - crewable: true, - dataset: {type: 'equipment', 'armor.type': 'vehicle'}, - columns: equipmentColumns - }, - passive: { - label: game.i18n.localize('SW5E.Features'), - items: [], - dataset: {type: 'feat'} - }, - reactions: { - label: game.i18n.localize('SW5E.ReactionPl'), - items: [], - dataset: {type: 'feat', 'activation.type': 'reaction'} - }, - weapons: { - label: game.i18n.localize('SW5E.ItemTypeWeaponPl'), - items: [], - crewable: true, - dataset: {type: 'weapon', 'weapon-type': 'siege'}, - columns: equipmentColumns - } - }; - - const cargo = { - crew: { - label: game.i18n.localize('SW5E.VehicleCrew'), - items: data.data.cargo.crew, - css: 'cargo-row crew', - editableName: true, - dataset: {type: 'crew'}, - columns: cargoColumns - }, - passengers: { - label: game.i18n.localize('SW5E.VehiclePassengers'), - items: data.data.cargo.passengers, - css: 'cargo-row passengers', - editableName: true, - dataset: {type: 'passengers'}, - columns: cargoColumns - }, - cargo: { - label: game.i18n.localize('SW5E.VehicleCargo'), - items: [], - dataset: {type: 'loot'}, - columns: [{ - label: game.i18n.localize('SW5E.Quantity'), - css: 'item-qty', - property: 'data.quantity', - editable: 'Number' - }, { - label: game.i18n.localize('SW5E.Price'), - css: 'item-price', - property: 'data.price', - editable: 'Number' - }, { - label: game.i18n.localize('SW5E.Weight'), - css: 'item-weight', - property: 'data.weight', - editable: 'Number' - }] - } - }; - - // Classify items owned by the vehicle and compute total cargo weight - let totalWeight = 0; - for (const item of data.items) { - this._prepareCrewedItem(item); - - // Handle cargo explicitly - const isCargo = item.flags.sw5e?.vehicleCargo === true; - if ( isCargo ) { - totalWeight += (item.data.weight || 0) * item.data.quantity; - cargo.cargo.items.push(item); - continue - } - - // Handle non-cargo item types - switch ( item.type ) { - case "weapon": - features.weapons.items.push(item); - break; - case "equipment": - features.equipment.items.push(item); - break; - case "feat": - if ( !item.data.activation.type || (item.data.activation.type === "none") ) features.passive.items.push(item); - else if (item.data.activation.type === 'reaction') features.reactions.items.push(item); - else features.actions.items.push(item); - break; - default: - totalWeight += (item.data.weight || 0) * item.data.quantity; - cargo.cargo.items.push(item); - } + /** + * Creates a new cargo entry for a vehicle Actor. + */ + static get newCargo() { + return { + name: "", + quantity: 1 + }; } - // Update the rendering context data - data.features = Object.values(features); - data.cargo = Object.values(cargo); - data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ + /** + * Compute the total weight of the vehicle's cargo. + * @param {Number} totalWeight The cumulative item weight from inventory items + * @param {Object} actorData The data object for the Actor being rendered + * @returns {{max: number, value: number, pct: number}} + * @private + */ + _computeEncumbrance(totalWeight, actorData) { + // Compute currency weight + const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); + totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; - /** @override */ - activateListeners(html) { - super.activateListeners(html); - if (!this.isEditable) return; + // Vehicle weights are an order of magnitude greater. + totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier; - html.find('.item-toggle').click(this._onToggleItem.bind(this)); - html.find('.item-hp input') - .click(evt => evt.target.select()) - .change(this._onHPChange.bind(this)); - - html.find('.item:not(.cargo-row) input[data-property]') - .click(evt => evt.target.select()) - .change(this._onEditInSheet.bind(this)); - - html.find('.cargo-row input') - .click(evt => evt.target.select()) - .change(this._onCargoRowChange.bind(this)); - - if (this.actor.data.data.attributes.actions.stations) { - html.find('.counter.actions, .counter.action-thresholds').hide(); - } - } - - /* -------------------------------------------- */ - - /** - * Handle saving a cargo row (i.e. crew or passenger) in-sheet. - * @param event {Event} - * @returns {Promise|null} - * @private - */ - _onCargoRowChange(event) { - event.preventDefault(); - const target = event.currentTarget; - const row = target.closest('.item'); - const idx = Number(row.dataset.itemId); - const property = row.classList.contains('crew') ? 'crew' : 'passengers'; - - // Get the cargo entry - const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); - const entry = cargo[idx]; - if (!entry) return null; - - // Update the cargo value - const key = target.dataset.property || 'name'; - const type = target.dataset.dtype; - let value = target.value; - if (type === 'Number') value = Number(value); - entry[key] = value; - - // Perform the Actor update - return this.actor.update({[`data.cargo.${property}`]: cargo}); - } - - /* -------------------------------------------- */ - - /** - * Handle editing certain values like quantity, price, and weight in-sheet. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onEditInSheet(event) { - event.preventDefault(); - const itemID = event.currentTarget.closest('.item').dataset.itemId; - const item = this.actor.items.get(itemID); - const property = event.currentTarget.dataset.property; - const type = event.currentTarget.dataset.dtype; - let value = event.currentTarget.value; - switch (type) { - case 'Number': value = parseInt(value); break; - case 'Boolean': value = value === 'true'; break; - } - return item.update({[`${property}`]: value}); - } - - /* -------------------------------------------- */ - - /** - * Handle creating a new crew or passenger row. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onItemCreate(event) { - event.preventDefault(); - const target = event.currentTarget; - const type = target.dataset.type; - if (type === 'crew' || type === 'passengers') { - const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); - cargo.push(this.constructor.newCargo); - return this.actor.update({[`data.cargo.${type}`]: cargo}); - } - return super._onItemCreate(event); - } - - /* -------------------------------------------- */ - - /** - * Handle deleting a crew or passenger row. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onItemDelete(event) { - event.preventDefault(); - const row = event.currentTarget.closest('.item'); - if (row.classList.contains('cargo-row')) { - const idx = Number(row.dataset.itemId); - const type = row.classList.contains('crew') ? 'crew' : 'passengers'; - const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); - return this.actor.update({[`data.cargo.${type}`]: cargo}); + // Compute overall encumbrance + const max = actorData.data.attributes.capacity.cargo; + const pct = Math.clamped((totalWeight * 100) / max, 0, 100); + return {value: totalWeight.toNearest(0.1), max, pct}; } - return super._onItemDelete(event); - } + /* -------------------------------------------- */ - /** @override */ - async _onDropItemCreate(itemData) { - const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"]; - const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo"); - foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo); - return super._onDropItemCreate(itemData); - } + /** @override */ + _getMovementSpeed(actorData, largestPrimary = true) { + return super._getMovementSpeed(actorData, largestPrimary); + } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Special handling for editing HP to clamp it within appropriate range. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onHPChange(event) { - event.preventDefault(); - const itemID = event.currentTarget.closest('.item').dataset.itemId; - const item = this.actor.items.get(itemID); - const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); - event.currentTarget.value = hp; - return item.update({'data.hp.value': hp}); - } + /** + * Prepare items that are mounted to a vehicle and require one or more crew + * to operate. + * @private + */ + _prepareCrewedItem(item) { + // Determine crewed status + const isCrewed = item.data.crewed; + item.toggleClass = isCrewed ? "active" : ""; + item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`); - /* -------------------------------------------- */ + // Handle crew actions + if (item.type === "feat" && item.data.activation.type === "crew") { + item.crew = item.data.activation.cost; + item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`); + if (item.data.cover === 0.5) item.cover = "½"; + else if (item.data.cover === 0.75) item.cover = "¾"; + else if (item.data.cover === null) item.cover = "—"; + if (item.crew < 1 || item.crew === null) item.crew = "—"; + } - /** - * Handle toggling an item's crewed status. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onToggleItem(event) { - event.preventDefault(); - const itemID = event.currentTarget.closest('.item').dataset.itemId; - const item = this.actor.items.get(itemID); - const crewed = !!item.data.data.crewed; - return item.update({'data.crewed': !crewed}); - } -}; + // Prepare vehicle weapons + if (item.type === "equipment" || item.type === "weapon") { + item.threshold = item.data.hp.dt ? item.data.hp.dt : "—"; + } + } + + /* -------------------------------------------- */ + + /** + * Organize Owned Items for rendering the Vehicle sheet. + * @private + */ + _prepareItems(data) { + const cargoColumns = [ + { + label: game.i18n.localize("SW5E.Quantity"), + css: "item-qty", + property: "quantity", + editable: "Number" + } + ]; + + const equipmentColumns = [ + { + label: game.i18n.localize("SW5E.Quantity"), + css: "item-qty", + property: "data.quantity" + }, + { + label: game.i18n.localize("SW5E.AC"), + css: "item-ac", + property: "data.armor.value" + }, + { + label: game.i18n.localize("SW5E.HP"), + css: "item-hp", + property: "data.hp.value", + editable: "Number" + }, + { + label: game.i18n.localize("SW5E.Threshold"), + css: "item-threshold", + property: "threshold" + } + ]; + + const features = { + actions: { + label: game.i18n.localize("SW5E.ActionPl"), + items: [], + crewable: true, + dataset: {"type": "feat", "activation.type": "crew"}, + columns: [ + { + label: game.i18n.localize("SW5E.VehicleCrew"), + css: "item-crew", + property: "crew" + }, + { + label: game.i18n.localize("SW5E.Cover"), + css: "item-cover", + property: "cover" + } + ] + }, + equipment: { + label: game.i18n.localize("SW5E.ItemTypeEquipment"), + items: [], + crewable: true, + dataset: {"type": "equipment", "armor.type": "vehicle"}, + columns: equipmentColumns + }, + passive: { + label: game.i18n.localize("SW5E.Features"), + items: [], + dataset: {type: "feat"} + }, + reactions: { + label: game.i18n.localize("SW5E.ReactionPl"), + items: [], + dataset: {"type": "feat", "activation.type": "reaction"} + }, + weapons: { + label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), + items: [], + crewable: true, + dataset: {"type": "weapon", "weapon-type": "siege"}, + columns: equipmentColumns + } + }; + + const cargo = { + crew: { + label: game.i18n.localize("SW5E.VehicleCrew"), + items: data.data.cargo.crew, + css: "cargo-row crew", + editableName: true, + dataset: {type: "crew"}, + columns: cargoColumns + }, + passengers: { + label: game.i18n.localize("SW5E.VehiclePassengers"), + items: data.data.cargo.passengers, + css: "cargo-row passengers", + editableName: true, + dataset: {type: "passengers"}, + columns: cargoColumns + }, + cargo: { + label: game.i18n.localize("SW5E.VehicleCargo"), + items: [], + dataset: {type: "loot"}, + columns: [ + { + label: game.i18n.localize("SW5E.Quantity"), + css: "item-qty", + property: "data.quantity", + editable: "Number" + }, + { + label: game.i18n.localize("SW5E.Price"), + css: "item-price", + property: "data.price", + editable: "Number" + }, + { + label: game.i18n.localize("SW5E.Weight"), + css: "item-weight", + property: "data.weight", + editable: "Number" + } + ] + } + }; + + // Classify items owned by the vehicle and compute total cargo weight + let totalWeight = 0; + for (const item of data.items) { + this._prepareCrewedItem(item); + + // Handle cargo explicitly + const isCargo = item.flags.sw5e?.vehicleCargo === true; + if (isCargo) { + totalWeight += (item.data.weight || 0) * item.data.quantity; + cargo.cargo.items.push(item); + continue; + } + + // Handle non-cargo item types + switch (item.type) { + case "weapon": + features.weapons.items.push(item); + break; + case "equipment": + features.equipment.items.push(item); + break; + case "feat": + if (!item.data.activation.type || item.data.activation.type === "none") + features.passive.items.push(item); + else if (item.data.activation.type === "reaction") features.reactions.items.push(item); + else features.actions.items.push(item); + break; + default: + totalWeight += (item.data.weight || 0) * item.data.quantity; + cargo.cargo.items.push(item); + } + } + + // Update the rendering context data + data.features = Object.values(features); + data.cargo = Object.values(cargo); + data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data); + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + if (!this.isEditable) return; + + html.find(".item-toggle").click(this._onToggleItem.bind(this)); + html.find(".item-hp input") + .click((evt) => evt.target.select()) + .change(this._onHPChange.bind(this)); + + html.find(".item:not(.cargo-row) input[data-property]") + .click((evt) => evt.target.select()) + .change(this._onEditInSheet.bind(this)); + + html.find(".cargo-row input") + .click((evt) => evt.target.select()) + .change(this._onCargoRowChange.bind(this)); + + if (this.actor.data.data.attributes.actions.stations) { + html.find(".counter.actions, .counter.action-thresholds").hide(); + } + } + + /* -------------------------------------------- */ + + /** + * Handle saving a cargo row (i.e. crew or passenger) in-sheet. + * @param event {Event} + * @returns {Promise|null} + * @private + */ + _onCargoRowChange(event) { + event.preventDefault(); + const target = event.currentTarget; + const row = target.closest(".item"); + const idx = Number(row.dataset.itemId); + const property = row.classList.contains("crew") ? "crew" : "passengers"; + + // Get the cargo entry + const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); + const entry = cargo[idx]; + if (!entry) return null; + + // Update the cargo value + const key = target.dataset.property || "name"; + const type = target.dataset.dtype; + let value = target.value; + if (type === "Number") value = Number(value); + entry[key] = value; + + // Perform the Actor update + return this.actor.update({[`data.cargo.${property}`]: cargo}); + } + + /* -------------------------------------------- */ + + /** + * Handle editing certain values like quantity, price, and weight in-sheet. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onEditInSheet(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const property = event.currentTarget.dataset.property; + const type = event.currentTarget.dataset.dtype; + let value = event.currentTarget.value; + switch (type) { + case "Number": + value = parseInt(value); + break; + case "Boolean": + value = value === "true"; + break; + } + return item.update({[`${property}`]: value}); + } + + /* -------------------------------------------- */ + + /** + * Handle creating a new crew or passenger row. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onItemCreate(event) { + event.preventDefault(); + const target = event.currentTarget; + const type = target.dataset.type; + if (type === "crew" || type === "passengers") { + const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); + cargo.push(this.constructor.newCargo); + return this.actor.update({[`data.cargo.${type}`]: cargo}); + } + return super._onItemCreate(event); + } + + /* -------------------------------------------- */ + + /** + * Handle deleting a crew or passenger row. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onItemDelete(event) { + event.preventDefault(); + const row = event.currentTarget.closest(".item"); + if (row.classList.contains("cargo-row")) { + const idx = Number(row.dataset.itemId); + const type = row.classList.contains("crew") ? "crew" : "passengers"; + const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); + return this.actor.update({[`data.cargo.${type}`]: cargo}); + } + + return super._onItemDelete(event); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropItemCreate(itemData) { + const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"]; + const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo"; + foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo); + return super._onDropItemCreate(itemData); + } + + /* -------------------------------------------- */ + + /** + * Special handling for editing HP to clamp it within appropriate range. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onHPChange(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); + event.currentTarget.value = hp; + return item.update({"data.hp.value": hp}); + } + + /* -------------------------------------------- */ + + /** + * Handle toggling an item's crewed status. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onToggleItem(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const crewed = !!item.data.data.crewed; + return item.update({"data.crewed": !crewed}); + } +} diff --git a/module/actor/sheets/oldSheets/base.js b/module/actor/sheets/oldSheets/base.js index 2fccb001..acab8b2a 100644 --- a/module/actor/sheets/oldSheets/base.js +++ b/module/actor/sheets/oldSheets/base.js @@ -5,7 +5,7 @@ import ActorHitDiceConfig from "../../../apps/hit-dice-config.js"; import ActorMovementConfig from "../../../apps/movement-config.js"; import ActorSensesConfig from "../../../apps/senses-config.js"; import ActorTypeConfig from "../../../apps/actor-type.js"; -import {SW5E} from '../../../config.js'; +import {SW5E} from "../../../config.js"; import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js"; /** @@ -14,901 +14,907 @@ import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effe * @extends {ActorSheet} */ export default class ActorSheet5e extends ActorSheet { - constructor(...args) { - super(...args); + constructor(...args) { + super(...args); + + /** + * Track the set of item filters which are applied + * @type {Set} + */ + this._filters = { + inventory: new Set(), + powerbook: new Set(), + features: new Set(), + effects: new Set() + }; + } + + /* -------------------------------------------- */ + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + scrollY: [ + ".inventory .inventory-list", + ".features .inventory-list", + ".powerbook .inventory-list", + ".effects .inventory-list" + ], + tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}] + }); + } + + /* -------------------------------------------- */ /** - * Track the set of item filters which are applied - * @type {Set} + * A set of item types that should be prevented from being dropped on this type of actor sheet. + * @type {Set} */ - this._filters = { - inventory: new Set(), - powerbook: new Set(), - features: new Set(), - effects: new Set() - }; - } + static unsupportedItemTypes = new Set(); - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - scrollY: [ - ".inventory .inventory-list", - ".features .inventory-list", - ".powerbook .inventory-list", - ".effects .inventory-list" - ], - tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}] - }); - } - - /* -------------------------------------------- */ - - /** - * A set of item types that should be prevented from being dropped on this type of actor sheet. - * @type {Set} - */ - static unsupportedItemTypes = new Set(); - - /* -------------------------------------------- */ - - - /** @override */ - get template() { - if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html"; - return `systems/sw5e/templates/actors/oldActor/${this.actor.data.type}-sheet.html`; - } - - /* -------------------------------------------- */ - - /** @override */ - getData(options) { - - // Basic data - let isOwner = this.actor.isOwner; - const data = { - owner: isOwner, - limited: this.actor.limited, - options: this.options, - editable: this.isEditable, - cssClass: isOwner ? "editable" : "locked", - isCharacter: this.actor.type === "character", - isNPC: this.actor.type === "npc", - isStarship: this.actor.type === "starship", - isVehicle: this.actor.type === 'vehicle', - config: CONFIG.SW5E, - rollData: this.actor.getRollData.bind(this.actor) - }; - - // The Actor's data - const actorData = this.actor.data.toObject(false); - data.actor = actorData; - data.data = actorData.data; - - // Owned Items - data.items = actorData.items; - for ( let i of data.items ) { - const item = this.actor.items.get(i._id); - i.labels = item.labels; - } - data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0)); - - // Labels and filters - data.labels = this.actor.labels || {}; - data.filters = this._filters; - - // Ability Scores - for ( let [a, abl] of Object.entries(actorData.data.abilities)) { - abl.icon = this._getProficiencyIcon(abl.proficient); - abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient]; - abl.label = CONFIG.SW5E.abilities[a]; + /** @override */ + get template() { + if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html"; + return `systems/sw5e/templates/actors/oldActor/${this.actor.data.type}-sheet.html`; } - // Skills - if (actorData.data.skills) { - for ( let [s, skl] of Object.entries(actorData.data.skills)) { - skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability]; - skl.icon = this._getProficiencyIcon(skl.value); - skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value]; - skl.label = CONFIG.SW5E.skills[s]; - } - } + /* -------------------------------------------- */ - // Movement speeds - data.movement = this._getMovementSpeed(actorData); + /** @override */ + getData(options) { + // Basic data + let isOwner = this.actor.isOwner; + const data = { + owner: isOwner, + limited: this.actor.limited, + options: this.options, + editable: this.isEditable, + cssClass: isOwner ? "editable" : "locked", + isCharacter: this.actor.type === "character", + isNPC: this.actor.type === "npc", + isStarship: this.actor.type === "starship", + isVehicle: this.actor.type === "vehicle", + config: CONFIG.SW5E, + rollData: this.actor.getRollData.bind(this.actor) + }; - // Senses - data.senses = this._getSenses(actorData); + // The Actor's data + const actorData = this.actor.data.toObject(false); + data.actor = actorData; + data.data = actorData.data; - // Update traits - this._prepareTraits(actorData.data.traits); - - // Prepare owned items - this._prepareItems(data); - - // Prepare active effects - data.effects = prepareActiveEffectCategories(this.actor.effects); - - // Return data to the sheet - return data - } - - /* -------------------------------------------- */ - - /** - * Prepare the display of movement speed data for the Actor* - * @param {object} actorData The Actor data being prepared. - * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk" - * @returns {{primary: string, special: string}} - * @private - */ - _getMovementSpeed(actorData, largestPrimary=false) { - const movement = actorData.data.attributes.movement || {}; - - // Prepare an array of available movement speeds - let speeds = [ - [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`], - [movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`], - [movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")], - [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`] - ] - if ( largestPrimary ) { - speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]); - } - - // Filter and sort speeds on their values - speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]); - - // Case 1: Largest as primary - if ( largestPrimary ) { - let primary = speeds.shift(); - return { - primary: `${primary ? primary[1] : "0"} ${movement.units}`, - special: speeds.map(s => s[1]).join(", ") - } - } - - // Case 2: Walk as primary - else { - return { - primary: `${movement.walk || 0} ${movement.units}`, - special: speeds.length ? speeds.map(s => s[1]).join(", ") : "" - } - } - } - - /* -------------------------------------------- */ - - _getSenses(actorData) { - const senses = actorData.data.attributes.senses || {}; - const tags = {}; - for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) { - const v = senses[k] ?? 0 - if ( v === 0 ) continue; - tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`; - } - if ( !!senses.special ) tags["special"] = senses.special; - return tags; - } - - /* -------------------------------------------- */ - - /** - * 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, - "di": CONFIG.SW5E.damageResistanceTypes, - "dv": CONFIG.SW5E.damageResistanceTypes, - "ci": CONFIG.SW5E.conditionTypes, - "languages": CONFIG.SW5E.languages, - "armorProf": CONFIG.SW5E.armorProficiencies, - "weaponProf": CONFIG.SW5E.weaponProficiencies, - "toolProf": CONFIG.SW5E.toolProficiencies - }; - for ( let [t, choices] of Object.entries(map) ) { - const trait = traits[t]; - if ( !trait ) continue; - let values = []; - if ( trait.value ) { - values = trait.value instanceof Array ? trait.value : [trait.value]; - } - trait.selected = values.reduce((obj, t) => { - obj[t] = choices[t]; - return obj; - }, {}); - - // Add custom entry - if ( trait.custom ) { - trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim()); - } - trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; - } - } - - /* -------------------------------------------- */ - - /** - * Insert a power into the powerbook object when rendering the character sheet - * @param {Object} data The Actor data being prepared - * @param {Array} powers The power data being prepared - * @private - */ - _preparePowerbook(data, powers) { - const owner = this.actor.isOwner; - const levels = data.data.powers; - const powerbook = {}; - - // Define some mappings - const sections = { - "atwill": -20, - "innate": -10, - "pact": 0.5 - }; - - // Label power slot uses headers - const useLabels = { - "-20": "-", - "-10": "-", - "0": "∞" - }; - - // Format a powerbook entry for a certain indexed level - const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => { - powerbook[i] = { - order: i, - label: label, - usesSlots: i > 0, - canCreate: owner, - canPrepare: (data.actor.type === "character") && (i >= 1), - powers: [], - 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 - }; - }; - - // Determine the maximum power level which has a slot - const maxLevel = Array.fromRange(10).reduce((max, i) => { - if ( i === 0 ) return max; - const level = levels[`power${i}`]; - if ( (level.max || level.override ) && ( i > max ) ) max = i; - return max; - }, 0); - - // 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++) { - const sl = `power${lvl}`; - registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]); - } - } - - // Pact magic users have cantrips and a pact magic section - // TODO: Check if this is needed, we've removed pacts everywhere else - if ( levels.pact && levels.pact.max ) { - if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]); - const l = levels.pact; - const config = CONFIG.SW5E.powerPreparationModes.pact; - const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`); - const label = `${config} — ${level}`; - registerSection("pact", sections.pact, label, { - prepMode: "pact", - value: l.value, - max: l.max, - override: l.override - }); - } - - // Iterate over every power item, adding powers to the powerbook by section - powers.forEach(power => { - const mode = power.data.preparation.mode || "prepared"; - let s = power.data.level || 0; - const sl = `power${s}`; - - // Specialized powercasting modes (if they exist) - if ( mode in sections ) { - s = sections[mode]; - if ( !powerbook[s] ){ - 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 - }); + // Owned Items + data.items = actorData.items; + for (let i of data.items) { + const item = this.actor.items.get(i._id); + i.labels = item.labels; } - } + data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0)); - // 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: levels[sl]}); - } + // Labels and filters + data.labels = this.actor.labels || {}; + data.filters = this._filters; - // Add the power to the relevant heading - powerbook[s].powers.push(power); - }); - - // Sort the powerbook by section level - const sorted = Object.values(powerbook); - sorted.sort((a, b) => a.order - b.order); - return sorted; - } - - /* -------------------------------------------- */ - - /** - * Determine whether an Owned Item will be shown based on the current set of filters - * @return {boolean} - * @private - */ - _filterItems(items, filters) { - return items.filter(item => { - const data = item.data; - - // Action usage - for ( let f of ["action", "bonus", "reaction"] ) { - if ( filters.has(f) ) { - if ((data.activation && (data.activation.type !== f))) return false; + // Ability Scores + for (let [a, abl] of Object.entries(actorData.data.abilities)) { + abl.icon = this._getProficiencyIcon(abl.proficient); + abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient]; + abl.label = CONFIG.SW5E.abilities[a]; } - } - // Power-specific filters - if ( filters.has("ritual") ) { - if (data.components.ritual !== true) return false; - } - if ( filters.has("concentration") ) { - if (data.components.concentration !== true) return false; - } - if ( filters.has("prepared") ) { - if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true; - if ( this.actor.data.type === "npc" ) return true; - return data.preparation.prepared; - } - - // Equipment-specific filters - if ( filters.has("equipped") ) { - if ( data.equipped !== true ) return false; - } - return true; - }); - } - - /* -------------------------------------------- */ - - /** - * Get the font-awesome icon used to display a certain level of skill proficiency - * @private - */ - _getProficiencyIcon(level) { - const icons = { - 0: '', - 0.5: '', - 1: '', - 2: '' - }; - return icons[level] || icons[0]; - } - - /* -------------------------------------------- */ - /* Event Listeners and Handlers - /* -------------------------------------------- */ - - /** @inheritdoc */ - activateListeners(html) { - - // Activate Item Filters - const filterLists = html.find(".filter-list"); - filterLists.each(this._initializeFilterItemList.bind(this)); - filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this)); - - // Item summaries - html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event)); - - // View Item Sheets - html.find('.item-edit').click(this._onItemEdit.bind(this)); - - // Editable Only Listeners - if ( this.isEditable ) { - - // Input focus and update - const inputs = html.find("input"); - inputs.focus(ev => ev.currentTarget.select()); - inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)); - - // Ability Proficiency - html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this)); - - // Toggle Skill Proficiency - html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this)); - - // Trait Selector - html.find('.trait-selector').click(this._onTraitSelector.bind(this)); - - // Configure Special Flags - html.find('.config-button').click(this._onConfigMenu.bind(this)); - - // Owned Item management - html.find('.item-create').click(this._onItemCreate.bind(this)); - 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(ev => onManageActiveEffect(ev, this.actor)); - } - - // Owner Only Listeners - if ( this.actor.isOwner ) { - - // Ability Checks - html.find('.ability-name').click(this._onRollAbilityTest.bind(this)); - - - // Roll Skill Checks - html.find('.skill-name').click(this._onRollSkillCheck.bind(this)); - - // Item Rolling - html.find('.item .item-image').click(event => this._onItemRoll(event)); - html.find('.item .item-recharge').click(event => this._onItemRecharge(event)); - } - - // Otherwise remove rollable classes - else { - html.find(".rollable").each((i, el) => el.classList.remove("rollable")); - } - - // Handle default listeners last so system listeners are triggered first - super.activateListeners(html); - } - - /* -------------------------------------------- */ - - /** - * Iinitialize Item list filters by activating the set of filters which are currently applied - * @private - */ - _initializeFilterItemList(i, ul) { - const set = this._filters[ul.dataset.filter]; - const filters = ul.querySelectorAll(".filter-item"); - for ( let li of filters ) { - if ( set.has(li.dataset.filter) ) li.classList.add("active"); - } - } - - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ - - /** - * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs - * @param event - * @private - */ - _onChangeInputDelta(event) { - const input = event.target; - const value = input.value; - if ( ["+", "-"].includes(value[0]) ) { - let delta = parseFloat(value); - input.value = getProperty(this.actor.data, input.name) + delta; - } else if ( value[0] === "=" ) { - input.value = value.slice(1); - } - } - - /* -------------------------------------------- */ - - /** - * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options - * @param {Event} event The click event which originated the selection - * @private - */ - _onConfigMenu(event) { - event.preventDefault(); - const button = event.currentTarget; - let app; - switch ( button.dataset.action ) { - case "hit-dice": - app = new ActorHitDiceConfig(this.object); - break; - case "movement": - app = new ActorMovementConfig(this.object); - break; - case "flags": - app = new ActorSheetFlags(this.object); - break; - case "senses": - app = new ActorSensesConfig(this.object); - break; - case "type": - new ActorTypeConfig(this.object).render(true); - break; - } - app?.render(true); - } - - /* -------------------------------------------- */ - - /** - * Handle cycling proficiency in a Skill - * @param {Event} event A click or contextmenu event which triggered the handler - * @private - */ - _onCycleSkillProficiency(event) { - event.preventDefault(); - const field = $(event.currentTarget).siblings('input[type="hidden"]'); - - // Get the current level and the array of levels - const level = parseFloat(field.val()); - const levels = [0, 1, 0.5, 2]; - let idx = levels.indexOf(level); - - // Toggle next level - forward on click, backwards on right - if ( event.type === "click" ) { - field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]); - } else if ( event.type === "contextmenu" ) { - field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]); - } - - // Update the field value and save the form - this._onSubmit(event); - } - - /* -------------------------------------------- */ - - /** @override */ - async _onDropActor(event, data) { - const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get('sw5e', 'allowPolymorphing')); - if ( !canPolymorph ) return false; - - // Get the target actor - let sourceActor = null; - if (data.pack) { - const pack = game.packs.find(p => p.collection === data.pack); - sourceActor = await pack.getEntity(data.id); - } else { - sourceActor = game.actors.get(data.id); - } - if ( !sourceActor ) return; - - // Define a function to record polymorph settings for future use - const rememberOptions = html => { - const options = {}; - html.find('input').each((i, el) => { - options[el.name] = el.checked; - }); - const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options); - game.settings.set('sw5e', 'polymorphSettings', settings); - return settings; - }; - - // Create and render the Dialog - return new Dialog({ - title: game.i18n.localize('SW5E.PolymorphPromptTitle'), - content: { - options: game.settings.get('sw5e', 'polymorphSettings'), - i18n: SW5E.polymorphSettings, - isToken: this.actor.isToken - }, - default: 'accept', - buttons: { - accept: { - icon: '', - label: game.i18n.localize('SW5E.PolymorphAcceptSettings'), - callback: html => this.actor.transformInto(sourceActor, rememberOptions(html)) - }, - wildshape: { - icon: '', - label: game.i18n.localize('SW5E.PolymorphWildShape'), - callback: html => this.actor.transformInto(sourceActor, { - keepBio: true, - keepClass: true, - keepMental: true, - mergeSaves: true, - mergeSkills: true, - transformTokens: rememberOptions(html).transformTokens - }) - }, - polymorph: { - icon: '', - label: game.i18n.localize('SW5E.Polymorph'), - callback: html => this.actor.transformInto(sourceActor, { - transformTokens: rememberOptions(html).transformTokens - }) - }, - cancel: { - icon: '', - label: game.i18n.localize('Cancel') + // Skills + if (actorData.data.skills) { + for (let [s, skl] of Object.entries(actorData.data.skills)) { + skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability]; + skl.icon = this._getProficiencyIcon(skl.value); + skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value]; + skl.label = CONFIG.SW5E.skills[s]; + } } - } - }, { - classes: ['dialog', 'sw5e'], - width: 600, - template: 'systems/sw5e/templates/apps/polymorph-prompt.html' - }).render(true); - } - /* -------------------------------------------- */ + // Movement speeds + data.movement = this._getMovementSpeed(actorData); - /** @override */ - async _onDropItemCreate(itemData) { + // Senses + data.senses = this._getSenses(actorData); - // Check to make sure items of this type are allowed on this actor - if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) { - return ui.notifications.warn(game.i18n.format("SW5E.ActorWarningInvalidItem", { - itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]), - actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type]) - })); + // Update traits + this._prepareTraits(actorData.data.traits); + + // Prepare owned items + this._prepareItems(data); + + // Prepare active effects + data.effects = prepareActiveEffectCategories(this.actor.effects); + + // Return data to the sheet + return data; } - // Create a Consumable power scroll on the Inventory tab - // TODO: This is pretty non functional as the base items for the scrolls, and the powers, are not defined, maybe consider using holocrons - if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) { - const scroll = await Item5e.createScrollFromPower(itemData); - itemData = scroll.data; + /* -------------------------------------------- */ + + /** + * Prepare the display of movement speed data for the Actor* + * @param {object} actorData The Actor data being prepared. + * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk" + * @returns {{primary: string, special: string}} + * @private + */ + _getMovementSpeed(actorData, largestPrimary = false) { + const movement = actorData.data.attributes.movement || {}; + + // Prepare an array of available movement speeds + let speeds = [ + [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`], + [movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`], + [ + movement.fly, + `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "") + ], + [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`] + ]; + if (largestPrimary) { + speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]); + } + + // Filter and sort speeds on their values + speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]); + + // Case 1: Largest as primary + if (largestPrimary) { + let primary = speeds.shift(); + return { + primary: `${primary ? primary[1] : "0"} ${movement.units}`, + special: speeds.map((s) => s[1]).join(", ") + }; + } + + // Case 2: Walk as primary + else { + return { + primary: `${movement.walk || 0} ${movement.units}`, + special: speeds.length ? speeds.map((s) => s[1]).join(", ") : "" + }; + } } - if ( itemData.data ) { - // Ignore certain statuses - ["equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]); + /* -------------------------------------------- */ - // Downgrade ATTUNED to REQUIRED - itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED); + _getSenses(actorData) { + const senses = actorData.data.attributes.senses || {}; + const tags = {}; + for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) { + const v = senses[k] ?? 0; + if (v === 0) continue; + tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`; + } + if (!!senses.special) tags["special"] = senses.special; + return tags; } - // Stack identical consumables - if ( itemData.type === "consumable" && itemData.flags.core?.sourceId ) { - const similarItem = this.actor.items.find(i => { - const sourceId = i.getFlag("core", "sourceId"); - return sourceId && (sourceId === itemData.flags.core?.sourceId) && - (i.type === "consumable"); - }); - if ( similarItem ) { - return similarItem.update({ - 'data.quantity': similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1) + /* -------------------------------------------- */ + + /** + * 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, + di: CONFIG.SW5E.damageResistanceTypes, + dv: CONFIG.SW5E.damageResistanceTypes, + ci: CONFIG.SW5E.conditionTypes, + languages: CONFIG.SW5E.languages, + armorProf: CONFIG.SW5E.armorProficiencies, + weaponProf: CONFIG.SW5E.weaponProficiencies, + toolProf: CONFIG.SW5E.toolProficiencies + }; + for (let [t, choices] of Object.entries(map)) { + const trait = traits[t]; + if (!trait) continue; + let values = []; + if (trait.value) { + values = trait.value instanceof Array ? trait.value : [trait.value]; + } + trait.selected = values.reduce((obj, t) => { + obj[t] = choices[t]; + return obj; + }, {}); + + // Add custom entry + if (trait.custom) { + trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim())); + } + trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; + } + } + + /* -------------------------------------------- */ + + /** + * Insert a power into the powerbook object when rendering the character sheet + * @param {Object} data The Actor data being prepared + * @param {Array} powers The power data being prepared + * @private + */ + _preparePowerbook(data, powers) { + const owner = this.actor.isOwner; + const levels = data.data.powers; + const powerbook = {}; + + // Define some mappings + const sections = { + atwill: -20, + innate: -10, + pact: 0.5 + }; + + // Label power slot uses headers + const useLabels = { + "-20": "-", + "-10": "-", + "0": "∞" + }; + + // Format a powerbook entry for a certain indexed level + const registerSection = (sl, i, label, {prepMode = "prepared", value, max, override} = {}) => { + powerbook[i] = { + order: i, + label: label, + usesSlots: i > 0, + canCreate: owner, + canPrepare: data.actor.type === "character" && i >= 1, + powers: [], + 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 + }; + }; + + // Determine the maximum power level which has a slot + const maxLevel = Array.fromRange(10).reduce((max, i) => { + if (i === 0) return max; + const level = levels[`power${i}`]; + if ((level.max || level.override) && i > max) max = i; + return max; + }, 0); + + // 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++) { + const sl = `power${lvl}`; + registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]); + } + } + + // Pact magic users have cantrips and a pact magic section + // TODO: Check if this is needed, we've removed pacts everywhere else + if (levels.pact && levels.pact.max) { + if (!powerbook["0"]) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]); + const l = levels.pact; + const config = CONFIG.SW5E.powerPreparationModes.pact; + const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`); + const label = `${config} — ${level}`; + registerSection("pact", sections.pact, label, { + prepMode: "pact", + value: l.value, + max: l.max, + override: l.override + }); + } + + // Iterate over every power item, adding powers to the powerbook by section + powers.forEach((power) => { + const mode = power.data.preparation.mode || "prepared"; + let s = power.data.level || 0; + const sl = `power${s}`; + + // Specialized powercasting modes (if they exist) + if (mode in sections) { + s = sections[mode]; + if (!powerbook[s]) { + 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 + }); + } + } + + // 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: levels[sl]}); + } + + // Add the power to the relevant heading + powerbook[s].powers.push(power); }); - } + + // Sort the powerbook by section level + const sorted = Object.values(powerbook); + sorted.sort((a, b) => a.order - b.order); + return sorted; } - // Create the owned item as normal - return super._onDropItemCreate(itemData); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Determine whether an Owned Item will be shown based on the current set of filters + * @return {boolean} + * @private + */ + _filterItems(items, filters) { + return items.filter((item) => { + const data = item.data; - /** - * Handle enabling editing for a power slot override value - * @param {MouseEvent} event The originating click event - * @private - */ - async _onPowerSlotOverride (event) { - const span = event.currentTarget.parentElement; - const level = span.dataset.level; - const override = this.actor.data.data.powers[level].override || span.dataset.slots; - const input = document.createElement("INPUT"); - input.type = "text"; - input.name = `data.powers.${level}.override`; - input.value = override; - input.placeholder = span.dataset.slots; - input.dataset.dtype = "Number"; + // Action usage + for (let f of ["action", "bonus", "reaction"]) { + if (filters.has(f)) { + if (data.activation && data.activation.type !== f) return false; + } + } - // Replace the HTML - const parent = span.parentElement; - parent.removeChild(span); - parent.appendChild(input); - } + // Power-specific filters + if (filters.has("ritual")) { + if (data.components.ritual !== true) return false; + } + if (filters.has("concentration")) { + if (data.components.concentration !== true) return false; + } + if (filters.has("prepared")) { + if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true; + if (this.actor.data.type === "npc") return true; + return data.preparation.prepared; + } - /* -------------------------------------------- */ - - /** - * Change the uses amount of an Owned Item within the Actor - * @param {Event} event The triggering click event - * @private - */ - async _onUsesChange(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max); - event.target.value = uses; - return item.update({ 'data.uses.value': uses }); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method - * @private - */ - _onItemRoll(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - return item.roll(); - } - - /* -------------------------------------------- */ - - /** - * Handle attempting to recharge an item usage by rolling a recharge check - * @param {Event} event The originating click event - * @private - */ - _onItemRecharge(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - return item.rollRecharge(); - }; - - /* -------------------------------------------- */ - - /** - * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method - * @private - */ - _onItemSummary(event) { - event.preventDefault(); - let li = $(event.currentTarget).parents(".item"), - item = this.actor.items.get(li.data("item-id")), - chatData = item.getChatData({secrets: this.actor.isOwner}); - - // Toggle summary - if ( li.hasClass("expanded") ) { - let summary = li.children(".item-summary"); - summary.slideUp(200, () => summary.remove()); - } else { - let div = $(`
${chatData.description.value}
`); - let props = $(`
`); - chatData.properties.forEach(p => props.append(`${p}`)); - div.append(props); - li.append(div.hide()); - div.slideDown(200); + // Equipment-specific filters + if (filters.has("equipped")) { + if (data.equipped !== true) return false; + } + return true; + }); } - li.toggleClass("expanded"); - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset - * @param {Event} event The originating click event - * @private - */ - _onItemCreate(event) { - event.preventDefault(); - const header = event.currentTarget; - const type = header.dataset.type; - const itemData = { - name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}), - type: type, - data: foundry.utils.deepClone(header.dataset) - }; - delete itemData.data["type"]; - return this.actor.createEmbeddedDocuments("Item", [itemData]); - } - - /* -------------------------------------------- */ - - /** - * Handle editing an existing Owned Item for the Actor - * @param {Event} event The originating click event - * @private - */ - _onItemEdit(event) { - event.preventDefault(); - const li = event.currentTarget.closest(".item"); - const item = this.actor.items.get(li.dataset.itemId); - return item.sheet.render(true); - } - - /* -------------------------------------------- */ - - /** - * Handle deleting an existing Owned Item for the Actor - * @param {Event} event The originating click event - * @private - */ - _onItemDelete(event) { - event.preventDefault(); - const item = this.actor.items.get(li.dataset.itemId); - if ( item ) return item.delete(); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling an Ability check, either a test or a saving throw - * @param {Event} event The originating click event - * @private - */ - _onRollAbilityTest(event) { - event.preventDefault(); - let ability = event.currentTarget.parentElement.dataset.ability; - return this.actor.rollAbility(ability, {event: event}); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling a Skill check - * @param {Event} event The originating click event - * @private - */ - _onRollSkillCheck(event) { - event.preventDefault(); - const skill = event.currentTarget.parentElement.dataset.skill; - return this.actor.rollSkill(skill, {event: event}); - } - - /* -------------------------------------------- */ - - /** - * Handle toggling Ability score proficiency level - * @param {Event} event The originating click event - * @private - */ - _onToggleAbilityProficiency(event) { - event.preventDefault(); - const field = event.currentTarget.previousElementSibling; - return this.actor.update({[field.name]: 1 - parseInt(field.value)}); - } - - /* -------------------------------------------- */ - - /** - * Handle toggling of filters to display a different set of owned items - * @param {Event} event The click event which triggered the toggle - * @private - */ - _onToggleFilter(event) { - event.preventDefault(); - const li = event.currentTarget; - const set = this._filters[li.parentElement.dataset.filter]; - const filter = li.dataset.filter; - if ( set.has(filter) ) set.delete(filter); - else set.add(filter); - return this.render(); - } - - /* -------------------------------------------- */ - - /** - * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options - * @param {Event} event The click event which originated the selection - * @private - */ - _onTraitSelector(event) { - event.preventDefault(); - const a = event.currentTarget; - const label = a.parentElement.querySelector("label"); - const choices = CONFIG.SW5E[a.dataset.options]; - const options = { name: a.dataset.target, title: label.innerText, choices }; - return new TraitSelector(this.actor, options).render(true) - } - - /* -------------------------------------------- */ - - /** @override */ - _getHeaderButtons() { - let buttons = super._getHeaderButtons(); - if ( this.actor.isPolymorphed ) { - buttons.unshift({ - label: 'SW5E.PolymorphRestoreTransformation', - class: "restore-transformation", - icon: "fas fa-backward", - onclick: () => this.actor.revertOriginalForm() - }); + /** + * Get the font-awesome icon used to display a certain level of skill proficiency + * @private + */ + _getProficiencyIcon(level) { + const icons = { + 0: '', + 0.5: '', + 1: '', + 2: '' + }; + return icons[level] || icons[0]; } - return buttons; - } -} \ No newline at end of file + + /* -------------------------------------------- */ + /* Event Listeners and Handlers + /* -------------------------------------------- */ + + /** @inheritdoc */ + activateListeners(html) { + // Activate Item Filters + const filterLists = html.find(".filter-list"); + filterLists.each(this._initializeFilterItemList.bind(this)); + filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this)); + + // Item summaries + html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event)); + + // View Item Sheets + html.find(".item-edit").click(this._onItemEdit.bind(this)); + + // Editable Only Listeners + if (this.isEditable) { + // Input focus and update + const inputs = html.find("input"); + inputs.focus((ev) => ev.currentTarget.select()); + inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)); + + // Ability Proficiency + html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this)); + + // Toggle Skill Proficiency + html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this)); + + // Trait Selector + html.find(".trait-selector").click(this._onTraitSelector.bind(this)); + + // Configure Special Flags + html.find(".config-button").click(this._onConfigMenu.bind(this)); + + // Owned Item management + html.find(".item-create").click(this._onItemCreate.bind(this)); + 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((ev) => onManageActiveEffect(ev, this.actor)); + } + + // Owner Only Listeners + if (this.actor.isOwner) { + // Ability Checks + html.find(".ability-name").click(this._onRollAbilityTest.bind(this)); + + // Roll Skill Checks + html.find(".skill-name").click(this._onRollSkillCheck.bind(this)); + + // Item Rolling + html.find(".item .item-image").click((event) => this._onItemRoll(event)); + html.find(".item .item-recharge").click((event) => this._onItemRecharge(event)); + } + + // Otherwise remove rollable classes + else { + html.find(".rollable").each((i, el) => el.classList.remove("rollable")); + } + + // Handle default listeners last so system listeners are triggered first + super.activateListeners(html); + } + + /* -------------------------------------------- */ + + /** + * Iinitialize Item list filters by activating the set of filters which are currently applied + * @private + */ + _initializeFilterItemList(i, ul) { + const set = this._filters[ul.dataset.filter]; + const filters = ul.querySelectorAll(".filter-item"); + for (let li of filters) { + if (set.has(li.dataset.filter)) li.classList.add("active"); + } + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** + * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs + * @param event + * @private + */ + _onChangeInputDelta(event) { + const input = event.target; + const value = input.value; + if (["+", "-"].includes(value[0])) { + let delta = parseFloat(value); + input.value = getProperty(this.actor.data, input.name) + delta; + } else if (value[0] === "=") { + input.value = value.slice(1); + } + } + + /* -------------------------------------------- */ + + /** + * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options + * @param {Event} event The click event which originated the selection + * @private + */ + _onConfigMenu(event) { + event.preventDefault(); + const button = event.currentTarget; + let app; + switch (button.dataset.action) { + case "hit-dice": + app = new ActorHitDiceConfig(this.object); + break; + case "movement": + app = new ActorMovementConfig(this.object); + break; + case "flags": + app = new ActorSheetFlags(this.object); + break; + case "senses": + app = new ActorSensesConfig(this.object); + break; + case "type": + new ActorTypeConfig(this.object).render(true); + break; + } + app?.render(true); + } + + /* -------------------------------------------- */ + + /** + * Handle cycling proficiency in a Skill + * @param {Event} event A click or contextmenu event which triggered the handler + * @private + */ + _onCycleSkillProficiency(event) { + event.preventDefault(); + const field = $(event.currentTarget).siblings('input[type="hidden"]'); + + // Get the current level and the array of levels + const level = parseFloat(field.val()); + const levels = [0, 1, 0.5, 2]; + let idx = levels.indexOf(level); + + // Toggle next level - forward on click, backwards on right + if (event.type === "click") { + field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]); + } else if (event.type === "contextmenu") { + field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]); + } + + // Update the field value and save the form + this._onSubmit(event); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropActor(event, data) { + const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing")); + if (!canPolymorph) return false; + + // Get the target actor + let sourceActor = null; + if (data.pack) { + const pack = game.packs.find((p) => p.collection === data.pack); + sourceActor = await pack.getEntity(data.id); + } else { + sourceActor = game.actors.get(data.id); + } + if (!sourceActor) return; + + // Define a function to record polymorph settings for future use + const rememberOptions = (html) => { + const options = {}; + html.find("input").each((i, el) => { + options[el.name] = el.checked; + }); + const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options); + game.settings.set("sw5e", "polymorphSettings", settings); + return settings; + }; + + // Create and render the Dialog + return new Dialog( + { + title: game.i18n.localize("SW5E.PolymorphPromptTitle"), + content: { + options: game.settings.get("sw5e", "polymorphSettings"), + i18n: SW5E.polymorphSettings, + isToken: this.actor.isToken + }, + default: "accept", + buttons: { + accept: { + icon: '', + label: game.i18n.localize("SW5E.PolymorphAcceptSettings"), + callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html)) + }, + wildshape: { + icon: '', + label: game.i18n.localize("SW5E.PolymorphWildShape"), + callback: (html) => + this.actor.transformInto(sourceActor, { + keepBio: true, + keepClass: true, + keepMental: true, + mergeSaves: true, + mergeSkills: true, + transformTokens: rememberOptions(html).transformTokens + }) + }, + polymorph: { + icon: '', + label: game.i18n.localize("SW5E.Polymorph"), + callback: (html) => + this.actor.transformInto(sourceActor, { + transformTokens: rememberOptions(html).transformTokens + }) + }, + cancel: { + icon: '', + label: game.i18n.localize("Cancel") + } + } + }, + { + classes: ["dialog", "sw5e"], + width: 600, + template: "systems/sw5e/templates/apps/polymorph-prompt.html" + } + ).render(true); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropItemCreate(itemData) { + // Check to make sure items of this type are allowed on this actor + if (this.constructor.unsupportedItemTypes.has(itemData.type)) { + return ui.notifications.warn( + game.i18n.format("SW5E.ActorWarningInvalidItem", { + itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]), + actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type]) + }) + ); + } + + // Create a Consumable power scroll on the Inventory tab + // TODO: This is pretty non functional as the base items for the scrolls, and the powers, are not defined, maybe consider using holocrons + if (itemData.type === "power" && this._tabs[0].active === "inventory") { + const scroll = await Item5e.createScrollFromPower(itemData); + itemData = scroll.data; + } + + if (itemData.data) { + // Ignore certain statuses + ["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]); + + // Downgrade ATTUNED to REQUIRED + itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED); + } + + // Stack identical consumables + if (itemData.type === "consumable" && itemData.flags.core?.sourceId) { + const similarItem = this.actor.items.find((i) => { + const sourceId = i.getFlag("core", "sourceId"); + return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable"; + }); + if (similarItem) { + return similarItem.update({ + "data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1) + }); + } + } + + // Create the owned item as normal + return super._onDropItemCreate(itemData); + } + + /* -------------------------------------------- */ + + /** + * Handle enabling editing for a power slot override value + * @param {MouseEvent} event The originating click event + * @private + */ + async _onPowerSlotOverride(event) { + const span = event.currentTarget.parentElement; + const level = span.dataset.level; + const override = this.actor.data.data.powers[level].override || span.dataset.slots; + const input = document.createElement("INPUT"); + input.type = "text"; + input.name = `data.powers.${level}.override`; + input.value = override; + input.placeholder = span.dataset.slots; + input.dataset.dtype = "Number"; + + // Replace the HTML + const parent = span.parentElement; + parent.removeChild(span); + parent.appendChild(input); + } + + /* -------------------------------------------- */ + + /** + * Change the uses amount of an Owned Item within the Actor + * @param {Event} event The triggering click event + * @private + */ + async _onUsesChange(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max); + event.target.value = uses; + return item.update({"data.uses.value": uses}); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method + * @private + */ + _onItemRoll(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + return item.roll(); + } + + /* -------------------------------------------- */ + + /** + * Handle attempting to recharge an item usage by rolling a recharge check + * @param {Event} event The originating click event + * @private + */ + _onItemRecharge(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + return item.rollRecharge(); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method + * @private + */ + _onItemSummary(event) { + event.preventDefault(); + let li = $(event.currentTarget).parents(".item"), + item = this.actor.items.get(li.data("item-id")), + chatData = item.getChatData({secrets: this.actor.isOwner}); + + // Toggle summary + if (li.hasClass("expanded")) { + let summary = li.children(".item-summary"); + summary.slideUp(200, () => summary.remove()); + } else { + let div = $(`
${chatData.description.value}
`); + let props = $(`
`); + chatData.properties.forEach((p) => props.append(`${p}`)); + div.append(props); + li.append(div.hide()); + div.slideDown(200); + } + li.toggleClass("expanded"); + } + + /* -------------------------------------------- */ + + /** + * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset + * @param {Event} event The originating click event + * @private + */ + _onItemCreate(event) { + event.preventDefault(); + const header = event.currentTarget; + const type = header.dataset.type; + const itemData = { + name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}), + type: type, + data: foundry.utils.deepClone(header.dataset) + }; + delete itemData.data["type"]; + return this.actor.createEmbeddedDocuments("Item", [itemData]); + } + + /* -------------------------------------------- */ + + /** + * Handle editing an existing Owned Item for the Actor + * @param {Event} event The originating click event + * @private + */ + _onItemEdit(event) { + event.preventDefault(); + const li = event.currentTarget.closest(".item"); + const item = this.actor.items.get(li.dataset.itemId); + return item.sheet.render(true); + } + + /* -------------------------------------------- */ + + /** + * Handle deleting an existing Owned Item for the Actor + * @param {Event} event The originating click event + * @private + */ + _onItemDelete(event) { + event.preventDefault(); + const li = event.currentTarget.closest(".item"); + const item = this.actor.items.get(li.dataset.itemId); + if (item) return item.delete(); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling an Ability check, either a test or a saving throw + * @param {Event} event The originating click event + * @private + */ + _onRollAbilityTest(event) { + event.preventDefault(); + let ability = event.currentTarget.parentElement.dataset.ability; + return this.actor.rollAbility(ability, {event: event}); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling a Skill check + * @param {Event} event The originating click event + * @private + */ + _onRollSkillCheck(event) { + event.preventDefault(); + const skill = event.currentTarget.parentElement.dataset.skill; + return this.actor.rollSkill(skill, {event: event}); + } + + /* -------------------------------------------- */ + + /** + * Handle toggling Ability score proficiency level + * @param {Event} event The originating click event + * @private + */ + _onToggleAbilityProficiency(event) { + event.preventDefault(); + const field = event.currentTarget.previousElementSibling; + return this.actor.update({[field.name]: 1 - parseInt(field.value)}); + } + + /* -------------------------------------------- */ + + /** + * Handle toggling of filters to display a different set of owned items + * @param {Event} event The click event which triggered the toggle + * @private + */ + _onToggleFilter(event) { + event.preventDefault(); + const li = event.currentTarget; + const set = this._filters[li.parentElement.dataset.filter]; + const filter = li.dataset.filter; + if (set.has(filter)) set.delete(filter); + else set.add(filter); + return this.render(); + } + + /* -------------------------------------------- */ + + /** + * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options + * @param {Event} event The click event which originated the selection + * @private + */ + _onTraitSelector(event) { + event.preventDefault(); + const a = event.currentTarget; + const label = a.parentElement.querySelector("label"); + const choices = CONFIG.SW5E[a.dataset.options]; + const options = {name: a.dataset.target, title: label.innerText, choices}; + return new TraitSelector(this.actor, options).render(true); + } + + /* -------------------------------------------- */ + + /** @override */ + _getHeaderButtons() { + let buttons = super._getHeaderButtons(); + if (this.actor.isPolymorphed) { + buttons.unshift({ + label: "SW5E.PolymorphRestoreTransformation", + class: "restore-transformation", + icon: "fas fa-backward", + onclick: () => this.actor.revertOriginalForm() + }); + } + return buttons; + } +} diff --git a/module/actor/sheets/oldSheets/character.js b/module/actor/sheets/oldSheets/character.js index bbd41b8e..038a28b4 100644 --- a/module/actor/sheets/oldSheets/character.js +++ b/module/actor/sheets/oldSheets/character.js @@ -7,295 +7,362 @@ import Actor5e from "../../entity.js"; * @type {ActorSheet5e} */ export default class ActorSheet5eCharacter extends ActorSheet5e { + /** + * Define default rendering options for the NPC sheet + * @return {Object} + */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["sw5e", "sheet", "actor", "character"], + width: 720, + height: 736 + }); + } - /** - * Define default rendering options for the NPC sheet - * @return {Object} - */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - classes: ["sw5e", "sheet", "actor", "character"], - width: 720, - height: 736 - }); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Add some extra data when rendering the sheet to reduce the amount of logic required within the template. + */ + getData() { + const sheetData = super.getData(); - /** - * Add some extra data when rendering the sheet to reduce the amount of logic required within the template. - */ - getData() { - const sheetData = super.getData(); + // Temporary HP + let hp = sheetData.data.attributes.hp; + if (hp.temp === 0) delete hp.temp; + if (hp.tempmax === 0) delete hp.tempmax; - // Temporary HP - let hp = sheetData.data.attributes.hp; - if (hp.temp === 0) delete hp.temp; - if (hp.tempmax === 0) delete hp.tempmax; + // Resources + sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => { + const res = sheetData.data.resources[r] || {}; + res.name = r; + res.placeholder = game.i18n.localize("SW5E.Resource" + r.titleCase()); + if (res && res.value === 0) delete res.value; + if (res && res.max === 0) delete res.max; + return arr.concat([res]); + }, []); - // Resources - sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => { - const res = sheetData.data.resources[r] || {}; - res.name = r; - res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase()); - if (res && res.value === 0) delete res.value; - if (res && res.max === 0) delete res.max; - return arr.concat([res]); - }, []); + // Experience Tracking + sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); + sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", "); + sheetData["multiclassLabels"] = this.actor.itemTypes.class + .map((c) => { + return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" "); + }) + .join(", "); - // Experience Tracking - sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); - sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", "); - sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => { - return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ') - }).join(', '); + // Return data for rendering + return sheetData; + } - // Return data for rendering - return sheetData; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Organize and classify Owned Items for Character sheets + * @private + */ + _prepareItems(data) { + // Categorize items as inventory, powerbook, features, and classes + const inventory = { + weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}}, + equipment: {label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"}}, + consumable: {label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"}}, + tool: {label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"}}, + backpack: {label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"}}, + loot: {label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"}} + }; - /** - * Organize and classify Owned Items for Character sheets - * @private - */ - _prepareItems(data) { + // Partition items by category + let [ + items, + powers, + feats, + classes, + species, + archetypes, + classfeatures, + backgrounds, + fightingstyles, + fightingmasteries, + lightsaberforms + ] = data.items.reduce( + (arr, item) => { + // Item details + item.img = item.img || CONST.DEFAULT_TOKEN; + item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; + item.attunement = { + [CONFIG.SW5E.attunementTypes.REQUIRED]: { + icon: "fa-sun", + cls: "not-attuned", + title: "SW5E.AttunementRequired" + }, + [CONFIG.SW5E.attunementTypes.ATTUNED]: { + icon: "fa-sun", + cls: "attuned", + title: "SW5E.AttunementAttuned" + } + }[item.data.attunement]; - // Categorize items as inventory, powerbook, features, and classes - const inventory = { - weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} }, - equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} }, - consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} }, - tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} }, - backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} }, - loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} } - }; + // Item usage + item.hasUses = item.data.uses && item.data.uses.max > 0; + item.isOnCooldown = + item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; + item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; + item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type); - // Partition items by category - let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => { + // Item toggle state + this._prepareItemToggleState(item); - // Item details - item.img = item.img || CONST.DEFAULT_TOKEN; - item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); - item.attunement = { - [CONFIG.SW5E.attunementTypes.REQUIRED]: { - icon: "fa-sun", - cls: "not-attuned", - title: "SW5E.AttunementRequired" - }, - [CONFIG.SW5E.attunementTypes.ATTUNED]: { - icon: "fa-sun", - cls: "attuned", - title: "SW5E.AttunementAttuned" + // Primary Class + if (item.type === "class") + item.isOriginalClass = item._id === this.actor.data.data.details.originalClass; + + // Classify items into types + 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 === "archetype") arr[5].push(item); + else if (item.type === "classfeature") arr[6].push(item); + else if (item.type === "background") arr[7].push(item); + else if (item.type === "fightingstyle") arr[8].push(item); + else if (item.type === "fightingmastery") arr[9].push(item); + else if (item.type === "lightsaberform") arr[10].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); + powers = this._filterItems(powers, this._filters.powerbook); + feats = this._filterItems(feats, this._filters.features); + + // Organize items + for (let i of items) { + i.data.quantity = i.data.quantity || 0; + i.data.weight = i.data.weight || 0; + i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1); + inventory[i.type].items.push(i); } - }[item.data.attunement]; - // Item usage - item.hasUses = item.data.uses && (item.data.uses.max > 0); - item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); - item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); - item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); + // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) + const powerbook = this._preparePowerbook(data, powers); + const nPrepared = powers.filter((s) => { + return s.data.level > 0 && s.data.preparation.mode === "prepared" && s.data.preparation.prepared; + }).length; - // Item toggle state - this._prepareItemToggleState(item); - - // Primary Class - if ( item.type === "class" ) item.isOriginalClass = ( item.data._id === this.actor.data.data.details.originalClass ); - - // Classify items into types - 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 === "archetype" ) arr[5].push(item); - else if ( item.type === "classfeature" ) arr[6].push(item); - else if ( item.type === "background" ) arr[7].push(item); - else if ( item.type === "fightingstyle" ) arr[8].push(item); - else if ( item.type === "fightingmastery" ) arr[9].push(item); - else if ( item.type === "lightsaberform" ) arr[10].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); - powers = this._filterItems(powers, this._filters.powerbook); - feats = this._filterItems(feats, this._filters.features); - - // Organize items - for ( let i of items ) { - i.data.quantity = i.data.quantity || 0; - i.data.weight = i.data.weight || 0; - i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1); - inventory[i.type].items.push(i); - } - - // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) - const powerbook = this._preparePowerbook(data, powers); - const nPrepared = powers.filter(s => { - return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared; - }).length; - - // Organize Features - const features = { - classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true }, - classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, 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 }, - background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true }, - fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true }, - fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true }, - lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true }, - active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, - passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} } - }; - for ( let f of feats ) { - if ( f.data.activation.type ) features.active.items.push(f); - else features.passive.items.push(f); - } - classes.sort((a, b) => b.data.levels - a.data.levels); - features.classes.items = classes; - features.classfeatures.items = classfeatures; - features.archetype.items = archetypes; - features.species.items = species; - features.background.items = backgrounds; - features.fightingstyles.items = fightingstyles; - features.fightingmasteries.items = fightingmasteries; - features.lightsaberforms.items = lightsaberforms; - - // Assign and return - data.inventory = Object.values(inventory); - data.powerbook = powerbook; - data.preparedPowers = nPrepared; - data.features = Object.values(features); - } - - /* -------------------------------------------- */ - - /** - * A helper method to establish the displayed preparation state for an item - * @param {Item} item - * @private - */ - _prepareItemToggleState(item) { - if (item.type === "power") { - const isAlways = getProperty(item.data, "preparation.mode") === "always"; - const isPrepared = getProperty(item.data, "preparation.prepared"); - item.toggleClass = isPrepared ? "active" : ""; - if ( isAlways ) item.toggleClass = "fixed"; - if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; - else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; - else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); - } - else { - const isActive = getProperty(item.data, "equipped"); - item.toggleClass = isActive ? "active" : ""; - item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); - } - } - - /* -------------------------------------------- */ - /* Event Listeners and Handlers - /* -------------------------------------------- */ - - /** - * Activate event listeners using the prepared sheet HTML - * @param html {jQuery} The prepared HTML object ready to be rendered into the DOM - */ - activateListeners(html) { - super.activateListeners(html); - if ( !this.isEditable ) return; - - // Item State Toggling - html.find('.item-toggle').click(this._onToggleItem.bind(this)); - - // Short and Long Rest - html.find('.short-rest').click(this._onShortRest.bind(this)); - html.find('.long-rest').click(this._onLongRest.bind(this)); - - // Rollable sheet actions - html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); - } - - /* -------------------------------------------- */ - - /** - * Handle mouse click events for character sheet actions - * @param {MouseEvent} event The originating click event - * @private - */ - _onSheetAction(event) { - event.preventDefault(); - const button = event.currentTarget; - switch( button.dataset.action ) { - case "rollDeathSave": - return this.actor.rollDeathSave({event: event}); - case "rollInitiative": - return this.actor.rollInitiative({createCombatants: true}); - } - } - - /* -------------------------------------------- */ - - /** - * Handle toggling the state of an Owned Item within the Actor - * @param {Event} event The triggering click event - * @private - */ - _onToggleItem(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; - return item.update({[attr]: !getProperty(item.data, attr)}); - } - - /* -------------------------------------------- */ - - /** - * Take a short rest, calling the relevant function on the Actor instance - * @param {Event} event The triggering click event - * @private - */ - async _onShortRest(event) { - event.preventDefault(); - await this._onSubmit(event); - return this.actor.shortRest(); - } - - /* -------------------------------------------- */ - - /** - * Take a long rest, calling the relevant function on the Actor instance - * @param {Event} event The triggering click event - * @private - */ - async _onLongRest(event) { - event.preventDefault(); - await this._onSubmit(event); - return this.actor.longRest(); - } - - /* -------------------------------------------- */ - - /** @override */ - async _onDropItemCreate(itemData) { - - // Increment the number of class levels a character instead of creating a new item - if ( itemData.type === "class" ) { - const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name); - let priorLevel = cls?.data.data.levels ?? 0; - if ( !!cls ) { - const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); - if ( next > priorLevel ) { - itemData.levels = next; - return cls.update({"data.levels": next}); + // Organize Features + const features = { + classes: { + label: "SW5E.ItemTypeClassPl", + items: [], + hasActions: false, + dataset: {type: "class"}, + isClass: true + }, + classfeatures: { + label: "SW5E.ItemTypeClassFeats", + items: [], + hasActions: true, + 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 + }, + background: { + label: "SW5E.ItemTypeBackground", + items: [], + hasActions: false, + dataset: {type: "background"}, + isBackground: true + }, + fightingstyles: { + label: "SW5E.ItemTypeFightingStylePl", + items: [], + hasActions: false, + dataset: {type: "fightingstyle"}, + isFightingstyle: true + }, + fightingmasteries: { + label: "SW5E.ItemTypeFightingMasteryPl", + items: [], + hasActions: false, + dataset: {type: "fightingmastery"}, + isFightingmastery: true + }, + lightsaberforms: { + label: "SW5E.ItemTypeLightsaberFormPl", + items: [], + hasActions: false, + dataset: {type: "lightsaberform"}, + isLightsaberform: true + }, + active: { + label: "SW5E.FeatureActive", + items: [], + hasActions: true, + dataset: {"type": "feat", "activation.type": "action"} + }, + passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}} + }; + for (let f of feats) { + if (f.data.activation.type) features.active.items.push(f); + else features.passive.items.push(f); } - } + classes.sort((a, b) => b.data.levels - a.data.levels); + features.classes.items = classes; + features.classfeatures.items = classfeatures; + features.archetype.items = archetypes; + features.species.items = species; + features.background.items = backgrounds; + features.fightingstyles.items = fightingstyles; + features.fightingmasteries.items = fightingmasteries; + features.lightsaberforms.items = lightsaberforms; + + // Assign and return + data.inventory = Object.values(inventory); + data.powerbook = powerbook; + data.preparedPowers = nPrepared; + data.features = Object.values(features); } - // Default drop handling if levels were not added - return super._onDropItemCreate(itemData); - } + /* -------------------------------------------- */ + + /** + * A helper method to establish the displayed preparation state for an item + * @param {Item} item + * @private + */ + _prepareItemToggleState(item) { + if (item.type === "power") { + const isAlways = getProperty(item.data, "preparation.mode") === "always"; + const isPrepared = getProperty(item.data, "preparation.prepared"); + item.toggleClass = isPrepared ? "active" : ""; + if (isAlways) item.toggleClass = "fixed"; + if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; + else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; + else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); + } else { + const isActive = getProperty(item.data, "equipped"); + item.toggleClass = isActive ? "active" : ""; + item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); + } + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers + /* -------------------------------------------- */ + + /** + * Activate event listeners using the prepared sheet HTML + * @param html {jQuery} The prepared HTML object ready to be rendered into the DOM + */ + activateListeners(html) { + super.activateListeners(html); + if (!this.isEditable) return; + + // Item State Toggling + html.find(".item-toggle").click(this._onToggleItem.bind(this)); + + // Short and Long Rest + html.find(".short-rest").click(this._onShortRest.bind(this)); + html.find(".long-rest").click(this._onLongRest.bind(this)); + + // Rollable sheet actions + html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Handle mouse click events for character sheet actions + * @param {MouseEvent} event The originating click event + * @private + */ + _onSheetAction(event) { + event.preventDefault(); + const button = event.currentTarget; + switch (button.dataset.action) { + case "rollDeathSave": + return this.actor.rollDeathSave({event: event}); + case "rollInitiative": + return this.actor.rollInitiative({createCombatants: true}); + } + } + + /* -------------------------------------------- */ + + /** + * Handle toggling the state of an Owned Item within the Actor + * @param {Event} event The triggering click event + * @private + */ + _onToggleItem(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; + return item.update({[attr]: !getProperty(item.data, attr)}); + } + + /* -------------------------------------------- */ + + /** + * Take a short rest, calling the relevant function on the Actor instance + * @param {Event} event The triggering click event + * @private + */ + async _onShortRest(event) { + event.preventDefault(); + await this._onSubmit(event); + return this.actor.shortRest(); + } + + /* -------------------------------------------- */ + + /** + * Take a long rest, calling the relevant function on the Actor instance + * @param {Event} event The triggering click event + * @private + */ + async _onLongRest(event) { + event.preventDefault(); + await this._onSubmit(event); + return this.actor.longRest(); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropItemCreate(itemData) { + // Increment the number of class levels a character instead of creating a new item + if (itemData.type === "class") { + const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name); + let priorLevel = cls?.data.data.levels ?? 0; + if (!!cls) { + const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); + if (next > priorLevel) { + itemData.levels = next; + return cls.update({"data.levels": next}); + } + } + } + + // Default drop handling if levels were not added + return super._onDropItemCreate(itemData); + } } diff --git a/module/actor/sheets/oldSheets/npc.js b/module/actor/sheets/oldSheets/npc.js index 12e85b1f..ce848b0b 100644 --- a/module/actor/sheets/oldSheets/npc.js +++ b/module/actor/sheets/oldSheets/npc.js @@ -6,130 +6,139 @@ import ActorSheet5e from "./base.js"; * @extends {ActorSheet5e} */ export default class ActorSheet5eNPC extends ActorSheet5e { - - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - classes: ["sw5e", "sheet", "actor", "npc"], - width: 600, - height: 680 - }); - } - - /* -------------------------------------------- */ - - /** @override */ - static unsupportedItemTypes = new Set(["class"]); - - /* -------------------------------------------- */ - - /** - * Organize Owned Items for rendering the NPC sheet - * @private - */ - _prepareItems(data) { - - // Categorize Items as Features and Powers - const features = { - weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} }, - actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, - passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} }, - equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} - }; - - // Start by classifying items into groups for rendering - let [powers, other] = data.items.reduce((arr, item) => { - item.img = item.img || CONST.DEFAULT_TOKEN; - item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); - item.hasUses = item.data.uses && (item.data.uses.max > 0); - item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); - item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); - item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); - if ( item.type === "power" ) arr[0].push(item); - else arr[1].push(item); - return arr; - }, [[], []]); - - // Apply item filters - powers = this._filterItems(powers, this._filters.powerbook); - other = this._filterItems(other, this._filters.features); - - // Organize Powerbook - const powerbook = this._preparePowerbook(data, powers); - - // Organize Features - for ( let item of other ) { - if ( item.type === "weapon" ) features.weapons.items.push(item); - else if ( item.type === "feat" ) { - if ( item.data.activation.type ) features.actions.items.push(item); - else features.passive.items.push(item); - } - else features.equipment.items.push(item); + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["sw5e", "sheet", "actor", "npc"], + width: 600, + height: 680 + }); } - // Assign and return - data.features = Object.values(features); - data.powerbook = powerbook; - } + /* -------------------------------------------- */ + /** @override */ + static unsupportedItemTypes = new Set(["class"]); - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @inheritdoc */ - getData(options) { - const data = super.getData(options); + /** + * Organize Owned Items for rendering the NPC sheet + * @private + */ + _prepareItems(data) { + // Categorize Items as Features and Powers + const features = { + weapons: { + label: game.i18n.localize("SW5E.AttackPl"), + items: [], + hasActions: true, + dataset: {"type": "weapon", "weapon-type": "natural"} + }, + actions: { + label: game.i18n.localize("SW5E.ActionPl"), + items: [], + hasActions: true, + dataset: {"type": "feat", "activation.type": "action"} + }, + passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}}, + equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} + }; - // Challenge Rating - const cr = parseFloat(data.data.details.cr || 0); - const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; - data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; + // Start by classifying items into groups for rendering + let [powers, other] = data.items.reduce( + (arr, item) => { + item.img = item.img || CONST.DEFAULT_TOKEN; + item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; + item.hasUses = item.data.uses && item.data.uses.max > 0; + item.isOnCooldown = + item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; + item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; + item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type); + if (item.type === "power") arr[0].push(item); + else arr[1].push(item); + return arr; + }, + [[], []] + ); - // Creature Type - data.labels["type"] = this.actor.labels.creatureType; - return data; - } + // Apply item filters + powers = this._filterItems(powers, this._filters.powerbook); + other = this._filterItems(other, this._filters.features); - /* -------------------------------------------- */ - /* Object Updates */ - /* -------------------------------------------- */ + // Organize Powerbook + const powerbook = this._preparePowerbook(data, powers); - /** @override */ - async _updateObject(event, formData) { + // Organize Features + for (let item of other) { + if (item.type === "weapon") features.weapons.items.push(item); + else if (item.type === "feat") { + if (item.data.activation.type) features.actions.items.push(item); + else features.passive.items.push(item); + } else features.equipment.items.push(item); + } - // Format NPC Challenge Rating - const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; - let crv = "data.details.cr"; - let cr = formData[crv]; - cr = crs[cr] || parseFloat(cr); - if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); + // Assign and return + data.features = Object.values(features); + data.powerbook = powerbook; + } - // Parent ActorSheet update steps - return super._updateObject(event, formData); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ + /** @inheritdoc */ + getData(options) { + const data = super.getData(options); - /** @override */ - activateListeners(html) { - super.activateListeners(html); - html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); - } + // Challenge Rating + const cr = parseFloat(data.data.details.cr || 0); + const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; + data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; - /* -------------------------------------------- */ + // Creature Type + data.labels["type"] = this.actor.labels.creatureType; + return data; + } - /** - * Handle rolling NPC health values using the provided formula - * @param {Event} event The original click event - * @private - */ - _onRollHPFormula(event) { - event.preventDefault(); - const formula = this.actor.data.data.attributes.hp.formula; - if ( !formula ) return; - const hp = new Roll(formula).roll().total; - AudioHelper.play({src: CONFIG.sounds.dice}); - this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); - } + /* -------------------------------------------- */ + /* Object Updates */ + /* -------------------------------------------- */ + + /** @override */ + async _updateObject(event, formData) { + // Format NPC Challenge Rating + const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; + let crv = "data.details.cr"; + let cr = formData[crv]; + cr = crs[cr] || parseFloat(cr); + if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr); + + // Parent ActorSheet update steps + return super._updateObject(event, formData); + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling NPC health values using the provided formula + * @param {Event} event The original click event + * @private + */ + _onRollHPFormula(event) { + event.preventDefault(); + const formula = this.actor.data.data.attributes.hp.formula; + if (!formula) return; + const hp = new Roll(formula).roll().total; + AudioHelper.play({src: CONFIG.sounds.dice}); + this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); + } } diff --git a/module/actor/sheets/oldSheets/vehicle.js b/module/actor/sheets/oldSheets/vehicle.js index b6ec2fc8..a5c6e2ea 100644 --- a/module/actor/sheets/oldSheets/vehicle.js +++ b/module/actor/sheets/oldSheets/vehicle.js @@ -6,411 +6,427 @@ import ActorSheet5e from "./base.js"; * @type {ActorSheet5e} */ export default class ActorSheet5eVehicle extends ActorSheet5e { - /** - * Define default rendering options for the Vehicle sheet. - * @returns {Object} - */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - classes: ["sw5e", "sheet", "actor", "vehicle"], - width: 605, - height: 680 - }); - } - - /* -------------------------------------------- */ - - /** @override */ - static unsupportedItemTypes = new Set(["class"]); - - /* -------------------------------------------- */ - - - /** - * Creates a new cargo entry for a vehicle Actor. - */ - static get newCargo() { - return { - name: '', - quantity: 1 - }; - } - - /* -------------------------------------------- */ - - /** - * Compute the total weight of the vehicle's cargo. - * @param {Number} totalWeight The cumulative item weight from inventory items - * @param {Object} actorData The data object for the Actor being rendered - * @returns {{max: number, value: number, pct: number}} - * @private - */ - _computeEncumbrance(totalWeight, actorData) { - - // Compute currency weight - const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); - totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; - - // Vehicle weights are an order of magnitude greater. - totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier; - - // Compute overall encumbrance - const max = actorData.data.attributes.capacity.cargo; - const pct = Math.clamped((totalWeight * 100) / max, 0, 100); - return {value: totalWeight.toNearest(0.1), max, pct}; - } - - /* -------------------------------------------- */ - - /** @override */ - _getMovementSpeed(actorData, largestPrimary=true) { - return super._getMovementSpeed(actorData, largestPrimary); - } - - /* -------------------------------------------- */ - - /** - * Prepare items that are mounted to a vehicle and require one or more crew - * to operate. - * @private - */ - _prepareCrewedItem(item) { - - // Determine crewed status - const isCrewed = item.data.crewed; - item.toggleClass = isCrewed ? 'active' : ''; - item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`); - - // Handle crew actions - if (item.type === 'feat' && item.data.activation.type === 'crew') { - item.crew = item.data.activation.cost; - item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`); - if (item.data.cover === .5) item.cover = '½'; - else if (item.data.cover === .75) item.cover = '¾'; - else if (item.data.cover === null) item.cover = '—'; - if (item.crew < 1 || item.crew === null) item.crew = '—'; + /** + * Define default rendering options for the Vehicle sheet. + * @returns {Object} + */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["sw5e", "sheet", "actor", "vehicle"], + width: 605, + height: 680 + }); } - // Prepare vehicle weapons - if (item.type === 'equipment' || item.type === 'weapon') { - item.threshold = item.data.hp.dt ? item.data.hp.dt : '—'; - } - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @override */ + static unsupportedItemTypes = new Set(["class"]); - /** - * Organize Owned Items for rendering the Vehicle sheet. - * @private - */ - _prepareItems(data) { - const cargoColumns = [{ - label: game.i18n.localize('SW5E.Quantity'), - css: 'item-qty', - property: 'quantity', - editable: 'Number' - }]; + /* -------------------------------------------- */ - const equipmentColumns = [{ - label: game.i18n.localize('SW5E.Quantity'), - css: 'item-qty', - property: 'data.quantity' - }, { - label: game.i18n.localize('SW5E.AC'), - css: 'item-ac', - property: 'data.armor.value' - }, { - label: game.i18n.localize('SW5E.HP'), - css: 'item-hp', - property: 'data.hp.value', - editable: 'Number' - }, { - label: game.i18n.localize('SW5E.Threshold'), - css: 'item-threshold', - property: 'threshold' - }]; - - const features = { - actions: { - label: game.i18n.localize('SW5E.ActionPl'), - items: [], - crewable: true, - dataset: {type: 'feat', 'activation.type': 'crew'}, - columns: [{ - label: game.i18n.localize('SW5E.VehicleCrew'), - css: 'item-crew', - property: 'crew' - }, { - label: game.i18n.localize('SW5E.Cover'), - css: 'item-cover', - property: 'cover' - }] - }, - equipment: { - label: game.i18n.localize('SW5E.ItemTypeEquipment'), - items: [], - crewable: true, - dataset: {type: 'equipment', 'armor.type': 'vehicle'}, - columns: equipmentColumns - }, - passive: { - label: game.i18n.localize('SW5E.Features'), - items: [], - dataset: {type: 'feat'} - }, - reactions: { - label: game.i18n.localize('SW5E.ReactionPl'), - items: [], - dataset: {type: 'feat', 'activation.type': 'reaction'} - }, - weapons: { - label: game.i18n.localize('SW5E.ItemTypeWeaponPl'), - items: [], - crewable: true, - dataset: {type: 'weapon', 'weapon-type': 'siege'}, - columns: equipmentColumns - } - }; - - const cargo = { - crew: { - label: game.i18n.localize('SW5E.VehicleCrew'), - items: data.data.cargo.crew, - css: 'cargo-row crew', - editableName: true, - dataset: {type: 'crew'}, - columns: cargoColumns - }, - passengers: { - label: game.i18n.localize('SW5E.VehiclePassengers'), - items: data.data.cargo.passengers, - css: 'cargo-row passengers', - editableName: true, - dataset: {type: 'passengers'}, - columns: cargoColumns - }, - cargo: { - label: game.i18n.localize('SW5E.VehicleCargo'), - items: [], - dataset: {type: 'loot'}, - columns: [{ - label: game.i18n.localize('SW5E.Quantity'), - css: 'item-qty', - property: 'data.quantity', - editable: 'Number' - }, { - label: game.i18n.localize('SW5E.Price'), - css: 'item-price', - property: 'data.price', - editable: 'Number' - }, { - label: game.i18n.localize('SW5E.Weight'), - css: 'item-weight', - property: 'data.weight', - editable: 'Number' - }] - } - }; - - // Classify items owned by the vehicle and compute total cargo weight - let totalWeight = 0; - for (const item of data.items) { - this._prepareCrewedItem(item); - - // Handle cargo explicitly - const isCargo = item.flags.sw5e?.vehicleCargo === true; - if ( isCargo ) { - totalWeight += (item.data.weight || 0) * item.data.quantity; - cargo.cargo.items.push(item); - continue; - } - - // Handle non-cargo item types - switch ( item.type ) { - case "weapon": - features.weapons.items.push(item); - break; - case "equipment": - features.equipment.items.push(item); - break; - case "feat": - if (!item.data.activation.type || (item.data.activation.type === "none")) features.passive.items.push(item); - else if (item.data.activation.type === 'reaction') features.reactions.items.push(item); - else features.actions.items.push(item); - break; - default: - totalWeight += (item.data.weight || 0) * item.data.quantity; - cargo.cargo.items.push(item); - } + /** + * Creates a new cargo entry for a vehicle Actor. + */ + static get newCargo() { + return { + name: "", + quantity: 1 + }; } - // Update the rendering context data - data.features = Object.values(features); - data.cargo = Object.values(cargo); - data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ + /** + * Compute the total weight of the vehicle's cargo. + * @param {Number} totalWeight The cumulative item weight from inventory items + * @param {Object} actorData The data object for the Actor being rendered + * @returns {{max: number, value: number, pct: number}} + * @private + */ + _computeEncumbrance(totalWeight, actorData) { + // Compute currency weight + const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); + totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; - /** @override */ - activateListeners(html) { - super.activateListeners(html); - if (!this.isEditable) return; + // Vehicle weights are an order of magnitude greater. + totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier; - html.find('.item-toggle').click(this._onToggleItem.bind(this)); - html.find('.item-hp input') - .click(evt => evt.target.select()) - .change(this._onHPChange.bind(this)); - - html.find('.item:not(.cargo-row) input[data-property]') - .click(evt => evt.target.select()) - .change(this._onEditInSheet.bind(this)); - - html.find('.cargo-row input') - .click(evt => evt.target.select()) - .change(this._onCargoRowChange.bind(this)); - - if (this.actor.data.data.attributes.actions.stations) { - html.find('.counter.actions, .counter.action-thresholds').hide(); - } - } - - /* -------------------------------------------- */ - - /** - * Handle saving a cargo row (i.e. crew or passenger) in-sheet. - * @param event {Event} - * @returns {Promise|null} - * @private - */ - _onCargoRowChange(event) { - event.preventDefault(); - const target = event.currentTarget; - const row = target.closest('.item'); - const idx = Number(row.dataset.itemId); - const property = row.classList.contains('crew') ? 'crew' : 'passengers'; - - // Get the cargo entry - const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); - const entry = cargo[idx]; - if (!entry) return null; - - // Update the cargo value - const key = target.dataset.property || 'name'; - const type = target.dataset.dtype; - let value = target.value; - if (type === 'Number') value = Number(value); - entry[key] = value; - - // Perform the Actor update - return this.actor.update({[`data.cargo.${property}`]: cargo}); - } - - /* -------------------------------------------- */ - - /** - * Handle editing certain values like quantity, price, and weight in-sheet. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onEditInSheet(event) { - event.preventDefault(); - const itemID = event.currentTarget.closest('.item').dataset.itemId; - const item = this.actor.items.get(itemID); - const property = event.currentTarget.dataset.property; - const type = event.currentTarget.dataset.dtype; - let value = event.currentTarget.value; - switch (type) { - case 'Number': value = parseInt(value); break; - case 'Boolean': value = value === 'true'; break; - } - return item.update({[`${property}`]: value}); - } - - /* -------------------------------------------- */ - - /** - * Handle creating a new crew or passenger row. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onItemCreate(event) { - event.preventDefault(); - const target = event.currentTarget; - const type = target.dataset.type; - if (type === 'crew' || type === 'passengers') { - const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); - cargo.push(this.constructor.newCargo); - return this.actor.update({[`data.cargo.${type}`]: cargo}); - } - return super._onItemCreate(event); - } - - /* -------------------------------------------- */ - - /** - * Handle deleting a crew or passenger row. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onItemDelete(event) { - event.preventDefault(); - const row = event.currentTarget.closest('.item'); - if (row.classList.contains('cargo-row')) { - const idx = Number(row.dataset.itemId); - const type = row.classList.contains('crew') ? 'crew' : 'passengers'; - const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); - return this.actor.update({[`data.cargo.${type}`]: cargo}); + // Compute overall encumbrance + const max = actorData.data.attributes.capacity.cargo; + const pct = Math.clamped((totalWeight * 100) / max, 0, 100); + return {value: totalWeight.toNearest(0.1), max, pct}; } - return super._onItemDelete(event); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @override */ + _getMovementSpeed(actorData, largestPrimary = true) { + return super._getMovementSpeed(actorData, largestPrimary); + } - /** @override */ - async _onDropItemCreate(itemData) { - const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"]; - const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo"); - foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo); - return super._onDropItemCreate(itemData); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Prepare items that are mounted to a vehicle and require one or more crew + * to operate. + * @private + */ + _prepareCrewedItem(item) { + // Determine crewed status + const isCrewed = item.data.crewed; + item.toggleClass = isCrewed ? "active" : ""; + item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`); - /** - * Special handling for editing HP to clamp it within appropriate range. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onHPChange(event) { - event.preventDefault(); - const itemID = event.currentTarget.closest('.item').dataset.itemId; - const item = this.actor.items.get(itemID); - const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); - event.currentTarget.value = hp; - return item.update({'data.hp.value': hp}); - } + // Handle crew actions + if (item.type === "feat" && item.data.activation.type === "crew") { + item.crew = item.data.activation.cost; + item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`); + if (item.data.cover === 0.5) item.cover = "½"; + else if (item.data.cover === 0.75) item.cover = "¾"; + else if (item.data.cover === null) item.cover = "—"; + if (item.crew < 1 || item.crew === null) item.crew = "—"; + } - /* -------------------------------------------- */ + // Prepare vehicle weapons + if (item.type === "equipment" || item.type === "weapon") { + item.threshold = item.data.hp.dt ? item.data.hp.dt : "—"; + } + } - /** - * Handle toggling an item's crewed status. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onToggleItem(event) { - event.preventDefault(); - const itemID = event.currentTarget.closest('.item').dataset.itemId; - const item = this.actor.items.get(itemID); - const crewed = !!item.data.data.crewed; - return item.update({'data.crewed': !crewed}); - } -}; + /* -------------------------------------------- */ + + /** + * Organize Owned Items for rendering the Vehicle sheet. + * @private + */ + _prepareItems(data) { + const cargoColumns = [ + { + label: game.i18n.localize("SW5E.Quantity"), + css: "item-qty", + property: "quantity", + editable: "Number" + } + ]; + + const equipmentColumns = [ + { + label: game.i18n.localize("SW5E.Quantity"), + css: "item-qty", + property: "data.quantity" + }, + { + label: game.i18n.localize("SW5E.AC"), + css: "item-ac", + property: "data.armor.value" + }, + { + label: game.i18n.localize("SW5E.HP"), + css: "item-hp", + property: "data.hp.value", + editable: "Number" + }, + { + label: game.i18n.localize("SW5E.Threshold"), + css: "item-threshold", + property: "threshold" + } + ]; + + const features = { + actions: { + label: game.i18n.localize("SW5E.ActionPl"), + items: [], + crewable: true, + dataset: {"type": "feat", "activation.type": "crew"}, + columns: [ + { + label: game.i18n.localize("SW5E.VehicleCrew"), + css: "item-crew", + property: "crew" + }, + { + label: game.i18n.localize("SW5E.Cover"), + css: "item-cover", + property: "cover" + } + ] + }, + equipment: { + label: game.i18n.localize("SW5E.ItemTypeEquipment"), + items: [], + crewable: true, + dataset: {"type": "equipment", "armor.type": "vehicle"}, + columns: equipmentColumns + }, + passive: { + label: game.i18n.localize("SW5E.Features"), + items: [], + dataset: {type: "feat"} + }, + reactions: { + label: game.i18n.localize("SW5E.ReactionPl"), + items: [], + dataset: {"type": "feat", "activation.type": "reaction"} + }, + weapons: { + label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), + items: [], + crewable: true, + dataset: {"type": "weapon", "weapon-type": "siege"}, + columns: equipmentColumns + } + }; + + const cargo = { + crew: { + label: game.i18n.localize("SW5E.VehicleCrew"), + items: data.data.cargo.crew, + css: "cargo-row crew", + editableName: true, + dataset: {type: "crew"}, + columns: cargoColumns + }, + passengers: { + label: game.i18n.localize("SW5E.VehiclePassengers"), + items: data.data.cargo.passengers, + css: "cargo-row passengers", + editableName: true, + dataset: {type: "passengers"}, + columns: cargoColumns + }, + cargo: { + label: game.i18n.localize("SW5E.VehicleCargo"), + items: [], + dataset: {type: "loot"}, + columns: [ + { + label: game.i18n.localize("SW5E.Quantity"), + css: "item-qty", + property: "data.quantity", + editable: "Number" + }, + { + label: game.i18n.localize("SW5E.Price"), + css: "item-price", + property: "data.price", + editable: "Number" + }, + { + label: game.i18n.localize("SW5E.Weight"), + css: "item-weight", + property: "data.weight", + editable: "Number" + } + ] + } + }; + + // Classify items owned by the vehicle and compute total cargo weight + let totalWeight = 0; + for (const item of data.items) { + this._prepareCrewedItem(item); + + // Handle cargo explicitly + const isCargo = item.flags.sw5e?.vehicleCargo === true; + if (isCargo) { + totalWeight += (item.data.weight || 0) * item.data.quantity; + cargo.cargo.items.push(item); + continue; + } + + // Handle non-cargo item types + switch (item.type) { + case "weapon": + features.weapons.items.push(item); + break; + case "equipment": + features.equipment.items.push(item); + break; + case "feat": + if (!item.data.activation.type || item.data.activation.type === "none") + features.passive.items.push(item); + else if (item.data.activation.type === "reaction") features.reactions.items.push(item); + else features.actions.items.push(item); + break; + default: + totalWeight += (item.data.weight || 0) * item.data.quantity; + cargo.cargo.items.push(item); + } + } + + // Update the rendering context data + data.features = Object.values(features); + data.cargo = Object.values(cargo); + data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data); + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + if (!this.isEditable) return; + + html.find(".item-toggle").click(this._onToggleItem.bind(this)); + html.find(".item-hp input") + .click((evt) => evt.target.select()) + .change(this._onHPChange.bind(this)); + + html.find(".item:not(.cargo-row) input[data-property]") + .click((evt) => evt.target.select()) + .change(this._onEditInSheet.bind(this)); + + html.find(".cargo-row input") + .click((evt) => evt.target.select()) + .change(this._onCargoRowChange.bind(this)); + + if (this.actor.data.data.attributes.actions.stations) { + html.find(".counter.actions, .counter.action-thresholds").hide(); + } + } + + /* -------------------------------------------- */ + + /** + * Handle saving a cargo row (i.e. crew or passenger) in-sheet. + * @param event {Event} + * @returns {Promise|null} + * @private + */ + _onCargoRowChange(event) { + event.preventDefault(); + const target = event.currentTarget; + const row = target.closest(".item"); + const idx = Number(row.dataset.itemId); + const property = row.classList.contains("crew") ? "crew" : "passengers"; + + // Get the cargo entry + const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); + const entry = cargo[idx]; + if (!entry) return null; + + // Update the cargo value + const key = target.dataset.property || "name"; + const type = target.dataset.dtype; + let value = target.value; + if (type === "Number") value = Number(value); + entry[key] = value; + + // Perform the Actor update + return this.actor.update({[`data.cargo.${property}`]: cargo}); + } + + /* -------------------------------------------- */ + + /** + * Handle editing certain values like quantity, price, and weight in-sheet. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onEditInSheet(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const property = event.currentTarget.dataset.property; + const type = event.currentTarget.dataset.dtype; + let value = event.currentTarget.value; + switch (type) { + case "Number": + value = parseInt(value); + break; + case "Boolean": + value = value === "true"; + break; + } + return item.update({[`${property}`]: value}); + } + + /* -------------------------------------------- */ + + /** + * Handle creating a new crew or passenger row. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onItemCreate(event) { + event.preventDefault(); + const target = event.currentTarget; + const type = target.dataset.type; + if (type === "crew" || type === "passengers") { + const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); + cargo.push(this.constructor.newCargo); + return this.actor.update({[`data.cargo.${type}`]: cargo}); + } + return super._onItemCreate(event); + } + + /* -------------------------------------------- */ + + /** + * Handle deleting a crew or passenger row. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onItemDelete(event) { + event.preventDefault(); + const row = event.currentTarget.closest(".item"); + if (row.classList.contains("cargo-row")) { + const idx = Number(row.dataset.itemId); + const type = row.classList.contains("crew") ? "crew" : "passengers"; + const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); + return this.actor.update({[`data.cargo.${type}`]: cargo}); + } + + return super._onItemDelete(event); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropItemCreate(itemData) { + const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"]; + const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo"; + foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo); + return super._onDropItemCreate(itemData); + } + + /* -------------------------------------------- */ + + /** + * Special handling for editing HP to clamp it within appropriate range. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onHPChange(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); + event.currentTarget.value = hp; + return item.update({"data.hp.value": hp}); + } + + /* -------------------------------------------- */ + + /** + * Handle toggling an item's crewed status. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onToggleItem(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const crewed = !!item.data.data.crewed; + return item.update({"data.crewed": !crewed}); + } +} diff --git a/module/apps/ability-use-dialog.js b/module/apps/ability-use-dialog.js index ca9067d3..6c43775d 100644 --- a/module/apps/ability-use-dialog.js +++ b/module/apps/ability-use-dialog.js @@ -3,220 +3,225 @@ * @type {Dialog} */ export default class AbilityUseDialog extends Dialog { - constructor(item, dialogData={}, options={}) { - super(dialogData, options); - this.options.classes = ["sw5e", "dialog"]; + constructor(item, dialogData = {}, options = {}) { + super(dialogData, options); + this.options.classes = ["sw5e", "dialog"]; + + /** + * Store a reference to the Item entity being used + * @type {Item5e} + */ + this.item = item; + } + + /* -------------------------------------------- */ + /* Rendering */ + /* -------------------------------------------- */ /** - * Store a reference to the Item entity being used - * @type {Item5e} + * 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 {Item5e} item + * @return {Promise} */ - this.item = item; - } + static async create(item) { + if (!item.isOwned) throw new Error("You cannot display an ability usage dialog for an unowned item"); - /* -------------------------------------------- */ - /* Rendering */ - /* -------------------------------------------- */ + // Prepare data + const actorData = item.actor.data.data; + const itemData = item.data.data; + const uses = itemData.uses || {}; + const quantity = itemData.quantity || 0; + const recharge = itemData.recharge || {}; + const recharges = !!recharge.value; + const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0; - /** - * 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 {Item5e} item - * @return {Promise} - */ - static async create(item) { - if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item"); + // Prepare dialog form data + const data = { + item: item.data, + title: game.i18n.format("SW5E.AbilityUseHint", { + type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), + name: item.name + }), + note: this._getAbilityUseNote(item.data, uses, recharge), + consumePowerSlot: false, + consumeRecharge: recharges, + consumeResource: !!itemData.consume.target, + consumeUses: uses.per && uses.max > 0, + canUse: recharges ? recharge.charged : sufficientUses, + createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget, + errors: [] + }; + if (item.data.type === "power") this._getPowerData(actorData, itemData, data); - // Prepare data - const actorData = item.actor.data.data; - const itemData = item.data.data; - const uses = itemData.uses || {}; - const quantity = itemData.quantity || 0; - const recharge = itemData.recharge || {}; - const recharges = !!recharge.value; - const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0; + // Render the ability usage template + const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data); - // Prepare dialog form data - const data = { - item: item.data, - title: game.i18n.format("SW5E.AbilityUseHint", item.data), - note: this._getAbilityUseNote(item.data, uses, recharge), - consumePowerSlot: false, - consumeRecharge: recharges, - consumeResource: !!itemData.consume.target, - consumeUses: uses.per && (uses.max > 0), - canUse: recharges ? recharge.charged : sufficientUses, - createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget, - errors: [] - }; - if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data); + // Create the Dialog and return data as a Promise + 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}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`, + content: html, + buttons: { + use: { + icon: ``, + label: label, + callback: (html) => { + const fd = new FormDataExtended(html[0].querySelector("form")); + resolve(fd.toObject()); + } + } + }, + default: "use", + close: () => resolve(null) + }); + dlg.render(true); + }); + } - // Render the ability usage template - const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data); + /* -------------------------------------------- */ + /* Helpers */ + /* -------------------------------------------- */ - // Create the Dialog and return data as a Promise - 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`, - content: html, - buttons: { - use: { - icon: ``, - label: label, - callback: html => { - const fd = new FormDataExtended(html[0].querySelector("form")); - resolve(fd.toObject()); + /** + * Get dialog data related to limited power slots + * @private + */ + static _getPowerData(actorData, itemData, data) { + // Determine whether the power may be up-cast + const lvl = itemData.level; + const consumePowerSlot = lvl > 0 && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode); + + // If can't upcast, return early and don't bother calculating available power slots + if (!consumePowerSlot) { + mergeObject(data, {isPower: true, consumePowerSlot}); + return; + } + + // Determine the levels which are feasible + let lmax = 0; + let points; + let powerType; + switch (itemData.school) { + case "lgt": + case "uni": + case "drk": { + powerType = "force"; + points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp; + break; + } + case "tec": { + powerType = "tech"; + points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp; + break; } - } - }, - default: "use", - close: () => resolve(null) - }); - dlg.render(true); - }); - } - - /* -------------------------------------------- */ - /* Helpers */ - /* -------------------------------------------- */ - - /** - * Get dialog data related to limited power slots - * @private - */ - static _getPowerData(actorData, itemData, data) { - - // Determine whether the power may be up-cast - const lvl = itemData.level; - const consumePowerSlot = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode); - - // If can't upcast, return early and don't bother calculating available power slots - if (!consumePowerSlot) { - mergeObject(data, { isPower: true, consumePowerSlot }); - return; - } - - // Determine the levels which are feasible - let lmax = 0; - let points; - let powerType; - switch (itemData.school){ - case "lgt": - case "uni": - case "drk": { - powerType = "force" - points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp; - break; - } - case "tec": { - powerType = "tech" - points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp; - break; - } - } - - // eliminate point usage for innate casters - if (actorData.attributes.powercasting === 'innate') points = 999; - - - let powerLevels - if (powerType === "force"){ - powerLevels = Array.fromRange(10).reduce((arr, i) => { - if ( i < lvl ) return arr; - const label = CONFIG.SW5E.powerLevels[i]; - const l = actorData.powers["power"+i] || {fmax: 0, foverride: null}; - let max = parseInt(l.foverride || l.fmax || 0); - let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max); - if ( max > 0 ) lmax = i; - if ((max > 0) && (slots > 0) && (points > i)){ - arr.push({ - level: i, - label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label, - canCast: max > 0, - hasSlots: slots > 0 - }); } - return arr; - }, []).filter(sl => sl.level <= lmax); - }else if (powerType === "tech"){ - powerLevels = Array.fromRange(10).reduce((arr, i) => { - if ( i < lvl ) return arr; - const label = CONFIG.SW5E.powerLevels[i]; - const l = actorData.powers["power"+i] || {tmax: 0, toverride: null}; - let max = parseInt(l.override || l.tmax || 0); - let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max); - if ( max > 0 ) lmax = i; - if ((max > 0) && (slots > 0) && (points > i)){ - arr.push({ - level: i, - label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label, - canCast: max > 0, - hasSlots: slots > 0 - }); + + // eliminate point usage for innate casters + if (actorData.attributes.powercasting === "innate") points = 999; + + let powerLevels; + if (powerType === "force") { + powerLevels = Array.fromRange(10) + .reduce((arr, i) => { + if (i < lvl) return arr; + const label = CONFIG.SW5E.powerLevels[i]; + const l = actorData.powers["power" + i] || {fmax: 0, foverride: null}; + let max = parseInt(l.foverride || l.fmax || 0); + let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max); + if (max > 0) lmax = i; + if (max > 0 && slots > 0 && points > i) { + arr.push({ + level: i, + label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label, + canCast: max > 0, + hasSlots: slots > 0 + }); + } + return arr; + }, []) + .filter((sl) => sl.level <= lmax); + } else if (powerType === "tech") { + powerLevels = Array.fromRange(10) + .reduce((arr, i) => { + if (i < lvl) return arr; + const label = CONFIG.SW5E.powerLevels[i]; + const l = actorData.powers["power" + i] || {tmax: 0, toverride: null}; + let max = parseInt(l.override || l.tmax || 0); + let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max); + if (max > 0) lmax = i; + if (max > 0 && slots > 0 && points > i) { + arr.push({ + level: i, + label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label, + canCast: max > 0, + hasSlots: slots > 0 + }); + } + return arr; + }, []) + .filter((sl) => sl.level <= lmax); } - return arr; - }, []).filter(sl => sl.level <= lmax); - } - - - const canCast = powerLevels.some(l => l.hasSlots); - if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", { - level: CONFIG.SW5E.powerLevels[lvl], - name: data.item.name - })); + const canCast = powerLevels.some((l) => l.hasSlots); + if (!canCast) + data.errors.push( + game.i18n.format("SW5E.PowerCastNoSlots", { + level: CONFIG.SW5E.powerLevels[lvl], + name: data.item.name + }) + ); - // Merge power casting data - return foundry.utils.mergeObject(data, { isPower: true, consumePowerSlot, powerLevels }); - } - - /* -------------------------------------------- */ - - /** - * Get the ability usage note that is displayed - * @private - */ - static _getAbilityUseNote(item, uses, recharge) { - - // Zero quantity - const quantity = item.data.quantity; - if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint"); - - // Abilities which use Recharge - if ( !!recharge.value ) { - return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", { - type: item.type, - }) + // Merge power casting data + return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels}); } - // Does not use any resource - if ( !uses.per || !uses.max ) return ""; + /* -------------------------------------------- */ - // Consumables - if ( item.type === "consumable" ) { - let str = "SW5E.AbilityUseNormalHint"; - if ( uses.value > 1 ) str = "SW5E.AbilityUseConsumableChargeHint"; - else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint"; - else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint"; - return game.i18n.format(str, { - type: item.data.consumableType, - value: uses.value, - quantity: item.data.quantity, - max: uses.max, - per: CONFIG.SW5E.limitedUsePeriods[uses.per] - }); - } + /** + * Get the ability usage note that is displayed + * @private + */ + static _getAbilityUseNote(item, uses, recharge) { + // Zero quantity + const quantity = item.data.quantity; + if (quantity <= 0) return game.i18n.localize("SW5E.AbilityUseUnavailableHint"); - // Other Items - else { - return game.i18n.format("SW5E.AbilityUseNormalHint", { - type: item.type, - value: uses.value, - max: uses.max, - per: CONFIG.SW5E.limitedUsePeriods[uses.per] - }); + // Abilities which use Recharge + if (!!recharge.value) { + return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", { + type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`) + }); + } + + // Does not use any resource + if (!uses.per || !uses.max) return ""; + + // Consumables + if (item.type === "consumable") { + let str = "SW5E.AbilityUseNormalHint"; + if (uses.value > 1) str = "SW5E.AbilityUseConsumableChargeHint"; + else if (item.data.quantity === 1 && uses.autoDestroy) str = "SW5E.AbilityUseConsumableDestroyHint"; + else if (item.data.quantity > 1) str = "SW5E.AbilityUseConsumableQuantityHint"; + return game.i18n.format(str, { + type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`), + value: uses.value, + quantity: item.data.quantity, + max: uses.max, + per: CONFIG.SW5E.limitedUsePeriods[uses.per] + }); + } + + // Other Items + else { + return game.i18n.format("SW5E.AbilityUseNormalHint", { + type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), + value: uses.value, + max: uses.max, + per: CONFIG.SW5E.limitedUsePeriods[uses.per] + }); + } } - } } diff --git a/module/apps/actor-flags.js b/module/apps/actor-flags.js index d4599996..8f5255a5 100644 --- a/module/apps/actor-flags.js +++ b/module/apps/actor-flags.js @@ -1,137 +1,139 @@ /** * An application class which provides advanced configuration for special character flags which modify an Actor - * @extends {DocumentSheet} + * @implements {DocumentSheet} */ export default class ActorSheetFlags extends DocumentSheet { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - id: "actor-flags", - classes: ["sw5e"], - template: "systems/sw5e/templates/apps/actor-flags.html", - width: 500, - closeOnSubmit: true - }); - } - - /* -------------------------------------------- */ - - /** @override */ - get title() { - return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`; - } - - /* -------------------------------------------- */ - - /** @override */ - getData() { - const data = {}; - data.actor = this.object; - data.classes = this._getClasses(); - data.flags = this._getFlags(); - data.bonuses = this._getBonuses(); - return data; - } - - /* -------------------------------------------- */ - - /** - * Prepare an object of sorted classes. - * @return {object} - * @private - */ - _getClasses() { - const classes = this.object.items.filter(i => i.type === "class"); - return classes.sort((a, b) => a.name.localeCompare(b.name)).reduce((obj, i) => { - obj[i.id] = i.name; - return obj; - }, {}); - } - - /* -------------------------------------------- */ - - /** - * Prepare an object of flags data which groups flags by section - * Add some additional data for rendering - * @return {object} - * @private - */ - _getFlags() { - const flags = {}; - const baseData = this.document.toJSON(); - for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) { - if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {}; - let flag = foundry.utils.deepClone(v); - flag.type = v.type.name; - flag.isCheckbox = v.type === Boolean; - flag.isSelect = v.hasOwnProperty('choices'); - flag.value = getProperty(baseData.flags, `sw5e.${k}`); - flags[v.section][`flags.sw5e.${k}`] = flag; + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: "actor-flags", + classes: ["sw5e"], + template: "systems/sw5e/templates/apps/actor-flags.html", + width: 500, + closeOnSubmit: true + }); } - return flags; - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Get the bonuses fields and their localization strings - * @return {Array} - * @private - */ - _getBonuses() { - const bonuses = [ - {name: "data.bonuses.mwak.attack", label: "SW5E.BonusMWAttack"}, - {name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"}, - {name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"}, - {name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"}, - {name: "data.bonuses.mpak.attack", label: "SW5E.BonusMPAttack"}, - {name: "data.bonuses.mpak.damage", label: "SW5E.BonusMPDamage"}, - {name: "data.bonuses.rpak.attack", label: "SW5E.BonusRPAttack"}, - {name: "data.bonuses.rpak.damage", label: "SW5E.BonusRPDamage"}, - {name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"}, - {name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"}, - {name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"}, - {name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"}, - {name: "data.bonuses.power.forceLightDC", label: "SW5E.BonusForceLightPowerDC"}, - {name: "data.bonuses.power.forceDarkDC", label: "SW5E.BonusForceDarkPowerDC"}, - {name: "data.bonuses.power.forceUnivDC", label: "SW5E.BonusForceUnivPowerDC"}, - {name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"} - ]; - for ( let b of bonuses ) { - b.value = getProperty(this.object._data, b.name) || ""; + /** @override */ + get title() { + return `${game.i18n.localize("SW5E.FlagsTitle")}: ${this.object.name}`; } - return bonuses; - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - async _updateObject(event, formData) { - const actor = this.object; - let updateData = expandObject(formData); + /** @override */ + getData() { + const data = {}; + data.actor = this.object; + data.classes = this._getClasses(); + data.flags = this._getFlags(); + data.bonuses = this._getBonuses(); + return data; + } - // Unset any flags which are "false" - let unset = false; - const flags = updateData.flags.sw5e; - //clone flags to dnd5e for module compatability - updateData.flags.dnd5e = updateData.flags.sw5e - for ( let [k, v] of Object.entries(flags) ) { - if ( [undefined, null, "", false, 0].includes(v) ) { - delete flags[k]; - if ( hasProperty(actor._data.flags, `sw5e.${k}`) ) { - unset = true; - flags[`-=${k}`] = null; + /* -------------------------------------------- */ + + /** + * Prepare an object of sorted classes. + * @return {object} + * @private + */ + _getClasses() { + const classes = this.object.items.filter((i) => i.type === "class"); + return classes + .sort((a, b) => a.name.localeCompare(b.name)) + .reduce((obj, i) => { + obj[i.id] = i.name; + return obj; + }, {}); + } + + /* -------------------------------------------- */ + + /** + * Prepare an object of flags data which groups flags by section + * Add some additional data for rendering + * @return {object} + * @private + */ + _getFlags() { + const flags = {}; + const baseData = this.document.toJSON(); + for (let [k, v] of Object.entries(CONFIG.SW5E.characterFlags)) { + if (!flags.hasOwnProperty(v.section)) flags[v.section] = {}; + let flag = foundry.utils.deepClone(v); + flag.type = v.type.name; + flag.isCheckbox = v.type === Boolean; + flag.isSelect = v.hasOwnProperty("choices"); + flag.value = getProperty(baseData.flags, `sw5e.${k}`); + flags[v.section][`flags.sw5e.${k}`] = flag; } - } + return flags; } - // Clear any bonuses which are whitespace only - for ( let b of Object.values(updateData.data.bonuses ) ) { - for ( let [k, v] of Object.entries(b) ) { - b[k] = v.trim(); - } + /* -------------------------------------------- */ + + /** + * Get the bonuses fields and their localization strings + * @return {Array} + * @private + */ + _getBonuses() { + const bonuses = [ + {name: "data.bonuses.mwak.attack", label: "SW5E.BonusMWAttack"}, + {name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"}, + {name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"}, + {name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"}, + {name: "data.bonuses.mpak.attack", label: "SW5E.BonusMPAttack"}, + {name: "data.bonuses.mpak.damage", label: "SW5E.BonusMPDamage"}, + {name: "data.bonuses.rpak.attack", label: "SW5E.BonusRPAttack"}, + {name: "data.bonuses.rpak.damage", label: "SW5E.BonusRPDamage"}, + {name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"}, + {name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"}, + {name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"}, + {name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"}, + {name: "data.bonuses.power.forceLightDC", label: "SW5E.BonusForceLightPowerDC"}, + {name: "data.bonuses.power.forceDarkDC", label: "SW5E.BonusForceDarkPowerDC"}, + {name: "data.bonuses.power.forceUnivDC", label: "SW5E.BonusForceUnivPowerDC"}, + {name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"} + ]; + for (let b of bonuses) { + b.value = getProperty(this.object._data, b.name) || ""; + } + return bonuses; } - // Diff the data against any applied overrides and apply - await actor.update(updateData, {diff: false}); - } + /* -------------------------------------------- */ + + /** @override */ + async _updateObject(event, formData) { + const actor = this.object; + let updateData = expandObject(formData); + + // Unset any flags which are "false" + let unset = false; + const flags = updateData.flags.sw5e; + //clone flags to dnd5e for module compatability + updateData.flags.dnd5e = updateData.flags.sw5e; + for (let [k, v] of Object.entries(flags)) { + if ([undefined, null, "", false, 0].includes(v)) { + delete flags[k]; + if (hasProperty(actor._data.flags, `sw5e.${k}`)) { + unset = true; + flags[`-=${k}`] = null; + } + } + } + + // Clear any bonuses which are whitespace only + for (let b of Object.values(updateData.data.bonuses)) { + for (let [k, v] of Object.entries(b)) { + b[k] = v.trim(); + } + } + + // Diff the data against any applied overrides and apply + await actor.update(updateData, {diff: false}); + } } diff --git a/module/apps/actor-type.js b/module/apps/actor-type.js index ad56e72b..7ad1ffe9 100644 --- a/module/apps/actor-type.js +++ b/module/apps/actor-type.js @@ -5,7 +5,6 @@ import Actor5e from "../actor/entity.js"; * @extends {FormApplication} */ export default class ActorTypeConfig extends FormApplication { - /** @inheritdoc */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { @@ -32,23 +31,23 @@ export default class ActorTypeConfig extends FormApplication { /** @override */ getData(options) { - // Get current value or new default - let attr = foundry.utils.getProperty(this.object.data.data, 'details.type'); - if ( foundry.utils.getType(attr) !== "Object" ) attr = { - value: (attr in CONFIG.SW5E.creatureTypes) ? attr : "humanoid", - subtype: "", - swarm: "", - custom: "" - }; + let attr = foundry.utils.getProperty(this.object.data.data, "details.type"); + if (foundry.utils.getType(attr) !== "Object") + attr = { + value: attr in CONFIG.SW5E.creatureTypes ? attr : "humanoid", + subtype: "", + swarm: "", + custom: "" + }; // Populate choices const types = {}; - for ( let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes) ) { + for (let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes)) { types[k] = { label: game.i18n.localize(v), chosen: attr.value === k - } + }; } // Return data for rendering @@ -61,12 +60,14 @@ export default class ActorTypeConfig extends FormApplication { }, subtype: attr.subtype, swarm: attr.swarm, - sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes)).reverse().reduce((obj, e) => { - obj[e[0]] = e[1]; - return obj; - }, {}), + sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes)) + .reverse() + .reduce((obj, e) => { + obj[e[0]] = e[1]; + return obj; + }, {}), preview: Actor5e.formatCreatureType(attr) || "–" - } + }; } /* -------------------------------------------- */ @@ -74,7 +75,7 @@ export default class ActorTypeConfig extends FormApplication { /** @override */ async _updateObject(event, formData) { const typeObject = foundry.utils.expandObject(formData); - return this.object.update({ 'data.details.type': typeObject }); + return this.object.update({"data.details.type": typeObject}); } /* -------------------------------------------- */ diff --git a/module/apps/hit-dice-config.js b/module/apps/hit-dice-config.js index d36d6bc2..f4fdf276 100644 --- a/module/apps/hit-dice-config.js +++ b/module/apps/hit-dice-config.js @@ -3,7 +3,6 @@ * @implements {DocumentSheet} */ export default class ActorHitDiceConfig extends DocumentSheet { - /** @override */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { @@ -26,20 +25,22 @@ export default class ActorHitDiceConfig extends DocumentSheet { /** @override */ getData(options) { return { - classes: this.object.items.reduce((classes, item) => { - if (item.data.type === "class") { - // Add the appropriate data only if this item is a "class" - classes.push({ - classItemId: item.data._id, - name: item.data.name, - diceDenom: item.data.data.hitDice, - currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed, - maxHitDice: item.data.data.levels, - canRoll: (item.data.data.levels - item.data.data.hitDiceUsed) > 0 - }); - } - return classes; - }, []).sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1))) + classes: this.object.items + .reduce((classes, item) => { + if (item.data.type === "class") { + // Add the appropriate data only if this item is a "class" + classes.push({ + classItemId: item.data._id, + name: item.data.name, + diceDenom: item.data.data.hitDice, + currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed, + maxHitDice: item.data.data.levels, + canRoll: item.data.data.levels - item.data.data.hitDiceUsed > 0 + }); + } + return classes; + }, []) + .sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1))) }; } @@ -50,7 +51,7 @@ export default class ActorHitDiceConfig extends DocumentSheet { super.activateListeners(html); // Hook up -/+ buttons to adjust the current value in the form - html.find("button.increment,button.decrement").click(event => { + html.find("button.increment,button.decrement").click((event) => { const button = event.currentTarget; const current = button.parentElement.querySelector(".current"); const max = button.parentElement.querySelector(".max"); @@ -67,8 +68,8 @@ export default class ActorHitDiceConfig extends DocumentSheet { async _updateObject(event, formData) { const actorItems = this.object.items; const classUpdates = Object.entries(formData).map(([id, hd]) => ({ - _id: id, - "data.hitDiceUsed": actorItems.get(id).data.data.levels - hd, + "_id": id, + "data.hitDiceUsed": actorItems.get(id).data.data.levels - hd })); return this.object.updateEmbeddedDocuments("Item", classUpdates); } diff --git a/module/apps/long-rest.js b/module/apps/long-rest.js index 6287e0f8..a34e53c7 100644 --- a/module/apps/long-rest.js +++ b/module/apps/long-rest.js @@ -3,65 +3,65 @@ * @extends {Dialog} */ export default class LongRestDialog extends Dialog { - constructor(actor, dialogData = {}, options = {}) { - super(dialogData, options); - this.actor = actor; - } + constructor(actor, dialogData = {}, options = {}) { + super(dialogData, options); + this.actor = actor; + } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - template: "systems/sw5e/templates/apps/long-rest.html", - classes: ["sw5e", "dialog"] - }); - } + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + template: "systems/sw5e/templates/apps/long-rest.html", + classes: ["sw5e", "dialog"] + }); + } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - getData() { - const data = super.getData(); - const variant = game.settings.get("sw5e", "restVariant"); - data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week - data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours) - return data; - } + /** @override */ + getData() { + const data = super.getData(); + const variant = game.settings.get("sw5e", "restVariant"); + data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week + data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours) + return data; + } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's - * workflow has been resolved. - * @param {Actor5e} actor - * @return {Promise} - */ - static async longRestDialog({ actor } = {}) { - return new Promise((resolve, reject) => { - const dlg = new this(actor, { - title: "Long Rest", - buttons: { - rest: { - icon: '', - label: "Rest", - callback: html => { - let newDay = true; - if (game.settings.get("sw5e", "restVariant") !== "gritty") - newDay = html.find('input[name="newDay"]')[0].checked; - resolve(newDay); - } - }, - cancel: { - icon: '', - label: "Cancel", - callback: reject - } - }, - default: 'rest', - close: reject - }); - dlg.render(true); - }); - } -} \ No newline at end of file + /** + * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's + * workflow has been resolved. + * @param {Actor5e} actor + * @return {Promise} + */ + static async longRestDialog({actor} = {}) { + return new Promise((resolve, reject) => { + const dlg = new this(actor, { + title: game.i18n.localize("SW5E.LongRest"), + buttons: { + rest: { + icon: '', + label: game.i18n.localize("SW5E.Rest"), + callback: (html) => { + let newDay = true; + if (game.settings.get("sw5e", "restVariant") !== "gritty") + newDay = html.find('input[name="newDay"]')[0].checked; + resolve(newDay); + } + }, + cancel: { + icon: '', + label: game.i18n.localize("Cancel"), + callback: reject + } + }, + default: "rest", + close: reject + }); + dlg.render(true); + }); + } +} diff --git a/module/apps/movement-config.js b/module/apps/movement-config.js index c507965d..3c43170e 100644 --- a/module/apps/movement-config.js +++ b/module/apps/movement-config.js @@ -3,37 +3,36 @@ * @extends {DocumentSheet} */ export default class ActorMovementConfig extends DocumentSheet { - - /** @override */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - classes: ["sw5e"], - template: "systems/sw5e/templates/apps/movement-config.html", - width: 300, - height: "auto" - }); - } - - /* -------------------------------------------- */ - - /** @override */ - get title() { - return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`; - } - - /* -------------------------------------------- */ - - /** @override */ - getData(options) { - const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {}; - const data = { - movement: foundry.utils.deepClone(sourceMovement), - units: CONFIG.SW5E.movementUnits - }; - for ( let [k, v] of Object.entries(data.movement) ) { - if ( ["units", "hover"].includes(k) ) continue; - data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0; + /** @override */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["sw5e"], + template: "systems/sw5e/templates/apps/movement-config.html", + width: 300, + height: "auto" + }); + } + + /* -------------------------------------------- */ + + /** @override */ + get title() { + return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`; + } + + /* -------------------------------------------- */ + + /** @override */ + getData(options) { + const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {}; + const data = { + movement: foundry.utils.deepClone(sourceMovement), + units: CONFIG.SW5E.movementUnits + }; + for (let [k, v] of Object.entries(data.movement)) { + if (["units", "hover"].includes(k)) continue; + data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0; + } + return data; } - return data; - } } diff --git a/module/apps/select-items-prompt.js b/module/apps/select-items-prompt.js index 0eac6497..cd99a278 100644 --- a/module/apps/select-items-prompt.js +++ b/module/apps/select-items-prompt.js @@ -3,7 +3,7 @@ * @type {Dialog} */ export default class SelectItemsPrompt extends Dialog { - constructor(items, dialogData={}, options={}) { + constructor(items, dialogData = {}, options = {}) { super(dialogData, options); this.options.classes = ["sw5e", "dialog", "select-items-prompt", "sheet"]; @@ -18,11 +18,11 @@ export default class SelectItemsPrompt extends Dialog { super.activateListeners(html); // render the item's sheet if its image is clicked - html.on('click', '.item-image', (event) => { + html.on("click", ".item-image", (event) => { const item = this.items.find((feature) => feature.id === event.currentTarget.dataset?.itemId); item?.sheet.render(true); - }) + }); } /** @@ -33,29 +33,27 @@ export default class SelectItemsPrompt extends Dialog { * @param {string} options.hint - Localized hint to display at the top of the prompt * @return {Promise} - list of item ids which the user has selected */ - static async create(items, { - hint - }) { + static async create(items, {hint}) { // Render the ability usage template const html = await renderTemplate("systems/sw5e/templates/apps/select-items-prompt.html", {items, hint}); return new Promise((resolve) => { const dlg = new this(items, { - title: game.i18n.localize('SW5E.SelectItemsPromptTitle'), + title: game.i18n.localize("SW5E.SelectItemsPromptTitle"), content: html, buttons: { apply: { icon: ``, - label: game.i18n.localize('SW5E.Apply'), - callback: html => { + label: game.i18n.localize("SW5E.Apply"), + callback: (html) => { const fd = new FormDataExtended(html[0].querySelector("form")).toObject(); - const selectedIds = Object.keys(fd).filter(itemId => fd[itemId]); + const selectedIds = Object.keys(fd).filter((itemId) => fd[itemId]); resolve(selectedIds); } }, cancel: { icon: '', - label: game.i18n.localize('SW5E.Skip'), + label: game.i18n.localize("SW5E.Skip"), callback: () => resolve([]) } }, diff --git a/module/apps/senses-config.js b/module/apps/senses-config.js index 69309493..e12e5478 100644 --- a/module/apps/senses-config.js +++ b/module/apps/senses-config.js @@ -1,43 +1,43 @@ /** - * A simple form to set actor movement speeds + * A simple form to set Actor movement speeds. * @extends {DocumentSheet} */ export default class ActorSensesConfig extends DocumentSheet { - - /** @inheritdoc */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - classes: ["sw5e"], - template: "systems/sw5e/templates/apps/senses-config.html", - width: 300, - height: "auto" - }); - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - get title() { - return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`; - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - getData(options) { - const senses = foundry.utils.getProperty(this.document.data._source, "data.attributes.senses") || {}; - const data = { - senses: {}, - special: senses.special ?? "", - units: senses.units, movementUnits: CONFIG.SW5E.movementUnits - }; - for ( let [name, label] of Object.entries(CONFIG.SW5E.senses) ) { - const v = senses[name]; - data.senses[name] = { - label: game.i18n.localize(label), - value: Number.isNumeric(v) ? v.toNearest(0.1) : 0 - } + /** @inheritdoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["sw5e"], + template: "systems/sw5e/templates/apps/senses-config.html", + width: 300, + height: "auto" + }); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + get title() { + return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`; + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + getData(options) { + const senses = foundry.utils.getProperty(this.document.data._source, "data.attributes.senses") || {}; + const data = { + senses: {}, + special: senses.special ?? "", + units: senses.units, + movementUnits: CONFIG.SW5E.movementUnits + }; + for (let [name, label] of Object.entries(CONFIG.SW5E.senses)) { + const v = senses[name]; + data.senses[name] = { + label: game.i18n.localize(label), + value: Number.isNumeric(v) ? v.toNearest(0.1) : 0 + }; + } + return data; } - return data; - } } diff --git a/module/apps/short-rest.js b/module/apps/short-rest.js index 8a164af7..95e7c68a 100644 --- a/module/apps/short-rest.js +++ b/module/apps/short-rest.js @@ -5,129 +5,130 @@ import LongRestDialog from "./long-rest.js"; * @extends {Dialog} */ export default class ShortRestDialog extends Dialog { - constructor(actor, dialogData={}, options={}) { - super(dialogData, options); + constructor(actor, dialogData = {}, options = {}) { + super(dialogData, options); - /** - * Store a reference to the Actor entity which is resting - * @type {Actor} - */ - this.actor = actor; + /** + * Store a reference to the Actor entity which is resting + * @type {Actor} + */ + this.actor = actor; - /** - * Track the most recently used HD denomination for re-rendering the form - * @type {string} - */ - this._denom = null; - } + /** + * Track the most recently used HD denomination for re-rendering the form + * @type {string} + */ + this._denom = null; + } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - template: "systems/sw5e/templates/apps/short-rest.html", - classes: ["sw5e", "dialog"] - }); - } + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + template: "systems/sw5e/templates/apps/short-rest.html", + classes: ["sw5e", "dialog"] + }); + } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - getData() { - const data = super.getData(); + /** @override */ + getData() { + const data = super.getData(); - // Determine Hit Dice - data.availableHD = this.actor.data.items.reduce((hd, item) => { - if ( item.type === "class" ) { - const d = item.data.data; - const denom = d.hitDice || "d6"; - const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0); - hd[denom] = denom in hd ? hd[denom] + available : available; - } - return hd; - }, {}); - data.canRoll = this.actor.data.data.attributes.hd > 0; - data.denomination = this._denom; - - // Determine rest type - const variant = game.settings.get("sw5e", "restVariant"); - data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute - data.newDay = false; // It may be a new day, but not by default - return data; - } - - /* -------------------------------------------- */ - - - /** @override */ - activateListeners(html) { - super.activateListeners(html); - let btn = html.find("#roll-hd"); - btn.click(this._onRollHitDie.bind(this)); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling a Hit Die as part of a Short Rest action - * @param {Event} event The triggering click event - * @private - */ - async _onRollHitDie(event) { - event.preventDefault(); - const btn = event.currentTarget; - this._denom = btn.form.hd.value; - await this.actor.rollHitDie(this._denom); - this.render(); - } - - /* -------------------------------------------- */ - - /** - * A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has - * been resolved. - * @param {Actor5e} actor - * @return {Promise} - */ - static async shortRestDialog({actor}={}) { - return new Promise((resolve, reject) => { - const dlg = new this(actor, { - title: "Short Rest", - buttons: { - rest: { - icon: '', - label: "Rest", - callback: html => { - let newDay = false; - if (game.settings.get("sw5e", "restVariant") === "gritty") - newDay = html.find('input[name="newDay"]')[0].checked; - resolve(newDay); + // Determine Hit Dice + data.availableHD = this.actor.data.items.reduce((hd, item) => { + if (item.type === "class") { + const d = item.data.data; + const denom = d.hitDice || "d6"; + const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0); + hd[denom] = denom in hd ? hd[denom] + available : available; } - }, - cancel: { - icon: '', - label: "Cancel", - callback: reject - } - }, - close: reject - }); - dlg.render(true); - }); - } + return hd; + }, {}); + data.canRoll = this.actor.data.data.attributes.hd > 0; + data.denomination = this._denom; - /* -------------------------------------------- */ + // Determine rest type + const variant = game.settings.get("sw5e", "restVariant"); + data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute + data.newDay = false; // It may be a new day, but not by default + return data; + } - /** - * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's - * workflow has been resolved. - * @deprecated - * @param {Actor5e} actor - * @return {Promise} - */ - static async longRestDialog({actor}={}) { - console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead."); - return LongRestDialog.longRestDialog(...arguments); - } + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + let btn = html.find("#roll-hd"); + btn.click(this._onRollHitDie.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling a Hit Die as part of a Short Rest action + * @param {Event} event The triggering click event + * @private + */ + async _onRollHitDie(event) { + event.preventDefault(); + const btn = event.currentTarget; + this._denom = btn.form.hd.value; + await this.actor.rollHitDie(this._denom); + this.render(); + } + + /* -------------------------------------------- */ + + /** + * A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has + * been resolved. + * @param {Actor5e} actor + * @return {Promise} + */ + static async shortRestDialog({actor} = {}) { + return new Promise((resolve, reject) => { + const dlg = new this(actor, { + title: game.i18n.localize("SW5E.ShortRest"), + buttons: { + rest: { + icon: '', + label: game.i18n.localize("SW5E.Rest"), + callback: (html) => { + let newDay = false; + if (game.settings.get("sw5e", "restVariant") === "gritty") + newDay = html.find('input[name="newDay"]')[0].checked; + resolve(newDay); + } + }, + cancel: { + icon: '', + label: game.i18n.localize("Cancel"), + callback: reject + } + }, + close: reject + }); + dlg.render(true); + }); + } + + /* -------------------------------------------- */ + + /** + * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's + * workflow has been resolved. + * @deprecated + * @param {Actor5e} actor + * @return {Promise} + */ + static async longRestDialog({actor} = {}) { + console.warn( + "WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead." + ); + return LongRestDialog.longRestDialog(...arguments); + } } diff --git a/module/apps/trait-selector.js b/module/apps/trait-selector.js index 5550aa3e..6c454cf5 100644 --- a/module/apps/trait-selector.js +++ b/module/apps/trait-selector.js @@ -3,86 +3,85 @@ * @extends {DocumentSheet} */ export default class TraitSelector extends DocumentSheet { - - /** @inheritDoc */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - id: "trait-selector", - classes: ["sw5e", "trait-selector", "subconfig"], - title: "Actor Trait Selection", - template: "systems/sw5e/templates/apps/trait-selector.html", - width: 320, - height: "auto", - choices: {}, - allowCustom: true, - minimum: 0, - maximum: null, - valueKey: "value", - customKey: "custom" - }); - } - - /* -------------------------------------------- */ - - /** - * Return a reference to the target attribute - * @type {string} - */ - get attribute() { - return this.options.name; - } - - /* -------------------------------------------- */ - - /** @override */ - getData() { - const attr = foundry.utils.getProperty(this.object.data, this.attribute); - const o = this.options; - const value = (o.valueKey) ? attr[o.valueKey] ?? [] : attr; - const custom = (o.customKey) ? attr[o.customKey] ?? "" : ""; - - // Populate choices - const choices = Object.entries(o.choices).reduce((obj, e) => { - let [k, v] = e; - obj[k] = { label: v, chosen: attr ? value.includes(k) : false }; - return obj; - }, {}) - - // Return data - return { - allowCustom: o.allowCustom, - choices: choices, - custom: custom - } - } - - /* -------------------------------------------- */ - - /** @override */ - async _updateObject(event, formData) { - const o = this.options; - - // Obtain choices - const chosen = []; - for ( let [k, v] of Object.entries(formData) ) { - if ( (k !== "custom") && v ) chosen.push(k); + /** @inheritdoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: "trait-selector", + classes: ["sw5e", "trait-selector", "subconfig"], + title: "Actor Trait Selection", + template: "systems/sw5e/templates/apps/trait-selector.html", + width: 320, + height: "auto", + choices: {}, + allowCustom: true, + minimum: 0, + maximum: null, + valueKey: "value", + customKey: "custom" + }); } - // Object including custom data - const updateData = {}; - if ( o.valueKey ) updateData[`${this.attribute}.${o.valueKey}`] = chosen; - else updateData[this.attribute] = chosen; - if ( o.allowCustom ) updateData[`${this.attribute}.${o.customKey}`] = formData.custom; + /* -------------------------------------------- */ - // Validate the number chosen - if ( o.minimum && (chosen.length < o.minimum) ) { - return ui.notifications.error(`You must choose at least ${o.minimum} options`); - } - if ( o.maximum && (chosen.length > o.maximum) ) { - return ui.notifications.error(`You may choose no more than ${o.maximum} options`); + /** + * Return a reference to the target attribute + * @type {string} + */ + get attribute() { + return this.options.name; } - // Update the object - this.object.update(updateData); - } + /* -------------------------------------------- */ + + /** @override */ + getData() { + const attr = foundry.utils.getProperty(this.object.data, this.attribute); + const o = this.options; + const value = o.valueKey ? attr[o.valueKey] ?? [] : attr; + const custom = o.customKey ? attr[o.customKey] ?? "" : ""; + + // Populate choices + const choices = Object.entries(o.choices).reduce((obj, e) => { + let [k, v] = e; + obj[k] = {label: v, chosen: attr ? value.includes(k) : false}; + return obj; + }, {}); + + // Return data + return { + allowCustom: o.allowCustom, + choices: choices, + custom: custom + }; + } + + /* -------------------------------------------- */ + + /** @override */ + async _updateObject(event, formData) { + const o = this.options; + + // Obtain choices + const chosen = []; + for (let [k, v] of Object.entries(formData)) { + if (k !== "custom" && v) chosen.push(k); + } + + // Object including custom data + const updateData = {}; + if (o.valueKey) updateData[`${this.attribute}.${o.valueKey}`] = chosen; + else updateData[this.attribute] = chosen; + if (o.allowCustom) updateData[`${this.attribute}.${o.customKey}`] = formData.custom; + + // Validate the number chosen + if (o.minimum && chosen.length < o.minimum) { + return ui.notifications.error(`You must choose at least ${o.minimum} options`); + } + if (o.maximum && chosen.length > o.maximum) { + return ui.notifications.error(`You may choose no more than ${o.maximum} options`); + } + + // Update the object + this.object.update(updateData); + } } diff --git a/module/canvas.js b/module/canvas.js index 622e2a30..72c60c81 100644 --- a/module/canvas.js +++ b/module/canvas.js @@ -1,38 +1,38 @@ /** @override */ -export const measureDistances = function(segments, options={}) { - if ( !options.gridSpaces ) return BaseGrid.prototype.measureDistances.call(this, segments, options); +export const measureDistances = function (segments, options = {}) { + if (!options.gridSpaces) return BaseGrid.prototype.measureDistances.call(this, segments, options); - // Track the total number of diagonals - let nDiagonal = 0; - const rule = this.parent.diagonalRule; - const d = canvas.dimensions; + // Track the total number of diagonals + let nDiagonal = 0; + const rule = this.parent.diagonalRule; + const d = canvas.dimensions; - // Iterate over measured segments - return segments.map(s => { - let r = s.ray; + // Iterate over measured segments + return segments.map((s) => { + let r = s.ray; - // Determine the total distance traveled - let nx = Math.abs(Math.ceil(r.dx / d.size)); - let ny = Math.abs(Math.ceil(r.dy / d.size)); + // Determine the total distance traveled + let nx = Math.abs(Math.ceil(r.dx / d.size)); + let ny = Math.abs(Math.ceil(r.dy / d.size)); - // Determine the number of straight and diagonal moves - let nd = Math.min(nx, ny); - let ns = Math.abs(ny - nx); - nDiagonal += nd; + // Determine the number of straight and diagonal moves + let nd = Math.min(nx, ny); + let ns = Math.abs(ny - nx); + nDiagonal += nd; - // Alternative DMG Movement - if (rule === "5105") { - let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2); - let spaces = (nd10 * 2) + (nd - nd10) + ns; - return spaces * canvas.dimensions.distance; - } + // Alternative DMG Movement + if (rule === "5105") { + let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2); + let spaces = nd10 * 2 + (nd - nd10) + ns; + return spaces * canvas.dimensions.distance; + } - // Euclidean Measurement - else if (rule === "EUCL") { - return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance); - } + // Euclidean Measurement + else if (rule === "EUCL") { + return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance); + } - // Standard PHB Movement - else return (ns + nd) * canvas.scene.data.gridDistance; - }); -}; \ No newline at end of file + // Standard PHB Movement + else return (ns + nd) * canvas.scene.data.gridDistance; + }); +}; diff --git a/module/characterImporter.js b/module/characterImporter.js index 8461976a..178be216 100644 --- a/module/characterImporter.js +++ b/module/characterImporter.js @@ -1,51 +1,51 @@ export default class CharacterImporter { - // transform JSON from sw5e.com to Foundry friendly format - // and insert new actor - static async transform(rawCharacter) { - const sourceCharacter = JSON.parse(rawCharacter); //source character + // transform JSON from sw5e.com to Foundry friendly format + // and insert new actor + static async transform(rawCharacter) { + const sourceCharacter = JSON.parse(rawCharacter); //source character - const details = { - species: sourceCharacter.attribs.find((e) => e.name == "race").current, - background: sourceCharacter.attribs.find((e) => e.name == "background").current, - alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current - }; + const details = { + species: sourceCharacter.attribs.find((e) => e.name == "race").current, + background: sourceCharacter.attribs.find((e) => e.name == "background").current, + alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current + }; - const hp = { - value: sourceCharacter.attribs.find((e) => e.name == "hp").current, - min: 0, - max: sourceCharacter.attribs.find((e) => e.name == "hp").current, - temp: sourceCharacter.attribs.find((e) => e.name == "hp_temp").current - }; + const hp = { + value: sourceCharacter.attribs.find((e) => e.name == "hp").current, + min: 0, + max: sourceCharacter.attribs.find((e) => e.name == "hp").current, + temp: sourceCharacter.attribs.find((e) => e.name == "hp_temp").current + }; - const abilities = { - str: { - value: sourceCharacter.attribs.find((e) => e.name == "strength").current, - proficient: sourceCharacter.attribs.find((e) => e.name == "strength_save_prof").current ? 1 : 0 - }, - dex: { - value: sourceCharacter.attribs.find((e) => e.name == "dexterity").current, - proficient: sourceCharacter.attribs.find((e) => e.name == "dexterity_save_prof").current ? 1 : 0 - }, - con: { - value: sourceCharacter.attribs.find((e) => e.name == "constitution").current, - proficient: sourceCharacter.attribs.find((e) => e.name == "constitution_save_prof").current ? 1 : 0 - }, - int: { - value: sourceCharacter.attribs.find((e) => e.name == "intelligence").current, - proficient: sourceCharacter.attribs.find((e) => e.name == "intelligence_save_prof").current ? 1 : 0 - }, - wis: { - value: sourceCharacter.attribs.find((e) => e.name == "wisdom").current, - proficient: sourceCharacter.attribs.find((e) => e.name == "wisdom_save_prof").current ? 1 : 0 - }, - cha: { - value: sourceCharacter.attribs.find((e) => e.name == "charisma").current, - proficient: sourceCharacter.attribs.find((e) => e.name == "charisma_save_prof").current ? 1 : 0 - } - }; + const abilities = { + str: { + value: sourceCharacter.attribs.find((e) => e.name == "strength").current, + proficient: sourceCharacter.attribs.find((e) => e.name == "strength_save_prof").current ? 1 : 0 + }, + dex: { + value: sourceCharacter.attribs.find((e) => e.name == "dexterity").current, + proficient: sourceCharacter.attribs.find((e) => e.name == "dexterity_save_prof").current ? 1 : 0 + }, + con: { + value: sourceCharacter.attribs.find((e) => e.name == "constitution").current, + proficient: sourceCharacter.attribs.find((e) => e.name == "constitution_save_prof").current ? 1 : 0 + }, + int: { + value: sourceCharacter.attribs.find((e) => e.name == "intelligence").current, + proficient: sourceCharacter.attribs.find((e) => e.name == "intelligence_save_prof").current ? 1 : 0 + }, + wis: { + value: sourceCharacter.attribs.find((e) => e.name == "wisdom").current, + proficient: sourceCharacter.attribs.find((e) => e.name == "wisdom_save_prof").current ? 1 : 0 + }, + cha: { + value: sourceCharacter.attribs.find((e) => e.name == "charisma").current, + proficient: sourceCharacter.attribs.find((e) => e.name == "charisma_save_prof").current ? 1 : 0 + } + }; - /* ----------------------------------------------------------------- */ - /* character.data.skills..value is all that matters + /* ----------------------------------------------------------------- */ + /* character.data.skills..value is all that matters /* values can be 0, 0.5, 1 or 2 /* 0 = regular /* 0.5 = half-proficient @@ -53,272 +53,274 @@ export default class CharacterImporter { /* 2 = expertise /* foundry takes care of calculating the rest /* ----------------------------------------------------------------- */ - const skills = { - acr: { - value: sourceCharacter.attribs.find((e) => e.name == "acrobatics_type").current - }, - ani: { - value: sourceCharacter.attribs.find((e) => e.name == "animal_handling_type").current - }, - ath: { - value: sourceCharacter.attribs.find((e) => e.name == "athletics_type").current - }, - dec: { - value: sourceCharacter.attribs.find((e) => e.name == "deception_type").current - }, - ins: { - value: sourceCharacter.attribs.find((e) => e.name == "insight_type").current - }, - inv: { - value: sourceCharacter.attribs.find((e) => e.name == "investigation_type").current - }, - itm: { - value: sourceCharacter.attribs.find((e) => e.name == "intimidation_type").current - }, - lor: { - value: sourceCharacter.attribs.find((e) => e.name == "lore_type").current - }, - med: { - value: sourceCharacter.attribs.find((e) => e.name == "medicine_type").current - }, - nat: { - value: sourceCharacter.attribs.find((e) => e.name == "nature_type").current - }, - per: { - value: sourceCharacter.attribs.find((e) => e.name == "persuasion_type").current - }, - pil: { - value: sourceCharacter.attribs.find((e) => e.name == "piloting_type").current - }, - prc: { - value: sourceCharacter.attribs.find((e) => e.name == "perception_type").current - }, - prf: { - value: sourceCharacter.attribs.find((e) => e.name == "performance_type").current - }, - slt: { - value: sourceCharacter.attribs.find((e) => e.name == "sleight_of_hand_type").current - }, - ste: { - value: sourceCharacter.attribs.find((e) => e.name == "stealth_type").current - }, - sur: { - value: sourceCharacter.attribs.find((e) => e.name == "survival_type").current - }, - tec: { - value: sourceCharacter.attribs.find((e) => e.name == "technology_type").current - } - }; - - const targetCharacter = { - name: sourceCharacter.name, - type: "character", - data: { - abilities: abilities, - details: details, - skills: skills, - attributes: { - hp: hp - } - } - }; - - let actor = await Actor.create(targetCharacter); - CharacterImporter.addProfessions(sourceCharacter, actor); - } - - // Parse all classes and add them to already created actor. - // "class" is a reserved word, therefore I use profession where I can. - static async addProfessions(sourceCharacter, actor) { - let result = []; - - // parse all class and multiclassX items - // couldn't get Array.filter to work here for some reason - // result = array of objects. each object is a separate class - sourceCharacter.attribs.forEach((e) => { - if (CharacterImporter.classOrMulticlass(e.name)) { - var t = { - profession: CharacterImporter.capitalize(e.current), - type: CharacterImporter.baseOrMulti(e.name), - level: CharacterImporter.getLevel(e, sourceCharacter) + const skills = { + acr: { + value: sourceCharacter.attribs.find((e) => e.name == "acrobatics_type").current + }, + ani: { + value: sourceCharacter.attribs.find((e) => e.name == "animal_handling_type").current + }, + ath: { + value: sourceCharacter.attribs.find((e) => e.name == "athletics_type").current + }, + dec: { + value: sourceCharacter.attribs.find((e) => e.name == "deception_type").current + }, + ins: { + value: sourceCharacter.attribs.find((e) => e.name == "insight_type").current + }, + inv: { + value: sourceCharacter.attribs.find((e) => e.name == "investigation_type").current + }, + itm: { + value: sourceCharacter.attribs.find((e) => e.name == "intimidation_type").current + }, + lor: { + value: sourceCharacter.attribs.find((e) => e.name == "lore_type").current + }, + med: { + value: sourceCharacter.attribs.find((e) => e.name == "medicine_type").current + }, + nat: { + value: sourceCharacter.attribs.find((e) => e.name == "nature_type").current + }, + per: { + value: sourceCharacter.attribs.find((e) => e.name == "persuasion_type").current + }, + pil: { + value: sourceCharacter.attribs.find((e) => e.name == "piloting_type").current + }, + prc: { + value: sourceCharacter.attribs.find((e) => e.name == "perception_type").current + }, + prf: { + value: sourceCharacter.attribs.find((e) => e.name == "performance_type").current + }, + slt: { + value: sourceCharacter.attribs.find((e) => e.name == "sleight_of_hand_type").current + }, + ste: { + value: sourceCharacter.attribs.find((e) => e.name == "stealth_type").current + }, + sur: { + value: sourceCharacter.attribs.find((e) => e.name == "survival_type").current + }, + tec: { + value: sourceCharacter.attribs.find((e) => e.name == "technology_type").current + } }; - result.push(t); - } - }); - // pull classes directly from system compendium and add them to current actor - const professionsPack = await game.packs.get("sw5e.classes").getContent(); - result.forEach((prof) => { - let assignedProfession = professionsPack.find((o) => o.name === prof.profession); - assignedProfession.data.data.levels = prof.level; - actor.createEmbeddedEntity("OwnedItem", assignedProfession.data, { displaySheet: false }); - }); + const targetCharacter = { + name: sourceCharacter.name, + type: "character", + data: { + abilities: abilities, + details: details, + skills: skills, + attributes: { + hp: hp + } + } + }; - this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor); - - this.addPowers( - sourceCharacter.attribs.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1).map((e) => e.current), - actor - ); - - const discoveredItems = sourceCharacter.attribs.filter( - (e) => e.name.search(/repeating_inventory.+_itemname/g) != -1 - ); - const items = discoveredItems.map((item) => { - const id = item.name.match(/-\w{19}/g); - - return { - name: item.current, - quantity: sourceCharacter.attribs.find((e) => e.name === `repeating_inventory_${id}_itemcount`).current - }; - }); - - this.addItems(items, actor); - } - - static async addClasses(profession, level, actor) { - let classes = await game.packs.get("sw5e.classes").getContent(); - let assignedClass = classes.find((c) => c.name === profession); - assignedClass.data.data.levels = level; - await actor.createEmbeddedEntity("OwnedItem", assignedClass.data, { displaySheet: false }); - } - - static classOrMulticlass(name) { - return name === "class" || (name.includes("multiclass") && name.length <= 12); - } - - static baseOrMulti(name) { - if (name === "class") { - return "base_class"; - } else { - return "multi_class"; + let actor = await Actor.create(targetCharacter); + CharacterImporter.addProfessions(sourceCharacter, actor); } - } - static getLevel(item, sourceCharacter) { - if (item.name === "class") { - let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current; - return parseInt(result); - } else { - let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current; - return parseInt(result); + // Parse all classes and add them to already created actor. + // "class" is a reserved word, therefore I use profession where I can. + static async addProfessions(sourceCharacter, actor) { + let result = []; + + // parse all class and multiclassX items + // couldn't get Array.filter to work here for some reason + // result = array of objects. each object is a separate class + sourceCharacter.attribs.forEach((e) => { + if (CharacterImporter.classOrMulticlass(e.name)) { + var t = { + profession: CharacterImporter.capitalize(e.current), + type: CharacterImporter.baseOrMulti(e.name), + level: CharacterImporter.getLevel(e, sourceCharacter) + }; + result.push(t); + } + }); + + // pull classes directly from system compendium and add them to current actor + const professionsPack = await game.packs.get("sw5e.classes").getDocuments(); + result.forEach((prof) => { + let assignedProfession = professionsPack.find((o) => o.name === prof.profession); + assignedProfession.data.data.levels = prof.level; + actor.createEmbeddedDocuments("Item", [assignedProfession.data], {displaySheet: false}); + }); + + this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor); + + this.addPowers( + sourceCharacter.attribs + .filter((e) => e.name.search(/repeating_power.+_powername/g) != -1) + .map((e) => e.current), + actor + ); + + const discoveredItems = sourceCharacter.attribs.filter( + (e) => e.name.search(/repeating_inventory.+_itemname/g) != -1 + ); + const items = discoveredItems.map((item) => { + const id = item.name.match(/-\w{19}/g); + + return { + name: item.current, + quantity: sourceCharacter.attribs.find((e) => e.name === `repeating_inventory_${id}_itemcount`).current + }; + }); + + this.addItems(items, actor); } - } - static capitalize(str) { - return str.charAt(0).toUpperCase() + str.slice(1); - } - - static async addSpecies(race, actor) { - const species = await game.packs.get("sw5e.species").getContent(); - const assignedSpecies = species.find((c) => c.name === race); - const activeEffects = assignedSpecies.data.effects[0].changes; - const actorData = { data: { abilities: { ...actor.data.data.abilities } } }; - - activeEffects.map((effect) => { - switch (effect.key) { - case "data.abilities.str.value": - actorData.data.abilities.str.value -= effect.value; - break; - - case "data.abilities.dex.value": - actorData.data.abilities.dex.value -= effect.value; - break; - - case "data.abilities.con.value": - actorData.data.abilities.con.value -= effect.value; - break; - - case "data.abilities.int.value": - actorData.data.abilities.int.value -= effect.value; - break; - - case "data.abilities.wis.value": - actorData.data.abilities.wis.value -= effect.value; - break; - - case "data.abilities.cha.value": - actorData.data.abilities.cha.value -= effect.value; - break; - - default: - break; - } - }); - - actor.update(actorData); - - await actor.createEmbeddedEntity("OwnedItem", assignedSpecies.data, { displaySheet: false }); - } - - static async addPowers(powers, actor) { - const forcePowers = await game.packs.get("sw5e.forcepowers").getContent(); - const techPowers = await game.packs.get("sw5e.techpowers").getContent(); - - for (const power of powers) { - const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power); - - if (createdPower) { - await actor.createEmbeddedEntity("OwnedItem", createdPower.data, { displaySheet: false }); - } + static async addClasses(profession, level, actor) { + let classes = await game.packs.get("sw5e.classes").getDocuments(); + let assignedClass = classes.find((c) => c.name === profession); + assignedClass.data.data.levels = level; + await actor.createEmbeddedDocuments("Item", [assignedClass.data], {displaySheet: false}); } - } - static async addItems(items, actor) { - const weapons = await game.packs.get("sw5e.weapons").getContent(); - const armors = await game.packs.get("sw5e.armor").getContent(); - const adventuringGear = await game.packs.get("sw5e.adventuringgear").getContent(); + static classOrMulticlass(name) { + return name === "class" || (name.includes("multiclass") && name.length <= 12); + } - for (const item of items) { - const createdItem = - weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) || - armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) || - adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase()); - - if (createdItem) { - if (item.quantity != 1) { - createdItem.data.data.quantity = item.quantity; + static baseOrMulti(name) { + if (name === "class") { + return "base_class"; + } else { + return "multi_class"; } - - await actor.createEmbeddedEntity("OwnedItem", createdItem.data, { displaySheet: false }); - } } - } - static addImportButton(html) { - const actionButtons = html.find(".header-actions"); - actionButtons[0].insertAdjacentHTML( - "afterend", - `
` - ); + static getLevel(item, sourceCharacter) { + if (item.name === "class") { + let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current; + return parseInt(result); + } else { + let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current; + return parseInt(result); + } + } - let characterImportButton = $(".cs-import-button"); - characterImportButton.click(() => { - let content = `

Saved Character JSON Import

+ static capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + static async addSpecies(race, actor) { + const species = await game.packs.get("sw5e.species").getDocuments(); + const assignedSpecies = species.find((c) => c.name === race); + const activeEffects = [...assignedSpecies.data.effects][0].data.changes; + const actorData = {data: {abilities: {...actor.data.data.abilities}}}; + + activeEffects.map((effect) => { + switch (effect.key) { + case "data.abilities.str.value": + actorData.data.abilities.str.value -= effect.value; + break; + + case "data.abilities.dex.value": + actorData.data.abilities.dex.value -= effect.value; + break; + + case "data.abilities.con.value": + actorData.data.abilities.con.value -= effect.value; + break; + + case "data.abilities.int.value": + actorData.data.abilities.int.value -= effect.value; + break; + + case "data.abilities.wis.value": + actorData.data.abilities.wis.value -= effect.value; + break; + + case "data.abilities.cha.value": + actorData.data.abilities.cha.value -= effect.value; + break; + + default: + break; + } + }); + + actor.update(actorData); + + await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], {displaySheet: false}); + } + + static async addPowers(powers, actor) { + const forcePowers = await game.packs.get("sw5e.forcepowers").getDocuments(); + const techPowers = await game.packs.get("sw5e.techpowers").getDocuments(); + + for (const power of powers) { + const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power); + + if (createdPower) { + await actor.createEmbeddedDocuments("Item", [createdPower.data], {displaySheet: false}); + } + } + } + + static async addItems(items, actor) { + const weapons = await game.packs.get("sw5e.weapons").getDocuments(); + const armors = await game.packs.get("sw5e.armor").getDocuments(); + const adventuringGear = await game.packs.get("sw5e.adventuringgear").getDocuments(); + + for (const item of items) { + const createdItem = + weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) || + armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) || + adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase()); + + if (createdItem) { + if (item.quantity != 1) { + createdItem.data.data.quantity = item.quantity; + } + + await actor.createEmbeddedDocuments("Item", [createdItem.data], {displaySheet: false}); + } + } + } + + static addImportButton(html) { + const actionButtons = html.find(".header-actions"); + actionButtons[0].insertAdjacentHTML( + "afterend", + `
` + ); + + let characterImportButton = $(".cs-import-button"); + characterImportButton.click(() => { + let content = `

Saved Character JSON Import


`; - let importDialog = new Dialog({ - title: "Import Character from SW5e.com", - content: content, - buttons: { - Import: { - icon: ``, - label: "Import Character", - callback: () => { - let characterData = $("#character-json").val(); - console.log("Parsing Character JSON"); - CharacterImporter.transform(characterData); - } - }, - Cancel: { - icon: ``, - label: "Cancel", - callback: () => {} - } - } - }); - importDialog.render(true); - }); - } + let importDialog = new Dialog({ + title: "Import Character from SW5e.com", + content: content, + buttons: { + Import: { + icon: ``, + label: "Import Character", + callback: () => { + let characterData = $("#character-json").val(); + console.log("Parsing Character JSON"); + CharacterImporter.transform(characterData); + } + }, + Cancel: { + icon: ``, + label: "Cancel", + callback: () => {} + } + } + }); + importDialog.render(true); + }); + } } diff --git a/module/chat.js b/module/chat.js index d024d8aa..42d2bf4f 100644 --- a/module/chat.js +++ b/module/chat.js @@ -1,30 +1,29 @@ - /** * Highlight critical success or failure on d20 rolls */ -export const highlightCriticalSuccessFailure = function(message, html, data) { - if ( !message.isRoll || !message.isContentVisible ) return; +export const highlightCriticalSuccessFailure = function (message, html, data) { + if (!message.isRoll || !message.isContentVisible) return; - // Highlight rolls where the first part is a d20 roll - const roll = message.roll; - if ( !roll.dice.length ) return; - const d = roll.dice[0]; + // Highlight rolls where the first part is a d20 roll + const roll = message.roll; + if (!roll.dice.length) return; + const d = roll.dice[0]; - // Ensure it is an un-modified d20 roll - const isD20 = (d.faces === 20) && ( d.values.length === 1 ); - if ( !isD20 ) return; - const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure; - if ( isModifiedRoll ) return; + // Ensure it is an un-modified d20 roll + const isD20 = d.faces === 20 && d.values.length === 1; + if (!isD20) return; + const isModifiedRoll = "success" in d.results[0] || d.options.marginSuccess || d.options.marginFailure; + if (isModifiedRoll) return; - // Highlight successes and failures - const critical = d.options.critical || 20; - const fumble = d.options.fumble || 1; - if ( d.total >= critical ) html.find(".dice-total").addClass("critical"); - else if ( d.total <= fumble ) html.find(".dice-total").addClass("fumble"); - else if ( d.options.target ) { - if ( roll.total >= d.options.target ) html.find(".dice-total").addClass("success"); - else html.find(".dice-total").addClass("failure"); - } + // Highlight successes and failures + const critical = d.options.critical || 20; + const fumble = d.options.fumble || 1; + if (d.total >= critical) html.find(".dice-total").addClass("critical"); + else if (d.total <= fumble) html.find(".dice-total").addClass("fumble"); + else if (d.options.target) { + if (roll.total >= d.options.target) html.find(".dice-total").addClass("success"); + else html.find(".dice-total").addClass("failure"); + } }; /* -------------------------------------------- */ @@ -32,24 +31,24 @@ export const highlightCriticalSuccessFailure = function(message, html, data) { /** * Optionally hide the display of chat card action buttons which cannot be performed by the user */ -export const displayChatActionButtons = function(message, html, data) { - const chatCard = html.find(".sw5e.chat-card"); - if ( chatCard.length > 0 ) { - const flavor = html.find(".flavor-text"); - if ( flavor.text() === html.find(".item-name").text() ) flavor.remove(); +export const displayChatActionButtons = function (message, html, data) { + const chatCard = html.find(".sw5e.chat-card"); + if (chatCard.length > 0) { + const flavor = html.find(".flavor-text"); + if (flavor.text() === html.find(".item-name").text()) flavor.remove(); - // If the user is the message author or the actor owner, proceed - let actor = game.actors.get(data.message.speaker.actor); - if ( actor && actor.isOwner ) return; - else if ( game.user.isGM || (data.author.id === game.user.id)) return; + // If the user is the message author or the actor owner, proceed + let actor = game.actors.get(data.message.speaker.actor); + if (actor && actor.isOwner) return; + else if (game.user.isGM || data.author.id === game.user.id) return; - // Otherwise conceal action buttons except for saving throw - const buttons = chatCard.find("button[data-action]"); - buttons.each((i, btn) => { - if ( btn.dataset.action === "save" ) return; - btn.style.display = "none" - }); - } + // Otherwise conceal action buttons except for saving throw + const buttons = chatCard.find("button[data-action]"); + buttons.each((i, btn) => { + if (btn.dataset.action === "save") return; + btn.style.display = "none"; + }); + } }; /* -------------------------------------------- */ @@ -63,38 +62,38 @@ export const displayChatActionButtons = function(message, html, data) { * * @return {Array} The extended options Array including new context choices */ -export const addChatMessageContextOptions = function(html, options) { - let canApply = li => { - const message = game.messages.get(li.data("messageId")); - return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length; - }; - options.push( - { - name: game.i18n.localize("SW5E.ChatContextDamage"), - icon: '', - condition: canApply, - callback: li => applyChatCardDamage(li, 1) - }, - { - name: game.i18n.localize("SW5E.ChatContextHealing"), - icon: '', - condition: canApply, - callback: li => applyChatCardDamage(li, -1) - }, - { - name: game.i18n.localize("SW5E.ChatContextDoubleDamage"), - icon: '', - condition: canApply, - callback: li => applyChatCardDamage(li, 2) - }, - { - name: game.i18n.localize("SW5E.ChatContextHalfDamage"), - icon: '', - condition: canApply, - callback: li => applyChatCardDamage(li, 0.5) - } - ); - return options; +export const addChatMessageContextOptions = function (html, options) { + let canApply = (li) => { + const message = game.messages.get(li.data("messageId")); + return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length; + }; + options.push( + { + name: game.i18n.localize("SW5E.ChatContextDamage"), + icon: '', + condition: canApply, + callback: (li) => applyChatCardDamage(li, 1) + }, + { + name: game.i18n.localize("SW5E.ChatContextHealing"), + icon: '', + condition: canApply, + callback: (li) => applyChatCardDamage(li, -1) + }, + { + name: game.i18n.localize("SW5E.ChatContextDoubleDamage"), + icon: '', + condition: canApply, + callback: (li) => applyChatCardDamage(li, 2) + }, + { + name: game.i18n.localize("SW5E.ChatContextHalfDamage"), + icon: '', + condition: canApply, + callback: (li) => applyChatCardDamage(li, 0.5) + } + ); + return options; }; /* -------------------------------------------- */ @@ -108,12 +107,14 @@ export const addChatMessageContextOptions = function(html, options) { * @return {Promise} */ function applyChatCardDamage(li, multiplier) { - const message = game.messages.get(li.data("messageId")); - const roll = message.roll; - return Promise.all(canvas.tokens.controlled.map(t => { - const a = t.actor; - return a.applyDamage(roll.total, multiplier); - })); + const message = game.messages.get(li.data("messageId")); + const roll = message.roll; + return Promise.all( + canvas.tokens.controlled.map((t) => { + const a = t.actor; + return a.applyDamage(roll.total, multiplier); + }) + ); } /* -------------------------------------------- */ diff --git a/module/classFeatures.js b/module/classFeatures.js index 7946c252..17155d10 100644 --- a/module/classFeatures.js +++ b/module/classFeatures.js @@ -1,4 +1 @@ -export const ClassFeatures = { - -}; - +export const ClassFeatures = {}; diff --git a/module/combat.js b/module/combat.js index 58535411..ff7594a5 100644 --- a/module/combat.js +++ b/module/combat.js @@ -1,27 +1,31 @@ - /** * Override the default Initiative formula to customize special behaviors of the SW5e system. * Apply advantage, proficiency, or bonuses where appropriate * Apply the dexterity score as a decimal tiebreaker if requested * See Combat._getInitiativeFormula for more detail. */ -export const _getInitiativeFormula = function() { - const actor = this.actor; - if ( !actor ) return "1d20"; - const init = actor.data.data.attributes.init; +export const _getInitiativeFormula = function () { + const actor = this.actor; + if (!actor) return "1d20"; + const init = actor.data.data.attributes.init; - // Construct initiative formula parts - let nd = 1; - let mods = ""; - if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=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]; + // Construct initiative formula parts + let nd = 1; + let mods = ""; + if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=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"); - if ( tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100); - return parts.filter(p => p !== null).join(" + "); + // Optionally apply Dexterity tiebreaker + const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker"); + if (tiebreaker) parts.push(actor.data.data.abilities.dex.value / 100); + return parts.filter((p) => p !== null).join(" + "); }; diff --git a/module/config.js b/module/config.js index 399fe55a..1770bcea 100644 --- a/module/config.js +++ b/module/config.js @@ -1,4 +1,4 @@ -import {ClassFeatures} from "./classFeatures.js" +import {ClassFeatures} from "./classFeatures.js"; // Namespace SW5e Configuration Values export const SW5E = {}; @@ -12,27 +12,26 @@ SW5E.ASCII = ` \\______ / \\__/\\ //______ /\\__ > \\/ \\/ \\/ \\/ `; - /** * The set of Ability Scores used within the system * @type {Object} */ SW5E.abilities = { - "str": "SW5E.AbilityStr", - "dex": "SW5E.AbilityDex", - "con": "SW5E.AbilityCon", - "int": "SW5E.AbilityInt", - "wis": "SW5E.AbilityWis", - "cha": "SW5E.AbilityCha" + str: "SW5E.AbilityStr", + dex: "SW5E.AbilityDex", + con: "SW5E.AbilityCon", + int: "SW5E.AbilityInt", + wis: "SW5E.AbilityWis", + cha: "SW5E.AbilityCha" }; SW5E.abilityAbbreviations = { - "str": "SW5E.AbilityStrAbbr", - "dex": "SW5E.AbilityDexAbbr", - "con": "SW5E.AbilityConAbbr", - "int": "SW5E.AbilityIntAbbr", - "wis": "SW5E.AbilityWisAbbr", - "cha": "SW5E.AbilityChaAbbr" + str: "SW5E.AbilityStrAbbr", + dex: "SW5E.AbilityDexAbbr", + con: "SW5E.AbilityConAbbr", + int: "SW5E.AbilityIntAbbr", + wis: "SW5E.AbilityWisAbbr", + cha: "SW5E.AbilityChaAbbr" }; /* -------------------------------------------- */ @@ -42,15 +41,15 @@ SW5E.abilityAbbreviations = { * @type {Object} */ SW5E.alignments = { - 'll': "SW5E.AlignmentLL", - 'nl': "SW5E.AlignmentNL", - 'cl': "SW5E.AlignmentCL", - 'lb': "SW5E.AlignmentLB", - 'bn': "SW5E.AlignmentBN", - 'cb': "SW5E.AlignmentCB", - 'ld': "SW5E.AlignmentLD", - 'nd': "SW5E.AlignmentND", - 'cd': "SW5E.AlignmentCD" + ll: "SW5E.AlignmentLL", + nl: "SW5E.AlignmentNL", + cl: "SW5E.AlignmentCL", + lb: "SW5E.AlignmentLB", + bn: "SW5E.AlignmentBN", + cb: "SW5E.AlignmentCB", + ld: "SW5E.AlignmentLD", + nd: "SW5E.AlignmentND", + cd: "SW5E.AlignmentCD" }; /* -------------------------------------------- */ @@ -60,9 +59,9 @@ SW5E.alignments = { * @enum {number} */ SW5E.attunementTypes = { - NONE: 0, - REQUIRED: 1, - ATTUNED: 2, + NONE: 0, + REQUIRED: 1, + ATTUNED: 2 }; /** @@ -70,37 +69,52 @@ SW5E.attunementTypes = { * @type {{"0": string, "1": string, "2": string}} */ SW5E.attunements = { - 0: "SW5E.AttunementNone", - 1: "SW5E.AttunementRequired", - 2: "SW5E.AttunementAttuned" + 0: "SW5E.AttunementNone", + 1: "SW5E.AttunementRequired", + 2: "SW5E.AttunementAttuned" }; /* -------------------------------------------- */ SW5E.weaponProficiencies = { - "blp": "SW5E.WeaponBlasterPistolProficiency", - "chk": "SW5E.WeaponChakramProficiency", - "dbb": "SW5E.WeaponDoubleBladeProficiency", - "dbs": "SW5E.WeaponDoubleSaberProficiency", - "dsh": "SW5E.WeaponDoubleShotoProficiency", - "dsw": "SW5E.WeaponDoubleSwordProficiency", - "hid": "SW5E.WeaponHiddenBladeProficiency", - "imp": "SW5E.WeaponImprovisedProficiency", - "lfl": "SW5E.WeaponLightFoilProficiency", - "lrg": "SW5E.WeaponLightRingProficiency", - "mar": "SW5E.WeaponMartialProficiency", - "mrb": "SW5E.WeaponMartialBlasterProficiency", - "mlw": "SW5E.WeaponMartialLightweaponProficiency", - "mvb": "SW5E.WeaponMartialVibroweaponProficiency", - "ntl": "SW5E.WeaponNaturalProficiency", - "swh": "SW5E.WeaponSaberWhipProficiency", - "sim": "SW5E.WeaponSimpleProficiency", - "smb": "SW5E.WeaponSimpleBlasterProficiency", - "slw": "SW5E.WeaponSimpleLightweaponProficiency", - "svb": "SW5E.WeaponSimpleVibroweaponProficiency", - "tch": "SW5E.WeaponTechbladeProficiency", - "vbr": "SW5E.WeaponVibrorapierProficiency", - "vbw": "SW5E.WeaponVibrowhipProficiency" + blp: "SW5E.WeaponBlasterPistolProficiency", + chk: "SW5E.WeaponChakramProficiency", + dbb: "SW5E.WeaponDoubleBladeProficiency", + dbs: "SW5E.WeaponDoubleSaberProficiency", + dsh: "SW5E.WeaponDoubleShotoProficiency", + dsw: "SW5E.WeaponDoubleSwordProficiency", + hid: "SW5E.WeaponHiddenBladeProficiency", + imp: "SW5E.WeaponImprovisedProficiency", + lfl: "SW5E.WeaponLightFoilProficiency", + lrg: "SW5E.WeaponLightRingProficiency", + mar: "SW5E.WeaponMartialProficiency", + mrb: "SW5E.WeaponMartialBlasterProficiency", + mlw: "SW5E.WeaponMartialLightweaponProficiency", + mvb: "SW5E.WeaponMartialVibroweaponProficiency", + ntl: "SW5E.WeaponNaturalProficiency", + swh: "SW5E.WeaponSaberWhipProficiency", + sim: "SW5E.WeaponSimpleProficiency", + smb: "SW5E.WeaponSimpleBlasterProficiency", + slw: "SW5E.WeaponSimpleLightweaponProficiency", + svb: "SW5E.WeaponSimpleVibroweaponProficiency", + tch: "SW5E.WeaponTechbladeProficiency", + vbr: "SW5E.WeaponVibrorapierProficiency", + vbw: "SW5E.WeaponVibrowhipProficiency" +}; + +/** + * A map of weapon item proficiency to actor item proficiency + * Used when a new player owned item is created + * @type {Object} + */ +SW5E.weaponProficienciesMap = { + natural: true, + simpleVW: "sim", + simpleB: "sim", + simpleLW: "sim", + martialVW: "mar", + martialB: "mar", + martialLW: "mar" }; // TODO: Check to see if this can be used @@ -157,36 +171,36 @@ SW5E.weaponIds = { /* -------------------------------------------- */ SW5E.toolProficiencies = { - "armor": "SW5E.ToolArmormech", - "arms": "SW5E.ToolArmstech", - "arti": "SW5E.ToolArtificer", - "art": "SW5E.ToolArtist", - "astro": "SW5E.ToolAstrotech", - "bio": "SW5E.ToolBiotech", - "con": "SW5E.ToolConstructor", - "cyb": "SW5E.ToolCybertech", - "jew": "SW5E.ToolJeweler", - "sur": "SW5E.ToolSurveyor", - "syn": "SW5E.ToolSynthweaver", - "tin": "SW5E.ToolTinker", - "ant": "SW5E.ToolAntitoxkit", - "arc": "SW5E.ToolArchaeologistKit", - "aud": "SW5E.ToolAudiotechKit", - "bioa": "SW5E.ToolBioanalysisKit", - "brew": "SW5E.ToolBrewerKit", - "chef": "SW5E.ToolChefKit", - "demo": "SW5E.ToolDemolitionKit", - "disg": "SW5E.ToolDisguiseKit", - "forg": "SW5E.ToolForgeryKit", - "mech": "SW5E.ToolMechanicKit", - "game": "SW5E.ToolGamingSet", - "poi": "SW5E.ToolPoisonKit", - "scav": "SW5E.ToolScavengingKit", - "secur": "SW5E.ToolSecurityKit", - "slic": "SW5E.ToolSlicerKit", - "spice": "SW5E.ToolSpiceKit", - "music": "SW5E.ToolMusicalInstrument", - "vehicle": "SW5E.ToolVehicle" + armor: "SW5E.ToolArmormech", + arms: "SW5E.ToolArmstech", + arti: "SW5E.ToolArtificer", + art: "SW5E.ToolArtist", + astro: "SW5E.ToolAstrotech", + bio: "SW5E.ToolBiotech", + con: "SW5E.ToolConstructor", + cyb: "SW5E.ToolCybertech", + jew: "SW5E.ToolJeweler", + sur: "SW5E.ToolSurveyor", + syn: "SW5E.ToolSynthweaver", + tin: "SW5E.ToolTinker", + ant: "SW5E.ToolAntitoxkit", + arc: "SW5E.ToolArchaeologistKit", + aud: "SW5E.ToolAudiotechKit", + bioa: "SW5E.ToolBioanalysisKit", + brew: "SW5E.ToolBrewerKit", + chef: "SW5E.ToolChefKit", + demo: "SW5E.ToolDemolitionKit", + disg: "SW5E.ToolDisguiseKit", + forg: "SW5E.ToolForgeryKit", + mech: "SW5E.ToolMechanicKit", + game: "SW5E.ToolGamingSet", + poi: "SW5E.ToolPoisonKit", + scav: "SW5E.ToolScavengingKit", + secur: "SW5E.ToolSecurityKit", + slic: "SW5E.ToolSlicerKit", + spice: "SW5E.ToolSpiceKit", + music: "SW5E.ToolMusicalInstrument", + vehicle: "SW5E.ToolVehicle" }; // TODO: Same as weapon IDs @@ -242,19 +256,18 @@ SW5E.toolIds = { * @type {Object} */ SW5E.timePeriods = { - "inst": "SW5E.TimeInst", - "turn": "SW5E.TimeTurn", - "round": "SW5E.TimeRound", - "minute": "SW5E.TimeMinute", - "hour": "SW5E.TimeHour", - "day": "SW5E.TimeDay", - "month": "SW5E.TimeMonth", - "year": "SW5E.TimeYear", - "perm": "SW5E.TimePerm", - "spec": "SW5E.Special" + inst: "SW5E.TimeInst", + turn: "SW5E.TimeTurn", + round: "SW5E.TimeRound", + minute: "SW5E.TimeMinute", + hour: "SW5E.TimeHour", + day: "SW5E.TimeDay", + month: "SW5E.TimeMonth", + year: "SW5E.TimeYear", + perm: "SW5E.TimePerm", + spec: "SW5E.Special" }; - /* -------------------------------------------- */ /** @@ -262,49 +275,47 @@ SW5E.timePeriods = { * @type {Object} */ SW5E.abilityActivationTypes = { - "none": "SW5E.None", - "action": "SW5E.Action", - "bonus": "SW5E.BonusAction", - "reaction": "SW5E.Reaction", - "minute": SW5E.timePeriods.minute, - "hour": SW5E.timePeriods.hour, - "day": SW5E.timePeriods.day, - "special": SW5E.timePeriods.spec, - "legendary": "SW5E.LegAct", - "lair": "SW5E.LairAct", - "crew": "SW5E.VehicleCrewAction" + none: "SW5E.None", + action: "SW5E.Action", + bonus: "SW5E.BonusAction", + reaction: "SW5E.Reaction", + minute: SW5E.timePeriods.minute, + hour: SW5E.timePeriods.hour, + day: SW5E.timePeriods.day, + special: SW5E.timePeriods.spec, + legendary: "SW5E.LegendaryActionLabel", + lair: "SW5E.LairActionLabel", + crew: "SW5E.VehicleCrewAction" }; /* -------------------------------------------- */ - SW5E.abilityConsumptionTypes = { - "ammo": "SW5E.ConsumeAmmunition", - "attribute": "SW5E.ConsumeAttribute", - "material": "SW5E.ConsumeMaterial", - "charges": "SW5E.ConsumeCharges" + ammo: "SW5E.ConsumeAmmunition", + attribute: "SW5E.ConsumeAttribute", + material: "SW5E.ConsumeMaterial", + charges: "SW5E.ConsumeCharges" }; - /* -------------------------------------------- */ // Creature Sizes SW5E.actorSizes = { - "tiny": "SW5E.SizeTiny", - "sm": "SW5E.SizeSmall", - "med": "SW5E.SizeMedium", - "lg": "SW5E.SizeLarge", - "huge": "SW5E.SizeHuge", - "grg": "SW5E.SizeGargantuan" + tiny: "SW5E.SizeTiny", + sm: "SW5E.SizeSmall", + med: "SW5E.SizeMedium", + lg: "SW5E.SizeLarge", + huge: "SW5E.SizeHuge", + grg: "SW5E.SizeGargantuan" }; SW5E.tokenSizes = { - "tiny": 1, - "sm": 1, - "med": 1, - "lg": 2, - "huge": 3, - "grg": 4 + tiny: 1, + sm: 1, + med: 1, + lg: 2, + huge: 3, + grg: 4 }; /** @@ -312,10 +323,10 @@ SW5E.tokenSizes = { * @enum {number} */ SW5E.tokenHPColors = { - temp: 0x66CCFF, - tempmax: 0x440066, - negmax: 0x550000 -} + temp: 0x66ccff, + tempmax: 0x440066, + negmax: 0x550000 +}; /* -------------------------------------------- */ @@ -324,17 +335,16 @@ SW5E.tokenHPColors = { * @type {Object} */ SW5E.creatureTypes = { - "aberration": "SW5E.CreatureAberration", - "beast": "SW5E.CreatureBeast", - "construct": "SW5E.CreatureConstruct", - "droid": "SW5E.CreatureDroid", - "force": "SW5E.CreatureForceEntity", - "humanoid": "SW5E.CreatureHumanoid", - "plant": "SW5E.CreaturePlant", - "undead": "SW5E.CreatureUndead" + aberration: "SW5E.CreatureAberration", + beast: "SW5E.CreatureBeast", + construct: "SW5E.CreatureConstruct", + droid: "SW5E.CreatureDroid", + force: "SW5E.CreatureForceEntity", + humanoid: "SW5E.CreatureHumanoid", + plant: "SW5E.CreaturePlant", + undead: "SW5E.CreatureUndead" }; - /* -------------------------------------------- */ /** @@ -342,22 +352,22 @@ SW5E.creatureTypes = { * @type {Object} */ SW5E.itemActionTypes = { - "mwak": "SW5E.ActionMWAK", - "rwak": "SW5E.ActionRWAK", - "mpak": "SW5E.ActionMPAK", - "rpak": "SW5E.ActionRPAK", - "save": "SW5E.ActionSave", - "heal": "SW5E.ActionHeal", - "abil": "SW5E.ActionAbil", - "util": "SW5E.ActionUtil", - "other": "SW5E.ActionOther" + mwak: "SW5E.ActionMWAK", + rwak: "SW5E.ActionRWAK", + mpak: "SW5E.ActionMPAK", + rpak: "SW5E.ActionRPAK", + save: "SW5E.ActionSave", + heal: "SW5E.ActionHeal", + abil: "SW5E.ActionAbil", + util: "SW5E.ActionUtil", + other: "SW5E.ActionOther" }; /* -------------------------------------------- */ SW5E.itemCapacityTypes = { - "items": "SW5E.ItemContainerCapacityItems", - "weight": "SW5E.ItemContainerCapacityWeight" + items: "SW5E.ItemContainerCapacityItems", + weight: "SW5E.ItemContainerCapacityWeight" }; /* -------------------------------------------- */ @@ -367,15 +377,14 @@ SW5E.itemCapacityTypes = { * @type {Object} */ SW5E.limitedUsePeriods = { - "sr": "SW5E.ShortRest", - "lr": "SW5E.LongRest", - "day": "SW5E.Day", - "charges": "SW5E.Charges", - "recharge": "SW5E.Recharge", - "refitting": "SW5E.Refitting" + sr: "SW5E.ShortRest", + lr: "SW5E.LongRest", + day: "SW5E.Day", + charges: "SW5E.Charges", + recharge: "SW5E.Recharge", + refitting: "SW5E.Refitting" }; - /* -------------------------------------------- */ /** @@ -383,23 +392,22 @@ SW5E.limitedUsePeriods = { * @type {Object} */ SW5E.equipmentTypes = { - "light": "SW5E.EquipmentLight", - "medium": "SW5E.EquipmentMedium", - "heavy": "SW5E.EquipmentHeavy", - "hyper": "SW5E.EquipmentHyperdrive", - "bonus": "SW5E.EquipmentBonus", - "natural": "SW5E.EquipmentNatural", - "powerc": "SW5E.EquipmentPowerCoupling", - "reactor": "SW5E.EquipmentReactor", - "shield": "SW5E.EquipmentShield", - "clothing": "SW5E.EquipmentClothing", - "trinket": "SW5E.EquipmentTrinket", - "ssarmor": "SW5E.EquipmentStarshipArmor", - "ssshield": "SW5E.EquipmentStarshipShield", - "vehicle": "SW5E.EquipmentVehicle" + light: "SW5E.EquipmentLight", + medium: "SW5E.EquipmentMedium", + heavy: "SW5E.EquipmentHeavy", + hyper: "SW5E.EquipmentHyperdrive", + bonus: "SW5E.EquipmentBonus", + natural: "SW5E.EquipmentNatural", + powerc: "SW5E.EquipmentPowerCoupling", + reactor: "SW5E.EquipmentReactor", + shield: "SW5E.EquipmentShield", + clothing: "SW5E.EquipmentClothing", + trinket: "SW5E.EquipmentTrinket", + ssarmor: "SW5E.EquipmentStarshipArmor", + ssshield: "SW5E.EquipmentStarshipShield", + vehicle: "SW5E.EquipmentVehicle" }; - /* -------------------------------------------- */ /** @@ -407,12 +415,25 @@ SW5E.equipmentTypes = { * @type {Object} */ SW5E.armorProficiencies = { - "lgt": SW5E.equipmentTypes.light, - "med": SW5E.equipmentTypes.medium, - "hvy": SW5E.equipmentTypes.heavy, - "shl": "SW5E.EquipmentShieldProficiency" + lgt: SW5E.equipmentTypes.light, + med: SW5E.equipmentTypes.medium, + hvy: SW5E.equipmentTypes.heavy, + shl: "SW5E.EquipmentShieldProficiency" }; +/** + * A map of armor item proficiency to actor item proficiency + * Used when a new player owned item is created + * @type {Object} + */ +SW5E.armorProficienciesMap = { + natural: true, + clothing: true, + light: "lgt", + medium: "med", + heavy: "hvy", + shield: "shl" +}; /* -------------------------------------------- */ @@ -421,16 +442,16 @@ SW5E.armorProficiencies = { * @type {Object} */ SW5E.consumableTypes = { - "adrenal": "SW5E.ConsumableAdrenal", - "poison": "SW5E.ConsumablePoison", - "explosive": "SW5E.ConsumableExplosive", - "food": "SW5E.ConsumableFood", - "medpac": "SW5E.ConsumableMedpac", - "technology": "SW5E.ConsumableTechnology", - "ammo": "SW5E.ConsumableAmmunition", - "trinket": "SW5E.ConsumableTrinket", - "force": "SW5E.ConsumableForce", - "tech": "SW5E.ConsumableTech" + adrenal: "SW5E.ConsumableAdrenal", + poison: "SW5E.ConsumablePoison", + explosive: "SW5E.ConsumableExplosive", + food: "SW5E.ConsumableFood", + medpac: "SW5E.ConsumableMedpac", + technology: "SW5E.ConsumableTechnology", + ammo: "SW5E.ConsumableAmmunition", + trinket: "SW5E.ConsumableTrinket", + force: "SW5E.ConsumableForce", + tech: "SW5E.ConsumableTech" }; /* -------------------------------------------- */ @@ -440,26 +461,25 @@ SW5E.consumableTypes = { * @type {Object} */ SW5E.currencies = { - "CR": "SW5E.CurrencyCR", - }; + CR: "SW5E.CurrencyCR" +}; /* -------------------------------------------- */ - // Damage Types SW5E.damageTypes = { - "acid": "SW5E.DamageAcid", - "cold": "SW5E.DamageCold", - "energy": "SW5E.DamageEnergy", - "fire": "SW5E.DamageFire", - "force": "SW5E.DamageForce", - "ion": "SW5E.DamageIon", - "kinetic": "SW5E.DamageKinetic", - "lightning": "SW5E.DamageLightning", - "necrotic": "SW5E.DamageNecrotic", - "poison": "SW5E.DamagePoison", - "psychic": "SW5E.DamagePsychic", - "sonic": "SW5E.DamageSonic" + acid: "SW5E.DamageAcid", + cold: "SW5E.DamageCold", + energy: "SW5E.DamageEnergy", + fire: "SW5E.DamageFire", + force: "SW5E.DamageForce", + ion: "SW5E.DamageIon", + kinetic: "SW5E.DamageKinetic", + lightning: "SW5E.DamageLightning", + necrotic: "SW5E.DamageNecrotic", + poison: "SW5E.DamagePoison", + psychic: "SW5E.DamagePsychic", + sonic: "SW5E.DamageSonic" }; // Damage Resistance Types @@ -467,39 +487,38 @@ SW5E.damageResistanceTypes = foundry.utils.deepClone(SW5E.damageTypes); /* -------------------------------------------- */ - // armor Types SW5E.armorPropertiesTypes = { -"Absorptive": "SW5E.ArmorProperAbsorptive", -"Agile": "SW5E.ArmorProperAgile", -"Anchor": "SW5E.ArmorProperAnchor", -"Avoidant": "SW5E.ArmorProperAvoidant", -"Barbed": "SW5E.ArmorProperBarbed", -"Bulky": "SW5E.ArmorProperBulky", -"Charging": "SW5E.ArmorProperCharging", -"Concealing": "SW5E.ArmorProperConcealing", -"Cumbersome": "SW5E.ArmorProperCumbersome", -"Gauntleted": "SW5E.ArmorProperGauntleted", -"Imbalanced": "SW5E.ArmorProperImbalanced", -"Impermeable": "SW5E.ArmorProperImpermeable", -"Insulated": "SW5E.ArmorProperInsulated", -"Interlocking": "SW5E.ArmorProperInterlocking", -"Lambent": "SW5E.ArmorProperLambent", -"Lightweight": "SW5E.ArmorProperLightweight", -"Magnetic": "SW5E.ArmorProperMagnetic", -"Obscured": "SW5E.ArmorProperObscured", -"Obtrusive": "SW5E.ArmorProperObtrusive", -"Powered": "SW5E.ArmorProperPowered", -"Reactive": "SW5E.ArmorProperReactive", -"Regulated": "SW5E.ArmorProperRegulated", -"Reinforced": "SW5E.ArmorProperReinforced", -"Responsive": "SW5E.ArmorProperResponsive", -"Rigid": "SW5E.ArmorProperRigid", -"Silent": "SW5E.ArmorProperSilent", -"Spiked": "SW5E.ArmorProperSpiked", -"Strength": "SW5E.ArmorProperStrength", -"Steadfast": "SW5E.ArmorProperSteadfast", -"Versatile": "SW5E.ArmorProperVersatile" + Absorptive: "SW5E.ArmorProperAbsorptive", + Agile: "SW5E.ArmorProperAgile", + Anchor: "SW5E.ArmorProperAnchor", + Avoidant: "SW5E.ArmorProperAvoidant", + Barbed: "SW5E.ArmorProperBarbed", + Bulky: "SW5E.ArmorProperBulky", + Charging: "SW5E.ArmorProperCharging", + Concealing: "SW5E.ArmorProperConcealing", + Cumbersome: "SW5E.ArmorProperCumbersome", + Gauntleted: "SW5E.ArmorProperGauntleted", + Imbalanced: "SW5E.ArmorProperImbalanced", + Impermeable: "SW5E.ArmorProperImpermeable", + Insulated: "SW5E.ArmorProperInsulated", + Interlocking: "SW5E.ArmorProperInterlocking", + Lambent: "SW5E.ArmorProperLambent", + Lightweight: "SW5E.ArmorProperLightweight", + Magnetic: "SW5E.ArmorProperMagnetic", + Obscured: "SW5E.ArmorProperObscured", + Obtrusive: "SW5E.ArmorProperObtrusive", + Powered: "SW5E.ArmorProperPowered", + Reactive: "SW5E.ArmorProperReactive", + Regulated: "SW5E.ArmorProperRegulated", + Reinforced: "SW5E.ArmorProperReinforced", + Responsive: "SW5E.ArmorProperResponsive", + Rigid: "SW5E.ArmorProperRigid", + Silent: "SW5E.ArmorProperSilent", + Spiked: "SW5E.ArmorProperSpiked", + Strength: "SW5E.ArmorProperStrength", + Steadfast: "SW5E.ArmorProperSteadfast", + Versatile: "SW5E.ArmorProperVersatile" }; /** @@ -508,15 +527,15 @@ SW5E.armorPropertiesTypes = { * @type {Object} */ SW5E.movementTypes = { - "burrow": "SW5E.MovementBurrow", - "climb": "SW5E.MovementClimb", - "crawl": "SW5E.MovementCrawl", - "fly": "SW5E.MovementFly", - "roll": "SW5E.MovementRoll", - "space": "SW5E.MovementSpace", - "swim": "SW5E.MovementSwim", - "turn": "SW5E.MovementTurn", - "walk": "SW5E.MovementWalk", + burrow: "SW5E.MovementBurrow", + climb: "SW5E.MovementClimb", + crawl: "SW5E.MovementCrawl", + fly: "SW5E.MovementFly", + roll: "SW5E.MovementRoll", + space: "SW5E.MovementSpace", + swim: "SW5E.MovementSwim", + turn: "SW5E.MovementTurn", + walk: "SW5E.MovementWalk" }; /** @@ -525,8 +544,8 @@ SW5E.movementTypes = { * @type {Object} */ SW5E.movementUnits = { - "ft": "SW5E.DistFt", - "mi": "SW5E.DistMi" + ft: "SW5E.DistFt", + mi: "SW5E.DistMi" }; /** @@ -535,27 +554,26 @@ SW5E.movementUnits = { * @type {Object} */ SW5E.distanceUnits = { - "none": "SW5E.None", - "self": "SW5E.DistSelf", - "touch": "SW5E.DistTouch", - "spec": "SW5E.Special", - "any": "SW5E.DistAny" + none: "SW5E.None", + self: "SW5E.DistSelf", + touch: "SW5E.DistTouch", + spec: "SW5E.Special", + any: "SW5E.DistAny" }; -for ( let [k, v] of Object.entries(SW5E.movementUnits) ) { - SW5E.distanceUnits[k] = v; +for (let [k, v] of Object.entries(SW5E.movementUnits)) { + SW5E.distanceUnits[k] = v; } /* -------------------------------------------- */ - /** * Configure aspects of encumbrance calculation so that it could be configured by modules * @type {Object} */ SW5E.encumbrance = { - currencyPerWeight: 50, - strMultiplier: 15, - vehicleWeightMultiplier: 2000 // 2000 lbs in a ton + currencyPerWeight: 50, + strMultiplier: 15, + vehicleWeightMultiplier: 2000 // 2000 lbs in a ton }; /* -------------------------------------------- */ @@ -565,59 +583,54 @@ SW5E.encumbrance = { * @type {Object} */ SW5E.targetTypes = { - "none": "SW5E.None", - "self": "SW5E.TargetSelf", - "creature": "SW5E.TargetCreature", - "droid": "SW5E.TargetDroid", - "ally": "SW5E.TargetAlly", - "enemy": "SW5E.TargetEnemy", - "object": "SW5E.TargetObject", - "space": "SW5E.TargetSpace", - "radius": "SW5E.TargetRadius", - "sphere": "SW5E.TargetSphere", - "cylinder": "SW5E.TargetCylinder", - "cone": "SW5E.TargetCone", - "square": "SW5E.TargetSquare", - "cube": "SW5E.TargetCube", - "line": "SW5E.TargetLine", - "starship": "SW5E.TargetStarship", - "wall": "SW5E.TargetWall", - "weapon": "SW5E.TargetWeapon" + none: "SW5E.None", + self: "SW5E.TargetSelf", + creature: "SW5E.TargetCreature", + droid: "SW5E.TargetDroid", + ally: "SW5E.TargetAlly", + enemy: "SW5E.TargetEnemy", + object: "SW5E.TargetObject", + space: "SW5E.TargetSpace", + radius: "SW5E.TargetRadius", + sphere: "SW5E.TargetSphere", + cylinder: "SW5E.TargetCylinder", + cone: "SW5E.TargetCone", + square: "SW5E.TargetSquare", + cube: "SW5E.TargetCube", + line: "SW5E.TargetLine", + starship: "SW5E.TargetStarship", + wall: "SW5E.TargetWall", + weapon: "SW5E.TargetWeapon" }; - /* -------------------------------------------- */ - /** * Map the subset of target types which produce a template area of effect * The keys are SW5E target types and the values are MeasuredTemplate shape types * @type {Object} */ SW5E.areaTargetTypes = { - cone: "cone", - cube: "rect", - cylinder: "circle", - line: "ray", - radius: "circle", - sphere: "circle", - square: "rect", - wall: "ray" + cone: "cone", + cube: "rect", + cylinder: "circle", + line: "ray", + radius: "circle", + sphere: "circle", + square: "rect", + wall: "ray" }; - /* -------------------------------------------- */ // Healing Types SW5E.healingTypes = { - "healing": "SW5E.Healing", - "temphp": "SW5E.HealingTemp" + healing: "SW5E.Healing", + temphp: "SW5E.HealingTemp" }; - /* -------------------------------------------- */ - /** * Enumerate the denominations of hit dice which can apply to classes in the SW5E system * @type {string[]} @@ -626,29 +639,120 @@ SW5E.hitDieTypes = ["d4", "d6", "d8", "d10", "d12", "d20"]; /* -------------------------------------------- */ - /** * Enumerate the denominations of power dice which can apply to starships in the SW5E system * @enum {string} */ SW5E.powerDieTypes = [1, "d4", "d6", "d8", "d10", "d12"]; - /* -------------------------------------------- */ /** * Enumerate the base stat and feature settings for starships based on size. - * @type {Array.} + * @type {Array.} */ SW5E.baseStarshipSettings = { - "tiny": {"changes":[{"key":"data.abilities.dex.value","value":4,"mode":2,"priority":20},{"key":"data.abilities.dex.proficient","value":1,"mode":4,"priority":20}, {"key":"data.abilities.con.value","value":-4,"mode":2,"priority":20}, {"key":"data.abilities.int.proficient","value":1,"mode":4,"priority":20}], "attributes":{"crewcap":null, "hd":"1d4", "hp":{"value":4, "max":4, "temp":4, "tempmax":4}, "hsm":1, "sd":"1d4", "mods":{"open":10, "max":10}, "suites":{"open":0, "max":0}, "movement":{"fly":300, "turn":300}}}, - "sm": {"changes":[{"key":"data.abilities.dex.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.dex.proficient","value":1,"mode":4,"priority":20},{"key":"data.abilities.con.value","value":-2,"mode":2,"priority":20},{"key":"data.abilities.str.proficient","value":1,"mode":4,"priority":20}], "attributes":{"crewcap":1, "hd":"3d6", "hp":{"value":6, "max":6, "temp":6, "tempmax":6}, "hsm":2, "sd":"3d6", "mods":{"open":20, "max":20}, "suites":{"open":-1, "max":-1}, "movement":{"fly":300, "turn":250}}}, - "med": {"attributes":{"crewcap":1, "hd":"5d8", "hp":{"value":8, "max":8, "temp":8, "tempmax":8}, "hsm":3, "sd":"5d8", "mods":{"open":30, "max":30}, "suites":{"open":3, "max":3}, "movement":{"fly":300, "turn":200}}}, - "lg": {"changes":[{"key":"data.abilities.dex.value","value":-2,"mode":2,"priority":20},{"key":"data.abilities.wis.proficient","value":1,"mode":4,"priority":20},{"key":"data.abilities.con.value","value":2,"mode":2,"priority":20}], "attributes":{"crewcap":200, "hd":"7d10", "hp":{"value":10, "max":10, "temp":10, "tempmax":10}, "hsm":4, "sd":"7d10", "mods":{"open":50, "max":50}, "suites":{"open":3, "max":3}, "movement":{"fly":300, "turn":150}}}, - "huge": {"changes":[{"key":"data.abilities.dex.value","value":-4,"mode":2,"priority":20},{"key":"data.abilities.wis.proficient","value":1,"mode":4,"priority":20},{"key":"data.abilities.con.value","value":4,"mode":2,"priority":20}], "attributes":{"crewcap":4000, "hd":"9d12", "hp":{"value":12, "max":12, "temp":12, "tempmax":12}, "hsm":2, "sd":"9d12", "mods":{"open":60, "max":60}, "suites":{"open":6, "max":6}, "movement":{"fly":300, "turn":100}}}, - "grg": {"changes":[{"key":"data.abilities.dex.value","value":-6,"mode":2,"priority":20},{"key":"data.abilities.wis.proficient","value":1,"mode":4,"priority":20},{"key":"data.abilities.con.value","value":6,"mode":2,"priority":20}], "attributes":{"crewcap":80000, "hd":"11d20", "hp":{"value":20, "max":20, "temp":20, "tempmax":20}, "hsm":3, "sd":"11d20", "mods":{"open":70, "max":70}, "suites":{"open":10, "max":10}, "movement":{"fly":300, "turn":50}}} -} + tiny: { + changes: [ + {key: "data.abilities.dex.value", value: 4, mode: 2, priority: 20}, + {key: "data.abilities.dex.proficient", value: 1, mode: 4, priority: 20}, + {key: "data.abilities.con.value", value: -4, mode: 2, priority: 20}, + {key: "data.abilities.int.proficient", value: 1, mode: 4, priority: 20} + ], + attributes: { + crewcap: null, + hd: "1d4", + hp: {value: 4, max: 4, temp: 4, tempmax: 4}, + hsm: 1, + sd: "1d4", + mods: {open: 10, max: 10}, + suites: {open: 0, max: 0}, + movement: {fly: 300, turn: 300} + } + }, + sm: { + changes: [ + {key: "data.abilities.dex.value", value: 2, mode: 2, priority: 20}, + {key: "data.abilities.dex.proficient", value: 1, mode: 4, priority: 20}, + {key: "data.abilities.con.value", value: -2, mode: 2, priority: 20}, + {key: "data.abilities.str.proficient", value: 1, mode: 4, priority: 20} + ], + attributes: { + crewcap: 1, + hd: "3d6", + hp: {value: 6, max: 6, temp: 6, tempmax: 6}, + hsm: 2, + sd: "3d6", + mods: {open: 20, max: 20}, + suites: {open: -1, max: -1}, + movement: {fly: 300, turn: 250} + } + }, + med: { + attributes: { + crewcap: 1, + hd: "5d8", + hp: {value: 8, max: 8, temp: 8, tempmax: 8}, + hsm: 3, + sd: "5d8", + mods: {open: 30, max: 30}, + suites: {open: 3, max: 3}, + movement: {fly: 300, turn: 200} + } + }, + lg: { + changes: [ + {key: "data.abilities.dex.value", value: -2, mode: 2, priority: 20}, + {key: "data.abilities.wis.proficient", value: 1, mode: 4, priority: 20}, + {key: "data.abilities.con.value", value: 2, mode: 2, priority: 20} + ], + attributes: { + crewcap: 200, + hd: "7d10", + hp: {value: 10, max: 10, temp: 10, tempmax: 10}, + hsm: 4, + sd: "7d10", + mods: {open: 50, max: 50}, + suites: {open: 3, max: 3}, + movement: {fly: 300, turn: 150} + } + }, + huge: { + changes: [ + {key: "data.abilities.dex.value", value: -4, mode: 2, priority: 20}, + {key: "data.abilities.wis.proficient", value: 1, mode: 4, priority: 20}, + {key: "data.abilities.con.value", value: 4, mode: 2, priority: 20} + ], + attributes: { + crewcap: 4000, + hd: "9d12", + hp: {value: 12, max: 12, temp: 12, tempmax: 12}, + hsm: 2, + sd: "9d12", + mods: {open: 60, max: 60}, + suites: {open: 6, max: 6}, + movement: {fly: 300, turn: 100} + } + }, + grg: { + changes: [ + {key: "data.abilities.dex.value", value: -6, mode: 2, priority: 20}, + {key: "data.abilities.wis.proficient", value: 1, mode: 4, priority: 20}, + {key: "data.abilities.con.value", value: 6, mode: 2, priority: 20} + ], + attributes: { + crewcap: 80000, + hd: "11d20", + hp: {value: 20, max: 20, temp: 20, tempmax: 20}, + hsm: 3, + sd: "11d20", + mods: {open: 70, max: 70}, + suites: {open: 10, max: 10}, + movement: {fly: 300, turn: 50} + } + } +}; /* -------------------------------------------- */ @@ -657,47 +761,46 @@ SW5E.baseStarshipSettings = { * @type {Object} */ - SW5E.starshipRolestiny = { -}; +SW5E.starshipRolestiny = {}; SW5E.starshipRolessm = { - "bmbr": "SW5E.StarshipBomber", - "intc": "SW5E.StarshipInterceptor", - "scout": "SW5E.StarshipScout", - "scrm": "SW5E.StarshipScrambler", - "shtl": "SW5E.StarshipShuttle", - "strf": "SW5E.StarshipStrikeFighter" + bmbr: "SW5E.StarshipBomber", + intc: "SW5E.StarshipInterceptor", + scout: "SW5E.StarshipScout", + scrm: "SW5E.StarshipScrambler", + shtl: "SW5E.StarshipShuttle", + strf: "SW5E.StarshipStrikeFighter" }; SW5E.starshipRolesmed = { - "cour": "SW5E.StarshipCourier", - "frtr": "SW5E.StarshipFreighter", - "gnbt": "SW5E.StarshipGunboat", - "msbt": "SW5E.StarshipMissileBoat", - "nvgt": "SW5E.StarshipNavigator", - "yacht": "SW5E.StarshipYacht" + cour: "SW5E.StarshipCourier", + frtr: "SW5E.StarshipFreighter", + gnbt: "SW5E.StarshipGunboat", + msbt: "SW5E.StarshipMissileBoat", + nvgt: "SW5E.StarshipNavigator", + yacht: "SW5E.StarshipYacht" }; SW5E.starshipRoleslg = { - "ambd": "SW5E.StarshipAmbassador", - "corv": "SW5E.StarshipCorvette", - "crui": "SW5E.StarshipCruiser", - "expl": "SW5E.StarshipExplorer", - "pics": "SW5E.StarshipPicketShip", - "shtd": "SW5E.StarshipShipsTender" + ambd: "SW5E.StarshipAmbassador", + corv: "SW5E.StarshipCorvette", + crui: "SW5E.StarshipCruiser", + expl: "SW5E.StarshipExplorer", + pics: "SW5E.StarshipPicketShip", + shtd: "SW5E.StarshipShipsTender" }; SW5E.starshipRoleshuge = { - "btls": "SW5E.StarshipBattleship", - "carr": "SW5E.StarshipCarrier", - "colo": "SW5E.StarshipColonizer", - "cmds": "SW5E.StarshipCommandShip", - "intd": "SW5E.StarshipInterdictor", - "jugg": "SW5E.StarshipJuggernaut" + btls: "SW5E.StarshipBattleship", + carr: "SW5E.StarshipCarrier", + colo: "SW5E.StarshipColonizer", + cmds: "SW5E.StarshipCommandShip", + intd: "SW5E.StarshipInterdictor", + jugg: "SW5E.StarshipJuggernaut" }; SW5E.starshipRolesgrg = { - "blks": "SW5E.StarshipBlockadeShip", - "flgs": "SW5E.StarshipFlagship", - "inct": "SW5E.StarshipIndustrialCenter", - "mbmt": "SW5E.StarshipMobileMetropolis", - "rsrc": "SW5E.StarshipResearcher", - "wars": "SW5E.StarshipWarship" + blks: "SW5E.StarshipBlockadeShip", + flgs: "SW5E.StarshipFlagship", + inct: "SW5E.StarshipIndustrialCenter", + mbmt: "SW5E.StarshipMobileMetropolis", + rsrc: "SW5E.StarshipResearcher", + wars: "SW5E.StarshipWarship" }; /* -------------------------------------------- */ @@ -707,52 +810,140 @@ SW5E.starshipRolesgrg = { * @type {Object} */ - SW5E.starshipRoleBonuses = { - "bmbr": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]}, - "intc": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20}]}, - "scout": {"changes":[{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]}, - "scrm": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20}]}, - "shtl": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20}]}, - "strf": {"changes":[{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]}, - "cour": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20}]}, - "frtr": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20}]}, - "gnbt": {"changes":[{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]}, - "msbt": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]}, - "nvgt": {"changes":[{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]}, - "yacht": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20}]}, - "ambd": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20}]}, - "corv": {"changes":[{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20}]}, - "crui": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]}, - "expl": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]}, - "pics": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]}, - "shtd": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]}, - "btls": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]}, - "carr": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]}, - "colo": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]}, - "cmds": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]}, - "intd": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]}, - "jugg": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]}, - "blks": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]}, - "flgs": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]}, - "inct": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]}, - "mbmt": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]}, - "rsrc": {"changes":[{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]}, - "wars": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]} +SW5E.starshipRoleBonuses = { + bmbr: {changes: [{key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}]}, + intc: {changes: [{key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}]}, + scout: {changes: [{key: "data.abilities.int.value", value: 1, mode: 2, priority: 20}]}, + scrm: {changes: [{key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}]}, + shtl: {changes: [{key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}]}, + strf: {changes: [{key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}]}, + cour: {changes: [{key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}]}, + frtr: {changes: [{key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}]}, + gnbt: {changes: [{key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}]}, + msbt: {changes: [{key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}]}, + nvgt: {changes: [{key: "data.abilities.int.value", value: 1, mode: 2, priority: 20}]}, + yacht: {changes: [{key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}]}, + ambd: { + changes: [ + {key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20} + ] + }, + corv: { + changes: [ + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20} + ] + }, + crui: { + changes: [ + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + expl: { + changes: [ + {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.int.value", value: 1, mode: 2, priority: 20} + ] + }, + pics: { + changes: [ + {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20} + ] + }, + shtd: { + changes: [ + {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + btls: { + changes: [ + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + carr: { + changes: [ + {key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.int.value", value: 1, mode: 2, priority: 20} + ] + }, + colo: { + changes: [ + {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.int.value", value: 1, mode: 2, priority: 20} + ] + }, + cmds: { + changes: [ + {key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20} + ] + }, + intd: { + changes: [ + {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + jugg: { + changes: [ + {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + blks: { + changes: [ + {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + flgs: { + changes: [ + {key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20} + ] + }, + inct: { + changes: [ + {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + mbmt: { + changes: [ + {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20} + ] + }, + rsrc: { + changes: [ + {key: "data.abilities.int.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20} + ] + }, + wars: { + changes: [ + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + } }; /* -------------------------------------------- */ - - /** * The set of possible sensory perception types which an Actor may have - * @type {object} + * @enum {string} */ SW5E.senses = { - "blindsight": "SW5E.SenseBlindsight", - "darkvision": "SW5E.SenseDarkvision", - "tremorsense": "SW5E.SenseTremorsense", - "truesight": "SW5E.SenseTruesight" + blindsight: "SW5E.SenseBlindsight", + darkvision: "SW5E.SenseDarkvision", + tremorsense: "SW5E.SenseTremorsense", + truesight: "SW5E.SenseTruesight" }; /* -------------------------------------------- */ @@ -762,24 +953,24 @@ SW5E.senses = { * @type {Object} */ SW5E.skills = { - "acr": "SW5E.SkillAcr", - "ani": "SW5E.SkillAni", - "ath": "SW5E.SkillAth", - "dec": "SW5E.SkillDec", - "ins": "SW5E.SkillIns", - "itm": "SW5E.SkillItm", - "inv": "SW5E.SkillInv", - "lor": "SW5E.SkillLor", - "med": "SW5E.SkillMed", - "nat": "SW5E.SkillNat", - "prc": "SW5E.SkillPrc", - "prf": "SW5E.SkillPrf", - "per": "SW5E.SkillPer", - "pil": "SW5E.SkillPil", - "slt": "SW5E.SkillSlt", - "ste": "SW5E.SkillSte", - "sur": "SW5E.SkillSur", - "tec": "SW5E.SkillTec" + acr: "SW5E.SkillAcr", + ani: "SW5E.SkillAni", + ath: "SW5E.SkillAth", + dec: "SW5E.SkillDec", + ins: "SW5E.SkillIns", + itm: "SW5E.SkillItm", + inv: "SW5E.SkillInv", + lor: "SW5E.SkillLor", + med: "SW5E.SkillMed", + nat: "SW5E.SkillNat", + prc: "SW5E.SkillPrc", + prf: "SW5E.SkillPrf", + per: "SW5E.SkillPer", + pil: "SW5E.SkillPil", + slt: "SW5E.SkillSlt", + ste: "SW5E.SkillSte", + sur: "SW5E.SkillSur", + tec: "SW5E.SkillTec" }; /* -------------------------------------------- */ @@ -789,29 +980,29 @@ SW5E.skills = { * @type {Object} */ SW5E.starshipSkills = { - "ast": "SW5E.StarshipSkillAst", - "bst": "SW5E.StarshipSkillBst", - "dat": "SW5E.StarshipSkillDat", - "hid": "SW5E.StarshipSkillHid", - "imp": "SW5E.StarshipSkillImp", - "int": "SW5E.StarshipSkillInt", - "man": "SW5E.StarshipSkillMan", - "men": "SW5E.StarshipSkillMen", - "pat": "SW5E.StarshipSkillPat", - "prb": "SW5E.StarshipSkillPrb", - "ram": "SW5E.StarshipSkillRam", - "reg": "SW5E.StarshipSkillReg", - "scn": "SW5E.StarshipSkillScn", - "swn": "SW5E.StarshipSkillSwn" + ast: "SW5E.StarshipSkillAst", + bst: "SW5E.StarshipSkillBst", + dat: "SW5E.StarshipSkillDat", + hid: "SW5E.StarshipSkillHid", + imp: "SW5E.StarshipSkillImp", + int: "SW5E.StarshipSkillInt", + man: "SW5E.StarshipSkillMan", + men: "SW5E.StarshipSkillMen", + pat: "SW5E.StarshipSkillPat", + prb: "SW5E.StarshipSkillPrb", + ram: "SW5E.StarshipSkillRam", + reg: "SW5E.StarshipSkillReg", + scn: "SW5E.StarshipSkillScn", + swn: "SW5E.StarshipSkillSwn" }; /* -------------------------------------------- */ SW5E.powerPreparationModes = { - "prepared": "SW5E.PowerPrepPrepared", - "always": "SW5E.PowerPrepAlways", - "atwill": "SW5E.PowerPrepAtWill", - "innate": "SW5E.PowerPrepInnate" + prepared: "SW5E.PowerPrepPrepared", + always: "SW5E.PowerPrepAlways", + atwill: "SW5E.PowerPrepAtWill", + innate: "SW5E.PowerPrepInnate" }; SW5E.powerUpcastModes = ["always", "prepared"]; @@ -822,12 +1013,12 @@ SW5E.powerUpcastModes = ["always", "prepared"]; */ SW5E.powerProgression = { - "none": "SW5E.PowerNone", - "consular": "SW5E.PowerProgCns", - "engineer": "SW5E.PowerProgEng", - "guardian": "SW5E.PowerProgGrd", - "scout": "SW5E.PowerProgSct", - "sentinel": "SW5E.PowerProgSnt" + none: "SW5E.PowerNone", + consular: "SW5E.PowerProgCns", + engineer: "SW5E.PowerProgEng", + guardian: "SW5E.PowerProgGrd", + scout: "SW5E.PowerProgSct", + sentinel: "SW5E.PowerProgSnt" }; /** @@ -835,12 +1026,12 @@ SW5E.powerProgression = { */ SW5E.powersKnown = { - "none": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], - "consular": [9,11,13,15,17,19,21,23,25,26,28,29,31,32,34,35,37,38,39,40], - "engineer": [6,7,9,10,12,13,15,16,18,19,21,22,23,24,25,26,27,28,29,30], - "guardian": [5,7,9,10,12,13,14,15,17,18,19,20,22,23,24,25,27,28,29,30], - "scout": [0,4,5,6,7,8,9,10,12,13,14,15,16,17,18,19,20,21,22,23], - "sentinel": [7,9,11,13,15,17,18,19,21,22,24,25,26,28,29,30,32,33,34,35] + none: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + consular: [9, 11, 13, 15, 17, 19, 21, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 39, 40], + engineer: [6, 7, 9, 10, 12, 13, 15, 16, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30], + guardian: [5, 7, 9, 10, 12, 13, 14, 15, 17, 18, 19, 20, 22, 23, 24, 25, 27, 28, 29, 30], + scout: [0, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], + sentinel: [7, 9, 11, 13, 15, 17, 18, 19, 21, 22, 24, 25, 26, 28, 29, 30, 32, 33, 34, 35] }; /** @@ -848,14 +1039,14 @@ SW5E.powersKnown = { */ SW5E.powerLimit = { - "none": [0,0,0,0,0,0,0,0,0], - "consular": [1000,1000,1000,1000,1000,1,1,1,1], - "engineer": [1000,1000,1000,1000,1000,1,1,1,1], - "guardian": [1000,1000,1000,1000,1,0,0,0,0], - "scout": [1000,1000,1000,1,1,0,0,0,0], - "sentinel": [1000,1000,1000,1000,1,1,1,0,0], - "innate": [1000,1000,1000,1000,1000,1000,1000,1000,1000], - "dual": [1000,1000,1000,1000,1000,1,1,1,1] + none: [0, 0, 0, 0, 0, 0, 0, 0, 0], + consular: [1000, 1000, 1000, 1000, 1000, 1, 1, 1, 1], + engineer: [1000, 1000, 1000, 1000, 1000, 1, 1, 1, 1], + guardian: [1000, 1000, 1000, 1000, 1, 0, 0, 0, 0], + scout: [1000, 1000, 1000, 1, 1, 0, 0, 0, 0], + sentinel: [1000, 1000, 1000, 1000, 1, 1, 1, 0, 0], + innate: [1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000], + dual: [1000, 1000, 1000, 1000, 1000, 1, 1, 1, 1] }; /** @@ -863,15 +1054,15 @@ SW5E.powerLimit = { */ SW5E.powerMaxLevel = { - "none": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], - "consular": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9], - "engineer": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9], - "guardian": [1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5], - "scout": [0,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5], - "sentinel": [1,1,2,2,2,3,3,3,4,4,5,5,5,6,6,6,7,7,7,7], - "multi": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9], - "innate": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9], - "dual": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9] + none: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + consular: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9], + engineer: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9], + guardian: [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5], + scout: [0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5], + sentinel: [1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 7], + multi: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9], + innate: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9], + dual: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9] }; /** @@ -879,12 +1070,12 @@ SW5E.powerMaxLevel = { */ SW5E.powerPoints = { - "none": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], - "consular": [4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80], - "engineer": [2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40], - "guardian": [2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40], - "scout": [0,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], - "sentinel": [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57,60] + none: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + consular: [4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80], + engineer: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40], + guardian: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40], + scout: [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], + sentinel: [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60] }; /* -------------------------------------------- */ @@ -894,37 +1085,34 @@ SW5E.powerPoints = { * @type {Object} */ SW5E.powerScalingModes = { - "none": "SW5E.PowerNone", - "atwill": "SW5E.PowerAtWill", - "level": "SW5E.PowerLevel" + none: "SW5E.PowerNone", + atwill: "SW5E.PowerAtWill", + level: "SW5E.PowerLevel" }; /* -------------------------------------------- */ - /** * Define the set of types which a weapon item can take * @type {Object} */ SW5E.weaponTypes = { - - "ammo": "SW5E.WeaponAmmo", - "improv": "SW5E.WeaponImprov", - "martialVW": "SW5E.WeaponMartialVW", - "martialB": "SW5E.WeaponMartialB", - "martialLW": "SW5E.WeaponMartialLW", - "natural": "SW5E.WeaponNatural", - "siege": "SW5E.WeaponSiege", - "simpleVW": "SW5E.WeaponSimpleVW", - "simpleB": "SW5E.WeaponSimpleB", - "simpleLW": "SW5E.WeaponSimpleLW", - "primary (starship)": "SW5E.WeaponPrimarySW", - "secondary (starship)": "SW5E.WeaponSecondarySW", - "tertiary (starship)": "SW5E.WeaponTertiarySW", - "quaternary (starship)": "SW5E.WeaponQuaternarySW" + "ammo": "SW5E.WeaponAmmo", + "improv": "SW5E.WeaponImprov", + "martialVW": "SW5E.WeaponMartialVW", + "martialB": "SW5E.WeaponMartialB", + "martialLW": "SW5E.WeaponMartialLW", + "natural": "SW5E.WeaponNatural", + "siege": "SW5E.WeaponSiege", + "simpleVW": "SW5E.WeaponSimpleVW", + "simpleB": "SW5E.WeaponSimpleB", + "simpleLW": "SW5E.WeaponSimpleLW", + "primary (starship)": "SW5E.WeaponPrimarySW", + "secondary (starship)": "SW5E.WeaponSecondarySW", + "tertiary (starship)": "SW5E.WeaponTertiarySW", + "quaternary (starship)": "SW5E.WeaponQuaternarySW" }; - /* -------------------------------------------- */ /** @@ -932,48 +1120,48 @@ SW5E.weaponTypes = { * @type {Object} */ SW5E.weaponProperties = { - "amm": "SW5E.WeaponPropertiesAmm", - "aut": "SW5E.WeaponPropertiesAut", - "bur": "SW5E.WeaponPropertiesBur", - "con": "SW5E.WeaponPropertiesCon", - "def": "SW5E.WeaponPropertiesDef", - "dex": "SW5E.WeaponPropertiesDex", - "dir": "SW5E.WeaponPropertiesDir", - "drm": "SW5E.WeaponPropertiesDrm", - "dgd": "SW5E.WeaponPropertiesDgd", - "dis": "SW5E.WeaponPropertiesDis", - "dpt": "SW5E.WeaponPropertiesDpt", - "dou": "SW5E.WeaponPropertiesDou", - "exp": "SW5E.WeaponPropertiesExp", - "fin": "SW5E.WeaponPropertiesFin", - "fix": "SW5E.WeaponPropertiesFix", - "foc": "SW5E.WeaponPropertiesFoc", - "hvy": "SW5E.WeaponPropertiesHvy", - "hid": "SW5E.WeaponPropertiesHid", - "hom": "SW5E.WeaponPropertiesHom", - "ion": "SW5E.WeaponPropertiesIon", - "ken": "SW5E.WeaponPropertiesKen", - "lgt": "SW5E.WeaponPropertiesLgt", - "lum": "SW5E.WeaponPropertiesLum", - "mlt": "SW5E.WeaponPropertiesMlt", - "mig": "SW5E.WeaponPropertiesMig", - "ovr": "SW5E.WeaponPropertiesOvr", - "pic": "SW5E.WeaponPropertiesPic", - "pow": "SW5E.WeaponPropertiesPow", - "rap": "SW5E.WeaponPropertiesRap", - "rch": "SW5E.WeaponPropertiesRch", - "rel": "SW5E.WeaponPropertiesRel", - "ret": "SW5E.WeaponPropertiesRet", - "sat": "SW5E.WeaponPropertiesSat", - "shk": "SW5E.WeaponPropertiesShk", - "sil": "SW5E.WeaponPropertiesSil", - "spc": "SW5E.WeaponPropertiesSpc", - "str": "SW5E.WeaponPropertiesStr", - "thr": "SW5E.WeaponPropertiesThr", - "two": "SW5E.WeaponPropertiesTwo", - "ver": "SW5E.WeaponPropertiesVer", - "vic": "SW5E.WeaponPropertiesVic", - "zon": "SW5E.WeaponPropertiesZon" + amm: "SW5E.WeaponPropertiesAmm", + aut: "SW5E.WeaponPropertiesAut", + bur: "SW5E.WeaponPropertiesBur", + con: "SW5E.WeaponPropertiesCon", + def: "SW5E.WeaponPropertiesDef", + dex: "SW5E.WeaponPropertiesDex", + dir: "SW5E.WeaponPropertiesDir", + drm: "SW5E.WeaponPropertiesDrm", + dgd: "SW5E.WeaponPropertiesDgd", + dis: "SW5E.WeaponPropertiesDis", + dpt: "SW5E.WeaponPropertiesDpt", + dou: "SW5E.WeaponPropertiesDou", + exp: "SW5E.WeaponPropertiesExp", + fin: "SW5E.WeaponPropertiesFin", + fix: "SW5E.WeaponPropertiesFix", + foc: "SW5E.WeaponPropertiesFoc", + hvy: "SW5E.WeaponPropertiesHvy", + hid: "SW5E.WeaponPropertiesHid", + hom: "SW5E.WeaponPropertiesHom", + ion: "SW5E.WeaponPropertiesIon", + ken: "SW5E.WeaponPropertiesKen", + lgt: "SW5E.WeaponPropertiesLgt", + lum: "SW5E.WeaponPropertiesLum", + mlt: "SW5E.WeaponPropertiesMlt", + mig: "SW5E.WeaponPropertiesMig", + ovr: "SW5E.WeaponPropertiesOvr", + pic: "SW5E.WeaponPropertiesPic", + pow: "SW5E.WeaponPropertiesPow", + rap: "SW5E.WeaponPropertiesRap", + rch: "SW5E.WeaponPropertiesRch", + rel: "SW5E.WeaponPropertiesRel", + ret: "SW5E.WeaponPropertiesRet", + sat: "SW5E.WeaponPropertiesSat", + shk: "SW5E.WeaponPropertiesShk", + sil: "SW5E.WeaponPropertiesSil", + spc: "SW5E.WeaponPropertiesSpc", + str: "SW5E.WeaponPropertiesStr", + thr: "SW5E.WeaponPropertiesThr", + two: "SW5E.WeaponPropertiesTwo", + ver: "SW5E.WeaponPropertiesVer", + vic: "SW5E.WeaponPropertiesVic", + zon: "SW5E.WeaponPropertiesZon" }; /* -------------------------------------------- */ @@ -983,42 +1171,42 @@ SW5E.weaponProperties = { * @type {Object} */ SW5E.weaponSizes = { - "tiny": "SW5E.SizeTiny", - "sm": "SW5E.SizeSmall", - "med": "SW5E.SizeMedium", - "lg": "SW5E.SizeLarge", - "huge": "SW5E.SizeHuge", - "grg": "SW5E.SizeGargantuan" -}; + tiny: "SW5E.SizeTiny", + sm: "SW5E.SizeSmall", + med: "SW5E.SizeMedium", + lg: "SW5E.SizeLarge", + huge: "SW5E.SizeHuge", + grg: "SW5E.SizeGargantuan" +}; // Power Components SW5E.powerComponents = { - "V": "SW5E.ComponentVerbal", - "S": "SW5E.ComponentSomatic", - "M": "SW5E.ComponentMaterial" + V: "SW5E.ComponentVerbal", + S: "SW5E.ComponentSomatic", + M: "SW5E.ComponentMaterial" }; // Power Schools SW5E.powerSchools = { - "lgt": "SW5E.SchoolLgt", - "uni": "SW5E.SchoolUni", - "drk": "SW5E.SchoolDrk", - "tec": "SW5E.SchoolTec", - "enh": "SW5E.SchoolEnh" + lgt: "SW5E.SchoolLgt", + uni: "SW5E.SchoolUni", + drk: "SW5E.SchoolDrk", + tec: "SW5E.SchoolTec", + enh: "SW5E.SchoolEnh" }; // Power Levels SW5E.powerLevels = { - 0: "SW5E.PowerLevel0", - 1: "SW5E.PowerLevel1", - 2: "SW5E.PowerLevel2", - 3: "SW5E.PowerLevel3", - 4: "SW5E.PowerLevel4", - 5: "SW5E.PowerLevel5", - 6: "SW5E.PowerLevel6", - 7: "SW5E.PowerLevel7", - 8: "SW5E.PowerLevel8", - 9: "SW5E.PowerLevel9" + 0: "SW5E.PowerLevel0", + 1: "SW5E.PowerLevel1", + 2: "SW5E.PowerLevel2", + 3: "SW5E.PowerLevel3", + 4: "SW5E.PowerLevel4", + 5: "SW5E.PowerLevel5", + 6: "SW5E.PowerLevel6", + 7: "SW5E.PowerLevel7", + 8: "SW5E.PowerLevel8", + 9: "SW5E.PowerLevel9" }; // TODO: This is used for spell scrolls, it maps the level to the compendium ID of the item the spell would be bound to @@ -1044,23 +1232,23 @@ SW5E.powerScrollIds = { * @enum {string} */ SW5E.sourcePacks = { - ITEMS: "sw5e.items" -} + ITEMS: "sw5e.items" +}; // Polymorph options. SW5E.polymorphSettings = { - keepPhysical: 'SW5E.PolymorphKeepPhysical', - keepMental: 'SW5E.PolymorphKeepMental', - keepSaves: 'SW5E.PolymorphKeepSaves', - keepSkills: 'SW5E.PolymorphKeepSkills', - mergeSaves: 'SW5E.PolymorphMergeSaves', - mergeSkills: 'SW5E.PolymorphMergeSkills', - keepClass: 'SW5E.PolymorphKeepClass', - keepFeats: 'SW5E.PolymorphKeepFeats', - keepPowers: 'SW5E.PolymorphKeepPowers', - keepItems: 'SW5E.PolymorphKeepItems', - keepBio: 'SW5E.PolymorphKeepBio', - keepVision: 'SW5E.PolymorphKeepVision' + keepPhysical: "SW5E.PolymorphKeepPhysical", + keepMental: "SW5E.PolymorphKeepMental", + keepSaves: "SW5E.PolymorphKeepSaves", + keepSkills: "SW5E.PolymorphKeepSkills", + mergeSaves: "SW5E.PolymorphMergeSaves", + mergeSkills: "SW5E.PolymorphMergeSkills", + keepClass: "SW5E.PolymorphKeepClass", + keepFeats: "SW5E.PolymorphKeepFeats", + keepPowers: "SW5E.PolymorphKeepPowers", + keepItems: "SW5E.PolymorphKeepItems", + keepBio: "SW5E.PolymorphKeepBio", + keepVision: "SW5E.PolymorphKeepVision" }; /* -------------------------------------------- */ @@ -1071,10 +1259,10 @@ SW5E.polymorphSettings = { * @type {Object} */ SW5E.proficiencyLevels = { - 0: "SW5E.NotProficient", - 1: "SW5E.Proficient", - 0.5: "SW5E.HalfProficient", - 2: "SW5E.Expertise" + 0: "SW5E.NotProficient", + 1: "SW5E.Proficient", + 0.5: "SW5E.HalfProficient", + 2: "SW5E.Expertise" }; /* -------------------------------------------- */ @@ -1085,157 +1273,156 @@ SW5E.proficiencyLevels = { * in play, we take the highest value. */ SW5E.cover = { - 0: 'SW5E.None', - .5: 'SW5E.CoverHalf', - .75: 'SW5E.CoverThreeQuarters', - 1: 'SW5E.CoverTotal' + 0: "SW5E.None", + 0.5: "SW5E.CoverHalf", + 0.75: "SW5E.CoverThreeQuarters", + 1: "SW5E.CoverTotal" }; /* -------------------------------------------- */ - // Condition Types SW5E.conditionTypes = { - "blinded": "SW5E.ConBlinded", - "charmed": "SW5E.ConCharmed", - "deafened": "SW5E.ConDeafened", - "diseased": "SW5E.ConDiseased", - "exhaustion": "SW5E.ConExhaustion", - "frightened": "SW5E.ConFrightened", - "grappled": "SW5E.ConGrappled", - "incapacitated": "SW5E.ConIncapacitated", - "invisible": "SW5E.ConInvisible", - "paralyzed": "SW5E.ConParalyzed", - "petrified": "SW5E.ConPetrified", - "poisoned": "SW5E.ConPoisoned", - "prone": "SW5E.ConProne", - "restrained": "SW5E.ConRestrained", - "shocked": "SW5E.ConShocked", - "slowed": "SW5E.ConSlowed", - "stunned": "SW5E.ConStunned", - "unconscious": "SW5E.ConUnconscious" + blinded: "SW5E.ConBlinded", + charmed: "SW5E.ConCharmed", + deafened: "SW5E.ConDeafened", + diseased: "SW5E.ConDiseased", + exhaustion: "SW5E.ConExhaustion", + frightened: "SW5E.ConFrightened", + grappled: "SW5E.ConGrappled", + incapacitated: "SW5E.ConIncapacitated", + invisible: "SW5E.ConInvisible", + paralyzed: "SW5E.ConParalyzed", + petrified: "SW5E.ConPetrified", + poisoned: "SW5E.ConPoisoned", + prone: "SW5E.ConProne", + restrained: "SW5E.ConRestrained", + shocked: "SW5E.ConShocked", + slowed: "SW5E.ConSlowed", + stunned: "SW5E.ConStunned", + unconscious: "SW5E.ConUnconscious" }; // Languages SW5E.languages = { - "abyssin": "SW5E.LanguagesAbyssin", - "aleena": "SW5E.LanguagesAleena", - "antarian": "SW5E.LanguagesAntarian", - "anzellan": "SW5E.LanguagesAnzellan", - "aqualish": "SW5E.LanguagesAqualish", - "arconese": "SW5E.LanguagesArconese", - "ardennian": "SW5E.LanguagesArdennian", - "arkanian": "SW5E.LanguagesArkanian", - "balosur": "SW5E.LanguagesBalosur", - "barabel": "SW5E.LanguagesBarabel", - "basic": "SW5E.LanguagesBasic", - "besalisk": "SW5E.LanguagesBesalisk", - "binary": "SW5E.LanguagesBinary", - "bith": "SW5E.LanguagesBith", - "bocce": "SW5E.LanguagesBocce", - "bothese": "SW5E.LanguagesBothese", - "catharese": "SW5E.LanguagesCatharese", - "cerean": "SW5E.LanguagesCerean", - "chadra-fan": "SW5E.LanguagesChadra-Fan", - "chagri": "SW5E.LanguagesChagri", - "cheunh": "SW5E.LanguagesCheunh", - "chevin": "SW5E.LanguagesChevin", - "chironan": "SW5E.LanguagesChironan", - "clawdite": "SW5E.LanguagesClawdite", - "codruese": "SW5E.LanguagesCodruese", - "colicoid": "SW5E.LanguagesColicoid", - "dashadi": "SW5E.LanguagesDashadi", - "defel": "SW5E.LanguagesDefel", - "devaronese": "SW5E.LanguagesDevaronese", - "dosh": "SW5E.LanguagesDosh", - "draethos": "SW5E.LanguagesDraethos", - "durese": "SW5E.LanguagesDurese", - "dug": "SW5E.LanguagesDug", - "ewokese": "SW5E.LanguagesEwokese", - "falleen": "SW5E.LanguagesFalleen", - "felucianese": "SW5E.LanguagesFelucianese", - "gamorrese": "SW5E.LanguagesGamorrese", - "gand": "SW5E.LanguagesGand", - "geonosian": "SW5E.LanguagesGeonosian", - "givin": "SW5E.LanguagesGivin", - "gran": "SW5E.LanguagesGran", - "gungan": "SW5E.LanguagesGungan", - "hapan": "SW5E.LanguagesHapan", - "harchese": "SW5E.LanguagesHarchese", - "herglese": "SW5E.LanguagesHerglese", - "honoghran": "SW5E.LanguagesHonoghran", - "huttese": "SW5E.LanguagesHuttese", - "iktotchese": "SW5E.LanguagesIktotchese", - "ithorese": "SW5E.LanguagesIthorese", - "jawaese": "SW5E.LanguagesJawaese", - "kaleesh": "SW5E.LanguagesKaleesh", - "kaminoan": "SW5E.LanguagesKaminoan", - "karkaran": "SW5E.LanguagesKarkaran", - "keldor": "SW5E.LanguagesKelDor", - "kharan": "SW5E.LanguagesKharan", - "killik": "SW5E.LanguagesKillik", - "klatooinian": "SW5E.LanguagesKlatooinian", - "kubazian": "SW5E.LanguagesKubazian", - "kushiban": "SW5E.LanguagesKushiban", - "kyuzo": "SW5E.LanguagesKyuzo", - "lannik": "SW5E.LanguagesLannik", - "lasat": "SW5E.LanguagesLasat", - "lowickese": "SW5E.LanguagesLowickese", - "lurmese": "SW5E.LanguagesLurmese", - "mandoa": "SW5E.LanguagesMandoa", - "miralukese": "SW5E.LanguagesMiralukese", - "mirialan": "SW5E.LanguagesMirialan", - "moncal": "SW5E.LanguagesMonCal", - "mustafarian": "SW5E.LanguagesMustafarian", - "muun": "SW5E.LanguagesMuun", - "nautila": "SW5E.LanguagesNautila", - "ortolan": "SW5E.LanguagesOrtolan", - "pakpak": "SW5E.LanguagesPakPak", - "pyke": "SW5E.LanguagesPyke", - "quarrenese": "SW5E.LanguagesQuarrenese", - "rakata": "SW5E.LanguagesRakata", - "rattataki": "SW5E.LanguagesRattataki", - "rishii": "SW5E.LanguagesRishii", - "rodese": "SW5E.LanguagesRodese", - "ryn": "SW5E.LanguagesRyn", - "selkatha": "SW5E.LanguagesSelkatha", - "semblan": "SW5E.LanguagesSemblan", - "shistavanen": "SW5E.LanguagesShistavanen", - "shyriiwook": "SW5E.LanguagesShyriiwook", - "sith": "SW5E.LanguagesSith", - "squibbian": "SW5E.LanguagesSquibbian", - "sriluurian": "SW5E.LanguagesSriluurian", - "ssi-ruuvi": "SW5E.LanguagesSsi-ruuvi", - "sullustese": "SW5E.LanguagesSullustese", - "talzzi": "SW5E.LanguagesTalzzi", - "tarasinese": "SW5E.LanguagesTarasinese", - "thisspiasian": "SW5E.LanguagesThisspiasian", - "togorese": "SW5E.LanguagesTogorese", - "togruti": "SW5E.LanguagesTogruti", - "toydarian": "SW5E.LanguagesToydarian", - "tusken": "SW5E.LanguagesTusken", - "twi'leki": "SW5E.LanguagesTwileki", - "ugnaught": "SW5E.LanguagesUgnaught", - "umbaran": "SW5E.LanguagesUmbaran", - "utapese": "SW5E.LanguagesUtapese", - "verpine": "SW5E.LanguagesVerpine", - "vong": "SW5E.LanguagesVong", - "voss": "SW5E.LanguagesVoss", - "yevethan": "SW5E.LanguagesYevethan", - "zabraki": "SW5E.LanguagesZabraki", - "zygerrian": "SW5E.LanguagesZygerrian" + "abyssin": "SW5E.LanguagesAbyssin", + "aleena": "SW5E.LanguagesAleena", + "antarian": "SW5E.LanguagesAntarian", + "anzellan": "SW5E.LanguagesAnzellan", + "aqualish": "SW5E.LanguagesAqualish", + "arconese": "SW5E.LanguagesArconese", + "ardennian": "SW5E.LanguagesArdennian", + "arkanian": "SW5E.LanguagesArkanian", + "balosur": "SW5E.LanguagesBalosur", + "barabel": "SW5E.LanguagesBarabel", + "basic": "SW5E.LanguagesBasic", + "besalisk": "SW5E.LanguagesBesalisk", + "binary": "SW5E.LanguagesBinary", + "bith": "SW5E.LanguagesBith", + "bocce": "SW5E.LanguagesBocce", + "bothese": "SW5E.LanguagesBothese", + "catharese": "SW5E.LanguagesCatharese", + "cerean": "SW5E.LanguagesCerean", + "chadra-fan": "SW5E.LanguagesChadra-Fan", + "chagri": "SW5E.LanguagesChagri", + "cheunh": "SW5E.LanguagesCheunh", + "chevin": "SW5E.LanguagesChevin", + "chironan": "SW5E.LanguagesChironan", + "clawdite": "SW5E.LanguagesClawdite", + "codruese": "SW5E.LanguagesCodruese", + "colicoid": "SW5E.LanguagesColicoid", + "dashadi": "SW5E.LanguagesDashadi", + "defel": "SW5E.LanguagesDefel", + "devaronese": "SW5E.LanguagesDevaronese", + "dosh": "SW5E.LanguagesDosh", + "draethos": "SW5E.LanguagesDraethos", + "durese": "SW5E.LanguagesDurese", + "dug": "SW5E.LanguagesDug", + "ewokese": "SW5E.LanguagesEwokese", + "falleen": "SW5E.LanguagesFalleen", + "felucianese": "SW5E.LanguagesFelucianese", + "gamorrese": "SW5E.LanguagesGamorrese", + "gand": "SW5E.LanguagesGand", + "geonosian": "SW5E.LanguagesGeonosian", + "givin": "SW5E.LanguagesGivin", + "gran": "SW5E.LanguagesGran", + "gungan": "SW5E.LanguagesGungan", + "hapan": "SW5E.LanguagesHapan", + "harchese": "SW5E.LanguagesHarchese", + "herglese": "SW5E.LanguagesHerglese", + "honoghran": "SW5E.LanguagesHonoghran", + "huttese": "SW5E.LanguagesHuttese", + "iktotchese": "SW5E.LanguagesIktotchese", + "ithorese": "SW5E.LanguagesIthorese", + "jawaese": "SW5E.LanguagesJawaese", + "kaleesh": "SW5E.LanguagesKaleesh", + "kaminoan": "SW5E.LanguagesKaminoan", + "karkaran": "SW5E.LanguagesKarkaran", + "keldor": "SW5E.LanguagesKelDor", + "kharan": "SW5E.LanguagesKharan", + "killik": "SW5E.LanguagesKillik", + "klatooinian": "SW5E.LanguagesKlatooinian", + "kubazian": "SW5E.LanguagesKubazian", + "kushiban": "SW5E.LanguagesKushiban", + "kyuzo": "SW5E.LanguagesKyuzo", + "lannik": "SW5E.LanguagesLannik", + "lasat": "SW5E.LanguagesLasat", + "lowickese": "SW5E.LanguagesLowickese", + "lurmese": "SW5E.LanguagesLurmese", + "mandoa": "SW5E.LanguagesMandoa", + "miralukese": "SW5E.LanguagesMiralukese", + "mirialan": "SW5E.LanguagesMirialan", + "moncal": "SW5E.LanguagesMonCal", + "mustafarian": "SW5E.LanguagesMustafarian", + "muun": "SW5E.LanguagesMuun", + "nautila": "SW5E.LanguagesNautila", + "ortolan": "SW5E.LanguagesOrtolan", + "pakpak": "SW5E.LanguagesPakPak", + "pyke": "SW5E.LanguagesPyke", + "quarrenese": "SW5E.LanguagesQuarrenese", + "rakata": "SW5E.LanguagesRakata", + "rattataki": "SW5E.LanguagesRattataki", + "rishii": "SW5E.LanguagesRishii", + "rodese": "SW5E.LanguagesRodese", + "ryn": "SW5E.LanguagesRyn", + "selkatha": "SW5E.LanguagesSelkatha", + "semblan": "SW5E.LanguagesSemblan", + "shistavanen": "SW5E.LanguagesShistavanen", + "shyriiwook": "SW5E.LanguagesShyriiwook", + "sith": "SW5E.LanguagesSith", + "squibbian": "SW5E.LanguagesSquibbian", + "sriluurian": "SW5E.LanguagesSriluurian", + "ssi-ruuvi": "SW5E.LanguagesSsi-ruuvi", + "sullustese": "SW5E.LanguagesSullustese", + "talzzi": "SW5E.LanguagesTalzzi", + "tarasinese": "SW5E.LanguagesTarasinese", + "thisspiasian": "SW5E.LanguagesThisspiasian", + "togorese": "SW5E.LanguagesTogorese", + "togruti": "SW5E.LanguagesTogruti", + "toydarian": "SW5E.LanguagesToydarian", + "tusken": "SW5E.LanguagesTusken", + "twi'leki": "SW5E.LanguagesTwileki", + "ugnaught": "SW5E.LanguagesUgnaught", + "umbaran": "SW5E.LanguagesUmbaran", + "utapese": "SW5E.LanguagesUtapese", + "verpine": "SW5E.LanguagesVerpine", + "vong": "SW5E.LanguagesVong", + "voss": "SW5E.LanguagesVoss", + "yevethan": "SW5E.LanguagesYevethan", + "zabraki": "SW5E.LanguagesZabraki", + "zygerrian": "SW5E.LanguagesZygerrian" }; // Character Level XP Requirements -SW5E.CHARACTER_EXP_LEVELS = [ - 0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000, - 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000] -; +SW5E.CHARACTER_EXP_LEVELS = [ + 0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000, 120000, 140000, 165000, 195000, 225000, + 265000, 305000, 355000 +]; // Challenge Rating XP Levels SW5E.CR_EXP_LEVELS = [ - 10, 200, 450, 700, 1100, 1800, 2300, 2900, 3900, 5000, 5900, 7200, 8400, 10000, 11500, 13000, 15000, 18000, - 20000, 22000, 25000, 33000, 41000, 50000, 62000, 75000, 90000, 105000, 120000, 135000, 155000 + 10, 200, 450, 700, 1100, 1800, 2300, 2900, 3900, 5000, 5900, 7200, 8400, 10000, 11500, 13000, 15000, 18000, 20000, + 22000, 25000, 33000, 41000, 50000, 62000, 75000, 90000, 105000, 120000, 135000, 155000 ]; // Character Features Per Class And Level @@ -1243,354 +1430,354 @@ SW5E.classFeatures = ClassFeatures; // Configure Optional Character Flags SW5E.characterFlags = { - "adaptiveResilience": { - name: "SW5E.FlagsAdaptiveResilience", - hint: "SW5E.FlagsAdaptiveResilienceHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "aggressive": { - name: "SW5E.FlagsAggressive", - hint: "SW5E.FlagsAggressiveHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "amphibious": { - name: "SW5E.FlagsAmphibious", - hint: "SW5E.FlagsAmphibiousHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "armorIntegration": { - name: "SW5E.FlagsArmorIntegration", - hint: "SW5E.FlagsArmorIntegrationHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "businessSavvy": { - name: "SW5E.FlagsBusinessSavvy", - hint: "SW5E.FlagsBusinessSavvyHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "cannibalize": { - name: "SW5E.FlagsCannibalize", - hint: "SW5E.FlagsCannibalizeHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "closedMind": { - name: "SW5E.FlagsClosedMind", - hint: "SW5E.FlagsClosedMindHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "crudeWeaponSpecialists": { - name: "SW5E.FlagsCrudeWeaponSpecialists", - hint: "SW5E.FlagsCrudeWeaponSpecialistsHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "defiant": { - name: "SW5E.FlagsDefiant", - hint: "SW5E.FlagsDefiantHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "detailOriented": { - name: "SW5E.FlagsDetailOriented", - hint: "SW5E.FlagsDetailOrientedHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "enthrallingPheromones": { - name: "SW5E.FlagsEnthrallingPheromones", - hint: "SW5E.FlagsEnthrallingPheromonesHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "extraArms": { - name: "SW5E.FlagsExtraArms", - hint: "SW5E.FlagsExtraArmsHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "forceContention": { - name: "SW5E.FlagsForceContention", - hint: "SW5E.FlagsForceContentionHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "forceInsensitive": { - name: "SW5E.FlagsForceInsensitive", - hint: "SW5E.FlagsForceInsensitiveHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "foreignBiology": { - name: "SW5E.FlagsForeignBiology", - hint: "SW5E.FlagsForeignBiologyHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "furyOfTheSmall": { - name: "SW5E.FlagsFuryOfTheSmall", - hint: "SW5E.FlagsFuryOfTheSmallHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "grovelCowerAndBeg": { - name: "SW5E.FlagsGrovelCowerAndBeg", - hint: "SW5E.FlagsGrovelCowerAndBegHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "inscrutable": { - name: "SW5E.FlagsInscrutable", - hint: "SW5E.FlagsInscrutableHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "keenSenses": { - name: "SW5E.FlagsKeenSenses", - hint: "SW5E.FlagsKeenSensesHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "longlimbed": { - name: "SW5E.FlagsLongLimbed", - hint: "SW5E.FlagsLongLimbedHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "maintenanceMode": { - name: "SW5E.FlagsMaintenanceMode", - hint: "SW5E.FlagsMaintenanceModeHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "maskOfTheWild": { - name: "SW5E.FlagsMaskOfTheWild", - hint: "SW5E.FlagsMaskOfTheWildHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "multipleHearts": { - name: "SW5E.FlagsMultipleHearts", - hint: "SW5E.FlagsMultipleHeartsHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "naturallyStealthy": { - name: "SW5E.FlagsNaturallyStealthy", - hint: "SW5E.FlagsNaturallyStealthyHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "nimbleAgility": { - name: "SW5E.FlagsNimbleAgility", - hint: "SW5E.FlagsNimbleAgilityHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "nimbleEscape": { - name: "SW5E.FlagsNimbleEscape", - hint: "SW5E.FlagsNimbleEscapeHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "nimbleness": { - name: "SW5E.FlagsNimbleness", - hint: "SW5E.FlagsNimblenessHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "pintsized": { - name: "SW5E.FlagsPintsized", - hint: "SW5E.FlagsPintsizedHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "powerfulBuild": { - name: "SW5E.FlagsPowerfulBuild", - hint: "SW5E.FlagsPowerfulBuildHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "precognition": { - name: "SW5E.FlagsPrecognition", - hint: "SW5E.FlagsPrecognitionHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "programmer": { - name: "SW5E.FlagsProgrammer", - hint: "SW5E.FlagsProgrammerHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "puny": { - name: "SW5E.FlagsPuny", - hint: "SW5E.FlagsPunyHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "rapidReconstruction": { - name: "SW5E.FlagsRapidReconstruction", - hint: "SW5E.FlagsRapidReconstructionHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "rapidlyRegenerative": { - name: "SW5E.FlagsRapidlyRegenerative", - hint: "SW5E.FlagsRapidlyRegenerativeHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "regenerative": { - name: "SW5E.FlagsRegenerative", - hint: "SW5E.FlagsRegenerativeHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "savageAttacks": { - name: "SW5E.FlagsSavageAttacks", - hint: "SW5E.FlagsSavageAttacksHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "shapechanger": { - name: "SW5E.FlagsShapechanger", - hint: "SW5E.FlagsShapechangerHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "strongLegged": { - name: "SW5E.FlagsStrongLegged", - hint: "SW5E.FlagsStrongLeggedHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "sunlightSensitivity": { - name: "SW5E.FlagsSunlightSensitivity", - hint: "SW5E.FlagsSunlightSensitivityHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "surpriseAttack": { - name: "SW5E.FlagsSurpriseAttack", - hint: "SW5E.FlagsSurpriseAttackHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "techImpaired": { - name: "SW5E.FlagsTechImpaired", - hint: "SW5E.FlagsTechImpairedHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "techResistance": { - name: "SW5E.FlagsTechResistance", - hint: "SW5E.FlagsTechResistanceHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "tinker": { - name: "SW5E.FlagsTinker", - hint: "SW5E.FlagsTinkerHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "toughness": { - name: "SW5E.FlagsToughness", - hint: "SW5E.FlagsToughnessHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "trance": { - name: "SW5E.FlagsTrance", - hint: "SW5E.FlagsTranceHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "unarmedCombatant": { - name: "SW5E.FlagsUnarmedCombatant", - hint: "SW5E.FlagsUnarmedCombatantHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "undersized": { - name: "SW5E.FlagsUndersized", - hint: "SW5E.FlagsUndersizedHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "unsettlingVisage": { - name: "SW5E.FlagsUnsettlingVisage", - hint: "SW5E.FlagsUnsettlingVisageHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "initiativeAdv": { - name: "SW5E.FlagsInitiativeAdv", - hint: "SW5E.FlagsInitiativeAdvHint", - section: "SW5E.Features", - type: Boolean - }, - "initiativeAlert": { - name: "SW5E.FlagsAlert", - hint: "SW5E.FlagsAlertHint", - section: "SW5E.Features", - type: Boolean - }, - "jackOfAllTrades": { - name: "SW5E.FlagsJOAT", - hint: "SW5E.FlagsJOATHint", - section: "SW5E.Features", - type: Boolean - }, - "observantFeat": { - name: "SW5E.FlagsObservant", - hint: "SW5E.FlagsObservantHint", - skills: ['prc','inv'], - section: "SW5E.Features", - type: Boolean - }, - "reliableTalent": { - name: "SW5E.FlagsReliableTalent", - hint: "SW5E.FlagsReliableTalentHint", - section: "SW5E.Features", - type: Boolean - }, - "remarkableAthlete": { - name: "SW5E.FlagsRemarkableAthlete", - hint: "SW5E.FlagsRemarkableAthleteHint", - abilities: ['str','dex','con'], - section: "SW5E.Features", - type: Boolean - }, - "weaponCriticalThreshold": { - name: "SW5E.FlagsWeaponCritThreshold", - hint: "SW5E.FlagsWeaponCritThresholdHint", - section: "SW5E.Features", - type: Number, - placeholder: 20 - }, - "powerCriticalThreshold": { - name: "SW5E.FlagsPowerCritThreshold", - hint: "SW5E.FlagsPowerCritThresholdHint", - section: "SW5E.Features", - type: Number, - placeholder: 20 - }, - "meleeCriticalDamageDice": { - name: "SW5E.FlagsMeleeCriticalDice", - hint: "SW5E.FlagsMeleeCriticalDiceHint", - section: "SW5E.Features", - type: Number, - placeholder: 0 - } + adaptiveResilience: { + name: "SW5E.FlagsAdaptiveResilience", + hint: "SW5E.FlagsAdaptiveResilienceHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + aggressive: { + name: "SW5E.FlagsAggressive", + hint: "SW5E.FlagsAggressiveHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + amphibious: { + name: "SW5E.FlagsAmphibious", + hint: "SW5E.FlagsAmphibiousHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + armorIntegration: { + name: "SW5E.FlagsArmorIntegration", + hint: "SW5E.FlagsArmorIntegrationHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + businessSavvy: { + name: "SW5E.FlagsBusinessSavvy", + hint: "SW5E.FlagsBusinessSavvyHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + cannibalize: { + name: "SW5E.FlagsCannibalize", + hint: "SW5E.FlagsCannibalizeHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + closedMind: { + name: "SW5E.FlagsClosedMind", + hint: "SW5E.FlagsClosedMindHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + crudeWeaponSpecialists: { + name: "SW5E.FlagsCrudeWeaponSpecialists", + hint: "SW5E.FlagsCrudeWeaponSpecialistsHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + defiant: { + name: "SW5E.FlagsDefiant", + hint: "SW5E.FlagsDefiantHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + detailOriented: { + name: "SW5E.FlagsDetailOriented", + hint: "SW5E.FlagsDetailOrientedHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + enthrallingPheromones: { + name: "SW5E.FlagsEnthrallingPheromones", + hint: "SW5E.FlagsEnthrallingPheromonesHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + extraArms: { + name: "SW5E.FlagsExtraArms", + hint: "SW5E.FlagsExtraArmsHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + forceContention: { + name: "SW5E.FlagsForceContention", + hint: "SW5E.FlagsForceContentionHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + forceInsensitive: { + name: "SW5E.FlagsForceInsensitive", + hint: "SW5E.FlagsForceInsensitiveHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + foreignBiology: { + name: "SW5E.FlagsForeignBiology", + hint: "SW5E.FlagsForeignBiologyHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + furyOfTheSmall: { + name: "SW5E.FlagsFuryOfTheSmall", + hint: "SW5E.FlagsFuryOfTheSmallHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + grovelCowerAndBeg: { + name: "SW5E.FlagsGrovelCowerAndBeg", + hint: "SW5E.FlagsGrovelCowerAndBegHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + inscrutable: { + name: "SW5E.FlagsInscrutable", + hint: "SW5E.FlagsInscrutableHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + keenSenses: { + name: "SW5E.FlagsKeenSenses", + hint: "SW5E.FlagsKeenSensesHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + longlimbed: { + name: "SW5E.FlagsLongLimbed", + hint: "SW5E.FlagsLongLimbedHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + maintenanceMode: { + name: "SW5E.FlagsMaintenanceMode", + hint: "SW5E.FlagsMaintenanceModeHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + maskOfTheWild: { + name: "SW5E.FlagsMaskOfTheWild", + hint: "SW5E.FlagsMaskOfTheWildHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + multipleHearts: { + name: "SW5E.FlagsMultipleHearts", + hint: "SW5E.FlagsMultipleHeartsHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + naturallyStealthy: { + name: "SW5E.FlagsNaturallyStealthy", + hint: "SW5E.FlagsNaturallyStealthyHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + nimbleAgility: { + name: "SW5E.FlagsNimbleAgility", + hint: "SW5E.FlagsNimbleAgilityHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + nimbleEscape: { + name: "SW5E.FlagsNimbleEscape", + hint: "SW5E.FlagsNimbleEscapeHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + nimbleness: { + name: "SW5E.FlagsNimbleness", + hint: "SW5E.FlagsNimblenessHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + pintsized: { + name: "SW5E.FlagsPintsized", + hint: "SW5E.FlagsPintsizedHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + powerfulBuild: { + name: "SW5E.FlagsPowerfulBuild", + hint: "SW5E.FlagsPowerfulBuildHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + precognition: { + name: "SW5E.FlagsPrecognition", + hint: "SW5E.FlagsPrecognitionHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + programmer: { + name: "SW5E.FlagsProgrammer", + hint: "SW5E.FlagsProgrammerHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + puny: { + name: "SW5E.FlagsPuny", + hint: "SW5E.FlagsPunyHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + rapidReconstruction: { + name: "SW5E.FlagsRapidReconstruction", + hint: "SW5E.FlagsRapidReconstructionHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + rapidlyRegenerative: { + name: "SW5E.FlagsRapidlyRegenerative", + hint: "SW5E.FlagsRapidlyRegenerativeHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + regenerative: { + name: "SW5E.FlagsRegenerative", + hint: "SW5E.FlagsRegenerativeHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + savageAttacks: { + name: "SW5E.FlagsSavageAttacks", + hint: "SW5E.FlagsSavageAttacksHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + shapechanger: { + name: "SW5E.FlagsShapechanger", + hint: "SW5E.FlagsShapechangerHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + strongLegged: { + name: "SW5E.FlagsStrongLegged", + hint: "SW5E.FlagsStrongLeggedHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + sunlightSensitivity: { + name: "SW5E.FlagsSunlightSensitivity", + hint: "SW5E.FlagsSunlightSensitivityHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + surpriseAttack: { + name: "SW5E.FlagsSurpriseAttack", + hint: "SW5E.FlagsSurpriseAttackHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + techImpaired: { + name: "SW5E.FlagsTechImpaired", + hint: "SW5E.FlagsTechImpairedHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + techResistance: { + name: "SW5E.FlagsTechResistance", + hint: "SW5E.FlagsTechResistanceHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + tinker: { + name: "SW5E.FlagsTinker", + hint: "SW5E.FlagsTinkerHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + toughness: { + name: "SW5E.FlagsToughness", + hint: "SW5E.FlagsToughnessHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + trance: { + name: "SW5E.FlagsTrance", + hint: "SW5E.FlagsTranceHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + unarmedCombatant: { + name: "SW5E.FlagsUnarmedCombatant", + hint: "SW5E.FlagsUnarmedCombatantHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + undersized: { + name: "SW5E.FlagsUndersized", + hint: "SW5E.FlagsUndersizedHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + unsettlingVisage: { + name: "SW5E.FlagsUnsettlingVisage", + hint: "SW5E.FlagsUnsettlingVisageHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + initiativeAdv: { + name: "SW5E.FlagsInitiativeAdv", + hint: "SW5E.FlagsInitiativeAdvHint", + section: "SW5E.Features", + type: Boolean + }, + initiativeAlert: { + name: "SW5E.FlagsAlert", + hint: "SW5E.FlagsAlertHint", + section: "SW5E.Features", + type: Boolean + }, + jackOfAllTrades: { + name: "SW5E.FlagsJOAT", + hint: "SW5E.FlagsJOATHint", + section: "SW5E.Features", + type: Boolean + }, + observantFeat: { + name: "SW5E.FlagsObservant", + hint: "SW5E.FlagsObservantHint", + skills: ["prc", "inv"], + section: "SW5E.Features", + type: Boolean + }, + reliableTalent: { + name: "SW5E.FlagsReliableTalent", + hint: "SW5E.FlagsReliableTalentHint", + section: "SW5E.Features", + type: Boolean + }, + remarkableAthlete: { + name: "SW5E.FlagsRemarkableAthlete", + hint: "SW5E.FlagsRemarkableAthleteHint", + abilities: ["str", "dex", "con"], + section: "SW5E.Features", + type: Boolean + }, + weaponCriticalThreshold: { + name: "SW5E.FlagsWeaponCritThreshold", + hint: "SW5E.FlagsWeaponCritThresholdHint", + section: "SW5E.Features", + type: Number, + placeholder: 20 + }, + powerCriticalThreshold: { + name: "SW5E.FlagsPowerCritThreshold", + hint: "SW5E.FlagsPowerCritThresholdHint", + section: "SW5E.Features", + type: Number, + placeholder: 20 + }, + meleeCriticalDamageDice: { + name: "SW5E.FlagsMeleeCriticalDice", + hint: "SW5E.FlagsMeleeCriticalDiceHint", + section: "SW5E.Features", + type: Number, + placeholder: 0 + } }; // Configure allowed status flags -SW5E.allowedActorFlags = ["isPolymorphed", "originalActor", "dataVersion"].concat(Object.keys(SW5E.characterFlags)); \ No newline at end of file +SW5E.allowedActorFlags = ["isPolymorphed", "originalActor", "dataVersion"].concat(Object.keys(SW5E.characterFlags)); diff --git a/module/dice.js b/module/dice.js index 7d3c2489..8abc0291 100644 --- a/module/dice.js +++ b/module/dice.js @@ -12,43 +12,55 @@ export {default as DamageRoll} from "./dice/damage-roll.js"; * @return {string} The resulting simplified formula */ export function simplifyRollFormula(formula, data, {constantFirst = false} = {}) { - const roll = new Roll(formula, data); // Parses the formula and replaces any @properties - const terms = roll.terms; + const roll = new Roll(formula, data); // Parses the formula and replaces any @properties + const terms = roll.terms; - // Some terms are "too complicated" for this algorithm to simplify - // In this case, the original formula is returned. - if (terms.some(_isUnsupportedTerm)) return roll.formula; + // Some terms are "too complicated" for this algorithm to simplify + // In this case, the original formula is returned. + if (terms.some(_isUnsupportedTerm)) return roll.formula; - const rollableTerms = []; // Terms that are non-constant, and their associated operators - const constantTerms = []; // Terms that are constant, and their associated operators - let operators = []; // Temporary storage for operators before they are moved to one of the above + const rollableTerms = []; // Terms that are non-constant, and their associated operators + const constantTerms = []; // Terms that are constant, and their associated operators + let operators = []; // Temporary storage for operators before they are moved to one of the above - for (let term of terms) { // For each term - if (term instanceof OperatorTerm) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array - else { // Otherwise the term is not an operator - if (term instanceof DiceTerm) { // If the term is something rollable - rollableTerms.push(...operators); // Place all the operators into the rollableTerms array - rollableTerms.push(term); // Then place this rollable term into it as well - } // - else { // Otherwise, this must be a constant - constantTerms.push(...operators); // Place the operators into the constantTerms array - constantTerms.push(term); // Then also add this constant term to that array. - } // - operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration. + for (let term of terms) { + // For each term + if (term instanceof OperatorTerm) operators.push(term); + // If the term is an addition/subtraction operator, push the term into the operators array + else { + // Otherwise the term is not an operator + if (term instanceof DiceTerm) { + // If the term is something rollable + rollableTerms.push(...operators); // Place all the operators into the rollableTerms array + rollableTerms.push(term); // Then place this rollable term into it as well + } // + else { + // Otherwise, this must be a constant + constantTerms.push(...operators); // Place the operators into the constantTerms array + constantTerms.push(term); // Then also add this constant term to that array. + } // + operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration. + } } - } - const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string - const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string + const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string + const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string - // Mathematically evaluate the constant formula to produce a single constant term - const constantPart = constantFormula ? Roll.safeEval(constantFormula) : undefined; + // Mathematically evaluate the constant formula to produce a single constant term + let constantPart = undefined; + if (constantFormula) { + try { + constantPart = Roll.safeEval(constantFormula); + } catch (err) { + console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`); + } + } - // Order the rollable and constant terms, either constant first or second depending on the optional argument - const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart]; + // Order the rollable and constant terms, either constant first or second depending on the optional argument + const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart]; - // Join the parts with a + sign, pass them to `Roll` once again to clean up the formula - return new Roll(parts.filterJoin(" + ")).formula; + // Join the parts with a + sign, pass them to `Roll` once again to clean up the formula + return new Roll(parts.filterJoin(" + ")).formula; } /* -------------------------------------------- */ @@ -59,11 +71,11 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {}) * @return {Boolean} True when unsupported, false if supported */ function _isUnsupportedTerm(term) { - const diceTerm = term instanceof DiceTerm; - const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator); - const number = term instanceof NumericTerm; + const diceTerm = term instanceof DiceTerm; + const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator); + const number = term instanceof NumericTerm; - return !(diceTerm || operator || number); + return !(diceTerm || operator || number); } /* -------------------------------------------- */ @@ -104,54 +116,75 @@ function _isUnsupportedTerm(term) { * @return {Promise} The evaluated D20Roll, or null if the workflow was cancelled */ export async function d20Roll({ - parts=[], data={}, // Roll creation - advantage, disadvantage, fumble=1, critical=20, targetValue, elvenAccuracy, halflingLucky, reliableTalent, // Roll customization - chooseModifier=false, fastForward=false, event, template, title, dialogOptions, // Dialog configuration - chatMessage=true, messageData={}, rollMode, speaker, flavor // Chat Message customization - }={}) { - - // Handle input arguments - const formula = ["1d20"].concat(parts).join(" + "); - const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event}); - const defaultRollMode = rollMode || game.settings.get("core", "rollMode"); - if ( chooseModifier && !isFF ) data["mod"] = "@mod"; - - // Construct the D20Roll instance - const roll = new CONFIG.Dice.D20Roll(formula, data, { - flavor: flavor || title, - advantageMode, - defaultRollMode, - critical, - fumble, + parts = [], + data = {}, // Roll creation + advantage, + disadvantage, + fumble = 1, + critical = 20, targetValue, elvenAccuracy, halflingLucky, - reliableTalent - }); + reliableTalent, // Roll customization + chooseModifier = false, + fastForward = false, + event, + template, + title, + dialogOptions, // Dialog configuration + chatMessage = true, + messageData = {}, + rollMode, + speaker, + flavor // Chat Message customization +} = {}) { + // Handle input arguments + const formula = ["1d20"].concat(parts).join(" + "); + const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event}); + const defaultRollMode = rollMode || game.settings.get("core", "rollMode"); + if (chooseModifier && !isFF) data["mod"] = "@mod"; - // Prompt a Dialog to further configure the D20Roll - if ( !isFF ) { - const configured = await roll.configureDialog({ - title, - chooseModifier, - defaultRollMode: defaultRollMode, - defaultAction: advantageMode, - defaultAbility: data?.item?.ability, - template - }, dialogOptions); - if ( configured === null ) return null; - } + // Construct the D20Roll instance + const roll = new CONFIG.Dice.D20Roll(formula, data, { + flavor: flavor || title, + advantageMode, + defaultRollMode, + critical, + fumble, + targetValue, + elvenAccuracy, + halflingLucky, + reliableTalent + }); - // Evaluate the configured roll - await roll.evaluate({async: true}); + // Prompt a Dialog to further configure the D20Roll + if (!isFF) { + const configured = await roll.configureDialog( + { + title, + chooseModifier, + defaultRollMode: defaultRollMode, + defaultAction: advantageMode, + defaultAbility: data?.item?.ability, + template + }, + dialogOptions + ); + if (configured === null) return null; + } - // Create a Chat Message - if ( speaker ) { - console.warn(`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`); - messageData.speaker = speaker; - } - if ( roll && chatMessage ) await roll.toMessage(messageData); - return roll; + // Evaluate the configured roll + await roll.evaluate({async: true}); + + // Create a Chat Message + if (speaker) { + console.warn( + `You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData` + ); + messageData.speaker = speaker; + } + if (roll && chatMessage) await roll.toMessage(messageData); + return roll; } /* -------------------------------------------- */ @@ -160,12 +193,13 @@ export async function d20Roll({ * Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied * @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode */ -function _determineAdvantageMode({event, advantage=false, disadvantage=false, fastForward=false}={}) { - const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)); - let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL; - if ( advantage || event?.altKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE; - else if ( disadvantage || event?.ctrlKey || event?.metaKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE; - return {isFF, advantageMode}; +function _determineAdvantageMode({event, advantage = false, disadvantage = false, fastForward = false} = {}) { + const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)); + let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL; + if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE; + else if (disadvantage || event?.ctrlKey || event?.metaKey) + advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE; + return {isFF, advantageMode}; } /* -------------------------------------------- */ @@ -203,49 +237,67 @@ function _determineAdvantageMode({event, advantage=false, disadvantage=false, fa * @return {Promise} The evaluated DamageRoll, or null if the workflow was canceled */ export async function damageRoll({ - parts=[], data, // Roll creation - critical=false, criticalBonusDice, criticalMultiplier, multiplyNumeric, powerfulCritical, // Damage customization - fastForward=false, event, allowCritical=true, template, title, dialogOptions, // Dialog configuration - chatMessage=true, messageData={}, rollMode, speaker, flavor, // Chat Message customization - }={}) { - - // Handle input arguments - const defaultRollMode = rollMode || game.settings.get("core", "rollMode"); - - // Construct the DamageRoll instance - const formula = parts.join(" + "); - const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event}); - const roll = new CONFIG.Dice.DamageRoll(formula, data, { - flavor: flavor || title, - critical: isCritical, + parts = [], + data, // Roll creation + critical = false, criticalBonusDice, criticalMultiplier, multiplyNumeric, - powerfulCritical - }); + powerfulCritical, // Damage customization + fastForward = false, + event, + allowCritical = true, + template, + title, + dialogOptions, // Dialog configuration + chatMessage = true, + messageData = {}, + rollMode, + speaker, + flavor // Chat Message customization +} = {}) { + // Handle input arguments + const defaultRollMode = rollMode || game.settings.get("core", "rollMode"); - // Prompt a Dialog to further configure the DamageRoll - if ( !isFF ) { - const configured = await roll.configureDialog({ - title, - defaultRollMode: defaultRollMode, - defaultCritical: isCritical, - template, - allowCritical - }, dialogOptions); - if ( configured === null ) return null; - } + // Construct the DamageRoll instance + const formula = parts.join(" + "); + const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event}); + const roll = new CONFIG.Dice.DamageRoll(formula, data, { + flavor: flavor || title, + critical: isCritical, + criticalBonusDice, + criticalMultiplier, + multiplyNumeric, + powerfulCritical + }); - // Evaluate the configured roll - await roll.evaluate({async: true}); + // Prompt a Dialog to further configure the DamageRoll + if (!isFF) { + const configured = await roll.configureDialog( + { + title, + defaultRollMode: defaultRollMode, + defaultCritical: isCritical, + template, + allowCritical + }, + dialogOptions + ); + if (configured === null) return null; + } - // Create a Chat Message - if ( speaker ) { - console.warn(`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`); - messageData.speaker = speaker; - } - if ( roll && chatMessage ) await roll.toMessage(messageData); - return roll; + // Evaluate the configured roll + await roll.evaluate({async: true}); + + // Create a Chat Message + if (speaker) { + console.warn( + `You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData` + ); + messageData.speaker = speaker; + } + if (roll && chatMessage) await roll.toMessage(messageData); + return roll; } /* -------------------------------------------- */ @@ -254,8 +306,8 @@ export async function damageRoll({ * Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied * @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit */ -function _determineCriticalMode({event, critical=false, fastForward=false}={}) { - const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)); - if ( event?.altKey ) critical = true; - return {isFF, isCritical: critical}; +function _determineCriticalMode({event, critical = false, fastForward = false} = {}) { + const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)); + if (event?.altKey) critical = true; + return {isFF, isCritical: critical}; } diff --git a/module/dice/d20-roll.js b/module/dice/d20-roll.js index c4b40824..fb8a18b0 100644 --- a/module/dice/d20-roll.js +++ b/module/dice/d20-roll.js @@ -16,7 +16,7 @@ export default class D20Roll extends Roll { constructor(formula, data, options) { super(formula, data, options); - if ( !((this.terms[0] instanceof Die) && (this.terms[0].faces === 20)) ) { + if (!(this.terms[0] instanceof Die && this.terms[0].faces === 20)) { throw new Error(`Invalid D20Roll formula provided ${this._formula}`); } this.configureModifiers(); @@ -31,8 +31,8 @@ export default class D20Roll extends Roll { static ADV_MODE = { NORMAL: 0, ADVANTAGE: 1, - DISADVANTAGE: -1, - } + DISADVANTAGE: -1 + }; /** * The HTML template path used to configure evaluation of this Roll @@ -71,28 +71,26 @@ export default class D20Roll extends Roll { d20.modifiers = []; // Halfling Lucky - if ( this.options.halflingLucky ) d20.modifiers.push("r1=1"); + if (this.options.halflingLucky) d20.modifiers.push("r1=1"); // Reliable Talent - if ( this.options.reliableTalent ) d20.modifiers.push("min10"); + if (this.options.reliableTalent) d20.modifiers.push("min10"); // Handle Advantage or Disadvantage - if ( this.hasAdvantage ) { + if (this.hasAdvantage) { d20.number = this.options.elvenAccuracy ? 3 : 2; d20.modifiers.push("kh"); d20.options.advantage = true; - } - else if ( this.hasDisadvantage ) { + } else if (this.hasDisadvantage) { d20.number = 2; d20.modifiers.push("kl"); d20.options.disadvantage = true; - } - else d20.number = 1; + } else d20.number = 1; // Assign critical and fumble thresholds - if ( this.options.critical ) d20.options.critical = this.options.critical; - if ( this.options.fumble ) d20.options.fumble = this.options.fumble; - if ( this.options.targetValue ) d20.options.target = this.options.targetValue; + if (this.options.critical) d20.options.critical = this.options.critical; + if (this.options.fumble) d20.options.fumble = this.options.fumble; + if (this.options.targetValue) d20.options.target = this.options.targetValue; // Re-compile the underlying formula this._formula = this.constructor.getFormula(this.terms); @@ -101,22 +99,21 @@ export default class D20Roll extends Roll { /* -------------------------------------------- */ /** @inheritdoc */ - async toMessage(messageData={}, options={}) { - + async toMessage(messageData = {}, options = {}) { // Evaluate the roll now so we have the results available to determine whether reliable talent came into play - if ( !this._evaluated ) await this.evaluate({async: true}); + if (!this._evaluated) await this.evaluate({async: true}); // Add appropriate advantage mode message flavor and sw5e roll flags messageData.flavor = messageData.flavor || this.options.flavor; - if ( this.hasAdvantage ) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`; - else if ( this.hasDisadvantage ) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`; + if (this.hasAdvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`; + else if (this.hasDisadvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`; // Add reliable talent to the d20-term flavor text if it applied - if ( this.options.reliableTalent ) { + if (this.options.reliableTalent) { const d20 = this.dice[0]; - const isRT = d20.results.every(r => !r.active || (r.result < 10)); + const isRT = d20.results.every((r) => !r.active || r.result < 10); const label = `(${game.i18n.localize("SW5E.FlagsReliableTalent")})`; - if ( isRT ) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label; + if (isRT) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label; } // Record the preferred rollMode @@ -140,8 +137,17 @@ export default class D20Roll extends Roll { * @param {object} options Additional Dialog customization options * @returns {Promise} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed */ - async configureDialog({title, defaultRollMode, defaultAction=D20Roll.ADV_MODE.NORMAL, chooseModifier=false, defaultAbility, template}={}, options={}) { - + async configureDialog( + { + title, + defaultRollMode, + defaultAction = D20Roll.ADV_MODE.NORMAL, + chooseModifier = false, + defaultAbility, + template + } = {}, + options = {} + ) { // Render the Dialog inner HTML const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, { formula: `${this.formula} + @bonus`, @@ -154,32 +160,39 @@ export default class D20Roll extends Roll { let defaultButton = "normal"; switch (defaultAction) { - case D20Roll.ADV_MODE.ADVANTAGE: defaultButton = "advantage"; break; - case D20Roll.ADV_MODE.DISADVANTAGE: defaultButton = "disadvantage"; break; + case D20Roll.ADV_MODE.ADVANTAGE: + defaultButton = "advantage"; + break; + case D20Roll.ADV_MODE.DISADVANTAGE: + defaultButton = "disadvantage"; + break; } // Create the Dialog window and await submission of the form - return new Promise(resolve => { - new Dialog({ - title, - content, - buttons: { - advantage: { - label: game.i18n.localize("SW5E.Advantage"), - callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE)) + return new Promise((resolve) => { + new Dialog( + { + title, + content, + buttons: { + advantage: { + label: game.i18n.localize("SW5E.Advantage"), + callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE)) + }, + normal: { + label: game.i18n.localize("SW5E.Normal"), + callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL)) + }, + disadvantage: { + label: game.i18n.localize("SW5E.Disadvantage"), + callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE)) + } }, - normal: { - label: game.i18n.localize("SW5E.Normal"), - callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL)) - }, - disadvantage: { - label: game.i18n.localize("SW5E.Disadvantage"), - callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE)) - } + default: defaultButton, + close: () => resolve(null) }, - default: defaultButton, - close: () => resolve(null) - }, options).render(true); + options + ).render(true); }); } @@ -195,16 +208,16 @@ export default class D20Roll extends Roll { const form = html[0].querySelector("form"); // Append a situational bonus term - if ( form.bonus.value ) { + if (form.bonus.value) { const bonus = new Roll(form.bonus.value, this.data); - if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"})); + if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"})); this.terms = this.terms.concat(bonus.terms); } // Customize the modifier - if ( form.ability?.value ) { + if (form.ability?.value) { const abl = this.data.abilities[form.ability.value]; - this.terms.findSplice(t => t.term === "@mod", new NumericTerm({number: abl.mod})); + this.terms.findSplice((t) => t.term === "@mod", new NumericTerm({number: abl.mod})); this.options.flavor += ` (${CONFIG.SW5E.abilities[form.ability.value]})`; } diff --git a/module/dice/damage-roll.js b/module/dice/damage-roll.js index cc901fb2..41545c8b 100644 --- a/module/dice/damage-roll.js +++ b/module/dice/damage-roll.js @@ -13,7 +13,7 @@ export default class DamageRoll extends Roll { constructor(formula, data, options) { super(formula, data, options); // For backwards compatibility, skip rolls which do not have the "critical" option defined - if ( this.options.critical !== undefined ) this.configureDamage(); + if (this.options.critical !== undefined) this.configureDamage(); } /** @@ -42,44 +42,44 @@ export default class DamageRoll extends Roll { */ configureDamage() { let flatBonus = 0; - for ( let [i, term] of this.terms.entries() ) { - + for (let [i, term] of this.terms.entries()) { // Multiply dice terms - if ( term instanceof DiceTerm ) { + if (term instanceof DiceTerm) { term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back term.number = term.options.baseNumber; - if ( this.isCritical ) { + if (this.isCritical) { let cm = this.options.criticalMultiplier ?? 2; // Powerful critical - maximize damage and reduce the multiplier by 1 - if ( this.options.powerfulCritical ) { - flatBonus += (term.number * term.faces); - cm = Math.max(1, cm-1); + if (this.options.powerfulCritical) { + flatBonus += term.number * term.faces; + cm = Math.max(1, cm - 1); } // Alter the damage term - let cb = (this.options.criticalBonusDice && (i === 0)) ? this.options.criticalBonusDice : 0; + let cb = this.options.criticalBonusDice && i === 0 ? this.options.criticalBonusDice : 0; term.alter(cm, cb); term.options.critical = true; } - } // Multiply numeric terms - else if ( this.options.multiplyNumeric && (term instanceof NumericTerm) ) { + else if (this.options.multiplyNumeric && term instanceof NumericTerm) { term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back term.number = term.options.baseNumber; - if ( this.isCritical ) { - term.number *= (this.options.criticalMultiplier ?? 2); + if (this.isCritical) { + term.number *= this.options.criticalMultiplier ?? 2; term.options.critical = true; } } } // Add powerful critical bonus - if ( this.options.powerfulCritical && (flatBonus > 0) ) { + if (this.options.powerfulCritical && flatBonus > 0) { this.terms.push(new OperatorTerm({operator: "+"})); - this.terms.push(new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")})); + this.terms.push( + new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")}) + ); } // Re-compile the underlying formula @@ -89,9 +89,9 @@ export default class DamageRoll extends Roll { /* -------------------------------------------- */ /** @inheritdoc */ - toMessage(messageData={}, options={}) { + toMessage(messageData = {}, options = {}) { messageData.flavor = messageData.flavor || this.options.flavor; - if ( this.isCritical ) { + if (this.isCritical) { const label = game.i18n.localize("SW5E.CriticalHit"); messageData.flavor = messageData.flavor ? `${messageData.flavor} (${label})` : label; } @@ -114,34 +114,39 @@ export default class DamageRoll extends Roll { * @param {object} options Additional Dialog customization options * @returns {Promise} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed */ - async configureDialog({title, defaultRollMode, defaultCritical=false, template, allowCritical=true}={}, options={}) { - + async configureDialog( + {title, defaultRollMode, defaultCritical = false, template, allowCritical = true} = {}, + options = {} + ) { // Render the Dialog inner HTML const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, { formula: `${this.formula} + @bonus`, defaultRollMode, - rollModes: CONFIG.Dice.rollModes, + rollModes: CONFIG.Dice.rollModes }); // Create the Dialog window and await submission of the form - return new Promise(resolve => { - new Dialog({ - title, - content, - buttons: { - critical: { - condition: allowCritical, - label: game.i18n.localize("SW5E.CriticalHit"), - callback: html => resolve(this._onDialogSubmit(html, true)) + return new Promise((resolve) => { + new Dialog( + { + title, + content, + buttons: { + critical: { + condition: allowCritical, + label: game.i18n.localize("SW5E.CriticalHit"), + callback: (html) => resolve(this._onDialogSubmit(html, true)) + }, + normal: { + label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"), + callback: (html) => resolve(this._onDialogSubmit(html, false)) + } }, - normal: { - label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"), - callback: html => resolve(this._onDialogSubmit(html, false)) - } + default: defaultCritical ? "critical" : "normal", + close: () => resolve(null) }, - default: defaultCritical ? "critical" : "normal", - close: () => resolve(null) - }, options).render(true); + options + ).render(true); }); } @@ -157,9 +162,9 @@ export default class DamageRoll extends Roll { const form = html[0].querySelector("form"); // Append a situational bonus term - if ( form.bonus.value ) { + if (form.bonus.value) { const bonus = new Roll(form.bonus.value, this.data); - if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"})); + if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"})); this.terms = this.terms.concat(bonus.terms); } diff --git a/module/effects.js b/module/effects.js index 7a70430b..b825af87 100644 --- a/module/effects.js +++ b/module/effects.js @@ -4,26 +4,28 @@ * @param {Actor|Item} owner The owning entity which manages this effect */ export function onManageActiveEffect(event, owner) { - event.preventDefault(); - const a = event.currentTarget; - const li = a.closest("li"); - const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null; - switch ( a.dataset.action ) { - case "create": - return owner.createEmbeddedDocuments("ActiveEffect", [{ - label: "New Effect", - icon: "icons/svg/aura.svg", - origin: owner.uuid, - "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, - disabled: li.dataset.effectType === "inactive" - }]); - case "edit": - return effect.sheet.render(true); - case "delete": - return effect.delete(); - case "toggle": - return effect.update({disabled: !effect.data.disabled}); - } + event.preventDefault(); + const a = event.currentTarget; + const li = a.closest("li"); + const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null; + switch (a.dataset.action) { + case "create": + return owner.createEmbeddedDocuments("ActiveEffect", [ + { + "label": game.i18n.localize("SW5E.EffectNew"), + "icon": "icons/svg/aura.svg", + "origin": owner.uuid, + "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, + "disabled": li.dataset.effectType === "inactive" + } + ]); + case "edit": + return effect.sheet.render(true); + case "delete": + return effect.delete(); + case "toggle": + return effect.update({disabled: !effect.data.disabled}); + } } /** @@ -32,32 +34,31 @@ export function onManageActiveEffect(event, owner) { * @return {object} Data for rendering */ export function prepareActiveEffectCategories(effects) { - // Define effect header categories const categories = { - temporary: { - type: "temporary", - label: "SW5E.EffectsCategoryTemporary", - effects: [] - }, - passive: { - type: "passive", - label: "SW5E.EffectsCategoryPassive", - effects: [] - }, - inactive: { - type: "inactive", - label: "SW5E.EffectsCategoryInactive", - effects: [] - } + temporary: { + type: "temporary", + label: game.i18n.localize("SW5E.EffectTemporary"), + effects: [] + }, + passive: { + type: "passive", + label: game.i18n.localize("SW5E.EffectPassive"), + effects: [] + }, + inactive: { + type: "inactive", + label: game.i18n.localize("SW5E.EffectInactive"), + effects: [] + } }; // Iterate over active effects, classifying them into categories - for ( let e of 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.passive.effects.push(e); + for (let e of 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.passive.effects.push(e); } return categories; -} \ No newline at end of file +} diff --git a/module/item/entity.js b/module/item/entity.js index 02824961..37784699 100644 --- a/module/item/entity.js +++ b/module/item/entity.js @@ -6,306 +6,313 @@ import AbilityUseDialog from "../apps/ability-use-dialog.js"; * @extends {Item} */ export default class Item5e extends Item { + /* -------------------------------------------- */ + /* Item Properties */ + /* -------------------------------------------- */ - /* -------------------------------------------- */ - /* Item Properties */ - /* -------------------------------------------- */ + /** + * Determine which ability score modifier is used by this item + * @type {string|null} + */ + get abilityMod() { + const itemData = this.data.data; + if (!("ability" in itemData)) return null; - /** - * Determine which ability score modifier is used by this item - * @type {string|null} - */ - get abilityMod() { - const itemData = this.data.data; - if (!("ability" in itemData)) return null; + // Case 1 - defined directly by the item + if (itemData.ability) return itemData.ability; + // Case 2 - inferred from a parent actor + else if (this.actor) { + const actorData = this.actor.data.data; - // Case 1 - defined directly by the item - if (itemData.ability) return itemData.ability; + // Powers - Use Actor powercasting modifier based on power school + if (this.data.type === "power") { + switch (this.data.data.school) { + case "lgt": + return "wis"; + case "uni": + return actorData.abilities["wis"].mod >= actorData.abilities["cha"].mod ? "wis" : "cha"; + case "drk": + return "cha"; + case "tec": + return "int"; + } + return "none"; + } - // Case 2 - inferred from a parent actor - else if (this.actor) { - const actorData = this.actor.data.data; + // Tools - default to Intelligence + else if (this.data.type === "tool") return "int"; + // Weapons + else if (this.data.type === "weapon") { + const wt = itemData.weaponType; - // Powers - Use Actor powercasting modifier based on power school - if (this.data.type === "power") { - switch (this.data.data.school) { - case "lgt": return "wis"; - case "uni": return (actorData.abilities["wis"].mod >= actorData.abilities["cha"].mod) ? "wis" : "cha"; - case "drk": return "cha"; - case "tec": return "int"; - } - return "none"; - } - + // Weapons using the powercasting modifier + // No current SW5e weapons use this, but it's worth checking just in case + if (["mpak", "rpak"].includes(itemData.actionType)) { + return actorData.attributes.powercasting || "int"; + } - // Tools - default to Intelligence - else if (this.data.type === "tool") return "int"; + // Finesse weapons - Str or Dex (PHB pg. 147) + else if (itemData.properties.fin === true) { + return actorData.abilities["dex"].mod >= actorData.abilities["str"].mod ? "dex" : "str"; + } - // Weapons - else if (this.data.type === "weapon") { - const wt = itemData.weaponType; - - // Weapons using the powercasting modifier - // No current SW5e weapons use this, but it's worth checking just in case - if (["mpak", "rpak"].includes(itemData.actionType)) { - return actorData.attributes.powercasting || "int"; + // Ranged weapons - Dex (PH p.194) + else if (["simpleB", "martialB"].includes(wt)) return "dex"; + } + return "str"; } - // Finesse weapons - Str or Dex (PHB pg. 147) - else if (itemData.properties.fin === true) { - return (actorData.abilities["dex"].mod >= actorData.abilities["str"].mod) ? "dex" : "str"; + // Case 3 - unknown + return null; + } + + /* -------------------------------------------- */ + + /** + * Does the Item implement an attack roll as part of its usage + * @type {boolean} + */ + get hasAttack() { + return ["mwak", "rwak", "mpak", "rpak"].includes(this.data.data.actionType); + } + + /* -------------------------------------------- */ + + /** + * Does the Item implement a damage roll as part of its usage + * @type {boolean} + */ + get hasDamage() { + return !!(this.data.data.damage && this.data.data.damage.parts.length); + } + + /* -------------------------------------------- */ + + /** + * Does the Item implement a versatile damage roll as part of its usage + * @type {boolean} + */ + get isVersatile() { + return !!(this.hasDamage && this.data.data.damage.versatile); + } + + /* -------------------------------------------- */ + + /** + * Does the item provide an amount of healing instead of conventional damage? + * @return {boolean} + */ + get isHealing() { + return this.data.data.actionType === "heal" && this.data.data.damage.parts.length; + } + + /* -------------------------------------------- */ + + /** + * Does the Item implement a saving throw as part of its usage + * @type {boolean} + */ + get hasSave() { + const save = this.data.data?.save || {}; + return !!(save.ability && save.scaling); + } + + /* -------------------------------------------- */ + + /** + * Does the Item have a target + * @type {boolean} + */ + get hasTarget() { + const target = this.data.data.target; + return target && !["none", ""].includes(target.type); + } + + /* -------------------------------------------- */ + + /** + * Does the Item have an area of effect target + * @type {boolean} + */ + get hasAreaTarget() { + const target = this.data.data.target; + return target && target.type in CONFIG.SW5E.areaTargetTypes; + } + + /* -------------------------------------------- */ + + /** + * A flag for whether this Item is limited in it's ability to be used by charges or by recharge. + * @type {boolean} + */ + get hasLimitedUses() { + let chg = this.data.data.recharge || {}; + let uses = this.data.data.uses || {}; + return !!chg.value || (uses.per && uses.max > 0); + } + + /* -------------------------------------------- */ + /* Data Preparation */ + /* -------------------------------------------- */ + + /** + * Augment the basic Item data model with additional dynamic data. + */ + prepareDerivedData() { + super.prepareDerivedData(); + + // Get the Item's data + const itemData = this.data; + const data = itemData.data; + const C = CONFIG.SW5E; + const labels = (this.labels = {}); + + // Classes + if (itemData.type === "class") { + data.levels = Math.clamped(data.levels, 1, 20); } - // Ranged weapons - Dex (PH p.194) - else if ( ["simpleB", "martialB"].includes(wt) ) return "dex"; - } - return "str"; + // Power Level, School, and Components + if (itemData.type === "power") { + data.preparation.mode = data.preparation.mode || "prepared"; + labels.level = C.powerLevels[data.level]; + labels.school = C.powerSchools[data.school]; + labels.components = Object.entries(data.components).reduce((arr, c) => { + if (c[1] !== true) return arr; + arr.push(c[0].titleCase().slice(0, 1)); + return arr; + }, []); + labels.materials = data?.materials?.value ?? null; + } + + // Feat Items + else if (itemData.type === "feat") { + const act = data.activation; + if (act && act.type === C.abilityActivationTypes.legendary) + labels.featType = game.i18n.localize("SW5E.LegendaryActionLabel"); + else if (act && act.type === C.abilityActivationTypes.lair) + labels.featType = game.i18n.localize("SW5E.LairActionLabel"); + else if (act && act.type) + labels.featType = game.i18n.localize(data.damage.length ? "SW5E.Attack" : "SW5E.Action"); + else labels.featType = game.i18n.localize("SW5E.Passive"); + } + + // TODO: Something with all this + // Species Items + else if (itemData.type === "species") { + // labels.species = C.species[data.species]; + } + // Archetype Items + else if (itemData.type === "archetype") { + // labels.archetype = C.archetype[data.archetype]; + } + // Background Items + else if (itemData.type === "background") { + // labels.background = C.background[data.background]; + } + // Class Feature Items + else if (itemData.type === "classfeature") { + // labels.classFeature = C.classFeature[data.classFeature]; + } + // Deployment Items + else if (itemData.type === "deployment") { + // labels.deployment = C.deployment[data.deployment]; + } + // Venture Items + else if (itemData.type === "venture") { + // labels.venture = C.venture[data.venture]; + } + // Fighting Style Items + else if (itemData.type === "fightingstyle") { + // labels.fightingstyle = C.fightingstyle[data.fightingstyle]; + } + // Fighting Mastery Items + else if (itemData.type === "fightingmastery") { + // labels.fightingmastery = C.fightingmastery[data.fightingmastery]; + } + // Lightsaber Form Items + else if (itemData.type === "lightsaberform") { + // labels.lightsaberform = C.lightsaberform[data.lightsaberform]; + } + + // Equipment Items + else if (itemData.type === "equipment") { + labels.armor = data.armor.value ? `${data.armor.value} ${game.i18n.localize("SW5E.AC")}` : ""; + } + + // Activated Items + if (data.hasOwnProperty("activation")) { + // Ability Activation Label + let act = data.activation || {}; + if (act) labels.activation = [act.cost, C.abilityActivationTypes[act.type]].filterJoin(" "); + + // Target Label + let tgt = data.target || {}; + if (["none", "touch", "self"].includes(tgt.units)) tgt.value = null; + if (["none", "self"].includes(tgt.type)) { + tgt.value = null; + tgt.units = null; + } + labels.target = [tgt.value, C.distanceUnits[tgt.units], C.targetTypes[tgt.type]].filterJoin(" "); + + // Range Label + let rng = data.range || {}; + if (["none", "touch", "self"].includes(rng.units)) { + rng.value = null; + rng.long = null; + } + labels.range = [rng.value, rng.long ? `/ ${rng.long}` : null, C.distanceUnits[rng.units]].filterJoin(" "); + + // Duration Label + let dur = data.duration || {}; + if (["inst", "perm"].includes(dur.units)) dur.value = null; + labels.duration = [dur.value, C.timePeriods[dur.units]].filterJoin(" "); + + // Recharge Label + let chg = data.recharge || {}; + labels.recharge = `${game.i18n.localize("SW5E.Recharge")} [${chg.value}${ + parseInt(chg.value) < 6 ? "+" : "" + }]`; + } + + // Item Actions + if (data.hasOwnProperty("actionType")) { + // Damage + let dam = data.damage || {}; + if (dam.parts) { + labels.damage = dam.parts + .map((d) => d[0]) + .join(" + ") + .replace(/\+ -/g, "- "); + labels.damageTypes = dam.parts.map((d) => C.damageTypes[d[1]]).join(", "); + } + } + + // if this item is owned, we prepareFinalAttributes() at the end of actor init + if (!this.isOwned) this.prepareFinalAttributes(); } - // Case 3 - unknown - return null - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Compute item attributes which might depend on prepared actor data. + */ + prepareFinalAttributes() { + if (this.data.data.hasOwnProperty("actionType")) { + // Saving throws + this.getSaveDC(); - /** - * Does the Item implement an attack roll as part of its usage - * @type {boolean} - */ - get hasAttack() { - return ["mwak", "rwak", "mpak", "rpak"].includes(this.data.data.actionType); - } + // To Hit + this.getAttackToHit(); - /* -------------------------------------------- */ + // Limited Uses + this.prepareMaxUses(); - /** - * Does the Item implement a damage roll as part of its usage - * @type {boolean} - */ - get hasDamage() { - return !!(this.data.data.damage && this.data.data.damage.parts.length); - } - - /* -------------------------------------------- */ - - /** - * Does the Item implement a versatile damage roll as part of its usage - * @type {boolean} - */ - get isVersatile() { - return !!(this.hasDamage && this.data.data.damage.versatile); - } - - /* -------------------------------------------- */ - - /** - * Does the item provide an amount of healing instead of conventional damage? - * @return {boolean} - */ - get isHealing() { - return (this.data.data.actionType === "heal") && this.data.data.damage.parts.length; - } - - /* -------------------------------------------- */ - - /** - * Does the Item implement a saving throw as part of its usage - * @type {boolean} - */ - get hasSave() { - const save = this.data.data?.save || {}; - return !!(save.ability && save.scaling); - } - - /* -------------------------------------------- */ - - /** - * Does the Item have a target - * @type {boolean} - */ - get hasTarget() { - const target = this.data.data.target; - return target && !["none",""].includes(target.type); - } - - /* -------------------------------------------- */ - - /** - * Does the Item have an area of effect target - * @type {boolean} - */ - get hasAreaTarget() { - const target = this.data.data.target; - return target && (target.type in CONFIG.SW5E.areaTargetTypes); - } - - /* -------------------------------------------- */ - - /** - * A flag for whether this Item is limited in it's ability to be used by charges or by recharge. - * @type {boolean} - */ - get hasLimitedUses() { - let chg = this.data.data.recharge || {}; - let uses = this.data.data.uses || {}; - return !!chg.value || (uses.per && (uses.max > 0)); - } - - /* -------------------------------------------- */ - /* Data Preparation */ - /* -------------------------------------------- */ - - /** - * Augment the basic Item data model with additional dynamic data. - */ - prepareDerivedData() { - super.prepareDerivedData(); - - // Get the Item's data - const itemData = this.data; - const data = itemData.data; - const C = CONFIG.SW5E; - const labels = this.labels = {}; - - // Classes - if ( itemData.type === "class" ) { - data.levels = Math.clamped(data.levels, 1, 20); + // Damage Label + this.getDerivedDamageLabel(); + } } - // Power Level, School, and Components - if ( itemData.type === "power" ) { - data.preparation.mode = data.preparation.mode || "prepared"; - labels.level = C.powerLevels[data.level]; - labels.school = C.powerSchools[data.school]; - labels.components = Object.entries(data.components).reduce((arr, c) => { - if ( c[1] !== true ) return arr; - arr.push(c[0].titleCase().slice(0, 1)); - return arr; - }, []); - labels.materials = data?.materials?.value ?? null; - } - - // Feat Items - else if ( itemData.type === "feat" ) { - const act = data.activation; - if ( act && (act.type === C.abilityActivationTypes.legendary) ) labels.featType = game.i18n.localize("SW5E.LegendaryActionLabel"); - else if ( act && (act.type === C.abilityActivationTypes.lair) ) labels.featType = game.i18n.localize("SW5E.LairActionLabel"); - else if ( act && act.type ) labels.featType = game.i18n.localize(data.damage.length ? "SW5E.Attack" : "SW5E.Action"); - else labels.featType = game.i18n.localize("SW5E.Passive"); - } - - // TODO: Something with all this - // Species Items - else if ( itemData.type === "species" ) { - // labels.species = C.species[data.species]; - } - // Archetype Items - else if ( itemData.type === "archetype" ) { - // labels.archetype = C.archetype[data.archetype]; - } - // Background Items - else if ( itemData.type === "background" ) { - // labels.background = C.background[data.background]; - } - // Class Feature Items - else if ( itemData.type === "classfeature" ) { - // labels.classFeature = C.classFeature[data.classFeature]; - } - // Deployment Items - else if ( itemData.type === "deployment" ) { - // labels.deployment = C.deployment[data.deployment]; - } - // Venture Items - else if ( itemData.type === "venture" ) { - // labels.venture = C.venture[data.venture]; - } - // Fighting Style Items - else if ( itemData.type === "fightingstyle" ) { - // labels.fightingstyle = C.fightingstyle[data.fightingstyle]; - } - // Fighting Mastery Items - else if ( itemData.type === "fightingmastery" ) { - // labels.fightingmastery = C.fightingmastery[data.fightingmastery]; - } - // Lightsaber Form Items - else if ( itemData.type === "lightsaberform" ) { - // labels.lightsaberform = C.lightsaberform[data.lightsaberform]; - } - - // Equipment Items - else if ( itemData.type === "equipment" ) { - labels.armor = data.armor.value ? `${data.armor.value} ${game.i18n.localize("SW5E.AC")}` : ""; - } - - // Activated Items - if ( data.hasOwnProperty("activation") ) { - - // Ability Activation Label - let act = data.activation || {}; - if ( act ) labels.activation = [act.cost, C.abilityActivationTypes[act.type]].filterJoin(" "); - - // Target Label - let tgt = data.target || {}; - if (["none", "touch", "self"].includes(tgt.units)) tgt.value = null; - if (["none", "self"].includes(tgt.type)) { - tgt.value = null; - tgt.units = null; - } - labels.target = [tgt.value, C.distanceUnits[tgt.units], C.targetTypes[tgt.type]].filterJoin(" "); - - // Range Label - let rng = data.range || {}; - if (["none", "touch", "self"].includes(rng.units) || (rng.value === 0)) { - rng.value = null; - rng.long = null; - } - labels.range = [rng.value, rng.long ? `/ ${rng.long}` : null, C.distanceUnits[rng.units]].filterJoin(" "); - - // Duration Label - let dur = data.duration || {}; - if (["inst", "perm"].includes(dur.units)) dur.value = null; - labels.duration = [dur.value, C.timePeriods[dur.units]].filterJoin(" "); - - // Recharge Label - let chg = data.recharge || {}; - labels.recharge = `${game.i18n.localize("SW5E.Recharge")} [${chg.value}${parseInt(chg.value) < 6 ? "+" : ""}]`; - } - - // Item Actions - if ( data.hasOwnProperty("actionType") ) { - // Damage - let dam = data.damage || {}; - if (dam.parts) { - labels.damage = dam.parts.map(d => d[0]).join(" + ").replace(/\+ -/g, "- "); - labels.damageTypes = dam.parts.map(d => C.damageTypes[d[1]]).join(", "); - } - } - - // if this item is owned, we prepareFinalAttributes() at the end of actor init - if (!this.isOwned) this.prepareFinalAttributes(); - } - - /* -------------------------------------------- */ - - /** - * Compute item attributes which might depend on prepared actor data. - */ - prepareFinalAttributes() { - if ( this.data.data.hasOwnProperty("actionType") ) { - // Saving throws - this.getSaveDC(); - - // To Hit - this.getAttackToHit(); - - // Limited Uses - this.prepareMaxUses(); - - // Damage Label - this.getDerivedDamageLabel(); - } - } - /* -------------------------------------------- */ /** @@ -316,1333 +323,1349 @@ export default class Item5e extends Item { * @returns {Array} array of objects with `formula` and `damageType` */ getDerivedDamageLabel() { - const itemData = this.data.data; - if ( !this.hasDamage || !itemData || !this.isOwned ) return []; + const itemData = this.data.data; + if (!this.hasDamage || !itemData || !this.isOwned) return []; - const rollData = this.getRollData(); + const rollData = this.getRollData(); - const derivedDamage = itemData.damage?.parts?.map((damagePart) => ({ - formula: simplifyRollFormula(damagePart[0], rollData, { constantFirst: false }), - damageType: damagePart[1], - })); + const derivedDamage = itemData.damage?.parts?.map((damagePart) => ({ + formula: simplifyRollFormula(damagePart[0], rollData, {constantFirst: false}), + damageType: damagePart[1] + })); - this.labels.derivedDamage = derivedDamage + this.labels.derivedDamage = derivedDamage; - return derivedDamage; + return derivedDamage; } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Update the derived power DC for an item that requires a saving throw - * @returns {number|null} - */ - getSaveDC() { - if ( !this.hasSave ) return; - const save = this.data.data?.save; + /** + * Update the derived power DC for an item that requires a saving throw + * @returns {number|null} + */ + getSaveDC() { + if (!this.hasSave) return; + const save = this.data.data?.save; - // Actor power-DC based scaling - if ( save.scaling === "power" ) { - switch (this.data.data.school) { - case "lgt": { - save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceLightDC") : null; - break; - } - case "uni": { - save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceUnivDC") : null; - break; - } - case "drk": { - save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceDarkDC") : null; - break; - } - case "tec": { - save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerTechDC") : null; - break; - } - } - } - - // Ability-score based scaling - else if ( save.scaling !== "flat" ) { - save.dc = this.isOwned ? getProperty(this.actor.data, `data.abilities.${save.scaling}.dc`) : null; - } - - // Update labels - const abl = CONFIG.SW5E.abilities[save.ability]; - this.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability: abl}); - return save.dc; - } - - /* -------------------------------------------- */ - - /** - * Update a label to the Item detailing its total to hit bonus. - * Sources: - * - item entity's innate attack bonus - * - item's actor's proficiency bonus if applicable - * - item's actor's global bonuses to the given item type - * - item's ammunition if applicable - * - * @returns {Object} returns `rollData` and `parts` to be used in the item's Attack roll - */ - getAttackToHit() { - const itemData = this.data.data; - if ( !this.hasAttack || !itemData ) return; - const rollData = this.getRollData(); - - // Define Roll bonuses - const parts = []; - - // Include the item's innate attack bonus as the initial value and label - if ( itemData.attackBonus ) { - parts.push(itemData.attackBonus) - this.labels.toHit = itemData.attackBonus; - } - - // Take no further action for un-owned items - if ( !this.isOwned ) return {rollData, parts}; - - // Ability score modifier - parts.push(`@mod`); - - // Add proficiency bonus if an explicit proficiency flag is present or for non-item features - if ( !["weapon", "consumable"].includes(this.data.type) || itemData.proficient ) { - parts.push("@prof"); - } - - // Actor-level global bonus to attack rolls - const actorBonus = this.actor.data.data.bonuses?.[itemData.actionType] || {}; - if ( actorBonus.attack ) parts.push(actorBonus.attack); - - // One-time bonus provided by consumed ammunition - if ( (itemData.consume?.type === 'ammo') && !!this.actor.items ) { - const ammoItemData = this.actor.items.get(itemData.consume.target)?.data; - - if (ammoItemData) { - const ammoItemQuantity = ammoItemData.data.quantity; - const ammoCanBeConsumed = ammoItemQuantity && (ammoItemQuantity - (itemData.consume.amount ?? 0) >= 0); - const ammoItemAttackBonus = ammoItemData.data.attackBonus; - const ammoIsTypeConsumable = (ammoItemData.type === "consumable") && (ammoItemData.data.consumableType === "ammo") - if ( ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable ) { - parts.push("@ammo"); - rollData["ammo"] = ammoItemAttackBonus; - } - } - } - - // Condense the resulting attack bonus formula into a simplified label - let toHitLabel = simplifyRollFormula(parts.join('+'), rollData).trim(); - if (toHitLabel.charAt(0) !== '-') { - toHitLabel = '+ ' + toHitLabel - } - this.labels.toHit = toHitLabel; - - // Update labels and return the prepared roll data - return {rollData, parts}; - } - - /* -------------------------------------------- */ - - /** - * Populates the max uses of an item. - * If the item is an owned item and the `max` is not numeric, calculate based on actor data. - */ - prepareMaxUses() { - const data = this.data.data; - if (!data.uses?.max) return; - let max = data.uses.max; - - // if this is an owned item and the max is not numeric, we need to calculate it - if (this.isOwned && !Number.isNumeric(max)) { - if (this.actor.data === undefined) return; - try { - max = Roll.replaceFormulaData(max, this.actor.getRollData(), {missing: 0, warn: true}); - max = Roll.safeEval(max); - } catch(e) { - console.error('Problem preparing Max uses for', this.data.name, e); - return; - } - } - data.uses.max = Number(max); - } - - /* -------------------------------------------- */ - - /** - * Roll the item to Chat, creating a chat card which contains follow up attack or damage roll options - * @param {boolean} [configureDialog] Display a configuration dialog for the item roll, if applicable? - * @param {string} [rollMode] The roll display mode with which to display (or not) the card - * @param {boolean} [createMessage] Whether to automatically create a chat message (if true) or simply return - * the prepared chat message data (if false). - * @return {Promise} - */ - async roll({configureDialog=true, rollMode, createMessage=true}={}) { - let item = this; - const id = this.data.data; // Item system data - const actor = this.actor; - const ad = actor.data.data; // Actor system data - - // Reference aspects of the item data necessary for usage - const hasArea = this.hasAreaTarget; // Is the ability usage an AoE? - const resource = id.consume || {}; // Resource consumption - const recharge = id.recharge || {}; // Recharge mechanic - const uses = id?.uses ?? {}; // Limited uses - const isPower = this.type === "power"; // Does the item require a power slot? - // TODO: Possibly Mod this to not consume slots based on class? - // We could use this for feats and architypes that let a character cast one slot every rest or so - const requirePowerSlot = isPower && (id.level > 0) && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode); - - // Define follow-up actions resulting from the item usage - let createMeasuredTemplate = hasArea; // Trigger a template creation - let consumeRecharge = !!recharge.value; // Consume recharge - let consumeResource = !!resource.target && resource.type !== "ammo" && !['simpleB', 'martialB'].includes(id.weaponType); // Consume a linked (non-ammo) resource, ignore if use is from a blaster - let consumePowerSlot = requirePowerSlot; // Consume a power slot - let consumeUsage = !!uses.per; // Consume limited uses - let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of uses - let consumePowerLevel = null; // Consume a specific category of power slot - if ( requirePowerSlot ) consumePowerLevel = id.preparation.mode === "pact" ? "pact" : `power${id.level}`; - - // Display a configuration dialog to customize the usage - const needsConfiguration = createMeasuredTemplate || consumeRecharge || consumeResource || consumePowerSlot || consumeUsage; - if (configureDialog && needsConfiguration) { - const configuration = await AbilityUseDialog.create(this); - if (!configuration) return; - - - // Determine consumption preferences - createMeasuredTemplate = Boolean(configuration.placeTemplate); - consumeUsage = Boolean(configuration.consumeUse); - consumeRecharge = Boolean(configuration.consumeRecharge); - consumeResource = Boolean(configuration.consumeResource); - consumePowerSlot = Boolean(configuration.consumeSlot); - - // Handle power upcasting - if ( requirePowerSlot ) { - consumePowerLevel = `power${configuration.level}`; - if (consumePowerSlot === false) consumePowerLevel = null; - const upcastLevel = parseInt(configuration.level); - if (upcastLevel !== id.level) { - item = this.clone({"data.level": upcastLevel}, {keepId: true}); - item.data.update({_id: this.id}); // Retain the original ID (needed until 0.8.2+) - item.prepareFinalAttributes(); // Power save DC, etc... - } - } - } - - // Determine whether the item can be used by testing for resource consumption - const usage = item._getUsageUpdates({consumeRecharge, consumeResource, consumePowerLevel, consumeUsage, consumeQuantity}); - if ( !usage ) return; - - const {actorUpdates, itemUpdates, resourceUpdates} = usage; - - // Commit pending data updates - if ( !foundry.utils.isObjectEmpty(itemUpdates) ) await item.update(itemUpdates); - if ( consumeQuantity && (item.data.data.quantity === 0) ) await item.delete(); - if ( !foundry.utils.isObjectEmpty(actorUpdates) ) await actor.update(actorUpdates); - if ( !foundry.utils.isObjectEmpty(resourceUpdates) ) { - const resource = actor.items.get(id.consume?.target); - if ( resource ) await resource.update(resourceUpdates); - } - - // Initiate measured template creation - if ( createMeasuredTemplate ) { - const template = game.sw5e.canvas.AbilityTemplate.fromItem(item); - if ( template ) template.drawPreview(); - } - - // Create or return the Chat Message data - return item.displayCard({rollMode, createMessage}); - } - - /* -------------------------------------------- */ - - /** - * Verify that the consumed resources used by an Item are available. - * Otherwise display an error and return false. - * @param {boolean} consumeQuantity Consume quantity of the item if other consumption modes are not available? - * @param {boolean} consumeRecharge Whether the item consumes the recharge mechanic - * @param {boolean} consumeResource Whether the item consumes a limited resource - * @param {string|null} consumePowerLevel The category of power slot to consume, or null - * @param {boolean} consumeUsage Whether the item consumes a limited usage - * @returns {object|boolean} A set of data changes to apply when the item is used, or false - * @private - */ - _getUsageUpdates({consumeQuantity, consumeRecharge, consumeResource, consumePowerLevel, consumeUsage}) { - - // Reference item data - const id = this.data.data; - const actorUpdates = {}; - const itemUpdates = {}; - const resourceUpdates = {}; - - // Consume Recharge - if ( consumeRecharge ) { - const recharge = id.recharge || {}; - if ( recharge.charged === false ) { - ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name})); - return false; - } - itemUpdates["data.recharge.charged"] = false; - } - - // Consume Limited Resource - if ( consumeResource ) { - const canConsume = this._handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates); - if ( canConsume === false ) return false; - } - - // Consume Power Slots and Force/Tech Points - if ( consumePowerLevel ) { - if ( Number.isNumeric(consumePowerLevel) ) consumePowerLevel = `power${consumePowerLevel}`; - const level = this.actor?.data.data.powers[consumePowerLevel]; - const fp = this.actor.data.data.attributes.force.points; - const tp = this.actor.data.data.attributes.tech.points; - const powerCost = id.level + 1; - const innatePower = this.actor.data.data.attributes.powercasting === 'innate'; - if (!innatePower){ - switch (id.school){ - case "lgt": - case "uni": - case "drk": { - const powers = Number(level?.fvalue ?? 0); - if ( powers === 0 ) { - const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`); - ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label})); - return false; + // Actor power-DC based scaling + if (save.scaling === "power") { + switch (this.data.data.school) { + case "lgt": { + save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceLightDC") : null; + break; + } + case "uni": { + save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceUnivDC") : null; + break; + } + case "drk": { + save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceDarkDC") : null; + break; + } + case "tec": { + save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerTechDC") : null; + break; + } } - actorUpdates[`data.powers.${consumePowerLevel}.fvalue`] = Math.max(powers - 1, 0); - if (fp.temp >= powerCost) { - actorUpdates["data.attributes.force.points.temp"] = fp.temp - powerCost; - }else{ - actorUpdates["data.attributes.force.points.value"] = fp.value + fp.temp - powerCost; - actorUpdates["data.attributes.force.points.temp"] = 0; + } + + // Ability-score based scaling + else if (save.scaling !== "flat") { + save.dc = this.isOwned ? getProperty(this.actor.data, `data.abilities.${save.scaling}.dc`) : null; + } + + // Update labels + const abl = CONFIG.SW5E.abilities[save.ability]; + this.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability: abl}); + return save.dc; + } + + /* -------------------------------------------- */ + + /** + * Update a label to the Item detailing its total to hit bonus. + * Sources: + * - item entity's innate attack bonus + * - item's actor's proficiency bonus if applicable + * - item's actor's global bonuses to the given item type + * - item's ammunition if applicable + * + * @returns {Object} returns `rollData` and `parts` to be used in the item's Attack roll + */ + getAttackToHit() { + const itemData = this.data.data; + if (!this.hasAttack || !itemData) return; + const rollData = this.getRollData(); + + // Define Roll bonuses + const parts = []; + + // Include the item's innate attack bonus as the initial value and label + if (itemData.attackBonus) { + parts.push(itemData.attackBonus); + this.labels.toHit = itemData.attackBonus; + } + + // Take no further action for un-owned items + if (!this.isOwned) return {rollData, parts}; + + // Ability score modifier + parts.push(`@mod`); + + // Add proficiency bonus if an explicit proficiency flag is present or for non-item features + if (!["weapon", "consumable"].includes(this.data.type) || itemData.proficient) { + parts.push("@prof"); + } + + // Actor-level global bonus to attack rolls + const actorBonus = this.actor.data.data.bonuses?.[itemData.actionType] || {}; + if (actorBonus.attack) parts.push(actorBonus.attack); + + // One-time bonus provided by consumed ammunition + if (itemData.consume?.type === "ammo" && !!this.actor.items) { + const ammoItemData = this.actor.items.get(itemData.consume.target)?.data; + + if (ammoItemData) { + const ammoItemQuantity = ammoItemData.data.quantity; + const ammoCanBeConsumed = ammoItemQuantity && ammoItemQuantity - (itemData.consume.amount ?? 0) >= 0; + const ammoItemAttackBonus = ammoItemData.data.attackBonus; + const ammoIsTypeConsumable = + ammoItemData.type === "consumable" && ammoItemData.data.consumableType === "ammo"; + if (ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable) { + parts.push("@ammo"); + rollData["ammo"] = ammoItemAttackBonus; + } } - break; - } - case "tec": { - const powers = Number(level?.tvalue ?? 0); - if ( powers === 0 ) { - const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`); - ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label})); - return false; + } + + // Condense the resulting attack bonus formula into a simplified label + let toHitLabel = simplifyRollFormula(parts.join("+"), rollData).trim(); + if (toHitLabel.charAt(0) !== "-") { + toHitLabel = "+ " + toHitLabel; + } + this.labels.toHit = toHitLabel; + + // Update labels and return the prepared roll data + return {rollData, parts}; + } + + /* -------------------------------------------- */ + + /** + * Populates the max uses of an item. + * If the item is an owned item and the `max` is not numeric, calculate based on actor data. + */ + prepareMaxUses() { + const data = this.data.data; + if (!data.uses?.max) return; + let max = data.uses.max; + + // if this is an owned item and the max is not numeric, we need to calculate it + if (this.isOwned && !Number.isNumeric(max)) { + if (this.actor.data === undefined) return; + try { + max = Roll.replaceFormulaData(max, this.actor.getRollData(), {missing: 0, warn: true}); + max = Roll.safeEval(max); + } catch (e) { + console.error("Problem preparing Max uses for", this.data.name, e); + return; } - actorUpdates[`data.powers.${consumePowerLevel}.tvalue`] = Math.max(powers - 1, 0); - if (tp.temp >= powerCost) { - actorUpdates["data.attributes.tech.points.temp"] = tp.temp - powerCost; - }else{ - actorUpdates["data.attributes.tech.points.value"] = tp.value + tp.temp - powerCost; - actorUpdates["data.attributes.tech.points.temp"] = 0; + } + data.uses.max = Number(max); + } + + /* -------------------------------------------- */ + + /** + * Roll the item to Chat, creating a chat card which contains follow up attack or damage roll options + * @param {boolean} [configureDialog] Display a configuration dialog for the item roll, if applicable? + * @param {string} [rollMode] The roll display mode with which to display (or not) the card + * @param {boolean} [createMessage] Whether to automatically create a chat message (if true) or simply return + * the prepared chat message data (if false). + * @return {Promise} + */ + async roll({configureDialog = true, rollMode, createMessage = true} = {}) { + let item = this; + const id = this.data.data; // Item system data + const actor = this.actor; + const ad = actor.data.data; // Actor system data + + // Reference aspects of the item data necessary for usage + const hasArea = this.hasAreaTarget; // Is the ability usage an AoE? + const resource = id.consume || {}; // Resource consumption + const recharge = id.recharge || {}; // Recharge mechanic + const uses = id?.uses ?? {}; // Limited uses + const isPower = this.type === "power"; // Does the item require a power slot? + // TODO: Possibly Mod this to not consume slots based on class? + // We could use this for feats and architypes that let a character cast one slot every rest or so + const requirePowerSlot = isPower && id.level > 0 && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode); + + // Define follow-up actions resulting from the item usage + let createMeasuredTemplate = hasArea; // Trigger a template creation + let consumeRecharge = !!recharge.value; // Consume recharge + let consumeResource = !!resource.target && resource.type !== "ammo"; // Consume a linked (non-ammo) resource + let consumePowerSlot = requirePowerSlot; // Consume a power slot + let consumeUsage = !!uses.per; // Consume limited uses + let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of uses + let consumePowerLevel = null; // Consume a specific category of power slot + if (requirePowerSlot) consumePowerLevel = id.preparation.mode === "pact" ? "pact" : `power${id.level}`; + + // Display a configuration dialog to customize the usage + const needsConfiguration = + createMeasuredTemplate || + consumeRecharge || + (consumeResource && !["simpleB", "martialB"].includes(id.weaponType)) || + consumePowerSlot || + (consumeUsage && !["simpleB", "martialB"].includes(id.weaponType)); + if (configureDialog && needsConfiguration) { + const configuration = await AbilityUseDialog.create(this); + if (!configuration) return; + + // Determine consumption preferences + createMeasuredTemplate = Boolean(configuration.placeTemplate); + consumeUsage = Boolean(configuration.consumeUse); + consumeRecharge = Boolean(configuration.consumeRecharge); + consumeResource = Boolean(configuration.consumeResource); + consumePowerSlot = Boolean(configuration.consumeSlot); + + // Handle power upcasting + if (requirePowerSlot) { + consumePowerLevel = `power${configuration.level}`; + if (consumePowerSlot === false) consumePowerLevel = null; + const upcastLevel = parseInt(configuration.level); + if (upcastLevel !== id.level) { + item = this.clone({"data.level": upcastLevel}, {keepId: true}); + item.data.update({_id: this.id}); // Retain the original ID (needed until 0.8.2+) + item.prepareFinalAttributes(); // Power save DC, etc... + } } - break; - } } - } - } - - // Consume Limited Usage - if ( consumeUsage ) { - const uses = id.uses || {}; - const available = Number(uses.value ?? 0); - let used = false; - - // Reduce usages - const remaining = Math.max(available - 1, 0); - if ( available >= 1 ) { - used = true; - itemUpdates["data.uses.value"] = remaining; - } - - // Reduce quantity if not reducing usages or if usages hit 0 and we are set to consumeQuantity - if ( consumeQuantity && (!used || (remaining === 0)) ) { - const q = Number(id.quantity ?? 1); - if ( q >= 1 ) { - used = true; - itemUpdates["data.quantity"] = Math.max(q - 1, 0); - itemUpdates["data.uses.value"] = uses.max ?? 1; - } - } - - // If the item was not used, return a warning - if ( !used ) { - ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name})); - return false; - } - } - - // Return the configured usage - return {itemUpdates, actorUpdates, resourceUpdates}; - } - - /* -------------------------------------------- */ - - /** - * Handle update actions required when consuming an external resource - * @param {object} itemUpdates An object of data updates applied to this item - * @param {object} actorUpdates An object of data updates applied to the item owner (Actor) - * @param {object} resourceUpdates An object of data updates applied to a different resource item (Item) - * @return {boolean|void} Return false to block further progress, or return nothing to continue - * @private - */ - _handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates) { - const actor = this.actor; - const itemData = this.data.data; - const consume = itemData.consume || {}; - if ( !consume.type ) return; - - // No consumed target - const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type]; - if ( !consume.target ) { - ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel})); - return false; - } - - // Identify the consumed resource and its current quantity - let resource = null; - let amount = Number(consume.amount ?? 1); - let quantity = 0; - switch ( consume.type ) { - case "attribute": - resource = getProperty(actor.data.data, consume.target); - quantity = resource || 0; - break; - case "ammo": - case "material": - resource = actor.items.get(consume.target); - quantity = resource ? resource.data.data.quantity : 0; - break; - case "charges": - resource = actor.items.get(consume.target); - if ( !resource ) break; - const uses = resource.data.data.uses; - if ( uses.per && uses.max ) quantity = uses.value; - else if ( resource.data.data.recharge?.value ) { - quantity = resource.data.data.recharge.charged ? 1 : 0; - amount = 1; - } - break; - } - - // Verify that a consumed resource is available - if ( !resource ) { - ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel})); - return false; - } - - // Verify that the required quantity is available - let remaining = quantity - amount; - if ( remaining < 0 ) { - ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel})); - return false; - } - - // Define updates to provided data objects - switch ( consume.type ) { - case "attribute": - actorUpdates[`data.${consume.target}`] = remaining; - break; - case "ammo": - case "material": - resourceUpdates["data.quantity"] = remaining; - break; - case "charges": - const uses = resource.data.data.uses || {}; - const recharge = resource.data.data.recharge || {}; - if ( uses.per && uses.max ) resourceUpdates["data.uses.value"] = remaining; - else if ( recharge.value ) resourceUpdates["data.recharge.charged"] = false; - break; - } - } - - /* -------------------------------------------- */ - - /** - * Display the chat card for an Item as a Chat Message - * @param {object} options Options which configure the display of the item chat card - * @param {string} rollMode The message visibility mode to apply to the created card - * @param {boolean} createMessage Whether to automatically create a ChatMessage entity (if true), or only return - * the prepared message data (if false) - */ - async displayCard({rollMode, createMessage=true}={}) { - - // Render the chat card template - const token = this.actor.token; - const templateData = { - actor: this.actor, - tokenId: token?.uuid || null, - item: this.data, - data: this.getChatData(), - labels: this.labels, - hasAttack: this.hasAttack, - isHealing: this.isHealing, - hasDamage: this.hasDamage, - isVersatile: this.isVersatile, - isPower: this.data.type === "power", - hasSave: this.hasSave, - hasAreaTarget: this.hasAreaTarget, - isTool: this.data.type === "tool" - }; - const html = await renderTemplate("systems/sw5e/templates/chat/item-card.html", templateData); - - // Create the ChatMessage data object - const chatData = { - user: game.user.data._id, - type: CONST.CHAT_MESSAGE_TYPES.OTHER, - content: html, - flavor: this.data.data.chatFlavor || this.name, - speaker: ChatMessage.getSpeaker({actor: this.actor, token}), - flags: {"core.canPopout": true} - }; - - // If the Item was destroyed in the process of displaying its card - embed the item data in the chat message - if ( (this.data.type === "consumable") && !this.actor.items.has(this.id) ) { - chatData.flags["sw5e.itemData"] = this.data; - } - - // Apply the roll mode to adjust message visibility - ChatMessage.applyRollMode(chatData, rollMode || game.settings.get("core", "rollMode")); - - // Create the Chat Message or return its data - return createMessage ? ChatMessage.create(chatData) : chatData; - } - - /* -------------------------------------------- */ - /* Chat Cards */ - /* -------------------------------------------- */ - - /** - * Prepare an object of chat data used to display a card for the Item in the chat log - * @param {Object} htmlOptions Options used by the TextEditor.enrichHTML function - * @return {Object} An object of chat data to render - */ - getChatData(htmlOptions={}) { - const data = foundry.utils.deepClone(this.data.data); - const labels = this.labels; - - // Rich text description - data.description.value = TextEditor.enrichHTML(data.description.value, htmlOptions); - - // Item type specific properties - const props = []; - const fn = this[`_${this.data.type}ChatData`]; - if ( fn ) fn.bind(this)(data, labels, props); - - // Equipment properties - if ( data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type) ) { - if ( data.attunement === CONFIG.SW5E.attunementTypes.REQUIRED ) props.push(game.i18n.localize(CONFIG.SW5E.attunements[CONFIG.SW5E.attunementTypes.REQUIRED])); - props.push( - game.i18n.localize(data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"), - game.i18n.localize(data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"), - ); - } - - // Ability activation properties - if ( data.hasOwnProperty("activation") ) { - props.push( - labels.activation + (data.activation?.condition ? ` (${data.activation.condition})` : ""), - labels.target, - labels.range, - labels.duration - ); - } - - // Filter properties and return - data.properties = props.filter(p => !!p); - return data; - } - - /* -------------------------------------------- */ - - /** - * Prepare chat card data for equipment type items - * @private - */ - _equipmentChatData(data, labels, props) { - props.push( - CONFIG.SW5E.equipmentTypes[data.armor.type], - labels.armor || null, - data.stealth.value ? game.i18n.localize("SW5E.StealthDisadvantage") : null - ); - } - - /* -------------------------------------------- */ - - /** - * Prepare chat card data for weapon type items - * @private - */ - _weaponChatData(data, labels, props) { - props.push( - CONFIG.SW5E.weaponTypes[data.weaponType], - ); - } - - /* -------------------------------------------- */ - - /** - * Prepare chat card data for consumable type items - * @private - */ - _consumableChatData(data, labels, props) { - props.push( - CONFIG.SW5E.consumableTypes[data.consumableType], - data.uses.value + "/" + data.uses.max + " " + game.i18n.localize("SW5E.Charges") - ); - data.hasCharges = data.uses.value >= 0; - } - - /* -------------------------------------------- */ - - /** - * Prepare chat card data for tool type items - * @private - */ - _toolChatData(data, labels, props) { - props.push( - CONFIG.SW5E.abilities[data.ability] || null, - CONFIG.SW5E.proficiencyLevels[data.proficient || 0] - ); - } - - /* -------------------------------------------- */ - - /** - * Prepare chat card data for loot type items - * @private - */ - _lootChatData(data, labels, props) { - props.push( - game.i18n.localize("SW5E.ItemTypeLoot"), - data.weight ? data.weight + " " + game.i18n.localize("SW5E.AbbreviationLbs") : null - ); - } - - /* -------------------------------------------- */ - - /** - * Render a chat card for Power type data - * @return {Object} - * @private - */ - _powerChatData(data, labels, props) { - props.push( - labels.level, - labels.components + (labels.materials ? ` (${labels.materials})` : "") - ); - } - - /* -------------------------------------------- */ - - /** - * Prepare chat card data for items of the "Feat" type - * @private - */ - _featChatData(data, labels, props) { - props.push(data.requirements); - } - - /* -------------------------------------------- */ - /* Item Rolls - Attack, Damage, Saves, Checks */ - /* -------------------------------------------- */ - - /** - * Place an attack roll using an item (weapon, feat, power, or equipment) - * Rely upon the d20Roll logic for the core implementation - * - * @param {object} options Roll options which are configured and provided to the d20Roll function - * @return {Promise} A Promise which resolves to the created Roll instance - */ - async rollAttack(options={}) { - const itemData = this.data.data; - const flags = this.actor.data.flags.sw5e || {}; - if ( !this.hasAttack ) { - throw new Error("You may not place an Attack Roll with this Item."); - } - let title = `${this.name} - ${game.i18n.localize("SW5E.AttackRoll")}`; - - // get the parts and rollData for this item's attack - const {parts, rollData} = this.getAttackToHit(); - - // Handle ammunition consumption - delete this._ammo; - let ammo = null; - let ammoUpdate = null; - const consume = itemData.consume; - if ( consume?.type === "ammo" ) { - ammo = this.actor.items.get(consume.target); - if (ammo?.data) { - const q = ammo.data.data.quantity; - const consumeAmount = consume.amount ?? 0; - if ( q && (q - consumeAmount >= 0) ) { - this._ammo = ammo; - title += ` [${ammo.name}]`; - } - } - - // Get pending ammunition update - const usage = this._getUsageUpdates({consumeResource: true}); - if ( usage === false ) return null; - ammoUpdate = usage.resourceUpdates || {}; - } - - // Compose roll options - const rollConfig = mergeObject({ - parts: parts, - actor: this.actor, - data: rollData, - title: title, - flavor: title, - speaker: ChatMessage.getSpeaker({actor: this.actor}), - dialogOptions: { - width: 400, - top: options.event ? options.event.clientY - 80 : null, - left: window.innerWidth - 710 - }, - messageData: {"flags.sw5e.roll": {type: "attack", itemId: this.id }} - }, options); - rollConfig.event = options.event; - - // Expanded critical hit thresholds - if (( this.data.type === "weapon" ) && flags.weaponCriticalThreshold) { - rollConfig.critical = parseInt(flags.weaponCriticalThreshold); - } else if (( this.data.type === "power" ) && flags.powerCriticalThreshold) { - rollConfig.critical = parseInt(flags.powerCriticalThreshold); - } - - // Elven Accuracy - if ( flags.elvenAccuracy && ["dex", "int", "wis", "cha"].includes(this.abilityMod) ) { - rollConfig.elvenAccuracy = true; - } - - - // Apply Halfling Lucky - if ( flags.halflingLucky ) rollConfig.halflingLucky = true; - - // Invoke the d20 roll helper - const roll = await d20Roll(rollConfig); - if ( roll === false ) return null; - - // Commit ammunition consumption on attack rolls resource consumption if the attack roll was made - if ( ammo && !isObjectEmpty(ammoUpdate) ) await ammo.update(ammoUpdate); - return roll; - } - - /* -------------------------------------------- */ - - /** - * Place a damage roll using an item (weapon, feat, power, or equipment) - * Rely upon the damageRoll logic for the core implementation. - * @param {MouseEvent} [event] An event which triggered this roll, if any - * @param {boolean} [critical] Should damage be rolled as a critical hit? - * @param {number} [powerLevel] If the item is a power, override the level for damage scaling - * @param {boolean} [versatile] If the item is a weapon, roll damage using the versatile formula - * @param {object} [options] Additional options passed to the damageRoll function - * @return {Promise} A Promise which resolves to the created Roll instance - */ - rollDamage({critical=false, event=null, powerLevel=null, versatile=false, options={}}={}) { - if ( !this.hasDamage ) throw new Error("You may not make a Damage Roll with this Item."); - const itemData = this.data.data; - const actorData = this.actor.data.data; - const messageData = {"flags.sw5e.roll": {type: "damage", itemId: this.id }}; - - // Get roll data - const parts = itemData.damage.parts.map(d => d[0]); - const rollData = this.getRollData(); - if ( powerLevel ) rollData.item.level = powerLevel; - - // Configure the damage roll - const actionFlavor = game.i18n.localize(itemData.actionType === "heal" ? "SW5E.Healing" : "SW5E.DamageRoll"); - const title = `${this.name} - ${actionFlavor}`; - const rollConfig = { - actor: this.actor, - critical: critical ?? event?.altKey ?? false, - data: rollData, - event: event, - fastForward: event ? event.shiftKey || event.altKey || event.ctrlKey || event.metaKey : false, - parts: parts, - title: title, - flavor: this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title, - speaker: ChatMessage.getSpeaker({actor: this.actor}), - dialogOptions: { - width: 400, - top: event ? event.clientY - 80 : null, - left: window.innerWidth - 710 - }, - messageData: messageData - }; - - // Adjust damage from versatile usage - if ( versatile && itemData.damage.versatile ) { - parts[0] = itemData.damage.versatile; - messageData["flags.sw5e.roll"].versatile = true; - } - - // Scale damage from up-casting powers - if ( (this.data.type === "power") ) { - if ( (itemData.scaling.mode === "atwill") ) { - const level = this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel; - this._scaleAtWillDamage(parts, itemData.scaling.formula, level, rollData); - } - else if ( powerLevel && (itemData.scaling.mode === "level") && itemData.scaling.formula ) { - const scaling = itemData.scaling.formula; - this._scalePowerDamage(parts, itemData.level, powerLevel, scaling, rollData); - } - } - - // Add damage bonus formula - const actorBonus = getProperty(actorData, `bonuses.${itemData.actionType}`) || {}; - if ( actorBonus.damage && (parseInt(actorBonus.damage) !== 0) ) { - parts.push(actorBonus.damage); - } - - // Handle ammunition damage - const ammoData = this._ammo?.data; - - // only add the ammunition damage if the ammution is a consumable with type 'ammo' - if ( this._ammo && (ammoData.type === "consumable") && (ammoData.data.consumableType === "ammo") ) { - parts.push("@ammo"); - rollData["ammo"] = ammoData.data.damage.parts.map(p => p[0]).join("+"); - rollConfig.flavor += ` [${this._ammo.name}]`; - delete this._ammo; - } - - // Scale melee critical hit damage - if ( itemData.actionType === "mwak" ) { - rollConfig.criticalBonusDice = this.actor.getFlag("sw5e", "meleeCriticalDamageDice") ?? 0; - } - - // Call the roll helper utility - return damageRoll(mergeObject(rollConfig, options)); - } - - /* -------------------------------------------- */ - - /** - * Adjust an at-will damage formula to scale it for higher level characters and monsters - * @private - */ - _scaleAtWillDamage(parts, scale, level, rollData) { - const add = Math.floor((level + 1) / 6); - if ( add === 0 ) return; - this._scaleDamage(parts, scale || parts.join(" + "), add, rollData); - } - - /* -------------------------------------------- */ - - /** - * Adjust the power damage formula to scale it for power level up-casting - * @param {Array} parts The original damage parts - * @param {number} baseLevel The default power level - * @param {number} powerLevel The casted power level - * @param {string} formula The scaling formula - * @param {object} rollData A data object that should be applied to the scaled damage roll - * @return {string[]} The scaled roll parts - * @private - */ - _scalePowerDamage(parts, baseLevel, powerLevel, formula, rollData) { - const upcastLevels = Math.max(powerLevel - baseLevel, 0); - if ( upcastLevels === 0 ) return parts; - this._scaleDamage(parts, formula, upcastLevels, rollData); - } - - /* -------------------------------------------- */ - - /** - * Scale an array of damage parts according to a provided scaling formula and scaling multiplier - * @param {string[]} parts Initial roll parts - * @param {string} scaling A scaling formula - * @param {number} times A number of times to apply the scaling formula - * @param {object} rollData A data object that should be applied to the scaled damage roll - * @return {string[]} The scaled roll parts - * @private - */ - _scaleDamage(parts, scaling, times, rollData) { - if ( times <= 0 ) return parts; - const p0 = new Roll(parts[0], rollData); - const s = new Roll(scaling, rollData).alter(times); - - // Attempt to simplify by combining like dice terms - let simplified = false; - if ( (s.terms[0] instanceof Die) && (s.terms.length === 1) ) { - const d0 = p0.terms[0]; - const s0 = s.terms[0]; - if ( (d0 instanceof Die) && (d0.faces === s0.faces) && d0.modifiers.equals(s0.modifiers) ) { - d0.number += s0.number; - parts[0] = p0.formula; - simplified = true; - } - } - - // Otherwise add to the first part - if ( !simplified ) { - parts[0] = `${parts[0]} + ${s.formula}`; - } - return parts; - } - - /* -------------------------------------------- */ - - /** - * Place an attack roll using an item (weapon, feat, power, or equipment) - * Rely upon the d20Roll logic for the core implementation - * - * @return {Promise} A Promise which resolves to the created Roll instance - */ - async rollFormula(options={}) { - if ( !this.data.data.formula ) { - throw new Error("This Item does not have a formula to roll!"); - } - - // Define Roll Data - const rollData = this.getRollData(); - if ( options.powerLevel ) rollData.item.level = options.powerLevel; - const title = `${this.name} - ${game.i18n.localize("SW5E.OtherFormula")}`; - - // Invoke the roll and submit it to chat - const roll = new Roll(rollData.item.formula, rollData).roll(); - roll.toMessage({ - speaker: ChatMessage.getSpeaker({actor: this.actor}), - flavor: title, - rollMode: game.settings.get("core", "rollMode"), - messageData: {"flags.sw5e.roll": {type: "other", itemId: this.id }} - }); - return roll; - } - - /* -------------------------------------------- */ - - /** - * Perform an ability recharge test for an item which uses the d6 recharge mechanic - * @return {Promise} A Promise which resolves to the created Roll instance - */ - async rollRecharge() { - const data = this.data.data; - if ( !data.recharge.value ) return; - - // Roll the check - const roll = new Roll("1d6").roll(); - const success = roll.total >= parseInt(data.recharge.value); - - // Display a Chat Message - const promises = [roll.toMessage({ - flavor: `${game.i18n.format("SW5E.ItemRechargeCheck", {name: this.name})} - ${game.i18n.localize(success ? "SW5E.ItemRechargeSuccess" : "SW5E.ItemRechargeFailure")}`, - speaker: ChatMessage.getSpeaker({actor: this.actor, token: this.actor.token}) - })]; - - // Update the Item data - if ( success ) promises.push(this.update({"data.recharge.charged": true})); - return Promise.all(promises).then(() => roll); - } - - /* -------------------------------------------- */ - - /** - * Roll a Tool Check. Rely upon the d20Roll logic for the core implementation - * @prarm {Object} options Roll configuration options provided to the d20Roll function - * @return {Promise} A Promise which resolves to the created Roll instance - */ - rollToolCheck(options={}) { - if ( this.type !== "tool" ) throw "Wrong item type!"; - - // Prepare roll data - let rollData = this.getRollData(); - const parts = [`@mod`, "@prof"]; - const title = `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`; - - // Add global actor bonus - const bonuses = getProperty(this.actor.data.data, "bonuses.abilities") || {}; - if ( bonuses.check ) { - parts.push("@checkBonus"); - rollData.checkBonus = bonuses.check; - } - - // Compose the roll data - const rollConfig = mergeObject({ - parts: parts, - data: rollData, - title: title, - speaker: ChatMessage.getSpeaker({actor: this.actor}), - flavor: title, - dialogOptions: { - width: 400, - top: options.event ? options.event.clientY - 80 : null, - left: window.innerWidth - 710, - }, - chooseModifier: true, - halflingLucky: this.actor.getFlag("sw5e", "halflingLucky" ) || false, - reliableTalent: (this.data.data.proficient >= 1) && this.actor.getFlag("sw5e", "reliableTalent"), - messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id }} - }, options); - rollConfig.event = options.event; - - // Call the roll helper utility - return d20Roll(rollConfig); - } - - /* -------------------------------------------- */ - - /** - * Prepare a data object which is passed to any Roll formulas which are created related to this Item - * @private - */ - getRollData() { - if ( !this.actor ) return null; - const rollData = this.actor.getRollData(); - rollData.item = foundry.utils.deepClone(this.data.data); - - // Include an ability score modifier if one exists - const abl = this.abilityMod; - if ( abl ) { - const ability = rollData.abilities[abl]; - rollData["mod"] = ability.mod || 0; - } - - // Include a proficiency score - const prof = ("proficient" in rollData.item) ? (rollData.item.proficient || 0) : 1; - rollData["prof"] = Math.floor(prof * (rollData.attributes.prof || 0)); - return rollData; - } - - /* -------------------------------------------- */ - /* Chat Message Helpers */ - /* -------------------------------------------- */ - - static chatListeners(html) { - html.on('click', '.card-buttons button', this._onChatCardAction.bind(this)); - html.on('click', '.item-name', this._onChatCardToggleContent.bind(this)); - } - - /* -------------------------------------------- */ - - /** - * Handle execution of a chat card action via a click event on one of the card buttons - * @param {Event} event The originating click event - * @returns {Promise} A promise which resolves once the handler workflow is complete - * @private - */ - static async _onChatCardAction(event) { - event.preventDefault(); - - // Extract card data - const button = event.currentTarget; - button.disabled = true; - const card = button.closest(".chat-card"); - const messageId = card.closest(".message").dataset.messageId; - const message = game.messages.get(messageId); - const action = button.dataset.action; - - // Validate permission to proceed with the roll - const isTargetted = action === "save"; - if ( !( isTargetted || game.user.isGM || message.isAuthor ) ) return; - - // Recover the actor for the chat card - const actor = await this._getChatCardActor(card); - if ( !actor ) return; - - // Get the Item from stored flag data or by the item ID on the Actor - const storedData = message.getFlag("sw5e", "itemData"); - const item = storedData ? new this(storedData, {parent: actor}) : actor.items.get(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; - - // Handle different actions - switch ( action ) { - case "attack": - await item.rollAttack({event}); break; - case "damage": - case "versatile": - await item.rollDamage({ - critical: event.altKey, - event: event, - powerLevel: powerLevel, - versatile: action === "versatile" + // Determine whether the item can be used by testing for resource consumption + const usage = item._getUsageUpdates({ + consumeRecharge, + consumeResource, + consumePowerLevel, + consumeUsage, + consumeQuantity }); - 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 }); + if (!usage) return; + + const {actorUpdates, itemUpdates, resourceUpdates} = usage; + + // Commit pending data updates + if (!foundry.utils.isObjectEmpty(itemUpdates)) await item.update(itemUpdates); + if (consumeQuantity && item.data.data.quantity === 0) await item.delete(); + if (!foundry.utils.isObjectEmpty(actorUpdates)) await actor.update(actorUpdates); + if (!foundry.utils.isObjectEmpty(resourceUpdates)) { + const resource = actor.items.get(id.consume?.target); + if (resource) await resource.update(resourceUpdates); } - break; - case "toolCheck": - await item.rollToolCheck({event}); break; - case "placeTemplate": - const template = game.sw5e.canvas.AbilityTemplate.fromItem(item); - if ( template ) template.drawPreview(); - break; + + // Initiate measured template creation + if (createMeasuredTemplate) { + const template = game.sw5e.canvas.AbilityTemplate.fromItem(item); + if (template) template.drawPreview(); + } + + // Create or return the Chat Message data + return item.displayCard({rollMode, createMessage}); } - // Re-enable the button - button.disabled = false; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Verify that the consumed resources used by an Item are available. + * Otherwise display an error and return false. + * @param {boolean} consumeQuantity Consume quantity of the item if other consumption modes are not available? + * @param {boolean} consumeRecharge Whether the item consumes the recharge mechanic + * @param {boolean} consumeResource Whether the item consumes a limited resource + * @param {string|null} consumePowerLevel The category of power slot to consume, or null + * @param {boolean} consumeUsage Whether the item consumes a limited usage + * @returns {object|boolean} A set of data changes to apply when the item is used, or false + * @private + */ + _getUsageUpdates({consumeQuantity, consumeRecharge, consumeResource, consumePowerLevel, consumeUsage}) { + // Reference item data + const id = this.data.data; + const actorUpdates = {}; + const itemUpdates = {}; + const resourceUpdates = {}; - /** - * Handle toggling the visibility of chat card content when the name is clicked - * @param {Event} event The originating click event - * @private - */ - static _onChatCardToggleContent(event) { - event.preventDefault(); - const header = event.currentTarget; - const card = header.closest(".chat-card"); - const content = card.querySelector(".card-content"); - content.style.display = content.style.display === "none" ? "block" : "none"; - } + // Consume Recharge + if (consumeRecharge) { + const recharge = id.recharge || {}; + if (recharge.charged === false) { + ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name})); + return false; + } + itemUpdates["data.recharge.charged"] = false; + } - /* -------------------------------------------- */ + // Consume Limited Resource + if (consumeResource) { + const canConsume = this._handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates); + if (canConsume === false) return false; + } - /** - * Get the Actor which is the author of a chat card - * @param {HTMLElement} card The chat card being used - * @return {Actor|null} The Actor entity or null - * @private - */ - static async _getChatCardActor(card) { + // Consume Power Slots and Force/Tech Points + if (consumePowerLevel) { + if (Number.isNumeric(consumePowerLevel)) consumePowerLevel = `power${consumePowerLevel}`; + const level = this.actor?.data.data.powers[consumePowerLevel]; + const fp = this.actor.data.data.attributes.force.points; + const tp = this.actor.data.data.attributes.tech.points; + const powerCost = id.level + 1; + const innatePower = this.actor.data.data.attributes.powercasting === "innate"; + if (!innatePower) { + switch (id.school) { + case "lgt": + case "uni": + case "drk": { + const powers = Number(level?.fvalue ?? 0); + if (powers === 0) { + const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`); + ui.notifications.warn( + game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}) + ); + return false; + } + actorUpdates[`data.powers.${consumePowerLevel}.fvalue`] = Math.max(powers - 1, 0); + if (fp.temp >= powerCost) { + actorUpdates["data.attributes.force.points.temp"] = fp.temp - powerCost; + } else { + actorUpdates["data.attributes.force.points.value"] = fp.value + fp.temp - powerCost; + actorUpdates["data.attributes.force.points.temp"] = 0; + } + break; + } + case "tec": { + const powers = Number(level?.tvalue ?? 0); + if (powers === 0) { + const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`); + ui.notifications.warn( + game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}) + ); + return false; + } + actorUpdates[`data.powers.${consumePowerLevel}.tvalue`] = Math.max(powers - 1, 0); + if (tp.temp >= powerCost) { + actorUpdates["data.attributes.tech.points.temp"] = tp.temp - powerCost; + } else { + actorUpdates["data.attributes.tech.points.value"] = tp.value + tp.temp - powerCost; + actorUpdates["data.attributes.tech.points.temp"] = 0; + } + break; + } + } + } + } - // Case 1 - a synthetic actor from a Token - if ( card.dataset.tokenId ) { - const token = await fromUuid(card.dataset.tokenId); - if ( !token ) return null; - return token.actor; + // Consume Limited Usage + if (consumeUsage) { + const uses = id.uses || {}; + const available = Number(uses.value ?? 0); + let used = false; + + // Reduce usages + const remaining = Math.max(available - 1, 0); + if (available >= 1) { + used = true; + itemUpdates["data.uses.value"] = remaining; + } + + // Reduce quantity if not reducing usages or if usages hit 0 and we are set to consumeQuantity + if (consumeQuantity && (!used || remaining === 0)) { + const q = Number(id.quantity ?? 1); + if (q >= 1) { + used = true; + itemUpdates["data.quantity"] = Math.max(q - 1, 0); + itemUpdates["data.uses.value"] = uses.max ?? 1; + } + } + + // If the item was not used, return a warning + if (!used) { + ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name})); + return false; + } + } + + // Return the configured usage + return {itemUpdates, actorUpdates, resourceUpdates}; } - // Case 2 - use Actor ID directory - const actorId = card.dataset.actorId; - return game.actors.get(actorId) || null; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Handle update actions required when consuming an external resource + * @param {object} itemUpdates An object of data updates applied to this item + * @param {object} actorUpdates An object of data updates applied to the item owner (Actor) + * @param {object} resourceUpdates An object of data updates applied to a different resource item (Item) + * @return {boolean|void} Return false to block further progress, or return nothing to continue + * @private + */ + _handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates) { + const actor = this.actor; + const itemData = this.data.data; + const consume = itemData.consume || {}; + if (!consume.type) return; - /** - * Get the Actor which is the author of a chat card - * @param {HTMLElement} card The chat card being used - * @return {Actor[]} An Array of Actor entities, if any - * @private - */ - static _getChatCardTargets(card) { - 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; - } + // No consumed target + const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type]; + if (!consume.target) { + ui.notifications.warn( + game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}) + ); + return false; + } - /* -------------------------------------------- */ - /* Event Handlers */ - /* -------------------------------------------- */ + // Identify the consumed resource and its current quantity + let resource = null; + let amount = Number(consume.amount ?? 1); + let quantity = 0; + switch (consume.type) { + case "attribute": + resource = getProperty(actor.data.data, consume.target); + quantity = resource || 0; + break; + case "ammo": + case "material": + resource = actor.items.get(consume.target); + quantity = resource ? resource.data.data.quantity : 0; + break; + case "charges": + resource = actor.items.get(consume.target); + if (!resource) break; + const uses = resource.data.data.uses; + if (uses.per && uses.max) quantity = uses.value; + else if (resource.data.data.recharge?.value) { + quantity = resource.data.data.recharge.charged ? 1 : 0; + amount = 1; + } + break; + } - /** @inheritdoc */ - async _preCreate(data, options, user) { - await super._preCreate(data, options, user); - if ( !this.isEmbedded || (this.parent.type === "vehicle") ) return; - const actorData = this.parent.data; - const isNPC = this.parent.type === "npc"; - let updates; - switch (data.type) { - case "equipment": - updates = this._onCreateOwnedEquipment(data, actorData, isNPC); - break; - case "weapon": - updates = this._onCreateOwnedWeapon(data, actorData, isNPC); - break; - case "power": - updates = this._onCreateOwnedPower(data, actorData, isNPC); - break; + // Verify that a consumed resource is available + if (!resource) { + ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel})); + return false; + } + + // Verify that the required quantity is available + let remaining = quantity - amount; + if (remaining < 0) { + ui.notifications.warn( + game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel}) + ); + return false; + } + + // Define updates to provided data objects + switch (consume.type) { + case "attribute": + actorUpdates[`data.${consume.target}`] = remaining; + break; + case "ammo": + case "material": + resourceUpdates["data.quantity"] = remaining; + break; + case "charges": + const uses = resource.data.data.uses || {}; + const recharge = resource.data.data.recharge || {}; + if (uses.per && uses.max) resourceUpdates["data.uses.value"] = remaining; + else if (recharge.value) resourceUpdates["data.recharge.charged"] = false; + break; + } } - if (updates) return this.data.update(updates); - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @inheritdoc */ - _onCreate(data, options, userId) { - super._onCreate(data, options, userId); + /** + * Display the chat card for an Item as a Chat Message + * @param {object} options Options which configure the display of the item chat card + * @param {string} rollMode The message visibility mode to apply to the created card + * @param {boolean} createMessage Whether to automatically create a ChatMessage entity (if true), or only return + * the prepared message data (if false) + */ + async displayCard({rollMode, createMessage = true} = {}) { + // Render the chat card template + const token = this.actor.token; + const templateData = { + actor: this.actor, + tokenId: token?.uuid || null, + item: this.data, + data: this.getChatData(), + labels: this.labels, + hasAttack: this.hasAttack, + isHealing: this.isHealing, + hasDamage: this.hasDamage, + isVersatile: this.isVersatile, + isPower: this.data.type === "power", + hasSave: this.hasSave, + hasAreaTarget: this.hasAreaTarget, + isTool: this.data.type === "tool" + }; + const html = await renderTemplate("systems/sw5e/templates/chat/item-card.html", templateData); - // The below options are only needed for character classes - if ( userId !== game.user.id ) return; - const isCharacterClass = this.parent && (this.parent.type !== "vehicle") && (this.type === "class"); - if ( !isCharacterClass ) return; + // Create the ChatMessage data object + const chatData = { + user: game.user.data._id, + type: CONST.CHAT_MESSAGE_TYPES.OTHER, + content: html, + flavor: this.data.data.chatFlavor || this.name, + speaker: ChatMessage.getSpeaker({actor: this.actor, token}), + flags: {"core.canPopout": true} + }; - // Assign a new primary class - const pc = this.parent.items.get(this.parent.data.data.details.originalClass); - if ( !pc ) this.parent._assignPrimaryClass(); + // If the Item was destroyed in the process of displaying its card - embed the item data in the chat message + if (this.data.type === "consumable" && !this.actor.items.has(this.id)) { + chatData.flags["sw5e.itemData"] = this.data; + } - // Prompt to add new class features - if (options.addFeatures === false) return; - this.parent.getClassFeatures({ - className: this.name, - archetypeName: this.data.data.archetype, - level: this.data.data.levels - }).then(features => { - return this.parent.addEmbeddedItems(features, options.promptAddFeatures); - }); - } + // Apply the roll mode to adjust message visibility + ChatMessage.applyRollMode(chatData, rollMode || game.settings.get("core", "rollMode")); - /* -------------------------------------------- */ - - /** @inheritdoc */ - _onUpdate(changed, options, userId) { - super._onUpdate(changed, options, userId); - - // The below options are only needed for character classes - if ( userId !== game.user.id ) return; - const isCharacterClass = this.parent && (this.parent.type !== "vehicle") && (this.type === "class"); - if ( !isCharacterClass ) return; - - // Prompt to add new class features - const addFeatures = changed["name"] || (changed.data && ["archetype", "levels"].some(k => k in changed.data)); - if ( !addFeatures || (options.addFeatures === false) ) return; - this.parent.getClassFeatures({ - className: changed.name || this.name, - archetypeName: changed.data?.archetype || this.data.data.archetype, - level: changed.data?.levels || this.data.data.levels - }).then(features => { - return this.parent.addEmbeddedItems(features, options.promptAddFeatures); - }); - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - _onDelete(options, userId) { - super._onDelete(options, userId); - - // Assign a new primary class - if ( this.parent && (this.type === "class") && (userId === game.user.id) ) { - if ( this.id !== this.parent.data.data.details.originalClass ) return; - this.parent._assignPrimaryClass(); + // Create the Chat Message or return its data + return createMessage ? ChatMessage.create(chatData) : chatData; } - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ + /* Chat Cards */ + /* -------------------------------------------- */ - /** - * Pre-creation logic for the automatic configuration of owned equipment type Items - * @private - */ - _onCreateOwnedEquipment(data, actorData, isNPC) { - const updates = {}; - if ( foundry.utils.getProperty(data, "data.equipped") === undefined ) { - updates["data.equipped"] = isNPC; // NPCs automatically equip equipment + /** + * Prepare an object of chat data used to display a card for the Item in the chat log + * @param {Object} htmlOptions Options used by the TextEditor.enrichHTML function + * @return {Object} An object of chat data to render + */ + getChatData(htmlOptions = {}) { + const data = foundry.utils.deepClone(this.data.data); + const labels = this.labels; + + // Rich text description + data.description.value = TextEditor.enrichHTML(data.description.value, htmlOptions); + + // Item type specific properties + const props = []; + const fn = this[`_${this.data.type}ChatData`]; + if (fn) fn.bind(this)(data, labels, props); + + // Equipment properties + if (data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type)) { + if (data.attunement === CONFIG.SW5E.attunementTypes.REQUIRED) + props.push(game.i18n.localize(CONFIG.SW5E.attunements[CONFIG.SW5E.attunementTypes.REQUIRED])); + props.push( + game.i18n.localize(data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"), + game.i18n.localize(data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient") + ); + } + + // Ability activation properties + if (data.hasOwnProperty("activation")) { + props.push( + labels.activation + (data.activation?.condition ? ` (${data.activation.condition})` : ""), + labels.target, + labels.range, + labels.duration + ); + } + + // Filter properties and return + data.properties = props.filter((p) => !!p); + return data; } - if ( foundry.utils.getProperty(data, "data.proficient") === undefined ) { - if ( isNPC ) { - updates["data.proficient"] = true; // NPCs automatically have equipment proficiency - } else { - const armorProf = { - "natural": true, - "clothing": true, - "light": "lgt", - "medium": "med", - "heavy": "hvy", - "shield": "shl" - }[data.data?.armor?.type]; // Player characters check proficiency - const actorArmorProfs = actorData.data.traits?.armorProf?.value || []; - updates["data.proficient"] = (armorProf === true) || actorArmorProfs.includes(armorProf); - } + + /* -------------------------------------------- */ + + /** + * Prepare chat card data for equipment type items + * @private + */ + _equipmentChatData(data, labels, props) { + props.push( + CONFIG.SW5E.equipmentTypes[data.armor.type], + labels.armor || null, + data.stealth.value ? game.i18n.localize("SW5E.StealthDisadvantage") : null + ); } - return updates; - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Pre-creation logic for the automatic configuration of owned power type Items - * @private - */ - _onCreateOwnedPower(data, actorData, isNPC) { - const updates = {}; - updates["data.preparation.prepared"] = true; // Automatically prepare powers for everyone - return updates; - } - - /* -------------------------------------------- */ - - /** - * Pre-creation logic for the automatic configuration of owned weapon type Items - * @private - */ - _onCreateOwnedWeapon(data, actorData, isNPC) { - const updates = {}; - if ( foundry.utils.getProperty(data, "data.equipped") === undefined ) { - updates["data.equipped"] = isNPC; // NPCs automatically equip weapons + /** + * Prepare chat card data for weapon type items + * @private + */ + _weaponChatData(data, labels, props) { + props.push(CONFIG.SW5E.weaponTypes[data.weaponType]); } - if ( foundry.utils.getProperty(data, "data.proficient") === undefined ) { - if ( isNPC ) { - updates["data.proficient"] = true; // NPCs automatically have equipment proficiency - } else { - // TODO: With the changes to make weapon proficiencies more verbose, this may need revising - const weaponProf = { - "natural": true, - "simpleVW": "sim", - "simpleB": "sim", - "simpleLW": "sim", - "martialVW": "mar", - "martialB": "mar", - "martialLW": "mar" - }[data.data?.weaponType]; // Player characters check proficiency - const actorWeaponProfs = actorData.data.traits?.weaponProf?.value || []; - updates["data.proficient"] = (weaponProf === true) || actorWeaponProfs.includes(weaponProf); - } + + /* -------------------------------------------- */ + + /** + * Prepare chat card data for consumable type items + * @private + */ + _consumableChatData(data, labels, props) { + props.push( + CONFIG.SW5E.consumableTypes[data.consumableType], + data.uses.value + "/" + data.uses.max + " " + game.i18n.localize("SW5E.Charges") + ); + data.hasCharges = data.uses.value >= 0; } - return updates; - } - /* -------------------------------------------- */ - /* Factory Methods */ - /* -------------------------------------------- */ -// TODO: Make work properly - /** - * Create a consumable power scroll Item from a power Item. - * @param {Item5e} power The power to be made into a scroll - * @return {Item5e} The created scroll consumable item - */ - static async createScrollFromPower(power) { + /* -------------------------------------------- */ - // Get power data - const itemData = power instanceof Item5e ? power.data : power; - const {actionType, description, source, activation, duration, target, range, damage, save, level} = itemData.data; + /** + * Prepare chat card data for tool type items + * @private + */ + _toolChatData(data, labels, props) { + props.push(CONFIG.SW5E.abilities[data.ability] || null, CONFIG.SW5E.proficiencyLevels[data.proficient || 0]); + } - // Get scroll data - const scrollUuid = `Compendium.${CONFIG.SW5E.sourcePacks.ITEMS}.${CONFIG.SW5E.powerScrollIds[level]}`; - const scrollItem = await fromUuid(scrollUuid); - const scrollData = scrollItem.data; - delete scrollData._id; + /* -------------------------------------------- */ - // Split the scroll description into an intro paragraph and the remaining details - const scrollDescription = scrollData.data.description.value; - const pdel = '

'; - const scrollIntroEnd = scrollDescription.indexOf(pdel); - const scrollIntro = scrollDescription.slice(0, scrollIntroEnd + pdel.length); - const scrollDetails = scrollDescription.slice(scrollIntroEnd + pdel.length); + /** + * Prepare chat card data for loot type items + * @private + */ + _lootChatData(data, labels, props) { + props.push( + game.i18n.localize("SW5E.ItemTypeLoot"), + data.weight ? data.weight + " " + game.i18n.localize("SW5E.AbbreviationLbs") : null + ); + } - // Create a composite description from the scroll description and the power details - const desc = `${scrollIntro}

${itemData.name} (Level ${level})


${description.value}

Scroll Details


${scrollDetails}`; + /* -------------------------------------------- */ - // Create the power scroll data - const powerScrollData = mergeObject(scrollData, { - name: `${game.i18n.localize("SW5E.PowerScroll")}: ${itemData.name}`, - img: itemData.img, - data: { - "description.value": desc.trim(), - source, - actionType, - activation, - duration, - target, - range, - damage, - save, - level - } - }); - return new this(powerScrollData); - } + /** + * Render a chat card for Power type data + * @return {Object} + * @private + */ + _powerChatData(data, labels, props) { + props.push(labels.level, labels.components + (labels.materials ? ` (${labels.materials})` : "")); + } + + /* -------------------------------------------- */ + + /** + * Prepare chat card data for items of the "Feat" type + * @private + */ + _featChatData(data, labels, props) { + props.push(data.requirements); + } + + /* -------------------------------------------- */ + /* Item Rolls - Attack, Damage, Saves, Checks */ + /* -------------------------------------------- */ + + /** + * Place an attack roll using an item (weapon, feat, power, or equipment) + * Rely upon the d20Roll logic for the core implementation + * + * @param {object} options Roll options which are configured and provided to the d20Roll function + * @return {Promise} A Promise which resolves to the created Roll instance + */ + async rollAttack(options = {}) { + const itemData = this.data.data; + const flags = this.actor.data.flags.sw5e || {}; + if (!this.hasAttack) { + throw new Error("You may not place an Attack Roll with this Item."); + } + let title = `${this.name} - ${game.i18n.localize("SW5E.AttackRoll")}`; + + // get the parts and rollData for this item's attack + const {parts, rollData} = this.getAttackToHit(); + + // Handle ammunition consumption + delete this._ammo; + let ammo = null; + let ammoUpdate = null; + const consume = itemData.consume; + if (consume?.type === "ammo") { + ammo = this.actor.items.get(consume.target); + if (ammo?.data) { + const q = ammo.data.data.quantity; + const consumeAmount = consume.amount ?? 0; + if (q && q - consumeAmount >= 0) { + this._ammo = ammo; + title += ` [${ammo.name}]`; + } + } + + // Get pending ammunition update + const usage = this._getUsageUpdates({consumeResource: true}); + if (usage === false) return null; + ammoUpdate = usage.resourceUpdates || {}; + } + + // Compose roll options + const rollConfig = mergeObject( + { + parts: parts, + actor: this.actor, + data: rollData, + title: title, + flavor: title, + speaker: ChatMessage.getSpeaker({actor: this.actor}), + dialogOptions: { + width: 400, + top: options.event ? options.event.clientY - 80 : null, + left: window.innerWidth - 710 + }, + messageData: {"flags.sw5e.roll": {type: "attack", itemId: this.id}} + }, + options + ); + rollConfig.event = options.event; + + // Expanded critical hit thresholds + if (this.data.type === "weapon" && flags.weaponCriticalThreshold) { + rollConfig.critical = parseInt(flags.weaponCriticalThreshold); + } else if (this.data.type === "power" && flags.powerCriticalThreshold) { + rollConfig.critical = parseInt(flags.powerCriticalThreshold); + } + + // Elven Accuracy + if (flags.elvenAccuracy && ["dex", "int", "wis", "cha"].includes(this.abilityMod)) { + rollConfig.elvenAccuracy = true; + } + + // Apply Halfling Lucky + if (flags.halflingLucky) rollConfig.halflingLucky = true; + + // Invoke the d20 roll helper + const roll = await d20Roll(rollConfig); + if (roll === false) return null; + + // Commit ammunition consumption on attack rolls resource consumption if the attack roll was made + if (ammo && !isObjectEmpty(ammoUpdate)) await ammo.update(ammoUpdate); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Place a damage roll using an item (weapon, feat, power, or equipment) + * Rely upon the damageRoll logic for the core implementation. + * @param {MouseEvent} [event] An event which triggered this roll, if any + * @param {boolean} [critical] Should damage be rolled as a critical hit? + * @param {number} [powerLevel] If the item is a power, override the level for damage scaling + * @param {boolean} [versatile] If the item is a weapon, roll damage using the versatile formula + * @param {object} [options] Additional options passed to the damageRoll function + * @return {Promise} A Promise which resolves to the created Roll instance + */ + rollDamage({critical = false, event = null, powerLevel = null, versatile = false, options = {}} = {}) { + if (!this.hasDamage) throw new Error("You may not make a Damage Roll with this Item."); + const itemData = this.data.data; + const actorData = this.actor.data.data; + const messageData = {"flags.sw5e.roll": {type: "damage", itemId: this.id}}; + + // Get roll data + const parts = itemData.damage.parts.map((d) => d[0]); + const rollData = this.getRollData(); + if (powerLevel) rollData.item.level = powerLevel; + + // Configure the damage roll + const actionFlavor = game.i18n.localize(itemData.actionType === "heal" ? "SW5E.Healing" : "SW5E.DamageRoll"); + const title = `${this.name} - ${actionFlavor}`; + const rollConfig = { + actor: this.actor, + critical: critical ?? event?.altKey ?? false, + data: rollData, + event: event, + fastForward: event ? event.shiftKey || event.altKey || event.ctrlKey || event.metaKey : false, + parts: parts, + title: title, + flavor: this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title, + speaker: ChatMessage.getSpeaker({actor: this.actor}), + dialogOptions: { + width: 400, + top: event ? event.clientY - 80 : null, + left: window.innerWidth - 710 + }, + messageData: messageData + }; + + // Adjust damage from versatile usage + if (versatile && itemData.damage.versatile) { + parts[0] = itemData.damage.versatile; + messageData["flags.sw5e.roll"].versatile = true; + } + + // Scale damage from up-casting powers + if (this.data.type === "power") { + if (itemData.scaling.mode === "atwill") { + const level = + this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel; + this._scaleAtWillDamage(parts, itemData.scaling.formula, level, rollData); + } else if (powerLevel && itemData.scaling.mode === "level" && itemData.scaling.formula) { + const scaling = itemData.scaling.formula; + this._scalePowerDamage(parts, itemData.level, powerLevel, scaling, rollData); + } + } + + // Add damage bonus formula + const actorBonus = getProperty(actorData, `bonuses.${itemData.actionType}`) || {}; + if (actorBonus.damage && parseInt(actorBonus.damage) !== 0) { + parts.push(actorBonus.damage); + } + + // Handle ammunition damage + const ammoData = this._ammo?.data; + + // only add the ammunition damage if the ammution is a consumable with type 'ammo' + if (this._ammo && ammoData.type === "consumable" && ammoData.data.consumableType === "ammo") { + parts.push("@ammo"); + rollData["ammo"] = ammoData.data.damage.parts.map((p) => p[0]).join("+"); + rollConfig.flavor += ` [${this._ammo.name}]`; + delete this._ammo; + } + + // Scale melee critical hit damage + if (itemData.actionType === "mwak") { + rollConfig.criticalBonusDice = this.actor.getFlag("sw5e", "meleeCriticalDamageDice") ?? 0; + } + + // Call the roll helper utility + return damageRoll(mergeObject(rollConfig, options)); + } + + /* -------------------------------------------- */ + + /** + * Adjust an at-will damage formula to scale it for higher level characters and monsters + * @private + */ + _scaleAtWillDamage(parts, scale, level, rollData) { + const add = Math.floor((level + 1) / 6); + if (add === 0) return; + this._scaleDamage(parts, scale || parts.join(" + "), add, rollData); + } + + /* -------------------------------------------- */ + + /** + * Adjust the power damage formula to scale it for power level up-casting + * @param {Array} parts The original damage parts + * @param {number} baseLevel The default power level + * @param {number} powerLevel The casted power level + * @param {string} formula The scaling formula + * @param {object} rollData A data object that should be applied to the scaled damage roll + * @return {string[]} The scaled roll parts + * @private + */ + _scalePowerDamage(parts, baseLevel, powerLevel, formula, rollData) { + const upcastLevels = Math.max(powerLevel - baseLevel, 0); + if (upcastLevels === 0) return parts; + this._scaleDamage(parts, formula, upcastLevels, rollData); + } + + /* -------------------------------------------- */ + + /** + * Scale an array of damage parts according to a provided scaling formula and scaling multiplier + * @param {string[]} parts Initial roll parts + * @param {string} scaling A scaling formula + * @param {number} times A number of times to apply the scaling formula + * @param {object} rollData A data object that should be applied to the scaled damage roll + * @return {string[]} The scaled roll parts + * @private + */ + _scaleDamage(parts, scaling, times, rollData) { + if (times <= 0) return parts; + const p0 = new Roll(parts[0], rollData); + const s = new Roll(scaling, rollData).alter(times); + + // Attempt to simplify by combining like dice terms + let simplified = false; + if (s.terms[0] instanceof Die && s.terms.length === 1) { + const d0 = p0.terms[0]; + const s0 = s.terms[0]; + if (d0 instanceof Die && d0.faces === s0.faces && d0.modifiers.equals(s0.modifiers)) { + d0.number += s0.number; + parts[0] = p0.formula; + simplified = true; + } + } + + // Otherwise add to the first part + if (!simplified) { + parts[0] = `${parts[0]} + ${s.formula}`; + } + return parts; + } + + /* -------------------------------------------- */ + + /** + * Place an attack roll using an item (weapon, feat, power, or equipment) + * Rely upon the d20Roll logic for the core implementation + * + * @return {Promise} A Promise which resolves to the created Roll instance + */ + async rollFormula(options = {}) { + if (!this.data.data.formula) { + throw new Error("This Item does not have a formula to roll!"); + } + + // Define Roll Data + const rollData = this.getRollData(); + if (options.powerLevel) rollData.item.level = options.powerLevel; + const title = `${this.name} - ${game.i18n.localize("SW5E.OtherFormula")}`; + + // Invoke the roll and submit it to chat + const roll = new Roll(rollData.item.formula, rollData).roll(); + roll.toMessage({ + speaker: ChatMessage.getSpeaker({actor: this.actor}), + flavor: title, + rollMode: game.settings.get("core", "rollMode"), + messageData: {"flags.sw5e.roll": {type: "other", itemId: this.id}} + }); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Perform an ability recharge test for an item which uses the d6 recharge mechanic + * @return {Promise} A Promise which resolves to the created Roll instance + */ + async rollRecharge() { + const data = this.data.data; + if (!data.recharge.value) return; + + // Roll the check + const roll = new Roll("1d6").roll(); + const success = roll.total >= parseInt(data.recharge.value); + + // Display a Chat Message + const promises = [ + roll.toMessage({ + flavor: `${game.i18n.format("SW5E.ItemRechargeCheck", {name: this.name})} - ${game.i18n.localize( + success ? "SW5E.ItemRechargeSuccess" : "SW5E.ItemRechargeFailure" + )}`, + speaker: ChatMessage.getSpeaker({actor: this.actor, token: this.actor.token}) + }) + ]; + + // Update the Item data + if (success) promises.push(this.update({"data.recharge.charged": true})); + return Promise.all(promises).then(() => roll); + } + + /* -------------------------------------------- */ + + /** + * Roll a Tool Check. Rely upon the d20Roll logic for the core implementation + * @prarm {Object} options Roll configuration options provided to the d20Roll function + * @return {Promise} A Promise which resolves to the created Roll instance + */ + rollToolCheck(options = {}) { + if (this.type !== "tool") throw "Wrong item type!"; + + // Prepare roll data + let rollData = this.getRollData(); + const parts = [`@mod`, "@prof"]; + const title = `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`; + + // Add global actor bonus + const bonuses = getProperty(this.actor.data.data, "bonuses.abilities") || {}; + if (bonuses.check) { + parts.push("@checkBonus"); + rollData.checkBonus = bonuses.check; + } + + // Compose the roll data + const rollConfig = mergeObject( + { + parts: parts, + data: rollData, + title: title, + speaker: ChatMessage.getSpeaker({actor: this.actor}), + flavor: title, + dialogOptions: { + width: 400, + top: options.event ? options.event.clientY - 80 : null, + left: window.innerWidth - 710 + }, + chooseModifier: true, + halflingLucky: this.actor.getFlag("sw5e", "halflingLucky") || false, + reliableTalent: this.data.data.proficient >= 1 && this.actor.getFlag("sw5e", "reliableTalent"), + messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id}} + }, + options + ); + rollConfig.event = options.event; + + // Call the roll helper utility + return d20Roll(rollConfig); + } + + /* -------------------------------------------- */ + + /** + * Prepare a data object which is passed to any Roll formulas which are created related to this Item + * @private + */ + getRollData() { + if (!this.actor) return null; + const rollData = this.actor.getRollData(); + rollData.item = foundry.utils.deepClone(this.data.data); + + // Include an ability score modifier if one exists + const abl = this.abilityMod; + if (abl) { + const ability = rollData.abilities[abl]; + if (!ability) { + console.warn( + `Item ${this.name} in Actor ${this.actor.name} has an invalid item ability modifier of ${abl} defined` + ); + } + rollData["mod"] = ability?.mod || 0; + } + + // Include a proficiency score + const prof = "proficient" in rollData.item ? rollData.item.proficient || 0 : 1; + rollData["prof"] = Math.floor(prof * (rollData.attributes.prof || 0)); + return rollData; + } + + /* -------------------------------------------- */ + /* Chat Message Helpers */ + /* -------------------------------------------- */ + + static chatListeners(html) { + html.on("click", ".card-buttons button", this._onChatCardAction.bind(this)); + html.on("click", ".item-name", this._onChatCardToggleContent.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Handle execution of a chat card action via a click event on one of the card buttons + * @param {Event} event The originating click event + * @returns {Promise} A promise which resolves once the handler workflow is complete + * @private + */ + static async _onChatCardAction(event) { + event.preventDefault(); + + // Extract card data + const button = event.currentTarget; + button.disabled = true; + const card = button.closest(".chat-card"); + const messageId = card.closest(".message").dataset.messageId; + const message = game.messages.get(messageId); + const action = button.dataset.action; + + // Validate permission to proceed with the roll + const isTargetted = action === "save"; + if (!(isTargetted || game.user.isGM || message.isAuthor)) return; + + // Recover the actor for the chat card + const actor = await this._getChatCardActor(card); + if (!actor) return; + + // Get the Item from stored flag data or by the item ID on the Actor + const storedData = message.getFlag("sw5e", "itemData"); + const item = storedData ? new this(storedData, {parent: actor}) : actor.items.get(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; + + // Handle different actions + switch (action) { + case "attack": + await item.rollAttack({event}); + break; + case "damage": + case "versatile": + await item.rollDamage({ + critical: event.altKey, + event: event, + powerLevel: powerLevel, + versatile: action === "versatile" + }); + 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 = game.sw5e.canvas.AbilityTemplate.fromItem(item); + if (template) template.drawPreview(); + break; + } + + // Re-enable the button + button.disabled = false; + } + + /* -------------------------------------------- */ + + /** + * Handle toggling the visibility of chat card content when the name is clicked + * @param {Event} event The originating click event + * @private + */ + static _onChatCardToggleContent(event) { + event.preventDefault(); + const header = event.currentTarget; + const card = header.closest(".chat-card"); + const content = card.querySelector(".card-content"); + content.style.display = content.style.display === "none" ? "block" : "none"; + } + + /* -------------------------------------------- */ + + /** + * Get the Actor which is the author of a chat card + * @param {HTMLElement} card The chat card being used + * @return {Actor|null} The Actor entity or null + * @private + */ + static async _getChatCardActor(card) { + // Case 1 - a synthetic actor from a Token + if (card.dataset.tokenId) { + const token = await fromUuid(card.dataset.tokenId); + if (!token) return null; + return token.actor; + } + + // Case 2 - use Actor ID directory + const actorId = card.dataset.actorId; + return game.actors.get(actorId) || null; + } + + /* -------------------------------------------- */ + + /** + * Get the Actor which is the author of a chat card + * @param {HTMLElement} card The chat card being used + * @return {Actor[]} An Array of Actor entities, if any + * @private + */ + static _getChatCardTargets(card) { + 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; + } + + /* -------------------------------------------- */ + /* Event Handlers */ + /* -------------------------------------------- */ + + /** @inheritdoc */ + async _preCreate(data, options, user) { + await super._preCreate(data, options, user); + if (!this.isEmbedded || this.parent.type === "vehicle") return; + const actorData = this.parent.data; + const isNPC = this.parent.type === "npc"; + let updates; + switch (data.type) { + case "equipment": + updates = this._onCreateOwnedEquipment(data, actorData, isNPC); + break; + case "weapon": + updates = this._onCreateOwnedWeapon(data, actorData, isNPC); + break; + case "power": + updates = this._onCreateOwnedPower(data, actorData, isNPC); + break; + } + if (updates) return this.data.update(updates); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + _onCreate(data, options, userId) { + super._onCreate(data, options, userId); + + // The below options are only needed for character classes + if (userId !== game.user.id) return; + const isCharacterClass = this.parent && this.parent.type !== "vehicle" && this.type === "class"; + if (!isCharacterClass) return; + + // Assign a new primary class + const pc = this.parent.items.get(this.parent.data.data.details.originalClass); + if (!pc) this.parent._assignPrimaryClass(); + + // Prompt to add new class features + if (options.addFeatures === false) return; + this.parent + .getClassFeatures({ + className: this.name, + archetypeName: this.data.data.archetype, + level: this.data.data.levels + }) + .then((features) => { + return this.parent.addEmbeddedItems(features, options.promptAddFeatures); + }); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + _onUpdate(changed, options, userId) { + super._onUpdate(changed, options, userId); + + // The below options are only needed for character classes + if (userId !== game.user.id) return; + const isCharacterClass = this.parent && this.parent.type !== "vehicle" && this.type === "class"; + if (!isCharacterClass) return; + + // Prompt to add new class features + const addFeatures = changed["name"] || (changed.data && ["archetype", "levels"].some((k) => k in changed.data)); + if (!addFeatures || options.addFeatures === false) return; + this.parent + .getClassFeatures({ + className: changed.name || this.name, + archetypeName: changed.data?.archetype || this.data.data.archetype, + level: changed.data?.levels || this.data.data.levels + }) + .then((features) => { + return this.parent.addEmbeddedItems(features, options.promptAddFeatures); + }); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + _onDelete(options, userId) { + super._onDelete(options, userId); + + // Assign a new primary class + if (this.parent && this.type === "class" && userId === game.user.id) { + if (this.id !== this.parent.data.data.details.originalClass) return; + this.parent._assignPrimaryClass(); + } + } + + /* -------------------------------------------- */ + + /** + * Pre-creation logic for the automatic configuration of owned equipment type Items + * @private + */ + _onCreateOwnedEquipment(data, actorData, isNPC) { + const updates = {}; + if (foundry.utils.getProperty(data, "data.equipped") === undefined) { + updates["data.equipped"] = isNPC; // NPCs automatically equip equipment + } + if (foundry.utils.getProperty(data, "data.proficient") === undefined) { + if (isNPC) { + updates["data.proficient"] = true; // NPCs automatically have equipment proficiency + } else { + const armorProf = CONFIG.SW5E.armorProficienciesMap[data.data?.armor?.type]; // Player characters check proficiency + const actorArmorProfs = actorData.data.traits?.armorProf?.value || []; + updates["data.proficient"] = armorProf === true || actorArmorProfs.includes(armorProf); + } + } + return updates; + } + + /* -------------------------------------------- */ + + /** + * Pre-creation logic for the automatic configuration of owned power type Items + * @private + */ + _onCreateOwnedPower(data, actorData, isNPC) { + const updates = {}; + updates["data.preparation.prepared"] = true; // Automatically prepare powers for everyone + return updates; + } + + /* -------------------------------------------- */ + + /** + * Pre-creation logic for the automatic configuration of owned weapon type Items + * @private + */ + _onCreateOwnedWeapon(data, actorData, isNPC) { + const updates = {}; + if (foundry.utils.getProperty(data, "data.equipped") === undefined) { + updates["data.equipped"] = isNPC; // NPCs automatically equip weapons + } + if (foundry.utils.getProperty(data, "data.proficient") === undefined) { + if (isNPC) { + updates["data.proficient"] = true; // NPCs automatically have equipment proficiency + } else { + // TODO: With the changes to make weapon proficiencies more verbose, this may need revising + const weaponProf = CONFIG.SW5E.weaponProficienciesMap[data.data?.weaponType]; // Player characters check proficiency + const actorWeaponProfs = actorData.data.traits?.weaponProf?.value || []; + updates["data.proficient"] = weaponProf === true || actorWeaponProfs.includes(weaponProf); + } + } + return updates; + } + + /* -------------------------------------------- */ + /* Factory Methods */ + /* -------------------------------------------- */ + // TODO: Make work properly + /** + * Create a consumable power scroll Item from a power Item. + * @param {Item5e} power The power to be made into a scroll + * @return {Item5e} The created scroll consumable item + */ + static async createScrollFromPower(power) { + // Get power data + const itemData = power instanceof Item5e ? power.data : power; + const {actionType, description, source, activation, duration, target, range, damage, save, level} = + itemData.data; + + // Get scroll data + const scrollUuid = `Compendium.${CONFIG.SW5E.sourcePacks.ITEMS}.${CONFIG.SW5E.powerScrollIds[level]}`; + const scrollItem = await fromUuid(scrollUuid); + const scrollData = scrollItem.data; + delete scrollData._id; + + // Split the scroll description into an intro paragraph and the remaining details + const scrollDescription = scrollData.data.description.value; + const pdel = "

"; + const scrollIntroEnd = scrollDescription.indexOf(pdel); + const scrollIntro = scrollDescription.slice(0, scrollIntroEnd + pdel.length); + const scrollDetails = scrollDescription.slice(scrollIntroEnd + pdel.length); + + // Create a composite description from the scroll description and the power details + const desc = `${scrollIntro}

${itemData.name} (Level ${level})


${description.value}

Scroll Details


${scrollDetails}`; + + // Create the power scroll data + const powerScrollData = mergeObject(scrollData, { + name: `${game.i18n.localize("SW5E.PowerScroll")}: ${itemData.name}`, + img: itemData.img, + data: { + "description.value": desc.trim(), + source, + actionType, + activation, + duration, + target, + range, + damage, + save, + level + } + }); + return new this(powerScrollData); + } } diff --git a/module/item/sheet.js b/module/item/sheet.js index 556032e7..0bb7f64b 100644 --- a/module/item/sheet.js +++ b/module/item/sheet.js @@ -1,362 +1,370 @@ import TraitSelector from "../apps/trait-selector.js"; -import { onManageActiveEffect, prepareActiveEffectCategories } from "../effects.js"; +import {onManageActiveEffect, prepareActiveEffectCategories} from "../effects.js"; /** * Override and extend the core ItemSheet implementation to handle specific item types * @extends {ItemSheet} */ export default class ItemSheet5e extends ItemSheet { - constructor(...args) { - super(...args); + constructor(...args) { + super(...args); - // Expand the default size of the class sheet - if (this.object.data.type === "class") { - this.options.width = this.position.width = 600; - this.options.height = this.position.height = 680; - } - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - width: 560, - height: 400, - classes: ["sw5e", "sheet", "item"], - resizable: true, - scrollY: [".tab.details"], - tabs: [{ navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description" }] - }); - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - get template() { - const path = "systems/sw5e/templates/items/"; - return `${path}/${this.item.data.type}.html`; - } - - /* -------------------------------------------- */ - - /** @override */ - async getData(options) { - const data = super.getData(options); - const itemData = data.data; - data.labels = this.item.labels; - data.config = CONFIG.SW5E; - - // Item Type, Status, and Details - data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`); - data.itemStatus = this._getItemStatus(itemData); - data.itemProperties = this._getItemProperties(itemData); - data.isPhysical = itemData.data.hasOwnProperty("quantity"); - - // Potential consumption targets - data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData); - - // Action Detail - data.hasAttackRoll = this.item.hasAttack; - data.isHealing = itemData.data.actionType === "heal"; - data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat"; - data.isLine = ["line", "wall"].includes(itemData.data.target?.type); - - // Original maximum uses formula - const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max"); - if ( sourceMax ) itemData.data.uses.max = sourceMax; - - // Vehicles - data.isCrewed = itemData.data.activation?.type === "crew"; - data.isMountable = this._isItemMountable(itemData); - - // Prepare Active Effects - data.effects = prepareActiveEffectCategories(this.item.effects); - - // Re-define the template data references (backwards compatible) - data.item = itemData; - data.data = itemData.data; - return data; - } - - /* -------------------------------------------- */ - - /** - * Get the valid item consumption targets which exist on the actor - * @param {Object} item Item data for the item being displayed - * @return {{string: string}} An object of potential consumption targets - * @private - */ - _getItemConsumptionTargets(item) { - const consume = item.data.consume || {}; - if (!consume.type) return []; - const actor = this.item.actor; - if (!actor) return {}; - - // Ammunition - if (consume.type === "ammo") { - return actor.itemTypes.consumable.reduce( - (ammo, i) => { - if (i.data.data.consumableType === "ammo") { - ammo[i.id] = `${i.name} (${i.data.data.quantity})`; - } - return ammo; - }, - { [item._id]: `${item.name} (${item.data.quantity})` } - ); - } - - // Attributes - else if (consume.type === "attribute") { - const attributes = TokenDocument.getTrackedAttributes(actor.data.data); - attributes.bar.forEach(a => a.push("value")); - return attributes.bar.concat(attributes.value).reduce((obj, a) => { - let k = a.join("."); - obj[k] = k; - return obj; - }, {}); - } - - // Materials - else if (consume.type === "material") { - return actor.items.reduce((obj, i) => { - if (["consumable", "loot"].includes(i.data.type) && !i.data.data.activation) { - obj[i.id] = `${i.name} (${i.data.data.quantity})`; + // Expand the default size of the class sheet + if (this.object.data.type === "class") { + this.options.width = this.position.width = 600; + this.options.height = this.position.height = 680; } - return obj; - }, {}); } - // Charges - else if (consume.type === "charges") { - return actor.items.reduce((obj, i) => { - // Limited-use items - const uses = i.data.data.uses || {}; - if (uses.per && uses.max) { - const label = - uses.per === "charges" - ? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", { value: uses.value })})` - : ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", { max: uses.max, per: uses.per })})`; - obj[i.id] = i.name + label; + /* -------------------------------------------- */ + + /** @inheritdoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + width: 560, + height: 400, + classes: ["sw5e", "sheet", "item"], + resizable: true, + scrollY: [".tab.details"], + tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}] + }); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + get template() { + const path = "systems/sw5e/templates/items/"; + return `${path}/${this.item.data.type}.html`; + } + + /* -------------------------------------------- */ + + /** @override */ + async getData(options) { + const data = super.getData(options); + const itemData = data.data; + data.labels = this.item.labels; + data.config = CONFIG.SW5E; + + // Item Type, Status, and Details + data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`); + data.itemStatus = this._getItemStatus(itemData); + data.itemProperties = this._getItemProperties(itemData); + data.isPhysical = itemData.data.hasOwnProperty("quantity"); + + // Potential consumption targets + data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData); + + // Action Details + data.hasAttackRoll = this.item.hasAttack; + data.isHealing = itemData.data.actionType === "heal"; + data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat"; + data.isLine = ["line", "wall"].includes(itemData.data.target?.type); + + // Original maximum uses formula + const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max"); + if (sourceMax) itemData.data.uses.max = sourceMax; + + // Vehicles + data.isCrewed = itemData.data.activation?.type === "crew"; + data.isMountable = this._isItemMountable(itemData); + + // Prepare Active Effects + data.effects = prepareActiveEffectCategories(this.item.effects); + + // Re-define the template data references (backwards compatible) + data.item = itemData; + data.data = itemData.data; + return data; + } + + /* -------------------------------------------- */ + + /** + * Get the valid item consumption targets which exist on the actor + * @param {Object} item Item data for the item being displayed + * @return {{string: string}} An object of potential consumption targets + * @private + */ + _getItemConsumptionTargets(item) { + const consume = item.data.consume || {}; + if (!consume.type) return []; + const actor = this.item.actor; + if (!actor) return {}; + + // Ammunition + if (consume.type === "ammo") { + return actor.itemTypes.consumable.reduce( + (ammo, i) => { + if (i.data.data.consumableType === "ammo") { + ammo[i.id] = `${i.name} (${i.data.data.quantity})`; + } + return ammo; + }, + {[item._id]: `${item.name} (${item.data.quantity})`} + ); } - // Recharging items - const recharge = i.data.data.recharge || {}; - if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`; - return obj; - }, {}); - } else return {}; - } + // Attributes + else if (consume.type === "attribute") { + const attributes = TokenDocument.getTrackedAttributes(actor.data.data); + attributes.bar.forEach((a) => a.push("value")); + return attributes.bar.concat(attributes.value).reduce((obj, a) => { + let k = a.join("."); + obj[k] = k; + return obj; + }, {}); + } - /* -------------------------------------------- */ + // Materials + else if (consume.type === "material") { + return actor.items.reduce((obj, i) => { + if (["consumable", "loot"].includes(i.data.type) && !i.data.data.activation) { + obj[i.id] = `${i.name} (${i.data.data.quantity})`; + } + return obj; + }, {}); + } - /** - * Get the text item status which is shown beneath the Item type in the top-right corner of the sheet - * @return {string} - * @private - */ - _getItemStatus(item) { - if (item.type === "power") { - return CONFIG.SW5E.powerPreparationModes[item.data.preparation]; - } else if (["weapon", "equipment"].includes(item.type)) { - return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"); - } else if (item.type === "tool") { - return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"); - } - } + // Charges + else if (consume.type === "charges") { + return actor.items.reduce((obj, i) => { + // Limited-use items + const uses = i.data.data.uses || {}; + if (uses.per && uses.max) { + const label = + uses.per === "charges" + ? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})` + : ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", { + max: uses.max, + per: uses.per + })})`; + obj[i.id] = i.name + label; + } - /* -------------------------------------------- */ - - /** - * Get the Array of item properties which are used in the small sidebar of the description tab - * @return {Array} - * @private - */ - _getItemProperties(item) { - const props = []; - const labels = this.item.labels; - - if (item.type === "weapon") { - props.push( - ...Object.entries(item.data.properties) - .filter((e) => e[1] === true) - .map((e) => CONFIG.SW5E.weaponProperties[e[0]]) - ); - } else if (item.type === "power") { - props.push( - labels.components, - labels.materials, - item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null, - item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null - ); - } else if (item.type === "equipment") { - props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]); - props.push(labels.armor); - } else if (item.type === "feat") { - props.push(labels.featType); - //TODO: Work out these - } else if (item.type === "species") { - //props.push(labels.species); - } else if (item.type === "archetype") { - //props.push(labels.archetype); - } else if (item.type === "background") { - //props.push(labels.background); - } else if (item.type === "classfeature") { - //props.push(labels.classfeature); - } else if (item.type === "deployment") { - //props.push(labels.deployment); - } else if (item.type === "venture") { - //props.push(labels.venture); - } else if (item.type === "fightingmastery") { - //props.push(labels.fightingmastery); - } else if (item.type === "fightingstyle") { - //props.push(labels.fightingstyle); - } else if (item.type === "lightsaberform") { - //props.push(labels.lightsaberform); + // Recharging items + const recharge = i.data.data.recharge || {}; + if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`; + return obj; + }, {}); + } else return {}; } - // Action type - if (item.data.actionType) { - props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]); + /* -------------------------------------------- */ + + /** + * Get the text item status which is shown beneath the Item type in the top-right corner of the sheet + * @return {string} + * @private + */ + _getItemStatus(item) { + if (item.type === "power") { + return CONFIG.SW5E.powerPreparationModes[item.data.preparation]; + } else if (["weapon", "equipment"].includes(item.type)) { + return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"); + } else if (item.type === "tool") { + return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"); + } } - // Action usage - if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) { - props.push(labels.activation, labels.range, labels.target, labels.duration); - } - return props.filter((p) => !!p); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Get the Array of item properties which are used in the small sidebar of the description tab + * @return {Array} + * @private + */ + _getItemProperties(item) { + const props = []; + const labels = this.item.labels; - /** - * Is this item a separate large object like a siege engine or vehicle - * component that is usually mounted on fixtures rather than equipped, and - * has its own AC and HP. - * @param item - * @returns {boolean} - * @private - */ - _isItemMountable(item) { - const data = item.data; - return ( - (item.type === "weapon" && data.weaponType === "siege") || - (item.type === "equipment" && data.armor.type === "vehicle") - ); - } + if (item.type === "weapon") { + props.push( + ...Object.entries(item.data.properties) + .filter((e) => e[1] === true) + .map((e) => CONFIG.SW5E.weaponProperties[e[0]]) + ); + } else if (item.type === "power") { + props.push( + labels.materials, + item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null, + item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null + ); + } else if (item.type === "equipment") { + props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]); + props.push(labels.armor); + } else if (item.type === "feat") { + props.push(labels.featType); + //TODO: Work out these + } else if (item.type === "species") { + //props.push(labels.species); + } else if (item.type === "archetype") { + //props.push(labels.archetype); + } else if (item.type === "background") { + //props.push(labels.background); + } else if (item.type === "classfeature") { + //props.push(labels.classfeature); + } else if (item.type === "deployment") { + //props.push(labels.deployment); + } else if (item.type === "venture") { + //props.push(labels.venture); + } else if (item.type === "fightingmastery") { + //props.push(labels.fightingmastery); + } else if (item.type === "fightingstyle") { + //props.push(labels.fightingstyle); + } else if (item.type === "lightsaberform") { + //props.push(labels.lightsaberform); + } - /* -------------------------------------------- */ + // Action type + if (item.data.actionType) { + props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]); + } - /** @inheritdoc */ - setPosition(position = {}) { - if (!(this._minimized || position.height)) { - position.height = this._tabs[0].active === "details" ? "auto" : this.options.height; - } - return super.setPosition(position); - } - - /* -------------------------------------------- */ - /* Form Submission */ - /* -------------------------------------------- */ - - /** @inheritdoc */ - _getSubmitData(updateData = {}) { - // Create the expanded update data object - const fd = new FormDataExtended(this.form, { editors: this.editors }); - let data = fd.toObject(); - if (updateData) data = mergeObject(data, updateData); - else data = expandObject(data); - - // Handle Damage array - const damage = data.data?.damage; - if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]); - - // Return the flattened submission data - return flattenObject(data); - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - activateListeners(html) { - super.activateListeners(html); - if (this.isEditable) { - html.find(".damage-control").click(this._onDamageControl.bind(this)); - html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this)); - html.find(".effect-control").click((ev) => { - if (this.item.isOwned) return ui.notifications.warn("Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update."); - onManageActiveEffect(ev, this.item); - }); - } - } - - /* -------------------------------------------- */ - - /** - * Add or remove a damage part from the damage formula - * @param {Event} event The original click event - * @return {Promise} - * @private - */ - async _onDamageControl(event) { - event.preventDefault(); - const a = event.currentTarget; - - // Add new damage component - if (a.classList.contains("add-damage")) { - await this._onSubmit(event); // Submit any unsaved changes - const damage = this.item.data.data.damage; - return this.item.update({ "data.damage.parts": damage.parts.concat([["", ""]]) }); + // Action usage + if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) { + props.push(labels.activation, labels.range, labels.target, labels.duration); + } + return props.filter((p) => !!p); } - // Remove a damage component - if (a.classList.contains("delete-damage")) { - await this._onSubmit(event); // Submit any unsaved changes - const li = a.closest(".damage-part"); - const damage = foundry.utils.deepClone(this.item.data.data.damage); - damage.parts.splice(Number(li.dataset.damagePart), 1); - return this.item.update({ "data.damage.parts": damage.parts }); + /* -------------------------------------------- */ + + /** + * Is this item a separate large object like a siege engine or vehicle + * component that is usually mounted on fixtures rather than equipped, and + * has its own AC and HP. + * @param item + * @returns {boolean} + * @private + */ + _isItemMountable(item) { + const data = item.data; + return ( + (item.type === "weapon" && data.weaponType === "siege") || + (item.type === "equipment" && data.armor.type === "vehicle") + ); } - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Handle spawning the TraitSelector application for selection various options. - * @param {Event} event The click event which originated the selection - * @private - */ - _onConfigureTraits(event) { - event.preventDefault(); - const a = event.currentTarget; - - const options = { - name: a.dataset.target, - title: a.parentElement.innerText, - choices: [], - allowCustom: false - }; - - switch(a.dataset.options) { - case 'saves': - options.choices = CONFIG.SW5E.abilities; - options.valueKey = null; - break; - case 'skills': - const skills = this.item.data.data.skills; - const choiceSet = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills); - options.choices = Object.fromEntries(Object.entries(CONFIG.SW5E.skills).filter(skill => choiceSet.includes(skill[0]))); - options.maximum = skills.number; - break; + /** @inheritdoc */ + setPosition(position = {}) { + if (!(this._minimized || position.height)) { + position.height = this._tabs[0].active === "details" ? "auto" : this.options.height; + } + return super.setPosition(position); } - new TraitSelector(this.item, options).render(true); - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ + /* Form Submission */ + /* -------------------------------------------- */ - /** @inheritdoc */ - async _onSubmit(...args) { - if (this._tabs[0].active === "details") this.position.height = "auto"; - await super._onSubmit(...args); - } + /** @inheritdoc */ + _getSubmitData(updateData = {}) { + // Create the expanded update data object + const fd = new FormDataExtended(this.form, {editors: this.editors}); + let data = fd.toObject(); + if (updateData) data = mergeObject(data, updateData); + else data = expandObject(data); + + // Handle Damage array + const damage = data.data?.damage; + if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]); + + // Return the flattened submission data + return flattenObject(data); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + activateListeners(html) { + super.activateListeners(html); + if (this.isEditable) { + html.find(".damage-control").click(this._onDamageControl.bind(this)); + html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this)); + html.find(".effect-control").click((ev) => { + if (this.item.isOwned) + return ui.notifications.warn( + "Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update." + ); + onManageActiveEffect(ev, this.item); + }); + } + } + + /* -------------------------------------------- */ + + /** + * Add or remove a damage part from the damage formula + * @param {Event} event The original click event + * @return {Promise} + * @private + */ + async _onDamageControl(event) { + event.preventDefault(); + const a = event.currentTarget; + + // Add new damage component + if (a.classList.contains("add-damage")) { + await this._onSubmit(event); // Submit any unsaved changes + const damage = this.item.data.data.damage; + return this.item.update({"data.damage.parts": damage.parts.concat([["", ""]])}); + } + + // Remove a damage component + if (a.classList.contains("delete-damage")) { + await this._onSubmit(event); // Submit any unsaved changes + const li = a.closest(".damage-part"); + const damage = foundry.utils.deepClone(this.item.data.data.damage); + damage.parts.splice(Number(li.dataset.damagePart), 1); + return this.item.update({"data.damage.parts": damage.parts}); + } + } + + /* -------------------------------------------- */ + + /** + * Handle spawning the TraitSelector application for selection various options. + * @param {Event} event The click event which originated the selection + * @private + */ + _onConfigureTraits(event) { + event.preventDefault(); + const a = event.currentTarget; + + const options = { + name: a.dataset.target, + title: a.parentElement.innerText, + choices: [], + allowCustom: false + }; + + switch (a.dataset.options) { + case "saves": + options.choices = CONFIG.SW5E.abilities; + options.valueKey = null; + break; + case "skills": + const skills = this.item.data.data.skills; + const choiceSet = + skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills); + options.choices = Object.fromEntries( + Object.entries(CONFIG.SW5E.skills).filter((skill) => choiceSet.includes(skill[0])) + ); + options.maximum = skills.number; + break; + } + new TraitSelector(this.item, options).render(true); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + async _onSubmit(...args) { + if (this._tabs[0].active === "details") this.position.height = "auto"; + await super._onSubmit(...args); + } } diff --git a/module/macros.js b/module/macros.js index 96bb3419..e8919864 100644 --- a/module/macros.js +++ b/module/macros.js @@ -1,4 +1,3 @@ - /* -------------------------------------------- */ /* Hotbar Macros */ /* -------------------------------------------- */ @@ -11,24 +10,24 @@ * @returns {Promise} */ export async function create5eMacro(data, slot) { - if ( data.type !== "Item" ) return; - if (!( "data" in data ) ) return ui.notifications.warn("You can only create macro buttons for owned Items"); - const item = data.data; + if (data.type !== "Item") return; + if (!("data" in data)) return ui.notifications.warn("You can only create macro buttons for owned Items"); + const item = data.data; - // Create the macro command - const command = `game.sw5e.rollItemMacro("${item.name}");`; - let macro = game.macros.entities.find(m => (m.name === item.name) && (m.command === command)); - if ( !macro ) { - macro = await Macro.create({ - name: item.name, - type: "script", - img: item.img, - command: command, - flags: {"sw5e.itemMacro": true} - }); - } - game.user.assignHotbarMacro(macro, slot); - return false; + // Create the macro command + const command = `game.sw5e.rollItemMacro("${item.name}");`; + let macro = game.macros.entities.find((m) => m.name === item.name && m.command === command); + if (!macro) { + macro = await Macro.create({ + name: item.name, + type: "script", + img: item.img, + command: command, + flags: {"sw5e.itemMacro": true} + }); + } + game.user.assignHotbarMacro(macro, slot); + return false; } /* -------------------------------------------- */ @@ -40,20 +39,22 @@ export async function create5eMacro(data, slot) { * @return {Promise} */ export function rollItemMacro(itemName) { - const speaker = ChatMessage.getSpeaker(); - let actor; - if ( speaker.token ) actor = game.actors.tokens[speaker.token]; - if ( !actor ) actor = game.actors.get(speaker.actor); + const speaker = ChatMessage.getSpeaker(); + let actor; + if (speaker.token) actor = game.actors.tokens[speaker.token]; + if (!actor) actor = game.actors.get(speaker.actor); - // Get matching items - const items = actor ? actor.items.filter(i => i.name === itemName) : []; - if ( items.length > 1 ) { - ui.notifications.warn(`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`); - } else if ( items.length === 0 ) { - return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`); - } - const item = items[0]; + // Get matching items + const items = actor ? actor.items.filter((i) => i.name === itemName) : []; + if (items.length > 1) { + ui.notifications.warn( + `Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.` + ); + } else if (items.length === 0) { + return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`); + } + const item = items[0]; - // Trigger the item roll - return item.roll(); + // Trigger the item roll + return item.roll(); } diff --git a/module/migration.js b/module/migration.js index ba772a1b..ab6eb420 100644 --- a/module/migration.js +++ b/module/migration.js @@ -2,65 +2,68 @@ * Perform a system migration for the entire World, applying migrations for Actors, Items, and Compendium packs * @return {Promise} A Promise which resolves once the migration is completed */ -export const migrateWorld = async function() { - ui.notifications.info(`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true}); +export const migrateWorld = async function () { + ui.notifications.info( + `Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, + {permanent: true} + ); - // Migrate World Actors - for await ( let a of game.actors.contents ) { - try { - console.log(`Checking Actor entity ${a.name} for migration needs`); - const updateData = await migrateActorData(a.data); - if ( !foundry.utils.isObjectEmpty(updateData) ) { - console.log(`Migrating Actor entity ${a.name}`); - await a.update(updateData, {enforceTypes: false}); - } - } catch(err) { - err.message = `Failed sw5e system migration for Actor ${a.name}: ${err.message}`; - console.error(err); + // Migrate World Actors + for await (let a of game.actors.contents) { + try { + console.log(`Checking Actor entity ${a.name} for migration needs`); + const updateData = await migrateActorData(a.data); + if (!foundry.utils.isObjectEmpty(updateData)) { + console.log(`Migrating Actor entity ${a.name}`); + await a.update(updateData, {enforceTypes: false}); + } + } catch (err) { + err.message = `Failed sw5e system migration for Actor ${a.name}: ${err.message}`; + console.error(err); + } } - } - // Migrate World Items - for ( let i of game.items.contents ) { - try { - const updateData = migrateItemData(i.toObject()); - if ( !foundry.utils.isObjectEmpty(updateData) ) { - console.log(`Migrating Item entity ${i.name}`); - await i.update(updateData, {enforceTypes: false}); - } - } catch(err) { - err.message = `Failed sw5e system migration for Item ${i.name}: ${err.message}`; - console.error(err); + // Migrate World Items + for (let i of game.items.contents) { + try { + const updateData = migrateItemData(i.toObject()); + if (!foundry.utils.isObjectEmpty(updateData)) { + console.log(`Migrating Item entity ${i.name}`); + await i.update(updateData, {enforceTypes: false}); + } + } catch (err) { + err.message = `Failed sw5e system migration for Item ${i.name}: ${err.message}`; + console.error(err); + } } - } - // Migrate Actor Override Tokens - for ( let s of game.scenes.contents ) { - try { - const updateData = await migrateSceneData(s.data); - if ( !foundry.utils.isObjectEmpty(updateData) ) { - console.log(`Migrating Scene entity ${s.name}`); - await s.update(updateData, {enforceTypes: false}); - // If we do not do this, then synthetic token actors remain in cache - // with the un-updated actorData. - s.tokens.contents.forEach(t => t._actor = null); - } - } catch(err) { - err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`; - console.error(err); + // Migrate Actor Override Tokens + for (let s of game.scenes.contents) { + try { + const updateData = await migrateSceneData(s.data); + if (!foundry.utils.isObjectEmpty(updateData)) { + console.log(`Migrating Scene entity ${s.name}`); + await s.update(updateData, {enforceTypes: false}); + // If we do not do this, then synthetic token actors remain in cache + // with the un-updated actorData. + s.tokens.contents.forEach((t) => (t._actor = null)); + } + } catch (err) { + err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`; + console.error(err); + } } - } - // Migrate World Compendium Packs - for ( let p of game.packs ) { - if ( p.metadata.package !== "world" ) continue; - if ( !["Actor", "Item", "Scene"].includes(p.metadata.entity) ) continue; - await migrateCompendium(p); - } + // Migrate World Compendium Packs + for (let p of game.packs) { + if (p.metadata.package !== "world") continue; + if (!["Actor", "Item", "Scene"].includes(p.metadata.entity)) continue; + await migrateCompendium(p); + } - // Set the migration as complete - game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version); - ui.notifications.info(`SW5e System Migration to version ${game.system.data.version} completed!`, {permanent: true}); + // Set the migration as complete + game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version); + ui.notifications.info(`SW5e System Migration to version ${game.system.data.version} completed!`, {permanent: true}); }; /* -------------------------------------------- */ @@ -70,50 +73,48 @@ export const migrateWorld = async function() { * @param pack * @return {Promise} */ -export const migrateCompendium = async function(pack) { - const entity = pack.metadata.entity; - if ( !["Actor", "Item", "Scene"].includes(entity) ) return; +export const migrateCompendium = async function (pack) { + const entity = pack.metadata.entity; + if (!["Actor", "Item", "Scene"].includes(entity)) return; - // Unlock the pack for editing - const wasLocked = pack.locked; - await pack.configure({locked: false}); + // Unlock the pack for editing + const wasLocked = pack.locked; + await pack.configure({locked: false}); - // Begin by requesting server-side data model migration and get the migrated content - await pack.migrate(); - const documents = await pack.getDocuments(); + // Begin by requesting server-side data model migration and get the migrated content + await pack.migrate(); + const documents = await pack.getDocuments(); - // Iterate over compendium entries - applying fine-tuned migration functions - for await ( let doc of documents ) { - let updateData = {}; - try { - switch (entity) { - case "Actor": - updateData = await migrateActorData(doc.data); - break; - case "Item": - updateData = migrateItemData(doc.toObject()); - break; - case "Scene": - updateData = await migrateSceneData(doc.data); - break; - } - if ( foundry.utils.isObjectEmpty(updateData) ) continue; + // Iterate over compendium entries - applying fine-tuned migration functions + for await (let doc of documents) { + let updateData = {}; + try { + switch (entity) { + case "Actor": + updateData = await migrateActorData(doc.data); + break; + case "Item": + updateData = migrateItemData(doc.toObject()); + break; + case "Scene": + updateData = await migrateSceneData(doc.data); + break; + } + if (foundry.utils.isObjectEmpty(updateData)) continue; - // Save the entry, if data was changed - await doc.update(updateData); - console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`); + // Save the entry, if data was changed + await doc.update(updateData); + console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`); + } catch (err) { + // Handle migration failures + err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`; + console.error(err); + } } - // Handle migration failures - catch(err) { - err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`; - console.error(err); - } - } - - // Apply the original locked status for the pack - await pack.configure({locked: wasLocked}); - console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`); + // Apply the original locked status for the pack + await pack.configure({locked: wasLocked}); + console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`); }; /* -------------------------------------------- */ @@ -126,95 +127,95 @@ export const migrateCompendium = async function(pack) { * @param {object} actor The actor data object to update * @return {Object} The updateData to apply */ -export const migrateActorData = async function(actor) { - const updateData = {}; +export const migrateActorData = async function (actor) { + const updateData = {}; - // Actor Data Updates - if(actor.data) { - _migrateActorMovement(actor, updateData); - _migrateActorSenses(actor, updateData); - _migrateActorType(actor, updateData); - } + // Actor Data Updates + if (actor.data) { + _migrateActorMovement(actor, updateData); + _migrateActorSenses(actor, updateData); + _migrateActorType(actor, updateData); + } - // Migrate Owned Items - if ( !!actor.items ) { - const items = await actor.items.reduce(async (memo, i) => { - const results = await memo; + // Migrate Owned Items + if (!!actor.items) { + const items = await actor.items.reduce(async (memo, i) => { + const results = await memo; - // Migrate the Owned Item - const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i - let itemUpdate = await migrateActorItemData(itemData, actor); + // Migrate the Owned Item + const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i; + let itemUpdate = await migrateActorItemData(itemData, actor); - // Prepared, Equipped, and Proficient for NPC actors - if ( actor.type === "npc" ) { - if (getProperty(itemData.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true; - if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true; - if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true; - } + // Prepared, Equipped, and Proficient for NPC actors + if (actor.type === "npc") { + if (getProperty(itemData.data, "preparation.prepared") === false) + itemUpdate["data.preparation.prepared"] = true; + if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true; + if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true; + } - // Update the Owned Item - if ( !isObjectEmpty(itemUpdate) ) { - itemUpdate._id = itemData.id; - console.log(`Migrating Actor ${actor.name}'s ${i.name}`); - results.push(expandObject(itemUpdate)); - } + // Update the Owned Item + if (!isObjectEmpty(itemUpdate)) { + itemUpdate._id = itemData._id; + console.log(`Migrating Actor ${actor.name}'s ${i.name}`); + results.push(expandObject(itemUpdate)); + } - return results; - }, []); + return results; + }, []); - if ( items.length > 0 ) updateData.items = items; - } + if (items.length > 0) updateData.items = items; + } - // Update NPC data with new datamodel information - if (actor.type === "npc") { - _updateNPCData(actor); - } + // Update NPC data with new datamodel information + if (actor.type === "npc") { + _updateNPCData(actor); + } - // migrate powers last since it relies on item classes being migrated first. - _migrateActorPowers(actor, updateData); - - return updateData; + // migrate powers last since it relies on item classes being migrated first. + _migrateActorPowers(actor, updateData); + + 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 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); + } - // 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; + // Return the scrubbed data + return actorData; } - /* -------------------------------------------- */ /** * Migrate a single Item entity to incorporate latest data model changes - * @param item + * + * @param {object} item Item data to migrate + * @return {object} The updateData to apply */ -export const migrateItemData = function(item) { - const updateData = {}; - _migrateItemClassPowerCasting(item, updateData); - _migrateItemAttunement(item, updateData); - return updateData; +export const migrateItemData = function (item) { + const updateData = {}; + _migrateItemClassPowerCasting(item, updateData); + _migrateItemAttunement(item, updateData); + return updateData; }; /* -------------------------------------------- */ @@ -224,12 +225,12 @@ export const migrateItemData = function(item) { * @param item * @param actor */ -export const migrateActorItemData = async function(item, actor) { - const updateData = {}; - _migrateItemClassPowerCasting(item, updateData); - _migrateItemAttunement(item, updateData); - await _migrateItemPower(item, actor, updateData); - return updateData; +export const migrateActorItemData = async function (item, actor) { + const updateData = {}; + _migrateItemClassPowerCasting(item, updateData); + _migrateItemAttunement(item, updateData); + await _migrateItemPower(item, actor, updateData); + return updateData; }; /* -------------------------------------------- */ @@ -240,33 +241,34 @@ export const migrateActorItemData = async function(item, actor) { * @param {Object} scene The Scene data to Update * @return {Object} The updateData to apply */ - export const migrateSceneData = async function(scene) { - const tokens = await Promise.all(scene.tokens.map(async token => { - const t = token.toJSON(); - if (!t.actorId || t.actorLink) { - t.actorData = {}; - } - else if (!game.actors.has(t.actorId)) { - t.actorId = null; - t.actorData = {}; - } else if ( !t.actorLink ) { - const actorData = duplicate(t.actorData); - actorData.type = token.actor?.type; - const update = migrateActorData(actorData); - ['items', 'effects'].forEach(embeddedName => { - if (!update[embeddedName]?.length) return; - const updates = new Map(update[embeddedName].map(u => [u._id, u])); - t.actorData[embeddedName].forEach(original => { - const update = updates.get(original._id); - if (update) mergeObject(original, update); - }); - delete update[embeddedName]; - }); +export const migrateSceneData = async function (scene) { + const tokens = await Promise.all( + scene.tokens.map(async (token) => { + const t = token.toJSON(); + if (!t.actorId || t.actorLink) { + t.actorData = {}; + } else if (!game.actors.has(t.actorId)) { + t.actorId = null; + t.actorData = {}; + } else if (!t.actorLink) { + const actorData = duplicate(t.actorData); + actorData.type = token.actor?.type; + const update = migrateActorData(actorData); + ["items", "effects"].forEach((embeddedName) => { + if (!update[embeddedName]?.length) return; + const updates = new Map(update[embeddedName].map((u) => [u._id, u])); + t.actorData[embeddedName].forEach((original) => { + const update = updates.get(original._id); + if (update) mergeObject(original, update); + }); + delete update[embeddedName]; + }); - mergeObject(t.actorData, update); - } - return t; - })); + mergeObject(t.actorData, update); + } + return t; + }) + ); return {tokens}; }; @@ -282,87 +284,93 @@ export const migrateActorItemData = async function(item, actor) { * @return {Object} The updated Actor */ function _updateNPCData(actor) { + let actorData = actor.data; + const updateData = {}; + // check for flag.core, if not there is no compendium monster so exit + const hasSource = actor?.flags?.core?.sourceId !== undefined; + if (!hasSource) return actor; + // shortcut out if dataVersion flag is set to 1.2.4 or higher + const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined; + if ( + hasDataVersion && + (actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion)) + ) + return actor; + // Check to see what the source of NPC is + const sourceId = actor.flags.core.sourceId; + const coreSource = sourceId.substr(0, sourceId.length - 17); + const core_id = sourceId.substr(sourceId.length - 16, 16); + if (coreSource === "Compendium.sw5e.monsters") { + game.packs + .get("sw5e.monsters") + .getEntity(core_id) + .then((monster) => { + const monsterData = monster.data.data; + // copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel + updateData["data.attributes.movement"] = monsterData.attributes.movement; + updateData["data.attributes.senses"] = monsterData.attributes.senses; + updateData["data.attributes.powercasting"] = monsterData.attributes.powercasting; + updateData["data.attributes.force"] = monsterData.attributes.force; + updateData["data.attributes.tech"] = monsterData.attributes.tech; + updateData["data.details.powerForceLevel"] = monsterData.details.powerForceLevel; + updateData["data.details.powerTechLevel"] = monsterData.details.powerTechLevel; + // push missing powers onto actor + let newPowers = []; + for (let i of monster.items) { + const itemData = i.data; + if (itemData.type === "power") { + const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0]; + let hasPower = !!actor.items.find( + (item) => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id + ); + if (!hasPower) { + // Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness. + const newPower = JSON.parse(JSON.stringify(itemData)); - let actorData = actor.data; - const updateData = {}; - // check for flag.core, if not there is no compendium monster so exit - const hasSource = actor?.flags?.core?.sourceId !== undefined; - if (!hasSource) return actor; - // shortcut out if dataVersion flag is set to 1.2.4 or higher - const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined; - if (hasDataVersion && (actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion))) return actor; - // Check to see what the source of NPC is - const sourceId = actor.flags.core.sourceId; - const coreSource = sourceId.substr(0,sourceId.length-17); - const core_id = sourceId.substr(sourceId.length-16,16); - if (coreSource === "Compendium.sw5e.monsters"){ - game.packs.get("sw5e.monsters").getEntity(core_id).then(monster => { - const monsterData = monster.data.data; - // copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel - updateData["data.attributes.movement"] = monsterData.attributes.movement; - updateData["data.attributes.senses"] = monsterData.attributes.senses; - updateData["data.attributes.powercasting"] = monsterData.attributes.powercasting; - updateData["data.attributes.force"] = monsterData.attributes.force; - updateData["data.attributes.tech"] = monsterData.attributes.tech; - updateData["data.details.powerForceLevel"] = monsterData.details.powerForceLevel; - updateData["data.details.powerTechLevel"] = monsterData.details.powerTechLevel; - // push missing powers onto actor - let newPowers = []; - for ( let i of monster.items ) { - const itemData = i.data; - if ( itemData.type === "power" ) { - const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0]; - let hasPower = !!actor.items.find(item => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id); - if (!hasPower) { - // Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness. - const newPower = JSON.parse(JSON.stringify(itemData)); + newPowers.push(newPower); + } + } + } - newPowers.push(newPower); - } - } - } + // get actor to create new powers + const liveActor = game.actors.get(actor._id); + // create the powers on the actor + liveActor.createEmbeddedEntity("OwnedItem", newPowers); - // get actor to create new powers - const liveActor = game.actors.get(actor._id); - // create the powers on the actor - liveActor.createEmbeddedEntity("OwnedItem", newPowers); + // set flag to check to see if migration has been done so we don't do it again. + liveActor.setFlag("sw5e", "dataVersion", "1.2.4"); + }); + } - // set flag to check to see if migration has been done so we don't do it again. - liveActor.setFlag("sw5e", "dataVersion", "1.2.4"); - }) - } - - - //merge object - actorData = mergeObject(actorData, updateData); - // Return the scrubbed data - return actor; + //merge object + actorData = mergeObject(actorData, updateData); + // Return the scrubbed data + return actor; } - /** * Migrate the actor speed string to movement object * @private */ function _migrateActorMovement(actorData, updateData) { - const ad = actorData.data; + const ad = actorData.data; - // Work is needed if old data is present - const old = actorData.type === 'vehicle' ? ad?.attributes?.speed : ad?.attributes?.speed?.value; - const hasOld = old !== undefined; - if ( hasOld ) { + // Work is needed if old data is present + const old = actorData.type === "vehicle" ? ad?.attributes?.speed : ad?.attributes?.speed?.value; + const hasOld = old !== undefined; + if (hasOld) { + // If new data is not present, migrate the old data + const hasNew = ad?.attributes?.movement?.walk !== undefined; + if (!hasNew && typeof old === "string") { + const s = (old || "").split(" "); + if (s.length > 0) + updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null; + } - // If new data is not present, migrate the old data - const hasNew = ad?.attributes?.movement?.walk !== undefined; - if ( !hasNew && (typeof old === "string") ) { - const s = (old || "").split(" "); - if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null; + // Remove the old attribute + updateData["data.attributes.-=speed"] = null; } - - // Remove the old attribute - updateData["data.attributes.-=speed"] = null; - } - return updateData + return updateData; } /* -------------------------------------------- */ @@ -372,58 +380,58 @@ function _migrateActorMovement(actorData, updateData) { * @private */ function _migrateActorPowers(actorData, updateData) { - const ad = actorData.data; + const ad = actorData.data; - // If new Force & Tech data is not present, create it - let hasNewAttrib = ad?.attributes?.force?.level !== undefined; - if ( !hasNewAttrib ) { - updateData["data.attributes.force.known.value"] = 0; - updateData["data.attributes.force.known.max"] = 0; - updateData["data.attributes.force.points.value"] = 0; - updateData["data.attributes.force.points.min"] = 0; - updateData["data.attributes.force.points.max"] = 0; - updateData["data.attributes.force.points.temp"] = null; - updateData["data.attributes.force.points.tempmax"] = null; - updateData["data.attributes.force.level"] = 0; - updateData["data.attributes.tech.known.value"] = 0; - updateData["data.attributes.tech.known.max"] = 0; - updateData["data.attributes.tech.points.value"] = 0; - updateData["data.attributes.tech.points.min"] = 0; - updateData["data.attributes.tech.points.max"] = 0; - updateData["data.attributes.tech.points.temp"] = null; - updateData["data.attributes.tech.points.tempmax"] = null; - updateData["data.attributes.tech.level"] = 0; - } - - // If new Power F/T split data is not present, create it - const hasNewLimit = ad?.powers?.power1?.foverride !== undefined; - if ( !hasNewLimit ) { - for (let i = 1; i <= 9; i++) { - // add new - updateData["data.powers.power" + i + ".fvalue"] = getProperty(ad.powers,"power" + i + ".value"); - updateData["data.powers.power" + i + ".fmax"] = getProperty(ad.powers,"power" + i + ".max"); - updateData["data.powers.power" + i + ".foverride"] = null; - updateData["data.powers.power" + i + ".tvalue"] = getProperty(ad.powers,"power" + i + ".value"); - updateData["data.powers.power" + i + ".tmax"] = getProperty(ad.powers,"power" + i + ".max"); - updateData["data.powers.power" + i + ".toverride"] = null; - //remove old - updateData["data.powers.power" + i + ".-=value"] = null; - updateData["data.powers.power" + i + ".-=override"] = null; + // If new Force & Tech data is not present, create it + let hasNewAttrib = ad?.attributes?.force?.level !== undefined; + if (!hasNewAttrib) { + updateData["data.attributes.force.known.value"] = 0; + updateData["data.attributes.force.known.max"] = 0; + updateData["data.attributes.force.points.value"] = 0; + updateData["data.attributes.force.points.min"] = 0; + updateData["data.attributes.force.points.max"] = 0; + updateData["data.attributes.force.points.temp"] = null; + updateData["data.attributes.force.points.tempmax"] = null; + updateData["data.attributes.force.level"] = 0; + updateData["data.attributes.tech.known.value"] = 0; + updateData["data.attributes.tech.known.max"] = 0; + updateData["data.attributes.tech.points.value"] = 0; + updateData["data.attributes.tech.points.min"] = 0; + updateData["data.attributes.tech.points.max"] = 0; + updateData["data.attributes.tech.points.temp"] = null; + updateData["data.attributes.tech.points.tempmax"] = null; + updateData["data.attributes.tech.level"] = 0; } - } - // If new Bonus Power DC data is not present, create it - const hasNewBonus = ad?.bonuses?.power?.forceLightDC !== undefined; - if ( !hasNewBonus ) { - updateData["data.bonuses.power.forceLightDC"] = ""; - updateData["data.bonuses.power.forceDarkDC"] = ""; - updateData["data.bonuses.power.forceUnivDC"] = ""; - updateData["data.bonuses.power.techDC"] = ""; - } - // Remove the Power DC Bonus - updateData["data.bonuses.power.-=dc"] = null; + // If new Power F/T split data is not present, create it + const hasNewLimit = ad?.powers?.power1?.foverride !== undefined; + if (!hasNewLimit) { + for (let i = 1; i <= 9; i++) { + // add new + updateData["data.powers.power" + i + ".fvalue"] = getProperty(ad.powers, "power" + i + ".value"); + updateData["data.powers.power" + i + ".fmax"] = getProperty(ad.powers, "power" + i + ".max"); + updateData["data.powers.power" + i + ".foverride"] = null; + updateData["data.powers.power" + i + ".tvalue"] = getProperty(ad.powers, "power" + i + ".value"); + updateData["data.powers.power" + i + ".tmax"] = getProperty(ad.powers, "power" + i + ".max"); + updateData["data.powers.power" + i + ".toverride"] = null; + //remove old + updateData["data.powers.power" + i + ".-=value"] = null; + updateData["data.powers.power" + i + ".-=override"] = null; + } + } + // If new Bonus Power DC data is not present, create it + const hasNewBonus = ad?.bonuses?.power?.forceLightDC !== undefined; + if (!hasNewBonus) { + updateData["data.bonuses.power.forceLightDC"] = ""; + updateData["data.bonuses.power.forceDarkDC"] = ""; + updateData["data.bonuses.power.forceUnivDC"] = ""; + updateData["data.bonuses.power.techDC"] = ""; + } - return updateData + // Remove the Power DC Bonus + updateData["data.bonuses.power.-=dc"] = null; + + return updateData; } /* -------------------------------------------- */ @@ -433,112 +441,115 @@ function _migrateActorPowers(actorData, updateData) { * @private */ function _migrateActorSenses(actor, updateData) { - const ad = actor.data; - if ( ad?.traits?.senses === undefined ) return; - const original = ad.traits.senses || ""; - if ( typeof original !== "string" ) return; + const ad = actor.data; + if (ad?.traits?.senses === undefined) return; + const original = ad.traits.senses || ""; + if (typeof original !== "string") return; - // Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft" - const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/; - let wasMatched = false; + // Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft" + const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/; + let wasMatched = false; - // Match each comma-separated term - for ( let s of original.split(",") ) { - s = s.trim(); - const match = s.match(pattern); - if ( !match ) continue; - const type = match[1].toLowerCase(); - if ( type in CONFIG.SW5E.senses ) { - updateData[`data.attributes.senses.${type}`] = Number(match[2]).toNearest(0.5); - wasMatched = true; + // Match each comma-separated term + for (let s of original.split(",")) { + s = s.trim(); + const match = s.match(pattern); + if (!match) continue; + const type = match[1].toLowerCase(); + if (type in CONFIG.SW5E.senses) { + updateData[`data.attributes.senses.${type}`] = Number(match[2]).toNearest(0.5); + wasMatched = true; + } } - } - // If nothing was matched, but there was an old string - put the whole thing in "special" - if ( !wasMatched && !!original ) { - updateData["data.attributes.senses.special"] = original; - } + // If nothing was matched, but there was an old string - put the whole thing in "special" + if (!wasMatched && !!original) { + updateData["data.attributes.senses.special"] = original; + } - // Remove the old traits.senses string once the migration is complete - updateData["data.traits.-=senses"] = null; - return updateData; + // Remove the old traits.senses string once the migration is complete + updateData["data.traits.-=senses"] = null; + return updateData; } +/* -------------------------------------------- */ + /** * Migrate the actor details.type string to object * @private */ function _migrateActorType(actor, updateData) { - const ad = actor.data; - const original = ad.details?.type; - if ( typeof original !== "string" ) return; + const ad = actor.data; + const original = ad.details?.type; + if (typeof original !== "string") return; - // New default data structure - let data = { - "value": "", - "subtype": "", - "swarm": "", - "custom": "" - } + // New default data structure + let data = { + value: "", + subtype: "", + swarm: "", + custom: "" + }; - // Specifics - // (Some of these have weird names, these need to be addressed individually) - if (original === "force entity") { - data.value = "force"; - data.subtype = "storm"; - } else if (original === "human") { - data.value = "humanoid"; - data.subtype = "human"; - } else if (["humanoid (any)", "humanoid (Villainous"].includes(original)) { - data.value = "humanoid"; - } else if (original === "tree") { - data.value = "plant"; - data.subtype = "tree"; - } else if (original === "(humanoid) or Large (beast) force entity") { - data.value = "force"; - } else if (original === "droid (appears human)") { - data.value = "droid"; - } else { - // Match the existing string - const pattern = /^(?:swarm of (?[\w\-]+) )?(?[^(]+?)(?:\((?[^)]+)\))?$/i; - const match = original.trim().match(pattern); - if (match) { + // Specifics + // (Some of these have weird names, these need to be addressed individually) + if (original === "force entity") { + data.value = "force"; + data.subtype = "storm"; + } else if (original === "human") { + data.value = "humanoid"; + data.subtype = "human"; + } else if (["humanoid (any)", "humanoid (Villainous"].includes(original)) { + data.value = "humanoid"; + } else if (original === "tree") { + data.value = "plant"; + data.subtype = "tree"; + } else if (original === "(humanoid) or Large (beast) force entity") { + data.value = "force"; + } else if (original === "droid (appears human)") { + data.value = "droid"; + } else { + // Match the existing string + const pattern = /^(?:swarm of (?[\w\-]+) )?(?[^(]+?)(?:\((?[^)]+)\))?$/i; + const match = original.trim().match(pattern); + if (match) { + // Match a known creature type + const typeLc = match.groups.type.trim().toLowerCase(); + const typeMatch = Object.entries(CONFIG.SW5E.creatureTypes).find(([k, v]) => { + return ( + typeLc === k || + typeLc === game.i18n.localize(v).toLowerCase() || + typeLc === game.i18n.localize(`${v}Pl`).toLowerCase() + ); + }); + if (typeMatch) data.value = typeMatch[0]; + else { + data.value = "custom"; + data.custom = match.groups.type.trim().titleCase(); + } + data.subtype = match.groups.subtype?.trim().titleCase() || ""; - // Match a known creature type - const typeLc = match.groups.type.trim().toLowerCase(); - const typeMatch = Object.entries(CONFIG.SW5E.creatureTypes).find(([k, v]) => { - return (typeLc === k) || - (typeLc === game.i18n.localize(v).toLowerCase()) || - (typeLc === game.i18n.localize(`${v}Pl`).toLowerCase()); - }); - if (typeMatch) data.value = typeMatch[0]; - else { - data.value = "custom"; - data.custom = match.groups.type.trim().titleCase(); - } - data.subtype = match.groups.subtype?.trim().titleCase() || ""; + // Match a swarm + const isNamedSwarm = actor.name.startsWith(game.i18n.localize("SW5E.CreatureSwarm")); + if (match.groups.size || isNamedSwarm) { + const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny"; + const sizeMatch = Object.entries(CONFIG.SW5E.actorSizes).find(([k, v]) => { + return sizeLc === k || sizeLc === game.i18n.localize(v).toLowerCase(); + }); + data.swarm = sizeMatch ? sizeMatch[0] : "tiny"; + } else data.swarm = ""; + } - // Match a swarm - const isNamedSwarm = actor.name.startsWith(game.i18n.localize("SW5E.CreatureSwarm")); - if (match.groups.size || isNamedSwarm) { - const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny"; - const sizeMatch = Object.entries(CONFIG.SW5E.actorSizes).find(([k, v]) => { - return (sizeLc === k) || (sizeLc === game.i18n.localize(v).toLowerCase()); - }); - data.swarm = sizeMatch ? sizeMatch[0] : "tiny"; - } else data.swarm = ""; + // No match found + else { + data.value = "custom"; + data.custom = original; + } } - // No match found - else { - data.value = "custom"; - data.custom = original; - } - } - - // Update the actor data - updateData["data.details.type"] = data; - return updateData; + // Update the actor data + updateData["data.details.type"] = data; + return updateData; } /* -------------------------------------------- */ @@ -547,42 +558,41 @@ function _migrateActorType(actor, updateData) { * @private */ function _migrateItemClassPowerCasting(item, updateData) { - if (item.type === "class"){ - switch (item.name){ - case "Consular": - updateData["data.powercasting"] = { - progression: "consular", - ability: "" - }; - break; - case "Engineer": - - updateData["data.powercasting"] = { - progression: "engineer", - ability: "" - }; - break; - case "Guardian": - updateData["data.powercasting"] = { - progression: "guardian", - ability: "" - }; - break; - case "Scout": - updateData["data.powercasting"] = { - progression: "scout", - ability: "" - }; - break; - case "Sentinel": - updateData["data.powercasting"] = { - progression: "sentinel", - ability: "" - }; - break; + if (item.type === "class") { + switch (item.name) { + case "Consular": + updateData["data.powercasting"] = { + progression: "consular", + ability: "" + }; + break; + case "Engineer": + updateData["data.powercasting"] = { + progression: "engineer", + ability: "" + }; + break; + case "Guardian": + updateData["data.powercasting"] = { + progression: "guardian", + ability: "" + }; + break; + case "Scout": + updateData["data.powercasting"] = { + progression: "scout", + ability: "" + }; + break; + case "Sentinel": + updateData["data.powercasting"] = { + progression: "sentinel", + ability: "" + }; + break; + } } - } - return updateData; + return updateData; } /* -------------------------------------------- */ @@ -594,42 +604,45 @@ function _migrateItemClassPowerCasting(item, updateData) { * @private */ async function _migrateItemPower(item, actor, updateData) { - // if item is not a power shortcut out - if (item.type !== "power") return updateData; + // if item is not a power shortcut out + if (item.type !== "power") return updateData; - console.log(`Checking Actor ${actor.name}'s ${item.name} for migration needs`); - // check for flag.core, if not there is no compendium power so exit - const hasSource = item?.flags?.core?.sourceId !== undefined; - if (!hasSource) return updateData; + console.log(`Checking Actor ${actor.name}'s ${item.name} for migration needs`); + // check for flag.core, if not there is no compendium power so exit + const hasSource = item?.flags?.core?.sourceId !== undefined; + if (!hasSource) return updateData; - // shortcut out if dataVersion flag is set to 1.2.4 or higher - const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined; - if (hasDataVersion && (item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion))) return updateData; - - // Check to see what the source of Power is - const sourceId = item.flags.core.sourceId; - const coreSource = sourceId.substr(0, sourceId.length - 17); - const core_id = sourceId.substr(sourceId.length - 16, 16); - - //if power type is not force or tech exit out - let powerType = "none"; - if (coreSource === "Compendium.sw5e.forcepowers") powerType = "sw5e.forcepowers"; - if (coreSource === "Compendium.sw5e.techpowers") powerType = "sw5e.techpowers"; - if (powerType === "none") return updateData; + // shortcut out if dataVersion flag is set to 1.2.4 or higher + const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined; + if ( + hasDataVersion && + (item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion)) + ) + return updateData; + + // Check to see what the source of Power is + const sourceId = item.flags.core.sourceId; + const coreSource = sourceId.substr(0, sourceId.length - 17); + const core_id = sourceId.substr(sourceId.length - 16, 16); + + //if power type is not force or tech exit out + let powerType = "none"; + if (coreSource === "Compendium.sw5e.forcepowers") powerType = "sw5e.forcepowers"; + if (coreSource === "Compendium.sw5e.techpowers") powerType = "sw5e.techpowers"; + if (powerType === "none") return updateData; const corePower = duplicate(await game.packs.get(powerType).getEntity(core_id)); console.log(`Updating Actor ${actor.name}'s ${item.name} from compendium`); const corePowerData = corePower.data; // copy Core Power Data over original Power updateData["data"] = corePowerData; - updateData["flags"] = {"sw5e": {"dataVersion": "1.2.4"}}; + updateData["flags"] = {sw5e: {dataVersion: "1.2.4"}}; return updateData; - - - //game.packs.get(powerType).getEntity(core_id).then(corePower => { - //}) + //game.packs.get(powerType).getEntity(core_id).then(corePower => { + + //}) } /* -------------------------------------------- */ @@ -643,10 +656,10 @@ async function _migrateItemPower(item, actor, updateData) { * @private */ function _migrateItemAttunement(item, updateData) { - if ( item.data.data.attuned === undefined ) return updateData; - updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE; - updateData["data.-=attuned"] = null; - return updateData; + if (item.data?.attuned === undefined) return updateData; + updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE; + updateData["data.-=attuned"] = null; + return updateData; } /* -------------------------------------------- */ @@ -657,43 +670,41 @@ function _migrateItemAttunement(item, updateData) { * @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; - }) + 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.updateEntity(update, {recursive: false}); - console.log(`Purged flags from ${entity.name}`); - } - await pack.configure({locked: true}); + await pack.configure({locked: true}); } /* -------------------------------------------- */ - /** * Purge the data model of any inner objects which have been flagged as _deprecated. * @param {object} data The data to clean * @private */ export function removeDeprecatedObjects(data) { - for ( let [k, v] of Object.entries(data) ) { - if ( getType(v) === "Object" ) { - if (v._deprecated === true) { - console.log(`Deleting deprecated object key ${k}`); - delete data[k]; - } - else removeDeprecatedObjects(v); + for (let [k, v] of Object.entries(data)) { + if (getType(v) === "Object") { + if (v._deprecated === true) { + console.log(`Deleting deprecated object key ${k}`); + delete data[k]; + } else removeDeprecatedObjects(v); + } } - } - return data; + return data; } diff --git a/module/pixi/ability-template.js b/module/pixi/ability-template.js index d72cb886..6799eaec 100644 --- a/module/pixi/ability-template.js +++ b/module/pixi/ability-template.js @@ -1,134 +1,132 @@ -import { SW5E } from "../config.js"; +import {SW5E} from "../config.js"; /** * A helper class for building MeasuredTemplates for 5e powers and abilities * @extends {MeasuredTemplate} */ export default class AbilityTemplate extends MeasuredTemplate { + /** + * A factory method to create an AbilityTemplate instance using provided data from an Item5e instance + * @param {Item5e} item The Item object for which to construct the template + * @return {AbilityTemplate|null} The template object, or null if the item does not produce a template + */ + static fromItem(item) { + const target = getProperty(item.data, "data.target") || {}; + const templateShape = SW5E.areaTargetTypes[target.type]; + if (!templateShape) return null; - /** - * A factory method to create an AbilityTemplate instance using provided data from an Item5e instance - * @param {Item5e} item The Item object for which to construct the template - * @return {AbilityTemplate|null} The template object, or null if the item does not produce a template - */ - static fromItem(item) { - const target = getProperty(item.data, "data.target") || {}; - const templateShape = SW5E.areaTargetTypes[target.type]; - if ( !templateShape ) return null; + // Prepare template data + const templateData = { + t: templateShape, + user: game.user.data._id, + distance: target.value, + direction: 0, + x: 0, + y: 0, + fillColor: game.user.color + }; - // Prepare template data - const templateData = { - t: templateShape, - user: game.user.data._id, - distance: target.value, - direction: 0, - x: 0, - y: 0, - fillColor: game.user.color - }; + // Additional type-specific data + switch (templateShape) { + case "cone": + templateData.angle = CONFIG.MeasuredTemplate.defaults.angle; + break; + case "rect": // 5e rectangular AoEs are always cubes + templateData.distance = Math.hypot(target.value, target.value); + templateData.width = target.value; + templateData.direction = 45; + break; + case "ray": // 5e rays are most commonly 1 square (5 ft) in width + templateData.width = target.width ?? canvas.dimensions.distance; + break; + default: + break; + } - // Additional type-specific data - switch ( templateShape ) { - case "cone": - templateData.angle = CONFIG.MeasuredTemplate.defaults.angle; - break; - case "rect": // 5e rectangular AoEs are always cubes - templateData.distance = Math.hypot(target.value, target.value); - templateData.width = target.value; - templateData.direction = 45; - break; - case "ray": // 5e rays are most commonly 1 square (5 ft) in width - templateData.width = target.width ?? canvas.dimensions.distance; - break; - default: - break; + // Return the template constructed from the item data + const cls = CONFIG.MeasuredTemplate.documentClass; + const template = new cls(templateData, {parent: canvas.scene}); + const object = new this(template); + object.item = item; + object.actorSheet = item.actor?.sheet || null; + return object; } - // Return the template constructed from the item data - const cls = CONFIG.MeasuredTemplate.documentClass; - const template = new cls(templateData, {parent: canvas.scene}); - const object = new this(template); - object.item = item; - object.actorSheet = item.actor?.sheet || null; - return object; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Creates a preview of the power template + */ + drawPreview() { + const initialLayer = canvas.activeLayer; - /** - * Creates a preview of the power template - */ - drawPreview() { - const initialLayer = canvas.activeLayer; + // Draw the template and switch to the template layer + this.draw(); + this.layer.activate(); + this.layer.preview.addChild(this); - // Draw the template and switch to the template layer - this.draw(); - this.layer.activate(); - this.layer.preview.addChild(this); + // Hide the sheet that originated the preview + if (this.actorSheet) this.actorSheet.minimize(); - // Hide the sheet that originated the preview - if ( this.actorSheet ) this.actorSheet.minimize(); + // Activate interactivity + this.activatePreviewListeners(initialLayer); + } - // Activate interactivity - this.activatePreviewListeners(initialLayer); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Activate listeners for the template preview + * @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete + */ + activatePreviewListeners(initialLayer) { + const handlers = {}; + let moveTime = 0; - /** - * Activate listeners for the template preview - * @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete - */ - activatePreviewListeners(initialLayer) { - const handlers = {}; - let moveTime = 0; + // Update placement (mouse-move) + handlers.mm = (event) => { + event.stopPropagation(); + let now = Date.now(); // Apply a 20ms throttle + if (now - moveTime <= 20) return; + const center = event.data.getLocalPosition(this.layer); + const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2); + this.data.update({x: snapped.x, y: snapped.y}); + this.refresh(); + moveTime = now; + }; - // Update placement (mouse-move) - handlers.mm = event => { - event.stopPropagation(); - let now = Date.now(); // Apply a 20ms throttle - if ( now - moveTime <= 20 ) return; - const center = event.data.getLocalPosition(this.layer); - const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2); - this.data.x = snapped.x; - this.data.y = snapped.y; - this.refresh(); - moveTime = now; - }; + // Cancel the workflow (right-click) + handlers.rc = (event) => { + this.layer.preview.removeChildren(); + canvas.stage.off("mousemove", handlers.mm); + canvas.stage.off("mousedown", handlers.lc); + canvas.app.view.oncontextmenu = null; + canvas.app.view.onwheel = null; + initialLayer.activate(); + this.actorSheet.maximize(); + }; - // Cancel the workflow (right-click) - handlers.rc = event => { - this.layer.preview.removeChildren(); - canvas.stage.off("mousemove", handlers.mm); - canvas.stage.off("mousedown", handlers.lc); - canvas.app.view.oncontextmenu = null; - canvas.app.view.onwheel = null; - initialLayer.activate(); - this.actorSheet.maximize(); - }; + // Confirm the workflow (left-click) + handlers.lc = (event) => { + handlers.rc(event); + const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2); + this.data.update(destination); + canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]); + }; - // Confirm the workflow (left-click) - handlers.lc = event => { - handlers.rc(event); - const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2); - this.data.update(destination); - canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]); - }; + // Rotate the template by 3 degree increments (mouse-wheel) + handlers.mw = (event) => { + if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window + event.stopPropagation(); + let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15; + let snap = event.shiftKey ? delta : 5; + this.data.update({direction: this.data.direction + snap * Math.sign(event.deltaY)}); + this.refresh(); + }; - // Rotate the template by 3 degree increments (mouse-wheel) - handlers.mw = event => { - if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window - event.stopPropagation(); - let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15; - let snap = event.shiftKey ? delta : 5; - this.data.update({direction: this.data.direction + (snap * Math.sign(event.deltaY))}); - this.refresh(); - }; - - // Activate listeners - canvas.stage.on("mousemove", handlers.mm); - canvas.stage.on("mousedown", handlers.lc); - canvas.app.view.oncontextmenu = handlers.rc; - canvas.app.view.onwheel = handlers.mw; - } + // Activate listeners + canvas.stage.on("mousemove", handlers.mm); + canvas.stage.on("mousedown", handlers.lc); + canvas.app.view.oncontextmenu = handlers.rc; + canvas.app.view.onwheel = handlers.mw; + } } diff --git a/module/settings.js b/module/settings.js index 2adde8d3..3b765c69 100644 --- a/module/settings.js +++ b/module/settings.js @@ -1,145 +1,144 @@ -export const registerSystemSettings = function() { +export const registerSystemSettings = function () { + /** + * Track the system version upon which point a migration was last applied + */ + game.settings.register("sw5e", "systemMigrationVersion", { + name: "System Migration Version", + scope: "world", + config: false, + type: String, + default: game.system.data.version + }); - /** - * Track the system version upon which point a migration was last applied - */ - game.settings.register("sw5e", "systemMigrationVersion", { - name: "System Migration Version", - scope: "world", - config: false, - type: String, - default: game.system.data.version - }); + /** + * Register resting variants + */ + game.settings.register("sw5e", "restVariant", { + name: "SETTINGS.5eRestN", + hint: "SETTINGS.5eRestL", + scope: "world", + config: true, + default: "normal", + type: String, + choices: { + normal: "SETTINGS.5eRestPHB", + gritty: "SETTINGS.5eRestGritty", + epic: "SETTINGS.5eRestEpic" + } + }); - /** - * Register resting variants - */ - game.settings.register("sw5e", "restVariant", { - name: "SETTINGS.5eRestN", - hint: "SETTINGS.5eRestL", - scope: "world", - config: true, - default: "normal", - type: String, - choices: { - "normal": "SETTINGS.5eRestPHB", - "gritty": "SETTINGS.5eRestGritty", - "epic": "SETTINGS.5eRestEpic", - } - }); + /** + * Register diagonal movement rule setting + */ + game.settings.register("sw5e", "diagonalMovement", { + name: "SETTINGS.5eDiagN", + hint: "SETTINGS.5eDiagL", + scope: "world", + config: true, + default: "555", + type: String, + choices: { + 555: "SETTINGS.5eDiagPHB", + 5105: "SETTINGS.5eDiagDMG", + EUCL: "SETTINGS.5eDiagEuclidean" + }, + onChange: (rule) => (canvas.grid.diagonalRule = rule) + }); - /** - * Register diagonal movement rule setting - */ - game.settings.register("sw5e", "diagonalMovement", { - name: "SETTINGS.5eDiagN", - hint: "SETTINGS.5eDiagL", - scope: "world", - config: true, - default: "555", - type: String, - choices: { - "555": "SETTINGS.5eDiagPHB", - "5105": "SETTINGS.5eDiagDMG", - "EUCL": "SETTINGS.5eDiagEuclidean", - }, - onChange: rule => canvas.grid.diagonalRule = rule - }); + /** + * Register Initiative formula setting + */ + game.settings.register("sw5e", "initiativeDexTiebreaker", { + name: "SETTINGS.5eInitTBN", + hint: "SETTINGS.5eInitTBL", + scope: "world", + config: true, + default: false, + type: Boolean + }); - /** - * Register Initiative formula setting - */ - game.settings.register("sw5e", "initiativeDexTiebreaker", { - name: "SETTINGS.5eInitTBN", - hint: "SETTINGS.5eInitTBL", - scope: "world", - config: true, - default: false, - type: Boolean - }); + /** + * Require Currency Carrying Weight + */ + game.settings.register("sw5e", "currencyWeight", { + name: "SETTINGS.5eCurWtN", + hint: "SETTINGS.5eCurWtL", + scope: "world", + config: true, + default: true, + type: Boolean + }); - /** - * Require Currency Carrying Weight - */ - game.settings.register("sw5e", "currencyWeight", { - name: "SETTINGS.5eCurWtN", - hint: "SETTINGS.5eCurWtL", - scope: "world", - config: true, - default: true, - type: Boolean - }); + /** + * Option to disable XP bar for session-based or story-based advancement. + */ + game.settings.register("sw5e", "disableExperienceTracking", { + name: "SETTINGS.5eNoExpN", + hint: "SETTINGS.5eNoExpL", + scope: "world", + config: true, + default: false, + type: Boolean + }); - /** - * Option to disable XP bar for session-based or story-based advancement. - */ - game.settings.register("sw5e", "disableExperienceTracking", { - name: "SETTINGS.5eNoExpN", - hint: "SETTINGS.5eNoExpL", - scope: "world", - config: true, - default: false, - type: Boolean, - }); + /** + * Option to automatically collapse Item Card descriptions + */ + game.settings.register("sw5e", "autoCollapseItemCards", { + name: "SETTINGS.5eAutoCollapseCardN", + hint: "SETTINGS.5eAutoCollapseCardL", + scope: "client", + config: true, + default: false, + type: Boolean, + onChange: (s) => { + ui.chat.render(); + } + }); - /** - * Option to automatically collapse Item Card descriptions - */ - game.settings.register("sw5e", "autoCollapseItemCards", { - name: "SETTINGS.5eAutoCollapseCardN", - hint: "SETTINGS.5eAutoCollapseCardL", - scope: "client", - config: true, - default: false, - type: Boolean, - onChange: s => { - ui.chat.render(); - } - }); + /** + * Option to allow GMs to restrict polymorphing to GMs only. + */ + game.settings.register("sw5e", "allowPolymorphing", { + name: "SETTINGS.5eAllowPolymorphingN", + hint: "SETTINGS.5eAllowPolymorphingL", + scope: "world", + config: true, + default: false, + type: Boolean + }); - /** - * Option to allow GMs to restrict polymorphing to GMs only. - */ - game.settings.register('sw5e', 'allowPolymorphing', { - name: 'SETTINGS.5eAllowPolymorphingN', - hint: 'SETTINGS.5eAllowPolymorphingL', - scope: 'world', - config: true, - default: false, - type: Boolean - }); - - /** - * Remember last-used polymorph settings. - */ - game.settings.register('sw5e', 'polymorphSettings', { - scope: 'client', - default: { - keepPhysical: false, - keepMental: false, - keepSaves: false, - keepSkills: false, - mergeSaves: false, - mergeSkills: false, - keepClass: false, - keepFeats: false, - keepPowers: false, - keepItems: false, - keepBio: false, - keepVision: true, - transformTokens: true - } - }); - game.settings.register("sw5e", "colorTheme", { - name: "SETTINGS.SWColorN", - hint: "SETTINGS.SWColorL", - scope: "world", - config: true, - default: "light", - type: String, - choices: { - "light": "SETTINGS.SWColorLight", - "dark": "SETTINGS.SWColorDark" - } - }); + /** + * Remember last-used polymorph settings. + */ + game.settings.register("sw5e", "polymorphSettings", { + scope: "client", + default: { + keepPhysical: false, + keepMental: false, + keepSaves: false, + keepSkills: false, + mergeSaves: false, + mergeSkills: false, + keepClass: false, + keepFeats: false, + keepPowers: false, + keepItems: false, + keepBio: false, + keepVision: true, + transformTokens: true + } + }); + game.settings.register("sw5e", "colorTheme", { + name: "SETTINGS.SWColorN", + hint: "SETTINGS.SWColorL", + scope: "world", + config: true, + default: "light", + type: String, + choices: { + light: "SETTINGS.SWColorLight", + dark: "SETTINGS.SWColorDark" + } + }); }; diff --git a/module/templates.js b/module/templates.js index a27bdf71..e810b6da 100644 --- a/module/templates.js +++ b/module/templates.js @@ -3,34 +3,33 @@ * Pre-loaded templates are compiled and cached for fast access when rendering * @return {Promise} */ -export const preloadHandlebarsTemplates = async function() { - return loadTemplates([ +export const preloadHandlebarsTemplates = async function () { + return loadTemplates([ + // Shared Partials + "systems/sw5e/templates/actors/parts/active-effects.html", - // Shared Partials - "systems/sw5e/templates/actors/parts/active-effects.html", + // Actor Sheet Partials + "systems/sw5e/templates/actors/oldActor/parts/actor-traits.html", + "systems/sw5e/templates/actors/oldActor/parts/actor-inventory.html", + "systems/sw5e/templates/actors/oldActor/parts/actor-features.html", + "systems/sw5e/templates/actors/oldActor/parts/actor-powerbook.html", + "systems/sw5e/templates/actors/oldActor/parts/actor-notes.html", - // Actor Sheet Partials - "systems/sw5e/templates/actors/oldActor/parts/actor-traits.html", - "systems/sw5e/templates/actors/oldActor/parts/actor-inventory.html", - "systems/sw5e/templates/actors/oldActor/parts/actor-features.html", - "systems/sw5e/templates/actors/oldActor/parts/actor-powerbook.html", - "systems/sw5e/templates/actors/oldActor/parts/actor-notes.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-biography.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-core.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-crew.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-features.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-force-powerbook.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-tech-powerbook.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-resources.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-traits.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-biography.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-core.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-crew.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-features.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-force-powerbook.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-tech-powerbook.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-resources.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-traits.html", - - // Item Sheet Partials - "systems/sw5e/templates/items/parts/item-action.html", - "systems/sw5e/templates/items/parts/item-activation.html", - "systems/sw5e/templates/items/parts/item-description.html", - "systems/sw5e/templates/items/parts/item-mountable.html" - ]); + // Item Sheet Partials + "systems/sw5e/templates/items/parts/item-action.html", + "systems/sw5e/templates/items/parts/item-activation.html", + "systems/sw5e/templates/items/parts/item-description.html", + "systems/sw5e/templates/items/parts/item-mountable.html" + ]); }; diff --git a/module/token.js b/module/token.js index db811603..873372cc 100644 --- a/module/token.js +++ b/module/token.js @@ -3,11 +3,10 @@ * @extends {TokenDocument} */ export class TokenDocument5e extends TokenDocument { - /** @inheritdoc */ getBarAttribute(...args) { const data = super.getBarAttribute(...args); - if ( data && (data.attribute === "attributes.hp") ) { + if (data && data.attribute === "attributes.hp") { data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0); data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0); } @@ -15,19 +14,16 @@ export class TokenDocument5e extends TokenDocument { } } - /* -------------------------------------------- */ - /** * Extend the base Token class to implement additional system-specific logic. * @extends {Token} */ export class Token5e extends Token { - /** @inheritdoc */ _drawBar(number, bar, data) { - if ( data.attribute === "attributes.hp" ) return this._drawHPBar(number, bar, data); + if (data.attribute === "attributes.hp") return this._drawHPBar(number, bar, data); return super._drawBar(number, bar, data); } @@ -41,7 +37,6 @@ export class Token5e extends Token { * @private */ _drawHPBar(number, bar, data) { - // Extract health data let {value, max, temp, tempmax} = this.document.actor.data.data.attributes.hp; temp = Number(temp || 0); @@ -58,42 +53,50 @@ export class Token5e extends Token { // Determine colors to use const blk = 0x000000; - const hpColor = PIXI.utils.rgb2hex([(1-(colorPct/2)), colorPct, 0]); + const hpColor = PIXI.utils.rgb2hex([1 - colorPct / 2, colorPct, 0]); const c = CONFIG.SW5E.tokenHPColors; // Determine the container size (logic borrowed from core) const w = this.w; - let h = Math.max((canvas.dimensions.size / 12), 8); - if ( this.data.height >= 2 ) h *= 1.6; + let h = Math.max(canvas.dimensions.size / 12, 8); + if (this.data.height >= 2) h *= 1.6; const bs = Math.clamped(h / 8, 1, 2); - const bs1 = bs+1; + const bs1 = bs + 1; // Overall bar container - bar.clear() + bar.clear(); bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3); // Temporary maximum HP if (tempmax > 0) { const pct = max / effectiveMax; - bar.beginFill(c.tempmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2); + bar.beginFill(c.tempmax, 1.0) + .lineStyle(1, blk, 1.0) + .drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2); } // Maximum HP penalty else if (tempmax < 0) { const pct = (max + tempmax) / max; - bar.beginFill(c.negmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2); + bar.beginFill(c.negmax, 1.0) + .lineStyle(1, blk, 1.0) + .drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2); } // Health bar - bar.beginFill(hpColor, 1.0).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, valuePct*w, h, 2) + bar.beginFill(hpColor, 1.0) + .lineStyle(bs, blk, 1.0) + .drawRoundedRect(0, 0, valuePct * w, h, 2); // Temporary hit points - if ( temp > 0 ) { - bar.beginFill(c.temp, 1.0).lineStyle(0).drawRoundedRect(bs1, bs1, (tempPct*w)-(2*bs1), h-(2*bs1), 1); + if (temp > 0) { + bar.beginFill(c.temp, 1.0) + .lineStyle(0) + .drawRoundedRect(bs1, bs1, tempPct * w - 2 * bs1, h - 2 * bs1, 1); } // Set position - let posY = (number === 0) ? (this.h - h) : 0; + let posY = number === 0 ? this.h - h : 0; bar.position.set(0, posY); } } diff --git a/package-lock.json b/package-lock.json index 759e9b50..b2fbf9ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1266,9 +1266,9 @@ } }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" }, "image-size": { "version": "0.5.5", @@ -3068,9 +3068,9 @@ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==" }, "yargs": { "version": "7.1.1", diff --git a/packs/Icons/Archetypes/Teras Kasi Order.webp b/packs/Icons/Archetypes/Teras Kasi Order.webp new file mode 100644 index 00000000..3f0a1585 Binary files /dev/null and b/packs/Icons/Archetypes/Teras Kasi Order.webp differ diff --git a/packs/Icons/Force Powers/Defensive Technique.webp b/packs/Icons/Force Powers/Defensive Technique.webp new file mode 100644 index 00000000..6736a7c0 Binary files /dev/null and b/packs/Icons/Force Powers/Defensive Technique.webp differ diff --git a/packs/Icons/Force Powers/Kinetite.webp b/packs/Icons/Force Powers/Kinetite.webp new file mode 100644 index 00000000..c8936b27 Binary files /dev/null and b/packs/Icons/Force Powers/Kinetite.webp differ diff --git a/packs/Icons/Force Powers/Tapas.webp b/packs/Icons/Force Powers/Tapas.webp new file mode 100644 index 00000000..14681378 Binary files /dev/null and b/packs/Icons/Force Powers/Tapas.webp differ diff --git a/packs/Icons/Force Powers/Telepathic Link.webp b/packs/Icons/Force Powers/Telepathic Link.webp new file mode 100644 index 00000000..4f1383fc Binary files /dev/null and b/packs/Icons/Force Powers/Telepathic Link.webp differ diff --git a/packs/Icons/Force Powers/Wakefulness.webp b/packs/Icons/Force Powers/Wakefulness.webp new file mode 100644 index 00000000..9aeec48a Binary files /dev/null and b/packs/Icons/Force Powers/Wakefulness.webp differ diff --git a/packs/Icons/Martial Blasters/Shoulder Cannon.webp b/packs/Icons/Martial Blasters/Shoulder Cannon.webp new file mode 100644 index 00000000..ec849135 Binary files /dev/null and b/packs/Icons/Martial Blasters/Shoulder Cannon.webp differ diff --git a/packs/Icons/Simple Vibroweapons/Unarmed Strike.png b/packs/Icons/Simple Vibroweapons/Unarmed Strike.png new file mode 100644 index 00000000..4b50289b Binary files /dev/null and b/packs/Icons/Simple Vibroweapons/Unarmed Strike.png differ diff --git a/packs/Icons/Tech Powers/Aid Droid.webp b/packs/Icons/Tech Powers/Aid Droid.webp new file mode 100644 index 00000000..a8433e01 Binary files /dev/null and b/packs/Icons/Tech Powers/Aid Droid.webp differ diff --git a/packs/Icons/Tech Powers/Alter Self.webp b/packs/Icons/Tech Powers/Alter Self.webp new file mode 100644 index 00000000..a8433e01 Binary files /dev/null and b/packs/Icons/Tech Powers/Alter Self.webp differ diff --git a/packs/Icons/Tech Powers/Mutate-Augment.webp b/packs/Icons/Tech Powers/Mutate-Augment.webp new file mode 100644 index 00000000..a8433e01 Binary files /dev/null and b/packs/Icons/Tech Powers/Mutate-Augment.webp differ diff --git a/packs/Icons/Tech Powers/Tri-Shot.webp b/packs/Icons/Tech Powers/Tri-Shot.webp new file mode 100644 index 00000000..a8433e01 Binary files /dev/null and b/packs/Icons/Tech Powers/Tri-Shot.webp differ diff --git a/packs/packs/adventuringgear.db b/packs/packs/adventuringgear.db index 18aedea4..1e4f8991 100644 --- a/packs/packs/adventuringgear.db +++ b/packs/packs/adventuringgear.db @@ -52,7 +52,7 @@ {"name":"Traz","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"tool","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":6,"price":300,"attuned":false,"equipped":false,"rarity":"","identified":true,"ability":"int","chatFlavor":"","proficient":0,"attributes":{"spelldc":10},"damage":{"parts":[]}},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false,"effects":[]}},"img":"systems/sw5e/packs/Icons/Musical%20Instrument/Traz.webp","_id":"UQu4duMtxYEXKAbo"} {"name":"Tent, two-person","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"loot","data":{"description":{"value":"","chat":"","unidentified":""},"source":"","quantity":1,"weight":5,"price":20,"attuned":false,"equipped":false,"rarity":"","identified":true,"attributes":{"spelldc":10},"damage":{"parts":[]}},"flags":{},"img":"systems/sw5e/packs/Icons/Utility/Tent.webp","_id":"UxL0trd3omeqzBk4"} {"name":"Homing Beacon","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"loot","data":{"description":{"value":"

A homing beacon is a device used to track starships or any other entity being transported. Homing beacons transmit using non-mass HoloNet transceivers able to be tracked through hyperspace. Homing beacons are small enough that they can easily be hidden inside a ship, or tucked into some crevice on its exterior.

","chat":"","unidentified":""},"source":"","quantity":1,"weight":1,"price":450,"attuned":false,"equipped":false,"rarity":"","identified":true,"attributes":{"spelldc":10},"damage":{"parts":[]}},"flags":{},"img":"systems/sw5e/packs/Icons/Utility/Homing%20Beacon.webp","_id":"V2hSxkLfq461mvNz"} -{"name":"Power Cell","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"consumable","data":{"description":{"value":"

Power cells fuel blaster weapons that deal energy or ion damage. Additionally, power cells are used to energize certain tools.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":10,"attuned":false,"equipped":false,"rarity":"","identified":true,"activation": {"type": "none","cost": null,"condition": ""},"duration": {"value": null,"units": ""},"target": {"value": null,"width": null,"units": "","type": ""},range":{"value": null,"long": null,"units": ""},"uses": {"value": 100,"max": "100","per": "charges","autoDestroy": false},"consume": {"type": "","target": "","amount": null},"ability": null,"actionType": "","attackBonus": 0,"chatFlavor": "","critical": null,"damage": {"parts": [],"versatile": ""},"formula": "","save": {"ability": "","dc": null,"scaling": "spell"},"consumableType": "ammo","attributes": {"spelldc": 10},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false,"effects":[]}},"img":"systems/sw5e/packs/Icons/Ammunition/Power%20Cell.webp","_id":"VUkO1T2aYMuUcBZM"} +{"name":"Power Cell","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"consumable","data":{"description":{"value":"

Power cells fuel blaster weapons that deal energy or ion damage. Additionally, power cells are used to energize certain tools.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":10,"attuned":false,"equipped":false,"rarity":"","identified":true,"activation": {"type": "none","cost": null,"condition": ""},"duration": {"value": null,"units": ""},"target": {"value": null,"width": null,"units": "","type": ""},"range":{"value": null,"long": null,"units": ""},"uses": {"value": 100,"max": "100","per": "charges","autoDestroy": false},"consume": {"type": "","target": "","amount": null},"ability": null,"actionType": "","attackBonus": 0,"chatFlavor": "","critical": null,"damage": {"parts": [],"versatile": ""},"formula": "","save": {"ability": "","dc": null,"scaling": "spell"},"consumableType": "ammo","attributes": {"spelldc": 10}},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false,"effects":[]}},"img":"systems/sw5e/packs/Icons/Ammunition/Power%20Cell.webp","_id":"VUkO1T2aYMuUcBZM"} {"name":"Propulsion pack","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"loot","data":{"description":{"value":"

Propulsion packs enhance underwater movement. Activating or deactivating the propulsion pack requires a bonus action and, while active, you have a swimming speed of 30 feet. The propulsion pack lasts for 1 minute per power cell (to a maximum of 10 minutes) and can be recharged by a power source or replacing the power cells.

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":20,"price":400,"attuned":false,"equipped":false,"rarity":"","identified":true,"attributes":{"spelldc":10},"damage":{"parts":[]}},"flags":{},"img":"systems/sw5e/packs/Icons/Weapon%20or%20Armor%20Accessory/Propulsion%20Pack.webp","_id":"XR1obpDj1PqDLfA8"} {"name":"Emergency Battery","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"consumable","data":{"description":{"value":"

All non-expendable droids need recharging as they are used. The battery has ten uses. As an action, you can expend one use of the kit to stabilize a droid that has 0 hit points, without needing to make an Intelligence (Technology) check.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":5,"price":70,"attuned":false,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":""},"uses":{"value":10,"max":10,"per":"charges","autoDestroy":true},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"Stabilize Droid","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"spell"},"consumableType":"potion","attributes":{"spelldc":10}},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false,"effects":[]}},"img":"systems/sw5e/packs/Icons/Medical/Emergency%20Battery.webp","_id":"Z0YM3aYCyCRhL6cx"} {"name":"Smugglepack","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"backpack","data":{"description":{"value":"

This backpack comes with a main compartment that can store up to 15 lb., not exceeding a volume of 1/2 cubic foot. Additionally, it has a hidden storage compartment that can hold up to 5 lb, not exceeding a volume of 1/4 cubic foot. Finding the hidden compartment requires a DC 15 Investigation check.

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":400,"attuned":false,"equipped":false,"rarity":"","identified":true,"capacity":{"type":"weight","value":20,"weightless":false},"currency":{"cp":0,"sp":0,"ep":0,"gp":0,"pp":0},"attributes":{"spelldc":10},"damage":{"parts":[]}},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false,"effects":[]}},"img":"systems/sw5e/packs/Icons/Storage/Smugglerpack.webp","_id":"Zlj5z56A4oVQ5iEC"} diff --git a/packs/packs/archetypes.db b/packs/packs/archetypes.db index 594e6a44..f4a4c086 100644 --- a/packs/packs/archetypes.db +++ b/packs/packs/archetypes.db @@ -11,6 +11,7 @@ {"_id":"6HRxOPMOmOiKmuIG","name":"Adept Specialist","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Fighter","description":{"value":"

Adept Specialist

\n

Those fighters who choose to become Adept Specialists tap into a latent Force-sensitivity to augment their martial prowess, blending the two to accelerate their bodies and blows. An adept speeds across the battlefield, attacking opponents in a flurry of blows before dashing off again.

\n

Forcecasting

\n

When you choose this specialty at 3rd level, you have learned powers, fragments of knowledge that imbue you with an abiding force ability. See chapter 10 for the general rules of forcecasting and chapter 11 for the force powers list.

\n

Force Powers Known

\n

You learn 4 force powers of your choice, and you learn more at higher levels, as shown in the Force Powers Known column of the Adept Specialist Forcecasting table. You may not learn a force power of a level higher than your Max Power Level, and you may learn a force power at the same time you learn its prerequisite.

\n

Force Points

\n

You have a number of force points equal to your fighter level, as shown in the Force Points column of the Adept Specialist Forcecasting table, + your Wisdom or Charisma modifier (your choice). You use these force points to cast force powers. You regain all expended force points when you finish a long rest.

\n

Max Power Level

\n

Many force powers can be overpowered, consuming more force points to create a greater effect. You can overpower these abilities to a maximum level, which increases at higher levels, as shown in the Max Power Level column of the Adept Specialist Forcecasting table.

\n

You may only cast force powers at 4th-level once. You regain the ability to do so after a long rest.

\n

Forcecasting Ability

\n

Your forcecasting ability varies based on the alignment of the powers you cast. You use your Wisdom for light side powers, Charisma for dark side powers, and Wisdom or Charisma for universal powers (your choice). You use this ability score modifier whenever a power refers to your forcecasting ability. In addition, you use this ability score modifier when setting the saving throw DC for a force power you cast and when making an attack roll with one.

\n

Force save DC = 8 + your proficiency bonus + your forcecasting ability modifier

\n

Force attack modifier = your proficiency bonus + your forcecasting ability modifier

\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Adept Specialist Forcecasting
LevelForce Powers KnownForce PointsMax Power Level
3rd431st
4th641st
5th751st
6th861st
7th1072nd
8th1182nd
9th1292nd
10th13102nd
11th14112nd
12th15122nd
13th17133rd
14th18143rd
15th19153rd
16th20163rd
17th22174th
18th23184th
19th24194th
20th25204th
\n
\n
\n

Growing Momentum

\n

Also at 3rd level, you can cast the @Compendium[sw5e.forcepowers.tAs3ogXXwpDkpZvF]{Burst of Speed} force power targeting yourself at 1st-level without expending force points. At 10th level, when you do so, your speed increases by an additional 10 feet. At 18th level, when you do so, your speed increases by an additional 10 feet.

\n

You can use this feature a number of times equal to your Wisdom or Charisma modifier (your choice, a minimum of once). You regain any expended uses when you finish a long rest.

\n

Whirling Weapons

\n

Beginning at 7th level, your constant blur of motion and attacks becomes an unending barrage as you build momentum. Once on your turn when you miss with a weapon attack you can make another weapon attack, no action required.

\n

Focused Breathing

\n

At 10th level, you learn to recover some of your expended power quickly. When you use your Second Wind you also regain a number of force points equal to your Wisdom or Charisma modifier (your choice, a minimum of one).

\n

Unstoppable Force

\n

Starting at 15th level, you learn to completely ignore many of the most devastating impediments of combat. You can expend a use of Indomitable to gain the effect of the @Compendium[sw5e.forcepowers.BqGw2Mt7mBqqVclc]{Freedom of Movement} force power until the end of your next turn.

\n

Instant Acceleration

\n

At 18th level you reach the pinnacle of your training, moving faster than eyes or most sensors can track. When you use Action Surge feature, you can teleport up to 30 feet to an unoccupied space you can see. You can teleport before or after the additional action.

"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":"Forcecaster"},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Adept%20Specialist.webp","effects":[]} {"_id":"6JrXY2jDqQiuuUGq","name":"Way of Negation","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Consular","description":{"value":"

Way of Negation

\n

Some force users seek mastery over the fundamentals of energy manipulation, known as tutaminis. Those consulars who follow the Way of Negation harness this power to limit the havoc that other force-wielders might wreak.

\n

Force Deflection

\n

At 3rd level, when you fail a saving throw, you can use your reaction to gain a +4 bonus to that saving throw.

\n

You can use this feature a number of times equal to your Wisdom or Charisma modifier (your choice, a minimum of once). You regain all expended uses when you finish a short or long rest.

\n

Power Surge

\n

Starting at 6th level, you learn to simultaneously limit a creature's force powers and store that power within yourself to later strengthen your damaging force powers.

\n

You can store a maximum number of power surges equal to your Wisdom or Charisma modifier (your choice, minimum of one). Whenever you successfully end a force power with a power such as @Compendium[sw5e.forcepowers.XYHAKmU4gHSzRK3I]{Force Suppression} or @Compendium[sw5e.forcepowers.LhMmIUjsFrytp0wc]{Sever Force}, or use your Force Shield or Force Deflection features to successfully avoid an attack or succeed on a saving throw, you gain one power surge, as you redirect the flow of the Force into yourself.

\n

Once per turn, when you deal damage to a creature or object with a force power, you can spend one power surge to deal extra damage to that target. The extra damage is of the same type as the power's damage, and it equals half your consular level (rounded down).

\n

Whenever you finish a long rest, your number of power surges resets to one. If you end a short rest with no power surges, you gain one power surge.

\n

Enduring Focus

\n

At 10th level, you can casually deflect attacks while channeling your power. While you are concentrating on a Force power, you have a +2 bonus to your AC and all saving throws.

\n

Additionally, you can extend your Force Deflection to a creature within 5 feet of you when they fail a saving throw.

\n

Conflux

\n

At 14th level, when you use your Force Deflection feature, you can cause a ripple in the Force to expand from you. Up to three creatures of your choice that you can see within 60 feet of you each take force damage equal to half your consular level.

\n

Tutaminis Mastery

\n

At 18th level, when you use a Force-Empowered Casting option, you can spend a power surge to use it without spending additional force points.

"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":"Forcecaster"},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Way%20of%20Negation.webp","effects":[]} {"_id":"7OqolLOiiu5K59rD","name":"Blademaster Specialist","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Fighter","description":{"value":"

Blademaster Specialist

\n

Those fighters who choose to become Blademaster Specialists hone their focus to a blade's edge, becoming so in tune with their arsenal that it becomes both their weapon and their armor.

\n

Unarmored Defense

\n

When you choose this specialty at 3rd level, while you are wearing no armor and not wielding a shield, your AC equals 10 + your Dexterity modifier + your Strength modifier.

\n

Adaptive Fighting

\n

Also at 3rd level, you've learned to make adaptations to your fighting style on the fly. You have three such effects: Change Up, Draw, and Stow. When you use your Adaptive Fighting, you choose which effect to create.

\n

You can use this features a number of times equal to your Strength or Dexterity modifier (your choice, a minimum of once). You regain all expended uses when you complete a short or long rest.

\n

Change Up

\n

You can use your object interaction and expend a use of your Adaptive Fighting to change the Fighting Style option granted to you by your fighter class feature. You can't take a Fighting Style option more than once.

\n

Draw

\n

When you use your object interaction to draw one or more weapons, you can expend a use of your Adaptive Fighting (no action required) to make your weapon attacks with the weapon(s) score a critical hit on a roll of 19 or 20 until the start of your next turn.

\n

Stow

\n

When you use your object interaction to stow a weapon, you can expend a use of your Adaptive Fighting (no action required) to take the Disengage action.

\n

Dervish

\n

Beginning at 7th level, when you score a critical hit with a melee weapon attack, you regain a use of your Adaptive Fighting, to a maximum of your Strength or Dexterity modifier (your choice, minimum of one).

\n

Resilient Fighting

\n

At 10th level, when you expend a use of your Adaptive Fighting, you gain resistance to energy and kinetic damage dealt by weapons until the start of your next turn.

\n

Adrenaline Rush

\n

Starting at 15th level, when you use your Action Surge feature, you can take an extra bonus action on top of the additional action.

\n

Bladestorm

\n

At 18th level, you can use your action to make a single melee weapon attack against each creature within your reach. Make a separate attack roll against each target. The first attack gains a +1 bonus to its attack roll, and each attack after the first gains an additional +1 bonus to its attack roll, cumulatively, to a maximum bonus of +6. If you are wielding two light- or vibro-weapons, or a weapon with the double property, you also add this bonus to your damage rolls, and you can use your bonus action to engage in Two-Weapon Fighting.

\n

Once you've used this feature, you must complete a short or long rest before you can use it again.

"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":""},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Blademaster%20Specialist.webp","effects":[]} +{"name":"Teräs Käsi Order","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"description":{"value":"

Teräs Käsi Order

\n

Monks of the Teräs Käsi Order are the ultimate masters of unarmed martial arts combat. They learn techniques to draw focus from the defeat of their enemies, resist the mental assaults of the Force, and unleash powerful rushes of strikes to subdue their foes.

\n

Forredari Stance

\n

Teräs Käsi Order: 3rd level
Your honed focus allows you to enter a stance of mental fortification and power. You can use your bonus action to enter this stance. When you do so, you unleash a flurry of blows on creatures of your choice that you can see within 10 feet of you. Each creature in the area must make a Dexterity saving throw (DC = 8 + your bonus to attacks with the weapon) or take the weapon's normal damage.

\n

This stance lasts for 1 minute. While you are in this stance, you gain the following benefits:

\n
    \n
  • Death Weave. When you reduce a creature to 0 hit points, you gain temporary hit points equal to your Wisdom or Charisma modifier (your choice) + half your monk level (rounded down, minimum of 1).
  • \n
  • Gundark Slap. Once on each of your turns, when you hit a creature with an unarmed strike or a monk weapon, you can choose to make it unable to take reactions until the end of your next turn.
  • \n
  • Sleeping Krayt. You can use your Wisdom or Charisma modifier (your choice) instead of Strength whenever you would make a Strength check or a Strength saving throw. Additionally, you have advantage on saving throws against being charmed or frightened.
  • \n
\n

These effects end early if you are incapacitated or die. Once you've used this feature, you can't use it again until you finish a long rest.

\n

While you have no remaining uses of this feature, you can instead expend 1 focus point to use it. When you do so, your maximum focus points are reduced by 1 until you complete a long rest.

\n

Steel Hands Form

\n

Teräs Käsi Order: 6th level

\n

You can draw upon your focus to utilize an array of practiced techniques to strike your opponents with precision and power. You can use your bonus action to enter this stance, and when you do so, you can also enter your Forredari Stance as a part of this same bonus action. This form lasts for 1 minute. While you are in this stance, you gain the following benefits:

\n
    \n
  • Charging Wampa. Once per round, when you are forced to make a saving throw against a force power, your movement speed increases by 10 feet until the end of your next turn.
  • \n
  • Nexu Grin. When a creature you can see within 10 feet of you casts a force power that requires a force attack roll against you or an an allied creature, you can use your reaction and expend 1 focus point to impose disadvantage on the attack roll. If the attack misses, and the higher of the two d20 rolls would also miss, the creature cannot cast that force power again until it completes a short or long rest.
  • \n
  • Screaming Squill. Once per turn, when you hit a creature that is concentrating on a force power and it makes a Constitution saving throw to maintain concentration, the DC for the check equals your focus save DC, unless the DC for the Constitution saving throw would be higher.
  • \n
\n

These effects end early if you are incapacitated or die. Once you've used this feature, you can't use it again until you finish a long rest.

\n

While you have no remaining uses of this feature, you can instead expend 1 focus point to use it. When you do so, your maximum focus points are reduced by 1 until you complete a long rest.

\n

Matter Over Mind

\n

Teräs Käsi Order: 11th level
You tap into the greater power of your focus. While both your Forredari Stance and your Steel Hands Form are active, you gain the following benefits:

\n
    \n
  • Dancing Dragonsnake. When you take force, lightning, or necrotic damage, you can use your reaction to deflect it. When you do so, the damage you take is reduced by 1d10 + your Wisdom or Charisma modifier (your choice) + your monk level.
  • \n
  • Aryx Slash. Once on each of your turns, when you hit a target with an unarmed strike or monk weapon, you can roll a Martial Arts die and deal additional psychic damage equal to the amount rolled.
  • \n
  • Piercing Gaze. When you would make an Insight check against a creature you know to wield the Force, you can use either your Wisdom or Charisma modifier for the roll, and you are considered to have expertise in the Insight skill. If you would already have expertise in the check, you instead have advantage on the roll.
  • \n
\n

Spitting Rawl

\n

Teräs Käsi Order: 17th level
You have learned to harness your strikes in a blistering fury. On your turn, when a creature takes damage from you three times, you can make up to three additional unarmed strikes against the creature (no action required). The first additional unarmed strike costs 1 focus point and deals 1d12 additional psychic damage on a hit, and each unarmed strike after the first costs 1 additional focus point and deals 1d12 additional psychic damage on a hit, cumulatively.

"},"source":"EC","className":"Monk","classCasterType":""},"flags":{"core":{"sourceId":"Item.8ZPX84rNvnHaPbT5"}},"img":"systems/sw5e/packs/Icons/Archetypes/Teras%20Kasi%20Order.webp","effects":[],"_id":"8H3eJ7yWj6t54MP2"} {"_id":"8UxY6Q7jXLR9tedX","name":"Kro Var Order","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Monk","description":{"value":"

Kro Var Order

\n

Monks of the Kro Var Order—known as shapers—utilize a unique power to bend the four elements to their will, creating effects as small as a floating flame to quakes that sunder the earth.

\n

Elemental Attunement

\n

Kro Var Order: 3rd, 11th, and 17th level

\n

You gain the ability to bend the elements to your will. As an action, you can manipulate these forces to create one of the following effects of your choice at a space within 10 feet of you:

\n
    \n
  • Create a harmless, instantaneous sensory effect related to air, earth, fire, or water, such as a shower of sparks, a puff of wind, a spray of light mist, or a gentle rumbling of stone.
  • \n
  • Instantaneously light or snuff out a candle, a torch, or a small campfire.
  • \n
  • Chill or warm up to 1 pound of nonliving material for up to 1 hour.
  • \n
  • Cause earth, fire, water, or mist that can fit within a 1 foot cube to shape itself into a crude form you designate for 1 minute.
  • \n
\n

This range increases to 30 feet at 11th level and 60 feet at 17th level.

\n

Additionally, as a bonus action on your turn, you can spend 1 focus point to conjure a weapon made of one of the four elements—air, earth, fire, or water—which lasts for 1 minute. Your weapon takes an appearance of your choice, it can only be used by you, and you can't be disarmed of it while you are conscious. You are proficient in this weapon, which counts as a monk weapon for you and uses your Martial Arts die for its damage rolls. You can only have one of these weapons at a time, and if you summon a new one the old one immediately disappears.

\n

Air

\n

Your whistling weapon has the thrown property with a range of 20/60 and deals sonic damage on a hit. Additionally, when you hit a creature with the weapon, it is deafened until the end of its next turn.

\n

Earth

\n

Your earthen weapon takes the form of stone and deals kinetic damage on a hit. Additionally, when you hit a creature with it, you can force the target to make a Strength saving throw. On a failed save, the target is pushed back 5 feet. Lastly, while active, you can use Strength instead of Dexterity when determining your AC, as long as it doesn't already include that modifier.

\n

Fire

\n

Your flaming weapon sheds bright light in a 10-foot radius and dim light for an additional 10 feet, deals fire damage, and when you hit a creature with it, the creature takes additional damage equal to half your Wisdom or Charisma modifier (your choice, rounded up, minimum of one).

\n

Water

\n

Your watery weapon has the reach property, deals cold damage on a hit, and you can use your Wisdom or Charisma modifier (your choice) instead of Dexterity or Strength for its attack and damage rolls. You must use the same modifier for both rolls. Additionally, when you would make a melee weapon attack, you can instead use your weapon to wet a 5-foot square within your weapon's reach. The affected area is difficult terrain. Each creature who starts its turn in the square or enters it for the first time must make a Dexterity saving throw, falling prone on a failed save.

\n

Elemental Adept

\n

Kro Var Order: 6th, 11th, and 17th level

\n

You gain two of the following features. You gain an additional option at 11th and 17th level.

\n

You can use these features a combined number of times equal to your proficiency bonus, as shown in the monk table. You regain all expended uses when you complete a long rest.

\n

While you have no remaining uses of this feature, you can instead expend 2 focus points to use it. When you do so, your maximum focus points are reduced by 2 until you complete a long rest.

\n

Burning Ember Flourish

\n

You can use your action to choose an area of flame that you can see and that fits within a 5-foot cube within 60 feet. You can extinguish the fire in that area, and you create either fireworks or smoke when you do so.

\n

  Fireworks. The target explodes with a dazzling display of colors. Each creature within 10 feet of the target must succeed on a Constitution saving throw or become blinded until the end of your next turn.

\n

  Smoke. Thick black smoke spreads out from the target in a 20-foot radius, moving around corners. The area of the smoke is heavily obscured. The smoke persists for 1 minute or until a strong wind disperses it.

\n

Curtain of Unyielding Wind

\n

As an action, you can spend 2 focus points to call up a mighty gale, which swirls around you in a 10-foot radius and moves with you, remaining centered on you. The wind lasts for 10 minutes. The wind deafens both you and other creatures in its area. It extinguishes unprotected flames in its area that are torch-sized or smaller and hedges out vapor, gas, and fog that can be dispersed by strong wind. The area is difficult terrain for creatures other than you, and the attack rolls of ranged weapon attacks have disadvantage if the attacks pass in or out of the wind.

\n

Crushing Hand of the Mountain

\n

As an action, you can choose a 5-foot-square unoccupied space on the ground that you can see within 30 feet. A Medium hand made from soil and stone, which lasts for 1 minute, rises in that space and reaches for one Large or smaller creature you can see within 5 feet of it. The target must make a Strength saving throw. On a failed save, the target takes 2d6 kinetic damage and is restrained for the duration. As a bonus action on each of your turns, you can cause the hand to crush the restrained target, which must make a Strength saving throw. It takes 2d6 kinetic damage on a failed save or half as much damage on a successful one. To break out, the restrained creature can use its action to make a Strength check against your focus save DC. On a success, the target escapes and is no longer restrained by the hand. As an action, you can cause the hand to reach for a different creature or to move to a different unoccupied space within range. The hand releases a restrained target if you do either.

\n

Hatchling's Flame

\n

As an action, you focus your energy into a torrent of fire that streaks away from you. A line of roaring flame 30 feet long and 5 feet wide emanates from you in a direction you choose. Each creature in the line must make a Dexterity saving throw. A creature takes 3d8 fire damage on a failed save, or half as much damage on a successful one.

\n

Patient Bantha Listens

\n

You reach out to the ground beneath you. You can use your bonus action to gain tremorsense with a range of 30 feet and a burrow speed equal to your walking speed for up to 1 minute. Your movement leaves behind a tunnel that remains for as long as this ability is active, after which it collapses.

\n

Rush of the Shyrack

\n

As an action, you can spend 2 focus points to form a line of strong wind 60 feet long and 10 feet wide that blasts from you in a direction you choose for one minute or until you lose concentration or dismiss the effect (no action required). Each Large or smaller creature that starts its turn in the line must succeed on a Strength saving throw or be pushed 15 feet away from you in a direction following the line. Any creature in the line must spend 2 feet of movement for every 1 foot it moves when moving closer to you. The gust disperses gas or vapor, and it extinguishes candles, torches, and similar unprotected flames in the area. It causes protected flames, such as those of lanterns, to dance wildly and has a 50 percent chance to extinguish them. As a bonus action on each of your turns before the effect ends, you can change the direction in which the line blasts from you.

\n

Shape the Raincloud

\n

You can use your action to pull water from air and return it to the atmosphere. In an open container, you can create up to 20 gallons of drinkable water. You may also produce a rain that falls within a 30-foot cube and extinguishes open-air flames. You can destroy the same amount of water in an open container, or destroy a 30-foot cube of fog.

\n

Swarming Ice Rabbit

\n

As an action, you can cause a flurry of ice crystals to erupt from a point you can see within 90 feet. Each creature in a 5-foot-radius sphere centered on that point must make a Dexterity saving throw. On a failed save, a creature takes 3d6 cold damage and gains 1 slowed level until the start of your next turn. On a successful save, a creature takes half as much damage and isn't slowed.

\n

Elemental Master

\n

Kro Var Order: 11th and 17th level

\n

At 11th level, you gain one of the following features. You gain an additional option at 17th level.

\n

You can use these features a combined number of times equal to half your proficiency bonus, as shown in the monk table. You regain all expended uses when you complete a long rest.

\n

While you have no remaining uses of this feature, you can instead expend 3 focus points to use it. When you do so, your maximum focus points are reduced by 3 until you complete a long rest.

\n

Earth Reaches for Sky

\n

Prerequisite: Crushing Hand of the Mountain or Patient Bantha Listens

\n

As an action, you can choose a point you can see on the ground within 120 feet. A fountain of churned earth and stone erupts in a 20-foot cube centered on that point. Each creature in that area must make a Dexterity saving throw. A creature takes 3d12 kinetic damage on a failed save, or half as much damage on a successful one. Additionally, the ground in that area becomes difficult terrain until cleared. Each 5-foot-square portion of the area requires at least 1 minute to clear by hand.

\n

Ride the Wind

\n

Prerequisite: Curtain of Unyielding Wind or Rush of the Shyrack

\n

As an action, you can gain a flying speed equal to your movement speed for 10 minutes. You can hover while this technique is active, but when it ends, you fall if you are still aloft, unless you can stop the fall.

\n

River of Hungry Flame

\n

Prerequisite: Burning Ember Flourish or Hatchling's Flame

\n

As an action, you can create a wall of fire on a solid surface within 120 feet. You can make the wall up to 60 feet long, 20 feet high, and 1 foot thick, or a ringed wall up to 20 feet in diameter, 20 feet high, and 1 foot thick. The wall is opaque and lasts for 1 minute.

\n

When the wall appears, each creature within its area must make a Dexterity saving throw. On a failed save, a creature takes 5d8 fire damage, or half as much damage on a successful save.

\n

One side of the wall, chosen by you when you use this feature, deals 5d8 fire damage to each creature that ends its turn within 10 feet of that side of the wall. A creature takes the same damage when it enters the wall for the first time on a turn or ends its turn there. The other side of the wall deals no damage.

\n

Shape the Flowing River

\n

Prerequisite: Shape the Raincloud or Swarming Ice Rabbit

\n

As an action, you can control any freestanding water within 300 feet of you inside an area you choose that is a cube up to 100 feet on a side. You can choose from any of the following effects when you use this feature. As an action on your turn, you can repeat the same effect or choose a different one.

\n

  Flood. You cause the water level of all standing water in the area to rise by as much as 20 feet. If the area includes a shore, the flooding water spills over onto dry land. If you choose an area in a large body of water, you instead create a 20-foot tall wave that travels from one side of the area to the other and then crashes down. Any Huge or smaller creatures in the wave's path are carried with it to the other side. Any Huge or smaller creatures struck by the wave have a 25 percent chance of being knocked prone. The water level remains elevated until the feature ends or you choose a different effect. If this effect produced a wave, the wave repeats on the start of your next turn while the flood effect lasts.

\n

  Part Water. You cause water in the area to move apart and create a trench. The trench extends across the feature's area, and the separated water forms a wall to either side. The trench remains until the feature ends or you choose a different effect. The water then slowly fills in the trench over the course of the next round until the normal water level is restored.

\n

  Redirect Flow. You cause flowing water in the area to move in a direction you choose, even if the water has to flow over obstacles, up walls, or in other unlikely directions. The water in the area moves as you direct it, but once it moves beyond the feature's area, it resumes its flow based on the terrain conditions. The water continues co move in the direction you chose until the feature ends or you choose a different effect.

\n

  Whirlpool. This effect requires a body of water at least 50 feet square and 25 feet deep. You cause a whirlpool to form in the center of the area. The whirlpool forms a vortex that is 5 feet wide at the base, up to 50 feet wide at the top, and 25 feet tall. Any creature or object in the water and within 25 feet of the vortex is pulled 10 feet toward it. A creature can swim away from the vortex by making a Strength (Athletics) check against your feature save DC. When a creature enters the vortex for the first time on a turn or starts its turn there, it must make a Strength saving throw. On a failed save, the creature takes 2d8 kinetic damage and is caught in the vortex until the feature ends. On a successful save, the creature takes half damage, and isn't caught in the vortex. A creature caught in the vortex can use its action to try to swim away from the vortex as described above, but has disadvantage on the Strength (Athletics) check to do so. The first time each turn that an object enters the vortex, the object takes 2d8 kinetic damage; this damage occurs each round it remains in the vortex.

\n

Elemental Paragon

\n

Kro Var Order: 17th level

\n

You gain one of the following features.

\n

Once you've used the chosen feature, you must complete a long rest before you can use it again.

\n

While you have no remaining uses of this feature, you can instead expend 4 focus points to use it. When you do so, your maximum focus points are reduced by 4 until you complete a long rest.

\n

Archetype of Air

\n

Prerequisite: Ride the Wind

\n

As an action, you conjure a whirlwind around you, granting the following benefits until the start of your next turn:

\n
    \n
  • Ranged attacks made against you have disadvantage.
  • \n
  • You gain a flying speed of 60 feet. If you are still flying when the technique ends, you fall, unless you can somehow prevent it.
  • \n
  • You can use your action to create a 15 foot cube of swirling wind centered on a point you can see within 60 feet of you. Each creature in that area must make a Constitution saving throw. A creature takes 2d10 kinetic damage on a failed save, or half as much damage on a successful one. If a Large or smaller creature fails the save, that creature is also pushed up to 10 feet away from the center of the cube.
  • \n
\n

At the start of each of your turns, you can use your bonus action to extend the benefits of this feature until the start of your next turn, to a maximum duration of 1 minute. This effect ends immediately if you are incapacitated or die.

\n

Figure of Flame

\n

Prerequisite: River of Hungry Flame

\n

As an action, you cause flames to race across your body, granting the following benefits until the start of your next turn:

\n
    \n
  • You have resistance to fire damage.
  • \n
  • The flames shed bright light in a 30 foot radius and dim light for an additional 30 feet.
  • \n
  • Any creature that moves within 5 feet of you for the first time on a turn or ends its turn there takes 1d10 fire damage.
  • \n
  • You can use your action to create a line of fire 15 feet long and 5 feet wide extending from you in a direction you choose. Each creature in the line must make a Dexterity saving throw, taking 4d8 fire damage on a failed save or half as much on a successful one.
  • \n
\n

At the start of each of your turns, you can use your bonus action to extend the benefits of this feature until the start of your next turn, to a maximum duration of 1 minute. This effect ends immediately if you are incapacitated or die.

\n

Icon of Ice

\n

Prerequisite: Shape the Flowing River

\n

As an action, you cause frost to chill the area around you, granting the following benefits until the start of your next turn:

\n
    \n
  • You have resistance to cold damage.
  • \n
  • You can move across difficult terrain created by ice or snow without spending extra movement.
  • \n
  • The ground in a 10 foot radius around you is icy and is difficult terrain for creatures other than you. The radius moves with you.
  • \n
  • You can use your action to create a 15 foot cone of freezing ice extending from your outstretched hand in a direction you choose. Each creature in the cone must make a Constitution saving throw. A creature takes 4d6 cold damage on a failed save or half as much damage on a successful one. A creature that fails its save against this effect gains 1 slowed level until the start of your next turn.
  • \n
\n

At the start of each of your turns, you can use your bonus action to extend the benefits of this feature until the start of your next turn, to a maximum duration of 1 minute. This effect ends immediately if you are incapacitated or die.

\n

Embodiment of Earth

\n

Prerequisite: Earth Reaches for Sky

\n

As an action, you cause rock to envelop you, granting the following benefits until the start of your next turn:

\n
    \n
  • You have resistance to kinetic and energy damage.
  • \n
  • You can move across difficult terrain made of earth or stone without spending extra movement. You can move through solid earth or stone as if it were air without destabilizing it, but you can't end your movement there. If you do so, you are immediately shunted to the nearest unoccupied space that you can occupy and take force damage equal to twice the number of feet you are moved.
  • \n
  • You can use your action to create a small earthquake on the ground in a 15 foot radius centered on you. Other creatures on that ground must succeed on a Dexterity saving throw or be knocked prone.
  • \n
\n

At the start of each of your turns, you can use your bonus action to extend the benefits of this feature until the start of your next turn, to a maximum duration of 1 minute. This effect ends immediately if you are incapacitated or die.

\n

Avatar

\n

Kro Var Order: 17th level

\n

As an ultimate display of your mastery of the elements, you can spend 5 focus points as an action to have the elements of water, earth, fire, and air form a protective sphere around your body, gaining multiple benefits for 1 minute. While this ability is active, you have resistance to cold, energy, fire, kinetic, lightning, and sonic damage. You also gain a burrow, fly, and swim speed equal to your movement speed. Lastly, you can use any of the following abilities as a bonus action:

\n
    \n
  • You create a small earthquake on the ground in a 15 foot radius around you. Each creature in that area must make a Dexterity saving throw. On a failed save, a creature takes 1 d6 kinetic damage and is knocked prone.
  • \n
  • You create a line of fire 15 feet long and 5 feet wide extending from you in a direction you choose. Each creature in the line must make a Dexterity saving throw. A creature takes 3d6 fire damage on a failed save, or half as much damage on a successful one.
  • \n
  • You create a 15 foot cube of swirling wind centered on a point you can see within 60 feet of you. Each creature in that area must make a Constitution saving throw. A creature takes 1 d10 kinetic damage on a failed save, or half as much damage on a successful one. If a Large or smaller creature fails the save, that creature is also pushed up to 10 feet away from the center of the cube.
  • \n
  • You create a 15 foot cone of ice shards extending from your outstretched hand in a direction you choose. Each creature in the cone must make a Constitution save throw. A creature takes 2d6 cold damage on a failed save, or half as much damage on a successful one. A creature that fails its save against this effect has its speed halved until the start of your next turn.
  • \n
"},"source":"EC","classCasterType":""},"flags":{"core":{"sourceId":"Item.O5AoHo3xLmzUywN0"},"dae":{"activeEquipped":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Kro%20Var%20Order.webp","effects":[]} {"_id":"9AQImD6JBpd9c1UK","name":"Path of the Corsair","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Sentinel","description":{"value":"

Path of the Corsair

\n

There may come a time when a sentinel finds themselves stranded, hunted, or in any situation where they must hide their nature as a force wielder. Those sentinels who follow the Path of the Corsair make use of alternative weaponry not commonly associated with force-wielders to great effect.

\n

Scavenger's Reach

\n

Starting when you choose this calling at 3rd level, you learn the @Compendium[sw5e.forcepowers.5o01ADUmNyzy470b]{Force Disarm} force power. Additionally, you can use your Kinetic Combat feature when you cast it as your action. Finally, when you cast the force disarm power, disarm a blaster weapon, and catch it, you can reload the weapon as a part of the same action.

\n

Corsair Weapons

\n

Also at 3rd level, you can use the force to quickly learn the use of unfamiliar weapons. When you hold a weapon that you are not proficient in, you can spend 1 force point (no action required) to gain proficiency with that weapon until the end of your next long rest. If that weapon is a blaster, you can use it to make Kinetic Combat attacks as long as the target of the attack is within the weapon's normal range.

\n

Additionally, when you throw a grenade, you can use Wisdom or Charisma instead of Strength when determining your throwing range.

\n

Force-Empowered Detonators

\n

At 7th level, you learn to infuse a number of small detonators with the Force. Over the course of a short or long rest, you can create a number of detonators equal to your Wisdom or Charisma modifier (your choice, minimum of one). Your detonators can only be used by you, and they lose their potency at the end of your next short or long rest.

\n

As a bonus action on each of your turns, you can throw one of your detonators at a point within range. Your detonators have a range equal to 30 feet + your Wisdom or Charisma modifier x 5. Make a universal ranged force attack. On a hit, the detonator adheres to the target, and if the target is a Large or smaller creature, it is pushed back 5 feet. On a miss, it falls to the ground. Hit or miss, the detonator then explodes. The target and each creature within 5 feet must make a Dexterity saving throw against your universal force save DC. If the detonator adhered to a target, the creature has disadvantage on the saving throw. A creature takes force damage equal to your Kinetic Combat Damage Die + your Wisdom or Charisma modifier (your choice) on a failed save, or half as much on a successful one.

\n

Energized Kinetics

\n

By 13th level, once per turn, when you deal damage with a Force-Empowered Detonator or your Double Strike feature, you can deal additional damage equal to your Kinetic Combat Damage Die. The damage type is force, lightning, necrotic, or psychic (your choice).

\n

Disorienting Detonations

\n

At 18th level, when a creature fails the saving throw against your Force-Empowered Detonators feature, you can spend 2 force points to subject it to the effects of one of the following force powers: @Compendium[sw5e.forcepowers.KCVdYOoxu0kfkSCn]{Affliction}, @Compendium[sw5e.forcepowers.bDdyZTL7KCiz2zXR]{Force Blind/Deafen}, @Compendium[sw5e.forcepowers.8UOBsZEV8esbx6IG]{Stun}, or @Compendium[sw5e.forcepowers.83VXTwWszl8bMlW6]{Stun Droid}. They automatically fail the saving throw for the selected power, but the effects only last until the end of your next turn.

"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":"Forcecaster"},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Path%20of%20the%20Corsair.webp","effects":[]} {"_id":"9Hva27QHj2ruQXTA","name":"Form IV: Ataru","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Guardian","description":{"value":"

Form IV: Ataru

\n

Form IV, also know as Aggression Form, is a kinetically active form that relies on speed, acrobatics, and power. Those guardians who focus on Ataru Form utilize high energy tactics to confuse and distract their opponents, quickly moving about the battlefield.

\n

Form Basics

\n

When you choose this form as your focus at 3rd level, you learn the basics of the chosen form. You gain the @Compendium[sw5e.lightsaberform.yfCFM4d8xPdcsNKe]{Ataru Lightsaber Form}, detailed in Chapter 6 of the Player’s Handbook. If you already know this form, you can instead choose another lightsaber form. You can’t take a lightsaber form option more than once, even if you later get to choose again.

\n

The Way of the Hawk-Bat

\n

Also at 3rd level, as a bonus action, you you can take an aggressive stance, leaping around the battlefield for 1 minute. As a part of this bonus action, and as a bonus action on each of your turns, you can cast the @Compendium[sw5e.forcepowers.oPi4Y7zezP7MxNDK]{Force Jump power} at 1st-level without expending force points. Additionally, when you cast force jump, you have advantage on the first attack roll you make against each creature within 5 feet of where you land.

\n

This effect ends early if you are incapacitated or die. Once you've used this feature, you can't use it again until you finish a long rest.

\n

Channel the Force

\n

Lastly at 3rd level, you gain the following Channel the Force option.

\n

Retreating Leap

\n

When a creature makes a melee attack roll against you, you can expend a use of your Channel the Force and your reaction to jump 10 feet in a direction of your choice, imposing disadvantage on the roll. This movement does not provoke opportunity attacks. You can wait until after the attack roll is made, but before the DM determines whether the attack hits.

\n

Hawk-Bat Swoop

\n

At 7th level, you gain the ability to move along vertical surfaces without falIing during the move. If you end your turn in the air, you fall immediately to the ground.

\n

Additionally, you no longer take damage when falling from a distance no greater than your walking speed.

\n

Whirlwind Attack

\n

At 15th level, you can use your action to make melee attacks against any number of creatures within 5 feet of you, with a separate attack roll for each target.

\n

Master of Aggression

\n

At 20th level, your presence on the field of battle is as a graceful blur of deadly blades and daring acrobatics. Your Dexterity and Wisdom or Charisma scores (your choice) increase by 2. Your maximum for those scores increases by 2. Additionally, you can use your action to gain the following benefits for 1 minute:

\n
    \n
  • You have resistance to kinetic and energy damage from unenhanced weapons.
  • \n
  • When an ally within 30 feet of you takes the Attack action, they can make one additional attack as a part of that same action.
  • \n
  • When you hit a creature with a weapon attack, you can move up to 10 feet. This movement does not provoke opportunity attacks.
  • \n
\n

This effect ends early if you are incapacitated or die. Once you've used this feature, you can't use it again until you finish a long rest.

"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":"Forcecaster"},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Ataru%20Form.webp","effects":[]} @@ -58,7 +59,7 @@ {"_id":"YByrgf4R9lfeVVBQ","name":"Sharpshooter Practice","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Operative","description":{"value":"

Sharpshooter Practice

\n

Those operatives who choose the Sharpshooter Practice are bringers of death. Striking from a safe distance, the Sharpshooter uses precision shooting to control the battlefield and bring targets down quickly.

\n

Assume the Position

\n

Beginning at 3rd level, you don't need advantage on your attack roll to use your Sneak Attack if your target is greater than 30 feet from you and no enemies are within 5 feet of you. In addition, standing up from prone now only costs 5 feet of movement.

\n

Additionally, you gain proficiency with two martial blasters of your choice.

\n

Place Shot

\n

Also at 3rd level, you perfect the art of placing distant shots for maximum effectiveness in debilitating and controlling your enemies. When you deal Sneak Attack damage to a creature, you may choose to forgo two of your Sneak Attack dice to make the attack a placed shot.

\n

Some of your placed shots require your target to make a saving throw to resist the placed shot's effects. The saving throw DC is calculated as follows:

\n

Placed Shot save DC = 8 + your proficiency bonus + your Dexterity modifier

\n

Disarming Shot

\n

You attempt to disarm a creature with your attack. The target must succeed on a Strength saving throw or be forced to drop one item of your choice that it's holding. The object lands at its feet.

\n

Penetrating Shot

\n

You attempt to damage another target with the same attack. Choose a second target within 15 feet of and directly behind your initial target. If the original attack roll would hit the second target, it takes two dice worth of Sneak Attack damage.

\n

The damage is of the same type dealt by the original attack.

\n

Suppressive Shot

\n

You attempt to pin the target to its location. The target must succeed on a Wisdom saving throw or be frightened of you until the end of its next turn.

\n

Head Shot

\n

At 9th level, you are at your deadliest when your enemies are unaware of the danger they are in. You have advantage on attack rolls against any creature that hasn't taken a turn in combat yet.

\n

Additionally, any hit you score against a creature that is surprised is a critical hit.

\n

Distracting Shot

\n

Starting at 13th level, you are able to defend your compatriots from afar. When a friendly creature you can see within your weapon's normal range is the target of a ranged attack, or forced to make a saving throw, and the source of the effect is within your weapon's normal range, you can use your reaction to make a ranged weapon attack against the source. On a hit, instead of dealing damage, the target of your attack has disadvantage on the attack roll against your ally, or your ally has advantage on the saving throw to resist the effect.

\n

Double Tap

\n

At 17th level, you've learned to capitalize when you have the advantage. When you take the Attack action and make an attack with advantage, you can choose to forgo the advantage. If you do, you can make an additional attack against the target or another creature within 5 feet of it (no action required). Both attacks can benefit from your Sneak Attack damage, instead of only one.

"},"source":"PHB","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":""},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Sharpshooter%20Practice.webp","effects":[]} {"_id":"YwAHQuiEetUQgshY","name":"Triage Technique","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Scout","description":{"value":"

Triage Technique

\n

With every conflict comes death and destruction. Followers of the Triage Technique excel at keeping their comrades in the fight with quick thinking and fastacting medicine.

\n

Triage Training

\n

Triage Technique: 3rd level

\n

You gain proficiency in the Medicine skill.

\n

Additionally, when you would use your action to make an ability check to stabilize a creature, expend a use of a traumakit, or use a medpac, you can instead use your bonus action.

\n

Mark of Triage

\n

Triage Technique: 3rd level

\n

You learn new ways to use your Ranger's Quarry.

\n
    \n
  • While a hostile creature is the target of your Ranger's Quarry, you always know any conditions it is suffering from, and you know at roughly what percentage its current hit points is relative to its maximum. Additionally, if the target is within 60 feet of you. when it is forced to make a Constitution saving throw, you can use your reaction to force it to make the roll with disadvantage. Once you've used this feature, you must complete a short or long rest before you can use it again.
  • \n
  • While a friendly creature is the target of your Ranger's Quarry, you have advantage on Wisdom (Medicine) checks made to stabilize it. Additionally, if the target is within 60 feet of you, you can use your bonus action and roll your Ranger's Quarry and either restore hit points equal to the amount rolled, or grant temporary hit points equal to the amount rolled. Once a friendly creature has benefited from this ability, they can not do so again until they complete a short or long rest.
  • \n
\n

Double Dose

\n

Triage Technique: 7th level

\n

Your application of medicine does not interfere with your own ability to recover from injuries. When you restore hit points or grant temporary hit points to another creature with a tech power or class feature, you recover the same amount of hit points or gain the same number of temporary hit points.

\n

You can use this feature a number of times equal to your proficiency bonus, as shown in the scout table. You regain all expended uses when you complete a long rest.

\n

Experimental Infusion

\n

Triage Technique: 11th level

\n

When you target a creature with your Ranger's Quarry, you can grant one of the following additional effects of your choice:

\n
    \n
  • Adrenaline/Tranquilizer. The creature's movement speed is doubled until the end of its next turn. Alternatively, it gains a level of slowed until the end of its next turn.
  • \n
  • Focus/Dizziness. The creature has either advantage or disadvantage (your choice) on the first ability check, attack roll, or saving throw it makes within the next minute.
  • \n
  • Toughen/Weaken. The creature gains temporary hit points equal to your scout level, which last for 1 minute. Alternatively, the creature must make a Constitution saving throw against your tech save DC. On a failure, it takes psychic damage equal to your scout level and it can't regain hit points until the start of your next turn.
  • \n
\n

You can use each feature once. You regain any expended uses when you complete a short or long rest.

\n

Cure-All

\n

Triage Technique: 15th level

\n

Your healing becomes even more potent When you restore hit points to a creature as a bonus action using your Mark of the Healer feature, you can also end one of the following conditions afflicting it: blinded, deafened, diseased, paralyzed, or poisoned.

"},"source":"EC","classCasterType":""},"flags":{"core":{"sourceId":"Item.p9sVycmJvwVtDzlD"}},"img":"systems/sw5e/packs/Icons/Archetypes/Triage%20Technique.webp","effects":[]} {"_id":"ZDNCB88TzeMFGY6i","name":"Deadeye Technique","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Scout","description":{"value":"

Deadeye Technique

\n

Some scouts becomes legends written in blaster burns. Followers of the Deadeye Technique the art of the blaster shot and utilize their incredible focus to make shots that most would deem impossible. When everything depends on one shot, you want a Deadeye pulling the trigger.

\n

Due to their uncanny focus, Deadeyes can make shots that other marksmen would never dare to attempt. Deadeyes know how to make the most of ranged weapons and can use them to greater effect than other Scouts.

\n

Focused Superiority

\n

When you choose this technique at 3rd level, you learn maneuvers that are fueled by special dice called superiority dice.

\n

Maneuvers

\n

You know two maneuvers of your choice, which are detailed under \"Maneuvers\" below, and you earn more at higher levels, as shown in the Maneuvers Known column of the Deadeye Technique Focused Superiority table. Many maneuvers enhance an attack in some way. You can use only one maneuver per attack, and you may only use each maneuver once per turn.

\n

Each time you learn new maneuvers, you can also replace one maneuver you know with a different one.

\n

Superiority Dice

\n

You have two superiority dice, which are d6s, and you earn more at higher levels, as shown in the Superiority Dice column of the Deadeye Technique Focused Superiority table. A superiority die is expended when you use it. You regain all of your expended superiority dice when you finish a short or long rest.

\n

Saving Throws

\n

Some of your maneuvers require your target to make a saving throw to resist the maneuver's effects. The saving throw DC is calculated as follows:

\n

Maneuver save DC = 8 + your proficiency bonus + your Dexterity modifier

\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Deadeye Technique Focused Superiority
LevelFocused SuperioritySuperiority DiceManeuvers Known
3rdd422
4thd422
5thd622
6thd622
7thd633
8thd633
9thd833
10thd833
11thd844
12thd844
13thd1044
14thd1044
15thd1055
16thd1055
17thd1255
18thd1255
19thd1255
20thd1255
\n
\n
\n

Maneuvers

\n

The maneuvers are presented in alphabetical order.

\n

Crippling Shot

\n

When you hit a creature with a ranged weapon attack, you can expend a superiority die to cripple its movement. Add the number rolled to the damage of the ranged weapon attack. The creature must succeed on a Constitution saving throw or have its movement speed halved. At the end of each of its turns, the target can make a Constitution saving throw to end the effect.

\n

Daring Escape

\n

You can expend one superiority die to take the Disengage action as a bonus action until the end of your turn. Until the end of this turn, you have advantage on all Strength (Athletics) checks.

\n

Covering Fire

\n

When you hit a creature with a ranged weapon attack, you can expend one superiority die to maneuver one of your comrades into a more advantageous position. You add the superiority die to the attack's damage roll, and you choose a friendly creature who can see or hear you.

\n

That creature can use its reaction to move up to half its speed without provoking opportunity attacks from the target of your attack.

\n

Disarming Shot

\n

When you hit a creature with a ranged weapon attack, you can expend one superiority die to attempt to disarm the target, forcing it to drop one item of your choice that it's holding. Add the superiority die to the attack's damage roll, and the target must make a Strength saving throw. On a failed save, it drops the object you choose. The object lands at its feet.

\n

Distracting Shot

\n

When you hit a creature with a ranged weapon attack, you can expend one superiority die to distract the creature, giving your allies an opening. You add the superiority die to the attack's damage roll. The next attack roll against the target by an attacker other than you has advantage if the attack is made before the start of your next turn.

\n

Exploit Weakness

\n

When you hit a creature with a weapon attack, you can expend a superiority die and deal additional damage equal to the number rolled. This damage cannot be reduced in any way.

\n

Penetrating Shot

\n

When you hit a creature with a ranged weapon attack, you can expend one superiority die to attempt to damage another creature with the same attack. Choose up to two creatures within 15 feet of and directly behind your initial target. If the original attack roll would hit the second creature(s), it takes damage equal to the number you roll on your superiority die.

\n

The damage is of the same type dealt by the original attack.

\n

Precision Attack

\n

When you make a weapon attack roll against a creature, you can expend one superiority die to add it to the roll. You can use this maneuver before or after making the attack roll, but before any effects of the attack are applied.

\n

Return Fire

\n

When a creature misses you with a ranged attack, you can use your reaction and expend one superiority die to make a ranged weapon attack against the creature. If you hit, you add the superiority die to the attack's damage roll.

\n

Mark of the Deadeye

\n

Also at 3rd level, the range of your Ranger's Quarry feature doubles. Additionally, when making ranged weapon attacks against the target of your Ranger's Quarry, the normal and long range of your ranged weapons double.

\n

Cover to Cover

\n

Beginning at 7th level, attack rolls made against you on your turn are made with disadvantage.

\n

Deadeye Technique Focused Superiority

\n

Shoot First

\n

Starting at 11th level, you have learned that the person who shoots first is often the one who walks out alive. When you make a ranged weapon attack against a creature that has not yet acted during your first turn in combat and you have advantage on the roll, you can reroll one of the dice once.

\n

Additionally, on a hit, you deal an extra 1d6 damage of the same type as the weapon.

\n

Overwatch

\n

At 15th level, you have become a master at protecting your allies from afar. When a creature attempts to make an opportunity attack against an allied creature, or forces your ally to make a saving throw, you can use your reaction to make an attack roll against the enemy creature.

\n

If your attack hits, either impose disadvantage on the enemy creature's opportunity attack roll or grant advantage to any allies making the saving throw.

"},"source":"PHB","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":"Techcaster"},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Deadeye%20Technique.webp","effects":[]} -{"_id":"avFn1m9oUpDgKAAF","name":"Astrotech Engineering","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Engineer","description":{"value":"

Astrotech Engineering

\n

Those engineers who choose the Astrotech Engineering discipline focus on crafting and upgrading their droid companions.

\n

Bonus Proficiencies

\n

When you choose this discipline at 3rd level, you gain proficiency in astrotech's tools. Additionally, when you engage in crafting with astrotech's tools, the rate at which you craft doubles.

\n

Droid Companion

\n

Also at 3rd level, you learn to employ all the knowledge you've accumulated to create and customize your own personalized droid companion.

\n

Choose your droid, which is detailed at the end of this discipline. Over the course of 8 hours, which can be done during a long rest, you can expend 500 cr worth of materials to finally finish your droid.

\n

If your droid is irreparably destroyed, or you want to interface with a different droid, you can spend an additional 250 credits and 1 hour to change the target of this feature. You may only have one droid companion at a time.

\n

Your droid gains a variety of benefits while it is interfaced with you:

\n
    \n
  • The droid obeys your commands as best it can. It acts on your turn, and you determine its actions, decisions, attitudes, and so on. If you are incapacitated or absent, your droid acts on its own.
  • \n
  • Your droid's level equals your engineer level, and for each engineer level you gain after 3rd, your droid companion gains an additional Hit Die and increases its hit points accordingly.
  • \n
  • Your droid has the proficiency bonus of a player character of the same level.
  • \n
  • Whenever you gain the Ability Score Improvement feature in this class, your droid's abilities also improve. Your droid can increase one ability score of your choice by 2, or it can increase two ability scores of your choice by 1. As normal, your droid can't increase an ability score above 20 using this feature unless its description specifies otherwise.
  • \n
  • Your droid can not wear armor, but you can have the armor professionally integrated into its chassis. Over the course of a long rest, you can expend materials equal to half the cost of the armor in order to integrate it into your droid. Your droid must be proficient in armor in order to have it integrated.
  • \n
  • Your droid is a valid target of the tracker droid interface tech power.
  • \n
\n

Additionally, you can modify your droid. Your droid companion has 4 modification slots, and it gains more at higher levels, as shown in the Modification Slots column of the engineer class table. For each modification installed, your tech point maximum is reduced by 1. Over the course of a long rest, you can replace or remove a number of modifications up to your Intelligence modifier (minimum of one).

\n

Potent Integration

\n

Lastly at 3rd level, when your droid makes an attack roll, you can use your reaction to expend one use of your Potent Aptitude to give it a boost. Roll the die and add it to both the attack and damage rolls, if the attack hits.

\n

Coordinated Attack

\n

Beginning at 6th level, when you take the Attack action, if your companion can see you, it can use its reaction to make a weapon attack against the same creature.

\n

Droid Defense

\n

At 14th level, while your droid can see you, it has advantage on all saving throws.

\n

Superior Droid Defense

\n

Starting at 18th level, whenever an attacker that your droid can see hits it with an attack, it can use its reaction to halve the attack's damage against it.

\n

Generating Your Droid

\n

Choosing your droid companion is an integral part of being an Astrotech Engineer. The class of droid you choose determines their features. Class II, III, and IV droids are all appropriate options, with their statistics listed below.

\n

Once you've selected your type of droid class, you assign your droid's ability scores using standard array (16, 14, 14, 12, 10, 8) as you see fit.

\n

Droid Features

\n

All droids share the following features.

\n

Resistances and Vulnerabilities

\n
    \n
  • Droid Resistances: Your droid is resistant to necrotic, poison, and psychic damage, and immune to poison and disease.
  • \n
  • Droid Vulnerabilities: Your droid is vulnerable to ion damage. Additionally, your droid has disadvantage on saving throws against effects that would deal ion or lightning damage.
  • \n
\n

Traits

\n
    \n
  • Creature Type: Droid
  • \n
  • Armor Integration: Your droid can not wear armor, but you can have the armor professionally integrated into its chassis. Over the course of a long rest, you can expend materials equal to half the cost of the armor in order to have it integrated. This work must be done by someone proficient with astrotech's tools. Your droid must be proficient in armor in order to have it integrated.
  • \n
\n

Class I Droid

\n

Class I droids are programmed for the mathematical, medical, or physical sciences. Subcategories of the first degree are medical droids, biological science droids, physical science droids, and mathematics droids.

\n

As a class I droid, your droid companion has the following features.

\n

Hit Points

\n
    \n
  • Hit Dice: 1d8 per class I droid level
  • \n
  • Hit Points at 1st Level: 8 + your droid's Constitution modifier
  • \n
  • Hit Points at Higher Levels: 1d8 (or 5) + your droid's Constitution modifier per class I droid level after 1st
  • \n
\n

Proficiencies

\n
    \n
  • Armor: Light armor plating
  • \n
  • Weapons: Simple blasters, simple vibroweapons
  • \n
  • Tools: None
  • \n
\n
    \n
  • Languages: Class I droids can speak, read, and write Galactic Basic and one language of your choice. They can understand spoken and written Binary, but can not speak it
  • \n
  • Saving Throws: Wisdom, Intelligence
  • \n
  • Skills: Two Intelligence, Wisdom, or Charisma skills of your choice
  • \n
\n

Features

\n
    \n
  • Size: Medium
  • \n
  • Speed: 25 ft.
  • \n
\n

Class II Droid

\n

Class II droids are programmed for engineering and other technical sciences. They differ from class I droids because they apply the science to real-life situations. Class II droids are rarely equipped with Basic vocabulators, instead communicating through Binary. There are five subcategories of class II droids. Astromech, exploration, environmental, engineering, and maintenance droids are all class II droids.

\n

As a class II droid, your droid companion has the following features.

\n

Hit Points

\n
    \n
  • Hit Dice: 1d6 per class II droid level
  • \n
  • Hit Points at 1st Level: 6 + your droid's Constitution modifier
  • \n
  • Hit Points at Higher Levels: 1d6 (or 4) + your droid's Constitution modifier per class II droid level after 1st
  • \n
\n

Proficiencies

\n
    \n
  • Armor: Light armor, medium armor
  • \n
  • Weapons: Simple blasters and simple vibroweapons with the light property
  • \n
  • Tools: Your choice of demolition's kit, security kit, or slicer's kit
  • \n
\n
    \n
  • Languages: Class II droids can speak, read, and write Binary. They can understand spoken and written Galactic Basic and one language of your choice, but can not speak it
  • \n
  • Saving Throws: Dexterity, Intelligence
  • \n
  • Skills: Two of your choice
  • \n
\n

Features

\n
    \n
  • Size: Small
  • \n
  • Speed: 25 ft.
  • \n
\n

Class III Droid

\n

Class III droids are programmed to interact with humans. They are said to be the most advanced droids ever invented. Protocol, servant, tutor, and child care droids are all class III droids.

\n

As a class III droid, your droid companion has the following features.

\n

Hit Points

\n
    \n
  • Hit Dice: 1d8 per class III droid level
  • \n
  • Hit Points at 1st Level: 8 + your droid's Constitution modifier
  • \n
  • Hit Points at Higher Levels: 1d8 (or 5) + your droid's Constitution modifier per class III droid level after 1st
  • \n
\n

Proficiencies

\n
    \n
  • Armor: Light armor
  • \n
  • Weapons: Simple blasters, simple vibroweapons
  • \n
  • Tools: None
  • \n
\n
    \n
  • Languages: Class III droids can speak and understand all registered languages
  • \n
  • Saving Throws: Wisdom, Charisma
  • \n
  • Skills: None
  • \n
\n

Features

\n
    \n
  • Size: Medium
  • \n
  • Speed: 25 ft.
  • \n
\n

Class IV Droid

\n

Class IV droids are programmed for military and security purposes. Such droids tend to perform tasks of violence or combat might be expected. Almost all class IV droids carry weapons. Armed combat droids are among the first droids ever created. Security, gladiator, battle, and assassin droids are all class IV droids.

\n

As a class IV droid, your droid companion has the following features.

\n

Hit Points

\n
    \n
  • Hit Dice: 1d8 per class IV droid level
  • \n
  • Hit Points at 1st Level: 8 + your droid's Constitution modifier
  • \n
  • Hit Points at Higher Levels: 1d8 (or 5) + your droid's Constitution modifier per class IV droid level after 1st
  • \n
\n

Proficiencies

\n
    \n
  • Armor: All armor
  • \n
  • Weapons: All blasters, All vibroweapons
  • \n
  • Tools: None
  • \n
\n
    \n
  • Languages: Class IV droids can speak, read, and write Galactic Basic and one language of your choice. They can understand spoken and written Binary, but can not speak it
  • \n
  • Saving Throws: Strength, Dexterity
  • \n
  • Skills: None
  • \n
\n

Features

\n
    \n
  • Size: Medium
  • \n
  • Speed: 30 ft.
  • \n
\n

Class V Droid

\n

Class V droids are programmed for menial and low-skill tasks. Such droids tend to perform basic tasks such as construction, lifting, maintenance, mining, sanitation, and transportation.

\n

As a class V droid, your droid companion has the following features.

\n

Hit Points

\n
    \n
  • Hit Dice: 1d8 per class V droid level
  • \n
  • Hit Points at 1st Level: 8 + your droid's Constitution modifier
  • \n
  • Hit Points at Higher Levels: 1d8 (or 5) + your droid's Constitution modifier per class V droid level after 1st
  • \n
\n

Proficiencies

\n
    \n
  • Armor: Light armor, medium armor
  • \n
  • Weapons: All vibroweapons, simple blasters
  • \n
  • Tools: One set of artisan's tools
  • \n
\n
    \n
  • Languages: Class V droids can speak, read, and write Binary. They can understand spoken and written Galactic Basic and one language of your choice, but can not speak it
  • \n
  • Saving Throws: Strength, Constitution
  • \n
  • Skills: Athletics
  • \n
\n

Features

\n
    \n
  • Size: Medium
  • \n
  • Speed: 30 ft.
  • \n
\n

Astrotech Modifications

\n

If a modification has prerequisites, you must meet them to install it. You can install the modification at the same time that you meet its prerequisites.

\n

Advanced Power Core

\n

Prerequisite: 7th level, d10 Hit Die
You greatly improve the power core of your droid. Its Hit Die becomes a d12.

\n

Alarm Protocol

\n

You install an alarm module in your droid, granting the following benefits:

\n
    \n
  • Your droid grants a +5 bonus to initiative to creatures within 5 feet of it.
  • \n
  • You and your droid can't be surprised while your droid is conscious.
  • \n
\n

Analysis Protocol

\n

Prerequisite: 7th level, Class I Droid
Your droid can analyze a target, develop a plan on how to best overcome any potential obstacle, and execute that plan with ruthless efficiency. As a bonus action on your droid's turn, your droid can analyze a target it can see within 60 feet of it. For the next minute, or until it analyzes another target, it gains the following benefits:

\n
    \n
  • When it analyzes a hostile creature, its attack and damage rolls made with weapons with the finesse property or blaster weapons against that target may use its Intelligence modifier instead of Strength or Dexterity.
  • \n
  • When it analyzes a friendly creature, the target can end your droid's Analysis Protocol on them (no action required) to add half your droid's Intelligence modifier (rounded down, minimum of +1) to one attack roll, ability check, or saving throw. Once a friendly creature has benefited from this ability, they can not do so again until they complete a short or long rest.
  • \n
\n

Arm Cannons

\n

You install dual arm cannons in your droid. The arm cannons have 2 charges. As an action, your droid can use charges to cast the overload tech power, using 1 charge per level. The saving throw is made against your droid's tech save DC (8 + your droid's proficiency bonus + your droid's Intelligence modifier).

\n

You can choose this modification multiple times. Each time you do so, the arm cannons gain another charge, to a maximum of 4. The arm cannons regain all charges after a long rest.

\n

Back-Up Protocol

\n

Prerequisite: 7th level
You install an emergency protocol in your droid, prompting a quick reboot after critical damage is taken. If your droid would be reduced to 0 hit points, it instead is reduced to 1.

\n

Once your droid uses this feature, it must finish a short or long rest before it can use it again.

\n

Celerity Augment

\n

You augment your droid to move a little faster. Your droid's speed increases by 5 feet.

\n

You can choose this modification twice.

\n

Charisma Chip

\n

Prerequisite: Class III Droid
You install a charisma chip in your droid. When an ally your droid can see makes an ability check, attack roll, or saving throw, your droid can use its reaction to give them advantage on the roll. It can do so before or after they roll the d20, but before the GM says the roll succeeds or fails. Once your droid uses this ability, it can't use it again until it finishes a short or long rest.

\n

Durability Module

\n

You enhance your droid's durability, granting the following benefits:

\n
    \n
  • When your droid rolls a Hit Die to regain hit points, the minimum number of hit points your droid can regain from the roll equals twice your droid's Constitution modifier (minimum of 2).
  • \n
  • Your droid's hit point maximum increases by an amount equal to twice its level when you install this protocol. Whenever your droid gains a level thereafter, its hit point maximum increases by an additional 2 hit points.
  • \n
\n

Emergency Mode

\n

Prerequisite: 15th level
Prerequisite: Back-Up Protocol
You modify your droid's back-up protocol. When your droid's back-up protocol is initiated, it can immediately use its reaction to make one attack roll against a target within range. If the target is the source of the damage that reduced your droid to 0, the attack roll has advantage.

\n

Energy Shield

\n

You install an energy shield in your droid. The energy shield has 1 charge. As an action, your droid can use 1 charge to cast the @Compendium[sw5e.techpowers.c7vvcY0lZDii7SOI]{Energy Shield} tech power.

\n

You can choose this modification multiple times. Each time you do so, the energy shield gains another charge, to a maximum of 3. The energy shield regains all expended charges after a long rest.

\n

Expertise Protocol

\n

Prerequisite: 5th level
You install a protocol in your droid that grants it expertise in a tool or skill. Choose a tool or skill that your droid is proficient in. Your droid gains expertise in it.

\n

False Combustion

\n

Prerequisite: Class II Droid
You install a panic protocol in your droid. As a reaction in response to taking damage, your droid can feign an explosion. For 1 minute, your droid appears to be destroyed to all outward inspection. A creature can use its action to inspect the droid and make an Intelligence (Investigation) check against your droid's tech save DC (8 + your droid's proficiency bonus + your droid's Intelligence modifier). If it succeeds, it becomes aware that your droid is still functioning.

\n

Fighting Style Protocol

\n

Your droid adopts a particular style of fighting as its specialty. Choose one of the Fighting Style options, detailed in chapter 6. Your droid can't take a Fighting Style option more than once, even if it later gets to choose again.

\n

Flamethrower

\n

You install a flamethrower in your droid. The flamethrower has 1 charge. As an action, your droid can cast the @Compendium[sw5e.techpowers.HoshRCTHW8vntDCg]{Jet of Flame} tech power or use 1 charge to cast the flame sweep tech power at 1st level. The saving throw is made against your droid's tech save DC (8 + your droid's proficiency bonus + your droid's Intelligence modifier).

\n

You can choose this modification multiple times. Each time you do so, the flamethrower gains another charge, to a maximum of 3. If the flamethrower has multiple charges, you can use multiple charges to cast @Compendium[sw5e.techpowers.6aZ0FG6HwrUO28WF]{Flame Sweep} at a higher level, 1 point per charge. The flamethrower regains all expended charges after a long rest.

\n

Four-Armed Combatant

\n

Prerequisite: Class IV Droid
You install two additional arms to improve your droid's combat capabilities, granting it four arms which it can use independently of one another. Your droid can only gain the benefit of items held by two of its arms at any given time, and once per round your droid can switch which arms it is benefiting from (no action required).

\n

While your droid has at least 3 arms free, it has a climbing speed equal to its walking speed.

\n

Heavy Plating

\n

Prerequisite: Medium Armor proficiency
Your droid gains proficiency in heavy armor. If your droid is already proficient in heavy armor, instead kinetic and energy damage that your droid takes from unenhanced weapons is reduced by an amount equal to its proficiency bonus.

\n

Light Plating

\n

Your droid gains proficiency in light armor. If your droid is already proficient in light armor, instead your droid's speed increases by 5 feet while light armor is integrated.

\n

Martial Protocol

\n

Prerequisite: 7th level, Class IV Droid
Your droid has martial training that allows it to perform special combat maneuvers. It gains the following benefits:

\n
    \n
  • It learns two maneuvers of your choice from among those available to the fighter class. If a maneuver it uses requires its target to make a saving throw to resist the maneuver's effects, the saving throw DC equals 8 + your droid's proficiency bonus + your droid's Strength or Dexterity modifier (your choice).
  • \n
  • Your droid has two superiority dice, which are d4s. These dice are used to fuel its maneuvers. A superiority die is expended when your droid uses it. It regain all of its expended superiority dice when you finish a short or long rest.
  • \n
\n

Medium Plating

\n

Prerequisite: Light Armor proficiency
Your droid gains proficiency in medium armor. If your droid is already proficient in medium armor, the maximum Dexterity bonus your droid can add to AC increases to 3 from 2 while medium armor is integrated.

\n

Memory Protocol

\n

Prerequisite: Class I Droid
Your droid can recall anything it has read in the past month that it understood. This includes but is not limited to books, maps, signs, and lists.

\n

Observant Protocol

\n

Prerequisite: 7th level
Prerequisite: Alarm Protocol
You modify the alarm module in your droid, granting the following benefits:

\n
    \n
  • If your droid can see a creature's mouth while it is speaking a language it understands, your droid can interpret what it's saying by reading its lips.
  • \n
  • Your droid is considered to have advantage when determining its passive Wisdom (Perception) and passive Intelligence (Investigation) scores.
  • \n
\n

Performance Protocol

\n

Prerequisite: 7th level, Class III Droid
You modify your droid's charisma chip, granting the following benefits:

\n
    \n
  • Your droid has advantage on Charisma (Performance) checks.
  • \n
  • Your droid can also use its bonus action to motivate an ally within 30 feet of it. Until the start of your droid's next turn, the ally can add the droid's Charisma modifier (minimum of +1) to the first attack roll, ability check, or saving throw they make. Your droid can use this feature a number of time equal to its Charisma modifier, and it regains all expended uses after it completes a long rest.
  • \n
\n

Powerful Droid

\n

Prerequisite: Class V Droid
Your droid count as one size larger when determining your carrying capacity and the weight you can push, drag, or lift.

\n

Powerful Grip

\n

Prerequisite: 7th level, Class V Droid
When your droid hits a creature with a melee weapon attack on its turn and has a free hand, it can use a bonus action to attempt to grapple the target. If it does so, and the grapple succeeds, your droid can make one additional attack against the target (no action required).

\n

Premium Power Core

\n

You improve the power core of your droid. Its Hit Die becomes a d8.

\n

Proficiency Protocol

\n

You install a protocol in your droid that grants it proficiency in a tool or skill. Your droid gains proficiency in a tool or skill of your choice.

\n

Prototype Power Core

\n

Prerequisite: d8 Hit Die
You further improve the power core of your droid. Its Hit Die becomes a d10.

\n

Repulsor Coil

\n

Prerequisite: 7th level, Class II Droid
You install repulsor coils in your droid's legs. Your droid gains a flying speed equal to its walking speed.

\n

Sensor Augmentation

\n

You augment your droid with an advanced sensor, granting the following benefits:

\n
    \n
  • Your droid has advantage on Wisdom (Perception) and Intelligence (Investigation) checks made to detect the presence of secret doors.
  • \n
  • Your droid has advantage on saving throws made to avoid or resist traps.
  • \n
  • Your droid has resistance to the damage dealt by traps.
  • \n
  • Your droid can search for traps while traveling at a normal pace, instead of only at a slow pace.
  • \n
\n

Stun Ray

\n

You install a stun ray in your droid. The stun ray has 1 charge. As an action, your droid can use 1 charge to cast the @Compendium[sw5e.techpowers.qHu258wCccbyajwo]{Hold Droid} or @Compendium[sw5e.techpowers.zXCnz8vBWC4fhvfw]{Paralyze Humanoid} tech power. The saving throw is made against your droid's tech save DC (8 + your droid's proficiency bonus + your droid's Intelligence modifier).

\n

You can choose this modification multiple times. Each time you do so, the stun ray gains another charge, to a maximum of 3. The stun ray regains all expended charges after a long rest.

\n

Techcasting Protocol

\n

Your droid learns two at-will tech powers, and one 1st-level tech power, which it casts at its lowest level. Once your droid casts it, your droid must finish a long rest before it can cast it again. Intelligence is your droid's techcasting ability for these powers. It does not require use of a wristpad for these powers.

\n

Toughness Module

\n

Prerequisite: 11th level
Prerequisite: Durability Module
You modify the durability module in your droid, granting the following benefit:

\n
    \n
  • Your droid becomes proficient in Constitution saving throws. If it is already proficient, it becomes proficient in another saving throw of your choice.
  • \n
  • Whenever your droid takes the Dodge action in combat, it can spend one Hit Die to heal itself. Roll the die, add its Constitution modifier, and it regains a number of hit points equal to the total (minimum of one).
  • \n
"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":"Techcaster"},"flags":{"dynamiceffects":{"equipActive":true,"alwaysActive":false,"effects":[{"modSpecKey":"data.traits.toolProf.value","value":"astro","mode":"+","targetSpecific":false,"id":1,"itemId":"avFn1m9oUpDgKAAF","active":false,"_targets":[]}]}},"img":"systems/sw5e/packs/Icons/Archetypes/Astrotech%20Engineering.webp","effects":[]} +{"_id":"avFn1m9oUpDgKAAF","name":"Astrotech Engineering","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Engineer","description":{"value":"

Astrotech Engineering

\n

Those engineers who choose the Astrotech Engineering discipline focus on utilizing their vast knowledge of droid technology to confuse, confound, and destroy enemy tech, droids and constructs.

\n

Bonus Proficiencies

\n

Astrotech Engineering: 3rd level

\n

You gain proficiency in astrotech's implements. Additionally, when you engage in crafting with astrotech's implements, the rate at which you craft doubles.

\n

Electronic Warfare Platform

\n

Astrotech Engineering: 3rd, 9th, and 17th level

\n

You learn to modify your astrotech's implements into a mobile electronic warfare platform. Over the course of a long rest, you can modify your astrotech's implements. You must have astrotech's implements in order to perform this modification.

\n

Your electronic warfare platform is enhanced, requires attunement, can only be used by you, and counts as a tech focus for your tech powers while you are attuned to it. Your electronic warfare platform has 4 modification slots, and it gains more at higher levels, as shown in the Modification Slots column of the engineer table. For each modification installed in excess of your proficiency bonus, your tech point maximum is reduced by 1. Over the course of a long rest, you can install, replace, or remove a number of modifications up to your Intelligence modifier (minimum of one).

\n

Some modification effects require saving throws. When you use such an effect from this class, the DC equals your tech save DC.

\n

Your electronic warfare platform comes equipped with electromagnetic projection and sensing systems, enabling three fields: ionizing, jamming, and targeting. As a bonus action, you can activate a field, which lasts for 1 minute, causing an effect determined by the field. Only one field can be active at a time, and you can end your field at any time on your turn, no action required. The range of your barriers increases to 15 feet at 9th level and 30 feet at 17th level. You can use these fields a combined number of times equal to your proficiency bonus, as shown in the engineer table. You regain all expended uses when you complete a long rest.

\n

Ionizing Field

\n

Whenever a droid or construct starts its turn within 5 feet of you, it gains 4 slowed levels and takes ion damage equal to half your engineer level (rounded down).

\n

Jamming Field

\n

Whenever you or a friendly creature within 10 feet of you are forced to make a saving throw by a tech power, the creature gains a bonus to the saving throw equal to your Intelligence modifier (minimum of +1).

\n

Targeting Field

\n

You and friendly creatures within 5 feet of you gain a bonus to the first ranged weapon damage rolls you make each round equal to your Intelligence modifier (minimum of+1).

\n

Potent Electromagnetism

\n

Astrotech Engineering: 3rd level

\n

Once per round, when you deal damage to a droid or construct, you can expend one use of your Potent Aptitude to deal an extra 2d6 damage to that target. The damage is the same type as the source of the damage.

\n

The damage increases when you reach certain levels in this class, increasing to 3d6 at 5th level, 5d6 at 10th level, and 8d6 at 15th level.

\n

Direct Controller

\n

Astrotech Engineering: 6th level

\n

Your mastery of droids improves your ability to manipulate them. When you cast a power that could affect only droids or constructs, and you only affect droids with the power, you can choose to treat the power as if cast at your Max Power Level.

\n

You can use this feature a number of times equal to your proficiency bonus, as shown in the engineer table. You regain all expended uses when you complete a long rest.

\n

Systems Hijack

\n

Astrotech Engineering: 14th level

\n

All droids, constructs, and creatures wearing or holding a techcasting focus within 120 feet are viable targets for any tech powers which you cast with a range of touch.

\n

Electromagnetic Burst

\n

Astrotech Engineering: 18th level

\n

As an action, you can end your field in an electromagnetic burst of power with an effect determined by the field you were generating.

\n

Once you've used this feature, you must complete a long rest before you can use it again.

\n

Ionizing Burst

\n

Choose up to 10 creatures of your choice that are within 60 feet. Each must make a Constitution saving throw. On a failed save, a target takes 14d6 ion damage and is stunned. On a success, it takes half damage and isn't stunned.

\n

Jamming Burst

\n

Choose up to 10 creatures of your choice that are within 60 feet. Once in the next minute, when a chosen creature fails a saving throw caused by a tech power or is hit by an attack power, the creature can choose to succeed on the saving throw or have the attack miss instead.

\n

Targeting Burst

\n

Choose up to 10 creatures of your choice that are within 60 feet. Each creature must succeed on a Constitution saving throw against your tech save DC or be blinded for 1d4+1 turns.

\n

Astrotech Modifications

\n

If a modification has prerequisites, you must meet them to install it. You can install the modification at the same time that you meet its prerequisites.

\n

AI Amplifier

\n

Prerequisite: 5th level

\n

You gain a +1 bonus to melee tech attack rolls. This bonus increases to +2 at 9th level and +3 at 13th level.

\n

AI Rangefinder

\n

Prerequisite: 5th level

\n

You gain a +1 bonus to ranged tech attack rolls. This bonus increases to +2 at 9th level and +3 at 13th level.

\n

Advanced Hardening

\n

Prerequisite: 15th level, Hardening

\n

You have resistance to ion damage.

\n

Advanced Grounding System

\n

Prerequisite: 15th level, Prototype Grounding System

\n

You have resistance to lightning damage.

\n

Advanced Jamming Phased Array

\n

Prerequisite: 15th level, Prototype Jamming Phased Array

\n

While your jamming field is active, any hostile creature within the field must make a Constitution saving throw at the end of each of its turns to maintain concentration on the power.

\n

Advanced Ionizing Phased Array

\n

Prerequisite: 15th level, Prototype Pulsating Phased Array

\n

While your ionizing field is active, your tech powers and class features ignore resistance to ion damage, and immunity to ion damage is instead treated as resistance from any creature within range of your field.

\n

Additionally, when you use your Ionizing Phased Array feature, you create a fourth blast.

\n

Advanced Targeting Phased Array

\n

Prerequisite: 15th level, Prototype Targeting Phased Array

\n

While your targeting field is active, as an action, you can allow a number of allies within the field up to your Intelligence modifier (minimum of one) to use their reaction to make a ranged weapon attack against a single target you can see.

\n

Once you've used this feature, you must complete a short or long rest before you can use it again.

\n

Droid Restraints

\n

When you would install a restraining bolt, you can do so in half the time. Additionally, when determining the save DC of a restraining bolt you control, you can use your tech save DC, if it would be higher than the item's DC.

\n

Flashlight Attachment

\n

You affix a targeted light to your electronic warfare platform. As a bonus action, you can toggle the light on or off. While on, your electronic warfare platform sheds bright light in a 60-foot cone.

\n

Frailcasting Controller

\n

Prerequisite: 5th level

\n

You gain a +1 bonus to the tech save DC of powers you cast that require a Strength or Constitution saving throw. This bonus increases to +2 at 9th level and +3 at 13th level.

\n

Grounding System

\n

You have advantage on saving throws against lightning damage.

\n

Hacked Communications

\n

As a bonus action, you may choose any number of creatures that you can see within 60 feet of you that have commlinks, headcomms, or other such communications devices. Each creature must succeed on a Constitution saving throw or take sonic damage equal to your Intelligence modifier (minimum of one). Additionally, on a failed save, their communication devices are disabled until rebooted.

\n

Once you've used this feature, you must complete a short or long rest before you can use it again.

\n

Hardening

\n

You have advantage on saving throws against ion damage.

\n

Intelligence Core Override

\n

Prerequisite: 9th level

\n

You can cast the override interface tech power at 5th level without spending tech points.

\n

Once you've used this feature, you must complete a long rest before you can use it again.

\n

Ionizing Phased Array

\n

Prerequisite: 5th level

\n

While your ionizing field is active, as an action, you can send forth blasts of directed ionic energy. Make two ranged tech attacks against targets within the field. These attacks can target the same creature or different ones. Make separate attack rolls for each blast. The attack deals 1d6 ion damage on a hit.

\n

Jamming Phased Array

\n

While your jamming field is active, as an action, you can choose a number of creatures concentrating on a power equal to your Intelligence modifier (a minimum of one) within your field, and force them to make a Concentration saving throw. If you cause at least one creature to lose concentration on a power using this feature, you can use your reaction to make all creatures that lost concentration take ion damage equal to your Intelligence modifier.

\n

Miniaturized Repulsor Coils

\n

Your electronic warfare platform can store 20 lb., not exceeding 1 cubic foot, which does not count towards your encumbrance.

\n

Prototype Transmitter

\n

Prerequisite: 7th level

\n

You further tweak your transmitter design, extending the area of your fields. When you activate a field you may choose to double the radius of your field.

\n

Once you've used this feature, you must complete a long rest before you can use it again.

\n

Prototype Grounding System

\n

Prerequisite: 7th level, Grounding System

\n

You have immunity to the shocked condition.

\n

Prototype Jamming Phased Array

\n

Prerequisite: 7th level, Jamming Phased Array

\n

While your jamming field is active, when a creature within your field attempts to cast a tech power, you can use your reaction to cast the tech override power at 3rd level without spending tech points. When you cast this power using this feature and you make the techcasting ability check as a part of this casting, you add your proficiency bonus to the check.

\n

Once you've used this feature, you must complete a short or long rest before you can use it again.

\n

Prototype Ionizing Phased Array

\n

Prerequisite: 7th level, Ionizing Phased Array

\n

While your ionizing field is active, when you cast a tech power or use a class feature that affects other creatures within the radius of your field, you can choose a number of them equal to 1 + the power's level. The chosen creatures automatically succeed on their saving throws against the power, and they take no damage if they would normally take half damage on a successful save.

\n

Additionally, when you use your Ionizing Phased Array feature, you create a third blast.

\n

Prototype Targeting Phased Array

\n

Prerequisite: 7th level, Target Marking Phased Array

\n

While your targeting field is active, as an action, you can cast the scramble interface power at 3rd level without spending tech points.

\n

Once you've used this feature, you must complete a short or long rest before you can use it again.

\n

Redundant Circuits

\n

Prerequisite: 15th level

\n

You can have two fields active at a time.

\n

Rendcasting Controller

\n

Prerequisite: 5th level

\n

You gain a +1 bonus to the tech save DC of powers you cast that requires a Dexterity or Intelligence saving throw. This bonus increases to +2 at 9th level and +3 at 13th level.

\n

Sensor Scramblers

\n

Prerequisite: 5th level

\n

All creatures within 10 feet of you become undetectable to electronic sensors and cameras. Anything these creatures are wearing or carrying is also undetectable, so long as it's on the creature's person. The creatures are still visible to regular vision.

\n

Once you've used this feature, you must complete a long rest before you can use it again.

\n

Stealth Field Generator

\n

Prerequisite: 7th level

\n

You integrate a portable, personal cloaking device into your electronic warfare platform. Activating or deactivating the generator requires a bonus action and, while active, you have advantage on Dexterity (Stealth) ability checks that rely on sight. The generator lasts for 1 minute. This effect ends early if you make an attack or cast a force or tech power.

\n

Once the generator has been activated, it can't be activated again until you finish a short or long rest.

\n

Targeting Phased Array

\n

Prerequisite: 5th level

\n

While your targeting field is active, as an action, you can choose a number of creatures equal to your Intelligence modifier you can see within 60 feet (a minimum of one creature), and force them to make a Dexterity saving throw. On a failed save, all ranged weapon attacks made by allies within your field against the target have advantage until the beginning of your next turn.

\n

Truelight Attachment

\n

Prerequisite: 11th level, Flashlight Attachment

\n

You modify your electronic warfare platform with a toggle allowing you to briefly gain enhanced sight. As a bonus action, you can activate the truesight feature of your electronic warfare platform. When toggled on, for the next minute your electronic warfare platform now automatically dispel illusions and can detect invisibility, as with truesight.

\n

Once you've used this feature, you must complete a short or long rest before you can use it again.

\n

Withercasting Controller

\n

Prerequisite: 5th level

\n

You gain a +1 bonus to the tech save DC of powers you cast that requires a Wisdom or Charisma saving throw. This bonus increases to +2 at 9th level and +3 at 13th level.

\n

X-Ray Targeting

\n

Prerequisite: 5th level

\n

You tweak your sensors to find weak points in thick armor. Your weapon attacks and tech powers deal double damage against constructs.

"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":"Techcaster"},"flags":{"dynamiceffects":{"equipActive":true,"alwaysActive":false,"effects":[{"modSpecKey":"data.traits.toolProf.value","value":"astro","mode":"+","targetSpecific":false,"id":1,"itemId":"avFn1m9oUpDgKAAF","active":false,"_targets":[]}]}},"img":"systems/sw5e/packs/Icons/Archetypes/Astrotech%20Engineering.webp","effects":[]} {"_id":"bBMsNrnCUOXGfb0h","name":"Cyclone Approach","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Berserker","description":{"value":"

Cyclone Approach

\n

The Cyclone Approach empowers the berserker’s ability to fight with weapons in each hand. Followers of this approach learn to move quickly to avoid attacks and can become a whirlwind of fury and steel, cleaving through hordes of enemies.

\n

Dual Wielder

\n

When you choose this approach at 3rd level, when you engage in Two-Weapon Fighting, you can add your Strength or Dexterity modifier (your choice) to the damage of your Two-Weapon Fighting attack as long as it doesn’t already include that modifier.

\n

Double Swing

\n

Also at 3rd level, once on each of your turns when you miss with an attack while raging, you can immediately make a melee attack with the weapon in your other hand.

\n

Twisting Winds

\n

At 6th level, your unpredictable movement makes you harder to hit and pin down. When you make a saving throw or ability checks to avoid being knocked prone, pushed, grappled, or restrained, it gains a bonus equal to your Strength or Dexterity modifier (your choice) as long as it doesn’t already include that modifier.

\n

Mighty Leap

\n

Starting at 10th level, the distance you can jump is doubled, and you do not provoke attacks of opportunity if you leave a hostile creature’s reach while jumping.

\n

Tornado

\n

At 14th level, you can become a tornado of attacks. When you take the Attack action on your turn, you can forgo one of your regular attacks to make a melee attack against any number of creatures within 5 feet of you, with a separate attack roll for each target. If you are wielding a separate melee weapon in each hand, each successful hit against a target deals damage equal to the damage dice of both weapons + your ability modifier + any other modifiers.

\n

You can use this feature a number of times equal to your Strength modifier (a minimum of once). You regain all expended uses when you finish a short or long rest.

"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":""},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Cyclone%20Approach.webp","effects":[]} {"_id":"c89hsFFZG4WGlYcV","name":"Zoologist Pursuit","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Scholar","description":{"value":"

Zoologist Pursuit

\n

Many academics develop an affinity for nature, studying the vast fauna that inhabit the different planets throughout the galaxy. Those scholars who choose the Zoologist Pursuit capitalize on their knowledge of animals, developing a strong bond with a companion with whom they gain an advantage on the battlefield.

\n

Wilderness Expert

\n

When you choose this pursuit at 3rd level, you gain proficiency your choice of Animal Handling or Nature skills. Additionally, when you make a Wisdom (Animal Handling) check, you gain a bonus to the check equal to your Intelligence modifier.

\n

Beast Companion

\n

Also at 3rd level, you learn to employ all the knowledge you've accumulated to forge a powerful bond with your own personal beast companion.

\n

Choose your beast, which is detailed at the end of this pursuit. Over the course of 8 hours, which can be done during a long rest, you can expend 500 cr worth of herbs and food to call forth an animal from the wilderness to serve as your companion.

\n

If your beast dies, or you want to bond with a different creature, you must first break the bond with your current beast companion. You may only have one beast companion at a time.

\n

Your beast gains a variety of benefits while it is bonded to you:

\n
    \n
  • The beast obeys your commands as best it can. It acts on your turn, and you determine its actions, decisions, attitudes, and so on. If you are incapacitated or absent, your beast acts on its own.
  • \n
  • Your beast's level equals your scholar level, and for each scholar level you gain after 3rd, your beast companion gains an additional hit die and increases its hit points accordingly.
  • \n
  • Your beast has the proficiency bonus of a player character of the same level.
  • \n
  • Whenever you gain the Ability Score Improvement feature in this class, your beast's abilities also improve. Your beast can increase one ability score of your choice by 2, or it can increase two ability scores of your choice by 1. As normal, your beast can't increase an ability score above 20 using this feature unless its description specifies otherwise.
  • \n
  • While your beast is the target of your Critical Analysis feature, it gains a bonus to ability checks, armor class, attack rolls, damage rolls, and saving throws equal to half your Intelligence modifier (rounded up).
  • \n
\n

Additional Maneuvers

\n

Lastly at 3rd level, you gain access to new maneuvers which reflect the progress of your studies into the biology and behavior of animals. Whenever you learn a new maneuver, you can choose from any of the following as well. The maneuvers are listed in alphabetical order.

\n

Loyal Bond

\n

Whenever you are hit with an attack, you can expend one superiority die to command your companion to immediately use its reaction and move up to its speed directly towards you. If it ends this movement within 5 feet of you, roll the superiority die. Your companion takes the damage instead of you, subtracting the amount you rolled from the total.

\n

Go Get 'Em

\n

While your companion is moving, you can expend a superiority die and add 5 times the number rolled to its movement speed.

\n

Pin Down

\n

When your beast attempts to grapple or knock a creature prone, you can expend a superiority die to give it direction as long as it can see or hear you. Roll a superiority die and add it to your beast's Strength (Athletics) check.

\n

Primal Endurance

\n

As an action, you can expend a superiority die to improve your beast's defense. Roll the die and add it to your beast's AC until the beginning of your next turn.

\n

Sic 'Em

\n

As an action, you can command your beast to savage a nearby enemy. Your beast can use its reaction to immediately move up to 10 feet and make one attack, adding the superiority dice to the damage roll on a hit.

\n

Spine-Chilling Howls

\n

As an action, you can expend one superiority die to command your beast to frighten another creature. The target must then succeed on a Wisdom saving throw against your Maneuver save DC or become frightened of both you and your beast for 1 minute.

\n

Wild Senses

\n

Whenever you make a Wisdom (Perception) or a Wisdom (Survival) check, you can request the aid of your beast by expending a superiority die, adding the number rolled to the check. You can use this maneuver before or after making the ability check, but before the results of the ability check are determined.

\n

Vicious Hunting

\n

Beginning at 6th level, your beast companion's strikes count as enhanced for the purpose of overcoming resistance and immunity to unenhanced attacks and damage.

\n

Creature Comprehension

\n

Starting at 9th level, when your beast makes an attack roll, ability check, or saving throw, you may expend a superiority die and apply the benefits of a maneuver you know from this class, as if you have taken the action yourself.

\n

Feral Ferocity

\n

Once you've reached 17th level, you have learned how to push your beast beyond its limits. If your beast is within 30 feet of you and can see or hear you, you can command it to enter a furious state. While raging, your beast gains the following benefits:

\n
    \n
  • Your beast has advantage on Strength checks and Strength saving throws if it is size Medium or larger.
  • \n
  • Your beast has advantage on Dexterity checks and Dexterity saving throws if it is size Small or smaller.
  • \n
  • When your hits with an attack, it deals bonus damage equal to your Intelligence modifier.
  • \n
  • Your beast has resistance to kinetic and energy damage.
  • \n
\n

Your beast's furious state lasts for 1 minute. It ends early if your beast is knocked unconscious. You can end your beast's furious state as a bonus action.

\n

Once you've used this feature, you can't use it again until you finish a long rest.

\n

Discoveries (Zoologist)

\n

When you select this pursuit, you gain access to new discoveries which reflect your studies in biology and behaviour of alien lifeforms. Whenever you learn a new discovery, you can choose from any of the following as well. The discoveries are listed in alphabetical order.

\n

Advantageous Companion

\n

When you make a Charisma (Intimidation) check against a creature that can see your beast companion, and your companion is size Medium or larger, you make the check with advantage.

\n

When you make a Charisma (Persuasion) check against a creature that can see your beast companion, and your companion is size Small or Tiny, you make the check with advantage.

\n

Colossal Companion

\n

Prerequisite: 15th level

\n

You can attempt to temporarily take control of a Huge beast. With the use of 10,000 cr worth of herbs and food, you can make a DC 15 Animal Handling check. On a success, the creature becomes your companion for 1d4 hours and gains the benefits of your Beast Companion feature. You can attempt to extend the duration by one hour by making additional Animal Handling checks. The DC for the first check is 20, and increases by 5 on each subsequent check. On a failure, the creature becomes hostile to you if it wasn't already and becomes immune to this feature for 24 hours.

\n

Holocam Attachment

\n

You have learned how to safely attach a holocam on the head of the companion. You learn the tracker droid interface tech power, and your beast becomes a valid target of this power.

\n

Neat Tricks

\n

Prerequisite: 5th level

\n

Your beast gains proficiency in one Strength or Dexterity skill of your choice. If your beast's size is Medium or larger and the chosen skill uses Strength, it has expertise in the chosen skill. If your beast's size is Small or smaller and the chosen skill uses Dexterity, it has expertise in the chosen skill.

\n

Protective Friend

\n

If a creature makes a melee attack against you or your companion, and your companion is within 5 feet of you, you can use your reaction to impose disadvantage on the attack roll.

\n

The More The Merrier

\n

Prerequisite: 7th level

\n

Whenever you attempt to call forth an animal as your companion, you can instead spend 1,000 cr worth of components and call forth a swarm of Tiny creatures. The swarm is composed of a number of creatures equal to your scholar level + your Intelligence modifier, and is size Medium. All of the creatures within the swarm act as a single creature.

\n

Generating Your Beast

\n

Choosing your beast companion is an integral part of being a Zoologist Scholar. Your beast takes a form of your choosing. Alternatively, your GM can choose what form your beast takes based on your environment.

\n

Once you've selected your type of beast, you assign your beast companion's ability scores using standard array (16, 14, 14, 12, 10, 8) as you see fit.

\n

Beast Features

\n

All beasts share the following traits.

\n

Hit Points

\n
    \n
  • Hit Dice: 1d4 per beast companion level
  • \n
  • Hit Points at 1st Level: 4 + your beast's Constitution modifier
  • \n
  • Hit Points at Higher Levels: 1d4 (or 3) + your beast's Constitution modifier per beast level after 1st
  • \n
\n

Proficiencies

\n
    \n
  • Languages: Your beast can understand simple commands spoken in two languages of your choice, as well as hand signals, but it can not speak
  • \n
  • Saving Throws: Choose one from Strength, Intelligence, or Charisma, and another from Dexterity, Constitution, or Wisdom
  • \n
  • Skills: Choose two from Acrobatics, Athletics, Intimidation, Perception, Performance, Survival, and Stealth
  • \n
\n

Features

\n
    \n
  • Armor Class: 10 + your beast's Dexterity modifier
  • \n
  • Bestial Traits: Your beast companion four bestial traits of your choice. It gains an additional trait at 5th level (5), 8th level (6), 11th level (7), 14th level (8), and 17th level (9). Whenever you gain a level in this class, you can exchange one trait for another one.
  • \n
  • Natural Weapon: Your beast companion attacks with a natural weapon, such as claws or a bite. On a hit, it deals 1d4 kinetic damage.
  • \n
  • Size: Tiny
  • \n
  • Speed: 20 ft.
  • \n
  • Type: Beast
  • \n
\n

Bestial Traits

\n

The traits are presented in alphabetical order.

\n

Aerial

\n

Your beast companion has a flying speed equal to its walking speed, and opportunity attacks made against it have disadvantage.

\n

Amphibious

\n

Your beast companion has a swimming speed equal to its walking speed, and it can breathe air and water.

\n

Burrower

\n

Your beast companion has a burrowing speed equal to its walking speed, and it has blindsight out to 10 feet.

\n

Charger

\n

If your beast moves at least half its speed straight towards a target before making a melee attack, it deals an additional 1d8 damage on a hit.

\n

Climber

\n

Your beast companion has a climbing speed equal to its walking speed, and it has advantage on Strength saving throws and Strength (Athletics) checks that involve climbing.

\n

Darkvision

\n

Your beast companion is accustomed to low-light environments. Your beast can see in dim light within 60 feet as if it were bright light, and in darkness as if it were dim light. Your beast can't discern color in darkness, only shades of gray.

\n

Evasive

\n

Your beast companion can take the Disengage action as a bonus action.

\n

Force Adept

\n

Prerequisite: Force Sensitive
Your beast companion knows one 2nd-level force power of your choice, and once per long rest it can cast it at 2nd-level without expending force points. Your beast's forcecasting ability is Wisdom or Charisma (depending on power alignment).

\n

Force Resistance

\n

Your beast companion has advantage on saving throws against force powers.

\n

Force Sensitive

\n

Your beast companion knows one 1st-level force power of your choice, and once per long rest it can cast it at 1st-level without expending force points. Your beast's forcecasting ability is Wisdom or Charisma (depending on power alignment).

\n

Grappler

\n

When your beast hits with a melee weapon attack, it can use a bonus action to attempt to grapple the target. On a success, the target is both grappled and restrained, and your beast can't attack again while it has a creature grappled.

\n

Heavy Hide

\n

Your beast companion's armor class becomes 14.

\n

Keen Hearing

\n

Your beast companion has advantage on Wisdom (Perception) checks that rely on hearing.

\n

Keen Sight

\n

Your beast companion has advantage on Wisdom (Perception) checks that rely on sight.

\n

Keen Smell

\n

Your beast companion has advantage on Wisdom (Perception) checks that rely on smell.

\n

Light Hide

\n

Your beast companion's armor class becomes 11 + it's Dexterity modifier.

\n

Medium Hide

\n

Your beast companion's armor class becomes 13 + it's Dexterity modifier, to a maximum of +2.

\n

Natural Camouflage

\n

When your beast companion attempts to hide, it can opt to not move on its turn. If it avoids moving, it is considered lightly obscured until it moves.

\n

Nimble Weapon

\n

Your beast companion can use Dexterity instead of Strength for its attack and damage rolls.

\n

Pack Tactics

\n

Your beast companion has advantage on an attack roll against a creature if at least one ally of your beast companion is within 5 feet of the creature and the ally isn't incapacitated.

\n

Pouncer

\n

If your beast moves at least half its speed straight toward a creature and hits it with a melee attack, the creature must make a Strength saving throw (DC = 8 + your beast's proficiency bonus + its Strength modifier). If the creature is larger than your beast, it makes this save with advantage. On a failed save, the creature is knocked prone, and your beast can make one additional attack against it as a bonus action.

\n

Powerful Build

\n

Your beast companion counts as one size larger when determining its carrying capacity and the weight it can push, drag, or lift.

\n

Rampager

\n

If your beast reduces a creature to 0 hit points with a melee attack on its turn, your beast and take a bonus action to move up to half its speed and make a melee attack.

\n

Ranged Weapon

\n

Your beast companion has a natural ranged weapon, such as a spitter or or tail spikes. It has a normal range of 30 feet and a long range of 90 feet, and on a hit it deal kinetic damage equal to its natural weapon damage die.

\n

Reach Weapon

\n

Your beast companion has a natural weapon with reach, such as a tail or wings. It has the reach property, and on a hit it deals kinetic damage equal to its natural weapon damage die.

\n

Size: Huge

\n

Prerequisite: Size Large
Your beast companion’s size is Huge. Its hit points increase by an amount equal to its level + 1, its Hit Die becomes a d12, its natural weapon damage die becomes a d12, and its walking speed increases to 40.

\n

Size: Large

\n

Prerequisite: Size Medium
Your beast companion's size is Large. Its hit points increase by an amount equal to its level + 1, its Hit Die becomes a d10, its natural weapon damage die becomes a d10, and it's walking speed increases to 35.

\n

Size: Medium

\n

Prerequisite: Size Small
Your beast companion's size is Medium. Its hit points increase by an amount equal to its level + 1, its Hit Die becomes a d8, its natural weapon damage die becomes a d8, and its walking speed increases to 30.

\n

Size: Small

\n

Your beast companion's size is Small. Its hit points increase by an amount equal to its level + 1, its Hit Die becomes a d6, its natural weapon damage die becomes a d6, and it's walking speed increases to 25.

\n

Sturdy-Legged

\n

Your beast companion's long jump is up to 20 feet and its high jump is up to 10 feet, with or without a running start, and it has advantage on Strength and Dexterity saving throws made against effects that would knock it prone.

\n

Swift

\n

Your beast companion can take the Dash action as a bonus action.

\n

Tremorsense

\n

Your beast companion gains tremorsense out to 30 feet.

\n

Venomous Weapon

\n

When your beast companion deals damage to a creature, it must make a Constitution saving throw (DC = 8 + your beast's proficiency bonus + your beast's Constitution modifier) or become poisoned until the end of its next turn.

"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":""},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Zoologist%20Pursuit.webp","effects":[]} {"_id":"cROcc25Zj1MT6Yf6","name":"Form I: Shii-Cho","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Guardian","description":{"value":"

Form I: Shii-Cho

\n

Form I, also known as Determination Form, uses wild, unpredictable attacks designed to distract and disarm their foes. Those guardians who focus on Shii-Cho Form make seemingly random, yet deliberate, attacks to knock their opponents off-balance.

\n

Form Basics

\n

When you choose this form as your focus at 3rd level, you learn the basics of the chosen form. You gain the @Compendium[sw5e.lightsaberform.rUL9jO0rPLWqOliO]{Shii-Cho Lightsaber Form}, detailed in Chapter 6 of the Player’s Handbook. If you already know this form, you can instead choose another lightsaber form. You can't take a lightsaber form option more than once, even if you later get to choose again.

\n

The Way of the Sarlaac

\n

Also at 3rd level, as a bonus action, you can enter a frenetic stance for one minute. While in this stance, the first time you hit a creature with a melee weapon attack on your turn, it has disadvantage on the next melee attack roll it makes against you before the start of your next turn. Additionally, if that creature is within 5 feet of you, it must make a Strength saving throw (DC = 8 + your proficiency bonus + your Strength or Dexterity modifier). On a failed save, it is pushed back 5 feet, and you can immediately move into the space it just vacated without provoking opportunity attacks.

\n

This effect ends early if you are incapacitated or die. Once you've used this feature, you can't use it again until you finish a long rest.

\n

Channel the Force

\n

Lastly at 3rd level, you gain the following Channel the Force option.

\n

Disarming Slash

\n

When you hit a creature with a melee weapon attack, you can expend a use of your Channel the Force (no action required) to attempt to disarm the target, forcing it to drop one item of your choice that it's holding. The creature must make a Strength saving throw. On a failed save, it drops the object you choose. If you are within 5 feet of the target, and you have a free hand, you can catch the item. Otherwise, the object lands at its feet.

\n

Unpredictable Motion

\n

Beginning at 7th level, while you are wielding a melee weapon, opportunity attacks against you are made at disadvantage.

\n

Sarlaac Sweep

\n

Starting at 15th level, when a creature moves to within 5 feet of you, you can use your reaction to make a melee weapon attack against that creature. If the attack hits, you can attempt to damage another creature within 5 feet of the original target and within your reach. If the original attack roll would hit the second creature, it takes damage equal to your Strength or Dexterity modifier (your choice). The damage is of the same type dealt by the original attack.

\n

Master of Determination

\n

At 20th level, the erratic fluidity of your movement confounds even the most determined of foes. Your Strength or Dexterity and Wisdom or Charisma scores (your choice) increase by 2. Your maximum for these scores increases by 2. Additionally, you can use your action to gain the following benefits for 1 minute:

\n
    \n
  • You have resistance to kinetic and energy damage from unenhanced weapons.
  • \n
  • Attack rolls made against you can't have advantage.
  • \n
  • When more than one creature is within 5 feet of you, you gain a bonus to your Armor Class equal to the number of creatures within 5 feet of you, up to your Wisdom or Charisma modifier (your choice, minimum of one).
  • \n
  • When you use your Sarlaac Sweep feature, you have advantage on the attack roll, and you can apply the bonus damage to every creature within 5 feet of you.
  • \n
\n

This effect ends early if you are incapacitated or die. Once you've used this feature, you can't use it again until you finish a long rest.

"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":"Forcecaster"},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Shii-Cho%20Form.webp","effects":[]} diff --git a/packs/packs/forcepowers.db b/packs/packs/forcepowers.db index c68e8319..062b9b3a 100644 --- a/packs/packs/forcepowers.db +++ b/packs/packs/forcepowers.db @@ -8,6 +8,7 @@ {"_id":"28uF6yN4NLoc1Mf7","name":"Master Speed","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Knight Speed

\n

Choose up to two willing creatures that you can see within range. Until the power ends, each targets’ speed is doubled, they gain a +2 bonus to AC, they have advantage on Dexterity saving throws, and they gain an additional action on each of their turns. That action can be used only to take the Attack (one weapon attack only), Dash, Disengage, Hide, or the Use an Object Action.

\n

When the power ends, each target can’t move or take actions until after its next turn, as a wave of lethargy sweeps over it.

\n

Force Potency. When you cast this power using a force slot of 8th-level or higher, you can target one additional creature for each slot level above 7th.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":2,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":7,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1 additional creature"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Master%20Speed.webp","effects":[]} {"_id":"2EdyvNwmyRD9yhmn","name":"Maddening Darkness","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Shroud of Darkness

\n

Terrifying darkness spreads from a point you choose within range to fill a 60-foot-radius sphere until the power ends. The darkness spreads around corners. A creature with darkvision can’t see through this darkness. Unenhanced light, as well as light created by powers of 8th level or lower, can’t illuminate the area.

\n

Shrieks, gibbering, and mad laughter can be heard within the sphere. Whenever a creature starts its turn in the sphere, it must make a Wisdom saving throw, taking 8d8 psychic damage on a failed save, or half as much damage on a successful one.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Have Shroud of Darkenss"},"duration":{"value":10,"units":"minute"},"target":{"value":60,"units":"ft","type":"radius"},"range":{"value":150,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["8d8","psychic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":8,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Maddening%20Darkness.webp","effects":[]} {"_id":"2MaAEt5XSnjEurWK","name":"Valor","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Resistance

\n

You bless up to three creatures of your choice within range. Whenever a target makes an attack roll or a saving throw before the power ends, the target can roll a d4 and add the number rolled to the attack roll or saving throw.

\n

Force Potency. When you cast this power using a force slot of 2nd level or higher, you can target one additional creature for each slot level above 1st.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":3,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d4","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1 additional creature"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Valor.webp","effects":[]} +{"name":"Wakefulness","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"

Prerequisite: Breath Control

\n

You use the Force to control your body's functions. For the duration, you gain the following benefits:

\n
    \n
  • You ignore the effects of any levels of exhaustion you have.
  • \n
  • You do not need to eat, drink, or sleep. You can't be forced to sleep by any means. To gain the benefits of a long rest, you can spend all 8 hours doing light activity, such as reading and keeping watch.
  • \n
\n

If you still have any levels of exhaustion when this power ends, you must make a DC 15 Constitution saving throw. On a failed save, you gain one level of exhaustion.

","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":8,"units":"hour"},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.ROvBWSja1cjiDOO0"}},"img":"systems/sw5e/packs/Icons/Force%20Powers/Wakefulness.webp","effects":[],"_id":"2obXjKljRPeuoqcl"} {"_id":"3IoaQSSoAtFsoavO","name":"Master Force Barrier","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Improved Force Barrier

\n

This power massively bolsters your allies with toughness and resolve. Creatures of your choice in a 30-foot radius around you when you cast this power gain the following benefits:

\n
    \n
  • The creature sheds dim light in a 5-foot radius.
  • \n
  • The creature has advantage on all saving throws
  • \n
  • Other creatures have disadvantage on attack rolls against them.
  • \n
  • When a dark side creature hits them with a melee attack, that creature must make a Constitution saving throw or be blinded until the power ends.
  • \n
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":30,"units":"ft","type":"radius"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":8,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Master%20Force%20Barrier.webp","effects":[]} {"_id":"3KGdCbHATzaghiwo","name":"Force Propel","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Force Push/Pull

\n

Choose one or more creatures or objects not being worn or carried within 60 feet that weigh up to a combined total of 15 pounds. The creatures or objects immediately move 60 feet in a direction of your choice. If the creatures or objects end this movement in the air, they immediately fall to the ground. If the creatures or objects collide with any one target during its travel, both the creatures or objects and the target take 3d8 kinetic damage. If the target is a creature, it must make a Dexterity saving throw. On a failed save, it takes 3d8 kinetic damage, or half as much on a successful one.

\n

Force Potency. When you cast this power using a force slot of 2nd level or higher, the maximum weight increases by 15 pounds and the damage increases by 1d8 for each slot level above 1st.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":null,"units":"any","type":"object"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["3d8","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d8"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Propel.webp","effects":[]} {"_id":"3odfJsD1RezuxzDG","name":"Mass Hysteria","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Hysteria

\n

Drawing on the deepest fears of a group of creatures, you create illusory creatures in their minds, manifesting their worst nightmares as an implacable threat visible only to them. Each creature in a 30-foot-radius sphere is frightened for the duration of the power. At the end of each of the frightened creature’s turns, it must succeed on a Wisdom saving throw or take 5d10 psychic damage. On a successful save, the power ends for that creature. This power has no effect on droids or constructs.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Have Hysteria Power"},"duration":{"value":1,"units":"minute"},"target":{"value":30,"units":"ft","type":"radius"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["5d8","psychic"]],"versatile":""},"formula":"","save":{"ability":"wis","dc":null,"scaling":"power"},"level":9,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Mass%20Hysteria.webp","effects":[]} @@ -19,7 +20,7 @@ {"_id":"562ItvjZZHhSmqeP","name":"Morichro","permission":{"default":0,"XA86Wm4MjngMySbc":3},"type":"power","data":{"description":{"value":"

Prerequisite: Cloud Mind

\n

You touch a willing creature and put it into a cataleptic state that is indistinguishable from death.

\n

For the power’s duration, or until you use an action to touch the target and dismiss the power, the target appears dead to all outward inspection and to powers used to determine the target’s status. The target is blinded and incapacitated, and its speed drops to 0. The target has resistance to all damage except psychic damage. If the target is diseased or poisoned when you cast the power, or becomes diseased or poisoned while under the power’s effect, the disease and poison have no effect until the power ends. This power has no effect on droids or constructs.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":null,"units":"","type":""},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":3,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Morichro.webp","effects":[]} {"_id":"5Gxqh2j4A258jCSY","name":"Revitalize","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Spare the Dying

\n

You return a dead creature you touch to life, provided that it has been dead no longer than 10 minutes. If the creature’s soul is both willing and at liberty to rejoin the body, the creature returns to life with 1 hit point.

\n

This power also neutralizes any poisons and cures diseases that affected the creature at the time it died.

\n

This power closes all mortal wounds, but it doesn’t restore missing body parts. If the creature is lacking body parts or organs integral for its survival, its head for instance, the power automatically fails.

\n

Coming back from the dead is an ordeal. The target takes a -4 penalty to all attack rolls, saving throws, and ability checks. Every time the target finishes a long rest, the penalty is reduced by 1 until it disappears.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"minute","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":5,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Revitalize.webp","effects":[]} {"_id":"5koTRABu4ER8g6Wo","name":"Horror","permission":{"default":0,"9BhUyjgxIxogl2ot":3},"type":"power","data":{"description":{"value":"

Prerequisite: Fear

\n

You project a phantasmal image of a creature’s worst fears. Each creature in a 30-foot cone must succeed on a Wisdom saving throw or drop whatever it is holding and become frightened for the duration. This power has no effect on constructs or droids.

\n

While frightened by this power, a creature must take the Dash action and move away from you by the safest available route on each of its turns, unless there is nowhere to move. If the creature ends its turn in a location where it doesn’t have line of sight to you, the creature can make a Wisdom saving throw. On a successful save, the power ends for that creature.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":30,"units":"ft","type":"cone"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"wis","dc":null,"scaling":"power"},"level":3,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Horror.webp","effects":[]} -{"_id":"5lOXTZUiRmL7aZi9","name":"Convulsion","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Tremor

\n

Choose a point you can see on the ground within range. A fountain of churned earth and stone erupts in a 20-foot cube centered on that point. Each creature in that area must make a Dexterity saving throw. A creature takes 3d12 kinetic damage on a failed save, or half as much damage on a successful one. Additionally, the ground in that area becomes difficult terrain until cleared. Each 5-foot-square portion of the area requires at least 1 minute to clear by hand.

\n

Force Potency. When you cast this power using a force slot of 4th level or higher, the damage increases by 1d12 for each slot level above 3rd.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":20,"units":"ft","type":"cube"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["3d12",""]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d12"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Convulsion.webp","effects":[]} +{"_id":"5lOXTZUiRmL7aZi9","name":"Convulsion","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Tremor

\n

Choose a point you can see on the ground within range. A fountain of churned earth and stone erupts in a 20-foot cube centered on that point. Each creature in that area must make a Dexterity saving throw. A creature takes 3d12 kinetic damage on a failed save, or half as much damage on a successful one. Additionally, the ground in that area becomes difficult terrain until cleared. Each 5-foot-square portion of the area requires at least 1 minute to clear by hand.

\n

Force Potency. When you cast this power using a force slot of 4th level or higher, the damage increases by 1d12 for each slot level above 3rd.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":20,"units":"ft","type":"cube"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["3d12",""]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":3,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d12"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Convulsion.webp","effects":[]} {"_id":"5o01ADUmNyzy470b","name":"Force Disarm","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

You select a weapon or object being worn or carried by a Large or smaller creature within range. The creature must make a Strength or Dexterity saving throw (the creature chooses the ability to use). If the item is being worn, this save is made with disadvantage. On a failed save, the creature takes 1d4 force damage and the item is pulled directly to you. If you have a free hand, you catch the weapon. Otherwise, it lands at your feet.

\n

This power’s damage increases by 1d4 when you reach 5th level (2d4), 11th level (3d4), and 17th level (4d4).

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":1,"units":"","type":"object"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4","force"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"atwill","formula":"1d4"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Disarm.webp","effects":[]} {"_id":"5raHPlhouud0Jpr4","name":"Telekinetic Storm","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Turbulence

\n

You stir the Force around you, creating a turbulent field of telekinetic energy that buffets enemies around you. The field extends out to a distance of 15 feet around you for the duration.

\n

When you cast this power, you can designate any number of creatures you can see to be unaffected by it. An affected creature’s speed is halved in the area, and when the creature enters the area for the first time on a turn or starts its turn there, it must make a Constitution saving throw. On a failed save, the creature takes 3d8 force damage. On a successful save, the creature takes half as much damage.

\n

Force Potency. When you cast this power using a force power slot of 4th level or higher, the damage increases by 1d8 for each slot level above 3rd.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":15,"units":"ft","type":"radius"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["3d8","force"]],"versatile":""},"formula":"","save":{"ability":"con","dc":null,"scaling":"power"},"level":3,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1d8"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Telekinetic%20Storm.webp","effects":[]} {"_id":"5tAwilkRcZsTc62q","name":"Skill Empowerment","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Your power with the Force deepens a creature’s understanding of its own talent. You touch one willing creature and give it expertise in one skill of your choice; until the power ends, the creature doubles its proficiency bonus for ability checks it makes that use the chosen skill.

\n

You must choose a skill in which the target is proficient and that isn’t already benefiting from an effect, such as Expertise, that doubles its proficiency bonus.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":5,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Skill%20Empowerment.webp","effects":[]} @@ -49,10 +50,12 @@ {"_id":"C2ROn2KK2QTLm9DW","name":"Guidance","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

You touch one willing creature. Once before the power ends, the target can roll a d4 and add the number rolled to one ability check of its choice. It can roll the die before or after making the ability check. The power then ends.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d4","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Guidance.webp","effects":[]} {"_id":"C5UBI7f4LoNg2dPM","name":"Force Meld","permission":{"default":0,"XA86Wm4MjngMySbc":3},"type":"power","data":{"description":{"value":"

Prerequisite: Force Whisper

\n

You forge a telepathic link among up to eight willing creatures of your choice within range, psychically linking each creature to all the others for the duration. Droids, constructs, and creatures with Intelligence scores of 2 or less aren’t affected by this power.

\n

Until the power ends, the targets can communicate telepathically through the bond whether or not they have a common language. The communication is possible over any distance, though it can’t extend beyond a single planet.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":8,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":5,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Meld.webp","effects":[]} {"_id":"CJR5SlWKoaCtY1bq","name":"Spare the Dying","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

You touch a living creature that has 0 hit points. The creature becomes stable. This power has no effect on droids or constructs.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Spare%20the%20Dying.webp","effects":[]} +{"name":"Telepathic Link","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"

Prerequisite: Force Whisper

\n

You establish a telepathic link with one willing humanoid you touch. Until the power ends, whenever you and the target can see each other, each of you can communicate with the other via telepathy.

\n

You don’t need to share a language with a creature for it to understand your telepathic utterances, and the creature understands you even if it lacks a language. The creature can respond to you telepathically as well, but it must understand at least one language in order to communicate this way.

\n

Force Potency. When you cast this power using a force slot of 3rd level or higher, the duration increases to Concentration, up to 1 hour, and if either you or the creature you are linked to are surprised, while both of you can see each other, the surprised creature can still act normally during the surprise round, as if it were not surprised.

","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.ribj4iIyIwS74McS"}},"img":"systems/sw5e/packs/Icons/Force%20Powers/Telepathic%20Link.webp","effects":[],"_id":"CjHXQcv4UPNIcIg0"} {"_id":"CkMXwj0l0H0rD9md","name":"Mass Malacia","permission":{"default":0,"XA86Wm4MjngMySbc":3},"type":"power","data":{"description":{"value":"

Prerequisite: Malacia

\n

Each creature in a 30-foot cube within range must make a Wisdom saving throw. On a failed save, the creature becomes charmed for the duration. While charmed by this power, the creature is incapacitated and has a speed of 0.

\n

The power ends for an affected creature if it takes any damage or if someone else uses an action to shake the creature out of its stupor. This power has no effect on droids or constructs.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":30,"units":"ft","type":"cube"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"wis","dc":null,"scaling":"power"},"level":3,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Mass%20Malacia.webp","effects":[]} {"_id":"CkwLRMWRdXp1ZOIh","name":"Greater Saber Throw","permission":{"default":0,"XA86Wm4MjngMySbc":3},"type":"power","data":{"description":{"value":"

Prerequisite: Improved Saber Throw

\n

As a part of the action used to cast this power, you must make a ranged force attack with a lightweapon or vibroweapon against one target within the power’s range, otherwise the power fails. On a hit, the target takes 6d8 damage of the same type as the weapon’s damage and must make a Constitution saving throw against an additional effect depending on your choice of casting ability:

\n

Wisdom. The target takes an additional 4d6 force damage and it gains four levels of slowed until the end of its next turn. On a success, the target takes half as much damage and its speed isn’t reduced.

\n

Charisma. The target takes an additional 4d6 necrotic damage and cannot regain hit points until the end of its next turn. On a success, the target takes half as much damage and its healing capability is unaffected.

\n

The weapon then immediately returns to your hand.

\n

Force Potency. When you cast this power using a force slot of 6th level or higher, the force or necrotic damage increases by 1d6 for each slot above 5th.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rpak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["6d8",""]],"versatile":""},"formula":"4d6","save":{"ability":"con","dc":null,"scaling":"power"},"level":5,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Greater%20Saber%20Throw.webp","effects":[]} {"_id":"CsLjEyQtegNiXKIZ","name":"Tremor","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Burst

\n

You cause a tremor in the ground within range. Each creature other than you in a 5-foot-radius sphere centered on that point must make a Dexterity saving throw. On a failed save, a creature takes 1d6 kinetic damage and is knocked prone. On a successful save, the creature takes half as much damage and isn’t knocked prone. If the ground in that area is loose earth or stone, it becomes difficult terrain until cleared, with each 5-foot-diameter portion requiring at least 1 minute to clear by hand.

\n

Force Potency. When you cast this power using a force slot of 2nd level or higher, the damage increases by 1d6 for each slot level above 1st.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":5,"units":"ft","type":"radius"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Tremor.webp","effects":[]} {"_id":"CuAbwhIt3j2V30Ey","name":"Saber Reflect","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

In response to being attacked, you raise your weapon to attempt to deflect. When you you use this power, the damage you take from the attack is reduced by 1d6. If you reduce the damage to 0, you’re wielding a lightweapon or vibroweapon, and the damage is energy or ion, you can reflect the attack at a target within range as part of the same reaction. Make a ranged force attack at a target you can see. The attack has a normal range of 20 feet and a long range of 60 feet. On a hit, the target takes the triggering attack’s normal damage.

\n

If you would have resistance to the triggering damage, resistance is applied before the damage reduction.

\n

The power’s damage reduction increases by 1d6 when you reach 5th level (2d6), 11th level (3d6), and 17th level (4d6).

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"reaction","cost":1,"condition":"which you take in response to being hit by a ranged attack"},"duration":{"value":null,"units":"inst"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d6","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"atwill","formula":"1d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Saber%20Reflect.webp","effects":[]} +{"name":"Tapas","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"

You use the Force to inure yourself to the elements. For the duration, you are considered adapted to hot and cold climates.

","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":24,"units":"hour"},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.Heu6br9Yn7ZOVnnA"}},"img":"systems/sw5e/packs/Icons/Force%20Powers/Tapas.webp","effects":[],"_id":"D9V8ShFjzvenV8ud"} {"_id":"DFpooO6icOfTdG5C","name":"Master Feedback","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Greater Feedback

\n

You unleash the power of your mind to blast the intellect of up to ten creatures of your choice that you can see within range. Creatures that have an Intelligence score of 2 or lower are unaffected.

\n

Each target must make an Intelligence saving throw. On a failed save, a target takes 14d6 psychic damage and is stunned. On a successful save, a target takes half as much damage and isn’t stunned.

\n

A stunned target can make a Wisdom saving throw at the end of each of its turns. On a successful save, the stunning effect ends.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Have Greater Feedback Power"},"duration":{"value":null,"units":"inst"},"target":{"value":10,"units":"","type":"creature"},"range":{"value":90,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["14d6","psychic"]],"versatile":""},"formula":"","save":{"ability":"int","dc":null,"scaling":"power"},"level":8,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Master%20Feedback.webp","effects":[]} {"_id":"ENNhFJKCcxrg481J","name":"Improved Phasestrike","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Phasestrike

\n

Choose up to five creatures you can see within range. Make a melee force attack against each one. On hit, a target takes 6d10 force damage. You can then teleport to an unoccupied space you can see within 5 feet of one of the creatures you chose.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":5,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"mpak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["6d10","force"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":5,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Improved%20Phasestrike.webp","effects":[]} {"_id":"EXK4sg6gKXfMnLnf","name":"Battle Meditation","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

You exude an aura out to 5 feet that boosts the morale and overall battle prowess you and your allies while simultaneously reducing the opposition’s combat-effectiveness by eroding their will to fight.

\n

Whenever you or a friendly creature within your meditation makes an attack roll or a saving throw, they can roll a d4 and add the number rolled to the attack roll or saving throw.

\n

Whenever a hostile creature enters your meditation or starts its turn there, it must make a Charisma saving throw. On a failed save, it must roll a d4 and subtract the number rolled from each attack roll or saving throw it makes before the end of your next turn. On a successful save, it is immune to this power for 1 day.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":5,"units":"ft","type":"radius"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d4","save":{"ability":"cha","dc":null,"scaling":"power"},"level":2,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Battle%20Meditation.webp","effects":[]} @@ -67,7 +70,7 @@ {"_id":"G8UVHP4MXW6Dudky","name":"Phasestrike","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Until the power ends, your movement doesn’t provoke opportunity attacks.

\n

Once before the power ends, you can give yourself advantage on one weapon attack roll on your turn. That attack deals an extra 1d8 force damage on a hit. Whether you hit or miss, your walking speed increases by 30 feet until the end of that turn.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8","force"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Phasestrike.webp","effects":[]} {"_id":"GBJRx32gU9xU1Q6s","name":"Improved Feedback","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Feedback

\n

You unleash a blast of psychic energy at a target within range. If the target can hear you (though it need not understand you), it must succeed on an Intelligence saving throw. On a failed save, it takes 3d6 psychic damage and must immediately use its reaction, if available, to move as far as its speed allows away from you. The creature doesn’t move into obviously dangerous ground, such as a fire or a pit. On a successful save, the target takes half as much damage and doesn’t have to move away. A deafened creature automatically succeeds on the save.

\n

Force Potency. When you cast this power using a force slot of 2nd level or higher, the damage increases by 1d6 for each slot level above 1st.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Have Feedback Power"},"duration":{"value":null,"units":"inst"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["3d6","psychic"]],"versatile":""},"formula":"","save":{"ability":"int","dc":null,"scaling":"power"},"level":1,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Improved%20Feedback.webp","effects":[]} {"_id":"H51uSnFY6eyBTXRJ","name":"Stasis","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Stun

\n

Choose a creature that you can see within range. The target must succeed on a Wisdom saving throw or be paralyzed for the duration. At the end of each of its turns, the target can make another Wisdom saving throw. On a success, the power ends on the target. This power has no effect on droids or constructs.

\n

Force Potency. When you cast this power using a force slot of 6th level or higher, you can target an additional creature for each slot level above 5th. The creatures must be within 30 feet of each other when you target them.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":90,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"wis","dc":null,"scaling":"power"},"level":5,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1 additional creature"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Stasis.webp","effects":[]} -{"_id":"HPtgWP83jKSyFWFp","name":"Aura of Purity","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Restoration

\n

Purifying energy radiates from you in a 30-foot radius. Until the power ends, the aura moves with you, centered on you. Each nonhostile creature in the aura (including you) can’t become diseased, has resistance to poison damage, and has advantage on saving throws against effects that cause any of the following conditions: blinded, charmed, deafened, frightened, paralyzed, poisoned, and stunned.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":30,"units":"ft","type":"radius"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":4,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Aura%20of%20Purity.webp","effects":[]} +{"_id":"HPtgWP83jKSyFWFp","name":"Aura of Purity","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Restoration

\n

Purifying energy radiates from you in a 30-foot radius. Until the power ends, the aura moves with you, centered on you. Each nonhostile creature in the aura (including you) can’t become diseased, has resistance to poison damage, and has advantage on saving throws against effects that cause any of the following conditions: blinded, charmed, deafened, frightened, paralyzed, poisoned, and stunned.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":30,"units":"ft","type":"radius"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":"0","max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":4,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Aura%20of%20Purity.webp","effects":[]} {"_id":"Hgn1yHJsMzp5qKr5","name":"Force Reflect","permission":{"default":0,"XA86Wm4MjngMySbc":3},"type":"power","data":{"description":{"value":"

Prerequisite: Saber Reflect

\n

In response to being attacked, you attempt to deflect the attack with the Force. When you use this power, the damage you take from the attack is reduced by 1d10. If you reduce the damage to 0 and the damage is energy, force, ion, kinetic, lightning, necrotic, or sonic, you can reflect the attack at a target within range as part of the same reaction. Make a ranged force attack at a target you can see. The attack has a normal range of 30 feet and a long range of 90 feet. On a hit, the target takes the triggering attack’s normal damage.

\n

Force Potency. When you cast this power using a force slot of 2nd level or higher, the damage reduction increases by 1d10 for each slot level above 1st.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"reaction","cost":1,"condition":"which you take in response to being hit by a ranged attack"},"duration":{"value":null,"units":""},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rpak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d10","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d10"}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Reflect.webp","effects":[]} {"_id":"HqxcYlFOZJZ91a98","name":"Precognition","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Danger Sense

\n

Your mastery of the Force gives you a limited ability to see into the immediate future. For the duration, you can’t be surprised and you have advantage on attack rolls, ability checks, and saving throws. Additionally, other creatures have disadvantage on attack rolls against you for the duration.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"minute","cost":1,"condition":""},"duration":{"value":8,"units":"hour"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":9,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Precognition.webp","effects":[]} {"_id":"HvyTel83im9br37p","name":"Force Leap","permission":{"default":0,"XA86Wm4MjngMySbc":3},"type":"power","data":{"description":{"value":"

Until the end of your next turn, you can use your forcecasting ability score instead of your Strength score when you jump, and always count as having made a running start before jumping.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":1,"units":"round"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":null,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Leap.webp","effects":[]} @@ -100,7 +103,7 @@ {"_id":"NrMpYXddHnoPKHT2","name":"Force Lightning Cone","permission":{"default":0,"9BhUyjgxIxogl2ot":3},"type":"power","data":{"description":{"value":"

Prerequisite: Force Chain Lightning

\n

Lightning arcs from your hands. Each creature in a 60-foot cone must make a Dexterity saving throw. A creatures takes 12d6 lightning damage on a failed save, or half as much on a successful one.

\n

Force Potency. When you cast this power using a force slot of 8th level or higher, the damage increases by 2d6 for each slot level above 7th.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":60,"units":"ft","type":"cone"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["12d6","lightning"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":7,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":"2d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Cone%20Lightning.webp","effects":[]} {"_id":"Ns34rjyKzqeydYBH","name":"Sense Emotion","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

You attune your senses to pick up the emotions of others for the duration. When you cast the power, and as your action on each turn until the power ends, you can focus your senses on one humanoid you can see within 30 feet of you. You instantly learn the target’s prevailing emotion, whether it’s love, anger, pain, fear, calm, or something else. If the target isn’t actually humanoid or it is immune to being charmed, you sense that it is calm.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":null,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Sense%20Emotion.webp","effects":[]} {"_id":"O5J8TTpTW7eHLDgG","name":"Sense Force","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

For the duration, you sense the use of the Force, or its presence in an inanimate object within 30 feet of you. If you sense the Force in this way, you can use your action to determine the direction from which it originates and, if it’s in line of sight, you see a faint aura around the person or object from which the Force emanates.

\n

Force Potency. When you cast this power using a 3rd-level force slot, the range increases to 60 feet. When you use a 5th-level force slot, the range increases to 500 feet. When you use a 7th-level force slot, the range increases to 1 mile. When you use a 9th-level force slot, the range increases to 10 miles.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":null,"units":"","type":"self"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Sense%20Force.webp","effects":[]} -{"_id":"OGLh7pBroWR6te2k","name":"Armor of Abeloth","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

A protective force surrounds you, manifesting as shimmering light that covers you and your gear. You gain 5 temporary hit points for the duration. If a creature hits you with a melee attack while you have these hit points, the creature takes 5 psychic damage.

\n

Force Potency. When you cast this power using a force slot of 2nd level or higher, both the temporary hit points and the psychic damage increase by 5 for each slot.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"drk","components":{"value":"","concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Armor%20of%20Abeloth.webp","effects":[]} +{"_id":"OGLh7pBroWR6te2k","name":"Armor of Abeloth","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

A protective force surrounds you, manifesting as shimmering light that covers you and your gear. You gain 5 temporary hit points for the duration. If a creature hits you with a melee attack while you have these hit points, the creature takes 5 psychic damage.

\n

Force Potency. When you cast this power using a force slot of 2nd level or higher, both the temporary hit points and the psychic damage increase by 5 for each slot.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"drk","components":{"value":"","concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Armor%20of%20Abeloth.webp","effects":[]} {"_id":"OYQAqS6rjVy6o5Oa","name":"Kill","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Ruin

\n

You compel one creature you can see within range to die instantly. If the creature you choose has 100 hit points or fewer, it dies. Otherwise, the power has no effect.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Have Ruin Power"},"duration":{"value":null,"units":"inst"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"Have over 100 HP left or Die!","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":9,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Kill.webp","effects":[]} {"_id":"Oxb3dpusa4SmIKQd","name":"Improved Battle Meditation","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Battle Meditation

\n

You exude an aura out to 15 feet that boosts the morale and overall battle prowess you and your allies while simultaneously reducing the opposition’s combat-effectiveness by eroding their will to fight.

\n

Whenever you or a friendly creature within your meditation makes an attack roll or a saving throw, they can roll a d6 and add the number rolled to the attack roll or saving throw.

\n

Whenever a hostile creature enters your meditation or starts its turn there, it must make a Charisma saving throw. On a failed save, it must roll a d6 and subtract the number rolled from each attack roll or saving throw it makes before the end of your next turn. On a successful save, it is immune to this power for 1 day.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":15,"units":"ft","type":"radius"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d6","save":{"ability":"","dc":null,"scaling":"power"},"level":5,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Improved%20Battle%20Meditation.webp","effects":[]} {"_id":"PUHY0RrLsi4tAnZx","name":"Force Sight","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Sense Force

\n

You shift your vision to see through use of the Force; colors fade and inanimate objects appear as shades of gray. You gain the following benefits.

\n
    \n
  • Living things glow with the power of the Force. Those with an affinity for the light side glow blue, those with an affinity for the dark side glow red, and those with no attunement to either side of the Force glow yellow. How bright they glow is determined by how strong their connection to the Force is.
  • \n
  • You gain blindsight to 30 feet.
  • \n
  • You have advantage on Wisdom (Perception) checks that rely on sight against living targets within 30 feet.
  • \n
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Sight.webp","effects":[]} @@ -122,7 +125,7 @@ {"_id":"VIi9ZD8Ha669UYRw","name":"Insanity","permission":{"default":0,"9BhUyjgxIxogl2ot":3},"type":"power","data":{"description":{"value":"

Prerequisite: Horror

\n

This power assaults and twists creatures’ minds, spawning delusions and provoking uncontrolled action. Each creature in a 30-foot-radius sphere centered on you must succeed on a Wisdom saving throw when you cast this power or be affected by it.

\n

An affected target can’t take reactions and must roll a d8 at the start of each of its turns to determine its behavior for that turn. This power has no effect on constructs or droids.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
d8Behavior
1The creature uses all its movement to move in a random direction. To determine the direction, roll a d8 and assign a direction to each die face. The creature doesn’t take an action this turn.
2-6The creature doesn’t move or take actions this turn.
7-8The creature uses its action to make a melee attack against a randomly determined creature within its reach. If there is no creature within its reach, the creature does nothing this turn.
\n

At the end of each of its turns, an affected target can make a Wisdom saving throw. If it succeeds, this effect ends for that target.

\n

Force Potency. When you cast this power using a power slot of 6th level or higher, the radius of the sphere increases by 5 feet for each force slot level above 5th.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":30,"units":"ft","type":"sphere"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d8","save":{"ability":"wis","dc":null,"scaling":"power"},"level":5,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Insanity.webp","effects":[]} {"_id":"Vsnr0aVZQIcRD0Ed","name":"Force Mask","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Mind Trick

\n

Until the power ends or you use an action to dismiss it, you can disguise yourself through use of the Force in many ways. You can appear to be shorter or taller by about a foot and change the appearance of your body and weight, but you cannot change the basic structure of your body. This effect can include your clothes, weapons, and other belongings on your person.

\n

This effect is only visual, so any sort of physical contact will only interact with the real size and shape of you. A creature that uses its action to examine you can identify this effect with a successful Intelligence (Investigation) check against your force save DC. This power has no effect on droids or constructs.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"abil","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"int","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Mask.webp","effects":[]} {"_id":"WBNy29tnkclypOVZ","name":"Danger Sense","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

You put your faith in the Force, feeling out the future and seeing whether your actions will lead to fortune or ruin. The GM chooses from the following possible omens:

\n
    \n
  • Peace, for results which are not dangerous
  • \n
  • Danger, for results which are dangerous but perhaps still worth the danger
  • \n
  • Ruin, for results which are certain to end in death or tragedy
  • \n
\n

The power doesn’t take into account any possible circumstances that might change the outcome, such as the use of additional powers or the loss or gain of a companion.

\n

If you use this power two or more times before completing your next long rest, there is a cumulative 25 percent chance for each casting after the first that you get a neutral result regardless of the actual outcome.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Danger%20Sense.webp","effects":[]} -{"_id":"WG79exTllchDSK6M","name":"Animate Weapon","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Force Disarm

\n

You select a melee weapon you wield, or one melee weapon within range that is not worn or carried by a conscious creature, and use the Force to cause it to levitate, acting as an extension of your will for the duration or until you cast this power again. When you use this power, you can cause the weapon to move up to 20 feet and make a melee force attack against a creature within 5 feet of it. On a hit, the target takes 1d8 + your forcecasting ability modifier damage. The type is of the normal damage dealt by the weapon.

\n

While the weapon is animated, on each of your turns you can use a bonus action to move the weapon up to 20 feet and repeat the attack against a creature within 5 feet of it. At any time, you can end this force power to return the animated weapon to your hand.

\n

An enemy can attempt to gain control of the weapon by using its action to make a Strength (Athletics) check against your force save DC. On a success, the creature gains control of the weapon and the power ends.

\n

Force Potency. When you cast this power using a force slot of 3rd level or higher, the weapon’s damage increases by 1d8 for every two slot levels above 2nd.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"object"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"wis","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod",""]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Animate%20Weapon.webp","effects":[]} +{"_id":"WG79exTllchDSK6M","name":"Animate Weapon","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Force Disarm

\n

You select a melee weapon you wield, or one melee weapon within range that is not worn or carried by a conscious creature, and use the Force to cause it to levitate, acting as an extension of your will for the duration or until you cast this power again. When you use this power, you can cause the weapon to move up to 20 feet and make a melee force attack against a creature within 5 feet of it. On a hit, the target takes 1d8 + your forcecasting ability modifier damage. The type is of the normal damage dealt by the weapon.

\n

While the weapon is animated, on each of your turns you can use a bonus action to move the weapon up to 20 feet and repeat the attack against a creature within 5 feet of it. At any time, you can end this force power to return the animated weapon to your hand.

\n

An enemy can attempt to gain control of the weapon by using its action to make a Strength (Athletics) check against your force save DC. On a success, the creature gains control of the weapon and the power ends.

\n

Force Potency. When you cast this power using a force slot of 3rd level or higher, the weapon’s damage increases by 1d8 for every two slot levels above 2nd.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"object"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":"0","max":"0","per":""},"ability":"wis","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod",""]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Animate%20Weapon.webp","effects":[]} {"_id":"WWCvfD4ldEZoR5fn","name":"Mind Trap","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Force Confusion

\n

You attempt to trap the mind of your target in a psychic cage. The target must make a Charisma saving throw. On a failed save, the creature’s mind is trapped. It can think, but it can’t have any contact with or perceive the outside world. If the creature takes damage, it makes another Charisma save. On a success, the power ends. This power has no effect on droids or constructs.

\n

Force Potency. When you cast this power using a force slot of 6th level or higher, after 1 minute of concentration the power’s duration becomes 24 hours and it no longer requires your concentration.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"cha","dc":null,"scaling":"power"},"level":4,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Mind%20Trap.webp","effects":[]} {"_id":"XJSLZaP9Nix5EJHY","name":"Curse","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Denounce

\n

Up to three creatures of your choice that you can see within range must make Charisma saving throws. Whenever a target that fails this saving throw makes an attack roll or a saving throw before the power ends, the target must roll a d4 and subtract the number rolled from the attack roll or saving throw.

\n

Force Potency. When you cast this power using a force slot of 2nd level or higher, you can target one additional creature for each slot level above 1st.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Have Denounce Power"},"duration":{"value":1,"units":"minute"},"target":{"value":3,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d4","save":{"ability":"cha","dc":null,"scaling":"power"},"level":1,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1 Addition Target"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Curse.webp","effects":[]} {"_id":"XYHAKmU4gHSzRK3I","name":"Force Suppression","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Choose one creature, object, or force effect within range. Any force power of 3rd level or lower on the target ends. For each force power of 4th level or higher on the target, make an ability check using your forcecasting ability. The DC equals 10 + the power’s level. On a success, the power ends.

\n

Force Potency. When you cast this power using a force slot of 4th level or higher, you automatically end the effects of a force power on the target if the power’s level is equal to or less than the level of the force slot you used.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"abil","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"wis","dc":null,"scaling":"power"},"level":3,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Suppression.webp","effects":[]} @@ -157,6 +160,7 @@ {"_id":"ebWGew0oR2CG7S43","name":"Disperse Force","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Saber Ward

\n

This power absorbs damage from incoming energy attacks, lessening its effect on you and distributing it throughout your body. You have resistance to the triggering damage type until the start of your next turn. Also, you gain 5 temporary hit points to potentially absorb the attack. These temporary hit points last until the start of your next turn.

\n

Force Potency. When you cast this power using a force slot of 2nd level or higher, the temporary hit points increases by 5 for each slot level above 1st.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"reaction","cost":1,"condition":"which you take when you take cold, energy, fire, ion, lightning, or sonic damage"},"duration":{"value":1,"units":"round"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"5 additional Temp HP"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Disperse%20Force.webp","effects":[]} {"_id":"ecDdTd9MW0z4h8Hb","name":"Sap Vitality","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Make a melee force attack against a creature you can reach. On a hit, the target takes 3d10 necrotic damage.

\n

Force Potency. When you cast this power using a force slot of 2nd level or higher, the damage increases by 1d10 for each slot level above 1st.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"mpak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["3d10","necrotic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d10"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Sap%20Vitality.webp","effects":[]} {"_id":"esyvxlavJubgPNmj","name":"Force Trance","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

You make a calming gesture, and up to three willing creatures of your choice that you can see within range fall unconscious for the power’s duration. The power ends on a target early if it takes damage or someone uses an action to shake or slap it awake. If a target remains unconscious for the full duration, that target gains the benefit of a short rest, and it can’t be affected by this power again until it finishes a long rest.

\n

Force Potency. When you cast this power using a force slot of 4th level or higher, you can target one additional willing creature for each slot level above 3rd.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":3,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":3,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1 additional creature"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Trance.webp","effects":[]} +{"name":"Kinetite","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"

Prerequisite: Sustained Lightning

\n

A 5-foot-diameter sphere of compressed lightning appears in an unoccupied space of your choice within range and lasts for the duration. Any creature that ends its turn within 5 feet of the sphere must make a Dexterity saving throw. The creature takes 2d6 lightning damage on a failed save, or half as much damage on a successful one.

\n

As a bonus action, you can move the sphere up to 30 feet. If you ram the sphere into a creature, that creature must make a Dexterity saving throw, taking 2d6 kinetic damage on a failed save or half as much damage on a successful one, and the sphere stops moving this turn.

\n

When you move the sphere, you can direct it over barriers up to 5 feet tall and jump it across pits up to 10 feet wide. The sphere ignites flammable objects not being worn or carried, and it sheds bright light in a 20-foot radius and dim light for an additional 20 feet.

\n

Force Potency. When you cast this power with a force slot of 3rd level or higher, both the lightning and kinetic damage increase by 1d6 for each slot level above 2nd.

","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":5,"width":null,"units":"ft","type":"sphere"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["2d6","lightning"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":2,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1d6"}},"flags":{"core":{"sourceId":"Item.sOyvjT8ig2Eninf3"}},"img":"systems/sw5e/packs/Icons/Force%20Powers/Kinetite.webp","effects":[],"_id":"f01f9EPd9qvVy6QA"} {"_id":"f41D14g0yIBuXUka","name":"Force Immunity","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

An immobile, faintly shimmering barrier springs into existence around you and remains for the duration. The barrier moves with you. Any force power of 3rd level or lower cast from outside the barrier can’t affect you, even if the power is cast using a higher level force slot. Such a power can target you, but the power has no effect on you. Similarly, the area within the barrier is excluded from the areas affected by such powers.

\n

Force Potency. When you cast this power using a force slot of 5th level or higher, the barrier blocks powers of one level higher for each slot level above 4th.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":4,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1 additional slot"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Immunity.webp","effects":[]} {"_id":"faTYDaOejYHPfXdl","name":"Earthquake","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Eruption

\n

You create a seismic disturbance at a point on the ground that you can see within range. For the duration, an intense tremor rips through the ground in a 100-foot-radius circle centered on that point and shakes creatures and structures in contact with the ground in that area.

\n

The ground in the area becomes difficult terrain. Each creature on the ground that is concentrating must make a Constitution saving throw. On a failed save, the creature’s concentration is broken.

\n

When you cast this power and at the end of each turn you spend concentrating on it, each creature on the ground in the area must make a Dexterity saving throw. On a failed save, the creature is knocked prone.

\n

This power can have additional effects depending on the terrain in the area, as determined by the GM.

\n

Fissures. Fissures open throughout the power’s area at the start of your next turn after you cast the power. A total of 1d6 such fissures open in locations chosen by the GM. Each is 1d10 x 10 feet deep, 10 feet wide, and extends from one edge of the power’s area to the opposite side. A creature standing on a spot where a fissure opens must succeed on a Dexterity saving throw or fall in. A creature that successfully saves moves with the fissure’s edge as it opens.

\n

A fissure that opens beneath a structure causes it to automatically collapse (see below).

\n

Structures. The tremor deals 50 kinetic damage to any structure in contact with the ground in the area when you cast the power and at the start of each of your turns until the power ends. If a structure drops to 0 hit points, it collapses and potentially damages nearby creatures. A creature within half the distance of a structure’s height must make a Dexterity saving throw. On a failed save, the creature takes 5d6 kinetic damage, is knocked prone, and is buried in the rubble, requiring a DC 20 Strength (Athletics) check as an action to escape. The GM can adjust the DC higher or lower, depending on the nature of the rubble. On a successful save, the creature takes half as much damage and doesn’t fall prone or become buried.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":100,"units":"ft","type":"radius"},"range":{"value":500,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":8,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Earthquake.webp","effects":[]} {"_id":"fcs6lsi5hMfPOgRb","name":"Resistance","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

You touch one willing creature. Once before the power ends, the target can roll a d4 and add the number rolled to one saving throw of its choice. It can roll the die before or after the saving throw. The power then ends.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d4","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Resistance.webp","effects":[]} @@ -184,6 +188,7 @@ {"_id":"qIelaqr9e9Cn0W8O","name":"Calm Emotions","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Sense Emotion

\n

You attempt to suppress strong emotions in a group of people. Each humanoid in a 20-foot-radius sphere centered on a point you choose within range must make a Charisma saving throw a creature can choose to fail this saving throw if it wishes. If a creature fails its saving throw, choose one of the following two effects.

\n
    \n
  • You can suppress any effect causing a target to be charmed or frightened. When this power ends, any suppressed effect resumes, provided that its duration has not expired in the meantime.
  • \n
  • You can make a target indifferent about creatures of your choice that it is hostile toward. This indifference ends if the target is attacked or harmed by a power or if it witnesses any of its friends being harmed.
  • \n
\n

When the power ends, the creature becomes hostile again, unless the GM rules otherwise.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":20,"units":"ft","type":"sphere"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"cha","dc":null,"scaling":"power"},"level":2,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Calm%20Emotions.webp","effects":[]} {"_id":"qYnIKhpoJpSflVZh","name":"Telekinetic Burst","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Telekinetic Storm

\n

A beam of Force energy flashes out from your hand in a 5-foot-wide, 60-foot-long line. Each creature in the line must make a Constitution saving throw. On a failed save, a creature takes 8d6 force damage and is knocked prone. On a successful save, it takes half as much damage and isn’t knocked prone.

\n

You can create a new telekinetic gust as your action on your turn until the power ends.

\n

Force Potency. When you cast this power using a force slot of 7th level or higher, the damage increases by 2d6 for each slot level above 6th.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":60,"units":"ft","type":"line","width":null},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["8d6","force"]],"versatile":""},"formula":"","save":{"ability":"con","dc":null,"scaling":"power"},"level":6,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"2d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Telekinetic%20Burst.webp","effects":[]} {"_id":"qkBzg8ZIJpglVMvi","name":"Beacon of Hope","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Heroism

\n

This power bestows hope and vitality. Choose any number of creatures within range. For the duration, each target has advantage on Wisdom saving throws and death saving throws, and regains the maximum number of hit points possible from any healing.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":null,"units":"any","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":3,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Beacon%20of%20Hope.webp","effects":[]} +{"name":"Defensive Technique","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"

As part of the action used to cast this power, you must make a melee weapon attack against one creature within your reach, otherwise the power fails. On a hit, the target suffers the attack's normal effects, and you ward yourself from it through the Force.

\n

If the target forces you to make a saving throw before the start of your next turn, the target takes an additional 1d6 psychic damage, and you can roll 1d4 and add the number rolled to your saving throw. The power then ends.

\n

The power's damage increases when you reach higher levels. At 5th level, the melee attack deals an extra 1d6 force damage to the target, and the psychic damage the target takes for forcing you to make a saving throw increases to 2d6. Both damage rolls increase by 1d6 at 11th level and 17th level.

","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":"you must make a melee weapon attack against one creature within your reach, otherwise the power fails"},"duration":{"value":1,"units":"round"},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":""},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.ielR5DiQGGDRsJB5"}},"img":"systems/sw5e/packs/Icons/Force%20Powers/Defensive%20Technique.webp","effects":[],"_id":"qv4hHFZKUwg7Ail2"} {"_id":"qykEFT52bywaQrNO","name":"Force Technique","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

You imbue your weapon with the purifying light of the Force. As part of the action used to cast this power, you must make a melee attack with a weapon against one creature within your weapon’s reach, otherwise the power fails. On a hit, the target suffers the attack’s normal effects, and it becomes wreathed in a glowing barrier of force energy until the start of your next turn. If the target willingly moves before then, it immediately takes 1d8 force damage, and the power ends.

\n

This power’s damage increases when you reach higher levels. At 5th level, the melee attack deals an extra 1d8 force damage to the target, and the damage the target takes for moving increases to 2d8. Both damage rolls increase by 1d8 at 11th level and 17th level.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"You must make a melee attack with a weapon against one creature within your weapon’s reach"},"duration":{"value":1,"units":"round"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8",""]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"atwill","formula":"1d8"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Technique.webp","effects":[]} {"_id":"rxvZoFgkC411tibT","name":"Break","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"

You inflict 10 force damage to an object you can see within range that is not being worn or carried, generating an explosion of sound that can be heard up to 100 feet away. Even if the object is not destroyed, small shards of shrapnel fly at creatures within 5 feet of it. Each creature must make a Dexterity saving throw. On a failed save, a creature takes 1d4 kinetic damage.

\n

This power's kinetic damage increases by 1d4 when you reach 5th level (2d4), 11th level (3d4), and 17th level (4d4), and the power's force damage also increases by 10 at each of these levels.

","chat":"","unidentified":""},"source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":1,"width":null,"units":"","type":"object"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rpak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["10","force"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.sVK2BXvC9pEX8By5"}},"img":"systems/sw5e/packs/Icons/Force%20Powers/Break.webp","effects":[]} {"_id":"sFLwKTBxnM6YfboP","name":"Wrack","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"

Prerequisite: Plague

\n

You wrack the body of a creature that you can see with a virulent, disease-like condition. The target must make a Constitution saving throw. On a failed save, it takes 14d6 necrotic damage, or half as much damage on a successful save. The damage can’t reduce the target’s hit points below 1. If the target fails the saving throw, its hit point maximum is reduced for 1 hour by an amount equal to the necrotic damage it took. Any effect that removes a disease allows a creature’s hit point maximum to return to normal before that time passes.

\n

Force Potency. If you cast this power using a force slot of 7th level or higher, the power deals an extra 2d6 damage for each slot level above 6th.

","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Plague"},"duration":{"value":null,"units":"inst"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["14d6","necrotic"]],"versatile":""},"formula":"","save":{"ability":"con","dc":null,"scaling":"power"},"level":6,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"2d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Wrack.webp","effects":[]} diff --git a/packs/packs/species.db b/packs/packs/species.db index 7c13ed35..b3ae3616 100644 --- a/packs/packs/species.db +++ b/packs/packs/species.db @@ -31,7 +31,7 @@ {"_id":"Hryj6S6P3DYsAdtj","name":"Dug","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

Dugs are slender, powerfully built beings with a somewhat humanoid build and a unique method of walking that hailed from the high gravity world Malastare. Their primary means of locomotion is their strong arms, and their lower limbs and feet were used for grappling and other fine motor manipulation. They hardly ever walk on their lower limbs. Although most Dugs may walk on all four limbs, others like to use their strong arms as legs and their feet as hands like they would normally do.

Society and Culture

Due to their oppression under their Gran rulers who colonized Malastare, many Dugs often feel the need to throw around their strength in bids to establish dominance. As a result, they are known for their ill-tempered demeanor, and many are bullying thugs.\r\n\r\nOn their homeworld of Malastare, the vast majority of Dugs are little more than laborers toiling for the enrichment of the Gran. With the species excluded from much of the power and money on Malastare, many Dugs turn to swoop racing or bounty hunting as their only means to achieve fame and fortune. In all other areas, the Dugs are exploited.

Names

Dug names are often 3 syllables long, mostly through big sounds rather than harsh tones. There are harsher tones in their names as well though, often in the forms of x's and k's. Female Dugs have softer names, but no one would call them beautiful. Surnames are usually passed down through family or clan.

  Male Names. Bawugri, Gadwouhx, Rorgukwa,

  Female Names. Bosix, Grugne, Jiwous, Pragiba,

  Surnames. Brundaare, Gninsaidi, Kedwir, Randaine","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Strength score increases by 2, and your Dexterity score increases by 1.

Age. Dugs reach adulthood in their early teens and live an average of 75 years. Their violent nature often leads to violent ends.

Alignment. Dugs' angry nature causes them to tend toward the dark side, though there are exceptions.

Size. Dugs typically stand between 3 and 4 feet tall. Regardless of your position in that range, your size is Small.

Speed. Your base walking speed is 25 feet.

Courageous. You have advantage on saving throws against being frightened.

Fisticuffs. Your unarmed strikes deal 1d4 kinetic damage. You can use your choice of your Strength or Dexterity modifier for the attack and damage rolls. You must use the same modifier for both rolls.

Fury of the Small. When you damage a creature with an attack or a power and the creature's size is larger than yours, you can cause the attack or power to deal extra damage to the creature. The extra damage equals your level. Once you use this trait, you can't use it again until you finish a short or long rest.

Menacing. You have proficiency in the Intimidation skill.

Strong and Small. You have a climbing speed of 25 feet.

Powerful Build. You count as two sizes larger when determining your carrying capacity and the weight you can push, drag, or lift.

Undersized. Your small stature makes it hard for you to wield bigger weapons. You can't use heavy shields. Additionally, you can't use martial weapons with the two-handed property unless it also has the light property, and if a martial weapon has the versatile property, you can only wield it in two hands.

Languages. You can speak, read, and write Galactic Basic and Dug.

"},"skinColorOptions":{"value":"Brown, purple, gray, or red"},"hairColorOptions":{"value":"None"},"eyeColorOptions":{"value":"Blue or yellow"},"distinctions":{"value":"Arms used as legs and legs used as arms"},"heightAverage":{"value":"3'2\""},"heightRollMod":{"value":"+2d6\""},"weightAverage":{"value":"50 lb."},"weightRollMod":{"value":"x(2d4) lb."},"homeworld":{"value":"Malastare"},"slanguage":{"value":"Dug"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"alwaysActive":false,"effects":[{"modSpecKey":"data.details.species","value":"Dug","mode":"=","targetSpecific":false,"id":1,"itemId":"567S1i2cx4Cv10jT","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.str.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"567S1i2cx4Cv10jT","active":false,"_targets":[],"label":"Abilities Strength"},{"modSpecKey":"data.abilities.dex.value","value":"1","mode":"+","targetSpecific":false,"id":3,"itemId":"567S1i2cx4Cv10jT","active":false,"_targets":[],"label":"Abilities Dexterity"},{"modSpecKey":"data.traits.size","value":"sm","mode":"=","targetSpecific":false,"id":4,"itemId":"567S1i2cx4Cv10jT","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"25","mode":"=","targetSpecific":false,"id":5,"itemId":"567S1i2cx4Cv10jT","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.skills.itm.value","value":"1","mode":"=","targetSpecific":false,"id":6,"itemId":"567S1i2cx4Cv10jT","active":false,"_targets":[],"label":"Skills Intimidation"},{"modSpecKey":"data.attributes.movement.climb","value":"25","mode":"=","targetSpecific":false,"id":7,"itemId":"567S1i2cx4Cv10jT","active":false,"_targets":[],"label":"Attributes Speed Special"},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":8,"itemId":"567S1i2cx4Cv10jT","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"dug","mode":"+","targetSpecific":false,"id":9,"itemId":"567S1i2cx4Cv10jT","active":false,"_targets":[]}]},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Dug.webp","effects":[{"_id":"ZXNTACyNdeD1Y7ui","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Dug","mode":5,"priority":5},{"key":"data.abilities.str.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20},{"key":"data.traits.size","value":"sm","mode":5,"priority":5},{"key":"data.attributes.movement.walk","value":25,"mode":5,"priority":5},{"key":"data.skills.itm.value","value":1,"mode":4,"priority":5},{"key":"data.attributes.movement.climb","value":25,"mode":5,"priority":5},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"dug","mode":0,"priority":0},{"key":"flags.sw5e.furyOfTheSmall","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.powerfulBuild","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.undersized","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Dug.webp","label":"Dug","tint":"","transfer":true}]} {"_id":"HyqiVZiyDKbiPHkM","name":"Cathar","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

\n

The Cathar have fur-covered bodies with thick manes as well as prominent, retractable claws that can deliver powerful killing attacks on foes and prey. Their bodies also possess rapid healing abilities. These traits make them the perfect hand-to-hand specialists. The Cathar species also has two subspecies, known as the Juhani and the Myr Rho. Both of these are notably less catlike than mainline Cathar. Cathar are born into a litter. The Cathar species is biologically similar to the Bothan species.

\n

Society and Culture

\n

On their homeworld, Cathar live in cities built into giant trees, and are organized into clans governed by elders. Stories of their great heroes were often carved into the trunks of these tree-homes for following generations to see. The Cathar mate for life, to the extent that when one mate dies, the survivor never has a relationship with another. Cathar clan society includes great pageants and celebrations, especially for their heroes. Their religion includes a ritual known as the \"Blood Hunt,\" in which Cathar warriors individually engaged in combat against entire nests of Kiltik in order to gain honor and purge themselves of inner darkness. The native language of the Cathar is Catharese, which included the emphasis of some spoken words with a growl.

\n

Names

\n

Cathar names can sound both melodic and fairly gutteral, but they almost always sound strong and fierce. Female names are typically longer than male names. Surnames are usually one syllable.

\n

  Male Names. Crurbirr, Isyrr, Nynorr, Suro, Tukarr

\n

  Female Names. Cuwin, Jyvohr, Mulahr, Solyri

\n

  Surnames. Jin, Ki, Mak, Rhir, Ta

","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Dexterity score increases by 2, and your Charisma score increases by 1.

\n

Age. Cathar reach adulthood in their late teens and live less than a century.

\n

Alignment. Cathar tend toward no particular alignment. The best and worst are found among them.

\n

Size. Cathar range from 5 to 7 feet tall, and can weigh up to 300 lbs. Regardless of your position in that range, your size is Medium.

\n

Speed. Your base walking speed is 30 feet.

\n

Darkvision. You have a cat's keen senses, especially in the dark. You can see in dim light within 60 feet of you as if it were bright light, and in darkness as if it were dim light. You can't discern color in darkness, only shades of gray.

\n

Leonine Agility. Your reflexes and agility allow you to move with a burst of speed. When you move on your turn in combat, you can double your speed until the end of the turn. Once you use this trait, you can't use it again until you move 0 feet on one of your turns.

\n

Cat's Claws. You have a climbing speed of 20 feet. Additionally, your unarmed strikes deal 1d4 kinetic damage. You can use your choice of your Strength or Dexterity modifier for the attack and damage rolls. You must use the same modifier for both rolls.

\n

Cat's Talent. You have proficiency in the Perception and Stealth skills.

\n

Languages. You can speak, read, and write Galactic Basic and Catharese.

"},"skinColorOptions":{"value":"Gold to yellow-brown with dark stripes"},"hairColorOptions":{"value":"Brown, black, or grey"},"eyeColorOptions":{"value":"Yellow or brown"},"distinctions":{"value":"Lion-like features"},"heightAverage":{"value":"4'9\""},"heightRollMod":{"value":"+2d12\""},"weightAverage":{"value":"130 lb."},"weightRollMod":{"value":"x(2d4) lb."},"homeworld":{"value":"Cathar"},"slanguage":{"value":"Catharese"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"PHB"},"flags":{"dynamiceffects":{"equipActive":true,"effects":[{"modSpecKey":"data.details.species","value":"Cathar","mode":"=","targetSpecific":false,"id":1,"itemId":"Ku1ZIFLPqS4PTiFl","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.dex.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"Ku1ZIFLPqS4PTiFl","active":false,"_targets":[],"label":"Abilities Dexterity"},{"modSpecKey":"data.abilities.cha.value","value":"1","mode":"+","targetSpecific":false,"id":3,"itemId":"Ku1ZIFLPqS4PTiFl","active":false,"_targets":[],"label":"Abilities Charisma"},{"modSpecKey":"data.traits.size","value":"med","mode":"=","targetSpecific":false,"id":4,"itemId":"Ku1ZIFLPqS4PTiFl","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"30","mode":"=","targetSpecific":false,"id":5,"itemId":"Ku1ZIFLPqS4PTiFl","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.traits.senses","value":"Darkvision (60 ft.)","mode":"+","targetSpecific":false,"id":6,"itemId":"Ku1ZIFLPqS4PTiFl","active":false,"_targets":[],"label":"Traits Senses"},{"modSpecKey":"data.attributes.movement.climb","value":"20","mode":"+","targetSpecific":false,"id":7,"itemId":"Ku1ZIFLPqS4PTiFl","active":false,"_targets":[],"label":"Attributes Speed Special"},{"modSpecKey":"data.skills.prc.value","value":"1","mode":"+","targetSpecific":false,"id":8,"itemId":"Ku1ZIFLPqS4PTiFl","active":false,"_targets":[],"label":"Skills Perception"},{"modSpecKey":"data.skills.ste.value","value":"1","mode":"+","targetSpecific":false,"id":9,"itemId":"Ku1ZIFLPqS4PTiFl","active":false,"_targets":[],"label":"Skills Stealth"},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":10,"itemId":"Ku1ZIFLPqS4PTiFl","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"catharese","mode":"+","targetSpecific":false,"id":11,"itemId":"Ku1ZIFLPqS4PTiFl","active":false,"_targets":[]}],"alwaysActive":false},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Cathar.webp","effects":[{"_id":"6fYB2NZ236PqSl6p","flags":{"dae":{"transfer":true,"stackable":false,"specialDuration":[]}},"changes":[{"key":"data.details.species","value":"Cathar","mode":5,"priority":5},{"key":"data.abilities.dex.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20},{"key":"data.traits.size","value":"med","mode":5,"priority":5},{"key":"data.attributes.movement.walk","value":30,"mode":5,"priority":5},{"key":"data.attributes.senses.darkvision","value":60,"mode":2,"priority":20},{"key":"data.attributes.movement.climb","value":20,"mode":2,"priority":20},{"key":"data.skills.prc.value","value":1,"mode":4,"priority":20},{"key":"data.skills.ste.value","value":1,"mode":4,"priority":20},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"catharese","mode":0,"priority":0},{"key":"flags.sw5e.nimbleAgility","value":"1","mode":0,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Cathar.webp","label":"Cathar","tint":"","transfer":true}]} {"_id":"IM7NrBROnrDOgr0l","name":"Chagrian","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

\n

Chagrians are born as tadpoles in clutches of three or more and raised in tubs of water in a family's private home. During this time, their arms, legs, and air-breathing lungs develop. Adult Chagrians are truly amphibious, retaining their ability to breathe underwater while also able to function without difficulty in air. They also possess acute low-light vision.

\n

The average Chagrian stands taller than a Human. They are distinguished by two fleshy growths protruding from the sides of their heads, which they call lethorns. As they age, the lethorns thicken. Males also sport two horns growing from the top of their skulls. These were once used in underwater duels to attract a mate, and are seen as a sign of the males' strength and virility. Females lack the superior cranial horns, but had more pronounced and longer posterior head plates; these can reach halfway down their back. Chagrians also have very long black forked tongues.

\n

Society and Culture

\n

As a species, Chagrians are generally peaceful and law-abiding to the point of becoming stoic and obstinate. Many Chagrians are motivated only by basic desires such as sustenance, shelter, and healthcare. Chagrian government ensures that every citizen is cared and provided for, so the standard of living for the poorest Chagrian is high compared to the members of other species. Chagrians who expect violence often wear red.

\n

Names

\n

Chagrian names have a very melodic tone. Male names are typically shorter than female names. Surnames are familial.

\n

  Male Names. Bom, Chen, Fiet, Nedd, Touk

\n

  Female Names. Chavik, Dabai, Fisil, Oolya, Tinto

\n

  Surnames. Kassin, Molya, Nigna, Onirali, Treen

","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Dexterity score increases by 2, and your Wisdom score increases by 1.

\n

Age. Chagrians reach adulthood in their late teens and live an average of 75 years.

\n

Alignment. Chagrians' peace-loving nature causes them to tend toward the light side, though there are exceptions.

\n

Size. Chagrians typically stand between 5 and 6 feet tall and weigh 150 lbs. Regardless of your position in that range, your size is Medium.

\n

Speed. Your base walking speed is 30 feet.

\n

Amphibious. You can breathe air and water.

\n

Darkvision. You have keen eyesight, especially in the dark. You can see in dim light within 60 feet of you as if it were bright light, and in darkness as if it were dim light. You can't discern color in darkness, only shades of gray.

\n

Natural Resistance. You have advantage on saving throws against poison, and you have resistance against poison damage (explained in chapter 9).

\n

Swim. You have a swimming speed of 30 feet.

\n

Languages. You can speak, read, and write Galactic Basic and Chagri.

"},"skinColorOptions":{"value":"Light to dark blue"},"hairColorOptions":{"value":"None"},"eyeColorOptions":{"value":"Black"},"distinctions":{"value":"Horns (male), lethorns, black forked tongues"},"heightAverage":{"value":"5'5\""},"heightRollMod":{"value":"+2d8\""},"weightAverage":{"value":"120 lb."},"weightRollMod":{"value":"x(2d4) lb."},"homeworld":{"value":"Champala"},"slanguage":{"value":"Chagri"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"effects":[{"modSpecKey":"data.details.species","value":"Chagrian","mode":"=","targetSpecific":false,"id":1,"itemId":"OeTZyxxYPu7ICMTX","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.dex.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"OeTZyxxYPu7ICMTX","active":false,"_targets":[],"label":"Abilities Dexterity"},{"modSpecKey":"data.abilities.wis.value","value":"1","mode":"+","targetSpecific":false,"id":3,"itemId":"OeTZyxxYPu7ICMTX","active":false,"_targets":[],"label":"Abilities Wisdom"},{"modSpecKey":"data.traits.size","value":"med","mode":"=","targetSpecific":false,"id":4,"itemId":"OeTZyxxYPu7ICMTX","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"30","mode":"=","targetSpecific":false,"id":5,"itemId":"OeTZyxxYPu7ICMTX","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.traits.senses","value":"Darkvision (60 ft.)","mode":"+","targetSpecific":false,"id":6,"itemId":"OeTZyxxYPu7ICMTX","active":false,"_targets":[],"label":"Traits Senses"},{"modSpecKey":"data.traits.dr.value","value":"poison","mode":"+","targetSpecific":false,"id":7,"itemId":"OeTZyxxYPu7ICMTX","active":false,"_targets":[],"label":"Traits Damage Resistance"},{"modSpecKey":"data.attributes.movement.swim","value":"30","mode":"+","targetSpecific":false,"id":8,"itemId":"OeTZyxxYPu7ICMTX","active":false,"_targets":[],"label":"Attributes Speed Special"},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":9,"itemId":"OeTZyxxYPu7ICMTX","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"chagri","mode":"+","targetSpecific":false,"id":10,"itemId":"OeTZyxxYPu7ICMTX","active":false,"_targets":[]}],"alwaysActive":false},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Chagrian.webp","effects":[{"_id":"yGJG8HBejLZV4hB0","flags":{"dae":{"transfer":true,"stackable":false,"specialDuration":[]}},"changes":[{"key":"data.details.species","value":"Chagrian","mode":5,"priority":5},{"key":"data.abilities.dex.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20},{"key":"data.traits.size","value":"med","mode":5,"priority":5},{"key":"data.attributes.movement.walk","value":30,"mode":5,"priority":5},{"key":"data.attributes.senses.darkvision","value":60,"mode":2,"priority":20},{"key":"data.traits.dr.value","value":"poison","mode":0,"priority":0},{"key":"data.attributes.movement.swim","value":30,"mode":2,"priority":20},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"chagri","mode":0,"priority":0},{"key":"flags.sw5e.amphibious","value":"1","mode":0,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Chagrian.webp","label":"Chagrian","tint":"","transfer":true}]} -{"_id":"J7HJQkcvghtwMAdF","name":"Anzellan","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

\n

The anzellans are a diminutive species hailing from the secluded planet Anzella. Their eyes have floating corneal micro-lenses that allow them to see microscopic details. Anzellans are a bubbly and receptive people. They are a jovial and trusting people that tend to welcome strangers with open arms. Due to their volcanic homeworld, anzellans are also adapted towards heat. This, coupled with their small size, make them well-suited to working in compact places.

\n

Society and Culture

\n

Anzella is a tropical planet covered in thousands of small volcanic islands. Many of these islands are developed as small villages, with the largest islands designed to accommodate larger species. Anzellan culture is generally based around tourism and crafting; in fact, anzellans are renowned craftsmen due to their discerning eyesight and ability to fit into small spaces. Anzellan government is generally casual. Each village has its own governing council of rotating members; these villages act independently from one another unless their decisions would affect more than a single island. In that case, all of the councils work together to come to a planet-wide decision.

\n

Names

\n

Anzellan names are rarely longer than two syllables, with a bouncy intonation to them. Their surnames are familial.

\n

  Male Names. Babu, Gridel, Moru, Rano, Yodel

\n

  Female Names. Dibi, Fing, Nooni, Teena, Zazi

\n

  Surnames. E'ayoo, Frik, Meer, Tanni, Vrut

","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Intelligence score increases by 2, and two other ability scores of your choice increase by 1.

Age. Anzellans are considered adults at ten years old. They are a short-lived species, however, that rarely lives longer than 60 years.

Alignment. Anzellans are a friendly and respectful people, which causes them to tend toward lawful light side, though there are exceptions.

Size. Anzellans stand between 1 and 2 feet tall and weigh around 10 lbs. Regardless of your position in that range, your size is Tiny.

Speed. Your base walking speed is 20 feet.

Crafters. You have proficiency in one tool of your choice.

Detail Oriented. You are practiced at scouring for details. You have advantage on Intelligence (Investigation) checks within 5 feet.

Pintsized. Your tiny stature makes it hard for you to wield bigger weapons. You can't use medium or heavy shields. Additionally, you can't wield weapons with the two-handed or versatile property, and you can only wield one-handed weapons in two hands unless they have the light property.

Puny. Anzellans are too small to pack much of a punch. You have disadvantage on Strength saving throws, and when determining your bonus to attack and damage rolls for weapon attacks using Strength, you can't add more than +3.

Small and Nimble. You are too small and fast to effectively target. You have a +1 bonus to AC, and you have advantage on Dexterity saving throws.

Tanned. You are naturally adapted to hot climates, as described in chapter 5 of the Dungeon Master's Guide.

Technician. You are proficient in the Technology skill.

Tinker. You have proficiency with tinker's tools. You can use these and spend 1 hour and 100 cr worth of materials to construct a Tiny Device (AC 5, 1 hp). You can take the Use an Object action to have your device cause one of the following effects: create a small explosion, create a repeating loud noise for 1 minute, create smoke for 1 minute, create a soothing melody for 1 minute. You can maintain a number of these devices up to your proficiency bonus at once, and a device stops functioning after 24 hours away from you. You can dismantle the device to reclaim the materials used to create it.

Languages. You can speak, read, and write Galactic Basic and Anzellan. Anzellan is characterized by its bouncy sound and emphasis on alternating syllables.

","source":"Expanded Content"},"skinColorOptions":{"value":"Brown, green, or tan"},"hairColorOptions":{"value":"Black, gray, or white"},"eyeColorOptions":{"value":"Black"},"distinctions":{"value":"Diminutive size, wispy eyebrows"},"heightAverage":{"value":"1'0\""},"heightRollMod":{"value":"+2d6\""},"weightAverage":{"value":"3 lb."},"weightRollMod":{"value":"x1 lb."},"homeworld":{"value":"Anzella"},"slanguage":{"value":"Anzellan"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"effects":[{"modSpecKey":"data.details.species","value":"Anzellan","mode":"=","targetSpecific":false,"id":1,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.int.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Abilities Intelligence"},{"modSpecKey":"data.traits.size","value":"tin","mode":"=","targetSpecific":false,"id":3,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"20","mode":"=","targetSpecific":false,"id":4,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.attributes.ac.min","value":"1","mode":"+","targetSpecific":false,"id":5,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Attributes Armor Class Min"},{"modSpecKey":"data.skills.tec.value","value":"1","mode":"+","targetSpecific":false,"id":6,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Skills Technology"},{"modSpecKey":"data.traits.toolProf.custom","value":"Tinker's tools","mode":"+","targetSpecific":false,"id":7,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Tool Prof Custom"},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":8,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Language"},{"modSpecKey":"data.traits.languages.value","value":"anzellan","mode":"+","targetSpecific":false,"id":9,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[]}],"alwaysActive":false},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Anzellan.webp","effects":[{"_id":"ZGdzAq1Gl4xaxDRD","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Anzellan","mode":5,"priority":5},{"key":"data.abilities.int.value","value":2,"mode":2,"priority":20},{"key":"data.traits.size","value":"grg","mode":5,"priority":5},{"key":"data.attributes.movement.walk","value":20,"mode":5,"priority":5},{"key":"data.attributes.ac.value","value":1,"mode":2,"priority":20},{"key":"data.skills.tec.value","value":1,"mode":4,"priority":20},{"key":"data.traits.toolProf.custom","value":"Tinker's tools","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"anzellan","mode":0,"priority":0},{"key":"flags.sw5e.detailOriented","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.pintsized","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.puny","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.tinker","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Anzellan.webp","label":"Anzellan","tint":"","transfer":true}]} +{"_id":"J7HJQkcvghtwMAdF","name":"Anzellan","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

\n

The anzellans are a diminutive species hailing from the secluded planet Anzella. Their eyes have floating corneal micro-lenses that allow them to see microscopic details. Anzellans are a bubbly and receptive people. They are a jovial and trusting people that tend to welcome strangers with open arms. Due to their volcanic homeworld, anzellans are also adapted towards heat. This, coupled with their small size, make them well-suited to working in compact places.

\n

Society and Culture

\n

Anzella is a tropical planet covered in thousands of small volcanic islands. Many of these islands are developed as small villages, with the largest islands designed to accommodate larger species. Anzellan culture is generally based around tourism and crafting; in fact, anzellans are renowned craftsmen due to their discerning eyesight and ability to fit into small spaces. Anzellan government is generally casual. Each village has its own governing council of rotating members; these villages act independently from one another unless their decisions would affect more than a single island. In that case, all of the councils work together to come to a planet-wide decision.

\n

Names

\n

Anzellan names are rarely longer than two syllables, with a bouncy intonation to them. Their surnames are familial.

\n

  Male Names. Babu, Gridel, Moru, Rano, Yodel

\n

  Female Names. Dibi, Fing, Nooni, Teena, Zazi

\n

  Surnames. E'ayoo, Frik, Meer, Tanni, Vrut

","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Intelligence score increases by 2, and two other ability scores of your choice increase by 1.

Age. Anzellans are considered adults at ten years old. They are a short-lived species, however, that rarely lives longer than 60 years.

Alignment. Anzellans are a friendly and respectful people, which causes them to tend toward lawful light side, though there are exceptions.

Size. Anzellans stand between 1 and 2 feet tall and weigh around 10 lbs. Regardless of your position in that range, your size is Tiny.

Speed. Your base walking speed is 20 feet.

Crafters. You have proficiency in one tool of your choice.

Detail Oriented. You are practiced at scouring for details. You have advantage on Intelligence (Investigation) checks within 5 feet.

Pintsized. Your tiny stature makes it hard for you to wield bigger weapons. You can't use medium or heavy shields. Additionally, you can't wield weapons with the two-handed or versatile property, and you can only wield one-handed weapons in two hands unless they have the light property.

Puny. Anzellans are too small to pack much of a punch. You have disadvantage on Strength saving throws, and when determining your bonus to attack and damage rolls for weapon attacks using Strength, you can't add more than +3.

Small and Nimble. You are too small and fast to effectively target. You have a +1 bonus to AC, and you have advantage on Dexterity saving throws.

Tanned. You are naturally adapted to hot climates, as described in chapter 5 of the Dungeon Master's Guide.

Technician. You are proficient in the Technology skill.

Tinker. You have proficiency with tinker's tools. You can use these and spend 1 hour and 100 cr worth of materials to construct a Tiny Device (AC 5, 1 hp). You can take the Use an Object action to have your device cause one of the following effects: create a small explosion, create a repeating loud noise for 1 minute, create smoke for 1 minute, create a soothing melody for 1 minute. You can maintain a number of these devices up to your proficiency bonus at once, and a device stops functioning after 24 hours away from you. You can dismantle the device to reclaim the materials used to create it.

Languages. You can speak, read, and write Galactic Basic and Anzellan. Anzellan is characterized by its bouncy sound and emphasis on alternating syllables.

","source":"Expanded Content"},"skinColorOptions":{"value":"Brown, green, or tan"},"hairColorOptions":{"value":"Black, gray, or white"},"eyeColorOptions":{"value":"Black"},"distinctions":{"value":"Diminutive size, wispy eyebrows"},"heightAverage":{"value":"1'0\""},"heightRollMod":{"value":"+2d6\""},"weightAverage":{"value":"3 lb."},"weightRollMod":{"value":"x1 lb."},"homeworld":{"value":"Anzella"},"slanguage":{"value":"Anzellan"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"effects":[{"modSpecKey":"data.details.species","value":"Anzellan","mode":"=","targetSpecific":false,"id":1,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.int.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Abilities Intelligence"},{"modSpecKey":"data.traits.size","value":"tin","mode":"=","targetSpecific":false,"id":3,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"20","mode":"=","targetSpecific":false,"id":4,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.attributes.ac.min","value":"1","mode":"+","targetSpecific":false,"id":5,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Attributes Armor Class Min"},{"modSpecKey":"data.skills.tec.value","value":"1","mode":"+","targetSpecific":false,"id":6,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Skills Technology"},{"modSpecKey":"data.traits.toolProf.custom","value":"Tinker's tools","mode":"+","targetSpecific":false,"id":7,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Tool Prof Custom"},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":8,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Language"},{"modSpecKey":"data.traits.languages.value","value":"anzellan","mode":"+","targetSpecific":false,"id":9,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[]}],"alwaysActive":false},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Anzellan.webp","effects":[{"_id":"ZGdzAq1Gl4xaxDRD","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Anzellan","mode":5,"priority":5},{"key":"data.abilities.int.value","value":2,"mode":2,"priority":20},{"key":"data.traits.size","value":"tiny","mode":5,"priority":5},{"key":"data.attributes.movement.walk","value":20,"mode":5,"priority":5},{"key":"data.attributes.ac.value","value":1,"mode":2,"priority":20},{"key":"data.skills.tec.value","value":1,"mode":4,"priority":20},{"key":"data.traits.toolProf.custom","value":"Tinker's tools","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"anzellan","mode":0,"priority":0},{"key":"flags.sw5e.detailOriented","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.pintsized","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.puny","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.tinker","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Anzellan.webp","label":"Anzellan","tint":"","transfer":true}]} {"_id":"JB327dJNFAiEOxyp","name":"Felucian","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

Felucians are a tall, bipedal species. Both their arms and legs end in four, large webbed digits with suction-cup fingertips. Springing from the underside of each forearm is a second short arm, ending in three large and agile fingers. A Felucian's head is a thick mass of long flexible tendrils featuring illuminated tips. The eyes and mouth appear as black holes or openings within this mass.

Society and Culture

Felucians are mysterious sentient beings native to the vast fungal swamps and jungles of Felucia. Though Felucia has long been colonized, the native Felucians avoided notice by living deep in the jungle. Such seclusion was easily maintained. Even the hardiest of colonists were loath to brave the perils of the dangerous wilderness without cause.\n\nThe Felucians are an unusual, amphibious species. They are highly adapted to surviving the wilds of their home planet, and fade easily into its confusing mass of plant life. They are equally at home on land or in the water, and they traverse the swamps with ease.\n\nAll Felucians are part of a single, planetwide tribe that is broken down into smaller villages and communities, each one led by shamans and chieftains. These shamans are very strong in the Force, using it to their own ends with incredible skill.

Names

Felucian names are usually two syllables and full of hard consonants. Surnames are a combination of tribe lineage.

  Male Names. Gokkuul, Kargrek, Hagark, Ruggorn

  Female Names. Lakko, Taarell, Duuna, Frula

  Surnames. s'Gokuul, d'Lakko, s'Kargrek, d'Frula","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Constitution score increases by 2, and your Wisdom score increases by 1.

\n

Age. Felucians reach adulthood in their late teens and live less than a century.

\n

Alignment. Felucians' connection to the Living Force causes them to tend toward the light side, though there are exceptions.

\n

Size. Felucians typically stand over 6 feet tall and generally weigh about 200 lbs. Regardless of your position in that range, your size is Medium.

\n

Speed. Your base walking speed is 30 feet.

\n

Force Sensitive. You know the burst at-will Force power. When you reach 3rd level, you can cast the beast trick Force power once per day. When you reach 5th level, you can also cast the plant surge Force power once per day. Wisdom is your forcecasting ability for these powers.

\n

Amphibious. You can breathe air and water.

\n

Stealthy. You are proficient in the Stealth skill.

\n

Mask of the Wild. You can attempt to hide even when you are only lightly obscured by foliage, heavy rain, falling snow, mist, and other natural phenomena.

\n

Languages. You can speak, read, and write Galactic Basic, Felucianese, and one more language of your choice. Felucianese is characterized by guttural, whisper-like vowels, interspersed with hard clicks.

"},"skinColorOptions":{"value":"Gray, with blue, red, or yellow markings"},"hairColorOptions":{"value":"None"},"eyeColorOptions":{"value":"Red"},"distinctions":{"value":"Extra limb at elbow, innate Force-sensitivity"},"heightAverage":{"value":"5'8\""},"heightRollMod":{"value":"+2d6\""},"weightAverage":{"value":"165 lb."},"weightRollMod":{"value":"x(2d4) lb."},"homeworld":{"value":"Felucia"},"slanguage":{"value":"Felucianese"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"alwaysActive":false,"effects":[{"modSpecKey":"data.details.species","value":"Felucian","mode":"=","targetSpecific":false,"id":1,"itemId":"aMOoVFBHoKmmBPe3","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.con.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"aMOoVFBHoKmmBPe3","active":false,"_targets":[],"label":"Abilities Constitution"},{"modSpecKey":"data.abilities.wis.value","value":"1","mode":"+","targetSpecific":false,"id":3,"itemId":"aMOoVFBHoKmmBPe3","active":false,"_targets":[],"label":"Abilities Wisdom"},{"modSpecKey":"data.traits.size","value":"med","mode":"=","targetSpecific":false,"id":4,"itemId":"aMOoVFBHoKmmBPe3","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"30","mode":"=","targetSpecific":false,"id":5,"itemId":"aMOoVFBHoKmmBPe3","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.skills.ste.value","value":"1","mode":"+","targetSpecific":false,"id":6,"itemId":"aMOoVFBHoKmmBPe3","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":7,"itemId":"aMOoVFBHoKmmBPe3","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"felucianese","mode":"+","targetSpecific":false,"id":8,"itemId":"aMOoVFBHoKmmBPe3","active":false,"_targets":[]}]},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Felucian.webp","effects":[{"_id":"0sYjEeYfNqJnGDdC","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Felucian","mode":5,"priority":5},{"key":"data.abilities.con.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20},{"key":"data.traits.size","value":"med","mode":0,"priority":5},{"key":"data.attributes.movement.walk","value":"30","mode":5,"priority":5},{"key":"data.skills.ste.value","value":1,"mode":4,"priority":20},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"felucianese","mode":0,"priority":0},{"key":"flags.sw5e.amphibious","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.maskOfTheWild","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Felucian.webp","label":"Felucian","tint":"","transfer":true}]} {"_id":"JxGOgqePQT2ztJ64","name":"Zeltron","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

Zeltrons are one of the few near-human species that differentiated from the baseline stock enough to be considered a new species of the Human genus, rather than simply a subspecies. They possess two biological traits of note. The first is that they all produce potent pheromones, similar to the Falleen species, which enhanced their attractiveness and likeability. The second is a limited telepathic ability, used to project emotions onto others, as well as allowing them to read and even feel the emotions of others; some Zeltrons have been hired by the Exchange for this ability. Because of their telepathic ability, positive emotions such as happiness, love and pleasure are very important to them, while negative ones such as anger, fear, or depression are shunned.

Society and Culture

Zeltron culture is highly influenced by sexuality and the pursuit of pleasure in general. Most of their art and literature is devoted to the subject, producing some of the raciest pieces in the galaxy. Zeltrons are known to dress in wildly colorful or revealing attire. It's common to see Zeltrons wearing shockingly bright shades of neon colors in wildly designed bikinis, or nearly skin tight clothing of other sorts with bizarre color designs, patterns, and symbols.

Names

Zeltron names often have an air of mystique to them, to evoke sensuality. For a Zeltron, a beautiful face is nothing without an equally beautiful name. It's not uncommon for a Zeltron to forsake their familial surname in favor of a more attractive-sounding one.

  Male Names. Marruc, Bahb, Rahulh, Demagol

  Female Names. Lyshaa, Dani, Vianna, Chantique

  Surnames. D'Pow, Blue, Duare, Sapphire","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Charisma score increases by 2, and your Constitution score increases by 1.

Age. Zeltron reach adulthood in their late teens and live about 80 years.

Alignment. Zeltron are a deeply sensual, hedonistic species, causing them to tend toward chaotic balanced or dark side alignments, though there are exceptions.

Size. Zeltron tend to be slender and statuesque, typically standing between 5 and 6 feet tall and rarely weighing more than 150 lb. Regardless of your position in that range, your size is Medium.

Speed. Your base walking speed is 30 feet.

Charismatic. You have proficiency with Deception or Persuasion (your choice).

Enthralling Pheromones. You can use your pheromones to influence individuals of both sexes. Whenever you roll a 1 on a Charisma (Persuasion) check, you can reroll the die and must use the new roll. Additionally, once per short or long rest, you can treat a d20 roll of 9 or lower on a Charisma check as a 10. This feature has no effect on droids or constructs.

Natural Empathy. Zeltron's limited telepathy allow them to sense mood shifts in those around them. You have advantage on Wisdom (Insight) checks to determine emotions against humanoids and beasts within 10 feet of you.

Two Livered. Zeltron have two livers, which makes them adept at filtering toxins. You have advantage on saving throws against poison, and you have resistance against poison damage (explained in chapter 9).

Languages. You can speak, read, and write Galactic Basic and one language of your choice.

"},"skinColorOptions":{"value":"Light pink to deep crimson"},"hairColorOptions":{"value":"Blue, brown, pink, or red"},"eyeColorOptions":{"value":"Hazel, silver, amber"},"distinctions":{"value":"Capable of producing powerful pheromones"},"heightAverage":{"value":"4'8\""},"heightRollMod":{"value":"+2d10\""},"weightAverage":{"value":"90 lb."},"weightRollMod":{"value":"x(2d4) lb."},"homeworld":{"value":"Zeltros"},"slanguage":{"value":"Galactic Basic"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"alwaysActive":false,"effects":[{"modSpecKey":"data.details.species","value":"Zeltron","mode":"=","targetSpecific":false,"id":1,"itemId":"TSnP7ODGs6vIPQNe","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.cha.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"TSnP7ODGs6vIPQNe","active":false,"_targets":[],"label":"Abilities Charisma"},{"modSpecKey":"data.abilities.con.value","value":"1","mode":"+","targetSpecific":false,"id":3,"itemId":"TSnP7ODGs6vIPQNe","active":false,"_targets":[],"label":"Abilities Constitution"},{"modSpecKey":"data.traits.size","value":"med","mode":"=","targetSpecific":false,"id":4,"itemId":"TSnP7ODGs6vIPQNe","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"30","mode":"=","targetSpecific":false,"id":5,"itemId":"TSnP7ODGs6vIPQNe","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.traits.dr.value","value":"poison","mode":"+","targetSpecific":false,"id":6,"itemId":"TSnP7ODGs6vIPQNe","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":7,"itemId":"TSnP7ODGs6vIPQNe","active":false,"_targets":[]}]},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Zeltron.webp","effects":[{"_id":"67J2jENh3a5uydk3","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Zeltron","mode":5,"priority":5},{"key":"data.abilities.cha.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.traits.size","value":"med","mode":0,"priority":5},{"key":"data.attributes.movement.walk","value":"30","mode":5,"priority":5},{"key":"data.traits.dr.value","value":"poison","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"flags.sw5e.enthrallingPheromones","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Zeltron.webp","label":"Zeltron","tint":"","transfer":true}]} {"_id":"L7jVJxWNNsWAPj6c","name":"Droid, Class V","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"
\n

Players as Droids

\n

Work with your GM to determine if playing as a droid is appropriate for your campaign. Droids are impervious to many effects and vulnerable to others. If your GM approves this choice of species, work with them to determine your droids designation, name, and appearance. If you want to play a different type of droid, work with your GM to find traits to realize your character.

\n
\n

Appearance

\n

Class V droids are typically vaguely human-like in both shape and size, standing at around 6 feet, although many can be larger. They are usually a polished metallic color, though this can vary based on tasks for which they are created, their affiliation, or quirks of their owner.

\n

They are noteworthy for their simple programming and quiet, focused work habits.

\n

Utility

\n

Class V droids are programmed for menial and low-skill tasks. Such droids tend to perform basic tasks such as construction, lifting, maintenance, mining, sanitation, and transportation. Labor droids are fifth-degree droids.

\n

Names

\n

Droids are typically called by their designation, given to them when they are created, or some affectation given to them by their owner. Often this affectation is a play on their designation.

\n

Occasionally, noteworthy droids will earn monikers based on their accomplishments.

","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Strength score increases by 2, and your Dexterity or Constitution score increases by 1.

\n

Age. Droids don’t age, though they require maintenance to retain functionality.

\n

Alignment. Droids tend toward no particular alignment. The best and worst are found among them.

\n

Size. Class V droids typically stand between 5 and 7 feet and weigh around 260 lbs. Regardless of your position in that range, your size is Medium.

\n

Speed. Your base walking speed is 30 feet.

\n

Type. Your creature type is droid.

\n

Armor Integration. You cannot wear armor, but you can have the armor professionally integrated into your chassis over the course of a long rest. This work must be done by someone proficient with astrotech’s implements. You must be proficient in armor in order to have it integrated.

\n

Droid Resistances. You are resistant to necrotic, poison, and psychic damage, and are immune to poison and disease.

\n

Droid Systems. You do not need to eat or drink. Additionally, you no longer require a tech focus to cast tech powers.

\n

Droid Vulnerabilities. You are vulnerable to ion damage. Additionally, you have disadvantage on saving throws against effects that would deal ion or lightning damage.

\n

Force Insensitive. While droids can be manipulated by many force powers, they cannot sense the Force. You can not use force powers or take levels in forcecasting classes.

\n

Maintenance Mode. Rather than sleep, you enter an inactive state to perform routine maintenance for 4 hours each day. You have disadvantage on Wisdom (Perception) checks while performing maintenance.

\n

Manual Laborer. You have proficiency in Athletics and one set of artisan’s implements of your choice.

\n

Powerful Build. Your carrying capacity and the weight you can push, drag, or lift doubles. If it would already double, it instead triples.

\n

Rapid Reconstruction. You are built with internal repair mechanisms. As a bonus action, you can choose to spend one of your Hit Dice to recover hit points.

\n

Languages. You can speak, read, and write Binary. You can understand spoken and written Galactic Basic and one language of your choice, but you cannot speak it.

"},"skinColorOptions":{"value":""},"hairColorOptions":{"value":""},"eyeColorOptions":{"value":""},"distinctions":{"value":""},"colorScheme":{"value":"Typically metallic"},"droidDistinctions":{"value":"Vaguely human-like size and shape, typically quiet"},"heightAverage":{"value":"5'2\""},"heightRollMod":{"value":"+2d12\""},"weightAverage":{"value":"140 lb."},"weightRollMod":{"value":"x(2d8) lb."},"homeworld":{"value":""},"slanguage":{"value":""},"manufacturer":{"value":"Cybot Galactica, Industrial Automaton"},"droidLanguage":{"value":"Binary"},"source":"PHB"},"flags":{"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Droid%20Class%20V.webp","effects":[{"_id":"Z3v7Cj4aSpJg4K8C","flags":{"dae":{"stackable":false,"transfer":true}},"changes":[{"key":"data.abilities.str.value","value":2,"mode":2,"priority":20},{"key":"data.details.species","value":"Droid, Class V","mode":5,"priority":20},{"key":"data.traits.size","value":"med","mode":0,"priority":20},{"key":"data.attributes.movement.walk","value":"30","mode":5,"priority":20},{"key":"data.traits.dr.value","value":"necrotic","mode":0,"priority":20},{"key":"data.traits.dr.value","value":"poison","mode":0,"priority":20},{"key":"data.traits.dr.value","value":"psychic","mode":0,"priority":20},{"key":"data.traits.ci.value","value":"diseased","mode":0,"priority":20},{"key":"data.traits.ci.value","value":"poisoned","mode":0,"priority":20},{"key":"data.traits.dv.value","value":"ion","mode":0,"priority":20},{"key":"data.skills.ath.value","value":1,"mode":4,"priority":20},{"key":"flags.sw5e.powerfulBuild","value":"1","mode":5,"priority":20},{"key":"data.traits.languages.value","value":"binary","mode":0,"priority":20},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":20},{"key":"flags.sw5e.forceInsensitive","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.maintenanceMode","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.rapidReconstruction","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Droid%20Class%20V.webp","label":"Droid, Class V","tint":"","transfer":true}]} @@ -119,3 +119,5 @@ {"_id":"ynuvnI54pMKLwF2a","name":"Echani","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

Echani are characterized by their white skin, hair, and eyes, and their remarkable tendency to look very much alike one another to outside observers, particularly amongst family members. It is thought that their origins stem from Arkanian experimentation on the human genome, a hypothesis that could explain their physical conformity.

Society and Culture

A matriarchal, caste-based society originating from the Inner Rim world of Eshan, the echani spread to encompass a confederacy of six worlds including Bengali and Thyrsus, known as the Six Sisters, governed by the all-female Echani Command. \r\n\t\r\nEchani generals are sometimes seen by others as having the ability to predict their opponent's next move. This is no biologicial trait inherent to the species, but rather stems from the fact that combat is so ingrained into every level of echani culture; the echani hold to the idea that combat is the truest form of communication, and to know someone fully, you must fight them. While their combat rituals require complete freedom of movement and unarmed martial arts, in warfare, they tend towards light armor and melee weapons, and are considered excellent craftsmen of such.

Names

Echani names tend to lack hard consonants, but are otherwise as variable as human ones. Echani surnames are tied directly to their place in the caste system.

  Male Names. Caelian, Inarin, Losor, Uelis, Yusanis

  Female Names. Astri, Brianna, Isena, Raskta, Senriel

  Surnames. Authal, Elysi, Fenni, Kinro, Lsu","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Dexterity score increases by 2, and your Wisdom score increases by 1.

\n

Age. Echani reach adulthood in their late teens and live less than a century.

\n

Alignment. Echani culture's emphasis on honor and combat cause them to tend towards lawful alignments, though there are exceptions.

\n

Size. Echani stand between 5 and a half and 6 feet tall and weigh around 150 lbs, with little variation between them. Your size is Medium.

\n

Speed. Your base walking speed is 30 feet.

\n

Allies of the Force. Whenever you make a Wisdom (Insight) check against someone you know to wield the Force, you are considered to have expertise in the Insight skill.

\n

Combative Culture. You have proficiency in Lore and Acrobatics.

\n

Echani Art. If a humanoid you can see makes a melee weapon attack, you can use your reaction to make a Wisdom (Insight) check against the target's Charisma (Deception). On a success you learn one of the following traits about that creature: it's Strength, Dexterity or Constitution score; bonus to Strength, Dexterity or Constitution saving throws; armor class; or current hit points. On a failure, the target becomes immune to this feature for one day. You can use this ability a number of times equal to your Wisdom modifier (a minimum of once). You regain all expended uses on a long rest.

\n

Martial Upbringing. You have proficiency in light armor, and gain proficiency with two martial vibroweapons of your choice.

\n

Unarmed Combatant. Your unarmed strikes deal 1d6 kinetic damage. You can use your choice of your Strength or Dexterity modifier for the attack and damage rolls. You must use the same modifier for both rolls.

\n

Languages. You can speak, read, and write Galactic Basic and one extra language of your choice.

"},"skinColorOptions":{"value":"Pale tones"},"hairColorOptions":{"value":"White"},"eyeColorOptions":{"value":"Silver"},"distinctions":{"value":"Fair skin, white hair and eyes, remarkable familial similarity."},"heightAverage":{"value":"5'1\""},"heightRollMod":{"value":"+1d10\""},"weightAverage":{"value":"105 lb."},"weightRollMod":{"value":"x(2d4) lb."},"homeworld":{"value":"Eshan"},"slanguage":{"value":"Galactic Basic"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"alwaysActive":false,"effects":[{"modSpecKey":"data.details.species","value":"Echani","mode":"=","targetSpecific":false,"id":1,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.dex.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Abilities Dexterity"},{"modSpecKey":"data.abilities.wis.value","value":"1","mode":"+","targetSpecific":false,"id":3,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Abilities Wisdom"},{"modSpecKey":"data.traits.size","value":"med","mode":"=","targetSpecific":false,"id":4,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"30","mode":"=","targetSpecific":false,"id":5,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.skills.lor.value","value":"1","mode":"+","targetSpecific":false,"id":6,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Skills Lore"},{"modSpecKey":"data.skills.acr.value","value":"1","mode":"+","targetSpecific":false,"id":7,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Skills Acrobatics"},{"modSpecKey":"data.traits.armorProf.value","value":"lgt","mode":"+","targetSpecific":false,"id":8,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":9,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[]}]},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Echani.webp","effects":[{"_id":"HNBMxZCToQEy6PSr","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Echani","mode":5,"priority":5},{"key":"data.abilities.dex.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20},{"key":"data.traits.size","value":"med","mode":0,"priority":5},{"key":"data.attributes.movement.walk","value":"30","mode":5,"priority":5},{"key":"data.skills.lor.value","value":1,"mode":4,"priority":20},{"key":"data.skills.acr.value","value":1,"mode":4,"priority":20},{"key":"data.traits.armorProf.value","value":"lgt","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"flags.sw5e.unarmedCombatant","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Echani.webp","label":"Echani","tint":"","transfer":true}]} {"_id":"yyCUAG4cUUKh4IUz","name":"Iktotchi","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

Iktotchi do not have hair, but rather they had a very resistant skin which protected them from the violent winds which crossed the satellite. Both males and females have down-curved cranial horns, which gave them an aggressive aspect. The males' horns are generally a little larger, a remnant from their mountain-dwelling, caprinaen ancestors. The horns are able to regenerate if damaged.

Society and Culture

The Iktotchi are a fiercely guarded and isolationist species - vaunted for their ability to hide their feelings and bury any semblance of emotion. Originating on the harsh, windy moon of Iktotch, which orbits the planet Iktotchon in the Expansion Region, the Iktotch are gifted with precognition, and are courted as often by Jedi as by pirates for their skills.\r\n\r\nIktotchi society is a stratified society. Upward mobility is both possible and encouraged. Iktotchi are an outwardly dispassionate people, which is evidenced by their culture. They have a robust legal system, and suffer little crime. Iktotchi are respectful of cultures other than their own and can easily integrate with others.\r\n\r\nIktotchi who distinguish themselves often earn a titular nickname, by which they are referred to in place of their name. Generally, this is done by accomplishing a remarkable feat that benefits the Iktotchi as whole.

Names

Iktotchi names are generally two syllables. Surnames are familial. Respected Iktotchi often adopt a nickname, which they use in place of their birth name.

  Male Names. Dilnam, Imruth, Kashkil, Yellam

  Female Names. Kemkal, Onyeth, Reshu, Zorlu

  Surnames. Hevil, Kaawi, Mimir, Nudaal, Zelend","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Intelligence score increases by 2, and your Strength score increases by 1.

\n

Age. Iktotchi reach adulthood in their late teens and live less than a century.

\n

Alignment. Iktotchi are lawful and tend toward the light side, though there are exceptions.

\n

Size. Iktotchi typically stand between 5 and 6 feet tall and weigh about 170 lbs. Regardless of your position in that range, your size is Medium.

\n

Speed. Your base walking speed is 30 feet.

\n

Precognition. You can see brief fragments of the future that allow you to turn failures into successes. When you roll a 1 on an attack roll, ability check, or saving throw, you can reroll the die and must use the new roll.

\n

Telepathy. You can communicate telepathically with creatures within 30 feet of you. You must share a language with the target in order to communicate in this way.

\n

Horns. Your horns are a natural weapon, which you can use to make unarmed strikes. If you hit with it, you deal kinetic damage equal to 1d6 + your Strength modifier.

\n

Pilot. You have proficiency in the Piloting skill.

\n

Languages. You can speak, read, and write Galactic Basic and Iktotchese.

"},"skinColorOptions":{"value":"Pink"},"hairColorOptions":{"value":"None"},"eyeColorOptions":{"value":"Black"},"distinctions":{"value":"Horns, precognition, telepathy, thick pink skin"},"heightAverage":{"value":"4'11\""},"heightRollMod":{"value":"+2d10\""},"weightAverage":{"value":"120 lb."},"weightRollMod":{"value":"x(2d4) lb."},"homeworld":{"value":"Iktotch, moon of Iktotchon"},"slanguage":{"value":"Iktotchese"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"colorScheme":{"value":""},"manufacturer":{"value":""},"planguage":{"value":""},"droidDistinctions":{"value":""},"droidLanguage":{"value":""},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"alwaysActive":false,"effects":[{"modSpecKey":"data.details.species","value":"Iktotchi","mode":"=","targetSpecific":false,"id":1,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.int.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[],"label":"Abilities Intelligence"},{"modSpecKey":"data.abilities.str.value","value":"1","mode":"+","targetSpecific":false,"id":3,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[],"label":"Abilities Strength"},{"modSpecKey":"data.traits.size","value":"med","mode":"=","targetSpecific":false,"id":4,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"30","mode":"=","targetSpecific":false,"id":5,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.skills.pil.value","value":"1","mode":"+","targetSpecific":false,"id":6,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":7,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"iktotchese","mode":"+","targetSpecific":false,"id":8,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.custom","value":"telepathy (30 ft.)","mode":"+","targetSpecific":false,"id":9,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[]}]},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Iktotchi.webp","effects":[{"_id":"MAjUMql3tivJTfVO","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Iktotchi","mode":5,"priority":5},{"key":"data.abilities.int.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20},{"key":"data.traits.size","value":"med","mode":0,"priority":5},{"key":"data.attributes.movement.walk","value":"30","mode":5,"priority":5},{"key":"data.skills.pil.value","value":1,"mode":4,"priority":20},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"iktotchese","mode":0,"priority":0},{"key":"data.traits.languages.custom","value":"telepathy (30 ft.)","mode":0,"priority":0},{"key":"flags.sw5e.precognition","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Iktotchi.webp","label":"Iktotchi","tint":"","transfer":true}]} {"_id":"zKpCsa8WCfz9abwv","name":"Killik","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

\n

Killiks possess a strong chitinous exoskeleton that is glossy and greenish with their carcasses capable of surviving thousands of years of erosion as seen by the colonists of Alderaan. The exoskeleton also contains a number of spiracles which served as their way of breathing. Typically, these Human-sized hive creatures have four arms with each ending in a powerful three-fingered claw. They stand on two stout legs that are capable of leaping great distances. Killiks can communicate with other Killiks through use of pheromones.

\n

Society and Culture

\n

The Killiks have a communal society, with each and every Killik being in mental contact with another. Due to their hive mind, every Killik nest is virtually one individual. Killiks are also peaceful in nature. Their telepathic connection is capable of extending to other species which includes non-insectoids. A willing creature can submit to this telepathy to become a Joiner. They effectively become another vessel of the hive mind. Killiks lose connection to their hive mind at great distances. Those who voluntarily leave the hive mind are referred to as Leavers. It is rare that they are allowed to rejoin their hive without reason.

\n

Names

\n

Killiks are a hive-mind insectoid that typically don't use names. On the off chance they do, it's usually an incomprehensible series of clicking noises. They are receptive to nicknames given by others.

","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Intelligence score increases by 2, and your Constitution score increases by 1.

Age. Killiks reach adulthood in their 40s and live an average of 200 years.

Alignment. Killiks' willingness to brainwash or kill their enemies cause them to tend towards the dark side, though there are exceptions.

Size. Killiks stand between 5 and 6 feet tall and weigh about 160 lbs. Regardless of your position in that range, your size is Medium.

Speed. Your base walking speed is 30 feet.

Four-Armed. Killiks have four arms which they can use independently of one another. You can only gain the benefit of items held by two of your arms at any given time, and once per round you can switch which arms you are benefiting from (no action required).

Hardened Carapace. While you are unarmored or wearing light armor, your AC is 13 + your Dexterity modifier.

Strong-Legged. When you make a long jump, you can cover a number of feet up to twice your Strength score. When you make a high jump, you can leap a number of feet up into the air equal to 3 + twice your Strength modifier.

Telepathy. You can communicate telepathically with creatures within 30 feet of you. You must share a language with the target in order to communicate in this way.

Languages. You can speak, read, and write Killik. You can understand spoken and written Galactic Basic, but your vocal cords do not allow you to speak it.

"},"skinColorOptions":{"value":"Brown, chestnut, green, red, scarlet, or yellow"},"hairColorOptions":{"value":"None"},"eyeColorOptions":{"value":"Black or orange"},"distinctions":{"value":"Chitinous armor, mandibles projected from face, four arms ending in long three toed claws protrude from their torsos"},"heightAverage":{"value":"4'9\""},"heightRollMod":{"value":"+2d10\""},"weightAverage":{"value":"110 lb."},"weightRollMod":{"value":"x(2d4) lb."},"homeworld":{"value":"Alderaan"},"slanguage":{"value":"Killik"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"alwaysActive":false,"effects":[{"modSpecKey":"data.details.species","value":"Killik","mode":"=","targetSpecific":false,"id":1,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.int.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[],"label":"Abilities Intelligence"},{"modSpecKey":"data.abilities.con.value","value":"1","mode":"+","targetSpecific":false,"id":3,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[],"label":"Abilities Constitution"},{"modSpecKey":"data.traits.size","value":"med","mode":"=","targetSpecific":false,"id":4,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"30","mode":"=","targetSpecific":false,"id":5,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.attributes.ac.min","value":"13","mode":"=","targetSpecific":false,"id":6,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[],"label":"Attributes Armor Class Min"},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":7,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"killik","mode":"+","targetSpecific":false,"id":8,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.custom","value":"telepathy (30 ft.)","mode":"+","targetSpecific":false,"id":9,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[]}]},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Killik.webp","effects":[{"_id":"EWQOMXrZbfi4zSPF","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Killik","mode":5,"priority":5},{"key":"data.abilities.int.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.traits.size","value":"med","mode":0,"priority":5},{"key":"data.attributes.movement.walk","value":"30","mode":5,"priority":5},{"key":"data.attributes.ac.value","value":"13+@abilities.dex.mod","mode":5,"priority":1},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"killik","mode":0,"priority":0},{"key":"data.traits.languages.custom","value":"telepathy (30 ft.)","mode":0,"priority":0},{"key":"flags.sw5e.strongLegged","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.extraArms","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Killik.webp","label":"Killik","tint":"","transfer":true}]} +{"_id":"J7HJQkcvghtwMAdF","name":"Anzellan","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

\n

The anzellans are a diminutive species hailing from the secluded planet Anzella. Their eyes have floating corneal micro-lenses that allow them to see microscopic details. Anzellans are a bubbly and receptive people. They are a jovial and trusting people that tend to welcome strangers with open arms. Due to their volcanic homeworld, anzellans are also adapted towards heat. This, coupled with their small size, make them well-suited to working in compact places.

\n

Society and Culture

\n

Anzella is a tropical planet covered in thousands of small volcanic islands. Many of these islands are developed as small villages, with the largest islands designed to accommodate larger species. Anzellan culture is generally based around tourism and crafting; in fact, anzellans are renowned craftsmen due to their discerning eyesight and ability to fit into small spaces. Anzellan government is generally casual. Each village has its own governing council of rotating members; these villages act independently from one another unless their decisions would affect more than a single island. In that case, all of the councils work together to come to a planet-wide decision.

\n

Names

\n

Anzellan names are rarely longer than two syllables, with a bouncy intonation to them. Their surnames are familial.

\n

  Male Names. Babu, Gridel, Moru, Rano, Yodel

\n

  Female Names. Dibi, Fing, Nooni, Teena, Zazi

\n

  Surnames. E'ayoo, Frik, Meer, Tanni, Vrut

","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Intelligence score increases by 2, and two other ability scores of your choice increase by 1.

Age. Anzellans are considered adults at ten years old. They are a short-lived species, however, that rarely lives longer than 60 years.

Alignment. Anzellans are a friendly and respectful people, which causes them to tend toward lawful light side, though there are exceptions.

Size. Anzellans stand between 1 and 2 feet tall and weigh around 10 lbs. Regardless of your position in that range, your size is Tiny.

Speed. Your base walking speed is 20 feet.

Crafters. You have proficiency in one tool of your choice.

Detail Oriented. You are practiced at scouring for details. You have advantage on Intelligence (Investigation) checks within 5 feet.

Pintsized. Your tiny stature makes it hard for you to wield bigger weapons. You can't use medium or heavy shields. Additionally, you can't wield weapons with the two-handed or versatile property, and you can only wield one-handed weapons in two hands unless they have the light property.

Puny. Anzellans are too small to pack much of a punch. You have disadvantage on Strength saving throws, and when determining your bonus to attack and damage rolls for weapon attacks using Strength, you can't add more than +3.

Small and Nimble. You are too small and fast to effectively target. You have a +1 bonus to AC, and you have advantage on Dexterity saving throws.

Tanned. You are naturally adapted to hot climates, as described in chapter 5 of the Dungeon Master's Guide.

Technician. You are proficient in the Technology skill.

Tinker. You have proficiency with tinker's tools. You can use these and spend 1 hour and 100 cr worth of materials to construct a Tiny Device (AC 5, 1 hp). You can take the Use an Object action to have your device cause one of the following effects: create a small explosion, create a repeating loud noise for 1 minute, create smoke for 1 minute, create a soothing melody for 1 minute. You can maintain a number of these devices up to your proficiency bonus at once, and a device stops functioning after 24 hours away from you. You can dismantle the device to reclaim the materials used to create it.

Languages. You can speak, read, and write Galactic Basic and Anzellan. Anzellan is characterized by its bouncy sound and emphasis on alternating syllables.

","source":"Expanded Content"},"skinColorOptions":{"value":"Brown, green, or tan"},"hairColorOptions":{"value":"Black, gray, or white"},"eyeColorOptions":{"value":"Black"},"distinctions":{"value":"Diminutive size, wispy eyebrows"},"heightAverage":{"value":"1'0\""},"heightRollMod":{"value":"+2d6\""},"weightAverage":{"value":"3 lb."},"weightRollMod":{"value":"x1 lb."},"homeworld":{"value":"Anzella"},"slanguage":{"value":"Anzellan"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"effects":[{"modSpecKey":"data.details.species","value":"Anzellan","mode":"=","targetSpecific":false,"id":1,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.int.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Abilities Intelligence"},{"modSpecKey":"data.traits.size","value":"tin","mode":"=","targetSpecific":false,"id":3,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"20","mode":"=","targetSpecific":false,"id":4,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.attributes.ac.min","value":"1","mode":"+","targetSpecific":false,"id":5,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Attributes Armor Class Min"},{"modSpecKey":"data.skills.tec.value","value":"1","mode":"+","targetSpecific":false,"id":6,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Skills Technology"},{"modSpecKey":"data.traits.toolProf.custom","value":"Tinker's tools","mode":"+","targetSpecific":false,"id":7,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Tool Prof Custom"},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":8,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Language"},{"modSpecKey":"data.traits.languages.value","value":"anzellan","mode":"+","targetSpecific":false,"id":9,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[]}],"alwaysActive":false},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Anzellan.webp","effects":[{"_id":"ZGdzAq1Gl4xaxDRD","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Anzellan","mode":5,"priority":5},{"key":"data.abilities.int.value","value":2,"mode":2,"priority":20},{"key":"data.traits.size","value":"tiny","mode":5,"priority":5},{"key":"data.attributes.movement.walk","value":20,"mode":5,"priority":5},{"key":"data.attributes.ac.value","value":1,"mode":2,"priority":20},{"key":"data.skills.tec.value","value":1,"mode":4,"priority":20},{"key":"data.traits.toolProf.custom","value":"Tinker's tools","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"anzellan","mode":0,"priority":0},{"key":"flags.sw5e.detailOriented","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.pintsized","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.puny","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.tinker","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Anzellan.webp","label":"Anzellan","tint":"","transfer":true}]} +{"_id":"J7HJQkcvghtwMAdF","name":"Anzellan","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

\n

The anzellans are a diminutive species hailing from the secluded planet Anzella. Their eyes have floating corneal micro-lenses that allow them to see microscopic details. Anzellans are a bubbly and receptive people. They are a jovial and trusting people that tend to welcome strangers with open arms. Due to their volcanic homeworld, anzellans are also adapted towards heat. This, coupled with their small size, make them well-suited to working in compact places.

\n

Society and Culture

\n

Anzella is a tropical planet covered in thousands of small volcanic islands. Many of these islands are developed as small villages, with the largest islands designed to accommodate larger species. Anzellan culture is generally based around tourism and crafting; in fact, anzellans are renowned craftsmen due to their discerning eyesight and ability to fit into small spaces. Anzellan government is generally casual. Each village has its own governing council of rotating members; these villages act independently from one another unless their decisions would affect more than a single island. In that case, all of the councils work together to come to a planet-wide decision.

\n

Names

\n

Anzellan names are rarely longer than two syllables, with a bouncy intonation to them. Their surnames are familial.

\n

  Male Names. Babu, Gridel, Moru, Rano, Yodel

\n

  Female Names. Dibi, Fing, Nooni, Teena, Zazi

\n

  Surnames. E'ayoo, Frik, Meer, Tanni, Vrut

","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Intelligence score increases by 2, and two other ability scores of your choice increase by 1.

Age. Anzellans are considered adults at ten years old. They are a short-lived species, however, that rarely lives longer than 60 years.

Alignment. Anzellans are a friendly and respectful people, which causes them to tend toward lawful light side, though there are exceptions.

Size. Anzellans stand between 1 and 2 feet tall and weigh around 10 lbs. Regardless of your position in that range, your size is Tiny.

Speed. Your base walking speed is 20 feet.

Crafters. You have proficiency in one tool of your choice.

Detail Oriented. You are practiced at scouring for details. You have advantage on Intelligence (Investigation) checks within 5 feet.

Pintsized. Your tiny stature makes it hard for you to wield bigger weapons. You can't use medium or heavy shields. Additionally, you can't wield weapons with the two-handed or versatile property, and you can only wield one-handed weapons in two hands unless they have the light property.

Puny. Anzellans are too small to pack much of a punch. You have disadvantage on Strength saving throws, and when determining your bonus to attack and damage rolls for weapon attacks using Strength, you can't add more than +3.

Small and Nimble. You are too small and fast to effectively target. You have a +1 bonus to AC, and you have advantage on Dexterity saving throws.

Tanned. You are naturally adapted to hot climates, as described in chapter 5 of the Dungeon Master's Guide.

Technician. You are proficient in the Technology skill.

Tinker. You have proficiency with tinker's tools. You can use these and spend 1 hour and 100 cr worth of materials to construct a Tiny Device (AC 5, 1 hp). You can take the Use an Object action to have your device cause one of the following effects: create a small explosion, create a repeating loud noise for 1 minute, create smoke for 1 minute, create a soothing melody for 1 minute. You can maintain a number of these devices up to your proficiency bonus at once, and a device stops functioning after 24 hours away from you. You can dismantle the device to reclaim the materials used to create it.

Languages. You can speak, read, and write Galactic Basic and Anzellan. Anzellan is characterized by its bouncy sound and emphasis on alternating syllables.

","source":"Expanded Content"},"skinColorOptions":{"value":"Brown, green, or tan"},"hairColorOptions":{"value":"Black, gray, or white"},"eyeColorOptions":{"value":"Black"},"distinctions":{"value":"Diminutive size, wispy eyebrows"},"heightAverage":{"value":"1'0\""},"heightRollMod":{"value":"+2d6\""},"weightAverage":{"value":"3 lb."},"weightRollMod":{"value":"x1 lb."},"homeworld":{"value":"Anzella"},"slanguage":{"value":"Anzellan"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"effects":[{"modSpecKey":"data.details.species","value":"Anzellan","mode":"=","targetSpecific":false,"id":1,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.int.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Abilities Intelligence"},{"modSpecKey":"data.traits.size","value":"tin","mode":"=","targetSpecific":false,"id":3,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"20","mode":"=","targetSpecific":false,"id":4,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.attributes.ac.min","value":"1","mode":"+","targetSpecific":false,"id":5,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Attributes Armor Class Min"},{"modSpecKey":"data.skills.tec.value","value":"1","mode":"+","targetSpecific":false,"id":6,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Skills Technology"},{"modSpecKey":"data.traits.toolProf.custom","value":"Tinker's tools","mode":"+","targetSpecific":false,"id":7,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Tool Prof Custom"},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":8,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Language"},{"modSpecKey":"data.traits.languages.value","value":"anzellan","mode":"+","targetSpecific":false,"id":9,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[]}],"alwaysActive":false},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Anzellan.webp","effects":[{"_id":"ZGdzAq1Gl4xaxDRD","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Anzellan","mode":5,"priority":5},{"key":"data.abilities.int.value","value":2,"mode":2,"priority":20},{"key":"data.traits.size","value":"tiny","mode":5,"priority":5},{"key":"data.attributes.movement.walk","value":20,"mode":5,"priority":5},{"key":"data.attributes.ac.value","value":1,"mode":2,"priority":20},{"key":"data.skills.tec.value","value":1,"mode":4,"priority":20},{"key":"data.traits.toolProf.custom","value":"Tinker's tools","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"anzellan","mode":0,"priority":0},{"key":"flags.sw5e.detailOriented","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.pintsized","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.puny","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.tinker","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Anzellan.webp","label":"Anzellan","tint":"","transfer":true}]} diff --git a/packs/packs/techpowers.db b/packs/packs/techpowers.db index 511b8ffb..aab4a3c4 100644 --- a/packs/packs/techpowers.db +++ b/packs/packs/techpowers.db @@ -20,6 +20,7 @@ {"_id":"6iIYMjmyWE2wqgzf","name":"Carbonite","permission":{"default":0},"type":"power","data":{"description":{"value":"

You attempt to freeze one creature that you can see within range into carbonite. The creature must make a Constitution saving throw. On a failed save, it is restrained as its flesh begins to harden. On a successful save, the creature isn't affected.

\n

A creature restrained by this power must make another Constitution saving throw at the end of each of its turns. If it successfully saves against this power three times, the power ends. If it fails its saves three times, it is turned to stone and subjected to the petrified condition for the duration. The successes and failures don't need to be consecutive; keep track of both until the target collects three of a kind.

\n

If the creature is physically broken while frozen in carbonite, it suffers from similar deformities if it reverts to its original state.

\n

If you maintain your concentration on this power for the entire possible duration, the creature is frozen in carbonite until the effect is removed.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"save","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"con","dc":null,"scaling":"power"},"level":6,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Carbonite.webp"} {"_id":"6oJ07fhBJLXJ7zv1","name":"Scramble Interface","permission":{"default":0},"type":"power","data":{"description":{"value":"

You choose one droid or construct you can see within range and scramble its ability to differentiate targets. The target must make an Intelligence saving throw. If the construct has the 'Piloted' trait, and has a pilot controlling it that is not incapacitated, it gains a bonus to the saving throw equal to the pilot's Intelligence modifier. On a failed save, the target loses the ability to distinguish friend from foe, regarding all creatures it can see as enemies until the power ends. Each time the target takes damage, it can repeat the saving throw, ending the effect on itself on a success.

\n

Whenever the affected creature chooses another creature as a target, it must choose the target at random from among the creatures it can see within range of the attack, power, or other ability it's using. If an enemy provokes an opportunity attack from the affected creature, the creature must make that attack if it is able to.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"save","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"int","dc":null,"scaling":"power"},"level":3,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/ScrambleInterface.webp"} {"_id":"73W8rKPEbN60y7L2","name":"Defibrillate","permission":{"default":0},"type":"power","data":{"description":{"value":"

You touch a creature that has died within the last minute and administer a shock to restore it to life. That creature returns to life with 1 hit point. This power can't return to life a creature that has died of old age, nor can it restore any missing body parts. If the creature is lacking body parts or organs integral for its survival�its head, for instance�the power automatically fails. Once this power has restored a creature to life, it cannot benefit from this power again until it finishes a short or long rest.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":4,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Defibrillate.webp"} +{"name":"Tri-Shot","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"

Choose up to three creatures within range, none of whom can be more than 10 feet apart. If you choose three creatures, each target must succeed on a Dexterity saving throw or take 1d4 energy damage. If you choose two creatures, each target takes 1d6 energy damage on a failed save instead. If you choose only one creature, the target takes 1d8 energy damage on a failed save.

\n

This power's damage increases by one die when you reach 5th level (two dice), 11th level (three dice), and 17th level (four dice).

","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":3,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.Tjrr6QDVOaLdRHZO"}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Tri-Shot.webp","effects":[],"_id":"782klgZPJ4zjZX4E"} {"_id":"7Twjeo1X2oUP9IZo","name":"Elemental Accelerant","permission":{"default":0},"type":"power","data":{"description":{"value":"

Choose one creature you can see and one damage type: acid, cold, fire, lightning, or sonic. The target must make a Constitution saving throw. If it fails, the first time on each turn when it takes damage of the chosen type, it takes an extra 2d6 damage of it. The target also loses resistance to the type until the power ends.

\n

Overcharge Tech. You can target one additional creature for each slot level above 4th. The creatures must be within 30 feet of each other when you target them.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":90,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"save","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["2d6",""]],"versatile":""},"formula":"","save":{"ability":"con","dc":null,"scaling":"power"},"level":4,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/ElementalAccelerant.webp"} {"_id":"7khirDTQvs7rtLbW","name":"Copy","permission":{"default":0},"type":"power","data":{"description":{"value":"

This power creates a perfect duplicate of any written, drawn, or digital visual, audio or text-based data that you touch onto a datapad or datacard you supply. You can copy up to 10 pages of text or 10 minutes of visual or audio data with one casting of this power. Enhanced documents, such as datacrons, blueprints, or encrypted documents, can't be copied with this power.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"object"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Copy.webp"} {"_id":"7o2xvsn9AVML11ME","name":"Storming Shot","permission":{"default":0},"type":"power","data":{"description":{"value":"

As a part of the action used to cast this power, you must make a ranged weapon attack against one creature within your weapon's range, otherwise the power fails. On a hit, the target suffers the attack's normal effects and becomes shocked until the end of your next turn. When this power hits a target, if there is a creature within 30 feet who is shocked, an arc of lightning courses between the two creatures, dealing 1d6 lightning damage to both of them. If there are multiple other creatures who are shocked, the lightning leaps to the closest creature.

\n

The power's damage increases when you reach higher levels. At 5th level, the effects of both the ranged weapon attack and discharge deal an extra 1d6 lightning damage. Both damage rolls increase by an additional 1d6 at 11th and 17th level.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"round"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"spec"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"other","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6","lightning"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"atwill","formula":"1d6"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/StormingShot.webp"} @@ -132,6 +133,7 @@ {"_id":"Zk697eWSqME5gpkF","name":"Condense/Vaporize","permission":{"default":0},"type":"power","data":{"description":{"value":"

In an open container, you can create up to 10 gallons of drinkable water. You may also produce a rain that falls within a 30-foot cube and extinguishes open-air flames. You can destroy the same amount of water in an open container, or destroy a 30-foot cube of fog.

\n

Overcharge Tech. When you cast this power using a tech slot of 2nd level or higher, the amount of water you can create increases by 10 gallons, or the size of the cube increases by 5 feet, for each slot level above 1st.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"object"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/CondenseVaporize.webp"} {"_id":"a4nZ05eHgTXZo4TU","name":"Cage","permission":{"default":0},"type":"power","data":{"description":{"value":"

An immobile, Invisible, cube-shaped prison composed of energy springs into existence around an area you choose within range. The prison can be a cage or a solid box as you choose.

\n

A prison in the shape of a cage can be up to 20 feet on a side and is made from 1/2-inch diameter bars spaced 1/2 inch apart.

\n

A prison in the shape of a box can be up to 10 feet on a side, creating a solid barrier that prevents any matter from passing through it and blocking any powers cast into or out of the area.

\n

When you cast the power, any creature that is completely inside the cage's area is trapped. Creatures only partially within the area, or those too large to fit inside the area, are pushed away from the center of the area until they are completely outside the area.

\n

A creature inside the cage can't leave it by unenhanced means. If the creature tries to teleport to leave the cage, it must first make a Charisma saving throw. On a success, the creature can use that power to exit the cage. On a failure, the creature can't exit the cage and wastes the use of the power or effect.

\n

This power can't be dispelled.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":20,"units":"ft","type":"cube"},"range":{"value":100,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":7,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Cage.webp"} {"_id":"aJqCpoejohXYuJ1J","name":"Sonic Strike","permission":{"default":0},"type":"power","data":{"description":{"value":"

As part of the action used to cast this power, you must make a melee weapon attack against one creature within your reach, otherwise the power fails. On a hit, the target suffers the attack's normal effects, and you begin to emanate a disturbing hum. If a hostile creature ends its turn within 5 feet of you before the start of your next turn, it takes 1d4 sonic damage.

\n

This power's damage increases when you reach higher levels. At 5th level, the melee attack deals an extra 1d8 sonic damage to the target, and the secondary damage increases by 1d4. Both damage rolls increase by 1d8 and 1d4, respectively, at 11th level and 17th level.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"round"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"spec"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"util","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4","sonic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"atwill","formula":"1d8"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/SonicStrike.webp"} +{"name":"Aid Droid","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"

Choose one droid or construct that you can see within range. The target can immediately use its reaction to regain hit points equal to 1d6 + your techcasting ability modifier.

\n

Overcharge Tech. When you cast this power using a tech slot of 2nd level or higher, the healing increases by 1d6 for each slot level above 1st.

","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":1,"width":null,"units":"","type":"droid"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"heal","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6+@abilities.int.mod","healing"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1d6"}},"flags":{"core":{"sourceId":"Item.xI9OeBQggss4H0AI"}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Aid%20Droid.webp","effects":[],"_id":"aW2gm0t0dNq2YMcm"} {"_id":"b0T7rxNgDtGx3mwh","name":"Stack the Deck","permission":{"default":0},"type":"power","data":{"description":{"value":"

You boost up to three creatures of your choice within range. Whenever a target makes an attack roll or a saving throw before the power ends, the target can roll a d4 and add the number rolled to the attack roll or saving throw.

\n

Overcharge Tech. When you cast this power with a tech slot of 2nd level or higher, you can target one additional creature for every two slot levels above 1st. When you cast this power at 3rd level or higher, the die size increases for every two slot levels above 1st (d6 at 3rd level, d8 at 5th level, d10 at 7th level).

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"util","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d4","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Stackthe%20Deck.webp"} {"_id":"bp55Q4R0gBpd0FiM","name":"Oil Slick","permission":{"default":0},"type":"power","data":{"description":{"value":"

You cover the ground in a 10-foot square within range in oil. For the duration, it is difficult terrain.

\n

When the oil appears, each creature standing in its area must succeed on a Dexterity saving throw or fall prone. A creature that enters the area or ends its turn there must also succeed on a Dexterity saving throw.

\n

The oil is flammable. Any 5 foot square of the oil exposed to fire burns away in one round. Each creature who enters the fire or starts it turn there must make a Dexterity saving throw, taking 3d6 fire damage on a failed save, or half as much on a successful one. The fire ignites any flammable objects in the area that aren't being worn or carried.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":10,"units":"ft","type":"square"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"save","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["3d6","fire"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/OilSlick.webp"} {"_id":"c17MNNJ8FplU5Txm","name":"Absorb Energy","permission":{"default":0},"type":"power","data":{"description":{"value":"

The power captures some of the incoming energy, lessening its effect on you and storing it for your next melee attack. You have resistance to the triggering damage type until the start of your next turn. Also, the first time you hit with a melee attack on your next turn, the target takes an extra 1d6 damage of the triggering type, and the power ends.

\n

Overcharge Tech. When you cast this power using a power slot of 2nd level or higher, the extra damage increases by 1d6 for each slot level above 1st.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"reaction","cost":1,"condition":"which you take when you take acid, cold, energy, fire, ion, kinetic, lightning, or sonic damage"},"duration":{"value":1,"units":"round"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"util","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6",""]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d6"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/AbsorbEnergy.webp","effects":[]} @@ -190,6 +192,7 @@ {"_id":"to84bBMy9F4zYZ5I","name":"Combustive Shot","permission":{"default":0},"type":"power","data":{"description":{"value":"

As part of the action used to cast this power, you must make a ranged weapon attack against one creature within your weapon's range, otherwise the power fails. On a hit, the target suffers the attack's normal effects, and it ignites in flame. At the start of your next turn, the creature takes fire damage equal to your techcasting ability modifier. If the target or a creature within 5 feet of it uses an action to put out the flames, or if some other effect douses the flames, the effect ends.

\n

This power's damage increases when you reach higher levels. At 5th level, the ranged attack deals an extra 1d6 fire damage to the target, and the damage at the start of your next turn increases to 1d4 + your tech casting ability modifier. Both damage rolls increase by 1d6 and 1d4, respectively, at 11th level and 17th level.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"round"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"spec"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"util","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["@abilities.int.mod","fire"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"atwill","formula":"1d6"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/CombustiveShot.webp"} {"_id":"uKVIsflQoqXlbejy","name":"Radiation","permission":{"default":0},"type":"power","data":{"description":{"value":"

Dim, greenish light spreads within a 30-foot-radius sphere centered on a point you choose within range. The light spreads around corners, and it lasts until the power ends.

\n

When a creature moves into the power's area for the first time on a turn or starts its turn there, that creature must succeed on a Constitution saving throw or take 4d10 necrotic damage, and it suffers one level of exhaustion and emits a dim, greenish light in a 5-foot radius. This light makes it impossible for the creature to benefit from being invisible. The light and any levels of exhaustion caused by this power go away when the power ends.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":30,"units":"ft","type":"radius"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"save","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["4d10","necrotic"]],"versatile":""},"formula":"","save":{"ability":"con","dc":null,"scaling":"power"},"level":4,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Radiation.webp"} {"_id":"uLB7E27wecdLLbCE","name":"Minor Defibrillation","permission":{"default":0},"type":"power","data":{"description":{"value":"

You generate a static charge that can aid or harm a creature you touch. Make a melee tech attack against the target. On a hit, the target takes 1d10 lightning damage. If the target is a living creature that has 0 hit points, it immediately gains one death saving throw success instead of taking damage.

\n

This power's damage increases by 1d10 when you reach 5th level (2d10), 11th level (3d10), and 17th level (4d10).

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"mpak","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d10","lightning"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"atwill","formula":"1d10"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/MinorDefibrillation.webp"} +{"name":"Alter Self","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"

You alter your form with tech. When you cast the power, choose one of the following options, the effects of which last for the duration of the power. While the power lasts, you can end one option as an action to gain the benefits of a different one.

\n

Aquatic Adaptation. You adapt your body to an aquatic environment. You can breathe underwater and gain a swimming speed equal to your walking speed.

\n

Change Appearance. You transform your appearance. You decide what you look like, including your height, weight, facial features, sound of your voice, hair length, coloration, and distinguishing characteristics, if any. You can make yourself appear as a member of another species (an organic species cannot appear as a droid, or vice versa), though none of your statistics change. You can also alter your vocal cords, enabling you to speak a language you know, but otherwise would be incapable of speaking. You also can’t appear as a creature of a different size than you, and your basic shape stays the same; if you’re bipedal, you can’t use this power to become quadrupedal, for instance. At any time for the duration of the power, you can use your action to change your appearance in this way again.

\n

Natural Weapons. You grow claws, fangs, spines, horns, or a different natural weapon of your choice. Your damage die for your unarmed strikes increases by one step (from 1 to d4, d4 to d6, or d6 to d8). Your unarmed strikes are considered enhanced and you have a +1 bonus to attack and damage rolls with them.

","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.lGXbhhtTzcGCZQ9b"}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Alter%20Self.webp","effects":[],"_id":"uuxkihWcbMdubsLx"} {"_id":"vo6OJqBxuOiyMyxX","name":"Kolto Cloud","permission":{"default":0},"type":"power","data":{"description":{"value":"

As you expel kolto, up to six creatures of your choice that you can see within range regain hit points equal to 1d4 + your techcasting ability modifier. This power has no effect on droids or constructs.

\n

Overcharge Tech. When you cast this power using a tech slot of 4th level or higher, the healing increases by 1d4 for each slot level above 3rd.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":6,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"heal","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @abilities.int.mod","healing"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":3,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"level","formula":"1d4"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/KoltoCloud.webp"} {"_id":"w8aWJGuIPq2kp4Qj","name":"Delayed Explosion","permission":{"default":0},"type":"power","data":{"description":{"value":"

You create a delayed explosion at a point within range. When the power ends, either because your concentration is broken or because you decide to end it, the explosion occurs. Each creature in a 20-foot-radius sphere centered on that point must make a Dexterity saving throw. A creature takes fire damage equal to the total accumulated damage on a failed save, or half as much damage on a successful one.

\n

The power's base damage is 12d6. If at the end of your turn the explosion has not yet occurred, the damage increases by 1d6.

\n

If the explosion is touched before the interval has expired, the creature touching it must make a Dexterity saving throw. On a failed save, the power ends immediately, causing the explosion.

\n

The fire spreads around corners. It ignites flammable objects in the area that aren't being worn or carried.

\n

Overcharge Tech. When you cast this power using a tech slot of 8th level or higher, the base damage increases by 1d6 for each slot level above 7th.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":20,"units":"ft","type":"radius"},"range":{"value":150,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"save","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["12d6",""]],"versatile":"1d6"},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":7,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"level","formula":"1d6"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/DelayedExplosion.webp"} {"_id":"wtSlQwtA4N2ewADb","name":"Preparedness","permission":{"default":0},"type":"power","data":{"description":{"value":"

You touch a willing creature. For the duration, the target can add 1d8 to its initiative rolls.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"minute","cost":1,"condition":""},"duration":{"value":8,"units":"hour"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Preparedness.webp"} @@ -197,6 +200,7 @@ {"_id":"xW2LoI7JTHMqPcuC","name":"Mobile Lights","permission":{"default":0},"type":"power","data":{"description":{"value":"

You create up to four orbs of light within range that hover in the air for the duration. You can also combine the four lights into one glowing vaguely humanoid form of Medium size. Whichever form you choose, each light sheds dim light in a 10-foot radius.

\n

As a bonus action on your turn, you can move the lights up to 60 feet to a new spot within range. A light must be within 20 feet of another light created by this power, and a light winks out if it exceeds the power's range.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":null,"units":"","type":"self"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/MobileLights.webp"} {"_id":"xXPplfLNPoXvhfjp","name":"Wire Line","permission":{"default":0},"type":"power","data":{"description":{"value":"

You launch a grappling wire toward a creature you can see within range. Make a melee tech attack against the target. If the attack hits, the creature takes 1d6 kinetic damage, and if the creature is Large or smaller, you pull the creature up to 10 feet closer to you.

\n

This power's damage increases by 1d6 when you reach 5th level (2d6), 11th level (3d6), and 17th level (4d6).

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"mpak","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"atwill","formula":"1d6"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/WireLine.webp"} {"_id":"xeBPmKsGTsXqEszc","name":"Greater Analyze","permission":{"default":0},"type":"power","data":{"description":{"value":"

Name or describe a person, place, or object. This power gives you a summary of significant lore about it. If the thing you named isn't known outside of one planetary system, you gain no information. The more information you already have, the more detailed the information you receive is.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"minute","cost":10,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"units":"","type":""},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":5,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/GreaterAnalyze.webp"} +{"name":"Mutate/Augment","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"

You alter your body with tech, temporarily gaining new properties which last for the duration. You can select three of the following properties. You cannot select a property more than once, unless that property says otherwise.

\n
    \n
  • Your body becomes more flexible. You have advantage on ability checks and saving throws against effects that would grapple or restrain you, and your movement is unaffected by difficult terrain or squeezing.
  • \n
  • You grow one additional appendage. This appendage serves as an arm and a hand, though it can take the shape of a limb, tentacle, or similar appendage. You can select this property twice to grow a maximum number of two appendages. You can only gain the benefit of items held by two of your arms at any given time, and once per round you can switch which arms you are benefiting from (no action required).
  • \n
  • You sprout antennae that give you tremorsense out to a range of 30 feet.
  • \n
  • You extend the length of your limbs. When you make a melee attack or attempt to grapple, shove, or trip a creature on your turn, your reach for it is 5 feet greater than normal.
  • \n
  • You gain a burrowing speed equal to your walking speed.
  • \n
  • Your flesh hardens. Your AC can't be less than 16, regardless of what kind of armor you're wearing.
  • \n
  • You can gain the benefits of any one option of the alter self power. You can select this property multiple times to gain the benefits of multiple options of the alter self power, choosing a different option each time.
  • \n
  • You gain the effect of the magnetic hold power, but you can affix to and move along any surface, instead of only metallic surfaces. You have a climbing speed equal to your walking speed.
  • \n
  • You can gain any one effect of the force enlightenment power.
  • \n
\n

Overcharge Tech. When you cast this power using a tech slot of 4th level or higher, you can select an additional property for each slot level above 3rd.

","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":3,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.pY0NUf5jwDrhhiz9"}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Mutate-Augment.webp","effects":[],"_id":"xoeFnjZ62vkY6ygA"} {"_id":"xs0igvxRAzGwZqik","name":"Venomous Strike","permission":{"default":0},"type":"power","data":{"description":{"value":"

As part of the action used to cast this power, you must make a melee weapon attack against one creature within your reach, otherwise the power fails. On a hit, the target suffers the attack's normal effects, and if you were hidden from it, it takes an additional 1d4 poison damage.

\n

This power's damage increases when you reach higher levels. At 5th level, the melee attack deals an extra 1d8 poison damage to the target, and the damage the target takes when you are hidden from it increases to 2d4. Both damage rolls increase by 1d8 and 1d4, respectively, at 11th level and 17th level.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"spec"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"util","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4","poison"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"atwill","formula":"1d8"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/VenomousStrike.webp"} {"_id":"xxrRddwkMSsJbPs0","name":"Kolto Infusion","permission":{"default":0},"type":"power","data":{"description":{"value":"

Choose a creature that you can see within range. A surge of kolto energy washes over the creature, causing it to regain 70 hit points. This power also ends blindness, deafness, and any diseases affecting the target. This power has no effect on droids or constructs.

\n

Overcharge Tech. When you cast this power using a tech slot of 7th level or higher, the amount of healing increases by 10 for each slot level above 6th.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"heal","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["+70","healing"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":6,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"level","formula":"10"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/KoltoInfusion.webp"} {"_id":"yBnreezRaiZpukCB","name":"Detect Invisibility","permission":{"default":0},"type":"power","data":{"description":{"value":"

For the duration, you see invisible creatures and objects as if they were visible.

\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":0,"units":"","type":""},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/DetectInvisibility.webp"} diff --git a/packs/packs/weapons.db b/packs/packs/weapons.db index d27d9503..39e87327 100644 --- a/packs/packs/weapons.db +++ b/packs/packs/weapons.db @@ -1,27 +1,27 @@ {"_id":"0ZRWgHRykApmHmoG","name":"Heavy Shotgun","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Burst 4, Reload 12, Strength 13

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":16,"price":400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Heavy%20Shotgun.webp","effects":[]} {"_id":"1wmSlQ3a91lL9c3H","name":"Slugpistol","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Rapid 8, Reload 16, Strength 11

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4800001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Slugpistol.webp","effects":[]} -{"_id":"1zm4z1AKDhzXC2ow","name":"Jagged Vibroblade","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":5,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8+@mod","kinetic"]],"versatile":"1d10+@mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":true,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":true},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7187501,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"1zm4z1AKDhzXC2ow","name":"Jagged Vibroblade","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":5,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8+@mod","kinetic"]],"versatile":"1d10+@mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":true,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":true},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7187501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Jagged%20Vibroblade.webp","effects":[]} {"_id":"2RG5RZxqJ2q7jziD","name":"Switch carbine","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 60/240), Reload 12, Switch (1d4 acid)

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":8,"price":4600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"1d4 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799904,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Switch%20Carbine.webp","effects":[]} {"_id":"2ylNmxKfD5IdGre8","name":"Shatter pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 40/160), Silent, Light, Reload 20

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":12},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":true,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899806,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Shatter%20Pistol.webp","effects":[]} {"_id":"3MBvpQfxedbIW6eH","name":"Vibrospear","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":120,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":"1d8 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibrospear.webp","effects":[]} {"_id":"3u5NHZJFld4ayeEI","name":"Lightdagger","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":true,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3400001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Lightweapons/Lightdagger.webp","effects":[]} {"_id":"4q1qOqW9eYyAmn8l","name":"Vibrodart","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Due to their diminutive size, vibrodarts make ineffective melee weapons. Melee attack rolls made with them are made at disadvantage.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":5,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibrodart.webp","effects":[]} {"_id":"50y7jToHJOvsO1Yr","name":"Repeating Blaster","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Burst 4, Rapid 4, Reload 8, Strength 13

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":18,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Repeater.webp","effects":[]} -{"_id":"5RF22K1v5Pra1uwf","name":"Chained dagger","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Disarming, Finesse, Reach, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":850,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6950001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} -{"_id":"5vPxd3p7M2m9j07G","name":"BKG","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 120/480), Auto, Burst 2, Disintegrate 13, Reload 2, Strength 19, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":42,"price":9000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":10,"width":null,"units":"","type":"cube"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":240},"ability":"dex","actionType":"save","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["3d4 + @mod","fire"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"dex"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":true,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":300001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} -{"_id":"62kO29wbh9pYXwb6","name":"Vibroknife","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Finesse, Light, Piercing 1

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":true,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899995,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"5RF22K1v5Pra1uwf","name":"Chained Dagger","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Disarming, Finesse, Reach, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":850,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6950001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Chained%20Dagger.webp","effects":[]} +{"_id":"5vPxd3p7M2m9j07G","name":"BKG","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 120/480), Auto, Burst 2, Disintegrate 13, Reload 2, Strength 19, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":42,"price":9000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":10,"width":null,"units":"","type":"cube"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":240},"ability":"dex","actionType":"save","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["3d4 + @mod","fire"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"dex"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":true,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":300001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/BKG.webp","effects":[]} +{"_id":"62kO29wbh9pYXwb6","name":"Vibroknife","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Finesse, Light, Piercing 1

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":true,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899995,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibroknife.webp","effects":[]} {"_id":"6VZqOHM0oqK6GiQ9","name":"Needler","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Rapid 10, Reload 20

The needler includes a specialized compartment for poison. One dose of poison, when installed in this compartment, retains its potency for 1 hour before drying. One dose of poison is effective for the next 10 shots fired by the weapon.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":275,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3900001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Needler.webp","effects":[]} -{"_id":"6nqChLn3T4yM9eyV","name":"Vibroglaive","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"","quantity":1,"weight":13,"price":1250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10+mod+(ceil(abilities.str.mod/2))","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7086720,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"6nqChLn3T4yM9eyV","name":"Vibroglaive","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"","quantity":1,"weight":13,"price":1250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10+mod+(ceil(abilities.str.mod/2))","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7086720,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibroglaive.webp","effects":[]} {"_id":"7BNvGSn4OCqYOXRW","name":"Doubleshoto","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Double (1d6 Energy)

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":1250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1500001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Doubleshoto.webp","effects":[]} -{"_id":"7TXR7ex23HIFCNOm","name":"Vibrobuster","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":15,"price":7777,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12+@mod+(ceil(abilities.str.mod/2))","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":true,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":true},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7199806,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"7TXR7ex23HIFCNOm","name":"Vibrobuster","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":15,"price":7777,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12+@mod+(ceil(abilities.str.mod/2))","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":true,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":true},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7199806,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrobuster.webp","effects":[]} {"_id":"7Tv59onLvYZHkoBo","name":"Wristsaber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Finesse, Fixed, Light, Luminous

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":1000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7081251,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Lightweapons/Wristsaber.webp","effects":[]} {"_id":"835xibigP1dU8CTe","name":"Doubleblade","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Double (1d6 Kinetic)

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":5,"price":625,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1300001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Doubleblade.webp","effects":[]} {"_id":"9luXJi8YnsiLIku5","name":"Tranquilizer Rifle","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 4

The tranquilizer rifle includes a specialized compartment for poison. One dose of poison, when installed in this compartment, retains its potency for 1 hour before drying. One dose of poison is effective for the next 4 shots fired by the weapon.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":10,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5500001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Tranquilizer%20Rifle.webp","effects":[]} {"_id":"AgOdU4CCgzkHE7k3","name":"Claymore saber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Dexterity 13, Luminous, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":9,"price":2100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["3d4","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6875001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Claymore%20Saber.webp","effects":[]} {"_id":"As8MEk6gS194cszO","name":"Lightsaber Pike","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Dexterity 11

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":6,"price":400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Lightsaber%20Pike.webp","effects":[]} -{"_id":"BkJeU3bFIAausZ0R","name":"Warsword","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Dexterity 13, Two-handed, Vicious 1

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":8,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":true,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7062501,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"BkJeU3bFIAausZ0R","name":"Warsword","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Dexterity 13, Two-handed, Vicious 1

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":8,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":true,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7062501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Warsword.webp","effects":[]} {"_id":"CZSl9ojBFp2P7NTo","name":"Hidden Blade","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":true,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2300001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Hidden%20Blade.webp","effects":[]} -{"_id":"CySLyAoF97hQ5OQE","name":"Shock Whip","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":3,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7198439,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"CySLyAoF97hQ5OQE","name":"Shock Whip","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":3,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7198439,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Shock%20Whip.webp","effects":[]} {"_id":"DNzVCv79gSDnvu29","name":"Nightstinger rifle","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 120/480), Silent, Reload 2, Strength 13, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":15,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":120},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":true,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799220,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Nightstinger%20Rifle.webp","effects":[]} {"_id":"ERnfcnNUZKTaEJXK","name":"Lightaxe","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Heavy, Luminous, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":10,"price":1800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6898439,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Lightaxe.webp","effects":[]} {"_id":"EVfeatRtUl44AxUD","name":"Energy bow","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 80/320), Mighty, Reload 12, Silent, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":true,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":true,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6893751,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Energy%20Bow.webp","effects":[]} @@ -30,7 +30,7 @@ {"_id":"FqG0kEsg92JQ5qte","name":"Blaster Pistol","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 16

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":15},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":600001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Blaster%20Pistol.webp","effects":[]} {"_id":"GHm4Ewtx59eisABa","name":"Sith saber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Dexterity 11, Hidden, Keen 1, Luminous, Versatile (1d10)

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":2400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":"1d10 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":true,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6587501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Sith%20Saber.webp","effects":[]} {"_id":"GajCi06DTH9AWoNB","name":"Retrosaber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

The retrosaber is an ancient type of lightweapon that requires a power cell to function. Once four attacks have been made with a retrosaber, a character must replace the power cell using an action or bonus action (the character’s choice). You must have one free hand to replace the power cell.

\n

Keen 1, Luminous, Special, Vicious 1

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":8,"price":800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":60},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":true,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":true,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6687501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Retrosaber.webp","effects":[]} -{"_id":"GueNpIQQ1PQVT173","name":"Unarmed Strike","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":0,"price":0,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"natural","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5600001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"GueNpIQQ1PQVT173","name":"Unarmed Strike","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":0,"price":0,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"natural","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5600001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Unarmed%20Strike.png","effects":[]} {"_id":"Gxo8kRr0dQje7a9y","name":"Heavy Repeater","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Burst 4, Rapid 2, Reload 8, Strength 15

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":16,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Heavy%20Repeater.webp","effects":[]} {"_id":"HFGuc5KM21MTRpbn","name":"Blaster Carbine","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 16

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":8,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":15},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":500001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Blaster%20Carbine.webp","effects":[]} {"_id":"HOG6cMpPNlpLWWOz","name":"Chaingun","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Rapid 6, Reload 12, Strength 15

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":42,"price":1500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Chaingun.webp","effects":[]} @@ -43,7 +43,7 @@ {"_id":"JP2mZAKluouY2JMR","name":"Disruptor pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 60/240), Disintegrate 13, Reload 16

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":4,"price":4000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":15},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","acid"]],"versatile":""},"formula":"","save":{"ability":"con","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":true,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6887501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Disruptor%20Pistol.webp","effects":[]} {"_id":"JYZF9unlf2YrPous","name":"Bolt-thrower","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 100/400), reload 2, silent, strength 11, two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":14,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":120},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":true,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7300001,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Bolt%20Thrower.webp","effects":[]} {"_id":"Jb5Ms4NFiQbB6JNS","name":"Heavy Pistol","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 12, Strength 11

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1900001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Heavy%20Pistol.webp","effects":[]} -{"_id":"JivFEWysm82fCFCO","name":"Sonic rifle","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 100/400), Reload 12, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":10,"price":800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","sonic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999611,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"JivFEWysm82fCFCO","name":"Sonic Rifle","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 100/400), Reload 12, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":10,"price":800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","sonic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999611,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Sonic%20Rifle.webp","effects":[]} {"_id":"K1d8enmp2viW5zG5","name":"Wristblaster","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 12

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":true,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":7200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Wrist%20Blaster.webp","effects":[]} {"_id":"KGopmgxTmVLnRXF7","name":"Scattergun","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Burst 4, Reload 4

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":20,"long":80,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4500001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Scattergun.webp","effects":[]} {"_id":"KbJhaEvG7rdvtczl","name":"Ion Pistol","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 16

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d3 + @mod","ion"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Ion%20Pistol.webp","effects":[]} @@ -52,8 +52,8 @@ {"_id":"LQEPGNraFXSViaWF","name":"Greatsaber","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Dexterity 11

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":1000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["2d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Greatsaber.webp","effects":[]} {"_id":"LYOgciOEThVXogkW","name":"Guard shoto","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Defensive 1, Finesse, Light, Luminous

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":1350,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6450001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Guard%20Shoto.webp","effects":[]} {"_id":"LfBkJ40l2SEWiRvE","name":"Vibrodagger","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":50,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibrodagger.webp","effects":[]} -{"_id":"LyUddEATg6hhVzup","name":"Sonic pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 40/160), Reload 12

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":650,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","sonic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6698439,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} -{"_id":"MKysOTuoqYnSc7Jj","name":"Riot Baton","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":4,"price":350,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7193751,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"LyUddEATg6hhVzup","name":"Sonic pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 40/160), Reload 12

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":650,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","sonic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6698439,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Sonic%20Pistol.webp","effects":[]} +{"_id":"MKysOTuoqYnSc7Jj","name":"Riot Baton","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":4,"price":350,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7193751,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Riot%20Baton.webp","effects":[]} {"_id":"MPdtUMvLZMeS0C0A","name":"Vibrowhip","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":150,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":7000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrowhip.webp","effects":[]} {"_id":"MQHnsYyAKXByZ0Gc","name":"IWS (Antiarmor)","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"
\n

The IWS is a heavy weapon that can fire in three different modes. On your turn, you can use your object interaction to switch between modes, detailed below.

\n

Antiarmor. While in this mode, rather than traditional power cells, the IWS fires grenades. When firing a grenade at long range, creatures within the radius of the grenade’s explosion have advantage on the saving throw.

\n

Blaster. While in this mode, the weapon uses traditional power cells.

\n

Sniper. While in this mode, the weapon uses traditional power cells.

\n
\n
Antiarmor: Special, Ammunition (range 60/240), reload 1, special
\n
Blaster: 1d8 Energy, Ammunition (range 80/320), reload 12
\n
Sniper: 1d12 Energy, Ammunition (range 120/480), reload 4
\n
 
\n
Special, Strength 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":7200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":"space"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6650001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/IWS.webp","effects":[]} {"_id":"MYRL4RqwpkvpGEF7","name":"ARC caster","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Burst 4, Rapid 2, Reload 4, Special, Strength 11, Two-handed

\n

When you score a critical hit with this weapon, a creature becomes shocked until the end of its next turn.

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":10,"price":2400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":60},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","lightning"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":100001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/ARC%20Caster.webp","effects":[{"_id":"xGXO5cLr25ibPpu5","flags":{"dae":{"stackable":false,"specialDuration":[],"macroRepeat":"none","transfer":false}},"changes":[{"key":"","value":"0","mode":2,"priority":0}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Martial%20Blasters/ARC%20Caster.webp","label":"Brutal","tint":"","transfer":false}]} @@ -62,88 +62,88 @@ {"_id":"NpBjZzBSs1Ob34wo","name":"Chakram","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":90,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":true,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Chakram.webp","effects":[]} {"_id":"NwH4qn7dnL70hCDK","name":"Buster Saber","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"","quantity":1,"weight":0,"price":0,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod + (ceil(@abilities.str.mod/2))","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":true,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":true,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7600001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Buster%20Saber.webp","effects":[]} {"_id":"Nwyshcc6h8hrwD0b","name":"Hunting Rifle","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 2, Strength 13

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":12,"price":600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":150,"long":600,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2500001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Hunting%20Rifle.webp","effects":[]} -{"_id":"OpfE2GuNbGjrKaMn","name":"Crossguard saber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Defensive 1, Dexterity 13, Heavy, Luminous, Versatile (1d10)

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":950,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":"1d10 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6987501,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"OpfE2GuNbGjrKaMn","name":"Crossguard saber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Defensive 1, Dexterity 13, Heavy, Luminous, Versatile (1d10)

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":950,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":"1d10 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6987501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Crossguard%20Saber.webp","effects":[]} {"_id":"OvrgbStdTiaZJOkp","name":"Vibropike","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Dexterity 11

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":6,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6500001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibropike.webp","effects":[]} -{"_id":"P7kHmua3GwxvDubc","name":"Switch pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 40/160), Light, Reload 12, Switch (1d4 fire)

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":4,"price":3100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"1d4 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899904,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"P7kHmua3GwxvDubc","name":"Switch Pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 40/160), Light, Reload 12, Switch (1d4 fire)

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":4,"price":3100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"1d4 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899904,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Switch%20Pistol.webp","effects":[]} {"_id":"PVE0NHcsMVC3J2nU","name":"Light Repeater","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Burst 8, Reload 16

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":12,"price":900,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Light%20Repeater.webp","effects":[]} {"_id":"RLMIkAhL6BzW5W0O","name":"Techaxe","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":75,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Techaxe.webp","effects":[]} -{"_id":"RPWhectchRTOaP5O","name":"Hooked Vibroblade","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":6,"price":700,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7175001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"RPWhectchRTOaP5O","name":"Hooked Vibroblade","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":6,"price":700,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7175001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Hooked%20Vibroblade.webp","effects":[]} {"_id":"Rcb4rtOCKFfbykMQ","name":"Cross-saber","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":13,"price":5500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod + (ceil(@abilities.str.mod/2))","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7078126,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Cross-saber.webp","effects":[]} {"_id":"SBFDSFPd249rnk4s","name":"Techblade","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5300001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Techblade.webp","effects":[]} {"_id":"SHP3va9YHWE2LRcD","name":"Heavy Slugpistol","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Rapid 2, Reload 8, Strength 13

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":5,"price":750,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Heavy%20Slugpistol.webp","effects":[]} -{"_id":"SuHWBtXDAMPQFP39","name":"Electroprod","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Disruptive, Shocking 13

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","lightning"]],"versatile":""},"formula":"1d4","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":true,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6050001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"SuHWBtXDAMPQFP39","name":"Electroprod","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Disruptive, Shocking 13

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","lightning"]],"versatile":""},"formula":"1d4","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":true,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6050001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Electroprod.webp","effects":[]} {"_id":"SxhcmZJeFmzuqNLa","name":"Saberaxe","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Brutal 1

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":5,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6693751,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Saberaxe.webp","effects":[]} -{"_id":"U6mqCK7zhOCdayoV","name":"Torpedo Launcher","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Rather than traditional power cells, the torpedo launcher fires specialized projectiles in the form of torpedoes. Torpedo launchers have advantage on attack rolls against Gargantuan creatures and disadvantage on attack rolls again Large and smaller creatures. Unlike other weapons, the torpedo launcher can only be loaded using an action, and you don’t add your Dexterity modifier to damage rolls you make with it.

\n

Before firing the torpedo launcher, you must first use your action to deploy it. While deployed, your speed is reduced by half. You can collapse the torpedo launcher as a bonus action.

","chat":"","unidentified":""},"source":"","quantity":1,"weight":25,"price":10000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":1200,"long":3600,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7199220,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"U6mqCK7zhOCdayoV","name":"Torpedo Launcher","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Rather than traditional power cells, the torpedo launcher fires specialized projectiles in the form of torpedoes. Torpedo launchers have advantage on attack rolls against Gargantuan creatures and disadvantage on attack rolls again Large and smaller creatures. Unlike other weapons, the torpedo launcher can only be loaded using an action, and you don’t add your Dexterity modifier to damage rolls you make with it.

\n

Before firing the torpedo launcher, you must first use your action to deploy it. While deployed, your speed is reduced by half. You can collapse the torpedo launcher as a bonus action.

","chat":"","unidentified":""},"source":"","quantity":1,"weight":25,"price":10000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":1200,"long":3600,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7199220,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Torpedo%20Launcher.webp","effects":[]} {"_id":"UAdGYMpSYZO9PgDN","name":"Doublesaber","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Double (1d8 Energy)

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1400001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Doublesaber.webp","effects":[]} {"_id":"UB1iH8foflZXxMSM","name":"Net","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

A Large or smaller creature hit by a net is restrained until it is freed. A net has no effect on formless or Huge or larger creatures. A creature can use its action to make a DC 13 Strength check, freeing itself or another creature within its reach on a success. The net has an AC of 10, 5 hit points, and immunity to all damage not dealt by melee weapons. Destroying the net frees the creature without harming it and immediately ends the net's effects. While a creature is restrained by a net, you can make no further attacks with it.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":15,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"str","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Net.webp","effects":[]} {"_id":"UtC6M3dEyUUh8IHj","name":"Railgun","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 150/600), Piercing 1, Reload 1, Strength 15, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":24,"price":6300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":150,"long":600,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":240},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d6","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":true,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6550001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Rail%20Gun.webp","effects":[]} {"_id":"UxEuTaODfHttK1Bn","name":"Vibrosword","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Dexterity 11

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":6,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["2d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6900001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrosword.webp","effects":[]} -{"_id":"VSPlcHGwB9dXwwKo","name":"Vibrotonfa","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Defensive 1, Finesse, Light

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":1000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mad","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999953,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} -{"_id":"Vea8C1Pacl9ZEPhI","name":"Vibrobattleaxe","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":15,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12+@mod+(ceil(@abilities.str.mod/2))","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7199611,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"VSPlcHGwB9dXwwKo","name":"Vibrotonfa","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Defensive 1, Finesse, Light

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":1000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mad","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999953,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrotonfa.webp","effects":[]} +{"_id":"Vea8C1Pacl9ZEPhI","name":"Vibrobattleaxe","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":15,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12+@mod+(ceil(@abilities.str.mod/2))","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7199611,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrobattleaxe.webp","effects":[]} {"_id":"Vx3poKOlfgF1QAl4","name":"Lightsaber","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":"1d8 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3600001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Lightweapons/Lightsaber.webp","effects":[]} {"_id":"WykXGVs1mPUlapA8","name":"Light Ring","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":90,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":true,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Light%20Ring.webp","effects":[]} {"_id":"XEEWBisdZKuTWrwS","name":"Heavy Bowcaster","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Burst 4, Reload 8, Strength 15

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":26,"price":1100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1800001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Heavy%20Bowcaster.webp","effects":[]} -{"_id":"XF7LLzqx4ayiY8Gf","name":"Shoulder Cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Mounted by the shoulder slot, a shoulder cannon does not require a free hand to use. Additionally, you have advantage on Strength ability checks and saving throws to avoid being disarmed of this weapon.

\n

Ammunition (range 60/240), Autotarget (15, +2), Burst 4, Reload 4, Special

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":9,"price":3200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":60},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + 2","energy"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":12,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6696876,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[{"_id":"KCrgDQZ4Fs0XYwyf","flags":{"dae":{"stackable":false,"specialDuration":[],"macroRepeat":"none","transfer":false}},"changes":[{"key":"data.bonuses.rwak.attack","value":"0","mode":2,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"icons/svg/mystery-man.svg","label":"Autotarget","tint":"","transfer":false}]} -{"_id":"Yi36jcAGqVrealrD","name":"Mancatcher","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

When you would make a Strength (Athletics) check to attempt to grapple a creature while wielding a weapon with the grappling property, you can instead make a melee weapon attack with it. If the attack hits, the creature becomes grappled by you, and it takes damage equal to your Strength modifier of the same type as the weapon’s damage.

\n

Reach, Special, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6798439,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} -{"_id":"Yr8n7ZT1OmoxDmhl","name":"Vibroshield","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Defensive 1, Fixed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":7,"price":900,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999904,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"XF7LLzqx4ayiY8Gf","name":"Shoulder Cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Mounted by the shoulder slot, a shoulder cannon does not require a free hand to use. Additionally, you have advantage on Strength ability checks and saving throws to avoid being disarmed of this weapon.

\n

Ammunition (range 60/240), Autotarget (15, +2), Burst 4, Reload 4, Special

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":9,"price":3200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":60},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + 2","energy"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":12,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6696876,"flags":{},"img":"systems/sw5e/packs/Icons/","effects":[{"_id":"KCrgDQZ4Fs0XYwyf","flags":{"dae":{"stackable":false,"specialDuration":[],"macroRepeat":"none","transfer":false}},"changes":[{"key":"data.bonuses.rwak.attack","value":"0","mode":2,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Martial%20Blasters/Shoulder%20Cannon.webp","label":"Autotarget","tint":"","transfer":false}]} +{"_id":"Yi36jcAGqVrealrD","name":"Mancatcher","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

When you would make a Strength (Athletics) check to attempt to grapple a creature while wielding a weapon with the grappling property, you can instead make a melee weapon attack with it. If the attack hits, the creature becomes grappled by you, and it takes damage equal to your Strength modifier of the same type as the weapon’s damage.

\n

Reach, Special, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6798439,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Mancatcher.webp","effects":[]} +{"_id":"Yr8n7ZT1OmoxDmhl","name":"Vibroshield","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Defensive 1, Fixed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":7,"price":900,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999904,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibroshield.webp","effects":[]} {"_id":"ZGERUZSkFxFxSid4","name":"Cycler Rifle","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Rapid 2, Reload 8

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":10,"price":450,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Cycler%20Rifle.webp","effects":[]} {"_id":"ZHImgyS79q62mnk3","name":"Wrist Launcher","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 1

Rather than traditional power cells, the wrist launcher fires specialized projectiles in the form of darts, small missiles, or specialized canisters.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":450,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":7100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Wrist%20Launcher.webp","effects":[]} -{"_id":"Zjy957ONj3PyVQm4","name":"Wristblade","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Fixed, Light

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":450,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7087501,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"Zjy957ONj3PyVQm4","name":"Wristblade","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Fixed, Light

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":450,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7087501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Wristblade.webp","effects":[]} {"_id":"a9lLCncsribwUR2g","name":"IWS (Sniper)","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"
\n

The IWS is a heavy weapon that can fire in three different modes. On your turn, you can use your object interaction to switch between modes, detailed below.

\n

Antiarmor. While in this mode, rather than traditional power cells, the IWS fires grenades. When firing a grenade at long range, creatures within the radius of the grenade’s explosion have advantage on the saving throw.

\n

Blaster. While in this mode, the weapon uses traditional power cells.

\n

Sniper. While in this mode, the weapon uses traditional power cells.

\n
\n
Antiarmor: Special, Ammunition (range 60/240), reload 1, special
\n
Blaster: 1d8 Energy, Ammunition (range 80/320), reload 12
\n
Sniper: 1d12 Energy, Ammunition (range 120/480), reload 4
\n
 
\n
Special, Strength 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":7200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":"space"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":60},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6650001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/IWS.webp","effects":[]} {"_id":"aVdtguY4dQuv2xhf","name":"Lightglaive","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Dexterity 13, Luminous, Reach, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":10,"price":1900,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899611,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Lightglaive.webp","effects":[]} {"_id":"ahWWUPiXhooCgdKf","name":"Saberwhip","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4400001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Saberwhip.webp","effects":[]} {"_id":"ankoAzbua0rlIBnh","name":"Subrepeater","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Rapid 8, Reload 16

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Subrepeater.webp","effects":[]} {"_id":"b0WBwJRTPPJpnHii","name":"Vibroblade","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":150,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":"1d10 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5900001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibroblade.webp","effects":[]} -{"_id":"cnqFdX8ihPdIgSRt","name":"Electrobaton","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Finesse, Light, Shocking 13

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":650,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":3350001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"cnqFdX8ihPdIgSRt","name":"Electrobaton","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Finesse, Light, Shocking 13

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":650,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":3350001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Electrobaton.webp","effects":[]} {"_id":"cpuAeXISuY1IRiw9","name":"Lightfist","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Disruptive, Disguised, Fixed, Light, Luminous

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":true,"dis":false,"dpt":true,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899220,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Lightfist.webp","effects":[]} -{"_id":"ctSLHcbXiV8P0PeG","name":"Riot Shocker","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":4,"price":750,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7196876,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} -{"_id":"e4WTWeABUAmn6GZc","name":"Vibrohammer","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Heavy, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":16,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899989,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"ctSLHcbXiV8P0PeG","name":"Riot Shocker","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":4,"price":750,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7196876,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Riot%20Shocker.webp","effects":[]} +{"_id":"e4WTWeABUAmn6GZc","name":"Vibrohammer","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Heavy, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":16,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899989,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrohammer.webp","effects":[]} {"_id":"eOf2AzadJEdKZ8NK","name":"Vibrolance","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

You have disadvantage when you use a vibrolance to attack a target within 5 feet of you. Also, a lance requires two hands to wield when you aren't mounted.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":6,"price":100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6300001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrolance.webp","effects":[]} -{"_id":"eT1EM9VkO6zJ4QYZ","name":"Switch sniper","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 120/480), Reload 2, Strength 13, Switch (1d10 cold), Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":16,"price":8250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":120},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"1d10 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6699220,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"eT1EM9VkO6zJ4QYZ","name":"Switch Sniper","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 120/480), Reload 2, Strength 13, Switch (1d10 cold), Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":16,"price":8250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":120},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"1d10 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6699220,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Switch%20Sniper.webp","effects":[]} {"_id":"fA347pHZ31UJT39y","name":"Vibrocutter","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Dexterity 11, Heavy, Vicious 1

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":5,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mos","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":true,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799977,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibrocutter.webp","effects":[]} {"_id":"fHvjrYSxapHKDmXt","name":"Vibroknuckler","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":60,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibroknuckler.webp","effects":[]} {"_id":"fOS7j0XoMXAkb2Iy","name":"Rocket launcher","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Rather than traditional power cells, the rocket launcher fires specialized projectiles in the form of rockets. When firing a rocket at long range, or if you don’t meet the rocket launcher’s strength requirement, creatures within the radius of the rocket’s explosion have advantage on the saving throw.

\n

Ammunition (range 100/400), Reload 1, Special, Strength 13, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":20,"price":2400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799611,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Rocket%20Launcher.webp","effects":[]} -{"_id":"fV8miB2X8CfTaFkM","name":"Vapor projector","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

The vapor projector does not make attack rolls. Rather than traditional power cells, the vapor projector uses specialized projector tanks, which, when fired, spray an area with the contents of the tank. Projector tanks require your target to make a saving throw to resist the tank’s effects. It can have different ammunition types loaded simultaneously, and you can choose which ammunition you’re using as you fire it (no action required). If you don’t meet the vapor projector’s strength requirement, creatures have advantage on their saving throws. If you lack proficiency in the vapor projector, you must roll the damage dice twice and take the lesser total.

\n

Ammunition (range special), Reload 5, Special, Strength 11, Two-handed

","chat":"","unidentified":""},"source":"","quantity":1,"weight":0,"price":0,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"space"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899953,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"fV8miB2X8CfTaFkM","name":"Vapor Projector","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

The vapor projector does not make attack rolls. Rather than traditional power cells, the vapor projector uses specialized projector tanks, which, when fired, spray an area with the contents of the tank. Projector tanks require your target to make a saving throw to resist the tank’s effects. It can have different ammunition types loaded simultaneously, and you can choose which ammunition you’re using as you fire it (no action required). If you don’t meet the vapor projector’s strength requirement, creatures have advantage on their saving throws. If you lack proficiency in the vapor projector, you must roll the damage dice twice and take the lesser total.

\n

Ammunition (range special), Reload 5, Special, Strength 11, Two-handed

","chat":"","unidentified":""},"source":"","quantity":1,"weight":0,"price":0,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"space"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899953,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Vapor%20Projector.webp","effects":[]} {"_id":"fzFc5SKhOn55embe","name":"Saber Gauntlet","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":2,"price":1300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8+@mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7084376,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Saber%20Guantlet.webp","effects":[]} {"_id":"gise2mIw1wbTUtlY","name":"Blaster Rifle","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 12

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":11,"price":400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Blaster%20Rifle.webp","effects":[]} {"_id":"gxIw7zHI4R5eef9Z","name":"Revolver","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Rapid 6, Reload 6, Strength 11

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":750,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Revolver.webp","effects":[]} -{"_id":"ioLa4LwWiQTOmgq2","name":"Bolas","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

A Large or smaller creature hit by a bolas is restrained until it is freed. A bolas has no effect on formless or Huge or larger creatures. A creature can use its action to make a DC 13 Dexterity check, freeing itself or another creature within its reach on a success. The bolas has an AC of 10, 5 hit points, and immunity to all damage not dealt by melee weapons. Destroying the bolas frees the creature without harming it and immediately ends the bolas’s effects. While a creature is restrained by a bolas, you can make no further attacks with it.

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":70,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":true,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6850001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} -{"_id":"k0jg2u21xDQ0zDzf","name":"Vibroclaymore","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":14,"price":1050,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["3d4+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7085939,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"ioLa4LwWiQTOmgq2","name":"Bolas","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

A Large or smaller creature hit by a bolas is restrained until it is freed. A bolas has no effect on formless or Huge or larger creatures. A creature can use its action to make a DC 13 Dexterity check, freeing itself or another creature within its reach on a success. The bolas has an AC of 10, 5 hit points, and immunity to all damage not dealt by melee weapons. Destroying the bolas frees the creature without harming it and immediately ends the bolas’s effects. While a creature is restrained by a bolas, you can make no further attacks with it.

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":70,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":true,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6850001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Bolas.webp","effects":[]} +{"_id":"k0jg2u21xDQ0zDzf","name":"Vibroclaymore","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":14,"price":1050,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["3d4+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7085939,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibroclaymore.webp","effects":[]} {"_id":"k5ippYtz0p4PjbOW","name":"Saberspear","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":450,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4300001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Saberspear.webp","effects":[]} {"_id":"kAnqkLwQT2s8V3tC","name":"Light Slugpistol","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 8

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Light%20Slugpistol.webp","effects":[]} {"_id":"kXba6Y0xOqeuT59S","name":"Shatter rifle","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 120/480), Silent, Reload 2, Strength 13, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":15,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":120},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":true,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6575001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Shatter%20Rifle.webp","effects":[]} {"_id":"kiF9Ubs4yyOlqG7d","name":"Dual-phase saber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Dexterity 11, Keen 1, Luminous, Versatile (1d10)

","chat":"","unidentified":""},"source":"","quantity":1,"weight":4,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":false,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":"1d10 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":true,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7500001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Dual-Phase%20Saber.webp","effects":[]} -{"_id":"lCCxKJ6dgNcKerQP","name":"Nervebaton","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Disruptive, Neuralizing 13

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":1500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"wis","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":true,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6998439,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} -{"_id":"mVeMPN2VDbmxW2LH","name":"Echostaff","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Double (1d6 kinetic), Finesse, Sonorous 13

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"con","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6775001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"lCCxKJ6dgNcKerQP","name":"Nervebaton","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Disruptive, Neuralizing 13

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":1500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"wis","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":true,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6998439,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Nervebaton.webp","effects":[]} +{"_id":"mVeMPN2VDbmxW2LH","name":"Echostaff","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Double (1d6 kinetic), Finesse, Sonorous 13

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"con","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6775001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Echostaff.webp","effects":[]} {"_id":"mn4HW9ZpkObfamy4","name":"Lightclub","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":5,"price":150,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3300001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Lightweapons/Lightclub.webp","effects":[]} {"_id":"nbBcy38sK2sejDo2","name":"Grenade launcher","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Rather than traditional power cells, the grenade launcher fires grenades. When firing a grenade at long range, or if you don’t meet the grenade launcher’s strength requirement, creatures within the radius of the grenade’s explosion have advantage on the saving throw. If you lack proficiency in the grenade launcher, you must roll the damage dice twice and take the lesser total.

\n

Ammunition (range 80/320), Reload 1, Strength 11, Special, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":10,"price":800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"space"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6896876,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Grenade%20Launcher.webp","effects":[]} {"_id":"nkHuIJ1RC1eLHPQ5","name":"Martial Lightsaber","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":"1d10 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3800001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Martial%20Lightsaber.webp","effects":[]} {"_id":"ojGildpRHvmzXCXV","name":"Lightbow","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 60/240), Mighty, Piercing 1, Reload 4, Strength 11, Two-handed

","chat":"","unidentified":""},"source":"","quantity":1,"weight":16,"price":400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":60},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":true,"pic":true,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6996876,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Lightbow.webp","effects":[]} {"_id":"ol1wR7Y9gTL4vWLN","name":"Bo-rifle (Staff)","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"
\n

The bo-rifle is a lasat weapon most commonly carried by the Honor Guard of Lasan, which has the unique property of functioning as both a rifle and a staff. On your turn, you can use your object interaction to switch between modes, detailed below.

\n

Rifle. While in this mode, the weapon uses traditional power cells.

\n

Staff. While in this mode, the weapon gains the shocking 13 property.

\n
\n
Rifle: 1d8 Energy, Ammunition (range 100/400), reload 6
\n
Staff: 1d8 Kinetic, Double (1d8 kinetic), shocking 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":7,"price":2300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":true,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6975001,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Bo%20Rifle.webp","effects":[]} -{"_id":"pjMVd0d3xi1E2oe9","name":"Switch rifle","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 60/240), Reload 8, Switch (1d6 lightning), Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":5500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":30},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"1d6 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799953,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"pjMVd0d3xi1E2oe9","name":"Switch Rifle","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 60/240), Reload 8, Switch (1d6 lightning), Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":5500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":30},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"1d6 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799953,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Switch%20Rifle.webp","effects":[]} {"_id":"qGACGd1CGJjFmyvC","name":"Vibrostaff","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":"2d4 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6800001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibrostaff.webp","effects":[]} {"_id":"qVeMdnOgsk1EkUGN","name":"Rotary cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Rather than traditional power cells, the rotary cannon uses specialized power generator that allow it to fire continuously for 10 minutes. Replacing a power generator takes an action.

\n

The rotary cannon requires the use of a tripod unless you meet its strength requirement, which is included in the price. Over the course of 1 minute, you can deploy or collapse the rotary cannon on the tripod. While deployed, your speed is reduced to 0.

\n

 

\n

Ammunition (range 100/400), Auto, Burst, Rapid, Special, Strength 19, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":76,"price":9800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":10,"width":null,"units":"ft","type":"cube"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"save","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"dex"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799806,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Rotary%20Cannon.webp","effects":[]} {"_id":"qcfcwHseK75zUy9U","name":"Assault Cannon","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Burst 4, Reload 8, Strength 11

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":24,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":30},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Assault%20Cannon.webp","effects":[]} -{"_id":"rh4a2Bq5FJJ0c6bk","name":"Electrovoulge","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Reach, Shocking 13, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":15,"price":1300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6787501,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"rh4a2Bq5FJJ0c6bk","name":"Electrovoulge","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Reach, Shocking 13, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":15,"price":1300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6787501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Electrovoulge.webp","effects":[]} {"_id":"s02ZWo9cCBqTOx2A","name":"Vibroclaw","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Finesse, Fixed, Light

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899977,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibroclaw.webp","effects":[]} {"_id":"sGXUQb1kU45kKt1b","name":"Sniper Rifle","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 2, Strength 13

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":12,"price":750,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":150,"long":600,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Sniper%20Rifle.webp","effects":[]} {"_id":"sZo7FkpCKkRv6Dnw","name":"Incinerator pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 60/240), Disintegrate 13, Reload 12, Strength 11

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":5,"price":2500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","fire"]],"versatile":""},"formula":"","save":{"ability":"con","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":true,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6675001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Incinerator%20Pistol.webp","effects":[]} {"_id":"t8KzQluS5V5RSnI4","name":"Bo-rifle (Rifle)","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"
\n

The bo-rifle is a lasat weapon most commonly carried by the Honor Guard of Lasan, which has the unique property of functioning as both a rifle and a staff. On your turn, you can use your object interaction to switch between modes, detailed below.

\n

Rifle. While in this mode, the weapon uses traditional power cells.

\n

Staff. While in this mode, the weapon gains the shocking 13 property.

\n
\n
Rifle: 1d8 Energy, Ammunition (range 100/400), reload 6
\n
Staff: 1d8 Kinetic, Double (1d8 kinetic), shocking 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":7,"price":2300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":100,"long":400,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":40},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6975001,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Bo%20Rifle.webp","effects":[]} -{"_id":"tLHpQLnx7iYcLPtw","name":"War hat","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Defensive 1, Disguised, Returning, Thrown (range 30/90)

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":1100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":30,"long":90,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":true,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":true,"shk":false,"sil":false,"spc":false,"str":false,"thr":true,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7075001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"tLHpQLnx7iYcLPtw","name":"War hat","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Defensive 1, Disguised, Returning, Thrown (range 30/90)

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":1100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":30,"long":90,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":true,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":true,"shk":false,"sil":false,"spc":false,"str":false,"thr":true,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7075001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/War%20Hat.webp","effects":[]} {"_id":"tXO7WB3V2wjjaBfE","name":"Shotgun","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Burst 2, Reload 4, Strength 11

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":12,"price":350,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4600001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Shotgun.webp","effects":[]} -{"_id":"tlcCSePqUtbhMadM","name":"Chained lightdagger","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Disarming, Finesse, Luminous, Reach, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":1700,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7050001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"tlcCSePqUtbhMadM","name":"Chained Lightdagger","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Disarming, Finesse, Luminous, Reach, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":1700,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7050001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Chained%20Lightdagger.webp","effects":[]} {"_id":"u0mVXUeoHsFZ1pcG","name":"Blaster Cannon","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Burst 2, Reload 4, Strength 15

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":36,"price":1600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":120},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":400001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Blaster%20Cannon.webp","effects":[]} -{"_id":"uGe5wQQJjZrQzltP","name":"Electrostaff","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Double (1d6 kinetic), Finesse, Shocking 13, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":1100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6993751,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"uGe5wQQJjZrQzltP","name":"Electrostaff","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Double (1d6 kinetic), Finesse, Shocking 13, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":1100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6993751,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Electrostaff.webp","effects":[]} {"_id":"uQ2AXesizBRcTjRl","name":"Ion Carbine","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 16

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":8,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d3 + @mod","ion"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2600001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Ion%20Carbine.webp","effects":[]} {"_id":"v55dQl0raOAucwgP","name":"Vibromace","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":12,"price":80,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6400001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibromace.webp","effects":[]} {"_id":"w62Yd7ahdYyTH61q","name":"Shatter cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 80/320), Burst 4, Reload 8, Silent, Strength 15, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":24,"price":1300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":30},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"dex"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":true,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999220,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Shatter%20Cannon.webp","effects":[]} -{"_id":"woDLArHK5OZHsTeU","name":"Disguised Blade","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":1,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":true,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7150001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"woDLArHK5OZHsTeU","name":"Disguised Blade","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":1,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":true,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7150001,"flags":{},"img":"systems/sw5e/packs/Icons/Disguised%20Blade,"effects":[]} {"_id":"xfIWfVXfe5ZfD8S2","name":"IWS (Blaster)","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"
\n

The IWS is a heavy weapon that can fire in three different modes. On your turn, you can use your object interaction to switch between modes, detailed below.

\n

Antiarmor. While in this mode, rather than traditional power cells, the IWS fires grenades. When firing a grenade at long range, creatures within the radius of the grenade’s explosion have advantage on the saving throw.

\n

Blaster. While in this mode, the weapon uses traditional power cells.

\n

Sniper. While in this mode, the weapon uses traditional power cells.

\n
\n
Antiarmor: Special, Ammunition (range 60/240), reload 1, special
\n
Blaster: 1d8 Energy, Ammunition (range 80/320), reload 12
\n
Sniper: 1d12 Energy, Ammunition (range 120/480), reload 4
\n
 
\n
Special, Strength 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":7200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":"space"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6650001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/IWS.webp","effects":[]} {"_id":"y6faozksI3Bhwnpq","name":"Bowcaster","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Burst 4, Reload 4, Strength 11

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":16,"price":400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":50,"long":200,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":800001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Bowcaster.webp","effects":[]} -{"_id":"yVxRMON2OWIGeU4n","name":"Disruptorshiv","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Disruptive, Finesse, Shocking 13

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":900,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":true,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6750001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} -{"_id":"yyBBJgGqeZ3Qx0OP","name":"Electrohammer","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Heavy, Shocking 13, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":18,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":4950001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"yVxRMON2OWIGeU4n","name":"Disruptorshiv","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Disruptive, Finesse, Shocking 13

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":900,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":true,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6750001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Disruptorshiv.webp","effects":[]} +{"_id":"yyBBJgGqeZ3Qx0OP","name":"Electrohammer","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Heavy, Shocking 13, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":18,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":4950001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Electrohammer.webp","effects":[]} {"_id":"z5Ms1OrmxDwLqGw8","name":"Shotosaber","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":true,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Lightweapons/Shotosaber.webp","effects":[]} -{"_id":"z70LYISIcmv1gpJi","name":"Switch cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 80/320), Burst 4, Reload 8, Strength 11, Switch (2d4 acid/cold/fire/lightning), Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":26,"price":9600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":80,"long":320,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":30},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"2d4 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999806,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]} +{"_id":"z70LYISIcmv1gpJi","name":"Switch Cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 80/320), Burst 4, Reload 8, Strength 11, Switch (2d4 acid/cold/fire/lightning), Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":26,"price":9600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":80,"long":320,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":30},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"2d4 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999806,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Switch%20Cannon.webp","effects":[]} {"_id":"z7LJqf8bjWnzqEw8","name":"Flechette cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

The flechette cannon does not make attack rolls. Rather than traditional power cells, the flechette cannon uses specialized cannon tanks, which, when fired, spray an area with the contents of the tank. Projector tanks require your target to make a saving throw to resist the tank’s effects. It can have different ammunition types loaded simultaneously, and you can choose which ammunition you’re using as you fire it (no action required). If you don’t meet the flechette cannon’s strength requirement, creatures have advantage on their saving throws. If you lack proficiency in the flechette cannon, you must roll the damage dice twice and take the lesser total.

\n

Ammunition (range special), Reload 5, Special, Strength 11, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":16,"price":1800,"attunement":0,"equipped":false,"rarity":"","identified":false,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6793751,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Flechette%20Cannon.webp","effects":[]} {"_id":"zArvVI9Nz8jA7vYF","name":"Vibroaxe","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Dexterity 11

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":11,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibroaxe.webp","effects":[]} {"_id":"zGU5Id8EUqoIzAmc","name":"Hold-Out","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 6

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2400001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Holdout%20Blaster.webp","effects":[]} diff --git a/sw5e.js b/sw5e.js index 3389f0b7..3a1f6439 100644 --- a/sw5e.js +++ b/sw5e.js @@ -8,17 +8,17 @@ */ // Import Modules -import { SW5E } from "./module/config.js"; -import { registerSystemSettings } from "./module/settings.js"; -import { preloadHandlebarsTemplates } from "./module/templates.js"; -import { _getInitiativeFormula } from "./module/combat.js"; -import { measureDistances } from "./module/canvas.js"; +import {SW5E} from "./module/config.js"; +import {registerSystemSettings} from "./module/settings.js"; +import {preloadHandlebarsTemplates} from "./module/templates.js"; +import {_getInitiativeFormula} from "./module/combat.js"; +import {measureDistances} from "./module/canvas.js"; // Import Documents import Actor5e from "./module/actor/entity.js"; import Item5e from "./module/item/entity.js"; import CharacterImporter from "./module/characterImporter.js"; -import { TokenDocument5e, Token5e } from "./module/token.js" +import {TokenDocument5e, Token5e} from "./module/token.js"; // Import Applications import AbilityTemplate from "./module/pixi/ability-template.js"; @@ -46,122 +46,137 @@ import * as migrations from "./module/migration.js"; /* Foundry VTT Initialization */ /* -------------------------------------------- */ -// Keep on while migrating to Foundry version 0.8 -CONFIG.debug.hooks = true; +Hooks.once("init", function () { + console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`); -Hooks.once("init", function() { - console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`); + // Create a SW5E namespace within the game global + game.sw5e = { + applications: { + AbilityUseDialog, + ActorSheetFlags, + ActorSheet5eCharacter, + ActorSheet5eCharacterNew, + ActorSheet5eNPC, + ActorSheet5eNPCNew, + ActorSheet5eVehicle, + ItemSheet5e, + ShortRestDialog, + TraitSelector, + ActorMovementConfig, + ActorSensesConfig + }, + canvas: { + AbilityTemplate + }, + config: SW5E, + dice: dice, + entities: { + Actor5e, + Item5e, + TokenDocument5e, + Token5e + }, + macros: macros, + migrations: migrations, + rollItemMacro: macros.rollItemMacro + }; - // Create a SW5E namespace within the game global - game.sw5e = { - applications: { - AbilityUseDialog, - ActorSheetFlags, - ActorSheet5eCharacter, - ActorSheet5eCharacterNew, - ActorSheet5eNPC, - ActorSheet5eNPCNew, - ActorSheet5eVehicle, - ItemSheet5e, - ShortRestDialog, - TraitSelector, - ActorMovementConfig, - ActorSensesConfig - }, - canvas: { - AbilityTemplate - }, - config: SW5E, - dice: dice, - entities: { - Actor5e, - Item5e, - TokenDocument5e, - Token5e, - }, - macros: macros, - migrations: migrations, - rollItemMacro: macros.rollItemMacro - }; + // Record Configuration Values + CONFIG.SW5E = SW5E; + CONFIG.Actor.documentClass = Actor5e; + CONFIG.Item.documentClass = Item5e; + CONFIG.Token.documentClass = TokenDocument5e; + CONFIG.Token.objectClass = Token5e; + CONFIG.time.roundTime = 6; + CONFIG.fontFamilies = ["Engli-Besh", "Open Sans", "Russo One"]; - // Record Configuration Values - CONFIG.SW5E = SW5E; - CONFIG.Actor.documentClass = Actor5e; - CONFIG.Item.documentClass = Item5e; - CONFIG.Token.documentClass = TokenDocument5e; - CONFIG.Token.objectClass = Token5e; - CONFIG.time.roundTime = 6; - CONFIG.fontFamilies = [ - "Engli-Besh", - "Open Sans", - "Russo One" - ]; + CONFIG.Dice.DamageRoll = dice.DamageRoll; + CONFIG.Dice.D20Roll = dice.D20Roll; - CONFIG.Dice.DamageRoll = dice.DamageRoll; - CONFIG.Dice.D20Roll = dice.D20Roll; + // 5e cone RAW should be 53.13 degrees + CONFIG.MeasuredTemplate.defaults.angle = 53.13; - // 5e cone RAW should be 53.13 degrees - CONFIG.MeasuredTemplate.defaults.angle = 53.13; + // Add DND5e namespace for module compatability + game.dnd5e = game.sw5e; + CONFIG.DND5E = CONFIG.SW5E; - // Add DND5e namespace for module compatability - game.dnd5e = game.sw5e; - CONFIG.DND5E = CONFIG.SW5E; + // Register System Settings + registerSystemSettings(); - // Register System Settings - registerSystemSettings(); + // Patch Core Functions + CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus"; + Combatant.prototype._getInitiativeFormula = _getInitiativeFormula; - // Patch Core Functions - CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus"; - Combatant.prototype._getInitiativeFormula = _getInitiativeFormula; + // Register Roll Extensions + CONFIG.Dice.rolls.push(dice.D20Roll); + CONFIG.Dice.rolls.push(dice.DamageRoll); - // Register Roll Extensions - CONFIG.Dice.rolls.push(dice.D20Roll); - CONFIG.Dice.rolls.push(dice.DamageRoll); + // Register sheet application classes + Actors.unregisterSheet("core", ActorSheet); + Actors.registerSheet("sw5e", ActorSheet5eCharacterNew, { + types: ["character"], + makeDefault: true, + label: "SW5E.SheetClassCharacter" + }); + Actors.registerSheet("sw5e", ActorSheet5eCharacter, { + types: ["character"], + makeDefault: false, + label: "SW5E.SheetClassCharacterOld" + }); + Actors.registerSheet("sw5e", ActorSheet5eNPCNew, { + types: ["npc"], + makeDefault: true, + label: "SW5E.SheetClassNPC" + }); + Actors.registerSheet("sw5e", ActorSheet5eNPC, { + types: ["npc"], + makeDefault: false, + label: "SW5E.SheetClassNPCOld" + }); + // Actors.registerSheet("sw5e", ActorSheet5eStarship, { + // types: ["starship"], + // makeDefault: true, + // label: "SW5E.SheetClassStarship" + // }); + Actors.registerSheet("sw5e", ActorSheet5eVehicle, { + types: ["vehicle"], + makeDefault: true, + label: "SW5E.SheetClassVehicle" + }); + Items.unregisterSheet("core", ItemSheet); + Items.registerSheet("sw5e", ItemSheet5e, { + types: [ + "weapon", + "equipment", + "consumable", + "tool", + "loot", + "class", + "power", + "feat", + "species", + "backpack", + "archetype", + "classfeature", + "background", + "fightingmastery", + "fightingstyle", + "lightsaberform", + "deployment", + "deploymentfeature", + "starship", + "starshipfeature", + "starshipmod", + "venture" + ], + makeDefault: true, + label: "SW5E.SheetClassItem" + }); - // Register sheet application classes - Actors.unregisterSheet("core", ActorSheet); - Actors.registerSheet("sw5e", ActorSheet5eCharacterNew, { - types: ["character"], - makeDefault: true, - label: "SW5E.SheetClassCharacter" - }); - Actors.registerSheet("sw5e", ActorSheet5eCharacter, { - types: ["character"], - makeDefault: false, - label: "SW5E.SheetClassCharacterOld" - }); - Actors.registerSheet("sw5e", ActorSheet5eNPCNew, { - types: ["npc"], - makeDefault: true, - label: "SW5E.SheetClassNPC" - }); - Actors.registerSheet("sw5e", ActorSheet5eNPC, { - types: ["npc"], - makeDefault: false, - label: "SW5E.SheetClassNPCOld" - }); - // Actors.registerSheet("sw5e", ActorSheet5eStarship, { - // types: ["starship"], - // makeDefault: true, - // label: "SW5E.SheetClassStarship" - // }); - Actors.registerSheet('sw5e', ActorSheet5eVehicle, { - types: ['vehicle'], - makeDefault: true, - label: "SW5E.SheetClassVehicle" - }); - Items.unregisterSheet("core", ItemSheet); - Items.registerSheet("sw5e", ItemSheet5e, { - types: ['weapon', 'equipment', 'consumable', 'tool', 'loot', 'class', 'power', 'feat', 'species', 'backpack', 'archetype', 'classfeature', 'background', 'fightingmastery', 'fightingstyle', 'lightsaberform', 'deployment', 'deploymentfeature', 'starship', 'starshipfeature', 'starshipmod', 'venture'], - makeDefault: true, - label: "SW5E.SheetClassItem" - }); - - // Preload Handlebars Templates - return preloadHandlebarsTemplates(); + // Preload Handlebars Templates + return preloadHandlebarsTemplates(); }); - /* -------------------------------------------- */ /* Foundry VTT Setup */ /* -------------------------------------------- */ @@ -169,131 +184,175 @@ Hooks.once("init", function() { /** * This function runs after game data has been requested and loaded from the servers, so entities exist */ -Hooks.once("setup", function() { +Hooks.once("setup", function () { + // Localize CONFIG objects once up-front + const toLocalize = [ + "abilities", + "abilityAbbreviations", + "abilityActivationTypes", + "abilityConsumptionTypes", + "actorSizes", + "alignments", + "armorProficiencies", + "armorPropertiesTypes", + "conditionTypes", + "consumableTypes", + "cover", + "currencies", + "damageResistanceTypes", + "damageTypes", + "distanceUnits", + "equipmentTypes", + "healingTypes", + "itemActionTypes", + "languages", + "limitedUsePeriods", + "movementTypes", + "movementUnits", + "polymorphSettings", + "proficiencyLevels", + "senses", + "skills", + "starshipRolessm", + "starshipRolesmed", + "starshipRoleslg", + "starshipRoleshuge", + "starshipRolesgrg", + "starshipSkills", + "powerComponents", + "powerLevels", + "powerPreparationModes", + "powerScalingModes", + "powerSchools", + "targetTypes", + "timePeriods", + "toolProficiencies", + "weaponProficiencies", + "weaponProperties", + "weaponSizes", + "weaponTypes" + ]; - // Localize CONFIG objects once up-front - const toLocalize = [ - "abilities", "abilityAbbreviations", "abilityActivationTypes", "abilityConsumptionTypes", "actorSizes", "alignments", - "armorProficiencies", "armorPropertiesTypes", "conditionTypes", "consumableTypes", "cover", "currencies", "damageResistanceTypes", - "damageTypes", "distanceUnits", "equipmentTypes", "healingTypes", "itemActionTypes", "languages", - "limitedUsePeriods", "movementTypes", "movementUnits", "polymorphSettings", "proficiencyLevels", "senses", "skills", - "starshipRolessm", "starshipRolesmed", "starshipRoleslg", "starshipRoleshuge", "starshipRolesgrg", "starshipSkills", - "powerComponents", "powerLevels", "powerPreparationModes", "powerScalingModes", "powerSchools", "targetTypes", - "timePeriods", "toolProficiencies", "weaponProficiencies", "weaponProperties", "weaponSizes", "weaponTypes" - ]; + // Exclude some from sorting where the default order matters + const noSort = [ + "abilities", + "alignments", + "currencies", + "distanceUnits", + "movementUnits", + "itemActionTypes", + "proficiencyLevels", + "limitedUsePeriods", + "powerComponents", + "powerLevels", + "powerPreparationModes", + "weaponTypes" + ]; - // Exclude some from sorting where the default order matters - const noSort = [ - "abilities", "alignments", "currencies", "distanceUnits", "movementUnits", "itemActionTypes", "proficiencyLevels", - "limitedUsePeriods", "powerComponents", "powerLevels", "powerPreparationModes", "weaponTypes" - ]; - - // Localize and sort CONFIG objects - for ( let o of toLocalize ) { - const localized = Object.entries(CONFIG.SW5E[o]).map(e => { - return [e[0], game.i18n.localize(e[1])]; - }); - if ( !noSort.includes(o) ) localized.sort((a, b) => a[1].localeCompare(b[1])); - CONFIG.SW5E[o] = localized.reduce((obj, e) => { - obj[e[0]] = e[1]; - return obj; - }, {}); - } - // add DND5E translation for module compatability - game.i18n.translations.DND5E = game.i18n.translations.SW5E; - // console.log(game.settings.get("sw5e", "colorTheme")); - let theme = game.settings.get("sw5e", "colorTheme") + '-theme'; - document.body.classList.add(theme); + // Localize and sort CONFIG objects + for (let o of toLocalize) { + const localized = Object.entries(CONFIG.SW5E[o]).map((e) => { + return [e[0], game.i18n.localize(e[1])]; + }); + if (!noSort.includes(o)) localized.sort((a, b) => a[1].localeCompare(b[1])); + CONFIG.SW5E[o] = localized.reduce((obj, e) => { + obj[e[0]] = e[1]; + return obj; + }, {}); + } + // add DND5E translation for module compatability + game.i18n.translations.DND5E = game.i18n.translations.SW5E; + // console.log(game.settings.get("sw5e", "colorTheme")); + let theme = game.settings.get("sw5e", "colorTheme") + "-theme"; + document.body.classList.add(theme); }); /* -------------------------------------------- */ /** * Once the entire VTT framework is initialized, check to see if we should perform a data migration */ -Hooks.once("ready", function() { +Hooks.once("ready", function () { + // Wait to register hotbar drop hook on ready so that modules could register earlier if they want to + Hooks.on("hotbarDrop", (bar, data, slot) => macros.create5eMacro(data, slot)); - // Wait to register hotbar drop hook on ready so that modules could register earlier if they want to - Hooks.on("hotbarDrop", (bar, data, slot) => macros.create5eMacro(data, slot)); + // Determine whether a system migration is required and feasible + if (!game.user.isGM) return; + const currentVersion = game.settings.get("sw5e", "systemMigrationVersion"); + const NEEDS_MIGRATION_VERSION = "1.3.5.R1-A6"; + // Check for R1 SW5E versions + const SW5E_NEEDS_MIGRATION_VERSION = "R1-A6"; + const COMPATIBLE_MIGRATION_VERSION = 0.8; + const needsMigration = + currentVersion && + (isNewerVersion(SW5E_NEEDS_MIGRATION_VERSION, currentVersion) || + isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion)); + if (!needsMigration && needsMigration !== "") return; - // Determine whether a system migration is required and feasible - if ( !game.user.isGM ) return; - const currentVersion = game.settings.get("sw5e", "systemMigrationVersion"); - const NEEDS_MIGRATION_VERSION = "1.3.0.R1-A6"; - // Check for R1 SW5E versions - const SW5E_NEEDS_MIGRATION_VERSION = "R1-A6"; - const COMPATIBLE_MIGRATION_VERSION = 0.80; - const needsMigration = currentVersion && (isNewerVersion(SW5E_NEEDS_MIGRATION_VERSION, currentVersion) || isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion)); - if (!needsMigration && needsMigration !== "") return; - - // Perform the migration - if ( currentVersion && isNewerVersion(COMPATIBLE_MIGRATION_VERSION, currentVersion) ) { - const warning = `Your SW5e system data is from too old a Foundry version and cannot be reliably migrated to the latest version. The process will be attempted, but errors may occur.`; - ui.notifications.error(warning, {permanent: true}); - } - migrations.migrateWorld(); + // Perform the migration + if (currentVersion && isNewerVersion(COMPATIBLE_MIGRATION_VERSION, currentVersion)) { + const warning = `Your SW5e system data is from too old a Foundry version and cannot be reliably migrated to the latest version. The process will be attempted, but errors may occur.`; + ui.notifications.error(warning, {permanent: true}); + } + migrations.migrateWorld(); }); /* -------------------------------------------- */ /* Canvas Initialization */ /* -------------------------------------------- */ -Hooks.on("canvasInit", function() { - // Extend Diagonal Measurement - canvas.grid.diagonalRule = game.settings.get("sw5e", "diagonalMovement"); - SquareGrid.prototype.measureDistances = measureDistances; +Hooks.on("canvasInit", function () { + // Extend Diagonal Measurement + canvas.grid.diagonalRule = game.settings.get("sw5e", "diagonalMovement"); + SquareGrid.prototype.measureDistances = measureDistances; }); - /* -------------------------------------------- */ /* Other Hooks */ /* -------------------------------------------- */ Hooks.on("renderChatMessage", (app, html, data) => { + // Display action buttons + chat.displayChatActionButtons(app, html, data); - // Display action buttons - chat.displayChatActionButtons(app, html, data); + // Highlight critical success or failure die + chat.highlightCriticalSuccessFailure(app, html, data); - // Highlight critical success or failure die - chat.highlightCriticalSuccessFailure(app, html, data); - - // Optionally collapse the content - if (game.settings.get("sw5e", "autoCollapseItemCards")) html.find(".card-content").hide(); + // Optionally collapse the content + if (game.settings.get("sw5e", "autoCollapseItemCards")) html.find(".card-content").hide(); }); Hooks.on("getChatLogEntryContext", chat.addChatMessageContextOptions); Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html)); Hooks.on("renderChatPopout", (app, html, data) => Item5e.chatListeners(html)); -Hooks.on('getActorDirectoryEntryContext', Actor5e.addDirectoryContextOptions); -Hooks.on("renderSceneDirectory", (app, html, data)=> { - //console.log(html.find("header.folder-header")); - setFolderBackground(html); +Hooks.on("getActorDirectoryEntryContext", Actor5e.addDirectoryContextOptions); +Hooks.on("renderSceneDirectory", (app, html, data) => { + //console.log(html.find("header.folder-header")); + setFolderBackground(html); }); -Hooks.on("renderActorDirectory", (app, html, data)=> { - setFolderBackground(html); - CharacterImporter.addImportButton(html); +Hooks.on("renderActorDirectory", (app, html, data) => { + setFolderBackground(html); + CharacterImporter.addImportButton(html); }); -Hooks.on("renderItemDirectory", (app, html, data)=> { - setFolderBackground(html); +Hooks.on("renderItemDirectory", (app, html, data) => { + setFolderBackground(html); }); -Hooks.on("renderJournalDirectory", (app, html, data)=> { - setFolderBackground(html); +Hooks.on("renderJournalDirectory", (app, html, data) => { + setFolderBackground(html); }); -Hooks.on("renderRollTableDirectory", (app, html, data)=> { - setFolderBackground(html); +Hooks.on("renderRollTableDirectory", (app, html, data) => { + setFolderBackground(html); }); Hooks.on("ActorSheet5eCharacterNew", (app, html, data) => { - console.log("renderSwaltSheet"); + console.log("renderSwaltSheet"); }); // FIXME: This helper is needed for the vehicle sheet. It should probably be refactored. -Handlebars.registerHelper('getProperty', function (data, property) { - return getProperty(data, property); +Handlebars.registerHelper("getProperty", function (data, property) { + return getProperty(data, property); }); - function setFolderBackground(html) { - html.find("header.folder-header").each(function() { - let bgColor = $(this).css("background-color"); - if(bgColor == undefined) - bgColor = "rgb(255,255,255)"; - $(this).closest('li').css("background-color", bgColor); - }) -} \ No newline at end of file + html.find("header.folder-header").each(function () { + let bgColor = $(this).css("background-color"); + if (bgColor == undefined) bgColor = "rgb(255,255,255)"; + $(this).closest("li").css("background-color", bgColor); + }); +} diff --git a/system.json b/system.json index ac481ad6..3a0a82ca 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "name": "sw5e", "title": "SW 5th Edition", "description": "A comprehensive game system for running games of SW 5th Edition in the Foundry VTT environment.", - "version": "1.3.3.R1-A6", + "version": "1.3.5.R1-A7", "author": "Dev Team", "scripts": [], "esmodules": ["sw5e.js"], @@ -151,8 +151,8 @@ "gridUnits": "ft", "primaryTokenAttribute": "attributes.hp", "secondaryTokenAttribute": null, - "minimumCoreVersion": "0.8.3", - "compatibleCoreVersion": "0.8.6", + "minimumCoreVersion": "0.8.2", + "compatibleCoreVersion": "0.8.8", "url": "https://github.com/unrealkakeman89/sw5e", "manifest": "https://raw.githubusercontent.com/unrealkakeman89/sw5e/master/system.json", "download": "https://github.com/unrealkakeman89/sw5e/archive/master.zip" diff --git a/templates/actors/newActor/npc-sheet.html b/templates/actors/newActor/npc-sheet.html index 70552323..463c6d57 100644 --- a/templates/actors/newActor/npc-sheet.html +++ b/templates/actors/newActor/npc-sheet.html @@ -97,13 +97,13 @@ value="{{ability.value}}" data-dtype="Number" placeholder="10" />
{{numberFormat ability.mod decimals=0 sign=true}} + title="{{ localize 'SW5E.Modifier' }}">{{numberFormat ability.mod decimals=0 sign=true}} {{numberFormat ability.save decimals=0 sign=true}} + title="{{ localize 'SW5E.SavingThrow' }}">{{numberFormat ability.save decimals=0 sign=true}}
{{/each}} diff --git a/templates/actors/newActor/parts/swalt-active-effects.html b/templates/actors/newActor/parts/swalt-active-effects.html index d712928d..70be6e1b 100644 --- a/templates/actors/newActor/parts/swalt-active-effects.html +++ b/templates/actors/newActor/parts/swalt-active-effects.html @@ -3,8 +3,8 @@ {{#each effects as |section sid|}}
  • {{localize section.label}}

    -
    Source
    -
    Duration
    +
    {{localize "SW5E.Source"}}
    +
    {{localize "SW5E.Duration"}}
  • {{/each}} diff --git a/templates/actors/newActor/parts/swalt-crew.html b/templates/actors/newActor/parts/swalt-crew.html index c67e67fe..16c50787 100644 --- a/templates/actors/newActor/parts/swalt-crew.html +++ b/templates/actors/newActor/parts/swalt-crew.html @@ -66,7 +66,7 @@
    - Level {{item.data.levels}}       + {{localize "SW5E.Level"}} {{item.data.levels}}      
    - Level {{item.data.levels}}       + {{localize "SW5E.Level"}} {{item.data.levels}}