Compare commits

..

11 commits

Author SHA1 Message Date
TJ
b701662dbe Initial commit for getting deployments to display 2021-06-11 23:30:06 -05:00
supervj
516e6a0bd1 Created Starship Deployment on actor drop
This lets actors drop onto ships.  It mostly works data wise.  Tokens are stored separately from characters at this point, need to bring that part in from polymorph.
Actors dropped do not appear on character sheet
2021-06-11 16:08:15 -04:00
supervj
18f11013c1 Attached equipment to starships
calculated attributes based on armor, shields, power coupling, hyperdrive, and reactor.
2021-06-10 23:38:05 -04:00
supervj
0ac4d45302 Connect derived data to starships
another update to connect derived data to starships sheet only based on size so far.
2021-06-09 17:29:04 -04:00
supervj
a1a7843454 Further Updates to starship sheet
With the help of Cyr we made some progress

Connected fuel burn and refuel
connected power die
connected power routing

added debug handlebar helper to help see scope of html pages
2021-06-09 01:16:38 -04:00
supervj
de6d3d4eb7 Fix Upgrade cost and some Starship actor sheet data
updated data to be inline with new structure, but not seeing it transfer through.  need to figure out why.
2021-06-04 17:02:30 -04:00
supervj
22c962d4d3 Update Starship Data with new structure
Updated to bring actor data inline with the starship class.  Still need to do derived data for starship
2021-06-04 16:21:21 -04:00
supervj
4cae2c0de3 Make Starship item type fields visible
getting fields on the form, probably broke starships because I changed names and locations in the template for ease of readability and consistency
2021-05-26 01:09:02 -04:00
supervj
0be98ab20c More work on starship sizes as "classes"
getting there, this seems to be droppable, but doesn't show on sheet yet.
2021-05-22 01:05:50 -04:00
supervj
b3344ba8b9 Integrate changes from the Prof
Received as-is changes from the Prof.
2021-05-21 14:58:11 -04:00
supervj
cacc43740e Flesh out New Dice structure - Not complete
add structure for Hull, Shield, and Power Dice to allow for recharge, refitting, and regeneration.  Not complete.
2021-05-20 03:45:18 -04:00
192 changed files with 13736 additions and 18145 deletions

3
.gitignore vendored
View file

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

View file

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

View file

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

View file

