Compare commits

..

1 commit

Author SHA1 Message Date
TJ
42ddf4b0d0 Initial styling config and auto-format of files 2021-03-24 19:41:50 -05:00
234 changed files with 12658 additions and 20976 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 +1,7 @@
{ {
"printWidth": 120, "printWidth": 120,
"tabWidth": 4, "tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "consistent",
"jsxSingleQuote": false,
"trailingComma": "none", "trailingComma": "none",
"bracketSpacing": false, "bracketSpacing": false,
"jsxBracketSameLine": false, "arrowParens": "avoid"
"arrowParens": "always",
"endOfLine": "lf"
} }

View file

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

View file

@ -1,7 +1,6 @@
{ {
"ACTOR.TypeCharacter": "Player Character", "ACTOR.TypeCharacter": "Player Character",
"ACTOR.TypeNpc": "Non-Player Character", "ACTOR.TypeNpc": "Non-Player Character",
"ACTOR.TypeStarship": "Starship",
"ACTOR.TypeVehicle": "Vehicle", "ACTOR.TypeVehicle": "Vehicle",
"ITEM.TypeArchetype": "Archetype", "ITEM.TypeArchetype": "Archetype",
"ITEM.TypeBackground": "Background", "ITEM.TypeBackground": "Background",
@ -9,8 +8,6 @@
"ITEM.TypeClass": "Class", "ITEM.TypeClass": "Class",
"ITEM.TypeClassfeature": "Class Feature", "ITEM.TypeClassfeature": "Class Feature",
"ITEM.TypeConsumable": "Consumable", "ITEM.TypeConsumable": "Consumable",
"ITEM.TypeDeployment": "Deployment",
"ITEM.TypeDeploymentfeature": "Deployment Feature",
"ITEM.TypeEquipment": "Equipment", "ITEM.TypeEquipment": "Equipment",
"ITEM.TypeFeat": "Feat", "ITEM.TypeFeat": "Feat",
"ITEM.TypeFightingmastery": "FightingMastery", "ITEM.TypeFightingmastery": "FightingMastery",
@ -19,12 +16,7 @@
"ITEM.TypeLoot": "Loot", "ITEM.TypeLoot": "Loot",
"ITEM.TypePower": "Power", "ITEM.TypePower": "Power",
"ITEM.TypeSpecies": "Species", "ITEM.TypeSpecies": "Species",
"ITEM.TypeStarshipfeature": "Starship Feature",
"ITEM.TypeStarshipfeaturePl": "Starship Features",
"ITEM.TypeStarshipmod": "Starship Modification",
"ITEM.TypeStarshipmodPl": "Starship Modifications",
"ITEM.TypeTool": "Tool", "ITEM.TypeTool": "Tool",
"ITEM.TypeVenture": "Venture",
"ITEM.TypeWeapon": "Weapon", "ITEM.TypeWeapon": "Weapon",
"SETTINGS.5eAllowPolymorphingL": "Allow players to polymorph their own actors.", "SETTINGS.5eAllowPolymorphingL": "Allow players to polymorph their own actors.",
"SETTINGS.5eAllowPolymorphingN": "Allow Polymorphing", "SETTINGS.5eAllowPolymorphingN": "Allow Polymorphing",
@ -43,13 +35,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 +70,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 +96,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 +110,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",
@ -193,9 +179,6 @@
"SW5E.BonusSaveForm": "Update Bonuses", "SW5E.BonusSaveForm": "Update Bonuses",
"SW5E.BonusTechPowerDC": "Global Tech Power DC Bonus", "SW5E.BonusTechPowerDC": "Global Tech Power DC Bonus",
"SW5E.BonusTitle": "Configure Actor Bonuses", "SW5E.BonusTitle": "Configure Actor Bonuses",
"SW5E.BurnFuel": "Burn",
"SW5E.CapacityMultiplier": "Capacity Multiplier",
"SW5E.CentStorageCapacity": "Central Storage Capacity",
"SW5E.ChallengeRating": "Challenge Rating", "SW5E.ChallengeRating": "Challenge Rating",
"SW5E.Charged": "Charged", "SW5E.Charged": "Charged",
"SW5E.Charges": "Charges", "SW5E.Charges": "Charges",
@ -205,14 +188,9 @@
"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.ComponentMaterial": "Material", "SW5E.ComponentMaterial": "Material",
"SW5E.ComponentSomatic": "Somatic", "SW5E.ComponentSomatic": "Somatic",
"SW5E.ComponentVerbal": "Verbal", "SW5E.ComponentVerbal": "Verbal",
@ -271,32 +249,6 @@
"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.Crewed": "Crewed",
"SW5E.Critical": "Critical", "SW5E.Critical": "Critical",
"SW5E.CriticalHit": "Critical Hit", "SW5E.CriticalHit": "Critical Hit",
"SW5E.Currency": "Currency", "SW5E.Currency": "Currency",
@ -330,11 +282,8 @@
"SW5E.DeathSavingThrow": "Death Saving Throw", "SW5E.DeathSavingThrow": "Death Saving Throw",
"SW5E.Default": "Default", "SW5E.Default": "Default",
"SW5E.DefaultAbilityCheck": "Default Ability Check", "SW5E.DefaultAbilityCheck": "Default Ability Check",
"SW5E.Deployment": "Deployment",
"SW5E.DeploymentPl": "Deployments",
"SW5E.description": "A comprehensive game system for running games of Star Wars 5th Edition in the Foundry VTT environment.", "SW5E.description": "A comprehensive game system for running games of Star Wars 5th Edition in the Foundry VTT environment.",
"SW5E.Description": "Description", "SW5E.Description": "Description",
"SW5E.DestructionSave": "Destruction Saves",
"SW5E.Details": "Details", "SW5E.Details": "Details",
"SW5E.Dimensions": "Dimensions", "SW5E.Dimensions": "Dimensions",
"SW5E.Disadvantage": "Disadvantage", "SW5E.Disadvantage": "Disadvantage",
@ -343,55 +292,38 @@
"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.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.EquipmentPowerCoupling": "Power Coupling",
"SW5E.EquipmentReactor": "Reactor",
"SW5E.EquipmentShield": "Shield", "SW5E.EquipmentShield": "Shield",
"SW5E.EquipmentShieldProficiency": "Shields", "SW5E.EquipmentShieldProficiency": "Shields",
"SW5E.EquipmentStarshipArmor": "Starship Armor",
"SW5E.EquipmentStarshipShield": "Starship Shield",
"SW5E.EquipmentTrinket": "Trinket", "SW5E.EquipmentTrinket": "Trinket",
"SW5E.EquipmentVehicle": "Vehicle Equipment", "SW5E.EquipmentVehicle": "Vehicle Equipment",
"SW5E.Equipped": "Equipped", "SW5E.Equipped": "Equipped",
"SW5E.Exhaustion": "Exhaustion", "SW5E.Exhaustion": "Exhaustion",
"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",
"SW5E.FeatureAttack": "Feature Attack", "SW5E.FeatureAttack": "Feature Attack",
"SW5E.FeatureCollapse": "Collapse Feature",
"SW5E.FeatureExpand": "Expand Feature",
"SW5E.FeaturePassive": "Passive Abilities", "SW5E.FeaturePassive": "Passive Abilities",
"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.FeetAbbr": "ft.", "SW5E.FeetAbbr": "ft.",
"SW5E.Filter": "Filter", "SW5E.Filter": "Filter",
@ -521,31 +453,19 @@
"SW5E.Flaws": "Flaws", "SW5E.Flaws": "Flaws",
"SW5E.ForcePowerbook": "Force Powers", "SW5E.ForcePowerbook": "Force Powers",
"SW5E.Formula": "Formula", "SW5E.Formula": "Formula",
"SW5E.FuelCapacity": "Fuel Capacity",
"SW5E.FuelCostPerUnit": "Fuel Cost per Unit",
"SW5E.FuelCostsMod": "Fuel Costs Modifier",
"SW5E.GrantedAbilities": "Granted Abilities", "SW5E.GrantedAbilities": "Granted Abilities",
"SW5E.HalfProficient": "Half Proficient", "SW5E.HalfProficient": "Half Proficient",
"SW5E.HardpointSizeMod": "Hardpoint Size Modifier",
"SW5E.Healing": "Healing", "SW5E.Healing": "Healing",
"SW5E.HealingTemp": "Healing (Temporary)", "SW5E.HealingTemp": "Healing (Temporary)",
"SW5E.Health": "Health", "SW5E.Health": "Health",
"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!",
"SW5E.HP": "Health", "SW5E.HP": "Health",
"SW5E.HPFormula": "Health Formula", "SW5E.HPFormula": "Health Formula",
"SW5E.HullDice": "Hull Dice",
"SW5E.HullPoints": "Hull Points",
"SW5E.HullPointsFormula": "Hull Points Formula",
"SW5E.HyperdriveClass": "Hyperdrive Class",
"SW5E.Ideals": "Ideals", "SW5E.Ideals": "Ideals",
"SW5E.Identified": "Identified", "SW5E.Identified": "Identified",
"SW5E.Initiative": "Initiative", "SW5E.Initiative": "Initiative",
@ -589,7 +509,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",
@ -598,10 +517,6 @@
"SW5E.ItemTypeConsumablePl": "Consumables", "SW5E.ItemTypeConsumablePl": "Consumables",
"SW5E.ItemTypeContainer": "Container", "SW5E.ItemTypeContainer": "Container",
"SW5E.ItemTypeContainerPl": "Containers", "SW5E.ItemTypeContainerPl": "Containers",
"SW5E.ItemTypeDeployment": "Deployment",
"SW5E.ItemTypeDeploymentFeature": "Deployment Feature",
"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",
@ -618,19 +533,13 @@
"SW5E.ItemTypePowerPl": "Powers", "SW5E.ItemTypePowerPl": "Powers",
"SW5E.ItemTypeSpecies": "Species", "SW5E.ItemTypeSpecies": "Species",
"SW5E.ItemTypeSpeciesPl": "Species", "SW5E.ItemTypeSpeciesPl": "Species",
"SW5E.ItemTypeStarshipMod": "Starship Modification",
"SW5E.ItemTypeStarshipModPl": "Starship Modifications",
"SW5E.ItemTypeTool": "Tool", "SW5E.ItemTypeTool": "Tool",
"SW5E.ItemTypeToolPl": "Tools", "SW5E.ItemTypeToolPl": "Tools",
"SW5E.ItemTypeVenture": "Venture",
"SW5E.ItemTypeVenturePl": "Ventures",
"SW5E.ItemTypeWeapon": "Weapon", "SW5E.ItemTypeWeapon": "Weapon",
"SW5E.ItemTypeWeaponPl": "Weapons", "SW5E.ItemTypeWeaponPl": "Weapons",
"SW5E.ItemWeaponAttack": "Weapon Attack", "SW5E.ItemWeaponAttack": "Weapon Attack",
"SW5E.ItemWeaponDetails": "Weapon Details", "SW5E.ItemWeaponDetails": "Weapon Details",
"SW5E.ItemWeaponProperties": "Weapon Properties", "SW5E.ItemWeaponProperties": "Weapon Properties",
"SW5E.ItemWeaponSize": "Starship Weapon Size",
"SW5E.ItemWeaponSizePl": "Starship Weapon Sizes",
"SW5E.ItemWeaponStatus": "Weapon Status", "SW5E.ItemWeaponStatus": "Weapon Status",
"SW5E.ItemWeaponType": "Weapon Type", "SW5E.ItemWeaponType": "Weapon Type",
"SW5E.ItemWeaponUsage": "Weapon Usage", "SW5E.ItemWeaponUsage": "Weapon Usage",
@ -756,7 +665,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,25 +684,18 @@
"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.Movement": "Movement", "SW5E.Movement": "Movement",
"SW5E.MovementBurrow": "Burrow", "SW5E.MovementBurrow": "Burrow",
"SW5E.MovementClimb": "Climb", "SW5E.MovementClimb": "Climb",
"SW5E.MovementConfig": "Configure Movement Speed", "SW5E.MovementConfig": "Configure Movement Speed",
"SW5E.MovementConfigHint": "Configure the movement speed and special movement attributes of this creature.", "SW5E.MovementConfigHint": "Configure the movement speed and special movement attributes of this creature.",
"SW5E.MovementCrawl": "Crawl",
"SW5E.MovementFly": "Fly", "SW5E.MovementFly": "Fly",
"SW5E.MovementHover": "Hover", "SW5E.MovementHover": "Hover",
"SW5E.MovementRoll": "Roll",
"SW5E.MovementSpace": "Space Flight",
"SW5E.MovementSwim": "Swim", "SW5E.MovementSwim": "Swim",
"SW5E.MovementTurn": "Turning",
"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 +748,7 @@
"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.PowerDieAlloc": "Power Die Allocation",
"SW5E.PowerDiePl": "Power Dice",
"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,7 +778,6 @@
"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.PowerTarget": "Power Target", "SW5E.PowerTarget": "Power Target",
@ -894,29 +789,21 @@
"SW5E.Proficient": "Proficient", "SW5E.Proficient": "Proficient",
"SW5E.Quantity": "Quantity", "SW5E.Quantity": "Quantity",
"SW5E.Range": "Range", "SW5E.Range": "Range",
"SW5E.Rank": "Rank",
"SW5E.RankPl": "Ranks",
"SW5E.Rarity": "Rarity", "SW5E.Rarity": "Rarity",
"SW5E.Reaction": "Reaction", "SW5E.Reaction": "Reaction",
"SW5E.ReactionPl": "Reactions", "SW5E.ReactionPl": "Reactions",
"SW5E.Recharge": "Recharge", "SW5E.Recharge": "Recharge",
"SW5E.Refitting": "Refitting",
"SW5E.Refuel": "Refuel",
"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.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 +816,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",
@ -948,10 +834,6 @@
"SW5E.SheetClassNPC": "Default NPC Sheet", "SW5E.SheetClassNPC": "Default NPC Sheet",
"SW5E.SheetClassNPCOld": "Old NPC Sheet", "SW5E.SheetClassNPCOld": "Old NPC Sheet",
"SW5E.SheetClassVehicle": "Default Vehicle Sheet", "SW5E.SheetClassVehicle": "Default Vehicle Sheet",
"SW5E.ShieldDice": "Shield Dice",
"SW5E.ShieldPoints": "Shield Points",
"SW5E.ShieldPointsFormula": "Shield Points Formula",
"SW5E.ShieldRegen": "Regen",
"SW5E.ShortRest": "Short Rest", "SW5E.ShortRest": "Short Rest",
"SW5E.ShortRestEpic": "Short Rest (5 minutes)", "SW5E.ShortRestEpic": "Short Rest (5 minutes)",
"SW5E.ShortRestGritty": "Short Rest (8 hours)", "SW5E.ShortRestGritty": "Short Rest (8 hours)",
@ -991,7 +873,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",
@ -1001,62 +882,8 @@
"SW5E.SpeciesTraits": "Species Traits", "SW5E.SpeciesTraits": "Species Traits",
"SW5E.Speed": "Speed", "SW5E.Speed": "Speed",
"SW5E.SpeedSpecial": "Special Movement", "SW5E.SpeedSpecial": "Special Movement",
"SW5E.StarshipAmbassador": "Ambassador",
"SW5E.StarshipArmorandShieldProps": "Starship Armor & Shield Properties",
"SW5E.StarshipArmorandShields": "Starship Armor and Shields",
"SW5E.StarshipBattleship": "Battleship",
"SW5E.StarshipBlockadeShip": "Blockade Ship",
"SW5E.StarshipBomber": "Bomber",
"SW5E.StarshipCarrier": "Carrier",
"SW5E.StarshipColonizer": "Colonizer",
"SW5E.StarshipCommandShip": "Command Ship",
"SW5E.StarshipCorvette": "Corvette",
"SW5E.StarshipCourier": "Courier",
"SW5E.StarshipCruiser": "Cruiser",
"SW5E.StarshipEquipment": "Starship Equipment",
"SW5E.StarshipEquipmentProps": "Starship Equipment Properties",
"SW5E.StarshipExplorer": "Explorer",
"SW5E.StarshipfeaturePl": "Starship Features",
"SW5E.StarshipFlagship": "Flagship",
"SW5E.StarshipFreighter": "Freighter",
"SW5E.StarshipGunboat": "Gunboat",
"SW5E.StarshipIndustrialCenter": "Industrial Center",
"SW5E.StarshipInterceptor": "Interceptor",
"SW5E.StarshipInterdictor": "Interdictor",
"SW5E.StarshipJuggernaut": "Juggernaut",
"SW5E.StarshipMissileBoat": "Missile Boat",
"SW5E.StarshipMobileMetropolis": "Mobile Metropolis",
"SW5E.StarshipmodPl": "Starship Modifications",
"SW5E.StarshipNavigator": "Navigator",
"SW5E.StarshipPicketShip": "Picket Ship",
"SW5E.StarshipResearcher": "Researcher",
"SW5E.StarshipScout": "Scout",
"SW5E.StarshipScrambler": "Scrambler",
"SW5E.StarshipShipsTender": "Ship's Tender",
"SW5E.StarshipShuttle": "Shuttle",
"SW5E.StarshipSkillAst": "Astrogation",
"SW5E.StarshipSkillBst": "Boost",
"SW5E.StarshipSkillDat": "Data",
"SW5E.StarshipSkillHid": "Hide",
"SW5E.StarshipSkillImp": "Impress",
"SW5E.StarshipSkillInt": "Interfere",
"SW5E.StarshipSkillMan": "Maneuvering",
"SW5E.StarshipSkillMen": "Menace",
"SW5E.StarshipSkillPat": "Patch",
"SW5E.StarshipSkillPrb": "Probe",
"SW5E.StarshipSkillRam": "Ram",
"SW5E.StarshipSkillReg": "Regulation",
"SW5E.StarshipSkillScn": "Scan",
"SW5E.StarshipSkillSwn": "Swindle",
"SW5E.StarshipStrikeFighter": "Strike Fighter",
"SW5E.StarshipTier": "Tier",
"SW5E.StarshipWarship": "Warship",
"SW5E.StarshipYacht": "Yacht",
"SW5E.StealthDisadvantage": "Stealth Disadvantage", "SW5E.StealthDisadvantage": "Stealth Disadvantage",
"SW5E.SuiteCap": "Suite Capacity",
"SW5E.Supply": "Supply", "SW5E.Supply": "Supply",
"SW5E.SysStorageCapacity": "System Storage Capacity",
"SW5E.SystemDrainage": "System Drainage",
"SW5E.Target": "Target", "SW5E.Target": "Target",
"SW5E.TargetAlly": "Ally", "SW5E.TargetAlly": "Ally",
"SW5E.TargetCone": "Cone", "SW5E.TargetCone": "Cone",
@ -1072,7 +899,6 @@
"SW5E.TargetSpace": "Space", "SW5E.TargetSpace": "Space",
"SW5E.TargetSphere": "Sphere", "SW5E.TargetSphere": "Sphere",
"SW5E.TargetSquare": "Square", "SW5E.TargetSquare": "Square",
"SW5E.TargetStarship": "Starship",
"SW5E.TargetWall": "Wall", "SW5E.TargetWall": "Wall",
"SW5E.TargetWeapon": "Weapon", "SW5E.TargetWeapon": "Weapon",
"SW5E.TargetWidth": "Line Width", "SW5E.TargetWidth": "Line Width",
@ -1128,8 +954,7 @@
"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": "Not Equipped",
"SW5E.Unequipped": "Unequipped",
"SW5E.UniversalPowerDC": "Universal Power DC", "SW5E.UniversalPowerDC": "Universal Power DC",
"SW5E.Unlimited": "Unlimited", "SW5E.Unlimited": "Unlimited",
"SW5E.Usage": "Usage", "SW5E.Usage": "Usage",
@ -1153,85 +978,50 @@
"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.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",
"SW5E.WeaponPropertiesCon": "Constitution",
"SW5E.WeaponPropertiesDef": "Defensive", "SW5E.WeaponPropertiesDef": "Defensive",
"SW5E.WeaponPropertiesDex": "Dexterity Rqt", "SW5E.WeaponPropertiesDex": "Dexterity Rqmt.",
"SW5E.WeaponPropertiesDgd": "Disguised", "SW5E.WeaponPropertiesDgd": "Disguised",
"SW5E.WeaponPropertiesDir": "Dire", "SW5E.WeaponPropertiesDir": "Dire",
"SW5E.WeaponPropertiesDis": "Disintegrate", "SW5E.WeaponPropertiesDis": "Disintegrate",
"SW5E.WeaponPropertiesDou": "Double", "SW5E.WeaponPropertiesDou": "Double",
"SW5E.WeaponPropertiesDpt": "Disruptive", "SW5E.WeaponPropertiesDpt": "Disruptive",
"SW5E.WeaponPropertiesDrm": "Disarming", "SW5E.WeaponPropertiesDrm": "Disarming",
"SW5E.WeaponPropertiesExp": "Explosive",
"SW5E.WeaponPropertiesFin": "Finesse", "SW5E.WeaponPropertiesFin": "Finesse",
"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.WeaponPropertiesIon": "Ionizing",
"SW5E.WeaponPropertiesKen": "Keen", "SW5E.WeaponPropertiesKen": "Keen",
"SW5E.WeaponPropertiesLgt": "Light", "SW5E.WeaponPropertiesLgt": "Light",
"SW5E.WeaponPropertiesLum": "Luminous", "SW5E.WeaponPropertiesLum": "Luminous",
"SW5E.WeaponPropertiesMig": "Mighty", "SW5E.WeaponPropertiesMig": "Mighty",
"SW5E.WeaponPropertiesMlt": "Melt",
"SW5E.WeaponPropertiesOvr": "Overheat",
"SW5E.WeaponPropertiesPic": "Piercing", "SW5E.WeaponPropertiesPic": "Piercing",
"SW5E.WeaponPropertiesPow": "Power",
"SW5E.WeaponPropertiesRan": "Range", "SW5E.WeaponPropertiesRan": "Range",
"SW5E.WeaponPropertiesRap": "Rapid", "SW5E.WeaponPropertiesRap": "Rapid",
"SW5E.WeaponPropertiesRch": "Reach", "SW5E.WeaponPropertiesRch": "Reach",
"SW5E.WeaponPropertiesRel": "Reload", "SW5E.WeaponPropertiesRel": "Reload",
"SW5E.WeaponPropertiesRet": "Returning", "SW5E.WeaponPropertiesRet": "Returning",
"SW5E.WeaponPropertiesSat": "Saturate",
"SW5E.WeaponPropertiesShk": "Shocking", "SW5E.WeaponPropertiesShk": "Shocking",
"SW5E.WeaponPropertiesSil": "Silent", "SW5E.WeaponPropertiesSil": "Silent",
"SW5E.WeaponPropertiesSpc": "Special", "SW5E.WeaponPropertiesSpc": "Special",
"SW5E.WeaponPropertiesStr": "Strength Rqt", "SW5E.WeaponPropertiesStr": "Strength Rqmt.",
"SW5E.WeaponPropertiesThr": "Thrown", "SW5E.WeaponPropertiesThr": "Thrown",
"SW5E.WeaponPropertiesTwo": "Two-Handed", "SW5E.WeaponPropertiesTwo": "Two-Handed",
"SW5E.WeaponPropertiesVer": "Versatile", "SW5E.WeaponPropertiesVer": "Versatile",
"SW5E.WeaponPropertiesVic": "Vicious", "SW5E.WeaponPropertiesVic": "Vicious",
"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.WeaponSimpleBlasterProficiency": "Simple Blasters",
"SW5E.WeaponSimpleLightweaponProficiency": "Simple Lightweapons",
"SW5E.WeaponSimpleLW": "Simple Lightweapon", "SW5E.WeaponSimpleLW": "Simple Lightweapon",
"SW5E.WeaponSimpleProficiency": "Simple Weapons", "SW5E.WeaponSimpleProficiency": "Simple Weapons",
"SW5E.WeaponSimpleVibroweaponProficiency": "Simple Vibroweapons",
"SW5E.WeaponSimpleVW": "Simple Vibroweapon", "SW5E.WeaponSimpleVW": "Simple Vibroweapon",
"SW5E.WeaponSizeAbb": "Size",
"SW5E.WeaponTechbladeProficiency": "Techblades",
"SW5E.WeaponTertiarySW": "Tertiary (Starship)",
"SW5E.WeaponVibrorapierProficiency": "Vibrorapier",
"SW5E.WeaponVibrowhipProficiency": "Vibrowhip",
"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,10 +1,8 @@
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,14 +46,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) if (!game.user.isGM && this.actor.limited)
@ -66,75 +56,63 @@ 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", isVehicle: this.entity.data.type === "vehicle",
isVehicle: this.actor.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];
if (data.actor.type === "starship") {
skl.label = CONFIG.SW5E.starshipSkills[s];
} else {
skl.label = CONFIG.SW5E.skills[s]; skl.label = CONFIG.SW5E.skills[s];
} }
} }
}
// 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;
@ -168,14 +146,14 @@ export default class ActorSheet5e extends ActorSheet {
} }
// 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(", ")
}; };
} }
@ -183,7 +161,7 @@ export default class ActorSheet5e extends ActorSheet {
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(", ") : ""
}; };
} }
} }
@ -246,11 +224,10 @@ 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 = {};
@ -264,7 +241,7 @@ export default class ActorSheet5e extends ActorSheet {
const useLabels = { const useLabels = {
"-20": "-", "-20": "-",
"-10": "-", "-10": "-",
"0": "&infin;" 0: "&infin;"
}; };
// Format a powerbook entry for a certain indexed level // Format a powerbook entry for a certain indexed level
@ -279,12 +256,7 @@ export default class ActorSheet5e extends ActorSheet {
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
}; };
}; };
@ -307,7 +279,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,7 +322,7 @@ 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
@ -370,7 +342,6 @@ export default class ActorSheet5e extends ActorSheet {
if (filters.has("prepared")) { if (filters.has("prepared")) {
if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true; if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true;
if (this.actor.data.type === "npc") return true; if (this.actor.data.type === "npc") return true;
if (this.actor.data.type === "starship") return true;
return data.preparation.prepared; return data.preparation.prepared;
} }
@ -402,7 +373,10 @@ 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");
@ -410,16 +384,13 @@ export default class ActorSheet5e extends ActorSheet {
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
@ -436,21 +407,20 @@ export default class ActorSheet5e extends ActorSheet {
// 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-edit").click(this._onItemEdit.bind(this));
html.find(".item-delete").click(this._onItemDelete.bind(this)); html.find(".item-delete").click(this._onItemDelete.bind(this));
html.find(".item-collapse").click(this._onItemCollapse.bind(this)); html
html.find(".item-uses input") .find(".item-uses input")
.click((ev) => ev.target.select()) .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(".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));
@ -458,8 +428,8 @@ export default class ActorSheet5e extends ActorSheet {
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 +485,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);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -567,13 +529,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,7 +543,7 @@ 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;
@ -605,12 +567,12 @@ export default class ActorSheet5e extends ActorSheet {
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,
@ -623,7 +585,7 @@ 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
}) })
@ -646,42 +608,15 @@ export default class ActorSheet5e extends ActorSheet {
/** @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,7 +657,7 @@ 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 +672,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,7 +686,7 @@ 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 +699,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 +709,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 +729,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 +747,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,77 +761,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();
}
/**
* Handle collapsing a Feature row on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onItemCollapse(event) {
event.preventDefault();
event.currentTarget.classList.toggle("active");
const li = event.currentTarget.closest("li");
const content = li.querySelector(".content");
if (content.style.display === "none") {
content.style.display = "block";
} else {
content.style.display = "none";
}
}
/**
* Handle incrementing class level on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onIncrementClassLevel(event) {
event.preventDefault();
const div = event.currentTarget.closest(".character");
const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1];
const itemId = li.dataset.itemId;
const actor = game.actors.get(actorId);
const item = actor.items.get(itemId);
let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: levels + 1}};
actor.updateEmbeddedDocuments("Item", [update]);
}
/**
* Handle decrementing class level on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onDecrementClassLevel(event) {
event.preventDefault();
const div = event.currentTarget.closest(".character");
const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1];
const itemId = li.dataset.itemId;
const actor = game.actors.get(actorId);
const item = actor.items.get(itemId);
let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: levels - 1}};
actor.updateEmbeddedDocuments("Item", [update]);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -909,7 +774,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 +787,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 +800,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 +817,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 +833,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 +841,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

@ -56,9 +56,9 @@ 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(", ");
@ -91,9 +91,6 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
techpowers, techpowers,
feats, feats,
classes, classes,
deployments,
deploymentfeatures,
ventures,
species, species,
archetypes, archetypes,
classfeatures, classfeatures,
@ -104,7 +101,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
] = data.items.reduce( ] = data.items.reduce(
(arr, item) => { (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]: {
@ -121,37 +118,29 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// 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);
else if (item.type === "feat") arr[3].push(item); else if (item.type === "feat") arr[3].push(item);
else if (item.type === "class") arr[4].push(item); else if (item.type === "class") arr[4].push(item);
else if (item.type === "deployment") arr[5].push(item); else if (item.type === "species") arr[5].push(item);
else if (item.type === "deploymentfeature") arr[6].push(item); else if (item.type === "archetype") arr[6].push(item);
else if (item.type === "venture") arr[7].push(item); else if (item.type === "classfeature") arr[7].push(item);
else if (item.type === "species") arr[8].push(item); else if (item.type === "background") arr[8].push(item);
else if (item.type === "archetype") arr[9].push(item); else if (item.type === "fightingstyle") arr[9].push(item);
else if (item.type === "classfeature") arr[10].push(item); else if (item.type === "fightingmastery") arr[10].push(item);
else if (item.type === "background") arr[11].push(item); else if (item.type === "lightsaberform") arr[11].push(item);
else if (item.type === "fightingstyle") arr[12].push(item);
else if (item.type === "fightingmastery") arr[13].push(item);
else if (item.type === "lightsaberform") arr[14].push(item);
else if (Object.keys(inventory).includes(item.type)) arr[0].push(item); else if (Object.keys(inventory).includes(item.type)) arr[0].push(item);
return arr; return arr;
}, },
[[], [], [], [], [], [], [], [], [], [], [], [], [], [], []] [[], [], [], [], [], [], [], [], [], [], [], []]
); );
// Apply active item filters // Apply active item filters
@ -164,7 +153,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,13 +163,7 @@ 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",
items: [],
hasActions: false,
dataset: {type: "class"},
isClass: true
},
classfeatures: { classfeatures: {
label: "SW5E.ItemTypeClassFeats", label: "SW5E.ItemTypeClassFeats",
items: [], items: [],
@ -195,27 +178,6 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
dataset: {type: "archetype"}, dataset: {type: "archetype"},
isArchetype: true 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: { species: {
label: "SW5E.ItemTypeSpecies", label: "SW5E.ItemTypeSpecies",
items: [], items: [],
@ -255,7 +217,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
label: "SW5E.FeatureActive", label: "SW5E.FeatureActive",
items: [], items: [],
hasActions: true, hasActions: true,
dataset: {"type": "feat", "activation.type": "action"} 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"}}
}; };
@ -263,13 +225,10 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
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;
features.deployments.items = deployments;
features.deploymentfeatures.items = deploymentfeatures;
features.ventures.items = ventures;
features.species.items = species; features.species.items = species;
features.background.items = backgrounds; features.background.items = backgrounds;
features.fightingstyles.items = fightingstyles; features.fightingstyles.items = fightingstyles;
@ -312,11 +271,11 @@ 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));
@ -332,15 +291,13 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
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,33 +307,29 @@ 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>`,
@ -384,7 +337,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
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);
} }
}, },
@ -426,7 +379,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 +414,9 @@ 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 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);
@ -474,21 +427,8 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
} }
} }
// Increment the number of deployment ranks of a character instead of creating a new item
// else if ( itemData.type === "deployment" ) {
// const rnk = this.actor.itemTypes.deployment.find(c => c.name === itemData.name);
// let priorRank = rnk?.data.data.ranks ?? 0;
// if ( !!rnk ) {
// const next = Math.min(priorLevel + 1, 5 + priorRank - this.actor.data.data.details.rank);
// if ( next > priorRank ) {
// itemData.ranks = next;
// return rnk.update({"data.ranks": next});
// }
// }
// }
// Default drop handling if levels were not added // 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) {
@ -566,12 +506,12 @@ async function addFavorites(app, html, data) {
isFav ? "Remove from Favourites" : "Add to Favourites" isFav ? "Remove from Favourites" : "Add to Favourites"
}"><i class="fas fa-star"></i></a>` }"><i class="fas fa-star"></i></a>`
); );
favBtn.click((ev) => { favBtn.click(ev => {
app.actor.items.get(item.data._id).update({ app.actor.getOwnedItem(item._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) {
@ -581,8 +521,8 @@ async function addFavorites(app, html, data) {
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;
@ -632,31 +572,31 @@ async function addFavorites(app, html, data) {
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();
@ -666,10 +606,10 @@ async function addFavorites(app, html, data) {
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, {
@ -677,9 +617,9 @@ async function addFavorites(app, html, data) {
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);
@ -720,7 +660,7 @@ async function addSubTabs(app, html, data) {
} }
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 {
@ -729,18 +669,23 @@ async function addSubTabs(app, html, data) {
}); });
} }
html.find("[data-subgroup-selection]") html
.find("[data-subgroup-selection]")
.children() .children()
.on("click", (event) => { .on("click", event => {
let subgroup = event.target.closest("[data-subgroup]").getAttribute("data-subgroup"); let subgroup = event.target.closest("[data-subgroup]").getAttribute("data-subgroup");
let target = event.target.closest("[data-target]").getAttribute("data-target"); let target = event.target.closest("[data-target]").getAttribute("data-target");
html.find(`[data-subgroup=${subgroup}]`).removeClass("active"); html.find(`[data-subgroup=${subgroup}]`).removeClass("active");
html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass("active"); html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass("active");
let tabId = data.options.subTabs[subgroup].find((tab) => { let tabId = data.options.subTabs[subgroup].find(tab => {
return tab.target == target; 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;
}); });
}); });

View file

@ -28,11 +28,6 @@ export default class ActorSheet5eNPCNew 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
@ -44,13 +39,13 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
label: game.i18n.localize("SW5E.AttackPl"), label: game.i18n.localize("SW5E.AttackPl"),
items: [], items: [],
hasActions: true, hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"} dataset: {type: "weapon", "weapon-type": "natural"}
}, },
actions: { actions: {
label: game.i18n.localize("SW5E.ActionPl"), label: game.i18n.localize("SW5E.ActionPl"),
items: [], items: [],
hasActions: true, hasActions: true,
dataset: {"type": "feat", "activation.type": "action"} 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"}}
@ -59,11 +54,10 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
// 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 || 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.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);
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);
@ -100,17 +94,14 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @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 +110,7 @@ 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 +119,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);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

@ -1,169 +0,0 @@
import ActorSheet5e from "./base.js";
/**
* An Actor sheet for starships in the SW5E system.
* Extends the base ActorSheet5e class.
* @extends {ActorSheet5e}
*/
export default class ActorSheet5eStarship extends ActorSheet5e {
/** @override */
get template() {
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
return `systems/sw5e/templates/actors/newActor/starship.html`;
}
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "starship"],
width: 800,
tabs: [
{
navSelector: ".root-tabs",
contentSelector: ".sheet-body",
initial: "attributes"
}
]
});
}
/* -------------------------------------------- */
/**
* Organize Owned Items for rendering the starship sheet
* @private
*/
_prepareItems(data) {
// Categorize Items as Features and Powers
const features = {
weapons: {
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
equipment: {label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}},
starshipfeatures: {
label: game.i18n.localize("SW5E.StarshipfeaturePl"),
items: [],
hasActions: true,
dataset: {type: "starshipfeature"}
},
starshipmods: {
label: game.i18n.localize("SW5E.StarshipmodPl"),
items: [],
hasActions: false,
dataset: {type: "starshipmod"}
}
};
// Start by classifying items into groups for rendering
let [forcepowers, techpowers, other] = data.items.reduce(
(arr, item) => {
item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
item.hasUses = item.data.uses && item.data.uses.max > 0;
item.isOnCooldown =
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
else arr[2].push(item);
return arr;
},
[[], [], []]
);
// Apply item filters
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
other = this._filterItems(other, this._filters.features);
// Organize Powerbook
// const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
// const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
// Organize Features
for (let item of other) {
if (item.type === "weapon") features.weapons.items.push(item);
else if (item.type === "feat") {
if (item.data.activation.type) features.actions.items.push(item);
else features.passive.items.push(item);
} else if (item.type === "starshipfeature") {
features.starshipfeatures.items.push(item);
} else if (item.type === "starshipmod") {
features.starshipmods.items.push(item);
} else features.equipment.items.push(item);
}
// Assign and return
data.features = Object.values(features);
// data.forcePowerbook = forcePowerbook;
// data.techPowerbook = techPowerbook;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
const data = super.getData(options);
// 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
const cr = parseFloat(data.data.details.cr || 0);
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
return data;
}
/* -------------------------------------------- */
/* Object Updates */
/* -------------------------------------------- */
/** @override */
async _updateObject(event, formData) {
// Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr";
let cr = formData[crv];
cr = crs[cr] || parseFloat(cr);
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps
return super._updateObject(event, formData);
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
}
/* -------------------------------------------- */
/**
* Handle rolling NPC health values using the provided formula
* @param {Event} event The original click event
* @private
*/
_onRollHPFormula(event) {
event.preventDefault();
const formula = this.actor.data.data.attributes.hp.formula;
if (!formula) return;
const hp = new Roll(formula).roll().total;
AudioHelper.play({src: CONFIG.sounds.dice});
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
}
}

