Merge pull request #232 from unrealkakeman89/Develop

Develop
This commit is contained in:
CK 2021-07-01 16:31:37 -04:00 committed by GitHub
commit 5d879f99e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
240 changed files with 12070 additions and 4093 deletions

3
.gitignore vendored
View file

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

View file

@ -1,6 +1,7 @@
{
"ACTOR.TypeCharacter": "Player Character",
"ACTOR.TypeNpc": "Non-Player Character",
"ACTOR.TypeStarship": "Starship",
"ACTOR.TypeVehicle": "Vehicle",
"ITEM.TypeArchetype": "Archetype",
"ITEM.TypeBackground": "Background",
@ -8,6 +9,8 @@
"ITEM.TypeClass": "Class",
"ITEM.TypeClassfeature": "Class Feature",
"ITEM.TypeConsumable": "Consumable",
"ITEM.TypeDeployment": "Deployment",
"ITEM.TypeDeploymentfeature": "Deployment Feature",
"ITEM.TypeEquipment": "Equipment",
"ITEM.TypeFeat": "Feat",
"ITEM.TypeFightingmastery": "Fighting Mastery",
@ -16,7 +19,12 @@
"ITEM.TypeLoot": "Loot",
"ITEM.TypePower": "Power",
"ITEM.TypeSpecies": "Species",
"ITEM.TypeStarshipfeature": "Starship Feature",
"ITEM.TypeStarshipfeaturePl": "Starship Features",
"ITEM.TypeStarshipmod": "Starship Modification",
"ITEM.TypeStarshipmodPl": "Starship Modifications",
"ITEM.TypeTool": "Tool",
"ITEM.TypeVenture": "Venture",
"ITEM.TypeWeapon": "Weapon",
"SETTINGS.5eAllowPolymorphingL": "Allow players to polymorph their own actors.",
"SETTINGS.5eAllowPolymorphingN": "Allow Polymorphing",
@ -35,11 +43,13 @@
"SETTINGS.5eInitTBN": "Initiative Dexterity Tiebreaker",
"SETTINGS.5eNoExpL": "Remove experience bars from character sheets.",
"SETTINGS.5eNoExpN": "Disable Experience Tracking",
"SETTINGS.5eReset": "Reset",
"SETTINGS.5eRestEpic": "Epic Heroism (LR: 1 hour, SR: 1 min)",
"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.5eRestN": "Rest Variant",
"SETTINGS.5eRestPHB": "Player's Handbook (LR: 8 hours, SR: 1 hour)",
"SETTINGS.5eUndoChanges": "Undo Changes",
"SETTINGS.SWColorDark": "Dark Theme",
"SETTINGS.SWColorL": "Set the color theme of the game",
"SETTINGS.SWColorLight": "Light Theme",
@ -70,6 +80,7 @@
"SW5E.AbilityUseCast": "Cast Power",
"SW5E.AbilityUseChargedHint": "This {type} is charged and ready to use!",
"SW5E.AbilityUseChargesLabel": "{value} Charges",
"SW5E.AbilityUseConfig": "Usage Configuration",
"SW5E.AbilityUseConsumableChargeHint": "Using this {type} will consume 1 charge of {value} remaining.",
"SW5E.AbilityUseConsumableDestroyHint": "Using this {type} will consume its final charge and it will be destroyed.",
"SW5E.AbilityUseConsumableLabel": "{max} per {per}",
@ -96,7 +107,9 @@
"SW5E.ActionUtil": "Utility",
"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.ActorWarningInvalidItem": "{itemType} items cannot be added to a {actorType}.",
"SW5E.Add": "Add",
"SW5E.AddEmbeddedItemPromptHint": "Do you want to add these items to your character sheet?",
"SW5E.AdditionalNotes": "Additional Notes",
"SW5E.Advantage": "Advantage",
"SW5E.Alignment": "Alignment",
@ -110,6 +123,7 @@
"SW5E.AlignmentND": "Neutral Dark",
"SW5E.AlignmentNL": "Neutral Light",
"SW5E.Appearance": "Appearance",
"SW5E.Apply": "Apply",
"SW5E.ArchetypeName": "Archetype Name",
"SW5E.Archetypes": "Archetypes",
"SW5E.ArmorClass": "Armor Class",
@ -179,6 +193,9 @@
"SW5E.BonusSaveForm": "Update Bonuses",
"SW5E.BonusTechPowerDC": "Global Tech Power DC Bonus",
"SW5E.BonusTitle": "Configure Actor Bonuses",
"SW5E.BurnFuel": "Burn",
"SW5E.CapacityMultiplier": "Capacity Multiplier",
"SW5E.CentStorageCapacity": "Central Storage Capacity",
"SW5E.ChallengeRating": "Challenge Rating",
"SW5E.Charged": "Charged",
"SW5E.Charges": "Charges",
@ -188,9 +205,14 @@
"SW5E.ChatContextHealing": "Apply Healing",
"SW5E.ChatFlavor": "Chat Message Flavor",
"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.ClassOriginal": "Original Class",
"SW5E.ClassSaves": "Saving Throws",
"SW5E.ClassSkillsChosen": "Chosen Class Skills",
"SW5E.ClassSkillsNumber": "Number of Starting Skills",
"SW5E.Collapse": "Collapse/Expand",
"SW5E.ComponentMaterial": "Material",
"SW5E.ComponentSomatic": "Somatic",
"SW5E.ComponentVerbal": "Verbal",
@ -249,6 +271,32 @@
"SW5E.CoverHalf": "Half",
"SW5E.CoverThreeQuarters": "Three Quarters",
"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.CriticalHit": "Critical Hit",
"SW5E.Currency": "Currency",
@ -282,8 +330,11 @@
"SW5E.DeathSavingThrow": "Death Saving Throw",
"SW5E.Default": "Default",
"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": "Description",
"SW5E.DestructionSave": "Destruction Saves",
"SW5E.Details": "Details",
"SW5E.Dimensions": "Dimensions",
"SW5E.Disadvantage": "Disadvantage",
@ -292,38 +343,55 @@
"SW5E.DistMi": "Miles",
"SW5E.DistSelf": "Self",
"SW5E.DistTouch": "Touch",
"SW5E.DmgRed": "Damage Reduction",
"SW5E.Duration": "Duration",
"SW5E.EffectsCategoryTemporary": "Temporary Effects",
"SW5E.EffectsCategoryPassive": "Passive Effects",
"SW5E.EffectsCategoryInactive": "Inactive Effects",
"SW5E.EffectCreate": "Create Effect",
"SW5E.EffectDelete": "Delete Effect",
"SW5E.EffectEdit": "Edit Effect",
"SW5E.EffectInactive": "Inactive Effects",
"SW5E.EffectNew": "New Effect",
"SW5E.EffectPassive": "Passive Effects",
"SW5E.Effects": "Effects",
"SW5E.EffectTemporary": "Temporary Effects",
"SW5E.EffectsCategoryInactive": "Inactive Effects",
"SW5E.EffectsCategoryPassive": "Passive Effects",
"SW5E.EffectsCategoryTemporary": "Temporary Effects",
"SW5E.EffectToggle": "Toggle Effect",
"SW5E.Engine": "Engine",
"SW5E.EnginePl": "Engines",
"SW5E.EquipmentBonus": "Magical Bonus",
"SW5E.EquipmentClothing": "Clothing",
"SW5E.EquipmentHeavy": "Heavy Armor",
"SW5E.EquipmentHyperdrive": "Hyperdrive",
"SW5E.EquipmentLight": "Light Armor",
"SW5E.EquipmentMedium": "Medium Armor",
"SW5E.EquipmentNatural": "Natural Armor",
"SW5E.EquipmentPowerCoupling": "Power Coupling",
"SW5E.EquipmentReactor": "Reactor",
"SW5E.EquipmentShield": "Shield",
"SW5E.EquipmentShieldProficiency": "Shields",
"SW5E.EquipmentStarshipArmor": "Starship Armor",
"SW5E.EquipmentStarshipShield": "Starship Shield",
"SW5E.EquipmentTrinket": "Trinket",
"SW5E.EquipmentVehicle": "Vehicle Equipment",
"SW5E.Equipped": "Equipped",
"SW5E.Exhaustion": "Exhaustion",
"SW5E.Expand": "Expand",
"SW5E.Expertise": "Expertise",
"SW5E.Favorites": "Favoris",
"SW5E.Favorites": "Favorites",
"SW5E.FavoritesAndNotes": "Favorites & Notes",
"SW5E.Feats": "Feats",
"SW5E.FeatureActionRecharge": "Action Recharge",
"SW5E.FeatureActive": "Active Abilities",
"SW5E.FeatureAdd": "Create Feature",
"SW5E.FeatureAttack": "Feature Attack",
"SW5E.FeatureCollapse": "Collapse Feature",
"SW5E.FeatureExpand": "Expand Feature",
"SW5E.FeaturePassive": "Passive Abilities",
"SW5E.FeatureRechargeOn": "Recharge On",
"SW5E.FeatureRechargeResult": "1d6 Result",
"SW5E.Features": "Features",
"SW5E.FeatureType": "Feature Type",
"SW5E.FeatureUsage": "Feature Usage",
"SW5E.FeetAbbr": "ft.",
"SW5E.Filter": "Filter",
@ -453,19 +521,31 @@
"SW5E.Flaws": "Flaws",
"SW5E.ForcePowerbook": "Force Powers",
"SW5E.Formula": "Formula",
"SW5E.FuelCapacity": "Fuel Capacity",
"SW5E.FuelCostPerUnit": "Fuel Cost per Unit",
"SW5E.FuelCostsMod": "Fuel Costs Modifier",
"SW5E.GrantedAbilities": "Granted Abilities",
"SW5E.HalfProficient": "Half Proficient",
"SW5E.HardpointSizeMod": "Hardpoint Size Modifier",
"SW5E.Healing": "Healing",
"SW5E.HealingTemp": "Healing (Temporary)",
"SW5E.Health": "Health",
"SW5E.HealthConditions": "Health Conditions",
"SW5E.HealthFormula": "Health Formula",
"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.HitDiceUsed": "Hit Dice Used",
"SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!",
"SW5E.HP": "Health",
"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.Identified": "Identified",
"SW5E.Initiative": "Initiative",
@ -509,6 +589,7 @@
"SW5E.ItemTypeArchetype": "Archetype",
"SW5E.ItemTypeBackground": "Background",
"Sw5E.ItemTypeBackgroundPl": "Backgrounds",
"SW5E.ItemTypeBackpack": "Container",
"SW5E.ItemTypeClass": "Class",
"SW5E.ItemTypeClassFeat": "Class Feature",
"SW5E.ItemTypeClassFeats": "Class Features",
@ -517,6 +598,10 @@
"SW5E.ItemTypeConsumablePl": "Consumables",
"SW5E.ItemTypeContainer": "Container",
"SW5E.ItemTypeContainerPl": "Containers",
"SW5E.ItemTypeDeployment": "Deployment",
"SW5E.ItemTypeDeploymentFeature": "Deployment Feature",
"SW5E.ItemTypeDeploymentFeaturePl": "Deployment Features",
"SW5E.ItemTypeDeploymentPl": "Deployments",
"SW5E.ItemTypeEquipment": "Equipment",
"SW5E.ItemTypeEquipmentPl": "Equipment",
"SW5E.ItemTypeFeat": "Feat",
@ -533,13 +618,19 @@
"SW5E.ItemTypePowerPl": "Powers",
"SW5E.ItemTypeSpecies": "Species",
"SW5E.ItemTypeSpeciesPl": "Species",
"SW5E.ItemTypeStarshipMod": "Starship Modification",
"SW5E.ItemTypeStarshipModPl": "Starship Modifications",
"SW5E.ItemTypeTool": "Tool",
"SW5E.ItemTypeToolPl": "Tools",
"SW5E.ItemTypeVenture": "Venture",
"SW5E.ItemTypeVenturePl": "Ventures",
"SW5E.ItemTypeWeapon": "Weapon",
"SW5E.ItemTypeWeaponPl": "Weapons",
"SW5E.ItemWeaponAttack": "Weapon Attack",
"SW5E.ItemWeaponDetails": "Weapon Details",
"SW5E.ItemWeaponProperties": "Weapon Properties",
"SW5E.ItemWeaponSize": "Starship Weapon Size",
"SW5E.ItemWeaponSizePl": "Starship Weapon Sizes",
"SW5E.ItemWeaponStatus": "Weapon Status",
"SW5E.ItemWeaponType": "Weapon Type",
"SW5E.ItemWeaponUsage": "Weapon Usage",
@ -665,6 +756,7 @@
"SW5E.LongRest": "Long Rest",
"SW5E.LongRestEpic": "Long Rest (1 hour)",
"SW5E.LongRestGritty": "Long Rest (7 days)",
"SW5E.LongRestHint": "Take a long rest? On a long rest you will recover hit points, half your maximum hit dice, class resources, limited use item charges, and power points.",
"SW5E.LongRestNormal": "Long Rest (8 hours)",
"SW5E.LongRestOvernight": "Long Rest (New Day)",
"SW5E.LongRestResult": "{name} takes a long rest.",
@ -684,18 +776,25 @@
"SW5E.LongRestResultTP": "{name} takes a long rest and recovers {tech} Tech Points.",
"SW5E.LongRestResultTPHD": "{name} takes a long rest and recovers {tech} Tech Points and {dice} Hit Dice.",
"SW5E.Max": "Max",
"SW5E.ModCap": "Modification Capacity",
"SW5E.Modifier": "Modifier",
"SW5E.Movement": "Movement",
"SW5E.MovementBurrow": "Burrow",
"SW5E.MovementClimb": "Climb",
"SW5E.MovementConfig": "Configure Movement Speed",
"SW5E.MovementConfigHint": "Configure the movement speed and special movement attributes of this creature.",
"SW5E.MovementCrawl": "Crawl",
"SW5E.MovementFly": "Fly",
"SW5E.MovementHover": "Hover",
"SW5E.MovementRoll": "Roll",
"SW5E.MovementSpace": "Space Flight",
"SW5E.MovementSwim": "Swim",
"SW5E.MovementTurn": "Turning",
"SW5E.MovementUnits": "Units",
"SW5E.MovementWalk": "Walk",
"SW5E.Name": "Character Name",
"SW5E.NewDay": "Is New Day?",
"SW5E.NewDayHint": "Recover limited use abilities which recharge \"per day\"?",
"SW5E.NoCharges": "No Charges",
"SW5E.None": "None",
"SW5E.NoPowerLevels": "This character has no powercaster levels, but you may add powers manually.",
@ -748,7 +847,12 @@
"SW5E.PowerCreate": "Create Power",
"SW5E.PowerDC": "Power DC",
"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.PowerfulCritical": "Powerful Critical",
"SW5E.PowerLevel": "Power Level",
"SW5E.PowerLevel0": "At-Will",
"SW5E.PowerLevel1": "1st Level",
@ -778,6 +882,7 @@
"SW5E.PowerProgression": "Power Progression",
"SW5E.PowerProgSct": "Scout",
"SW5E.PowerProgSnt": "Sentinel",
"SW5E.PowerRouting": "Power Routing",
"SW5E.PowerSchool": "Power School",
"SW5E.PowersKnown": "Powers Known",
"SW5E.PowerTarget": "Power Target",
@ -789,21 +894,29 @@
"SW5E.Proficient": "Proficient",
"SW5E.Quantity": "Quantity",
"SW5E.Range": "Range",
"SW5E.Rank": "Rank",
"SW5E.RankPl": "Ranks",
"SW5E.Rarity": "Rarity",
"SW5E.Reaction": "Reaction",
"SW5E.ReactionPl": "Reactions",
"SW5E.Recharge": "Recharge",
"SW5E.Refitting": "Refitting",
"SW5E.Refuel": "Refuel",
"SW5E.RegenerationRateCoefficient": "Regeneration Rate Coefficient",
"SW5E.RequiredMaterials": "Required Materials",
"SW5E.Requirements": "Requirements",
"SW5E.ResourcesAndTraits": "Resources & Traits",
"SW5E.ResourcePrimary": "Resource 1",
"SW5E.ResourcesAndTraits": "Resources & Traits",
"SW5E.ResourceSecondary": "Resource 2",
"SW5E.ResourceTertiary": "Resource 3",
"SW5E.Rest": "Rest",
"SW5E.RestL": "L. Rest",
"SW5E.RestS": "S. Rest",
"SW5E.Ritual": "Ritual",
"SW5E.Role": "Role",
"SW5E.RolePl": "Roles",
"SW5E.Roll": "Roll",
"SW5E.RollExample": "e.g. +1d4",
"SW5E.RollExample": "e.g. 1d4",
"SW5E.RollMode": "Roll Mode",
"SW5E.RollSituationalBonus": "Situational Bonus?",
"SW5E.Save": "Save",
@ -816,6 +929,7 @@
"SW5E.SchoolLgt": "Light",
"SW5E.SchoolTec": "Tech",
"SW5E.SchoolUni": "Universal",
"SW5E.SelectItemsPromptTitle": "Select Items",
"SW5E.SenseBlindsight": "Blindsight",
"SW5E.SenseBS": "Blindsight",
"SW5E.SenseDarkvision": "Darkvision",
@ -834,6 +948,10 @@
"SW5E.SheetClassNPC": "Default NPC Sheet",
"SW5E.SheetClassNPCOld": "Old NPC Sheet",
"SW5E.SheetClassVehicle": "Default Vehicle Sheet",
"SW5E.ShieldDice": "Shield Dice",
"SW5E.ShieldPoints": "Shield Points",
"SW5E.ShieldPointsFormula": "Shield Points Formula",
"SW5E.ShieldRegen": "Regen",
"SW5E.ShortRest": "Short Rest",
"SW5E.ShortRestEpic": "Short Rest (5 minutes)",
"SW5E.ShortRestGritty": "Short Rest (8 hours)",
@ -873,6 +991,7 @@
"SW5E.SkillSte": "Stealth",
"SW5E.SkillSur": "Survival",
"SW5E.SkillTec": "Technology",
"SW5E.Skip": "Skip",
"SW5E.Slots": "Slots",
"SW5E.Source": "Source",
"SW5E.Special": "Special",
@ -882,8 +1001,62 @@
"SW5E.SpeciesTraits": "Species Traits",
"SW5E.Speed": "Speed",
"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.SuiteCap": "Suite Capacity",
"SW5E.Supply": "Supply",
"SW5E.SysStorageCapacity": "System Storage Capacity",
"SW5E.SystemDrainage": "System Drainage",
"SW5E.Target": "Target",
"SW5E.TargetAlly": "Ally",
"SW5E.TargetCone": "Cone",
@ -899,6 +1072,7 @@
"SW5E.TargetSpace": "Space",
"SW5E.TargetSphere": "Sphere",
"SW5E.TargetSquare": "Square",
"SW5E.TargetStarship": "Starship",
"SW5E.TargetWall": "Wall",
"SW5E.TargetWeapon": "Weapon",
"SW5E.TargetWidth": "Line Width",
@ -954,7 +1128,8 @@
"SW5E.TraitToolProf": "Tool Proficiencies",
"SW5E.TraitWeaponProf": "Weapon Proficiencies",
"SW5E.Type": "Type",
"SW5E.Unequipped": "Not Equipped",
"SW5E.Uncrewed": "Uncrewed",
"SW5E.Unequipped": "Unequipped",
"SW5E.UniversalPowerDC": "Universal Power DC",
"SW5E.Unlimited": "Unlimited",
"SW5E.Usage": "Usage",
@ -978,50 +1153,85 @@
"SW5E.VersatileDamage": "Versatile Damage",
"SW5E.VsDC": "vs DC.",
"SW5E.WeaponAmmo": "Ammunition",
"SW5E.WeaponBlasterPistolProficiency": "Blaster Pistol",
"SW5E.WeaponChakramProficiency": "Chakrams",
"SW5E.WeaponDoubleBladeProficiency": "Doubleblade",
"SW5E.WeaponDoubleSaberProficiency": "Doublesaber",
"SW5E.WeaponDoubleShotoProficiency": "Doubleshoto",
"SW5E.WeaponDoubleSwordProficiency": "Doublesword",
"SW5E.WeaponHiddenBladeProficiency": "Hidden Blade",
"SW5E.WeaponImprov": "Improvised",
"SW5E.WeaponImprovisedProficiency": "Improvised Weapons",
"SW5E.WeaponLightFoilProficiency": "Lightfoil",
"SW5E.WeaponLightRingProficiency": "Light Ring",
"SW5E.WeaponMartialB": "Martial Blaster",
"SW5E.WeaponMartialBlasterProficiency": "Martial Blasters",
"SW5E.WeaponMartialLightweaponProficiency": "Martial Lightweapons",
"SW5E.WeaponMartialLW": "Martial Lightweapon",
"SW5E.WeaponMartialProficiency": "Martial Weapons",
"SW5E.WeaponMartialVibroweaponProficiency": "Martial Vibroweapons",
"SW5E.WeaponMartialVW": "Martial Vibroweapon",
"SW5E.WeaponNatural": "Natural",
"SW5E.WeaponNaturalProficiency": "Natural Weapons",
"SW5E.WeaponPrimarySW": "Primary (Starship)",
"SW5E.WeaponPropertiesAmm": "Ammunition",
"SW5E.WeaponPropertiesAut": "Auto",
"SW5E.WeaponPropertiesBur": "Burst",
"SW5E.WeaponPropertiesCon": "Constitution",
"SW5E.WeaponPropertiesDef": "Defensive",
"SW5E.WeaponPropertiesDex": "Dexterity Rqmt.",
"SW5E.WeaponPropertiesDex": "Dexterity Rqt",
"SW5E.WeaponPropertiesDgd": "Disguised",
"SW5E.WeaponPropertiesDir": "Dire",
"SW5E.WeaponPropertiesDis": "Disintegrate",
"SW5E.WeaponPropertiesDou": "Double",
"SW5E.WeaponPropertiesDpt": "Disruptive",
"SW5E.WeaponPropertiesDrm": "Disarming",
"SW5E.WeaponPropertiesExp": "Explosive",
"SW5E.WeaponPropertiesFin": "Finesse",
"SW5E.WeaponPropertiesFix": "Fixed",
"SW5E.WeaponPropertiesFoc": "Focus",
"SW5E.WeaponPropertiesHid": "Hidden",
"SW5E.WeaponPropertiesHom": "Homing",
"SW5E.WeaponPropertiesHvy": "Heavy",
"SW5E.WeaponPropertiesIon": "Ionizing",
"SW5E.WeaponPropertiesKen": "Keen",
"SW5E.WeaponPropertiesLgt": "Light",
"SW5E.WeaponPropertiesLum": "Luminous",
"SW5E.WeaponPropertiesMig": "Mighty",
"SW5E.WeaponPropertiesMlt": "Melt",
"SW5E.WeaponPropertiesOvr": "Overheat",
"SW5E.WeaponPropertiesPic": "Piercing",
"SW5E.WeaponPropertiesPow": "Power",
"SW5E.WeaponPropertiesRan": "Range",
"SW5E.WeaponPropertiesRap": "Rapid",
"SW5E.WeaponPropertiesRch": "Reach",
"SW5E.WeaponPropertiesRel": "Reload",
"SW5E.WeaponPropertiesRet": "Returning",
"SW5E.WeaponPropertiesSat": "Saturate",
"SW5E.WeaponPropertiesShk": "Shocking",
"SW5E.WeaponPropertiesSil": "Silent",
"SW5E.WeaponPropertiesSpc": "Special",
"SW5E.WeaponPropertiesStr": "Strength Rqmt.",
"SW5E.WeaponPropertiesStr": "Strength Rqt",
"SW5E.WeaponPropertiesThr": "Thrown",
"SW5E.WeaponPropertiesTwo": "Two-Handed",
"SW5E.WeaponPropertiesVer": "Versatile",
"SW5E.WeaponPropertiesVic": "Vicious",
"SW5E.WeaponPropertiesZon": "Zone",
"SW5E.WeaponQuaternarySW": "Quaternary (Starship)",
"SW5E.WeaponSaberWhipProficiency": "Saberwhip",
"SW5E.WeaponSecondarySW": "Secondary (Starship)",
"SW5E.WeaponSiege": "Siege",
"SW5E.WeaponSimpleB": "Simple Blaster",
"SW5E.WeaponSimpleBlasterProficiency": "Simple Blasters",
"SW5E.WeaponSimpleLightweaponProficiency": "Simple Lightweapons",
"SW5E.WeaponSimpleLW": "Simple Lightweapon",
"SW5E.WeaponSimpleProficiency": "Simple Weapons",
"SW5E.WeaponSimpleVibroweaponProficiency": "Simple Vibroweapons",
"SW5E.WeaponSimpleVW": "Simple Vibroweapon",
"SW5E.WeaponSizeAbb": "Size",
"SW5E.WeaponTechbladeProficiency": "Techblades",
"SW5E.WeaponTertiarySW": "Tertiary (Starship)",
"SW5E.WeaponVibrorapierProficiency": "Vibrorapier",
"SW5E.WeaponVibrowhipProficiency": "Vibrowhip",
"SW5E.Weight": "Weight"
}

