Compare commits

..

1 commit

Author SHA1 Message Date
Professor Bunbury
fb1a4f538a "My Dudes" Updates - 7/8 - 7/15/2021
+ Adds 45 new multiclass feats and associated artwork to the Feats compendium.
^ Updates Artillerist Technique and associated artwork in the Archetypes compendium.
2021-07-16 13:47:24 -04:00
208 changed files with 11497 additions and 17465 deletions

3
.gitignore vendored
View file

@ -22,9 +22,6 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid* hs_err_pid*
# Mac-OS file
.DS_Store
# IDE Folders # IDE Folders
.idea/ .idea/
.vs/ .vs/

View file

@ -1,14 +0,0 @@
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "consistent",
"jsxSingleQuote": false,
"trailingComma": "none",
"bracketSpacing": false,
"jsxBracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf"
}

View file

@ -1,3 +1,2 @@
{ {
"editor.formatOnSave": true
} }

View file

@ -43,13 +43,11 @@
"SETTINGS.5eInitTBN": "Initiative Dexterity Tiebreaker", "SETTINGS.5eInitTBN": "Initiative Dexterity Tiebreaker",
"SETTINGS.5eNoExpL": "Remove experience bars from character sheets.", "SETTINGS.5eNoExpL": "Remove experience bars from character sheets.",
"SETTINGS.5eNoExpN": "Disable Experience Tracking", "SETTINGS.5eNoExpN": "Disable Experience Tracking",
"SETTINGS.5eReset": "Reset",
"SETTINGS.5eRestEpic": "Epic Heroism (LR: 1 hour, SR: 1 min)", "SETTINGS.5eRestEpic": "Epic Heroism (LR: 1 hour, SR: 1 min)",
"SETTINGS.5eRestGritty": "Gritty Realism (LR: 7 days, SR: 8 hours)", "SETTINGS.5eRestGritty": "Gritty Realism (LR: 7 days, SR: 8 hours)",
"SETTINGS.5eRestL": "Configure which rest variant should be used for games within this system.", "SETTINGS.5eRestL": "Configure which rest variant should be used for games within this system.",
"SETTINGS.5eRestN": "Rest Variant", "SETTINGS.5eRestN": "Rest Variant",
"SETTINGS.5eRestPHB": "Player's Handbook (LR: 8 hours, SR: 1 hour)", "SETTINGS.5eRestPHB": "Player's Handbook (LR: 8 hours, SR: 1 hour)",
"SETTINGS.5eUndoChanges": "Undo Changes",
"SETTINGS.SWColorDark": "Dark Theme", "SETTINGS.SWColorDark": "Dark Theme",
"SETTINGS.SWColorL": "Set the color theme of the game", "SETTINGS.SWColorL": "Set the color theme of the game",
"SETTINGS.SWColorLight": "Light Theme", "SETTINGS.SWColorLight": "Light Theme",
@ -80,7 +78,6 @@
"SW5E.AbilityUseCast": "Cast Power", "SW5E.AbilityUseCast": "Cast Power",
"SW5E.AbilityUseChargedHint": "This {type} is charged and ready to use!", "SW5E.AbilityUseChargedHint": "This {type} is charged and ready to use!",
"SW5E.AbilityUseChargesLabel": "{value} Charges", "SW5E.AbilityUseChargesLabel": "{value} Charges",
"SW5E.AbilityUseConfig": "Usage Configuration",
"SW5E.AbilityUseConsumableChargeHint": "Using this {type} will consume 1 charge of {value} remaining.", "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.AbilityUseConsumableDestroyHint": "Using this {type} will consume its final charge and it will be destroyed.",
"SW5E.AbilityUseConsumableLabel": "{max} per {per}", "SW5E.AbilityUseConsumableLabel": "{max} per {per}",
@ -107,9 +104,7 @@
"SW5E.ActionUtil": "Utility", "SW5E.ActionUtil": "Utility",
"SW5E.ActionWarningNoItem": "The requested item {item} no longer exists on Actor {name}", "SW5E.ActionWarningNoItem": "The requested item {item} no longer exists on Actor {name}",
"SW5E.ActionWarningNoToken": "You must have one or more controlled Tokens in order to use this option.", "SW5E.ActionWarningNoToken": "You must have one or more controlled Tokens in order to use this option.",
"SW5E.ActorWarningInvalidItem": "{itemType} items cannot be added to a {actorType}.",
"SW5E.Add": "Add", "SW5E.Add": "Add",
"SW5E.AddEmbeddedItemPromptHint": "Do you want to add these items to your character sheet?",
"SW5E.AdditionalNotes": "Additional Notes", "SW5E.AdditionalNotes": "Additional Notes",
"SW5E.Advantage": "Advantage", "SW5E.Advantage": "Advantage",
"SW5E.Alignment": "Alignment", "SW5E.Alignment": "Alignment",
@ -123,7 +118,6 @@
"SW5E.AlignmentND": "Neutral Dark", "SW5E.AlignmentND": "Neutral Dark",
"SW5E.AlignmentNL": "Neutral Light", "SW5E.AlignmentNL": "Neutral Light",
"SW5E.Appearance": "Appearance", "SW5E.Appearance": "Appearance",
"SW5E.Apply": "Apply",
"SW5E.ArchetypeName": "Archetype Name", "SW5E.ArchetypeName": "Archetype Name",
"SW5E.Archetypes": "Archetypes", "SW5E.Archetypes": "Archetypes",
"SW5E.ArmorClass": "Armor Class", "SW5E.ArmorClass": "Armor Class",
@ -205,11 +199,7 @@
"SW5E.ChatContextHealing": "Apply Healing", "SW5E.ChatContextHealing": "Apply Healing",
"SW5E.ChatFlavor": "Chat Message Flavor", "SW5E.ChatFlavor": "Chat Message Flavor",
"SW5E.ClassLevels": "Class Levels", "SW5E.ClassLevels": "Class Levels",
"SW5E.ClassMakeOriginal": "Original Class",
"SW5E.ClassMakeOriginalHint": "First class taken by character used to determine certain class traits when multiclassing.",
"SW5E.ClassName": "Class Name", "SW5E.ClassName": "Class Name",
"SW5E.ClassOriginal": "Original Class",
"SW5E.ClassSaves": "Saving Throws",
"SW5E.ClassSkillsChosen": "Chosen Class Skills", "SW5E.ClassSkillsChosen": "Chosen Class Skills",
"SW5E.ClassSkillsNumber": "Number of Starting Skills", "SW5E.ClassSkillsNumber": "Number of Starting Skills",
"SW5E.Collapse": "Collapse/Expand", "SW5E.Collapse": "Collapse/Expand",
@ -271,32 +261,7 @@
"SW5E.CoverHalf": "Half", "SW5E.CoverHalf": "Half",
"SW5E.CoverThreeQuarters": "Three Quarters", "SW5E.CoverThreeQuarters": "Three Quarters",
"SW5E.CoverTotal": "Total", "SW5E.CoverTotal": "Total",
"SW5E.CreatureAberration": "Aberration",
"SW5E.CreatureAberrationPl": "Aberrations",
"SW5E.CreatureBeast": "Beast",
"SW5E.CreatureBeastPl": "Beasts",
"SW5E.CreatureConstruct": "Construct",
"SW5E.CreatureConstructPl": "Constructs",
"SW5E.CreatureDroid": "Droid",
"SW5E.CreatureDroidPl": "Droids",
"SW5E.CreatureForceEntity": "Force Entity",
"SW5E.CreatureForceEntityPl": "Force Entities",
"SW5E.CreatureHumanoid": "Humanoid",
"SW5E.CreatureHumanoidPl": "Humanoids",
"SW5E.CreaturePlant": "Plant",
"SW5E.CreaturePlantPl": "Plants",
"SW5E.CreatureSwarm": "Swarm",
"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.CreatureTypeTitle": "Configure Creature Type",
"SW5E.CreatureUndead": "Undead",
"SW5E.CreatureUndeadPl": "Undead",
"SW5E.CrewCap": "Crew Capacity", "SW5E.CrewCap": "Crew Capacity",
"SW5E.Crewed": "Crewed",
"SW5E.Critical": "Critical", "SW5E.Critical": "Critical",
"SW5E.CriticalHit": "Critical Hit", "SW5E.CriticalHit": "Critical Hit",
"SW5E.Currency": "Currency", "SW5E.Currency": "Currency",
@ -318,6 +283,7 @@
"SW5E.DamageRoll": "Damage Roll", "SW5E.DamageRoll": "Damage Roll",
"SW5E.DamageSonic": "Sonic", "SW5E.DamageSonic": "Sonic",
"SW5E.DamImm": "Damage Immunities", "SW5E.DamImm": "Damage Immunities",
"SW5E.DmgRed": "Damage Reduction",
"SW5E.DamRes": "Damage Resistances", "SW5E.DamRes": "Damage Resistances",
"SW5E.DamVuln": "Damage Vulnerabilities", "SW5E.DamVuln": "Damage Vulnerabilities",
"SW5E.DarkPowerDC": "Dark Power DC", "SW5E.DarkPowerDC": "Dark Power DC",
@ -343,29 +309,24 @@
"SW5E.DistMi": "Miles", "SW5E.DistMi": "Miles",
"SW5E.DistSelf": "Self", "SW5E.DistSelf": "Self",
"SW5E.DistTouch": "Touch", "SW5E.DistTouch": "Touch",
"SW5E.DmgRed": "Damage Reduction",
"SW5E.Duration": "Duration", "SW5E.Duration": "Duration",
"SW5E.EffectsCategoryTemporary": "Temporary Effects",
"SW5E.EffectsCategoryPassive": "Passive Effects",
"SW5E.EffectsCategoryInactive": "Inactive Effects",
"SW5E.EffectCreate": "Create Effect", "SW5E.EffectCreate": "Create Effect",
"SW5E.EffectDelete": "Delete Effect", "SW5E.EffectDelete": "Delete Effect",
"SW5E.EffectEdit": "Edit Effect", "SW5E.EffectEdit": "Edit Effect",
"SW5E.EffectInactive": "Inactive Effects",
"SW5E.EffectNew": "New Effect",
"SW5E.EffectPassive": "Passive Effects",
"SW5E.Effects": "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.EffectToggle": "Toggle Effect",
"SW5E.Engine": "Engine", "SW5E.Engine": "Engine",
"SW5E.EnginePl": "Engines", "SW5E.EnginePl": "Engines",
"SW5E.EquipmentBonus": "Magical Bonus", "SW5E.EquipmentBonus": "Magical Bonus",
"SW5E.EquipmentClothing": "Clothing", "SW5E.EquipmentClothing": "Clothing",
"SW5E.EquipmentHeavy": "Heavy Armor", "SW5E.EquipmentHeavy": "Heavy Armor",
"SW5E.EquipmentHyperdrive": "Hyperdrive",
"SW5E.EquipmentLight": "Light Armor", "SW5E.EquipmentLight": "Light Armor",
"SW5E.EquipmentMedium": "Medium Armor", "SW5E.EquipmentMedium": "Medium Armor",
"SW5E.EquipmentNatural": "Natural Armor", "SW5E.EquipmentNatural": "Natural Armor",
"SW5E.EquipmentHyperdrive": "Hyperdrive",
"SW5E.EquipmentPowerCoupling": "Power Coupling", "SW5E.EquipmentPowerCoupling": "Power Coupling",
"SW5E.EquipmentReactor": "Reactor", "SW5E.EquipmentReactor": "Reactor",
"SW5E.EquipmentShield": "Shield", "SW5E.EquipmentShield": "Shield",
@ -378,9 +339,8 @@
"SW5E.Exhaustion": "Exhaustion", "SW5E.Exhaustion": "Exhaustion",
"SW5E.Expand": "Expand", "SW5E.Expand": "Expand",
"SW5E.Expertise": "Expertise", "SW5E.Expertise": "Expertise",
"SW5E.Favorites": "Favorites", "SW5E.Favorites": "Favoris",
"SW5E.FavoritesAndNotes": "Favorites & Notes", "SW5E.FavoritesAndNotes": "Favorites & Notes",
"SW5E.Feats": "Feats",
"SW5E.FeatureActionRecharge": "Action Recharge", "SW5E.FeatureActionRecharge": "Action Recharge",
"SW5E.FeatureActive": "Active Abilities", "SW5E.FeatureActive": "Active Abilities",
"SW5E.FeatureAdd": "Create Feature", "SW5E.FeatureAdd": "Create Feature",
@ -391,8 +351,8 @@
"SW5E.FeatureRechargeOn": "Recharge On", "SW5E.FeatureRechargeOn": "Recharge On",
"SW5E.FeatureRechargeResult": "1d6 Result", "SW5E.FeatureRechargeResult": "1d6 Result",
"SW5E.Features": "Features", "SW5E.Features": "Features",
"SW5E.FeatureType": "Feature Type",
"SW5E.FeatureUsage": "Feature Usage", "SW5E.FeatureUsage": "Feature Usage",
"SW5E.FeatureType": "Feature Type",
"SW5E.FeetAbbr": "ft.", "SW5E.FeetAbbr": "ft.",
"SW5E.Filter": "Filter", "SW5E.Filter": "Filter",
"SW5E.FilterNoPowers": "No powers found for this set of filters.", "SW5E.FilterNoPowers": "No powers found for this set of filters.",
@ -522,8 +482,8 @@
"SW5E.ForcePowerbook": "Force Powers", "SW5E.ForcePowerbook": "Force Powers",
"SW5E.Formula": "Formula", "SW5E.Formula": "Formula",
"SW5E.FuelCapacity": "Fuel Capacity", "SW5E.FuelCapacity": "Fuel Capacity",
"SW5E.FuelCostPerUnit": "Fuel Cost per Unit",
"SW5E.FuelCostsMod": "Fuel Costs Modifier", "SW5E.FuelCostsMod": "Fuel Costs Modifier",
"SW5E.FuelCostPerUnit": "Fuel Cost per Unit",
"SW5E.GrantedAbilities": "Granted Abilities", "SW5E.GrantedAbilities": "Granted Abilities",
"SW5E.HalfProficient": "Half Proficient", "SW5E.HalfProficient": "Half Proficient",
"SW5E.HardpointSizeMod": "Hardpoint Size Modifier", "SW5E.HardpointSizeMod": "Hardpoint Size Modifier",
@ -533,10 +493,6 @@
"SW5E.HealthConditions": "Health Conditions", "SW5E.HealthConditions": "Health Conditions",
"SW5E.HealthFormula": "Health Formula", "SW5E.HealthFormula": "Health Formula",
"SW5E.HitDice": "Hit Dice", "SW5E.HitDice": "Hit Dice",
"SW5E.HitDiceConfig": "Adjust Hit Dice",
"SW5E.HitDiceConfigHint": "Adjust remaining hit dice levels for each class.",
"SW5E.HitDiceMax": "Maximum Hit Dice",
"SW5E.HitDiceRemaining": "Remaining Hit Dice",
"SW5E.HitDiceRoll": "Roll Hit Dice", "SW5E.HitDiceRoll": "Roll Hit Dice",
"SW5E.HitDiceUsed": "Hit Dice Used", "SW5E.HitDiceUsed": "Hit Dice Used",
"SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!", "SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!",
@ -589,7 +545,6 @@
"SW5E.ItemTypeArchetype": "Archetype", "SW5E.ItemTypeArchetype": "Archetype",
"SW5E.ItemTypeBackground": "Background", "SW5E.ItemTypeBackground": "Background",
"Sw5E.ItemTypeBackgroundPl": "Backgrounds", "Sw5E.ItemTypeBackgroundPl": "Backgrounds",
"SW5E.ItemTypeBackpack": "Container",
"SW5E.ItemTypeClass": "Class", "SW5E.ItemTypeClass": "Class",
"SW5E.ItemTypeClassFeat": "Class Feature", "SW5E.ItemTypeClassFeat": "Class Feature",
"SW5E.ItemTypeClassFeats": "Class Features", "SW5E.ItemTypeClassFeats": "Class Features",
@ -599,9 +554,9 @@
"SW5E.ItemTypeContainer": "Container", "SW5E.ItemTypeContainer": "Container",
"SW5E.ItemTypeContainerPl": "Containers", "SW5E.ItemTypeContainerPl": "Containers",
"SW5E.ItemTypeDeployment": "Deployment", "SW5E.ItemTypeDeployment": "Deployment",
"SW5E.ItemTypeDeploymentPl": "Deployments",
"SW5E.ItemTypeDeploymentFeature": "Deployment Feature", "SW5E.ItemTypeDeploymentFeature": "Deployment Feature",
"SW5E.ItemTypeDeploymentFeaturePl": "Deployment Features", "SW5E.ItemTypeDeploymentFeaturePl": "Deployment Features",
"SW5E.ItemTypeDeploymentPl": "Deployments",
"SW5E.ItemTypeEquipment": "Equipment", "SW5E.ItemTypeEquipment": "Equipment",
"SW5E.ItemTypeEquipmentPl": "Equipment", "SW5E.ItemTypeEquipmentPl": "Equipment",
"SW5E.ItemTypeFeat": "Feat", "SW5E.ItemTypeFeat": "Feat",
@ -756,7 +711,6 @@
"SW5E.LongRest": "Long Rest", "SW5E.LongRest": "Long Rest",
"SW5E.LongRestEpic": "Long Rest (1 hour)", "SW5E.LongRestEpic": "Long Rest (1 hour)",
"SW5E.LongRestGritty": "Long Rest (7 days)", "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.LongRestNormal": "Long Rest (8 hours)",
"SW5E.LongRestOvernight": "Long Rest (New Day)", "SW5E.LongRestOvernight": "Long Rest (New Day)",
"SW5E.LongRestResult": "{name} takes a long rest.", "SW5E.LongRestResult": "{name} takes a long rest.",
@ -776,8 +730,8 @@
"SW5E.LongRestResultTP": "{name} takes a long rest and recovers {tech} Tech Points.", "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.LongRestResultTPHD": "{name} takes a long rest and recovers {tech} Tech Points and {dice} Hit Dice.",
"SW5E.Max": "Max", "SW5E.Max": "Max",
"SW5E.ModCap": "Modification Capacity",
"SW5E.Modifier": "Modifier", "SW5E.Modifier": "Modifier",
"SW5E.ModCap": "Modification Capacity",
"SW5E.Movement": "Movement", "SW5E.Movement": "Movement",
"SW5E.MovementBurrow": "Burrow", "SW5E.MovementBurrow": "Burrow",
"SW5E.MovementClimb": "Climb", "SW5E.MovementClimb": "Climb",
@ -793,8 +747,6 @@
"SW5E.MovementUnits": "Units", "SW5E.MovementUnits": "Units",
"SW5E.MovementWalk": "Walk", "SW5E.MovementWalk": "Walk",
"SW5E.Name": "Character Name", "SW5E.Name": "Character Name",
"SW5E.NewDay": "Is New Day?",
"SW5E.NewDayHint": "Recover limited use abilities which recharge \"per day\"?",
"SW5E.NoCharges": "No Charges", "SW5E.NoCharges": "No Charges",
"SW5E.None": "None", "SW5E.None": "None",
"SW5E.NoPowerLevels": "This character has no powercaster levels, but you may add powers manually.", "SW5E.NoPowerLevels": "This character has no powercaster levels, but you may add powers manually.",
@ -847,12 +799,11 @@
"SW5E.PowerCreate": "Create Power", "SW5E.PowerCreate": "Create Power",
"SW5E.PowerDC": "Power DC", "SW5E.PowerDC": "Power DC",
"SW5E.PowerDetails": "Power Details", "SW5E.PowerDetails": "Power Details",
"SW5E.PowerDiceRecovery": "Power Dice Recovery",
"SW5E.PowerDie": "Power Die", "SW5E.PowerDie": "Power Die",
"SW5E.PowerDieAlloc": "Power Die Allocation",
"SW5E.PowerDiePl": "Power Dice", "SW5E.PowerDiePl": "Power Dice",
"SW5E.PowerDieAlloc": "Power Die Allocation",
"SW5E.PowerDiceRecovery": "Power Dice Recovery",
"SW5E.PowerEffects": "Power Effects", "SW5E.PowerEffects": "Power Effects",
"SW5E.PowerfulCritical": "Powerful Critical",
"SW5E.PowerLevel": "Power Level", "SW5E.PowerLevel": "Power Level",
"SW5E.PowerLevel0": "At-Will", "SW5E.PowerLevel0": "At-Will",
"SW5E.PowerLevel1": "1st Level", "SW5E.PowerLevel1": "1st Level",
@ -882,9 +833,9 @@
"SW5E.PowerProgression": "Power Progression", "SW5E.PowerProgression": "Power Progression",
"SW5E.PowerProgSct": "Scout", "SW5E.PowerProgSct": "Scout",
"SW5E.PowerProgSnt": "Sentinel", "SW5E.PowerProgSnt": "Sentinel",
"SW5E.PowerRouting": "Power Routing",
"SW5E.PowerSchool": "Power School", "SW5E.PowerSchool": "Power School",
"SW5E.PowersKnown": "Powers Known", "SW5E.PowersKnown": "Powers Known",
"SW5E.PowerRouting": "Power Routing",
"SW5E.PowerTarget": "Power Target", "SW5E.PowerTarget": "Power Target",
"SW5E.PowerUnprepared": "Unprepared", "SW5E.PowerUnprepared": "Unprepared",
"SW5E.PowerUsage": "Power Usage", "SW5E.PowerUsage": "Power Usage",
@ -905,18 +856,17 @@
"SW5E.RegenerationRateCoefficient": "Regeneration Rate Coefficient", "SW5E.RegenerationRateCoefficient": "Regeneration Rate Coefficient",
"SW5E.RequiredMaterials": "Required Materials", "SW5E.RequiredMaterials": "Required Materials",
"SW5E.Requirements": "Requirements", "SW5E.Requirements": "Requirements",
"SW5E.ResourcePrimary": "Resource 1",
"SW5E.ResourcesAndTraits": "Resources & Traits", "SW5E.ResourcesAndTraits": "Resources & Traits",
"SW5E.ResourcePrimary": "Resource 1",
"SW5E.ResourceSecondary": "Resource 2", "SW5E.ResourceSecondary": "Resource 2",
"SW5E.ResourceTertiary": "Resource 3", "SW5E.ResourceTertiary": "Resource 3",
"SW5E.Rest": "Rest",
"SW5E.RestL": "L. Rest", "SW5E.RestL": "L. Rest",
"SW5E.RestS": "S. Rest", "SW5E.RestS": "S. Rest",
"SW5E.Ritual": "Ritual", "SW5E.Ritual": "Ritual",
"SW5E.Role": "Role", "SW5E.Role": "Role",
"SW5E.RolePl": "Roles", "SW5E.RolePl": "Roles",
"SW5E.Roll": "Roll", "SW5E.Roll": "Roll",
"SW5E.RollExample": "e.g. 1d4", "SW5E.RollExample": "e.g. +1d4",
"SW5E.RollMode": "Roll Mode", "SW5E.RollMode": "Roll Mode",
"SW5E.RollSituationalBonus": "Situational Bonus?", "SW5E.RollSituationalBonus": "Situational Bonus?",
"SW5E.Save": "Save", "SW5E.Save": "Save",
@ -929,7 +879,6 @@
"SW5E.SchoolLgt": "Light", "SW5E.SchoolLgt": "Light",
"SW5E.SchoolTec": "Tech", "SW5E.SchoolTec": "Tech",
"SW5E.SchoolUni": "Universal", "SW5E.SchoolUni": "Universal",
"SW5E.SelectItemsPromptTitle": "Select Items",
"SW5E.SenseBlindsight": "Blindsight", "SW5E.SenseBlindsight": "Blindsight",
"SW5E.SenseBS": "Blindsight", "SW5E.SenseBS": "Blindsight",
"SW5E.SenseDarkvision": "Darkvision", "SW5E.SenseDarkvision": "Darkvision",
@ -991,7 +940,6 @@
"SW5E.SkillSte": "Stealth", "SW5E.SkillSte": "Stealth",
"SW5E.SkillSur": "Survival", "SW5E.SkillSur": "Survival",
"SW5E.SkillTec": "Technology", "SW5E.SkillTec": "Technology",
"SW5E.Skip": "Skip",
"SW5E.Slots": "Slots", "SW5E.Slots": "Slots",
"SW5E.Source": "Source", "SW5E.Source": "Source",
"SW5E.Special": "Special", "SW5E.Special": "Special",
@ -1002,8 +950,8 @@
"SW5E.Speed": "Speed", "SW5E.Speed": "Speed",
"SW5E.SpeedSpecial": "Special Movement", "SW5E.SpeedSpecial": "Special Movement",
"SW5E.StarshipAmbassador": "Ambassador", "SW5E.StarshipAmbassador": "Ambassador",
"SW5E.StarshipArmorandShieldProps": "Starship Armor & Shield Properties",
"SW5E.StarshipArmorandShields": "Starship Armor and Shields", "SW5E.StarshipArmorandShields": "Starship Armor and Shields",
"SW5E.StarshipArmorandShieldProps": "Starship Armor & Shield Properties",
"SW5E.StarshipBattleship": "Battleship", "SW5E.StarshipBattleship": "Battleship",
"SW5E.StarshipBlockadeShip": "Blockade Ship", "SW5E.StarshipBlockadeShip": "Blockade Ship",
"SW5E.StarshipBomber": "Bomber", "SW5E.StarshipBomber": "Bomber",
@ -1128,7 +1076,6 @@
"SW5E.TraitToolProf": "Tool Proficiencies", "SW5E.TraitToolProf": "Tool Proficiencies",
"SW5E.TraitWeaponProf": "Weapon Proficiencies", "SW5E.TraitWeaponProf": "Weapon Proficiencies",
"SW5E.Type": "Type", "SW5E.Type": "Type",
"SW5E.Uncrewed": "Uncrewed",
"SW5E.Unequipped": "Unequipped", "SW5E.Unequipped": "Unequipped",
"SW5E.UniversalPowerDC": "Universal Power DC", "SW5E.UniversalPowerDC": "Universal Power DC",
"SW5E.Unlimited": "Unlimited", "SW5E.Unlimited": "Unlimited",
@ -1153,27 +1100,16 @@
"SW5E.VersatileDamage": "Versatile Damage", "SW5E.VersatileDamage": "Versatile Damage",
"SW5E.VsDC": "vs DC.", "SW5E.VsDC": "vs DC.",
"SW5E.WeaponAmmo": "Ammunition", "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.WeaponImprov": "Improvised",
"SW5E.WeaponImprovisedProficiency": "Improvised Weapons",
"SW5E.WeaponLightFoilProficiency": "Lightfoil",
"SW5E.WeaponLightRingProficiency": "Light Ring",
"SW5E.WeaponMartialB": "Martial Blaster", "SW5E.WeaponMartialB": "Martial Blaster",
"SW5E.WeaponMartialBlasterProficiency": "Martial Blasters",
"SW5E.WeaponMartialLightweaponProficiency": "Martial Lightweapons",
"SW5E.WeaponMartialLW": "Martial Lightweapon", "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.WeaponMartialProficiency": "Martial Weapons",
"SW5E.WeaponMartialVibroweaponProficiency": "Martial Vibroweapons",
"SW5E.WeaponMartialVW": "Martial Vibroweapon", "SW5E.WeaponMartialVW": "Martial Vibroweapon",
"SW5E.WeaponNatural": "Natural", "SW5E.WeaponNatural": "Natural",
"SW5E.WeaponNaturalProficiency": "Natural Weapons",
"SW5E.WeaponPrimarySW": "Primary (Starship)",
"SW5E.WeaponPropertiesAmm": "Ammunition", "SW5E.WeaponPropertiesAmm": "Ammunition",
"SW5E.WeaponPropertiesAut": "Auto", "SW5E.WeaponPropertiesAut": "Auto",
"SW5E.WeaponPropertiesBur": "Burst", "SW5E.WeaponPropertiesBur": "Burst",
@ -1191,14 +1127,14 @@
"SW5E.WeaponPropertiesFix": "Fixed", "SW5E.WeaponPropertiesFix": "Fixed",
"SW5E.WeaponPropertiesFoc": "Focus", "SW5E.WeaponPropertiesFoc": "Focus",
"SW5E.WeaponPropertiesHid": "Hidden", "SW5E.WeaponPropertiesHid": "Hidden",
"SW5E.WeaponPropertiesHom": "Homing",
"SW5E.WeaponPropertiesHvy": "Heavy", "SW5E.WeaponPropertiesHvy": "Heavy",
"SW5E.WeaponPropertiesHom": "Homing",
"SW5E.WeaponPropertiesIon": "Ionizing", "SW5E.WeaponPropertiesIon": "Ionizing",
"SW5E.WeaponPropertiesKen": "Keen", "SW5E.WeaponPropertiesKen": "Keen",
"SW5E.WeaponPropertiesLgt": "Light", "SW5E.WeaponPropertiesLgt": "Light",
"SW5E.WeaponPropertiesLum": "Luminous", "SW5E.WeaponPropertiesLum": "Luminous",
"SW5E.WeaponPropertiesMig": "Mighty",
"SW5E.WeaponPropertiesMlt": "Melt", "SW5E.WeaponPropertiesMlt": "Melt",
"SW5E.WeaponPropertiesMig": "Mighty",
"SW5E.WeaponPropertiesOvr": "Overheat", "SW5E.WeaponPropertiesOvr": "Overheat",
"SW5E.WeaponPropertiesPic": "Piercing", "SW5E.WeaponPropertiesPic": "Piercing",
"SW5E.WeaponPropertiesPow": "Power", "SW5E.WeaponPropertiesPow": "Power",
@ -1217,21 +1153,32 @@
"SW5E.WeaponPropertiesVer": "Versatile", "SW5E.WeaponPropertiesVer": "Versatile",
"SW5E.WeaponPropertiesVic": "Vicious", "SW5E.WeaponPropertiesVic": "Vicious",
"SW5E.WeaponPropertiesZon": "Zone", "SW5E.WeaponPropertiesZon": "Zone",
"SW5E.WeaponQuaternarySW": "Quaternary (Starship)",
"SW5E.WeaponSaberWhipProficiency": "Saberwhip",
"SW5E.WeaponSecondarySW": "Secondary (Starship)",
"SW5E.WeaponSiege": "Siege", "SW5E.WeaponSiege": "Siege",
"SW5E.WeaponSimpleB": "Simple Blaster", "SW5E.WeaponSimpleB": "Simple Blaster",
"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.WeaponSimpleBlasterProficiency": "Simple Blasters",
"SW5E.WeaponSimpleLightweaponProficiency": "Simple Lightweapons", "SW5E.WeaponSimpleLightweaponProficiency": "Simple Lightweapons",
"SW5E.WeaponSimpleLW": "Simple Lightweapon",
"SW5E.WeaponSimpleProficiency": "Simple Weapons",
"SW5E.WeaponSimpleVibroweaponProficiency": "Simple Vibroweapons", "SW5E.WeaponSimpleVibroweaponProficiency": "Simple Vibroweapons",
"SW5E.WeaponSimpleVW": "Simple Vibroweapon", "SW5E.WeaponSimpleVW": "Simple Vibroweapon",
"SW5E.WeaponSizeAbb": "Size",
"SW5E.WeaponTechbladeProficiency": "Techblades", "SW5E.WeaponTechbladeProficiency": "Techblades",
"SW5E.WeaponTertiarySW": "Tertiary (Starship)",
"SW5E.WeaponVibrorapierProficiency": "Vibrorapier", "SW5E.WeaponVibrorapierProficiency": "Vibrorapier",
"SW5E.WeaponVibrowhipProficiency": "Vibrowhip", "SW5E.WeaponVibrowhipProficiency": "Vibrowhip",
"SW5E.WeaponSizeAbb": "Size",
"SW5E.Weight": "Weight" "SW5E.Weight": "Weight"
} }