@ -19,6 +19,7 @@
"ITEM.TypeLoot": "Loot", "ITEM.TypeLoot": "Loot",
"ITEM.TypePower": "Power", "ITEM.TypePower": "Power",
"ITEM.TypeSpecies": "Species", "ITEM.TypeSpecies": "Species",
"ITEM.TypeStarship": "Starship",
"ITEM.TypeStarshipfeature": "Starship Feature", "ITEM.TypeStarshipfeature": "Starship Feature",
"ITEM.TypeStarshipfeaturePl": "Starship Features", "ITEM.TypeStarshipfeaturePl": "Starship Features",
"ITEM.TypeStarshipmod": "Starship Modification", "ITEM.TypeStarshipmod": "Starship Modification",
@ -43,13 +44,11 @@
"SETTINGS.5eInitTBN": "Initiative Dexterity Tiebreaker", "SETTINGS.5eInitTBN": "Initiative Dexterity Tiebreaker",
"SETTINGS.5eNoExpL": "Remove experience bars from character sheets.", "SETTINGS.5eNoExpL": "Remove experience bars from character sheets.",
"SETTINGS.5eNoExpN": "Disable Experience Tracking", "SETTINGS.5eNoExpN": "Disable Experience Tracking",
"SETTINGS.5eReset": "Reset",
"SETTINGS.5eRestEpic": "Epic Heroism (LR: 1 hour, SR: 1 min)", "SETTINGS.5eRestEpic": "Epic Heroism (LR: 1 hour, SR: 1 min)",
"SETTINGS.5eRestGritty": "Gritty Realism (LR: 7 days, SR: 8 hours)", "SETTINGS.5eRestGritty": "Gritty Realism (LR: 7 days, SR: 8 hours)",
"SETTINGS.5eRestL": "Configure which rest variant should be used for games within this system.", "SETTINGS.5eRestL": "Configure which rest variant should be used for games within this system.",
"SETTINGS.5eRestN": "Rest Variant", "SETTINGS.5eRestN": "Rest Variant",
"SETTINGS.5eRestPHB": "Player's Handbook (LR: 8 hours, SR: 1 hour)", "SETTINGS.5eRestPHB": "Player's Handbook (LR: 8 hours, SR: 1 hour)",
"SETTINGS.5eUndoChanges": "Undo Changes",
"SETTINGS.SWColorDark": "Dark Theme", "SETTINGS.SWColorDark": "Dark Theme",
"SETTINGS.SWColorL": "Set the color theme of the game", "SETTINGS.SWColorL": "Set the color theme of the game",
"SETTINGS.SWColorLight": "Light Theme", "SETTINGS.SWColorLight": "Light Theme",
@ -80,7 +79,6 @@
"SW5E.AbilityUseCast": "Cast Power", "SW5E.AbilityUseCast": "Cast Power",
"SW5E.AbilityUseChargedHint": "This {type} is charged and ready to use!", "SW5E.AbilityUseChargedHint": "This {type} is charged and ready to use!",
"SW5E.AbilityUseChargesLabel": "{value} Charges", "SW5E.AbilityUseChargesLabel": "{value} Charges",
"SW5E.AbilityUseConfig": "Usage Configuration",
"SW5E.AbilityUseConsumableChargeHint": "Using this {type} will consume 1 charge of {value} remaining.", "SW5E.AbilityUseConsumableChargeHint": "Using this {type} will consume 1 charge of {value} remaining.",
"SW5E.AbilityUseConsumableDestroyHint": "Using this {type} will consume its final charge and it will be destroyed.", "SW5E.AbilityUseConsumableDestroyHint": "Using this {type} will consume its final charge and it will be destroyed.",
"SW5E.AbilityUseConsumableLabel": "{max} per {per}", "SW5E.AbilityUseConsumableLabel": "{max} per {per}",
@ -107,9 +105,7 @@
"SW5E.ActionUtil": "Utility", "SW5E.ActionUtil": "Utility",
"SW5E.ActionWarningNoItem": "The requested item {item} no longer exists on Actor {name}", "SW5E.ActionWarningNoItem": "The requested item {item} no longer exists on Actor {name}",
"SW5E.ActionWarningNoToken": "You must have one or more controlled Tokens in order to use this option.", "SW5E.ActionWarningNoToken": "You must have one or more controlled Tokens in order to use this option.",
"SW5E.ActorWarningInvalidItem": "{itemType} items cannot be added to a {actorType}.",
"SW5E.Add": "Add", "SW5E.Add": "Add",
"SW5E.AddEmbeddedItemPromptHint": "Do you want to add these items to your character sheet?",
"SW5E.AdditionalNotes": "Additional Notes", "SW5E.AdditionalNotes": "Additional Notes",
"SW5E.Advantage": "Advantage", "SW5E.Advantage": "Advantage",
"SW5E.Alignment": "Alignment", "SW5E.Alignment": "Alignment",
@ -123,7 +119,6 @@
"SW5E.AlignmentND": "Neutral Dark", "SW5E.AlignmentND": "Neutral Dark",
"SW5E.AlignmentNL": "Neutral Light", "SW5E.AlignmentNL": "Neutral Light",
"SW5E.Appearance": "Appearance", "SW5E.Appearance": "Appearance",
"SW5E.Apply": "Apply",
"SW5E.ArchetypeName": "Archetype Name", "SW5E.ArchetypeName": "Archetype Name",
"SW5E.Archetypes": "Archetypes", "SW5E.Archetypes": "Archetypes",
"SW5E.ArmorClass": "Armor Class", "SW5E.ArmorClass": "Armor Class",
@ -205,11 +200,7 @@
"SW5E.ChatContextHealing": "Apply Healing", "SW5E.ChatContextHealing": "Apply Healing",
"SW5E.ChatFlavor": "Chat Message Flavor", "SW5E.ChatFlavor": "Chat Message Flavor",
"SW5E.ClassLevels": "Class Levels", "SW5E.ClassLevels": "Class Levels",
"SW5E.ClassMakeOriginal": "Original Class",
"SW5E.ClassMakeOriginalHint": "First class taken by character used to determine certain class traits when multiclassing.",
"SW5E.ClassName": "Class Name", "SW5E.ClassName": "Class Name",
"SW5E.ClassOriginal": "Original Class",
"SW5E.ClassSaves": "Saving Throws",
"SW5E.ClassSkillsChosen": "Chosen Class Skills", "SW5E.ClassSkillsChosen": "Chosen Class Skills",
"SW5E.ClassSkillsNumber": "Number of Starting Skills", "SW5E.ClassSkillsNumber": "Number of Starting Skills",
"SW5E.Collapse": "Collapse/Expand", "SW5E.Collapse": "Collapse/Expand",
@ -271,32 +262,7 @@
"SW5E.CoverHalf": "Half", "SW5E.CoverHalf": "Half",
"SW5E.CoverThreeQuarters": "Three Quarters", "SW5E.CoverThreeQuarters": "Three Quarters",
"SW5E.CoverTotal": "Total", "SW5E.CoverTotal": "Total",
"SW5E.CreatureAberration": "Aberration", "SW5E.CrewCap": "Minimum Crew Capacity",
"SW5E.CreatureAberrationPl": "Aberrations",
"SW5E.CreatureBeast": "Beast",
"SW5E.CreatureBeastPl": "Beasts",
"SW5E.CreatureConstruct": "Construct",
"SW5E.CreatureConstructPl": "Constructs",
"SW5E.CreatureDroid": "Droid",
"SW5E.CreatureDroidPl": "Droids",
"SW5E.CreatureForceEntity": "Force Entity",
"SW5E.CreatureForceEntityPl": "Force Entities",
"SW5E.CreatureHumanoid": "Humanoid",
"SW5E.CreatureHumanoidPl": "Humanoids",
"SW5E.CreaturePlant": "Plant",
"SW5E.CreaturePlantPl": "Plants",
"SW5E.CreatureSwarm": "Swarm",
"SW5E.CreatureSwarmPhrase": "Swarm of {size} {type}",
"SW5E.CreatureSwarmSize": "Swarm Size",
"SW5E.CreatureType": "Creature Type",
"SW5E.CreatureTypeConfig": "Configure Creature Type",
"SW5E.CreatureTypeSelectorCustom": "Custom Type",
"SW5E.CreatureTypeSelectorSubtype": "Subtype",
"SW5E.CreatureTypeTitle": "Configure Creature Type",
"SW5E.CreatureUndead": "Undead",
"SW5E.CreatureUndeadPl": "Undead",
"SW5E.CrewCap": "Crew Capacity",
"SW5E.Crewed": "Crewed",
"SW5E.Critical": "Critical", "SW5E.Critical": "Critical",
"SW5E.CriticalHit": "Critical Hit", "SW5E.CriticalHit": "Critical Hit",
"SW5E.Currency": "Currency", "SW5E.Currency": "Currency",
@ -318,6 +284,7 @@
"SW5E.DamageRoll": "Damage Roll", "SW5E.DamageRoll": "Damage Roll",
"SW5E.DamageSonic": "Sonic", "SW5E.DamageSonic": "Sonic",
"SW5E.DamImm": "Damage Immunities", "SW5E.DamImm": "Damage Immunities",
"SW5E.DmgRed": "Damage Reduction",
"SW5E.DamRes": "Damage Resistances", "SW5E.DamRes": "Damage Resistances",
"SW5E.DamVuln": "Damage Vulnerabilities", "SW5E.DamVuln": "Damage Vulnerabilities",
"SW5E.DarkPowerDC": "Dark Power DC", "SW5E.DarkPowerDC": "Dark Power DC",
@ -331,7 +298,17 @@
"SW5E.Default": "Default", "SW5E.Default": "Default",
"SW5E.DefaultAbilityCheck": "Default Ability Check", "SW5E.DefaultAbilityCheck": "Default Ability Check",
"SW5E.Deployment": "Deployment", "SW5E.Deployment": "Deployment",
"SW5E.DeploymentAcceptSettings": "Deploy Actor",
"SW5E.DeploymentPl": "Deployments", "SW5E.DeploymentPl": "Deployments",
"SW5E.DeploymentPromptTitle": "Deploying Actor",
"SW5E.DeploymentTypeCoordinator": "Coordinator",
"SW5E.DeploymentTypeCrew": "Crew",
"SW5E.DeploymentTypeGunner": "Gunner",
"SW5E.DeploymentTypeMechanic": "Mechanic",
"SW5E.DeploymentTypeOperator": "Operator",
"SW5E.DeploymentTypePilot": "Pilot",
"SW5E.DeploymentTypePassenger" : "Passenger",
"SW5E.DeploymentTypeTechnician": "Technician",
"SW5E.description": "A comprehensive game system for running games of Star Wars 5th Edition in the Foundry VTT environment.", "SW5E.description": "A comprehensive game system for running games of Star Wars 5th Edition in the Foundry VTT environment.",
"SW5E.Description": "Description", "SW5E.Description": "Description",
"SW5E.DestructionSave": "Destruction Saves", "SW5E.DestructionSave": "Destruction Saves",
@ -343,29 +320,24 @@
"SW5E.DistMi": "Miles", "SW5E.DistMi": "Miles",
"SW5E.DistSelf": "Self", "SW5E.DistSelf": "Self",
"SW5E.DistTouch": "Touch", "SW5E.DistTouch": "Touch",
"SW5E.DmgRed": "Damage Reduction",
"SW5E.Duration": "Duration", "SW5E.Duration": "Duration",
"SW5E.EffectsCategoryTemporary": "Temporary Effects",
"SW5E.EffectsCategoryPassive": "Passive Effects",
"SW5E.EffectsCategoryInactive": "Inactive Effects",
"SW5E.EffectCreate": "Create Effect", "SW5E.EffectCreate": "Create Effect",
"SW5E.EffectDelete": "Delete Effect", "SW5E.EffectDelete": "Delete Effect",
"SW5E.EffectEdit": "Edit Effect", "SW5E.EffectEdit": "Edit Effect",
"SW5E.EffectInactive": "Inactive Effects",
"SW5E.EffectNew": "New Effect",
"SW5E.EffectPassive": "Passive Effects",
"SW5E.Effects": "Effects", "SW5E.Effects": "Effects",
"SW5E.EffectTemporary": "Temporary Effects",
"SW5E.EffectsCategoryInactive": "Inactive Effects",
"SW5E.EffectsCategoryPassive": "Passive Effects",
"SW5E.EffectsCategoryTemporary": "Temporary Effects",
"SW5E.EffectToggle": "Toggle Effect", "SW5E.EffectToggle": "Toggle Effect",
"SW5E.Engine": "Engine", "SW5E.Engine": "Engine",
"SW5E.EnginePl": "Engines", "SW5E.EnginePl": "Engines",
"SW5E.EquipmentBonus": "Magical Bonus", "SW5E.EquipmentBonus": "Magical Bonus",
"SW5E.EquipmentClothing": "Clothing", "SW5E.EquipmentClothing": "Clothing",
"SW5E.EquipmentHeavy": "Heavy Armor", "SW5E.EquipmentHeavy": "Heavy Armor",
"SW5E.EquipmentHyperdrive": "Hyperdrive",
"SW5E.EquipmentLight": "Light Armor", "SW5E.EquipmentLight": "Light Armor",
"SW5E.EquipmentMedium": "Medium Armor", "SW5E.EquipmentMedium": "Medium Armor",
"SW5E.EquipmentNatural": "Natural Armor", "SW5E.EquipmentNatural": "Natural Armor",
"SW5E.EquipmentHyperdrive": "Hyperdrive",
"SW5E.EquipmentPowerCoupling": "Power Coupling", "SW5E.EquipmentPowerCoupling": "Power Coupling",
"SW5E.EquipmentReactor": "Reactor", "SW5E.EquipmentReactor": "Reactor",
"SW5E.EquipmentShield": "Shield", "SW5E.EquipmentShield": "Shield",
@ -378,9 +350,8 @@
"SW5E.Exhaustion": "Exhaustion", "SW5E.Exhaustion": "Exhaustion",
"SW5E.Expand": "Expand", "SW5E.Expand": "Expand",
"SW5E.Expertise": "Expertise", "SW5E.Expertise": "Expertise",
"SW5E.Favorites": "Favorites", "SW5E.Favorites": "Favoris",
"SW5E.FavoritesAndNotes": "Favorites & Notes", "SW5E.FavoritesAndNotes": "Favorites & Notes",
"SW5E.Feats": "Feats",
"SW5E.FeatureActionRecharge": "Action Recharge", "SW5E.FeatureActionRecharge": "Action Recharge",
"SW5E.FeatureActive": "Active Abilities", "SW5E.FeatureActive": "Active Abilities",
"SW5E.FeatureAdd": "Create Feature", "SW5E.FeatureAdd": "Create Feature",
@ -391,8 +362,8 @@
"SW5E.FeatureRechargeOn": "Recharge On", "SW5E.FeatureRechargeOn": "Recharge On",
"SW5E.FeatureRechargeResult": "1d6 Result", "SW5E.FeatureRechargeResult": "1d6 Result",
"SW5E.Features": "Features", "SW5E.Features": "Features",
"SW5E.FeatureType": "Feature Type",
"SW5E.FeatureUsage": "Feature Usage", "SW5E.FeatureUsage": "Feature Usage",
"SW5E.FeatureType": "Feature Type",
"SW5E.FeetAbbr": "ft.", "SW5E.FeetAbbr": "ft.",
"SW5E.Filter": "Filter", "SW5E.Filter": "Filter",
"SW5E.FilterNoPowers": "No powers found for this set of filters.", "SW5E.FilterNoPowers": "No powers found for this set of filters.",
@ -522,27 +493,26 @@
"SW5E.ForcePowerbook": "Force Powers", "SW5E.ForcePowerbook": "Force Powers",
"SW5E.Formula": "Formula", "SW5E.Formula": "Formula",
"SW5E.FuelCapacity": "Fuel Capacity", "SW5E.FuelCapacity": "Fuel Capacity",
"SW5E.FuelCostPerUnit": "Fuel Cost per Unit",
"SW5E.FuelCostsMod": "Fuel Costs Modifier", "SW5E.FuelCostsMod": "Fuel Costs Modifier",
"SW5E.FuelCostPerUnit": "Fuel Cost per Unit",
"SW5E.GrantedAbilities": "Granted Abilities", "SW5E.GrantedAbilities": "Granted Abilities",
"SW5E.HalfProficient": "Half Proficient", "SW5E.HalfProficient": "Half Proficient",
"SW5E.HardpointSizeMod": "Hardpoint Size Modifier", "SW5E.HardpointsPerRound": "Max Hardpoints Fired Per Round",
"SW5E.Healing": "Healing", "SW5E.Healing": "Healing",
"SW5E.HealingTemp": "Healing (Temporary)", "SW5E.HealingTemp": "Healing (Temporary)",
"SW5E.Health": "Health", "SW5E.Health": "Health",
"SW5E.HealthConditions": "Health Conditions", "SW5E.HealthConditions": "Health Conditions",
"SW5E.HealthFormula": "Health Formula", "SW5E.HealthFormula": "Health Formula",
"SW5E.HitDice": "Hit Dice", "SW5E.HitDice": "Hit Dice",
"SW5E.HitDiceConfig": "Adjust Hit Dice",
"SW5E.HitDiceConfigHint": "Adjust remaining hit dice levels for each class.",
"SW5E.HitDiceMax": "Maximum Hit Dice",
"SW5E.HitDiceRemaining": "Remaining Hit Dice",
"SW5E.HitDiceRoll": "Roll Hit Dice", "SW5E.HitDiceRoll": "Roll Hit Dice",
"SW5E.HitDiceUsed": "Hit Dice Used", "SW5E.HitDiceUsed": "Hit Dice Used",
"SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!", "SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!",
"SW5E.HP": "Health", "SW5E.HP": "Health",
"SW5E.HPFormula": "Health Formula", "SW5E.HPFormula": "Health Formula",
"SW5E.HullDice": "Hull Dice", "SW5E.HullDice": "Hull Dice",
"SW5E.HullDiceRoll": "Roll Hull Dice",
"SW5E.HullDiceUsed": "Hull Dice Used",
"SW5E.HullDiceWarn": "{name} has no available {formula} Hull Dice remaining!",
"SW5E.HullPoints": "Hull Points", "SW5E.HullPoints": "Hull Points",
"SW5E.HullPointsFormula": "Hull Points Formula", "SW5E.HullPointsFormula": "Hull Points Formula",
"SW5E.HyperdriveClass": "Hyperdrive Class", "SW5E.HyperdriveClass": "Hyperdrive Class",
@ -589,7 +559,6 @@
"SW5E.ItemTypeArchetype": "Archetype", "SW5E.ItemTypeArchetype": "Archetype",
"SW5E.ItemTypeBackground": "Background", "SW5E.ItemTypeBackground": "Background",
"Sw5E.ItemTypeBackgroundPl": "Backgrounds", "Sw5E.ItemTypeBackgroundPl": "Backgrounds",
"SW5E.ItemTypeBackpack": "Container",
"SW5E.ItemTypeClass": "Class", "SW5E.ItemTypeClass": "Class",
"SW5E.ItemTypeClassFeat": "Class Feature", "SW5E.ItemTypeClassFeat": "Class Feature",
"SW5E.ItemTypeClassFeats": "Class Features", "SW5E.ItemTypeClassFeats": "Class Features",
@ -599,9 +568,9 @@
"SW5E.ItemTypeContainer": "Container", "SW5E.ItemTypeContainer": "Container",
"SW5E.ItemTypeContainerPl": "Containers", "SW5E.ItemTypeContainerPl": "Containers",
"SW5E.ItemTypeDeployment": "Deployment", "SW5E.ItemTypeDeployment": "Deployment",
"SW5E.ItemTypeDeploymentPl": "Deployments",
"SW5E.ItemTypeDeploymentFeature": "Deployment Feature", "SW5E.ItemTypeDeploymentFeature": "Deployment Feature",
"SW5E.ItemTypeDeploymentFeaturePl": "Deployment Features", "SW5E.ItemTypeDeploymentFeaturePl": "Deployment Features",
"SW5E.ItemTypeDeploymentPl": "Deployments",
"SW5E.ItemTypeEquipment": "Equipment", "SW5E.ItemTypeEquipment": "Equipment",
"SW5E.ItemTypeEquipmentPl": "Equipment", "SW5E.ItemTypeEquipmentPl": "Equipment",
"SW5E.ItemTypeFeat": "Feat", "SW5E.ItemTypeFeat": "Feat",
@ -618,6 +587,7 @@
"SW5E.ItemTypePowerPl": "Powers", "SW5E.ItemTypePowerPl": "Powers",
"SW5E.ItemTypeSpecies": "Species", "SW5E.ItemTypeSpecies": "Species",
"SW5E.ItemTypeSpeciesPl": "Species", "SW5E.ItemTypeSpeciesPl": "Species",
"SW5E.ItemTypeStarship": "Starship",
"SW5E.ItemTypeStarshipMod": "Starship Modification", "SW5E.ItemTypeStarshipMod": "Starship Modification",
"SW5E.ItemTypeStarshipModPl": "Starship Modifications", "SW5E.ItemTypeStarshipModPl": "Starship Modifications",
"SW5E.ItemTypeTool": "Tool", "SW5E.ItemTypeTool": "Tool",
@ -756,7 +726,6 @@
"SW5E.LongRest": "Long Rest", "SW5E.LongRest": "Long Rest",
"SW5E.LongRestEpic": "Long Rest (1 hour)", "SW5E.LongRestEpic": "Long Rest (1 hour)",
"SW5E.LongRestGritty": "Long Rest (7 days)", "SW5E.LongRestGritty": "Long Rest (7 days)",
"SW5E.LongRestHint": "Take a long rest? On a long rest you will recover hit points, half your maximum hit dice, class resources, limited use item charges, and power points.",
"SW5E.LongRestNormal": "Long Rest (8 hours)", "SW5E.LongRestNormal": "Long Rest (8 hours)",
"SW5E.LongRestOvernight": "Long Rest (New Day)", "SW5E.LongRestOvernight": "Long Rest (New Day)",
"SW5E.LongRestResult": "{name} takes a long rest.", "SW5E.LongRestResult": "{name} takes a long rest.",
@ -776,8 +745,8 @@
"SW5E.LongRestResultTP": "{name} takes a long rest and recovers {tech} Tech Points.", "SW5E.LongRestResultTP": "{name} takes a long rest and recovers {tech} Tech Points.",
"SW5E.LongRestResultTPHD": "{name} takes a long rest and recovers {tech} Tech Points and {dice} Hit Dice.", "SW5E.LongRestResultTPHD": "{name} takes a long rest and recovers {tech} Tech Points and {dice} Hit Dice.",
"SW5E.Max": "Max", "SW5E.Max": "Max",
"SW5E.ModCap": "Modification Capacity",
"SW5E.Modifier": "Modifier", "SW5E.Modifier": "Modifier",
"SW5E.ModCap": "Modification Capacity",
"SW5E.Movement": "Movement", "SW5E.Movement": "Movement",
"SW5E.MovementBurrow": "Burrow", "SW5E.MovementBurrow": "Burrow",
"SW5E.MovementClimb": "Climb", "SW5E.MovementClimb": "Climb",
@ -793,8 +762,6 @@
"SW5E.MovementUnits": "Units", "SW5E.MovementUnits": "Units",
"SW5E.MovementWalk": "Walk", "SW5E.MovementWalk": "Walk",
"SW5E.Name": "Character Name", "SW5E.Name": "Character Name",
"SW5E.NewDay": "Is New Day?",
"SW5E.NewDayHint": "Recover limited use abilities which recharge \"per day\"?",
"SW5E.NoCharges": "No Charges", "SW5E.NoCharges": "No Charges",
"SW5E.None": "None", "SW5E.None": "None",
"SW5E.NoPowerLevels": "This character has no powercaster levels, but you may add powers manually.", "SW5E.NoPowerLevels": "This character has no powercaster levels, but you may add powers manually.",
@ -847,12 +814,11 @@
"SW5E.PowerCreate": "Create Power", "SW5E.PowerCreate": "Create Power",
"SW5E.PowerDC": "Power DC", "SW5E.PowerDC": "Power DC",
"SW5E.PowerDetails": "Power Details", "SW5E.PowerDetails": "Power Details",
"SW5E.PowerDiceRecovery": "Power Dice Recovery",
"SW5E.PowerDie": "Power Die", "SW5E.PowerDie": "Power Die",
"SW5E.PowerDieAlloc": "Power Die Allocation",
"SW5E.PowerDiePl": "Power Dice", "SW5E.PowerDiePl": "Power Dice",
"SW5E.PowerDieAlloc": "Power Die Allocation",
"SW5E.PowerDiceRecovery": "Power Dice Recovery",
"SW5E.PowerEffects": "Power Effects", "SW5E.PowerEffects": "Power Effects",
"SW5E.PowerfulCritical": "Powerful Critical",
"SW5E.PowerLevel": "Power Level", "SW5E.PowerLevel": "Power Level",
"SW5E.PowerLevel0": "At-Will", "SW5E.PowerLevel0": "At-Will",
"SW5E.PowerLevel1": "1st Level", "SW5E.PowerLevel1": "1st Level",
@ -882,9 +848,9 @@
"SW5E.PowerProgression": "Power Progression", "SW5E.PowerProgression": "Power Progression",
"SW5E.PowerProgSct": "Scout", "SW5E.PowerProgSct": "Scout",
"SW5E.PowerProgSnt": "Sentinel", "SW5E.PowerProgSnt": "Sentinel",
"SW5E.PowerRouting": "Power Routing",
"SW5E.PowerSchool": "Power School", "SW5E.PowerSchool": "Power School",
"SW5E.PowersKnown": "Powers Known", "SW5E.PowersKnown": "Powers Known",
"SW5E.PowerRouting": "Power Routing",
"SW5E.PowerTarget": "Power Target", "SW5E.PowerTarget": "Power Target",
"SW5E.PowerUnprepared": "Unprepared", "SW5E.PowerUnprepared": "Unprepared",
"SW5E.PowerUsage": "Power Usage", "SW5E.PowerUsage": "Power Usage",
@ -900,23 +866,38 @@
"SW5E.Reaction": "Reaction", "SW5E.Reaction": "Reaction",
"SW5E.ReactionPl": "Reactions", "SW5E.ReactionPl": "Reactions",
"SW5E.Recharge": "Recharge", "SW5E.Recharge": "Recharge",
"SW5E.RechargeRestHint": "Take a recharge rest? On a recharge rest you may spend remaining Hull Dice and recover Shields.",
"SW5E.RechargetRestNoHD": "No Hull Dice remaining",
"SW5E.RechargeRestNormal": "Recharge Rest (1 hour)",
"SW5E.RechargeRestOvernight": "Recharge Rest (New Day)",
"SW5E.RechargeRestResult": "{name} takes a recharge rest spending {dice} Hull Dice to recover {health} Hull Points.",
"SW5E.RechargeRestResultShort": "{name} takes a recharge rest.",
"SW5E.RechargeRestSelect": "Select Dice to Roll",
"SW5E.Refitting": "Refitting", "SW5E.Refitting": "Refitting",
"SW5E.RefittingRest": "Refitting Rest",
"SW5E.RefittingRestEpic": "Refitting Rest (1 hour)",
"SW5E.RefittingRestGritty": "Refitting Rest (7 days)",
"SW5E.RefittingRestNormal": "Refitting Rest (8 hours)",
"SW5E.RefittingRestOvernight": "Refitting Rest (New Day)",
"SW5E.RefittingRestResult": "{name} takes a refitting rest.",
"SW5E.RefittingRestResultHD": "{name} takes a refitting rest and recovers {dice} Hull Dice.",
"SW5E.RefittingRestResultHP": "{name} takes a refitting rest and recovers {health} Hull Points.",
"SW5E.RefittingRestResultHPHD": "{name} takes a refitting rest and recovers {health} Hull Points and {dice} Hull Dice.",
"SW5E.Refuel": "Refuel", "SW5E.Refuel": "Refuel",
"SW5E.RegenerationRateCoefficient": "Regeneration Rate Coefficient", "SW5E.RegenerationRateCoefficient": "Regeneration Rate Coefficient",
"SW5E.RequiredMaterials": "Required Materials", "SW5E.RequiredMaterials": "Required Materials",
"SW5E.Requirements": "Requirements", "SW5E.Requirements": "Requirements",
"SW5E.ResourcePrimary": "Resource 1",
"SW5E.ResourcesAndTraits": "Resources & Traits", "SW5E.ResourcesAndTraits": "Resources & Traits",
"SW5E.ResourcePrimary": "Resource 1",
"SW5E.ResourceSecondary": "Resource 2", "SW5E.ResourceSecondary": "Resource 2",
"SW5E.ResourceTertiary": "Resource 3", "SW5E.ResourceTertiary": "Resource 3",
"SW5E.Rest": "Rest",
"SW5E.RestL": "L. Rest", "SW5E.RestL": "L. Rest",
"SW5E.RestS": "S. Rest", "SW5E.RestS": "S. Rest",
"SW5E.Ritual": "Ritual", "SW5E.Ritual": "Ritual",
"SW5E.Role": "Role", "SW5E.Role": "Role",
"SW5E.RolePl": "Roles", "SW5E.RolePl": "Roles",
"SW5E.Roll": "Roll", "SW5E.Roll": "Roll",
"SW5E.RollExample": "e.g. 1d4", "SW5E.RollExample": "e.g. +1d4",
"SW5E.RollMode": "Roll Mode", "SW5E.RollMode": "Roll Mode",
"SW5E.RollSituationalBonus": "Situational Bonus?", "SW5E.RollSituationalBonus": "Situational Bonus?",
"SW5E.Save": "Save", "SW5E.Save": "Save",
@ -929,7 +910,6 @@
"SW5E.SchoolLgt": "Light", "SW5E.SchoolLgt": "Light",
"SW5E.SchoolTec": "Tech", "SW5E.SchoolTec": "Tech",
"SW5E.SchoolUni": "Universal", "SW5E.SchoolUni": "Universal",
"SW5E.SelectItemsPromptTitle": "Select Items",
"SW5E.SenseBlindsight": "Blindsight", "SW5E.SenseBlindsight": "Blindsight",
"SW5E.SenseBS": "Blindsight", "SW5E.SenseBS": "Blindsight",
"SW5E.SenseDarkvision": "Darkvision", "SW5E.SenseDarkvision": "Darkvision",
@ -949,6 +929,9 @@
"SW5E.SheetClassNPCOld": "Old NPC Sheet", "SW5E.SheetClassNPCOld": "Old NPC Sheet",
"SW5E.SheetClassVehicle": "Default Vehicle Sheet", "SW5E.SheetClassVehicle": "Default Vehicle Sheet",
"SW5E.ShieldDice": "Shield Dice", "SW5E.ShieldDice": "Shield Dice",
"SW5E.ShieldDiceRoll": "Roll Shield Dice",
"SW5E.ShieldDiceUsed": "Shield Dice Used",
"SW5E.ShieldDiceWarn": "{name} has no available {formula} Shield Dice remaining!",
"SW5E.ShieldPoints": "Shield Points", "SW5E.ShieldPoints": "Shield Points",
"SW5E.ShieldPointsFormula": "Shield Points Formula", "SW5E.ShieldPointsFormula": "Shield Points Formula",
"SW5E.ShieldRegen": "Regen", "SW5E.ShieldRegen": "Regen",
@ -991,7 +974,6 @@
"SW5E.SkillSte": "Stealth", "SW5E.SkillSte": "Stealth",
"SW5E.SkillSur": "Survival", "SW5E.SkillSur": "Survival",
"SW5E.SkillTec": "Technology", "SW5E.SkillTec": "Technology",
"SW5E.Skip": "Skip",
"SW5E.Slots": "Slots", "SW5E.Slots": "Slots",
"SW5E.Source": "Source", "SW5E.Source": "Source",
"SW5E.Special": "Special", "SW5E.Special": "Special",
@ -1002,8 +984,8 @@
"SW5E.Speed": "Speed", "SW5E.Speed": "Speed",
"SW5E.SpeedSpecial": "Special Movement", "SW5E.SpeedSpecial": "Special Movement",
"SW5E.StarshipAmbassador": "Ambassador", "SW5E.StarshipAmbassador": "Ambassador",
"SW5E.StarshipArmorandShieldProps": "Starship Armor & Shield Properties",
"SW5E.StarshipArmorandShields": "Starship Armor and Shields", "SW5E.StarshipArmorandShields": "Starship Armor and Shields",
"SW5E.StarshipArmorandShieldProps": "Starship Armor & Shield Properties",
"SW5E.StarshipBattleship": "Battleship", "SW5E.StarshipBattleship": "Battleship",
"SW5E.StarshipBlockadeShip": "Blockade Ship", "SW5E.StarshipBlockadeShip": "Blockade Ship",
"SW5E.StarshipBomber": "Bomber", "SW5E.StarshipBomber": "Bomber",
@ -1039,7 +1021,7 @@
"SW5E.StarshipSkillDat": "Data", "SW5E.StarshipSkillDat": "Data",
"SW5E.StarshipSkillHid": "Hide", "SW5E.StarshipSkillHid": "Hide",
"SW5E.StarshipSkillImp": "Impress", "SW5E.StarshipSkillImp": "Impress",
"SW5E.StarshipSkillInt": "Interfere", "SW5E.StarshipSkillInf": "Interfere",
"SW5E.StarshipSkillMan": "Maneuvering", "SW5E.StarshipSkillMan": "Maneuvering",
"SW5E.StarshipSkillMen": "Menace", "SW5E.StarshipSkillMen": "Menace",
"SW5E.StarshipSkillPat": "Patch", "SW5E.StarshipSkillPat": "Patch",
@ -1128,7 +1110,6 @@
"SW5E.TraitToolProf": "Tool Proficiencies", "SW5E.TraitToolProf": "Tool Proficiencies",
"SW5E.TraitWeaponProf": "Weapon Proficiencies", "SW5E.TraitWeaponProf": "Weapon Proficiencies",
"SW5E.Type": "Type", "SW5E.Type": "Type",
"SW5E.Uncrewed": "Uncrewed",
"SW5E.Unequipped": "Unequipped", "SW5E.Unequipped": "Unequipped",
"SW5E.UniversalPowerDC": "Universal Power DC", "SW5E.UniversalPowerDC": "Universal Power DC",
"SW5E.Unlimited": "Unlimited", "SW5E.Unlimited": "Unlimited",
@ -1153,27 +1134,16 @@
"SW5E.VersatileDamage": "Versatile Damage", "SW5E.VersatileDamage": "Versatile Damage",
"SW5E.VsDC": "vs DC.", "SW5E.VsDC": "vs DC.",
"SW5E.WeaponAmmo": "Ammunition", "SW5E.WeaponAmmo": "Ammunition",
"SW5E.WeaponBlasterPistolProficiency": "Blaster Pistol",
"SW5E.WeaponChakramProficiency": "Chakrams",
"SW5E.WeaponDoubleBladeProficiency": "Doubleblade",
"SW5E.WeaponDoubleSaberProficiency": "Doublesaber",
"SW5E.WeaponDoubleShotoProficiency": "Doubleshoto",
"SW5E.WeaponDoubleSwordProficiency": "Doublesword",
"SW5E.WeaponHiddenBladeProficiency": "Hidden Blade",
"SW5E.WeaponImprov": "Improvised", "SW5E.WeaponImprov": "Improvised",
"SW5E.WeaponImprovisedProficiency": "Improvised Weapons",
"SW5E.WeaponLightFoilProficiency": "Lightfoil",
"SW5E.WeaponLightRingProficiency": "Light Ring",
"SW5E.WeaponMartialB": "Martial Blaster", "SW5E.WeaponMartialB": "Martial Blaster",
"SW5E.WeaponMartialBlasterProficiency": "Martial Blasters",
"SW5E.WeaponMartialLightweaponProficiency": "Martial Lightweapons",
"SW5E.WeaponMartialLW": "Martial Lightweapon", "SW5E.WeaponMartialLW": "Martial Lightweapon",
"SW5E.WeaponPrimarySW": "Primary (Starship)",
"SW5E.WeaponSecondarySW": "Secondary (Starship)",
"SW5E.WeaponTertiarySW": "Tertiary (Starship)",
"SW5E.WeaponQuaternarySW": "Quaternary (Starship)",
"SW5E.WeaponMartialProficiency": "Martial Weapons", "SW5E.WeaponMartialProficiency": "Martial Weapons",
"SW5E.WeaponMartialVibroweaponProficiency": "Martial Vibroweapons",
"SW5E.WeaponMartialVW": "Martial Vibroweapon", "SW5E.WeaponMartialVW": "Martial Vibroweapon",
"SW5E.WeaponNatural": "Natural", "SW5E.WeaponNatural": "Natural",
"SW5E.WeaponNaturalProficiency": "Natural Weapons",
"SW5E.WeaponPrimarySW": "Primary (Starship)",
"SW5E.WeaponPropertiesAmm": "Ammunition", "SW5E.WeaponPropertiesAmm": "Ammunition",
"SW5E.WeaponPropertiesAut": "Auto", "SW5E.WeaponPropertiesAut": "Auto",
"SW5E.WeaponPropertiesBur": "Burst", "SW5E.WeaponPropertiesBur": "Burst",
@ -1191,14 +1161,14 @@
"SW5E.WeaponPropertiesFix": "Fixed", "SW5E.WeaponPropertiesFix": "Fixed",
"SW5E.WeaponPropertiesFoc": "Focus", "SW5E.WeaponPropertiesFoc": "Focus",
"SW5E.WeaponPropertiesHid": "Hidden", "SW5E.WeaponPropertiesHid": "Hidden",
"SW5E.WeaponPropertiesHom": "Homing",
"SW5E.WeaponPropertiesHvy": "Heavy", "SW5E.WeaponPropertiesHvy": "Heavy",
"SW5E.WeaponPropertiesHom": "Homing",
"SW5E.WeaponPropertiesIon": "Ionizing", "SW5E.WeaponPropertiesIon": "Ionizing",
"SW5E.WeaponPropertiesKen": "Keen", "SW5E.WeaponPropertiesKen": "Keen",
"SW5E.WeaponPropertiesLgt": "Light", "SW5E.WeaponPropertiesLgt": "Light",
"SW5E.WeaponPropertiesLum": "Luminous", "SW5E.WeaponPropertiesLum": "Luminous",
"SW5E.WeaponPropertiesMig": "Mighty",
"SW5E.WeaponPropertiesMlt": "Melt", "SW5E.WeaponPropertiesMlt": "Melt",
"SW5E.WeaponPropertiesMig": "Mighty",
"SW5E.WeaponPropertiesOvr": "Overheat", "SW5E.WeaponPropertiesOvr": "Overheat",
"SW5E.WeaponPropertiesPic": "Piercing", "SW5E.WeaponPropertiesPic": "Piercing",
"SW5E.WeaponPropertiesPow": "Power", "SW5E.WeaponPropertiesPow": "Power",
@ -1217,21 +1187,11 @@
"SW5E.WeaponPropertiesVer": "Versatile", "SW5E.WeaponPropertiesVer": "Versatile",
"SW5E.WeaponPropertiesVic": "Vicious", "SW5E.WeaponPropertiesVic": "Vicious",
"SW5E.WeaponPropertiesZon": "Zone", "SW5E.WeaponPropertiesZon": "Zone",
"SW5E.WeaponQuaternarySW": "Quaternary (Starship)",
"SW5E.WeaponSaberWhipProficiency": "Saberwhip",
"SW5E.WeaponSecondarySW": "Secondary (Starship)",
"SW5E.WeaponSiege": "Siege", "SW5E.WeaponSiege": "Siege",
"SW5E.WeaponSimpleB": "Simple Blaster", "SW5E.WeaponSimpleB": "Simple Blaster",
"SW5E.WeaponSimpleBlasterProficiency": "Simple Blasters",
"SW5E.WeaponSimpleLightweaponProficiency": "Simple Lightweapons",
"SW5E.WeaponSimpleLW": "Simple Lightweapon", "SW5E.WeaponSimpleLW": "Simple Lightweapon",
"SW5E.WeaponSimpleProficiency": "Simple Weapons", "SW5E.WeaponSimpleProficiency": "Simple Weapons",
"SW5E.WeaponSimpleVibroweaponProficiency": "Simple Vibroweapons",
"SW5E.WeaponSimpleVW": "Simple Vibroweapon", "SW5E.WeaponSimpleVW": "Simple Vibroweapon",
"SW5E.WeaponSizeAbb": "Size", "SW5E.WeaponSizeAbb": "Size",
"SW5E.WeaponTechbladeProficiency": "Techblades",
"SW5E.WeaponTertiarySW": "Tertiary (Starship)",
"SW5E.WeaponVibrorapierProficiency": "Vibrorapier",
"SW5E.WeaponVibrowhipProficiency": "Vibrowhip",
"SW5E.Weight": "Weight" "SW5E.Weight": "Weight"
} }