View file

@ -20,11 +20,6 @@ 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.
*/ */
@ -139,7 +134,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
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"),
@ -157,7 +152,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
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: {
@ -168,13 +163,13 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
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
} }
}; };
@ -223,40 +218,22 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
} }
}; };
// 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") {
} if (!item.data.activation.type || item.data.activation.type === "none") {
// Handle non-cargo item types
switch (item.type) {
case "weapon":
features.weapons.items.push(item);
break;
case "equipment":
features.equipment.items.push(item);
break;
case "feat":
if (!item.data.activation.type || item.data.activation.type === "none")
features.passive.items.push(item); 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,19 +246,22 @@ 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
.click((evt) => evt.target.select()) .find(".item-hp input")
.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
.click((evt) => evt.target.select()) .find(".item:not(.cargo-row) input[data-property]")
.click(evt => evt.target.select())
.change(this._onEditInSheet.bind(this)); .change(this._onEditInSheet.bind(this));
html.find(".cargo-row input") html
.click((evt) => evt.target.select()) .find(".cargo-row input")
.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) {
@ -305,7 +285,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
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;
@ -359,7 +339,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
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});
} }
@ -380,7 +360,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
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 +369,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}

View file

@ -1,10 +1,8 @@
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,42 @@ 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", isVehicle: this.entity.data.type === "vehicle",
isVehicle: this.actor.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,19 +97,19 @@ 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;
@ -161,14 +143,14 @@ export default class ActorSheet5e extends ActorSheet {
} }
// 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(", ")
}; };
} }
@ -176,7 +158,7 @@ export default class ActorSheet5e extends ActorSheet {
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(", ") : ""
}; };
} }
} }
@ -242,7 +224,7 @@ 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 = {};
@ -257,7 +239,7 @@ export default class ActorSheet5e extends ActorSheet {
const useLabels = { const useLabels = {
"-20": "-", "-20": "-",
"-10": "-", "-10": "-",
"0": "&infin;" 0: "&infin;"
}; };
// Format a powerbook entry for a certain indexed level // Format a powerbook entry for a certain indexed level
@ -272,7 +254,7 @@ export default class ActorSheet5e extends ActorSheet {
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: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode}, dataset: {type: "power", level: prepMode in sections ? 1 : i, "preparation.mode": prepMode},
prop: sl prop: sl
}; };
}; };
@ -295,14 +277,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 +290,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,7 +333,7 @@ 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
@ -405,7 +384,10 @@ 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");
@ -413,16 +395,13 @@ export default class ActorSheet5e extends ActorSheet {
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
@ -439,18 +418,20 @@ export default class ActorSheet5e extends ActorSheet {
// 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-edit").click(this._onItemEdit.bind(this));
html.find(".item-delete").click(this._onItemDelete.bind(this)); html.find(".item-delete").click(this._onItemDelete.bind(this));
html.find(".item-uses input") html
.click((ev) => ev.target.select()) .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));
@ -458,8 +439,8 @@ export default class ActorSheet5e extends ActorSheet {
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 +496,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);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -567,13 +540,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,7 +554,7 @@ 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;
@ -605,12 +578,12 @@ export default class ActorSheet5e extends ActorSheet {
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,
@ -623,7 +596,7 @@ 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
}) })
@ -646,42 +619,15 @@ export default class ActorSheet5e extends ActorSheet {
/** @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,7 +668,7 @@ 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 +683,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,7 +697,7 @@ 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 +710,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 +720,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 +740,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 +758,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 +772,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 +785,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 +798,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 +811,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 +828,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 +844,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 +852,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

@ -44,9 +44,9 @@ 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(", ");
@ -88,7 +88,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
] = data.items.reduce( ] = data.items.reduce(
(arr, item) => { (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]: {
@ -105,18 +105,13 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// 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);
@ -149,19 +144,13 @@ 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",
items: [],
hasActions: false,
dataset: {type: "class"},
isClass: true
},
classfeatures: { classfeatures: {
label: "SW5E.ItemTypeClassFeats", label: "SW5E.ItemTypeClassFeats",
items: [], items: [],
@ -215,7 +204,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
label: "SW5E.FeatureActive", label: "SW5E.FeatureActive",
items: [], items: [],
hasActions: true, hasActions: true,
dataset: {"type": "feat", "activation.type": "action"} 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"}}
}; };
@ -223,7 +212,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
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;
@ -269,11 +258,11 @@ 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));
@ -314,7 +303,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)});
} }
@ -351,7 +340,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
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 +352,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

@ -17,11 +17,6 @@ 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
@ -33,13 +28,13 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
label: game.i18n.localize("SW5E.AttackPl"), label: game.i18n.localize("SW5E.AttackPl"),
items: [], items: [],
hasActions: true, hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"} dataset: {type: "weapon", "weapon-type": "natural"}
}, },
actions: { actions: {
label: game.i18n.localize("SW5E.ActionPl"), label: game.i18n.localize("SW5E.ActionPl"),
items: [], items: [],
hasActions: true, hasActions: true,
dataset: {"type": "feat", "activation.type": "action"} 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"}}
@ -48,11 +43,10 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
// 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 || 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.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);
if (item.type === "power") arr[0].push(item); if (item.type === "power") arr[0].push(item);
@ -85,17 +79,14 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @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 +95,7 @@ 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 +104,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,11 +20,6 @@ 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.
*/ */
@ -139,7 +134,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
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"),
@ -157,7 +152,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
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: {
@ -168,13 +163,13 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
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
} }
}; };
@ -223,40 +218,22 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
} }
}; };
// 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") {
} if (!item.data.activation.type || item.data.activation.type === "none") {
// Handle non-cargo item types
switch (item.type) {
case "weapon":
features.weapons.items.push(item);
break;
case "equipment":
features.equipment.items.push(item);
break;
case "feat":
if (!item.data.activation.type || item.data.activation.type === "none")
features.passive.items.push(item); 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,19 +246,22 @@ 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
.click((evt) => evt.target.select()) .find(".item-hp input")
.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
.click((evt) => evt.target.select()) .find(".item:not(.cargo-row) input[data-property]")
.click(evt => evt.target.select())
.change(this._onEditInSheet.bind(this)); .change(this._onEditInSheet.bind(this));
html.find(".cargo-row input") html
.click((evt) => evt.target.select()) .find(".cargo-row input")
.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) {
@ -305,7 +285,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
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;
@ -359,7 +339,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
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});
} }
@ -380,7 +360,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
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 +369,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}

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: []
@ -60,15 +57,15 @@ export default class AbilityUseDialog extends Dialog {
// Create the Dialog and return data as a Promise // Create the Dialog and return data as a Promise
const icon = data.isPower ? "fa-magic" : "fa-fist-raised"; const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
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());
} }
@ -142,7 +139,7 @@ export default class AbilityUseDialog extends Dialog {
} }
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) => {
@ -162,10 +159,10 @@ export default class AbilityUseDialog extends Dialog {
} }
return arr; return arr;
}, []) }, [])
.filter((sl) => sl.level <= lmax); .filter(sl => sl.level <= lmax);
} }
const canCast = powerLevels.some((l) => l.hasSlots); const canCast = powerLevels.some(l => l.hasSlots);
if (!canCast) if (!canCast)
data.errors.push( data.errors.push(
game.i18n.format("SW5E.PowerCastNoSlots", { game.i18n.format("SW5E.PowerCastNoSlots", {
@ -175,7 +172,7 @@ export default class AbilityUseDialog extends Dialog {
); );
// Merge power casting data // Merge power casting data
return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels}); return mergeObject(data, {isPower: true, consumePowerSlot, powerLevels});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -192,7 +189,7 @@ 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
}); });
} }
@ -206,7 +203,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 +214,15 @@ 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",
@ -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,35 +34,17 @@ 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");

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,21 +40,22 @@ 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
} }
}, },