1027
lang/it.json Normal file

File diff suppressed because it is too large Load diff

View file

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

View file

@ -10,9 +10,7 @@
.sw5e {
.window-content {
background: @sheetBackground;
font-size: 13px;
color: @colorDark;
}
/* ----------------------------------------- */
@ -44,6 +42,8 @@
select:disabled,
textarea:disabled {
color: @colorOlive;
border: 1px solid transparent !important;
outline: none !important;
&:hover,
&:focus {
box-shadow: none !important;
@ -58,28 +58,6 @@
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-group {
label {
@ -98,11 +76,12 @@
// Stacked Groups
.form-group.stacked {
label {
> label {
flex: 0 0 100%;
margin: 0;
}
label.checkbox {
label.checkbox,
label.radio {
flex: auto;
text-align: left;
}
@ -131,6 +110,34 @@
}
/* ----------------------------------------- */
/* 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 */
/* ----------------------------------------- */
@ -475,7 +482,7 @@
/* Trait Selector
/* ----------------------------------------- */
#trait-selector {
.trait-selector {
.trait-list {
list-style: none;
margin: 0;
@ -488,6 +495,59 @@
}
}
/* ----------------------------------------- */
/* 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
/* ----------------------------------------- */

View file

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

View file

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

View file

@ -30,5 +30,28 @@
.summary {
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,6 +140,7 @@
height: auto;
.russoOne(17px);
line-height: 24px;
width: 100%;
}
.proficiency {
@ -184,7 +185,7 @@
display: inline-block;
text-align: right;
padding: 0px 3px;
padding: 0 3px;
&:last-child {
text-align: left;
@ -779,7 +780,7 @@
display: block;
width: 100%;
text-align: right;
padding: 0px 3px;
padding: 0 3px;
&:last-child {
text-align: left;
}
@ -955,7 +956,7 @@
display: block;
width: 100%;
text-align: right;
padding: 0px 3px;
padding: 0 3px;
&:last-child {
text-align: left;
}
@ -1053,10 +1054,35 @@
h1.character-name {
align-self: auto;
}
.npc-size {
.npc-size, .creature-type {
.russoOne(18px);
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 {
grid-template-columns: repeat(3, 1fr);
footer {

View file

@ -408,6 +408,9 @@
&.npc {
.swalt-sheet {
header {
div.creature-type:hover {
border-color: @inputBorderFocus;
}
.experience {
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 {
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea, .roundTransition {
border-radius: 4px;
transition: all 0.3s;
&:hover {

View file

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

View file

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

File diff suppressed because it is too large Load diff

2040
module/actor/old_entity.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,10 @@
import Item5e from "../../../item/entity.js";
import TraitSelector from "../../../apps/trait-selector.js";
import ActorSheetFlags from "../../../apps/actor-flags.js";
import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js";
import {SW5E} from '../../../config.js';
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
@ -46,6 +48,14 @@ 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 */
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
@ -55,64 +65,76 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/** @override */
getData() {
getData(options) {
// Basic data
let isOwner = this.entity.owner;
let isOwner = this.actor.isOwner;
const data = {
owner: isOwner,
limited: this.entity.limited,
limited: this.actor.limited,
options: this.options,
editable: this.isEditable,
cssClass: isOwner ? "editable" : "locked",
isCharacter: this.entity.data.type === "character",
isNPC: this.entity.data.type === "npc",
isVehicle: this.entity.data.type === 'vehicle',
isCharacter: this.actor.type === "character",
isNPC: this.actor.type === "npc",
isStarship: this.actor.type === "starship",
isVehicle: this.actor.type === 'vehicle',
config: CONFIG.SW5E,
rollData: this.actor.getRollData.bind(this.actor)
};
// The Actor and its Items
data.actor = duplicate(this.actor.data);
data.items = this.actor.items.map(i => {
i.data.labels = i.labels;
return i.data;
});
// The Actor's data
const actorData = this.actor.data.toObject(false);
data.actor = actorData;
data.data = actorData.data;
// Owned Items
data.items = actorData.items;
for ( let i of data.items ) {
const item = this.actor.items.get(i._id);
i.labels = item.labels;
}
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
data.data = data.actor.data;
// Labels and filters
data.labels = this.actor.labels || {};
data.filters = this._filters;
// Ability Scores
for ( let [a, abl] of Object.entries(data.actor.data.abilities)) {
for ( let [a, abl] of Object.entries(actorData.data.abilities)) {
abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a];
}
// Skills
if (data.actor.data.skills) {
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
if (actorData.data.skills) {
for (let [s, skl] of Object.entries(actorData.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
if (data.actor.type === "starship") {
skl.label = CONFIG.SW5E.starshipSkills[s];
}else{
skl.label = CONFIG.SW5E.skills[s];
}
}
}
// Movement speeds
data.movement = this._getMovementSpeed(data.actor);
data.movement = this._getMovementSpeed(actorData);
// Senses
data.senses = this._getSenses(data.actor);
data.senses = this._getSenses(actorData);
// Update traits
this._prepareTraits(data.actor.data.traits);
this._prepareTraits(actorData.data.traits);
// Prepare owned items
this._prepareItems(data);
// Prepare active effects
data.effects = prepareActiveEffectCategories(this.entity.effects);
data.effects = prepareActiveEffectCategories(this.actor.effects);
// Return data to the sheet
return data
@ -220,10 +242,11 @@ export default class ActorSheet5e extends ActorSheet {
* Insert a power into the powerbook object when rendering the character sheet
* @param {Object} data The Actor data being prepared
* @param {Array} powers The power data being prepared
* @param {string} school The school of the powerbook being prepared
* @private
*/
_preparePowerbook(data, powers, school) {
const owner = this.actor.owner;
const owner = this.actor.isOwner;
const levels = data.data.powers;
const powerbook = {};
@ -338,6 +361,7 @@ export default class ActorSheet5e extends ActorSheet {
if ( filters.has("prepared") ) {
if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true;
if ( this.actor.data.type === "npc" ) return true;
if ( this.actor.data.type === "starship" ) return true;
return data.preparation.prepared;
}
@ -369,10 +393,7 @@ export default class ActorSheet5e extends ActorSheet {
/* Event Listeners and Handlers
/* -------------------------------------------- */
/**
* Activate event listeners using the prepared sheet HTML
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/
/** @inheritdoc */
activateListeners(html) {
// Activate Item Filters
@ -383,6 +404,9 @@ export default class ActorSheet5e extends ActorSheet {
// Item summaries
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
// View Item Sheets
html.find('.item-edit').click(this._onItemEdit.bind(this));
// Editable Only Listeners
if ( this.isEditable ) {
@ -405,17 +429,19 @@ export default class ActorSheet5e extends ActorSheet {
// Owned Item management
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-collapse').click(this._onItemCollapse.bind(this));
html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
html.find('.increment-class-level').click(this._onIncrementClassLevel.bind(this));
html.find('.decrement-class-level').click(this._onDecrementClassLevel.bind(this));
// Active Effect management
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.entity));
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor));
}
// Owner Only Listeners
if ( this.actor.owner ) {
if ( this.actor.isOwner ) {
// Ability Checks
html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
@ -482,17 +508,25 @@ export default class ActorSheet5e extends ActorSheet {
_onConfigMenu(event) {
event.preventDefault();
const button = event.currentTarget;
let app;
switch ( button.dataset.action ) {
case "hit-dice":
app = new ActorHitDiceConfig(this.object);
break;
case "movement":
new ActorMovementConfig(this.object).render(true);
app = new ActorMovementConfig(this.object);
break;
case "flags":
new ActorSheetFlags(this.object).render(true);
app = new ActorSheetFlags(this.object);
break;
case "senses":
new ActorSensesConfig(this.object).render(true);
app = new ActorSensesConfig(this.object);
break;
case "type":
new ActorTypeConfig(this.object).render(true);
break;
}
app?.render(true);
}
/* -------------------------------------------- */
@ -526,7 +560,7 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get('sw5e', 'allowPolymorphing'));
if ( !canPolymorph ) return false;
// Get the target actor
@ -601,15 +635,40 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
async _onDropItemCreate(itemData) {
// Check to make sure items of this type are allowed on this actor
if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) {
return ui.notifications.warn(game.i18n.format("SW5E.ActorWarningInvalidItem", {
itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
}));
}
// Create a Consumable power scroll on the Inventory tab
if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data;
}
// Ignore certain statuses
if ( itemData.data ) {
["attunement", "equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
// Ignore certain statuses
["equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
// Downgrade ATTUNED to REQUIRED
itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
}
// Stack identical consumables
if ( itemData.type === "consumable" && itemData.flags.core?.sourceId ) {
const similarItem = this.actor.items.find(i => {
const sourceId = i.getFlag("core", "sourceId");
return sourceId && (sourceId === itemData.flags.core?.sourceId) &&
(i.type === "consumable");
});
if ( similarItem && itemData.name !== "Power Cell" ) { // Always create a new powercell instead of increasing quantity
return similarItem.update({
'data.quantity': similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
});
}
}
// Create the owned item as normal
@ -650,7 +709,7 @@ export default class ActorSheet5e extends ActorSheet {
async _onUsesChange(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const item = this.actor.items.get(itemId);
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
event.target.value = uses;
return item.update({ 'data.uses.value': uses });
@ -665,7 +724,7 @@ export default class ActorSheet5e extends ActorSheet {
_onItemRoll(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const item = this.actor.items.get(itemId);
return item.roll();
}
@ -679,7 +738,7 @@ export default class ActorSheet5e extends ActorSheet {
_onItemRecharge(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const item = this.actor.items.get(itemId);
return item.rollRecharge();
};
@ -692,8 +751,8 @@ export default class ActorSheet5e extends ActorSheet {
_onItemSummary(event) {
event.preventDefault();
let li = $(event.currentTarget).parents(".item"),
item = this.actor.getOwnedItem(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.owner});
item = this.actor.items.get(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.isOwner});
// Toggle summary
if ( li.hasClass("expanded") ) {
@ -722,12 +781,12 @@ export default class ActorSheet5e extends ActorSheet {
const header = event.currentTarget;
const type = header.dataset.type;
const itemData = {
name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}),
type: type,
data: duplicate(header.dataset)
data: foundry.utils.deepClone(header.dataset)
};
delete itemData.data["type"];
return this.actor.createEmbeddedEntity("OwnedItem", itemData);
return this.actor.createEmbeddedDocuments("Item", [itemData]);
}
/* -------------------------------------------- */
@ -740,8 +799,8 @@ export default class ActorSheet5e extends ActorSheet {
_onItemEdit(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
const item = this.actor.getOwnedItem(li.dataset.itemId);
item.sheet.render(true);
const item = this.actor.items.get(li.dataset.itemId);
return item.sheet.render(true);
}
/* -------------------------------------------- */
@ -754,7 +813,77 @@ export default class ActorSheet5e extends ActorSheet {
_onItemDelete(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
this.actor.deleteOwnedItem(li.dataset.itemId);
const item = this.actor.items.get(li.dataset.itemId);
if ( item ) return item.delete();
}
/**
* Handle collapsing a Feature row on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onItemCollapse(event) {
event.preventDefault();
event.currentTarget.classList.toggle("active");
const li = event.currentTarget.closest("li");
const content = li.querySelector(".content");
if (content.style.display === "none") {
content.style.display = "block";
} else {
content.style.display = "none";
}
}
/**
* Handle incrementing class level on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onIncrementClassLevel(event) {
event.preventDefault();
const div = event.currentTarget.closest(".character")
const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1];
const itemId = li.dataset.itemId;
const actor = game.actors.get(actorId);
const item = actor.items.get(itemId);
let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: (levels + 1) }};
actor.updateEmbeddedDocuments("Item", [update]);
}
/**
* Handle decrementing class level on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onDecrementClassLevel(event) {
event.preventDefault();
const div = event.currentTarget.closest(".character")
const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1];
const itemId = li.dataset.itemId;
const actor = game.actors.get(actorId);
const item = actor.items.get(itemId);
let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: (levels - 1) }};
actor.updateEmbeddedDocuments("Item", [update]);
}
/* -------------------------------------------- */
@ -767,7 +896,7 @@ export default class ActorSheet5e extends ActorSheet {
_onRollAbilityTest(event) {
event.preventDefault();
let ability = event.currentTarget.parentElement.dataset.ability;
this.actor.rollAbility(ability, {event: event});
return this.actor.rollAbility(ability, {event: event});
}
/* -------------------------------------------- */
@ -780,7 +909,7 @@ export default class ActorSheet5e extends ActorSheet {
_onRollSkillCheck(event) {
event.preventDefault();
const skill = event.currentTarget.parentElement.dataset.skill;
this.actor.rollSkill(skill, {event: event});
return this.actor.rollSkill(skill, {event: event});
}
/* -------------------------------------------- */
@ -793,7 +922,7 @@ export default class ActorSheet5e extends ActorSheet {
_onToggleAbilityProficiency(event) {
event.preventDefault();
const field = event.currentTarget.previousElementSibling;
this.actor.update({[field.name]: 1 - parseInt(field.value)});
return this.actor.update({[field.name]: 1 - parseInt(field.value)});
}
/* -------------------------------------------- */
@ -810,7 +939,7 @@ export default class ActorSheet5e extends ActorSheet {
const filter = li.dataset.filter;
if ( set.has(filter) ) set.delete(filter);
else set.add(filter);
this.render();
return this.render();
}
/* -------------------------------------------- */
@ -826,7 +955,7 @@ export default class ActorSheet5e extends ActorSheet {
const label = a.parentElement.querySelector("label");
const choices = CONFIG.SW5E[a.dataset.options];
const options = { name: a.dataset.target, title: label.innerText, choices };
new TraitSelector(this.actor, options).render(true)
return new TraitSelector(this.actor, options).render(true)
}
/* -------------------------------------------- */
@ -834,15 +963,14 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
// Add button to revert polymorph
if ( !this.actor.isPolymorphed || this.actor.isToken ) return buttons;
if (this.actor.isPolymorphed) {
buttons.unshift({
label: 'SW5E.PolymorphRestoreTransformation',
class: "restore-transformation",
icon: "fas fa-backward",
onclick: ev => this.actor.revertOriginalForm()
onclick: () => this.actor.revertOriginalForm()
});
}
return buttons;
}
}

View file

@ -84,10 +84,10 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
};
// Partition items by category
let [items, forcepowers, techpowers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
let [items, forcepowers, techpowers, feats, classes, deployments, deploymentfeatures, ventures, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
// Item details
item.img = item.img || DEFAULT_TOKEN;
item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.attunement = {
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
@ -111,21 +111,27 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// Item toggle state
this._prepareItemToggleState(item);
// Primary Class
if ( item.type === "class" ) item.isOriginalClass = ( item._id === this.actor.data.data.details.originalClass );
// Classify items into types
if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[1].push(item);
else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[2].push(item);
else if ( item.type === "feat" ) arr[3].push(item);
else if ( item.type === "class" ) arr[4].push(item);
else if ( item.type === "species" ) arr[5].push(item);
else if ( item.type === "archetype" ) arr[6].push(item);
else if ( item.type === "classfeature" ) arr[7].push(item);
else if ( item.type === "background" ) arr[8].push(item);
else if ( item.type === "fightingstyle" ) arr[9].push(item);
else if ( item.type === "fightingmastery" ) arr[10].push(item);
else if ( item.type === "lightsaberform" ) arr[11].push(item);
else if ( item.type === "deployment" ) arr[5].push(item);
else if ( item.type === "deploymentfeature" ) arr[6].push(item);
else if ( item.type === "venture" ) arr[7].push(item);
else if ( item.type === "species" ) arr[8].push(item);
else if ( item.type === "archetype" ) arr[9].push(item);
else if ( item.type === "classfeature" ) arr[10].push(item);
else if ( item.type === "background" ) arr[11].push(item);
else if ( item.type === "fightingstyle" ) arr[12].push(item);
else if ( item.type === "fightingmastery" ) arr[13].push(item);
else if ( item.type === "lightsaberform" ) arr[14].push(item);
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
return arr;
}, [[], [], [], [], [], [], [], [], [], [], [], []]);
}, [[], [], [], [], [], [], [], [], [], [], [], [], [], [], []]);
// Apply active item filters
items = this._filterItems(items, this._filters.inventory);
@ -137,7 +143,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
for ( let i of items ) {
i.data.quantity = i.data.quantity || 0;
i.data.weight = i.data.weight || 0;
i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10;
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
inventory[i.type].items.push(i);
}
@ -150,6 +156,9 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true },
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
deployments: { label: "SW5E.ItemTypeDeploymentPl", items: [], hasActions: false, dataset: {type: "deployment"}, isDeployment: true },
deploymentfeatures: { label: "SW5E.ItemTypeDeploymentFeaturePl", items: [], hasActions: true, dataset: {type: "deploymentfeature"}, isDeploymentfeature: true },
ventures: { label: "SW5E.ItemTypeVenturePl", items: [], hasActions: false, dataset: {type: "venture"}, isVenture: true },
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true },
@ -162,10 +171,13 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
if ( f.data.activation.type ) features.active.items.push(f);
else features.passive.items.push(f);
}
classes.sort((a, b) => b.levels - a.levels);
classes.sort((a, b) => b.data.levels - a.data.levels);
features.classes.items = classes;
features.classfeatures.items = classfeatures;
features.archetype.items = archetypes;
features.deployments.items = deployments;
features.deploymentfeatures.items = deploymentfeatures;
features.ventures.items = ventures;
features.species.items = species;
features.background.items = backgrounds;
features.fightingstyles.items = fightingstyles;
@ -209,11 +221,11 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/**
* Activate event listeners using the prepared sheet HTML
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
*/
activateListeners(html) {
super.activateListeners(html);
if ( !this.options.editable ) return;
if ( !this.isEditable ) return;
// Inventory Functions
// html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
@ -231,11 +243,11 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// Send Languages to Chat onClick
html.find('[data-options="share-languages"]').click(event => {
event.preventDefault();
let langs = this.actor.data.data.traits.languages.value.map(l => SW5E.languages[l] || l).join(", ");
let langs = this.actor.data.data.traits.languages.value.map(l => CONFIG.SW5E.languages[l] || l).join(", ");
let custom = this.actor.data.data.traits.languages.custom;
if (custom) langs += ", " + custom.replace(/;/g, ",");
let content = `
<div class="sw5e chat-card item-card" data-acor-id="${this.actor._id}">
<div class="sw5e chat-card item-card" data-acor-id="${this.actor.data._id}">
<header class="card-header flexrow">
<img src="${this.actor.data.token.img}" title="" width="36" height="36" style="border: none;"/>
<h3>Known Languages</h3>
@ -245,21 +257,25 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
`;
// Send to Chat
let rollWhisper = null;
let rollBlind = false;
let rollMode = game.settings.get("core", "rollMode");
if (["gmroll", "blindroll"].includes(rollMode)) rollWhisper = ChatMessage.getWhisperIDs("GM");
if (rollMode === "blindroll") rollBlind = true;
ChatMessage.create({
user: game.user._id,
let data = {
user: game.user.data._id,
content: content,
blind: rollBlind,
speaker: {
actor: this.actor._id,
actor: this.actor.data._id,
token: this.actor.token,
alias: this.actor.name
},
type: CONST.CHAT_MESSAGE_TYPES.OTHER
});
};
if (["gmroll", "blindroll"].includes(rollMode)) data["whisper"] = ChatMessage.getWhisperRecipients("GM");
else if (rollMode === "selfroll") data["whisper"] = [game.users.get(game.user.data._id)];
ChatMessage.create(data);
});
// Item Delete Confirmation
@ -267,7 +283,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
html.find('.item-delete').click(event => {
let li = $(event.currentTarget).parents('.item');
let itemId = li.attr("data-item-id");
let item = this.actor.getOwnedItem(itemId);
let item = this.actor.items.get(itemId);
new Dialog({
title: `Deleting ${item.data.name}`,
content: `<p>Are you sure you want to delete ${item.data.name}?</p>`,
@ -318,7 +334,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
_onToggleItem(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const item = this.actor.items.get(itemId);
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
return item.update({[attr]: !getProperty(item.data, attr)});
}
@ -354,7 +370,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/** @override */
async _onDropItemCreate(itemData) {
// Increment the number of class levels a character instead of creating a new item
// Increment the number of class levels of a character instead of creating a new item
if ( itemData.type === "class" ) {
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
let priorLevel = cls?.data.data.levels ?? 0;
@ -367,8 +383,21 @@ 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
super._onDropItemCreate(itemData);
return super._onDropItemCreate(itemData);
}
}
async function addFavorites(app, html, data) {
@ -443,11 +472,11 @@ async function addFavorites(app, html, data) {
if (app.options.editable) {
let favBtn = $(`<a class="item-control item-toggle item-fav ${isFav ? "active" : ""}" data-fav="${isFav}" title="${isFav ? "Remove from Favourites" : "Add to Favourites"}"><i class="fas fa-star"></i></a>`);
favBtn.click(ev => {
app.actor.getOwnedItem(item._id).update({
app.actor.items.get(item.data._id).update({
"flags.favtab.isFavourite": !item.flags.favtab.isFavourite
});
});
html.find(`.item[data-item-id="${item._id}"]`).find('.item-controls').prepend(favBtn);
html.find(`.item[data-item-id="${item.data._id}"]`).find('.item-controls').prepend(favBtn);
}
if (isFav) {
@ -457,8 +486,8 @@ async function addFavorites(app, html, data) {
let v = (comps.vocal) ? "V" : "";
let s = (comps.somatic) ? "S" : "";
let m = (comps.material) ? "M" : "";
let c = (comps.concentration) ? true : false;
let r = (comps.ritual) ? true : false;
let c = !!(comps.concentration);
let r = !!(comps.ritual);
item.powerComps = `${v}${s}${m}`;
item.powerCon = c;
item.powerRit = r;
@ -521,12 +550,12 @@ async function addFavorites(app, html, data) {
//favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event));
favtabHtml.find('.item-edit').click(ev => {
let itemId = $(ev.target).parents('.item')[0].dataset.itemId;
app.actor.getOwnedItem(itemId).sheet.render(true);
app.actor.items.get(itemId).sheet.render(true);
});
favtabHtml.find('.item-fav').click(ev => {
let itemId = $(ev.target).parents('.item')[0].dataset.itemId;
let val = !app.actor.getOwnedItem(itemId).data.flags.favtab.isFavourite
app.actor.getOwnedItem(itemId).update({
let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite
app.actor.items.get(itemId).update({
"flags.favtab.isFavourite": val
});
});
@ -542,10 +571,10 @@ async function addFavorites(app, html, data) {
let list = null;
if (dropData.data.type === 'feat') list = favFeats;
else list = favItems;
let dragSource = list.find(i => i._id === dropData.data._id);
let siblings = list.filter(i => i._id !== dropData.data._id);
let dragSource = list.find(i => i.data._id === dropData.data._id);
let siblings = list.filter(i => i.data._id !== dropData.data._id);
let targetId = ev.target.closest('.item').dataset.itemId;
let dragTarget = siblings.find(s => s._id === targetId);
let dragTarget = siblings.find(s => s.data._id === targetId);
if (dragTarget === undefined) return;
const sortUpdates = SortingHelpers.performIntegerSort(dragSource, {
@ -555,7 +584,7 @@ async function addFavorites(app, html, data) {
});
const updateData = sortUpdates.map(u => {
const update = u.update;
update._id = u.target._id;
update._id = u.target.data._id;
return update;
});
app.actor.updateEmbeddedEntity("OwnedItem", updateData);
@ -614,11 +643,7 @@ async function addSubTabs(app, html, data) {
return tab.target == target
});
data.options.subTabs[subgroup].map(el => {
if(el.target == target) {
el.active = true;
} else {
el.active = false;
}
el.active = el.target == target;
return el;
})

View file

@ -27,6 +27,11 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
/* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/**
* Organize Owned Items for rendering the NPC sheet
* @private
@ -43,7 +48,7 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
// Start by classifying items into groups for rendering
let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
item.img = item.img || DEFAULT_TOKEN;
item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.hasUses = item.data.uses && (item.data.uses.max > 0);
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
@ -80,17 +85,19 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
data.techPowerbook = techPowerbook;
}
/* -------------------------------------------- */
/** @override */
getData() {
const data = super.getData();
/** @inheritdoc */
getData(options) {
const data = super.getData(options);
// Challenge Rating
const cr = parseFloat(data.data.details.cr || 0);
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
// Creature Type
data.labels["type"] = this.actor.labels.creatureType;
return data;
}
@ -99,7 +106,7 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
/* -------------------------------------------- */
/** @override */
_updateObject(event, formData) {
async _updateObject(event, formData) {
// Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
@ -109,7 +116,7 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps
super._updateObject(event, formData);
return super._updateObject(event, formData);
}
/* -------------------------------------------- */

View file

@ -0,0 +1,155 @@
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,6 +20,12 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/**
* Creates a new cargo entry for a vehicle Actor.
*/
@ -206,24 +212,39 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
}
};
// Classify items owned by the vehicle and compute total cargo weight
let totalWeight = 0;
for (const item of data.items) {
this._prepareCrewedItem(item);
if (item.type === 'weapon') features.weapons.items.push(item);
else if (item.type === 'equipment') features.equipment.items.push(item);
else if (item.type === 'loot') {
// Handle cargo explicitly
const isCargo = item.flags.sw5e?.vehicleCargo === true;
if ( isCargo ) {
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
continue;
}
// Handle non-cargo item types
switch ( item.type ) {
case "weapon":
features.weapons.items.push(item);
break;
case "equipment":
features.equipment.items.push(item);
break;
case "feat":
if ( !item.data.activation.type || (item.data.activation.type === "none") ) features.passive.items.push(item);
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
else features.actions.items.push(item);
break;
default:
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
}
else if (item.type === 'feat') {
if (!item.data.activation.type || item.data.activation.type === 'none') {
features.passive.items.push(item);
}
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
else features.actions.items.push(item);
}
}
// Update the rendering context data
data.features = Object.values(features);
data.cargo = Object.values(cargo);
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
@ -236,7 +257,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/** @override */
activateListeners(html) {
super.activateListeners(html);
if (!this.options.editable) return;
if (!this.isEditable) return;
html.find('.item-toggle').click(this._onToggleItem.bind(this));
html.find('.item-hp input')
@ -272,7 +293,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
const property = row.classList.contains('crew') ? 'crew' : 'passengers';
// Get the cargo entry
const cargo = duplicate(this.actor.data.data.cargo[property]);
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
const entry = cargo[idx];
if (!entry) return null;
@ -322,7 +343,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
const target = event.currentTarget;
const type = target.dataset.type;
if (type === 'crew' || type === 'passengers') {
const cargo = duplicate(this.actor.data.data.cargo[type]);
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
@ -343,7 +364,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
if (row.classList.contains('cargo-row')) {
const idx = Number(row.dataset.itemId);
const type = row.classList.contains('crew') ? 'crew' : 'passengers';
const cargo = duplicate(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
@ -352,6 +373,16 @@ 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.
* @param event {Event}

View file

@ -1,8 +1,10 @@
import Item5e from "../../../item/entity.js";
import TraitSelector from "../../../apps/trait-selector.js";
import ActorSheetFlags from "../../../apps/actor-flags.js";
import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js";
import {SW5E} from '../../../config.js';
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
@ -44,6 +46,15 @@ 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 */
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html";
@ -53,43 +64,51 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/** @override */
getData() {
getData(options) {
// Basic data
let isOwner = this.entity.owner;
let isOwner = this.actor.isOwner;
const data = {
owner: isOwner,
limited: this.entity.limited,
limited: this.actor.limited,
options: this.options,
editable: this.isEditable,
cssClass: isOwner ? "editable" : "locked",
isCharacter: this.entity.data.type === "character",
isNPC: this.entity.data.type === "npc",
isVehicle: this.entity.data.type === 'vehicle',
isCharacter: this.actor.type === "character",
isNPC: this.actor.type === "npc",
isStarship: this.actor.type === "starship",
isVehicle: this.actor.type === 'vehicle',
config: CONFIG.SW5E,
rollData: this.actor.getRollData.bind(this.actor)
};
// The Actor and its Items
data.actor = duplicate(this.actor.data);
data.items = this.actor.items.map(i => {
i.data.labels = i.labels;
return i.data;
});
// The Actor's data
const actorData = this.actor.data.toObject(false);
data.actor = actorData;
data.data = actorData.data;
// Owned Items
data.items = actorData.items;
for ( let i of data.items ) {
const item = this.actor.items.get(i._id);
i.labels = item.labels;
}
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
data.data = data.actor.data;
// Labels and filters
data.labels = this.actor.labels || {};
data.filters = this._filters;
// Ability Scores
for ( let [a, abl] of Object.entries(data.actor.data.abilities)) {
for ( let [a, abl] of Object.entries(actorData.data.abilities)) {
abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a];
}
// Skills
if (data.actor.data.skills) {
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
if (actorData.data.skills) {
for ( let [s, skl] of Object.entries(actorData.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
@ -98,19 +117,19 @@ export default class ActorSheet5e extends ActorSheet {
}
// Movement speeds
data.movement = this._getMovementSpeed(data.actor);
data.movement = this._getMovementSpeed(actorData);
// Senses
data.senses = this._getSenses(data.actor);
data.senses = this._getSenses(actorData);
// Update traits
this._prepareTraits(data.actor.data.traits);
this._prepareTraits(actorData.data.traits);
// Prepare owned items
this._prepareItems(data);
// Prepare active effects
data.effects = prepareActiveEffectCategories(this.entity.effects);
data.effects = prepareActiveEffectCategories(this.actor.effects);
// Return data to the sheet
return data
@ -221,7 +240,7 @@ export default class ActorSheet5e extends ActorSheet {
* @private
*/
_preparePowerbook(data, powers) {
const owner = this.actor.owner;
const owner = this.actor.isOwner;
const levels = data.data.powers;
const powerbook = {};
@ -274,11 +293,14 @@ export default class ActorSheet5e extends ActorSheet {
}
// Pact magic users have cantrips and a pact magic section
// TODO: Check if this is needed, we've removed pacts everywhere else
if ( levels.pact && levels.pact.max ) {
if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
const l = levels.pact;
const config = CONFIG.SW5E.powerPreparationModes.pact;
registerSection("pact", sections.pact, config, {
const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`);
const label = `${config}${level}`;
registerSection("pact", sections.pact, label, {
prepMode: "pact",
value: l.value,
max: l.max,
@ -381,10 +403,7 @@ export default class ActorSheet5e extends ActorSheet {
/* Event Listeners and Handlers
/* -------------------------------------------- */
/**
* Activate event listeners using the prepared sheet HTML
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/
/** @inheritdoc */
activateListeners(html) {
// Activate Item Filters
@ -395,6 +414,9 @@ export default class ActorSheet5e extends ActorSheet {
// Item summaries
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
// View Item Sheets
html.find('.item-edit').click(this._onItemEdit.bind(this));
// Editable Only Listeners
if ( this.isEditable ) {
@ -417,17 +439,16 @@ export default class ActorSheet5e extends ActorSheet {
// Owned Item management
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-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
// Active Effect management
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.entity));
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor));
}
// Owner Only Listeners
if ( this.actor.owner ) {
if ( this.actor.isOwner ) {
// Ability Checks
html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
@ -494,17 +515,25 @@ export default class ActorSheet5e extends ActorSheet {
_onConfigMenu(event) {
event.preventDefault();
const button = event.currentTarget;
let app;
switch ( button.dataset.action ) {
case "hit-dice":
app = new ActorHitDiceConfig(this.object);
break;
case "movement":
new ActorMovementConfig(this.object).render(true);
app = new ActorMovementConfig(this.object);
break;
case "flags":
new ActorSheetFlags(this.object).render(true);
app = new ActorSheetFlags(this.object);
break;
case "senses":
new ActorSensesConfig(this.object).render(true);
app = new ActorSensesConfig(this.object);
break;
case "type":
new ActorTypeConfig(this.object).render(true);
break;
}
app?.render(true);
}
/* -------------------------------------------- */
@ -538,7 +567,7 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get('sw5e', 'allowPolymorphing'));
if ( !canPolymorph ) return false;
// Get the target actor
@ -613,15 +642,41 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
async _onDropItemCreate(itemData) {
// Check to make sure items of this type are allowed on this actor
if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) {
return ui.notifications.warn(game.i18n.format("SW5E.ActorWarningInvalidItem", {
itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
}));
}
// Create a Consumable power scroll on the Inventory tab
// TODO: This is pretty non functional as the base items for the scrolls, and the powers, are not defined, maybe consider using holocrons
if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data;
}
// Ignore certain statuses
if ( itemData.data ) {
["attunement", "equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
// Ignore certain statuses
["equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
// Downgrade ATTUNED to REQUIRED
itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
}
// Stack identical consumables
if ( itemData.type === "consumable" && itemData.flags.core?.sourceId ) {
const similarItem = this.actor.items.find(i => {
const sourceId = i.getFlag("core", "sourceId");
return sourceId && (sourceId === itemData.flags.core?.sourceId) &&
(i.type === "consumable");
});
if ( similarItem ) {
return similarItem.update({
'data.quantity': similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
});
}
}
// Create the owned item as normal
@ -662,7 +717,7 @@ export default class ActorSheet5e extends ActorSheet {
async _onUsesChange(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const item = this.actor.items.get(itemId);
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
event.target.value = uses;
return item.update({ 'data.uses.value': uses });
@ -677,7 +732,7 @@ export default class ActorSheet5e extends ActorSheet {
_onItemRoll(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const item = this.actor.items.get(itemId);
return item.roll();
}
@ -691,7 +746,7 @@ export default class ActorSheet5e extends ActorSheet {
_onItemRecharge(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const item = this.actor.items.get(itemId);
return item.rollRecharge();
};
@ -704,8 +759,8 @@ export default class ActorSheet5e extends ActorSheet {
_onItemSummary(event) {
event.preventDefault();
let li = $(event.currentTarget).parents(".item"),
item = this.actor.getOwnedItem(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.owner});
item = this.actor.items.get(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.isOwner});
// Toggle summary
if ( li.hasClass("expanded") ) {
@ -734,12 +789,12 @@ export default class ActorSheet5e extends ActorSheet {
const header = event.currentTarget;
const type = header.dataset.type;
const itemData = {
name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}),
type: type,
data: duplicate(header.dataset)
data: foundry.utils.deepClone(header.dataset)
};
delete itemData.data["type"];
return this.actor.createEmbeddedEntity("OwnedItem", itemData);
return this.actor.createEmbeddedDocuments("Item", [itemData]);
}
/* -------------------------------------------- */
@ -752,8 +807,8 @@ export default class ActorSheet5e extends ActorSheet {
_onItemEdit(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
const item = this.actor.getOwnedItem(li.dataset.itemId);
item.sheet.render(true);
const item = this.actor.items.get(li.dataset.itemId);
return item.sheet.render(true);
}
/* -------------------------------------------- */
@ -766,7 +821,8 @@ export default class ActorSheet5e extends ActorSheet {
_onItemDelete(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
this.actor.deleteOwnedItem(li.dataset.itemId);
const item = this.actor.items.get(li.dataset.itemId);
if ( item ) return item.delete();
}
/* -------------------------------------------- */
@ -779,7 +835,7 @@ export default class ActorSheet5e extends ActorSheet {
_onRollAbilityTest(event) {
event.preventDefault();
let ability = event.currentTarget.parentElement.dataset.ability;
this.actor.rollAbility(ability, {event: event});
return this.actor.rollAbility(ability, {event: event});
}
/* -------------------------------------------- */
@ -792,7 +848,7 @@ export default class ActorSheet5e extends ActorSheet {
_onRollSkillCheck(event) {
event.preventDefault();
const skill = event.currentTarget.parentElement.dataset.skill;
this.actor.rollSkill(skill, {event: event});
return this.actor.rollSkill(skill, {event: event});
}
/* -------------------------------------------- */
@ -805,7 +861,7 @@ export default class ActorSheet5e extends ActorSheet {
_onToggleAbilityProficiency(event) {
event.preventDefault();
const field = event.currentTarget.previousElementSibling;
this.actor.update({[field.name]: 1 - parseInt(field.value)});
return this.actor.update({[field.name]: 1 - parseInt(field.value)});
}
/* -------------------------------------------- */
@ -822,7 +878,7 @@ export default class ActorSheet5e extends ActorSheet {
const filter = li.dataset.filter;
if ( set.has(filter) ) set.delete(filter);
else set.add(filter);
this.render();
return this.render();
}
/* -------------------------------------------- */
@ -838,7 +894,7 @@ export default class ActorSheet5e extends ActorSheet {
const label = a.parentElement.querySelector("label");
const choices = CONFIG.SW5E[a.dataset.options];
const options = { name: a.dataset.target, title: label.innerText, choices };
new TraitSelector(this.actor, options).render(true)
return new TraitSelector(this.actor, options).render(true)
}
/* -------------------------------------------- */
@ -846,15 +902,14 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
// Add button to revert polymorph
if ( !this.actor.isPolymorphed || this.actor.isToken ) return buttons;
if ( this.actor.isPolymorphed ) {
buttons.unshift({
label: 'SW5E.PolymorphRestoreTransformation',
class: "restore-transformation",
icon: "fas fa-backward",
onclick: ev => this.actor.revertOriginalForm()
onclick: () => this.actor.revertOriginalForm()
});
}
return buttons;
}
}

View file

@ -76,7 +76,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
// Item details
item.img = item.img || DEFAULT_TOKEN;
item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.attunement = {
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
@ -100,6 +100,9 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// Item toggle state
this._prepareItemToggleState(item);
// Primary Class
if ( item.type === "class" ) item.isOriginalClass = ( item._id === this.actor.data.data.details.originalClass );
// Classify items into types
if ( item.type === "power" ) arr[1].push(item);
else if ( item.type === "feat" ) arr[2].push(item);
@ -151,7 +154,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
if ( f.data.activation.type ) features.active.items.push(f);
else features.passive.items.push(f);
}
classes.sort((a, b) => b.levels - a.levels);
classes.sort((a, b) => b.data.levels - a.data.levels);
features.classes.items = classes;
features.classfeatures.items = classfeatures;
features.archetype.items = archetypes;
@ -198,11 +201,11 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/**
* Activate event listeners using the prepared sheet HTML
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
*/
activateListeners(html) {
super.activateListeners(html);
if ( !this.options.editable ) return;
if ( !this.isEditable ) return;
// Item State Toggling
html.find('.item-toggle').click(this._onToggleItem.bind(this));
@ -243,7 +246,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
_onToggleItem(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const item = this.actor.items.get(itemId);
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
return item.update({[attr]: !getProperty(item.data, attr)});
}
@ -293,6 +296,6 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
}
// Default drop handling if levels were not added
super._onDropItemCreate(itemData);
return super._onDropItemCreate(itemData);
}
}

View file

@ -18,6 +18,11 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
/* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/**
* Organize Owned Items for rendering the NPC sheet
* @private
@ -34,7 +39,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
// Start by classifying items into groups for rendering
let [powers, other] = data.items.reduce((arr, item) => {
item.img = item.img || DEFAULT_TOKEN;
item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.hasUses = item.data.uses && (item.data.uses.max > 0);
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
@ -70,14 +75,17 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
/* -------------------------------------------- */
/** @override */
getData() {
const data = super.getData();
/** @inheritdoc */
getData(options) {
const data = super.getData(options);
// Challenge Rating
const cr = parseFloat(data.data.details.cr || 0);
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
// Creature Type
data.labels["type"] = this.actor.labels.creatureType;
return data;
}
@ -86,7 +94,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
/* -------------------------------------------- */
/** @override */
_updateObject(event, formData) {
async _updateObject(event, formData) {
// Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
@ -96,7 +104,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps
super._updateObject(event, formData);
return super._updateObject(event, formData);
}
/* -------------------------------------------- */

View file

@ -20,6 +20,12 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/**
* Creates a new cargo entry for a vehicle Actor.
*/
@ -206,24 +212,39 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
}
};
// Classify items owned by the vehicle and compute total cargo weight
let totalWeight = 0;
for (const item of data.items) {
this._prepareCrewedItem(item);
if (item.type === 'weapon') features.weapons.items.push(item);
else if (item.type === 'equipment') features.equipment.items.push(item);
else if (item.type === 'loot') {
// Handle cargo explicitly
const isCargo = item.flags.sw5e?.vehicleCargo === true;
if ( isCargo ) {
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
continue;
}
// Handle non-cargo item types
switch ( item.type ) {
case "weapon":
features.weapons.items.push(item);
break;
case "equipment":
features.equipment.items.push(item);
break;
case "feat":
if (!item.data.activation.type || (item.data.activation.type === "none")) features.passive.items.push(item);
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
else features.actions.items.push(item);
break;
default:
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
}
else if (item.type === 'feat') {
if (!item.data.activation.type || item.data.activation.type === 'none') {
features.passive.items.push(item);
}
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
else features.actions.items.push(item);
}
}
// Update the rendering context data
data.features = Object.values(features);
data.cargo = Object.values(cargo);
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
@ -236,7 +257,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/** @override */
activateListeners(html) {
super.activateListeners(html);
if (!this.options.editable) return;
if (!this.isEditable) return;
html.find('.item-toggle').click(this._onToggleItem.bind(this));
html.find('.item-hp input')
@ -272,7 +293,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
const property = row.classList.contains('crew') ? 'crew' : 'passengers';
// Get the cargo entry
const cargo = duplicate(this.actor.data.data.cargo[property]);
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
const entry = cargo[idx];
if (!entry) return null;
@ -322,7 +343,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
const target = event.currentTarget;
const type = target.dataset.type;
if (type === 'crew' || type === 'passengers') {
const cargo = duplicate(this.actor.data.data.cargo[type]);
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
@ -343,7 +364,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
if (row.classList.contains('cargo-row')) {
const idx = Number(row.dataset.itemId);
const type = row.classList.contains('crew') ? 'crew' : 'passengers';
const cargo = duplicate(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
@ -352,6 +373,16 @@ 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.
* @param event {Event}

View file

@ -39,12 +39,12 @@ export default class AbilityUseDialog extends Dialog {
// Prepare dialog form data
const data = {
item: item.data,
title: game.i18n.format("SW5E.AbilityUseHint", item.data),
title: game.i18n.format("SW5E.AbilityUseHint", {type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), name: item.name}),
note: this._getAbilityUseNote(item.data, uses, recharge),
consumePowerSlot: false,
consumeRecharge: recharges,
consumeResource: !!itemData.consume.target,
consumeUses: uses.max,
consumeUses: uses.per && (uses.max > 0),
canUse: recharges ? recharge.charged : sufficientUses,
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
errors: []
@ -59,7 +59,7 @@ export default class AbilityUseDialog extends Dialog {
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
return new Promise((resolve) => {
const dlg = new this(item, {
title: `${item.name}: Usage Configuration`,
title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`,
content: html,
buttons: {
use: {
@ -169,7 +169,7 @@ export default class AbilityUseDialog extends Dialog {
}));
// Merge power casting data
return mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
return foundry.utils.mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
}
/* -------------------------------------------- */
@ -187,7 +187,7 @@ export default class AbilityUseDialog extends Dialog {
// Abilities which use Recharge
if ( !!recharge.value ) {
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
type: item.type,
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
})
}
@ -201,7 +201,7 @@ export default class AbilityUseDialog extends Dialog {
else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint";
else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint";
return game.i18n.format(str, {
type: item.data.consumableType,
type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`),
value: uses.value,
quantity: item.data.quantity,
max: uses.max,
@ -212,17 +212,11 @@ export default class AbilityUseDialog extends Dialog {
// Other Items
else {
return game.i18n.format("SW5E.AbilityUseNormalHint", {
type: item.type,
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
value: uses.value,
max: uses.max,
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
});
}
}
/* -------------------------------------------- */
static _handleSubmit(formData, item) {
}
}

View file

@ -1,11 +1,10 @@
/**
* An application class which provides advanced configuration for special character flags which modify an Actor
* @implements {BaseEntitySheet}
* @implements {DocumentSheet}
*/
export default class ActorSheetFlags extends BaseEntitySheet {
export default class ActorSheetFlags extends DocumentSheet {
static get defaultOptions() {
const options = super.defaultOptions;
return mergeObject(options, {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "actor-flags",
classes: ["sw5e"],
template: "systems/sw5e/templates/apps/actor-flags.html",
@ -27,6 +26,7 @@ export default class ActorSheetFlags extends BaseEntitySheet {
getData() {
const data = {};
data.actor = this.object;
data.classes = this._getClasses();
data.flags = this._getFlags();
data.bonuses = this._getBonuses();
return data;
@ -34,17 +34,33 @@ export default class ActorSheetFlags extends BaseEntitySheet {
/* -------------------------------------------- */
/**
* Prepare an object of sorted classes.
* @return {object}
* @private
*/
_getClasses() {
const classes = this.object.items.filter(i => i.type === "class");
return classes.sort((a, b) => a.name.localeCompare(b.name)).reduce((obj, i) => {
obj[i.id] = i.name;
return obj;
}, {});
}
/* -------------------------------------------- */
/**
* Prepare an object of flags data which groups flags by section
* Add some additional data for rendering
* @return {object}
* @private
*/
_getFlags() {
const flags = {};
const baseData = this.entity._data;
const baseData = this.document.toJSON();
for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) {
if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {};
let flag = duplicate(v);
let flag = foundry.utils.deepClone(v);
flag.type = v.type.name;
flag.isCheckbox = v.type === Boolean;
flag.isSelect = v.hasOwnProperty('choices');

110
module/apps/actor-type.js Normal file
View file

@ -0,0 +1,110 @@
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

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

View file

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

View file

@ -0,0 +1,68 @@
/**
* 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,12 +1,12 @@
/**
* A simple form to set actor movement speeds
* @implements {BaseEntitySheet}
* A simple form to set Actor movement speeds.
* @extends {DocumentSheet}
*/
export default class ActorSensesConfig extends BaseEntitySheet {
export default class ActorSensesConfig extends DocumentSheet {
/** @override */
/** @inheritdoc */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e"],
template: "systems/sw5e/templates/apps/senses-config.html",
width: 300,
@ -16,16 +16,16 @@ export default class ActorSensesConfig extends BaseEntitySheet {
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
get title() {
return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.entity.name}`;
return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`;
}
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
getData(options) {
const senses = this.entity._data.data.attributes?.senses ?? {};
const senses = foundry.utils.getProperty(this.document.data._source, "data.attributes.senses") || {};
const data = {
senses: {},
special: senses.special ?? "",

View file

@ -40,7 +40,7 @@ export default class ShortRestDialog extends Dialog {
// Determine Hit Dice
data.availableHD = this.actor.data.items.reduce((hd, item) => {
if ( item.type === "class" ) {
const d = item.data;
const d = item.data.data;
const denom = d.hitDice || "d6";
const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
hd[denom] = denom in hd ? hd[denom] + available : available;
@ -93,11 +93,11 @@ export default class ShortRestDialog extends Dialog {
static async shortRestDialog({actor}={}) {
return new Promise((resolve, reject) => {
const dlg = new this(actor, {
title: "Short Rest",
title: game.i18n.localize("SW5E.ShortRest"),
buttons: {
rest: {
icon: '<i class="fas fa-bed"></i>',
label: "Rest",
label: game.i18n.localize("SW5E.Rest"),
callback: html => {
let newDay = false;
if (game.settings.get("sw5e", "restVariant") === "gritty")
@ -107,7 +107,7 @@ export default class ShortRestDialog extends Dialog {
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel",
label: game.i18n.localize("Cancel"),
callback: reject
}
},

View file

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

View file

@ -36,19 +36,3 @@ export const measureDistances = function(segments, options={}) {
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

@ -1,52 +1,113 @@
export default class CharacterImporter {
// transform JSON from sw5e.com to Foundry friendly format
// and insert new actor
static async transform(rawCharacter) {
const sourceCharacter = JSON.parse(rawCharacter); //source character
const details = {
species: sourceCharacter.attribs.find(e => e.name == "race").current,
background: sourceCharacter.attribs.find(e => e.name == "background").current,
alignment: sourceCharacter.attribs.find(e => e.name == "alignment").current
}
const hp = {
value: sourceCharacter.attribs.find(e => e.name == "hp").current,
min: 0,
max: sourceCharacter.attribs.find(e => e.name == "hp").current,
temp: sourceCharacter.attribs.find(e => e.name == "hp_temp").current
species: sourceCharacter.attribs.find((e) => e.name == "race").current,
background: sourceCharacter.attribs.find((e) => e.name == "background").current,
alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current
};
const ac = {
value: sourceCharacter.attribs.find(e => e.name == "ac").current
const hp = {
value: sourceCharacter.attribs.find((e) => e.name == "hp").current,
min: 0,
max: sourceCharacter.attribs.find((e) => e.name == "hp").current,
temp: sourceCharacter.attribs.find((e) => e.name == "hp_temp").current
};
const abilities = {
str: {
value: sourceCharacter.attribs.find(e => e.name == "strength").current,
proficient: sourceCharacter.attribs.find(e => e.name == 'strength_save_prof').current ? 1 : 0
value: sourceCharacter.attribs.find((e) => e.name == "strength").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "strength_save_prof").current ? 1 : 0
},
dex: {
value: sourceCharacter.attribs.find(e => e.name == "dexterity").current,
proficient: sourceCharacter.attribs.find(e => e.name == 'dexterity_save_prof').current ? 1 : 0
value: sourceCharacter.attribs.find((e) => e.name == "dexterity").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "dexterity_save_prof").current ? 1 : 0
},
con: {
value: sourceCharacter.attribs.find(e => e.name == "constitution").current,
proficient: sourceCharacter.attribs.find(e => e.name == 'constitution_save_prof').current ? 1 : 0
value: sourceCharacter.attribs.find((e) => e.name == "constitution").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "constitution_save_prof").current ? 1 : 0
},
int: {
value: sourceCharacter.attribs.find(e => e.name == "intelligence").current,
proficient: sourceCharacter.attribs.find(e => e.name == 'intelligence_save_prof').current ? 1 : 0
value: sourceCharacter.attribs.find((e) => e.name == "intelligence").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "intelligence_save_prof").current ? 1 : 0
},
wis: {
value: sourceCharacter.attribs.find(e => e.name == "wisdom").current,
proficient: sourceCharacter.attribs.find(e => e.name == 'wisdom_save_prof').current ? 1 : 0
value: sourceCharacter.attribs.find((e) => e.name == "wisdom").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "wisdom_save_prof").current ? 1 : 0
},
cha: {
value: sourceCharacter.attribs.find(e => e.name == "charisma").current,
proficient: sourceCharacter.attribs.find(e => e.name == 'charisma_save_prof').current ? 1 : 0
value: sourceCharacter.attribs.find((e) => e.name == "charisma").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "charisma_save_prof").current ? 1 : 0
}
};
/* ----------------------------------------------------------------- */
/* character.data.skills.<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
}
};
const targetCharacter = {
@ -55,66 +116,208 @@ export default class CharacterImporter {
data: {
abilities: abilities,
details: details,
skills: skills,
attributes: {
ac: ac,
hp: hp
}
}
};
let actor = await Actor.create(targetCharacter);
CharacterImporter.addProfessions(sourceCharacter, actor);
}
const profession = sourceCharacter.attribs.find(e => e.name == "class").current;
let professionLevel = sourceCharacter.attribs.find(e => e.name == "class_display").current;
professionLevel = parseInt( professionLevel.replace(/[^0-9]/g,'') ); //remove a-z, leaving only integers
CharacterImporter.addClasses(profession, professionLevel, actor);
// Parse all classes and add them to already created actor.
// "class" is a reserved word, therefore I use profession where I can.
static async addProfessions(sourceCharacter, actor) {
let result = [];
// parse all class and multiclassX items
// couldn't get Array.filter to work here for some reason
// result = array of objects. each object is a separate class
sourceCharacter.attribs.forEach((e) => {
if (CharacterImporter.classOrMulticlass(e.name)) {
var t = {
profession: CharacterImporter.capitalize(e.current),
type: CharacterImporter.baseOrMulti(e.name),
level: CharacterImporter.getLevel(e, sourceCharacter)
};
result.push(t);
}
});
// pull classes directly from system compendium and add them to current actor
const professionsPack = await game.packs.get("sw5e.classes").getContent();
result.forEach((prof) => {
let assignedProfession = professionsPack.find((o) => o.name === prof.profession);
assignedProfession.data.data.levels = prof.level;
actor.createEmbeddedEntity("OwnedItem", assignedProfession.data, { displaySheet: false });
});
this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor);
this.addPowers(
sourceCharacter.attribs.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1).map((e) => e.current),
actor
);
const discoveredItems = sourceCharacter.attribs.filter(
(e) => e.name.search(/repeating_inventory.+_itemname/g) != -1
);
const items = discoveredItems.map((item) => {
const id = item.name.match(/-\w{19}/g);
return {
name: item.current,
quantity: sourceCharacter.attribs.find((e) => e.name === `repeating_inventory_${id}_itemcount`).current
};
});
this.addItems(items, actor);
}
static async addClasses(profession, level, actor) {
let classes = await game.packs.get('sw5e.classes').getContent();
let assignedClass = classes.find( c => c.name === profession );
let classes = await game.packs.get("sw5e.classes").getContent();
let assignedClass = classes.find((c) => c.name === profession);
assignedClass.data.data.levels = level;
await actor.createEmbeddedEntity("OwnedItem", assignedClass.data, { displaySheet: false });
}
static addImportButton(html){
const header = $("#actors").find("header.directory-header");
const search = $("#actors").children().find("div.header-search");
const newImportButtonDiv = $("#actors").children().find("div.header-actions").clone();
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);
static classOrMulticlass(name) {
return name === "class" || (name.includes("multiclass") && name.length <= 12);
}
let characterImportButton = $("#cs-import-button");
characterImportButton.click(ev => {
let content = '<h1>Saved Character JSON Import</h1> '
+ '<label for="character-json">Paste character JSON here:</label> '
+ '</br>'
+ '<textarea id="character-json" name="character-json" rows="10" cols="50"></textarea>';
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").getContent();
const assignedSpecies = species.find((c) => c.name === race);
const activeEffects = assignedSpecies.data.effects[0].changes;
const actorData = { data: { abilities: { ...actor.data.data.abilities } } };
activeEffects.map((effect) => {
switch (effect.key) {
case "data.abilities.str.value":
actorData.data.abilities.str.value -= effect.value;
break;
case "data.abilities.dex.value":
actorData.data.abilities.dex.value -= effect.value;
break;
case "data.abilities.con.value":
actorData.data.abilities.con.value -= effect.value;
break;
case "data.abilities.int.value":
actorData.data.abilities.int.value -= effect.value;
break;
case "data.abilities.wis.value":
actorData.data.abilities.wis.value -= effect.value;
break;
case "data.abilities.cha.value":
actorData.data.abilities.cha.value -= effect.value;
break;
default:
break;
}
});
actor.update(actorData);
await actor.createEmbeddedEntity("OwnedItem", assignedSpecies.data, { displaySheet: false });
}
static async addPowers(powers, actor) {
const forcePowers = await game.packs.get("sw5e.forcepowers").getContent();
const techPowers = await game.packs.get("sw5e.techpowers").getContent();
for (const power of powers) {
const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power);
if (createdPower) {
await actor.createEmbeddedEntity("OwnedItem", createdPower.data, { displaySheet: false });
}
}
}
static async addItems(items, actor) {
const weapons = await game.packs.get("sw5e.weapons").getContent();
const armors = await game.packs.get("sw5e.armor").getContent();
const adventuringGear = await game.packs.get("sw5e.adventuringgear").getContent();
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.createEmbeddedEntity("OwnedItem", createdItem.data, { displaySheet: false });
}
}
}
static addImportButton(html) {
const actionButtons = html.find(".header-actions");
actionButtons[0].insertAdjacentHTML(
"afterend",
`<div class="header-actions action-buttons flexrow"><button class="create-entity cs-import-button"><i class="fas fa-upload"></i> Import Character</button></div>`
);
let characterImportButton = $(".cs-import-button");
characterImportButton.click(() => {
let content = `<h1>Saved Character JSON Import</h1>
<label for="character-json">Paste character JSON here:</label>
</br>
<textarea id="character-json" name="character-json" rows="10" cols="50"></textarea>`;
let importDialog = new Dialog({
title: "Import Character from SW5e.com",
content: content,
buttons: {
"Import": {
icon: '<i class="fas fa-file-import"></i>',
Import: {
icon: `<i class="fas fa-file-import"></i>`,
label: "Import Character",
callback: (e) => {
let characterData = $('#character-json').val();
console.log('Parsing Character JSON');
callback: () => {
let characterData = $("#character-json").val();
console.log("Parsing Character JSON");
CharacterImporter.transform(characterData);
}
},
"Cancel": {
icon: '<i class="fas fa-times-circle"></i>',
Cancel: {
icon: `<i class="fas fa-times-circle"></i>`,
label: "Cancel",
callback: () => {},
callback: () => {}
}
}
})
});
importDialog.render(true);
});
}

View file

@ -40,7 +40,7 @@ export const displayChatActionButtons = function(message, html, data) {
// If the user is the message author or the actor owner, proceed
let actor = game.actors.get(data.message.speaker.actor);
if ( actor && actor.owner ) return;
if ( actor && actor.isOwner ) return;
else if ( game.user.isGM || (data.author.id === game.user.id)) return;
// Otherwise conceal action buttons except for saving throw
@ -66,7 +66,7 @@ export const displayChatActionButtons = function(message, html, data) {
export const addChatMessageContextOptions = function(html, options) {
let canApply = li => {
const message = game.messages.get(li.data("messageId"));
return message?.isRoll && message?.isContentVisible && canvas?.tokens.controlled.length;
return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
};
options.push(
{

View file

@ -5,20 +5,19 @@
* Apply the dexterity score as a decimal tiebreaker if requested
* See Combat._getInitiativeFormula for more detail.
*/
export const _getInitiativeFormula = function(combatant) {
const actor = combatant.actor;
export const _getInitiativeFormula = function() {
const actor = this.actor;
if ( !actor ) return "1d20";
const init = actor.data.data.attributes.init;
// Construct initiative formula parts
let nd = 1;
let mods = "";
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1";
if (actor.getFlag("sw5e", "initiativeAdv")) {
nd = 2;
mods += "kh";
}
const parts = [`${nd}d20${mods}`, init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
// Optionally apply Dexterity tiebreaker

View file

@ -63,7 +63,7 @@ SW5E.attunementTypes = {
NONE: 0,
REQUIRED: 1,
ATTUNED: 2,
}
};
/**
* An enumeration of item attunement states
@ -77,12 +77,100 @@ SW5E.attunements = {
/* -------------------------------------------- */
SW5E.weaponProficiencies = {
"blp": "SW5E.WeaponBlasterPistolProficiency",
"chk": "SW5E.WeaponChakramProficiency",
"dbb": "SW5E.WeaponDoubleBladeProficiency",
"dbs": "SW5E.WeaponDoubleSaberProficiency",
"dsh": "SW5E.WeaponDoubleShotoProficiency",
"dsw": "SW5E.WeaponDoubleSwordProficiency",
"hid": "SW5E.WeaponHiddenBladeProficiency",
"imp": "SW5E.WeaponImprovisedProficiency",
"lfl": "SW5E.WeaponLightFoilProficiency",
"lrg": "SW5E.WeaponLightRingProficiency",
"mar": "SW5E.WeaponMartialProficiency",
"mrb": "SW5E.WeaponMartialBlasterProficiency",
"mlw": "SW5E.WeaponMartialLightweaponProficiency",
"mvb": "SW5E.WeaponMartialVibroweaponProficiency",
"ntl": "SW5E.WeaponNaturalProficiency",
"swh": "SW5E.WeaponSaberWhipProficiency",
"sim": "SW5E.WeaponSimpleProficiency",
"mar": "SW5E.WeaponMartialProficiency"
"smb": "SW5E.WeaponSimpleBlasterProficiency",
"slw": "SW5E.WeaponSimpleLightweaponProficiency",
"svb": "SW5E.WeaponSimpleVibroweaponProficiency",
"tch": "SW5E.WeaponTechbladeProficiency",
"vbr": "SW5E.WeaponVibrorapierProficiency",
"vbw": "SW5E.WeaponVibrowhipProficiency"
};
/**
* A map of weapon item proficiency to actor item proficiency
* Used when a new player owned item is created
* @type {Object}
*/
SW5E.weaponProficienciesMap = {
"natural": true,
"simpleVW": "sim",
"simpleB": "sim",
"simpleLW": "sim",
"martialVW": "mar",
"martialB": "mar",
"martialLW": "mar"
};
// TODO: Check to see if this can be used
// It's not actually been used anywhere in DND5e 1.3.2
// Note name mapped to ID in compendium
/**
* The basic weapon types in 5e. This enables specific weapon proficiencies or
* starting equipment provided by classes and backgrounds.
*
* @enum {string}
SW5E.weaponIds = {
"battleaxe": "I0WocDSuNpGJayPb",
"blowgun": "wNWK6yJMHG9ANqQV",
"club": "nfIRTECQIG81CvM4",
"dagger": "0E565kQUBmndJ1a2",
"dart": "3rCO8MTIdPGSW6IJ",
"flail": "UrH3sMdnUDckIHJ6",
"glaive": "rOG1OM2ihgPjOvFW",
"greataxe": "1Lxk6kmoRhG8qQ0u",
"greatclub": "QRCsxkCwWNwswL9o",
"greatsword": "xMkP8BmFzElcsMaR",
"halberd": "DMejWAc8r8YvDPP1",
"handaxe": "eO7Fbv5WBk5zvGOc",
"handcrossbow": "qaSro7kFhxD6INbZ",
"heavycrossbow": "RmP0mYRn2J7K26rX",
"javelin": "DWLMnODrnHn8IbAG",
"lance": "RnuxdHUAIgxccVwj",
"lightcrossbow": "ddWvQRLmnnIS0eLF",
"lighthammer": "XVK6TOL4sGItssAE",
"longbow": "3cymOVja8jXbzrdT",
"longsword": "10ZP2Bu3vnCuYMIB",
"mace": "Ajyq6nGwF7FtLhDQ",
"maul": "DizirD7eqjh8n95A",
"morningstar": "dX8AxCh9o0A9CkT3",
"net": "aEiM49V8vWpWw7rU",
"pike": "tC0kcqZT9HHAO0PD",
"quarterstaff": "g2dWN7PQiMRYWzyk",
"rapier": "Tobce1hexTnDk4sV",
"scimitar": "fbC0Mg1a73wdFbqO",
"shortsword": "osLzOwQdPtrK3rQH",
"sickle": "i4NeNZ30ycwPDHMx",
"spear": "OG4nBBydvmfWYXIk",
"shortbow": "GJv6WkD7D2J6rP6M",
"sling": "3gynWO9sN4OLGMWD",
"trident": "F65ANO66ckP8FDMa",
"warpick": "2YdfjN1PIIrSHZii",
"warhammer": "F0Df164Xv1gWcYt0",
"whip": "QKTyxoO0YDnAsbYe"
};
*/
/* -------------------------------------------- */
SW5E.toolProficiencies = {
"armor": "SW5E.ToolArmormech",
"arms": "SW5E.ToolArmstech",
@ -116,6 +204,51 @@ SW5E.toolProficiencies = {
"vehicle": "SW5E.ToolVehicle"
};
// TODO: Same as weapon IDs
// Also unused, and SW5E.toolProficiencies is already pretty verbose anyway
/**
* The basic tool types in 5e. This enables specific tool proficiencies or
* starting equipment provided by classes and backgrounds.
*
* @enum {string}
SW5E.toolIds = {
"alchemist": "SztwZhbhZeCqyAes",
"bagpipes": "yxHi57T5mmVt0oDr",
"brewer": "Y9S75go1hLMXUD48",
"calligrapher": "jhjo20QoiD5exf09",
"card": "YwlHI3BVJapz4a3E",
"carpenter": "8NS6MSOdXtUqD7Ib",
"cartographer": "fC0lFK8P4RuhpfaU",
"cobbler": "hM84pZnpCqKfi8XH",
"cook": "Gflnp29aEv5Lc1ZM",
"dice": "iBuTM09KD9IoM5L8",
"disg": "IBhDAr7WkhWPYLVn",
"drum": "69Dpr25pf4BjkHKb",
"dulcimer": "NtdDkjmpdIMiX7I2",
"flute": "eJOrPcAz9EcquyRQ",
"forg": "cG3m4YlHfbQlLEOx",
"glassblower": "rTbVrNcwApnuTz5E",
"herb": "i89okN7GFTWHsvPy",
"horn": "aa9KuBy4dst7WIW9",
"jeweler": "YfBwELTgPFHmQdHh",
"leatherworker": "PUMfwyVUbtyxgYbD",
"lute": "qBydtUUIkv520DT7",
"lyre": "EwG1EtmbgR3bM68U",
"mason": "skUih6tBvcBbORzA",
"navg": "YHCmjsiXxZ9UdUhU",
"painter": "ccm5xlWhx74d6lsK",
"panflute": "G5m5gYIx9VAUWC3J",
"pois": "il2GNi8C0DvGLL9P",
"potter": "hJS8yEVkqgJjwfWa",
"shawm": "G3cqbejJpfB91VhP",
"smith": "KndVe2insuctjIaj",
"thief": "woWZ1sO5IUVGzo58",
"tinker": "0d08g1i5WXnNrCNA",
"viol": "baoe3U5BfMMMxhCU",
"weaver": "ap9prThUB2y9lDyj",
"woodcarver": "xKErqkLo4ASYr5EP",
};
*/
/* -------------------------------------------- */
@ -152,8 +285,8 @@ SW5E.abilityActivationTypes = {
"hour": SW5E.timePeriods.hour,
"day": SW5E.timePeriods.day,
"special": SW5E.timePeriods.spec,
"legendary": "SW5E.LegAct",
"lair": "SW5E.LairAct",
"legendary": "SW5E.LegendaryActionLabel",
"lair": "SW5E.LairActionLabel",
"crew": "SW5E.VehicleCrewAction"
};
@ -189,6 +322,34 @@ SW5E.tokenSizes = {
"grg": 4
};
/**
* Colors used to visualize temporary and temporary maximum HP in token health bars
* @enum {number}
*/
SW5E.tokenHPColors = {
temp: 0x66CCFF,
tempmax: 0x440066,
negmax: 0x550000
}
/* -------------------------------------------- */
/**
* Creature types
* @type {Object}
*/
SW5E.creatureTypes = {
"aberration": "SW5E.CreatureAberration",
"beast": "SW5E.CreatureBeast",
"construct": "SW5E.CreatureConstruct",
"droid": "SW5E.CreatureDroid",
"force": "SW5E.CreatureForceEntity",
"humanoid": "SW5E.CreatureHumanoid",
"plant": "SW5E.CreaturePlant",
"undead": "SW5E.CreatureUndead"
};
/* -------------------------------------------- */
/**
@ -224,25 +385,32 @@ SW5E.limitedUsePeriods = {
"sr": "SW5E.ShortRest",
"lr": "SW5E.LongRest",
"day": "SW5E.Day",
"charges": "SW5E.Charges"
"charges": "SW5E.Charges",
"recharge": "SW5E.Recharge",
"refitting": "SW5E.Refitting"
};
/* -------------------------------------------- */
/**
* The set of equipment types for armor, clothing, and other objects which can ber worn by the character
* The set of equipment types for armor, clothing, and other objects which can be worn by the character
* @type {Object}
*/
SW5E.equipmentTypes = {
"light": "SW5E.EquipmentLight",
"medium": "SW5E.EquipmentMedium",
"heavy": "SW5E.EquipmentHeavy",
"hyper": "SW5E.EquipmentHyperdrive",
"bonus": "SW5E.EquipmentBonus",
"natural": "SW5E.EquipmentNatural",
"powerc": "SW5E.EquipmentPowerCoupling",
"reactor": "SW5E.EquipmentReactor",
"shield": "SW5E.EquipmentShield",
"clothing": "SW5E.EquipmentClothing",
"trinket": "SW5E.EquipmentTrinket",
"ssarmor": "SW5E.EquipmentStarshipArmor",
"ssshield": "SW5E.EquipmentStarshipShield",
"vehicle": "SW5E.EquipmentVehicle"
};
@ -260,6 +428,19 @@ SW5E.armorProficiencies = {
"shl": "SW5E.EquipmentShieldProficiency"
};
/**
* A map of armor item proficiency to actor item proficiency
* Used when a new player owned item is created
* @type {Object}
*/
SW5E.armorProficienciesMap = {
"natural": true,
"clothing": true,
"light": "lgt",
"medium": "med",
"heavy": "hvy",
"shield": "shl"
}
/* -------------------------------------------- */
@ -310,7 +491,7 @@ SW5E.damageTypes = {
};
// Damage Resistance Types
SW5E.damageResistanceTypes = duplicate(SW5E.damageTypes);
SW5E.damageResistanceTypes = foundry.utils.deepClone(SW5E.damageTypes);
/* -------------------------------------------- */
@ -357,10 +538,14 @@ SW5E.armorPropertiesTypes = {
SW5E.movementTypes = {
"burrow": "SW5E.MovementBurrow",
"climb": "SW5E.MovementClimb",
"crawl": "SW5E.MovementCrawl",
"fly": "SW5E.MovementFly",
"roll": "SW5E.MovementRoll",
"space": "SW5E.MovementSpace",
"swim": "SW5E.MovementSwim",
"turn": "SW5E.MovementTurn",
"walk": "SW5E.MovementWalk",
}
};
/**
* The valid units of measure for movement distances in the game system.
@ -370,7 +555,7 @@ SW5E.movementTypes = {
SW5E.movementUnits = {
"ft": "SW5E.DistFt",
"mi": "SW5E.DistMi"
}
};
/**
* The valid units of measure for the range of an action or effect.
@ -423,6 +608,7 @@ SW5E.targetTypes = {
"square": "SW5E.TargetSquare",
"cube": "SW5E.TargetCube",
"line": "SW5E.TargetLine",
"starship": "SW5E.TargetStarship",
"wall": "SW5E.TargetWall",
"weapon": "SW5E.TargetWeapon"
};
@ -462,16 +648,133 @@ SW5E.healingTypes = {
/**
* Enumerate the denominations of hit dice which can apply to classes in the SW5E system
* @type {Array.<string>}
* @type {string[]}
*/
SW5E.hitDieTypes = ["d4", "d6", "d8", "d10", "d12", "d20"];
/* -------------------------------------------- */
/**
* Enumerate the denominations of power dice which can apply to starships in the SW5E system
* @enum {string}
*/
SW5E.powerDieTypes = [1, "d4", "d6", "d8", "d10", "d12"];
/* -------------------------------------------- */
/**
* Enumerate the base stat and feature settings for starships based on size.
* @type {Array.<string>}
*/
SW5E.baseStarshipSettings = {
"tiny": {"changes":[{"key":"data.abilities.dex.value","value":4,"mode":2,"priority":20},{"key":"data.abilities.dex.proficient","value":1,"mode":4,"priority":20}, {"key":"data.abilities.con.value","value":-4,"mode":2,"priority":20}, {"key":"data.abilities.int.proficient","value":1,"mode":4,"priority":20}], "attributes":{"crewcap":null, "hd":"1d4", "hp":{"value":4, "max":4, "temp":4, "tempmax":4}, "hsm":1, "sd":"1d4", "mods":{"open":10, "max":10}, "suites":{"open":0, "max":0}, "movement":{"fly":300, "turn":300}}},
"sm": {"changes":[{"key":"data.abilities.dex.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.dex.proficient","value":1,"mode":4,"priority":20},{"key":"data.abilities.con.value","value":-2,"mode":2,"priority":20},{"key":"data.abilities.str.proficient","value":1,"mode":4,"priority":20}], "attributes":{"crewcap":1, "hd":"3d6", "hp":{"value":6, "max":6, "temp":6, "tempmax":6}, "hsm":2, "sd":"3d6", "mods":{"open":20, "max":20}, "suites":{"open":-1, "max":-1}, "movement":{"fly":300, "turn":250}}},
"med": {"attributes":{"crewcap":1, "hd":"5d8", "hp":{"value":8, "max":8, "temp":8, "tempmax":8}, "hsm":3, "sd":"5d8", "mods":{"open":30, "max":30}, "suites":{"open":3, "max":3}, "movement":{"fly":300, "turn":200}}},
"lg": {"changes":[{"key":"data.abilities.dex.value","value":-2,"mode":2,"priority":20},{"key":"data.abilities.wis.proficient","value":1,"mode":4,"priority":20},{"key":"data.abilities.con.value","value":2,"mode":2,"priority":20}], "attributes":{"crewcap":200, "hd":"7d10", "hp":{"value":10, "max":10, "temp":10, "tempmax":10}, "hsm":4, "sd":"7d10", "mods":{"open":50, "max":50}, "suites":{"open":3, "max":3}, "movement":{"fly":300, "turn":150}}},
"huge": {"changes":[{"key":"data.abilities.dex.value","value":-4,"mode":2,"priority":20},{"key":"data.abilities.wis.proficient","value":1,"mode":4,"priority":20},{"key":"data.abilities.con.value","value":4,"mode":2,"priority":20}], "attributes":{"crewcap":4000, "hd":"9d12", "hp":{"value":12, "max":12, "temp":12, "tempmax":12}, "hsm":2, "sd":"9d12", "mods":{"open":60, "max":60}, "suites":{"open":6, "max":6}, "movement":{"fly":300, "turn":100}}},
"grg": {"changes":[{"key":"data.abilities.dex.value","value":-6,"mode":2,"priority":20},{"key":"data.abilities.wis.proficient","value":1,"mode":4,"priority":20},{"key":"data.abilities.con.value","value":6,"mode":2,"priority":20}], "attributes":{"crewcap":80000, "hd":"11d20", "hp":{"value":20, "max":20, "temp":20, "tempmax":20}, "hsm":3, "sd":"11d20", "mods":{"open":70, "max":70}, "suites":{"open":10, "max":10}, "movement":{"fly":300, "turn":50}}}
}
/* -------------------------------------------- */
/**
* The set of starship roles which can be selected in SW5e
* @type {Object}
*/
SW5E.starshipRolestiny = {
};
SW5E.starshipRolessm = {
"bmbr": "SW5E.StarshipBomber",
"intc": "SW5E.StarshipInterceptor",
"scout": "SW5E.StarshipScout",
"scrm": "SW5E.StarshipScrambler",
"shtl": "SW5E.StarshipShuttle",
"strf": "SW5E.StarshipStrikeFighter"
};
SW5E.starshipRolesmed = {
"cour": "SW5E.StarshipCourier",
"frtr": "SW5E.StarshipFreighter",
"gnbt": "SW5E.StarshipGunboat",
"msbt": "SW5E.StarshipMissileBoat",
"nvgt": "SW5E.StarshipNavigator",
"yacht": "SW5E.StarshipYacht"
};
SW5E.starshipRoleslg = {
"ambd": "SW5E.StarshipAmbassador",
"corv": "SW5E.StarshipCorvette",
"crui": "SW5E.StarshipCruiser",
"expl": "SW5E.StarshipExplorer",
"pics": "SW5E.StarshipPicketShip",
"shtd": "SW5E.StarshipShipsTender"
};
SW5E.starshipRoleshuge = {
"btls": "SW5E.StarshipBattleship",
"carr": "SW5E.StarshipCarrier",
"colo": "SW5E.StarshipColonizer",
"cmds": "SW5E.StarshipCommandShip",
"intd": "SW5E.StarshipInterdictor",
"jugg": "SW5E.StarshipJuggernaut"
};
SW5E.starshipRolesgrg = {
"blks": "SW5E.StarshipBlockadeShip",
"flgs": "SW5E.StarshipFlagship",
"inct": "SW5E.StarshipIndustrialCenter",
"mbmt": "SW5E.StarshipMobileMetropolis",
"rsrc": "SW5E.StarshipResearcher",
"wars": "SW5E.StarshipWarship"
};
/* -------------------------------------------- */
/**
* The set of starship role bonuses to starships which can be selected in SW5e
* @type {Object}
*/
SW5E.starshipRoleBonuses = {
"bmbr": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
"intc": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20}]},
"scout": {"changes":[{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]},
"scrm": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20}]},
"shtl": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20}]},
"strf": {"changes":[{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
"cour": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20}]},
"frtr": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20}]},
"gnbt": {"changes":[{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
"msbt": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
"nvgt": {"changes":[{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]},
"yacht": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20}]},
"ambd": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20}]},
"corv": {"changes":[{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20}]},
"crui": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
"expl": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]},
"pics": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
"shtd": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
"btls": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
"carr": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]},
"colo": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]},
"cmds": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
"intd": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
"jugg": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
"blks": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
"flgs": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
"inct": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
"mbmt": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
"rsrc": {"changes":[{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
"wars": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]}
};
/* -------------------------------------------- */
/**
* The set of possible sensory perception types which an Actor may have
* @type {object}
* @enum {string}
*/
SW5E.senses = {
"blindsight": "SW5E.SenseBlindsight",
@ -507,6 +810,28 @@ SW5E.skills = {
"tec": "SW5E.SkillTec"
};
/* -------------------------------------------- */
/**
* The set of starship skills which can be trained in SW5e
* @type {Object}
*/
SW5E.starshipSkills = {
"ast": "SW5E.StarshipSkillAst",
"bst": "SW5E.StarshipSkillBst",
"dat": "SW5E.StarshipSkillDat",
"hid": "SW5E.StarshipSkillHid",
"imp": "SW5E.StarshipSkillImp",
"int": "SW5E.StarshipSkillInt",
"man": "SW5E.StarshipSkillMan",
"men": "SW5E.StarshipSkillMen",
"pat": "SW5E.StarshipSkillPat",
"prb": "SW5E.StarshipSkillPrb",
"ram": "SW5E.StarshipSkillRam",
"reg": "SW5E.StarshipSkillReg",
"scn": "SW5E.StarshipSkillScn",
"swn": "SW5E.StarshipSkillSwn"
};
/* -------------------------------------------- */
@ -610,16 +935,21 @@ SW5E.powerScalingModes = {
* @type {Object}
*/
SW5E.weaponTypes = {
"simpleVW": "SW5E.WeaponSimpleVW",
"simpleB": "SW5E.WeaponSimpleB",
"simpleLW": "SW5E.WeaponSimpleLW",
"ammo": "SW5E.WeaponAmmo",
"improv": "SW5E.WeaponImprov",
"martialVW": "SW5E.WeaponMartialVW",
"martialB": "SW5E.WeaponMartialB",
"martialLW": "SW5E.WeaponMartialLW",
"natural": "SW5E.WeaponNatural",
"improv": "SW5E.WeaponImprov",
"ammo": "SW5E.WeaponAmmo",
"siege": "SW5E.WeaponSiege"
"siege": "SW5E.WeaponSiege",
"simpleVW": "SW5E.WeaponSimpleVW",
"simpleB": "SW5E.WeaponSimpleB",
"simpleLW": "SW5E.WeaponSimpleLW",
"primary (starship)": "SW5E.WeaponPrimarySW",
"secondary (starship)": "SW5E.WeaponSecondarySW",
"tertiary (starship)": "SW5E.WeaponTertiarySW",
"quaternary (starship)": "SW5E.WeaponQuaternarySW"
};
@ -633,6 +963,7 @@ SW5E.weaponProperties = {
"amm": "SW5E.WeaponPropertiesAmm",
"aut": "SW5E.WeaponPropertiesAut",
"bur": "SW5E.WeaponPropertiesBur",
"con": "SW5E.WeaponPropertiesCon",
"def": "SW5E.WeaponPropertiesDef",
"dex": "SW5E.WeaponPropertiesDex",
"dir": "SW5E.WeaponPropertiesDir",
@ -641,20 +972,27 @@ SW5E.weaponProperties = {
"dis": "SW5E.WeaponPropertiesDis",
"dpt": "SW5E.WeaponPropertiesDpt",
"dou": "SW5E.WeaponPropertiesDou",
"exp": "SW5E.WeaponPropertiesExp",
"fin": "SW5E.WeaponPropertiesFin",
"fix": "SW5E.WeaponPropertiesFix",
"foc": "SW5E.WeaponPropertiesFoc",
"hvy": "SW5E.WeaponPropertiesHvy",
"hid": "SW5E.WeaponPropertiesHid",
"hom": "SW5E.WeaponPropertiesHom",
"ion": "SW5E.WeaponPropertiesIon",
"ken": "SW5E.WeaponPropertiesKen",
"lgt": "SW5E.WeaponPropertiesLgt",
"lum": "SW5E.WeaponPropertiesLum",
"mlt": "SW5E.WeaponPropertiesMlt",
"mig": "SW5E.WeaponPropertiesMig",
"ovr": "SW5E.WeaponPropertiesOvr",
"pic": "SW5E.WeaponPropertiesPic",
"pow": "SW5E.WeaponPropertiesPow",
"rap": "SW5E.WeaponPropertiesRap",
"rch": "SW5E.WeaponPropertiesRch",
"rel": "SW5E.WeaponPropertiesRel",
"ret": "SW5E.WeaponPropertiesRet",
"sat": "SW5E.WeaponPropertiesSat",
"shk": "SW5E.WeaponPropertiesShk",
"sil": "SW5E.WeaponPropertiesSil",
"spc": "SW5E.WeaponPropertiesSpc",
@ -662,9 +1000,24 @@ SW5E.weaponProperties = {
"thr": "SW5E.WeaponPropertiesThr",
"two": "SW5E.WeaponPropertiesTwo",
"ver": "SW5E.WeaponPropertiesVer",
"vic": "SW5E.WeaponPropertiesVic"
"vic": "SW5E.WeaponPropertiesVic",
"zon": "SW5E.WeaponPropertiesZon"
};
/* -------------------------------------------- */
/**
* Define the set of starship weapon size flags which can exist on a weapon
* @type {Object}
*/
SW5E.weaponSizes = {
"tiny": "SW5E.SizeTiny",
"sm": "SW5E.SizeSmall",
"med": "SW5E.SizeMedium",
"lg": "SW5E.SizeLarge",
"huge": "SW5E.SizeHuge",
"grg": "SW5E.SizeGargantuan"
};
// Power Components
SW5E.powerComponents = {
@ -696,6 +1049,32 @@ SW5E.powerLevels = {
9: "SW5E.PowerLevel9"
};
// TODO: This is used for spell scrolls, it maps the level to the compendium ID of the item the spell would be bound to
// We could use this with, say, holocrons to produce scrolls
/*
// Power Scroll Compendium UUIDs
SW5E.powerScrollIds = {
0: "rQ6sO7HDWzqMhSI3",
1: "9GSfMg0VOA2b4uFN",
2: "XdDp6CKh9qEvPTuS",
3: "hqVKZie7x9w3Kqds",
4: "DM7hzgL836ZyUFB1",
5: "wa1VF8TXHmkrrR35",
6: "tI3rWx4bxefNCexS",
7: "mtyw4NS1s7j2EJaD",
8: "aOrinPg7yuDZEuWr",
9: "O4YbkJkLlnsgUszZ"
};
*/
/**
* Compendium packs used for localized items.
* @enum {string}
*/
SW5E.sourcePacks = {
ITEMS: "sw5e.items"
}
// Polymorph options.
SW5E.polymorphSettings = {
keepPhysical: 'SW5E.PolymorphKeepPhysical',

View file

@ -1,3 +1,6 @@
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
*
@ -21,7 +24,7 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
let operators = []; // Temporary storage for operators before they are moved to one of the above
for (let term of terms) { // For each term
if (["+", "-"].includes(term)) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array
if (term instanceof OperatorTerm) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array
else { // Otherwise the term is not an operator
if (term instanceof DiceTerm) { // If the term is something rollable
rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
@ -35,13 +38,21 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
}
}
const constantFormula = Roll.cleanFormula(constantTerms); // Cleans up the 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
const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string
const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string
const constantPart = roll._safeEval(constantFormula); // Mathematically evaluate the constant formula to produce a single constant term
// 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`);
}
}
const parts = constantFirst ? // Order the rollable and constant terms, either constant first or second depending on the optional argumen
[constantPart, rollableFormula] : [rollableFormula, constantPart];
// Order the rollable and constant terms, either constant first or second depending on the optional argument
const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart];
// Join the parts with a + sign, pass them to `Roll` once again to clean up the formula
return new Roll(parts.filterJoin(" + ")).formula;
@ -56,315 +67,202 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
*/
function _isUnsupportedTerm(term) {
const diceTerm = term instanceof DiceTerm;
const operator = ["+", "-"].includes(term);
const number = !isNaN(Number(term));
const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator);
const number = term instanceof NumericTerm;
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".
* This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively
*
* @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 {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 {string[]} parts The dice roll component parts, excluding the initial d20
* @param {object} data Actor or item data against which to parse the roll
*
* @return {Promise} A Promise which resolves once the roll workflow has completed
* @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} [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({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null,
flavor=null, fastForward=null, dialogOptions,
advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null,
elvenAccuracy=false, halflingLucky=false, reliableTalent=false,
chatMessage=true, messageData={}}={}) {
export async function d20Roll({
parts=[], data={}, // Roll creation
advantage, disadvantage, fumble=1, critical=20, targetValue, elvenAccuracy, halflingLucky, reliableTalent, // Roll customization
chooseModifier=false, fastForward=false, event, template, title, dialogOptions, // Dialog configuration
chatMessage=true, messageData={}, rollMode, speaker, flavor // Chat Message customization
}={}) {
// 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 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";
// 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;
// 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;
}
// 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, title, parts, data, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll});
// Evaluate the configured roll
await roll.evaluate({async: true});
// Create a Chat Message
if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
if ( speaker ) {
console.warn(`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`);
messageData.speaker = speaker;
}
if ( roll && chatMessage ) await roll.toMessage(messageData);
return roll;
}
/* -------------------------------------------- */
/**
* Present a Dialog form which creates a d20 roll once submitted
* @return {Promise<Roll>}
* @private
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode
*/
async function _d20RollDialog({template, title, parts, data, rollMode, dialogOptions, roll}={}) {
// Render modal dialog
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
let dialogData = {
formula: parts.join(" + "),
data: data,
rollMode: rollMode,
rollModes: CONFIG.Dice.rollModes,
config: CONFIG.SW5E
};
const html = await renderTemplate(template, dialogData);
// Create the Dialog window
return new Promise(resolve => {
new Dialog({
title: title,
content: html,
buttons: {
advantage: {
label: game.i18n.localize("SW5E.Advantage"),
callback: html => resolve(roll(parts, 1, html[0].querySelector("form")))
},
normal: {
label: game.i18n.localize("SW5E.Normal"),
callback: html => resolve(roll(parts, 0, html[0].querySelector("form")))
},
disadvantage: {
label: game.i18n.localize("SW5E.Disadvantage"),
callback: html => resolve(roll(parts, -1, html[0].querySelector("form")))
}
},
default: "normal",
close: () => resolve(null)
}, dialogOptions).render(true);
});
function _determineAdvantageMode({event, advantage=false, disadvantage=false, fastForward=false}={}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;
if ( advantage || event?.altKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
else if ( disadvantage || event?.ctrlKey || event?.metaKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
return {isFF, advantageMode};
}
/* -------------------------------------------- */
/* Damage Roll */
/* -------------------------------------------- */
/**
* A standardized helper function for managing core 5e "d20 rolls"
* A standardized helper function for managing core 5e damage rolls.
*
* 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
*
* @param {Array} parts The dice roll component parts, excluding the initial d20
* @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 {string[]} parts The dice roll component parts, excluding the initial d20
* @param {object} [data] Actor or item data against which to parse the roll
*
* @return {Promise} A Promise which resolves once the roll workflow has completed
* @param {boolean} [critical=false] Flag this roll as a critical hit for the purposes of fast-forward or default dialog action
* @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({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor,
allowCritical=true, critical=false, criticalBonusDice=0, criticalMultiplier=2, fastForward=null,
dialogOptions={}, chatMessage=true, messageData={}}={}) {
export async function damageRoll({
parts=[], data, // Roll creation
critical=false, criticalBonusDice, criticalMultiplier, multiplyNumeric, powerfulCritical, // Damage customization
fastForward=false, event, allowCritical=true, template, title, dialogOptions, // Dialog configuration
chatMessage=true, messageData={}, rollMode, speaker, flavor, // Chat Message customization
}={}) {
// 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 input arguments
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
// Define inner roll function
const _roll = function(parts, crit, form) {
// Optionally include a situational bonus
if ( form ) {
data['bonus'] = form.bonus.value;
messageOptions.rollMode = form.rollMode.value;
}
if (!data["bonus"]) parts.pop();
// Create the damage roll
let roll = new Roll(parts.join("+"), data);
// Modify the damage formula for critical hits
if ( crit === true ) {
roll.alter(criticalMultiplier, 0); // Multiply all dice
if ( roll.terms[0] instanceof Die ) { // Add bonus dice for only the main dice term
roll.terms[0].alter(1, criticalBonusDice);
roll._formula = roll.formula;
}
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
}
// Execute the roll
try {
roll.evaluate()
if ( crit ) roll.dice.forEach(d => d.options.critical = true); // TODO workaround core bug which wipes Roll#options on roll
return roll;
} catch(err) {
console.error(err);
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
return null;
}
};
// Create the Roll instance
const roll = fastForward ? _roll(parts, critical) : await _damageRollDialog({
template, title, parts, data, allowCritical, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll
// Construct the DamageRoll instance
const formula = parts.join(" + ");
const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event});
const roll = new CONFIG.Dice.DamageRoll(formula, data, {
flavor: flavor || title,
critical: isCritical,
criticalBonusDice,
criticalMultiplier,
multiplyNumeric,
powerfulCritical
});
// Create a Chat Message
if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
return 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
if ( speaker ) {
console.warn(`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`);
messageData.speaker = speaker;
}
if ( roll && chatMessage ) await roll.toMessage(messageData);
return roll;
}
/* -------------------------------------------- */
/**
* Present a Dialog form which creates a damage roll once submitted
* @return {Promise<Roll>}
* @private
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit
*/
async function _damageRollDialog({template, title, parts, data, allowCritical, rollMode, dialogOptions, roll}={}) {
// Render modal dialog
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
let dialogData = {
formula: parts.join(" + "),
data: data,
rollMode: rollMode,
rollModes: CONFIG.Dice.rollModes
};
const html = await renderTemplate(template, dialogData);
// Create the Dialog window
return new Promise(resolve => {
new Dialog({
title: title,
content: html,
buttons: {
critical: {
condition: allowCritical,
label: game.i18n.localize("SW5E.CriticalHit"),
callback: html => resolve(roll(parts, true, html[0].querySelector("form")))
},
normal: {
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
callback: html => resolve(roll(parts, false, html[0].querySelector("form")))
},
},
default: "normal",
close: () => resolve(null)
}, dialogOptions).render(true);
});
function _determineCriticalMode({event, critical=false, fastForward=false}={}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
if ( event?.altKey ) critical = true;
return {isFF, isCritical: critical};
}

217
module/dice/d20-roll.js Normal file
View file

@ -0,0 +1,217 @@
/**
* 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;
}
}

181
module/dice/damage-roll.js Normal file
View file

@ -0,0 +1,181 @@
/**
* 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

@ -0,0 +1,15 @@
/**
* @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`);
}

12
module/effects.js vendored
View file

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

View file

@ -2,7 +2,8 @@ import {simplifyRollFormula, d20Roll, damageRoll} from "../dice.js";
import AbilityUseDialog from "../apps/ability-use-dialog.js";
/**
* Override and extend the basic :class:`Item` implementation
* Override and extend the basic Item implementation
* @extends {Item}
*/
export default class Item5e extends Item {
@ -44,12 +45,15 @@ export default class Item5e extends Item {
else if (this.data.type === "weapon") {
const wt = itemData.weaponType;
// Melee weapons - Str or Dex if Finesse (PHB pg. 147)
if ( ["simpleVW", "martialVW", "simpleLW", "martialLW"].includes(wt) ) {
if (itemData.properties.fin === true) { // Finesse weapons
return (actorData.abilities["dex"].mod >= actorData.abilities["str"].mod) ? "dex" : "str";
// Weapons using the powercasting modifier
// No current SW5e weapons use this, but it's worth checking just in case
if (["mpak", "rpak"].includes(itemData.actionType)) {
return actorData.attributes.powercasting || "int";
}
return "str";
// Finesse weapons - Str or Dex (PHB pg. 147)
else if (itemData.properties.fin === true) {
return (actorData.abilities["dex"].mod >= actorData.abilities["str"].mod) ? "dex" : "str";
}
// Ranged weapons - Dex (PH p.194)
@ -144,7 +148,7 @@ export default class Item5e extends Item {
get hasLimitedUses() {
let chg = this.data.data.recharge || {};
let uses = this.data.data.uses || {};
return !!chg.value || (!!uses.per && (uses.max > 0));
return !!chg.value || (uses.per && (uses.max > 0));
}
/* -------------------------------------------- */
@ -154,8 +158,8 @@ export default class Item5e extends Item {
/**
* Augment the basic Item data model with additional dynamic data.
*/
prepareData() {
super.prepareData();
prepareDerivedData() {
super.prepareDerivedData();
// Get the Item's data
const itemData = this.data;
@ -190,6 +194,7 @@ export default class Item5e extends Item {
else labels.featType = game.i18n.localize("SW5E.Passive");
}
// TODO: Something with all this
// Species Items
else if ( itemData.type === "species" ) {
// labels.species = C.species[data.species];
@ -206,6 +211,14 @@ export default class Item5e extends Item {
else if ( itemData.type === "classfeature" ) {
// labels.classFeature = C.classFeature[data.classFeature];
}
// Deployment Items
else if ( itemData.type === "deployment" ) {
// labels.deployment = C.deployment[data.deployment];
}
// Venture Items
else if ( itemData.type === "venture" ) {
// labels.venture = C.venture[data.venture];
}
// Fighting Style Items
else if ( itemData.type === "fightingstyle" ) {
// labels.fightingstyle = C.fightingstyle[data.fightingstyle];
@ -242,7 +255,7 @@ export default class Item5e extends Item {
// Range Label
let rng = data.range || {};
if (["none", "touch", "self"].includes(rng.units) || (rng.value === 0)) {
if ( ["none", "touch", "self"].includes(rng.units) ) {
rng.value = null;
rng.long = null;
}
@ -260,32 +273,62 @@ export default class Item5e extends Item {
// Item Actions
if ( data.hasOwnProperty("actionType") ) {
// if this item is owned, we populate the label and saving throw during actor init
if (!this.isOwned) {
// Saving throws
this.getSaveDC();
// To Hit
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(", ");
}
}
// if this item is owned, we prepareFinalAttributes() at the end of actor init
if (!this.isOwned) this.prepareFinalAttributes();
}
/* -------------------------------------------- */
/**
* Compute item attributes which might depend on prepared actor data.
*/
prepareFinalAttributes() {
if ( this.data.data.hasOwnProperty("actionType") ) {
// Saving throws
this.getSaveDC();
// To Hit
this.getAttackToHit();
// Limited Uses
if ( this.isOwned && !!data.uses?.max ) {
let max = data.uses.max;
if ( !Number.isNumeric(max) ) {
max = Roll.replaceFormulaData(max, this.actor.getRollData());
if ( Roll.MATH_PROXY.safeEval ) max = Roll.MATH_PROXY.safeEval(max);
}
data.uses.max = Number(max);
this.prepareMaxUses();
// Damage Label
this.getDerivedDamageLabel();
}
}
/* -------------------------------------------- */
/**
* 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;
}
/* -------------------------------------------- */
@ -401,6 +444,31 @@ 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
* @param {boolean} [configureDialog] Display a configuration dialog for the item roll, if applicable?
@ -411,16 +479,18 @@ export default class Item5e extends Item {
*/
async roll({configureDialog=true, rollMode, createMessage=true}={}) {
let item = this;
const id = this.data.data; // Item system data
const actor = this.actor;
const ad = actor.data.data; // Actor system data
// Reference aspects of the item data necessary for usage
const id = this.data.data; // Item data
const hasArea = this.hasAreaTarget; // Is the ability usage an AoE?
const resource = id.consume || {}; // Resource consumption
const recharge = id.recharge || {}; // Recharge mechanic
const uses = id?.uses ?? {}; // Limited uses
const isPower = this.type === "power"; // Does the item require a power slot?
// TODO: Possibly Mod this to not consume slots based on class?
// We could use this for feats and architypes that let a character cast one slot every rest or so
const requirePowerSlot = isPower && (id.level > 0) && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode);
// Define follow-up actions resulting from the item usage
@ -430,6 +500,8 @@ export default class Item5e extends Item {
let consumePowerSlot = requirePowerSlot; // Consume a power slot
let consumeUsage = !!uses.per; // Consume limited uses
let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of uses
let consumePowerLevel = null; // Consume a specific category of power slot
if ( requirePowerSlot ) consumePowerLevel = id.preparation.mode === "pact" ? "pact" : `power${id.level}`;
// Display a configuration dialog to customize the usage
const needsConfiguration = createMeasuredTemplate || consumeRecharge || (consumeResource && !['simpleB', 'martialB'].includes(id.weaponType)) || consumePowerSlot || (consumeUsage && !['simpleB', 'martialB'].includes(id.weaponType));
@ -447,28 +519,28 @@ export default class Item5e extends Item {
// Handle power upcasting
if ( requirePowerSlot ) {
const slotLevel = configuration.level;
const powerLevel = parseInt(slotLevel);
if (powerLevel !== id.level) {
const upcastData = mergeObject(this.data, {"data.level": powerLevel}, {inplace: false});
item = this.constructor.createOwned(upcastData, actor); // Replace the item with an upcast version
consumePowerLevel = `power${configuration.level}`;
if (consumePowerSlot === false) consumePowerLevel = null;
const upcastLevel = parseInt(configuration.level);
if (upcastLevel !== id.level) {
item = this.clone({"data.level": upcastLevel}, {keepId: true});
item.data.update({_id: this.id}); // Retain the original ID (needed until 0.8.2+)
item.prepareFinalAttributes(); // Power save DC, etc...
}
if ( consumePowerSlot ) consumePowerSlot = `power${powerLevel}`;
}
}
// Determine whether the item can be used by testing for resource consumption
const usage = item._getUsageUpdates({consumeRecharge, consumeResource, consumePowerSlot, consumeUsage, consumeQuantity});
const usage = item._getUsageUpdates({consumeRecharge, consumeResource, consumePowerLevel, consumeUsage, consumeQuantity});
if ( !usage ) return;
const {actorUpdates, itemUpdates, resourceUpdates} = usage;
// Commit pending data updates
if ( !isObjectEmpty(itemUpdates) ) await item.update(itemUpdates);
if ( !foundry.utils.isObjectEmpty(itemUpdates) ) await item.update(itemUpdates);
if ( consumeQuantity && (item.data.data.quantity === 0) ) await item.delete();
if ( !isObjectEmpty(actorUpdates) ) await actor.update(actorUpdates);
if ( !isObjectEmpty(resourceUpdates) ) {
if ( !foundry.utils.isObjectEmpty(actorUpdates) ) await actor.update(actorUpdates);
if ( !foundry.utils.isObjectEmpty(resourceUpdates) ) {
const resource = actor.items.get(id.consume?.target);
if ( resource ) await resource.update(resourceUpdates);
}
@ -491,12 +563,12 @@ export default class Item5e extends Item {
* @param {boolean} consumeQuantity Consume quantity of the item if other consumption modes are not available?
* @param {boolean} consumeRecharge Whether the item consumes the recharge mechanic
* @param {boolean} consumeResource Whether the item consumes a limited resource
* @param {string|boolean} consumePowerSlot A level of power slot consumed, or false
* @param {string|null} consumePowerLevel The category of power slot to consume, or null
* @param {boolean} consumeUsage Whether the item consumes a limited usage
* @returns {object|boolean} A set of data changes to apply when the item is used, or false
* @private
*/
_getUsageUpdates({consumeQuantity=false, consumeRecharge=false, consumeResource=false, consumePowerSlot=false, consumeUsage=false}) {
_getUsageUpdates({consumeQuantity, consumeRecharge, consumeResource, consumePowerLevel, consumeUsage}) {
// Reference item data
const id = this.data.data;
@ -521,8 +593,9 @@ export default class Item5e extends Item {
}
// Consume Power Slots and Force/Tech Points
if ( consumePowerSlot ) {
const level = this.actor?.data.data.powers[consumePowerSlot];
if ( consumePowerLevel ) {
if ( Number.isNumeric(consumePowerLevel) ) consumePowerLevel = `power${consumePowerLevel}`;
const level = this.actor?.data.data.powers[consumePowerLevel];
const fp = this.actor.data.data.attributes.force.points;
const tp = this.actor.data.data.attributes.tech.points;
const powerCost = id.level + 1;
@ -538,7 +611,7 @@ export default class Item5e extends Item {
ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}));
return false;
}
actorUpdates[`data.powers.${consumePowerSlot}.fvalue`] = Math.max(powers - 1, 0);
actorUpdates[`data.powers.${consumePowerLevel}.fvalue`] = Math.max(powers - 1, 0);
if (fp.temp >= powerCost) {
actorUpdates["data.attributes.force.points.temp"] = fp.temp - powerCost;
}else{
@ -554,7 +627,7 @@ export default class Item5e extends Item {
ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}));
return false;
}
actorUpdates[`data.powers.${consumePowerSlot}.tvalue`] = Math.max(powers - 1, 0);
actorUpdates[`data.powers.${consumePowerLevel}.tvalue`] = Math.max(powers - 1, 0);
if (tp.temp >= powerCost) {
actorUpdates["data.attributes.tech.points.temp"] = tp.temp - powerCost;
}else{
@ -693,11 +766,11 @@ export default class Item5e extends Item {
*/
async displayCard({rollMode, createMessage=true}={}) {
// Basic template rendering data
// Render the chat card template
const token = this.actor.token;
const templateData = {
actor: this.actor,
tokenId: token ? `${token.scene._id}.${token.id}` : null,
tokenId: token?.uuid || null,
item: this.data,
data: this.getChatData(),
labels: this.labels,
@ -707,17 +780,14 @@ export default class Item5e extends Item {
isVersatile: this.isVersatile,
isPower: this.data.type === "power",
hasSave: this.hasSave,
hasAreaTarget: this.hasAreaTarget
hasAreaTarget: this.hasAreaTarget,
isTool: this.data.type === "tool"
};
// 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);
const html = await renderTemplate("systems/sw5e/templates/chat/item-card.html", templateData);
// Create the ChatMessage data object
const chatData = {
user: game.user._id,
user: game.user.data._id,
type: CONST.CHAT_MESSAGE_TYPES.OTHER,
content: html,
flavor: this.data.data.chatFlavor || this.name,
@ -747,7 +817,7 @@ export default class Item5e extends Item {
* @return {Object} An object of chat data to render
*/
getChatData(htmlOptions={}) {
const data = duplicate(this.data.data);
const data = foundry.utils.deepClone(this.data.data);
const labels = this.labels;
// Rich text description
@ -941,11 +1011,10 @@ export default class Item5e extends Item {
}
// Elven Accuracy
if ( ["weapon", "power"].includes(this.data.type) ) {
if ( flags.elvenAccuracy && ["dex", "int", "wis", "cha"].includes(this.abilityMod) ) {
rollConfig.elvenAccuracy = true;
}
}
// Apply Halfling Lucky
if ( flags.halflingLucky ) rollConfig.halflingLucky = true;
@ -1181,11 +1250,17 @@ export default class Item5e extends Item {
const parts = [`@mod`, "@prof"];
const title = `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`;
// Add global actor bonus
const bonuses = getProperty(this.actor.data.data, "bonuses.abilities") || {};
if ( bonuses.check ) {
parts.push("@checkBonus");
rollData.checkBonus = bonuses.check;
}
// Compose the roll data
const rollConfig = mergeObject({
parts: parts,
data: rollData,
template: "systems/sw5e/templates/chat/tool-roll-dialog.html",
title: title,
speaker: ChatMessage.getSpeaker({actor: this.actor}),
flavor: title,
@ -1194,6 +1269,7 @@ export default class Item5e extends Item {
top: options.event ? options.event.clientY - 80 : null,
left: window.innerWidth - 710,
},
chooseModifier: true,
halflingLucky: this.actor.getFlag("sw5e", "halflingLucky" ) || false,
reliableTalent: (this.data.data.proficient >= 1) && this.actor.getFlag("sw5e", "reliableTalent"),
messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id }}
@ -1213,13 +1289,16 @@ export default class Item5e extends Item {
getRollData() {
if ( !this.actor ) return null;
const rollData = this.actor.getRollData();
rollData.item = duplicate(this.data.data);
rollData.item = foundry.utils.deepClone(this.data.data);
// Include an ability score modifier if one exists
const abl = this.abilityMod;
if ( abl ) {
const ability = rollData.abilities[abl];
rollData["mod"] = ability.mod || 0;
if ( !ability ) {
console.warn(`Item ${this.name} in Actor ${this.actor.name} has an invalid item ability modifier of ${abl} defined`);
}
rollData["mod"] = ability?.mod || 0;
}
// Include a proficiency score
@ -1261,12 +1340,12 @@ export default class Item5e extends Item {
if ( !( isTargetted || game.user.isGM || message.isAuthor ) ) return;
// Recover the actor for the chat card
const actor = this._getChatCardActor(card);
const actor = await this._getChatCardActor(card);
if ( !actor ) return;
// Get the Item from stored flag data or by the item ID on the Actor
const storedData = message.getFlag("sw5e", "itemData");
const item = storedData ? this.createOwned(storedData, actor) : actor.getOwnedItem(card.dataset.itemId);
const item = storedData ? new this(storedData, {parent: actor}) : actor.items.get(card.dataset.itemId);
if ( !item ) {
return ui.notifications.error(game.i18n.format("SW5E.ActionWarningNoItem", {item: card.dataset.itemId, name: actor.name}))
}
@ -1329,17 +1408,12 @@ export default class Item5e extends Item {
* @return {Actor|null} The Actor entity or null
* @private
*/
static _getChatCardActor(card) {
static async _getChatCardActor(card) {
// Case 1 - a synthetic actor from a Token
const tokenKey = card.dataset.tokenId;
if (tokenKey) {
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);
if ( card.dataset.tokenId ) {
const token = await fromUuid(card.dataset.tokenId);
if ( !token ) return null;
return token.actor;
}
@ -1353,7 +1427,7 @@ export default class Item5e extends Item {
/**
* Get the Actor which is the author of a chat card
* @param {HTMLElement} card The chat card being used
* @return {Array.<Actor>} An Array of Actor entities, if any
* @return {Actor[]} An Array of Actor entities, if any
* @private
*/
static _getChatCardTargets(card) {
@ -1364,14 +1438,159 @@ export default class Item5e extends Item {
}
/* -------------------------------------------- */
/* Factory Methods */
/* Event Handlers */
/* -------------------------------------------- */
/** @inheritdoc */
async _preCreate(data, options, user) {
await super._preCreate(data, options, user);
if ( !this.isEmbedded || (this.parent.type === "vehicle") ) return;
const actorData = this.parent.data;
const isNPC = this.parent.type === "npc";
let updates;
switch (data.type) {
case "equipment":
updates = this._onCreateOwnedEquipment(data, actorData, isNPC);
break;
case "weapon":
updates = this._onCreateOwnedWeapon(data, actorData, isNPC);
break;
case "power":
updates = this._onCreateOwnedPower(data, actorData, isNPC);
break;
}
if (updates) return this.data.update(updates);
}
/* -------------------------------------------- */
/** @inheritdoc */
_onCreate(data, options, userId) {
super._onCreate(data, options, userId);
// The below options are only needed for character classes
if ( userId !== game.user.id ) return;
const isCharacterClass = this.parent && (this.parent.type !== "vehicle") && (this.type === "class");
if ( !isCharacterClass ) return;
// Assign a new primary class
const pc = this.parent.items.get(this.parent.data.data.details.originalClass);
if ( !pc ) this.parent._assignPrimaryClass();
// Prompt to add new class features
if (options.addFeatures === false) return;
this.parent.getClassFeatures({
className: this.name,
archetypeName: this.data.data.archetype,
level: this.data.data.levels
}).then(features => {
return this.parent.addEmbeddedItems(features, options.promptAddFeatures);
});
}
/* -------------------------------------------- */
/** @inheritdoc */
_onUpdate(changed, options, userId) {
super._onUpdate(changed, options, userId);
// The below options are only needed for character classes
if ( userId !== game.user.id ) return;
const isCharacterClass = this.parent && (this.parent.type !== "vehicle") && (this.type === "class");
if ( !isCharacterClass ) return;
// Prompt to add new class features
const addFeatures = changed["name"] || (changed.data && ["archetype", "levels"].some(k => k in changed.data));
if ( !addFeatures || (options.addFeatures === false) ) return;
this.parent.getClassFeatures({
className: changed.name || this.name,
archetypeName: changed.data?.archetype || this.data.data.archetype,
level: changed.data?.levels || this.data.data.levels
}).then(features => {
return this.parent.addEmbeddedItems(features, options.promptAddFeatures);
});
}
/* -------------------------------------------- */
/** @inheritdoc */
_onDelete(options, userId) {
super._onDelete(options, userId);
// Assign a new primary class
if ( this.parent && (this.type === "class") && (userId === game.user.id) ) {
if ( this.id !== this.parent.data.data.details.originalClass ) return;
this.parent._assignPrimaryClass();
}
}
/* -------------------------------------------- */
/**
* Pre-creation logic for the automatic configuration of owned equipment type Items
* @private
*/
_onCreateOwnedEquipment(data, actorData, isNPC) {
const updates = {};
if ( foundry.utils.getProperty(data, "data.equipped") === undefined ) {
updates["data.equipped"] = isNPC; // NPCs automatically equip equipment
}
if ( foundry.utils.getProperty(data, "data.proficient") === undefined ) {
if ( isNPC ) {
updates["data.proficient"] = true; // NPCs automatically have equipment proficiency
} else {
const armorProf = CONFIG.SW5E.armorProficienciesMap[data.data?.armor?.type]; // Player characters check proficiency
const actorArmorProfs = actorData.data.traits?.armorProf?.value || [];
updates["data.proficient"] = (armorProf === true) || actorArmorProfs.includes(armorProf);
}
}
return updates;
}
/* -------------------------------------------- */
/**
* Pre-creation logic for the automatic configuration of owned power type Items
* @private
*/
_onCreateOwnedPower(data, actorData, isNPC) {
const updates = {};
updates["data.preparation.prepared"] = true; // Automatically prepare powers for everyone
return updates;
}
/* -------------------------------------------- */
/**
* Pre-creation logic for the automatic configuration of owned weapon type Items
* @private
*/
_onCreateOwnedWeapon(data, actorData, isNPC) {
const updates = {};
if ( foundry.utils.getProperty(data, "data.equipped") === undefined ) {
updates["data.equipped"] = isNPC; // NPCs automatically equip weapons
}
if ( foundry.utils.getProperty(data, "data.proficient") === undefined ) {
if ( isNPC ) {
updates["data.proficient"] = true; // NPCs automatically have equipment proficiency
} else {
// TODO: With the changes to make weapon proficiencies more verbose, this may need revising
const weaponProf = CONFIG.SW5E.weaponProficienciesMap[data.data?.weaponType]; // Player characters check proficiency
const actorWeaponProfs = actorData.data.traits?.weaponProf?.value || [];
updates["data.proficient"] = (weaponProf === true) || actorWeaponProfs.includes(weaponProf);
}
}
return updates;
}
/* -------------------------------------------- */
/* Factory Methods */
/* -------------------------------------------- */
// TODO: Make work properly
/**
* Create a consumable power scroll Item from a power Item.
* @param {Item5e} power The power to be made into a scroll
* @return {Item5e} The created scroll consumable item
* @private
*/
static async createScrollFromPower(power) {
@ -1380,7 +1599,7 @@ export default class Item5e extends Item {
const {actionType, description, source, activation, duration, target, range, damage, save, level} = itemData.data;
// Get scroll data
const scrollUuid = CONFIG.SW5E.powerScrollIds[level];
const scrollUuid = `Compendium.${CONFIG.SW5E.sourcePacks.ITEMS}.${CONFIG.SW5E.powerScrollIds[level]}`;
const scrollItem = await fromUuid(scrollUuid);
const scrollData = scrollItem.data;
delete scrollData._id;

View file

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

View file

@ -6,11 +6,11 @@ export const migrateWorld = async function() {
ui.notifications.info(`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
// Migrate World Actors
for await ( let a of game.actors.entities ) {
for await ( let a of game.actors.contents ) {
try {
console.log(`Checking Actor entity ${a.name} for migration needs`);
const updateData = await migrateActorData(a.data);
if ( !isObjectEmpty(updateData) ) {
if ( !foundry.utils.isObjectEmpty(updateData) ) {
console.log(`Migrating Actor entity ${a.name}`);
await a.update(updateData, {enforceTypes: false});
}
@ -21,10 +21,10 @@ export const migrateWorld = async function() {
}
// Migrate World Items
for ( let i of game.items.entities ) {
for ( let i of game.items.contents ) {
try {
const updateData = migrateItemData(i.data);
if ( !isObjectEmpty(updateData) ) {
const updateData = migrateItemData(i.toObject());
if ( !foundry.utils.isObjectEmpty(updateData) ) {
console.log(`Migrating Item entity ${i.name}`);
await i.update(updateData, {enforceTypes: false});
}
@ -35,12 +35,15 @@ export const migrateWorld = async function() {
}
// Migrate Actor Override Tokens
for ( let s of game.scenes.entities ) {
for ( let s of game.scenes.contents ) {
try {
const updateData = await migrateSceneData(s.data);
if ( !isObjectEmpty(updateData) ) {
if ( !foundry.utils.isObjectEmpty(updateData) ) {
console.log(`Migrating Scene entity ${s.name}`);
await s.update(updateData, {enforceTypes: false});
// If we do not do this, then synthetic token actors remain in cache
// with the un-updated actorData.
s.tokens.contents.forEach(t => t._actor = null);
}
} catch(err) {
err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
@ -77,40 +80,39 @@ export const migrateCompendium = async function(pack) {
// Begin by requesting server-side data model migration and get the migrated content
await pack.migrate();
const content = await pack.getContent();
const documents = await pack.getDocuments();
// Iterate over compendium entries - applying fine-tuned migration functions
for await ( let ent of content ) {
for await ( let doc of documents ) {
let updateData = {};
try {
switch (entity) {
case "Actor":
updateData = await migrateActorData(ent.data);
updateData = await migrateActorData(doc.data);
break;
case "Item":
updateData = migrateItemData(ent.data);
updateData = migrateItemData(doc.toObject());
break;
case "Scene":
updateData = await migrateSceneData(ent.data);
updateData = await migrateSceneData(doc.data);
break;
}
if ( isObjectEmpty(updateData) ) continue;
if ( foundry.utils.isObjectEmpty(updateData) ) continue;
// Save the entry, if data was changed
updateData["_id"] = ent._id;
await pack.updateEntity(updateData);
console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`);
await doc.update(updateData);
console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`);
}
// Handle migration failures
catch(err) {
err.message = `Failed sw5e system migration for entity ${ent.name} in pack ${pack.collection}: ${err.message}`;
err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`;
console.error(err);
}
}
// Apply the original locked status for the pack
pack.configure({locked: wasLocked});
await pack.configure({locked: wasLocked});
console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`);
};
@ -128,34 +130,39 @@ export const migrateActorData = async function(actor) {
const updateData = {};
// Actor Data Updates
if(actor.data) {
_migrateActorMovement(actor, updateData);
_migrateActorSenses(actor, updateData);
_migrateActorType(actor, updateData);
}
// Migrate Owned Items
if ( !!actor.items ) {
let hasItemUpdates = false;
const items = await actor.items.reduce(async (memo, i) => {
const results = await memo;
// Migrate the Owned Item
let itemUpdate = await migrateActorItemData(i, actor);
const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i;
let itemUpdate = await migrateActorItemData(itemData, actor);
// Prepared, Equipped, and Proficient for NPC actors
if ( actor.type === "npc" ) {
if (getProperty(i.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
if (getProperty(i.data, "equipped") === false) itemUpdate["data.equipped"] = true;
if (getProperty(i.data, "proficient") === false) itemUpdate["data.proficient"] = true;
if (getProperty(itemData.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true;
if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true;
}
// Update the Owned Item
if ( !isObjectEmpty(itemUpdate) ) {
hasItemUpdates = true;
itemUpdate._id = itemData._id;
console.log(`Migrating Actor ${actor.name}'s ${i.name}`);
return [...results, mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false})];
} else return [...results, i];
results.push(expandObject(itemUpdate));
}
return results;
}, []);
if ( hasItemUpdates ) updateData.items = items;
if ( items.length > 0 ) updateData.items = items;
}
// Update NPC data with new datamodel information
@ -201,7 +208,9 @@ function cleanActorData(actorData) {
/**
* Migrate a single Item entity to incorporate latest data model changes
* @param item
*
* @param {object} item Item data to migrate
* @return {object} The updateData to apply
*/
export const migrateItemData = function(item) {
const updateData = {};
@ -234,24 +243,33 @@ export const migrateActorItemData = async function(item, actor) {
* @return {Object} The updateData to apply
*/
export const migrateSceneData = async function(scene) {
const tokens = duplicate(scene.tokens);
return {
tokens: await Promise.all(tokens.map(async (t) => {
if (!t.actorId || t.actorLink || !t.actorData.data) {
const tokens = await Promise.all(scene.tokens.map(async token => {
const t = token.toJSON();
if (!t.actorId || t.actorLink) {
t.actorData = {};
return t;
}
const token = new Token(t);
if ( !token.actor ) {
else if (!game.actors.has(t.actorId)) {
t.actorId = null;
t.actorData = {};
} else if ( !t.actorLink ) {
const updateData = await migrateActorData(token.data.actorData);
t.actorData = mergeObject(token.data.actorData, updateData);
const actorData = duplicate(t.actorData);
actorData.type = token.actor?.type;
const update = migrateActorData(actorData);
['items', 'effects'].forEach(embeddedName => {
if (!update[embeddedName]?.length) return;
const updates = new Map(update[embeddedName].map(u => [u._id, u]));
t.actorData[embeddedName].forEach(original => {
const update = updates.get(original._id);
if (update) mergeObject(original, update);
});
delete update[embeddedName];
});
mergeObject(t.actorData, update);
}
return t;
}))
};
}));
return {tokens};
};
/* -------------------------------------------- */
@ -366,16 +384,16 @@ function _migrateActorPowers(actorData, updateData) {
updateData["data.attributes.force.points.value"] = 0;
updateData["data.attributes.force.points.min"] = 0;
updateData["data.attributes.force.points.max"] = 0;
updateData["data.attributes.force.points.temp"] = 0;
updateData["data.attributes.force.points.tempmax"] = 0;
updateData["data.attributes.force.points.temp"] = null;
updateData["data.attributes.force.points.tempmax"] = null;
updateData["data.attributes.force.level"] = 0;
updateData["data.attributes.tech.known.value"] = 0;
updateData["data.attributes.tech.known.max"] = 0;
updateData["data.attributes.tech.points.value"] = 0;
updateData["data.attributes.tech.points.min"] = 0;
updateData["data.attributes.tech.points.max"] = 0;
updateData["data.attributes.tech.points.temp"] = 0;
updateData["data.attributes.tech.points.tempmax"] = 0;
updateData["data.attributes.tech.points.temp"] = null;
updateData["data.attributes.tech.points.tempmax"] = null;
updateData["data.attributes.tech.level"] = 0;
}
@ -420,6 +438,7 @@ function _migrateActorSenses(actor, updateData) {
const ad = actor.data;
if ( ad?.traits?.senses === undefined ) return;
const original = ad.traits.senses || "";
if ( typeof original !== "string" ) return;
// Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/;
@ -449,6 +468,85 @@ 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
*/
@ -456,19 +554,35 @@ function _migrateItemClassPowerCasting(item, updateData) {
if (item.type === "class"){
switch (item.name){
case "Consular":
updateData["data.powercasting"] = "consular";
updateData["data.powercasting"] = {
progression: "consular",
ability: ""
};
break;
case "Engineer":
updateData["data.powercasting"] = "engineer";
updateData["data.powercasting"] = {
progression: "engineer",
ability: ""
};
break;
case "Guardian":
updateData["data.powercasting"] = "guardian";
updateData["data.powercasting"] = {
progression: "guardian",
ability: ""
};
break;
case "Scout":
updateData["data.powercasting"] = "scout";
updateData["data.powercasting"] = {
progression: "scout",
ability: ""
};
break;
case "Sentinel":
updateData["data.powercasting"] = "sentinel";
updateData["data.powercasting"] = {
progression: "sentinel",
ability: ""
};
break;
}
}
@ -526,10 +640,14 @@ async function _migrateItemPower(item, actor, updateData) {
/**
* 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
*/
function _migrateItemAttunement(item, updateData) {
if ( item.data.attuned === undefined ) return;
if ( item.data?.attuned === undefined ) return updateData;
updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE;
updateData["data.-=attuned"] = null;
return updateData;

View file

@ -19,7 +19,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
// Prepare template data
const templateData = {
t: templateShape,
user: game.user._id,
user: game.user.data._id,
distance: target.value,
direction: 0,
x: 0,
@ -45,10 +45,12 @@ export default class AbilityTemplate extends MeasuredTemplate {
}
// Return the template constructed from the item data
const template = new this(templateData);
template.item = item;
template.actorSheet = item.actor?.sheet || null;
return template;
const cls = CONFIG.MeasuredTemplate.documentClass;
const template = new cls(templateData, {parent: canvas.scene});
const object = new this(template);
object.item = item;
object.actorSheet = item.actor?.sheet || null;
return object;
}
/* -------------------------------------------- */
@ -88,8 +90,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
if ( now - moveTime <= 20 ) return;
const center = event.data.getLocalPosition(this.layer);
const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2);
this.data.x = snapped.x;
this.data.y = snapped.y;
this.data.update({x: snapped.x, y: snapped.y});
this.refresh();
moveTime = now;
};
@ -108,14 +109,9 @@ export default class AbilityTemplate extends MeasuredTemplate {
// Confirm the workflow (left-click)
handlers.lc = event => {
handlers.rc(event);
// Confirm final snapped position
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);
const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
this.data.update(destination);
canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]);
};
// Rotate the template by 3 degree increments (mouse-wheel)
@ -124,7 +120,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
event.stopPropagation();
let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
let snap = event.shiftKey ? delta : 5;
this.data.direction += (snap * Math.sign(event.deltaY));
this.data.update({direction: this.data.direction + (snap * Math.sign(event.deltaY))});
this.refresh();
};

View file

@ -8,7 +8,7 @@ export const registerSystemSettings = function() {
scope: "world",
config: false,
type: String,
default: ""
default: game.system.data.version
});
/**

View file

@ -18,6 +18,7 @@ export const preloadHandlebarsTemplates = async function() {
"systems/sw5e/templates/actors/newActor/parts/swalt-biography.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-core.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-crew.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-features.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html",

99
module/token.js Normal file
View file

@ -0,0 +1,99 @@
/**
* 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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

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