File diff suppressed because it is too large Load diff

View file

@ -71,7 +71,7 @@
} }
// Movement Configuration // Movement Configuration
.movement, .hit-dice { .movement {
h4.attribute-name { h4.attribute-name {
position: relative; position: relative;
} }
@ -655,15 +655,6 @@
// Empty powerbook controls // Empty powerbook controls
.powerbook-empty .item-controls { flex: 1; } .powerbook-empty .item-controls { flex: 1; }
/* ----------------------------------------- */
/* Features Tab */
/* ----------------------------------------- */
// Original class icon
.features i.original-class {
color: #4b4a44
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* TinyMCE */ /* TinyMCE */
/* ----------------------------------------- */ /* ----------------------------------------- */
@ -671,6 +662,77 @@
.editor { .editor {
padding: 0 8px; padding: 0 8px;
} }
.fuel {
flex: 0 0 12px;
background: #7a7971;
margin: 1px 15px 0 1px;
border: 1px solid #191813;
border-radius: 3px;
position: relative;
.fuel-bar {
position: absolute;
top: 1px;
left: 1px;
background: #6c8aa5;
height: 8px;
border: 1px solid #cde4ff;
border-radius: 2px;
}
.fuel-label {
height: 10px;
padding: 0 5px;
position: absolute;
top: 0;
right: 0;
font-size: 13px;
line-height: 12px;
text-align: right;
color: #EEE;
text-shadow: 0 0 5px #000;
}
.fuel-breakpoint {
display: block;
position: absolute;
}
.fuel-breakpoint.fuel-20 {
left: 20%;
}
.fuel-breakpoint.fuel-40 {
left: 40%;
}
.fuel-breakpoint.fuel-60 {
left: 60%;
}
.fuel-breakpoint.fuel-80 {
left: 80%;
}
.arrow-up {
bottom: 0;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid #000;
}
.arrow-down {
top: 0;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #000;
}
}
.fuel.fueled {
.arrow-up {
border-bottom: 4px solid #fff;
border-bottom: 4px solid #000;
}
.arrow-down {
border-top: 4px solid #fff;
border-top: 4px solid #000;
}
}
} }
#actor-flags { #actor-flags {

View file

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

View file

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

View file

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

View file

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

View file

@ -140,7 +140,6 @@
height: auto; height: auto;
.russoOne(17px); .russoOne(17px);
line-height: 24px; line-height: 24px;
width: 100%;
} }
.proficiency { .proficiency {
@ -185,7 +184,7 @@
display: inline-block; display: inline-block;
text-align: right; text-align: right;
padding: 0 3px; padding: 0px 3px;
&:last-child { &:last-child {
text-align: left; text-align: left;
@ -251,6 +250,44 @@
} }
} }
.panel.resources {
.traits {
.fuel-wrapper {
display: grid;
grid-template-columns: 300px 100px;
width: 400px;
justify-self: end;
.fuel-label {
font-size: 12px;
line-height: 14px;
width: 100%;
text-shadow: none;
padding: 0;
margin: 0;
height: auto;
text-align: center;
margin-left: -2px;
border-radius: 0 4px 4px 0;
}
.fuel {
position: relative;
border-radius: 4px;
height: 16px;
margin: 0;
width: 100%;
.fuel-bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
border-radius: 4px;
border: none;
}
}
}
}
}
nav.sheet-navigation { nav.sheet-navigation {
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(7, 1fr);
@ -780,7 +817,7 @@
display: block; display: block;
width: 100%; width: 100%;
text-align: right; text-align: right;
padding: 0 3px; padding: 0px 3px;
&:last-child { &:last-child {
text-align: left; text-align: left;
} }
@ -956,7 +993,7 @@
display: block; display: block;
width: 100%; width: 100%;
text-align: right; text-align: right;
padding: 0 3px; padding: 0px 3px;
&:last-child { &:last-child {
text-align: left; text-align: left;
} }
@ -1054,35 +1091,10 @@
h1.character-name { h1.character-name {
align-self: auto; align-self: auto;
} }
.npc-size, .creature-type { .npc-size {
.russoOne(18px); .russoOne(18px);
line-height: 28px; line-height: 28px;
} }
div.creature-type {
display: flex;
justify-content: space-between;
padding: 1px 4px;
border: 1px solid transparent;
overflow-x: auto;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-button {
display: none;
font-size: 12px;
font-weight: normal;
line-height: 2em;
}
&:hover .config-button {
display: block;
}
}
.attributes { .attributes {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
footer { footer {
@ -1123,4 +1135,44 @@
} }
} }
} }
input[type=range][orient=vertical] {
-webkit-appearance: slider-vertical;
width: 10px;
height: 60px !important;
padding: 0 0 !important;
background-color: #c40f0f !important;
box-sizing: border-box;
&::-webkit-slider-runnable-track {
-webkit-appearance: slider-vertical !important;
height: 60px !important;
width: 10px !important;
line-height: 60px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-top: 0 0 !important;
border-radius: 3px !important;
background: linear-gradient( to top, #c40f0f 50%, #0dce0d 50% );
}
&::-webkit-slider-thumb {
-webkit-appearance: none !important;
background-color: #c40f0f !important;
margin-right: -4px !important;
margin-top: 0px !important;
cursor: grab !important;
border-radius: 0 0 0 0 !important;
width: 10px !important;
height: 5px !important;
font-size: 10px;
}
}
output {
display: block;
margin: 5px auto;
font-size: 1.75em;
}
input {
.vertslider {
height: 60px;
}
}
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -35,6 +35,28 @@ body.dark-theme {
box-shadow: @blockquoteShadow; box-shadow: @blockquoteShadow;
} }
.sw5e.sheet.actor {
.swalt-sheet {
.panel.resources {
.traits {
.fuel-wrapper {
.fuel-label {
background: #D6D6D6;
color: #1C1C1C;
border: 1px solid #1C1C1C;
}
.fuel {
background: #c40f0f;
.fuel-bar {
background: #0dce0d;
}
}
}
}
}
}
}
hr { hr {
border-width: 0 0 1px 0; border-width: 0 0 1px 0;
border-bottom: 1px solid @hrColor; border-bottom: 1px solid @hrColor;

View file

@ -35,6 +35,28 @@ body.light-theme {
box-shadow: @blockquoteShadow; box-shadow: @blockquoteShadow;
} }
.sw5e.sheet.actor {
.swalt-sheet {
.panel.resources {
.traits {
.fuel-wrapper {
.fuel-label {
background: #D6D6D6;
color: #1C1C1C;
border: 1px solid #1C1C1C;
}
.fuel {
background: #c40f0f;
.fuel-bar {
background: #0dce0d;
}
}
}
}
}
}
}
hr { hr {
border-width: 0 0 1px 0; border-width: 0 0 1px 0;
border-bottom: 1px solid @hrColor; border-bottom: 1px solid @hrColor;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e} * @extends {ActorSheet5e}
*/ */
export default class ActorSheet5eStarship extends ActorSheet5e { export default class ActorSheet5eStarship extends ActorSheet5e {
/** @override */ /** @override */
get template() { get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
@ -16,13 +17,12 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "starship"], classes: ["sw5e", "sheet", "actor", "starship"],
width: 800, width: 800,
tabs: [ height: 775,
{ tabs: [{
navSelector: ".root-tabs", navSelector: ".root-tabs",
contentSelector: ".sheet-body", contentSelector: ".sheet-body",
initial: "attributes" initial: "attributes"
} }],
]
}); });
} }
@ -33,47 +33,29 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize Items as Features and Powers // Categorize Items as Features and Powers
const features = { const features = {
weapons: { weapons: { label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), items: [], hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} }, passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}}, equipment: { label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}},
starshipfeatures: { starshipfeatures: { label: game.i18n.localize("SW5E.StarshipfeaturePl"), items: [], hasActions: true, dataset: {type: "starshipfeature"} },
label: game.i18n.localize("SW5E.StarshipfeaturePl"), starshipmods: { label: game.i18n.localize("SW5E.StarshipmodPl"), items: [], hasActions: false, dataset: {type: "starshipmod"} }
items: [],
hasActions: true,
dataset: {type: "starshipfeature"}
},
starshipmods: {
label: game.i18n.localize("SW5E.StarshipmodPl"),
items: [],
hasActions: false,
dataset: {type: "starshipmod"}
}
}; };
// Start by classifying items into groups for rendering // Start by classifying items into groups for rendering
let [forcepowers, techpowers, other] = data.items.reduce( let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
(arr, item) => { item.img = item.img || DEFAULT_TOKEN;
item.img = item.img || CONST.DEFAULT_TOKEN; item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; item.hasUses = item.data.uses && (item.data.uses.max > 0);
item.hasUses = item.data.uses && item.data.uses.max > 0; item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
item.isOnCooldown = item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item); if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item);
else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item); else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item);
else arr[2].push(item); else arr[2].push(item);
return arr; return arr;
}, }, [[], [], []]);
[[], [], []]
);
// Apply item filters // Apply item filters
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
@ -90,11 +72,14 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
else if ( item.type === "feat" ) { else if ( item.type === "feat" ) {
if ( item.data.activation.type ) features.actions.items.push(item); if ( item.data.activation.type ) features.actions.items.push(item);
else features.passive.items.push(item); else features.passive.items.push(item);
} else if (item.type === "starshipfeature") { }
else if ( item.type === "starshipfeature" ) {
features.starshipfeatures.items.push(item); features.starshipfeatures.items.push(item);
} else if (item.type === "starshipmod") { }
else if ( item.type === "starshipmod" ) {
features.starshipmods.items.push(item); features.starshipmods.items.push(item);
} else features.equipment.items.push(item); }
else features.equipment.items.push(item);
} }
// Assign and return // Assign and return
@ -103,11 +88,12 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
// data.techPowerbook = techPowerbook; // data.techPowerbook = techPowerbook;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
getData(options) { getData() {
const data = super.getData(options); const data = super.getData();
// Add Size info // Add Size info
data.isTiny = data.actor.data.traits.size === "tiny"; data.isTiny = data.actor.data.traits.size === "tiny";
@ -129,7 +115,8 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _updateObject(event, formData) { _updateObject(event, formData) {
// Format NPC Challenge Rating // Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr"; let crv = "data.details.cr";
@ -138,7 +125,7 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps // Parent ActorSheet update steps
return super._updateObject(event, formData); super._updateObject(event, formData);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -149,6 +136,11 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
html.find('.refuel').click(this._onIncrementFuelLevel.bind(this));
html.find('.burnfuel').click(this._onDecrementFuelLevel.bind(this));
html.find('#engineslidervalue')[0].addEventListener('input', this._engineSliderUpdate.bind(this));
html.find('#shieldslidervalue')[0].addEventListener('input', this._shieldSliderUpdate.bind(this));
html.find('#weaponslidervalue')[0].addEventListener('input', this._weaponSliderUpdate.bind(this));
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -166,4 +158,96 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
AudioHelper.play({src: CONFIG.sounds.dice}); AudioHelper.play({src: CONFIG.sounds.dice});
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
} }
/* -------------------------------------------- */
/**
* Handle refueling a starship
* @param {Event} event The original click event
* @private
*/
_onIncrementFuelLevel(event) {
// event.preventDefault();
// const fuelcaparray = this.actor.data.effects.changes;
// var fuelcappos = fuelcaparray.indexOf('fuel.cap');
// const refuel = this.actor.data.effect.changes[fuelcappos].value;
this.actor.update({"data.attributes.fuel.value": this.actor.data.data.attributes.fuel.cap});
}
/* -------------------------------------------- */
/**
* Handle a starship burning fuel
* @param {Event} event The original click event
* @private
*/
_onDecrementFuelLevel(event) {
// event.preventDefault();
// const fuelcaparray = this.actor.data.effects.changes;
// var fuelcappos = fuelcaparray.indexOf('fuel.cap');
// const refuel = this.actor.data.effect.changes[fuelcappos].value;
this.actor.update({"data.attributes.fuel.value": this.actor.data.data.attributes.fuel.value - 1});
}
_engineSliderUpdate(input) {
var symbol;
var coefficient;
switch(input.target.value) {
case "0":
symbol = "↓";
coefficient = 0.5;
break;
case "1":
symbol = "=";
coefficient = 1;
break;
case "2":
symbol = "↑";
coefficient = 2;
};
let slideroutput = symbol;
document.querySelector('#engineslideroutput').value = slideroutput;
this.actor.update({"data.attributes.power.routing.engines": coefficient});
}
_shieldSliderUpdate(input) {
var symbol;
var coefficient;
switch(input.target.value) {
case "0":
symbol = "↓";
coefficient = 0.5;
break;
case "1":
symbol = "=";
coefficient = 1;
break;
case "2":
symbol = "↑";
coefficient = 2;
};
let slideroutput = symbol;
document.querySelector('#shieldslideroutput').value = slideroutput;
this.actor.update({"data.attributes.power.routing.shields": coefficient});
}
_weaponSliderUpdate(input) {
var symbol;
var coefficient;
switch(input.target.value) {
case "0":
symbol = "↓";
coefficient = 0.5;
break;
case "1":
symbol = "=";
coefficient = 1;
break;
case "2":
symbol = "↑";
coefficient = 2;
};
let slideroutput = symbol;
document.querySelector('#weaponslideroutput').value = slideroutput;
this.actor.update({"data.attributes.power.routing.weapons": coefficient});
}
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,111 +0,0 @@
import Actor5e from "../actor/entity.js";
/**
* A specialized form used to select from a checklist of attributes, traits, or properties
* @extends {FormApplication}
*/
export default class ActorTypeConfig extends FormApplication {
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e", "actor-type", "trait-selector"],
template: "systems/sw5e/templates/apps/actor-type.html",
title: "SW5E.CreatureTypeTitle",
width: 280,
height: "auto",
choices: {},
allowCustom: true,
minimum: 0,
maximum: null
});
}
/* -------------------------------------------- */
/** @override */
get id() {
return `actor-type-${this.object.id}`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
// Get current value or new default
let attr = foundry.utils.getProperty(this.object.data.data, "details.type");
if (foundry.utils.getType(attr) !== "Object")
attr = {
value: attr in CONFIG.SW5E.creatureTypes ? attr : "humanoid",
subtype: "",
swarm: "",
custom: ""
};
// Populate choices
const types = {};
for (let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes)) {
types[k] = {
label: game.i18n.localize(v),
chosen: attr.value === k
};
}
// Return data for rendering
return {
types: types,
custom: {
value: attr.custom,
label: game.i18n.localize("SW5E.CreatureTypeSelectorCustom"),
chosen: attr.value === "custom"
},
subtype: attr.subtype,
swarm: attr.swarm,
sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes))
.reverse()
.reduce((obj, e) => {
obj[e[0]] = e[1];
return obj;
}, {}),
preview: Actor5e.formatCreatureType(attr) || ""
};
}
/* -------------------------------------------- */
/** @override */
async _updateObject(event, formData) {
const typeObject = foundry.utils.expandObject(formData);
return this.object.update({"data.details.type": typeObject});
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @inheritdoc */
activateListeners(html) {
super.activateListeners(html);
html.find("input[name='custom']").focusin(this._onCustomFieldFocused.bind(this));
}
/* -------------------------------------------- */
/** @inheritdoc */
_onChangeInput(event) {
super._onChangeInput(event);
const typeObject = foundry.utils.expandObject(this._getSubmitData());
this.form["preview"].value = Actor5e.formatCreatureType(typeObject) || "—";
}
/* -------------------------------------------- */
/**
* Select the custom radio button when the custom text field is focused.
* @param {FocusEvent} event The original focusin event
* @private
*/
_onCustomFieldFocused(event) {
this.form.querySelector("input[name='value'][value='custom']").checked = true;
this._onChangeInput(event);
}
}