View file

@ -1,11 +1,11 @@
/** /**
* 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,16 +17,15 @@ 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)) {

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,11 @@
/** /**
* 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,16 +15,16 @@ 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 ?? "",

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;
@ -92,12 +92,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 +106,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
} }
}, },

View file

@ -1,13 +1,13 @@
/** /**
* 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 +15,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 +23,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 +33,51 @@ export default class TraitSelector extends DocumentSheet {
/** @override */ /** @override */
getData() { getData() {
const attr = foundry.utils.getProperty(this.object.data, this.attribute); // Get current values
const o = this.options; let attr = getProperty(this.object._data, this.attribute);
const value = o.valueKey ? attr[o.valueKey] ?? [] : attr; if (getType(attr) !== "Object") attr = {value: [], custom: ""};
const custom = o.customKey ? attr[o.customKey] ?? "" : "";
// 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
@ -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

@ -5,108 +5,46 @@ export default class CharacterImporter {
const sourceCharacter = JSON.parse(rawCharacter); //source character const sourceCharacter = JSON.parse(rawCharacter); //source character
const details = { const details = {
species: sourceCharacter.attribs.find((e) => e.name == "race").current, species: sourceCharacter.attribs.find(e => e.name == "race").current,
background: sourceCharacter.attribs.find((e) => e.name == "background").current, background: sourceCharacter.attribs.find(e => e.name == "background").current,
alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current alignment: sourceCharacter.attribs.find(e => e.name == "alignment").current
}; };
const hp = { const hp = {
value: sourceCharacter.attribs.find((e) => e.name == "hp").current, value: sourceCharacter.attribs.find(e => e.name == "hp").current,
min: 0, min: 0,
max: sourceCharacter.attribs.find((e) => e.name == "hp").current, max: sourceCharacter.attribs.find(e => e.name == "hp").current,
temp: sourceCharacter.attribs.find((e) => e.name == "hp_temp").current temp: sourceCharacter.attribs.find(e => e.name == "hp_temp").current
};
const ac = {
value: sourceCharacter.attribs.find(e => e.name == "ac").current
}; };
const abilities = { const abilities = {
str: { str: {
value: sourceCharacter.attribs.find((e) => e.name == "strength").current, value: sourceCharacter.attribs.find(e => e.name == "strength").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "strength_save_prof").current ? 1 : 0 proficient: sourceCharacter.attribs.find(e => e.name == "strength_save_prof").current ? 1 : 0
}, },
dex: { dex: {
value: sourceCharacter.attribs.find((e) => e.name == "dexterity").current, value: sourceCharacter.attribs.find(e => e.name == "dexterity").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "dexterity_save_prof").current ? 1 : 0 proficient: sourceCharacter.attribs.find(e => e.name == "dexterity_save_prof").current ? 1 : 0
}, },
con: { con: {
value: sourceCharacter.attribs.find((e) => e.name == "constitution").current, value: sourceCharacter.attribs.find(e => e.name == "constitution").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "constitution_save_prof").current ? 1 : 0 proficient: sourceCharacter.attribs.find(e => e.name == "constitution_save_prof").current ? 1 : 0
}, },
int: { int: {
value: sourceCharacter.attribs.find((e) => e.name == "intelligence").current, value: sourceCharacter.attribs.find(e => e.name == "intelligence").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "intelligence_save_prof").current ? 1 : 0 proficient: sourceCharacter.attribs.find(e => e.name == "intelligence_save_prof").current ? 1 : 0
}, },
wis: { wis: {
value: sourceCharacter.attribs.find((e) => e.name == "wisdom").current, value: sourceCharacter.attribs.find(e => e.name == "wisdom").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "wisdom_save_prof").current ? 1 : 0 proficient: sourceCharacter.attribs.find(e => e.name == "wisdom_save_prof").current ? 1 : 0
}, },
cha: { cha: {
value: sourceCharacter.attribs.find((e) => e.name == "charisma").current, value: sourceCharacter.attribs.find(e => e.name == "charisma").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "charisma_save_prof").current ? 1 : 0 proficient: sourceCharacter.attribs.find(e => e.name == "charisma_save_prof").current ? 1 : 0
}
};
/* ----------------------------------------------------------------- */
/* character.data.skills.<skill_name>.value is all that matters
/* values can be 0, 0.5, 1 or 2
/* 0 = regular
/* 0.5 = half-proficient
/* 1 = proficient
/* 2 = expertise
/* foundry takes care of calculating the rest
/* ----------------------------------------------------------------- */
const skills = {
acr: {
value: sourceCharacter.attribs.find((e) => e.name == "acrobatics_type").current
},
ani: {
value: sourceCharacter.attribs.find((e) => e.name == "animal_handling_type").current
},
ath: {
value: sourceCharacter.attribs.find((e) => e.name == "athletics_type").current
},
dec: {
value: sourceCharacter.attribs.find((e) => e.name == "deception_type").current
},
ins: {
value: sourceCharacter.attribs.find((e) => e.name == "insight_type").current
},
inv: {
value: sourceCharacter.attribs.find((e) => e.name == "investigation_type").current
},
itm: {
value: sourceCharacter.attribs.find((e) => e.name == "intimidation_type").current
},
lor: {
value: sourceCharacter.attribs.find((e) => e.name == "lore_type").current
},
med: {
value: sourceCharacter.attribs.find((e) => e.name == "medicine_type").current
},
nat: {
value: sourceCharacter.attribs.find((e) => e.name == "nature_type").current
},
per: {
value: sourceCharacter.attribs.find((e) => e.name == "persuasion_type").current
},
pil: {
value: sourceCharacter.attribs.find((e) => e.name == "piloting_type").current
},
prc: {
value: sourceCharacter.attribs.find((e) => e.name == "perception_type").current
},
prf: {
value: sourceCharacter.attribs.find((e) => e.name == "performance_type").current
},
slt: {
value: sourceCharacter.attribs.find((e) => e.name == "sleight_of_hand_type").current
},
ste: {
value: sourceCharacter.attribs.find((e) => e.name == "stealth_type").current
},
sur: {
value: sourceCharacter.attribs.find((e) => e.name == "survival_type").current
},
tec: {
value: sourceCharacter.attribs.find((e) => e.name == "technology_type").current
} }
}; };
@ -116,205 +54,64 @@ export default class CharacterImporter {
data: { data: {
abilities: abilities, abilities: abilities,
details: details, details: details,
skills: skills,
attributes: { attributes: {
ac: ac,
hp: hp hp: hp
} }
} }
}; };
let actor = await Actor.create(targetCharacter); let actor = await Actor.create(targetCharacter);
CharacterImporter.addProfessions(sourceCharacter, actor);
}
// Parse all classes and add them to already created actor. const profession = sourceCharacter.attribs.find(e => e.name == "class").current;
// "class" is a reserved word, therefore I use profession where I can. let professionLevel = sourceCharacter.attribs.find(e => e.name == "class_display").current;
static async addProfessions(sourceCharacter, actor) { professionLevel = parseInt(professionLevel.replace(/[^0-9]/g, "")); //remove a-z, leaving only integers
let result = []; CharacterImporter.addClasses(profession, professionLevel, actor);
// parse all class and multiclassX items
// couldn't get Array.filter to work here for some reason
// result = array of objects. each object is a separate class
sourceCharacter.attribs.forEach((e) => {
if (CharacterImporter.classOrMulticlass(e.name)) {
var t = {
profession: CharacterImporter.capitalize(e.current),
type: CharacterImporter.baseOrMulti(e.name),
level: CharacterImporter.getLevel(e, sourceCharacter)
};
result.push(t);
}
});
// pull classes directly from system compendium and add them to current actor
const professionsPack = await game.packs.get("sw5e.classes").getDocuments();
result.forEach((prof) => {
let assignedProfession = professionsPack.find((o) => o.name === prof.profession);
assignedProfession.data.data.levels = prof.level;
actor.createEmbeddedDocuments("Item", [assignedProfession.data], {displaySheet: false});
});
this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor);
this.addPowers(
sourceCharacter.attribs
.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1)
.map((e) => e.current),
actor
);
const discoveredItems = sourceCharacter.attribs.filter(
(e) => e.name.search(/repeating_inventory.+_itemname/g) != -1
);
const items = discoveredItems.map((item) => {
const id = item.name.match(/-\w{19}/g);
return {
name: item.current,
quantity: sourceCharacter.attribs.find((e) => e.name === `repeating_inventory_${id}_itemcount`).current
};
});
this.addItems(items, actor);
} }
static 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) {
return name === "class" || (name.includes("multiclass") && name.length <= 12);
}
static baseOrMulti(name) {
if (name === "class") {
return "base_class";
} else {
return "multi_class";
}
}
static getLevel(item, sourceCharacter) {
if (item.name === "class") {
let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current;
return parseInt(result);
} else {
let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current;
return parseInt(result);
}
}
static capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
static async addSpecies(race, actor) {
const species = await game.packs.get("sw5e.species").getDocuments();
const assignedSpecies = species.find((c) => c.name === race);
const activeEffects = [...assignedSpecies.data.effects][0].data.changes;
const actorData = {data: {abilities: {...actor.data.data.abilities}}};
activeEffects.map((effect) => {
switch (effect.key) {
case "data.abilities.str.value":
actorData.data.abilities.str.value -= effect.value;
break;
case "data.abilities.dex.value":
actorData.data.abilities.dex.value -= effect.value;
break;
case "data.abilities.con.value":
actorData.data.abilities.con.value -= effect.value;
break;
case "data.abilities.int.value":
actorData.data.abilities.int.value -= effect.value;
break;
case "data.abilities.wis.value":
actorData.data.abilities.wis.value -= effect.value;
break;
case "data.abilities.cha.value":
actorData.data.abilities.cha.value -= effect.value;
break;
default:
break;
}
});
actor.update(actorData);
await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], {displaySheet: false});
}
static async addPowers(powers, actor) {
const forcePowers = await game.packs.get("sw5e.forcepowers").getDocuments();
const techPowers = await game.packs.get("sw5e.techpowers").getDocuments();
for (const power of powers) {
const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power);
if (createdPower) {
await actor.createEmbeddedDocuments("Item", [createdPower.data], {displaySheet: false});
}
}
}
static async addItems(items, actor) {
const weapons = await game.packs.get("sw5e.weapons").getDocuments();
const armors = await game.packs.get("sw5e.armor").getDocuments();
const adventuringGear = await game.packs.get("sw5e.adventuringgear").getDocuments();
for (const item of items) {
const createdItem =
weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase());
if (createdItem) {
if (item.quantity != 1) {
createdItem.data.data.quantity = item.quantity;
}
await actor.createEmbeddedDocuments("Item", [createdItem.data], {displaySheet: false});
}
}
} }
static addImportButton(html) { static addImportButton(html) {
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(ev => {
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: e => {
let characterData = $("#character-json").val(); let characterData = $("#character-json").val();
console.log("Parsing Character JSON"); console.log("Parsing Character JSON");
CharacterImporter.transform(characterData); CharacterImporter.transform(characterData);
} }
}, },
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

@ -39,7 +39,7 @@ 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
@ -63,34 +63,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;
@ -110,7 +110,7 @@ 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

@ -4,28 +4,24 @@
* 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
* *
@ -25,7 +22,7 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
for (let term of terms) { for (let term of terms) {
// For each term // For each term
if (term instanceof OperatorTerm) operators.push(term); if (["+", "-"].includes(term)) operators.push(term);
// If the term is an addition/subtraction operator, push the term into the operators array // If the term is an addition/subtraction operator, push the term into the operators array
else { else {
// Otherwise the term is not an operator // Otherwise the term is not an operator
@ -43,21 +40,14 @@ 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 +62,366 @@ 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 = [], parts = [],
data = {}, // Roll creation data = {},
advantage, event = {},
disadvantage, rollMode = null,
fumble = 1, template = null,
title = null,
speaker = null,
flavor = null,
fastForward = null,
dialogOptions,
advantage = null,
disadvantage = null,
critical = 20, critical = 20,
targetValue, fumble = 1,
elvenAccuracy, targetValue = null,
halflingLucky, elvenAccuracy = false,
reliableTalent, // Roll customization halflingLucky = false,
chooseModifier = false, reliableTalent = false,
fastForward = false, chatMessage = true,
event, messageData = {}
} = {}) {
// Prepare Message Data
messageData.flavor = flavor || title;
messageData.speaker = speaker || ChatMessage.getSpeaker();
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
parts = parts.concat(["@bonus"]);
// Handle fast-forward events
let adv = 0;
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
if (fastForward) {
if (advantage ?? event.altKey) adv = 1;
else if (disadvantage ?? (event.ctrlKey || event.metaKey)) adv = -1;
}
// Define the inner roll function
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, template,
title, title,
dialogOptions, // Dialog configuration parts,
chatMessage = true, data,
messageData = {}, rollMode: messageOptions.rollMode,
rollMode, dialogOptions,
speaker, roll: _roll
flavor // Chat Message customization
} = {}) {
// Handle input arguments
const formula = ["1d20"].concat(parts).join(" + ");
const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event});
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
if (chooseModifier && !isFF) data["mod"] = "@mod";
// Construct the D20Roll instance
const roll = new CONFIG.Dice.D20Roll(formula, data, {
flavor: flavor || title,
advantageMode,
defaultRollMode,
critical,
fumble,
targetValue,
elvenAccuracy,
halflingLucky,
reliableTalent
}); });
// Prompt a Dialog to further configure the D20Roll
if (!isFF) {
const configured = await roll.configureDialog(
{
title,
chooseModifier,
defaultRollMode: defaultRollMode,
defaultAction: advantageMode,
defaultAbility: data?.item?.ability,
template
},
dialogOptions
);
if (configured === null) return null;
}
// 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 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)); // Render modal dialog
let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL; template = template || "systems/sw5e/templates/chat/roll-dialog.html";
if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE; let dialogData = {
else if (disadvantage || event?.ctrlKey || event?.metaKey) formula: parts.join(" + "),
advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE; data: data,
return {isFF, advantageMode}; 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 = [], parts,
data, // Roll creation actor,
critical = false, data,
criticalBonusDice, event = {},
criticalMultiplier, rollMode = null,
multiplyNumeric,
powerfulCritical, // Damage customization
fastForward = false,
event,
allowCritical = true,
template, template,
title, title,
dialogOptions, // Dialog configuration
chatMessage = true,
messageData = {},
rollMode,
speaker, speaker,
flavor // Chat Message customization flavor,
allowCritical = true,
critical = false,
criticalBonusDice = 0,
criticalMultiplier = 2,
fastForward = null,
dialogOptions = {},
chatMessage = true,
messageData = {}
} = {}) { } = {}) {
// Handle input arguments // Prepare Message Data
const defaultRollMode = rollMode || game.settings.get("core", "rollMode"); messageData.flavor = flavor || title;
messageData.speaker = speaker || ChatMessage.getSpeaker();
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
parts = parts.concat(["@bonus"]);
// Construct the DamageRoll instance // Define inner roll function
const formula = parts.join(" + "); const _roll = function (parts, crit, form) {
const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event}); // Optionally include a situational bonus
const roll = new CONFIG.Dice.DamageRoll(formula, data, { if (form) {
flavor: flavor || title, data["bonus"] = form.bonus.value;
critical: isCritical, messageOptions.rollMode = form.rollMode.value;
criticalBonusDice, }
criticalMultiplier, if (!data["bonus"]) parts.pop();
multiplyNumeric,
powerfulCritical // 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)); // Render modal dialog
if (event?.altKey) critical = true; template = template || "systems/sw5e/templates/chat/roll-dialog.html";
return {isFF, isCritical: critical}; 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,16 @@ 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": game.i18n.localize("SW5E.EffectNew"), label: "New Effect",
"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":
@ -38,17 +39,17 @@ export function prepareActiveEffectCategories(effects) {
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: []
} }
}; };