File diff suppressed because it is too large Load diff

View file

@ -71,7 +71,7 @@
} }
// Movement Configuration // Movement Configuration
.movement, .hit-dice { .movement {
h4.attribute-name { h4.attribute-name {
position: relative; position: relative;
} }
@ -655,15 +655,6 @@
// Empty powerbook controls // Empty powerbook controls
.powerbook-empty .item-controls { flex: 1; } .powerbook-empty .item-controls { flex: 1; }
/* ----------------------------------------- */
/* Features Tab */
/* ----------------------------------------- */
// Original class icon
.features i.original-class {
color: #4b4a44
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* TinyMCE */ /* TinyMCE */
/* ----------------------------------------- */ /* ----------------------------------------- */

View file

@ -10,7 +10,9 @@
.sw5e { .sw5e {
.window-content { .window-content {
background: @sheetBackground;
font-size: 13px; font-size: 13px;
color: @colorDark;
} }
/* ----------------------------------------- */ /* ----------------------------------------- */
@ -42,8 +44,6 @@
select:disabled, select:disabled,
textarea:disabled { textarea:disabled {
color: @colorOlive; color: @colorOlive;
border: 1px solid transparent !important;
outline: none !important;
&:hover, &:hover,
&:focus { &:focus {
box-shadow: none !important; box-shadow: none !important;
@ -58,6 +58,28 @@
border: @borderGroove; border: @borderGroove;
} }
// Checkbox Labels
// TODO: THIS CAN BE MOSTLY REMOVED NOW THAT IT IS IN CORE, see core forms.less
label.checkbox {
flex: auto;
padding: 0;
margin: 0;
height: 22px;
line-height: 22px;
font-size: 11px;
> input[type="checkbox"] {
width: 16px;
height: 16px;
margin: 0 2px 0 0;
position: relative;
top: 4px;
}
&.right > input[type="checkbox"] {
margin: 0 0 0 2px;
}
}
/* Form Groups */ /* Form Groups */
.form-group { .form-group {
label { label {
@ -76,12 +98,11 @@
// Stacked Groups // Stacked Groups
.form-group.stacked { .form-group.stacked {
> label { label {
flex: 0 0 100%; flex: 0 0 100%;
margin: 0; margin: 0;
} }
label.checkbox, label.checkbox {
label.radio {
flex: auto; flex: auto;
text-align: left; text-align: left;
} }
@ -110,34 +131,6 @@
} }
/* ----------------------------------------- */
/* Hit Dice Config Sheet Specifically */
/* ----------------------------------------- */
.sw5e.hd-config {
.form-group {
button.increment, button.decrement {
flex: 0 0 1rem;
line-height: 1rem;
}
button.decrement {
margin-right: 0;
}
span.sep {
margin: 0;
}
input {
flex: 0 0 2rem;
text-align: center;
margin-left: 2px;
margin-right: 2px;
}
}
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Entity Sheets Specifically */ /* Entity Sheets Specifically */
/* ----------------------------------------- */ /* ----------------------------------------- */
@ -482,7 +475,7 @@
/* Trait Selector /* Trait Selector
/* ----------------------------------------- */ /* ----------------------------------------- */
.trait-selector { #trait-selector {
.trait-list { .trait-list {
list-style: none; list-style: none;
margin: 0; margin: 0;
@ -495,59 +488,6 @@
} }
} }
/* ----------------------------------------- */
/* Actor Type Config Sheet Specifically */
/* ----------------------------------------- */
.actor-type {
.trait-list {
display: flex;
flex-wrap: wrap;
li {
flex-basis: 50%;
flex-grow: 1;
}
li.form-group {
flex-basis: 100%;
}
}
label.radio {
display: flex;
flex: auto;
font-size: 12px;
line-height: 20px;
font-weight: normal;
> input[type="radio"] {
margin: 0 5px 0 0;
}
}
li.custom-type input[type="radio"] {
display: none;
}
}
/* ----------------------------------------- */
/* Add Feature Prompt Specifically */
/* ----------------------------------------- */
.sw5e.select-items-prompt {
.dialog-content {
margin-bottom: 1em;
}
.items-list {
margin-top: 0.5em;
}
.item-name > label, .item-image, input {
cursor: pointer;
}
.item-name > label {
align-items: center;
}
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* HUD /* HUD
/* ----------------------------------------- */ /* ----------------------------------------- */

View file

@ -89,7 +89,7 @@
// Custom Resources // Custom Resources
.resource .attribute-value { .resource .attribute-value {
> input { input {
flex: 0 0 25%; flex: 0 0 25%;
} }
label.recharge { label.recharge {
@ -99,7 +99,6 @@
font-size: 11px; font-size: 11px;
text-align: center; text-align: center;
color: @colorOlive; color: @colorOlive;
align-items: center;
input[type="checkbox"] { input[type="checkbox"] {
height: 14px; height: 14px;
width: 14px; width: 14px;

View file

@ -106,17 +106,17 @@
&:nth-child(even) { &:nth-child(even) {
width: 150px; width: 150px;
margin: 0.5em 0.5em; margin: 0.5em 0.5em;
padding: 0 10px 0 10px; padding: 0px 10px 0px 10px;
text-align: left; text-align: left;
} }
} }
thead { thead {
border-bottom: 0; border-bottom: 0px;
} }
th { th {
color: #000000; color: #000000;
text-shadow: none; text-shadow: none;
border-bottom: 0; border-bottom: 0px;
background-color: #bdc8cc; background-color: #bdc8cc;
text-transform: none; text-transform: none;
font-weight: bold; font-weight: bold;
@ -129,7 +129,7 @@
&:nth-child(even) { &:nth-child(even) {
width: 150px; width: 150px;
margin: 0.5em 0.5em; margin: 0.5em 0.5em;
padding: 0 10px 0 10px; padding: 0px 10px 0px 10px;
text-align: left; text-align: left;
} }
} }
@ -137,7 +137,7 @@
.medtable { .medtable {
table { table {
width: 500px; width: 500px;
border: 0; border: 0px;
margin: 0.5em 0.5em; margin: 0.5em 0.5em;
} }
td { td {
@ -149,17 +149,17 @@
&:nth-child(even) { &:nth-child(even) {
width: 450px; width: 450px;
margin: 0.5em 0.5em; margin: 0.5em 0.5em;
padding: 0 10px 0 0; padding: 0px 10px 0px 0px;
text-align: left; text-align: left;
} }
} }
thead { thead {
border-bottom: 0; border-bottom: 0px;
} }
th { th {
color: #000000; color: #000000;
text-shadow: none; text-shadow: none;
border-bottom: 0; border-bottom: 0px;
background-color: #bdc8cc; background-color: #bdc8cc;
text-transform: none; text-transform: none;
font-weight: bold; font-weight: bold;
@ -174,8 +174,8 @@
} }
.classtable { .classtable {
blockquote { blockquote {
border-left: 0; border-left: 0px;
border-right: 0; border-right: 0px;
background-color: #bdc8cc; background-color: #bdc8cc;
width: 600px; width: 600px;
h3 { h3 {
@ -189,8 +189,8 @@
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);
border-left: 0; border-left: 0px;
border-right: 0; border-right: 0px;
border-top: 0; border-top: 0;
border-bottom: 0; border-bottom: 0;
margin: 0.5em 0; margin: 0.5em 0;
@ -200,7 +200,7 @@
thead { thead {
color: #000000; color: #000000;
text-shadow: none; text-shadow: none;
border-bottom: 0; border-bottom: 0px;
background-color: #bdc8cc; background-color: #bdc8cc;
text-transform: none; text-transform: none;
font-style: normal; font-style: normal;
@ -209,7 +209,7 @@
th { th {
color: #000000; color: #000000;
text-shadow: none; text-shadow: none;
border-bottom: 0; border-bottom: 0px;
background-color: #bdc8cc; background-color: #bdc8cc;
text-transform: none; text-transform: none;
font-style: normal; font-style: normal;
@ -246,7 +246,7 @@
width: 100%; width: 100%;
line-height: 18px; line-height: 18px;
margin-bottom: 15px; margin-bottom: 15px;
border: 0; border: 0 0 0 0;
border-bottom: none; border-bottom: none;
overflow-x: auto; overflow-x: auto;
tbody { tbody {

View file

@ -30,28 +30,5 @@
.summary { .summary {
font-size: 18px; font-size: 18px;
li.creature-type {
display: flex;
justify-content: space-between;
width: 1em;
padding: 0 3px;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-button {
display: none;
font-size: 12px;
font-weight: normal;
line-height: 2em;
}
&:hover .config-button {
display: block;
}
}
} }
} }

View file

@ -140,7 +140,6 @@
height: auto; height: auto;
.russoOne(17px); .russoOne(17px);
line-height: 24px; line-height: 24px;
width: 100%;
} }
.proficiency { .proficiency {
@ -185,7 +184,7 @@
display: inline-block; display: inline-block;
text-align: right; text-align: right;
padding: 0 3px; padding: 0px 3px;
&:last-child { &:last-child {
text-align: left; text-align: left;
@ -780,7 +779,7 @@
display: block; display: block;
width: 100%; width: 100%;
text-align: right; text-align: right;
padding: 0 3px; padding: 0px 3px;
&:last-child { &:last-child {
text-align: left; text-align: left;
} }
@ -956,7 +955,7 @@
display: block; display: block;
width: 100%; width: 100%;
text-align: right; text-align: right;
padding: 0 3px; padding: 0px 3px;
&:last-child { &:last-child {
text-align: left; text-align: left;
} }
@ -1054,35 +1053,10 @@
h1.character-name { h1.character-name {
align-self: auto; align-self: auto;
} }
.npc-size, .creature-type { .npc-size {
.russoOne(18px); .russoOne(18px);
line-height: 28px; line-height: 28px;
} }
div.creature-type {
display: flex;
justify-content: space-between;
padding: 1px 4px;
border: 1px solid transparent;
overflow-x: auto;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-button {
display: none;
font-size: 12px;
font-weight: normal;
line-height: 2em;
}
&:hover .config-button {
display: block;
}
}
.attributes { .attributes {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
footer { footer {

View file

@ -408,9 +408,6 @@
&.npc { &.npc {
.swalt-sheet { .swalt-sheet {
header { header {
div.creature-type:hover {
border-color: @inputBorderFocus;
}
.experience { .experience {
color: @actorProficiencyTextColor; color: @actorProficiencyTextColor;
} }

View file

@ -1,4 +1,4 @@
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea, .roundTransition { input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea {
border-radius: 4px; border-radius: 4px;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {

View file

@ -166,12 +166,6 @@
.token-name { .token-name {
text-shadow: none; text-shadow: none;
} }
.ce-image-wrapper {
.token-image {
width: auto;
height: auto;
}
}
h4 { h4 {
color: @colorBlack; color: @colorBlack;
} }
@ -231,7 +225,7 @@
padding-bottom: 4px; padding-bottom: 4px;
.folder { .folder {
& > .folder-header { & > .folder-header {
line-height: initial; line-height: default;
padding: 0 0 0 8px; padding: 0 0 0 8px;
position: relative; position: relative;
border: none; border: none;

View file

@ -302,7 +302,7 @@
} }
.folder { .folder {
& > .folder-header { & > .folder-header {
line-height: initial; line-height: default;
padding: 0 0 0 8px; padding: 0 0 0 8px;
position: relative; position: relative;
border: none; border: none;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,9 @@
import Item5e from "../../../item/entity.js"; import Item5e from "../../../item/entity.js";
import TraitSelector from "../../../apps/trait-selector.js"; import TraitSelector from "../../../apps/trait-selector.js";
import ActorSheetFlags from "../../../apps/actor-flags.js"; import ActorSheetFlags from "../../../apps/actor-flags.js";
import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js"; import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-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"; import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/** /**
@ -48,68 +46,53 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* A set of item types that should be prevented from being dropped on this type of actor sheet.
* @type {Set<string>}
*/
static unsupportedItemTypes = new Set();
/* -------------------------------------------- */
/** @override */ /** @override */
get template() { get template() {
if (!game.user.isGM && this.actor.limited) if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`; return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
getData(options) { getData() {
// Basic data // Basic data
let isOwner = this.actor.isOwner; let isOwner = this.entity.owner;
const data = { const data = {
owner: isOwner, owner: isOwner,
limited: this.actor.limited, limited: this.entity.limited,
options: this.options, options: this.options,
editable: this.isEditable, editable: this.isEditable,
cssClass: isOwner ? "editable" : "locked", cssClass: isOwner ? "editable" : "locked",
isCharacter: this.actor.type === "character", isCharacter: this.entity.data.type === "character",
isNPC: this.actor.type === "npc", isNPC: this.entity.data.type === "npc",
isStarship: this.actor.type === "starship", isStarship: this.entity.data.type === "starship",
isVehicle: this.actor.type === "vehicle", isVehicle: this.entity.data.type === 'vehicle',
config: CONFIG.SW5E, config: CONFIG.SW5E,
rollData: this.actor.getRollData.bind(this.actor)
}; };
// The Actor's data // The Actor and its Items
const actorData = this.actor.data.toObject(false); data.actor = duplicate(this.actor.data);
data.actor = actorData; data.items = this.actor.items.map(i => {
data.data = actorData.data; i.data.labels = i.labels;
return i.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)); data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
data.data = data.actor.data;
// Labels and filters
data.labels = this.actor.labels || {}; data.labels = this.actor.labels || {};
data.filters = this._filters; data.filters = this._filters;
// Ability Scores // Ability Scores
for (let [a, abl] of Object.entries(actorData.data.abilities)) { for ( let [a, abl] of Object.entries(data.actor.data.abilities)) {
abl.icon = this._getProficiencyIcon(abl.proficient); abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient]; abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a]; abl.label = CONFIG.SW5E.abilities[a];
} }
// Skills // Skills
if (actorData.data.skills) { if (data.actor.data.skills) {
for (let [s, skl] of Object.entries(actorData.data.skills)) { for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability]; skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.icon = this._getProficiencyIcon(skl.value); skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value]; skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
@ -122,22 +105,22 @@ export default class ActorSheet5e extends ActorSheet {
} }
// Movement speeds // Movement speeds
data.movement = this._getMovementSpeed(actorData); data.movement = this._getMovementSpeed(data.actor);
// Senses // Senses
data.senses = this._getSenses(actorData); data.senses = this._getSenses(data.actor);
// Update traits // Update traits
this._prepareTraits(actorData.data.traits); this._prepareTraits(data.actor.data.traits);
// Prepare owned items // Prepare owned items
this._prepareItems(data); this._prepareItems(data);
// Prepare active effects // Prepare active effects
data.effects = prepareActiveEffectCategories(this.actor.effects); data.effects = prepareActiveEffectCategories(this.entity.effects);
// Return data to the sheet // Return data to the sheet
return data; return data
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -156,35 +139,31 @@ export default class ActorSheet5e extends ActorSheet {
let speeds = [ let speeds = [
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`], [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`], [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.fly,
`${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` +
(movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")
],
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`] [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
]; ]
if ( largestPrimary ) { if ( largestPrimary ) {
speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]); speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
} }
// Filter and sort speeds on their values // Filter and sort speeds on their values
speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]); speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
// Case 1: Largest as primary // Case 1: Largest as primary
if ( largestPrimary ) { if ( largestPrimary ) {
let primary = speeds.shift(); let primary = speeds.shift();
return { return {
primary: `${primary ? primary[1] : "0"} ${movement.units}`, primary: `${primary ? primary[1] : "0"} ${movement.units}`,
special: speeds.map((s) => s[1]).join(", ") special: speeds.map(s => s[1]).join(", ")
}; }
} }
// Case 2: Walk as primary // Case 2: Walk as primary
else { else {
return { return {
primary: `${movement.walk || 0} ${movement.units}`, primary: `${movement.walk || 0} ${movement.units}`,
special: speeds.length ? speeds.map((s) => s[1]).join(", ") : "" special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
}; }
} }
} }
@ -194,7 +173,7 @@ export default class ActorSheet5e extends ActorSheet {
const senses = actorData.data.attributes.senses || {}; const senses = actorData.data.attributes.senses || {};
const tags = {}; const tags = {};
for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) { for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
const v = senses[k] ?? 0; const v = senses[k] ?? 0
if ( v === 0 ) continue; if ( v === 0 ) continue;
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`; tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
} }
@ -211,14 +190,14 @@ export default class ActorSheet5e extends ActorSheet {
*/ */
_prepareTraits(traits) { _prepareTraits(traits) {
const map = { const map = {
dr: CONFIG.SW5E.damageResistanceTypes, "dr": CONFIG.SW5E.damageResistanceTypes,
di: CONFIG.SW5E.damageResistanceTypes, "di": CONFIG.SW5E.damageResistanceTypes,
dv: CONFIG.SW5E.damageResistanceTypes, "dv": CONFIG.SW5E.damageResistanceTypes,
ci: CONFIG.SW5E.conditionTypes, "ci": CONFIG.SW5E.conditionTypes,
languages: CONFIG.SW5E.languages, "languages": CONFIG.SW5E.languages,
armorProf: CONFIG.SW5E.armorProficiencies, "armorProf": CONFIG.SW5E.armorProficiencies,
weaponProf: CONFIG.SW5E.weaponProficiencies, "weaponProf": CONFIG.SW5E.weaponProficiencies,
toolProf: CONFIG.SW5E.toolProficiencies "toolProf": CONFIG.SW5E.toolProficiencies
}; };
for ( let [t, choices] of Object.entries(map) ) { for ( let [t, choices] of Object.entries(map) ) {
const trait = traits[t]; const trait = traits[t];
@ -234,7 +213,7 @@ export default class ActorSheet5e extends ActorSheet {
// Add custom entry // Add custom entry
if ( trait.custom ) { if ( trait.custom ) {
trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim())); trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim());
} }
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
} }
@ -246,18 +225,17 @@ export default class ActorSheet5e extends ActorSheet {
* Insert a power into the powerbook object when rendering the character sheet * Insert a power into the powerbook object when rendering the character sheet
* @param {Object} data The Actor data being prepared * @param {Object} data The Actor data being prepared
* @param {Array} powers The power data being prepared * @param {Array} powers The power data being prepared
* @param {string} school The school of the powerbook being prepared
* @private * @private
*/ */
_preparePowerbook(data, powers, school) { _preparePowerbook(data, powers, school) {
const owner = this.actor.isOwner; const owner = this.actor.owner;
const levels = data.data.powers; const levels = data.data.powers;
const powerbook = {}; const powerbook = {};
// Define some mappings // Define some mappings
const sections = { const sections = {
atwill: -20, "atwill": -20,
innate: -10 "innate": -10,
}; };
// Label power slot uses headers // Label power slot uses headers
@ -274,17 +252,12 @@ export default class ActorSheet5e extends ActorSheet {
label: label, label: label,
usesSlots: i > 0, usesSlots: i > 0,
canCreate: owner, canCreate: owner,
canPrepare: data.actor.type === "character" && i >= 1, canPrepare: (data.actor.type === "character") && (i >= 1),
powers: [], powers: [],
uses: useLabels[i] || value || 0, uses: useLabels[i] || value || 0,
slots: useLabels[i] || max || 0, slots: useLabels[i] || max || 0,
override: override || 0, override: override || 0,
dataset: { dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode, "school": school},
"type": "power",
"level": prepMode in sections ? 1 : i,
"preparation.mode": prepMode,
"school": school
},
prop: sl prop: sl
}; };
}; };
@ -293,7 +266,7 @@ export default class ActorSheet5e extends ActorSheet {
const maxLevel = Array.fromRange(10).reduce((max, i) => { const maxLevel = Array.fromRange(10).reduce((max, i) => {
if ( i === 0 ) return max; if ( i === 0 ) return max;
const level = levels[`power${i}`]; const level = levels[`power${i}`];
if ((level.max || level.override) && i > max) max = i; if ( (level.max || level.override ) && ( i > max ) ) max = i;
return max; return max;
}, 0); }, 0);
@ -307,7 +280,7 @@ export default class ActorSheet5e extends ActorSheet {
} }
// Iterate over every power item, adding powers to the powerbook by section // Iterate over every power item, adding powers to the powerbook by section
powers.forEach((power) => { powers.forEach(power => {
const mode = power.data.preparation.mode || "prepared"; const mode = power.data.preparation.mode || "prepared";
let s = power.data.level || 0; let s = power.data.level || 0;
const sl = `power${s}`; const sl = `power${s}`;
@ -350,13 +323,13 @@ export default class ActorSheet5e extends ActorSheet {
* @private * @private
*/ */
_filterItems(items, filters) { _filterItems(items, filters) {
return items.filter((item) => { return items.filter(item => {
const data = item.data; const data = item.data;
// Action usage // Action usage
for ( let f of ["action", "bonus", "reaction"] ) { for ( let f of ["action", "bonus", "reaction"] ) {
if ( filters.has(f) ) { if ( filters.has(f) ) {
if (data.activation && data.activation.type !== f) return false; if ((data.activation && (data.activation.type !== f))) return false;
} }
} }
@ -402,64 +375,67 @@ export default class ActorSheet5e extends ActorSheet {
/* Event Listeners and Handlers /* Event Listeners and Handlers
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /**
* Activate event listeners using the prepared sheet HTML
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/
activateListeners(html) { activateListeners(html) {
// Activate Item Filters // Activate Item Filters
const filterLists = html.find(".filter-list"); const filterLists = html.find(".filter-list");
filterLists.each(this._initializeFilterItemList.bind(this)); filterLists.each(this._initializeFilterItemList.bind(this));
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this)); filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
// Item summaries // Item summaries
html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event)); 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 // Editable Only Listeners
if ( this.isEditable ) { if ( this.isEditable ) {
// Input focus and update // Input focus and update
const inputs = html.find("input"); const inputs = html.find("input");
inputs.focus((ev) => ev.currentTarget.select()); inputs.focus(ev => ev.currentTarget.select());
inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)); inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Ability Proficiency // Ability Proficiency
html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this)); html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
// Toggle Skill Proficiency // Toggle Skill Proficiency
html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this)); html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this));
// Trait Selector // Trait Selector
html.find(".trait-selector").click(this._onTraitSelector.bind(this)); html.find('.trait-selector').click(this._onTraitSelector.bind(this));
// Configure Special Flags // Configure Special Flags
html.find(".config-button").click(this._onConfigMenu.bind(this)); html.find('.config-button').click(this._onConfigMenu.bind(this));
// Owned Item management // Owned Item management
html.find(".item-create").click(this._onItemCreate.bind(this)); html.find('.item-create').click(this._onItemCreate.bind(this));
html.find(".item-delete").click(this._onItemDelete.bind(this)); html.find('.item-edit').click(this._onItemEdit.bind(this));
html.find(".item-collapse").click(this._onItemCollapse.bind(this)); html.find('.item-delete').click(this._onItemDelete.bind(this));
html.find(".item-uses input") html.find('.item-collapse').click(this._onItemCollapse.bind(this));
.click((ev) => ev.target.select()) html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
.change(this._onUsesChange.bind(this)); html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this)); html.find('.increment-class-level').click(this._onIncrementClassLevel.bind(this));
html.find(".increment-class-level").click(this._onIncrementClassLevel.bind(this)); html.find('.decrement-class-level').click(this._onDecrementClassLevel.bind(this));
html.find(".decrement-class-level").click(this._onDecrementClassLevel.bind(this));
// Active Effect management // Active Effect management
html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor)); html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.entity));
} }
// Owner Only Listeners // Owner Only Listeners
if (this.actor.isOwner) { if ( this.actor.owner ) {
// Ability Checks // Ability Checks
html.find(".ability-name").click(this._onRollAbilityTest.bind(this)); html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
// Roll Skill Checks // Roll Skill Checks
html.find(".skill-name").click(this._onRollSkillCheck.bind(this)); html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
// Item Rolling // Item Rolling
html.find(".item .item-image").click((event) => this._onItemRoll(event)); html.find('.item .item-image').click(event => this._onItemRoll(event));
html.find(".item .item-recharge").click((event) => this._onItemRecharge(event)); html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
} }
// Otherwise remove rollable classes // Otherwise remove rollable classes
@ -515,25 +491,17 @@ export default class ActorSheet5e extends ActorSheet {
_onConfigMenu(event) { _onConfigMenu(event) {
event.preventDefault(); event.preventDefault();
const button = event.currentTarget; const button = event.currentTarget;
let app;
switch ( button.dataset.action ) { switch ( button.dataset.action ) {
case "hit-dice":
app = new ActorHitDiceConfig(this.object);
break;
case "movement": case "movement":
app = new ActorMovementConfig(this.object); new ActorMovementConfig(this.object).render(true);
break; break;
case "flags": case "flags":
app = new ActorSheetFlags(this.object); new ActorSheetFlags(this.object).render(true);
break; break;
case "senses": case "senses":
app = new ActorSensesConfig(this.object); new ActorSensesConfig(this.object).render(true);
break;
case "type":
new ActorTypeConfig(this.object).render(true);
break; break;
} }
app?.render(true);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -554,9 +522,9 @@ export default class ActorSheet5e extends ActorSheet {
// Toggle next level - forward on click, backwards on right // Toggle next level - forward on click, backwards on right
if ( event.type === "click" ) { if ( event.type === "click" ) {
field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]); field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
} else if ( event.type === "contextmenu" ) { } else if ( event.type === "contextmenu" ) {
field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]); field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
} }
// Update the field value and save the form // Update the field value and save the form
@ -567,13 +535,13 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */ /** @override */
async _onDropActor(event, data) { async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing")); const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
if ( !canPolymorph ) return false; if ( !canPolymorph ) return false;
// Get the target actor // Get the target actor
let sourceActor = null; let sourceActor = null;
if (data.pack) { if (data.pack) {
const pack = game.packs.find((p) => p.collection === data.pack); const pack = game.packs.find(p => p.collection === data.pack);
sourceActor = await pack.getEntity(data.id); sourceActor = await pack.getEntity(data.id);
} else { } else {
sourceActor = game.actors.get(data.id); sourceActor = game.actors.get(data.id);
@ -581,37 +549,35 @@ export default class ActorSheet5e extends ActorSheet {
if ( !sourceActor ) return; if ( !sourceActor ) return;
// Define a function to record polymorph settings for future use // Define a function to record polymorph settings for future use
const rememberOptions = (html) => { const rememberOptions = html => {
const options = {}; const options = {};
html.find("input").each((i, el) => { html.find('input').each((i, el) => {
options[el.name] = el.checked; options[el.name] = el.checked;
}); });
const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options); const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options);
game.settings.set("sw5e", "polymorphSettings", settings); game.settings.set('sw5e', 'polymorphSettings', settings);
return settings; return settings;
}; };
// Create and render the Dialog // Create and render the Dialog
return new Dialog( return new Dialog({
{ title: game.i18n.localize('SW5E.PolymorphPromptTitle'),
title: game.i18n.localize("SW5E.PolymorphPromptTitle"),
content: { content: {
options: game.settings.get("sw5e", "polymorphSettings"), options: game.settings.get('sw5e', 'polymorphSettings'),
i18n: SW5E.polymorphSettings, i18n: SW5E.polymorphSettings,
isToken: this.actor.isToken isToken: this.actor.isToken
}, },
default: "accept", default: 'accept',
buttons: { buttons: {
accept: { accept: {
icon: '<i class="fas fa-check"></i>', icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("SW5E.PolymorphAcceptSettings"), label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html)) callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
}, },
wildshape: { wildshape: {
icon: '<i class="fas fa-paw"></i>', icon: '<i class="fas fa-paw"></i>',
label: game.i18n.localize("SW5E.PolymorphWildShape"), label: game.i18n.localize('SW5E.PolymorphWildShape'),
callback: (html) => callback: html => this.actor.transformInto(sourceActor, {
this.actor.transformInto(sourceActor, {
keepBio: true, keepBio: true,
keepClass: true, keepClass: true,
keepMental: true, keepMental: true,
@ -622,66 +588,37 @@ export default class ActorSheet5e extends ActorSheet {
}, },
polymorph: { polymorph: {
icon: '<i class="fas fa-pastafarianism"></i>', icon: '<i class="fas fa-pastafarianism"></i>',
label: game.i18n.localize("SW5E.Polymorph"), label: game.i18n.localize('SW5E.Polymorph'),
callback: (html) => callback: html => this.actor.transformInto(sourceActor, {
this.actor.transformInto(sourceActor, {
transformTokens: rememberOptions(html).transformTokens transformTokens: rememberOptions(html).transformTokens
}) })
}, },
cancel: { cancel: {
icon: '<i class="fas fa-times"></i>', icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("Cancel") label: game.i18n.localize('Cancel')
} }
} }
}, }, {
{ classes: ['dialog', 'sw5e'],
classes: ["dialog", "sw5e"],
width: 600, width: 600,
template: "systems/sw5e/templates/apps/polymorph-prompt.html" template: 'systems/sw5e/templates/apps/polymorph-prompt.html'
} }).render(true);
).render(true);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _onDropItemCreate(itemData) { 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 // Create a Consumable power scroll on the Inventory tab
if (itemData.type === "power" && this._tabs[0].active === "inventory") { if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
const scroll = await Item5e.createScrollFromPower(itemData); const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data; itemData = scroll.data;
} }
if (itemData.data) {
// Ignore certain statuses // Ignore certain statuses
["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]); if ( itemData.data ) {
["attunement", "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 // Create the owned item as normal
@ -722,10 +659,10 @@ export default class ActorSheet5e extends ActorSheet {
async _onUsesChange(event) { async _onUsesChange(event) {
event.preventDefault(); event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId; const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId); const item = this.actor.getOwnedItem(itemId);
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max); const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
event.target.value = uses; event.target.value = uses;
return item.update({"data.uses.value": uses}); return item.update({ 'data.uses.value': uses });
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -737,7 +674,7 @@ export default class ActorSheet5e extends ActorSheet {
_onItemRoll(event) { _onItemRoll(event) {
event.preventDefault(); event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId; const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId); const item = this.actor.getOwnedItem(itemId);
return item.roll(); return item.roll();
} }
@ -751,9 +688,9 @@ export default class ActorSheet5e extends ActorSheet {
_onItemRecharge(event) { _onItemRecharge(event) {
event.preventDefault(); event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId; const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId); const item = this.actor.getOwnedItem(itemId);
return item.rollRecharge(); return item.rollRecharge();
} };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -764,8 +701,8 @@ export default class ActorSheet5e extends ActorSheet {
_onItemSummary(event) { _onItemSummary(event) {
event.preventDefault(); event.preventDefault();
let li = $(event.currentTarget).parents(".item"), let li = $(event.currentTarget).parents(".item"),
item = this.actor.items.get(li.data("item-id")), item = this.actor.getOwnedItem(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.isOwner}); chatData = item.getChatData({secrets: this.actor.owner});
// Toggle summary // Toggle summary
if ( li.hasClass("expanded") ) { if ( li.hasClass("expanded") ) {
@ -774,7 +711,7 @@ export default class ActorSheet5e extends ActorSheet {
} else { } else {
let div = $(`<div class="item-summary">${chatData.description.value}</div>`); let div = $(`<div class="item-summary">${chatData.description.value}</div>`);
let props = $(`<div class="item-properties"></div>`); let props = $(`<div class="item-properties"></div>`);
chatData.properties.forEach((p) => props.append(`<span class="tag">${p}</span>`)); chatData.properties.forEach(p => props.append(`<span class="tag">${p}</span>`));
div.append(props); div.append(props);
li.append(div.hide()); li.append(div.hide());
div.slideDown(200); div.slideDown(200);
@ -794,12 +731,12 @@ export default class ActorSheet5e extends ActorSheet {
const header = event.currentTarget; const header = event.currentTarget;
const type = header.dataset.type; const type = header.dataset.type;
const itemData = { const itemData = {
name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}), name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
type: type, type: type,
data: foundry.utils.deepClone(header.dataset) data: duplicate(header.dataset)
}; };
delete itemData.data["type"]; delete itemData.data["type"];
return this.actor.createEmbeddedDocuments("Item", [itemData]); return this.actor.createEmbeddedEntity("OwnedItem", itemData);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -812,8 +749,8 @@ export default class ActorSheet5e extends ActorSheet {
_onItemEdit(event) { _onItemEdit(event) {
event.preventDefault(); event.preventDefault();
const li = event.currentTarget.closest(".item"); const li = event.currentTarget.closest(".item");
const item = this.actor.items.get(li.dataset.itemId); const item = this.actor.getOwnedItem(li.dataset.itemId);
return item.sheet.render(true); item.sheet.render(true);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -826,8 +763,7 @@ export default class ActorSheet5e extends ActorSheet {
_onItemDelete(event) { _onItemDelete(event) {
event.preventDefault(); event.preventDefault();
const li = event.currentTarget.closest(".item"); const li = event.currentTarget.closest(".item");
const item = this.actor.items.get(li.dataset.itemId); this.actor.deleteOwnedItem(li.dataset.itemId);
if (item) return item.delete();
} }
/** /**
@ -860,19 +796,19 @@ export default class ActorSheet5e extends ActorSheet {
_onIncrementClassLevel(event) { _onIncrementClassLevel(event) {
event.preventDefault(); event.preventDefault();
const div = event.currentTarget.closest(".character"); const div = event.currentTarget.closest(".character")
const li = event.currentTarget.closest("li"); const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1]; const actorId = div.id.split("-")[1];
const itemId = li.dataset.itemId; const itemId = li.dataset.itemId;
const actor = game.actors.get(actorId); const actor = game.actors.get(actorId);
const item = actor.items.get(itemId); const item = actor.getOwnedItem(itemId);
let levels = item.data.data.levels; let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: levels + 1}}; const update = {_id: item._id, data: {levels: (levels + 1) }};
actor.updateEmbeddedDocuments("Item", [update]); actor.updateOwnedItem(update)
} }
/** /**
@ -884,19 +820,19 @@ export default class ActorSheet5e extends ActorSheet {
_onDecrementClassLevel(event) { _onDecrementClassLevel(event) {
event.preventDefault(); event.preventDefault();
const div = event.currentTarget.closest(".character"); const div = event.currentTarget.closest(".character")
const li = event.currentTarget.closest("li"); const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1]; const actorId = div.id.split("-")[1];
const itemId = li.dataset.itemId; const itemId = li.dataset.itemId;
const actor = game.actors.get(actorId); const actor = game.actors.get(actorId);
const item = actor.items.get(itemId); const item = actor.getOwnedItem(itemId);
let levels = item.data.data.levels; let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: levels - 1}}; const update = {_id: item._id, data: {levels: (levels - 1) }};
actor.updateEmbeddedDocuments("Item", [update]); actor.updateOwnedItem(update)
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -909,7 +845,7 @@ export default class ActorSheet5e extends ActorSheet {
_onRollAbilityTest(event) { _onRollAbilityTest(event) {
event.preventDefault(); event.preventDefault();
let ability = event.currentTarget.parentElement.dataset.ability; let ability = event.currentTarget.parentElement.dataset.ability;
return this.actor.rollAbility(ability, {event: event}); this.actor.rollAbility(ability, {event: event});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -922,7 +858,7 @@ export default class ActorSheet5e extends ActorSheet {
_onRollSkillCheck(event) { _onRollSkillCheck(event) {
event.preventDefault(); event.preventDefault();
const skill = event.currentTarget.parentElement.dataset.skill; const skill = event.currentTarget.parentElement.dataset.skill;
return this.actor.rollSkill(skill, {event: event}); this.actor.rollSkill(skill, {event: event});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -935,7 +871,7 @@ export default class ActorSheet5e extends ActorSheet {
_onToggleAbilityProficiency(event) { _onToggleAbilityProficiency(event) {
event.preventDefault(); event.preventDefault();
const field = event.currentTarget.previousElementSibling; const field = event.currentTarget.previousElementSibling;
return this.actor.update({[field.name]: 1 - parseInt(field.value)}); this.actor.update({[field.name]: 1 - parseInt(field.value)});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -952,7 +888,7 @@ export default class ActorSheet5e extends ActorSheet {
const filter = li.dataset.filter; const filter = li.dataset.filter;
if ( set.has(filter) ) set.delete(filter); if ( set.has(filter) ) set.delete(filter);
else set.add(filter); else set.add(filter);
return this.render(); this.render();
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -968,7 +904,7 @@ export default class ActorSheet5e extends ActorSheet {
const label = a.parentElement.querySelector("label"); const label = a.parentElement.querySelector("label");
const choices = CONFIG.SW5E[a.dataset.options]; const choices = CONFIG.SW5E[a.dataset.options];
const options = { name: a.dataset.target, title: label.innerText, choices }; const options = { name: a.dataset.target, title: label.innerText, choices };
return new TraitSelector(this.actor, options).render(true); new TraitSelector(this.actor, options).render(true)
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -976,14 +912,15 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */ /** @override */
_getHeaderButtons() { _getHeaderButtons() {
let buttons = super._getHeaderButtons(); let buttons = super._getHeaderButtons();
if (this.actor.isPolymorphed) {
// Add button to revert polymorph
if ( !this.actor.isPolymorphed || this.actor.isToken ) return buttons;
buttons.unshift({ buttons.unshift({
label: "SW5E.PolymorphRestoreTransformation", label: 'SW5E.PolymorphRestoreTransformation',
class: "restore-transformation", class: "restore-transformation",
icon: "fas fa-backward", icon: "fas fa-backward",
onclick: () => this.actor.revertOriginalForm() onclick: ev => this.actor.revertOriginalForm()
}); });
}
return buttons; return buttons;
} }
} }

View file

@ -7,6 +7,7 @@ import Actor5e from "../../entity.js";
* @type {ActorSheet5e} * @type {ActorSheet5e}
*/ */
export default class ActorSheet5eCharacterNew extends ActorSheet5e { export default class ActorSheet5eCharacterNew extends ActorSheet5e {
get template() { get template() {
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; 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"; return "systems/sw5e/templates/actors/newActor/character-sheet.html";
@ -16,18 +17,17 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
* @return {Object} * @return {Object}
*/ */
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["swalt", "sw5e", "sheet", "actor", "character"], classes: ["swalt", "sw5e", "sheet", "actor", "character"],
blockFavTab: true, blockFavTab: true,
subTabs: null, subTabs: null,
width: 800, width: 800,
tabs: [ tabs: [{
{
navSelector: ".root-tabs", navSelector: ".root-tabs",
contentSelector: ".sheet-body", contentSelector: ".sheet-body",
initial: "attributes" initial: "attributes"
} }],
]
}); });
} }
@ -56,12 +56,10 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// Experience Tracking // Experience Tracking
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", "); sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", ");
sheetData["multiclassLabels"] = this.actor.itemTypes.class sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => {
.map((c) => { return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ')
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" "); }).join(', ');
})
.join(", ");
// Return data for rendering // Return data for rendering
return sheetData; return sheetData;
@ -74,6 +72,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize items as inventory, powerbook, features, and classes // Categorize items as inventory, powerbook, features, and classes
const inventory = { const inventory = {
weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} }, weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} },
@ -85,27 +84,11 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
}; };
// Partition items by category // Partition items by category
let [ let [items, forcepowers, techpowers, feats, classes, deployments, deploymentfeatures, ventures, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
items,
forcepowers,
techpowers,
feats,
classes,
deployments,
deploymentfeatures,
ventures,
species,
archetypes,
classfeatures,
backgrounds,
fightingstyles,
fightingmasteries,
lightsaberforms
] = data.items.reduce(
(arr, item) => {
// Item details // Item details
item.img = item.img || CONST.DEFAULT_TOKEN; item.img = item.img || DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.attunement = { item.attunement = {
[CONFIG.SW5E.attunementTypes.REQUIRED]: { [CONFIG.SW5E.attunementTypes.REQUIRED]: {
icon: "fa-sun", icon: "fa-sun",
@ -120,19 +103,14 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
}[item.data.attunement]; }[item.data.attunement];
// Item usage // Item usage
item.hasUses = item.data.uses && item.data.uses.max > 0; item.hasUses = item.data.uses && (item.data.uses.max > 0);
item.isOnCooldown = item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
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.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
// Item toggle state // Item toggle state
this._prepareItemToggleState(item); this._prepareItemToggleState(item);
// Primary Class
if (item.type === "class")
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
// Classify items into types // Classify items into types
if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[1].push(item); 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 === "power" && ["tec"].includes(item.data.school) ) arr[2].push(item);
@ -150,9 +128,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
else if ( item.type === "lightsaberform" ) arr[14].push(item); else if ( item.type === "lightsaberform" ) arr[14].push(item);
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item); else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
return arr; return arr;
}, }, [[], [], [], [], [], [], [], [], [], [], [], [], [], [], []]);
[[], [], [], [], [], [], [], [], [], [], [], [], [], [], []]
);
// Apply active item filters // Apply active item filters
items = this._filterItems(items, this._filters.inventory); items = this._filterItems(items, this._filters.inventory);
@ -164,7 +140,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
for ( let i of items ) { for ( let i of items ) {
i.data.quantity = i.data.quantity || 0; i.data.quantity = i.data.quantity || 0;
i.data.weight = i.data.weight || 0; i.data.weight = i.data.weight || 0;
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1); i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10;
inventory[i.type].items.push(i); inventory[i.type].items.push(i);
} }
@ -174,96 +150,25 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// Organize Features // Organize Features
const features = { const features = {
classes: { classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
label: "SW5E.ItemTypeClassPl", classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true },
items: [], archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
hasActions: false, deployments: { label: "SW5E.ItemTypeDeploymentPl", items: [], hasActions: false, dataset: {type: "deployment"}, isDeployment: true },
dataset: {type: "class"}, deploymentfeatures: { label: "SW5E.ItemTypeDeploymentFeaturePl", items: [], hasActions: true, dataset: {type: "deploymentfeature"}, isDeploymentfeature: true },
isClass: 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 },
classfeatures: { background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
label: "SW5E.ItemTypeClassFeats", fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true },
items: [], fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true },
hasActions: true, lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
dataset: {type: "classfeature"}, active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
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"} } passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
}; };
for ( let f of feats ) { for ( let f of feats ) {
if ( f.data.activation.type ) features.active.items.push(f); if ( f.data.activation.type ) features.active.items.push(f);
else features.passive.items.push(f); else features.passive.items.push(f);
} }
classes.sort((a, b) => b.data.levels - a.data.levels); classes.sort((a, b) => b.levels - a.levels);
features.classes.items = classes; features.classes.items = classes;
features.classfeatures.items = classfeatures; features.classfeatures.items = classfeatures;
features.archetype.items = archetypes; features.archetype.items = archetypes;
@ -299,7 +204,8 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
} else { }
else {
const isActive = getProperty(item.data, "equipped"); const isActive = getProperty(item.data, "equipped");
item.toggleClass = isActive ? "active" : ""; item.toggleClass = isActive ? "active" : "";
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
@ -312,35 +218,33 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/** /**
* Activate event listeners using the prepared sheet HTML * Activate event listeners using the prepared sheet HTML
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM * @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/ */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
if (!this.isEditable) return; if ( !this.options.editable ) return;
// Inventory Functions // Inventory Functions
// html.find(".currency-convert").click(this._onConvertCurrency.bind(this)); // html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
// Item State Toggling // Item State Toggling
html.find(".item-toggle").click(this._onToggleItem.bind(this)); html.find('.item-toggle').click(this._onToggleItem.bind(this));
// Short and Long Rest // Short and Long Rest
html.find(".short-rest").click(this._onShortRest.bind(this)); html.find('.short-rest').click(this._onShortRest.bind(this));
html.find(".long-rest").click(this._onLongRest.bind(this)); html.find('.long-rest').click(this._onLongRest.bind(this));
// Rollable sheet actions // Rollable sheet actions
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
// Send Languages to Chat onClick // Send Languages to Chat onClick
html.find('[data-options="share-languages"]').click((event) => { html.find('[data-options="share-languages"]').click(event => {
event.preventDefault(); event.preventDefault();
let langs = this.actor.data.data.traits.languages.value let langs = this.actor.data.data.traits.languages.value.map(l => SW5E.languages[l] || l).join(", ");
.map((l) => CONFIG.SW5E.languages[l] || l)
.join(", ");
let custom = this.actor.data.data.traits.languages.custom; let custom = this.actor.data.data.traits.languages.custom;
if (custom) langs += ", " + custom.replace(/;/g, ","); if (custom) langs += ", " + custom.replace(/;/g, ",");
let content = ` let content = `
<div class="sw5e chat-card item-card" data-acor-id="${this.actor.data._id}"> <div class="sw5e chat-card item-card" data-acor-id="${this.actor._id}">
<header class="card-header flexrow"> <header class="card-header flexrow">
<img src="${this.actor.data.token.img}" title="" width="36" height="36" style="border: none;"/> <img src="${this.actor.data.token.img}" title="" width="36" height="36" style="border: none;"/>
<h3>Known Languages</h3> <h3>Known Languages</h3>
@ -350,50 +254,46 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
`; `;
// Send to Chat // Send to Chat
let rollWhisper = null;
let rollBlind = false; let rollBlind = false;
let rollMode = game.settings.get("core", "rollMode"); let rollMode = game.settings.get("core", "rollMode");
if (["gmroll", "blindroll"].includes(rollMode)) rollWhisper = ChatMessage.getWhisperIDs("GM");
if (rollMode === "blindroll") rollBlind = true; if (rollMode === "blindroll") rollBlind = true;
let data = { ChatMessage.create({
user: game.user.data._id, user: game.user._id,
content: content, content: content,
blind: rollBlind,
speaker: { speaker: {
actor: this.actor.data._id, actor: this.actor._id,
token: this.actor.token, token: this.actor.token,
alias: this.actor.name alias: this.actor.name
}, },
type: CONST.CHAT_MESSAGE_TYPES.OTHER 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)];
ChatMessage.create(data);
}); });
// Item Delete Confirmation // Item Delete Confirmation
html.find(".item-delete").off("click"); html.find('.item-delete').off("click");
html.find(".item-delete").click((event) => { html.find('.item-delete').click(event => {
let li = $(event.currentTarget).parents(".item"); let li = $(event.currentTarget).parents('.item');
let itemId = li.attr("data-item-id"); let itemId = li.attr("data-item-id");
let item = this.actor.items.get(itemId); let item = this.actor.getOwnedItem(itemId);
new Dialog({ new Dialog({
title: `Deleting ${item.data.name}`, title: `Deleting ${item.data.name}`,
content: `<p>Are you sure you want to delete ${item.data.name}?</p>`, content: `<p>Are you sure you want to delete ${item.data.name}?</p>`,
buttons: { buttons: {
Yes: { Yes: {
icon: '<i class="fa fa-check"></i>', icon: '<i class="fa fa-check"></i>',
label: "Yes", label: 'Yes',
callback: (dlg) => { callback: dlg => {
this.actor.deleteOwnedItem(itemId); this.actor.deleteOwnedItem(itemId);
} }
}, },
cancel: { cancel: {
icon: '<i class="fas fa-times"></i>', icon: '<i class="fas fa-times"></i>',
label: "No" label: 'No'
}
}, },
default: "cancel" },
default: 'cancel'
}).render(true); }).render(true);
}); });
} }
@ -418,6 +318,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Handle toggling the state of an Owned Item within the Actor * Handle toggling the state of an Owned Item within the Actor
* @param {Event} event The triggering click event * @param {Event} event The triggering click event
@ -426,7 +327,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
_onToggleItem(event) { _onToggleItem(event) {
event.preventDefault(); event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId; const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId); const item = this.actor.getOwnedItem(itemId);
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
return item.update({[attr]: !getProperty(item.data, attr)}); return item.update({[attr]: !getProperty(item.data, attr)});
} }
@ -461,9 +362,10 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/** @override */ /** @override */
async _onDropItemCreate(itemData) { async _onDropItemCreate(itemData) {
// Increment the number of class levels of a character instead of creating a new item // Increment the number of class levels of a character instead of creating a new item
if ( itemData.type === "class" ) { if ( itemData.type === "class" ) {
const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name); const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
let priorLevel = cls?.data.data.levels ?? 0; let priorLevel = cls?.data.data.levels ?? 0;
if ( !!cls ) { if ( !!cls ) {
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
@ -488,7 +390,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// } // }
// Default drop handling if levels were not added // Default drop handling if levels were not added
return super._onDropItemCreate(itemData); super._onDropItemCreate(itemData);
} }
} }
async function addFavorites(app, html, data) { async function addFavorites(app, html, data) {
@ -548,9 +450,9 @@ async function addFavorites(app, html, data) {
value: data.actor.data.powers.power9.value, value: data.actor.data.powers.power9.value,
max: data.actor.data.powers.power9.max max: data.actor.data.powers.power9.max
} }
}; }
let powerCount = 0; let powerCount = 0
let items = data.actor.items; let items = data.actor.items;
for (let item of items) { for (let item of items) {
if (item.type == "class") continue; if (item.type == "class") continue;
@ -561,28 +463,24 @@ async function addFavorites(app, html, data) {
} }
let isFav = item.flags.favtab.isFavourite; let isFav = item.flags.favtab.isFavourite;
if (app.options.editable) { if (app.options.editable) {
let favBtn = $( let favBtn = $(`<a class="item-control item-toggle item-fav ${isFav ? "active" : ""}" data-fav="${isFav}" title="${isFav ? "Remove from Favourites" : "Add to Favourites"}"><i class="fas fa-star"></i></a>`);
`<a class="item-control item-toggle item-fav ${isFav ? "active" : ""}" data-fav="${isFav}" title="${ favBtn.click(ev => {
isFav ? "Remove from Favourites" : "Add to Favourites" app.actor.getOwnedItem(item._id).update({
}"><i class="fas fa-star"></i></a>`
);
favBtn.click((ev) => {
app.actor.items.get(item.data._id).update({
"flags.favtab.isFavourite": !item.flags.favtab.isFavourite "flags.favtab.isFavourite": !item.flags.favtab.isFavourite
}); });
}); });
html.find(`.item[data-item-id="${item.data._id}"]`).find(".item-controls").prepend(favBtn); html.find(`.item[data-item-id="${item._id}"]`).find('.item-controls').prepend(favBtn);
} }
if (isFav) { if (isFav) {
item.powerComps = ""; item.powerComps = "";
if (item.data.components) { if (item.data.components) {
let comps = item.data.components; let comps = item.data.components;
let v = comps.vocal ? "V" : ""; let v = (comps.vocal) ? "V" : "";
let s = comps.somatic ? "S" : ""; let s = (comps.somatic) ? "S" : "";
let m = comps.material ? "M" : ""; let m = (comps.material) ? "M" : "";
let c = !!comps.concentration; let c = (comps.concentration) ? true : false;
let r = !!comps.ritual; let r = (comps.ritual) ? true : false;
item.powerComps = `${v}${s}${m}`; item.powerComps = `${v}${s}${m}`;
item.powerCon = c; item.powerCon = c;
item.powerRit = r; item.powerRit = r;
@ -590,15 +488,15 @@ async function addFavorites(app, html, data) {
item.editable = app.options.editable; item.editable = app.options.editable;
switch (item.type) { switch (item.type) {
case "feat": case 'feat':
if (item.flags.favtab.sort === undefined) { if (item.flags.favtab.sort === undefined) {
item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present
} }
favFeats.push(item); favFeats.push(item);
break; break;
case "power": case 'power':
if (item.data.preparation.mode) { if (item.data.preparation.mode) {
item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})`; item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})`
} }
if (item.data.level) { if (item.data.level) {
favPowers[item.data.level].powers.push(item); favPowers[item.data.level].powers.push(item);
@ -624,62 +522,62 @@ async function addFavorites(app, html, data) {
// html.find('.favourite .item-controls').css('flex', '0 0 22px'); // html.find('.favourite .item-controls').css('flex', '0 0 22px');
// } // }
let tabContainer = html.find(".favtabtarget"); let tabContainer = html.find('.favtabtarget');
data.favItems = favItems.length > 0 ? favItems.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false; 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.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false;
data.favPowers = powerCount > 0 ? favPowers : false; data.favPowers = powerCount > 0 ? favPowers : false;
data.editable = app.options.editable; data.editable = app.options.editable;
await loadTemplates(["systems/sw5e/templates/actors/newActor/item.hbs"]); await loadTemplates(['systems/sw5e/templates/actors/newActor/item.hbs']);
let favtabHtml = $(await renderTemplate("systems/sw5e/templates/actors/newActor/template.hbs", data)); let favtabHtml = $(await renderTemplate('systems/sw5e/templates/actors/newActor/template.hbs', data));
favtabHtml.find(".item-name h4").click((event) => app._onItemSummary(event)); favtabHtml.find('.item-name h4').click(event => app._onItemSummary(event));
if (app.options.editable) { if (app.options.editable) {
favtabHtml.find(".item-image").click((ev) => app._onItemRoll(ev)); favtabHtml.find('.item-image').click(ev => app._onItemRoll(ev));
let handler = (ev) => app._onDragStart(ev); let handler = ev => app._onDragStart(ev);
favtabHtml.find(".item").each((i, li) => { favtabHtml.find('.item').each((i, li) => {
if (li.classList.contains("inventory-header")) return; if (li.classList.contains("inventory-header")) return;
li.setAttribute("draggable", true); li.setAttribute("draggable", true);
li.addEventListener("dragstart", handler, false); li.addEventListener("dragstart", handler, false);
}); });
//favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event)); //favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event));
favtabHtml.find(".item-edit").click((ev) => { favtabHtml.find('.item-edit').click(ev => {
let itemId = $(ev.target).parents(".item")[0].dataset.itemId; let itemId = $(ev.target).parents('.item')[0].dataset.itemId;
app.actor.items.get(itemId).sheet.render(true); app.actor.getOwnedItem(itemId).sheet.render(true);
}); });
favtabHtml.find(".item-fav").click((ev) => { favtabHtml.find('.item-fav').click(ev => {
let itemId = $(ev.target).parents(".item")[0].dataset.itemId; let itemId = $(ev.target).parents('.item')[0].dataset.itemId;
let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite; let val = !app.actor.getOwnedItem(itemId).data.flags.favtab.isFavourite
app.actor.items.get(itemId).update({ app.actor.getOwnedItem(itemId).update({
"flags.favtab.isFavourite": val "flags.favtab.isFavourite": val
}); });
}); });
// Sorting // Sorting
favtabHtml.find(".item").on("drop", (ev) => { favtabHtml.find('.item').on('drop', ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData("text/plain")); 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 || dropData.data.type === 'power') return;
if (dropData.actorId !== app.actor.id) return; if (dropData.actorId !== app.actor.id) return;
let list = null; let list = null;
if (dropData.data.type === "feat") list = favFeats; if (dropData.data.type === 'feat') list = favFeats;
else list = favItems; else list = favItems;
let dragSource = list.find((i) => i.data._id === dropData.data._id); let dragSource = list.find(i => i._id === dropData.data._id);
let siblings = list.filter((i) => i.data._id !== dropData.data._id); let siblings = list.filter(i => i._id !== dropData.data._id);
let targetId = ev.target.closest(".item").dataset.itemId; let targetId = ev.target.closest('.item').dataset.itemId;
let dragTarget = siblings.find((s) => s.data._id === targetId); let dragTarget = siblings.find(s => s._id === targetId);
if (dragTarget === undefined) return; if (dragTarget === undefined) return;
const sortUpdates = SortingHelpers.performIntegerSort(dragSource, { const sortUpdates = SortingHelpers.performIntegerSort(dragSource, {
target: dragTarget, target: dragTarget,
siblings: siblings, siblings: siblings,
sortKey: "flags.favtab.sort" sortKey: 'flags.favtab.sort'
}); });
const updateData = sortUpdates.map((u) => { const updateData = sortUpdates.map(u => {
const update = u.update; const update = u.update;
update._id = u.target.data._id; update._id = u.target._id;
return update; return update;
}); });
app.actor.updateEmbeddedEntity("OwnedItem", updateData); app.actor.updateEmbeddedEntity("OwnedItem", updateData);
@ -706,44 +604,50 @@ async function addSubTabs(app, html, data) {
if(data.options.subTabs == null) { if(data.options.subTabs == null) {
//let subTabs = []; //{subgroup: '', target: '', active: false} //let subTabs = []; //{subgroup: '', target: '', active: false}
data.options.subTabs = {}; data.options.subTabs = {};
html.find("[data-subgroup-selection] [data-subgroup]").each((idx, el) => { html.find('[data-subgroup-selection] [data-subgroup]').each((idx, el) => {
let subgroup = el.getAttribute("data-subgroup"); let subgroup = el.getAttribute('data-subgroup');
let target = el.getAttribute("data-target"); let target = el.getAttribute('data-target');
let targetObj = {target: target, active: el.classList.contains("active")}; let targetObj = {target: target, active: el.classList.contains("active")}
if(data.options.subTabs.hasOwnProperty(subgroup)) { if(data.options.subTabs.hasOwnProperty(subgroup)) {
data.options.subTabs[subgroup].push(targetObj); data.options.subTabs[subgroup].push(targetObj);
} else { } else {
data.options.subTabs[subgroup] = []; data.options.subTabs[subgroup] = [];
data.options.subTabs[subgroup].push(targetObj); data.options.subTabs[subgroup].push(targetObj);
} }
}); })
} }
for(const group in data.options.subTabs) { for(const group in data.options.subTabs) {
data.options.subTabs[group].forEach((tab) => { data.options.subTabs[group].forEach(tab => {
if(tab.active) { if(tab.active) {
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass("active"); html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass('active');
} else { } else {
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass("active"); html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass('active');
} }
}); })
} }
html.find("[data-subgroup-selection]") html.find('[data-subgroup-selection]').children().on('click', event => {
.children() let subgroup = event.target.closest('[data-subgroup]').getAttribute('data-subgroup');
.on("click", (event) => { let target = event.target.closest('[data-target]').getAttribute('data-target');
let subgroup = event.target.closest("[data-subgroup]").getAttribute("data-subgroup"); html.find(`[data-subgroup=${subgroup}]`).removeClass('active');
let target = event.target.closest("[data-target]").getAttribute("data-target"); html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass('active');
html.find(`[data-subgroup=${subgroup}]`).removeClass("active"); let tabId = data.options.subTabs[subgroup].find(tab => {
html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass("active"); return tab.target == target
let tabId = data.options.subTabs[subgroup].find((tab) => {
return tab.target == target;
}); });
data.options.subTabs[subgroup].map((el) => { data.options.subTabs[subgroup].map(el => {
el.active = el.target == target; if(el.target == target) {
el.active = true;
} else {
el.active = false;
}
return el; return el;
}); })
});
})
} }
Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => { Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => {

View file

@ -6,6 +6,7 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e} * @extends {ActorSheet5e}
*/ */
export default class ActorSheet5eNPCNew extends ActorSheet5e { export default class ActorSheet5eNPCNew extends ActorSheet5e {
/** @override */ /** @override */
get template() { get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
@ -16,63 +17,43 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "npc"], classes: ["sw5e", "sheet", "actor", "npc"],
width: 800, width: 800,
tabs: [ tabs: [{
{
navSelector: ".root-tabs", navSelector: ".root-tabs",
contentSelector: ".sheet-body", contentSelector: ".sheet-body",
initial: "attributes" initial: "attributes"
} }],
]
}); });
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/** /**
* Organize Owned Items for rendering the NPC sheet * Organize Owned Items for rendering the NPC sheet
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize Items as Features and Powers // Categorize Items as Features and Powers
const features = { const features = {
weapons: { weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
label: game.i18n.localize("SW5E.AttackPl"), actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
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"} }, passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
}; };
// Start by classifying items into groups for rendering // Start by classifying items into groups for rendering
let [forcepowers, techpowers, other] = data.items.reduce( let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
(arr, item) => { item.img = item.img || DEFAULT_TOKEN;
item.img = item.img || CONST.DEFAULT_TOKEN; item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; item.hasUses = item.data.uses && (item.data.uses.max > 0);
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.isOnCooldown = item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
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); 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 if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item);
else arr[2].push(item); else arr[2].push(item);
return arr; return arr;
}, }, [[], [], []]);
[[], [], []]
);
// Apply item filters // Apply item filters
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
@ -89,7 +70,8 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
else if ( item.type === "feat" ) { else if ( item.type === "feat" ) {
if ( item.data.activation.type ) features.actions.items.push(item); if ( item.data.activation.type ) features.actions.items.push(item);
else features.passive.items.push(item); else features.passive.items.push(item);
} else features.equipment.items.push(item); }
else features.equipment.items.push(item);
} }
// Assign and return // Assign and return
@ -98,19 +80,17 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
data.techPowerbook = techPowerbook; data.techPowerbook = techPowerbook;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
getData(options) { getData() {
const data = super.getData(options); const data = super.getData();
// Challenge Rating // Challenge Rating
const cr = parseFloat(data.data.details.cr || 0); const cr = parseFloat(data.data.details.cr || 0);
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; 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; data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
// Creature Type
data.labels["type"] = this.actor.labels.creatureType;
return data; return data;
} }
@ -119,7 +99,8 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _updateObject(event, formData) { _updateObject(event, formData) {
// Format NPC Challenge Rating // Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr"; let crv = "data.details.cr";
@ -128,7 +109,7 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps // Parent ActorSheet update steps
return super._updateObject(event, formData); super._updateObject(event, formData);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -157,3 +138,4 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
} }
} }

View file

@ -6,6 +6,7 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e} * @extends {ActorSheet5e}
*/ */
export default class ActorSheet5eStarship extends ActorSheet5e { export default class ActorSheet5eStarship extends ActorSheet5e {
/** @override */ /** @override */
get template() { get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
@ -16,13 +17,11 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "starship"], classes: ["sw5e", "sheet", "actor", "starship"],
width: 800, width: 800,
tabs: [ tabs: [{
{
navSelector: ".root-tabs", navSelector: ".root-tabs",
contentSelector: ".sheet-body", contentSelector: ".sheet-body",
initial: "attributes" initial: "attributes"
} }],
]
}); });
} }
@ -33,47 +32,29 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize Items as Features and Powers // Categorize Items as Features and Powers
const features = { const features = {
weapons: { weapons: { label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), items: [], hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
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"} }, passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}}, equipment: { label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}},
starshipfeatures: { starshipfeatures: { label: game.i18n.localize("SW5E.StarshipfeaturePl"), items: [], hasActions: true, dataset: {type: "starshipfeature"} },
label: game.i18n.localize("SW5E.StarshipfeaturePl"), starshipmods: { label: game.i18n.localize("SW5E.StarshipmodPl"), items: [], hasActions: false, dataset: {type: "starshipmod"} }
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 // Start by classifying items into groups for rendering
let [forcepowers, techpowers, other] = data.items.reduce( let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
(arr, item) => { item.img = item.img || DEFAULT_TOKEN;
item.img = item.img || CONST.DEFAULT_TOKEN; item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; item.hasUses = item.data.uses && (item.data.uses.max > 0);
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.isOnCooldown = item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
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); 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 if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item);
else arr[2].push(item); else arr[2].push(item);
return arr; return arr;
}, }, [[], [], []]);
[[], [], []]
);
// Apply item filters // Apply item filters
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
@ -90,11 +71,14 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
else if ( item.type === "feat" ) { else if ( item.type === "feat" ) {
if ( item.data.activation.type ) features.actions.items.push(item); if ( item.data.activation.type ) features.actions.items.push(item);
else features.passive.items.push(item); else features.passive.items.push(item);
} else if (item.type === "starshipfeature") { }
else if ( item.type === "starshipfeature" ) {
features.starshipfeatures.items.push(item); features.starshipfeatures.items.push(item);
} else if (item.type === "starshipmod") { }
else if ( item.type === "starshipmod" ) {
features.starshipmods.items.push(item); features.starshipmods.items.push(item);
} else features.equipment.items.push(item); }
else features.equipment.items.push(item);
} }
// Assign and return // Assign and return
@ -103,19 +87,12 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
// data.techPowerbook = techPowerbook; // data.techPowerbook = techPowerbook;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
getData(options) { getData() {
const data = super.getData(options); const data = super.getData();
// 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";
// Challenge Rating // Challenge Rating
const cr = parseFloat(data.data.details.cr || 0); const cr = parseFloat(data.data.details.cr || 0);
@ -129,7 +106,8 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _updateObject(event, formData) { _updateObject(event, formData) {
// Format NPC Challenge Rating // Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr"; let crv = "data.details.cr";
@ -138,7 +116,7 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps // Parent ActorSheet update steps
return super._updateObject(event, formData); super._updateObject(event, formData);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -166,4 +144,5 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
AudioHelper.play({src: CONFIG.sounds.dice}); AudioHelper.play({src: CONFIG.sounds.dice});
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
} }
} }

View file

@ -20,17 +20,12 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/** /**
* Creates a new cargo entry for a vehicle Actor. * Creates a new cargo entry for a vehicle Actor.
*/ */
static get newCargo() { static get newCargo() {
return { return {
name: "", name: '',
quantity: 1 quantity: 1
}; };
} }
@ -45,6 +40,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private * @private
*/ */
_computeEncumbrance(totalWeight, actorData) { _computeEncumbrance(totalWeight, actorData) {
// Compute currency weight // Compute currency weight
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
@ -73,24 +69,25 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private * @private
*/ */
_prepareCrewedItem(item) { _prepareCrewedItem(item) {
// Determine crewed status // Determine crewed status
const isCrewed = item.data.crewed; const isCrewed = item.data.crewed;
item.toggleClass = isCrewed ? "active" : ""; item.toggleClass = isCrewed ? 'active' : '';
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`); item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
// Handle crew actions // Handle crew actions
if (item.type === "feat" && item.data.activation.type === "crew") { if (item.type === 'feat' && item.data.activation.type === 'crew') {
item.crew = item.data.activation.cost; item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`); item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
if (item.data.cover === 0.5) item.cover = "½"; if (item.data.cover === .5) item.cover = '½';
else if (item.data.cover === 0.75) item.cover = "¾"; else if (item.data.cover === .75) item.cover = '¾';
else if (item.data.cover === null) item.cover = "—"; else if (item.data.cover === null) item.cover = '—';
if (item.crew < 1 || item.crew === null) item.crew = "—"; if (item.crew < 1 || item.crew === null) item.crew = '—';
} }
// Prepare vehicle weapons // Prepare vehicle weapons
if (item.type === "equipment" || item.type === "weapon") { if (item.type === 'equipment' || item.type === 'weapon') {
item.threshold = item.data.hp.dt ? item.data.hp.dt : "—"; item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
} }
} }
@ -101,162 +98,132 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
const cargoColumns = [ const cargoColumns = [{
{ label: game.i18n.localize('SW5E.Quantity'),
label: game.i18n.localize("SW5E.Quantity"), css: 'item-qty',
css: "item-qty", property: 'quantity',
property: "quantity", editable: 'Number'
editable: "Number" }];
}
];
const equipmentColumns = [ const equipmentColumns = [{
{ label: game.i18n.localize('SW5E.Quantity'),
label: game.i18n.localize("SW5E.Quantity"), css: 'item-qty',
css: "item-qty", property: 'data.quantity'
property: "data.quantity" }, {
}, label: game.i18n.localize('SW5E.AC'),
{ css: 'item-ac',
label: game.i18n.localize("SW5E.AC"), property: 'data.armor.value'
css: "item-ac", }, {
property: "data.armor.value" label: game.i18n.localize('SW5E.HP'),
}, css: 'item-hp',
{ property: 'data.hp.value',
label: game.i18n.localize("SW5E.HP"), editable: 'Number'
css: "item-hp", }, {
property: "data.hp.value", label: game.i18n.localize('SW5E.Threshold'),
editable: "Number" css: 'item-threshold',
}, property: 'threshold'
{ }];
label: game.i18n.localize("SW5E.Threshold"),
css: "item-threshold",
property: "threshold"
}
];
const features = { const features = {
actions: { actions: {
label: game.i18n.localize("SW5E.ActionPl"), label: game.i18n.localize('SW5E.ActionPl'),
items: [], items: [],
crewable: true, crewable: true,
dataset: {"type": "feat", "activation.type": "crew"}, dataset: {type: 'feat', 'activation.type': 'crew'},
columns: [ columns: [{
{ label: game.i18n.localize('SW5E.VehicleCrew'),
label: game.i18n.localize("SW5E.VehicleCrew"), css: 'item-crew',
css: "item-crew", property: 'crew'
property: "crew" }, {
}, label: game.i18n.localize('SW5E.Cover'),
{ css: 'item-cover',
label: game.i18n.localize("SW5E.Cover"), property: 'cover'
css: "item-cover", }]
property: "cover"
}
]
}, },
equipment: { equipment: {
label: game.i18n.localize("SW5E.ItemTypeEquipment"), label: game.i18n.localize('SW5E.ItemTypeEquipment'),
items: [], items: [],
crewable: true, crewable: true,
dataset: {"type": "equipment", "armor.type": "vehicle"}, dataset: {type: 'equipment', 'armor.type': 'vehicle'},
columns: equipmentColumns columns: equipmentColumns
}, },
passive: { passive: {
label: game.i18n.localize("SW5E.Features"), label: game.i18n.localize('SW5E.Features'),
items: [], items: [],
dataset: {type: "feat"} dataset: {type: 'feat'}
}, },
reactions: { reactions: {
label: game.i18n.localize("SW5E.ReactionPl"), label: game.i18n.localize('SW5E.ReactionPl'),
items: [], items: [],
dataset: {"type": "feat", "activation.type": "reaction"} dataset: {type: 'feat', 'activation.type': 'reaction'}
}, },
weapons: { weapons: {
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
items: [], items: [],
crewable: true, crewable: true,
dataset: {"type": "weapon", "weapon-type": "siege"}, dataset: {type: 'weapon', 'weapon-type': 'siege'},
columns: equipmentColumns columns: equipmentColumns
} }
}; };
const cargo = { const cargo = {
crew: { crew: {
label: game.i18n.localize("SW5E.VehicleCrew"), label: game.i18n.localize('SW5E.VehicleCrew'),
items: data.data.cargo.crew, items: data.data.cargo.crew,
css: "cargo-row crew", css: 'cargo-row crew',
editableName: true, editableName: true,
dataset: {type: "crew"}, dataset: {type: 'crew'},
columns: cargoColumns columns: cargoColumns
}, },
passengers: { passengers: {
label: game.i18n.localize("SW5E.VehiclePassengers"), label: game.i18n.localize('SW5E.VehiclePassengers'),
items: data.data.cargo.passengers, items: data.data.cargo.passengers,
css: "cargo-row passengers", css: 'cargo-row passengers',
editableName: true, editableName: true,
dataset: {type: "passengers"}, dataset: {type: 'passengers'},
columns: cargoColumns columns: cargoColumns
}, },
cargo: { cargo: {
label: game.i18n.localize("SW5E.VehicleCargo"), label: game.i18n.localize('SW5E.VehicleCargo'),
items: [], items: [],
dataset: {type: "loot"}, dataset: {type: 'loot'},
columns: [ columns: [{
{ label: game.i18n.localize('SW5E.Quantity'),
label: game.i18n.localize("SW5E.Quantity"), css: 'item-qty',
css: "item-qty", property: 'data.quantity',
property: "data.quantity", editable: 'Number'
editable: "Number" }, {
}, label: game.i18n.localize('SW5E.Price'),
{ css: 'item-price',
label: game.i18n.localize("SW5E.Price"), property: 'data.price',
css: "item-price", editable: 'Number'
property: "data.price", }, {
editable: "Number" label: game.i18n.localize('SW5E.Weight'),
}, css: 'item-weight',
{ property: 'data.weight',
label: game.i18n.localize("SW5E.Weight"), editable: 'Number'
css: "item-weight", }]
property: "data.weight",
editable: "Number"
}
]
} }
}; };
// Classify items owned by the vehicle and compute total cargo weight
let totalWeight = 0; let totalWeight = 0;
for (const item of data.items) { for (const item of data.items) {
this._prepareCrewedItem(item); this._prepareCrewedItem(item);
if (item.type === 'weapon') features.weapons.items.push(item);
// Handle cargo explicitly else if (item.type === 'equipment') features.equipment.items.push(item);
const isCargo = item.flags.sw5e?.vehicleCargo === true; else if (item.type === 'loot') {
if (isCargo) {
totalWeight += (item.data.weight || 0) * item.data.quantity; totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item); cargo.cargo.items.push(item);
continue;
} }
else if (item.type === 'feat') {
// Handle non-cargo item types if (!item.data.activation.type || item.data.activation.type === 'none') {
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); features.passive.items.push(item);
else if (item.data.activation.type === "reaction") features.reactions.items.push(item); }
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
else features.actions.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.features = Object.values(features);
data.cargo = Object.values(cargo); data.cargo = Object.values(cargo);
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data); data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
@ -269,23 +236,23 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/** @override */ /** @override */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
if (!this.isEditable) return; if (!this.options.editable) return;
html.find(".item-toggle").click(this._onToggleItem.bind(this)); html.find('.item-toggle').click(this._onToggleItem.bind(this));
html.find(".item-hp input") html.find('.item-hp input')
.click((evt) => evt.target.select()) .click(evt => evt.target.select())
.change(this._onHPChange.bind(this)); .change(this._onHPChange.bind(this));
html.find(".item:not(.cargo-row) input[data-property]") html.find('.item:not(.cargo-row) input[data-property]')
.click((evt) => evt.target.select()) .click(evt => evt.target.select())
.change(this._onEditInSheet.bind(this)); .change(this._onEditInSheet.bind(this));
html.find(".cargo-row input") html.find('.cargo-row input')
.click((evt) => evt.target.select()) .click(evt => evt.target.select())
.change(this._onCargoRowChange.bind(this)); .change(this._onCargoRowChange.bind(this));
if (this.actor.data.data.attributes.actions.stations) { if (this.actor.data.data.attributes.actions.stations) {
html.find(".counter.actions, .counter.action-thresholds").hide(); html.find('.counter.actions, .counter.action-thresholds').hide();
} }
} }
@ -300,20 +267,20 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
_onCargoRowChange(event) { _onCargoRowChange(event) {
event.preventDefault(); event.preventDefault();
const target = event.currentTarget; const target = event.currentTarget;
const row = target.closest(".item"); const row = target.closest('.item');
const idx = Number(row.dataset.itemId); const idx = Number(row.dataset.itemId);
const property = row.classList.contains("crew") ? "crew" : "passengers"; const property = row.classList.contains('crew') ? 'crew' : 'passengers';
// Get the cargo entry // Get the cargo entry
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); const cargo = duplicate(this.actor.data.data.cargo[property]);
const entry = cargo[idx]; const entry = cargo[idx];
if (!entry) return null; if (!entry) return null;
// Update the cargo value // Update the cargo value
const key = target.dataset.property || "name"; const key = target.dataset.property || 'name';
const type = target.dataset.dtype; const type = target.dataset.dtype;
let value = target.value; let value = target.value;
if (type === "Number") value = Number(value); if (type === 'Number') value = Number(value);
entry[key] = value; entry[key] = value;
// Perform the Actor update // Perform the Actor update
@ -330,18 +297,14 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onEditInSheet(event) { _onEditInSheet(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId; const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const property = event.currentTarget.dataset.property; const property = event.currentTarget.dataset.property;
const type = event.currentTarget.dataset.dtype; const type = event.currentTarget.dataset.dtype;
let value = event.currentTarget.value; let value = event.currentTarget.value;
switch (type) { switch (type) {
case "Number": case 'Number': value = parseInt(value); break;
value = parseInt(value); case 'Boolean': value = value === 'true'; break;
break;
case "Boolean":
value = value === "true";
break;
} }
return item.update({[`${property}`]: value}); return item.update({[`${property}`]: value});
} }
@ -358,8 +321,8 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
event.preventDefault(); event.preventDefault();
const target = event.currentTarget; const target = event.currentTarget;
const type = target.dataset.type; const type = target.dataset.type;
if (type === "crew" || type === "passengers") { if (type === 'crew' || type === 'passengers') {
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); const cargo = duplicate(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo); cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo}); return this.actor.update({[`data.cargo.${type}`]: cargo});
} }
@ -376,11 +339,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onItemDelete(event) { _onItemDelete(event) {
event.preventDefault(); event.preventDefault();
const row = event.currentTarget.closest(".item"); const row = event.currentTarget.closest('.item');
if (row.classList.contains("cargo-row")) { if (row.classList.contains('cargo-row')) {
const idx = Number(row.dataset.itemId); const idx = Number(row.dataset.itemId);
const type = row.classList.contains("crew") ? "crew" : "passengers"; const type = row.classList.contains('crew') ? 'crew' : 'passengers';
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); const cargo = duplicate(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo}); return this.actor.update({[`data.cargo.${type}`]: cargo});
} }
@ -389,16 +352,6 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @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. * Special handling for editing HP to clamp it within appropriate range.
* @param event {Event} * @param event {Event}
@ -407,11 +360,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onHPChange(event) { _onHPChange(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId; const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
event.currentTarget.value = hp; event.currentTarget.value = hp;
return item.update({"data.hp.value": hp}); return item.update({'data.hp.value': hp});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -424,9 +377,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onToggleItem(event) { _onToggleItem(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId; const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const crewed = !!item.data.data.crewed; const crewed = !!item.data.data.crewed;
return item.update({"data.crewed": !crewed}); return item.update({'data.crewed': !crewed});
}
} }
};

View file

@ -1,11 +1,9 @@
import Item5e from "../../../item/entity.js"; import Item5e from "../../../item/entity.js";
import TraitSelector from "../../../apps/trait-selector.js"; import TraitSelector from "../../../apps/trait-selector.js";
import ActorSheetFlags from "../../../apps/actor-flags.js"; import ActorSheetFlags from "../../../apps/actor-flags.js";
import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js"; import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-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"; import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/** /**
@ -46,14 +44,6 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* A set of item types that should be prevented from being dropped on this type of actor sheet.
* @type {Set<string>}
*/
static unsupportedItemTypes = new Set();
/* -------------------------------------------- */
/** @override */ /** @override */
get template() { get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html"; if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html";
@ -63,50 +53,44 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
getData(options) { getData() {
// Basic data // Basic data
let isOwner = this.actor.isOwner; let isOwner = this.entity.owner;
const data = { const data = {
owner: isOwner, owner: isOwner,
limited: this.actor.limited, limited: this.entity.limited,
options: this.options, options: this.options,
editable: this.isEditable, editable: this.isEditable,
cssClass: isOwner ? "editable" : "locked", cssClass: isOwner ? "editable" : "locked",
isCharacter: this.actor.type === "character", isCharacter: this.entity.data.type === "character",
isNPC: this.actor.type === "npc", isNPC: this.entity.data.type === "npc",
isStarship: this.actor.type === "starship", isStarship: this.entity.data.type === "starship",
isVehicle: this.actor.type === "vehicle", isVehicle: this.entity.data.type === 'vehicle',
config: CONFIG.SW5E, config: CONFIG.SW5E,
rollData: this.actor.getRollData.bind(this.actor)
}; };
// The Actor's data // The Actor and its Items
const actorData = this.actor.data.toObject(false); data.actor = duplicate(this.actor.data);
data.actor = actorData; data.items = this.actor.items.map(i => {
data.data = actorData.data; i.data.labels = i.labels;
return i.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)); data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
data.data = data.actor.data;
// Labels and filters
data.labels = this.actor.labels || {}; data.labels = this.actor.labels || {};
data.filters = this._filters; data.filters = this._filters;
// Ability Scores // Ability Scores
for (let [a, abl] of Object.entries(actorData.data.abilities)) { for ( let [a, abl] of Object.entries(data.actor.data.abilities)) {
abl.icon = this._getProficiencyIcon(abl.proficient); abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient]; abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a]; abl.label = CONFIG.SW5E.abilities[a];
} }
// Skills // Skills
if (actorData.data.skills) { if (data.actor.data.skills) {
for (let [s, skl] of Object.entries(actorData.data.skills)) { for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability]; skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.icon = this._getProficiencyIcon(skl.value); skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value]; skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
@ -115,22 +99,22 @@ export default class ActorSheet5e extends ActorSheet {
} }
// Movement speeds // Movement speeds
data.movement = this._getMovementSpeed(actorData); data.movement = this._getMovementSpeed(data.actor);
// Senses // Senses
data.senses = this._getSenses(actorData); data.senses = this._getSenses(data.actor);
// Update traits // Update traits
this._prepareTraits(actorData.data.traits); this._prepareTraits(data.actor.data.traits);
// Prepare owned items // Prepare owned items
this._prepareItems(data); this._prepareItems(data);
// Prepare active effects // Prepare active effects
data.effects = prepareActiveEffectCategories(this.actor.effects); data.effects = prepareActiveEffectCategories(this.entity.effects);
// Return data to the sheet // Return data to the sheet
return data; return data
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -149,35 +133,31 @@ export default class ActorSheet5e extends ActorSheet {
let speeds = [ let speeds = [
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`], [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`], [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.fly,
`${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` +
(movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")
],
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`] [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
]; ]
if ( largestPrimary ) { if ( largestPrimary ) {
speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]); speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
} }
// Filter and sort speeds on their values // Filter and sort speeds on their values
speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]); speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
// Case 1: Largest as primary // Case 1: Largest as primary
if ( largestPrimary ) { if ( largestPrimary ) {
let primary = speeds.shift(); let primary = speeds.shift();
return { return {
primary: `${primary ? primary[1] : "0"} ${movement.units}`, primary: `${primary ? primary[1] : "0"} ${movement.units}`,
special: speeds.map((s) => s[1]).join(", ") special: speeds.map(s => s[1]).join(", ")
}; }
} }
// Case 2: Walk as primary // Case 2: Walk as primary
else { else {
return { return {
primary: `${movement.walk || 0} ${movement.units}`, primary: `${movement.walk || 0} ${movement.units}`,
special: speeds.length ? speeds.map((s) => s[1]).join(", ") : "" special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
}; }
} }
} }
@ -187,7 +167,7 @@ export default class ActorSheet5e extends ActorSheet {
const senses = actorData.data.attributes.senses || {}; const senses = actorData.data.attributes.senses || {};
const tags = {}; const tags = {};
for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) { for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
const v = senses[k] ?? 0; const v = senses[k] ?? 0
if ( v === 0 ) continue; if ( v === 0 ) continue;
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`; tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
} }
@ -204,14 +184,14 @@ export default class ActorSheet5e extends ActorSheet {
*/ */
_prepareTraits(traits) { _prepareTraits(traits) {
const map = { const map = {
dr: CONFIG.SW5E.damageResistanceTypes, "dr": CONFIG.SW5E.damageResistanceTypes,
di: CONFIG.SW5E.damageResistanceTypes, "di": CONFIG.SW5E.damageResistanceTypes,
dv: CONFIG.SW5E.damageResistanceTypes, "dv": CONFIG.SW5E.damageResistanceTypes,
ci: CONFIG.SW5E.conditionTypes, "ci": CONFIG.SW5E.conditionTypes,
languages: CONFIG.SW5E.languages, "languages": CONFIG.SW5E.languages,
armorProf: CONFIG.SW5E.armorProficiencies, "armorProf": CONFIG.SW5E.armorProficiencies,
weaponProf: CONFIG.SW5E.weaponProficiencies, "weaponProf": CONFIG.SW5E.weaponProficiencies,
toolProf: CONFIG.SW5E.toolProficiencies "toolProf": CONFIG.SW5E.toolProficiencies
}; };
for ( let [t, choices] of Object.entries(map) ) { for ( let [t, choices] of Object.entries(map) ) {
const trait = traits[t]; const trait = traits[t];
@ -227,7 +207,7 @@ export default class ActorSheet5e extends ActorSheet {
// Add custom entry // Add custom entry
if ( trait.custom ) { if ( trait.custom ) {
trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim())); trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim());
} }
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
} }
@ -242,15 +222,15 @@ export default class ActorSheet5e extends ActorSheet {
* @private * @private
*/ */
_preparePowerbook(data, powers) { _preparePowerbook(data, powers) {
const owner = this.actor.isOwner; const owner = this.actor.owner;
const levels = data.data.powers; const levels = data.data.powers;
const powerbook = {}; const powerbook = {};
// Define some mappings // Define some mappings
const sections = { const sections = {
atwill: -20, "atwill": -20,
innate: -10, "innate": -10,
pact: 0.5 "pact": 0.5
}; };
// Label power slot uses headers // Label power slot uses headers
@ -267,7 +247,7 @@ export default class ActorSheet5e extends ActorSheet {
label: label, label: label,
usesSlots: i > 0, usesSlots: i > 0,
canCreate: owner, canCreate: owner,
canPrepare: data.actor.type === "character" && i >= 1, canPrepare: (data.actor.type === "character") && (i >= 1),
powers: [], powers: [],
uses: useLabels[i] || value || 0, uses: useLabels[i] || value || 0,
slots: useLabels[i] || max || 0, slots: useLabels[i] || max || 0,
@ -281,7 +261,7 @@ export default class ActorSheet5e extends ActorSheet {
const maxLevel = Array.fromRange(10).reduce((max, i) => { const maxLevel = Array.fromRange(10).reduce((max, i) => {
if ( i === 0 ) return max; if ( i === 0 ) return max;
const level = levels[`power${i}`]; const level = levels[`power${i}`];
if ((level.max || level.override) && i > max) max = i; if ( (level.max || level.override ) && ( i > max ) ) max = i;
return max; return max;
}, 0); }, 0);
@ -295,14 +275,11 @@ export default class ActorSheet5e extends ActorSheet {
} }
// Pact magic users have cantrips and a pact magic section // 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 ( levels.pact && levels.pact.max ) {
if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]); if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
const l = levels.pact; const l = levels.pact;
const config = CONFIG.SW5E.powerPreparationModes.pact; const config = CONFIG.SW5E.powerPreparationModes.pact;
const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`); registerSection("pact", sections.pact, config, {
const label = `${config}${level}`;
registerSection("pact", sections.pact, label, {
prepMode: "pact", prepMode: "pact",
value: l.value, value: l.value,
max: l.max, max: l.max,
@ -311,7 +288,7 @@ export default class ActorSheet5e extends ActorSheet {
} }
// Iterate over every power item, adding powers to the powerbook by section // Iterate over every power item, adding powers to the powerbook by section
powers.forEach((power) => { powers.forEach(power => {
const mode = power.data.preparation.mode || "prepared"; const mode = power.data.preparation.mode || "prepared";
let s = power.data.level || 0; let s = power.data.level || 0;
const sl = `power${s}`; const sl = `power${s}`;
@ -354,13 +331,13 @@ export default class ActorSheet5e extends ActorSheet {
* @private * @private
*/ */
_filterItems(items, filters) { _filterItems(items, filters) {
return items.filter((item) => { return items.filter(item => {
const data = item.data; const data = item.data;
// Action usage // Action usage
for ( let f of ["action", "bonus", "reaction"] ) { for ( let f of ["action", "bonus", "reaction"] ) {
if ( filters.has(f) ) { if ( filters.has(f) ) {
if (data.activation && data.activation.type !== f) return false; if ((data.activation && (data.activation.type !== f))) return false;
} }
} }
@ -405,61 +382,64 @@ export default class ActorSheet5e extends ActorSheet {
/* Event Listeners and Handlers /* Event Listeners and Handlers
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /**
* Activate event listeners using the prepared sheet HTML
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/
activateListeners(html) { activateListeners(html) {
// Activate Item Filters // Activate Item Filters
const filterLists = html.find(".filter-list"); const filterLists = html.find(".filter-list");
filterLists.each(this._initializeFilterItemList.bind(this)); filterLists.each(this._initializeFilterItemList.bind(this));
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this)); filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
// Item summaries // Item summaries
html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event)); 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 // Editable Only Listeners
if ( this.isEditable ) { if ( this.isEditable ) {
// Input focus and update // Input focus and update
const inputs = html.find("input"); const inputs = html.find("input");
inputs.focus((ev) => ev.currentTarget.select()); inputs.focus(ev => ev.currentTarget.select());
inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)); inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Ability Proficiency // Ability Proficiency
html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this)); html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
// Toggle Skill Proficiency // Toggle Skill Proficiency
html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this)); html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this));
// Trait Selector // Trait Selector
html.find(".trait-selector").click(this._onTraitSelector.bind(this)); html.find('.trait-selector').click(this._onTraitSelector.bind(this));
// Configure Special Flags // Configure Special Flags
html.find(".config-button").click(this._onConfigMenu.bind(this)); html.find('.config-button').click(this._onConfigMenu.bind(this));
// Owned Item management // Owned Item management
html.find(".item-create").click(this._onItemCreate.bind(this)); html.find('.item-create').click(this._onItemCreate.bind(this));
html.find(".item-delete").click(this._onItemDelete.bind(this)); html.find('.item-edit').click(this._onItemEdit.bind(this));
html.find(".item-uses input") html.find('.item-delete').click(this._onItemDelete.bind(this));
.click((ev) => ev.target.select()) html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
.change(this._onUsesChange.bind(this)); html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this));
// Active Effect management // Active Effect management
html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor)); html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.entity));
} }
// Owner Only Listeners // Owner Only Listeners
if (this.actor.isOwner) { if ( this.actor.owner ) {
// Ability Checks // Ability Checks
html.find(".ability-name").click(this._onRollAbilityTest.bind(this)); html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
// Roll Skill Checks // Roll Skill Checks
html.find(".skill-name").click(this._onRollSkillCheck.bind(this)); html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
// Item Rolling // Item Rolling
html.find(".item .item-image").click((event) => this._onItemRoll(event)); html.find('.item .item-image').click(event => this._onItemRoll(event));
html.find(".item .item-recharge").click((event) => this._onItemRecharge(event)); html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
} }
// Otherwise remove rollable classes // Otherwise remove rollable classes
@ -515,25 +495,17 @@ export default class ActorSheet5e extends ActorSheet {
_onConfigMenu(event) { _onConfigMenu(event) {
event.preventDefault(); event.preventDefault();
const button = event.currentTarget; const button = event.currentTarget;
let app;
switch ( button.dataset.action ) { switch ( button.dataset.action ) {
case "hit-dice":
app = new ActorHitDiceConfig(this.object);
break;
case "movement": case "movement":
app = new ActorMovementConfig(this.object); new ActorMovementConfig(this.object).render(true);
break; break;
case "flags": case "flags":
app = new ActorSheetFlags(this.object); new ActorSheetFlags(this.object).render(true);
break; break;
case "senses": case "senses":
app = new ActorSensesConfig(this.object); new ActorSensesConfig(this.object).render(true);
break;
case "type":
new ActorTypeConfig(this.object).render(true);
break; break;
} }
app?.render(true);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -554,9 +526,9 @@ export default class ActorSheet5e extends ActorSheet {
// Toggle next level - forward on click, backwards on right // Toggle next level - forward on click, backwards on right
if ( event.type === "click" ) { if ( event.type === "click" ) {
field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]); field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
} else if ( event.type === "contextmenu" ) { } else if ( event.type === "contextmenu" ) {
field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]); field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
} }
// Update the field value and save the form // Update the field value and save the form
@ -567,13 +539,13 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */ /** @override */
async _onDropActor(event, data) { async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing")); const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
if ( !canPolymorph ) return false; if ( !canPolymorph ) return false;
// Get the target actor // Get the target actor
let sourceActor = null; let sourceActor = null;
if (data.pack) { if (data.pack) {
const pack = game.packs.find((p) => p.collection === data.pack); const pack = game.packs.find(p => p.collection === data.pack);
sourceActor = await pack.getEntity(data.id); sourceActor = await pack.getEntity(data.id);
} else { } else {
sourceActor = game.actors.get(data.id); sourceActor = game.actors.get(data.id);
@ -581,37 +553,35 @@ export default class ActorSheet5e extends ActorSheet {
if ( !sourceActor ) return; if ( !sourceActor ) return;
// Define a function to record polymorph settings for future use // Define a function to record polymorph settings for future use
const rememberOptions = (html) => { const rememberOptions = html => {
const options = {}; const options = {};
html.find("input").each((i, el) => { html.find('input').each((i, el) => {
options[el.name] = el.checked; options[el.name] = el.checked;
}); });
const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options); const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options);
game.settings.set("sw5e", "polymorphSettings", settings); game.settings.set('sw5e', 'polymorphSettings', settings);
return settings; return settings;
}; };
// Create and render the Dialog // Create and render the Dialog
return new Dialog( return new Dialog({
{ title: game.i18n.localize('SW5E.PolymorphPromptTitle'),
title: game.i18n.localize("SW5E.PolymorphPromptTitle"),
content: { content: {
options: game.settings.get("sw5e", "polymorphSettings"), options: game.settings.get('sw5e', 'polymorphSettings'),
i18n: SW5E.polymorphSettings, i18n: SW5E.polymorphSettings,
isToken: this.actor.isToken isToken: this.actor.isToken
}, },
default: "accept", default: 'accept',
buttons: { buttons: {
accept: { accept: {
icon: '<i class="fas fa-check"></i>', icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("SW5E.PolymorphAcceptSettings"), label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html)) callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
}, },
wildshape: { wildshape: {
icon: '<i class="fas fa-paw"></i>', icon: '<i class="fas fa-paw"></i>',
label: game.i18n.localize("SW5E.PolymorphWildShape"), label: game.i18n.localize('SW5E.PolymorphWildShape'),
callback: (html) => callback: html => this.actor.transformInto(sourceActor, {
this.actor.transformInto(sourceActor, {
keepBio: true, keepBio: true,
keepClass: true, keepClass: true,
keepMental: true, keepMental: true,
@ -622,66 +592,37 @@ export default class ActorSheet5e extends ActorSheet {
}, },
polymorph: { polymorph: {
icon: '<i class="fas fa-pastafarianism"></i>', icon: '<i class="fas fa-pastafarianism"></i>',
label: game.i18n.localize("SW5E.Polymorph"), label: game.i18n.localize('SW5E.Polymorph'),
callback: (html) => callback: html => this.actor.transformInto(sourceActor, {
this.actor.transformInto(sourceActor, {
transformTokens: rememberOptions(html).transformTokens transformTokens: rememberOptions(html).transformTokens
}) })
}, },
cancel: { cancel: {
icon: '<i class="fas fa-times"></i>', icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("Cancel") label: game.i18n.localize('Cancel')
} }
} }
}, }, {
{ classes: ['dialog', 'sw5e'],
classes: ["dialog", "sw5e"],
width: 600, width: 600,
template: "systems/sw5e/templates/apps/polymorph-prompt.html" template: 'systems/sw5e/templates/apps/polymorph-prompt.html'
} }).render(true);
).render(true);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _onDropItemCreate(itemData) { 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 // 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") ) {
if (itemData.type === "power" && this._tabs[0].active === "inventory") {
const scroll = await Item5e.createScrollFromPower(itemData); const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data; itemData = scroll.data;
} }
if (itemData.data) {
// Ignore certain statuses // Ignore certain statuses
["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]); if ( itemData.data ) {
["attunement", "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 // Create the owned item as normal
@ -722,10 +663,10 @@ export default class ActorSheet5e extends ActorSheet {
async _onUsesChange(event) { async _onUsesChange(event) {
event.preventDefault(); event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId; const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId); const item = this.actor.getOwnedItem(itemId);
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max); const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
event.target.value = uses; event.target.value = uses;
return item.update({"data.uses.value": uses}); return item.update({ 'data.uses.value': uses });
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -737,7 +678,7 @@ export default class ActorSheet5e extends ActorSheet {
_onItemRoll(event) { _onItemRoll(event) {
event.preventDefault(); event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId; const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId); const item = this.actor.getOwnedItem(itemId);
return item.roll(); return item.roll();
} }
@ -751,9 +692,9 @@ export default class ActorSheet5e extends ActorSheet {
_onItemRecharge(event) { _onItemRecharge(event) {
event.preventDefault(); event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId; const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId); const item = this.actor.getOwnedItem(itemId);
return item.rollRecharge(); return item.rollRecharge();
} };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -764,8 +705,8 @@ export default class ActorSheet5e extends ActorSheet {
_onItemSummary(event) { _onItemSummary(event) {
event.preventDefault(); event.preventDefault();
let li = $(event.currentTarget).parents(".item"), let li = $(event.currentTarget).parents(".item"),
item = this.actor.items.get(li.data("item-id")), item = this.actor.getOwnedItem(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.isOwner}); chatData = item.getChatData({secrets: this.actor.owner});
// Toggle summary // Toggle summary
if ( li.hasClass("expanded") ) { if ( li.hasClass("expanded") ) {
@ -774,7 +715,7 @@ export default class ActorSheet5e extends ActorSheet {
} else { } else {
let div = $(`<div class="item-summary">${chatData.description.value}</div>`); let div = $(`<div class="item-summary">${chatData.description.value}</div>`);
let props = $(`<div class="item-properties"></div>`); let props = $(`<div class="item-properties"></div>`);
chatData.properties.forEach((p) => props.append(`<span class="tag">${p}</span>`)); chatData.properties.forEach(p => props.append(`<span class="tag">${p}</span>`));
div.append(props); div.append(props);
li.append(div.hide()); li.append(div.hide());
div.slideDown(200); div.slideDown(200);
@ -794,12 +735,12 @@ export default class ActorSheet5e extends ActorSheet {
const header = event.currentTarget; const header = event.currentTarget;
const type = header.dataset.type; const type = header.dataset.type;
const itemData = { const itemData = {
name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}), name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
type: type, type: type,
data: foundry.utils.deepClone(header.dataset) data: duplicate(header.dataset)
}; };
delete itemData.data["type"]; delete itemData.data["type"];
return this.actor.createEmbeddedDocuments("Item", [itemData]); return this.actor.createEmbeddedEntity("OwnedItem", itemData);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -812,8 +753,8 @@ export default class ActorSheet5e extends ActorSheet {
_onItemEdit(event) { _onItemEdit(event) {
event.preventDefault(); event.preventDefault();
const li = event.currentTarget.closest(".item"); const li = event.currentTarget.closest(".item");
const item = this.actor.items.get(li.dataset.itemId); const item = this.actor.getOwnedItem(li.dataset.itemId);
return item.sheet.render(true); item.sheet.render(true);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -826,8 +767,7 @@ export default class ActorSheet5e extends ActorSheet {
_onItemDelete(event) { _onItemDelete(event) {
event.preventDefault(); event.preventDefault();
const li = event.currentTarget.closest(".item"); const li = event.currentTarget.closest(".item");
const item = this.actor.items.get(li.dataset.itemId); this.actor.deleteOwnedItem(li.dataset.itemId);
if (item) return item.delete();
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -840,7 +780,7 @@ export default class ActorSheet5e extends ActorSheet {
_onRollAbilityTest(event) { _onRollAbilityTest(event) {
event.preventDefault(); event.preventDefault();
let ability = event.currentTarget.parentElement.dataset.ability; let ability = event.currentTarget.parentElement.dataset.ability;
return this.actor.rollAbility(ability, {event: event}); this.actor.rollAbility(ability, {event: event});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -853,7 +793,7 @@ export default class ActorSheet5e extends ActorSheet {
_onRollSkillCheck(event) { _onRollSkillCheck(event) {
event.preventDefault(); event.preventDefault();
const skill = event.currentTarget.parentElement.dataset.skill; const skill = event.currentTarget.parentElement.dataset.skill;
return this.actor.rollSkill(skill, {event: event}); this.actor.rollSkill(skill, {event: event});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -866,7 +806,7 @@ export default class ActorSheet5e extends ActorSheet {
_onToggleAbilityProficiency(event) { _onToggleAbilityProficiency(event) {
event.preventDefault(); event.preventDefault();
const field = event.currentTarget.previousElementSibling; const field = event.currentTarget.previousElementSibling;
return this.actor.update({[field.name]: 1 - parseInt(field.value)}); this.actor.update({[field.name]: 1 - parseInt(field.value)});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -883,7 +823,7 @@ export default class ActorSheet5e extends ActorSheet {
const filter = li.dataset.filter; const filter = li.dataset.filter;
if ( set.has(filter) ) set.delete(filter); if ( set.has(filter) ) set.delete(filter);
else set.add(filter); else set.add(filter);
return this.render(); this.render();
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -899,7 +839,7 @@ export default class ActorSheet5e extends ActorSheet {
const label = a.parentElement.querySelector("label"); const label = a.parentElement.querySelector("label");
const choices = CONFIG.SW5E[a.dataset.options]; const choices = CONFIG.SW5E[a.dataset.options];
const options = { name: a.dataset.target, title: label.innerText, choices }; const options = { name: a.dataset.target, title: label.innerText, choices };
return new TraitSelector(this.actor, options).render(true); new TraitSelector(this.actor, options).render(true)
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -907,14 +847,15 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */ /** @override */
_getHeaderButtons() { _getHeaderButtons() {
let buttons = super._getHeaderButtons(); let buttons = super._getHeaderButtons();
if (this.actor.isPolymorphed) {
// Add button to revert polymorph
if ( !this.actor.isPolymorphed || this.actor.isToken ) return buttons;
buttons.unshift({ buttons.unshift({
label: "SW5E.PolymorphRestoreTransformation", label: 'SW5E.PolymorphRestoreTransformation',
class: "restore-transformation", class: "restore-transformation",
icon: "fas fa-backward", icon: "fas fa-backward",
onclick: () => this.actor.revertOriginalForm() onclick: ev => this.actor.revertOriginalForm()
}); });
}
return buttons; return buttons;
} }
} }

View file

@ -7,6 +7,7 @@ import Actor5e from "../../entity.js";
* @type {ActorSheet5e} * @type {ActorSheet5e}
*/ */
export default class ActorSheet5eCharacter extends ActorSheet5e { export default class ActorSheet5eCharacter extends ActorSheet5e {
/** /**
* Define default rendering options for the NPC sheet * Define default rendering options for the NPC sheet
* @return {Object} * @return {Object}
@ -44,12 +45,10 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// Experience Tracking // Experience Tracking
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", "); sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", ");
sheetData["multiclassLabels"] = this.actor.itemTypes.class sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => {
.map((c) => { return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ')
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" "); }).join(', ');
})
.join(", ");
// Return data for rendering // Return data for rendering
return sheetData; return sheetData;
@ -62,6 +61,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize items as inventory, powerbook, features, and classes // Categorize items as inventory, powerbook, features, and classes
const inventory = { const inventory = {
weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} }, weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} },
@ -73,23 +73,11 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
}; };
// Partition items by category // Partition items by category
let [ let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
items,
powers,
feats,
classes,
species,
archetypes,
classfeatures,
backgrounds,
fightingstyles,
fightingmasteries,
lightsaberforms
] = data.items.reduce(
(arr, item) => {
// Item details // Item details
item.img = item.img || CONST.DEFAULT_TOKEN; item.img = item.img || DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.attunement = { item.attunement = {
[CONFIG.SW5E.attunementTypes.REQUIRED]: { [CONFIG.SW5E.attunementTypes.REQUIRED]: {
icon: "fa-sun", icon: "fa-sun",
@ -104,19 +92,14 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
}[item.data.attunement]; }[item.data.attunement];
// Item usage // Item usage
item.hasUses = item.data.uses && item.data.uses.max > 0; item.hasUses = item.data.uses && (item.data.uses.max > 0);
item.isOnCooldown = item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
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.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
// Item toggle state // Item toggle state
this._prepareItemToggleState(item); this._prepareItemToggleState(item);
// Primary Class
if (item.type === "class")
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
// Classify items into types // Classify items into types
if ( item.type === "power" ) arr[1].push(item); if ( item.type === "power" ) arr[1].push(item);
else if ( item.type === "feat" ) arr[2].push(item); else if ( item.type === "feat" ) arr[2].push(item);
@ -130,9 +113,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
else if ( item.type === "lightsaberform" ) arr[10].push(item); else if ( item.type === "lightsaberform" ) arr[10].push(item);
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item); else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
return arr; return arr;
}, }, [[], [], [], [], [], [], [], [], [], [], []]);
[[], [], [], [], [], [], [], [], [], [], []]
);
// Apply active item filters // Apply active item filters
items = this._filterItems(items, this._filters.inventory); items = this._filterItems(items, this._filters.inventory);
@ -149,81 +130,28 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
const powerbook = this._preparePowerbook(data, powers); const powerbook = this._preparePowerbook(data, powers);
const nPrepared = powers.filter((s) => { const nPrepared = powers.filter(s => {
return s.data.level > 0 && s.data.preparation.mode === "prepared" && s.data.preparation.prepared; return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
}).length; }).length;
// Organize Features // Organize Features
const features = { const features = {
classes: { classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
label: "SW5E.ItemTypeClassPl", classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true },
items: [], archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
hasActions: false, species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
dataset: {type: "class"}, background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
isClass: 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 },
classfeatures: { lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
label: "SW5E.ItemTypeClassFeats", active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
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"} } passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
}; };
for ( let f of feats ) { for ( let f of feats ) {
if ( f.data.activation.type ) features.active.items.push(f); if ( f.data.activation.type ) features.active.items.push(f);
else features.passive.items.push(f); else features.passive.items.push(f);
} }
classes.sort((a, b) => b.data.levels - a.data.levels); classes.sort((a, b) => b.levels - a.levels);
features.classes.items = classes; features.classes.items = classes;
features.classfeatures.items = classfeatures; features.classfeatures.items = classfeatures;
features.archetype.items = archetypes; features.archetype.items = archetypes;
@ -256,7 +184,8 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
} else { }
else {
const isActive = getProperty(item.data, "equipped"); const isActive = getProperty(item.data, "equipped");
item.toggleClass = isActive ? "active" : ""; item.toggleClass = isActive ? "active" : "";
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
@ -269,18 +198,18 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/** /**
* Activate event listeners using the prepared sheet HTML * Activate event listeners using the prepared sheet HTML
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM * @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/ */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
if (!this.isEditable) return; if ( !this.options.editable ) return;
// Item State Toggling // Item State Toggling
html.find(".item-toggle").click(this._onToggleItem.bind(this)); html.find('.item-toggle').click(this._onToggleItem.bind(this));
// Short and Long Rest // Short and Long Rest
html.find(".short-rest").click(this._onShortRest.bind(this)); html.find('.short-rest').click(this._onShortRest.bind(this));
html.find(".long-rest").click(this._onLongRest.bind(this)); html.find('.long-rest').click(this._onLongRest.bind(this));
// Rollable sheet actions // Rollable sheet actions
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
@ -314,7 +243,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
_onToggleItem(event) { _onToggleItem(event) {
event.preventDefault(); event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId; const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId); const item = this.actor.getOwnedItem(itemId);
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
return item.update({[attr]: !getProperty(item.data, attr)}); return item.update({[attr]: !getProperty(item.data, attr)});
} }
@ -349,9 +278,10 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/** @override */ /** @override */
async _onDropItemCreate(itemData) { async _onDropItemCreate(itemData) {
// Increment the number of class levels a character instead of creating a new item // Increment the number of class levels a character instead of creating a new item
if ( itemData.type === "class" ) { if ( itemData.type === "class" ) {
const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name); const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
let priorLevel = cls?.data.data.levels ?? 0; let priorLevel = cls?.data.data.levels ?? 0;
if ( !!cls ) { if ( !!cls ) {
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
@ -363,6 +293,6 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
} }
// Default drop handling if levels were not added // Default drop handling if levels were not added
return super._onDropItemCreate(itemData); super._onDropItemCreate(itemData);
} }
} }

View file

@ -6,6 +6,7 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e} * @extends {ActorSheet5e}
*/ */
export default class ActorSheet5eNPC extends ActorSheet5e { export default class ActorSheet5eNPC extends ActorSheet5e {
/** @override */ /** @override */
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
@ -17,50 +18,32 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/** /**
* Organize Owned Items for rendering the NPC sheet * Organize Owned Items for rendering the NPC sheet
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize Items as Features and Powers // Categorize Items as Features and Powers
const features = { const features = {
weapons: { weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
label: game.i18n.localize("SW5E.AttackPl"), actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
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"} }, passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
}; };
// Start by classifying items into groups for rendering // Start by classifying items into groups for rendering
let [powers, other] = data.items.reduce( let [powers, other] = data.items.reduce((arr, item) => {
(arr, item) => { item.img = item.img || DEFAULT_TOKEN;
item.img = item.img || CONST.DEFAULT_TOKEN; item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; item.hasUses = item.data.uses && (item.data.uses.max > 0);
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.isOnCooldown = item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
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); if ( item.type === "power" ) arr[0].push(item);
else arr[1].push(item); else arr[1].push(item);
return arr; return arr;
}, }, [[], []]);
[[], []]
);
// Apply item filters // Apply item filters
powers = this._filterItems(powers, this._filters.powerbook); powers = this._filterItems(powers, this._filters.powerbook);
@ -75,7 +58,8 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
else if ( item.type === "feat" ) { else if ( item.type === "feat" ) {
if ( item.data.activation.type ) features.actions.items.push(item); if ( item.data.activation.type ) features.actions.items.push(item);
else features.passive.items.push(item); else features.passive.items.push(item);
} else features.equipment.items.push(item); }
else features.equipment.items.push(item);
} }
// Assign and return // Assign and return
@ -83,19 +67,17 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
data.powerbook = powerbook; data.powerbook = powerbook;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
getData(options) { getData() {
const data = super.getData(options); const data = super.getData();
// Challenge Rating // Challenge Rating
const cr = parseFloat(data.data.details.cr || 0); const cr = parseFloat(data.data.details.cr || 0);
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; 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; data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
// Creature Type
data.labels["type"] = this.actor.labels.creatureType;
return data; return data;
} }
@ -104,7 +86,8 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _updateObject(event, formData) { _updateObject(event, formData) {
// Format NPC Challenge Rating // Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr"; let crv = "data.details.cr";
@ -113,7 +96,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps // Parent ActorSheet update steps
return super._updateObject(event, formData); super._updateObject(event, formData);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

@ -20,17 +20,12 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/** /**
* Creates a new cargo entry for a vehicle Actor. * Creates a new cargo entry for a vehicle Actor.
*/ */
static get newCargo() { static get newCargo() {
return { return {
name: "", name: '',
quantity: 1 quantity: 1
}; };
} }
@ -45,6 +40,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private * @private
*/ */
_computeEncumbrance(totalWeight, actorData) { _computeEncumbrance(totalWeight, actorData) {
// Compute currency weight // Compute currency weight
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
@ -73,24 +69,25 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private * @private
*/ */
_prepareCrewedItem(item) { _prepareCrewedItem(item) {
// Determine crewed status // Determine crewed status
const isCrewed = item.data.crewed; const isCrewed = item.data.crewed;
item.toggleClass = isCrewed ? "active" : ""; item.toggleClass = isCrewed ? 'active' : '';
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`); item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
// Handle crew actions // Handle crew actions
if (item.type === "feat" && item.data.activation.type === "crew") { if (item.type === 'feat' && item.data.activation.type === 'crew') {
item.crew = item.data.activation.cost; item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`); item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
if (item.data.cover === 0.5) item.cover = "½"; if (item.data.cover === .5) item.cover = '½';
else if (item.data.cover === 0.75) item.cover = "¾"; else if (item.data.cover === .75) item.cover = '¾';
else if (item.data.cover === null) item.cover = "—"; else if (item.data.cover === null) item.cover = '—';
if (item.crew < 1 || item.crew === null) item.crew = "—"; if (item.crew < 1 || item.crew === null) item.crew = '—';
} }
// Prepare vehicle weapons // Prepare vehicle weapons
if (item.type === "equipment" || item.type === "weapon") { if (item.type === 'equipment' || item.type === 'weapon') {
item.threshold = item.data.hp.dt ? item.data.hp.dt : "—"; item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
} }
} }
@ -101,162 +98,132 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
const cargoColumns = [ const cargoColumns = [{
{ label: game.i18n.localize('SW5E.Quantity'),
label: game.i18n.localize("SW5E.Quantity"), css: 'item-qty',
css: "item-qty", property: 'quantity',
property: "quantity", editable: 'Number'
editable: "Number" }];
}
];
const equipmentColumns = [ const equipmentColumns = [{
{ label: game.i18n.localize('SW5E.Quantity'),
label: game.i18n.localize("SW5E.Quantity"), css: 'item-qty',
css: "item-qty", property: 'data.quantity'
property: "data.quantity" }, {
}, label: game.i18n.localize('SW5E.AC'),
{ css: 'item-ac',
label: game.i18n.localize("SW5E.AC"), property: 'data.armor.value'
css: "item-ac", }, {
property: "data.armor.value" label: game.i18n.localize('SW5E.HP'),
}, css: 'item-hp',
{ property: 'data.hp.value',
label: game.i18n.localize("SW5E.HP"), editable: 'Number'
css: "item-hp", }, {
property: "data.hp.value", label: game.i18n.localize('SW5E.Threshold'),
editable: "Number" css: 'item-threshold',
}, property: 'threshold'
{ }];
label: game.i18n.localize("SW5E.Threshold"),
css: "item-threshold",
property: "threshold"
}
];
const features = { const features = {
actions: { actions: {
label: game.i18n.localize("SW5E.ActionPl"), label: game.i18n.localize('SW5E.ActionPl'),
items: [], items: [],
crewable: true, crewable: true,
dataset: {"type": "feat", "activation.type": "crew"}, dataset: {type: 'feat', 'activation.type': 'crew'},
columns: [ columns: [{
{ label: game.i18n.localize('SW5E.VehicleCrew'),
label: game.i18n.localize("SW5E.VehicleCrew"), css: 'item-crew',
css: "item-crew", property: 'crew'
property: "crew" }, {
}, label: game.i18n.localize('SW5E.Cover'),
{ css: 'item-cover',
label: game.i18n.localize("SW5E.Cover"), property: 'cover'
css: "item-cover", }]
property: "cover"
}
]
}, },
equipment: { equipment: {
label: game.i18n.localize("SW5E.ItemTypeEquipment"), label: game.i18n.localize('SW5E.ItemTypeEquipment'),
items: [], items: [],
crewable: true, crewable: true,
dataset: {"type": "equipment", "armor.type": "vehicle"}, dataset: {type: 'equipment', 'armor.type': 'vehicle'},
columns: equipmentColumns columns: equipmentColumns
}, },
passive: { passive: {
label: game.i18n.localize("SW5E.Features"), label: game.i18n.localize('SW5E.Features'),
items: [], items: [],
dataset: {type: "feat"} dataset: {type: 'feat'}
}, },
reactions: { reactions: {
label: game.i18n.localize("SW5E.ReactionPl"), label: game.i18n.localize('SW5E.ReactionPl'),
items: [], items: [],
dataset: {"type": "feat", "activation.type": "reaction"} dataset: {type: 'feat', 'activation.type': 'reaction'}
}, },
weapons: { weapons: {
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
items: [], items: [],
crewable: true, crewable: true,
dataset: {"type": "weapon", "weapon-type": "siege"}, dataset: {type: 'weapon', 'weapon-type': 'siege'},
columns: equipmentColumns columns: equipmentColumns
} }
}; };
const cargo = { const cargo = {
crew: { crew: {
label: game.i18n.localize("SW5E.VehicleCrew"), label: game.i18n.localize('SW5E.VehicleCrew'),
items: data.data.cargo.crew, items: data.data.cargo.crew,
css: "cargo-row crew", css: 'cargo-row crew',
editableName: true, editableName: true,
dataset: {type: "crew"}, dataset: {type: 'crew'},
columns: cargoColumns columns: cargoColumns
}, },
passengers: { passengers: {
label: game.i18n.localize("SW5E.VehiclePassengers"), label: game.i18n.localize('SW5E.VehiclePassengers'),
items: data.data.cargo.passengers, items: data.data.cargo.passengers,
css: "cargo-row passengers", css: 'cargo-row passengers',
editableName: true, editableName: true,
dataset: {type: "passengers"}, dataset: {type: 'passengers'},
columns: cargoColumns columns: cargoColumns
}, },
cargo: { cargo: {
label: game.i18n.localize("SW5E.VehicleCargo"), label: game.i18n.localize('SW5E.VehicleCargo'),
items: [], items: [],
dataset: {type: "loot"}, dataset: {type: 'loot'},
columns: [ columns: [{
{ label: game.i18n.localize('SW5E.Quantity'),
label: game.i18n.localize("SW5E.Quantity"), css: 'item-qty',
css: "item-qty", property: 'data.quantity',
property: "data.quantity", editable: 'Number'
editable: "Number" }, {
}, label: game.i18n.localize('SW5E.Price'),
{ css: 'item-price',
label: game.i18n.localize("SW5E.Price"), property: 'data.price',
css: "item-price", editable: 'Number'
property: "data.price", }, {
editable: "Number" label: game.i18n.localize('SW5E.Weight'),
}, css: 'item-weight',
{ property: 'data.weight',
label: game.i18n.localize("SW5E.Weight"), editable: 'Number'
css: "item-weight", }]
property: "data.weight",
editable: "Number"
}
]
} }
}; };
// Classify items owned by the vehicle and compute total cargo weight
let totalWeight = 0; let totalWeight = 0;
for (const item of data.items) { for (const item of data.items) {
this._prepareCrewedItem(item); this._prepareCrewedItem(item);
if (item.type === 'weapon') features.weapons.items.push(item);
// Handle cargo explicitly else if (item.type === 'equipment') features.equipment.items.push(item);
const isCargo = item.flags.sw5e?.vehicleCargo === true; else if (item.type === 'loot') {
if (isCargo) {
totalWeight += (item.data.weight || 0) * item.data.quantity; totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item); cargo.cargo.items.push(item);
continue;
} }
else if (item.type === 'feat') {
// Handle non-cargo item types if (!item.data.activation.type || item.data.activation.type === 'none') {
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); features.passive.items.push(item);
else if (item.data.activation.type === "reaction") features.reactions.items.push(item); }
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
else features.actions.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.features = Object.values(features);
data.cargo = Object.values(cargo); data.cargo = Object.values(cargo);
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data); data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
@ -269,23 +236,23 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/** @override */ /** @override */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
if (!this.isEditable) return; if (!this.options.editable) return;
html.find(".item-toggle").click(this._onToggleItem.bind(this)); html.find('.item-toggle').click(this._onToggleItem.bind(this));
html.find(".item-hp input") html.find('.item-hp input')
.click((evt) => evt.target.select()) .click(evt => evt.target.select())
.change(this._onHPChange.bind(this)); .change(this._onHPChange.bind(this));
html.find(".item:not(.cargo-row) input[data-property]") html.find('.item:not(.cargo-row) input[data-property]')
.click((evt) => evt.target.select()) .click(evt => evt.target.select())
.change(this._onEditInSheet.bind(this)); .change(this._onEditInSheet.bind(this));
html.find(".cargo-row input") html.find('.cargo-row input')
.click((evt) => evt.target.select()) .click(evt => evt.target.select())
.change(this._onCargoRowChange.bind(this)); .change(this._onCargoRowChange.bind(this));
if (this.actor.data.data.attributes.actions.stations) { if (this.actor.data.data.attributes.actions.stations) {
html.find(".counter.actions, .counter.action-thresholds").hide(); html.find('.counter.actions, .counter.action-thresholds').hide();
} }
} }
@ -300,20 +267,20 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
_onCargoRowChange(event) { _onCargoRowChange(event) {
event.preventDefault(); event.preventDefault();
const target = event.currentTarget; const target = event.currentTarget;
const row = target.closest(".item"); const row = target.closest('.item');
const idx = Number(row.dataset.itemId); const idx = Number(row.dataset.itemId);
const property = row.classList.contains("crew") ? "crew" : "passengers"; const property = row.classList.contains('crew') ? 'crew' : 'passengers';
// Get the cargo entry // Get the cargo entry
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); const cargo = duplicate(this.actor.data.data.cargo[property]);
const entry = cargo[idx]; const entry = cargo[idx];
if (!entry) return null; if (!entry) return null;
// Update the cargo value // Update the cargo value
const key = target.dataset.property || "name"; const key = target.dataset.property || 'name';
const type = target.dataset.dtype; const type = target.dataset.dtype;
let value = target.value; let value = target.value;
if (type === "Number") value = Number(value); if (type === 'Number') value = Number(value);
entry[key] = value; entry[key] = value;
// Perform the Actor update // Perform the Actor update
@ -330,18 +297,14 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onEditInSheet(event) { _onEditInSheet(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId; const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const property = event.currentTarget.dataset.property; const property = event.currentTarget.dataset.property;
const type = event.currentTarget.dataset.dtype; const type = event.currentTarget.dataset.dtype;
let value = event.currentTarget.value; let value = event.currentTarget.value;
switch (type) { switch (type) {
case "Number": case 'Number': value = parseInt(value); break;
value = parseInt(value); case 'Boolean': value = value === 'true'; break;
break;
case "Boolean":
value = value === "true";
break;
} }
return item.update({[`${property}`]: value}); return item.update({[`${property}`]: value});
} }
@ -358,8 +321,8 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
event.preventDefault(); event.preventDefault();
const target = event.currentTarget; const target = event.currentTarget;
const type = target.dataset.type; const type = target.dataset.type;
if (type === "crew" || type === "passengers") { if (type === 'crew' || type === 'passengers') {
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); const cargo = duplicate(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo); cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo}); return this.actor.update({[`data.cargo.${type}`]: cargo});
} }
@ -376,11 +339,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onItemDelete(event) { _onItemDelete(event) {
event.preventDefault(); event.preventDefault();
const row = event.currentTarget.closest(".item"); const row = event.currentTarget.closest('.item');
if (row.classList.contains("cargo-row")) { if (row.classList.contains('cargo-row')) {
const idx = Number(row.dataset.itemId); const idx = Number(row.dataset.itemId);
const type = row.classList.contains("crew") ? "crew" : "passengers"; const type = row.classList.contains('crew') ? 'crew' : 'passengers';
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); const cargo = duplicate(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo}); return this.actor.update({[`data.cargo.${type}`]: cargo});
} }
@ -389,16 +352,6 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @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. * Special handling for editing HP to clamp it within appropriate range.
* @param event {Event} * @param event {Event}
@ -407,11 +360,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onHPChange(event) { _onHPChange(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId; const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
event.currentTarget.value = hp; event.currentTarget.value = hp;
return item.update({"data.hp.value": hp}); return item.update({'data.hp.value': hp});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -424,9 +377,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onToggleItem(event) { _onToggleItem(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId; const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const crewed = !!item.data.data.crewed; const crewed = !!item.data.data.crewed;
return item.update({"data.crewed": !crewed}); return item.update({'data.crewed': !crewed});
}
} }
};

View file

@ -39,15 +39,12 @@ export default class AbilityUseDialog extends Dialog {
// Prepare dialog form data // Prepare dialog form data
const data = { const data = {
item: item.data, item: item.data,
title: game.i18n.format("SW5E.AbilityUseHint", { title: game.i18n.format("SW5E.AbilityUseHint", item.data),
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
name: item.name
}),
note: this._getAbilityUseNote(item.data, uses, recharge), note: this._getAbilityUseNote(item.data, uses, recharge),
consumePowerSlot: false, consumePowerSlot: false,
consumeRecharge: recharges, consumeRecharge: recharges,
consumeResource: !!itemData.consume.target, consumeResource: !!itemData.consume.target,
consumeUses: uses.per && uses.max > 0, consumeUses: uses.max,
canUse: recharges ? recharge.charged : sufficientUses, canUse: recharges ? recharge.charged : sufficientUses,
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget, createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
errors: [] errors: []
@ -62,13 +59,13 @@ export default class AbilityUseDialog extends Dialog {
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use")); const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
return new Promise((resolve) => { return new Promise((resolve) => {
const dlg = new this(item, { const dlg = new this(item, {
title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`, title: `${item.name}: Usage Configuration`,
content: html, content: html,
buttons: { buttons: {
use: { use: {
icon: `<i class="fas ${icon}"></i>`, icon: `<i class="fas ${icon}"></i>`,
label: label, label: label,
callback: (html) => { callback: html => {
const fd = new FormDataExtended(html[0].querySelector("form")); const fd = new FormDataExtended(html[0].querySelector("form"));
resolve(fd.toObject()); resolve(fd.toObject());
} }
@ -90,9 +87,10 @@ export default class AbilityUseDialog extends Dialog {
* @private * @private
*/ */
static _getPowerData(actorData, itemData, data) { static _getPowerData(actorData, itemData, data) {
// Determine whether the power may be up-cast // Determine whether the power may be up-cast
const lvl = itemData.level; const lvl = itemData.level;
const consumePowerSlot = lvl > 0 && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode); 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 can't upcast, return early and don't bother calculating available power slots
if (!consumePowerSlot) { if (!consumePowerSlot) {
@ -108,74 +106,70 @@ export default class AbilityUseDialog extends Dialog {
case "lgt": case "lgt":
case "uni": case "uni":
case "drk": { case "drk": {
powerType = "force"; powerType = "force"
points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp; points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp;
break; break;
} }
case "tec": { case "tec": {
powerType = "tech"; powerType = "tech"
points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp; points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
break; break;
} }
} }
// eliminate point usage for innate casters // eliminate point usage for innate casters
if (actorData.attributes.powercasting === "innate") points = 999; if (actorData.attributes.powercasting === 'innate') points = 999;
let powerLevels;
let powerLevels
if (powerType === "force"){ if (powerType === "force"){
powerLevels = Array.fromRange(10) powerLevels = Array.fromRange(10).reduce((arr, i) => {
.reduce((arr, i) => {
if ( i < lvl ) return arr; if ( i < lvl ) return arr;
const label = CONFIG.SW5E.powerLevels[i]; const label = CONFIG.SW5E.powerLevels[i];
const l = actorData.powers["power"+i] || {fmax: 0, foverride: null}; const l = actorData.powers["power"+i] || {fmax: 0, foverride: null};
let max = parseInt(l.foverride || l.fmax || 0); let max = parseInt(l.foverride || l.fmax || 0);
let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max); let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max);
if ( max > 0 ) lmax = i; if ( max > 0 ) lmax = i;
if (max > 0 && slots > 0 && points > i) { if ((max > 0) && (slots > 0) && (points > i)){
arr.push({ arr.push({
level: i, level: i,
label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label, label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
canCast: max > 0, canCast: max > 0,
hasSlots: slots > 0 hasSlots: slots > 0
}); });
} }
return arr; return arr;
}, []) }, []).filter(sl => sl.level <= lmax);
.filter((sl) => sl.level <= lmax);
}else if (powerType === "tech"){ }else if (powerType === "tech"){
powerLevels = Array.fromRange(10) powerLevels = Array.fromRange(10).reduce((arr, i) => {
.reduce((arr, i) => {
if ( i < lvl ) return arr; if ( i < lvl ) return arr;
const label = CONFIG.SW5E.powerLevels[i]; const label = CONFIG.SW5E.powerLevels[i];
const l = actorData.powers["power"+i] || {tmax: 0, toverride: null}; const l = actorData.powers["power"+i] || {tmax: 0, toverride: null};
let max = parseInt(l.override || l.tmax || 0); let max = parseInt(l.override || l.tmax || 0);
let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max); let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max);
if ( max > 0 ) lmax = i; if ( max > 0 ) lmax = i;
if (max > 0 && slots > 0 && points > i) { if ((max > 0) && (slots > 0) && (points > i)){
arr.push({ arr.push({
level: i, level: i,
label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label, label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
canCast: max > 0, canCast: max > 0,
hasSlots: slots > 0 hasSlots: slots > 0
}); });
} }
return arr; return arr;
}, []) }, []).filter(sl => sl.level <= lmax);
.filter((sl) => sl.level <= lmax);
} }
const canCast = powerLevels.some((l) => l.hasSlots);
if (!canCast)
data.errors.push( const canCast = powerLevels.some(l => l.hasSlots);
game.i18n.format("SW5E.PowerCastNoSlots", { if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", {
level: CONFIG.SW5E.powerLevels[lvl], level: CONFIG.SW5E.powerLevels[lvl],
name: data.item.name name: data.item.name
}) }));
);
// Merge power casting data // Merge power casting data
return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels}); return mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -185,6 +179,7 @@ export default class AbilityUseDialog extends Dialog {
* @private * @private
*/ */
static _getAbilityUseNote(item, uses, recharge) { static _getAbilityUseNote(item, uses, recharge) {
// Zero quantity // Zero quantity
const quantity = item.data.quantity; const quantity = item.data.quantity;
if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint"); if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
@ -192,8 +187,8 @@ export default class AbilityUseDialog extends Dialog {
// Abilities which use Recharge // Abilities which use Recharge
if ( !!recharge.value ) { if ( !!recharge.value ) {
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", { return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`) type: item.type,
}); })
} }
// Does not use any resource // Does not use any resource
@ -206,7 +201,7 @@ export default class AbilityUseDialog extends Dialog {
else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint"; else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint";
else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint"; else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint";
return game.i18n.format(str, { return game.i18n.format(str, {
type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`), type: item.data.consumableType,
value: uses.value, value: uses.value,
quantity: item.data.quantity, quantity: item.data.quantity,
max: uses.max, max: uses.max,
@ -217,11 +212,17 @@ export default class AbilityUseDialog extends Dialog {
// Other Items // Other Items
else { else {
return game.i18n.format("SW5E.AbilityUseNormalHint", { return game.i18n.format("SW5E.AbilityUseNormalHint", {
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), type: item.type,
value: uses.value, value: uses.value,
max: uses.max, max: uses.max,
per: CONFIG.SW5E.limitedUsePeriods[uses.per] per: CONFIG.SW5E.limitedUsePeriods[uses.per]
}); });
} }
} }
/* -------------------------------------------- */
static _handleSubmit(formData, item) {
}
} }

View file

@ -1,10 +1,11 @@
/** /**
* An application class which provides advanced configuration for special character flags which modify an Actor * An application class which provides advanced configuration for special character flags which modify an Actor
* @implements {DocumentSheet} * @implements {BaseEntitySheet}
*/ */
export default class ActorSheetFlags extends DocumentSheet { export default class ActorSheetFlags extends BaseEntitySheet {
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { const options = super.defaultOptions;
return mergeObject(options, {
id: "actor-flags", id: "actor-flags",
classes: ["sw5e"], classes: ["sw5e"],
template: "systems/sw5e/templates/apps/actor-flags.html", template: "systems/sw5e/templates/apps/actor-flags.html",
@ -17,7 +18,7 @@ export default class ActorSheetFlags extends DocumentSheet {
/** @override */ /** @override */
get title() { get title() {
return `${game.i18n.localize("SW5E.FlagsTitle")}: ${this.object.name}`; return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -26,7 +27,6 @@ export default class ActorSheetFlags extends DocumentSheet {
getData() { getData() {
const data = {}; const data = {};
data.actor = this.object; data.actor = this.object;
data.classes = this._getClasses();
data.flags = this._getFlags(); data.flags = this._getFlags();
data.bonuses = this._getBonuses(); data.bonuses = this._getBonuses();
return data; return data;
@ -34,38 +34,20 @@ export default class ActorSheetFlags extends DocumentSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* 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 * Prepare an object of flags data which groups flags by section
* Add some additional data for rendering * Add some additional data for rendering
* @return {object} * @return {object}
* @private
*/ */
_getFlags() { _getFlags() {
const flags = {}; const flags = {};
const baseData = this.document.toJSON(); const baseData = this.entity._data;
for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) { for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) {
if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {}; if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {};
let flag = foundry.utils.deepClone(v); let flag = duplicate(v);
flag.type = v.type.name; flag.type = v.type.name;
flag.isCheckbox = v.type === Boolean; flag.isCheckbox = v.type === Boolean;
flag.isSelect = v.hasOwnProperty("choices"); flag.isSelect = v.hasOwnProperty('choices');
flag.value = getProperty(baseData.flags, `sw5e.${k}`); flag.value = getProperty(baseData.flags, `sw5e.${k}`);
flags[v.section][`flags.sw5e.${k}`] = flag; flags[v.section][`flags.sw5e.${k}`] = flag;
} }
@ -115,7 +97,7 @@ export default class ActorSheetFlags extends DocumentSheet {
let unset = false; let unset = false;
const flags = updateData.flags.sw5e; const flags = updateData.flags.sw5e;
//clone flags to dnd5e for module compatability //clone flags to dnd5e for module compatability
updateData.flags.dnd5e = updateData.flags.sw5e; updateData.flags.dnd5e = updateData.flags.sw5e
for ( let [k, v] of Object.entries(flags) ) { for ( let [k, v] of Object.entries(flags) ) {
if ( [undefined, null, "", false, 0].includes(v) ) { if ( [undefined, null, "", false, 0].includes(v) ) {
delete flags[k]; delete flags[k];

View file

@ -1,111 +0,0 @@
import Actor5e from "../actor/entity.js";
/**
* A specialized form used to select from a checklist of attributes, traits, or properties
* @extends {FormApplication}
*/
export default class ActorTypeConfig extends FormApplication {
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e", "actor-type", "trait-selector"],
template: "systems/sw5e/templates/apps/actor-type.html",
title: "SW5E.CreatureTypeTitle",
width: 280,
height: "auto",
choices: {},
allowCustom: true,
minimum: 0,
maximum: null
});
}
/* -------------------------------------------- */
/** @override */
get id() {
return `actor-type-${this.object.id}`;
}
/* -------------------------------------------- */
/** @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: ""
};
// Populate choices
const types = {};
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
return {
types: types,
custom: {
value: attr.custom,
label: game.i18n.localize("SW5E.CreatureTypeSelectorCustom"),
chosen: attr.value === "custom"
},
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;
}, {}),
preview: Actor5e.formatCreatureType(attr) || ""
};
}
/* -------------------------------------------- */
/** @override */
async _updateObject(event, formData) {
const typeObject = foundry.utils.expandObject(formData);
return this.object.update({"data.details.type": typeObject});
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @inheritdoc */
activateListeners(html) {
super.activateListeners(html);
html.find("input[name='custom']").focusin(this._onCustomFieldFocused.bind(this));
}
/* -------------------------------------------- */
/** @inheritdoc */
_onChangeInput(event) {
super._onChangeInput(event);
const typeObject = foundry.utils.expandObject(this._getSubmitData());
this.form["preview"].value = Actor5e.formatCreatureType(typeObject) || "—";
}
/* -------------------------------------------- */
/**
* Select the custom radio button when the custom text field is focused.
* @param {FocusEvent} event The original focusin event
* @private
*/
_onCustomFieldFocused(event) {
this.form.querySelector("input[name='value'][value='custom']").checked = true;
this._onChangeInput(event);
}
}

View file

@ -1,92 +0,0 @@
/**
* A simple form to set actor hit dice amounts
* @implements {DocumentSheet}
*/
export default class ActorHitDiceConfig extends DocumentSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e", "hd-config", "dialog"],
template: "systems/sw5e/templates/apps/hit-dice-config.html",
width: 360,
height: "auto"
});
}
/* -------------------------------------------- */
/** @override */
get title() {
return `${game.i18n.localize("SW5E.HitDiceConfig")}: ${this.object.name}`;
}
/* -------------------------------------------- */
/** @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)))
};
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Hook up -/+ buttons to adjust the current value in the form
html.find("button.increment,button.decrement").click((event) => {
const button = event.currentTarget;
const current = button.parentElement.querySelector(".current");
const max = button.parentElement.querySelector(".max");
const direction = button.classList.contains("increment") ? 1 : -1;
current.value = Math.clamped(parseInt(current.value) + direction, 0, parseInt(max.value));
});
html.find("button.roll-hd").click(this._onRollHitDie.bind(this));
}
/* -------------------------------------------- */
/** @override */
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
}));
return this.object.updateEmbeddedDocuments("Item", classUpdates);
}
/* -------------------------------------------- */
/**
* Rolls the hit die corresponding with the class row containing the event's target button.
* @param {MouseEvent} event
* @private
*/
async _onRollHitDie(event) {
event.preventDefault();
const button = event.currentTarget;
await this.object.rollHitDie(button.dataset.hdDenom);
// Re-render dialog to reflect changed hit dice quantities
this.render();
}
}

View file

@ -40,25 +40,27 @@ export default class LongRestDialog extends Dialog {
static async longRestDialog({ actor } = {}) { static async longRestDialog({ actor } = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const dlg = new this(actor, { const dlg = new this(actor, {
title: game.i18n.localize("SW5E.LongRest"), title: "Long Rest",
buttons: { buttons: {
rest: { rest: {
icon: '<i class="fas fa-bed"></i>', icon: '<i class="fas fa-bed"></i>',
label: game.i18n.localize("SW5E.Rest"), label: "Rest",
callback: (html) => { callback: html => {
let newDay = true; let newDay = false;
if (game.settings.get("sw5e", "restVariant") !== "gritty") if (game.settings.get("sw5e", "restVariant") === "normal")
newDay = html.find('input[name="newDay"]')[0].checked; newDay = html.find('input[name="newDay"]')[0].checked;
else if(game.settings.get("sw5e", "restVariant") === "gritty")
newDay = true;
resolve(newDay); resolve(newDay);
} }
}, },
cancel: { cancel: {
icon: '<i class="fas fa-times"></i>', icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("Cancel"), label: "Cancel",
callback: reject callback: reject
} }
}, },
default: "rest", default: 'rest',
close: reject close: reject
}); });
dlg.render(true); dlg.render(true);

View file

@ -1,11 +1,12 @@
/** /**
* A simple form to set actor movement speeds * A simple form to set actor movement speeds
* @extends {DocumentSheet} * @implements {BaseEntitySheet}
*/ */
export default class ActorMovementConfig extends DocumentSheet { export default class ActorMovementConfig extends BaseEntitySheet {
/** @override */ /** @override */
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["sw5e"], classes: ["sw5e"],
template: "systems/sw5e/templates/apps/movement-config.html", template: "systems/sw5e/templates/apps/movement-config.html",
width: 300, width: 300,
@ -17,18 +18,17 @@ export default class ActorMovementConfig extends DocumentSheet {
/** @override */ /** @override */
get title() { get title() {
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`; return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.entity.name}`;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
getData(options) { getData(options) {
const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {};
const data = { const data = {
movement: foundry.utils.deepClone(sourceMovement), movement: duplicate(this.entity._data.data.attributes.movement),
units: CONFIG.SW5E.movementUnits units: CONFIG.SW5E.movementUnits
}; }
for ( let [k, v] of Object.entries(data.movement) ) { for ( let [k, v] of Object.entries(data.movement) ) {
if ( ["units", "hover"].includes(k) ) continue; if ( ["units", "hover"].includes(k) ) continue;
data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0; data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;

View file

@ -1,66 +0,0 @@
/**
* A Dialog to prompt the user to select from a list of items.
* @type {Dialog}
*/
export default class SelectItemsPrompt extends Dialog {
constructor(items, dialogData = {}, options = {}) {
super(dialogData, options);
this.options.classes = ["sw5e", "dialog", "select-items-prompt", "sheet"];
/**
* Store a reference to the Item entities being used
* @type {Array<Item5e>}
*/
this.items = items;
}
activateListeners(html) {
super.activateListeners(html);
// render the item's sheet if its image is clicked
html.on("click", ".item-image", (event) => {
const item = this.items.find((feature) => feature.id === event.currentTarget.dataset?.itemId);
item?.sheet.render(true);
});
}
/**
* A constructor function which displays the AddItemPrompt app for a given Actor and Item set.
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
* @param {Array<Item5e>} items
* @param {Object} options
* @param {string} options.hint - Localized hint to display at the top of the prompt
* @return {Promise<string[]>} - list of item ids which the user has selected
*/
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"),
content: html,
buttons: {
apply: {
icon: `<i class="fas fa-user-plus"></i>`,
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]);
resolve(selectedIds);
}
},
cancel: {
icon: '<i class="fas fa-forward"></i>',
label: game.i18n.localize("SW5E.Skip"),
callback: () => resolve([])
}
},
default: "apply",
close: () => resolve([])
});
dlg.render(true);
});
}
}

View file

@ -1,11 +1,12 @@
/** /**
* A simple form to set Actor movement speeds. * A simple form to set actor movement speeds
* @extends {DocumentSheet} * @implements {BaseEntitySheet}
*/ */
export default class ActorSensesConfig extends DocumentSheet { export default class ActorSensesConfig extends BaseEntitySheet {
/** @inheritdoc */
/** @override */
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["sw5e"], classes: ["sw5e"],
template: "systems/sw5e/templates/apps/senses-config.html", template: "systems/sw5e/templates/apps/senses-config.html",
width: 300, width: 300,
@ -15,28 +16,27 @@ export default class ActorSensesConfig extends DocumentSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
get title() { get title() {
return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`; return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.entity.name}`;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
getData(options) { getData(options) {
const senses = foundry.utils.getProperty(this.document.data._source, "data.attributes.senses") || {}; const senses = this.entity._data.data.attributes?.senses ?? {};
const data = { const data = {
senses: {}, senses: {},
special: senses.special ?? "", special: senses.special ?? "",
units: senses.units, units: senses.units, movementUnits: CONFIG.SW5E.movementUnits
movementUnits: CONFIG.SW5E.movementUnits
}; };
for ( let [name, label] of Object.entries(CONFIG.SW5E.senses) ) { for ( let [name, label] of Object.entries(CONFIG.SW5E.senses) ) {
const v = senses[name]; const v = senses[name];
data.senses[name] = { data.senses[name] = {
label: game.i18n.localize(label), label: game.i18n.localize(label),
value: Number.isNumeric(v) ? v.toNearest(0.1) : 0 value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
}; }
} }
return data; return data;
} }

View file

@ -40,7 +40,7 @@ export default class ShortRestDialog extends Dialog {
// Determine Hit Dice // Determine Hit Dice
data.availableHD = this.actor.data.items.reduce((hd, item) => { data.availableHD = this.actor.data.items.reduce((hd, item) => {
if ( item.type === "class" ) { if ( item.type === "class" ) {
const d = item.data.data; const d = item.data;
const denom = d.hitDice || "d6"; const denom = d.hitDice || "d6";
const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0); const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
hd[denom] = denom in hd ? hd[denom] + available : available; hd[denom] = denom in hd ? hd[denom] + available : available;
@ -59,6 +59,7 @@ export default class ShortRestDialog extends Dialog {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
@ -92,12 +93,12 @@ export default class ShortRestDialog extends Dialog {
static async shortRestDialog({actor}={}) { static async shortRestDialog({actor}={}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const dlg = new this(actor, { const dlg = new this(actor, {
title: game.i18n.localize("SW5E.ShortRest"), title: "Short Rest",
buttons: { buttons: {
rest: { rest: {
icon: '<i class="fas fa-bed"></i>', icon: '<i class="fas fa-bed"></i>',
label: game.i18n.localize("SW5E.Rest"), label: "Rest",
callback: (html) => { callback: html => {
let newDay = false; let newDay = false;
if (game.settings.get("sw5e", "restVariant") === "gritty") if (game.settings.get("sw5e", "restVariant") === "gritty")
newDay = html.find('input[name="newDay"]')[0].checked; newDay = html.find('input[name="newDay"]')[0].checked;
@ -106,7 +107,7 @@ export default class ShortRestDialog extends Dialog {
}, },
cancel: { cancel: {
icon: '<i class="fas fa-times"></i>', icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("Cancel"), label: "Cancel",
callback: reject callback: reject
} }
}, },
@ -126,9 +127,7 @@ export default class ShortRestDialog extends Dialog {
* @return {Promise} * @return {Promise}
*/ */
static async longRestDialog({actor}={}) { static async longRestDialog({actor}={}) {
console.warn( console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead.");
"WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead."
);
return LongRestDialog.longRestDialog(...arguments); return LongRestDialog.longRestDialog(...arguments);
} }
} }

View file

@ -1,13 +1,14 @@
/** /**
* A specialized form used to select from a checklist of attributes, traits, or properties * A specialized form used to select from a checklist of attributes, traits, or properties
* @extends {DocumentSheet} * @implements {FormApplication}
*/ */
export default class TraitSelector extends DocumentSheet { export default class TraitSelector extends FormApplication {
/** @inheritdoc */
/** @override */
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
id: "trait-selector", id: "trait-selector",
classes: ["sw5e", "trait-selector", "subconfig"], classes: ["sw5e"],
title: "Actor Trait Selection", title: "Actor Trait Selection",
template: "systems/sw5e/templates/apps/trait-selector.html", template: "systems/sw5e/templates/apps/trait-selector.html",
width: 320, width: 320,
@ -15,9 +16,7 @@ export default class TraitSelector extends DocumentSheet {
choices: {}, choices: {},
allowCustom: true, allowCustom: true,
minimum: 0, minimum: 0,
maximum: null, maximum: null
valueKey: "value",
customKey: "custom"
}); });
} }
@ -25,7 +24,7 @@ export default class TraitSelector extends DocumentSheet {
/** /**
* Return a reference to the target attribute * Return a reference to the target attribute
* @type {string} * @type {String}
*/ */
get attribute() { get attribute() {
return this.options.name; return this.options.name;
@ -35,50 +34,52 @@ export default class TraitSelector extends DocumentSheet {
/** @override */ /** @override */
getData() { getData() {
const attr = foundry.utils.getProperty(this.object.data, this.attribute);
const o = this.options; // Get current values
const value = o.valueKey ? attr[o.valueKey] ?? [] : attr; let attr = getProperty(this.object._data, this.attribute);
const custom = o.customKey ? attr[o.customKey] ?? "" : ""; if ( getType(attr) !== "Object" ) attr = {value: [], custom: ""};
// Populate choices // Populate choices
const choices = Object.entries(o.choices).reduce((obj, e) => { const choices = duplicate(this.options.choices);
let [k, v] = e; for ( let [k, v] of Object.entries(choices) ) {
obj[k] = {label: v, chosen: attr ? value.includes(k) : false}; choices[k] = {
return obj; label: v,
}, {}); chosen: attr ? attr.value.includes(k) : false
}
}
// Return data // Return data
return { return {
allowCustom: o.allowCustom, allowCustom: this.options.allowCustom,
choices: choices, choices: choices,
custom: custom custom: attr ? attr.custom : ""
}; }
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _updateObject(event, formData) { _updateObject(event, formData) {
const o = this.options; const updateData = {};
// Obtain choices // Obtain choices
const chosen = []; const chosen = [];
for ( let [k, v] of Object.entries(formData) ) { for ( let [k, v] of Object.entries(formData) ) {
if (k !== "custom" && v) chosen.push(k); if ( (k !== "custom") && v ) chosen.push(k);
} }
updateData[`${this.attribute}.value`] = chosen;
// 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 // Validate the number chosen
if (o.minimum && chosen.length < o.minimum) { if ( this.options.minimum && (chosen.length < this.options.minimum) ) {
return ui.notifications.error(`You must choose at least ${o.minimum} options`); return ui.notifications.error(`You must choose at least ${this.options.minimum} options`);
} }
if (o.maximum && chosen.length > o.maximum) { if ( this.options.maximum && (chosen.length > this.options.maximum) ) {
return ui.notifications.error(`You may choose no more than ${o.maximum} options`); return ui.notifications.error(`You may choose no more than ${this.options.maximum} options`);
}
// Include custom
if ( this.options.allowCustom ) {
updateData[`${this.attribute}.custom`] = formData.custom;
} }
// Update the object // Update the object

View file

@ -8,7 +8,7 @@ export const measureDistances = function (segments, options = {}) {
const d = canvas.dimensions; const d = canvas.dimensions;
// Iterate over measured segments // Iterate over measured segments
return segments.map((s) => { return segments.map(s => {
let r = s.ray; let r = s.ray;
// Determine the total distance traveled // Determine the total distance traveled
@ -23,7 +23,7 @@ export const measureDistances = function (segments, options = {}) {
// Alternative DMG Movement // Alternative DMG Movement
if (rule === "5105") { if (rule === "5105") {
let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2); let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
let spaces = nd10 * 2 + (nd - nd10) + ns; let spaces = (nd10 * 2) + (nd - nd10) + ns;
return spaces * canvas.dimensions.distance; return spaces * canvas.dimensions.distance;
} }
@ -36,3 +36,19 @@ export const measureDistances = function (segments, options = {}) {
else return (ns + nd) * canvas.scene.data.gridDistance; else return (ns + nd) * canvas.scene.data.gridDistance;
}); });
}; };
/* -------------------------------------------- */
/**
* Hijack Token health bar rendering to include temporary and temp-max health in the bar display
* TODO: This should probably be replaced with a formal Token class extension
*/
const _TokenGetBarAttribute = Token.prototype.getBarAttribute;
export const getBarAttribute = function(...args) {
const data = _TokenGetBarAttribute.bind(this)(...args);
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);
}
return data;
};

View file

@ -147,19 +147,17 @@ export default class CharacterImporter {
}); });
// pull classes directly from system compendium and add them to current actor // pull classes directly from system compendium and add them to current actor
const professionsPack = await game.packs.get("sw5e.classes").getDocuments(); const professionsPack = await game.packs.get("sw5e.classes").getContent();
result.forEach((prof) => { result.forEach((prof) => {
let assignedProfession = professionsPack.find((o) => o.name === prof.profession); let assignedProfession = professionsPack.find((o) => o.name === prof.profession);
assignedProfession.data.data.levels = prof.level; assignedProfession.data.data.levels = prof.level;
actor.createEmbeddedDocuments("Item", [assignedProfession.data], {displaySheet: false}); actor.createEmbeddedEntity("OwnedItem", assignedProfession.data, { displaySheet: false });
}); });
this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor); this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor);
this.addPowers( this.addPowers(
sourceCharacter.attribs sourceCharacter.attribs.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1).map((e) => e.current),
.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1)
.map((e) => e.current),
actor actor
); );
@ -179,10 +177,10 @@ export default class CharacterImporter {
} }
static async addClasses(profession, level, actor) { static async addClasses(profession, level, actor) {
let classes = await game.packs.get("sw5e.classes").getDocuments(); let classes = await game.packs.get("sw5e.classes").getContent();
let assignedClass = classes.find((c) => c.name === profession); let assignedClass = classes.find((c) => c.name === profession);
assignedClass.data.data.levels = level; assignedClass.data.data.levels = level;
await actor.createEmbeddedDocuments("Item", [assignedClass.data], {displaySheet: false}); await actor.createEmbeddedEntity("OwnedItem", assignedClass.data, { displaySheet: false });
} }
static classOrMulticlass(name) { static classOrMulticlass(name) {
@ -212,9 +210,9 @@ export default class CharacterImporter {
} }
static async addSpecies(race, actor) { static async addSpecies(race, actor) {
const species = await game.packs.get("sw5e.species").getDocuments(); const species = await game.packs.get("sw5e.species").getContent();
const assignedSpecies = species.find((c) => c.name === race); const assignedSpecies = species.find((c) => c.name === race);
const activeEffects = [...assignedSpecies.data.effects][0].data.changes; const activeEffects = assignedSpecies.data.effects[0].changes;
const actorData = { data: { abilities: { ...actor.data.data.abilities } } }; const actorData = { data: { abilities: { ...actor.data.data.abilities } } };
activeEffects.map((effect) => { activeEffects.map((effect) => {
@ -250,26 +248,26 @@ export default class CharacterImporter {
actor.update(actorData); actor.update(actorData);
await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], {displaySheet: false}); await actor.createEmbeddedEntity("OwnedItem", assignedSpecies.data, { displaySheet: false });
} }
static async addPowers(powers, actor) { static async addPowers(powers, actor) {
const forcePowers = await game.packs.get("sw5e.forcepowers").getDocuments(); const forcePowers = await game.packs.get("sw5e.forcepowers").getContent();
const techPowers = await game.packs.get("sw5e.techpowers").getDocuments(); const techPowers = await game.packs.get("sw5e.techpowers").getContent();
for (const power of powers) { for (const power of powers) {
const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power); const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power);
if (createdPower) { if (createdPower) {
await actor.createEmbeddedDocuments("Item", [createdPower.data], {displaySheet: false}); await actor.createEmbeddedEntity("OwnedItem", createdPower.data, { displaySheet: false });
} }
} }
} }
static async addItems(items, actor) { static async addItems(items, actor) {
const weapons = await game.packs.get("sw5e.weapons").getDocuments(); const weapons = await game.packs.get("sw5e.weapons").getContent();
const armors = await game.packs.get("sw5e.armor").getDocuments(); const armors = await game.packs.get("sw5e.armor").getContent();
const adventuringGear = await game.packs.get("sw5e.adventuringgear").getDocuments(); const adventuringGear = await game.packs.get("sw5e.adventuringgear").getContent();
for (const item of items) { for (const item of items) {
const createdItem = const createdItem =
@ -282,30 +280,38 @@ export default class CharacterImporter {
createdItem.data.data.quantity = item.quantity; createdItem.data.data.quantity = item.quantity;
} }
await actor.createEmbeddedDocuments("Item", [createdItem.data], {displaySheet: false}); await actor.createEmbeddedEntity("OwnedItem", createdItem.data, { displaySheet: false });
} }
} }
} }
static addImportButton(html) { static addImportButton() {
const actionButtons = html.find(".header-actions"); const header = $("#actors").find("header.directory-header");
actionButtons[0].insertAdjacentHTML( const search = $("#actors").children().find("div.header-search");
"afterend", const newImportButtonDiv = $("#actors").children().find("div.header-actions").clone();
`<div class="header-actions action-buttons flexrow"><button class="create-entity cs-import-button"><i class="fas fa-upload"></i> Import Character</button></div>` const newSearch = search.clone();
search.remove();
newImportButtonDiv.attr("id", "character-sheet-import");
header.append(newImportButtonDiv);
newImportButtonDiv.children("button").remove();
newImportButtonDiv.append(
"<button class='create-entity' id='cs-import-button'><i class='fas fa-upload'></i> Import Character</button>"
); );
newSearch.appendTo(header);
let characterImportButton = $(".cs-import-button"); let characterImportButton = $("#cs-import-button");
characterImportButton.click(() => { characterImportButton.click(() => {
let content = `<h1>Saved Character JSON Import</h1> let content =
<label for="character-json">Paste character JSON here:</label> "<h1>Saved Character JSON Import</h1> " +
</br> '<label for="character-json">Paste character JSON here:</label> ' +
<textarea id="character-json" name="character-json" rows="10" cols="50"></textarea>`; "</br>" +
'<textarea id="character-json" name="character-json" rows="10" cols="50"></textarea>';
let importDialog = new Dialog({ let importDialog = new Dialog({
title: "Import Character from SW5e.com", title: "Import Character from SW5e.com",
content: content, content: content,
buttons: { buttons: {
Import: { Import: {
icon: `<i class="fas fa-file-import"></i>`, icon: '<i class="fas fa-file-import"></i>',
label: "Import Character", label: "Import Character",
callback: () => { callback: () => {
let characterData = $("#character-json").val(); let characterData = $("#character-json").val();
@ -314,7 +320,7 @@ export default class CharacterImporter {
} }
}, },
Cancel: { Cancel: {
icon: `<i class="fas fa-times-circle"></i>`, icon: '<i class="fas fa-times-circle"></i>',
label: "Cancel", label: "Cancel",
callback: () => {} callback: () => {}
} }

View file

@ -1,3 +1,4 @@
/** /**
* Highlight critical success or failure on d20 rolls * Highlight critical success or failure on d20 rolls
*/ */
@ -10,9 +11,9 @@ export const highlightCriticalSuccessFailure = function (message, html, data) {
const d = roll.dice[0]; const d = roll.dice[0];
// Ensure it is an un-modified d20 roll // Ensure it is an un-modified d20 roll
const isD20 = d.faces === 20 && d.values.length === 1; const isD20 = (d.faces === 20) && ( d.values.length === 1 );
if ( !isD20 ) return; if ( !isD20 ) return;
const isModifiedRoll = "success" in d.results[0] || d.options.marginSuccess || d.options.marginFailure; const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure;
if ( isModifiedRoll ) return; if ( isModifiedRoll ) return;
// Highlight successes and failures // Highlight successes and failures
@ -39,14 +40,14 @@ export const displayChatActionButtons = function (message, html, data) {
// If the user is the message author or the actor owner, proceed // If the user is the message author or the actor owner, proceed
let actor = game.actors.get(data.message.speaker.actor); let actor = game.actors.get(data.message.speaker.actor);
if (actor && actor.isOwner) return; if ( actor && actor.owner ) return;
else if (game.user.isGM || data.author.id === game.user.id) return; else if ( game.user.isGM || (data.author.id === game.user.id)) return;
// Otherwise conceal action buttons except for saving throw // Otherwise conceal action buttons except for saving throw
const buttons = chatCard.find("button[data-action]"); const buttons = chatCard.find("button[data-action]");
buttons.each((i, btn) => { buttons.each((i, btn) => {
if ( btn.dataset.action === "save" ) return; if ( btn.dataset.action === "save" ) return;
btn.style.display = "none"; btn.style.display = "none"
}); });
} }
}; };
@ -63,34 +64,34 @@ export const displayChatActionButtons = function (message, html, data) {
* @return {Array} The extended options Array including new context choices * @return {Array} The extended options Array including new context choices
*/ */
export const addChatMessageContextOptions = function(html, options) { export const addChatMessageContextOptions = function(html, options) {
let canApply = (li) => { let canApply = li => {
const message = game.messages.get(li.data("messageId")); const message = game.messages.get(li.data("messageId"));
return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length; return message?.isRoll && message?.isContentVisible && canvas?.tokens.controlled.length;
}; };
options.push( options.push(
{ {
name: game.i18n.localize("SW5E.ChatContextDamage"), name: game.i18n.localize("SW5E.ChatContextDamage"),
icon: '<i class="fas fa-user-minus"></i>', icon: '<i class="fas fa-user-minus"></i>',
condition: canApply, condition: canApply,
callback: (li) => applyChatCardDamage(li, 1) callback: li => applyChatCardDamage(li, 1)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextHealing"), name: game.i18n.localize("SW5E.ChatContextHealing"),
icon: '<i class="fas fa-user-plus"></i>', icon: '<i class="fas fa-user-plus"></i>',
condition: canApply, condition: canApply,
callback: (li) => applyChatCardDamage(li, -1) callback: li => applyChatCardDamage(li, -1)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"), name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
icon: '<i class="fas fa-user-injured"></i>', icon: '<i class="fas fa-user-injured"></i>',
condition: canApply, condition: canApply,
callback: (li) => applyChatCardDamage(li, 2) callback: li => applyChatCardDamage(li, 2)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextHalfDamage"), name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
icon: '<i class="fas fa-user-shield"></i>', icon: '<i class="fas fa-user-shield"></i>',
condition: canApply, condition: canApply,
callback: (li) => applyChatCardDamage(li, 0.5) callback: li => applyChatCardDamage(li, 0.5)
} }
); );
return options; return options;
@ -109,12 +110,10 @@ export const addChatMessageContextOptions = function (html, options) {
function applyChatCardDamage(li, multiplier) { function applyChatCardDamage(li, multiplier) {
const message = game.messages.get(li.data("messageId")); const message = game.messages.get(li.data("messageId"));
const roll = message.roll; const roll = message.roll;
return Promise.all( return Promise.all(canvas.tokens.controlled.map(t => {
canvas.tokens.controlled.map((t) => {
const a = t.actor; const a = t.actor;
return a.applyDamage(roll.total, multiplier); return a.applyDamage(roll.total, multiplier);
}) }));
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

@ -1 +1,4 @@
export const ClassFeatures = {}; export const ClassFeatures = {
};

View file

@ -1,31 +1,28 @@
/** /**
* Override the default Initiative formula to customize special behaviors of the SW5e system. * Override the default Initiative formula to customize special behaviors of the SW5e system.
* Apply advantage, proficiency, or bonuses where appropriate * Apply advantage, proficiency, or bonuses where appropriate
* Apply the dexterity score as a decimal tiebreaker if requested * Apply the dexterity score as a decimal tiebreaker if requested
* See Combat._getInitiativeFormula for more detail. * See Combat._getInitiativeFormula for more detail.
*/ */
export const _getInitiativeFormula = function () { export const _getInitiativeFormula = function(combatant) {
const actor = this.actor; const actor = combatant.actor;
if ( !actor ) return "1d20"; if ( !actor ) return "1d20";
const init = actor.data.data.attributes.init; const init = actor.data.data.attributes.init;
// Construct initiative formula parts
let nd = 1; let nd = 1;
let mods = ""; let mods = "";
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1"; if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1";
if (actor.getFlag("sw5e", "initiativeAdv")) { if (actor.getFlag("sw5e", "initiativeAdv")) {
nd = 2; nd = 2;
mods += "kh"; mods += "kh";
} }
const parts = [
`${nd}d20${mods}`, const parts = [`${nd}d20${mods}`, init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
init.mod,
init.prof !== 0 ? init.prof : null,
init.bonus !== 0 ? init.bonus : null
];
// Optionally apply Dexterity tiebreaker // Optionally apply Dexterity tiebreaker
const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker"); const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker");
if ( tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100); if ( tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100);
return parts.filter((p) => p !== null).join(" + "); return parts.filter(p => p !== null).join(" + ");
}; };

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,3 @@
export {default as D20Roll} from "./dice/d20-roll.js";
export {default as DamageRoll} from "./dice/damage-roll.js";
/** /**
* A standardized helper function for simplifying the constant parts of a multipart roll formula * A standardized helper function for simplifying the constant parts of a multipart roll formula
* *
@ -23,19 +20,14 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
const constantTerms = []; // Terms that are 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 let operators = []; // Temporary storage for operators before they are moved to one of the above
for (let term of terms) { for (let term of terms) { // For each term
// For each term if (["+", "-"].includes(term)) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array
if (term instanceof OperatorTerm) operators.push(term); else { // Otherwise the term is not an operator
// If the term is an addition/subtraction operator, push the term into the operators array if (term instanceof DiceTerm) { // If the term is something rollable
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(...operators); // Place all the operators into the rollableTerms array
rollableTerms.push(term); // Then place this rollable term into it as well rollableTerms.push(term); // Then place this rollable term into it as well
} // } //
else { else { // Otherwise, this must be a constant
// Otherwise, this must be a constant
constantTerms.push(...operators); // Place the operators into the constantTerms array constantTerms.push(...operators); // Place the operators into the constantTerms array
constantTerms.push(term); // Then also add this constant term to that array. constantTerms.push(term); // Then also add this constant term to that array.
} // } //
@ -43,21 +35,13 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
} }
} }
const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string const constantFormula = Roll.cleanFormula(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 rollableFormula = Roll.cleanFormula(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 = roll._safeEval(constantFormula); // 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 ? // Order the rollable and constant terms, either constant first or second depending on the optional argumen
const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart]; [constantPart, rollableFormula] : [rollableFormula, constantPart];
// Join the parts with a + sign, pass them to `Roll` once again to clean up the formula // Join the parts with a + sign, pass them to `Roll` once again to clean up the formula
return new Roll(parts.filterJoin(" + ")).formula; return new Roll(parts.filterJoin(" + ")).formula;
@ -72,242 +56,315 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
*/ */
function _isUnsupportedTerm(term) { function _isUnsupportedTerm(term) {
const diceTerm = term instanceof DiceTerm; const diceTerm = term instanceof DiceTerm;
const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator); const operator = ["+", "-"].includes(term);
const number = term instanceof NumericTerm; const number = !isNaN(Number(term));
return !(diceTerm || operator || number); return !(diceTerm || operator || number);
} }
/* -------------------------------------------- */
/* D20 Roll */
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* A standardized helper function for managing core 5e d20 rolls. * A standardized helper function for managing core 5e "d20 rolls"
*
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward". * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
* This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively * This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively
* *
* @param {string[]} parts The dice roll component parts, excluding the initial d20 * @param {Array} parts The dice roll component parts, excluding the initial d20
* @param {object} data Actor or item data against which to parse the roll * @param {Object} data Actor or item data against which to parse the roll
* @param {Event|object} event The triggering event which initiated the roll
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
* @param {string|null} template The HTML template used to render the roll dialog
* @param {string|null} title The dice roll UI window title
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
* @param {string|null} flavor Flavor text to use in the posted chat message
* @param {Boolean} fastForward Allow fast-forward advantage selection
* @param {Function} onClose Callback for actions to take when the dialog form is closed
* @param {Object} dialogOptions Modal dialog options
* @param {boolean} advantage Apply advantage to the roll (unless otherwise specified)
* @param {boolean} disadvantage Apply disadvantage to the roll (unless otherwise specified)
* @param {number} critical The value of d20 result which represents a critical success
* @param {number} fumble The value of d20 result which represents a critical failure
* @param {number} targetValue Assign a target value against which the result of this roll should be compared
* @param {boolean} elvenAccuracy Allow Elven Accuracy to modify this roll?
* @param {boolean} halflingLucky Allow Halfling Luck to modify this roll?
* @param {boolean} reliableTalent Allow Reliable Talent to modify this roll?
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
* *
* @param {boolean} [advantage] Apply advantage to the roll (unless otherwise specified) * @return {Promise} A Promise which resolves once the roll workflow has completed
* @param {boolean} [disadvantage] Apply disadvantage to the roll (unless otherwise specified)
* @param {number} [critical] The value of d20 result which represents a critical success
* @param {number} [fumble] The value of d20 result which represents a critical failure
* @param {number} [targetValue] Assign a target value against which the result of this roll should be compared
* @param {boolean} [elvenAccuracy] Allow Elven Accuracy to modify this roll?
* @param {boolean} [halflingLucky] Allow Halfling Luck to modify this roll?
* @param {boolean} [reliableTalent] Allow Reliable Talent to modify this roll?
* @param {boolean} [chooseModifier=false] Choose the ability modifier that should be used when the roll is made
* @param {boolean} [fastForward=false] Allow fast-forward advantage selection
* @param {Event} [event] The triggering event which initiated the roll
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {string} [template] The HTML template used to render the roll dialog
* @param {string} [title] The dialog window title
* @param {Object} [dialogOptions] Modal dialog options
*
* @param {boolean} [chatMessage=true] Automatically create a Chat Message for the result of this roll
* @param {object} [messageData={}] Additional data which is applied to the created Chat Message, if any
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {object} [speaker] The ChatMessage speaker to pass when creating the chat
* @param {string} [flavor] Flavor text to use in the posted chat message
*
* @return {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled
*/ */
export async function d20Roll({ export async function d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null,
parts = [], flavor=null, fastForward=null, dialogOptions,
data = {}, // Roll creation advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null,
advantage, elvenAccuracy=false, halflingLucky=false, reliableTalent=false,
disadvantage, chatMessage=true, messageData={}}={}) {
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 // Prepare Message Data
const roll = new CONFIG.Dice.D20Roll(formula, data, { messageData.flavor = flavor || title;
flavor: flavor || title, messageData.speaker = speaker || ChatMessage.getSpeaker();
advantageMode, const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
defaultRollMode, parts = parts.concat(["@bonus"]);
critical,
fumble,
targetValue,
elvenAccuracy,
halflingLucky,
reliableTalent
});
// Prompt a Dialog to further configure the D20Roll // Handle fast-forward events
if (!isFF) { let adv = 0;
const configured = await roll.configureDialog( fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
{ if (fastForward) {
title, if ( advantage ?? event.altKey ) adv = 1;
chooseModifier, else if ( disadvantage ?? (event.ctrlKey || event.metaKey) ) adv = -1;
defaultRollMode: defaultRollMode,
defaultAction: advantageMode,
defaultAbility: data?.item?.ability,
template
},
dialogOptions
);
if (configured === null) return null;
} }
// Evaluate the configured roll // Define the inner roll function
await roll.evaluate({async: true}); const _roll = (parts, adv, form) => {
// Determine the d20 roll and modifiers
let nd = 1;
let mods = halflingLucky ? "r1=1" : "";
// Handle advantage
if (adv === 1) {
nd = elvenAccuracy ? 3 : 2;
messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].advantage = true;
mods += "kh";
}
// Handle disadvantage
else if (adv === -1) {
nd = 2;
messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].disadvantage = true;
mods += "kl";
}
// Prepend the d20 roll
let formula = `${nd}d20${mods}`;
if (reliableTalent) formula = `{${nd}d20${mods},10}kh`;
parts.unshift(formula);
// Optionally include a situational bonus
if ( form ) {
data['bonus'] = form.bonus.value;
messageOptions.rollMode = form.rollMode.value;
}
if (!data["bonus"]) parts.pop();
// Optionally include an ability score selection (used for tool checks)
const ability = form ? form.ability : null;
if (ability && ability.value) {
data.ability = ability.value;
const abl = data.abilities[data.ability];
if (abl) {
data.mod = abl.mod;
messageData.flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`;
}
}
// Execute the roll
let roll = new Roll(parts.join(" + "), data);
try {
roll.roll();
} catch (err) {
console.error(err);
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
return null;
}
// Flag d20 options for any 20-sided dice in the roll
for (let d of roll.dice) {
if (d.faces === 20) {
d.options.critical = critical;
d.options.fumble = fumble;
if ( adv === 1 ) d.options.advantage = true;
else if ( adv === -1 ) d.options.disadvantage = true;
if (targetValue) d.options.target = targetValue;
}
}
// If reliable talent was applied, add it to the flavor text
if (reliableTalent && roll.dice[0].total < 10) {
messageData.flavor += ` (${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
}
return roll;
};
// Create the Roll instance
const roll = fastForward ? _roll(parts, adv) :
await _d20RollDialog({template, title, parts, data, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll});
// Create a Chat Message // Create a Chat Message
if (speaker) { if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
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; return roll;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied * Present a Dialog form which creates a d20 roll once submitted
* @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode * @return {Promise<Roll>}
* @private
*/ */
function _determineAdvantageMode({event, advantage = false, disadvantage = false, fastForward = false} = {}) { async function _d20RollDialog({template, title, parts, data, rollMode, dialogOptions, roll}={}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL; // Render modal dialog
if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE; template = template || "systems/sw5e/templates/chat/roll-dialog.html";
else if (disadvantage || event?.ctrlKey || event?.metaKey) let dialogData = {
advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE; formula: parts.join(" + "),
return {isFF, advantageMode}; data: data,
rollMode: rollMode,
rollModes: CONFIG.Dice.rollModes,
config: CONFIG.SW5E
};
const html = await renderTemplate(template, dialogData);
// Create the Dialog window
return new Promise(resolve => {
new Dialog({
title: title,
content: html,
buttons: {
advantage: {
label: game.i18n.localize("SW5E.Advantage"),
callback: html => resolve(roll(parts, 1, html[0].querySelector("form")))
},
normal: {
label: game.i18n.localize("SW5E.Normal"),
callback: html => resolve(roll(parts, 0, html[0].querySelector("form")))
},
disadvantage: {
label: game.i18n.localize("SW5E.Disadvantage"),
callback: html => resolve(roll(parts, -1, html[0].querySelector("form")))
}
},
default: "normal",
close: () => resolve(null)
}, dialogOptions).render(true);
});
} }
/* -------------------------------------------- */
/* Damage Roll */
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* A standardized helper function for managing core 5e damage rolls. * A standardized helper function for managing core 5e "d20 rolls"
* *
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward". * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
* This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively * This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively
* *
* @param {string[]} parts The dice roll component parts, excluding the initial d20 * @param {Array} parts The dice roll component parts, excluding the initial d20
* @param {object} [data] Actor or item data against which to parse the roll * @param {Actor} actor The Actor making the damage roll
* @param {Object} data Actor or item data against which to parse the roll
* @param {Event|object}[event The triggering event which initiated the roll
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
* @param {String} template The HTML template used to render the roll dialog
* @param {String} title The dice roll UI window title
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
* @param {string} flavor Flavor text to use in the posted chat message
* @param {boolean} allowCritical Allow the opportunity for a critical hit to be rolled
* @param {Boolean} critical Flag this roll as a critical hit for the purposes of fast-forward rolls
* @param {number} criticalBonusDice A number of bonus damage dice that are added for critical hits
* @param {number} criticalMultiplier A critical hit multiplier which is applied to critical hits
* @param {Boolean} fastForward Allow fast-forward advantage selection
* @param {Function} onClose Callback for actions to take when the dialog form is closed
* @param {Object} dialogOptions Modal dialog options
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
* *
* @param {boolean} [critical=false] Flag this roll as a critical hit for the purposes of fast-forward or default dialog action * @return {Promise} A Promise which resolves once the roll workflow has completed
* @param {number} [criticalBonusDice=0] A number of bonus damage dice that are added for critical hits
* @param {number} [criticalMultiplier=2] A critical hit multiplier which is applied to critical hits
* @param {boolean} [multiplyNumeric=false] Multiply numeric terms by the critical multiplier
* @param {boolean} [powerfulCritical=false] Apply the "powerful criticals" house rule to critical hits
* @param {boolean} [fastForward=false] Allow fast-forward advantage selection
* @param {Event}[event] The triggering event which initiated the roll
* @param {boolean} [allowCritical=true] Allow the opportunity for a critical hit to be rolled
* @param {string} [template] The HTML template used to render the roll dialog
* @param {string} [title] The dice roll UI window title
* @param {object} [dialogOptions] Configuration dialog options
*
* @param {boolean} [chatMessage=true] Automatically create a Chat Message for the result of this roll
* @param {object} [messageData={}] Additional data which is applied to the created Chat Message, if any
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {object} [speaker] The ChatMessage speaker to pass when creating the chat
* @param {string} [flavor] Flavor text to use in the posted chat message
*
* @return {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled
*/ */
export async function damageRoll({ export async function damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor,
parts = [], allowCritical=true, critical=false, criticalBonusDice=0, criticalMultiplier=2, fastForward=null,
data, // Roll creation dialogOptions={}, chatMessage=true, messageData={}}={}) {
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 // Prepare Message Data
const formula = parts.join(" + "); messageData.flavor = flavor || title;
const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event}); messageData.speaker = speaker || ChatMessage.getSpeaker();
const roll = new CONFIG.Dice.DamageRoll(formula, data, { const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
flavor: flavor || title, parts = parts.concat(["@bonus"]);
critical: isCritical,
criticalBonusDice, // Define inner roll function
criticalMultiplier, const _roll = function(parts, crit, form) {
multiplyNumeric,
powerfulCritical // Optionally include a situational bonus
if ( form ) {
data['bonus'] = form.bonus.value;
messageOptions.rollMode = form.rollMode.value;
}
if (!data["bonus"]) parts.pop();
// Create the damage roll
let roll = new Roll(parts.join("+"), data);
// Modify the damage formula for critical hits
if ( crit === true ) {
roll.alter(criticalMultiplier, 0); // Multiply all dice
if ( roll.terms[0] instanceof Die ) { // Add bonus dice for only the main dice term
roll.terms[0].alter(1, criticalBonusDice);
roll._formula = roll.formula;
}
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
}
// Execute the roll
try {
roll.evaluate()
if ( crit ) roll.dice.forEach(d => d.options.critical = true); // TODO workaround core bug which wipes Roll#options on roll
return roll;
} catch(err) {
console.error(err);
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
return null;
}
};
// Create the Roll instance
const roll = fastForward ? _roll(parts, critical) : await _damageRollDialog({
template, title, parts, data, allowCritical, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll
}); });
// 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;
}
// Evaluate the configured roll
await roll.evaluate({async: true});
// Create a Chat Message // Create a Chat Message
if (speaker) { if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
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; return roll;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied * Present a Dialog form which creates a damage roll once submitted
* @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit * @return {Promise<Roll>}
* @private
*/ */
function _determineCriticalMode({event, critical = false, fastForward = false} = {}) { async function _damageRollDialog({template, title, parts, data, allowCritical, rollMode, dialogOptions, roll}={}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
if (event?.altKey) critical = true; // Render modal dialog
return {isFF, isCritical: critical}; template = template || "systems/sw5e/templates/chat/roll-dialog.html";
let dialogData = {
formula: parts.join(" + "),
data: data,
rollMode: rollMode,
rollModes: CONFIG.Dice.rollModes
};
const html = await renderTemplate(template, dialogData);
// Create the Dialog window
return new Promise(resolve => {
new Dialog({
title: title,
content: html,
buttons: {
critical: {
condition: allowCritical,
label: game.i18n.localize("SW5E.CriticalHit"),
callback: html => resolve(roll(parts, true, html[0].querySelector("form")))
},
normal: {
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
callback: html => resolve(roll(parts, false, html[0].querySelector("form")))
},
},
default: "normal",
close: () => resolve(null)
}, dialogOptions).render(true);
});
} }

View file

@ -1,230 +0,0 @@
/**
* A type of Roll specific to a d20-based check, save, or attack roll in the 5e system.
* @param {string} formula The string formula to parse
* @param {object} data The data object against which to parse attributes within the formula
* @param {object} [options={}] Extra optional arguments which describe or modify the D20Roll
* @param {number} [options.advantageMode] What advantage modifier to apply to the roll (none, advantage, disadvantage)
* @param {number} [options.critical] The value of d20 result which represents a critical success
* @param {number} [options.fumble] The value of d20 result which represents a critical failure
* @param {(number)} [options.targetValue] Assign a target value against which the result of this roll should be compared
* @param {boolean} [options.elvenAccuracy=false] Allow Elven Accuracy to modify this roll?
* @param {boolean} [options.halflingLucky=false] Allow Halfling Luck to modify this roll?
* @param {boolean} [options.reliableTalent=false] Allow Reliable Talent to modify this roll?
*/
// TODO: Check elven accuracy, halfling lucky, and reliable talent are required
// Elven Accuracy is Supreme accuracy feat, Reliable Talent is operative's Reliable Talent Class Feat
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)) {
throw new Error(`Invalid D20Roll formula provided ${this._formula}`);
}
this.configureModifiers();
}
/* -------------------------------------------- */
/**
* Advantage mode of a 5e d20 roll
* @enum {number}
*/
static ADV_MODE = {
NORMAL: 0,
ADVANTAGE: 1,
DISADVANTAGE: -1
};
/**
* The HTML template path used to configure evaluation of this Roll
* @type {string}
*/
static EVALUATION_TEMPLATE = "systems/sw5e/templates/chat/roll-dialog.html";
/* -------------------------------------------- */
/**
* A convenience reference for whether this D20Roll has advantage
* @type {boolean}
*/
get hasAdvantage() {
return this.options.advantageMode === D20Roll.ADV_MODE.ADVANTAGE;
}
/**
* A convenience reference for whether this D20Roll has disadvantage
* @type {boolean}
*/
get hasDisadvantage() {
return this.options.advantageMode === D20Roll.ADV_MODE.DISADVANTAGE;
}
/* -------------------------------------------- */
/* D20 Roll Methods */
/* -------------------------------------------- */
/**
* Apply optional modifiers which customize the behavior of the d20term
* @private
*/
configureModifiers() {
const d20 = this.terms[0];
d20.modifiers = [];
// Halfling Lucky
if (this.options.halflingLucky) d20.modifiers.push("r1=1");
// Reliable Talent
if (this.options.reliableTalent) d20.modifiers.push("min10");
// Handle Advantage or Disadvantage
if (this.hasAdvantage) {
d20.number = this.options.elvenAccuracy ? 3 : 2;
d20.modifiers.push("kh");
d20.options.advantage = true;
} else if (this.hasDisadvantage) {
d20.number = 2;
d20.modifiers.push("kl");
d20.options.disadvantage = true;
} 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;
// Re-compile the underlying formula
this._formula = this.constructor.getFormula(this.terms);
}
/* -------------------------------------------- */
/** @inheritdoc */
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});
// 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")})`;
// Add reliable talent to the d20-term flavor text if it applied
if (this.options.reliableTalent) {
const d20 = this.dice[0];
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;
}
// Record the preferred rollMode
options.rollMode = options.rollMode ?? this.options.rollMode;
return super.toMessage(messageData, options);
}
/* -------------------------------------------- */
/* Configuration Dialog */
/* -------------------------------------------- */
/**
* Create a Dialog prompt used to configure evaluation of an existing D20Roll instance.
* @param {object} data Dialog configuration data
* @param {string} [data.title] The title of the shown dialog window
* @param {number} [data.defaultRollMode] The roll mode that the roll mode select element should default to
* @param {number} [data.defaultAction] The button marked as default
* @param {boolean} [data.chooseModifier] Choose which ability modifier should be applied to the roll?
* @param {string} [data.defaultAbility] For tool rolls, the default ability modifier applied to the roll
* @param {string} [data.template] A custom path to an HTML template to use instead of the default
* @param {object} options Additional Dialog customization options
* @returns {Promise<D20Roll|null>} 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 = {}
) {
// Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`,
defaultRollMode,
rollModes: CONFIG.Dice.rollModes,
chooseModifier,
defaultAbility,
abilities: CONFIG.SW5E.abilities
});
let defaultButton = "normal";
switch (defaultAction) {
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))
},
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)
},
options
).render(true);
});
}
/* -------------------------------------------- */
/**
* Handle submission of the Roll evaluation configuration Dialog
* @param {jQuery} html The submitted dialog content
* @param {number} advantageMode The chosen advantage mode
* @private
*/
_onDialogSubmit(html, advantageMode) {
const form = html[0].querySelector("form");
// Append a situational bonus term
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: "+"}));
this.terms = this.terms.concat(bonus.terms);
}
// Customize the modifier
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.options.flavor += ` (${CONFIG.SW5E.abilities[form.ability.value]})`;
}
// Apply advantage or disadvantage
this.options.advantageMode = advantageMode;
this.options.rollMode = form.rollMode.value;
this.configureModifiers();
return this;
}
}

View file

@ -1,186 +0,0 @@
/**
* A type of Roll specific to a damage (or healing) roll in the 5e system.
* @param {string} formula The string formula to parse
* @param {object} data The data object against which to parse attributes within the formula
* @param {object} [options={}] Extra optional arguments which describe or modify the DamageRoll
* @param {number} [options.criticalBonusDice=0] A number of bonus damage dice that are added for critical hits
* @param {number} [options.criticalMultiplier=2] A critical hit multiplier which is applied to critical hits
* @param {boolean} [options.multiplyNumeric=false] Multiply numeric terms by the critical multiplier
* @param {boolean} [options.powerfulCritical=false] Apply the "powerful criticals" house rule to critical hits
*
*/
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();
}
/**
* The HTML template path used to configure evaluation of this Roll
* @type {string}
*/
static EVALUATION_TEMPLATE = "systems/sw5e/templates/chat/roll-dialog.html";
/* -------------------------------------------- */
/**
* A convenience reference for whether this DamageRoll is a critical hit
* @type {boolean}
*/
get isCritical() {
return this.options.critical;
}
/* -------------------------------------------- */
/* Damage Roll Methods */
/* -------------------------------------------- */
/**
* Apply optional modifiers which customize the behavior of the d20term
* @private
*/
configureDamage() {
let flatBonus = 0;
for (let [i, term] of this.terms.entries()) {
// Multiply dice terms
if (term instanceof DiceTerm) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
term.number = term.options.baseNumber;
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);
}
// Alter the damage term
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) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
term.number = term.options.baseNumber;
if (this.isCritical) {
term.number *= this.options.criticalMultiplier ?? 2;
term.options.critical = true;
}
}
}
// Add powerful critical bonus
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")})
);
}
// Re-compile the underlying formula
this._formula = this.constructor.getFormula(this.terms);
}
/* -------------------------------------------- */
/** @inheritdoc */
toMessage(messageData = {}, options = {}) {
messageData.flavor = messageData.flavor || this.options.flavor;
if (this.isCritical) {
const label = game.i18n.localize("SW5E.CriticalHit");
messageData.flavor = messageData.flavor ? `${messageData.flavor} (${label})` : label;
}
options.rollMode = options.rollMode ?? this.options.rollMode;
return super.toMessage(messageData, options);
}
/* -------------------------------------------- */
/* Configuration Dialog */
/* -------------------------------------------- */
/**
* Create a Dialog prompt used to configure evaluation of an existing D20Roll instance.
* @param {object} data Dialog configuration data
* @param {string} [data.title] The title of the shown dialog window
* @param {number} [data.defaultRollMode] The roll mode that the roll mode select element should default to
* @param {string} [data.defaultCritical] Should critical be selected as default
* @param {string} [data.template] A custom path to an HTML template to use instead of the default
* @param {boolean} [data.allowCritical=true] Allow critical hit to be chosen as a possible damage mode
* @param {object} options Additional Dialog customization options
* @returns {Promise<D20Roll|null>} 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 = {}
) {
// Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`,
defaultRollMode,
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))
},
normal: {
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
callback: (html) => resolve(this._onDialogSubmit(html, false))
}
},
default: defaultCritical ? "critical" : "normal",
close: () => resolve(null)
},
options
).render(true);
});
}
/* -------------------------------------------- */
/**
* Handle submission of the Roll evaluation configuration Dialog
* @param {jQuery} html The submitted dialog content
* @param {boolean} isCritical Is the damage a critical hit?
* @private
*/
_onDialogSubmit(html, isCritical) {
const form = html[0].querySelector("form");
// Append a situational bonus term
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: "+"}));
this.terms = this.terms.concat(bonus.terms);
}
// Apply advantage or disadvantage
this.options.critical = isCritical;
this.options.rollMode = form.rollMode.value;
this.configureDamage();
return this;
}
/* -------------------------------------------- */
/** @inheritdoc */
static fromData(data) {
const roll = super.fromData(data);
roll._formula = this.getFormula(roll.terms);
return roll;
}
}

View file

@ -1,15 +0,0 @@
/**
* @deprecated since 1.3.0
* @ignore
*/
async function d20Dialog(data, options) {
throw new Error(`The d20Dialog helper method is deprecated in favor of D20Roll#configureDialog`);
}
/**
* @deprecated since 1.3.0
* @ignore
*/
async function damageDialog(data, options) {
throw new Error(`The damageDialog helper method is deprecated in favor of DamageRoll#configureDialog`);
}

21
module/effects.js vendored
View file

@ -10,15 +10,13 @@ export function onManageActiveEffect(event, owner) {
const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null; const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
switch ( a.dataset.action ) { switch ( a.dataset.action ) {
case "create": case "create":
return owner.createEmbeddedDocuments("ActiveEffect", [ return ActiveEffect.create({
{ label: "New Effect",
"label": game.i18n.localize("SW5E.EffectNew"), icon: "icons/svg/aura.svg",
"icon": "icons/svg/aura.svg", origin: owner.uuid,
"origin": owner.uuid,
"duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
"disabled": li.dataset.effectType === "inactive" disabled: li.dataset.effectType === "inactive"
} }, owner).create();
]);
case "edit": case "edit":
return effect.sheet.render(true); return effect.sheet.render(true);
case "delete": case "delete":
@ -34,21 +32,22 @@ export function onManageActiveEffect(event, owner) {
* @return {object} Data for rendering * @return {object} Data for rendering
*/ */
export function prepareActiveEffectCategories(effects) { export function prepareActiveEffectCategories(effects) {
// Define effect header categories // Define effect header categories
const categories = { const categories = {
temporary: { temporary: {
type: "temporary", type: "temporary",
label: game.i18n.localize("SW5E.EffectTemporary"), label: "SW5E.EffectsCategoryTemporary",
effects: [] effects: []
}, },
passive: { passive: {
type: "passive", type: "passive",
label: game.i18n.localize("SW5E.EffectPassive"), label: "SW5E.EffectsCategoryPassive",
effects: [] effects: []
}, },
inactive: { inactive: {
type: "inactive", type: "inactive",
label: game.i18n.localize("SW5E.EffectInactive"), label: "SW5E.EffectsCategoryInactive",
effects: [] effects: []
} }
}; };

File diff suppressed because it is too large Load diff

View file

@ -18,9 +18,9 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
width: 560, width: 560,
height: 400, height: 400,
classes: ["sw5e", "sheet", "item"], classes: ["sw5e", "sheet", "item"],
@ -32,7 +32,7 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
get template() { get template() {
const path = "systems/sw5e/templates/items/"; const path = "systems/sw5e/templates/items/";
return `${path}/${this.item.data.type}.html`; return `${path}/${this.item.data.type}.html`;
@ -43,39 +43,33 @@ export default class ItemSheet5e extends ItemSheet {
/** @override */ /** @override */
async getData(options) { async getData(options) {
const data = super.getData(options); const data = super.getData(options);
const itemData = data.data;
data.labels = this.item.labels; data.labels = this.item.labels;
data.config = CONFIG.SW5E; data.config = CONFIG.SW5E;
// Item Type, Status, and Details // Item Type, Status, and Details
data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`); data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
data.itemStatus = this._getItemStatus(itemData); data.itemStatus = this._getItemStatus(data.item);
data.itemProperties = this._getItemProperties(itemData); data.itemProperties = this._getItemProperties(data.item);
data.isPhysical = itemData.data.hasOwnProperty("quantity"); data.isPhysical = data.item.data.hasOwnProperty("quantity");
// Potential consumption targets // Potential consumption targets
data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData); data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item);
// Action Details // Action Detail
data.hasAttackRoll = this.item.hasAttack; data.hasAttackRoll = this.item.hasAttack;
data.isHealing = itemData.data.actionType === "heal"; data.isHealing = data.item.data.actionType === "heal";
data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat"; data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
data.isLine = ["line", "wall"].includes(itemData.data.target?.type); data.isLine = ["line", "wall"].includes(data.item.data.target?.type);
// Original maximum uses formula // Original maximum uses formula
const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max"); if (this.item._data.data?.uses?.max) data.data.uses.max = this.item._data.data.uses.max;
if (sourceMax) itemData.data.uses.max = sourceMax;
// Vehicles // Vehicles
data.isCrewed = itemData.data.activation?.type === "crew"; data.isCrewed = data.item.data.activation?.type === "crew";
data.isMountable = this._isItemMountable(itemData); data.isMountable = this._isItemMountable(data.item);
// Prepare Active Effects // Prepare Active Effects
data.effects = prepareActiveEffectCategories(this.item.effects); data.effects = prepareActiveEffectCategories(this.entity.effects);
// Re-define the template data references (backwards compatible)
data.item = itemData;
data.data = itemData.data;
return data; return data;
} }
@ -108,11 +102,9 @@ export default class ItemSheet5e extends ItemSheet {
// Attributes // Attributes
else if (consume.type === "attribute") { else if (consume.type === "attribute") {
const attributes = TokenDocument.getTrackedAttributes(actor.data.data); const attributes = Object.values(CombatTrackerConfig.prototype.getAttributeChoices())[0]; // Bit of a hack
attributes.bar.forEach((a) => a.push("value")); return attributes.reduce((obj, a) => {
return attributes.bar.concat(attributes.value).reduce((obj, a) => { obj[a] = a;
let k = a.join(".");
obj[k] = k;
return obj; return obj;
}, {}); }, {});
} }
@ -136,10 +128,7 @@ export default class ItemSheet5e extends ItemSheet {
const label = const label =
uses.per === "charges" uses.per === "charges"
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", { value: uses.value })})` ? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", { value: uses.value })})`
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", { : ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", { max: uses.max, per: uses.per })})`;
max: uses.max,
per: uses.per
})})`;
obj[i.id] = i.name + label; obj[i.id] = i.name + label;
} }
@ -187,6 +176,7 @@ export default class ItemSheet5e extends ItemSheet {
); );
} else if (item.type === "power") { } else if (item.type === "power") {
props.push( props.push(
labels.components,
labels.materials, labels.materials,
item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null, item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
@ -196,7 +186,6 @@ export default class ItemSheet5e extends ItemSheet {
props.push(labels.armor); props.push(labels.armor);
} else if (item.type === "feat") { } else if (item.type === "feat") {
props.push(labels.featType); props.push(labels.featType);
//TODO: Work out these
} else if (item.type === "species") { } else if (item.type === "species") {
//props.push(labels.species); //props.push(labels.species);
} else if (item.type === "archetype") { } else if (item.type === "archetype") {
@ -249,7 +238,7 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
setPosition(position = {}) { setPosition(position = {}) {
if (!(this._minimized || position.height)) { if (!(this._minimized || position.height)) {
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height; position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
@ -261,7 +250,7 @@ export default class ItemSheet5e extends ItemSheet {
/* Form Submission */ /* Form Submission */
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
_getSubmitData(updateData = {}) { _getSubmitData(updateData = {}) {
// Create the expanded update data object // Create the expanded update data object
const fd = new FormDataExtended(this.form, { editors: this.editors }); const fd = new FormDataExtended(this.form, { editors: this.editors });
@ -279,12 +268,12 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
if (this.isEditable) { if (this.isEditable) {
html.find(".damage-control").click(this._onDamageControl.bind(this)); html.find(".damage-control").click(this._onDamageControl.bind(this));
html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this)); html.find(".trait-selector.class-skills").click(this._onConfigureClassSkills.bind(this));
html.find(".effect-control").click((ev) => { html.find(".effect-control").click((ev) => {
if (this.item.isOwned) if (this.item.isOwned)
return ui.notifications.warn( return ui.notifications.warn(
@ -318,7 +307,7 @@ export default class ItemSheet5e extends ItemSheet {
if (a.classList.contains("delete-damage")) { if (a.classList.contains("delete-damage")) {
await this._onSubmit(event); // Submit any unsaved changes await this._onSubmit(event); // Submit any unsaved changes
const li = a.closest(".damage-part"); const li = a.closest(".damage-part");
const damage = foundry.utils.deepClone(this.item.data.data.damage); const damage = duplicate(this.item.data.data.damage);
damage.parts.splice(Number(li.dataset.damagePart), 1); damage.parts.splice(Number(li.dataset.damagePart), 1);
return this.item.update({ "data.damage.parts": damage.parts }); return this.item.update({ "data.damage.parts": damage.parts });
} }
@ -327,42 +316,33 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Handle spawning the TraitSelector application for selection various options. * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection * @param {Event} event The click event which originated the selection
* @private * @private
*/ */
_onConfigureTraits(event) { _onConfigureClassSkills(event) {
event.preventDefault(); 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 skills = this.item.data.data.skills;
const choiceSet = const choices = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills); const a = event.currentTarget;
options.choices = Object.fromEntries( const label = a.parentElement;
Object.entries(CONFIG.SW5E.skills).filter((skill) => choiceSet.includes(skill[0]))
); // Render the Trait Selector dialog
options.maximum = skills.number; new TraitSelector(this.item, {
break; name: a.dataset.target,
} title: label.innerText,
new TraitSelector(this.item, options).render(true); choices: Object.entries(CONFIG.SW5E.skills).reduce((obj, e) => {
if (choices.includes(e[0])) obj[e[0]] = e[1];
return obj;
}, {}),
minimum: skills.number,
maximum: skills.number
}).render(true);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
async _onSubmit(...args) { async _onSubmit(...args) {
if (this._tabs[0].active === "details") this.position.height = "auto"; if (this._tabs[0].active === "details") this.position.height = "auto";
await super._onSubmit(...args); await super._onSubmit(...args);

View file

@ -1,3 +1,4 @@
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Hotbar Macros */ /* Hotbar Macros */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -16,7 +17,7 @@ export async function create5eMacro(data, slot) {
// Create the macro command // Create the macro command
const command = `game.sw5e.rollItemMacro("${item.name}");`; const command = `game.sw5e.rollItemMacro("${item.name}");`;
let macro = game.macros.entities.find((m) => m.name === item.name && m.command === command); let macro = game.macros.entities.find(m => (m.name === item.name) && (m.command === command));
if ( !macro ) { if ( !macro ) {
macro = await Macro.create({ macro = await Macro.create({
name: item.name, name: item.name,
@ -45,11 +46,9 @@ export function rollItemMacro(itemName) {
if ( !actor ) actor = game.actors.get(speaker.actor); if ( !actor ) actor = game.actors.get(speaker.actor);
// Get matching items // Get matching items
const items = actor ? actor.items.filter((i) => i.name === itemName) : []; const items = actor ? actor.items.filter(i => i.name === itemName) : [];
if ( items.length > 1 ) { if ( items.length > 1 ) {
ui.notifications.warn( ui.notifications.warn(`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`);
`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 ) { } else if ( items.length === 0 ) {
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`); return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
} }

View file

@ -3,17 +3,14 @@
* @return {Promise} A Promise which resolves once the migration is completed * @return {Promise} A Promise which resolves once the migration is completed
*/ */
export const migrateWorld = async function() { export const migrateWorld = async function() {
ui.notifications.info( 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});
`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 // Migrate World Actors
for await (let a of game.actors.contents) { for await ( let a of game.actors.entities ) {
try { try {
console.log(`Checking Actor entity ${a.name} for migration needs`); console.log(`Checking Actor entity ${a.name} for migration needs`);
const updateData = await migrateActorData(a.data); const updateData = await migrateActorData(a.data);
if (!foundry.utils.isObjectEmpty(updateData)) { if ( !isObjectEmpty(updateData) ) {
console.log(`Migrating Actor entity ${a.name}`); console.log(`Migrating Actor entity ${a.name}`);
await a.update(updateData, {enforceTypes: false}); await a.update(updateData, {enforceTypes: false});
} }
@ -24,10 +21,10 @@ export const migrateWorld = async function () {
} }
// Migrate World Items // Migrate World Items
for (let i of game.items.contents) { for ( let i of game.items.entities ) {
try { try {
const updateData = migrateItemData(i.toObject()); const updateData = migrateItemData(i.data);
if (!foundry.utils.isObjectEmpty(updateData)) { if ( !isObjectEmpty(updateData) ) {
console.log(`Migrating Item entity ${i.name}`); console.log(`Migrating Item entity ${i.name}`);
await i.update(updateData, {enforceTypes: false}); await i.update(updateData, {enforceTypes: false});
} }
@ -38,15 +35,12 @@ export const migrateWorld = async function () {
} }
// Migrate Actor Override Tokens // Migrate Actor Override Tokens
for (let s of game.scenes.contents) { for ( let s of game.scenes.entities ) {
try { try {
const updateData = await migrateSceneData(s.data); const updateData = await migrateSceneData(s.data);
if (!foundry.utils.isObjectEmpty(updateData)) { if ( !isObjectEmpty(updateData) ) {
console.log(`Migrating Scene entity ${s.name}`); console.log(`Migrating Scene entity ${s.name}`);
await s.update(updateData, {enforceTypes: false}); 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) { } catch(err) {
err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`; err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
@ -83,37 +77,40 @@ export const migrateCompendium = async function (pack) {
// Begin by requesting server-side data model migration and get the migrated content // Begin by requesting server-side data model migration and get the migrated content
await pack.migrate(); await pack.migrate();
const documents = await pack.getDocuments(); const content = await pack.getContent();
// Iterate over compendium entries - applying fine-tuned migration functions // Iterate over compendium entries - applying fine-tuned migration functions
for await (let doc of documents) { for await ( let ent of content ) {
let updateData = {}; let updateData = {};
try { try {
switch (entity) { switch (entity) {
case "Actor": case "Actor":
updateData = await migrateActorData(doc.data); updateData = await migrateActorData(ent.data);
break; break;
case "Item": case "Item":
updateData = migrateItemData(doc.toObject()); updateData = migrateItemData(ent.data);
break; break;
case "Scene": case "Scene":
updateData = await migrateSceneData(doc.data); updateData = await migrateSceneData(ent.data);
break; break;
} }
if (foundry.utils.isObjectEmpty(updateData)) continue; if ( isObjectEmpty(updateData) ) continue;
// Save the entry, if data was changed // Save the entry, if data was changed
await doc.update(updateData); updateData["_id"] = ent._id;
console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`); await pack.updateEntity(updateData);
} catch (err) { console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`);
}
// Handle migration failures // Handle migration failures
err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`; catch(err) {
err.message = `Failed sw5e system migration for entity ${ent.name} in pack ${pack.collection}: ${err.message}`;
console.error(err); console.error(err);
} }
} }
// Apply the original locked status for the pack // Apply the original locked status for the pack
await pack.configure({locked: wasLocked}); pack.configure({locked: wasLocked});
console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`); console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`);
}; };
@ -131,40 +128,34 @@ export const migrateActorData = async function (actor) {
const updateData = {}; const updateData = {};
// Actor Data Updates // Actor Data Updates
if (actor.data) {
_migrateActorMovement(actor, updateData); _migrateActorMovement(actor, updateData);
_migrateActorSenses(actor, updateData); _migrateActorSenses(actor, updateData);
_migrateActorType(actor, updateData);
}
// Migrate Owned Items // Migrate Owned Items
if ( !!actor.items ) { if ( !!actor.items ) {
let hasItemUpdates = false;
const items = await actor.items.reduce(async (memo, i) => { const items = await actor.items.reduce(async (memo, i) => {
const results = await memo; const results = await memo;
// Migrate the Owned Item // Migrate the Owned Item
const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i; let itemUpdate = await migrateActorItemData(i, actor);
let itemUpdate = await migrateActorItemData(itemData, actor);
// Prepared, Equipped, and Proficient for NPC actors // Prepared, Equipped, and Proficient for NPC actors
if ( actor.type === "npc" ) { if ( actor.type === "npc" ) {
if (getProperty(itemData.data, "preparation.prepared") === false) if (getProperty(i.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
itemUpdate["data.preparation.prepared"] = true; if (getProperty(i.data, "equipped") === false) itemUpdate["data.equipped"] = true;
if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true; if (getProperty(i.data, "proficient") === false) itemUpdate["data.proficient"] = true;
if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true;
} }
// Update the Owned Item // Update the Owned Item
if ( !isObjectEmpty(itemUpdate) ) { if ( !isObjectEmpty(itemUpdate) ) {
itemUpdate._id = itemData._id; hasItemUpdates = true;
console.log(`Migrating Actor ${actor.name}'s ${i.name}`); console.log(`Migrating Actor ${actor.name}'s ${i.name}`);
results.push(expandObject(itemUpdate)); return [...results, mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false})];
} } else return [...results, i];
return results;
}, []); }, []);
if (items.length > 0) updateData.items = items; if ( hasItemUpdates ) updateData.items = items;
} }
// Update NPC data with new datamodel information // Update NPC data with new datamodel information
@ -180,12 +171,14 @@ export const migrateActorData = async function (actor) {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Scrub an Actor's system data, removing all keys which are not explicitly defined in the system template * 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 * @param {Object} actorData The data object for an Actor
* @return {Object} The scrubbed Actor data * @return {Object} The scrubbed Actor data
*/ */
function cleanActorData(actorData) { function cleanActorData(actorData) {
// Scrub system data // Scrub system data
const model = game.system.model.Actor[actorData.type]; const model = game.system.model.Actor[actorData.type];
actorData.data = filterObject(actorData.data, model); actorData.data = filterObject(actorData.data, model);
@ -203,13 +196,12 @@ function cleanActorData(actorData) {
return actorData; return actorData;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Migrate a single Item entity to incorporate latest data model changes * 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) { export const migrateItemData = function(item) {
const updateData = {}; const updateData = {};
@ -242,34 +234,24 @@ export const migrateActorItemData = async function (item, actor) {
* @return {Object} The updateData to apply * @return {Object} The updateData to apply
*/ */
export const migrateSceneData = async function(scene) { export const migrateSceneData = async function(scene) {
const tokens = await Promise.all( const tokens = duplicate(scene.tokens);
scene.tokens.map(async (token) => { return {
const t = token.toJSON(); tokens: await Promise.all(tokens.map(async (t) => {
if (!t.actorId || t.actorLink) { if (!t.actorId || t.actorLink || !t.actorData.data) {
t.actorData = {}; t.actorData = {};
} else if (!game.actors.has(t.actorId)) { return t;
}
const token = new Token(t);
if ( !token.actor ) {
t.actorId = null; t.actorId = null;
t.actorData = {}; t.actorData = {};
} else if ( !t.actorLink ) { } else if ( !t.actorLink ) {
const actorData = duplicate(t.actorData); const updateData = await migrateActorData(token.data.actorData);
actorData.type = token.actor?.type; t.actorData = mergeObject(token.data.actorData, updateData);
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; return t;
}) }))
); };
return {tokens};
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -284,6 +266,7 @@ export const migrateSceneData = async function (scene) {
* @return {Object} The updated Actor * @return {Object} The updated Actor
*/ */
function _updateNPCData(actor) { function _updateNPCData(actor) {
let actorData = actor.data; let actorData = actor.data;
const updateData = {}; const updateData = {};
// check for flag.core, if not there is no compendium monster so exit // check for flag.core, if not there is no compendium monster so exit
@ -291,20 +274,13 @@ function _updateNPCData(actor) {
if (!hasSource) return actor; if (!hasSource) return actor;
// shortcut out if dataVersion flag is set to 1.2.4 or higher // shortcut out if dataVersion flag is set to 1.2.4 or higher
const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined; const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined;
if ( if (hasDataVersion && (actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion))) return actor;
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 // Check to see what the source of NPC is
const sourceId = actor.flags.core.sourceId; const sourceId = actor.flags.core.sourceId;
const coreSource = sourceId.substr(0,sourceId.length-17); const coreSource = sourceId.substr(0,sourceId.length-17);
const core_id = sourceId.substr(sourceId.length-16,16); const core_id = sourceId.substr(sourceId.length-16,16);
if (coreSource === "Compendium.sw5e.monsters"){ if (coreSource === "Compendium.sw5e.monsters"){
game.packs game.packs.get("sw5e.monsters").getEntity(core_id).then(monster => {
.get("sw5e.monsters")
.getEntity(core_id)
.then((monster) => {
const monsterData = monster.data.data; const monsterData = monster.data.data;
// copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel // copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel
updateData["data.attributes.movement"] = monsterData.attributes.movement; updateData["data.attributes.movement"] = monsterData.attributes.movement;
@ -320,9 +296,7 @@ function _updateNPCData(actor) {
const itemData = i.data; const itemData = i.data;
if ( itemData.type === "power" ) { if ( itemData.type === "power" ) {
const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0]; const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0];
let hasPower = !!actor.items.find( let hasPower = !!actor.items.find(item => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id);
(item) => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id
);
if (!hasPower) { if (!hasPower) {
// Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness. // 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)); const newPower = JSON.parse(JSON.stringify(itemData));
@ -339,15 +313,17 @@ function _updateNPCData(actor) {
// set flag to check to see if migration has been done so we don't do it again. // 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"); liveActor.setFlag("sw5e", "dataVersion", "1.2.4");
}); })
} }
//merge object //merge object
actorData = mergeObject(actorData, updateData); actorData = mergeObject(actorData, updateData);
// Return the scrubbed data // Return the scrubbed data
return actor; return actor;
} }
/** /**
* Migrate the actor speed string to movement object * Migrate the actor speed string to movement object
* @private * @private
@ -356,21 +332,21 @@ function _migrateActorMovement(actorData, updateData) {
const ad = actorData.data; const ad = actorData.data;
// Work is needed if old data is present // Work is needed if old data is present
const old = actorData.type === "vehicle" ? ad?.attributes?.speed : ad?.attributes?.speed?.value; const old = actorData.type === 'vehicle' ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
const hasOld = old !== undefined; const hasOld = old !== undefined;
if ( hasOld ) { if ( hasOld ) {
// If new data is not present, migrate the old data // If new data is not present, migrate the old data
const hasNew = ad?.attributes?.movement?.walk !== undefined; const hasNew = ad?.attributes?.movement?.walk !== undefined;
if (!hasNew && typeof old === "string") { if ( !hasNew && (typeof old === "string") ) {
const s = (old || "").split(" "); const s = (old || "").split(" ");
if (s.length > 0) if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
} }
// Remove the old attribute // Remove the old attribute
updateData["data.attributes.-=speed"] = null; updateData["data.attributes.-=speed"] = null;
} }
return updateData; return updateData
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -431,7 +407,7 @@ function _migrateActorPowers(actorData, updateData) {
// Remove the Power DC Bonus // Remove the Power DC Bonus
updateData["data.bonuses.power.-=dc"] = null; updateData["data.bonuses.power.-=dc"] = null;
return updateData; return updateData
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -444,7 +420,6 @@ function _migrateActorSenses(actor, updateData) {
const ad = actor.data; const ad = actor.data;
if ( ad?.traits?.senses === undefined ) return; if ( ad?.traits?.senses === undefined ) return;
const original = ad.traits.senses || ""; 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" // 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]+)?/; const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/;
@ -474,86 +449,6 @@ function _migrateActorSenses(actor, 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;
// 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 (?<size>[\w\-]+) )?(?<type>[^(]+?)(?:\((?<subtype>[^)]+)\))?$/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 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;
}
}
// Update the actor data
updateData["data.details.type"] = data;
return updateData;
}
/* -------------------------------------------- */
/** /**
* @private * @private
*/ */
@ -561,34 +456,19 @@ function _migrateItemClassPowerCasting(item, updateData) {
if (item.type === "class"){ if (item.type === "class"){
switch (item.name){ switch (item.name){
case "Consular": case "Consular":
updateData["data.powercasting"] = { updateData["data.powercasting"] = "consular";
progression: "consular",
ability: ""
};
break; break;
case "Engineer": case "Engineer":
updateData["data.powercasting"] = { updateData["data.powercasting"] = "engineer";
progression: "engineer",
ability: ""
};
break; break;
case "Guardian": case "Guardian":
updateData["data.powercasting"] = { updateData["data.powercasting"] = "guardian";
progression: "guardian",
ability: ""
};
break; break;
case "Scout": case "Scout":
updateData["data.powercasting"] = { updateData["data.powercasting"] = "scout";
progression: "scout",
ability: ""
};
break; break;
case "Sentinel": case "Sentinel":
updateData["data.powercasting"] = { updateData["data.powercasting"] = "sentinel";
progression: "sentinel",
ability: ""
};
break; break;
} }
} }
@ -614,11 +494,7 @@ async function _migrateItemPower(item, actor, updateData) {
// shortcut out if dataVersion flag is set to 1.2.4 or higher // shortcut out if dataVersion flag is set to 1.2.4 or higher
const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined; const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined;
if ( if (hasDataVersion && (item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion))) return updateData;
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 // Check to see what the source of Power is
const sourceId = item.flags.core.sourceId; const sourceId = item.flags.core.sourceId;
@ -636,10 +512,11 @@ async function _migrateItemPower(item, actor, updateData) {
const corePowerData = corePower.data; const corePowerData = corePower.data;
// copy Core Power Data over original Power // copy Core Power Data over original Power
updateData["data"] = corePowerData; updateData["data"] = corePowerData;
updateData["flags"] = {sw5e: {dataVersion: "1.2.4"}}; updateData["flags"] = {"sw5e": {"dataVersion": "1.2.4"}};
return updateData; return updateData;
//game.packs.get(powerType).getEntity(core_id).then(corePower => { //game.packs.get(powerType).getEntity(core_id).then(corePower => {
//}) //})
@ -649,14 +526,10 @@ async function _migrateItemPower(item, actor, updateData) {
/** /**
* Delete the old data.attuned boolean * Delete the old data.attuned boolean
*
* @param {object} item Item data to migrate
* @param {object} updateData Existing update to expand upon
* @return {object} The updateData to apply
* @private * @private
*/ */
function _migrateItemAttunement(item, updateData) { function _migrateItemAttunement(item, updateData) {
if (item.data?.attuned === undefined) return updateData; if ( item.data.attuned === undefined ) return;
updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE; updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE;
updateData["data.-=attuned"] = null; updateData["data.-=attuned"] = null;
return updateData; return updateData;
@ -679,10 +552,10 @@ export async function purgeFlags(pack) {
for ( let entity of content ) { for ( let entity of content ) {
const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)}; const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)};
if ( pack.entity === "Actor" ) { if ( pack.entity === "Actor" ) {
update.items = entity.data.items.map((i) => { update.items = entity.data.items.map(i => {
i.flags = cleanFlags(i.flags); i.flags = cleanFlags(i.flags);
return i; return i;
}); })
} }
await pack.updateEntity(update, {recursive: false}); await pack.updateEntity(update, {recursive: false});
console.log(`Purged flags from ${entity.name}`); console.log(`Purged flags from ${entity.name}`);
@ -692,6 +565,7 @@ export async function purgeFlags(pack) {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Purge the data model of any inner objects which have been flagged as _deprecated. * Purge the data model of any inner objects which have been flagged as _deprecated.
* @param {object} data The data to clean * @param {object} data The data to clean
@ -703,7 +577,8 @@ export function removeDeprecatedObjects(data) {
if (v._deprecated === true) { if (v._deprecated === true) {
console.log(`Deleting deprecated object key ${k}`); console.log(`Deleting deprecated object key ${k}`);
delete data[k]; delete data[k];
} else removeDeprecatedObjects(v); }
else removeDeprecatedObjects(v);
} }
} }
return data; return data;

View file

@ -5,6 +5,7 @@ import {SW5E} from "../config.js";
* @extends {MeasuredTemplate} * @extends {MeasuredTemplate}
*/ */
export default class AbilityTemplate extends MeasuredTemplate { export default class AbilityTemplate extends MeasuredTemplate {
/** /**
* A factory method to create an AbilityTemplate instance using provided data from an Item5e instance * 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 * @param {Item5e} item The Item object for which to construct the template
@ -18,7 +19,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
// Prepare template data // Prepare template data
const templateData = { const templateData = {
t: templateShape, t: templateShape,
user: game.user.data._id, user: game.user._id,
distance: target.value, distance: target.value,
direction: 0, direction: 0,
x: 0, x: 0,
@ -44,12 +45,10 @@ export default class AbilityTemplate extends MeasuredTemplate {
} }
// Return the template constructed from the item data // Return the template constructed from the item data
const cls = CONFIG.MeasuredTemplate.documentClass; const template = new this(templateData);
const template = new cls(templateData, {parent: canvas.scene}); template.item = item;
const object = new this(template); template.actorSheet = item.actor?.sheet || null;
object.item = item; return template;
object.actorSheet = item.actor?.sheet || null;
return object;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -83,19 +82,20 @@ export default class AbilityTemplate extends MeasuredTemplate {
let moveTime = 0; let moveTime = 0;
// Update placement (mouse-move) // Update placement (mouse-move)
handlers.mm = (event) => { handlers.mm = event => {
event.stopPropagation(); event.stopPropagation();
let now = Date.now(); // Apply a 20ms throttle let now = Date.now(); // Apply a 20ms throttle
if ( now - moveTime <= 20 ) return; if ( now - moveTime <= 20 ) return;
const center = event.data.getLocalPosition(this.layer); const center = event.data.getLocalPosition(this.layer);
const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2); const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2);
this.data.update({x: snapped.x, y: snapped.y}); this.data.x = snapped.x;
this.data.y = snapped.y;
this.refresh(); this.refresh();
moveTime = now; moveTime = now;
}; };
// Cancel the workflow (right-click) // Cancel the workflow (right-click)
handlers.rc = (event) => { handlers.rc = event => {
this.layer.preview.removeChildren(); this.layer.preview.removeChildren();
canvas.stage.off("mousemove", handlers.mm); canvas.stage.off("mousemove", handlers.mm);
canvas.stage.off("mousedown", handlers.lc); canvas.stage.off("mousedown", handlers.lc);
@ -106,20 +106,25 @@ export default class AbilityTemplate extends MeasuredTemplate {
}; };
// Confirm the workflow (left-click) // Confirm the workflow (left-click)
handlers.lc = (event) => { handlers.lc = event => {
handlers.rc(event); handlers.rc(event);
const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
this.data.update(destination); // Confirm final snapped position
canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]); const destination = canvas.grid.getSnappedPosition(this.x, this.y, 2);
this.data.x = destination.x;
this.data.y = destination.y;
// Create the template
canvas.scene.createEmbeddedEntity("MeasuredTemplate", this.data);
}; };
// Rotate the template by 3 degree increments (mouse-wheel) // Rotate the template by 3 degree increments (mouse-wheel)
handlers.mw = (event) => { handlers.mw = event => {
if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window
event.stopPropagation(); event.stopPropagation();
let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15; let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
let snap = event.shiftKey ? delta : 5; let snap = event.shiftKey ? delta : 5;
this.data.update({direction: this.data.direction + snap * Math.sign(event.deltaY)}); this.data.direction += (snap * Math.sign(event.deltaY));
this.refresh(); this.refresh();
}; };

View file

@ -1,4 +1,5 @@
export const registerSystemSettings = function() { export const registerSystemSettings = function() {
/** /**
* Track the system version upon which point a migration was last applied * Track the system version upon which point a migration was last applied
*/ */
@ -7,7 +8,7 @@ export const registerSystemSettings = function () {
scope: "world", scope: "world",
config: false, config: false,
type: String, type: String,
default: game.system.data.version default: ""
}); });
/** /**
@ -21,9 +22,9 @@ export const registerSystemSettings = function () {
default: "normal", default: "normal",
type: String, type: String,
choices: { choices: {
normal: "SETTINGS.5eRestPHB", "normal": "SETTINGS.5eRestPHB",
gritty: "SETTINGS.5eRestGritty", "gritty": "SETTINGS.5eRestGritty",
epic: "SETTINGS.5eRestEpic" "epic": "SETTINGS.5eRestEpic",
} }
}); });
@ -38,11 +39,11 @@ export const registerSystemSettings = function () {
default: "555", default: "555",
type: String, type: String,
choices: { choices: {
555: "SETTINGS.5eDiagPHB", "555": "SETTINGS.5eDiagPHB",
5105: "SETTINGS.5eDiagDMG", "5105": "SETTINGS.5eDiagDMG",
EUCL: "SETTINGS.5eDiagEuclidean" "EUCL": "SETTINGS.5eDiagEuclidean",
}, },
onChange: (rule) => (canvas.grid.diagonalRule = rule) onChange: rule => canvas.grid.diagonalRule = rule
}); });
/** /**
@ -78,7 +79,7 @@ export const registerSystemSettings = function () {
scope: "world", scope: "world",
config: true, config: true,
default: false, default: false,
type: Boolean type: Boolean,
}); });
/** /**
@ -91,7 +92,7 @@ export const registerSystemSettings = function () {
config: true, config: true,
default: false, default: false,
type: Boolean, type: Boolean,
onChange: (s) => { onChange: s => {
ui.chat.render(); ui.chat.render();
} }
}); });
@ -99,10 +100,10 @@ export const registerSystemSettings = function () {
/** /**
* Option to allow GMs to restrict polymorphing to GMs only. * Option to allow GMs to restrict polymorphing to GMs only.
*/ */
game.settings.register("sw5e", "allowPolymorphing", { game.settings.register('sw5e', 'allowPolymorphing', {
name: "SETTINGS.5eAllowPolymorphingN", name: 'SETTINGS.5eAllowPolymorphingN',
hint: "SETTINGS.5eAllowPolymorphingL", hint: 'SETTINGS.5eAllowPolymorphingL',
scope: "world", scope: 'world',
config: true, config: true,
default: false, default: false,
type: Boolean type: Boolean
@ -111,8 +112,8 @@ export const registerSystemSettings = function () {
/** /**
* Remember last-used polymorph settings. * Remember last-used polymorph settings.
*/ */
game.settings.register("sw5e", "polymorphSettings", { game.settings.register('sw5e', 'polymorphSettings', {
scope: "client", scope: 'client',
default: { default: {
keepPhysical: false, keepPhysical: false,
keepMental: false, keepMental: false,
@ -137,8 +138,8 @@ export const registerSystemSettings = function () {
default: "light", default: "light",
type: String, type: String,
choices: { choices: {
light: "SETTINGS.SWColorLight", "light": "SETTINGS.SWColorLight",
dark: "SETTINGS.SWColorDark" "dark": "SETTINGS.SWColorDark"
} }
}); });
}; };

View file

@ -5,6 +5,7 @@
*/ */
export const preloadHandlebarsTemplates = async function() { export const preloadHandlebarsTemplates = async function() {
return loadTemplates([ return loadTemplates([
// Shared Partials // Shared Partials
"systems/sw5e/templates/actors/parts/active-effects.html", "systems/sw5e/templates/actors/parts/active-effects.html",

View file

@ -1,102 +0,0 @@
/**
* Extend the base TokenDocument class to implement system-specific HP bar logic.
* @extends {TokenDocument}
*/
export class TokenDocument5e extends TokenDocument {
/** @inheritdoc */
getBarAttribute(...args) {
const data = super.getBarAttribute(...args);
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);
}
return data;
}
}
/* -------------------------------------------- */
/**
* 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);
return super._drawBar(number, bar, data);
}
/* -------------------------------------------- */
/**
* Specialized drawing function for HP bars.
* @param {number} number The Bar number
* @param {PIXI.Graphics} bar The Bar container
* @param {object} data Resource data for this bar
* @private
*/
_drawHPBar(number, bar, data) {
// Extract health data
let {value, max, temp, tempmax} = this.document.actor.data.data.attributes.hp;
temp = Number(temp || 0);
tempmax = Number(tempmax || 0);
// Differentiate between effective maximum and displayed maximum
const effectiveMax = Math.max(0, max + tempmax);
let displayMax = max + (tempmax > 0 ? tempmax : 0);
// Allocate percentages of the total
const tempPct = Math.clamped(temp, 0, displayMax) / displayMax;
const valuePct = Math.clamped(value, 0, effectiveMax) / displayMax;
const colorPct = Math.clamped(value, 0, effectiveMax) / displayMax;
// Determine colors to use
const blk = 0x000000;
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;
const bs = Math.clamped(h / 8, 1, 2);
const bs1 = bs + 1;
// Overall bar container
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);
}
// 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);
}
// Health bar
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);
}
// Set position
let posY = number === 0 ? this.h - h : 0;
bar.position.set(0, posY);
}
}

12
package-lock.json generated
View file

@ -1266,9 +1266,9 @@
} }
}, },
"hosted-git-info": { "hosted-git-info": {
"version": "2.8.9", "version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
}, },
"image-size": { "image-size": {
"version": "0.5.5", "version": "0.5.5",
@ -3068,9 +3068,9 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
}, },
"y18n": { "y18n": {
"version": "3.2.2", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
"integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==" "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
}, },
"yargs": { "yargs": {
"version": "7.1.1", "version": "7.1.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

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