View file

@ -1,92 +0,0 @@
/**
* A simple form to set actor hit dice amounts
* @implements {DocumentSheet}
*/
export default class ActorHitDiceConfig extends DocumentSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e", "hd-config", "dialog"],
template: "systems/sw5e/templates/apps/hit-dice-config.html",
width: 360,
height: "auto"
});
}
/* -------------------------------------------- */
/** @override */
get title() {
return `${game.i18n.localize("SW5E.HitDiceConfig")}: ${this.object.name}`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
return {
classes: this.object.items
.reduce((classes, item) => {
if (item.data.type === "class") {
// Add the appropriate data only if this item is a "class"
classes.push({
classItemId: item.data._id,
name: item.data.name,
diceDenom: item.data.data.hitDice,
currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed,
maxHitDice: item.data.data.levels,
canRoll: item.data.data.levels - item.data.data.hitDiceUsed > 0
});
}
return classes;
}, [])
.sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1)))
};
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Hook up -/+ buttons to adjust the current value in the form
html.find("button.increment,button.decrement").click((event) => {
const button = event.currentTarget;
const current = button.parentElement.querySelector(".current");
const max = button.parentElement.querySelector(".max");
const direction = button.classList.contains("increment") ? 1 : -1;
current.value = Math.clamped(parseInt(current.value) + direction, 0, parseInt(max.value));
});
html.find("button.roll-hd").click(this._onRollHitDie.bind(this));
}
/* -------------------------------------------- */
/** @override */
async _updateObject(event, formData) {
const actorItems = this.object.items;
const classUpdates = Object.entries(formData).map(([id, hd]) => ({
"_id": id,
"data.hitDiceUsed": actorItems.get(id).data.data.levels - hd
}));
return this.object.updateEmbeddedDocuments("Item", classUpdates);
}
/* -------------------------------------------- */
/**
* Rolls the hit die corresponding with the class row containing the event's target button.
* @param {MouseEvent} event
* @private
*/
async _onRollHitDie(event) {
event.preventDefault();
const button = event.currentTarget;
await this.object.rollHitDie(button.dataset.hdDenom);
// Re-render dialog to reflect changed hit dice quantities
this.render();
}
}