View file

@ -2,8 +2,7 @@ import {simplifyRollFormula, d20Roll, damageRoll} from "../dice.js";
import AbilityUseDialog from "../apps/ability-use-dialog.js"; import AbilityUseDialog from "../apps/ability-use-dialog.js";
/** /**
* Override and extend the basic Item implementation * Override and extend the basic :class:`Item` implementation
* @extends {Item}
*/ */
export default class Item5e extends Item { export default class Item5e extends Item {
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -45,16 +44,14 @@ export default class Item5e extends Item {
else if (this.data.type === "weapon") { else if (this.data.type === "weapon") {
const wt = itemData.weaponType; const wt = itemData.weaponType;
// Weapons using the powercasting modifier // Melee weapons - Str or Dex if Finesse (PHB pg. 147)
// No current SW5e weapons use this, but it's worth checking just in case if (["simpleVW", "martialVW", "simpleLW", "martialLW"].includes(wt)) {
if (["mpak", "rpak"].includes(itemData.actionType)) { if (itemData.properties.fin === true) {
return actorData.attributes.powercasting || "int"; // Finesse weapons
}
// Finesse weapons - Str or Dex (PHB pg. 147)
else if (itemData.properties.fin === true) {
return actorData.abilities["dex"].mod >= actorData.abilities["str"].mod ? "dex" : "str"; return actorData.abilities["dex"].mod >= actorData.abilities["str"].mod ? "dex" : "str";
} }
return "str";
}
// Ranged weapons - Dex (PH p.194) // Ranged weapons - Dex (PH p.194)
else if (["simpleB", "martialB"].includes(wt)) return "dex"; else if (["simpleB", "martialB"].includes(wt)) return "dex";
@ -148,7 +145,7 @@ export default class Item5e extends Item {
get hasLimitedUses() { get hasLimitedUses() {
let chg = this.data.data.recharge || {}; let chg = this.data.data.recharge || {};
let uses = this.data.data.uses || {}; let uses = this.data.data.uses || {};
return !!chg.value || (uses.per && uses.max > 0); return !!chg.value || (!!uses.per && uses.max > 0);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -158,8 +155,8 @@ export default class Item5e extends Item {
/** /**
* Augment the basic Item data model with additional dynamic data. * Augment the basic Item data model with additional dynamic data.
*/ */
prepareDerivedData() { prepareData() {
super.prepareDerivedData(); super.prepareData();
// Get the Item's data // Get the Item's data
const itemData = this.data; const itemData = this.data;
@ -197,7 +194,6 @@ export default class Item5e extends Item {
else labels.featType = game.i18n.localize("SW5E.Passive"); else labels.featType = game.i18n.localize("SW5E.Passive");
} }
// TODO: Something with all this
// Species Items // Species Items
else if (itemData.type === "species") { else if (itemData.type === "species") {
// labels.species = C.species[data.species]; // labels.species = C.species[data.species];
@ -214,14 +210,6 @@ export default class Item5e extends Item {
else if (itemData.type === "classfeature") { else if (itemData.type === "classfeature") {
// labels.classFeature = C.classFeature[data.classFeature]; // labels.classFeature = C.classFeature[data.classFeature];
} }
// Deployment Items
else if (itemData.type === "deployment") {
// labels.deployment = C.deployment[data.deployment];
}
// Venture Items
else if (itemData.type === "venture") {
// labels.venture = C.venture[data.venture];
}
// Fighting Style Items // Fighting Style Items
else if (itemData.type === "fightingstyle") { else if (itemData.type === "fightingstyle") {
// labels.fightingstyle = C.fightingstyle[data.fightingstyle]; // labels.fightingstyle = C.fightingstyle[data.fightingstyle];
@ -257,7 +245,7 @@ export default class Item5e extends Item {
// Range Label // Range Label
let rng = data.range || {}; let rng = data.range || {};
if (["none", "touch", "self"].includes(rng.units)) { if (["none", "touch", "self"].includes(rng.units) || rng.value === 0) {
rng.value = null; rng.value = null;
rng.long = null; rng.long = null;
} }
@ -270,72 +258,40 @@ export default class Item5e extends Item {
// Recharge Label // Recharge Label
let chg = data.recharge || {}; let chg = data.recharge || {};
labels.recharge = `${game.i18n.localize("SW5E.Recharge")} [${chg.value}${ labels.recharge = `${game.i18n.localize("SW5E.Recharge")} [${chg.value}${parseInt(chg.value) < 6 ? "+" : ""}]`;
parseInt(chg.value) < 6 ? "+" : ""
}]`;
} }
// Item Actions // Item Actions
if (data.hasOwnProperty("actionType")) { if (data.hasOwnProperty("actionType")) {
// Damage // if this item is owned, we populate the label and saving throw during actor init
let dam = data.damage || {}; if (!this.isOwned) {
if (dam.parts) {
labels.damage = dam.parts
.map((d) => d[0])
.join(" + ")
.replace(/\+ -/g, "- ");
labels.damageTypes = dam.parts.map((d) => C.damageTypes[d[1]]).join(", ");
}
}
// if this item is owned, we prepareFinalAttributes() at the end of actor init
if (!this.isOwned) this.prepareFinalAttributes();
}
/* -------------------------------------------- */
/**
* Compute item attributes which might depend on prepared actor data.
*/
prepareFinalAttributes() {
if (this.data.data.hasOwnProperty("actionType")) {
// Saving throws // Saving throws
this.getSaveDC(); this.getSaveDC();
// To Hit // To Hit
this.getAttackToHit(); this.getAttackToHit();
}
// Damage
let dam = data.damage || {};
if (dam.parts) {
labels.damage = dam.parts
.map(d => d[0])
.join(" + ")
.replace(/\+ -/g, "- ");
labels.damageTypes = dam.parts.map(d => C.damageTypes[d[1]]).join(", ");
}
// Limited Uses // Limited Uses
this.prepareMaxUses(); if (this.isOwned && !!data.uses?.max) {
let max = data.uses.max;
// Damage Label if (!Number.isNumeric(max)) {
this.getDerivedDamageLabel(); max = Roll.replaceFormulaData(max, this.actor.getRollData());
if (Roll.MATH_PROXY.safeEval) max = Roll.MATH_PROXY.safeEval(max);
}
data.uses.max = Number(max);
} }
} }
/* -------------------------------------------- */
/**
* Populate a label with the compiled and simplified damage formula
* based on owned item actor data. This is only used for display
* purposes and is not related to Item5e#rollDamage
*
* @returns {Array} array of objects with `formula` and `damageType`
*/
getDerivedDamageLabel() {
const itemData = this.data.data;
if (!this.hasDamage || !itemData || !this.isOwned) return [];
const rollData = this.getRollData();
const derivedDamage = itemData.damage?.parts?.map((damagePart) => ({
formula: simplifyRollFormula(damagePart[0], rollData, {constantFirst: false}),
damageType: damagePart[1]
}));
this.labels.derivedDamage = derivedDamage;
return derivedDamage;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -430,8 +386,7 @@ export default class Item5e extends Item {
const ammoItemQuantity = ammoItemData.data.quantity; const ammoItemQuantity = ammoItemData.data.quantity;
const ammoCanBeConsumed = ammoItemQuantity && ammoItemQuantity - (itemData.consume.amount ?? 0) >= 0; const ammoCanBeConsumed = ammoItemQuantity && ammoItemQuantity - (itemData.consume.amount ?? 0) >= 0;
const ammoItemAttackBonus = ammoItemData.data.attackBonus; const ammoItemAttackBonus = ammoItemData.data.attackBonus;
const ammoIsTypeConsumable = const ammoIsTypeConsumable = ammoItemData.type === "consumable" && ammoItemData.data.consumableType === "ammo";
ammoItemData.type === "consumable" && ammoItemData.data.consumableType === "ammo";
if (ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable) { if (ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable) {
parts.push("@ammo"); parts.push("@ammo");
rollData["ammo"] = ammoItemAttackBonus; rollData["ammo"] = ammoItemAttackBonus;
@ -452,31 +407,6 @@ export default class Item5e extends Item {
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* Populates the max uses of an item.
* If the item is an owned item and the `max` is not numeric, calculate based on actor data.
*/
prepareMaxUses() {
const data = this.data.data;
if (!data.uses?.max) return;
let max = data.uses.max;
// if this is an owned item and the max is not numeric, we need to calculate it
if (this.isOwned && !Number.isNumeric(max)) {
if (this.actor.data === undefined) return;
try {
max = Roll.replaceFormulaData(max, this.actor.getRollData(), {missing: 0, warn: true});
max = Roll.safeEval(max);
} catch (e) {
console.error("Problem preparing Max uses for", this.data.name, e);
return;
}
}
data.uses.max = Number(max);
}
/* -------------------------------------------- */
/** /**
* Roll the item to Chat, creating a chat card which contains follow up attack or damage roll options * Roll the item to Chat, creating a chat card which contains follow up attack or damage roll options
* @param {boolean} [configureDialog] Display a configuration dialog for the item roll, if applicable? * @param {boolean} [configureDialog] Display a configuration dialog for the item roll, if applicable?
@ -487,37 +417,30 @@ export default class Item5e extends Item {
*/ */
async roll({configureDialog = true, rollMode, createMessage = true} = {}) { async roll({configureDialog = true, rollMode, createMessage = true} = {}) {
let item = this; let item = this;
const id = this.data.data; // Item system data
const actor = this.actor; const actor = this.actor;
const ad = actor.data.data; // Actor system data
// Reference aspects of the item data necessary for usage // Reference aspects of the item data necessary for usage
const id = this.data.data; // Item data
const hasArea = this.hasAreaTarget; // Is the ability usage an AoE? const hasArea = this.hasAreaTarget; // Is the ability usage an AoE?
const resource = id.consume || {}; // Resource consumption const resource = id.consume || {}; // Resource consumption
const recharge = id.recharge || {}; // Recharge mechanic const recharge = id.recharge || {}; // Recharge mechanic
const uses = id?.uses ?? {}; // Limited uses const uses = id?.uses ?? {}; // Limited uses
const isPower = this.type === "power"; // Does the item require a power slot? const isPower = this.type === "power"; // Does the item require a power slot?
// TODO: Possibly Mod this to not consume slots based on class? // TODO: Possibly Mod this to not consume slots based on class?
// We could use this for feats and architypes that let a character cast one slot every rest or so
const requirePowerSlot = isPower && id.level > 0 && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode); const requirePowerSlot = isPower && id.level > 0 && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode);
// Define follow-up actions resulting from the item usage // Define follow-up actions resulting from the item usage
let createMeasuredTemplate = hasArea; // Trigger a template creation let createMeasuredTemplate = hasArea; // Trigger a template creation
let consumeRecharge = !!recharge.value; // Consume recharge let consumeRecharge = !!recharge.value; // Consume recharge
let consumeResource = !!resource.target && resource.type !== "ammo"; // Consume a linked (non-ammo) resource let consumeResource =
!!resource.target && resource.type !== "ammo" && !["simpleB", "martialB"].includes(id.weaponType); // Consume a linked (non-ammo) resource, ignore if use is from a blaster
let consumePowerSlot = requirePowerSlot; // Consume a power slot let consumePowerSlot = requirePowerSlot; // Consume a power slot
let consumeUsage = !!uses.per; // Consume limited uses let consumeUsage = !!uses.per; // Consume limited uses
let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of uses let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of uses
let consumePowerLevel = null; // Consume a specific category of power slot
if (requirePowerSlot) consumePowerLevel = id.preparation.mode === "pact" ? "pact" : `power${id.level}`;
// Display a configuration dialog to customize the usage // Display a configuration dialog to customize the usage
const needsConfiguration = const needsConfiguration =
createMeasuredTemplate || createMeasuredTemplate || consumeRecharge || consumeResource || consumePowerSlot || consumeUsage;
consumeRecharge ||
(consumeResource && !["simpleB", "martialB"].includes(id.weaponType)) ||
consumePowerSlot ||
(consumeUsage && !["simpleB", "martialB"].includes(id.weaponType));
if (configureDialog && needsConfiguration) { if (configureDialog && needsConfiguration) {
const configuration = await AbilityUseDialog.create(this); const configuration = await AbilityUseDialog.create(this);
if (!configuration) return; if (!configuration) return;
@ -531,14 +454,14 @@ export default class Item5e extends Item {
// Handle power upcasting // Handle power upcasting
if (requirePowerSlot) { if (requirePowerSlot) {
consumePowerLevel = `power${configuration.level}`; const slotLevel = configuration.level;
if (consumePowerSlot === false) consumePowerLevel = null; const powerLevel = parseInt(slotLevel);
const upcastLevel = parseInt(configuration.level);
if (upcastLevel !== id.level) { if (powerLevel !== id.level) {
item = this.clone({"data.level": upcastLevel}, {keepId: true}); const upcastData = mergeObject(this.data, {"data.level": powerLevel}, {inplace: false});
item.data.update({_id: this.id}); // Retain the original ID (needed until 0.8.2+) item = this.constructor.createOwned(upcastData, actor); // Replace the item with an upcast version
item.prepareFinalAttributes(); // Power save DC, etc...
} }
if (consumePowerSlot) consumePowerSlot = `power${powerLevel}`;
} }
} }
@ -546,7 +469,7 @@ export default class Item5e extends Item {
const usage = item._getUsageUpdates({ const usage = item._getUsageUpdates({
consumeRecharge, consumeRecharge,
consumeResource, consumeResource,
consumePowerLevel, consumePowerSlot,
consumeUsage, consumeUsage,
consumeQuantity consumeQuantity
}); });
@ -555,10 +478,10 @@ export default class Item5e extends Item {
const {actorUpdates, itemUpdates, resourceUpdates} = usage; const {actorUpdates, itemUpdates, resourceUpdates} = usage;
// Commit pending data updates // Commit pending data updates
if (!foundry.utils.isObjectEmpty(itemUpdates)) await item.update(itemUpdates); if (!isObjectEmpty(itemUpdates)) await item.update(itemUpdates);
if (consumeQuantity && item.data.data.quantity === 0) await item.delete(); if (consumeQuantity && item.data.data.quantity === 0) await item.delete();
if (!foundry.utils.isObjectEmpty(actorUpdates)) await actor.update(actorUpdates); if (!isObjectEmpty(actorUpdates)) await actor.update(actorUpdates);
if (!foundry.utils.isObjectEmpty(resourceUpdates)) { if (!isObjectEmpty(resourceUpdates)) {
const resource = actor.items.get(id.consume?.target); const resource = actor.items.get(id.consume?.target);
if (resource) await resource.update(resourceUpdates); if (resource) await resource.update(resourceUpdates);
} }
@ -581,12 +504,18 @@ export default class Item5e extends Item {
* @param {boolean} consumeQuantity Consume quantity of the item if other consumption modes are not available? * @param {boolean} consumeQuantity Consume quantity of the item if other consumption modes are not available?
* @param {boolean} consumeRecharge Whether the item consumes the recharge mechanic * @param {boolean} consumeRecharge Whether the item consumes the recharge mechanic
* @param {boolean} consumeResource Whether the item consumes a limited resource * @param {boolean} consumeResource Whether the item consumes a limited resource
* @param {string|null} consumePowerLevel The category of power slot to consume, or null * @param {string|boolean} consumePowerSlot A level of power slot consumed, or false
* @param {boolean} consumeUsage Whether the item consumes a limited usage * @param {boolean} consumeUsage Whether the item consumes a limited usage
* @returns {object|boolean} A set of data changes to apply when the item is used, or false * @returns {object|boolean} A set of data changes to apply when the item is used, or false
* @private * @private
*/ */
_getUsageUpdates({consumeQuantity, consumeRecharge, consumeResource, consumePowerLevel, consumeUsage}) { _getUsageUpdates({
consumeQuantity = false,
consumeRecharge = false,
consumeResource = false,
consumePowerSlot = false,
consumeUsage = false
}) {
// Reference item data // Reference item data
const id = this.data.data; const id = this.data.data;
const actorUpdates = {}; const actorUpdates = {};
@ -610,9 +539,8 @@ export default class Item5e extends Item {
} }
// Consume Power Slots and Force/Tech Points // Consume Power Slots and Force/Tech Points
if (consumePowerLevel) { if (consumePowerSlot) {
if (Number.isNumeric(consumePowerLevel)) consumePowerLevel = `power${consumePowerLevel}`; const level = this.actor?.data.data.powers[consumePowerSlot];
const level = this.actor?.data.data.powers[consumePowerLevel];
const fp = this.actor.data.data.attributes.force.points; const fp = this.actor.data.data.attributes.force.points;
const tp = this.actor.data.data.attributes.tech.points; const tp = this.actor.data.data.attributes.tech.points;
const powerCost = id.level + 1; const powerCost = id.level + 1;
@ -625,12 +553,10 @@ export default class Item5e extends Item {
const powers = Number(level?.fvalue ?? 0); const powers = Number(level?.fvalue ?? 0);
if (powers === 0) { if (powers === 0) {
const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`); const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`);
ui.notifications.warn( ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}));
game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label})
);
return false; return false;
} }
actorUpdates[`data.powers.${consumePowerLevel}.fvalue`] = Math.max(powers - 1, 0); actorUpdates[`data.powers.${consumePowerSlot}.fvalue`] = Math.max(powers - 1, 0);
if (fp.temp >= powerCost) { if (fp.temp >= powerCost) {
actorUpdates["data.attributes.force.points.temp"] = fp.temp - powerCost; actorUpdates["data.attributes.force.points.temp"] = fp.temp - powerCost;
} else { } else {
@ -643,12 +569,10 @@ export default class Item5e extends Item {
const powers = Number(level?.tvalue ?? 0); const powers = Number(level?.tvalue ?? 0);
if (powers === 0) { if (powers === 0) {
const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`); const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`);
ui.notifications.warn( ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}));
game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label})
);
return false; return false;
} }
actorUpdates[`data.powers.${consumePowerLevel}.tvalue`] = Math.max(powers - 1, 0); actorUpdates[`data.powers.${consumePowerSlot}.tvalue`] = Math.max(powers - 1, 0);
if (tp.temp >= powerCost) { if (tp.temp >= powerCost) {
actorUpdates["data.attributes.tech.points.temp"] = tp.temp - powerCost; actorUpdates["data.attributes.tech.points.temp"] = tp.temp - powerCost;
} else { } else {
@ -714,9 +638,7 @@ export default class Item5e extends Item {
// No consumed target // No consumed target
const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type]; const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type];
if (!consume.target) { if (!consume.target) {
ui.notifications.warn( ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}));
game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel})
);
return false; return false;
} }
@ -755,9 +677,7 @@ export default class Item5e extends Item {
// Verify that the required quantity is available // Verify that the required quantity is available
let remaining = quantity - amount; let remaining = quantity - amount;
if (remaining < 0) { if (remaining < 0) {
ui.notifications.warn( ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel}));
game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel})
);
return false; return false;
} }
@ -789,11 +709,11 @@ export default class Item5e extends Item {
* the prepared message data (if false) * the prepared message data (if false)
*/ */
async displayCard({rollMode, createMessage = true} = {}) { async displayCard({rollMode, createMessage = true} = {}) {
// Render the chat card template // Basic template rendering data
const token = this.actor.token; const token = this.actor.token;
const templateData = { const templateData = {
actor: this.actor, actor: this.actor,
tokenId: token?.uuid || null, tokenId: token ? `${token.scene._id}.${token.id}` : null,
item: this.data, item: this.data,
data: this.getChatData(), data: this.getChatData(),
labels: this.labels, labels: this.labels,
@ -803,14 +723,17 @@ export default class Item5e extends Item {
isVersatile: this.isVersatile, isVersatile: this.isVersatile,
isPower: this.data.type === "power", isPower: this.data.type === "power",
hasSave: this.hasSave, hasSave: this.hasSave,
hasAreaTarget: this.hasAreaTarget, hasAreaTarget: this.hasAreaTarget
isTool: this.data.type === "tool"
}; };
const html = await renderTemplate("systems/sw5e/templates/chat/item-card.html", templateData);
// Render the chat card template
const templateType = ["tool"].includes(this.data.type) ? this.data.type : "item";
const template = `systems/sw5e/templates/chat/${templateType}-card.html`;
const html = await renderTemplate(template, templateData);
// Create the ChatMessage data object // Create the ChatMessage data object
const chatData = { const chatData = {
user: game.user.data._id, user: game.user._id,
type: CONST.CHAT_MESSAGE_TYPES.OTHER, type: CONST.CHAT_MESSAGE_TYPES.OTHER,
content: html, content: html,
flavor: this.data.data.chatFlavor || this.name, flavor: this.data.data.chatFlavor || this.name,
@ -840,7 +763,7 @@ export default class Item5e extends Item {
* @return {Object} An object of chat data to render * @return {Object} An object of chat data to render
*/ */
getChatData(htmlOptions = {}) { getChatData(htmlOptions = {}) {
const data = foundry.utils.deepClone(this.data.data); const data = duplicate(this.data.data);
const labels = this.labels; const labels = this.labels;
// Rich text description // Rich text description
@ -872,7 +795,7 @@ export default class Item5e extends Item {
} }
// Filter properties and return // Filter properties and return
data.properties = props.filter((p) => !!p); data.properties = props.filter(p => !!p);
return data; return data;
} }
@ -1030,9 +953,11 @@ export default class Item5e extends Item {
} }
// Elven Accuracy // Elven Accuracy
if (["weapon", "power"].includes(this.data.type)) {
if (flags.elvenAccuracy && ["dex", "int", "wis", "cha"].includes(this.abilityMod)) { if (flags.elvenAccuracy && ["dex", "int", "wis", "cha"].includes(this.abilityMod)) {
rollConfig.elvenAccuracy = true; rollConfig.elvenAccuracy = true;
} }
}
// Apply Halfling Lucky // Apply Halfling Lucky
if (flags.halflingLucky) rollConfig.halflingLucky = true; if (flags.halflingLucky) rollConfig.halflingLucky = true;
@ -1065,7 +990,7 @@ export default class Item5e extends Item {
const messageData = {"flags.sw5e.roll": {type: "damage", itemId: this.id}}; const messageData = {"flags.sw5e.roll": {type: "damage", itemId: this.id}};
// Get roll data // Get roll data
const parts = itemData.damage.parts.map((d) => d[0]); const parts = itemData.damage.parts.map(d => d[0]);
const rollData = this.getRollData(); const rollData = this.getRollData();
if (powerLevel) rollData.item.level = powerLevel; if (powerLevel) rollData.item.level = powerLevel;
@ -1099,8 +1024,7 @@ export default class Item5e extends Item {
// Scale damage from up-casting powers // Scale damage from up-casting powers
if (this.data.type === "power") { if (this.data.type === "power") {
if (itemData.scaling.mode === "atwill") { if (itemData.scaling.mode === "atwill") {
const level = const level = this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel;
this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel;
this._scaleAtWillDamage(parts, itemData.scaling.formula, level, rollData); this._scaleAtWillDamage(parts, itemData.scaling.formula, level, rollData);
} else if (powerLevel && itemData.scaling.mode === "level" && itemData.scaling.formula) { } else if (powerLevel && itemData.scaling.mode === "level" && itemData.scaling.formula) {
const scaling = itemData.scaling.formula; const scaling = itemData.scaling.formula;
@ -1120,7 +1044,7 @@ export default class Item5e extends Item {
// only add the ammunition damage if the ammution is a consumable with type 'ammo' // only add the ammunition damage if the ammution is a consumable with type 'ammo'
if (this._ammo && ammoData.type === "consumable" && ammoData.data.consumableType === "ammo") { if (this._ammo && ammoData.type === "consumable" && ammoData.data.consumableType === "ammo") {
parts.push("@ammo"); parts.push("@ammo");
rollData["ammo"] = ammoData.data.damage.parts.map((p) => p[0]).join("+"); rollData["ammo"] = ammoData.data.damage.parts.map(p => p[0]).join("+");
rollConfig.flavor += ` [${this._ammo.name}]`; rollConfig.flavor += ` [${this._ammo.name}]`;
delete this._ammo; delete this._ammo;
} }
@ -1272,18 +1196,12 @@ export default class Item5e extends Item {
const parts = [`@mod`, "@prof"]; const parts = [`@mod`, "@prof"];
const title = `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`; const title = `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`;
// Add global actor bonus
const bonuses = getProperty(this.actor.data.data, "bonuses.abilities") || {};
if (bonuses.check) {
parts.push("@checkBonus");
rollData.checkBonus = bonuses.check;
}
// Compose the roll data // Compose the roll data
const rollConfig = mergeObject( const rollConfig = mergeObject(
{ {
parts: parts, parts: parts,
data: rollData, data: rollData,
template: "systems/sw5e/templates/chat/tool-roll-dialog.html",
title: title, title: title,
speaker: ChatMessage.getSpeaker({actor: this.actor}), speaker: ChatMessage.getSpeaker({actor: this.actor}),
flavor: title, flavor: title,
@ -1292,7 +1210,6 @@ export default class Item5e extends Item {
top: options.event ? options.event.clientY - 80 : null, top: options.event ? options.event.clientY - 80 : null,
left: window.innerWidth - 710 left: window.innerWidth - 710
}, },
chooseModifier: true,
halflingLucky: this.actor.getFlag("sw5e", "halflingLucky") || false, halflingLucky: this.actor.getFlag("sw5e", "halflingLucky") || false,
reliableTalent: this.data.data.proficient >= 1 && this.actor.getFlag("sw5e", "reliableTalent"), reliableTalent: this.data.data.proficient >= 1 && this.actor.getFlag("sw5e", "reliableTalent"),
messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id}} messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id}}
@ -1314,18 +1231,13 @@ export default class Item5e extends Item {
getRollData() { getRollData() {
if (!this.actor) return null; if (!this.actor) return null;
const rollData = this.actor.getRollData(); const rollData = this.actor.getRollData();
rollData.item = foundry.utils.deepClone(this.data.data); rollData.item = duplicate(this.data.data);
// Include an ability score modifier if one exists // Include an ability score modifier if one exists
const abl = this.abilityMod; const abl = this.abilityMod;
if (abl) { if (abl) {
const ability = rollData.abilities[abl]; const ability = rollData.abilities[abl];
if (!ability) { rollData["mod"] = ability.mod || 0;
console.warn(
`Item ${this.name} in Actor ${this.actor.name} has an invalid item ability modifier of ${abl} defined`
);
}
rollData["mod"] = ability?.mod || 0;
} }
// Include a proficiency score // Include a proficiency score
@ -1367,12 +1279,12 @@ export default class Item5e extends Item {
if (!(isTargetted || game.user.isGM || message.isAuthor)) return; if (!(isTargetted || game.user.isGM || message.isAuthor)) return;
// Recover the actor for the chat card // Recover the actor for the chat card
const actor = await this._getChatCardActor(card); const actor = this._getChatCardActor(card);
if (!actor) return; if (!actor) return;
// Get the Item from stored flag data or by the item ID on the Actor // Get the Item from stored flag data or by the item ID on the Actor
const storedData = message.getFlag("sw5e", "itemData"); const storedData = message.getFlag("sw5e", "itemData");
const item = storedData ? new this(storedData, {parent: actor}) : actor.items.get(card.dataset.itemId); const item = storedData ? this.createOwned(storedData, actor) : actor.getOwnedItem(card.dataset.itemId);
if (!item) { if (!item) {
return ui.notifications.error( return ui.notifications.error(
game.i18n.format("SW5E.ActionWarningNoItem", {item: card.dataset.itemId, name: actor.name}) game.i18n.format("SW5E.ActionWarningNoItem", {item: card.dataset.itemId, name: actor.name})
@ -1440,11 +1352,16 @@ export default class Item5e extends Item {
* @return {Actor|null} The Actor entity or null * @return {Actor|null} The Actor entity or null
* @private * @private
*/ */
static async _getChatCardActor(card) { static _getChatCardActor(card) {
// Case 1 - a synthetic actor from a Token // Case 1 - a synthetic actor from a Token
if (card.dataset.tokenId) { const tokenKey = card.dataset.tokenId;
const token = await fromUuid(card.dataset.tokenId); if (tokenKey) {
if (!token) return null; const [sceneId, tokenId] = tokenKey.split(".");
const scene = game.scenes.get(sceneId);
if (!scene) return null;
const tokenData = scene.getEmbeddedEntity("Token", tokenId);
if (!tokenData) return null;
const token = new Token(tokenData);
return token.actor; return token.actor;
} }
@ -1458,183 +1375,33 @@ export default class Item5e extends Item {
/** /**
* Get the Actor which is the author of a chat card * Get the Actor which is the author of a chat card
* @param {HTMLElement} card The chat card being used * @param {HTMLElement} card The chat card being used
* @return {Actor[]} An Array of Actor entities, if any * @return {Array.<Actor>} An Array of Actor entities, if any
* @private * @private
*/ */
static _getChatCardTargets(card) { static _getChatCardTargets(card) {
let targets = canvas.tokens.controlled.filter((t) => !!t.actor); let targets = canvas.tokens.controlled.filter(t => !!t.actor);
if (!targets.length && game.user.character) targets = targets.concat(game.user.character.getActiveTokens()); if (!targets.length && game.user.character) targets = targets.concat(game.user.character.getActiveTokens());
if (!targets.length) ui.notifications.warn(game.i18n.localize("SW5E.ActionWarningNoToken")); if (!targets.length) ui.notifications.warn(game.i18n.localize("SW5E.ActionWarningNoToken"));
return targets; return targets;
} }
/* -------------------------------------------- */
/* Event Handlers */
/* -------------------------------------------- */
/** @inheritdoc */
async _preCreate(data, options, user) {
await super._preCreate(data, options, user);
if (!this.isEmbedded || this.parent.type === "vehicle") return;
const actorData = this.parent.data;
const isNPC = this.parent.type === "npc";
let updates;
switch (data.type) {
case "equipment":
updates = this._onCreateOwnedEquipment(data, actorData, isNPC);
break;
case "weapon":
updates = this._onCreateOwnedWeapon(data, actorData, isNPC);
break;
case "power":
updates = this._onCreateOwnedPower(data, actorData, isNPC);
break;
}
if (updates) return this.data.update(updates);
}
/* -------------------------------------------- */
/** @inheritdoc */
_onCreate(data, options, userId) {
super._onCreate(data, options, userId);
// The below options are only needed for character classes
if (userId !== game.user.id) return;
const isCharacterClass = this.parent && this.parent.type !== "vehicle" && this.type === "class";
if (!isCharacterClass) return;
// Assign a new primary class
const pc = this.parent.items.get(this.parent.data.data.details.originalClass);
if (!pc) this.parent._assignPrimaryClass();
// Prompt to add new class features
if (options.addFeatures === false) return;
this.parent
.getClassFeatures({
className: this.name,
archetypeName: this.data.data.archetype,
level: this.data.data.levels
})
.then((features) => {
return this.parent.addEmbeddedItems(features, options.promptAddFeatures);
});
}
/* -------------------------------------------- */
/** @inheritdoc */
_onUpdate(changed, options, userId) {
super._onUpdate(changed, options, userId);
// The below options are only needed for character classes
if (userId !== game.user.id) return;
const isCharacterClass = this.parent && this.parent.type !== "vehicle" && this.type === "class";
if (!isCharacterClass) return;
// Prompt to add new class features
const addFeatures = changed["name"] || (changed.data && ["archetype", "levels"].some((k) => k in changed.data));
if (!addFeatures || options.addFeatures === false) return;
this.parent
.getClassFeatures({
className: changed.name || this.name,
archetypeName: changed.data?.archetype || this.data.data.archetype,
level: changed.data?.levels || this.data.data.levels
})
.then((features) => {
return this.parent.addEmbeddedItems(features, options.promptAddFeatures);
});
}
/* -------------------------------------------- */
/** @inheritdoc */
_onDelete(options, userId) {
super._onDelete(options, userId);
// Assign a new primary class
if (this.parent && this.type === "class" && userId === game.user.id) {
if (this.id !== this.parent.data.data.details.originalClass) return;
this.parent._assignPrimaryClass();
}
}
/* -------------------------------------------- */
/**
* Pre-creation logic for the automatic configuration of owned equipment type Items
* @private
*/
_onCreateOwnedEquipment(data, actorData, isNPC) {
const updates = {};
if (foundry.utils.getProperty(data, "data.equipped") === undefined) {
updates["data.equipped"] = isNPC; // NPCs automatically equip equipment
}
if (foundry.utils.getProperty(data, "data.proficient") === undefined) {
if (isNPC) {
updates["data.proficient"] = true; // NPCs automatically have equipment proficiency
} else {
const armorProf = CONFIG.SW5E.armorProficienciesMap[data.data?.armor?.type]; // Player characters check proficiency
const actorArmorProfs = actorData.data.traits?.armorProf?.value || [];
updates["data.proficient"] = armorProf === true || actorArmorProfs.includes(armorProf);
}
}
return updates;
}
/* -------------------------------------------- */
/**
* Pre-creation logic for the automatic configuration of owned power type Items
* @private
*/
_onCreateOwnedPower(data, actorData, isNPC) {
const updates = {};
updates["data.preparation.prepared"] = true; // Automatically prepare powers for everyone
return updates;
}
/* -------------------------------------------- */
/**
* Pre-creation logic for the automatic configuration of owned weapon type Items
* @private
*/
_onCreateOwnedWeapon(data, actorData, isNPC) {
const updates = {};
if (foundry.utils.getProperty(data, "data.equipped") === undefined) {
updates["data.equipped"] = isNPC; // NPCs automatically equip weapons
}
if (foundry.utils.getProperty(data, "data.proficient") === undefined) {
if (isNPC) {
updates["data.proficient"] = true; // NPCs automatically have equipment proficiency
} else {
// TODO: With the changes to make weapon proficiencies more verbose, this may need revising
const weaponProf = CONFIG.SW5E.weaponProficienciesMap[data.data?.weaponType]; // Player characters check proficiency
const actorWeaponProfs = actorData.data.traits?.weaponProf?.value || [];
updates["data.proficient"] = weaponProf === true || actorWeaponProfs.includes(weaponProf);
}
}
return updates;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Factory Methods */ /* Factory Methods */
/* -------------------------------------------- */ /* -------------------------------------------- */
// TODO: Make work properly
/** /**
* Create a consumable power scroll Item from a power Item. * Create a consumable power scroll Item from a power Item.
* @param {Item5e} power The power to be made into a scroll * @param {Item5e} power The power to be made into a scroll
* @return {Item5e} The created scroll consumable item * @return {Item5e} The created scroll consumable item
* @private
*/ */
static async createScrollFromPower(power) { static async createScrollFromPower(power) {
// Get power data // Get power data
const itemData = power instanceof Item5e ? power.data : power; const itemData = power instanceof Item5e ? power.data : power;
const {actionType, description, source, activation, duration, target, range, damage, save, level} = const {actionType, description, source, activation, duration, target, range, damage, save, level} = itemData.data;
itemData.data;
// Get scroll data // Get scroll data
const scrollUuid = `Compendium.${CONFIG.SW5E.sourcePacks.ITEMS}.${CONFIG.SW5E.powerScrollIds[level]}`; const scrollUuid = CONFIG.SW5E.powerScrollIds[level];
const scrollItem = await fromUuid(scrollUuid); const scrollItem = await fromUuid(scrollUuid);
const scrollData = scrollItem.data; const scrollData = scrollItem.data;
delete scrollData._id; delete scrollData._id;

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;
} }
@ -182,11 +171,12 @@ export default class ItemSheet5e extends ItemSheet {
if (item.type === "weapon") { if (item.type === "weapon") {
props.push( props.push(
...Object.entries(item.data.properties) ...Object.entries(item.data.properties)
.filter((e) => e[1] === true) .filter(e => e[1] === true)
.map((e) => CONFIG.SW5E.weaponProperties[e[0]]) .map(e => CONFIG.SW5E.weaponProperties[e[0]])
); );
} 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") {
@ -205,10 +194,6 @@ export default class ItemSheet5e extends ItemSheet {
//props.push(labels.background); //props.push(labels.background);
} else if (item.type === "classfeature") { } else if (item.type === "classfeature") {
//props.push(labels.classfeature); //props.push(labels.classfeature);
} else if (item.type === "deployment") {
//props.push(labels.deployment);
} else if (item.type === "venture") {
//props.push(labels.venture);
} else if (item.type === "fightingmastery") { } else if (item.type === "fightingmastery") {
//props.push(labels.fightingmastery); //props.push(labels.fightingmastery);
} else if (item.type === "fightingstyle") { } else if (item.type === "fightingstyle") {
@ -226,7 +211,7 @@ export default class ItemSheet5e extends ItemSheet {
if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) { if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) {
props.push(labels.activation, labels.range, labels.target, labels.duration); props.push(labels.activation, labels.range, labels.target, labels.duration);
} }
return props.filter((p) => !!p); return props.filter(p => !!p);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -249,7 +234,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 +246,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});
@ -271,7 +256,7 @@ export default class ItemSheet5e extends ItemSheet {
// Handle Damage array // Handle Damage array
const damage = data.data?.damage; const damage = data.data?.damage;
if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]); if (damage) damage.parts = Object.values(damage?.parts || {}).map(d => [d[0] || "", d[1] || ""]);
// Return the flattened submission data // Return the flattened submission data
return flattenObject(data); return flattenObject(data);
@ -279,13 +264,13 @@ 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(
"Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update." "Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update."
@ -318,7 +303,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 +312,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

@ -16,7 +16,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,7 +45,7 @@ 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.`

View file

@ -9,11 +9,11 @@ export const migrateWorld = async function () {
); );
// 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 +24,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 +38,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 +80,38 @@ 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);
console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`);
} catch (err) { } catch (err) {
// Handle migration failures // Handle migration failures
err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`; 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 +129,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
@ -207,9 +199,7 @@ function cleanActorData(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 +232,26 @@ 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(
if (!t.actorId || t.actorLink) { tokens.map(async t => {
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}; };
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -304,7 +286,7 @@ function _updateNPCData(actor) {
game.packs game.packs
.get("sw5e.monsters") .get("sw5e.monsters")
.getEntity(core_id) .getEntity(core_id)
.then((monster) => { .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;
@ -321,7 +303,7 @@ function _updateNPCData(actor) {
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.
@ -363,8 +345,7 @@ function _migrateActorMovement(actorData, updateData) {
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
@ -444,7 +425,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 +454,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 +461,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;
} }
} }
@ -649,14 +534,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;
@ -670,7 +551,7 @@ function _migrateItemAttunement(item, updateData) {
* @private * @private
*/ */
export async function purgeFlags(pack) { export async function purgeFlags(pack) {
const cleanFlags = (flags) => { const cleanFlags = flags => {
const flags5e = flags.sw5e || null; const flags5e = flags.sw5e || null;
return flags5e ? {sw5e: flags5e} : {}; return flags5e ? {sw5e: flags5e} : {};
}; };
@ -679,7 +560,7 @@ 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;
}); });

View file

@ -18,7 +18,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 +44,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 +81,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 +105,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

@ -7,7 +7,7 @@ export const registerSystemSettings = function () {
scope: "world", scope: "world",
config: false, config: false,
type: String, type: String,
default: game.system.data.version default: ""
}); });
/** /**
@ -42,7 +42,7 @@ export const registerSystemSettings = function () {
5105: "SETTINGS.5eDiagDMG", 5105: "SETTINGS.5eDiagDMG",
EUCL: "SETTINGS.5eDiagEuclidean" EUCL: "SETTINGS.5eDiagEuclidean"
}, },
onChange: (rule) => (canvas.grid.diagonalRule = rule) onChange: rule => (canvas.grid.diagonalRule = rule)
}); });
/** /**
@ -91,7 +91,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();
} }
}); });

View file

@ -17,7 +17,6 @@ export const preloadHandlebarsTemplates = async function () {
"systems/sw5e/templates/actors/newActor/parts/swalt-biography.html", "systems/sw5e/templates/actors/newActor/parts/swalt-biography.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-core.html", "systems/sw5e/templates/actors/newActor/parts/swalt-core.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-crew.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html", "systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-features.html", "systems/sw5e/templates/actors/newActor/parts/swalt-features.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html", "systems/sw5e/templates/actors/newActor/parts/swalt-inventory.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: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

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