View file

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

View file

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

View file

@ -0,0 +1,117 @@
/**
* A helper Dialog subclass for rolling Hit Dice on a recharge rest
* @extends {Dialog}
*/
export default class RechargeRestDialog extends Dialog {
constructor(actor, dialogData={}, options={}) {
super(dialogData, options);
/**
* Store a reference to the Actor entity which is resting
* @type {Actor}
*/
this.actor = actor;
/**
* Track the most recently used HD denomination for re-rendering the form
* @type {string}
*/
this._denom = null;
}
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
template: "systems/sw5e/templates/apps/recharge-rest.html",
classes: ["sw5e", "dialog"]
});
}
/* -------------------------------------------- */
/** @override */
getData() {
const data = super.getData();
// Determine Hull Dice
data.availableHD = this.actor.data.items.reduce((hd, item) => {
if ( item.type === "starship" ) {
const d = item.data;
const denom = d.hullDice || "d6";
const available = parseInt(d.hullDiceStart || 1) + parseInt(d.tier || 0) - parseInt(d.hullDiceUsed || 0);
hd[denom] = denom in hd ? hd[denom] + available : available;
}
return hd;
}, {});
data.canRoll = this.actor.data.data.attributes.hull.dice > 0;
data.denomination = this._denom;
// Determine rest type
const variant = game.settings.get("sw5e", "restVariant");
data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute
data.newDay = false; // It may be a new day, but not by default
return data;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
let btn = html.find("#roll-hulld");
btn.click(this._onRollHullDie.bind(this));
}
/* -------------------------------------------- */
/**
* Handle rolling a Hull Die as part of a Recharge Rest action
* @param {Event} event The triggering click event
* @private
*/
async _onRollHullDie(event) {
event.preventDefault();
const btn = event.currentTarget;
this._denom = btn.form.hulld.value;
await this.actor.rollHullDie(this._denom);
this.render();
}
/* -------------------------------------------- */
/**
* A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has
* been resolved.
* @param {Actor5e} actor
* @return {Promise}
*/
static async rechargeRestDialog({actor}={}) {
return new Promise((resolve, reject) => {
const dlg = new this(actor, {
title: "Recharge Rest",
buttons: {
rest: {
icon: '<i class="fas fa-bed"></i>',
label: "Rest",
callback: html => {
let newDay = false;
if (game.settings.get("sw5e", "restVariant") === "gritty")
newDay = html.find('input[name="newDay"]')[0].checked;
resolve(newDay);
}
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel",
callback: reject
}
},
close: reject
});
dlg.render(true);
});
}
}

View file

@ -0,0 +1,69 @@
/**
* A helper Dialog subclass for completing a refitting rest
* @extends {Dialog}
*/
export default class RefittingRestDialog extends Dialog {
constructor(actor, dialogData = {}, options = {}) {
super(dialogData, options);
this.actor = actor;
}
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
template: "systems/sw5e/templates/apps/refitting-rest.html",
classes: ["sw5e", "dialog"]
});
}
/* -------------------------------------------- */
/** @override */
getData() {
const data = super.getData();
const variant = game.settings.get("sw5e", "restVariant");
data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week
data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours)
return data;
}
/* -------------------------------------------- */
/**
* A helper constructor function which displays the Refitting Rest confirmation dialog and returns a Promise once it's
* workflow has been resolved.
* @param {Actor5e} actor
* @return {Promise}
*/
static async refittingRestDialog({ actor } = {}) {
return new Promise((resolve, reject) => {
const dlg = new this(actor, {
title: "Refitting Rest",
buttons: {
rest: {
icon: '<i class="fas fa-bed"></i>',
label: "Rest",
callback: html => {
let newDay = false;
if (game.settings.get("sw5e", "restVariant") === "normal")
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",
callback: reject
}
},
default: 'rest',
close: reject
});
dlg.render(true);
});
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

21
module/effects.js vendored
View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,102 +0,0 @@
/**
* Extend the base TokenDocument class to implement system-specific HP bar logic.
* @extends {TokenDocument}
*/
export class TokenDocument5e extends TokenDocument {
/** @inheritdoc */
getBarAttribute(...args) {
const data = super.getBarAttribute(...args);
if (data && data.attribute === "attributes.hp") {
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
}
return data;
}
}
/* -------------------------------------------- */
/**
* Extend the base Token class to implement additional system-specific logic.
* @extends {Token}
*/
export class Token5e extends Token {
/** @inheritdoc */
_drawBar(number, bar, data) {
if (data.attribute === "attributes.hp") return this._drawHPBar(number, bar, data);
return super._drawBar(number, bar, data);
}
/* -------------------------------------------- */
/**
* Specialized drawing function for HP bars.
* @param {number} number The Bar number
* @param {PIXI.Graphics} bar The Bar container
* @param {object} data Resource data for this bar
* @private
*/
_drawHPBar(number, bar, data) {
// Extract health data
let {value, max, temp, tempmax} = this.document.actor.data.data.attributes.hp;
temp = Number(temp || 0);
tempmax = Number(tempmax || 0);
// Differentiate between effective maximum and displayed maximum
const effectiveMax = Math.max(0, max + tempmax);
let displayMax = max + (tempmax > 0 ? tempmax : 0);
// Allocate percentages of the total
const tempPct = Math.clamped(temp, 0, displayMax) / displayMax;
const valuePct = Math.clamped(value, 0, effectiveMax) / displayMax;
const colorPct = Math.clamped(value, 0, effectiveMax) / displayMax;
// Determine colors to use
const blk = 0x000000;
const hpColor = PIXI.utils.rgb2hex([1 - colorPct / 2, colorPct, 0]);
const c = CONFIG.SW5E.tokenHPColors;
// Determine the container size (logic borrowed from core)
const w = this.w;
let h = Math.max(canvas.dimensions.size / 12, 8);
if (this.data.height >= 2) h *= 1.6;
const bs = Math.clamped(h / 8, 1, 2);
const bs1 = bs + 1;
// Overall bar container
bar.clear();
bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3);
// Temporary maximum HP
if (tempmax > 0) {
const pct = max / effectiveMax;
bar.beginFill(c.tempmax, 1.0)
.lineStyle(1, blk, 1.0)
.drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
}
// Maximum HP penalty
else if (tempmax < 0) {
const pct = (max + tempmax) / max;
bar.beginFill(c.negmax, 1.0)
.lineStyle(1, blk, 1.0)
.drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
}
// Health bar
bar.beginFill(hpColor, 1.0)
.lineStyle(bs, blk, 1.0)
.drawRoundedRect(0, 0, valuePct * w, h, 2);
// Temporary hit points
if (temp > 0) {
bar.beginFill(c.temp, 1.0)
.lineStyle(0)
.drawRoundedRect(bs1, bs1, tempPct * w - 2 * bs1, h - 2 * bs1, 1);
}
// Set position
let posY = number === 0 ? this.h - h : 0;
bar.position.set(0, posY);
}
}

12
package-lock.json generated
View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

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